provozni zaloha

This commit is contained in:
lachtan
2026-06-24 08:11:12 +02:00
parent 9295dba19f
commit 1db3ec4756
97 changed files with 7698 additions and 817 deletions

118
skills/note/scripts/note.py Normal file → Executable file
View File

@@ -22,6 +22,10 @@ DB_PATH = Path(__file__).resolve().parent.parent.parent.parent / "db" / "note.sq
LOG_PATH = Path(__file__).resolve().parent.parent.parent.parent / "log" / "note.log"
_TAG_RE = re.compile(r"^[a-z][a-z0-9-]*$")
# A URL together with an immediately preceding "Label:" token, if any.
# The leading separator class swallows the connector that introduced the URL
# (em-dash, comma, etc.) so it does not dangle once the URL moves to its own line.
_LABELED_URL_RE = re.compile(r"[\s,;—–-]*([^\s,]+:\s*)?(https?://[^\s,]+)")
SCHEMA = """
CREATE TABLE IF NOT EXISTS notes (
@@ -31,6 +35,10 @@ CREATE TABLE IF NOT EXISTS notes (
created_at TEXT NOT NULL,
deleted_at TEXT
);
CREATE TABLE IF NOT EXISTS tags (
name TEXT PRIMARY KEY,
created_at TEXT NOT NULL
);
"""
@@ -47,6 +55,23 @@ def _migrate(conn: sqlite3.Connection) -> None:
if "deleted_at" not in cols:
conn.execute("ALTER TABLE notes ADD COLUMN deleted_at TEXT")
conn.commit()
_backfill_tags(conn)
def _backfill_tags(conn: sqlite3.Connection) -> None:
"""On first introduction of the registry, seed it from tags already used in notes."""
existing = {row[0] for row in conn.execute("SELECT name FROM tags")}
if existing:
return
used = {row[0] for row in conn.execute("SELECT DISTINCT value FROM notes, json_each(notes.tags)")}
if not used:
return
now = datetime.now(timezone.utc).isoformat()
conn.executemany(
"INSERT OR IGNORE INTO tags(name, created_at) VALUES(?, ?)",
[(tag, now) for tag in sorted(used)],
)
conn.commit()
@contextmanager
@@ -76,6 +101,23 @@ def _tags_display(tags_json: str) -> str:
return " [" + " ".join(f"#{t}" for t in tags) + "]"
def _urls_on_own_lines(text: str) -> str:
"""Lay out each URL (and its inline "Label:", if any) on its own bullet line.
The chat UI merges two adjacent links into one block and hides the second,
which also overlays the list number. Putting each URL on its own line keeps
them separate and the number visible. URLs stay bare so they autolink.
"""
if not _LABELED_URL_RE.search(text):
return text
def repl(match: re.Match[str]) -> str:
label = match.group(1) or ""
return f"\n - {label}{match.group(2)}"
return _LABELED_URL_RE.sub(repl, text)
def _log(op: str, detail: str) -> None:
LOG_PATH.parent.mkdir(parents=True, exist_ok=True)
ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")[:-3]
@@ -101,6 +143,11 @@ def cmd_add(args: argparse.Namespace) -> int:
tags_json = json.dumps(tags)
created_at = datetime.now(timezone.utc).isoformat()
with _connect() as conn:
known = {row[0] for row in conn.execute("SELECT name FROM tags")}
unknown = [tag for tag in tags if tag not in known]
if unknown:
print(f"Unknown tag(s): {', '.join(unknown)}", file=sys.stderr)
return 2
cur = conn.execute(
"INSERT INTO notes(content, tags, created_at) VALUES(?, ?, ?)",
(content, tags_json, created_at),
@@ -144,7 +191,8 @@ def cmd_list(args: argparse.Namespace) -> int:
print("No notes.")
return 0
for row in rows:
print(f"{id_to_display[row['id']]}. {row['content']}{_tags_display(row['tags'])}")
head, sep, rest = _urls_on_own_lines(row["content"]).partition("\n")
print(f"{id_to_display[row['id']]}. {head}{_tags_display(row['tags'])}{sep}{rest}")
return 0
@@ -169,6 +217,60 @@ def cmd_delete(args: argparse.Namespace) -> int:
return 0
def cmd_show(args: argparse.Namespace) -> int:
display_id: int = args.id
with _connect() as conn:
ids = _active_ids(conn)
idx = display_id - 1
if idx < 0 or idx >= len(ids):
print(f"No active note with display id={display_id}.")
return 1
nid = ids[idx]
row = conn.execute(
"SELECT id, content, tags, created_at FROM notes WHERE id = ?", (nid,)
).fetchone()
_log("SHOW", f"display_id={display_id} id={nid}")
tags = json.loads(row["tags"])
tags_line = " ".join(f"#{t}" for t in tags) if tags else "(none)"
print(f"Note [#{display_id}] (id={row['id']})")
print(f"created: {row['created_at']}")
print(f"tags: {tags_line}")
print(f"content: {row['content']}")
return 0
def cmd_tag_add(args: argparse.Namespace) -> int:
name = args.name.strip()
try:
_validate_tags([name])
except ValueError as exc:
print(str(exc), file=sys.stderr)
return 1
with _connect() as conn:
exists = conn.execute("SELECT 1 FROM tags WHERE name = ?", (name,)).fetchone()
if exists:
print(f"Tag '#{name}' already exists.")
return 0
created_at = datetime.now(timezone.utc).isoformat()
conn.execute("INSERT INTO tags(name, created_at) VALUES(?, ?)", (name, created_at))
conn.commit()
_log("TAG-ADD", f"name={name}")
print(f"Tag created: #{name}")
return 0
def cmd_tag_list(args: argparse.Namespace) -> int:
with _connect() as conn:
rows = conn.execute("SELECT name FROM tags ORDER BY name").fetchall()
_log("TAG-LIST", f"returned={len(rows)}")
if not rows:
print("No tags.")
return 0
for row in rows:
print(f"#{row['name']}")
return 0
def _main() -> int:
parser = argparse.ArgumentParser(description="Note store")
sub = parser.add_subparsers(dest="cmd", required=True)
@@ -182,17 +284,31 @@ def _main() -> int:
p_list.add_argument("--offset", type=int, default=0)
p_list.add_argument("--tag", nargs="+", metavar="TAG", help="Filter by tag (OR logic)")
p_show = sub.add_parser("show", help="Show one note in full by display ID")
p_show.add_argument("id", type=int, help="Display ID")
p_del = sub.add_parser("delete", help="Soft-delete a note by ID")
p_del.add_argument("id", type=int, help="Note ID")
p_tag_add = sub.add_parser("tag-add", help="Register a tag")
p_tag_add.add_argument("name", help="Tag name (lowercase, hyphens allowed)")
sub.add_parser("tag-list", help="List registered tags")
args = parser.parse_args()
if args.cmd == "add":
return cmd_add(args)
if args.cmd == "list":
return cmd_list(args)
if args.cmd == "show":
return cmd_show(args)
if args.cmd == "delete":
return cmd_delete(args)
if args.cmd == "tag-add":
return cmd_tag_add(args)
if args.cmd == "tag-list":
return cmd_tag_list(args)
return 0