shadok-ai 0.1.148 → 0.1.149

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 (2) hide show
  1. package/README.md +147 -90
  2. package/package.json +1 -1
package/README.md CHANGED
@@ -3,7 +3,8 @@
3
3
  A **web cockpit that drives multiple real Claude Code sessions in parallel** —
4
4
  each channel is one `claude` process, on your Claude subscription (not the API).
5
5
  Pilot them from a browser **and** from **Telegram** (one topic = one agent),
6
- with git-worktree isolation, per-repo secrets, a diff panel, and quota gauges.
6
+ with git-worktree isolation, agent profiles, a secret vault, a diff panel, and
7
+ quota gauges.
7
8
 
8
9
  ## Quick start
9
10
 
@@ -15,25 +16,83 @@ Then open **http://localhost:3789**.
15
16
 
16
17
  **Prerequisites:** Node ≥ 20 and the [`claude`](https://claude.com/claude-code)
17
18
  CLI installed and signed in on the machine (shadok-ai drives your existing
18
- Claude Code, on your subscription).
19
+ Claude Code, on your subscription). `tmux` is optional but recommended — with
20
+ it, agents survive the server restarting.
19
21
 
20
- On the first run it asks once for an optional **Telegram bot token** (press Enter
21
- to skip; you can add it later). Flags: `--port <n>`, `--no-telegram`,
22
- `--version`.
22
+ On the first run it asks once for an optional **Telegram bot token** (press
23
+ Enter to skip; you can add it later from the web UI).
24
+
25
+ | Flag | Effect |
26
+ |---|---|
27
+ | `--port, -p <n>` | HTTP/WS port (default 3789; falls back to the next free one) |
28
+ | `--no-telegram` | web-only; don't prompt for or use a bot token |
29
+ | `--password <p>` | require this password to open the GUI (stored in config) |
30
+ | `--version, -v` · `--help, -h` | version / help |
31
+
32
+ ## What you get
33
+
34
+ - **Channels** — one `claude` process each, running in parallel. Renaming,
35
+ grouping, and closing all sync live between the browser and Telegram: it's
36
+ one list, server-owned, not two copies.
37
+ - **Worktree isolation** — spawned agents get their own git worktree and branch
38
+ by default, so parallel agents never collide. Work is never auto-discarded:
39
+ an empty worktree is reclaimed on close, anything with changes or commits
40
+ stays (and `Recover` reopens it).
41
+ - **Interactive dialogs** — the TUI's permission prompts and multiple-choice
42
+ questions become clickable buttons in the chat, and an inline keyboard in
43
+ Telegram.
44
+ - **Agent profiles** — a named bundle of role prompt, permission guardrails
45
+ (e.g. forbid `git commit`), model, and which secrets to inject. Applied at
46
+ spawn, remembered across resume.
47
+ - **Secret vault** — stored under `~/.shadok-ai`, never in your repo, injected
48
+ as env vars into the agents that need them.
49
+ - **Quota gauges + pace guard** — 5h and 7d subscription usage, with an
50
+ optional block when you're burning faster than the window elapses (any
51
+ message can force through).
52
+ - **Engine room** — the raw TUI screen, live, with clickable keys, for anything
53
+ the chat can't express.
54
+ - **Diff panel** — what an agent actually changed, against its base.
55
+ - **Self-update** — polls npm and can update and reload itself in place.
23
56
 
24
57
  ### Telegram (optional)
25
58
 
26
59
  Add your bot to a group with **Topics** enabled, make it an admin with *Manage
27
- topics* (and *Delete messages* for `/secret` scrubbing), then in the group:
60
+ topics* (and *Delete messages* so `/secret` can scrub values), then in the group:
28
61
 
