agent-log-viewer 0.1.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (131) hide show
  1. package/README.md +171 -51
  2. package/bin/cli.mjs +89 -22
  3. package/bin/tailscale.mjs +101 -41
  4. package/dist/standalone/.next/BUILD_ID +1 -1
  5. package/dist/standalone/.next/app-path-routes-manifest.json +10 -5
  6. package/dist/standalone/.next/build-manifest.json +2 -2
  7. package/dist/standalone/.next/prerender-manifest.json +3 -3
  8. package/dist/standalone/.next/routes-manifest.json +36 -0
  9. package/dist/standalone/.next/server/app/_global-error/page.js +3 -3
  10. package/dist/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  11. package/dist/standalone/.next/server/app/_global-error.html +1 -1
  12. package/dist/standalone/.next/server/app/_global-error.rsc +1 -1
  13. package/dist/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  14. package/dist/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  15. package/dist/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  16. package/dist/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  17. package/dist/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  18. package/dist/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  19. package/dist/standalone/.next/server/app/_not-found/page.js +2 -2
  20. package/dist/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  21. package/dist/standalone/.next/server/app/_not-found.html +1 -1
  22. package/dist/standalone/.next/server/app/_not-found.rsc +4 -4
  23. package/dist/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +4 -4
  24. package/dist/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  25. package/dist/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +2 -2
  26. package/dist/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  27. package/dist/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  28. package/dist/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  29. package/dist/standalone/.next/server/app/api/access/route.js +1 -1
  30. package/dist/standalone/.next/server/app/api/answer/route.js +1 -1
  31. package/dist/standalone/.next/server/app/api/files/route.js +1 -1
  32. package/dist/standalone/.next/server/app/api/flows/[id]/route.js +1 -1
  33. package/dist/standalone/.next/server/app/api/flows/[id]/route.js.nft.json +1 -1
  34. package/dist/standalone/.next/server/app/api/flows/route.js +1 -1
  35. package/dist/standalone/.next/server/app/api/flows/route.js.nft.json +1 -1
  36. package/dist/standalone/.next/server/app/api/inbox/route.js +1 -1
  37. package/dist/standalone/.next/server/app/api/limits/route.js +1 -1
  38. package/dist/standalone/.next/server/app/api/log/route.js +1 -1
  39. package/dist/standalone/.next/server/app/api/proc/route.js +1 -1
  40. package/dist/standalone/.next/server/app/api/push/route.js +1 -1
  41. package/dist/standalone/.next/server/app/api/spawn/route.js +1 -1
  42. package/dist/standalone/.next/server/app/api/tasks/[id]/route.js +1 -0
  43. package/dist/standalone/.next/server/app/api/tasks/[id]/route.js.nft.json +1 -0
  44. package/dist/standalone/.next/server/app/api/tasks/[id]/route_client-reference-manifest.js +1 -0
  45. package/dist/standalone/.next/server/app/api/tasks/[id]/send/route.js +2 -0
  46. package/dist/standalone/.next/server/app/api/tasks/[id]/send/route.js.nft.json +1 -0
  47. package/dist/standalone/.next/server/app/api/tasks/[id]/send/route_client-reference-manifest.js +1 -0
  48. package/dist/standalone/.next/server/app/api/tasks/[id]/spawn/route.js +1 -0
  49. package/dist/standalone/.next/server/app/api/tasks/[id]/spawn/route.js.nft.json +1 -0
  50. package/dist/standalone/.next/server/app/api/tasks/[id]/spawn/route_client-reference-manifest.js +1 -0
  51. package/dist/standalone/.next/server/app/api/tasks/route.js +1 -0
  52. package/dist/standalone/.next/server/app/api/tasks/route.js.nft.json +1 -0
  53. package/dist/standalone/.next/server/app/api/tasks/route_client-reference-manifest.js +1 -0
  54. package/dist/standalone/.next/server/app/api/timeline/route.js +1 -1
  55. package/dist/standalone/.next/server/app/api/tmux/route.js +2 -2
  56. package/dist/standalone/.next/server/app/api/transcribe/route.js +1 -1
  57. package/dist/standalone/.next/server/app/api/transcribe/token/route.js +1 -0
  58. package/dist/standalone/.next/server/app/api/transcribe/token/route.js.nft.json +1 -0
  59. package/dist/standalone/.next/server/app/api/transcribe/token/route_client-reference-manifest.js +1 -0
  60. package/dist/standalone/.next/server/app/favicon.ico/route.js +1 -1
  61. package/dist/standalone/.next/server/app/index.html +1 -1
  62. package/dist/standalone/.next/server/app/index.rsc +5 -5
  63. package/dist/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
  64. package/dist/standalone/.next/server/app/index.segments/_full.segment.rsc +5 -5
  65. package/dist/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  66. package/dist/standalone/.next/server/app/index.segments/_index.segment.rsc +2 -2
  67. package/dist/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  68. package/dist/standalone/.next/server/app/page.js +3 -6
  69. package/dist/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  70. package/dist/standalone/.next/server/app-paths-manifest.json +10 -5
  71. package/dist/standalone/.next/server/chunks/289.js +2 -0
  72. package/dist/standalone/.next/server/chunks/871.js +2 -3
  73. package/dist/standalone/.next/server/functions-config-manifest.json +6 -1
  74. package/dist/standalone/.next/server/middleware-build-manifest.js +1 -1
  75. package/dist/standalone/.next/server/pages/404.html +1 -1
  76. package/dist/standalone/.next/server/pages/500.html +1 -1
  77. package/dist/standalone/.next/server/server-reference-manifest.json +1 -1
  78. package/dist/standalone/.next/static/chunks/258-a0398b569708ffc3.js +1 -0
  79. package/dist/standalone/.next/static/chunks/app/_global-error/page-f4e3cd62c5cbfd22.js +1 -0
  80. package/dist/standalone/.next/static/chunks/app/api/access/route-f4e3cd62c5cbfd22.js +1 -0
  81. package/dist/standalone/.next/static/chunks/app/api/answer/route-f4e3cd62c5cbfd22.js +1 -0
  82. package/dist/standalone/.next/static/chunks/app/api/files/route-f4e3cd62c5cbfd22.js +1 -0
  83. package/dist/standalone/.next/static/chunks/app/api/flows/[id]/route-f4e3cd62c5cbfd22.js +1 -0
  84. package/dist/standalone/.next/static/chunks/app/api/flows/route-f4e3cd62c5cbfd22.js +1 -0
  85. package/dist/standalone/.next/static/chunks/app/api/inbox/route-f4e3cd62c5cbfd22.js +1 -0
  86. package/dist/standalone/.next/static/chunks/app/api/limits/route-f4e3cd62c5cbfd22.js +1 -0
  87. package/dist/standalone/.next/static/chunks/app/api/log/route-f4e3cd62c5cbfd22.js +1 -0
  88. package/dist/standalone/.next/static/chunks/app/api/proc/route-f4e3cd62c5cbfd22.js +1 -0
  89. package/dist/standalone/.next/static/chunks/app/api/push/route-f4e3cd62c5cbfd22.js +1 -0
  90. package/dist/standalone/.next/static/chunks/app/api/spawn/route-f4e3cd62c5cbfd22.js +1 -0
  91. package/dist/standalone/.next/static/chunks/app/api/tasks/[id]/route-f4e3cd62c5cbfd22.js +1 -0
  92. package/dist/standalone/.next/static/chunks/app/api/tasks/[id]/send/route-f4e3cd62c5cbfd22.js +1 -0
  93. package/dist/standalone/.next/static/chunks/app/api/tasks/[id]/spawn/route-f4e3cd62c5cbfd22.js +1 -0
  94. package/dist/standalone/.next/static/chunks/app/api/tasks/route-f4e3cd62c5cbfd22.js +1 -0
  95. package/dist/standalone/.next/static/chunks/app/api/timeline/route-f4e3cd62c5cbfd22.js +1 -0
  96. package/dist/standalone/.next/static/chunks/app/api/tmux/route-f4e3cd62c5cbfd22.js +1 -0
  97. package/dist/standalone/.next/static/chunks/app/api/transcribe/route-f4e3cd62c5cbfd22.js +1 -0
  98. package/dist/standalone/.next/static/chunks/app/api/transcribe/token/route-f4e3cd62c5cbfd22.js +1 -0
  99. package/dist/standalone/.next/static/chunks/app/page-cd0f70a472e144bd.js +2 -0
  100. package/dist/standalone/.next/static/chunks/next/dist/client/components/builtin/app-error-f4e3cd62c5cbfd22.js +1 -0
  101. package/dist/standalone/.next/static/chunks/next/dist/client/components/builtin/forbidden-f4e3cd62c5cbfd22.js +1 -0
  102. package/dist/standalone/.next/static/chunks/next/dist/client/components/builtin/not-found-f4e3cd62c5cbfd22.js +1 -0
  103. package/dist/standalone/.next/static/chunks/next/dist/client/components/builtin/unauthorized-f4e3cd62c5cbfd22.js +1 -0
  104. package/dist/standalone/.next/static/css/3f6c482d54e91e64.css +1 -0
  105. package/dist/standalone/.next/static/dxjlPz3fKtXT-7BfdVO_q/_buildManifest.js +1 -0
  106. package/dist/standalone/package.json +4 -2
  107. package/package.json +4 -2
  108. package/dist/standalone/.next/static/0YxRBiEgYCSV92_fZOmmC/_buildManifest.js +0 -1
  109. package/dist/standalone/.next/static/chunks/703-1f675e3bf3e228de.js +0 -1
  110. package/dist/standalone/.next/static/chunks/app/_global-error/page-b58291c8001879c8.js +0 -1
  111. package/dist/standalone/.next/static/chunks/app/api/access/route-b58291c8001879c8.js +0 -1
  112. package/dist/standalone/.next/static/chunks/app/api/answer/route-b58291c8001879c8.js +0 -1
  113. package/dist/standalone/.next/static/chunks/app/api/files/route-b58291c8001879c8.js +0 -1
  114. package/dist/standalone/.next/static/chunks/app/api/flows/[id]/route-b58291c8001879c8.js +0 -1
  115. package/dist/standalone/.next/static/chunks/app/api/flows/route-b58291c8001879c8.js +0 -1
  116. package/dist/standalone/.next/static/chunks/app/api/inbox/route-b58291c8001879c8.js +0 -1
  117. package/dist/standalone/.next/static/chunks/app/api/limits/route-b58291c8001879c8.js +0 -1
  118. package/dist/standalone/.next/static/chunks/app/api/log/route-b58291c8001879c8.js +0 -1
  119. package/dist/standalone/.next/static/chunks/app/api/proc/route-b58291c8001879c8.js +0 -1
  120. package/dist/standalone/.next/static/chunks/app/api/push/route-b58291c8001879c8.js +0 -1
  121. package/dist/standalone/.next/static/chunks/app/api/spawn/route-b58291c8001879c8.js +0 -1
  122. package/dist/standalone/.next/static/chunks/app/api/timeline/route-b58291c8001879c8.js +0 -1
  123. package/dist/standalone/.next/static/chunks/app/api/tmux/route-b58291c8001879c8.js +0 -1
  124. package/dist/standalone/.next/static/chunks/app/api/transcribe/route-b58291c8001879c8.js +0 -1
  125. package/dist/standalone/.next/static/chunks/app/page-169f116d08445993.js +0 -5
  126. package/dist/standalone/.next/static/chunks/next/dist/client/components/builtin/app-error-b58291c8001879c8.js +0 -1
  127. package/dist/standalone/.next/static/chunks/next/dist/client/components/builtin/forbidden-b58291c8001879c8.js +0 -1
  128. package/dist/standalone/.next/static/chunks/next/dist/client/components/builtin/not-found-b58291c8001879c8.js +0 -1
  129. package/dist/standalone/.next/static/chunks/next/dist/client/components/builtin/unauthorized-b58291c8001879c8.js +0 -1
  130. package/dist/standalone/.next/static/css/e3457e19c8be328e.css +0 -1
  131. /package/dist/standalone/.next/static/{0YxRBiEgYCSV92_fZOmmC → dxjlPz3fKtXT-7BfdVO_q}/_ssgManifest.js +0 -0
