subrouter-cli 0.0.0-stage → 0.1.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 (62) hide show
  1. package/CLAUDE.md +51 -0
  2. package/README.md +397 -2
  3. package/config.example.json +9 -0
  4. package/package.json +21 -4
  5. package/scripts/sse-harness.ts +252 -0
  6. package/scripts/sub-wrapper.sh +11 -0
  7. package/src/adapters/anthropic.ts +311 -0
  8. package/src/adapters/openai.ts +227 -0
  9. package/src/approval.ts +21 -0
  10. package/src/chatviewport.ts +123 -0
  11. package/src/client.ts +398 -0
  12. package/src/clipboard.ts +62 -0
  13. package/src/commandpolicy.ts +272 -0
  14. package/src/commands.ts +18 -0
  15. package/src/config.ts +107 -0
  16. package/src/effort.ts +18 -0
  17. package/src/images.ts +77 -0
  18. package/src/index.ts +248 -0
  19. package/src/lineinput.ts +726 -0
  20. package/src/loop.ts +201 -0
  21. package/src/markdown.ts +244 -0
  22. package/src/repl.ts +700 -0
  23. package/src/sessions.ts +58 -0
  24. package/src/sse.ts +64 -0
  25. package/src/terminal.ts +228 -0
  26. package/src/token.ts +36 -0
  27. package/src/toolpreview.ts +54 -0
  28. package/src/tools.ts +790 -0
  29. package/src/types.ts +73 -0
  30. package/src/ui.ts +813 -0
  31. package/src/usage.ts +186 -0
  32. package/test/absolute-tools.test.ts +296 -0
  33. package/test/absolute-ui.test.ts +153 -0
  34. package/test/adapters.test.ts +205 -0
  35. package/test/anthropic.test.ts +246 -0
  36. package/test/auto-ui.test.ts +106 -0
  37. package/test/chatviewport.test.ts +49 -0
  38. package/test/client.test.ts +327 -0
  39. package/test/clipboard.test.ts +63 -0
  40. package/test/command-input.test.ts +183 -0
  41. package/test/command-tools.test.ts +141 -0
  42. package/test/commandpolicy.test.ts +252 -0
  43. package/test/disk-tools.test.ts +220 -0
  44. package/test/effort.test.ts +123 -0
  45. package/test/fixtures/openai-tools.sse +84 -0
  46. package/test/image-adapters.test.ts +78 -0
  47. package/test/image-ui.test.ts +151 -0
  48. package/test/images.test.ts +60 -0
  49. package/test/loop.test.ts +387 -0
  50. package/test/m5.test.ts +127 -0
  51. package/test/markdown.test.ts +201 -0
  52. package/test/repl-ui.test.ts +293 -0
  53. package/test/sse.test.ts +74 -0
  54. package/test/steering-ui.test.ts +182 -0
  55. package/test/steering.test.ts +68 -0
  56. package/test/terminal-ui.test.ts +144 -0
  57. package/test/terminal.test.ts +229 -0
  58. package/test/toolpreview.test.ts +51 -0
  59. package/test/tools.test.ts +227 -0
  60. package/test/ui.test.ts +635 -0
  61. package/test/usage-footer.test.ts +180 -0
  62. package/tsconfig.json +17 -0
