← Commit history

Publish 0.1.0

Ray ·b9c70d04a1 ·3mo ago ·parent d78f488
6 files changed +290
SKILL.mdadded+42
@@ -0,0 +1,42 @@+---+name: adom-agent-watch+description: A background cron that pulls your unresolved Adom Wiki notifications and wakes your AI to resolve them. Use when the user wants their agent to auto-follow-up on wiki issue updates (closes, replies) in the background, or asks to "watch my notifications", "wake my agent on updates", or "resolve wiki updates automatically".+---++# adom-agent-watch++The last mile for the notifications feed (adom/wiki #58): pull YOUR unresolved+wiki notifications and wake your agent to resolve them — verify a shipped fix,+reply to a review, resume related work — then mark them handled.++## Use it++```bash+adompkg install adom-agent-watch      # PATH binary + cron (every 30 min)+adom-agent-watch status               # source, wake cmd, watched pages+adom-agent-watch run                  # one poll cycle now+adom-agent-watch test-wake            # confirm the wake command fires+```++## How it resolves updates++Each cycle it reads your unresolved notifications from the best source available+and, on anything new, wakes your agent with a digest:++- **feed** — `adom-wiki notifications list --unread --json` (the #58 inbox), else+- **interim** — polls issues you authored on the watched pages (default+  `adom/wiki`) and diffs status/reply-count vs a local snapshot.++The wake command (default `claude -p` when `claude` is on PATH; configurable in+`~/.config/adom-agent-watch/config.json`) is invoked with the digest + an+instruction to open each item, do the follow-up, and mark it handled+(`adom-wiki notifications read <id>`). It writes the digest to+`~/.local/state/adom-agent-watch/inbox.md` regardless, so nothing is lost if the+wake command isn't set.++## When you (the agent) are woken by it++You'll get a prompt listing unresolved wiki updates. For each: open it+(`adom-wiki issue view` / the page), take the follow-up (verify a shipped fix on+the live page, reply, or resume the related work), then mark it handled. If+nothing is actionable, say why. Be concise.
bin/adom-agent-watchadded+201
@@ -0,0 +1,201 @@+#!/usr/bin/env python3+"""adom-agent-watch — pull YOUR unresolved Adom Wiki notifications and wake your+agent to resolve them in the background. One poll cycle; run it from cron.++Sources, best available first (degrades gracefully, so it works today):+  1. the #58 notifications feed:  adom-wiki notifications list --unread --json+  2. interim fallback:            poll issues you authored on watched pages,+                                  diff each poll vs a local snapshot.++On new items it writes a digest to ~/.local/state/adom-agent-watch/inbox.md and+invokes your wake command (default: `claude -p <prompt>` if `claude` is on PATH),+so your agent opens the issues, does the follow-up, and marks them handled.++Config (optional): ~/.config/adom-agent-watch/config.json+  { "wake_cmd": "claude -p", "pages": ["adom/wiki"], "user": "<handle>" }++Subcommands:  run (default) | status | test-wake+"""+import os, sys, json, subprocess, pathlib++HOME = pathlib.Path.home()+STATE_DIR = HOME / ".local/state/adom-agent-watch"+STATE = STATE_DIR / "state.json"+INBOX = STATE_DIR / "inbox.md"+CONFIG = HOME / ".config/adom-agent-watch/config.json"+++def cfg():+    try:+        return json.loads(CONFIG.read_text())+    except Exception:+        return {}+++def sh(args, timeout=60):+    try:+        return subprocess.run(args, capture_output=True, text=True, timeout=timeout)+    except Exception:+        return None+++def load_state():+    try:+        return json.loads(STATE.read_text())+    except Exception:+        return {"seen": {}}+++def save_state(s):+    STATE_DIR.mkdir(parents=True, exist_ok=True)+    STATE.write_text(json.dumps(s))+++def have(cmd):+    r = sh(["bash", "-lc", f"command -v {cmd}"])+    return bool(r and r.returncode == 0 and r.stdout.strip())+++# ---- source 1: the #58 notifications feed ---------------------------------+def from_feed():+    """Returns a list of unread items, or None if the endpoint isn't available."""+    r = sh(["adom-wiki", "notifications", "list", "--unread", "--json"])+    if not r or r.returncode != 0:+        return None+    try:+        d = json.loads(r.stdout)+    except Exception:+        return None+    if d.get("status") == "unavailable":   # verb wired, endpoint not deployed yet+        return None+    items = (d.get("data") or {}).get("notifications")+    if items is None:+        return None+    return [{"id": i.get("id"), "ref": i.get("ref"), "title": i.get("title"),+             "actor": i.get("actor"), "type": i.get("type")} for i in items]+++# ---- source 2: interim direct-issue poll ----------------------------------+def whoami():+    r = sh(["adom-wiki", "whoami", "--json"])+    if not r or r.returncode != 0:+        return None+    try:+        u = (json.loads(r.stdout).get("data") or {})+        return (u.get("user") or u).get("username") or u.get("name")+    except Exception:+        return None+++def from_interim(pages, me, state):+    seen = state.setdefault("seen", {})+    updates = []+    for page in pages:+        r = sh(["adom-wiki", "issue", "list", page, "--json"])+        if not r or r.returncode != 0:+            continue+        try:+            data = json.loads(r.stdout).get("data", [])+        except Exception:+            continue+        rows = data if isinstance(data, list) else data.get("discussions", [])+        for it in rows:+            if me and (it.get("author_name") or "").lower() != me.lower():+                continue+            key = f"{page}#{it.get('number')}"+            cur = {"status": it.get("status"), "replies": it.get("reply_count")}+            prev = seen.get(key)+            seen[key] = cur+            if prev is None:                # first sight: record, don't alert+                continue+            if prev != cur:+                what = []+                if prev.get("status") != cur.get("status"):+                    what.append(str(cur.get("status")))+                if (cur.get("replies") or 0) > (prev.get("replies") or 0):+                    what.append("new reply")+                updates.append({"id": None, "ref": key, "title": it.get("title"),+                                "actor": None, "type": ", ".join(what) or "updated"})+    return updates+++def digest_of(updates, mode):+    lines = []+    for u in updates:+        line = f"- [{u.get('type', 'update')}] {u.get('ref')} — {u.get('title') or ''}"+        if u.get("actor"):+            line += f" (by {u['actor']})"+        lines.append(line)+    return f"You have {len(updates)} unresolved Adom Wiki update(s) [{mode}]:\n" + "\n".join(lines)+++def wake(prompt, c):+    cmd = c.get("wake_cmd")+    if cmd is None:+        cmd = "claude -p" if have("claude") else None+    if not cmd:+        return None+    try:+        subprocess.Popen(cmd.split() + [prompt],+                         stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,+                         stdin=subprocess.DEVNULL, start_new_session=True)+        return cmd+    except Exception:+        return None+++def cmd_run():+    c = cfg()+    state = load_state()+    feed = from_feed()+    if feed is not None:+        # feed items are already the UNREAD set; the server's read-state is the dedup.+        updates, mode, feed_ids = feed, "feed", [u["id"] for u in feed if u.get("id")]+    else:+        me = c.get("user") or whoami()+        updates, mode, feed_ids = from_interim(c.get("pages") or ["adom/wiki"], me, state), "interim", []+    save_state(state)+    if not updates:+        return++    d = digest_of(updates, mode)+    STATE_DIR.mkdir(parents=True, exist_ok=True)+    INBOX.write_text(d + "\n")+    prompt = (d + "\n\nFor each: open it (`adom-wiki issue view` / the page), do the follow-up "+              "(verify a shipped fix on the live page, reply, or resume the related work), then "+              "mark it handled (`adom-wiki notifications read <id>` when available). If nothing is "+              "actionable, note why. Be concise.")+    used = wake(prompt, c)+    if used and feed_ids:+        # Woke the agent — clear these from the server inbox so they don't re-fire.+        sh(["adom-wiki", "notifications", "read"] + [str(i) for i in feed_ids])+    if used:+        print(f"woke agent on {len(updates)} update(s) via `{used}`")+    else:+        print(f"{len(updates)} update(s) written to {INBOX} "+              f"(set wake_cmd in {CONFIG} or install `claude` to auto-resolve)")+++def cmd_status():+    c = cfg()+    print(f"config:  {CONFIG}  {'(present)' if CONFIG.exists() else '(defaults)'}")+    print(f"wake:    {c.get('wake_cmd') or ('claude -p' if have('claude') else 'NONE — set wake_cmd')}")+    print(f"source:  {'feed (#58)' if from_feed() is not None else 'interim poll (feed not live yet)'}")+    print(f"pages:   {c.get('pages') or ['adom/wiki']}")+    print(f"state:   {STATE}  {'(present)' if STATE.exists() else '(empty)'}")+    if INBOX.exists():+        print("last inbox:\n" + INBOX.read_text())+++def cmd_test_wake():+    used = wake("adom-agent-watch test wake — you can ignore this. Reply 'ok'.", cfg())+    print(f"wake via `{used}`" if used else "no wake command (set wake_cmd or install claude)")+++def main():+    cmd = sys.argv[1] if len(sys.argv) > 1 else "run"+    {"run": cmd_run, "status": cmd_status, "test-wake": cmd_test_wake}.get(cmd, cmd_run)()+++if __name__ == "__main__":+    main()
install.shadded+22
@@ -0,0 +1,22 @@+#!/usr/bin/env bash+# Install adom-agent-watch: put the poller on PATH + a cron that runs it every+# 30 min. No secrets. Idempotent.+set -uo pipefail+DIR="$(cd "$(dirname "$0")" && pwd)"+mkdir -p "$HOME/.local/bin" "$HOME/.config/adom-agent-watch"+install -m0755 "$DIR/bin/adom-agent-watch" "$HOME/.local/bin/adom-agent-watch"+[ -f "$HOME/.config/adom-agent-watch/config.json" ] || cat > "$HOME/.config/adom-agent-watch/config.json" <<JSON+{+  "_comment": "wake_cmd runs your agent with the digest appended; pages is the interim-poll watchlist (used only until the #58 feed is live).",+  "wake_cmd": null,+  "pages": ["adom/wiki"]+}+JSON+BIN="$HOME/.local/bin/adom-agent-watch"+CRON="*/30 * * * * $BIN run >> /tmp/adom-agent-watch.log 2>&1"+EXISTING="$(crontab -l 2>/dev/null || true)"+FILTERED="$(echo "$EXISTING" | grep -v 'adom-agent-watch' || true)"+printf '%s\n%s\n' "$FILTERED" "$CRON" | sed '/^$/d' | crontab -+echo "adom-agent-watch installed (cron every 30 min)."+echo "  wake command: $("$BIN" status 2>/dev/null | grep -m1 wake || echo 'set wake_cmd in ~/.config/adom-agent-watch/config.json')"+echo "  try:  adom-agent-watch status   |   adom-agent-watch run"
package.jsonadded+20
@@ -0,0 +1,20 @@+{+  "slug": "adom-agent-watch",+  "version": "0.1.0",+  "type": "app",+  "description": "Pull your unresolved Adom Wiki notifications and wake your AI to resolve them in the background — the last mile that turns the notifications feed (#58) into your agent actually doing the follow-up.",+  "dependencies": {},+  "license": "MIT",+  "needs_sudo": false,+  "files": ["bin/**", "install.sh", "uninstall.sh", "package.json", "page.json", "README.md", "SKILL.md", "screenshots/**"],+  "scripts": { "install": "./install.sh", "uninstall": "./uninstall.sh" },+  "tags": ["notifications", "agent", "automation", "cron", "background", "feed", "ai", "wiki"],+  "discovery_triggers": ["agent watch", "notification agent", "background resolver", "wake my agent", "auto resolve issues", "wiki notifications agent", "watch my issues", "poll my notifications"],+  "discovery_pitch": "A background cron that pulls your unresolved wiki notifications and wakes your AI to resolve them — prefers the #58 feed, falls back to a direct issue poll, so it works today.",+  "sample_prompts": [+    {"label": "Set it up", "prompt": "install adom-agent-watch so my AI resolves wiki updates in the background"},+    {"label": "Check status", "prompt": "run adom-agent-watch status"},+    {"label": "One cycle now", "prompt": "run adom-agent-watch once and resolve anything new"}+  ],+  "hero": { "path": "screenshots/hero.png" }+}
screenshots/hero.pngadded
⋯ 1 unchanged line ⋯
uninstall.shadded+5
@@ -0,0 +1,5 @@+#!/usr/bin/env bash+set -uo pipefail+rm -f "$HOME/.local/bin/adom-agent-watch"+printf '%s\n' "$(crontab -l 2>/dev/null | grep -v 'adom-agent-watch' || true)" | sed '/^$/d' | crontab - 2>/dev/null || true+echo "adom-agent-watch removed (cron + binary). Config/state left at ~/.config & ~/.local/state."