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.
- {ring_cli-0.4.0 → ring_cli-0.6.0}/PKG-INFO +91 -9
- {ring_cli-0.4.0 → ring_cli-0.6.0}/README.en.md +89 -7
- {ring_cli-0.4.0 → ring_cli-0.6.0}/pyproject.toml +18 -4
- {ring_cli-0.4.0 → ring_cli-0.6.0}/src/ring/cli.py +98 -3
- ring_cli-0.6.0/src/ring/commands/completion.py +167 -0
- ring_cli-0.6.0/src/ring/commands/stats.py +74 -0
- {ring_cli-0.4.0 → ring_cli-0.6.0}/src/ring/config.py +17 -1
- {ring_cli-0.4.0 → ring_cli-0.6.0}/src/ring/focus/__init__.py +11 -3
- ring_cli-0.6.0/src/ring/focus/linux_wm.py +117 -0
- {ring_cli-0.4.0 → ring_cli-0.6.0}/src/ring/hook.py +20 -3
- {ring_cli-0.4.0 → ring_cli-0.6.0}/src/ring/hook_protocol.py +44 -0
- ring_cli-0.6.0/src/ring/locale/en/LC_MESSAGES/ring.mo +0 -0
- {ring_cli-0.4.0 → ring_cli-0.6.0}/src/ring/locale/en/LC_MESSAGES/ring.po +151 -5
- {ring_cli-0.4.0 → ring_cli-0.6.0}/src/ring/locale/ring.pot +340 -184
- {ring_cli-0.4.0 → ring_cli-0.6.0}/src/ring/notify/__init__.py +31 -4
- {ring_cli-0.4.0 → ring_cli-0.6.0}/src/ring/notify/base.py +16 -3
- {ring_cli-0.4.0 → ring_cli-0.6.0}/src/ring/notify/notify_send.py +2 -3
- ring_cli-0.6.0/src/ring/notify/ntfy.py +66 -0
- {ring_cli-0.4.0 → ring_cli-0.6.0}/src/ring/notify/osascript_notifier.py +2 -3
- {ring_cli-0.4.0 → ring_cli-0.6.0}/src/ring/notify/terminal_notifier.py +2 -3
- ring_cli-0.6.0/src/ring/notify/webhook.py +63 -0
- ring_cli-0.6.0/src/ring/plugins.py +75 -0
- {ring_cli-0.4.0 → ring_cli-0.6.0}/src/ring/registry.py +10 -192
- ring_cli-0.6.0/src/ring/stats.py +152 -0
- ring_cli-0.6.0/src/ring/transcript.py +204 -0
- {ring_cli-0.4.0 → ring_cli-0.6.0}/src/ring/tui.py +31 -4
- ring_cli-0.4.0/src/ring/locale/en/LC_MESSAGES/ring.mo +0 -0
- {ring_cli-0.4.0 → ring_cli-0.6.0}/LICENSE +0 -0
- {ring_cli-0.4.0 → ring_cli-0.6.0}/src/ring/__init__.py +0 -0
- {ring_cli-0.4.0 → ring_cli-0.6.0}/src/ring/__main__.py +0 -0
- {ring_cli-0.4.0 → ring_cli-0.6.0}/src/ring/commands/__init__.py +0 -0
- {ring_cli-0.4.0 → ring_cli-0.6.0}/src/ring/commands/_args.py +0 -0
- {ring_cli-0.4.0 → ring_cli-0.6.0}/src/ring/commands/doctor.py +0 -0
- {ring_cli-0.4.0 → ring_cli-0.6.0}/src/ring/commands/focus.py +0 -0
- {ring_cli-0.4.0 → ring_cli-0.6.0}/src/ring/commands/gc.py +0 -0
- {ring_cli-0.4.0 → ring_cli-0.6.0}/src/ring/commands/hook.py +0 -0
- {ring_cli-0.4.0 → ring_cli-0.6.0}/src/ring/focus/applescript.py +0 -0
- {ring_cli-0.4.0 → ring_cli-0.6.0}/src/ring/focus/base.py +0 -0
- {ring_cli-0.4.0 → ring_cli-0.6.0}/src/ring/focus/iterm2.py +0 -0
- {ring_cli-0.4.0 → ring_cli-0.6.0}/src/ring/focus/terminal.py +0 -0
- {ring_cli-0.4.0 → ring_cli-0.6.0}/src/ring/focus/tmux.py +0 -0
- {ring_cli-0.4.0 → ring_cli-0.6.0}/src/ring/gc.py +0 -0
- {ring_cli-0.4.0 → ring_cli-0.6.0}/src/ring/i18n.py +0 -0
- {ring_cli-0.4.0 → ring_cli-0.6.0}/src/ring/ipc.py +0 -0
- {ring_cli-0.4.0 → ring_cli-0.6.0}/src/ring/labels.py +0 -0
- {ring_cli-0.4.0 → ring_cli-0.6.0}/src/ring/notify/command.py +0 -0
- {ring_cli-0.4.0 → ring_cli-0.6.0}/src/ring/osascript.py +0 -0
- {ring_cli-0.4.0 → ring_cli-0.6.0}/src/ring/sources/__init__.py +0 -0
- {ring_cli-0.4.0 → ring_cli-0.6.0}/src/ring/sources/base.py +0 -0
- {ring_cli-0.4.0 → ring_cli-0.6.0}/src/ring/sources/claude_code.py +0 -0
- {ring_cli-0.4.0 → ring_cli-0.6.0}/src/ring/sources/codex.py +0 -0
- {ring_cli-0.4.0 → ring_cli-0.6.0}/src/ring/sources/hook_registry.py +0 -0
- {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.
|
|
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 ::
|
|
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,
|
|
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
|
|
@@ -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
|
-
|
|
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**:
|
|
341
|
-
`~/.config/ring/`. No network
|
|
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,
|
|
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
|
|
@@ -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
|
-
|
|
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**:
|
|
316
|
-
`~/.config/ring/`. No network
|
|
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.
|
|
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 = [
|
|
12
|
+
keywords = [
|
|
13
|
+
"claude-code",
|
|
14
|
+
"codex",
|
|
15
|
+
"tui",
|
|
16
|
+
"dashboard",
|
|
17
|
+
"session",
|
|
18
|
+
"monitor",
|
|
19
|
+
"rich",
|
|
20
|
+
]
|
|
13
21
|
classifiers = [
|
|
14
|
-
"Development Status ::
|
|
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 = [
|
|
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
|