package/README.md CHANGED
@@ -1,9 +1,16 @@
1
1
  # Live Log Viewer
2
2
 
3
- A local web UI that turns raw Codex / Claude Code agent logs into a readable,
4
- live-updating chat feed. It discovers every session, subagent, Codex companion
5
- job and background shell task on your machine, links them into a parent→child
6
- tree, and tails the selected one in real time.
3
+ `agent-log-viewer` is a local web UI that turns raw Codex / Claude Code agent
4
+ logs into a readable, live-updating chat feed. It discovers every session,
5
+ subagent, Codex companion job and background shell task on your machine, links
6
+ them into a parent→child tree, and tails the selected one in real time.
7
+
8
+ Everything runs locally against files already on disk. There is no database and
9
+ no external service — the app reads `~/.claude` and `~/.codex` and renders what
10
+ it finds.
11
+
12
+ ![The chat feed: user bubbles, assistant prose, and tool-call cards with
13
+ statuses and expandable output](docs/media/chat-feed.png)
7
14
 
8
15
  ## What it shows
9
16
 
@@ -24,29 +31,42 @@ tree, and tails the selected one in real time.
24
31
  - **Parentage tree**: session → subagents → codex jobs → rollouts → background
25
32
  tasks, built server-side by scanning transcripts (append-only incremental,
26
33
  cached — the warm `/api/files` poll stays around 100 ms).
