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.
- package/CLAUDE.md +51 -0
- package/README.md +397 -2
- package/config.example.json +9 -0
- package/package.json +21 -4
- package/scripts/sse-harness.ts +252 -0
- package/scripts/sub-wrapper.sh +11 -0
- package/src/adapters/anthropic.ts +311 -0
- package/src/adapters/openai.ts +227 -0
- package/src/approval.ts +21 -0
- package/src/chatviewport.ts +123 -0
- package/src/client.ts +398 -0
- package/src/clipboard.ts +62 -0
- package/src/commandpolicy.ts +272 -0
- package/src/commands.ts +18 -0
- package/src/config.ts +107 -0
- package/src/effort.ts +18 -0
- package/src/images.ts +77 -0
- package/src/index.ts +248 -0
- package/src/lineinput.ts +726 -0
- package/src/loop.ts +201 -0
- package/src/markdown.ts +244 -0
- package/src/repl.ts +700 -0
- package/src/sessions.ts +58 -0
- package/src/sse.ts +64 -0
- package/src/terminal.ts +228 -0
- package/src/token.ts +36 -0
- package/src/toolpreview.ts +54 -0
- package/src/tools.ts +790 -0
- package/src/types.ts +73 -0
- package/src/ui.ts +813 -0
- package/src/usage.ts +186 -0
- package/test/absolute-tools.test.ts +296 -0
- package/test/absolute-ui.test.ts +153 -0
- package/test/adapters.test.ts +205 -0
- package/test/anthropic.test.ts +246 -0
- package/test/auto-ui.test.ts +106 -0
- package/test/chatviewport.test.ts +49 -0
- package/test/client.test.ts +327 -0
- package/test/clipboard.test.ts +63 -0
- package/test/command-input.test.ts +183 -0
- package/test/command-tools.test.ts +141 -0
- package/test/commandpolicy.test.ts +252 -0
- package/test/disk-tools.test.ts +220 -0
- package/test/effort.test.ts +123 -0
- package/test/fixtures/openai-tools.sse +84 -0
- package/test/image-adapters.test.ts +78 -0
- package/test/image-ui.test.ts +151 -0
- package/test/images.test.ts +60 -0
- package/test/loop.test.ts +387 -0
- package/test/m5.test.ts +127 -0
- package/test/markdown.test.ts +201 -0
- package/test/repl-ui.test.ts +293 -0
- package/test/sse.test.ts +74 -0
- package/test/steering-ui.test.ts +182 -0
- package/test/steering.test.ts +68 -0
- package/test/terminal-ui.test.ts +144 -0
- package/test/terminal.test.ts +229 -0
- package/test/toolpreview.test.ts +51 -0
- package/test/tools.test.ts +227 -0
- package/test/ui.test.ts +635 -0
- package/test/usage-footer.test.ts +180 -0
- 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
|
-
#
|
|
1
|
+
# sub
|
|
2
2
|
|
|
3
|
-
|
|
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).
|
package/package.json
CHANGED
|
@@ -1,6 +1,23 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "subrouter-cli",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
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
|
+
}
|