ring-cli 0.4.0__tar.gz → 0.6.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. {ring_cli-0.4.0 → ring_cli-0.6.0}/PKG-INFO +91 -9
  2. {ring_cli-0.4.0 → ring_cli-0.6.0}/README.en.md +89 -7
  3. {ring_cli-0.4.0 → ring_cli-0.6.0}/pyproject.toml +18 -4
  4. {ring_cli-0.4.0 → ring_cli-0.6.0}/src/ring/cli.py +98 -3
  5. ring_cli-0.6.0/src/ring/commands/completion.py +167 -0
  6. ring_cli-0.6.0/src/ring/commands/stats.py +74 -0
  7. {ring_cli-0.4.0 → ring_cli-0.6.0}/src/ring/config.py +17 -1
  8. {ring_cli-0.4.0 → ring_cli-0.6.0}/src/ring/focus/__init__.py +11 -3
  9. ring_cli-0.6.0/src/ring/focus/linux_wm.py +117 -0
  10. {ring_cli-0.4.0 → ring_cli-0.6.0}/src/ring/hook.py +20 -3
  11. {ring_cli-0.4.0 → ring_cli-0.6.0}/src/ring/hook_protocol.py +44 -0
  12. ring_cli-0.6.0/src/ring/locale/en/LC_MESSAGES/ring.mo +0 -0
  13. {ring_cli-0.4.0 → ring_cli-0.6.0}/src/ring/locale/en/LC_MESSAGES/ring.po +151 -5
  14. {ring_cli-0.4.0 → ring_cli-0.6.0}/src/ring/locale/ring.pot +340 -184
  15. {ring_cli-0.4.0 → ring_cli-0.6.0}/src/ring/notify/__init__.py +31 -4
  16. {ring_cli-0.4.0 → ring_cli-0.6.0}/src/ring/notify/base.py +16 -3
  17. {ring_cli-0.4.0 → ring_cli-0.6.0}/src/ring/notify/notify_send.py +2 -3
  18. ring_cli-0.6.0/src/ring/notify/ntfy.py +66 -0
  19. {ring_cli-0.4.0 → ring_cli-0.6.0}/src/ring/notify/osascript_notifier.py +2 -3
  20. {ring_cli-0.4.0 → ring_cli-0.6.0}/src/ring/notify/terminal_notifier.py +2 -3
  21. ring_cli-0.6.0/src/ring/notify/webhook.py +63 -0
  22. ring_cli-0.6.0/src/ring/plugins.py +75 -0
  23. {ring_cli-0.4.0 → ring_cli-0.6.0}/src/ring/registry.py +10 -192
  24. ring_cli-0.6.0/src/ring/stats.py +152 -0
  25. ring_cli-0.6.0/src/ring/transcript.py +204 -0
  26. {ring_cli-0.4.0 → ring_cli-0.6.0}/src/ring/tui.py +31 -4
  27. ring_cli-0.4.0/src/ring/locale/en/LC_MESSAGES/ring.mo +0 -0
  28. {ring_cli-0.4.0 → ring_cli-0.6.0}/LICENSE +0 -0
  29. {ring_cli-0.4.0 → ring_cli-0.6.0}/src/ring/__init__.py +0 -0
  30. {ring_cli-0.4.0 → ring_cli-0.6.0}/src/ring/__main__.py +0 -0
  31. {ring_cli-0.4.0 → ring_cli-0.6.0}/src/ring/commands/__init__.py +0 -0
  32. {ring_cli-0.4.0 → ring_cli-0.6.0}/src/ring/commands/_args.py +0 -0
  33. {ring_cli-0.4.0 → ring_cli-0.6.0}/src/ring/commands/doctor.py +0 -0
  34. {ring_cli-0.4.0 → ring_cli-0.6.0}/src/ring/commands/focus.py +0 -0
  35. {ring_cli-0.4.0 → ring_cli-0.6.0}/src/ring/commands/gc.py +0 -0
  36. {ring_cli-0.4.0 → ring_cli-0.6.0}/src/ring/commands/hook.py +0 -0
  37. {ring_cli-0.4.0 → ring_cli-0.6.0}/src/ring/focus/applescript.py +0 -0
  38. {ring_cli-0.4.0 → ring_cli-0.6.0}/src/ring/focus/base.py +0 -0
  39. {ring_cli-0.4.0 → ring_cli-0.6.0}/src/ring/focus/iterm2.py +0 -0
  40. {ring_cli-0.4.0 → ring_cli-0.6.0}/src/ring/focus/terminal.py +0 -0
  41. {ring_cli-0.4.0 → ring_cli-0.6.0}/src/ring/focus/tmux.py +0 -0
  42. {ring_cli-0.4.0 → ring_cli-0.6.0}/src/ring/gc.py +0 -0
  43. {ring_cli-0.4.0 → ring_cli-0.6.0}/src/ring/i18n.py +0 -0
  44. {ring_cli-0.4.0 → ring_cli-0.6.0}/src/ring/ipc.py +0 -0
  45. {ring_cli-0.4.0 → ring_cli-0.6.0}/src/ring/labels.py +0 -0
  46. {ring_cli-0.4.0 → ring_cli-0.6.0}/src/ring/notify/command.py +0 -0
  47. {ring_cli-0.4.0 → ring_cli-0.6.0}/src/ring/osascript.py +0 -0
  48. {ring_cli-0.4.0 → ring_cli-0.6.0}/src/ring/sources/__init__.py +0 -0
  49. {ring_cli-0.4.0 → ring_cli-0.6.0}/src/ring/sources/base.py +0 -0
  50. {ring_cli-0.4.0 → ring_cli-0.6.0}/src/ring/sources/claude_code.py +0 -0
  51. {ring_cli-0.4.0 → ring_cli-0.6.0}/src/ring/sources/codex.py +0 -0
  52. {ring_cli-0.4.0 → ring_cli-0.6.0}/src/ring/sources/hook_registry.py +0 -0
  53. {ring_cli-0.4.0 → ring_cli-0.6.0}/src/ring/watcher.py +0 -0