29
- - `/setup` — bind this group as the board (one group per instance)
30
- - `/spawn <name>` — a new isolated agent in its own topic (also appears as a web tab)
31
- - `/secrets` · `/secret KEY value` · `/unsecret KEY` — per-repo env secrets
32
- - `/update` — fetch `@latest` and respawn (self-update)
33
- - `/list` · `/new` · `/end`
34
-
35
- Web tabs and Telegram topics are **one list** — spawn, rename, and close sync
36
- both ways, live.
62
+ | Command | |
63
+ |---|---|
64
+ | `/setup` | bind this group as the board (one group per instance) |
65
+ | `/spawn [profile] <name>` | new isolated agent in its own topic (also a web tab) |
66
+ | `/stop` (alias `/esc`) | interrupt the current turn — does **not** end the session |
67
+ | `/new` · `/end` | reset · kill the session |
68
+ | `/restart` | respawn the agent in place (e.g. to pick up new secrets) |
69
+ | `/profiles` · `/list` | list profiles · list bindings |
70
+ | `/secrets` · `/secret KEY value` · `/unsecret KEY` | the secret vault |
71
+ | `/update` | fetch `@latest` and respawn |
72
+
73
+ Creating a topic by hand also spawns an agent. Anything that isn't a command is
74
+ sent to that topic's agent as a prompt — including photos and files, which are
75
+ downloaded and handed to Claude Code.
76
+
77
+ ### Configuration
78
+
79
+ Config lives in `~/.shadok-ai/config.json` (mode 600) and is **authoritative
80
+ over the environment once set** from the GUI. The Telegram token, allowed
81
+ chats, and the bridge on/off switch are **per launch directory** — running the
82
+ server from another repo gives you a different cockpit and a different bot.
83
+
84
+ | Env var | |
85
+ |---|---|
86
+ | `PORT` | HTTP/WS port |
87
+ | `SHADOK_GUI_PASSWORD` | require a password for the GUI |
88
+ | `SHADOK_TMUX=0` | force the node-pty transport instead of tmux |
89
+ | `SHADOK_IDLE_MIN` | minutes with no client before a session is reclaimed (60) |
90
+ | `SHADOK_PERMISSION_MODE` | mode new agents start in (default `acceptEdits`) |
91
+ | `SHADOK_AUTOUPDATE` | fallback only — the GUI setting wins once used |
92
+ | `SHADOK_PILOT_PROMPT=0` | don't inject the cockpit system prompt |
93
+ | `SHADOK_RESUME_SUMMARY=1` | don't auto-answer the resume-from-summary prompt |
94
+ | `TELEGRAM_BOT_TOKEN` · `TELEGRAM_ALLOWED_CHATS` | override the stored config |
95
+ | `CLAUDE_CODE_OAUTH_TOKEN` | only for the usage gauges; `claude` itself uses the keychain |
37
96
 
38
97
  ---
39
98
 
@@ -42,7 +101,8 @@ both ways, live.
42
101
 
43
102
  ## How it works
44
103
 
45
- Drives the **Claude Code TUI** (the interactive `claude` CLI) through a pseudo-terminal.
104
+ Drives the **Claude Code TUI** (the interactive `claude` CLI) through a real
105
+ terminal, and reads the answers from Claude Code's own transcript.
46
106
 
47
107
  > ⚠️ The Claude Code TUI is not a stable API: a CLI update can break the
48
108
  > detection heuristics (`❯`, `⏺`, `esc to interrupt` markers).
