ring-cli 0.5.0__tar.gz → 0.7.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 (54) hide show
  1. {ring_cli-0.5.0 → ring_cli-0.7.0}/PKG-INFO +88 -6
  2. {ring_cli-0.5.0 → ring_cli-0.7.0}/README.en.md +87 -5
  3. {ring_cli-0.5.0 → ring_cli-0.7.0}/pyproject.toml +17 -3
  4. {ring_cli-0.5.0 → ring_cli-0.7.0}/src/ring/cli.py +98 -3
  5. ring_cli-0.7.0/src/ring/commands/completion.py +167 -0
  6. {ring_cli-0.5.0 → ring_cli-0.7.0}/src/ring/commands/doctor.py +4 -1
  7. ring_cli-0.7.0/src/ring/commands/stats.py +74 -0
  8. {ring_cli-0.5.0 → ring_cli-0.7.0}/src/ring/config.py +17 -1
  9. {ring_cli-0.5.0 → ring_cli-0.7.0}/src/ring/focus/__init__.py +16 -4
  10. {ring_cli-0.5.0 → ring_cli-0.7.0}/src/ring/focus/base.py +4 -1
  11. ring_cli-0.7.0/src/ring/focus/neovim.py +184 -0
  12. {ring_cli-0.5.0 → ring_cli-0.7.0}/src/ring/hook.py +19 -0
  13. {ring_cli-0.5.0 → ring_cli-0.7.0}/src/ring/hook_protocol.py +44 -0
  14. ring_cli-0.7.0/src/ring/locale/en/LC_MESSAGES/ring.mo +0 -0
  15. {ring_cli-0.5.0 → ring_cli-0.7.0}/src/ring/locale/en/LC_MESSAGES/ring.po +154 -5
  16. {ring_cli-0.5.0 → ring_cli-0.7.0}/src/ring/locale/ring.pot +344 -184
  17. {ring_cli-0.5.0 → ring_cli-0.7.0}/src/ring/notify/__init__.py +31 -4
  18. {ring_cli-0.5.0 → ring_cli-0.7.0}/src/ring/notify/base.py +16 -3
  19. {ring_cli-0.5.0 → ring_cli-0.7.0}/src/ring/notify/notify_send.py +2 -3
  20. ring_cli-0.7.0/src/ring/notify/ntfy.py +66 -0
  21. {ring_cli-0.5.0 → ring_cli-0.7.0}/src/ring/notify/osascript_notifier.py +2 -3
  22. {ring_cli-0.5.0 → ring_cli-0.7.0}/src/ring/notify/terminal_notifier.py +2 -3
  23. ring_cli-0.7.0/src/ring/notify/webhook.py +63 -0
  24. ring_cli-0.7.0/src/ring/plugins.py +75 -0
  25. {ring_cli-0.5.0 → ring_cli-0.7.0}/src/ring/registry.py +2 -0
  26. ring_cli-0.7.0/src/ring/stats.py +152 -0
  27. {ring_cli-0.5.0 → ring_cli-0.7.0}/src/ring/tui.py +31 -4
  28. ring_cli-0.5.0/src/ring/locale/en/LC_MESSAGES/ring.mo +0 -0
  29. {ring_cli-0.5.0 → ring_cli-0.7.0}/LICENSE +0 -0
  30. {ring_cli-0.5.0 → ring_cli-0.7.0}/src/ring/__init__.py +0 -0
  31. {ring_cli-0.5.0 → ring_cli-0.7.0}/src/ring/__main__.py +0 -0
  32. {ring_cli-0.5.0 → ring_cli-0.7.0}/src/ring/commands/__init__.py +0 -0
  33. {ring_cli-0.5.0 → ring_cli-0.7.0}/src/ring/commands/_args.py +0 -0
  34. {ring_cli-0.5.0 → ring_cli-0.7.0}/src/ring/commands/focus.py +0 -0
  35. {ring_cli-0.5.0 → ring_cli-0.7.0}/src/ring/commands/gc.py +0 -0
  36. {ring_cli-0.5.0 → ring_cli-0.7.0}/src/ring/commands/hook.py +0 -0
  37. {ring_cli-0.5.0 → ring_cli-0.7.0}/src/ring/focus/applescript.py +0 -0
  38. {ring_cli-0.5.0 → ring_cli-0.7.0}/src/ring/focus/iterm2.py +0 -0
  39. {ring_cli-0.5.0 → ring_cli-0.7.0}/src/ring/focus/linux_wm.py +0 -0
  40. {ring_cli-0.5.0 → ring_cli-0.7.0}/src/ring/focus/terminal.py +0 -0
  41. {ring_cli-0.5.0 → ring_cli-0.7.0}/src/ring/focus/tmux.py +0 -0
  42. {ring_cli-0.5.0 → ring_cli-0.7.0}/src/ring/gc.py +0 -0
  43. {ring_cli-0.5.0 → ring_cli-0.7.0}/src/ring/i18n.py +0 -0
  44. {ring_cli-0.5.0 → ring_cli-0.7.0}/src/ring/ipc.py +0 -0
  45. {ring_cli-0.5.0 → ring_cli-0.7.0}/src/ring/labels.py +0 -0
  46. {ring_cli-0.5.0 → ring_cli-0.7.0}/src/ring/notify/command.py +0 -0
  47. {ring_cli-0.5.0 → ring_cli-0.7.0}/src/ring/osascript.py +0 -0
  48. {ring_cli-0.5.0 → ring_cli-0.7.0}/src/ring/sources/__init__.py +0 -0
  49. {ring_cli-0.5.0 → ring_cli-0.7.0}/src/ring/sources/base.py +0 -0
  50. {ring_cli-0.5.0 → ring_cli-0.7.0}/src/ring/sources/claude_code.py +0 -0
  51. {ring_cli-0.5.0 → ring_cli-0.7.0}/src/ring/sources/codex.py +0 -0
  52. {ring_cli-0.5.0 → ring_cli-0.7.0}/src/ring/sources/hook_registry.py +0 -0
  53. {ring_cli-0.5.0 → ring_cli-0.7.0}/src/ring/transcript.py +0 -0
  54. {ring_cli-0.5.0 → ring_cli-0.7.0}/src/ring/watcher.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ring-cli