@@ -1,13 +1,13 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ring-cli
3
- Version: 0.4.0
3
+ Version: 0.6.0
4
4
  Summary: RiNG — Realtime Instance Notification Grid for active agent CLI sessions.
5
5
  Keywords: claude-code,codex,tui,dashboard,session,monitor,rich
6
6
  Author: Wei Lee
7
7
  Author-email: Wei Lee <weilee.rx@gmail.com>
8
8
  License-Expression: MIT
9
9
  License-File: LICENSE
10
- Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Development Status :: 4 - Beta
11
11
  Classifier: Environment :: Console
12
12
  Classifier: Intended Audience :: Developers
13
13
  Classifier: Operating System :: MacOS
@@ -57,9 +57,12 @@ RiNG puts them on one board, with sessions waiting for you sorted first.
57
57
 
58
58
  - **One board for every session**: Claude Code / Codex are built in; other tools can feed `ring hook`.
59
59
  - **Waiting first**: sessions that need your response are highlighted and sorted above the rest.
60
- - **Jump back to the terminal**: in the TUI, select a session and press `Enter` / `Space` to focus tmux, iTerm2, or Terminal.app.
60
+ - **Jump back to the terminal**: in the TUI, select a session and press `Enter` / `Space` to focus tmux, iTerm2, Terminal.app (macOS), or a Linux X11 window (`wmctrl`).
61
61
  - **Notifies you without the board open**: with hooks installed, the moment a session turns 🔴 waiting it beeps and fires a system notification — even with no RiNG board running. With `terminal-notifier`, clicking the notification jumps back.
62
- - **Name your sessions**: press `n` in the TUI to add a local label such as `maigo · auth refactor`.
62
+ - **Name your sessions**: press `n` in the TUI to name a session, e.g. "auth refactor"; once named, the board and notifications show the name instead of the workspace directory.
63
+ - **See what it is waiting for**: in hook mode, a 🔴 waiting session carries the concrete pending item (the command to run, the question asked) — shown in the TUI and in the notification body.
64
+ - **Fits your status bar**: `ring --format oneline` prints a `🔴2 🟢1 🟡3` one-liner for tmux / SwiftBar / waybar; `--format json` feeds scripts.
65
+ - **Rings your phone too**: built-in ntfy / webhook notifiers push 🔴 waiting to your phone when you are away from the desk.
63
66
  - **Local and extensible**: RiNG only reads local Claude Code / Codex data and writes `~/.config/ring/`; session sources, focusers, and notifiers are pluggable.
64
67
 
65
68
  ## Run
@@ -119,12 +122,39 @@ Hooks only apply to new sessions, so restart Claude Code / Codex sessions after
119
122
  | `ring doctor` | Read-only environment diagnosis |
120
123
  | `ring gc --dry-run` | Preview RiNG-owned stale state cleanup |
121
124
  | `ring gc` | Clean RiNG-owned stale state files |
125
+ | `ring --format json` | Machine-readable board snapshot (for jq / scripts) |
126
+ | `ring --format oneline` | `🔴2 🟢1 🟡3` one-liner (for status bars) |
127
+ | `ring stats` | Waiting stats: how long agents kept 🔴 waiting in the last 7 days |
128
+ | `ring completion zsh` | Print a shell completion script (zsh / bash) |
129
+
130
+ ### Status Bar Integration (`--format`)
131
+
132
+ ```sh
133
+ ring --format oneline # 🔴2 🟢1 🟡3 (empty output when no sessions, so the segment collapses)
134
+ ring --format json | jq '.counts.waiting'
135
+ ```
136
+
137
+ - **tmux**: `set -g status-right '#(ring --format oneline) …'` (with `status-interval 5`).
138
+ - **SwiftBar / xbar / waybar**: wrap `ring --format oneline` or consume the JSON.
139
+ - JSON keys are a stable interface (additive only), safe to script against.
140
+
141
+ ### Shell Completion (`ring completion`)
142
+
143
+ ```sh
144
+ # ~/.zshrc
145
+ eval "$(ring completion zsh)"
146
+ # ~/.bashrc
147
+ eval "$(ring completion bash)"
148
+ ```
149
+
150
+ Completes subcommands, flags, and `config set` keys.
122
151
 