package/CLAUDE.md ADDED
@@ -0,0 +1,51 @@
1
+ # subrouter-cli
2
+
3
+ `sub` — a personal agentic coding CLI (Claude Code / pi style) backed by the subrouter
4
+ AI router (https://router.eva.pink). TypeScript on Node 24+, run straight from source
5
+ via Node's native type stripping — no build step, no runtime dependencies.
6
+
7
+ ## Running
8
+
9
+ ```bash
10
+ npm install # devDependencies only (typescript + @types/node, for typechecking)
11
+ npm link # puts `sub` on your PATH
12
+ sub --check # connectivity + catalog diagnostic
13
+ node scripts/sse-harness.ts --model <id> --dialect openai|anthropic|auto [--tools] [--save test/fixtures/x.sse]
14
+ ```
15
+
16
+ ## Constraints
17
+
18
+ - **Zero runtime dependencies.** Only Node built-ins in `src/` and `scripts/`.
19
+ - **Never print, log, or commit an API key — not even a masked form of it.** Real keys
20
+ live in `~/.config/subrouter-cli/config.json` (chmod 600) or the `SUBROUTER_API_KEY`
21
+ env var. `config.example.json` is the safe, committed template.
22
+ - **Never log message content** to stdout/stderr; session transcripts go only to
23
+ `~/.config/subrouter-cli/sessions/`.
24
+ - **macOS only (v1).** Raw-mode TTY handling and ANSI output assume a macOS Terminal.
25
+ - **Terminal authorization and absolute access.** The user explicitly requested
26
+ guarded auto and a separate absolute permissions mode with whole-drive access.
27
+ `run_command` uses fixed `/bin/zsh -f -c` via Node built-ins, an existing cwd,
28
+ bounded output/time, ignored stdin and process-group cancellation. Commands require
29
+ separate explicit yes, except recognized routine tasks in session-only guarded
30
+ auto (`--auto`, `/mode auto`) or arbitrary commands in acknowledged absolute mode
31
+ (`--absolute`, `/mode absolute`). Destructive, elevated, unknown or opaque commands
32
+ never qualify in guarded auto. Absolute requires an interactive warning and typed
33
+ `ABSOLUTE` before activation, is never persisted, and bypasses per-call approvals
34
+ and the command classifier. It also lifts file-tool project/.git/recovery path
35
+ guards and allows absolute, home, parent and external paths; other modes retain
36
+ confinement. This grants current-account access, not root or a macOS permission
37
+ bypass. Tool-format/size/collision/tree safeguards, recoverable removal, known-key
38
+ protection and cancellation remain. Absolute capabilities are host-only and scoped
39
+ per execution with live generation checks; queued grants cannot survive revocation.
40
+ File approve-all and legacy `--auto-approve` remain file-only. Command grants use
41
+ exact snapshots; guarded auto also revalidates policy at execution. Tests/builds
42
+ execute trusted project code: a classifier is not proof of safety. Commands are
43
+ **not sandboxed** and may access the
44
+ wider machine/network. Do not claim cwd confinement limits command effects or
45
+ guarantee cleanup of descendants that detach into another process group. Do not
46
+ expose persistent background jobs or interactive command input. Use a minimal
47
+ environment allowlist and sanitize/redact known router credentials before output
48
+ reaches model history, transcripts or UI. No raw command-output progress cards.
49
+ - **Clipboard helper stays user-triggered.** Image paste uses
50
+ `execFile('/usr/bin/osascript', …)` with fixed AppKit/JXA code, no shell, and no
51
+ clipboard-derived code or arguments. No native builds or third-party dependencies.
package/README.md CHANGED
@@ -1,3 +1,398 @@
1
- # Temporary Holding Version
1
+ # sub
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ A personal agentic coding CLI — Claude Code / pi style — backed by the
4
+ [subrouter](https://router.eva.pink) AI router. Streaming chat REPL plus an agent loop
5
+ with **filesystem tools** for creating and organizing projects, reading/editing code,
6
+ searching, copying assets, and recoverable removal, plus **guarded terminal commands**
7
+ for builds, tests, and other requested work. Opt-in auto mode handles recognized routine
8
+ tasks without prompts; destructive or unknown commands still ask in guarded auto.
9
+ A separate warned **absolute mode** permits whole-drive file access and arbitrary
10
+ commands without further approvals.
11
+
12
+ TypeScript on Node ≥ 24, run straight from source via Node's native type stripping:
13
+ **zero build step, zero runtime dependencies** (only `typescript` + `@types/node` as
14
+ devDependencies for typechecking). macOS only.
15
+
16
+ ## Setup
17
+
18
+ ```bash
19
+ npm install # devDependencies only (typescript + @types/node)
20
+ npm run typecheck # tsc --noEmit
21
+ npm test # node --test — the full suite is offline
22
+
23
+ # put `sub` on your PATH (no root needed):
24
+ mkdir -p ~/.local/bin
25
+ ln -s "$PWD/scripts/sub-wrapper.sh" ~/.local/bin/sub # or copy it; chmod +x
26
+ ```
27
+
28
+ The API key comes from `SUBROUTER_API_KEY` or `~/.config/subrouter-cli/config.json`
29
+ (see `config.example.json`). The config file is written chmod 600; keys are never
30
+ printed, logged, or committed — not even masked.
31
+
32
+ ```bash
33
+ sub --check # connectivity + model catalog diagnostic
34
+ sub # the REPL
35
+ sub -p "explain this repo" # one-shot, non-interactive
36
+ sub --absolute # entire drive + arbitrary commands; warning and typed ABSOLUTE required
37
+ sub --auto # guarded auto for files + recognized project tasks
38
+ sub --auto-approve # file-only auto; every terminal command still asks
39
+ sub --model <id> --debug
40
+ ```
41
+
42
+ ## The REPL
43
+
44
+ - **Terminal UI:** chat scrolls above a bottom-anchored panel, with the selected model
45
+ on its top divider, the violet `❯` input between dividers, and permission/activity
46
+ status below. The pi/Claude Code-inspired layout uses Subrouter's periwinkle-blue
47
+ accent (#8fa7ff) for branding, models, menus, and availability, with violet (#a674ff)
48
+ for effort and headings. Green ready/success states and amber/red warnings remain,
49
+ instead of
50
+ dimming everything. Command menus and views open immediately **below the input**, outside
51
+ the scrolling conversation. While a command is open, the normal status/usage rows
52
+ are hidden so only its controls remain. Reasoning text is hidden; answers and
53
+ in-flow tool activity cards stay in chat. The compact header shows only the
54
+ working directory. Pipes and very small terminals keep a simple layout; `--no-color`
55
+ disables styling and animation. Restart `sub` after source changes — existing sessions
56
+ do not hot-reload.
57
+ - **Panel usage:** the latest turn's input/output tokens and the selected provider's
58
+ available pool percentages, refreshed every 30s and after turns. Model changes switch
59
+ the provider filter immediately. A separate account row shows router-reported RPM,
60
+ daily token usage/limit, and weekly spend/limit. These are shared account quotas, not
61
+ guessed provider subscription caps. Missing values remain unknown; explicit null limits
62
+ show unlimited. Usage fetch failures never block chat.
63
+ - **Dynamic slash commands:** type `/` to open a live filtered palette below the input.
64
+ ↑/↓ selects, Tab completes, Enter runs, and Esc dismisses. `/model` and `/models`
65
+ open the model picker; `/model <id>` still sets
66
+ directly and persists the default. `/usage` opens a compact selected-provider report:
67
+ each pool window appears once with availability/reset time, followed by shared account
68
+ limits. Large token totals use K/M/B abbreviations. ↑/↓ scrolls, `r` bypasses the 30s
69
+ cache to refresh, Enter/Esc closes. `/mode` picks `ask`, `files`, guarded `auto`,
70
+ or warned `absolute`; `/mode auto` switches directly, `/mode absolute` opens the
71
+ acknowledgement, and `/mode ask` returns to prompting. Modes are session-only.
72
+ `/clear` resets the visible conversation and starts
73
+ a fresh transcript; `/exit` quits. Command notices and controls stay below the
74
+ input; agent answers and tool cards stay in chat. The absolute warning disappears
75
+ after its acknowledgement closes. Pipes and tiny terminals use plain command output.
76
+ - **Anchored scrolling:** mouse wheel and Page Up/Down scroll conversation history while
77
+ the input stays fixed, even during a streaming answer. New output does not pull you
78
+ away from the history you are reading. Scroll down to return live; submitting a new
79
+ turn also returns live. Open command panels/pickers receive these keys instead.
80
+ Display history retains the latest 10,000 logical lines; full transcripts stay on disk.
81
+ Mouse reporting is disabled on exit or when the terminal becomes too small.
82
+ - **Effort:** `/effort` opens a picker for `auto`, `low`, `medium`, `high`, `xhigh`, `max`;
83
+ `/effort high` sets directly. The choice is shown in the bar and saved as
84
+ `reasoning_effort`. `sub --effort high` overrides it for this run, including `-p`.
85
+ `auto` omits an explicit API setting. Chat Completions uses `reasoning_effort`; Messages
86
+ uses thinking budgets (2,048 / 4,096 / 8,192 / 16,384 / 32,000 tokens), raising total
87
+ `max_tokens` when necessary to leave at least 4,096 tokens for the answer. Supported
88
+ levels depend on the model/router; a rejection is reported, not silently downgraded.
89
+ Signed thinking blocks are retained for tool continuations, never shown in chat.
90
+ - **Live agent cards:** the thinking indicator updates in place in the chat area, above
91
+ the composer, not in the bottom status strip. It is transient (not transcript/history
92
+ content), disappears when the answer starts, and stays hidden while viewing older
93
+ chat. Hidden model reasoning remains hidden. Tool activity updates a card **in the
94
+ conversation**, rather than pinning it to the composer; file writes/edits show pending
95
+ code as arguments stream, then update that same card to success/denied/error. Added
96
+ and removed lines are green/red, without language syntax highlighting. Completed
97
+ cards remain in chat history, and later replies follow them. Previews do not execute
98
+ changes: writes follow the selected permission mode.
99
+ - **Image attachments:** **Ctrl+V** reads an image from the macOS clipboard and queues
100
+ it for your next prompt; your typed draft stays intact. Copy an image, or capture a
101
+ screenshot directly to the clipboard with **Ctrl+Cmd+Shift+4**, then Ctrl+V in `sub`.
102
+ **Cmd+V** still handles ordinary terminal text paste. An empty/text-only clipboard
103
+ produces a notice, not a message to the agent. Clipboard access occurs only on this
104
+ explicit shortcut, never in the background or via agent tools.
105
+ Pending filenames appear in the footer. Type your question and Enter, or Enter alone
106
+ to send an image-only turn. **Ctrl+X** removes pending attachments; `/clear` removes
107
+ both queued attachments and images already in conversation history. Images stay in
108
+ history across tool rounds and follow-up questions. Failed requests restore pending
109
+ attachments for retry. Use an **image-capable model**: router/model support varies,
110
+ and unsupported images are reported rather than silently dropped.
111
+ - Up to 4 images per prompt, 10 MiB each, 20 MiB total. Clipboard PNG is preserved;
112
+ screenshot TIFF is converted to PNG using AppKit. No resizing or HEIC conversion.
113
+ TIFF input is bounded to 40 MiB / 40 million pixels before PNG conversion.
114
+ - `sub --image ./shot.png -p "Explain this screenshot"` works in one-shot mode;
115
+ repeat `--image` for multiple files. Without `-p`, images are queued in the REPL.
116
+ Local files support PNG, JPEG, WebP, GIF, quoted paths, `~/`, and absolute paths.
117
+ - No image slash commands or `/help`: type `/` for the remaining command palette.
118
+ Clipboard images are local snapshots; nothing is uploaded until you submit a prompt.
119
+ Remote image URLs and Finder file-copy clipboard references are not supported.
120
+ - Clipboard access uses macOS's built-in `/usr/bin/osascript` with fixed AppKit/JXA
121
+ code via `execFile` (no shell, packages, native compilation, or temporary image files).
122
+ Reads time out after 10 seconds and Ctrl+C cancels; failed/cancelled reads keep the
123
+ queue intact. Shortcuts are disabled during turns, permission prompts, model pickers
124
+ and usage panels.
125
+ - UI and Markdown transcripts show filenames/metadata only. Private JSONL transcripts
126
+ (mode 0600) retain the encoded image bytes, so they can be much larger and include
127
+ private image content. No clipboard payload is printed to the terminal.
128
+ - Context warnings use exact Messages token counting when available, otherwise a rough
129
+ 2,048-token allowance per image; actual image tokens vary by model and resolution.
130
+ - **Live typing and steering:** the composer stays editable while the AI streams or
131
+ runs tools. **Enter** queues your text as steering; muted grey message rows above
132
+ the composer show what's pending, with the count in the existing footer. The latest
133
+ three messages are visible (earlier ones are summarized); long rows use an ellipsis.
134
+ Smaller terminals reduce this preview to preserve chat and the anchored composer.
135
+ Messages are delivered in order at the next safe model boundary,
136
+ after all results from an in-flight tool batch. A queued message received during
137
+ a final answer starts another response in the same turn. It does not interrupt a
138
+ running request or undo tools already executing — **Ctrl+C** stops the turn.
139
+ Pending previews update in place and disappear when sent. Applied messages use the
140
+ ordinary user-message styling, with no “steer:” prefix, and are included in the same
141
+ active workflow/transcript. Delivering them does not stop or restart the agent or a
142
+ running terminal command. A provider request already in flight cannot accept new
143
+ input retroactively; the next continuation receives the messages after tool results.
144
+ Pending text is display-only.
145
+ Unsubmitted drafts survive turn completion and temporarily yield to permission
146
+ questions, then return afterward. Permission answers are not steering. Ctrl+C
147
+ during approval cancels rather than approving the write. Unapplied steering on
148
+ cancellation, error or the tool-round cap returns to the editor for an explicit
149
+ retry; multiple restored messages are joined in order. Slash commands work during
150
+ a turn: `/usage`, `/model`, `/models`, `/effort`, and `/mode` open their usual controls while
151
+ the AI continues. Model/effort changes apply to the **next turn**; current requests
152
+ and tool continuations retain their original settings, and usage/transcripts remain
153
+ attributed to the active turn's model.
154
+ Cancelling a command does not cancel the agent. Permission questions take priority
155
+ over open command controls. `/clear` cancels and awaits the active turn before
156
+ resetting history/queued steering; `/exit` stops both. Clipboard shortcuts remain
157
+ idle-only.
158
+ - **Agent loop:** up to 25 tool rounds per turn (`max_tool_rounds` in config).
159
+ File and terminal approvals appear in a bordered panel above the composer with a
160
+ separate choice row, not as text in the normal input bar. File changes accept
161
+ `y` = once, `a` = all file changes (this session), `n` = deny.
162
+ `--auto-approve` or `"auto_approve": true` skips **file-write** prompts only.
163
+ `/mode auto` or `--auto` also authorizes recognized routine terminal tasks.
164
+ Outside acknowledged absolute mode, all other terminal commands show the full
165
+ command/cwd/timeout and require their own explicit `y`/`yes`; blank, `a`/`all`, EOF
166
+ and cancellation do not authorize them. Absolute mode skips file/command prompts
167
+ after its typed warning acknowledgement. `/mode ask` revokes automatic grants;
168
+ queued auto/absolute commands and absolute file mutations cannot start after
169
+ revocation. Already-running operations are not interrupted.
170
+ - **Keys:** Enter submits, arrows walk history, Ctrl+C interrupts (aborts a streaming
171
+ turn), Ctrl+D exits. Multi-line bracketed pastes submit as one turn. The model picker
172
+ navigates with ↑/↓ (or `j`/`k`), confirms with Enter, cancels with Esc or Ctrl+C.
173
+ - **Markdown:** answers render as markdown — `**bold**`, `` `code` ``, fenced code
174
+ blocks (periwinkle blue), `#` headings, list markers, and `[links](url)` (URL dropped). Italic is
175
+ intentionally unsupported (`*`/`_` collide with identifiers). Transcripts keep the raw
176
+ markdown; styling is display-only. Partial prose, bold/code spans, and code-block
177
+ content appear as chunks arrive, without waiting for a newline or Ctrl+C. Only
178
+ ambiguous Markdown prefixes/links use bounded lookahead. Streamed replies discard
179
+ oversized leading/trailing blank gaps and use one blank line between prose paragraphs;
180
+ code-block whitespace is preserved.
181
+ - **Sessions:** every turn appends to `~/.config/subrouter-cli/sessions/` — a
182
+ markdown log plus a JSONL transcript (shaped for a future `--resume`).
183
+ - Model IDs auto-route to the right wire dialect: `anthropic/*` speaks the Messages
184
+ API; everything else speaks Chat Completions. Bare `claude-*` and
185
+ `gpt-*`/`grok-*`/`deepseek-*` also work; other bare IDs ask for the full
186
+ provider-prefixed ID from `/models`.
187
+
188
+ ## Tool policy
189
+
190
+ Tools are available in the interactive agent REPL. Launch `sub` from the folder you
191
+ want it to work in. In `ask`, `files`, and guarded `auto`, **filesystem-tool paths**
192
+ are relative to that folder and confined there: absolute paths, `~`, parent-directory
193
+ escapes, and symlink escapes are refused. In acknowledged `absolute`, tools can use
194
+ absolute paths, `~/`, parent paths and external symlink parents across the drive,
195
+ wherever your current macOS account has permission. Terminal commands are separately
196
+ authorized (explicitly, by guarded auto, or by acknowledged absolute mode) and are
197
+ **not sandboxed**. Their starting directory is validated, but their effects can extend
198
+ to other files on your PC and the network. One-shot `-p` is still chat-only.
199
+
200
+ | Tool | What it does | Approval |
201
+ | --- | --- | --- |
202
+ | `read_file` | Read a text file, up to 256 KiB | No |
203
+ | `read_files` | Read 1–20 text files with per-file errors and a shared 256 KiB content budget | No |
204
+ | `file_info` | Inspect type, size, permissions and modification time; no file content | No |
205
+ | `list_dir` | List up to 500 directory entries | No |
206
+ | `find_files` | Find paths by literal substring, optionally files/directories only | No |
207
+ | `grep` | Search text content by literal substring | No |
208
+ | `write_file` | Create/overwrite text, including missing parents, up to 1 MiB | Yes |
209
+ | `edit_file` | Replace a nonempty, unique exact match; resulting file up to 1 MiB | Yes |
210
+ | `make_dir` | Create folders and missing parents | Yes |
211
+ | `copy_path` | Copy a file (including binary assets) or directory tree | Yes |
212
+ | `move_path` | Move/rename a file or directory | Yes |
213
+ | `remove_path` | Move a file/directory into private project recovery storage | Yes |
214
+ | `list_removed` | List recovery IDs, original paths and removal times | No |
215
+ | `restore_file` | Restore by recovery ID, optionally to a new destination | Yes |
216
+ | `run_command` | Noninteractive terminal command with bounded output/time | Explicit yes, guarded auto for recognized tasks, or acknowledged absolute |
217
+
218
+ File mutations marked Yes ask in `ask` mode and run automatically in `files`/`auto`/`absolute`.
219
+ Neither file approve-all nor `--auto-approve` grants terminal authorization.
220
+
221
+ `copy_path`, `move_path`, and `restore_file` **never overwrite an existing destination**.
222
+ Copy/move/remove/restore preflight trees with a 5,000-entry / 100 MiB limit. Tree
223
+ operations reject symlinks and special files rather than silently omitting entries.
224
+ Outside absolute mode they also reject `.git` internals and recovery storage;
225
+ mutations reject symlink paths (even links staying inside the project), the working
226
+ directory itself, and `.git` internals. Absolute lifts those project/protected-name
227
+ path guards, but write/edit still refuse final symlinks and non-regular files, and
228
+ tree operations still refuse symlink entries. Text tools reject NUL-containing binary
229
+ files. Write/edit may overwrite according to the selected permission mode; read before
230
+ changing an existing file.
231
+
232
+ `find_files` caps at 500 results / 5,000 inspected entries; `grep` caps at 200 results /
233
+ 5,000 files. Both skip symlinks, `.git`, `.sub-recovery`, dependencies and common build
234
+ directories during recursion. Find patterns are literal, not globs or regular expressions.
235
+ Mutations from concurrent tool batches are serialized per project in this process.
236
+ Confinement is a path guard, not an OS sandbox against another process changing the
237
+ filesystem concurrently; avoid moving project folders or changing symlinks mid-operation.
238
+
239
+ Removed items live in `.sub-recovery/` inside the working folder (directory mode 0700,
240
+ metadata mode 0600), survive CLI restarts, and are **not automatically purged**. A local
241
+ ignore-all `.gitignore` is created in the recovery folder to reduce accidental commits;
242
+ other upload/backup tools may still include it. Recovery
243
+ is project-local, not Finder Trash or a backup of overwritten files. Outside absolute
244
+ mode the agent cannot read or modify that storage directly; it uses `list_removed`
245
+ and `restore_file`. Absolute mode can access the storage directly and removed external
246
+ items are kept here too; restoring their external original paths requires absolute
247
+ mode. Treat the folder as private data: do not commit, upload, or delete it if recovery
248
+ is needed.
249
+ Recovery items remain until restored or you manually manage the storage. These file-tool
250
+ protections are not an OS sandbox: an approved terminal command can access or delete
251
+ recovery data too.
252
+
253
+ ### Terminal commands
254
+
255
+ `run_command(command, cwd?, timeout_ms?)` uses macOS `/bin/zsh -f -c`, with ignored
256
+ stdin and piped stdout/stderr, not an interactive terminal. `cwd` must be an existing
257
+ project directory (default project root), except absolute mode also permits existing
258
+ directories elsewhere on the drive. The default timeout is 60 seconds, maximum
259
+ 5 minutes; commands are limited to 16 KiB and captured output to a combined 64 KiB.
260
+ Overflow terminates the command and reports truncation, not unlimited collection.
261
+ Results include status, exit code/signal, stdout/stderr and truncation; failure is
262
+ reported faithfully. The live card shows lifecycle only, not raw command output.
263
+
264
+ In ask/file-only mode, every command gets a separate approval. Guarded auto skips this
265
+ question only for recognized routine tasks; acknowledged absolute mode skips it for
266
+ arbitrary commands. When a command needs approval, its full text,
267
+ canonical cwd and effective timeout appear in chat before the anchored yes/no question;
268
+ control/invisible characters and backslashes are escaped visibly rather than executed
269
+ by the terminal. Known router-key-bearing commands are rejected before display.
270
+ Commands use a minimal environment allowlist (standard path/home/user/temp/locale
271
+ variables, `TERM=dumb`), not ambient credential variables. Known router credentials
272
+ are redacted from captured output before model/history/transcript use. This is not
273
+ comprehensive secret isolation: commands can still read local configuration or make
274
+ network requests. Review what you approve. `/bin/zsh -f` disables ordinary user rc
275
+ loading; it does not isolate system startup behavior.
276
+
277
+ ### Guarded auto
278
+
279
+ Enable with `sub --auto`, `/mode auto`, or the `/mode` picker. It is opt-in for this
280
+ session, never saved as a default. `files` mode and legacy `auto_approve` remain file-only;
281
+ `--auto`, `--auto-approve`, and `--absolute` are mutually exclusive.
282
+
283
+ Recognized examples include `pwd`, `node --test`,
284
+ `node --test --test-concurrency=2 --test-reporter=dot`, `tsc --noEmit`, `tsc --build`,
285
+ `vite build`, `eslint src --max-warnings=0`, and guarded Git inspection such as
286
+ `git --no-pager status --short` or
287
+ `git --no-pager diff --no-ext-diff --no-textconv --stat`.
288
+ `npm test` and `npm run build|test|lint|typecheck|check` qualify only when the current
289
+ folder's bounded, regular `package.json` contains recognized script bodies and pre/post
290
+ lifecycle hooks. Nested tasks are checked with recursion/work limits; changed scripts
291
+ are rechecked when queued execution begins. Missing or opaque scripts ask.
292
+
293
+ The parser accepts a narrow literal/quoted-word syntax and `&&` only when every part
294
+ qualifies. Unknown flags/commands, shell substitutions, redirections, pipes, wrappers,
295
+ interpreters/eval, installs/downloads, `sudo`, shell deletion (including `rm -rf`), disk
296
+ operations, and destructive Git operations **never run automatically in guarded auto**.
297
+ They require explicit approval once outside absolute mode; the agent should prefer
298
+ recoverable file tools for removal.
299
+ This is intentionally more conservative than an “anything except rm” blacklist.
300
+
301
+ **Auto assumes a trusted project and toolchain.** Tests, builds, plugins, executable
302
+ resolution and tool configuration can execute arbitrary code, including code with
303
+ side effects. Recognizing a command is not proof that it is harmless, and this is not
304
+ an OS sandbox. Do not enable auto in an untrusted repository. There is still an external
305
+ filesystem race between validation and execution; command text inspection cannot
306
+ provide isolation. Turning auto off revokes queued auto grants, including after turning
307
+ it on again, but does not interrupt work already running.
308
+
309
+ ### Absolute permissions
310
+
311
+ Enable with `sub --absolute`, `/mode absolute`, or the `/mode` picker. A prominent
312
+ warning explains the whole-drive scope and possibility of permanent deletion,
313
+ overwrites, downloaded code and network access. Type **`ABSOLUTE`** to acknowledge;
314
+ blank, any other answer, Esc, Ctrl+C or EOF cancels. Selecting the picker entry alone
315
+ does not enable it. The warning and typed acknowledgement leave the visible chat
316
+ once the prompt closes; the persistent mode badge remains. CLI startup stays in ask
317
+ mode until acknowledged; a cancelled in-session request retains the previous mode. An interactive terminal is required;
318
+ `--absolute` is rejected with `-p`, `--check`, or piped input/output.
319
+
320
+ **After acknowledgement there are no file/command approval prompts or guarded-auto
321
+ command filtering.** File tools can read/change paths across the drive, including
322
+ absolute paths and `~/`, and commands may start outside the project. A persistent red
323
+ `ABSOLUTE — NO APPROVALS` badge stays visible while the agent works and command controls
324
+ are open. Use only when you trust the task and the files/code the agent will encounter.
325
+
326
+ This is current-account access, **not automatic root/sudo or a bypass of macOS privacy
327
+ permissions**. No interactive password entry is provided. Tool byte/tree limits,
328
+ regular-file and symlink-tree rules, no-overwrite copy/move/restore, recoverable
329
+ `remove_path`, bounded command output/time and cancellation remain. Commands can still
330
+ perform permanent deletion. Known router credentials are rejected in commands and
331
+ redacted from command output and absolute file-tool results/errors before history;
332
+ this is not comprehensive secret isolation or a network sandbox.
333
+
334
+ Absolute is session-only, never saved or enabled by `auto_approve`. `/mode ask` disables
335
+ it; queued absolute commands/mutations recheck their mode generation before executing.
336
+ Turning it back on cannot revive old grants. Work already running is not interrupted;
337
+ use Ctrl+C to stop the active turn.
338
+
339
+ ### Command cancellation
340
+
341
+ Ctrl+C, `/clear`, `/exit`, timeout and output overflow terminate the process group,
342
+ with TERM→KILL escalation. Normal shell completion also cleans up ordinary background
343
+ descendants. There is no persistent-background-job or interactive-input API; programs
344
+ requiring a TTY or password entry may fail. Descendants that detach into another
345
+ process group/session can escape cleanup; no sandbox guarantee is made. Commands
346
+ serialize with project mutations in this process and recheck cancellation before
347
+ queued execution starts. Tool results remain paired with calls after cancellation,
348
+ so continuing the conversation does not send broken tool history.
349
+
350
+ For example: “Create a small website in `site/`, copy my assets into it, and run its
351
+ tests.” In ask mode, the agent creates the structure with approval and asks before
352
+ running tests. In guarded auto, recognized tasks can proceed without those prompts.
353
+ Package installs and downloads are now possible through approved commands, not
354
+ silently performed through filesystem tools.
355
+
356
+ ## Development
357
+
358
+ ```
359
+ src/index.ts entry, flags, bootstrap test/ node:test suites
360
+ src/client.ts RouterClient (HTTP, retry) scripts/ SSE harness (live)
361
+ src/adapters/ openai.ts, anthropic.ts
362
+ src/tools.ts file tools + confinement src/loop.ts agent round driver
363
+ src/repl.ts REPL shell src/lineinput.ts raw-mode editor
364
+ src/sessions.ts transcripts src/usage.ts /usage render
365
+ src/markdown.ts terminal markdown renderer
366
+ ```
367
+
368
+ Live wire-shape verification:
369
+
370
+ ```bash
371
+ node scripts/sse-harness.ts --model <id> --dialect openai|anthropic|auto [--tools] [--save test/fixtures/x.sse]
372
+ ```
373
+
374
+ ## Manual E2E checklist
375
+
376
+ - [ ] `sub --check` green; `npm test` green; `npm run typecheck` clean
377
+ - [ ] `/model` → arrow-key picker → streamed chat answers (markdown-rendered)
378
+ - [ ] multi-tool task (create → edit → grep) exercising y / a / n approvals
379
+ - [ ] `/usage` shows compact provider/account limits without duplicate footer rows
380
+ - [ ] mouse wheel / Page Up/Down scroll chat with input anchored, including mid-stream
381
+ - [ ] Ctrl+V queues a clipboard screenshot without submitting; Ctrl+X clears pending images
382
+ - [ ] text Cmd+V still works; `/` palette has no image commands or `/help`
383
+ - [ ] `/clear` starts a fresh transcript; session files exist
384
+ - [ ] type during streaming; Enter queues steering and leaves tool/result ordering valid
385
+ - [ ] an unfinished draft survives a permission question and turn completion
386
+ - [ ] Ctrl+C mid-stream prints `*[stopped]*` and restores any unapplied steering
387
+ - [ ] `sub -p "hi"` one-shot; `echo "…" | sub` piped mode works
388
+ - [ ] bad key exits with the 401 hint; `SUBROUTER_BASE_URL` pointing at a dead port
389
+ reports healthz FAILED
390
+ - [ ] `sub --debug` shows raw SSE frames with credentials redacted
391
+
392
+ ## Notes
393
+
394
+ - **Zero runtime deps** — `src/` and `scripts/` use Node built-ins only.
395
+ - **Never print an API key** — not even masked. Real keys live in the config file or
396
+ the env var.
397
+ - Message content is written only to session transcripts under `~/.config/…/sessions/`,
398
+ never to stdout/stderr (except `--debug`, which dumps raw SSE frames on request).
@@ -0,0 +1,9 @@
1
+ {
2
+ "base_url": "https://router.eva.pink/v1",
3
+ "api_key": "sk-sub-your-key-here",
4
+ "default_model": null,
5
+ "reasoning_effort": "auto",
6
+ "max_output_tokens": 8192,
7
+ "max_tool_rounds": 25,
8
+ "auto_approve": false
9
+ }
package/package.json CHANGED
@@ -1,6 +1,23 @@
1
1
  {
2
2
  "name": "subrouter-cli",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "sub — a personal agentic coding CLI for the subrouter AI router",
5
+ "type": "module",
6
+ "bin": {
7
+ "sub": "src/index.ts"
8
+ },
9
+ "scripts": {
10
+ "start": "node src/index.ts",
11
+ "test": "node --test",
12
+ "typecheck": "tsc --noEmit",
13
+ "harness": "node scripts/sse-harness.ts"
14
+ },
15
+ "engines": {
16
+ "node": ">=24"
17
+ },
18
+ "dependencies": {},
19
+ "devDependencies": {
20
+ "@types/node": "^24",
21
+ "typescript": "^5.8"
22
+ }
23
+ }