27
- - **Conversations first**: filter chips «Все · Розмови · Активні», stable
28
- alphabetical project groups, most-recently-updated first inside a project,
29
- technical noise collected under a collapsed «⚙ Технічне» group.
30
- - **Live activity**: content-based badges — a transcript is "працює" while
31
- mid-turn and "закінчив" once the final assistant message lands.
34
+ - **Live activity**: content-based badges a transcript reads *working* while
35
+ it is mid-turn and *done* once the final assistant message lands.
32
36
  - **Deep links**: every selection is reflected in the URL (`#f=<path>`), so a
33
37
  link opens that exact log.
34
- - Model chips (`fable-5`, `gpt-5.5`, `sonnet`…), collapsible tree with
35
- persisted state, follow-mode autoscroll, service-event toggle, line filter.
38
+ - **Project scheme**: each project is a pannable, zoomable diagram — the root
39
+ conversation on top, spawned agents one generation below, arrows colored by
40
+ engine. Quiet branches and tasks collapse under their nearest ancestor.
41
+ - **English or Ukrainian UI**, model chips (`fable-5`, `gpt-5.5`, `sonnet`…),
42
+ collapsible tree with persisted state, follow-mode autoscroll, service-event
43
+ toggle, and a line filter.
36
44
 
37
- ## Run
45
+ ## Screenshots
46
+
47
+ The session parentage tree, wiring root conversations to their spawned agents:
48
+
49
+ ![Session parentage tree](docs/media/session-tree.png)
38
50
 