@@ -50,31 +110,50 @@ Drives the **Claude Code TUI** (the interactive `claude` CLI) through a pseudo-t
50
110
  > [Claude Agent SDK](https://www.npmjs.com/package/@anthropic-ai/claude-agent-sdk)
51
111
  > or `claude -p --output-format stream-json`.
52
112
 
53
- - **node-pty** spawns `claude` inside a real pseudo-terminal (the TUI believes it talks to a human);
54
- - **@xterm/headless** replays the ANSI stream into a virtual screen: we read
55
- the screen as it would be displayed instead of parsing escape sequences;
56
- - the **responses to terminal queries** (cursor position `\x1b[?6n`,
57
- identification `\x1b[c`…) generated by xterm are forwarded back to the
58
- PTY — without this, the TUI ignores every keystroke;
59
- - prompts are sent via **bracketed paste** (`\x1b[200~…\x1b[201~`) with
60
- retries, because the TUI flushes stdin received during its initialization;
61
- - the "Claude is done" state is detected heuristically: no
62
- `esc to interrupt` marker + screen stable for N ms.
113
+ Two transports, same interface:
114
+
115
+ - **tmux (default when installed)** — `claude` runs in a detached tmux session,
116
+ so it survives the server restarting or crashing and is reattached on the
117
+ next start. tmux answers terminal queries itself, which removes a whole class
118
+ of PTY hacks. It does not survive a machine reboot.
119
+ - **node-pty** — `claude` runs as a child of the server inside a pseudo-terminal
120
+ (the TUI believes it talks to a human), with **@xterm/headless** replaying the
121
+ ANSI stream into a virtual screen we can read. Terminal query responses
122
+ (cursor position `\x1b[?6n`, identification `\x1b[c`…) must be forwarded back
123
+ to the PTY, or the TUI ignores every keystroke. Dies with the server.
124
+
125
+ In both cases prompts are sent via **bracketed paste** with retries (the TUI
126
+ flushes stdin received during initialization), and "Claude is done" is detected
127
+ heuristically: no `esc to interrupt` marker + screen stable for N ms.
128
+
129
+ **Responses are not scraped from the screen.** Claude Code writes every turn to
130
+ a `.jsonl` transcript; that file is the source of truth for chat content, which
131
+ is what makes long answers reliable. The screen is used only for control —
132
+ submitting, detecting the end of a turn, reading dialogs, and mirroring the raw
133
+ TUI in the engine room.
63
134
 
64
- ## Install
135
+ ## Build & test
65
136
 
66
137
  ```bash
67
138
  npm install
68
- npm run build
139
+ npm run build # tsc → dist/
140
+ npm test
141
+ npm run web # run the server directly (no supervisor, no auto-update)
69
142
  ```
70
143
 
71
144
  The `postinstall` script fixes the executable bit of node-pty's
72
145
  `spawn-helper` (a known npm prebuilds bug on macOS).
73
146
 
74
- ## CLI
147
+ Note that `npx shadok-ai` does **not** run your working tree: it runs a
148
+ supervisor that manages an auto-updating npm-installed copy. To test a local
149
+ build, see "Running YOUR build" in [`CLAUDE.md`](CLAUDE.md).
150
+
151
+ ## One-shot CLI
152
+
153
+ Separate from the cockpit: run a single prompt and print the answer.
75
154
 
76
155
  ```bash
77
- node dist/cli.js [options] "<prompt>"
156
+ shadok-ai-run [options] "<prompt>" # or: node dist/cli.js …
78
157
 
79
158
  --cwd <dir> working directory of the claude session
80
159
  --continue, -c resume the latest session of this directory
@@ -84,94 +163,67 @@ node dist/cli.js [options] "<prompt>"
84
163
  --timeout <sec> max wait for the response (default 600)
85
164
  ```
86
165
 
87
- Without `--continue`/`--resume`, every run starts a **new** session.
88
- The session id is printed at the end of the run (found in
166
+ Without `--continue`/`--resume`, every run starts a **new** session. The
167
+ session id is printed at the end of the run (found in
89
168
  `~/.claude/projects/<encoded cwd>/`, like any Claude Code session):
90
169
 
91
170
  ```bash
92
- node dist/cli.js --cwd ~/my-project "Explain this project's structure"
171
+ shadok-ai-run --cwd ~/my-project "Explain this project's structure"
93
172
  # ▶ session: 5fe046dd-…
94
173
 
95
- node dist/cli.js --cwd ~/my-project --resume 5fe046dd-… "Now refactor it"
96
- node dist/cli.js --cwd ~/my-project -c "Continue on the latest session"
174
+ shadok-ai-run --cwd ~/my-project --resume 5fe046dd-… "Now refactor it"
175
+ shadok-ai-run --cwd ~/my-project -c "Continue on the latest session"
97
176
  ```
98
177
 
99
- ## Web interface
100
-
101
- ```bash
102
- npm run web # http://localhost:3789 (PORT=… to change)
103
- ```
104
-
105
- A chat in the browser driving persistent TUI sessions, in parallel through
106
- channels:
107
-
108
- - **channels** (left column): "+ new channel" opens a new independent
109
- claude session (one WebSocket connection each). Each channel's LED shows
110
- its state (green: ready, amber: responding, red: ended); the × closes the
111
- channel and detaches from its session; double-click a name to rename it
112
- (the name is remembered by session id and reapplied on resume). The
113
- header, composer and Terminal view follow the active channel. Channels
114
- are remembered (localStorage): on the next page load each one is restored
115
- automatically — session resumed via `--resume` and history replayed.
116
- Closing a channel (×) removes it from the remembered list;
117
- - **interactive dialogs**: when the TUI shows a choice (multiple-choice
118
- question, permission prompt…), the options appear as clickable buttons in
119
- the chat. Single select: one click validates. Multi-select (`[ ]`
120
- checkboxes in the TUI): clicks toggle (the displayed state is re-read
121
- from the TUI screen), then "Submit selection" confirms (Tab → Submit page
122
- → Enter). The composer is locked while a choice is pending. The
123
- "Type something" option becomes a free-form text input inside the dialog
124
- bubble;
125
- - **history on resume**: when resuming a session (`--continue` or
126
- `--resume`), the transcript is re-read from the session's `.jsonl` and
127
- replayed in the chat (last 100 turns, dimmed). The markdown of the
128
- responses is rendered as HTML (tables, code, lists) via `marked`, served
129
- locally;
130
- - **"engine room"** (Terminal button): the raw TUI screen live, with
131
- clickable keys (Esc, Enter, arrows, 1/2/3, y/n…) to answer permission
132
- dialogs, and "Resync chat" to resynchronize the chat after a manual
133
- intervention.
134
-
135
- ### WebSocket protocol (to replace the interface)
178
+ ## WebSocket protocol (to replace the interface)
136
179
 
137
180
  The interface is a plain static page: all the intelligence lives server-side.
138
- Any client (another front-end, a Slack bot, a script…) can replace it by
139
- speaking this JSON protocol on `ws://…/ws`.
181
+ Any client (another front-end, a bot, a script…) can replace it by speaking
182
+ this JSON protocol on `ws://…/ws`. The built-in Telegram bridge *is* such a
183
+ client — it connects to the same server over loopback.
140
184
 
141
- **Shared sessions**: the server keeps a single claude process per session
142
- id. If several clients (tabs, browsers, interfaces) "start" the same id,
143
- they attach to the same process and all receive the same events — other
144
- clients' prompts (`prompt-echo`), answers, dialogs, screen. Closing a
145
- connection detaches the client; the process is only stopped when the last
146
- client detaches (session stays resumable) or on an explicit `stop` (ends it
147
- for everyone).
185
+ **Shared sessions**: the server keeps a single claude process per session id.
186
+ If several clients (tabs, browsers, interfaces) `start` the same id, they
187
+ attach to the same process and all receive the same events — other clients'
188
+ prompts (`prompt-echo`), answers, dialogs, screen. Closing a connection
189
+ detaches the client; the session is reclaimed after `SHADOK_IDLE_MIN` with no
190
+ client, or immediately on an explicit `stop` (which ends it for everyone).
148
191
 
149
192
  **client → server**
150
193
 
151
194
  | Message | Purpose |
152
195
  |---|---|
153
- | `{type:"start", cwd?, resume?, continue?}` | starts or attaches to the session (once per connection) |
154
- | `{type:"prompt", text}` | sends a prompt |
196
+ | `{type:"start", cwd?, resume?, continue?, worktree?, branch?, repo?, profile?}` | starts or attaches to the session (once per connection) |
197
+ | `{type:"prompt", text, force?}` | sends a prompt (`force` bypasses the pace guard) |
155
198
  | `{type:"choose", n}` | single-select dialog: picks and validates option n |
156
- | `{type:"toggle", n}` | multi-select dialog: toggles option n |
157
- | `{type:"confirm"}` | multi-select dialog: submits the selection |
199
+ | `{type:"toggle", n}` / `{type:"confirm"}` | multi-select: toggles option n / submits |
158
200
  | `{type:"freetext", n, text}` | "Type something" option: sends a free-form answer |
159
201
  | `{type:"key", key}` | raw keystroke (`enter`, `escape`, `up`, `down`, `tab`, `ctrl-c`, or a single character) |
160
202
  | `{type:"settle"}` | after a manual intervention: waits for the turn to finish |
161
- | `{type:"stop"}` | closes the session cleanly (/exit) for all clients |
203
+ | `{type:"restart"}` | respawns the agent in place (picks up new secrets/profile) |
204
+ | `{type:"stop", sessionId?}` | ends the session for all clients; `sessionId` targets another channel (zombie cleanup) |
162
205
 
163
206
  **server → client**
164
207
 
165
208
  | Message | Purpose |
166
209
  |---|---|
167
210
  | `{type:"ready", sessionId, cwd}` | session started (or attached) |
168
- | `{type:"working"}` | turn in progress |
211
+ | `{type:"working"}` / `{type:"turn-done", sessionId}` | turn started / finished |
212
+ | `{type:"stream-text", text}` | a complete assistant text block, from the transcript |
213
+ | `{type:"stream-tool", id, name, summary}` / `{type:"stream-result", …}` | tool call / tool result |
214
+ | `{type:"tokens", tokens}` / `{type:"context", pct}` | token usage / context fill |
169
215
  | `{type:"prompt-echo", text}` | prompt sent by another client of the session |
170
- | `{type:"answer", text, sessionId}` | response extracted from the transcript |
171
216
  | `{type:"dialog", question, options:[{n,label,hint,checked?}], multi}` | choice pending |
172
- | `{type:"history", turns:[{role,text}]}` | transcript replayed when resuming/attaching |
217
+ | `{type:"history", turns:[…]}` | transcript replayed when resuming/attaching |
173
218
  | `{type:"screen", text, working}` | rendered TUI screen (whenever it changes) |
174
- | `{type:"error", message}` / `{type:"exited", code}` / `{type:"stopped"}` | errors and termination |
219
+ | `{type:"pace-blocked"}` / `{type:"pace-hold"}` / `{type:"pace-resumed"}` | quota guardrail |
220
+ | `{type:"auto-retry"}` / `-cancelled` / `-gave-up` | transient API error being retried |
221
+ | `{type:"version", …}` / `{type:"server-reload", version}` | update available / server updated, reload |
222
+ | `{type:"gone"}` / `{type:"error", message}` / `{type:"exited", code}` / `{type:"stopped"}` | session lost, errors, termination |
223
+
224
+ HTTP endpoints (same auth): `/usage`, `/live`, `/sessions`, `/recover`,
225
+ `/diff`, `/channels`, `/groups`, `/profiles`, `/secrets`, `/telegram`,
226
+ `/defaults`, `/version`, `/autoupdate`, `/permission-mode`.
175
227
 
176
228
  ## Library
177
229
 
@@ -205,13 +257,18 @@ Main API:
205
257
  | `isWorking()` | true while Claude is working |
206
258
  | `onData(cb)` / `onExit(cb)` | raw ANSI stream (mirror mode) / process exit |
207
259
 
208
- `examples/two-turns.mjs` shows a two-turn conversation;
260
+ `TmuxPilot` exposes the same surface and additionally survives a server
261
+ restart. `examples/two-turns.mjs` shows a two-turn conversation;
209
262
  `debug/probe.mjs` logs the terminal sequences exchanged (useful when a CLI
210
263
  update breaks the detection).
211
264
 
212
265
  ## Known limits
213
266
 
214
267
  - Every session launched consumes your Claude quota like a normal session.
268
+ - Profile guardrails are **soft**: agents run as the same OS user. It prevents
269
+ misfires, it is not a sandbox.
270
+ - Agent worktrees are branched at spawn and never rebased, so a long-running
271
+ agent drifts from a moving main branch.
215
272
  - Dialogs the chat cannot handle in one click are managed through the
216
273
  engine room (`waitFor()` + `press()` in library mode).
217
274
  - The TUI runs in the alternate screen: `fullBuffer()` ≈ visible screen;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shadok-ai",
3
- "version": "0.1.148",
3
+ "version": "0.1.149",
4
4
  "main": "dist/session.js",
5
5
  "scripts": {
6
6
  "test": "node --import tsx --test test/*.ts .claude/skills/shadok-ai-agents/test/*.test.mjs",