123
152
  ## Watch Mode
124
153
 
125
154
  - With **Textual** (`[tui]` extra) in a real terminal: interactive TUI.
126
155
  Use `↑/↓` to select, `Enter` / `Space` to jump, `n` to name a session, `a` to toggle ended sessions, `r` to refresh, and `q` to quit.
127
156
  If you have vim muscle memory like I do, `j/k` move up/down and `g/G` jump to the first/last row.
157
+ When the selected row is 🔴 waiting, a line under the table shows **what it is concretely waiting for** (the command to run, the question asked; hook mode only).
128
158
  - Otherwise: Rich polling; without Rich, plain text.
129
159
 
130
160
  ### Jump To A Session
@@ -133,6 +163,7 @@ Select a session and press `Enter`. RiNG focuses the terminal where that session
133
163
 
134
164
  - **tmux**: switches directly to the pane via `switch-client`.
135
165
  - **iTerm2 / Terminal.app** on macOS: uses the session `tty` and AppleScript to focus the matching tab. The first run may ask for macOS Automation permission.
166
+ - **Linux X11 window** (`wmctrl`, best-effort fallback): for Linux without tmux — walks from the `tty` up to the terminal window that owns it and raises it via `wmctrl`. **Limits**: X11 only (usually a no-op on Wayland), raises the whole window but cannot pick the tab, and gnome-terminal's client/server model may not match. Requires `apt install wmctrl`.
136
167
 
137
168
  TTY matching is most accurate in hook mode. Without hooks, Codex falls back to zero-config matching:
138
169
  one live Codex session per cwd can jump correctly; multiple live Codex sessions in the same cwd are shown conservatively to avoid focusing the wrong tab.
@@ -154,6 +185,20 @@ Without it, RiNG falls back to macOS text notifications without click-to-focus.
154
185
  Notification sound, repeat timing, and backend selection are configurable; notification backends
155
186
  are also pluggable via the `Notifier` extension point.
156
187
 
