{"id":50634872,"url":"https://github.com/j7-dev/watcher","last_synced_at":"2026-06-07T01:30:44.471Z","repository":{"id":357369404,"uuid":"1236452811","full_name":"j7-dev/watcher","owner":"j7-dev","description":"每當 Claude Code 停下時  Codex 幫忙決策","archived":false,"fork":false,"pushed_at":"2026-05-12T12:37:17.000Z","size":46,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"master","last_synced_at":"2026-05-12T14:32:06.760Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/j7-dev.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-05-12T09:01:30.000Z","updated_at":"2026-05-12T12:37:18.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/j7-dev/watcher","commit_stats":null,"previous_names":["j7-dev/watcher"],"tags_count":3,"template":false,"template_full_name":null,"purl":"pkg:github/j7-dev/watcher","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/j7-dev%2Fwatcher","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/j7-dev%2Fwatcher/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/j7-dev%2Fwatcher/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/j7-dev%2Fwatcher/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/j7-dev","download_url":"https://codeload.github.com/j7-dev/watcher/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/j7-dev%2Fwatcher/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34006037,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-06T02:00:07.033Z","response_time":107,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-06-07T01:30:43.680Z","updated_at":"2026-06-07T01:30:44.466Z","avatar_url":"https://github.com/j7-dev.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# watcher\n\n監聽 **Windows 上 WezTerm** 裡所有跑 Claude Code 的 pane，偵測到「等使用者輸入」時把畫面 + session 脈絡餵給 Codex CLI 決定怎麼回，再 `wezterm cli send-text` 送答案回去。\n\n```\n                        ┌─► Claude Code Stop hook ─► TCP loopback ─► watcher ─┐\nWezTerm pane (claude) ──┤        (帶 transcript_path)                         ├─► codex exec ─► wezterm cli send-text\n                        └─► 每 180s 輪詢 (fallback) ──────────────────────────┘\n```\n\n主觸發是 Stop hook（即時、零延遲）；輪詢只是 fallback。Stop hook payload 內帶 `transcript_path`，daemon 會從中抽出輕量 session metadata（主題、用戶最初/當前需求）注入 codex prompt，幫助消歧義——但**畫面永遠是真實來源**，脈絡只能輔助。\n\n專案以 **Claude Code plugin** 形式發行：marketplace 安裝負責 hook 自動註冊；daemon 則從 clone 出來的目錄跑（plugin 不會代啟動）。\n\n---\n\n## 環境需求\n\n- **Windows 10/11**（Windows-only — 已移除 WSL2 / tmux 支援）\n- **WezTerm**（任意近期版本，需含 `wezterm cli`）\n- **PowerShell 7+**（`pwsh`，用於 daemon 控制腳本）\n- **uv**（Python 套件 / 環境管理器；自動建 .venv）\n- **Python 3.12+**（uv 會處理）\n- **Node.js + `@openai/codex` ≥ 0.130.0** + 已 `codex login`（ChatGPT 訂閱授權）\n- **git for Windows**（含 bash，用於 pre-push release hook）\n\n---\n\n## 安裝\n\n**1. 裝 plugin（hook + slash commands）**\n\n在 Claude Code 內：\n\n```\n/plugin marketplace add j7-dev/watcher\n/plugin install watcher@watcher\n```\n\n或 CLI：\n\n```powershell\nclaude plugin marketplace add j7-dev/watcher\nclaude plugin install watcher@watcher\n```\n\n**2. clone repo 跑 daemon**\n\n```powershell\ngit clone https://github.com/j7-dev/watcher $env:USERPROFILE\\DEV\\watcher\ncd $env:USERPROFILE\\DEV\\watcher\nuv sync\nuv run watcher.py\n```\n\n\u003e 為什麼分兩步：marketplace 把 plugin 放在 `~/.claude/plugins/cache/.../\u003csha\u003e/`，路徑會跟著版本 SHA 變動，不適合做 daemon 啟動目錄。Hook 走 `${CLAUDE_PLUGIN_ROOT}` 自動解析沒問題；daemon 從固定 clone 路徑跑比較穩。\n\n建議常駐方式（讓 daemon 跑在獨立 WezTerm window 內、可隨時 attach 看 log）：\n\n```powershell\npwsh scripts\\watcher-daemon.ps1 start\n# attach 看 live log：\nwezterm cli activate-pane --pane-id \u003c印在 start 輸出的 id\u003e\n```\n\n---\n\n## 更新\n\n```\n/plugin update watcher@watcher\n```\n\n或 CLI：\n\n```powershell\nclaude plugin marketplace update watcher\nclaude plugin update watcher@watcher\n```\n\nDaemon 端：\n\n```powershell\ncd $env:USERPROFILE\\DEV\\watcher\ngit pull\nuv sync\npwsh scripts\\watcher-daemon.ps1 stop\npwsh scripts\\watcher-daemon.ps1 start\n```\n\n---\n\n## 移除\n\n```\n/plugin uninstall watcher@watcher\n/plugin marketplace remove watcher\n```\n\nDaemon：`pwsh scripts\\watcher-daemon.ps1 stop`。clone 目錄可選擇刪除。\n\n---\n\n## 主要用法\n\nwatcher 是常駐 daemon。三種啟動模式：\n\n| 指令 | 用途 |\n|------|------|\n| `uv run watcher.py --once` | 跑一輪偵測就結束，不呼叫 codex、不 send-text。第一次驗證 pane 偵測 |\n| `uv run watcher.py --dry-run` | 完整迴圈呼叫 codex，但**不** send-text。建議搭配 `$env:WATCHER_LOG_ENABLED = \"true\"` |\n| `uv run watcher.py` | 正式模式 |\n\nwatcher **不做 PID/process pre-filter**——`wezterm cli list` JSON 不含 process 欄位，所以 daemon 對所有 pane 都 capture，靠 `classify()` 視覺指紋（NBSP `❯` + HR 線 + Braille spinner）過濾出真正的 Claude pane。非 Claude pane 一律分類為 `other` 並跳過。\n\n停止：前景 `Ctrl-C`，或透過 daemon 控制腳本 `pwsh scripts\\watcher-daemon.ps1 stop`。\n\n驗證 hook：在另一 pane 跑 Claude Code 回一個 turn → watcher stderr 幾秒內應印 `hook recv pane=N`。Daemon 監聽 port 寫在 `~/.watcher/socket-info.json`。\n\n---\n\n## 設定\n\n**優先序**：`WATCHER_\u003cKEY\u003e` 環境變數 \u003e `config.toml` \u003e 內建預設值。設定檔位置：clone 目錄下的 `config.toml`（可省略；缺檔走預設）。改 `config.toml` 後**必須重啟 watcher**。\n\n### 可調項目\n\n| 鍵 | 預設 | 說明 |\n|----|------|------|\n| **觸發** | | |\n| `poll_interval_seconds` | `180` | fallback 輪詢間隔（秒）。hook 路徑即時、不受此影響 |\n| `per_pane_cooldown_seconds` | `15` | 同 pane 觸發後幾秒內不再觸發 |\n| `max_responses_per_window` | `5` | killswitch：時間窗內最多回應次數 |\n| `response_window_minutes` | `5` | killswitch 時間窗（分鐘） |\n| **Hook 通道 (TCP loopback)** | | |\n| `socket_enabled` | `true` | 開啟 TCP server 接 Stop hook |\n| `socket_host` | `\"127.0.0.1\"` | 通常不需要改；非 loopback 會被 handler 拒絕 |\n| `socket_port` | `47823` | port 被占用會自動嘗試 +1..+10，實際綁定值寫到 `~/.watcher/socket-info.json` |\n| `socket_token` | `\"\"` | 非空時 hook payload 必須帶相同 token（多 user 環境用） |\n| **Codex** | | |\n| `codex_binary` | `\"codex\"` | codex 執行檔 |\n| `codex_timeout_seconds` | `90` | 單次 `codex exec` 最長等待 |\n| **省 codex 的兩道短路** | | |\n| `skip_predictor_enabled` | `true` | 啟發式 skip 預測器（畫面分類為 `input` 但回看視窗找不到問題標記 → 直接 skip，不打 codex） |\n| `skip_predictor_lookback_lines` | `15` | 從 `❯` 往上看幾行非空行做判定 |\n| `skip_predictor_extra_markers` | `[]` | 額外問題標記字串（小寫子字串比對）。內建含 `?` / `？` / `do you` / `continue` / `是否` / `請問` 等。env var 用逗號分隔 |\n| `skip_decision_cache_ttl_seconds` | `300` | codex 回 `skip` 後快取 TTL（秒）。同畫面 + 同 user 當前需求在 TTL 內再出現直接命中、不打 codex。`0` = 不快取 |\n| **畫面擷取** | | |\n| `capture_scrollback_lines` | `100` | 每次抓畫面含多少行 scrollback（初始）。100 已涵蓋 classify 最大 lookback |\n| `capture_escalation_step` | `400` | 偵測到邊框截斷（`╰` 多於 `╭`）時，每次往上加抓的行數 |\n| `max_capture_scrollback_lines` | `2000` | 自適應擷取的上限 |\n| `prompt_context_lines` | `60` | 送進 codex 的畫面行數上限（裁切到當前決策框） |\n| `hr_min_length` | `50` | 水平線最小 `─` 字元數 |\n| **日誌** | | |\n| `log_enabled` | `false` | 整體 log 開關。預設關（快照可能含 token） |\n| `log_format` | `\"%(asctime)s %(levelname)s %(message)s\"` | Python logging 格式字串 |\n| `log_datefmt` | `\"%Y-%m-%d %H:%M:%S\"` | `asctime` 時間格式 |\n| `log_max_bytes` | `10_000_000` | `watcher.log` 大小上限（byte） |\n| `log_backups` | `3` | rotate 後保留份數 |\n| `log_retention_days` | `0` | `\u003e0` 時每小時清 `logs/triggers/` 內 mtime 超過 N 天的檔 |\n| `log_pane_ids` | `[]` | 只把 `logs/triggers/*.json` 與 `codex-out-*.txt` 限定在指定 pane（主 `watcher.log` 仍全量）。元素接受 `18` / `\"18\"` / `\"%18\"` |\n| **Milestone runner（`/watcher:milestone-*`）** | | |\n| `milestone_toggle_state_path` | `\"\"`（自動推導） | toggle 狀態檔位置。預設 `$XDG_STATE_HOME/watcher/milestone-toggle.json` 或 `~/.local/state/watcher/milestone-toggle.json` |\n| `milestone_command_text` | `\"/milestone-runner\"` | toggle ON 時要往 pane 注入的指令字串 |\n| `milestone_max_reinjects_per_window` | `5` | milestone 重注 killswitch（時間窗內最多重注次數） |\n| `milestone_reinject_cooldown_seconds` | `5` | 重注後 cooldown 秒數 |\n\n### 常用 override（PowerShell）\n\n```powershell\n$env:WATCHER_LOG_ENABLED = \"true\"; $env:WATCHER_POLL_INTERVAL_SECONDS = \"60\"; uv run watcher.py\n$env:WATCHER_SOCKET_ENABLED = \"false\"; uv run watcher.py        # 純輪詢、停 hook 通道\n$env:WATCHER_LOG_PANE_IDS = \"18,19\"; uv run watcher.py          # 只記指定 pane 的 audit\n$env:WATCHER_SOCKET_PORT = \"47900\"; uv run watcher.py           # 自訂 port\n```\n\n---\n\n## 工作原理\n\n**路徑 A：Stop hook（主、即時）**\n1. Claude Code turn 結束 → 觸發 Stop hook\n2. `claude-stop-notify.py` 讀 `$WEZTERM_PANE` env var + stdin JSON payload（含 `session_id` / `transcript_path` / `cwd`），連 TCP `127.0.0.1:47823`（或 `socket-info.json` 指定 port）\n3. watcher 收到 → 把 `transcript_path` 存到 `PaneState` → `wezterm cli get-text` → `classify()` → 兩道短路（cache hit / 啟發式 skip 預測）→ codex → `wezterm cli send-text`\n\n**路徑 B：輪詢（fallback）**\n1. 每 `poll_interval_seconds` 跑 `wezterm cli list --format json`\n2. 對每個 pane 跑 capture + classify（**無 PID pre-filter**——WezTerm JSON 不給 process 欄位）\n3. 通過 classify 即觸發評估；先前收過 hook 的 pane 仍可用 stored `transcript_path` 抽 session metadata\n\n**分類** (`classify()`)：\n- `working`：title 首字是 Braille spinner（U+2800–U+28FF），**或**畫面**最後 5 行**含 `esc to interrupt` / `(ctrl+o to expand)` → 跳過\n- `menu`：尾段有 `❯ 1.`、`❯ 2.` 編號選單\n- `input`：底部 `─...─` + `❯ ` 空輸入（NBSP）+ `─...─` 輸入框\n- `other`：以上皆非 → 跳過（非 Claude pane、外部 shell pane 等）\n\n只有 `input` 與 `menu` 送 codex。\n\n**送 codex 前的兩道短路**（省 token + 省 codex round-trip）：\n1. **決策快取**：`sha256(清理後畫面 + 當前 user 需求)` 為 key，TTL 內 cache hit 直接 skip\n2. **啟發式 skip 預測**：分類為 `input` 但回看視窗找不到任何問題標記（`?` / `？` / `do you` / `continue` / `是否` / 編號列表 ...）→ 視為閒置 pane、不打 codex\n\n**Codex 回傳** (`response_schema.json` 強制)：\n\n```json\n{ \"action\": \"text\" | \"key\" | \"enter\" | \"skip\", \"value\": \"...\" }\n```\n\n- `text` → 兩次 `wezterm cli send-text --no-paste --pane-id N` 呼叫：先送文字、`sleep 0.15s`、再送 `\\r`。**不可合併單次**——Claude Code TUI 會把單次長字串 + `\\r` 當 paste-like 處理，trailing CR 變成輸入內換行而非 submit\n- `key` → 單次 call，payload `\u003cdigit\u003e\\r`（短 payload 不觸發 paste 啟發）\n- `enter` → 單次 call，payload `\\r`\n- `skip` → 不動，寫 audit\n\n送出前**再抓一次**畫面比對（chrome-stripped）；若已被人動過就 abort，outcome `aborted-pane-changed`。`--no-paste` 是必要的——預設 bracketed-paste 模式會被 Claude Code 當作貼上資料而非按鍵。\n\n\u003e Killswitch 跳閘後該 pane **永久**停用到 daemon 重啟。防 codex/claude 互相鏈式對話爆走。\n\n---\n\n## Session 脈絡注入\n\nStop hook 觸發時，daemon 從 stdin payload 取 `transcript_path`（即 `~/.claude/projects/\u003cencoded-cwd\u003e/\u003csession-id\u003e.jsonl`），forward-scan 抽出三類輕量 metadata entries：\n\n| Entry type | 抽出欄位 | 用途 |\n|------------|---------|------|\n| `ai-title` | `aiTitle`（每輪覆寫，留最後一筆） | 一句話主題 |\n| `last-prompt` | `lastPrompt`（每輪 append；第一筆 = 用戶最初需求、最後一筆 = 用戶當前需求） | 意圖消歧義 |\n| `summary` | `summary`（只在 `/compact` 後生成） | 中段對話 AI 摘要 |\n\n每欄上限 800 chars，整體加進 prompt \u003c 1KB，對 codex 延遲幾乎無感。注入後的 prompt 加一條規則：**脈絡僅供消歧義，與畫面衝突時以畫面為準**——保留「螢幕為唯一真實」鐵則。\n\n決策快取 key 把 `current_request` 拌進 hash，避免同畫面在不同 user 需求下共用 skip 決策。\n\n從未收過 Stop hook 的 pane（純輪詢觸發）`transcript_path` 為 `None`，走原本 screen-only 模式。Daemon 重啟後 `PaneState` 丟失，下個 Stop hook 重新填回。\n\n---\n\n## 日誌與審計\n\n預設 `log_enabled = false`——只輸出 stderr，不落地（畫面快照可能含 token / API key）。開啟後：\n\n```\nlogs/                              # 0700，.gitignore 已加\n├── watcher.log                    # 主 log（rotate：10MB × 3 份）\n└── triggers/\n    ├── 1715500000-3.json          # 每次觸發：快照 + codex 回覆 + 動作\n    └── codex-out-*.txt\n```\n\n要瘦身 forensic 檔，用 `log_pane_ids` 鎖定特定 pane：\n\n```toml\nlog_pane_ids = [18]                # 或 [\"18\"] 或歷史相容 [\"%18\"]\n```\n\n或 env var：`$env:WATCHER_LOG_PANE_IDS = \"18,19\"`（逗號分隔）。主 `watcher.log` 不受影響，仍全量記錄。`log_enabled = false` 仍是總開關。\n\n---\n\n## 疑難排解\n\n| 症狀 | 修法 |\n|------|------|\n| log 沒出現 `hook recv pane=N` | `claude --debug` 看 hook 是否註冊；確認 `/plugin list` 有 watcher |\n| `hook event for unknown pane N` | 重開 Claude Code |\n| hook timeout | 確認 daemon 還活著（`pwsh scripts\\watcher-daemon.ps1 status`）、`~/.watcher/socket-info.json` 還在 |\n| `codex: command not found` | `Get-Command codex` + `codex login status`，確認 PATH 含 nodejs 目錄 |\n| `wezterm cli list` 失敗 | 確認 WezTerm GUI 正在跑（mux server 需要 GUI 啟動） |\n| watcher 偵測不到 pane | `uv run watcher.py --once` 看分類；確認該 pane 真的在跑 Claude Code 且處於 `input`/`menu` 狀態 |\n| pane title 是 Braille spinner 但 user 已 type 完 | Claude Code 偶爾不重置 title。watcher 把 Braille title 一律判 `working` 跳過——這是刻意的避免誤觸。下個 Stop hook 會修正狀態 |\n| killswitch 一直跳 | 調高 `per_pane_cooldown_seconds` 或降 `max_responses_per_window`，跳了**必須重啟 daemon** |\n| codex 一直回 `skip` | 開 log 看 `logs/triggers/*.json`，常因提示太保守 |\n| Port 47823 被占用 | daemon 會自動嘗試 +1..+10；查 `~/.watcher/socket-info.json` 看實際 port |\n| codex 收不到 session 脈絡 | 該 pane 從未收過 Stop hook（純輪詢觸發），或 daemon 剛重啟。讓 Claude Code 跑完一個 turn 觸發 hook 即會記下 `transcript_path` |\n\n---\n\n## Plugin 結構\n\n```\nwatcher/\n├── .claude-plugin/\n│   ├── plugin.json          # plugin manifest\n│   └── marketplace.json     # marketplace 定義（單 plugin）\n├── hooks/\n│   ├── hooks.json           # 自動註冊 Stop hook（用 ${CLAUDE_PLUGIN_ROOT}）\n│   └── claude-stop-notify.py\n├── commands/                # /watcher:* slash commands\n│   ├── start.md             # 啟動 daemon\n│   ├── stop.md              # 停止 daemon\n│   ├── status.md            # 查 daemon 狀態\n│   ├── install-hook.md      # 安裝 Stop hook 進 ~/.claude/settings.json\n│   ├── cron-setup.md        # 註冊 Windows Task Scheduler cron（periodic claude -p）\n│   ├── cron-status.md       # 看 cron 狀態\n│   ├── cron-remove.md       # 移除 cron\n│   ├── milestone-on.md      # 對當前 pane 啟用 /milestone-runner 自動重注\n│   ├── milestone-off.md     # 關閉\n│   └── milestone-status.md  # 看所有 milestone-toggle 狀態\n├── scripts/\n│   ├── watcher-daemon.ps1   # daemon start/stop/status（PowerShell）\n│   ├── install-hook.py      # 自動寫 settings.json\n│   ├── milestone-toggle.py  # /watcher:milestone-* 後端\n│   ├── cron-setup.ps1       # Windows Task Scheduler 註冊\n│   ├── release.sh           # pre-push 自動版號 bump（bash via git-for-windows）\n│   └── git-hooks/pre-push\n├── watcher.py               # daemon 本體（plugin 不啟動，要自己跑）\n├── config.toml\n└── response_schema.json\n```\n\n---\n\n## Slash commands 一覽\n\n| 指令 | 用途 |\n|------|------|\n| `/watcher:start` | 在獨立 WezTerm window 啟動 daemon |\n| `/watcher:stop` | 停止 daemon（殺掉 daemon 那個 pane） |\n| `/watcher:status` | 顯示 daemon pid / port / pane id |\n| `/watcher:install-hook` | 把 Stop hook 寫進 `~/.claude/settings.json`（plugin marketplace 已自動處理；此命令是手動 fallback） |\n| `/watcher:milestone-on` | 對當前 pane 啟用 `/milestone-runner` 自動重注（適用於：每次 Stop 都希望 Claude 繼續往下推進的場景） |\n| `/watcher:milestone-off` | 對當前 pane 關閉 |\n| `/watcher:milestone-status` | 列出所有有 milestone-toggle 紀錄的 pane 與其狀態 |\n| `/watcher:cron-setup [interval]` | 對當前專案註冊 Windows Task Scheduler 定期 `claude -p`（與 daemon 無關，是另一條 cron 路徑） |\n| `/watcher:cron-status [--all]` | 查 cron 狀態 |\n| `/watcher:cron-remove [--all]` | 移除 cron |\n\n---\n\n## 已知限制\n\n- 沒有 Windows Service / 開機自動啟動（要手動 `watcher-daemon.ps1 start` 或 `/watcher:start`）\n- Persona 內嵌在 `PROMPT_TEMPLATE`，不支援多 persona 切換\n- WezTerm GUI 必須先啟動，daemon 才能透過 `wezterm cli` 與其溝通\n- `wezterm cli get-text` 對 Claude Code 的 alt-screen 只看得到 viewport（~40-50 行），無法回溯更早的 scrollback；對 classify() 已足夠，但 forensic audit 看不到完整對話歷史\n- Session 脈絡注入只在 Stop hook 觸發時或先前收過 hook 的 pane 才有；純輪詢的 pane 沒 `transcript_path` → 走 screen-only fallback\n- `PaneState` 在 daemon 重啟後丟失，包含已記錄的 `transcript_path` 與 killswitch 計數——重啟等於完全 reset\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fj7-dev%2Fwatcher","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fj7-dev%2Fwatcher","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fj7-dev%2Fwatcher/lists"}