provozni zaloha
This commit is contained in:
118
skills/note/scripts/note.py
Normal file → Executable file
118
skills/note/scripts/note.py
Normal file → Executable 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
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user