188
+ #### Push To Your Phone (ntfy / webhook)
189
+
190
+ Desktop notifications do not help when you are away from the desk. Point RiNG at an
191
+ [ntfy](https://ntfy.sh) topic to push to your phone:
192
+
193
+ ```toml
194
+ # ~/.config/ring/config.toml
195
+ notify_ntfy_url = "https://ntfy.sh/my-ring-topic" # subscribe to the same topic in the ntfy app
196
+ notify_also = ["ntfy"] # desktop notification as usual, plus a copy to the phone
197
+ ```
198
+
199
+ `notify_backend = "ntfy"` pushes to the phone only. For Slack / your own bot / IFTTT, set
200
+ `notify_webhook_url` to use the generic webhook backend (JSON POST with a stable, additive-only payload).
201
+
157
202
  ### Cleaning RiNG State Files (`ring gc`)
158
203
 
159
204
  When RiNG receives `SessionEnd`, it removes its own hook registry entry. If an agent crashes or the
@@ -170,6 +215,19 @@ ring gc --all-ended # delete every registry file currently classified as en
170
215
  `ring gc` only cleans state files RiNG owns under `~/.config/ring/`. It does not touch Claude Code /
171
216
  Codex transcripts or state. `ring doctor` remains read-only and never deletes files.
172
217
 
218
+ ### Waiting Stats (`ring stats`)
219
+
220
+ In hook mode, RiNG logs session **state transitions** to `~/.config/ring/events.jsonl`
221
+ (transitions only, tiny, self-trimming past a size cap). `ring stats` then tells you, per project,
222
+ how many times an agent 🔴 waited on you and for how long (avg / max / total).
223
+
224
+ ```sh
225
+ ring stats # last 7 days
226
+ ring stats --since 12h # custom window
227
+ ```
228
+
229
+ Like precise notifications, 🔴 waiting is invisible to zero-config, so stats also needs hook mode.
230
+
173
231
  ## Session Sources
174
232
 
175
233
  RiNG collects sessions from registered sources. Built-ins:
@@ -310,7 +368,11 @@ notify_ignore_dnd = false
310
368
  notify_backend = "auto" # auto / terminal-notifier / osascript / notify-send / agent-hooks / none
311
369
  notify_repeat_seconds = [30, 120, 300]
312
370
  notify_repeat_max = 3
313
- focusers = ["tmux", "iTerm2", "Terminal"]
371
+ notify_ntfy_url = "" # full ntfy topic URL enables phone push (e.g. https://ntfy.sh/my-topic)
372
+ notify_webhook_url = "" # URL enables the generic webhook backend (JSON POST)
373
+ notify_also = [] # extra backends fired besides the primary, e.g. ["ntfy"]
374
+ focusers = ["tmux", "iTerm2", "Terminal", "linux-wm"]
375
+ plugins = [] # external plugin modules imported at startup (see Extending)
314
376
 
315
377
  [colors]
316
378
  waiting = "bold red"
@@ -329,16 +391,36 @@ RiNG is not tied to a specific tool or terminal.
329
391
  | Extension Point | Purpose | Built-ins |
330
392
  |-----------------|---------|-----------|
331
393
  | `SessionSource` | find sessions | Claude Code, Codex, hook registry |
332
- | `Focuser` | jump to terminals | tmux, iTerm2, Terminal.app |
333
- | `Notifier` | notify when sessions are waiting | terminal-notifier, osascript, notify-send |
394
+ | `Focuser` | jump to terminals | tmux, iTerm2, Terminal.app, Linux X11 (wmctrl) |
395
+ | `Notifier` | notify when sessions are waiting | terminal-notifier, osascript, notify-send, ntfy, webhook |
334
396
 
335
397
  Each backend is a small module under `ring/sources/`, `ring/focus/`, or `ring/notify/`, registered via `register_*()`.
336
398
 
399
+ ### Loading Your Plugin Into An Installed `ring`
400
+
401
+ `register_*()` only counts if something runs it. The installed `ring` command loads plugins from
402
+ two places at startup:
403
+
404
+ 1. **Entry point** (for published packages) — declare it in your package's `pyproject.toml`,
405
+ pointing at a module or callable (a module registers on import; a callable is invoked once
406
+ with no arguments):
407
+
408
+ ```toml
409
+ [project.entry-points."ring.plugins"]
410
+ mytool = "ring_mytool.plugin"
411
+ ```
412
+
413
+ 2. **Config** (for local scripts) — add `plugins = ["my_module"]` to `~/.config/ring/config.toml`;
414
+ the module must be importable (site-packages or `PYTHONPATH`).
415
+
416
+ A broken plugin prints one warning line to stderr and never blocks the board.
417
+
337
418
  ## Platform & Privacy
338
419
 
339
420
  - **Platform**: macOS / Linux. Windows is not supported.
340
- - **Privacy**: entirely local. RiNG only reads local `~/.claude/` and `~/.codex/` data and writes
341
- `~/.config/ring/`. No network, uploads, or telemetry.
421
+ - **Privacy**: local by default. RiNG only reads local `~/.claude/` and `~/.codex/` data and writes
422
+ `~/.config/ring/`. No telemetry. The only network calls are the optional ntfy / webhook notifiers,
423
+ and only to URLs you configure yourself.
342
424
 
343
425
  ## Name
344
426
 
@@ -32,9 +32,12 @@ RiNG puts them on one board, with sessions waiting for you sorted first.
32
32
 
33
33
  - **One board for every session**: Claude Code / Codex are built in; other tools can feed `ring hook`.
34
34
  - **Waiting first**: sessions that need your response are highlighted and sorted above the rest.
35
- - **Jump back to the terminal**: in the TUI, select a session and press `Enter` / `Space` to focus tmux, iTerm2, or Terminal.app.
35
+ - **Jump back to the terminal**: in the TUI, select a session and press `Enter` / `Space` to focus tmux, iTerm2, Terminal.app (macOS), or a Linux X11 window (`wmctrl`).
36
36
  - **Notifies you without the board open**: with hooks installed, the moment a session turns 🔴 waiting it beeps and fires a system notification — even with no RiNG board running. With `terminal-notifier`, clicking the notification jumps back.
37
- - **Name your sessions**: press `n` in the TUI to add a local label such as `maigo · auth refactor`.
37
+ - **Name your sessions**: press `n` in the TUI to name a session, e.g. "auth refactor"; once named, the board and notifications show the name instead of the workspace directory.
38
+ - **See what it is waiting for**: in hook mode, a 🔴 waiting session carries the concrete pending item (the command to run, the question asked) — shown in the TUI and in the notification body.
39
+ - **Fits your status bar**: `ring --format oneline` prints a `🔴2 🟢1 🟡3` one-liner for tmux / SwiftBar / waybar; `--format json` feeds scripts.
40
+ - **Rings your phone too**: built-in ntfy / webhook notifiers push 🔴 waiting to your phone when you are away from the desk.
38
41
  - **Local and extensible**: RiNG only reads local Claude Code / Codex data and writes `~/.config/ring/`; session sources, focusers, and notifiers are pluggable.
39
42
 
40
43
  ## Run
@@ -94,12 +97,39 @@ Hooks only apply to new sessions, so restart Claude Code / Codex sessions after
94
97
  | `ring doctor` | Read-only environment diagnosis |
95
98
  | `ring gc --dry-run` | Preview RiNG-owned stale state cleanup |
96
99
  | `ring gc` | Clean RiNG-owned stale state files |
100
+ | `ring --format json` | Machine-readable board snapshot (for jq / scripts) |
101
+ | `ring --format oneline` | `🔴2 🟢1 🟡3` one-liner (for status bars) |
102
+ | `ring stats` | Waiting stats: how long agents kept 🔴 waiting in the last 7 days |
103
+ | `ring completion zsh` | Print a shell completion script (zsh / bash) |
104
+
105
+ ### Status Bar Integration (`--format`)
106
+
107
+ ```sh
108
+ ring --format oneline # 🔴2 🟢1 🟡3 (empty output when no sessions, so the segment collapses)
109
+ ring --format json | jq '.counts.waiting'
110
+ ```
111
+
112
+ - **tmux**: `set -g status-right '#(ring --format oneline) …'` (with `status-interval 5`).
113
+ - **SwiftBar / xbar / waybar**: wrap `ring --format oneline` or consume the JSON.
114
+ - JSON keys are a stable interface (additive only), safe to script against.
115
+
116
+ ### Shell Completion (`ring completion`)
117
+
118
+ ```sh
119
+ # ~/.zshrc
120
+ eval "$(ring completion zsh)"
121
+ # ~/.bashrc
122
+ eval "$(ring completion bash)"
123
+ ```
124
+
125
+ Completes subcommands, flags, and `config set` keys.
97
126
 
98
127
  ## Watch Mode
99
128
 
100
129
  - With **Textual** (`[tui]` extra) in a real terminal: interactive TUI.
101
130
  Use `↑/↓` to select, `Enter` / `Space` to jump, `n` to name a session, `a` to toggle ended sessions, `r` to refresh, and `q` to quit.
102
131
  If you have vim muscle memory like I do, `j/k` move up/down and `g/G` jump to the first/last row.
132
+ When the selected row is 🔴 waiting, a line under the table shows **what it is concretely waiting for** (the command to run, the question asked; hook mode only).
103
133
  - Otherwise: Rich polling; without Rich, plain text.
104
134
 
105
135
  ### Jump To A Session
@@ -108,6 +138,7 @@ Select a session and press `Enter`. RiNG focuses the terminal where that session
108
138
 
109
139
  - **tmux**: switches directly to the pane via `switch-client`.
110
140
  - **iTerm2 / Terminal.app** on macOS: uses the session `tty` and AppleScript to focus the matching tab. The first run may ask for macOS Automation permission.
141
+ - **Linux X11 window** (`wmctrl`, best-effort fallback): for Linux without tmux — walks from the `tty` up to the terminal window that owns it and raises it via `wmctrl`. **Limits**: X11 only (usually a no-op on Wayland), raises the whole window but cannot pick the tab, and gnome-terminal's client/server model may not match. Requires `apt install wmctrl`.
111
142
 
112
143
  TTY matching is most accurate in hook mode. Without hooks, Codex falls back to zero-config matching:
113
144
  one live Codex session per cwd can jump correctly; multiple live Codex sessions in the same cwd are shown conservatively to avoid focusing the wrong tab.
@@ -129,6 +160,20 @@ Without it, RiNG falls back to macOS text notifications without click-to-focus.
129
160
  Notification sound, repeat timing, and backend selection are configurable; notification backends
130
161
  are also pluggable via the `Notifier` extension point.
131
162
 
163
+ #### Push To Your Phone (ntfy / webhook)
164
+
165
+ Desktop notifications do not help when you are away from the desk. Point RiNG at an
166
+ [ntfy](https://ntfy.sh) topic to push to your phone:
167
+
168
+ ```toml
169
+ # ~/.config/ring/config.toml
170
+ notify_ntfy_url = "https://ntfy.sh/my-ring-topic" # subscribe to the same topic in the ntfy app
171
+ notify_also = ["ntfy"] # desktop notification as usual, plus a copy to the phone
172
+ ```
173
+
174
+ `notify_backend = "ntfy"` pushes to the phone only. For Slack / your own bot / IFTTT, set
175
+ `notify_webhook_url` to use the generic webhook backend (JSON POST with a stable, additive-only payload).
176
+
132
177
  ### Cleaning RiNG State Files (`ring gc`)
133
178
 
134
179
  When RiNG receives `SessionEnd`, it removes its own hook registry entry. If an agent crashes or the
@@ -145,6 +190,19 @@ ring gc --all-ended # delete every registry file currently classified as en
145
190
  `ring gc` only cleans state files RiNG owns under `~/.config/ring/`. It does not touch Claude Code /
146
191
  Codex transcripts or state. `ring doctor` remains read-only and never deletes files.
147
192
 
193
+ ### Waiting Stats (`ring stats`)
194
+
195
+ In hook mode, RiNG logs session **state transitions** to `~/.config/ring/events.jsonl`
196
+ (transitions only, tiny, self-trimming past a size cap). `ring stats` then tells you, per project,
197
+ how many times an agent 🔴 waited on you and for how long (avg / max / total).
198
+
199
+ ```sh
200
+ ring stats # last 7 days
201
+ ring stats --since 12h # custom window
202
+ ```
203
+
204
+ Like precise notifications, 🔴 waiting is invisible to zero-config, so stats also needs hook mode.
205
+
148
206
  ## Session Sources
149
207
 
150
208
  RiNG collects sessions from registered sources. Built-ins:
@@ -285,7 +343,11 @@ notify_ignore_dnd = false
285
343
  notify_backend = "auto" # auto / terminal-notifier / osascript / notify-send / agent-hooks / none
286
344
  notify_repeat_seconds = [30, 120, 300]
287
345
  notify_repeat_max = 3
288
- focusers = ["tmux", "iTerm2", "Terminal"]
346
+ notify_ntfy_url = "" # full ntfy topic URL enables phone push (e.g. https://ntfy.sh/my-topic)
347
+ notify_webhook_url = "" # URL enables the generic webhook backend (JSON POST)
348
+ notify_also = [] # extra backends fired besides the primary, e.g. ["ntfy"]
349
+ focusers = ["tmux", "iTerm2", "Terminal", "linux-wm"]
350
+ plugins = [] # external plugin modules imported at startup (see Extending)
289
351
 
290
352
  [colors]
291
353
  waiting = "bold red"
@@ -304,16 +366,36 @@ RiNG is not tied to a specific tool or terminal.
304
366
  | Extension Point | Purpose | Built-ins |
305
367
  |-----------------|---------|-----------|
306
368
  | `SessionSource` | find sessions | Claude Code, Codex, hook registry |
307
- | `Focuser` | jump to terminals | tmux, iTerm2, Terminal.app |
308
- | `Notifier` | notify when sessions are waiting | terminal-notifier, osascript, notify-send |
369
+ | `Focuser` | jump to terminals | tmux, iTerm2, Terminal.app, Linux X11 (wmctrl) |
370
+ | `Notifier` | notify when sessions are waiting | terminal-notifier, osascript, notify-send, ntfy, webhook |
309
371
 
310
372
  Each backend is a small module under `ring/sources/`, `ring/focus/`, or `ring/notify/`, registered via `register_*()`.
311
373
 
374
+ ### Loading Your Plugin Into An Installed `ring`
375
+
376
+ `register_*()` only counts if something runs it. The installed `ring` command loads plugins from
377
+ two places at startup:
378
+
379
+ 1. **Entry point** (for published packages) — declare it in your package's `pyproject.toml`,
380
+ pointing at a module or callable (a module registers on import; a callable is invoked once
381
+ with no arguments):
382
+
383
+ ```toml
384
+ [project.entry-points."ring.plugins"]
385
+ mytool = "ring_mytool.plugin"
386
+ ```
387
+
388
+ 2. **Config** (for local scripts) — add `plugins = ["my_module"]` to `~/.config/ring/config.toml`;
389
+ the module must be importable (site-packages or `PYTHONPATH`).
390
+
391
+ A broken plugin prints one warning line to stderr and never blocks the board.
392
+
312
393
  ## Platform & Privacy
313
394
 
314
395
  - **Platform**: macOS / Linux. Windows is not supported.
315
- - **Privacy**: entirely local. RiNG only reads local `~/.claude/` and `~/.codex/` data and writes
316
- `~/.config/ring/`. No network, uploads, or telemetry.
396
+ - **Privacy**: local by default. RiNG only reads local `~/.claude/` and `~/.codex/` data and writes
397
+ `~/.config/ring/`. No telemetry. The only network calls are the optional ntfy / webhook notifiers,
398
+ and only to URLs you configure yourself.
317
399
 
318
400
  ## Name
319
401
 
@@ -2,16 +2,24 @@
2
2
  # import 的 module(見 [tool.uv.build-backend] module-name)與 CLI 指令仍是 `ring`。
3
3
  [project]
4
4
  name = "ring-cli"
5
- version = "0.4.0"
5
+ version = "0.6.0"
6
6
  description = "RiNG — Realtime Instance Notification Grid for active agent CLI sessions."
7
7
  readme = "README.en.md"
8
8
  requires-python = ">=3.13"
9
9
  license = "MIT"
10
10
  license-files = ["LICENSE"]
11
11
  authors = [{ name = "Wei Lee", email = "weilee.rx@gmail.com" }]
12
- keywords = ["claude-code", "codex", "tui", "dashboard", "session", "monitor", "rich"]
12
+ keywords = [
13
+ "claude-code",
14
+ "codex",
15
+ "tui",
16
+ "dashboard",
17
+ "session",
18
+ "monitor",
19
+ "rich",
20
+ ]
13
21
  classifiers = [
14
- "Development Status :: 3 - Alpha",
22
+ "Development Status :: 4 - Beta",
15
23
  "Environment :: Console",
16
24
  "Intended Audience :: Developers",
17
25
  "Operating System :: MacOS",
@@ -40,7 +48,13 @@ dev = [
40
48
  { include-group = "linters" },
41
49
  ]
42
50
  base = ["poethepoet>=0.41.0", "babel>=2.14"]
43
- test = ["pytest>=9", "pytest-cov>=4", "pytest-xdist>=3.1.0", "pytest-asyncio>=0.24", "textual>=0.80"]
51
+ test = [
52
+ "pytest>=9",
53
+ "pytest-cov>=4",
54
+ "pytest-xdist>=3.1.0",
55
+ "pytest-asyncio>=0.24",
56
+ "textual>=0.80",
57
+ ]
44
58
  linters = ["ruff>=0.15.1", "mypy>=1.19.1", "prek>=0.3.3", "commitizen>=4.13.9"]
45
59
 
46
60
  [build-system]
@@ -10,6 +10,7 @@
10
10
  from __future__ import annotations
11
11
 
12
12
  import argparse
13
+ import json
13
14
  import sys
14
15
  import time
15
16
  from dataclasses import fields
@@ -18,10 +19,12 @@ from typing import Any
18
19
 
19
20
  from ring import __version__
20
21
  from ring.commands._args import strip_lang as _strip_lang
22
+ from ring.commands.completion import run_completion
21
23
  from ring.commands.doctor import run_doctor
22
24
  from ring.commands.focus import run_focus
23
25
  from ring.commands.gc import run_gc
24
26
  from ring.commands.hook import run_hook_command, run_install_hooks, run_remove_hooks
27
+ from ring.commands.stats import run_stats
25
28
  from ring.config import CONFIG_PATH as CONFIG_PATH
26
29
  from ring.config import Config as Config
27
30
  from ring.config import ConfigError as ConfigError
@@ -30,6 +33,7 @@ from ring.config import set_value as set_value
30
33
  from ring.i18n import gettext as _
31
34
  from ring.i18n import ngettext, set_lang
32
35
  from ring.labels import load_labels
36
+ from ring.plugins import load_plugins
33
37
  from ring.registry import Session, Status, running_agent_pids
34
38
  from ring.sources import discover_sessions
35
39
 
@@ -134,8 +138,12 @@ def provider_label(provider: str) -> str:
134
138
 
135
139
 
136
140
  def labeled_project(project: str, label: str) -> str:
137
- """專案名後接使用者自訂標籤(有的話):``maigo · 重構登入``。"""
138
- return f"{project} · {label}" if label else project
141
+ """看板顯示名:使用者取過名(TUI ``n``)就用名字,否則用專案(目錄)名。
142
+
143
+ 名字本來就是「這個 session 在做什麼」的更精準描述,取了就直接取代 workspace 名,
144
+ 不再併排(``maigo · 重構登入`` 太佔欄寬)。
145
+ """
146
+ return label or project
139
147
 
140
148
 
141
149
  def show_tool_column(sessions: list[Session]) -> bool:
@@ -234,6 +242,55 @@ def _render_plain(sessions: list[Session], show_legend: bool, show_tool: bool =
234
242
  return "\n".join(lines)
235
243
 
236
244
 
245
+ # ----------------------------------------------------------------------------- machine-readable
246
+ def render_json(sessions: list[Session]) -> str:
247
+ """整個看板的機器可讀快照(給 jq / 腳本 / 自訂 status bar widget 用)。
248
+
249
+ 鍵名視為穩定介面:只加不改。``label`` 是使用者在 TUI 按 ``n`` 取的名字,沒取過為空字串。
250
+ """
251
+ labels = load_labels()
252
+ data = {
253
+ "generated_at": time.time(),
254
+ "agent_processes": len(running_agent_pids()),
255
+ "counts": {st.value: sum(1 for s in sessions if s.status is st) for st in Status},
256
+ "sessions": [
257
+ {
258
+ "session_id": s.session_id,
259
+ "provider": s.provider,
260
+ "project": s.project,
261
+ "label": labels.get(s.session_id, ""),
262
+ "status": s.status.value,
263
+ "marker": s.status.marker,
264
+ "cwd": s.cwd,
265
+ "location": s.location,
266
+ "tmux_target": s.tmux_target,
267
+ "tty": s.tty,
268
+ "idle_seconds": round(s.idle_for, 1),
269
+ "last_active": s.last_active,
270
+ "last_action": s.last_action,
271
+ "waiting_detail": s.waiting_detail,
272
+ "todo": {"done": s.todo[0], "total": s.todo[1]} if s.todo else None,
273
+ "source": s.source,
274
+ }
275
+ for s in sessions
276
+ ],
277
+ }
278
+ return json.dumps(data, ensure_ascii=False, indent=2)
279
+
280
+
281
+ def render_oneline(sessions: list[Session]) -> str:
282
+ """超精簡單行摘要(給 tmux status bar / SwiftBar / waybar 用):``🔴2 🟢1 🟡3``。
283
+
284
+ 只列非零狀態;完全沒 session 時回空字串,讓 status bar 段落自然收起來。
285
+ """
286
+ parts = [
287
+ f"{st.marker}{n}"
288
+ for st in (Status.WAITING, Status.WORKING, Status.IDLE, Status.ENDED)
289
+ if (n := sum(1 for s in sessions if s.status is st))
290
+ ]
291
+ return " ".join(parts)
292
+
293
+
237
294
  # ----------------------------------------------------------------------------- entry
238
295
  def print_snapshot(sessions: list[Session], show_legend: bool) -> None:
239
296
  show_tool = show_tool_column(sessions)
@@ -389,6 +446,8 @@ commands:
389
446
  focus SESSION_ID 聚焦指定 session;TUI 在跑時會回到 RiNG 並選中該列
390
447
  gc [--dry-run] 清理 RiNG 自己的 stale 狀態檔
391
448
  doctor 顯示環境診斷(唯讀)——hook、通知、focuser、維護提示
449
+ stats [--since 7d] 等待統計:你讓 agent 🔴 等了多久(hook 模式)
450
+ completion SHELL 印出 shell 補全腳本(zsh / bash)
392
451
  """
393
452
  )
394
453
 
@@ -443,6 +502,22 @@ options:
443
502
 
444
503
  唯讀環境診斷:逐節報告 hook 安裝狀態、通知後端可用性、focuser 可用性、維護提示與設定檔位置。
445
504
  不寫任何檔案、不安裝、不發通知;固定回傳 0。
505
+ """
506
+ ),
507
+ "stats": _(
508
+ """usage: ring stats [--since DURATION]
509
+
510
+ 等待統計:最近一段時間各專案讓你回應的次數與 🔴 等待時長(平均 / 最長 / 總計)。
511
+ 資料來自 hook 寫的狀態轉換 log(~/.config/ring/events.jsonl),zero-config 測不到等待。
512
+
513
+ options:
514
+ --since DURATION 統計時間窗(例如 12h、7d、30d;預設 7d)
515
+ """
516
+ ),
517
+ "completion": _(
518
+ """usage: ring completion zsh|bash
519
+
520
+ 印出 shell 補全腳本。zsh 放 ~/.zshrc:eval "$(ring completion zsh)";bash 同理。
446
521
  """
447
522
  ),
448
523
  }
@@ -453,10 +528,12 @@ def main(argv: list[str] | None = None) -> int:
453
528
  raw = list(sys.argv[1:] if argv is None else argv)
454
529
  cfg = get_config()
455
530
  set_lang(_peek_lang(raw) or cfg.lang) # 在 import ring.tui 前設好,Footer 按鍵說明也跟著語言
531
+ load_plugins() # 在任何 dispatch 之前:hook(notifier plugin)與看板(source plugin)都吃得到
456
532
 
457
533
  if (
458
534
  raw
459
- and raw[0] in {"hook", "install-hooks", "remove-hooks", "config", "focus", "gc", "doctor"}
535
+ and raw[0]
536
+ in {"hook", "install-hooks", "remove-hooks", "config", "focus", "gc", "doctor", "stats", "completion"}
460
537
  and any(arg in {"-h", "--help"} for arg in raw[1:])
461
538
  ):
462
539
  print(_subcommand_help(raw[0]), end="")
@@ -474,6 +551,10 @@ def main(argv: list[str] | None = None) -> int:
474
551
  return run_gc(raw[1:])
475
552
  if raw and raw[0] == "doctor":
476
553
  return run_doctor(raw[1:])
554
+ if raw and raw[0] == "stats":
555
+ return run_stats(raw[1:])
556
+ if raw and raw[0] == "completion":
557
+ return run_completion(raw[1:])
477
558
  if raw and raw[0] == "focus" and len(raw) >= 2:
478
559
  return run_focus(raw[1:])
479
560
 
@@ -495,8 +576,22 @@ def main(argv: list[str] | None = None) -> int:
495
576
  help=_("顯示顏色圖例(--no-legend 關閉)"),
496
577
  )
497
578
  parser.add_argument("--lang", help=_("語言(如 en / zh-Hant;也吃 config / RING_LANG / LANG)"))
579
+ parser.add_argument(
580
+ "--format",
581
+ choices=["table", "json", "oneline"],
582
+ default="table",
583
+ help=_("輸出格式:table(預設)/ json(機器可讀)/ oneline(status bar 單行摘要)"),
584
+ )
498
585
  args = parser.parse_args(raw)
499
586
 
587
+ if args.format != "table":
588
+ if args.watch:
589
+ print(_("--format {fmt} 只能用在快照模式,不能配 --watch。", fmt=args.format), file=sys.stderr)
590
+ return 2
591
+ sessions = board(args.all)
592
+ print(render_json(sessions) if args.format == "json" else render_oneline(sessions))
593
+ return 0
594
+
500
595
  if args.watch:
501
596
  if HAVE_TEXTUAL and sys.stdout.isatty():
502
597
  from ring.tui import run_tui