@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.
Files changed (156) hide show
  1. package/CHANGELOG.md +292 -0
  2. package/README.md +558 -0
  3. package/dist/agent/coalesce.d.ts +14 -0
  4. package/dist/agent/coalesce.js +41 -0
  5. package/dist/agent/controller.d.ts +132 -0
  6. package/dist/agent/controller.js +332 -0
  7. package/dist/agent/headless.d.ts +22 -0
  8. package/dist/agent/headless.js +74 -0
  9. package/dist/agent/reducer.d.ts +181 -0
  10. package/dist/agent/reducer.js +420 -0
  11. package/dist/agent/system-prompt.d.ts +30 -0
  12. package/dist/agent/system-prompt.js +58 -0
  13. package/dist/agent/usage.d.ts +13 -0
  14. package/dist/agent/usage.js +41 -0
  15. package/dist/cli.d.ts +11 -0
  16. package/dist/cli.js +482 -0
  17. package/dist/commands/builtins.d.ts +16 -0
  18. package/dist/commands/builtins.js +223 -0
  19. package/dist/commands/registry.d.ts +48 -0
  20. package/dist/commands/registry.js +45 -0
  21. package/dist/commands/skills.d.ts +22 -0
  22. package/dist/commands/skills.js +625 -0
  23. package/dist/config/app-paths.d.ts +11 -0
  24. package/dist/config/app-paths.js +16 -0
  25. package/dist/config/env.d.ts +28 -0
  26. package/dist/config/env.js +92 -0
  27. package/dist/config/load.d.ts +48 -0
  28. package/dist/config/load.js +158 -0
  29. package/dist/config/migrate-legacy-state.d.ts +78 -0
  30. package/dist/config/migrate-legacy-state.js +181 -0
  31. package/dist/config/schema.d.ts +249 -0
  32. package/dist/config/schema.js +247 -0
  33. package/dist/config/store.d.ts +44 -0
  34. package/dist/config/store.js +126 -0
  35. package/dist/input/keymap.d.ts +20 -0
  36. package/dist/input/keymap.js +85 -0
  37. package/dist/session/persist.d.ts +24 -0
  38. package/dist/session/persist.js +51 -0
  39. package/dist/skills/archive.d.ts +41 -0
  40. package/dist/skills/archive.js +152 -0
  41. package/dist/skills/cli-commands.d.ts +52 -0
  42. package/dist/skills/cli-commands.js +502 -0
  43. package/dist/skills/fetch-source.d.ts +104 -0
  44. package/dist/skills/fetch-source.js +448 -0
  45. package/dist/skills/installer.d.ts +117 -0
  46. package/dist/skills/installer.js +414 -0
  47. package/dist/skills/lock.d.ts +75 -0
  48. package/dist/skills/lock.js +223 -0
  49. package/dist/skills/manifest.d.ts +47 -0
  50. package/dist/skills/manifest.js +95 -0
  51. package/dist/skills/node-host.d.ts +23 -0
  52. package/dist/skills/node-host.js +59 -0
  53. package/dist/skills/paths.d.ts +61 -0
  54. package/dist/skills/paths.js +107 -0
  55. package/dist/skills/service.d.ts +256 -0
  56. package/dist/skills/service.js +730 -0
  57. package/dist/skills/tools.d.ts +35 -0
  58. package/dist/skills/tools.js +164 -0
  59. package/dist/skills/updater.d.ts +101 -0
  60. package/dist/skills/updater.js +330 -0
  61. package/dist/skills/usage.d.ts +72 -0
  62. package/dist/skills/usage.js +216 -0
  63. package/dist/tools/bash-tool.d.ts +12 -0
  64. package/dist/tools/bash-tool.js +131 -0
  65. package/dist/tools/diff.d.ts +15 -0
  66. package/dist/tools/diff.js +50 -0
  67. package/dist/tools/fs-tools.d.ts +16 -0
  68. package/dist/tools/fs-tools.js +215 -0
  69. package/dist/tools/index.d.ts +93 -0
  70. package/dist/tools/index.js +157 -0
  71. package/dist/tools/search-tools.d.ts +11 -0
  72. package/dist/tools/search-tools.js +225 -0
  73. package/dist/ui/App.d.ts +24 -0
  74. package/dist/ui/App.js +522 -0
  75. package/dist/ui/AutocompletePopup.d.ts +21 -0
  76. package/dist/ui/AutocompletePopup.js +19 -0
  77. package/dist/ui/Composer.d.ts +40 -0
  78. package/dist/ui/Composer.js +61 -0
  79. package/dist/ui/Header.d.ts +30 -0
  80. package/dist/ui/Header.js +25 -0
  81. package/dist/ui/Logo.d.ts +50 -0
  82. package/dist/ui/Logo.js +85 -0
  83. package/dist/ui/Markdown.d.ts +23 -0
  84. package/dist/ui/Markdown.js +122 -0
  85. package/dist/ui/PromptInput.d.ts +81 -0
  86. package/dist/ui/PromptInput.js +383 -0
  87. package/dist/ui/SessionOpener.d.ts +30 -0
  88. package/dist/ui/SessionOpener.js +12 -0
  89. package/dist/ui/StatusBar.d.ts +39 -0
  90. package/dist/ui/StatusBar.js +43 -0
  91. package/dist/ui/ToastStack.d.ts +24 -0
  92. package/dist/ui/ToastStack.js +38 -0
  93. package/dist/ui/Transcript.d.ts +62 -0
  94. package/dist/ui/Transcript.js +128 -0
  95. package/dist/ui/capabilities.d.ts +25 -0
  96. package/dist/ui/capabilities.js +59 -0
  97. package/dist/ui/console-bridge.d.ts +19 -0
  98. package/dist/ui/console-bridge.js +52 -0
  99. package/dist/ui/density.d.ts +26 -0
  100. package/dist/ui/density.js +34 -0
  101. package/dist/ui/entries/AssistantEntry.d.ts +23 -0
  102. package/dist/ui/entries/AssistantEntry.js +13 -0
  103. package/dist/ui/entries/EntryFrame.d.ts +23 -0
  104. package/dist/ui/entries/EntryFrame.js +6 -0
  105. package/dist/ui/entries/ToolCard.d.ts +38 -0
  106. package/dist/ui/entries/ToolCard.js +72 -0
  107. package/dist/ui/entries/ToolPreview.d.ts +57 -0
  108. package/dist/ui/entries/ToolPreview.js +144 -0
  109. package/dist/ui/entries/UserEntry.d.ts +14 -0
  110. package/dist/ui/entries/UserEntry.js +5 -0
  111. package/dist/ui/exit-snapshot.d.ts +27 -0
  112. package/dist/ui/exit-snapshot.js +26 -0
  113. package/dist/ui/gauge.d.ts +24 -0
  114. package/dist/ui/gauge.js +36 -0
  115. package/dist/ui/glyphs.d.ts +87 -0
  116. package/dist/ui/glyphs.js +127 -0
  117. package/dist/ui/gradient.d.ts +11 -0
  118. package/dist/ui/gradient.js +50 -0
  119. package/dist/ui/layout/AppShell.d.ts +24 -0
  120. package/dist/ui/layout/AppShell.js +9 -0
  121. package/dist/ui/layout/Gutter.d.ts +67 -0
  122. package/dist/ui/layout/Gutter.js +42 -0
  123. package/dist/ui/layout/OverlayFrame.d.ts +62 -0
  124. package/dist/ui/layout/OverlayFrame.js +92 -0
  125. package/dist/ui/layout/ScrollViewport.d.ts +33 -0
  126. package/dist/ui/layout/ScrollViewport.js +71 -0
  127. package/dist/ui/layout/budget.d.ts +42 -0
  128. package/dist/ui/layout/budget.js +47 -0
  129. package/dist/ui/layout/frame.d.ts +58 -0
  130. package/dist/ui/layout/frame.js +75 -0
  131. package/dist/ui/layout/overlay-window.d.ts +43 -0
  132. package/dist/ui/layout/overlay-window.js +51 -0
  133. package/dist/ui/layout/scroll.d.ts +28 -0
  134. package/dist/ui/layout/scroll.js +76 -0
  135. package/dist/ui/layout/useTerminalSize.d.ts +13 -0
  136. package/dist/ui/layout/useTerminalSize.js +50 -0
  137. package/dist/ui/markdown-blocks.d.ts +62 -0
  138. package/dist/ui/markdown-blocks.js +120 -0
  139. package/dist/ui/overlays/ConfirmDialog.d.ts +30 -0
  140. package/dist/ui/overlays/ConfirmDialog.js +25 -0
  141. package/dist/ui/overlays/HelpOverlay.d.ts +17 -0
  142. package/dist/ui/overlays/HelpOverlay.js +60 -0
  143. package/dist/ui/overlays/ModelPicker.d.ts +29 -0
  144. package/dist/ui/overlays/ModelPicker.js +29 -0
  145. package/dist/ui/overlays/SettingsScreen.d.ts +37 -0
  146. package/dist/ui/overlays/SettingsScreen.js +106 -0
  147. package/dist/ui/palettes.d.ts +79 -0
  148. package/dist/ui/palettes.js +115 -0
  149. package/dist/ui/screen.d.ts +27 -0
  150. package/dist/ui/screen.js +57 -0
  151. package/dist/ui/theme.d.ts +78 -0
  152. package/dist/ui/theme.js +127 -0
  153. package/dist/ui/transcript-text.d.ts +27 -0
  154. package/dist/ui/transcript-text.js +110 -0
  155. package/package.json +73 -0
  156. 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
+ }