39
- Requires [bun](https://bun.sh) (or npm/pnpm), Node 20.9+, and
40
- [tmux](https://github.com/tmux/tmux) for the composer/spawn features (`brew
41
- install tmux` on macOS, or your distro's package on Linux).
51
+ | A Codex CLI session | The overview board |
52
+ | --- | --- |
53
+ | ![Codex session with command cards and patches](docs/media/codex-session.png) | ![Overview board across projects](docs/media/overview-board.png) |
42
54
 
43
- Once the package is published to npm, the quickstart is:
55
+ ## Run
56
+
57
+ The package is published to npm, so the quickstart needs no clone:
44
58
 
45
59
  ```bash
46
60
  bunx agent-log-viewer
61
+ # or
62
+ npx agent-log-viewer
47
63
  ```
48
64
 
49
- Local clone workflow:
65
+ This starts the server on `127.0.0.1:8898` and opens your browser.
66
+
67
+ ### From a local clone
68
+
69
+ With bun:
50
70
 
51
71
  ```bash
52
72
  bun install
@@ -55,20 +75,91 @@ bun start --port 8898 --hostname 127.0.0.1
55
75
  # open http://127.0.0.1:8898/
56
76
  ```
57
77
 
58
- `bun dev` works too (needs a high OS file-watch limit for large homedirs).
78
+ With npm (note the `--` that forwards flags to the start script):
79
+
80
+ ```bash
81
+ npm install
82
+ npm run build
83
+ npm start -- --port 8898 --hostname 127.0.0.1
84
+ ```
85
+
86
+ `start` serves the output of the last `build`, so run `build` first. For
87
+ development, `bun dev` runs the app with hot reload (it needs a high OS
88
+ file-watch limit for large home directories).
89
+
90
+ **Prerequisites:** Node ≥ 20.9, and bun or npm/pnpm. `tmux` is optional — see
91
+ [Platform support](#platform-support).
92
+
93
+ ### CLI options
94
+
95
+ ```
96
+ agent-log-viewer [options]
97
+ ```
98
+
99
+ | Option | Description |
100
+ | --- | --- |
101
+ | `-p, --port <n>` | Port for the local server (default `8898`). |
102
+ | `-H, --hostname <h>` | Bind address (default `127.0.0.1`). |
103
+ | `--tailscale` | Expose the viewer inside your tailnet (see below). |
104
+ | `--new-token` | Generate a fresh access key and invalidate old cookies. |
105
+ | `--no-open` | Don't open the browser on start. |
106
+ | `-v, --version` | Print the version. |
107
+ | `-h, --help` | Show usage. |
59
108
 
60
109
  ## Platform support
61
110
 
62
- Linux is the native target: process discovery reads `/proc` directly. macOS
63
- is supported through a portable backend that shells out to `ps` and `lsof`
111
+ Linux is the native target: process discovery reads `/proc` directly. macOS is
112
+ supported through a portable backend that shells out to `ps` and `lsof`
64
113
  instead — same live-process detection, tmux composer targeting, agent
65
- spawn/kill and background-task discovery, just a bit more subprocess
66
- overhead per scan. The backend is chosen automatically by `process.platform`
67
- (see `src/lib/proc/`); `VIEWER_PROC_BACKEND=portable` forces the portable
68
- path on Linux too, for testing.
114
+ spawn/kill and background-task discovery, just a bit more subprocess overhead
115
+ per scan. The backend is chosen automatically by `process.platform` (see
116
+ `src/lib/proc/`); `VIEWER_PROC_BACKEND=portable` forces the portable path on
117
+ Linux too, for testing.
69
118
 
70
- Without tmux installed, log viewing still works; the composer, agent spawn
71
- and resume-into-pane features are unavailable.
119
+ The package supports Linux and macOS. The package's `os` field blocks Windows
120
+ installs with `EBADPLATFORM`. WSL works as Linux.
121
+
122
+ `tmux` is optional. Without it, log viewing, the parentage tree, live activity
123
+ and deep links all work; the composer, agent spawn/kill and resume-into-pane
124
+ features need tmux (`brew install tmux` on macOS, or your distro's package on
125
+ Linux).
126
+
127
+ ## Language
128
+
129
+ The UI defaults to English and shows a compact EN/UK toggle in the project
130
+ list header. The locale is resolved as `localStorage` key `llv_lang` first,
131
+ then the browser language (Ukrainian if the browser prefers it), then English.
132
+
133
+ CLI messages are English by default, and switch to Ukrainian with
134
+ `LLV_LANG=uk` or a `uk_*` value in `LANG`/`LC_ALL`.
135
+
136
+ ## Dictation / voice input
137
+
138
+ Composers that talk to agents have a mic button for dictating messages. By
139
+ default transcription runs fully locally via faster-whisper — no audio leaves
140
+ the machine. Run `scripts/setup-whisper.sh` once to install the local engine.
141
+
142
+ Two cloud backends are available as an explicit per-machine opt-in (never a UI
143
+ toggle): ChatGPT (reuses your local Codex login) and ElevenLabs Scribe (the
144
+ only one with live, streaming transcription). Select with
145
+ `LLV_TRANSCRIBE_BACKEND=local|chatgpt|elevenlabs` or by writing the backend
146
+ name to `~/.config/agent-log-viewer/transcribe-backend`; local is the default.
147
+
148
+ See [docs/transcription.md](docs/transcription.md) for setup, key locations,
149
+ and troubleshooting.
150
+
151
+ ## Review loops
152
+
153
+ The viewer orchestrates implement→review cycles: a long-lived implementer
154
+ agent in tmux, a fresh read-only reviewer per round over the full diff,
155
+ automatic relay of findings, and a verdict deck in the scheme view. Start one
156
+ from the **Flow** chip above a conversation pane; presets pair engines and
157
+ reasoning efforts per role (e.g. `Codex high → Fable`).
158
+
159
+ See [docs/review-loop.md](docs/review-loop.md) for the round protocol,
160
+ presets, the HTTP automation API, and troubleshooting. A Claude Code skill
161
+ for driving flows from an agent ships in `.claude/skills/review-loop/` —
162
+ agents working in a clone pick it up automatically.
72
163
 
73
164
  ## Tailscale access
74
165
 
@@ -76,29 +167,29 @@ and resume-into-pane features are unavailable.
76
167
  bunx agent-log-viewer --tailscale
77
168
  ```
78
169
 
79
- `--tailscale` starts the local server on `127.0.0.1` and exposes it inside
80
- your tailnet through a foreground `tailscale serve <port>` process. The
81
- public internet (Funnel) is never used.
82
-
83
- The CLI generates a 32-character access key, appends it to the tailnet URL
84
- as `?k=...`, and after the first visit the server sets an `llv_auth` cookie
85
- for 30 days. `--new-token` generates a fresh key and immediately invalidates
86
- every old cookie — each request compares the hash against the current token,
87
- so a cookie minted with a previous key no longer passes.
88
-
89
- The terminal prints the tailnet URL along with a QR code to scan with a
90
- phone. The same QR is available inside the web UI: the QR-icon button in the
91
- project list header opens a popover with the code and the link as text (with
92
- a copy button). The QR is rendered entirely client-side (the `qrcode`
93
- package, no external requests) and is served only to already-authorized
94
- clients — the same token gate from `src/proxy.ts` also protects
95
- `/api/access`. When the server runs without `--tailscale`, the button shows
96
- a hint to start `bunx agent-log-viewer --tailscale`.
170
+ `--tailscale` starts the local server on `127.0.0.1` and exposes it inside your
171
+ tailnet through a foreground `tailscale serve <port>` process. The public
172
+ internet (Funnel) is never used.
173
+
174
+ The CLI generates a 32-character access key, appends it to the tailnet URL as
175
+ `?k=...`, and after the first visit the server sets an `llv_auth` cookie for 30
176
+ days. `--new-token` generates a fresh key and immediately invalidates every old
177
+ cookie — each request compares the hash against the current token, so a cookie
178
+ minted with a previous key no longer passes.
179
+
180
+ The terminal prints the tailnet URL along with a QR code to scan with a phone.
181
+ The same QR is available inside the web UI: the QR-icon button in the project
182
+ list header opens a popover with the code and the link as text (with a copy
183
+ button). The QR is rendered entirely client-side (the `qrcode` package, no
184
+ external requests) and is served only to already-authorized clients — the same
185
+ token gate from `src/proxy.ts` also protects `/api/access`. When the server
186
+ runs without `--tailscale`, the button shows a hint to start
187
+ `bunx agent-log-viewer --tailscale`.
97
188
 
98
189
  Anyone with tailnet access to this URL can read all agent transcripts,
99
- including any sensitive data that landed in a session, and can execute
100
- commands through `/api/tmux` and `/api/spawn`. Treat the tailnet URL as a
101
- secret — do not forward it to anyone else.
190
+ including any sensitive data that landed in a session, and can execute commands
191
+ through `/api/tmux` and `/api/spawn`. Treat the tailnet URL as a secret — do
192
+ not forward it to anyone else.
102
193
 
103
194
  ## Security model
104
195
 
@@ -111,12 +202,41 @@ inside the tailnet via `tailscale serve` and guarded by the token gate in
111
202
  `src/proxy.ts`. Non-loopback binds also force token mode. Treat any URL
112
203
  containing `?k=` as a credential.
113
204
 
205
+ ## Environment variables
206
+
207
+ All optional. Transcription variables are documented in full in
208
+ [docs/transcription.md](docs/transcription.md).
209
+
210
+ | Variable | Effect |
211
+ | --- | --- |
212
+ | `VIEWER_PROC_BACKEND` | `portable` or `linux` — force the process-discovery backend (auto-selected by default). |
213
+ | `LLV_LANG` | `uk` or `en` — force the CLI message language. |
214
+ | `LLV_TRANSCRIBE_BACKEND` | `local`, `chatgpt`, or `elevenlabs` — pick the dictation backend (default `local`). |
215
+ | `LLV_WHISPER_MODEL` | faster-whisper model size (default `small`). |
216
+ | `LLV_WHISPER_DEVICE` | `cpu` (default) or `cuda`. |
217
+ | `LLV_WHISPER_VENV` | Path to the whisper virtualenv (default `~/.cache/agent-log-viewer/whisper-venv`). |
218
+ | `LLV_ELEVENLABS_STT_MODEL` | ElevenLabs batch model override. |
219
+ | `ELEVENLABS_API_KEY` | ElevenLabs API key for the ElevenLabs backend. |
220
+
221
+ ## Config paths
222
+
223
+ The viewer keeps its state under the standard XDG directories, named after the
224
+ package:
225
+
226
+ - `~/.config/agent-log-viewer/` — access `token`, `transcribe-backend`, and
227
+ `elevenlabs-api-key`.
228
+ - `~/.cache/agent-log-viewer/whisper-venv` — the local transcription
229
+ virtualenv.
230
+
231
+ Legacy `live-log-viewer` paths are still honored as read-only fallbacks, so
232
+ existing setups keep working without changes.
233
+
114
234
  ## Architecture
115
235
 
116
- See [ARCHITECTURE.md](ARCHITECTURE.md): route handlers under
117
- `src/app/api/*`, a pure scanner pipeline under `src/lib/scanner/*`
118
- (discover describe → activity → model → links), React components under
119
- `src/components/*`. Caches live on `globalThis` and survive dev hot-reload.
236
+ See [ARCHITECTURE.md](ARCHITECTURE.md): route handlers under `src/app/api/*`, a
237
+ pure scanner pipeline under `src/lib/scanner/*` (discover → describe → activity
238
+ → model → links), React components under `src/components/*`. Caches live on
239
+ `globalThis` and survive dev hot-reload.
120
240
 
121
241
  ## License
122
242
 
package/bin/cli.mjs CHANGED
@@ -16,8 +16,50 @@ const READINESS_INTERVAL_MS = 200;
16
16
  const cliPath = fileURLToPath(import.meta.url);
17
17
  const cliDir = dirname(cliPath);
18
18
 
19
- function usage() {
20
- return `Використання: agent-log-viewer [опції]
19
+ /* Dependency-free CLI localization: English by default, Ukrainian when
20
+ LLV_LANG=uk or the locale (LC_ALL/LANG) is a uk_* / uk.* variant. */
21
+ function detectLang() {
22
+ const explicit = (process.env.LLV_LANG || "").toLowerCase();
23
+ if (explicit === "uk" || explicit === "en") return explicit;
24
+ const loc = (process.env.LC_ALL || process.env.LANG || "").toLowerCase();
25
+ return loc === "uk" || loc.startsWith("uk_") || loc.startsWith("uk.") ? "uk" : "en";
26
+ }
27
+
28
+ const LANG = detectLang();
29
+
30
+ const MESSAGES = {
31
+ en: {
32
+ usage: () => `Usage: agent-log-viewer [options]
33
+
34
+ Options:
35
+ -p, --port <n> Port for the local server (default ${DEFAULT_PORT})
36
+ -H, --hostname <h> Bind address (default ${DEFAULT_HOSTNAME})
37
+ --tailscale Access over Tailscale
38
+ --no-open Don't open the browser
39
+ --new-token Create a new access key
40
+ -v, --version Show the version
41
+ -h, --help Show this help`,
42
+ badPort: (value) => `Invalid port: ${value}`,
43
+ flagNeedsValue: (flag) => `Option ${flag} requires a value.`,
44
+ hostnameNeedsValue: () => "Option --hostname requires a value.",
45
+ unknownOption: (arg) => `Unknown option: ${arg}`,
46
+ noPackageJson: () => "Couldn't find package.json for agent-log-viewer.",
47
+ readPackageJsonErr: (detail) => `Couldn't read package.json: ${detail}`,
48
+ readPackageJsonErrGeneric: () => "Couldn't read package.json.",
49
+ noServer: () => "No standalone server.js or local next found.",
50
+ portBusy: (port) => `Port ${port} is busy. Try: bunx agent-log-viewer --port ${port + 1}`,
51
+ serverStartFail: (detail) => `Couldn't start the server: ${detail}`,
52
+ serverTimeout: (seconds) => `The server didn't respond within ${seconds} seconds.`,
53
+ bannerOpened: (url) => ` Opened: ${url}`,
54
+ bannerReads: () => " Reads logs from ~/.claude/projects, ~/.codex/sessions.",
55
+ bannerStop: () => " Ctrl+C — stop. --tailscale — access from your phone.",
56
+ tsLinkWarn: () => " The link contains an access key — don't forward it to others.",
57
+ tsCookie: () => " After the first open the key is stored in a cookie for 30 days.",
58
+ nonLocalWarn: () => "Warning: a non-local address exposes the viewer to the network, so access-key mode is forced on.",
59
+ serverNotReady: () => "Server not ready.",
60
+ },
61
+ uk: {
62
+ usage: () => `Використання: agent-log-viewer [опції]
21
63
 
22
64
  Опції:
23
65
  -p, --port <n> Порт для локального сервера (типово ${DEFAULT_PORT})
@@ -26,7 +68,32 @@ function usage() {
26
68
  --no-open Не відкривати браузер
27
69
  --new-token Створити новий ключ доступу
28
70
  -v, --version Показати версію
29
- -h, --help Показати довідку`;
71
+ -h, --help Показати довідку`,
72
+ badPort: (value) => `Некоректний порт: ${value}`,
73
+ flagNeedsValue: (flag) => `Опція ${flag} потребує значення.`,
74
+ hostnameNeedsValue: () => "Опція --hostname потребує значення.",
75
+ unknownOption: (arg) => `Невідома опція: ${arg}`,
76
+ noPackageJson: () => "Не вдалося знайти package.json для agent-log-viewer.",
77
+ readPackageJsonErr: (detail) => `Не вдалося прочитати package.json: ${detail}`,
78
+ readPackageJsonErrGeneric: () => "Не вдалося прочитати package.json.",
79
+ noServer: () => "Не знайдено standalone server.js або локальний next.",
80
+ portBusy: (port) => `Порт ${port} зайнятий. Спробуйте: bunx agent-log-viewer --port ${port + 1}`,
81
+ serverStartFail: (detail) => `Не вдалося запустити сервер: ${detail}`,
82
+ serverTimeout: (seconds) => `Сервер не відповів за ${seconds} секунд.`,
83
+ bannerOpened: (url) => ` Відкрито: ${url}`,
84
+ bannerReads: () => " Читає логи з ~/.claude/projects, ~/.codex/sessions.",
85
+ bannerStop: () => " Ctrl+C — зупинити. --tailscale — доступ з телефона.",
86
+ tsLinkWarn: () => " Посилання містить ключ доступу — не пересилайте його стороннім.",
87
+ tsCookie: () => " Після першого відкриття ключ зберігається у cookie на 30 днів.",
88
+ nonLocalWarn: () => "Увага: нелокальна адреса відкриває viewer для мережі, тому режим ключа доступу увімкнено примусово.",
89
+ serverNotReady: () => "Сервер не готовий.",
90
+ },
91
+ };
92
+
93
+ const m = MESSAGES[LANG];
94
+
95
+ function usage() {
96
+ return m.usage();
30
97
  }
31
98
 
32
99
  function fail(message) {
@@ -37,7 +104,7 @@ function fail(message) {
37
104
  function parsePort(value) {
38
105
  const port = Number(value);
39
106
  if (!Number.isInteger(port) || port < 1 || port > 65_535) {
40
- fail(`Некоректний порт: ${value}`);
107
+ fail(m.badPort(value));
41
108
  }
42
109
  return port;
43
110
  }
@@ -45,7 +112,7 @@ function parsePort(value) {
45
112
  function requireValue(args, index, flag) {
46
113
  const value = args[index + 1];
47
114
  if (!value || value.startsWith("-")) {
48
- fail(`Опція ${flag} потребує значення.`);
115
+ fail(m.flagNeedsValue(flag));
49
116
  }
50
117
  return value;
51
118
  }
@@ -76,7 +143,7 @@ function parseArgs(args) {
76
143
  } else if (arg.startsWith("--hostname=")) {
77
144
  const value = arg.slice("--hostname=".length);
78
145
  if (!value) {
79
- fail("Опція --hostname потребує значення.");
146
+ fail(m.hostnameNeedsValue());
80
147
  }
81
148
  options.hostname = value;
82
149
  } else if (arg === "--tailscale") {
@@ -90,7 +157,7 @@ function parseArgs(args) {
90
157
  } else if (arg === "-h" || arg === "--help") {
91
158
  options.help = true;
92
159
  } else {
93
- fail(`Невідома опція: ${arg}`);
160
+ fail(m.unknownOption(arg));
94
161
  }
95
162
  }
96
163
 
@@ -112,7 +179,7 @@ function findPackageRoot(startDir) {
112
179
 
113
180
  const parentDir = dirname(currentDir);
114
181
  if (parentDir === currentDir) {
115
- fail("Не вдалося знайти package.json для agent-log-viewer.");
182
+ fail(m.noPackageJson());
116
183
  }
117
184
  currentDir = parentDir;
118
185
  }
@@ -125,8 +192,8 @@ function readPackageJson(packageRoot) {
125
192
  } catch (error) {
126
193
  fail(
127
194
  error instanceof Error
128
- ? `Не вдалося прочитати package.json: ${error.message}`
129
- : "Не вдалося прочитати package.json.",
195
+ ? m.readPackageJsonErr(error.message)
196
+ : m.readPackageJsonErrGeneric(),
130
197
  );
131
198
  }
132
199
  }
@@ -154,7 +221,7 @@ function resolveServer(packageRoot) {
154
221
 
155
222
  const nextBin = join(packageRoot, "node_modules", ".bin", "next");
156
223
  if (!existsSync(nextBin)) {
157
- fail("Не знайдено standalone server.js або локальний next.");
224
+ fail(m.noServer());
158
225
  }
159
226
 
160
227
  return {
@@ -204,7 +271,7 @@ function startServer(server, options, runtime, tailscaleProcessRef) {
204
271
  const text = chunk.toString("utf8");
205
272
  if (text.includes("EADDRINUSE")) {
206
273
  state.sawAddressInUse = true;
207
- console.error(`Порт ${options.port} зайнятий. Спробуйте: bunx agent-log-viewer --port ${options.port + 1}`);
274
+ console.error(m.portBusy(options.port));
208
275
  if (!child.killed) {
209
276
  child.kill("SIGTERM");
210
277
  }
@@ -215,7 +282,7 @@ function startServer(server, options, runtime, tailscaleProcessRef) {
215
282
  });
216
283
 
217
284
  child.on("error", (error) => {
218
- fail(`Не вдалося запустити сервер: ${error.message}`);
285
+ fail(m.serverStartFail(error.message));
219
286
  });
220
287
 
221
288
  child.on("exit", async (code, signal) => {
@@ -285,7 +352,7 @@ async function waitForReadiness(port) {
285
352
  await wait(READINESS_INTERVAL_MS);
286
353
  }
287
354
 
288
- throw new Error(`Сервер не відповів за ${READINESS_TIMEOUT_MS / 1000} секунд.`);
355
+ throw new Error(m.serverTimeout(READINESS_TIMEOUT_MS / 1000));
289
356
  }
290
357
 
291
358
  function localUrl(options) {
@@ -295,9 +362,9 @@ function localUrl(options) {
295
362
 
296
363
  function printBanner(version, options) {
297
364
  console.log(` ✳ Live Log Viewer v${version}`);
298
- console.log(` Відкрито: ${localUrl(options)}`);
299
- console.log(" Читає логи з ~/.claude/projects, ~/.codex/sessions.");
300
- console.log(" Ctrl+C — зупинити. --tailscale — доступ з телефона.");
365
+ console.log(m.bannerOpened(localUrl(options)));
366
+ console.log(m.bannerReads());
367
+ console.log(m.bannerStop());
301
368
  }
302
369
 
303
370
  async function printTailscaleBanner(runtime) {
@@ -314,8 +381,8 @@ async function printTailscaleBanner(runtime) {
314
381
  resolve();
315
382
  });
316
383
  });
317
- console.log(" Посилання містить ключ доступу — не пересилайте його стороннім.");
318
- console.log(" Після першого відкриття ключ зберігається у cookie на 30 днів.");
384
+ console.log(m.tsLinkWarn());
385
+ console.log(m.tsCookie());
319
386
  }
320
387
 
321
388
  function openBrowser(url) {
@@ -386,7 +453,7 @@ async function prepareRuntime(options) {
386
453
 
387
454
  const nonLoopbackBind = !isLoopbackHostname(options.hostname);
388
455
  if (nonLoopbackBind) {
389
- console.error("Увага: нелокальна адреса відкриває viewer для мережі, тому режим ключа доступу увімкнено примусово.");
456
+ console.error(m.nonLocalWarn());
390
457
  }
391
458
 
392
459
  if (options.tailscale) {
@@ -436,7 +503,7 @@ async function main() {
436
503
  }
437
504
 
438
505
  if (await portAlreadyResponds(options.port)) {
439
- console.error(`Порт ${options.port} зайнятий. Спробуйте: bunx agent-log-viewer --port ${options.port + 1}`);
506
+ console.error(m.portBusy(options.port));
440
507
  process.exit(1);
441
508
  }
442
509
 
@@ -453,7 +520,7 @@ async function main() {
453
520
  await waitForReadiness(options.port);
454
521
  } catch (error) {
455
522
  await stopAll(serverProcess, tailscaleProcessRef.current);
456
- fail(error instanceof Error ? error.message : "Сервер не готовий.");
523
+ fail(error instanceof Error ? error.message : m.serverNotReady());
457
524
  }
458
525
 
459
526
  if (