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.
- {ring_cli-0.5.0 → ring_cli-0.7.0}/PKG-INFO +88 -6
- {ring_cli-0.5.0 → ring_cli-0.7.0}/README.en.md +87 -5
- {ring_cli-0.5.0 → ring_cli-0.7.0}/pyproject.toml +17 -3
- {ring_cli-0.5.0 → ring_cli-0.7.0}/src/ring/cli.py +98 -3
- ring_cli-0.7.0/src/ring/commands/completion.py +167 -0
- {ring_cli-0.5.0 → ring_cli-0.7.0}/src/ring/commands/doctor.py +4 -1
- ring_cli-0.7.0/src/ring/commands/stats.py +74 -0
- {ring_cli-0.5.0 → ring_cli-0.7.0}/src/ring/config.py +17 -1
- {ring_cli-0.5.0 → ring_cli-0.7.0}/src/ring/focus/__init__.py +16 -4
- {ring_cli-0.5.0 → ring_cli-0.7.0}/src/ring/focus/base.py +4 -1
- ring_cli-0.7.0/src/ring/focus/neovim.py +184 -0
- {ring_cli-0.5.0 → ring_cli-0.7.0}/src/ring/hook.py +19 -0
- {ring_cli-0.5.0 → ring_cli-0.7.0}/src/ring/hook_protocol.py +44 -0
- ring_cli-0.7.0/src/ring/locale/en/LC_MESSAGES/ring.mo +0 -0
- {ring_cli-0.5.0 → ring_cli-0.7.0}/src/ring/locale/en/LC_MESSAGES/ring.po +154 -5
- {ring_cli-0.5.0 → ring_cli-0.7.0}/src/ring/locale/ring.pot +344 -184
- {ring_cli-0.5.0 → ring_cli-0.7.0}/src/ring/notify/__init__.py +31 -4
- {ring_cli-0.5.0 → ring_cli-0.7.0}/src/ring/notify/base.py +16 -3
- {ring_cli-0.5.0 → ring_cli-0.7.0}/src/ring/notify/notify_send.py +2 -3
- ring_cli-0.7.0/src/ring/notify/ntfy.py +66 -0
- {ring_cli-0.5.0 → ring_cli-0.7.0}/src/ring/notify/osascript_notifier.py +2 -3
- {ring_cli-0.5.0 → ring_cli-0.7.0}/src/ring/notify/terminal_notifier.py +2 -3
- ring_cli-0.7.0/src/ring/notify/webhook.py +63 -0
- ring_cli-0.7.0/src/ring/plugins.py +75 -0
- {ring_cli-0.5.0 → ring_cli-0.7.0}/src/ring/registry.py +2 -0
- ring_cli-0.7.0/src/ring/stats.py +152 -0
- {ring_cli-0.5.0 → ring_cli-0.7.0}/src/ring/tui.py +31 -4
- ring_cli-0.5.0/src/ring/locale/en/LC_MESSAGES/ring.mo +0 -0
- {ring_cli-0.5.0 → ring_cli-0.7.0}/LICENSE +0 -0
- {ring_cli-0.5.0 → ring_cli-0.7.0}/src/ring/__init__.py +0 -0
- {ring_cli-0.5.0 → ring_cli-0.7.0}/src/ring/__main__.py +0 -0
- {ring_cli-0.5.0 → ring_cli-0.7.0}/src/ring/commands/__init__.py +0 -0
- {ring_cli-0.5.0 → ring_cli-0.7.0}/src/ring/commands/_args.py +0 -0
- {ring_cli-0.5.0 → ring_cli-0.7.0}/src/ring/commands/focus.py +0 -0
- {ring_cli-0.5.0 → ring_cli-0.7.0}/src/ring/commands/gc.py +0 -0
- {ring_cli-0.5.0 → ring_cli-0.7.0}/src/ring/commands/hook.py +0 -0
- {ring_cli-0.5.0 → ring_cli-0.7.0}/src/ring/focus/applescript.py +0 -0
- {ring_cli-0.5.0 → ring_cli-0.7.0}/src/ring/focus/iterm2.py +0 -0
- {ring_cli-0.5.0 → ring_cli-0.7.0}/src/ring/focus/linux_wm.py +0 -0
- {ring_cli-0.5.0 → ring_cli-0.7.0}/src/ring/focus/terminal.py +0 -0
- {ring_cli-0.5.0 → ring_cli-0.7.0}/src/ring/focus/tmux.py +0 -0
- {ring_cli-0.5.0 → ring_cli-0.7.0}/src/ring/gc.py +0 -0
- {ring_cli-0.5.0 → ring_cli-0.7.0}/src/ring/i18n.py +0 -0
- {ring_cli-0.5.0 → ring_cli-0.7.0}/src/ring/ipc.py +0 -0
- {ring_cli-0.5.0 → ring_cli-0.7.0}/src/ring/labels.py +0 -0
- {ring_cli-0.5.0 → ring_cli-0.7.0}/src/ring/notify/command.py +0 -0
- {ring_cli-0.5.0 → ring_cli-0.7.0}/src/ring/osascript.py +0 -0
- {ring_cli-0.5.0 → ring_cli-0.7.0}/src/ring/sources/__init__.py +0 -0
- {ring_cli-0.5.0 → ring_cli-0.7.0}/src/ring/sources/base.py +0 -0
- {ring_cli-0.5.0 → ring_cli-0.7.0}/src/ring/sources/claude_code.py +0 -0
- {ring_cli-0.5.0 → ring_cli-0.7.0}/src/ring/sources/codex.py +0 -0
- {ring_cli-0.5.0 → ring_cli-0.7.0}/src/ring/sources/hook_registry.py +0 -0
- {ring_cli-0.5.0 → ring_cli-0.7.0}/src/ring/transcript.py +0 -0
- {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.
|
|
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
|
|
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
|
-
|
|
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**:
|
|
342
|
-
`~/.config/ring/`. No network
|
|
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
|
|
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
|
-
|
|
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**:
|
|
317
|
-
`~/.config/ring/`. No network
|
|
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
|
+
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 = [
|
|
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 = [
|
|
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
|
-
"""
|
|
138
|
-
|
|
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]
|
|
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
|