ring-cli 0.11.3__tar.gz → 0.12.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.11.3/README.en.md → ring_cli-0.12.0/PKG-INFO +36 -2
- ring_cli-0.11.3/PKG-INFO → ring_cli-0.12.0/README.en.md +11 -27
- {ring_cli-0.11.3 → ring_cli-0.12.0}/pyproject.toml +3 -1
- {ring_cli-0.11.3 → ring_cli-0.12.0}/src/ring/config.py +7 -0
- {ring_cli-0.11.3 → ring_cli-0.12.0}/src/ring/hook.py +7 -2
- {ring_cli-0.11.3 → ring_cli-0.12.0}/src/ring/hook_protocol.py +7 -0
- ring_cli-0.12.0/src/ring/payload_log.py +110 -0
- {ring_cli-0.11.3 → ring_cli-0.12.0}/src/ring/registry.py +4 -1
- {ring_cli-0.11.3 → ring_cli-0.12.0}/src/ring/sources/__init__.py +3 -1
- ring_cli-0.12.0/src/ring/sources/local_llm.py +157 -0
- {ring_cli-0.11.3 → ring_cli-0.12.0}/LICENSE +0 -0
- {ring_cli-0.11.3 → ring_cli-0.12.0}/src/ring/__init__.py +0 -0
- {ring_cli-0.11.3 → ring_cli-0.12.0}/src/ring/__main__.py +0 -0
- {ring_cli-0.11.3 → ring_cli-0.12.0}/src/ring/cli.py +0 -0
- {ring_cli-0.11.3 → ring_cli-0.12.0}/src/ring/commands/__init__.py +0 -0
- {ring_cli-0.11.3 → ring_cli-0.12.0}/src/ring/commands/_args.py +0 -0
- {ring_cli-0.11.3 → ring_cli-0.12.0}/src/ring/commands/completion.py +0 -0
- {ring_cli-0.11.3 → ring_cli-0.12.0}/src/ring/commands/digest.py +0 -0
- {ring_cli-0.11.3 → ring_cli-0.12.0}/src/ring/commands/doctor.py +0 -0
- {ring_cli-0.11.3 → ring_cli-0.12.0}/src/ring/commands/focus.py +0 -0
- {ring_cli-0.11.3 → ring_cli-0.12.0}/src/ring/commands/gc.py +0 -0
- {ring_cli-0.11.3 → ring_cli-0.12.0}/src/ring/commands/hook.py +0 -0
- {ring_cli-0.11.3 → ring_cli-0.12.0}/src/ring/commands/stats.py +0 -0
- {ring_cli-0.11.3 → ring_cli-0.12.0}/src/ring/focus/__init__.py +0 -0
- {ring_cli-0.11.3 → ring_cli-0.12.0}/src/ring/focus/applescript.py +0 -0
- {ring_cli-0.11.3 → ring_cli-0.12.0}/src/ring/focus/base.py +0 -0
- {ring_cli-0.11.3 → ring_cli-0.12.0}/src/ring/focus/iterm2.py +0 -0
- {ring_cli-0.11.3 → ring_cli-0.12.0}/src/ring/focus/linux_wm.py +0 -0
- {ring_cli-0.11.3 → ring_cli-0.12.0}/src/ring/focus/neovim.py +0 -0
- {ring_cli-0.11.3 → ring_cli-0.12.0}/src/ring/focus/terminal.py +0 -0
- {ring_cli-0.11.3 → ring_cli-0.12.0}/src/ring/focus/tmux.py +0 -0
- {ring_cli-0.11.3 → ring_cli-0.12.0}/src/ring/gc.py +0 -0
- {ring_cli-0.11.3 → ring_cli-0.12.0}/src/ring/i18n.py +0 -0
- {ring_cli-0.11.3 → ring_cli-0.12.0}/src/ring/ipc.py +0 -0
- {ring_cli-0.11.3 → ring_cli-0.12.0}/src/ring/labels.py +0 -0
- {ring_cli-0.11.3 → ring_cli-0.12.0}/src/ring/locale/en/LC_MESSAGES/ring.mo +0 -0
- {ring_cli-0.11.3 → ring_cli-0.12.0}/src/ring/locale/en/LC_MESSAGES/ring.po +0 -0
- {ring_cli-0.11.3 → ring_cli-0.12.0}/src/ring/locale/ring.pot +0 -0
- {ring_cli-0.11.3 → ring_cli-0.12.0}/src/ring/notify/__init__.py +0 -0
- {ring_cli-0.11.3 → ring_cli-0.12.0}/src/ring/notify/base.py +0 -0
- {ring_cli-0.11.3 → ring_cli-0.12.0}/src/ring/notify/command.py +0 -0
- {ring_cli-0.11.3 → ring_cli-0.12.0}/src/ring/notify/notify_send.py +0 -0
- {ring_cli-0.11.3 → ring_cli-0.12.0}/src/ring/notify/ntfy.py +0 -0
- {ring_cli-0.11.3 → ring_cli-0.12.0}/src/ring/notify/osascript_notifier.py +0 -0
- {ring_cli-0.11.3 → ring_cli-0.12.0}/src/ring/notify/terminal_notifier.py +0 -0
- {ring_cli-0.11.3 → ring_cli-0.12.0}/src/ring/notify/webhook.py +0 -0
- {ring_cli-0.11.3 → ring_cli-0.12.0}/src/ring/osascript.py +0 -0
- {ring_cli-0.11.3 → ring_cli-0.12.0}/src/ring/permission.py +0 -0
- {ring_cli-0.11.3 → ring_cli-0.12.0}/src/ring/plugins.py +0 -0
- {ring_cli-0.11.3 → ring_cli-0.12.0}/src/ring/sources/base.py +0 -0
- {ring_cli-0.11.3 → ring_cli-0.12.0}/src/ring/sources/claude_code.py +0 -0
- {ring_cli-0.11.3 → ring_cli-0.12.0}/src/ring/sources/codex.py +0 -0
- {ring_cli-0.11.3 → ring_cli-0.12.0}/src/ring/sources/hook_registry.py +0 -0
- {ring_cli-0.11.3 → ring_cli-0.12.0}/src/ring/stats.py +0 -0
- {ring_cli-0.11.3 → ring_cli-0.12.0}/src/ring/transcript.py +0 -0
- {ring_cli-0.11.3 → ring_cli-0.12.0}/src/ring/tui.py +0 -0
- {ring_cli-0.11.3 → ring_cli-0.12.0}/src/ring/watcher.py +0 -0
|
@@ -1,3 +1,28 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ring-cli
|
|
3
|
+
Version: 0.12.0
|
|
4
|
+
Summary: RiNG — Realtime Instance Notification Grid for active agent CLI sessions.
|
|
5
|
+
Keywords: claude-code,codex,ollama,llama-cpp,tui,dashboard,session,monitor,rich
|
|
6
|
+
Author: Wei Lee
|
|
7
|
+
Author-email: Wei Lee <weilee.rx@gmail.com>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Environment :: Console
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Operating System :: MacOS
|
|
14
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Topic :: Software Development
|
|
17
|
+
Classifier: Topic :: Utilities
|
|
18
|
+
Requires-Dist: rich>=13
|
|
19
|
+
Requires-Dist: textual>=0.80 ; extra == 'tui'
|
|
20
|
+
Requires-Python: >=3.13
|
|
21
|
+
Project-URL: Homepage, https://github.com/Lee-W/ring
|
|
22
|
+
Project-URL: Repository, https://github.com/Lee-W/ring
|
|
23
|
+
Provides-Extra: tui
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
|
|
1
26
|
# RiNG 🎤
|
|
2
27
|
|
|
3
28
|
[台灣漢語](https://github.com/Lee-W/ring/blob/main/README.md) · **English**
|
|
@@ -243,11 +268,18 @@ RiNG collects sessions from registered sources. Built-ins:
|
|
|
243
268
|
|--------|------------|-----------|
|
|
244
269
|
| **Claude Code zero-config** | `~/.claude/projects/**/*.jsonl`, mtimes, and `cwd` fields | no setup; detects recent activity and turn completion. Precise user-action prompts require hooks |
|
|
245
270
|
| **Codex zero-config** | `~/.codex/state_5.sqlite`, rollout JSONL, and live `codex` processes | no setup; detects live / ended / turn completion. Use hooks for precise jumps when multiple sessions share a cwd |
|
|
271
|
+
| **Ollama zero-config** | interactive `ollama run` processes with a controlling terminal | process-liveness only; shows cwd, TTY, and model, and excludes `ollama serve` |
|
|
272
|
+
| **llama.cpp zero-config** | interactive `llama-cli` processes with a controlling terminal | process-liveness only; shows cwd, TTY, and model, and excludes `llama-server` |
|
|
246
273
|
| **hook registry** | `~/.config/ring/sessions/`, written by `ring hook` | precise: 🔴 waiting / 🟢 working / 🟡 idle / ⚫ ended |
|
|
247
274
|
|
|
248
275
|
Zero-config needs no setup. For precise “who needs me”, install hooks so provider events feed the RiNG registry.
|
|
249
276
|
RiNG includes installers for Claude Code and Codex; other tools can use the provider-neutral `ring hook` protocol.
|
|
250
277
|
|
|
278
|
+
Ollama and llama.cpp do not expose a session transcript or interaction hooks that RiNG can read, so their
|
|
279
|
+
zero-config rows stay 🟡: the row means the interactive CLI is alive, not that RiNG can distinguish generation
|
|
280
|
+
from waiting for the next prompt. The row disappears when the CLI exits. An outer agent that can emit lifecycle
|
|
281
|
+
events can use the provider-neutral hook protocol for precise states.
|
|
282
|
+
|
|
251
283
|
## States
|
|
252
284
|
|
|
253
285
|
RiNG reduces every session to four user-facing states. 🔴 waiting is sorted first.
|
|
@@ -294,7 +326,9 @@ Claude Code events:
|
|
|
294
326
|
| `PermissionRequest` / `PreToolUse` with `AskUserQuestion` | 🔴 waiting |
|
|
295
327
|
| `SessionEnd` | removed from the board |
|
|
296
328
|
|
|
297
|
-
Codex currently installs the supported interactive events: `PreToolUse`, `PermissionRequest`, and `Stop`.
|
|
329
|
+
Codex currently installs the supported interactive events: `PreToolUse`, `PermissionRequest`, and `Stop`. Codex
|
|
330
|
+
also emits `PermissionRequest` before an existing policy auto-approves the call; a bare event therefore stays
|
|
331
|
+
🟢 working, and it only turns 🔴 waiting when the payload explicitly carries `requires_action` / `waiting_for`.
|
|
298
332
|
|
|
299
333
|
Verify that hooks are writing:
|
|
300
334
|
|
|
@@ -404,7 +438,7 @@ RiNG is not tied to a specific tool or terminal.
|
|
|
404
438
|
|
|
405
439
|
| Extension Point | Purpose | Built-ins |
|
|
406
440
|
|-----------------|---------|-----------|
|
|
407
|
-
| `SessionSource` | find sessions | Claude Code, Codex, hook registry |
|
|
441
|
+
| `SessionSource` | find sessions | Claude Code, Codex, Ollama, llama.cpp, hook registry |
|
|
408
442
|
| `Focuser` | jump to terminals | tmux, iTerm2, Terminal.app, Linux X11 (wmctrl) |
|
|
409
443
|
| `Notifier` | notify when sessions are waiting | terminal-notifier, osascript, notify-send, ntfy, webhook |
|
|
410
444
|
|
|
@@ -1,28 +1,3 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: ring-cli
|
|
3
|
-
Version: 0.11.3
|
|
4
|
-
Summary: RiNG — Realtime Instance Notification Grid for active agent CLI sessions.
|
|
5
|
-
Keywords: claude-code,codex,tui,dashboard,session,monitor,rich
|
|
6
|
-
Author: Wei Lee
|
|
7
|
-
Author-email: Wei Lee <weilee.rx@gmail.com>
|
|
8
|
-
License-Expression: MIT
|
|
9
|
-
License-File: LICENSE
|
|
10
|
-
Classifier: Development Status :: 4 - Beta
|
|
11
|
-
Classifier: Environment :: Console
|
|
12
|
-
Classifier: Intended Audience :: Developers
|
|
13
|
-
Classifier: Operating System :: MacOS
|
|
14
|
-
Classifier: Operating System :: POSIX :: Linux
|
|
15
|
-
Classifier: Programming Language :: Python :: 3
|
|
16
|
-
Classifier: Topic :: Software Development
|
|
17
|
-
Classifier: Topic :: Utilities
|
|
18
|
-
Requires-Dist: rich>=13
|
|
19
|
-
Requires-Dist: textual>=0.80 ; extra == 'tui'
|
|
20
|
-
Requires-Python: >=3.13
|
|
21
|
-
Project-URL: Homepage, https://github.com/Lee-W/ring
|
|
22
|
-
Project-URL: Repository, https://github.com/Lee-W/ring
|
|
23
|
-
Provides-Extra: tui
|
|
24
|
-
Description-Content-Type: text/markdown
|
|
25
|
-
|
|
26
1
|
# RiNG 🎤
|
|
27
2
|
|
|
28
3
|
[台灣漢語](https://github.com/Lee-W/ring/blob/main/README.md) · **English**
|
|
@@ -268,11 +243,18 @@ RiNG collects sessions from registered sources. Built-ins:
|
|
|
268
243
|
|--------|------------|-----------|
|
|
269
244
|
| **Claude Code zero-config** | `~/.claude/projects/**/*.jsonl`, mtimes, and `cwd` fields | no setup; detects recent activity and turn completion. Precise user-action prompts require hooks |
|
|
270
245
|
| **Codex zero-config** | `~/.codex/state_5.sqlite`, rollout JSONL, and live `codex` processes | no setup; detects live / ended / turn completion. Use hooks for precise jumps when multiple sessions share a cwd |
|
|
246
|
+
| **Ollama zero-config** | interactive `ollama run` processes with a controlling terminal | process-liveness only; shows cwd, TTY, and model, and excludes `ollama serve` |
|
|
247
|
+
| **llama.cpp zero-config** | interactive `llama-cli` processes with a controlling terminal | process-liveness only; shows cwd, TTY, and model, and excludes `llama-server` |
|
|
271
248
|
| **hook registry** | `~/.config/ring/sessions/`, written by `ring hook` | precise: 🔴 waiting / 🟢 working / 🟡 idle / ⚫ ended |
|
|
272
249
|
|
|
273
250
|
Zero-config needs no setup. For precise “who needs me”, install hooks so provider events feed the RiNG registry.
|
|
274
251
|
RiNG includes installers for Claude Code and Codex; other tools can use the provider-neutral `ring hook` protocol.
|
|
275
252
|
|
|
253
|
+
Ollama and llama.cpp do not expose a session transcript or interaction hooks that RiNG can read, so their
|
|
254
|
+
zero-config rows stay 🟡: the row means the interactive CLI is alive, not that RiNG can distinguish generation
|
|
255
|
+
from waiting for the next prompt. The row disappears when the CLI exits. An outer agent that can emit lifecycle
|
|
256
|
+
events can use the provider-neutral hook protocol for precise states.
|
|
257
|
+
|
|
276
258
|
## States
|
|
277
259
|
|
|
278
260
|
RiNG reduces every session to four user-facing states. 🔴 waiting is sorted first.
|
|
@@ -319,7 +301,9 @@ Claude Code events:
|
|
|
319
301
|
| `PermissionRequest` / `PreToolUse` with `AskUserQuestion` | 🔴 waiting |
|
|
320
302
|
| `SessionEnd` | removed from the board |
|
|
321
303
|
|
|
322
|
-
Codex currently installs the supported interactive events: `PreToolUse`, `PermissionRequest`, and `Stop`.
|
|
304
|
+
Codex currently installs the supported interactive events: `PreToolUse`, `PermissionRequest`, and `Stop`. Codex
|
|
305
|
+
also emits `PermissionRequest` before an existing policy auto-approves the call; a bare event therefore stays
|
|
306
|
+
🟢 working, and it only turns 🔴 waiting when the payload explicitly carries `requires_action` / `waiting_for`.
|
|
323
307
|
|
|
324
308
|
Verify that hooks are writing:
|
|
325
309
|
|
|
@@ -429,7 +413,7 @@ RiNG is not tied to a specific tool or terminal.
|
|
|
429
413
|
|
|
430
414
|
| Extension Point | Purpose | Built-ins |
|
|
431
415
|
|-----------------|---------|-----------|
|
|
432
|
-
| `SessionSource` | find sessions | Claude Code, Codex, hook registry |
|
|
416
|
+
| `SessionSource` | find sessions | Claude Code, Codex, Ollama, llama.cpp, hook registry |
|
|
433
417
|
| `Focuser` | jump to terminals | tmux, iTerm2, Terminal.app, Linux X11 (wmctrl) |
|
|
434
418
|
| `Notifier` | notify when sessions are waiting | terminal-notifier, osascript, notify-send, ntfy, webhook |
|
|
435
419
|
|
|
@@ -2,7 +2,7 @@
|
|
|
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.12.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"
|
|
@@ -12,6 +12,8 @@ authors = [{ name = "Wei Lee", email = "weilee.rx@gmail.com" }]
|
|
|
12
12
|
keywords = [
|
|
13
13
|
"claude-code",
|
|
14
14
|
"codex",
|
|
15
|
+
"ollama",
|
|
16
|
+
"llama-cpp",
|
|
15
17
|
"tui",
|
|
16
18
|
"dashboard",
|
|
17
19
|
"session",
|
|
@@ -27,6 +27,10 @@
|
|
|
27
27
|
notify_also = ["ntfy"] # 主後端之外「加發」的後端(例如桌面通知+手機各一份)
|
|
28
28
|
focusers = ["Neovim", "tmux", "iTerm2", "Terminal", "linux-wm"] # 跳轉嘗試順序;省略=內建預設
|
|
29
29
|
plugins = ["my_ring_plugin"] # 啟動時 import 的外部 plugin 模組(自行 register_*)
|
|
30
|
+
debug_payload_log = false # 診斷用:記錄 hook 收到的原始 payload
|
|
31
|
+
# (見 payload_log.py,寫到 ~/.config/ring/hook_payloads.jsonl);
|
|
32
|
+
# 預設關閉,payload 可能含使用者輸入才要開;
|
|
33
|
+
# 環境變數 RING_DEBUG_PAYLOAD_LOG 可覆寫(優先於本鍵)
|
|
30
34
|
"""
|
|
31
35
|
|
|
32
36
|
from __future__ import annotations
|
|
@@ -77,6 +81,7 @@ class Config:
|
|
|
77
81
|
notify_also: tuple[str, ...] = () # 主後端之外加發的後端名(如 ["ntfy"])
|
|
78
82
|
focusers: tuple[str, ...] = () # 空=用內建預設順序
|
|
79
83
|
plugins: tuple[str, ...] = () # 啟動時 import 的外部 plugin 模組(entry point 之外的本機路)
|
|
84
|
+
debug_payload_log: bool = False # 診斷用:記錄 hook 收到的原始 payload;預設關閉
|
|
80
85
|
colors: dict[str, str] = field(default_factory=lambda: dict(_DEFAULT_COLORS))
|
|
81
86
|
|
|
82
87
|
|
|
@@ -148,6 +153,7 @@ def load(path: Path | None = None) -> Config:
|
|
|
148
153
|
notify_also=_as_str_tuple(raw.get("notify_also")),
|
|
149
154
|
focusers=_as_str_tuple(raw.get("focusers")),
|
|
150
155
|
plugins=_as_str_tuple(raw.get("plugins")),
|
|
156
|
+
debug_payload_log=_as_bool(raw.get("debug_payload_log"), d.debug_payload_log),
|
|
151
157
|
colors=_parse_colors(raw.get("colors")),
|
|
152
158
|
)
|
|
153
159
|
|
|
@@ -218,6 +224,7 @@ _SETTERS: dict[str, Callable[[str], object]] = {
|
|
|
218
224
|
"notify_also": _coerce_str_list,
|
|
219
225
|
"focusers": _coerce_str_list,
|
|
220
226
|
"plugins": _coerce_str_list,
|
|
227
|
+
"debug_payload_log": _coerce_bool,
|
|
221
228
|
}
|
|
222
229
|
|
|
223
230
|
|
|
@@ -31,6 +31,7 @@ from ring.config import get_config
|
|
|
31
31
|
from ring.hook_protocol import HOOK_EVENTS, adapter_for, provider_from_payload
|
|
32
32
|
from ring.i18n import gettext as _
|
|
33
33
|
from ring.i18n import set_lang
|
|
34
|
+
from ring.payload_log import maybe_log_raw_payload
|
|
34
35
|
from ring.registry import (
|
|
35
36
|
RING_REGISTRY,
|
|
36
37
|
Session,
|
|
@@ -44,8 +45,9 @@ from ring.transcript import _extract_todo, _latest_action, _tail_records
|
|
|
44
45
|
_HOOK_EVENTS = list(HOOK_EVENTS)
|
|
45
46
|
|
|
46
47
|
# Codex 的 hooks.json 用跟 Claude 同樣的 PascalCase 事件名,但只支援其中一小撮。
|
|
47
|
-
# 保守取有實證可用的:PermissionRequest
|
|
48
|
-
# Stop(→ 🟡 回合結束、清掉 waiting)。多裝 Codex
|
|
48
|
+
# 保守取有實證可用的:PermissionRequest(裸事件只代表權限準備判定;明確需互動才 → 🔴)、
|
|
49
|
+
# PreToolUse(→ 動作/清除)、Stop(→ 🟡 回合結束、清掉 waiting)。多裝 Codex 不認的事件
|
|
50
|
+
# 有風險,故不照搬 Claude 全套。
|
|
49
51
|
_CODEX_HOOK_EVENTS = ["PreToolUse", "PermissionRequest", "Stop"]
|
|
50
52
|
|
|
51
53
|
# hook command 的 timeout(秒)。給足,因為 notify_backend="agent-hooks" 時權限 modal 會
|
|
@@ -156,6 +158,9 @@ def run_hook(provider: str = "claude-code") -> int:
|
|
|
156
158
|
return 0
|
|
157
159
|
|
|
158
160
|
selected_provider = provider_from_payload(data, fallback=provider)
|
|
161
|
+
# 取證 log:診斷用、預設關閉(見 payload_log.py)。故意放在任何狀態判定/改寫之前,
|
|
162
|
+
# 記下 hook 實際收到的原始資料——不影響、不參與下面的正規化流程。
|
|
163
|
+
maybe_log_raw_payload(selected_provider, data)
|
|
159
164
|
_record_session_state(data, selected_provider)
|
|
160
165
|
return _delegate_to_agent_hooks(raw, selected_provider)
|
|
161
166
|
|
|
@@ -105,6 +105,13 @@ class CommonHookAdapter:
|
|
|
105
105
|
status = Status.WORKING
|
|
106
106
|
elif explicit_requires_action is not None:
|
|
107
107
|
status = Status.WAITING if explicit_requires_action else Status.IDLE
|
|
108
|
+
elif event == "PermissionRequest" and self.provider == "codex":
|
|
109
|
+
# Codex 會在權限「準備判定」時送 PermissionRequest;即使既有 policy
|
|
110
|
+
# 隨後自動放行、畫面從未停下來等人,也會經過這個 hook。裸事件因此只能
|
|
111
|
+
# 證明 agent 還在處理工具呼叫,不能當成使用者需要回應。若 payload 有
|
|
112
|
+
# requires_action / waiting_for 等明確訊號,已由上面的分支判成 WAITING;
|
|
113
|
+
# 明確的 requires_action=false 也在上面判成 IDLE,不會落到這裡的 WORKING。
|
|
114
|
+
status = Status.WORKING
|
|
108
115
|
elif event == "Notification":
|
|
109
116
|
status = Status.WAITING if _is_action_required_notification(data) else Status.IDLE
|
|
110
117
|
elif event == "PreToolUse":
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""原始 hook payload 取證 logger——診斷用,預設關閉。
|
|
2
|
+
|
|
3
|
+
診斷「claude-code 裸 PermissionRequest 被誤判 WAITING」與「codex 裸 PermissionRequest
|
|
4
|
+
永遠判 WORKING」這類問題時,需要看到 hook 實際收到的原始 event payload——現有的
|
|
5
|
+
registry(``~/.config/ring/sessions/*.json``)與 ``events.jsonl`` 都只留正規化後的
|
|
6
|
+
status,原始資料在 ``hook_protocol.normalize`` 跑完就遺失了。
|
|
7
|
+
|
|
8
|
+
這支 logger 在 hook 收到 stdin 的當下、任何狀態判定/改寫之前,把原始 payload 整段
|
|
9
|
+
append 一行,供事後分析用。設計原則跟 ``stats.log_transition`` 一致:
|
|
10
|
+
|
|
11
|
+
- append-only、JSONL,一行一事件
|
|
12
|
+
- 檔案超過上限自動砍半保新,不無限成長
|
|
13
|
+
- 任何錯誤(含讀 config 失敗、payload 無法序列化)一律安靜吞掉,絕不影響 hook 主流程
|
|
14
|
+
|
|
15
|
+
開關:``debug_payload_log`` config 鍵,或環境變數 ``RING_DEBUG_PAYLOAD_LOG``
|
|
16
|
+
(``1``/``true``/``yes``/``on`` 開,``0``/``false``/``no``/``off`` 關;env 優先於 config)。
|
|
17
|
+
**預設關閉**——payload 可能含使用者輸入/檔案內容,開了才寫。
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import json
|
|
23
|
+
import os
|
|
24
|
+
import time
|
|
25
|
+
from collections.abc import Mapping
|
|
26
|
+
from pathlib import Path
|
|
27
|
+
from typing import Any
|
|
28
|
+
|
|
29
|
+
PAYLOAD_LOG_PATH = Path.home() / ".config" / "ring" / "hook_payloads.jsonl"
|
|
30
|
+
|
|
31
|
+
# log 檔上限;超過就砍半保新(append 前檢查)。原始 payload 比 events.jsonl 的轉換記錄
|
|
32
|
+
# 大很多(可能含完整 tool_input/transcript 片段),給足空間再砍半。
|
|
33
|
+
_MAX_BYTES = 20 * 1024 * 1024
|
|
34
|
+
|
|
35
|
+
_ENV_FLAG = "RING_DEBUG_PAYLOAD_LOG"
|
|
36
|
+
|
|
37
|
+
_TRUE = {"1", "true", "yes", "on"}
|
|
38
|
+
_FALSE = {"0", "false", "no", "off"}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def payload_log_enabled() -> bool:
|
|
42
|
+
"""是否要記原始 payload:env var 優先,其次 config 鍵,預設關閉。"""
|
|
43
|
+
env = os.environ.get(_ENV_FLAG, "").strip().lower()
|
|
44
|
+
if env in _TRUE:
|
|
45
|
+
return True
|
|
46
|
+
if env in _FALSE:
|
|
47
|
+
return False
|
|
48
|
+
try:
|
|
49
|
+
from ring.config import get_config
|
|
50
|
+
|
|
51
|
+
return get_config().debug_payload_log
|
|
52
|
+
except Exception:
|
|
53
|
+
return False
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _raw_event_name(data: Mapping[str, Any]) -> str:
|
|
57
|
+
"""從原始 payload 猜事件名(只為了記錄好讀,不影響任何判定邏輯)。"""
|
|
58
|
+
for key in ("event", "event_name", "hook_event_name", "hookEventName"):
|
|
59
|
+
v = data.get(key)
|
|
60
|
+
if isinstance(v, str) and v:
|
|
61
|
+
return v
|
|
62
|
+
return ""
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def maybe_log_raw_payload(
|
|
66
|
+
provider: str,
|
|
67
|
+
data: Mapping[str, Any],
|
|
68
|
+
*,
|
|
69
|
+
path: Path | None = None,
|
|
70
|
+
now: float | None = None,
|
|
71
|
+
) -> None:
|
|
72
|
+
"""開關開啟時,把 hook 收到的原始 payload 整段 append 一行(診斷用)。
|
|
73
|
+
|
|
74
|
+
呼叫端應放在 hook 處理最前端,早於 ``hook_protocol`` 的任何狀態判定/改寫。
|
|
75
|
+
開關預設關閉(見模組 docstring);任何錯誤一律安靜吞掉,呼叫端不需要自己包 try。
|
|
76
|
+
"""
|
|
77
|
+
try:
|
|
78
|
+
if not payload_log_enabled():
|
|
79
|
+
return
|
|
80
|
+
p = path or PAYLOAD_LOG_PATH
|
|
81
|
+
line = json.dumps(
|
|
82
|
+
{
|
|
83
|
+
"ts": now if now is not None else time.time(),
|
|
84
|
+
"provider": provider,
|
|
85
|
+
"event": _raw_event_name(data),
|
|
86
|
+
"payload": data,
|
|
87
|
+
},
|
|
88
|
+
ensure_ascii=False,
|
|
89
|
+
default=str,
|
|
90
|
+
)
|
|
91
|
+
p.parent.mkdir(parents=True, exist_ok=True)
|
|
92
|
+
_trim_if_oversized(p)
|
|
93
|
+
with p.open("a", encoding="utf-8") as f:
|
|
94
|
+
f.write(line + "\n")
|
|
95
|
+
except Exception:
|
|
96
|
+
pass
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _trim_if_oversized(p: Path) -> None:
|
|
100
|
+
"""log 超過 ``_MAX_BYTES`` 時砍半保新,避免無上限成長。失敗安靜放棄(不擋 append)。"""
|
|
101
|
+
try:
|
|
102
|
+
if p.stat().st_size <= _MAX_BYTES:
|
|
103
|
+
return
|
|
104
|
+
lines = p.read_text(encoding="utf-8").splitlines()
|
|
105
|
+
keep = lines[len(lines) // 2 :]
|
|
106
|
+
tmp = p.with_suffix(".jsonl.tmp")
|
|
107
|
+
tmp.write_text("\n".join(keep) + "\n", encoding="utf-8")
|
|
108
|
+
tmp.replace(p) # atomic
|
|
109
|
+
except Exception:
|
|
110
|
+
pass
|
|
@@ -872,7 +872,10 @@ def running_agent_pids() -> list[int]:
|
|
|
872
872
|
顯示用途,不是存活判定;真正的 ENDED 判定路徑(``_hook_sessions``)用的是
|
|
873
873
|
未攤平的 ``running_claude_pids`` / ``running_codex_pids`` 原始回傳值。
|
|
874
874
|
"""
|
|
875
|
-
|
|
875
|
+
# 延後 import,避免 registry(Session model)與 sources package 初始化時循環相依。
|
|
876
|
+
from ring.sources.local_llm import running_pids as running_local_llm_pids
|
|
877
|
+
|
|
878
|
+
return [*(running_claude_pids() or []), *(running_codex_pids() or []), *running_local_llm_pids()]
|
|
876
879
|
|
|
877
880
|
|
|
878
881
|
def _pids_cwd(pids: list[int]) -> dict[int, str] | None:
|
|
@@ -13,9 +13,11 @@ from ring.sources.base import SessionSource
|
|
|
13
13
|
from ring.sources.claude_code import source as _claude_code
|
|
14
14
|
from ring.sources.codex import source as _codex
|
|
15
15
|
from ring.sources.hook_registry import source as _hook_registry
|
|
16
|
+
from ring.sources.local_llm import llama_cpp_source as _llama_cpp
|
|
17
|
+
from ring.sources.local_llm import ollama_source as _ollama
|
|
16
18
|
|
|
17
19
|
# 註冊表(順序=彙整順序)。hook registry 先於 zero-config source,精準事件優先。
|
|
18
|
-
_SOURCES: list[SessionSource] = [_hook_registry, _claude_code, _codex]
|
|
20
|
+
_SOURCES: list[SessionSource] = [_hook_registry, _claude_code, _codex, _ollama, _llama_cpp]
|
|
19
21
|
|
|
20
22
|
|
|
21
23
|
def register_source(source: SessionSource, *, first: bool = False) -> None:
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"""Ollama 與 llama.cpp 互動式 CLI 的 zero-config 行程來源。
|
|
2
|
+
|
|
3
|
+
兩者都沒有可供 RiNG 讀取的 session transcript;這個來源因此只承諾行程層級的
|
|
4
|
+
存活資訊。只收有控制終端的 ``ollama run`` / ``llama-cli``,刻意排除長駐 API
|
|
5
|
+
server,避免把基礎設施誤當成「需要使用者回去處理」的 session。
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
import subprocess
|
|
12
|
+
import time
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
|
|
15
|
+
import ring.registry as registry
|
|
16
|
+
from ring.registry import Session, Status
|
|
17
|
+
|
|
18
|
+
_CACHE_TTL = 1.0
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass(frozen=True)
|
|
22
|
+
class LocalLLMProcess:
|
|
23
|
+
pid: int
|
|
24
|
+
provider: str
|
|
25
|
+
cwd: str
|
|
26
|
+
tty: str
|
|
27
|
+
started_at: float
|
|
28
|
+
model: str
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
_process_cache: tuple[float, list[LocalLLMProcess] | None] = (-1.0, [])
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _elapsed_seconds(value: str) -> int | None:
|
|
35
|
+
"""解析 ps ``etime``(``[[dd-]hh:]mm:ss``)。"""
|
|
36
|
+
try:
|
|
37
|
+
day_part, clock = value.split("-", 1) if "-" in value else ("0", value)
|
|
38
|
+
fields = [int(part) for part in clock.split(":")]
|
|
39
|
+
if len(fields) == 2:
|
|
40
|
+
hours, minutes, seconds = 0, fields[0], fields[1]
|
|
41
|
+
elif len(fields) == 3:
|
|
42
|
+
hours, minutes, seconds = fields
|
|
43
|
+
else:
|
|
44
|
+
return None
|
|
45
|
+
return int(day_part) * 86400 + hours * 3600 + minutes * 60 + seconds
|
|
46
|
+
except ValueError:
|
|
47
|
+
return None
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _command_tokens(comm: str, args: str) -> tuple[str, list[str]]:
|
|
51
|
+
"""找出實際 executable 與其後參數;ps args 不保證保留 shell quoting。"""
|
|
52
|
+
tokens = args.split()
|
|
53
|
+
executable = os.path.basename(comm.strip())
|
|
54
|
+
for index, token in enumerate(tokens):
|
|
55
|
+
if os.path.basename(token) == executable:
|
|
56
|
+
return executable, tokens[index + 1 :]
|
|
57
|
+
return executable, tokens[1:] if tokens else []
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _classify(comm: str, args: str) -> tuple[str, str] | None:
|
|
61
|
+
executable, argv = _command_tokens(comm, args)
|
|
62
|
+
if executable == "ollama":
|
|
63
|
+
if len(argv) < 2 or argv[0] != "run":
|
|
64
|
+
return None
|
|
65
|
+
return "ollama", argv[1]
|
|
66
|
+
if executable == "llama-cli":
|
|
67
|
+
model_flags = {"-m", "--model", "-hf", "-hfr", "--hf-repo", "-mu", "--model-url"}
|
|
68
|
+
for index, token in enumerate(argv[:-1]):
|
|
69
|
+
if token in model_flags:
|
|
70
|
+
return "llama.cpp", argv[index + 1]
|
|
71
|
+
return "llama.cpp", "llama-cli"
|
|
72
|
+
return None
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _scan_processes() -> list[LocalLLMProcess] | None:
|
|
76
|
+
"""一次 ps + 一次批次 lsof 找出兩種本機互動 CLI;失敗以 ``None`` 表示未知。"""
|
|
77
|
+
global _process_cache
|
|
78
|
+
monotonic_now = time.monotonic()
|
|
79
|
+
if 0 <= monotonic_now - _process_cache[0] <= _CACHE_TTL:
|
|
80
|
+
return _process_cache[1]
|
|
81
|
+
|
|
82
|
+
try:
|
|
83
|
+
result = subprocess.run(
|
|
84
|
+
["ps", "-Ao", "pid=,tty=,etime=,comm=,args="],
|
|
85
|
+
capture_output=True,
|
|
86
|
+
text=True,
|
|
87
|
+
timeout=3,
|
|
88
|
+
)
|
|
89
|
+
except (OSError, subprocess.SubprocessError):
|
|
90
|
+
return None
|
|
91
|
+
if result.returncode != 0:
|
|
92
|
+
return None
|
|
93
|
+
|
|
94
|
+
now = time.time()
|
|
95
|
+
candidates: list[tuple[int, str, float, str, str]] = []
|
|
96
|
+
for line in result.stdout.splitlines():
|
|
97
|
+
parts = line.split(None, 4)
|
|
98
|
+
if len(parts) < 4:
|
|
99
|
+
continue
|
|
100
|
+
tty = registry._normalize_tty(parts[1])
|
|
101
|
+
if not tty:
|
|
102
|
+
continue
|
|
103
|
+
classified = _classify(parts[3], parts[4] if len(parts) == 5 else "")
|
|
104
|
+
elapsed = _elapsed_seconds(parts[2])
|
|
105
|
+
if classified is None or elapsed is None:
|
|
106
|
+
continue
|
|
107
|
+
try:
|
|
108
|
+
pid = int(parts[0])
|
|
109
|
+
except ValueError:
|
|
110
|
+
continue
|
|
111
|
+
provider, model = classified
|
|
112
|
+
candidates.append((pid, tty, now - elapsed, provider, model))
|
|
113
|
+
|
|
114
|
+
cwd_by_pid = registry._pids_cwd([pid for pid, *_rest in candidates])
|
|
115
|
+
if cwd_by_pid is None:
|
|
116
|
+
return None
|
|
117
|
+
processes = [
|
|
118
|
+
LocalLLMProcess(pid, provider, cwd, tty, started_at, model)
|
|
119
|
+
for pid, tty, started_at, provider, model in candidates
|
|
120
|
+
if (cwd := cwd_by_pid.get(pid, ""))
|
|
121
|
+
]
|
|
122
|
+
_process_cache = (monotonic_now, processes)
|
|
123
|
+
return processes
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def running_pids() -> list[int]:
|
|
127
|
+
"""顯示用途的 live local-LLM CLI pid;掃描未知時回空清單。"""
|
|
128
|
+
return [process.pid for process in (_scan_processes() or [])]
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
class LocalLLMSource:
|
|
132
|
+
def __init__(self, provider: str) -> None:
|
|
133
|
+
self.name = provider
|
|
134
|
+
|
|
135
|
+
def discover(self) -> list[Session]:
|
|
136
|
+
processes = _scan_processes()
|
|
137
|
+
if processes is None:
|
|
138
|
+
return []
|
|
139
|
+
return [
|
|
140
|
+
Session(
|
|
141
|
+
session_id=f"{self.name}:pid-{process.pid}",
|
|
142
|
+
cwd=process.cwd,
|
|
143
|
+
status=Status.IDLE,
|
|
144
|
+
last_active=process.started_at,
|
|
145
|
+
last_action=process.model,
|
|
146
|
+
source=self.name,
|
|
147
|
+
tty=process.tty,
|
|
148
|
+
provider=self.name,
|
|
149
|
+
origin_cwd=process.cwd,
|
|
150
|
+
)
|
|
151
|
+
for process in processes
|
|
152
|
+
if process.provider == self.name
|
|
153
|
+
]
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
ollama_source = LocalLLMSource("ollama")
|
|
157
|
+
llama_cpp_source = LocalLLMSource("llama.cpp")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|