3
- Version: 0.5.0
3
+ Version: 0.7.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
@@ -59,7 +59,10 @@ RiNG puts them on one board, with sessions waiting for you sorted first.
59
59
  - **Waiting first**: sessions that need your response are highlighted and sorted above the rest.
60
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
@@ -132,6 +162,7 @@ Hooks only apply to new sessions, so restart Claude Code / Codex sessions after
132
162
  Select a session and press `Enter`. RiNG focuses the terminal where that session is running.
133
163
 
134
164
  - **tmux**: switches directly to the pane via `switch-client`.
165
+ - **Neovim `:terminal`**: uses the terminal job's inherited `$NVIM` server socket to switch to the exact buffer, then lets the outer tmux or terminal focuser raise its pane/window.
135
166
  - **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.
136
167
  - **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`.
137
168
 
@@ -155,6 +186,20 @@ Without it, RiNG falls back to macOS text notifications without click-to-focus.
155
186
  Notification sound, repeat timing, and backend selection are configurable; notification backends
156
187
  are also pluggable via the `Notifier` extension point.
157
188
 
189
+ #### Push To Your Phone (ntfy / webhook)
190
+
191
+ Desktop notifications do not help when you are away from the desk. Point RiNG at an
192
+ [ntfy](https://ntfy.sh) topic to push to your phone:
193
+
194
+ ```toml
195
+ # ~/.config/ring/config.toml
196
+ notify_ntfy_url = "https://ntfy.sh/my-ring-topic" # subscribe to the same topic in the ntfy app
197
+ notify_also = ["ntfy"] # desktop notification as usual, plus a copy to the phone
198
+ ```
199
+
200
+ `notify_backend = "ntfy"` pushes to the phone only. For Slack / your own bot / IFTTT, set
201
+ `notify_webhook_url` to use the generic webhook backend (JSON POST with a stable, additive-only payload).
202
+
158
203
  ### Cleaning RiNG State Files (`ring gc`)
159
204
 
160
205
  When RiNG receives `SessionEnd`, it removes its own hook registry entry. If an agent crashes or the
@@ -171,6 +216,19 @@ ring gc --all-ended # delete every registry file currently classified as en
171
216
  `ring gc` only cleans state files RiNG owns under `~/.config/ring/`. It does not touch Claude Code /
172
217
  Codex transcripts or state. `ring doctor` remains read-only and never deletes files.
173
218
 
219
+ ### Waiting Stats (`ring stats`)
220
+
221
+ In hook mode, RiNG logs session **state transitions** to `~/.config/ring/events.jsonl`
222
+ (transitions only, tiny, self-trimming past a size cap). `ring stats` then tells you, per project,
223
+ how many times an agent 🔴 waited on you and for how long (avg / max / total).
224
+
225
+ ```sh
226
+ ring stats # last 7 days
227
+ ring stats --since 12h # custom window
228
+ ```
229
+
230
+ Like precise notifications, 🔴 waiting is invisible to zero-config, so stats also needs hook mode.
231
+
174
232
  ## Session Sources
175
233
 
176
234
  RiNG collects sessions from registered sources. Built-ins:
@@ -311,7 +369,11 @@ notify_ignore_dnd = false
311
369
  notify_backend = "auto" # auto / terminal-notifier / osascript / notify-send / agent-hooks / none
312
370
  notify_repeat_seconds = [30, 120, 300]
313
371
  notify_repeat_max = 3
314
- focusers = ["tmux", "iTerm2", "Terminal", "linux-wm"]
372
+ notify_ntfy_url = "" # full ntfy topic URL enables phone push (e.g. https://ntfy.sh/my-topic)
373
+ notify_webhook_url = "" # URL enables the generic webhook backend (JSON POST)
374
+ notify_also = [] # extra backends fired besides the primary, e.g. ["ntfy"]
375
+ focusers = ["Neovim", "tmux", "iTerm2", "Terminal", "linux-wm"]
376
+ plugins = [] # external plugin modules imported at startup (see Extending)
315
377
 
316
378
  [colors]
317
379
  waiting = "bold red"
@@ -331,15 +393,35 @@ RiNG is not tied to a specific tool or terminal.
331
393
  |-----------------|---------|-----------|
332
394
  | `SessionSource` | find sessions | Claude Code, Codex, hook registry |
333
395
  | `Focuser` | jump to terminals | tmux, iTerm2, Terminal.app, Linux X11 (wmctrl) |
334
- | `Notifier` | notify when sessions are waiting | terminal-notifier, osascript, notify-send |
396
+ | `Notifier` | notify when sessions are waiting | terminal-notifier, osascript, notify-send, ntfy, webhook |
335
397
 
336
398
  Each backend is a small module under `ring/sources/`, `ring/focus/`, or `ring/notify/`, registered via `register_*()`.
337
399
 
400
+ ### Loading Your Plugin Into An Installed `ring`
401
+
402
+ `register_*()` only counts if something runs it. The installed `ring` command loads plugins from
403
+ two places at startup:
404
+
405
+ 1. **Entry point** (for published packages) — declare it in your package's `pyproject.toml`,
406
+ pointing at a module or callable (a module registers on import; a callable is invoked once
407
+ with no arguments):
408
+
409
+ ```toml
410
+ [project.entry-points."ring.plugins"]
411
+ mytool = "ring_mytool.plugin"
412
+ ```
413
+
414
+ 2. **Config** (for local scripts) — add `plugins = ["my_module"]` to `~/.config/ring/config.toml`;
415
+ the module must be importable (site-packages or `PYTHONPATH`).
416
+
417
+ A broken plugin prints one warning line to stderr and never blocks the board.
418
+
338
419
  ## Platform & Privacy
339
420
 
340
421
  - **Platform**: macOS / Linux. Windows is not supported.
341
- - **Privacy**: entirely local. RiNG only reads local `~/.claude/` and `~/.codex/` data and writes
342
- `~/.config/ring/`. No network, uploads, or telemetry.
422
+ - **Privacy**: local by default. RiNG only reads local `~/.claude/` and `~/.codex/` data and writes
423
+ `~/.config/ring/`. No telemetry. The only network calls are the optional ntfy / webhook notifiers,
424
+ and only to URLs you configure yourself.
343
425
 
344
426
  ## Name
345
427
 
@@ -34,7 +34,10 @@ RiNG puts them on one board, with sessions waiting for you sorted first.
34
34
  - **Waiting first**: sessions that need your response are highlighted and sorted above the rest.
35
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
@@ -107,6 +137,7 @@ Hooks only apply to new sessions, so restart Claude Code / Codex sessions after
107
137
  Select a session and press `Enter`. RiNG focuses the terminal where that session is running.
108
138
 
109
139
  - **tmux**: switches directly to the pane via `switch-client`.
140
+ - **Neovim `:terminal`**: uses the terminal job's inherited `$NVIM` server socket to switch to the exact buffer, then lets the outer tmux or terminal focuser raise its pane/window.
110
141
  - **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.
111
142
  - **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`.
