diff --git a/.claude/skills/coord/SKILL.md b/.claude/skills/coord/SKILL.md index 25546681..9a5dd26e 100644 --- a/.claude/skills/coord/SKILL.md +++ b/.claude/skills/coord/SKILL.md @@ -27,6 +27,7 @@ bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" "$CLAUDETOOLS_ROOT/.claude/skills | `msg send "" --body-file ` | Same, body from a file (use for long/multi-line bodies — avoids shell-quoting breakage). | | `msg inbox [--all]` | Your unread messages (or `--all`). Print these prominently per the coord rule. | | `msg read ` | Mark a message read. | +| `msg purge --before YYYY-MM-DD [--to ] [--yes]` | Delete dealt-with messages older than the cutoff date. **DRY-RUN by default** (previews what would go); add `--yes` to actually delete. `--to` scopes to one recipient session. Refuses to run without `--before` (can't wipe the store by accident). Destructive + fleet-wide — coordination msgs are ephemeral; the durable record is in session logs. | | `todo add "" [--user U] [--project KEY] [--parent ID] [--source TXT] [--auto]` | Create a todo (auto-fills `created_by_user`/`created_by_machine`). `--text-file` for long text. | | `todo list [--user U] [--project KEY] [--status pending\|done\|all]` | List todos. | | `todo done ` | Mark a todo done (sets `completed_by`). | diff --git a/.claude/skills/coord/scripts/coord.py b/.claude/skills/coord/scripts/coord.py index fd2a2d5e..d60a29ff 100644 --- a/.claude/skills/coord/scripts/coord.py +++ b/.claude/skills/coord/scripts/coord.py @@ -14,6 +14,9 @@ Usage: : ALL | | /claude-main coord.py msg inbox [--all] # messages to me (unread by default) coord.py msg read + coord.py msg purge --before YYYY-MM-DD [--to ] [--yes] + # delete dealt-with messages older than the cutoff. DRY-RUN by default; + # add --yes to actually delete. --to scopes to one recipient session. coord.py todo add [--text-file PATH] [--user U] [--project KEY] [--parent ID] [--source TEXT] coord.py todo list [--user U] [--project KEY] [--status pending|done|all] coord.py todo done @@ -150,6 +153,56 @@ def c_msg_read(a): print(f"[coord] marked read: {a.id}") +def c_msg_purge(a): + """Delete dealt-with messages older than --before (date cutoff). DRY-RUN unless --yes. + + Refuses to run without --before so it can never wipe the whole store by accident. + Coordination messages are ephemeral; the durable record is in session logs. + """ + before = (a.before or "").strip() + if not (len(before) == 10 and before[4] == "-" and before[7] == "-" + and before.replace("-", "").isdigit()): + print("[coord] --before must be YYYY-MM-DD", file=sys.stderr); sys.exit(2) + to = resolve_to(a.to) if a.to else None + # Fetch all messages, paginating over the API's 1000-row limit cap. + msgs, skip = [], 0 + while True: + q = {"limit": 1000, "skip": skip} + if to: + q["to_session"] = to + st, r = call("GET", "/messages", query=q); die(st, r, ok=(200,)) + batch = listify(r, "messages") + msgs += batch + if len(batch) < 1000: + break + skip += 1000 + # Strictly before the cutoff DATE (compare the date prefix, so the cutoff day is kept). + targets = [m for m in msgs if (m.get("created_at") or "")[:10] < before] + scope = f" to {to}" if to else "" + print(f"[coord] {len(targets)} message(s){scope} before {before} (of {len(msgs)} fetched)") + if not targets: + return + if not a.yes: + for m in targets[:20]: + print(f" {(m.get('created_at') or '')[:16]} | {m.get('from_session')} -> " + f"{m.get('to_session')} | {str(m.get('subject',''))[:50]}") + if len(targets) > 20: + print(f" ... and {len(targets) - 20} more") + print(f"[coord] DRY RUN - re-run with --yes to delete these {len(targets)} message(s)") + return + ok = fail = 0 + for m in targets: + st, r = call("DELETE", f"/messages/{m.get('id')}") + if st in (200, 204): + ok += 1 + else: + fail += 1 + print(f"[coord] purged {ok} message(s)" + (f", {fail} FAILED" if fail else "")) + if fail: + _log_skill_error("coord", f"msg purge: {fail} of {len(targets)} deletes failed", + context=f"before={before}") + + def c_todo_add(a): text = a.text if a.text_file: @@ -236,6 +289,8 @@ def main(): s.add_argument("--body-file"); s.add_argument("--project"); s.set_defaults(fn=c_msg_send) ib = m.add_parser("inbox"); ib.add_argument("--all", action="store_true"); ib.set_defaults(fn=c_msg_inbox) rd = m.add_parser("read"); rd.add_argument("id"); rd.set_defaults(fn=c_msg_read) + pg = m.add_parser("purge"); pg.add_argument("--before", required=True); pg.add_argument("--to") + pg.add_argument("--yes", action="store_true"); pg.set_defaults(fn=c_msg_purge) t = sub.add_parser("todo").add_subparsers(dest="sub", required=True) ta = t.add_parser("add"); ta.add_argument("text", nargs="?"); ta.add_argument("--text-file")