@aragon-agent/cli 0.4.3
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/CHANGELOG.md +292 -0
- package/README.md +558 -0
- package/dist/agent/coalesce.d.ts +14 -0
- package/dist/agent/coalesce.js +41 -0
- package/dist/agent/controller.d.ts +132 -0
- package/dist/agent/controller.js +332 -0
- package/dist/agent/headless.d.ts +22 -0
- package/dist/agent/headless.js +74 -0
- package/dist/agent/reducer.d.ts +181 -0
- package/dist/agent/reducer.js +420 -0
- package/dist/agent/system-prompt.d.ts +30 -0
- package/dist/agent/system-prompt.js +58 -0
- package/dist/agent/usage.d.ts +13 -0
- package/dist/agent/usage.js +41 -0
- package/dist/cli.d.ts +11 -0
- package/dist/cli.js +482 -0
- package/dist/commands/builtins.d.ts +16 -0
- package/dist/commands/builtins.js +223 -0
- package/dist/commands/registry.d.ts +48 -0
- package/dist/commands/registry.js +45 -0
- package/dist/commands/skills.d.ts +22 -0
- package/dist/commands/skills.js +625 -0
- package/dist/config/app-paths.d.ts +11 -0
- package/dist/config/app-paths.js +16 -0
- package/dist/config/env.d.ts +28 -0
- package/dist/config/env.js +92 -0
- package/dist/config/load.d.ts +48 -0
- package/dist/config/load.js +158 -0
- package/dist/config/migrate-legacy-state.d.ts +78 -0
- package/dist/config/migrate-legacy-state.js +181 -0
- package/dist/config/schema.d.ts +249 -0
- package/dist/config/schema.js +247 -0
- package/dist/config/store.d.ts +44 -0
- package/dist/config/store.js +126 -0
- package/dist/input/keymap.d.ts +20 -0
- package/dist/input/keymap.js +85 -0
- package/dist/session/persist.d.ts +24 -0
- package/dist/session/persist.js +51 -0
- package/dist/skills/archive.d.ts +41 -0
- package/dist/skills/archive.js +152 -0
- package/dist/skills/cli-commands.d.ts +52 -0
- package/dist/skills/cli-commands.js +502 -0
- package/dist/skills/fetch-source.d.ts +104 -0
- package/dist/skills/fetch-source.js +448 -0
- package/dist/skills/installer.d.ts +117 -0
- package/dist/skills/installer.js +414 -0
- package/dist/skills/lock.d.ts +75 -0
- package/dist/skills/lock.js +223 -0
- package/dist/skills/manifest.d.ts +47 -0
- package/dist/skills/manifest.js +95 -0
- package/dist/skills/node-host.d.ts +23 -0
- package/dist/skills/node-host.js +59 -0
- package/dist/skills/paths.d.ts +61 -0
- package/dist/skills/paths.js +107 -0
- package/dist/skills/service.d.ts +256 -0
- package/dist/skills/service.js +730 -0
- package/dist/skills/tools.d.ts +35 -0
- package/dist/skills/tools.js +164 -0
- package/dist/skills/updater.d.ts +101 -0
- package/dist/skills/updater.js +330 -0
- package/dist/skills/usage.d.ts +72 -0
- package/dist/skills/usage.js +216 -0
- package/dist/tools/bash-tool.d.ts +12 -0
- package/dist/tools/bash-tool.js +131 -0
- package/dist/tools/diff.d.ts +15 -0
- package/dist/tools/diff.js +50 -0
- package/dist/tools/fs-tools.d.ts +16 -0
- package/dist/tools/fs-tools.js +215 -0
- package/dist/tools/index.d.ts +93 -0
- package/dist/tools/index.js +157 -0
- package/dist/tools/search-tools.d.ts +11 -0
- package/dist/tools/search-tools.js +225 -0
- package/dist/ui/App.d.ts +24 -0
- package/dist/ui/App.js +522 -0
- package/dist/ui/AutocompletePopup.d.ts +21 -0
- package/dist/ui/AutocompletePopup.js +19 -0
- package/dist/ui/Composer.d.ts +40 -0
- package/dist/ui/Composer.js +61 -0
- package/dist/ui/Header.d.ts +30 -0
- package/dist/ui/Header.js +25 -0
- package/dist/ui/Logo.d.ts +50 -0
- package/dist/ui/Logo.js +85 -0
- package/dist/ui/Markdown.d.ts +23 -0
- package/dist/ui/Markdown.js +122 -0
- package/dist/ui/PromptInput.d.ts +81 -0
- package/dist/ui/PromptInput.js +383 -0
- package/dist/ui/SessionOpener.d.ts +30 -0
- package/dist/ui/SessionOpener.js +12 -0
- package/dist/ui/StatusBar.d.ts +39 -0
- package/dist/ui/StatusBar.js +43 -0
- package/dist/ui/ToastStack.d.ts +24 -0
- package/dist/ui/ToastStack.js +38 -0
- package/dist/ui/Transcript.d.ts +62 -0
- package/dist/ui/Transcript.js +128 -0
- package/dist/ui/capabilities.d.ts +25 -0
- package/dist/ui/capabilities.js +59 -0
- package/dist/ui/console-bridge.d.ts +19 -0
- package/dist/ui/console-bridge.js +52 -0
- package/dist/ui/density.d.ts +26 -0
- package/dist/ui/density.js +34 -0
- package/dist/ui/entries/AssistantEntry.d.ts +23 -0
- package/dist/ui/entries/AssistantEntry.js +13 -0
- package/dist/ui/entries/EntryFrame.d.ts +23 -0
- package/dist/ui/entries/EntryFrame.js +6 -0
- package/dist/ui/entries/ToolCard.d.ts +38 -0
- package/dist/ui/entries/ToolCard.js +72 -0
- package/dist/ui/entries/ToolPreview.d.ts +57 -0
- package/dist/ui/entries/ToolPreview.js +144 -0
- package/dist/ui/entries/UserEntry.d.ts +14 -0
- package/dist/ui/entries/UserEntry.js +5 -0
- package/dist/ui/exit-snapshot.d.ts +27 -0
- package/dist/ui/exit-snapshot.js +26 -0
- package/dist/ui/gauge.d.ts +24 -0
- package/dist/ui/gauge.js +36 -0
- package/dist/ui/glyphs.d.ts +87 -0
- package/dist/ui/glyphs.js +127 -0
- package/dist/ui/gradient.d.ts +11 -0
- package/dist/ui/gradient.js +50 -0
- package/dist/ui/layout/AppShell.d.ts +24 -0
- package/dist/ui/layout/AppShell.js +9 -0
- package/dist/ui/layout/Gutter.d.ts +67 -0
- package/dist/ui/layout/Gutter.js +42 -0
- package/dist/ui/layout/OverlayFrame.d.ts +62 -0
- package/dist/ui/layout/OverlayFrame.js +92 -0
- package/dist/ui/layout/ScrollViewport.d.ts +33 -0
- package/dist/ui/layout/ScrollViewport.js +71 -0
- package/dist/ui/layout/budget.d.ts +42 -0
- package/dist/ui/layout/budget.js +47 -0
- package/dist/ui/layout/frame.d.ts +58 -0
- package/dist/ui/layout/frame.js +75 -0
- package/dist/ui/layout/overlay-window.d.ts +43 -0
- package/dist/ui/layout/overlay-window.js +51 -0
- package/dist/ui/layout/scroll.d.ts +28 -0
- package/dist/ui/layout/scroll.js +76 -0
- package/dist/ui/layout/useTerminalSize.d.ts +13 -0
- package/dist/ui/layout/useTerminalSize.js +50 -0
- package/dist/ui/markdown-blocks.d.ts +62 -0
- package/dist/ui/markdown-blocks.js +120 -0
- package/dist/ui/overlays/ConfirmDialog.d.ts +30 -0
- package/dist/ui/overlays/ConfirmDialog.js +25 -0
- package/dist/ui/overlays/HelpOverlay.d.ts +17 -0
- package/dist/ui/overlays/HelpOverlay.js +60 -0
- package/dist/ui/overlays/ModelPicker.d.ts +29 -0
- package/dist/ui/overlays/ModelPicker.js +29 -0
- package/dist/ui/overlays/SettingsScreen.d.ts +37 -0
- package/dist/ui/overlays/SettingsScreen.js +106 -0
- package/dist/ui/palettes.d.ts +79 -0
- package/dist/ui/palettes.js +115 -0
- package/dist/ui/screen.d.ts +27 -0
- package/dist/ui/screen.js +57 -0
- package/dist/ui/theme.d.ts +78 -0
- package/dist/ui/theme.js +127 -0
- package/dist/ui/transcript-text.d.ts +27 -0
- package/dist/ui/transcript-text.js +110 -0
- package/package.json +73 -0
- package/skills/skill-creator/SKILL.md +96 -0
package/README.md
ADDED
|
@@ -0,0 +1,558 @@
|
|
|
1
|
+
# @aragon-agent/cli
|
|
2
|
+
|
|
3
|
+
A Claude-Code / Codex-style **interactive terminal UI** for the
|
|
4
|
+
[`@aragon-agent/core`](../core) engine. Type a task, watch the agent think and
|
|
5
|
+
stream its answer, see each tool call render as a live card, and interrupt /
|
|
6
|
+
steer / configure the session — all without leaving the terminal.
|
|
7
|
+
|
|
8
|
+
> ⚠️ **Full permission, no sandbox.** This first version runs at **maximum
|
|
9
|
+
> permission**: `bash` executes directly, file writes hit the real filesystem,
|
|
10
|
+
> and there is **no per-action approval gate by default**. Only run it in
|
|
11
|
+
> workspaces you trust. An opt-in confirmation mode is available via `--confirm`.
|
|
12
|
+
|
|
13
|
+
## Install
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
# Global
|
|
17
|
+
npm i -g @aragon-agent/cli
|
|
18
|
+
aragon
|
|
19
|
+
|
|
20
|
+
# Zero-install
|
|
21
|
+
npx @aragon-agent/cli
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
> **Release note (R3):** `npx @aragon-agent/cli` resolves `@aragon-agent/core`
|
|
25
|
+
> from the npm registry, so `@aragon-agent/core@^0.1.0` must be published first
|
|
26
|
+
> (or bundled into the CLI `dist`). During local development inside this
|
|
27
|
+
> monorepo, npm workspaces link the local `packages/core` automatically.
|
|
28
|
+
|
|
29
|
+
## Quick start
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
# Set a key (any one of these)
|
|
33
|
+
export ANTHROPIC_API_KEY=sk-ant-...
|
|
34
|
+
# ...or paste it live in the TUI via /settings, or pass --api-key
|
|
35
|
+
|
|
36
|
+
aragon # interactive TUI
|
|
37
|
+
aragon "summarize README.md" # interactive, auto-submitting the prompt
|
|
38
|
+
echo "list files" | aragon -p # one-shot, prints to stdout then exits
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Usage
|
|
42
|
+
|
|
43
|
+
```
|
|
44
|
+
aragon [prompt] Start the interactive TUI (or one-shot with -p / piped stdin)
|
|
45
|
+
aragon -p, --print [prompt] Headless: stream the answer to stdout, then exit
|
|
46
|
+
aragon config Open the settings screen
|
|
47
|
+
aragon config set <key> <value> Non-interactive config write (e.g. model, provider)
|
|
48
|
+
aragon config path Print the config file path
|
|
49
|
+
aragon models [--provider p] List builtin + discovered models
|
|
50
|
+
aragon --version | -v Print the version
|
|
51
|
+
aragon --help | -h Print help
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Global flags
|
|
55
|
+
|
|
56
|
+
| Flag | Meaning |
|
|
57
|
+
| --- | --- |
|
|
58
|
+
| `--provider <id>` | `anthropic` \| `openai` \| `google` |
|
|
59
|
+
| `--model <id>` | Model id |
|
|
60
|
+
| `--base-url <url>` | Override the provider base URL (OpenAI-compatible endpoints) |
|
|
61
|
+
| `--api-key <key>` | One-shot key override (not persisted) |
|
|
62
|
+
| `--thinking <level>` | `off\|minimal\|low\|medium\|high\|xhigh` |
|
|
63
|
+
| `--max-tokens <n>` | Output token cap |
|
|
64
|
+
| `--cwd <dir>` | Working directory for tools |
|
|
65
|
+
| `--confirm` | Confirm each mutating tool call |
|
|
66
|
+
| `--tool-timeout <ms>` | Per-tool executor ceiling (default 180000) |
|
|
67
|
+
| `--idle-timeout <ms>` | Watchdog idle timeout (auto-raised to ≥ tool-timeout + 30s) |
|
|
68
|
+
| `--theme <name>` | `auto\|warm\|cool\|light` (`dark` is a compatibility alias for `cool`) |
|
|
69
|
+
| `--compact` / `--no-compact` | Transcript density: no blank rows between turns, or the default |
|
|
70
|
+
| `--hints` / `--no-hints` | Show or hide the composer hint row |
|
|
71
|
+
| `--fullscreen` | Force the full-screen TUI, overriding the automatic downgrades |
|
|
72
|
+
| `--no-fullscreen` | Force the inline renderer (the 0.2.0 behavior) |
|
|
73
|
+
| `--no-exit-transcript` | Do not replay the session summary after exiting |
|
|
74
|
+
| `--no-skills` | Disable the skill system entirely (no catalog, no skill tools) |
|
|
75
|
+
| `--skill <name>` | Force-load a skill's full body for this run (repeatable) |
|
|
76
|
+
| `--skills-yes` | Approve skill installs for this run (CI / headless) |
|
|
77
|
+
| `--no-color` | Disable ANSI color |
|
|
78
|
+
| `--quiet` | (print mode) suppress tool/usage lines on stderr |
|
|
79
|
+
|
|
80
|
+
Exit codes: `0` success · `1` agent/runtime error · `2` config/usage error ·
|
|
81
|
+
`130` interrupted.
|
|
82
|
+
|
|
83
|
+
## Slash commands (interactive)
|
|
84
|
+
|
|
85
|
+
| Command | Action |
|
|
86
|
+
| --- | --- |
|
|
87
|
+
| `/help` | Keybindings + command help |
|
|
88
|
+
| `/model` | Open the model picker |
|
|
89
|
+
| `/settings` | Open the settings screen |
|
|
90
|
+
| `/thinking <level>` | Set the thinking level |
|
|
91
|
+
| `/tools` | List active tools |
|
|
92
|
+
| `/clear` | Clear the visible transcript |
|
|
93
|
+
| `/reset` | New conversation |
|
|
94
|
+
| `/cwd [dir]` | Show / change the tool working directory |
|
|
95
|
+
| `/save [file]` | Save the session to JSON |
|
|
96
|
+
| `/resume [file]` | Load a saved session |
|
|
97
|
+
| `/theme <auto\|warm\|cool\|light>` | Switch the color theme live (persisted) |
|
|
98
|
+
| `/expand [n]` | Expand / collapse the n-th-from-last (default last) tool card |
|
|
99
|
+
| `/copy` | Copy the last answer to the clipboard |
|
|
100
|
+
| `/skills [sub]` | List and manage skills (see below) |
|
|
101
|
+
| `/<skill-name> [args]` | Run an installed skill directly |
|
|
102
|
+
| `/exit` (`/quit`) | Exit |
|
|
103
|
+
|
|
104
|
+
A leading `/` meant literally can be escaped as `//`. Typing `/` opens a
|
|
105
|
+
command palette and `@` opens a file-path completion popup — `Tab` / `→`
|
|
106
|
+
completes, `Up` / `Down` moves the selection, `Esc` closes it.
|
|
107
|
+
|
|
108
|
+
## Keybindings
|
|
109
|
+
|
|
110
|
+
| Key | Action |
|
|
111
|
+
| --- | --- |
|
|
112
|
+
| `Enter` | Submit (idle) / queue a steering message (running) |
|
|
113
|
+
| `Alt+Enter` / `Shift+Enter` | Insert a newline |
|
|
114
|
+
| `Esc` | Abort the run / close an overlay / close a popup |
|
|
115
|
+
| `Ctrl+C` ×2 | Exit (first press warns) |
|
|
116
|
+
| `Ctrl+L` | Redraw the frame (clear the screen in inline mode) |
|
|
117
|
+
| `Ctrl+T` | Toggle thinking blocks |
|
|
118
|
+
| `Ctrl+O` | Expand / collapse the most recent tool card |
|
|
119
|
+
| `PgUp` / `PgDn` | Scroll the transcript a page (full-screen mode) |
|
|
120
|
+
| `Shift+↑` / `Shift+↓` | Scroll the transcript a line (full-screen mode) |
|
|
121
|
+
| `Home` / `End`, `Ctrl+A` / `Ctrl+E` | Cursor to line start / end |
|
|
122
|
+
| `Alt/Ctrl+←` / `→` | Word-wise cursor jump |
|
|
123
|
+
| `Ctrl+W`, `Alt+Backspace` | Delete the previous word |
|
|
124
|
+
| `Ctrl+U` / `Ctrl+K` | Kill to line start / end |
|
|
125
|
+
| `Up` / `Down` | Move between draft lines; recall prompt history at the edges |
|
|
126
|
+
| `?` | Open help (empty input) |
|
|
127
|
+
|
|
128
|
+
In **full-screen mode** the transcript is scrolled by the app itself: `PgUp` /
|
|
129
|
+
`PgDn` and `Shift+↑` / `Shift+↓`. While pinned to the bottom the viewport follows
|
|
130
|
+
new output automatically; once you scroll away the status bar shows `↑N` and a
|
|
131
|
+
hint counts the lines below you. Submitting a message always re-pins to the
|
|
132
|
+
newest output. In **inline mode** (`--no-fullscreen`) history lives in your
|
|
133
|
+
terminal's native scrollback, printed once via Ink `<Static>`.
|
|
134
|
+
|
|
135
|
+
## Themes
|
|
136
|
+
|
|
137
|
+
Three palettes plus `auto`:
|
|
138
|
+
|
|
139
|
+
| Name | Look |
|
|
140
|
+
| --- | --- |
|
|
141
|
+
| `warm` | Terracotta + amber over warm neutrals. The default (`auto` resolves here). |
|
|
142
|
+
| `cool` | The blue/violet palette that shipped as `dark` through 0.3.x. |
|
|
143
|
+
| `light` | For light terminal backgrounds. |
|
|
144
|
+
|
|
145
|
+
`auto` never guesses `light`: terminals cannot report their background reliably,
|
|
146
|
+
and guessing wrong makes the app unreadable rather than merely wrong-looking.
|
|
147
|
+
|
|
148
|
+
**Migrating from 0.3.x** — `dark` was renamed `cool` when `warm` became the
|
|
149
|
+
default, and the old name still works everywhere: `--theme dark`,
|
|
150
|
+
`aragon config set theme dark`, `/theme dark`, and a config file containing
|
|
151
|
+
`"theme": "dark"` all resolve to `cool`, so a screen you explicitly chose does
|
|
152
|
+
not change under you. The value is rewritten as `cool` the next time the config
|
|
153
|
+
is saved. Only users who never picked a theme see the new default.
|
|
154
|
+
|
|
155
|
+
Density and hints are settable the same three ways: `--compact` / `--no-hints`
|
|
156
|
+
for one run, `aragon config set density compact` / `aragon config set hints false`
|
|
157
|
+
to persist. The composer hint row also shortens to `? help` on its own after a
|
|
158
|
+
few sessions — except while a run is in progress, when it always spells out
|
|
159
|
+
`esc abort` in full.
|
|
160
|
+
|
|
161
|
+
## Full-screen mode
|
|
162
|
+
|
|
163
|
+
`aragon` takes over the terminal's **alternate screen buffer** — the mechanism
|
|
164
|
+
`vim`, `htop`, and `lazygit` use. Two consequences worth knowing:
|
|
165
|
+
|
|
166
|
+
- Your previous shell output is **covered, not erased**, and returns untouched
|
|
167
|
+
when you exit. Nothing in your scrollback is destroyed.
|
|
168
|
+
- The frame is fixed at `rows - 1` tall, which is what keeps the composer and the
|
|
169
|
+
status bar at the bottom of the screen even on an empty session.
|
|
170
|
+
|
|
171
|
+
On exit the session is replayed into the normal buffer as plain text so the
|
|
172
|
+
conversation survives leaving the screen (`--no-exit-transcript` opts out;
|
|
173
|
+
`/save` still exports the full JSON).
|
|
174
|
+
|
|
175
|
+
It downgrades to the inline renderer automatically when stdout is not a TTY,
|
|
176
|
+
`TERM=dumb`, a CI environment variable is set, or the terminal is under 12 rows
|
|
177
|
+
or 40 columns. `--fullscreen` overrides all of those except the non-TTY check —
|
|
178
|
+
writing screen-control sequences into a pipe or a redirected file is never safe.
|
|
179
|
+
`--no-fullscreen`, `ARAGON_FULLSCREEN=0`, or `aragon config set fullscreen false`
|
|
180
|
+
opt out permanently.
|
|
181
|
+
|
|
182
|
+
The terminal's native scrollback and mouse wheel do not scroll the transcript in
|
|
183
|
+
this mode, and mouse tracking is deliberately left off because enabling it costs
|
|
184
|
+
text selection and copy in most terminals. If a crash ever strands your terminal
|
|
185
|
+
on the alternate screen, `reset` restores it.
|
|
186
|
+
|
|
187
|
+
## Built-in tools
|
|
188
|
+
|
|
189
|
+
`read_file`, `write_file`, `edit_file`, `list_dir`, `glob`, `grep`, `bash` — all
|
|
190
|
+
at full permission. Paths resolve against the session working directory. The
|
|
191
|
+
per-tool timeout and 100 KB output truncation are enforced by the core executor;
|
|
192
|
+
`bash`'s `timeout` param only shrinks within that ceiling.
|
|
193
|
+
|
|
194
|
+
When skills are enabled, four more are added — the two read tools `skill` and
|
|
195
|
+
`skill_find`, then the two mutating ones `skill_install` and `skill_create` (see
|
|
196
|
+
below). `--no-skills` removes them again. Updating a skill is deliberately not
|
|
197
|
+
among them; that is a `aragon skills update` decision for you, not the model.
|
|
198
|
+
|
|
199
|
+
## Skills
|
|
200
|
+
|
|
201
|
+
A skill is a reusable expert procedure stored on disk: a directory containing a
|
|
202
|
+
`SKILL.md` (YAML frontmatter + Markdown body) and, optionally, `reference/`,
|
|
203
|
+
`scripts/` and `assets/`. The format matches Claude Code's, so an existing
|
|
204
|
+
community skill directory works as-is.
|
|
205
|
+
|
|
206
|
+
### Progressive disclosure
|
|
207
|
+
|
|
208
|
+
This is the part that makes skills cheap enough to install a lot of:
|
|
209
|
+
|
|
210
|
+
| Level | What the model gets | When |
|
|
211
|
+
| --- | --- | --- |
|
|
212
|
+
| 1 | Name, scope, and a one-line "what + when" | Always, in the system prompt. Whole catalog capped at 6 000 bytes. |
|
|
213
|
+
| 2 | The full `SKILL.md` body + a list of bundled files | Only when the model calls `skill(name=…)` |
|
|
214
|
+
| 3 | The bundled files themselves | Only when it reads them with `read_file` / `bash` |
|
|
215
|
+
|
|
216
|
+
A skill costs a line in the prompt rather than its whole body, so installing
|
|
217
|
+
many of them stays cheap. The model decides on its own whether a task matches a
|
|
218
|
+
skill — you do not have to name one.
|
|
219
|
+
|
|
220
|
+
At Level 2 the file list is split into what to read and what to run, and carries
|
|
221
|
+
the skill's absolute root so the model can build real paths. The accompanying
|
|
222
|
+
guidance is **platform-specific**: on Windows the `bash` tool runs `cmd.exe`, so
|
|
223
|
+
the model is told to run `.py` with `python <abs path>` and that a `.sh` file
|
|
224
|
+
will not work — rather than the previous, flatly untrue "run scripts with bash".
|
|
225
|
+
`aragon skills doctor` warns when a skill bundles only scripts for the other
|
|
226
|
+
shell.
|
|
227
|
+
|
|
228
|
+
**How many fit in Level 1 depends on your descriptions, and on the language they
|
|
229
|
+
are written in.** The 6 000-byte ceiling is measured in UTF-8 bytes: a terse
|
|
230
|
+
English description costs roughly 180 bytes a line (~30 skills), while a Chinese
|
|
231
|
+
one at the 220-character cap can cost 660 (~10 skills). Past that point the
|
|
232
|
+
catalog truncates.
|
|
233
|
+
|
|
234
|
+
Truncation is not a dead end. Entries are ranked before it happens, and what
|
|
235
|
+
falls off is still reachable:
|
|
236
|
+
|
|
237
|
+
- **Ranking**: scope first (`env` > `project` > `user` > `bundled`), then how
|
|
238
|
+
recently and how often you have actually used each skill. A skill you ran an
|
|
239
|
+
hour ago outranks one you have never used, so what survives truncation is what
|
|
240
|
+
you actually work with — not whatever sorts first alphabetically.
|
|
241
|
+
- **`skill_find`**: when the catalog *does* truncate, it says so and names this
|
|
242
|
+
tool, which searches everything installed by name, description and keyword.
|
|
243
|
+
It only ever searches **what is already on this machine** — it never reaches
|
|
244
|
+
the network and never suggests something to install.
|
|
245
|
+
|
|
246
|
+
Both are off the critical path: with no usage data recorded the order is exactly
|
|
247
|
+
the plain scope-then-name order, and `skill_find` is only advertised in the
|
|
248
|
+
prompt when something was actually omitted.
|
|
249
|
+
|
|
250
|
+
### Using them
|
|
251
|
+
|
|
252
|
+
```bash
|
|
253
|
+
aragon skills list [--sort=recent] # what is installed
|
|
254
|
+
aragon skills info <name> # version, path, provenance, files
|
|
255
|
+
aragon skills install <source> [--yes] # dir | git repo | https .md / .zip
|
|
256
|
+
aragon skills install github:owner/repo#v1.2.0
|
|
257
|
+
aragon skills update <name> [--yes] [--force] [--dry-run]
|
|
258
|
+
aragon skills update --all [--yes] # every skill that has an upstream
|
|
259
|
+
aragon skills update --check [<name>|--all] # is there a newer version?
|
|
260
|
+
aragon skills remove <name> [--yes]
|
|
261
|
+
aragon skills create <name> --description "what + when" [--yes]
|
|
262
|
+
aragon skills path # user skills directory
|
|
263
|
+
aragon skills doctor # validate + detect tampering
|
|
264
|
+
aragon skills usage [--json] # local use counters
|
|
265
|
+
aragon skills usage --reset --yes # delete them
|
|
266
|
+
aragon skills trust <dir> # allow a project skills directory
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
#### Updating
|
|
270
|
+
|
|
271
|
+
`update` re-fetches a skill from the source recorded in its install manifest,
|
|
272
|
+
keeping `installedAt` and recording `updatedAt` + `previousVersion`.
|
|
273
|
+
|
|
274
|
+
- **Local edits are protected.** If you have changed an installed skill, `update`
|
|
275
|
+
refuses and names the files, *before downloading anything*. `--force`
|
|
276
|
+
overwrites them — and **`--force` discards your edits**; copy them out first.
|
|
277
|
+
The replaced directory is parked under the staging trash, so a mistake is
|
|
278
|
+
recoverable by hand.
|
|
279
|
+
- **The host allowlist is re-checked** against your *current*
|
|
280
|
+
`skills.allowedHosts`, not the one in force when you installed. Narrowing that
|
|
281
|
+
list therefore also constrains updates.
|
|
282
|
+
- **`--all` never stops on one failure.** Skills that cannot be updated
|
|
283
|
+
(hand-authored, bundled, or locally modified) are reported as `skipped`, which
|
|
284
|
+
is not an error; only a genuine I/O or validation failure exits non-zero.
|
|
285
|
+
- **`--check` reports `unknown` freely.** It compares the commit recorded at
|
|
286
|
+
install time against `git ls-remote`, so it can only answer for git sources
|
|
287
|
+
installed by a version that records one. Anything else is honestly reported as
|
|
288
|
+
unknown rather than guessed.
|
|
289
|
+
- **The model cannot call `update`.** Installing is something you pointed it at;
|
|
290
|
+
swapping working instructions for different ones is a maintenance decision and
|
|
291
|
+
stays with you.
|
|
292
|
+
|
|
293
|
+
Interactively the same lives under `/skills …` (`/skills update <name> [--force]`,
|
|
294
|
+
`/skills update --all`, `/skills list --sort=recent`, `/skills policy [mode]`,
|
|
295
|
+
`/skills unload`, `/skills usage [--reset]`). Every usable skill also becomes a
|
|
296
|
+
slash command:
|
|
297
|
+
|
|
298
|
+
```
|
|
299
|
+
/pdf-forms invoice.pdf --flatten
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
Both delivery paths share `skills.bodyMaxBytes`, so an oversized `SKILL.md` is
|
|
303
|
+
truncated with a visible marker rather than submitted whole. Loading the same
|
|
304
|
+
skill twice in one conversation returns a short digest (description, file list,
|
|
305
|
+
guidance) instead of a second copy of the body; the model can ask for the full
|
|
306
|
+
text back with `skill(name="…", force=true)`.
|
|
307
|
+
|
|
308
|
+
Bodies may use `$ARGUMENTS` and `$1..$9`; `$$` is a literal `$`. If a skill
|
|
309
|
+
declares no placeholder, the arguments are appended under an `## Arguments`
|
|
310
|
+
heading instead. Where a skill's name collides with a built-in command, the
|
|
311
|
+
**built-in wins** and the skill is reachable as `/skill:<name>`.
|
|
312
|
+
|
|
313
|
+
### Where skills come from
|
|
314
|
+
|
|
315
|
+
Four scopes, later ones overriding earlier ones by name:
|
|
316
|
+
|
|
317
|
+
| Scope | Location | Writable |
|
|
318
|
+
| --- | --- | --- |
|
|
319
|
+
| `bundled` | `<package>/skills/` | no |
|
|
320
|
+
| `user` | `aragon skills path` | yes — the default install target |
|
|
321
|
+
| `project` | `<cwd>/.aragon/skills/` | yes |
|
|
322
|
+
| `project` | `<cwd>/.claude/skills/` | no — read-only Claude Code interop |
|
|
323
|
+
| `env` | `ARAGON_SKILLS_PATH` (`:`/`;`-separated) | no |
|
|
324
|
+
|
|
325
|
+
A shadowed skill is not lost: `/skills` shows it as `~shadowed by <scope>`.
|
|
326
|
+
|
|
327
|
+
### Frontmatter
|
|
328
|
+
|
|
329
|
+
```yaml
|
|
330
|
+
---
|
|
331
|
+
name: pdf-forms # required, kebab-case, ≤ 64 chars
|
|
332
|
+
description: Fill, flatten and validate AcroForm PDFs. Use when the task
|
|
333
|
+
mentions PDF forms, field filling, or flattening a fillable PDF.
|
|
334
|
+
version: 1.2.0
|
|
335
|
+
license: MIT
|
|
336
|
+
author: Jane Doe
|
|
337
|
+
homepage: https://github.com/jane/pdf-forms
|
|
338
|
+
keywords: [pdf, forms]
|
|
339
|
+
allowed-tools: [read_file, bash] # enforced as a per-turn tool ceiling (union)
|
|
340
|
+
activation: auto # auto | always | manual
|
|
341
|
+
---
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
`description` is the only text a model sees before deciding to load the skill,
|
|
345
|
+
so it has to say both what the skill does and when to use it. Unknown keys are
|
|
346
|
+
preserved, so a newer Claude Code field will not break parsing.
|
|
347
|
+
|
|
348
|
+
`activation: always` injects the body into every session's system prompt (12 000
|
|
349
|
+
bytes total across all such skills); `manual` keeps it out of the catalog so it
|
|
350
|
+
runs only when explicitly invoked.
|
|
351
|
+
|
|
352
|
+
### Security model — and its limits
|
|
353
|
+
|
|
354
|
+
Be clear about what this does and does not buy you:
|
|
355
|
+
|
|
356
|
+
- **Installing requires a human.** The approval gate checks whether a person is
|
|
357
|
+
actually reachable *before* asking, and refuses when nobody is. Under `-p`
|
|
358
|
+
there is no TUI and therefore no approver, so `skill_install` fails with an
|
|
359
|
+
actionable message instead of writing to disk unattended. Opt out
|
|
360
|
+
deliberately with `--skills-yes` or `skills.requireApproval=false`.
|
|
361
|
+
- **Project directories need trusting once.** Cloning an unfamiliar repository
|
|
362
|
+
does not grant it the right to inject instructions into your session.
|
|
363
|
+
- **Nothing is executed at install time.** No postinstall, no dependency
|
|
364
|
+
install; the executable bit is not preserved. Scripts run only when the model
|
|
365
|
+
invokes `bash`.
|
|
366
|
+
- **Archives are gated on real bytes** — total size, per-entry size, compression
|
|
367
|
+
ratio and entry count, all abortable mid-extraction. Path traversal,
|
|
368
|
+
absolute paths, symlinks and Windows device names are refused.
|
|
369
|
+
- **Git is never run through a shell**, and ref / owner / repo fragments are
|
|
370
|
+
whitelisted and refused if they start with `-`.
|
|
371
|
+
- **Third-party text cannot break out of its prompt block**: angle brackets in
|
|
372
|
+
descriptions, bodies and file names are neutralized before rendering.
|
|
373
|
+
|
|
374
|
+
- **A changed `SKILL.md` is noticed at load time.** Every install records a
|
|
375
|
+
sha256; discovery re-checks the one file that reaches the model's context and
|
|
376
|
+
flags it as `!modified` if it no longer matches what you approved. See
|
|
377
|
+
`skills.integrity` below.
|
|
378
|
+
- **Concurrent installs cannot corrupt a skill.** Writes take a per-directory
|
|
379
|
+
advisory lock, so two `aragon` processes installing at once queue instead of
|
|
380
|
+
interleaving their atomic-replace steps. *Limit:* the lock relies on exclusive
|
|
381
|
+
file creation, which is not reliably atomic on every NFS implementation. It
|
|
382
|
+
self-expires after 60 seconds and waits at most 5, so the worst case there is
|
|
383
|
+
no mutual exclusion — never a hang.
|
|
384
|
+
|
|
385
|
+
What it does **not** do: there is **no sandbox**. Approval blocks *silent*
|
|
386
|
+
installation; it does not make a skill you approved safe. A skill you accept can
|
|
387
|
+
contain adversarial instructions, and its scripts run with your full user
|
|
388
|
+
permissions — the same trust model as pasting text into the prompt and the same
|
|
389
|
+
as the `bash` tool. `/skills info` records provenance and a sha256 per file so
|
|
390
|
+
you can audit what arrived and notice if it changed. The integrity check answers
|
|
391
|
+
"is this still the file I approved?", **not** "is this file from someone I
|
|
392
|
+
trust" — there is no signing and no key distribution.
|
|
393
|
+
|
|
394
|
+
`allowed-tools` **is** enforced now (see below), but read the next section before
|
|
395
|
+
treating it as a permission boundary: it is blast-radius control, not a security
|
|
396
|
+
boundary, and a skill can widen its own ceiling by loading another skill.
|
|
397
|
+
|
|
398
|
+
### The tool ceiling (`allowed-tools`)
|
|
399
|
+
|
|
400
|
+
A skill that declares `allowed-tools` constrains which tools the model may use
|
|
401
|
+
**for the rest of the current turn**:
|
|
402
|
+
|
|
403
|
+
```yaml
|
|
404
|
+
allowed-tools: [read_file, write_file]
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
- **Scope is one turn.** The ceiling appears when a skill is loaded — by the
|
|
408
|
+
model calling `skill(...)`, or by you running `/<skill-name>` — and it is gone
|
|
409
|
+
as soon as you send the next message. A skill you used this morning does not
|
|
410
|
+
restrict unrelated work this afternoon.
|
|
411
|
+
- **Read-only tools are always allowed**, declared or not: `read_file`,
|
|
412
|
+
`list_dir`, `glob`, `grep`, plus `skill` and `skill_find`. What the declaration
|
|
413
|
+
actually controls is the five tools that change something — `write_file`,
|
|
414
|
+
`edit_file`, `bash`, `skill_install`, `skill_create`.
|
|
415
|
+
- **Claude Code names work.** `[Read, Bash, MultiEdit]` maps onto this host's
|
|
416
|
+
`read_file` / `bash` / `edit_file`. Tools that exist there but not here
|
|
417
|
+
(`WebFetch`, `Task`, `TodoWrite`, …) are ignored rather than treated as errors.
|
|
418
|
+
- **A name we cannot resolve waives the whole declaration**, and says so. One
|
|
419
|
+
typo means that skill imposes no ceiling at all — never a narrower one you did
|
|
420
|
+
not write. `aragon skills doctor` reports these up front.
|
|
421
|
+
- **`activation: always` skills never impose a ceiling.** They are ambient
|
|
422
|
+
reference material, not a procedure you chose to run.
|
|
423
|
+
|
|
424
|
+
Three modes, via `skills.toolPolicy` (default `enforce`),
|
|
425
|
+
`aragon --skill-tool-policy <mode>` for one run, or `/skills policy <mode>` for
|
|
426
|
+
the current session (which takes effect immediately, even mid-turn):
|
|
427
|
+
|
|
428
|
+
| Mode | Behaviour |
|
|
429
|
+
| --- | --- |
|
|
430
|
+
| `enforce` | The call is refused; the model is told what it may use instead. |
|
|
431
|
+
| `warn` | The call runs, and you get one notice per tool per turn. |
|
|
432
|
+
| `off` | No ceiling, and nothing about `allowed-tools` reaches the model. |
|
|
433
|
+
|
|
434
|
+
`/skills unload` drops the ceiling for the current turn. It does **not** remove
|
|
435
|
+
skill text already in the conversation — nothing can.
|
|
436
|
+
|
|
437
|
+
**What this is and is not.** It is **blast-radius control, not a security
|
|
438
|
+
boundary**. The declaration is written by the skill's own author, so a malicious
|
|
439
|
+
skill can simply declare `[bash]`. Its value is that a declaration is now
|
|
440
|
+
binding: you can read one in `/skills info` and decide whether to install, and an
|
|
441
|
+
honest author's `[read_file]` means a "while you're there, run `rm -rf`" line
|
|
442
|
+
smuggled into that skill's text — or into a file it tells the model to read —
|
|
443
|
+
cannot execute.
|
|
444
|
+
|
|
445
|
+
**And a known limit:** the ceiling is a **union** across every skill loaded this
|
|
446
|
+
turn, and `skill` is always available. So a skill can widen its own ceiling by
|
|
447
|
+
instructing the model to load a second, more permissive skill. This is not
|
|
448
|
+
blocked — blocking it would either strand the model inside a skill it cannot
|
|
449
|
+
escape, or break every multi-skill workflow — but every widening is printed to
|
|
450
|
+
the transcript:
|
|
451
|
+
|
|
452
|
+
```
|
|
453
|
+
Tool ceiling widened by skill "skill-creator": +write_file, +skill_create
|
|
454
|
+
```
|
|
455
|
+
|
|
456
|
+
So it stops the smuggled instruction; it does not stop a skill that deliberately
|
|
457
|
+
teaches the model to reach for another one.
|
|
458
|
+
|
|
459
|
+
### Usage counters
|
|
460
|
+
|
|
461
|
+
To rank the catalog, the CLI keeps a small file next to your skills directory:
|
|
462
|
+
|
|
463
|
+
```jsonc
|
|
464
|
+
// <aragon skills path>/../skill-usage.json
|
|
465
|
+
{ "schema": 1, "skills": { "deploy-preview": { "useCount": 12, "lastUsedAt": 1785050000000 } } }
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
**It contains the skill name, a count, and a timestamp. Nothing else** — no
|
|
469
|
+
arguments, no conversation content, no working directory, no machine identifier.
|
|
470
|
+
It is never transmitted anywhere, by anything. Delete it whenever you like; the
|
|
471
|
+
only effect is that the catalog falls back to plain scope-then-name order. Set
|
|
472
|
+
`skills.usageTracking=false` to switch it off entirely, after which the file is
|
|
473
|
+
neither read nor written.
|
|
474
|
+
|
|
475
|
+
Automatically injected `activation: always` skills are deliberately **not**
|
|
476
|
+
counted — they load on every turn, so counting them would say nothing about what
|
|
477
|
+
you actually use.
|
|
478
|
+
|
|
479
|
+
## Configuration
|
|
480
|
+
|
|
481
|
+
Config is layered (highest priority last): **defaults → user config file →
|
|
482
|
+
env / `.env` → CLI flags**.
|
|
483
|
+
|
|
484
|
+
- **Env / `.env`**: `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`,
|
|
485
|
+
`GOOGLE_API_KEY` / `GEMINI_API_KEY`, plus `ARAGON_PROVIDER`, `ARAGON_MODEL`,
|
|
486
|
+
`ARAGON_BASE_URL`, `ARAGON_THINKING`, `ARAGON_MAX_TOKENS`, `ARAGON_THEME`,
|
|
487
|
+
`ARAGON_FULLSCREEN`, `ARAGON_SKILLS` (`0` disables), `ARAGON_SKILLS_PATH`,
|
|
488
|
+
`ARAGON_SKILLS_DISABLED` (comma-separated names).
|
|
489
|
+
- **User config file** (`aragon config path`): JSON, written `0600` on POSIX.
|
|
490
|
+
Holds provider/model/keys and MRU lists. Keys are masked in the UI and never
|
|
491
|
+
logged.
|
|
492
|
+
|
|
493
|
+
| Config key | Default | Meaning |
|
|
494
|
+
| --- | --- | --- |
|
|
495
|
+
| `fullscreen` | `true` | Use the full-screen TUI (still subject to the automatic downgrades). Setting it to `false` opts out permanently; leaving it `true` is *not* a force — only `--fullscreen` / `ARAGON_FULLSCREEN=1` override the heuristics. |
|
|
496
|
+
| `exitTranscript` | `true` | Replay a plain-text session summary after exiting (full-screen only). |
|
|
497
|
+
| `transcriptWindow` | `300` | How many trailing entries the full-screen viewport renders, clamped to `[50, 2000]`. The full history stays in memory and in `/save`. |
|
|
498
|
+
| `skills.enabled` | `true` | Master switch for the skill system. |
|
|
499
|
+
| `skills.requireApproval` | `true` | Require a human to approve `skill_install` / `skill_create`. |
|
|
500
|
+
| `skills.catalogMaxBytes` | `6000` | Level 1 catalog ceiling, in UTF-8 **bytes**, clamped to `[500, 40000]`. |
|
|
501
|
+
| `skills.bodyMaxBytes` | `30000` | Level 2 body ceiling, in UTF-8 **bytes**, clamped to `[1000, 50000]`. |
|
|
502
|
+
| `skills.allowedHosts` | GitHub / GitLab hosts | Hosts an install **or update** may fetch from. Re-checked on every update, not just at install time. |
|
|
503
|
+
| `skills.disabled` | `[]` | Skill names switched off. |
|
|
504
|
+
| `skills.trustedProjectDirs` | `[]` | Project skill directories you have trusted. |
|
|
505
|
+
| `skills.integrity` | `warn` | What to do when `SKILL.md` no longer matches the copy you approved. `off` skips the check; `warn` flags it in `/skills list` and once per session; `strict` removes it from the catalog and makes `skill()` refuse it. Defaults to `warn` because editing an installed skill is a legitimate thing to do — `strict` suits CI and managed environments. |
|
|
506
|
+
| `skills.usageTracking` | `true` | Keep local per-skill use counters to rank the catalog. Name, count and timestamp only; never transmitted. Inspect with `aragon skills usage`, erase with `--reset --yes`. |
|
|
507
|
+
| `skills.toolPolicy` | `enforce` | How hard a skill's `allowed-tools` bites, for the turn it is loaded in. `off` disables it and says nothing about it to the model; `warn` runs the tool and reports it; `enforce` refuses. Unlike `skills.integrity`, a false positive here means a skill AUTHOR under-declared their own skill — which `aragon skills doctor` reports before it can bite. |
|
|
508
|
+
|
|
509
|
+
Byte, not character: `ToolExecutor` truncates at 100 000 **bytes**, so a
|
|
510
|
+
character-based budget would be roughly triple its nominal size for CJK text
|
|
511
|
+
and would get cut mid-tag.
|
|
512
|
+
|
|
513
|
+
These are additive: a config file written by an earlier version loads
|
|
514
|
+
unchanged, with no migration and no version bump.
|
|
515
|
+
|
|
516
|
+
### Theming & color
|
|
517
|
+
|
|
518
|
+
`--theme` / `config.theme` / `/theme` pick a palette: `warm`, `cool`, `light`, or
|
|
519
|
+
`auto` (which resolves to `warm` — terminals can't reliably report their
|
|
520
|
+
background). `dark` remains accepted everywhere as an alias for `cool`.
|
|
521
|
+
Colors degrade automatically to your terminal's depth (truecolor → 256 → 16 →
|
|
522
|
+
monochrome) and glyphs fall back to ASCII on terminals without Unicode. Color is
|
|
523
|
+
disabled — and the UI renders plain monochrome with ASCII glyphs — when any of
|
|
524
|
+
`NO_COLOR`, `--no-color`, `TERM=dumb`, or `config.color=false` is set; `FORCE_COLOR`
|
|
525
|
+
is respected. `reducedMotion` (also `ARAGON_REDUCED_MOTION`, and implied by
|
|
526
|
+
`--no-color`) replaces spinners with a static glyph.
|
|
527
|
+
|
|
528
|
+
### Timeout invariant
|
|
529
|
+
|
|
530
|
+
The core idle watchdog (60 s by default) is **shorter** than the tool timeout
|
|
531
|
+
(120 s), so a long single tool run (a build, `npm install`) would be killed
|
|
532
|
+
mid-run. The CLI fixes this by always constructing the agent with
|
|
533
|
+
`idleTimeout ≥ toolTimeout` (defaults 210 s ≥ 180 s), re-derived after config
|
|
534
|
+
merge. Raise both `--tool-timeout` / `--idle-timeout` for genuinely long jobs.
|
|
535
|
+
|
|
536
|
+
## Development
|
|
537
|
+
|
|
538
|
+
```bash
|
|
539
|
+
npm run build -w packages/cli # tsc + shebang → dist/cli.js
|
|
540
|
+
npm test -w packages/cli # vitest
|
|
541
|
+
node packages/cli/dist/cli.js --version
|
|
542
|
+
```
|
|
543
|
+
|
|
544
|
+
### Manual smoke checklist
|
|
545
|
+
|
|
546
|
+
- `npm run build -w packages/cli` produces an executable `dist/cli.js` with a
|
|
547
|
+
shebang; `node packages/cli/dist/cli.js --version` prints the version.
|
|
548
|
+
- `npm pack -w packages/cli --dry-run --json` lists `dist/cli.js` under `files`.
|
|
549
|
+
- With `ANTHROPIC_API_KEY` set: `aragon` streams a reply, a tool card renders,
|
|
550
|
+
`Esc` aborts mid-run, `/model` switches models, `/settings` saves a key,
|
|
551
|
+
`Ctrl+C` twice exits.
|
|
552
|
+
- `echo "list files" | node dist/cli.js -p` prints an answer and exits `0`.
|
|
553
|
+
- A missing/invalid key produces a visible, actionable message (never a blank
|
|
554
|
+
screen), both in the TUI and headless.
|
|
555
|
+
|
|
556
|
+
## License
|
|
557
|
+
|
|
558
|
+
MIT
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Streaming delta coalescer (spec §3.8). Pure: merges a run of consecutive
|
|
3
|
+
* `textDelta` / `thinkingDelta` view actions into a single action of the same
|
|
4
|
+
* kind while preserving order — any non-delta action (a tool call, `turnEnd`,
|
|
5
|
+
* `runEnd`, `notice`) flushes the pending run first, so `text, tool, text`
|
|
6
|
+
* stays `text, tool, text` and is never reordered.
|
|
7
|
+
*
|
|
8
|
+
* App buffers per-event actions and runs them through this before dispatching,
|
|
9
|
+
* so a fast token stream re-renders the transcript once per frame instead of
|
|
10
|
+
* once per token. The reducer and event semantics are unchanged.
|
|
11
|
+
*/
|
|
12
|
+
import type { ViewAction } from './reducer.js';
|
|
13
|
+
/** Merge consecutive same-kind deltas; flush on kind change or a non-delta. */
|
|
14
|
+
export declare function mergeDeltas(actions: ViewAction[]): ViewAction[];
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Streaming delta coalescer (spec §3.8). Pure: merges a run of consecutive
|
|
3
|
+
* `textDelta` / `thinkingDelta` view actions into a single action of the same
|
|
4
|
+
* kind while preserving order — any non-delta action (a tool call, `turnEnd`,
|
|
5
|
+
* `runEnd`, `notice`) flushes the pending run first, so `text, tool, text`
|
|
6
|
+
* stays `text, tool, text` and is never reordered.
|
|
7
|
+
*
|
|
8
|
+
* App buffers per-event actions and runs them through this before dispatching,
|
|
9
|
+
* so a fast token stream re-renders the transcript once per frame instead of
|
|
10
|
+
* once per token. The reducer and event semantics are unchanged.
|
|
11
|
+
*/
|
|
12
|
+
function isDelta(action) {
|
|
13
|
+
return action.type === 'textDelta' || action.type === 'thinkingDelta';
|
|
14
|
+
}
|
|
15
|
+
/** Merge consecutive same-kind deltas; flush on kind change or a non-delta. */
|
|
16
|
+
export function mergeDeltas(actions) {
|
|
17
|
+
const out = [];
|
|
18
|
+
let pendingType = null;
|
|
19
|
+
let pendingDelta = '';
|
|
20
|
+
const flush = () => {
|
|
21
|
+
if (pendingType !== null) {
|
|
22
|
+
out.push({ type: pendingType, delta: pendingDelta });
|
|
23
|
+
pendingType = null;
|
|
24
|
+
pendingDelta = '';
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
for (const action of actions) {
|
|
28
|
+
if (isDelta(action)) {
|
|
29
|
+
if (pendingType !== null && pendingType !== action.type)
|
|
30
|
+
flush();
|
|
31
|
+
pendingType = action.type;
|
|
32
|
+
pendingDelta += action.delta;
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
flush();
|
|
36
|
+
out.push(action);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
flush();
|
|
40
|
+
return out;
|
|
41
|
+
}
|