112
143
 
@@ -130,6 +161,20 @@ Without it, RiNG falls back to macOS text notifications without click-to-focus.
130
161
  Notification sound, repeat timing, and backend selection are configurable; notification backends
131
162
  are also pluggable via the `Notifier` extension point.
132
163
 
164
+ #### Push To Your Phone (ntfy / webhook)
165
+
166
+ Desktop notifications do not help when you are away from the desk. Point RiNG at an
167
+ [ntfy](https://ntfy.sh) topic to push to your phone:
168
+
169
+ ```toml
170
+ # ~/.config/ring/config.toml
171
+ notify_ntfy_url = "https://ntfy.sh/my-ring-topic" # subscribe to the same topic in the ntfy app
172
+ notify_also = ["ntfy"] # desktop notification as usual, plus a copy to the phone
173
+ ```
174
+
175
+ `notify_backend = "ntfy"` pushes to the phone only. For Slack / your own bot / IFTTT, set
176
+ `notify_webhook_url` to use the generic webhook backend (JSON POST with a stable, additive-only payload).
177
+
133
178
  ### Cleaning RiNG State Files (`ring gc`)
134
179
 
135
180
  When RiNG receives `SessionEnd`, it removes its own hook registry entry. If an agent crashes or the
@@ -146,6 +191,19 @@ ring gc --all-ended # delete every registry file currently classified as en
146
191
  `ring gc` only cleans state files RiNG owns under `~/.config/ring/`. It does not touch Claude Code /
147
192
  Codex transcripts or state. `ring doctor` remains read-only and never deletes files.
148
193
 
194
+ ### Waiting Stats (`ring stats`)
195
+
196
+ In hook mode, RiNG logs session **state transitions** to `~/.config/ring/events.jsonl`
197
+ (transitions only, tiny, self-trimming past a size cap). `ring stats` then tells you, per project,
198
+ how many times an agent 🔴 waited on you and for how long (avg / max / total).
199
+
200
+ ```sh
201
+ ring stats # last 7 days
202
+ ring stats --since 12h # custom window
203
+ ```
204
+
205
+ Like precise notifications, 🔴 waiting is invisible to zero-config, so stats also needs hook mode.
206
+
149
207
  ## Session Sources
150
208
 
151
209
  RiNG collects sessions from registered sources. Built-ins:
@@ -286,7 +344,11 @@ notify_ignore_dnd = false
286
344
  notify_backend = "auto" # auto / terminal-notifier / osascript / notify-send / agent-hooks / none
287
345
  notify_repeat_seconds = [30, 120, 300]
288
346
  notify_repeat_max = 3
289
- focusers = ["tmux", "iTerm2", "Terminal", "linux-wm"]
347
+ notify_ntfy_url = "" # full ntfy topic URL enables phone push (e.g. https://ntfy.sh/my-topic)
348
+ notify_webhook_url = "" # URL enables the generic webhook backend (JSON POST)
349
+ notify_also = [] # extra backends fired besides the primary, e.g. ["ntfy"]
350
+ focusers = ["Neovim", "tmux", "iTerm2", "Terminal", "linux-wm"]
351
+ plugins = [] # external plugin modules imported at startup (see Extending)
290
352
 
291
353
  [colors]
292
354
  waiting = "bold red"
@@ -306,15 +368,35 @@ RiNG is not tied to a specific tool or terminal.
306
368
  |-----------------|---------|-----------|
307
369
  | `SessionSource` | find sessions | Claude Code, Codex, hook registry |
308
370
  | `Focuser` | jump to terminals | tmux, iTerm2, Terminal.app, Linux X11 (wmctrl) |
309
- | `Notifier` | notify when sessions are waiting | terminal-notifier, osascript, notify-send |
371
+ | `Notifier` | notify when sessions are waiting | terminal-notifier, osascript, notify-send, ntfy, webhook |
310
372
 
311
373
  Each backend is a small module under `ring/sources/`, `ring/focus/`, or `ring/notify/`, registered via `register_*()`.
312
374
 
375
+ ### Loading Your Plugin Into An Installed `ring`
376
+
377
+ `register_*()` only counts if something runs it. The installed `ring` command loads plugins from
378
+ two places at startup:
379
+
380
+ 1. **Entry point** (for published packages) — declare it in your package's `pyproject.toml`,
381
+ pointing at a module or callable (a module registers on import; a callable is invoked once
382
+ with no arguments):
383
+
384
+ ```toml
385
+ [project.entry-points."ring.plugins"]
386
+ mytool = "ring_mytool.plugin"
387
+ ```
388
+
389
+ 2. **Config** (for local scripts) — add `plugins = ["my_module"]` to `~/.config/ring/config.toml`;
390
+ the module must be importable (site-packages or `PYTHONPATH`).
391
+
392
+ A broken plugin prints one warning line to stderr and never blocks the board.
393
+
313
394
  ## Platform & Privacy
314
395
 
315
396
  - **Platform**: macOS / Linux. Windows is not supported.
316
- - **Privacy**: entirely local. RiNG only reads local `~/.claude/` and `~/.codex/` data and writes
317
- `~/.config/ring/`. No network, uploads, or telemetry.
397
+ - **Privacy**: local by default. RiNG only reads local `~/.claude/` and `~/.codex/` data and writes
398
+ `~/.config/ring/`. No telemetry. The only network calls are the optional ntfy / webhook notifiers,
399
+ and only to URLs you configure yourself.
318
400
 
319
401
  ## Name
320
402
 
@@ -2,14 +2,22 @@
2
2
  # import 的 module(見 [tool.uv.build-backend] module-name)與 CLI 指令仍是 `ring`。
3
3
  [project]
4
4
  name = "ring-cli"
5
- version = "0.5.0"
5
+ version = "0.7.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
22
  "Development Status :: 4 - Beta",
15
23
  "Environment :: Console",
@@ -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