command-code 1.82.5 → 1.84.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/CHANGELOG.md +18 -0
- package/dist/bundled/command-code-knowledge/live-models.ts +73 -16
- package/dist/bundled/command-code-knowledge/reference/custom-slash-commands.md +125 -0
- package/dist/bundled/command-code-knowledge/reference/product-help.md +3 -0
- package/dist/bundled/mod-builder/SKILL.md +1 -0
- package/dist/bundled/mod-builder/examples/model-router.ts +43 -0
- package/dist/bundled/mod-builder/examples/pane-and-buttons.ts +60 -0
- package/dist/bundled/mod-builder/examples/pane-form.ts +68 -0
- package/dist/bundled/mod-builder/examples/permission-policy.ts +34 -0
- package/dist/bundled/mod-builder/examples/redact-secrets.ts +46 -0
- package/dist/bundled/mod-builder/examples/spinner-count.ts +47 -0
- package/dist/bundled/mod-builder/examples/stream-filter.ts +70 -0
- package/dist/bundled/mod-builder/reference/api.md +40 -5
- package/dist/bundled/mod-builder/reference/hooks-and-events.md +37 -1
- package/dist/bundled/mod-builder/reference/overview.md +19 -4
- package/dist/bundled/mod-builder/reference/packaging.md +14 -1
- package/dist/bundled/mod-builder/reference/ui.md +192 -20
- package/dist/bundled/mod-builder/reference/verify.md +7 -3
- package/dist/cli.mjs +5 -5
- package/package.json +1 -1
- package/vsix/commandcode-vscode.vsix +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# command-code
|
|
2
2
|
|
|
3
|
+
## 1.84.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- feat: Add `/statusline` to customize what shows on the line under the input
|
|
8
|
+
|
|
9
|
+
## 1.83.0
|
|
10
|
+
|
|
11
|
+
### Minor Changes
|
|
12
|
+
|
|
13
|
+
- feat: Planning and Implementation models follow the live model catalog
|
|
14
|
+
- feat: mods get panes, widgets, toasts, hot reload, permission answers, transcript redaction, per-request model routing
|
|
15
|
+
- feat: mods can use `cmd.state` / `cmd.store` / `cmd.clock` / `cmd.model` / `cmd.prompt`, sub-agent types, `/mods`, `cmd mods inspect`, and `--safe-mode`
|
|
16
|
+
- feat: panes host `cmd.ui` input, select, and markdown elements with a keyboard focus model (Alt+↑, Tab, Esc), docking beside the prompt on wide terminals
|
|
17
|
+
- feat: mods can copy to the clipboard with `cmd.ui.copy` and play sounds with `cmd.audio.play` / `cmd.audio.bell`
|
|
18
|
+
- feat: mods can rewrite the model's answer as it streams via `hooks.transformTextDelta` (committed text matches what was shown) and stream a child process's output with `cmd.process.spawn`
|
|
19
|
+
- feat: mods can restyle built-in TUI rows with `cmd.ui.override` (spinner, tool rows, user messages, command output), never the permission prompt
|
|
20
|
+
|
|
3
21
|
## 1.82.5
|
|
4
22
|
|
|
5
23
|
### Patch Changes
|
|
@@ -81,7 +81,39 @@ function changedSinceBuild(model: CatalogModelV1): boolean {
|
|
|
81
81
|
);
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
-
|
|
84
|
+
export interface LiveModelSessionModel {
|
|
85
|
+
readonly id: string;
|
|
86
|
+
// The catalog the session model runs on: the session pin, which may be older than the section's.
|
|
87
|
+
readonly catalog: EffectiveModelCatalog;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// A row answers to its id and its aliases, as the catalog resolves ids.
|
|
91
|
+
function rowNames(row: CatalogModelV1, id: string): boolean {
|
|
92
|
+
return row.id === id || row.aliases.includes(id);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// The session model's row from the catalog it runs on, so the section never tells the model a
|
|
96
|
+
// window or effort levels the session is not running on; any other model's row as served.
|
|
97
|
+
interface ServedRowParams {
|
|
98
|
+
readonly model: CatalogModelV1;
|
|
99
|
+
readonly sessionModel?: LiveModelSessionModel;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function servedRow(params: ServedRowParams): CatalogModelV1 {
|
|
103
|
+
const {model, sessionModel} = params;
|
|
104
|
+
if (!sessionModel || !rowNames(model, sessionModel.id)) return model;
|
|
105
|
+
return (
|
|
106
|
+
sessionModel.catalog.catalog.models.find(row =>
|
|
107
|
+
rowNames(row, sessionModel.id),
|
|
108
|
+
) ?? model
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function bucketModels(params: {
|
|
113
|
+
readonly effective: EffectiveModelCatalog;
|
|
114
|
+
readonly sessionModel?: LiveModelSessionModel;
|
|
115
|
+
}): ModelBuckets {
|
|
116
|
+
const {effective, sessionModel} = params;
|
|
85
117
|
const known = listSelectableModelIds({catalog: builtCatalog()});
|
|
86
118
|
const knownSet = new Set(known);
|
|
87
119
|
const added: CatalogModelV1[] = [];
|
|
@@ -97,11 +129,12 @@ function bucketModels(effective: EffectiveModelCatalog): ModelBuckets {
|
|
|
97
129
|
continue;
|
|
98
130
|
}
|
|
99
131
|
if (state !== 'selectable') continue;
|
|
100
|
-
|
|
101
|
-
|
|
132
|
+
const row = servedRow({model, sessionModel});
|
|
133
|
+
if (!knownSet.has(row.id)) {
|
|
134
|
+
added.push(row);
|
|
102
135
|
continue;
|
|
103
136
|
}
|
|
104
|
-
if (changedSinceBuild(
|
|
137
|
+
if (changedSinceBuild(row)) changed.push(row);
|
|
105
138
|
}
|
|
106
139
|
// A shipped id the catalog no longer carries at all (no row, no alias), or now marks update
|
|
107
140
|
// required, is refused by /model and sub-agents too.
|
|
@@ -119,6 +152,8 @@ function bucketModels(effective: EffectiveModelCatalog): ModelBuckets {
|
|
|
119
152
|
|
|
120
153
|
export interface BuildLiveModelSectionParams {
|
|
121
154
|
readonly catalog?: EffectiveModelCatalog;
|
|
155
|
+
// The session's model and its pin, when the pin knows it: that row wins over `catalog`'s.
|
|
156
|
+
readonly sessionModel?: LiveModelSessionModel;
|
|
122
157
|
}
|
|
123
158
|
|
|
124
159
|
/**
|
|
@@ -132,7 +167,10 @@ export function buildLiveModelSection(
|
|
|
132
167
|
const effective =
|
|
133
168
|
params.catalog ?? getInstalledModelCatalogManager()?.getEffective();
|
|
134
169
|
if (!effective) return '';
|
|
135
|
-
const {added, hidden, outdated, changed} = bucketModels(
|
|
170
|
+
const {added, hidden, outdated, changed} = bucketModels({
|
|
171
|
+
effective,
|
|
172
|
+
sessionModel: params.sessionModel,
|
|
173
|
+
});
|
|
136
174
|
if (added.length + hidden.length + outdated.length + changed.length === 0)
|
|
137
175
|
return '';
|
|
138
176
|
const parts = [
|
|
@@ -166,23 +204,42 @@ export function buildLiveModelSection(
|
|
|
166
204
|
return parts.join('\n\n');
|
|
167
205
|
}
|
|
168
206
|
|
|
207
|
+
// One built section, keyed by the inputs it was built from.
|
|
208
|
+
interface LiveModelSectionMemo {
|
|
209
|
+
readonly catalog: EffectiveModelCatalog;
|
|
210
|
+
readonly sessionModel: LiveModelSessionModel | undefined;
|
|
211
|
+
readonly text: string;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function sameSessionModel(
|
|
215
|
+
left: LiveModelSessionModel | undefined,
|
|
216
|
+
right: LiveModelSessionModel | undefined,
|
|
217
|
+
): boolean {
|
|
218
|
+
return left?.id === right?.id && left?.catalog === right?.catalog;
|
|
219
|
+
}
|
|
220
|
+
|
|
169
221
|
/**
|
|
170
|
-
* The section built once per catalog `
|
|
171
|
-
* pin came from the live catalog, else the newest catalog
|
|
172
|
-
* tool, a `/command-code-knowledge` turn, an
|
|
173
|
-
* lands or /model re-pins.
|
|
222
|
+
* The section built once per catalog and session model `getParams` answers with. A session hands in
|
|
223
|
+
* its pin when the pin came from the live catalog, else the newest catalog, plus its own model held
|
|
224
|
+
* to the pin — so every activation (the activate_skill tool, a `/command-code-knowledge` turn, an
|
|
225
|
+
* inline mention) reads the same text until a catalog lands or /model re-pins.
|
|
174
226
|
*/
|
|
175
227
|
export function createLiveModelSectionCache(
|
|
176
|
-
|
|
228
|
+
getParams: () => BuildLiveModelSectionParams,
|
|
177
229
|
): () => string {
|
|
178
|
-
let cached:
|
|
179
|
-
| {readonly catalog: EffectiveModelCatalog; readonly text: string}
|
|
180
|
-
| undefined;
|
|
230
|
+
let cached: LiveModelSectionMemo | undefined;
|
|
181
231
|
return () => {
|
|
182
|
-
const catalog =
|
|
232
|
+
const {catalog, sessionModel} = getParams();
|
|
183
233
|
if (!catalog) return '';
|
|
184
|
-
if (
|
|
185
|
-
cached
|
|
234
|
+
if (
|
|
235
|
+
cached?.catalog !== catalog ||
|
|
236
|
+
!sameSessionModel(cached.sessionModel, sessionModel)
|
|
237
|
+
) {
|
|
238
|
+
cached = {
|
|
239
|
+
catalog,
|
|
240
|
+
sessionModel,
|
|
241
|
+
text: buildLiveModelSection({catalog, sessionModel}),
|
|
242
|
+
};
|
|
186
243
|
}
|
|
187
244
|
return cached.text;
|
|
188
245
|
};
|
|
@@ -38,6 +38,7 @@ Every built-in command, grouped by area. Aliases are shown in parentheses on the
|
|
|
38
38
|
| `/unshare` | Stop sharing conversation |
|
|
39
39
|
| `/exit` (`/quit`) | Exit Command Code |
|
|
40
40
|
| `/reload` | Restart Command Code and resume this session (applies a staged update) |
|
|
41
|
+
| `/mods` | List loaded [mods](https://commandcode.ai/docs/mods) and panes; `/mods reload [name]` hot-reloads, `/mods open\|close <pane>` |
|
|
41
42
|
|
|
42
43
|
### Context & compaction
|
|
43
44
|
|
|
@@ -121,6 +122,7 @@ Every built-in command, grouped by area. Aliases are shown in parentheses on the
|
|
|
121
122
|
| `/config [query]` | Search and change settings |
|
|
122
123
|
| `/theme [dark\|light\|auto]` | Switch themes; `auto` matches your terminal background |
|
|
123
124
|
| `/status` | Show comprehensive environment status |
|
|
125
|
+
| `/statusline [off\|on\|reset]` | Configure Command Code's statusline UI (terminal only) |
|
|
124
126
|
| `/changelog [all]` | See what's new in Command Code (`all` for the full history) |
|
|
125
127
|
| `/learn` | Open the Command Code learn hub in your browser |
|
|
126
128
|
| `/update` | Update Command Code to the latest version |
|
|
@@ -152,6 +154,7 @@ Most commands are **blocked mid-turn** - mode flips, session switches, model cha
|
|
|
152
154
|
- `/session-file` - read-only session facts
|
|
153
155
|
- `/copy` - clipboard only
|
|
154
156
|
- `/todos` (also `ctrl+x`) - edits local todo state, most useful mid-turn while a plan is being driven (needs the `todos` mod enabled)
|
|
157
|
+
- `/statusline off`, `/statusline on`, `/statusline reset` - a local display preference (bare `/statusline` opens its editor once the turn ends)
|
|
155
158
|
- `/exit` / `/quit` - always available
|
|
156
159
|
- `/goal clear` - stops the goal loop; the current step finishes, then the loop stops. Setting a **new** goal mid-turn is refused with a busy notice.
|
|
157
160
|
|
|
@@ -165,6 +168,128 @@ Most other commands - `/clear`, `/mode`, `/plan`, `/model`, `/compact`, `/worktr
|
|
|
165
168
|
|
|
166
169
|
`/loop <interval> <task>` runs the task now and repeats it on a fixed cadence such as `5m`, `2h`, `1d`, or a trailing `every 2 hours`. `/loop <task>` runs it now and lets the agent pick each next delay, between one minute and one hour. Bare `/loop` runs the default maintenance task, which a `.commandcode/loop.md` file replaces. Esc cancels the conversation's loops. The [`/loop`](https://commandcode.ai/docs/loop) page has the normalization rules, `loop.md`, and resume behavior.
|
|
167
170
|
|
|
171
|
+
### `/statusline` in detail
|
|
172
|
+
|
|
173
|
+
The status line is the row directly under the input. It stays visible while you type and steps aside only for the slash menu, the `@` file list, the slash hint, and a pending Ctrl+C exit:
|
|
174
|
+
|
|
175
|
+
```text
|
|
176
|
+
> fix the parser
|
|
177
|
+
branch: main · mymod: ok
|
|
178
|
+
? for shortcuts · PR #5329 · taste on
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
Bare `/statusline` slash command opens the statusline editor. Select items from the list to add to the statusline. An item with nothing to show right now says why in brackets beside its label, for example `Git branch (not on a git branch)`, instead of showing a placeholder.
|
|
182
|
+
|
|
183
|
+
These items sit in the row, in the order you choose. Items marked *off* are unchecked by default:
|
|
184
|
+
|
|
185
|
+
| **Item** | **Shows** |
|
|
186
|
+
| ----------------- | ---------------------------------------------------------------------------------- |
|
|
187
|
+
| Model | This session's model (*off*) |
|
|
188
|
+
| Model + reasoning | The model and its reasoning level, e.g. `Opus 4.7 with high effort` (*off*) |
|
|
189
|
+
| Reasoning level | The reasoning level alone, e.g. `high effort` (*off*) |
|
|
190
|
+
| Current directory | The working directory, e.g. `~/work/app` (*off*) |
|
|
191
|
+
| Git branch | The checked-out branch, e.g. `branch: main`; re-read after each turn (*off*) |
|
|
192
|
+
| Session name | The session's name, from `/rename` or the generated title (*off*) |
|
|
193
|
+
| Version | The Command Code version, e.g. `v1.77.0` (*off*) |
|
|
194
|
+
| Phase model | The planning or implementation model, while one is in charge; always at the right end of the row |
|
|
195
|
+
|
|
196
|
+
These items keep their own place under the input; you can only turn them on or off. All are on by default:
|
|
197
|
+
|
|
198
|
+
| **Item** | **Controls** |
|
|
199
|
+
| ----------------- | ---------------------------------------------------------------------------------- |
|
|
200
|
+
| Permission mode | The accept-edits and plan mode badges. Bypass and dont-ask always show |
|
|
201
|
+
| IDE context | The IDE file or selection and the IDE connection indicator |
|
|
202
|
+
| Pull request | The open or merged PR badge for this branch, as a link |
|
|
203
|
+
| Taste learning | `taste on` or `taste off` (`taste off (byok)` when a BYOK session skips learning) |
|
|
204
|
+
| Background tasks | Running shells and monitors; press `↓` to manage them |
|
|
205
|
+
| Shortcut hints | `? for shortcuts` and the paste hint. The stash badge shows whenever a prompt is stashed |
|
|
206
|
+
|
|
207
|
+
The Ctrl+C exit prompt, credit warnings, and update, share and taste notices always show.
|
|
208
|
+
|
|
209
|
+
| **Key** | **Action** |
|
|
210
|
+
| ------------------ | ---------------------------------------------------------- |
|
|
211
|
+
| `↑` / `↓` | Move between items |
|
|
212
|
+
| `Space` / `Enter` | Check or uncheck the focused item |
|
|
213
|
+
| `←` / `→` | Move the focused item left or right in the row (only row items move; Phase model stays at the right end) |
|
|
214
|
+
| `Esc` | Save your changes and close (just close if nothing changed; during a save, close at once; after another session's change, discard yours) |
|
|
215
|
+
| `Ctrl+C` | Close without saving |
|
|
216
|
+
|
|
217
|
+
Unchecked items keep their place, so checking one again puts it back where it was. Nothing changes under the input until Esc saves the layout. If the write fails, the editor stays open with your changes, and Esc tries again; if another session changed the layout while you were editing, it never overwrites it: Enter reloads the saved version, Esc discards your changes. Closing the editor gives you back the prompt you were writing.
|
|
218
|
+
|
|
219
|
+
Output from [mods](https://commandcode.ai/docs/mods) (`cmd.ui.setStatus`) always appears after the built-ins, and before the phase model at the right end, in the order the mods loaded. Unchecking every built-in still shows mod output; `/statusline off` is the way to hide the whole row.
|
|
220
|
+
|
|
221
|
+
#### Add your own items with mods
|
|
222
|
+
|
|
223
|
+
The editor lists only the built-in items. To show anything else in the row, such as a CI result, a ticket number or a count, use a [mod](https://commandcode.ai/docs/mods) that calls `cmd.ui.setStatus(text)`. Each mod gets one segment: a new call replaces its text, and `null` removes it. You don't need to write the mod yourself; describe what you want and Command Code builds it:
|
|
224
|
+
|
|
225
|
+
```text copy filename="Prompt"
|
|
226
|
+
Build me a mod that shows how many files I've changed in this git repo in my status line, in yellow, and updates it after every turn. Show nothing when the tree is clean.
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
Command Code writes something like this and saves it in `~/.commandcode/mods/changed-files.ts`, where it loads in every session:
|
|
230
|
+
|
|
231
|
+
```ts copy filename="~/.commandcode/mods/changed-files.ts"
|
|
232
|
+
import type {ModApi} from '@commandcode/harness';
|
|
233
|
+
|
|
234
|
+
export default function (cmd: ModApi): void {
|
|
235
|
+
const refresh = async (): Promise<void> => {
|
|
236
|
+
const {stdout, code} = await cmd.exec({command: 'git', args: ['status', '--porcelain']});
|
|
237
|
+
const count = stdout.split('\n').filter(Boolean).length;
|
|
238
|
+
// null clears the segment: nothing shows on a clean tree or outside a repo.
|
|
239
|
+
cmd.ui.setStatus(code === 0 && count > 0 ? `\u001b[33m✎ ${count} changed\u001b[39m` : null);
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
cmd.hooks({onSessionStart: refresh});
|
|
243
|
+
cmd.on('turn_end', () => void refresh());
|
|
244
|
+
}
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
The next session shows it in the row:
|
|
248
|
+
|
|
249
|
+
```text
|
|
250
|
+
> fix the parser
|
|
251
|
+
✎ 2 changed
|
|
252
|
+
? for shortcuts · PR #5329 · taste on
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
To try a mod without installing it, run `cmd --mod ./changed-files.ts`; after editing an installed one, run `/reload`. Mod text can use ANSI colors and OSC 8 links; other escape codes are removed. To hide mod output but keep the built-ins, set `legacy-mods` to `"enabled": false` in `config.json` (see below). The [UI surface](https://commandcode.ai/docs/mods#ui-surface) section of the mods page has the full `setStatus` contract.
|
|
256
|
+
|
|
257
|
+
| **Command** | **Effect** |
|
|
258
|
+
| -------------------- | --------------------------------------------------------------------------------- |
|
|
259
|
+
| `/statusline` | Open the editor |
|
|
260
|
+
| `/statusline off` | Hide the whole row, mod output included. Your selection is kept, and the on/off-only items above are unaffected |
|
|
261
|
+
| `/statusline on` | Show the row again with your saved selection, or the default one |
|
|
262
|
+
| `/statusline reset` | Forget your layout and go back to the default, in the order above |
|
|
263
|
+
|
|
264
|
+
Your items always get exactly one line under the input, in the order you set. When they don't fit your terminal, the line is cut with `…` at the edge rather than wrapping or reordering. The phase model sits at the right end of that line when it fits beside it, otherwise at the right end of the line below.
|
|
265
|
+
|
|
266
|
+
The layout is saved in `~/.commandcode/config.json` under `statusline`. You rarely need to edit it by hand:
|
|
267
|
+
|
|
268
|
+
```json
|
|
269
|
+
{
|
|
270
|
+
"statusline": {
|
|
271
|
+
"version": 1,
|
|
272
|
+
"enabled": true,
|
|
273
|
+
"items": [
|
|
274
|
+
{"id": "model", "enabled": false},
|
|
275
|
+
{"id": "pull-request", "enabled": true},
|
|
276
|
+
{"id": "taste", "enabled": true},
|
|
277
|
+
{"id": "phase-model", "enabled": true},
|
|
278
|
+
{"id": "background-tasks", "enabled": true},
|
|
279
|
+
{"id": "git-branch", "enabled": true},
|
|
280
|
+
{"id": "permission-mode", "enabled": true},
|
|
281
|
+
{"id": "legacy-mods", "enabled": true}
|
|
282
|
+
]
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
`items` sets the order, except that `phase-model` always draws at the right end. `enabled: false` on an item unchecks it; `enabled: false` at the top level is `/statusline off`. An item missing from `items` is unchecked, except the on/off-only items (`permission-mode`, `ide`, `pull-request`, `taste`, `background-tasks`, `hints`), which stay on. Other ids: `model-reasoning`, `reasoning`, `cwd`, `session-name`, `version`. `legacy-mods` is where mod output goes; leave it out (or set `"items": []`) and mod output is hidden.
|
|
288
|
+
|
|
289
|
+
A layout saved by a newer version of Command Code is never overwritten: the row uses the default layout and the editor opens read-only, until you update or run `/statusline reset`. A `statusline` value that isn't valid is also left untouched and shown read-only; fix or remove the `statusline` key, or run `/statusline reset`. If `config.json` itself isn't valid JSON, nothing can save the layout, `/statusline reset` included, until you fix or remove that file.
|
|
290
|
+
|
|
291
|
+
The row doesn't watch the file. A hand edit, or a save from another Command Code window, shows up the next time you open `/statusline`, run `/statusline off`, `on` or `reset`, or restart.
|
|
292
|
+
|
|
168
293
|
### `/rc` in detail
|
|
169
294
|
|
|
170
295
|
Bare `/rc` opens the picker: platform, setup, permission mode. `/rc default`, `/rc auto-accept` and `/rc yolo` connect straight into a mode. `/rc status` reports what is on, `/rc off` disconnects, and `/rc off clear` also deletes the bot's own messages. `/rc` is refused when the model asks for it and when it arrives from the chat, so the bot cannot change its own token or allowlist. See [`/rc`](https://commandcode.ai/docs/remote-control) for setup and what the chat can send.
|
|
@@ -68,11 +68,13 @@
|
|
|
68
68
|
- /pr-comments: Fetch all PR comments for current branch
|
|
69
69
|
- /add-dir: Manage additional directory scope
|
|
70
70
|
- /status: Show comprehensive environment status
|
|
71
|
+
- /statusline [off|on|reset]: Configure Command Code's statusline UI: pick and order the row under the input (model, reasoning, phase model, directory, git branch, session name, version) and turn the mode badge, IDE context, PR, taste, background tasks and shortcut hints on or off; mod status follows. Terminal only
|
|
71
72
|
- /usage: Display credits, plan, and usage metrics
|
|
72
73
|
- /upgrade: Open billing page to upgrade your plan
|
|
73
74
|
- /extra: Buy extra usage at API cost, zero markup
|
|
74
75
|
- /update: Update Command Code to the latest version
|
|
75
76
|
- /reload: Restart Command Code and resume this session (applies a staged update)
|
|
77
|
+
- /mods: List loaded mods and panes; /mods reload [name] hot-reloads, /mods open|close <pane>
|
|
76
78
|
- /help: Display help information
|
|
77
79
|
- /exit: Exit Command Code
|
|
78
80
|
- /quit: Exit Command Code (alias of /exit)
|
|
@@ -139,6 +141,7 @@
|
|
|
139
141
|
- --base <branch>: Base branch for a new cloud session
|
|
140
142
|
- --mod <path>: Load a mod file or directory for this session (repeatable)
|
|
141
143
|
- --mod-option <name=value>: Set a mod-declared flag value (repeatable)
|
|
144
|
+
- --safe-mode: Load built-in mods only for this session (no installed mods, no --mod)
|
|
142
145
|
- --skill <path>: Load extra skills from a path (a skill directory or a directory of skills); repeatable
|
|
143
146
|
- --no-skills: Skip skill discovery; paths given via --skill still load
|
|
144
147
|
- --skip-onboarding: Skip taste onboarding (for automated runs)
|
|
@@ -59,6 +59,7 @@ the job to the seam, then open the matching example file:
|
|
|
59
59
|
| react to what happens | `cmd.on(event, handler)` | `examples/observe-events.ts` |
|
|
60
60
|
| print custom styled feed rows | `cmd.addRenderer` + `cmd.showEntry` | `examples/custom-entry-renderer.ts` |
|
|
61
61
|
| take configurable options | `cmd.addFlag` + `cmd.getFlag` | `examples/flags-and-options.ts` |
|
|
62
|
+
| rewrite the answer as it streams / stream a process's output | `cmd.hooks({transformTextDelta})` + `cmd.process.spawn` | `examples/stream-filter.ts` |
|
|
62
63
|
| everything at once (a tour) | — | `examples/kitchen-sink.ts` |
|
|
63
64
|
|
|
64
65
|
## 3. The one rule: hooks mutate, `on` observes
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// Example mod: route requests between models, and call a model yourself.
|
|
2
|
+
//
|
|
3
|
+
// hooks.beforeModelRequest runs at the start of each request's round, so its tools and prompt are
|
|
4
|
+
// built for the model it picks. Returning {model} sends THAT request to another model; the next request goes back to the session's model (use
|
|
5
|
+
// prepareNextTurn to switch for the rest of a run). Here: the first request of each run, which
|
|
6
|
+
// usually just reads the prompt and picks tools, goes to a cheaper model.
|
|
7
|
+
//
|
|
8
|
+
// cmd.model.complete runs one prompt on the session's own client, with no transcript and no
|
|
9
|
+
// tools. It always resolves: {isAnswered: true, text} or {isAnswered: false, reason}.
|
|
10
|
+
|
|
11
|
+
import type {ModApi} from '@commandcode/harness';
|
|
12
|
+
|
|
13
|
+
export default function (cmd: ModApi): void {
|
|
14
|
+
cmd.addFlag('router-model', {
|
|
15
|
+
type: 'string',
|
|
16
|
+
description: 'Model for the first request of each run',
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
cmd.hooks({
|
|
20
|
+
beforeModelRequest: async ({turnNumber}) => {
|
|
21
|
+
const cheap = cmd.getFlag('router-model');
|
|
22
|
+
if (turnNumber !== 1 || typeof cheap !== 'string' || !cheap)
|
|
23
|
+
return undefined;
|
|
24
|
+
return {model: cheap};
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
cmd.addCommand({
|
|
29
|
+
name: 'haiku',
|
|
30
|
+
description:
|
|
31
|
+
'Write a haiku about the argument with a one-shot model call',
|
|
32
|
+
argumentHint: '<topic>',
|
|
33
|
+
handler: async ({args}) => {
|
|
34
|
+
const result = await cmd.model.complete({
|
|
35
|
+
prompt: `Write a haiku about: ${args || 'code review'}`,
|
|
36
|
+
maxOutputTokens: 200,
|
|
37
|
+
});
|
|
38
|
+
if (!result.isAnswered)
|
|
39
|
+
return {message: `No haiku (${result.reason}).`};
|
|
40
|
+
return {message: result.text};
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// Example mod: a live pane with buttons, a band above the prompt, and a toast.
|
|
2
|
+
//
|
|
3
|
+
// cmd.ui.pane draws a titled panel the person opens and closes; cmd.ui.widget draws a band of
|
|
4
|
+
// lines around the prompt. Both take a `render` that returns plain data: strings, or elements
|
|
5
|
+
// ({type: 'text' | 'row' | 'box' | 'button' | 'progress' | 'divider'}). Never React components,
|
|
6
|
+
// so the same mod draws in any host.
|
|
7
|
+
//
|
|
8
|
+
// Values a drawing reads live in cmd.state: host-held, so they survive a hot reload, and
|
|
9
|
+
// setting one repaints every widget and pane. While the pane is open, Alt+<hotkey> presses its
|
|
10
|
+
// buttons and Alt+0 closes it.
|
|
11
|
+
|
|
12
|
+
import type {ModApi} from '@commandcode/harness';
|
|
13
|
+
|
|
14
|
+
export default function (cmd: ModApi): void {
|
|
15
|
+
const calls = (): number => cmd.state.get<number>('calls') ?? 0;
|
|
16
|
+
|
|
17
|
+
cmd.on('tool_completed', () => {
|
|
18
|
+
cmd.state.set('calls', calls() + 1);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
// The band above the prompt: one line, always visible.
|
|
22
|
+
cmd.ui.widget({
|
|
23
|
+
placement: 'above-editor',
|
|
24
|
+
render: () => [
|
|
25
|
+
{type: 'text', text: `tool calls: ${calls()}`, dim: true},
|
|
26
|
+
],
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
cmd.ui.pane({
|
|
30
|
+
id: 'calls',
|
|
31
|
+
title: 'Tool calls',
|
|
32
|
+
render: () => [
|
|
33
|
+
{type: 'progress', label: 'budget', value: calls() / 50, width: 20},
|
|
34
|
+
{
|
|
35
|
+
type: 'row',
|
|
36
|
+
children: [
|
|
37
|
+
{type: 'text', text: `${calls()} of 50`, bold: true},
|
|
38
|
+
{
|
|
39
|
+
type: 'button',
|
|
40
|
+
label: 'Reset',
|
|
41
|
+
hotkey: 'r',
|
|
42
|
+
onPress: () => {
|
|
43
|
+
cmd.state.set('calls', 0);
|
|
44
|
+
cmd.ui.toast('Counter reset', {level: 'success'});
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
],
|
|
48
|
+
},
|
|
49
|
+
],
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// /calls opens the pane.
|
|
53
|
+
cmd.addCommand({
|
|
54
|
+
name: 'calls',
|
|
55
|
+
description: 'Show the tool-call counter pane',
|
|
56
|
+
handler: () => {
|
|
57
|
+
cmd.ui.openPane('calls');
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// Example mod: a small form in a pane, with a text input, a select, markdown, and a button.
|
|
2
|
+
//
|
|
3
|
+
// `input` and `select` elements only take keys while their pane has focus: `/standup` opens the
|
|
4
|
+
// pane with focus (dialog-like), and Alt+↑ focuses the newest open pane from the prompt. While
|
|
5
|
+
// focused, Tab walks the fields, ←/→ choose an option, Enter submits or picks, and Esc goes
|
|
6
|
+
// back to the prompt. Typing never reaches a pane that does not have focus.
|
|
7
|
+
//
|
|
8
|
+
// The values live in cmd.state, so they survive a hot reload and setting one repaints the pane.
|
|
9
|
+
|
|
10
|
+
import type {ModApi} from '@commandcode/harness';
|
|
11
|
+
|
|
12
|
+
const MOODS = [
|
|
13
|
+
{label: 'Great', value: 'great'},
|
|
14
|
+
{label: 'Okay', value: 'okay'},
|
|
15
|
+
{label: 'Blocked', value: 'blocked'},
|
|
16
|
+
] as const;
|
|
17
|
+
|
|
18
|
+
export default function (cmd: ModApi): void {
|
|
19
|
+
const note = (): string => cmd.state.get<string>('note') ?? '';
|
|
20
|
+
const mood = (): string => cmd.state.get<string>('mood') ?? 'okay';
|
|
21
|
+
|
|
22
|
+
cmd.ui.pane({
|
|
23
|
+
id: 'standup',
|
|
24
|
+
title: 'Standup',
|
|
25
|
+
render: () => [
|
|
26
|
+
{
|
|
27
|
+
type: 'markdown',
|
|
28
|
+
text: '**Today**: one line, then pick how it is going.',
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
type: 'input',
|
|
32
|
+
key: 'note',
|
|
33
|
+
placeholder: 'what are you working on?',
|
|
34
|
+
value: note(),
|
|
35
|
+
onSubmit: text => cmd.state.set('note', text.trim()),
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
type: 'select',
|
|
39
|
+
key: 'mood',
|
|
40
|
+
options: MOODS,
|
|
41
|
+
value: mood(),
|
|
42
|
+
onSelect: value => cmd.state.set('mood', value),
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
type: 'button',
|
|
46
|
+
label: 'Post',
|
|
47
|
+
hotkey: 's',
|
|
48
|
+
onPress: () => {
|
|
49
|
+
if (!note()) {
|
|
50
|
+
cmd.ui.toast('Type a note first', {level: 'warning'});
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
cmd.ui.notify(`standup (${mood()}): ${note()}`);
|
|
54
|
+
cmd.ui.closePane('standup');
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
],
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// /standup opens the form with the keyboard in it.
|
|
61
|
+
cmd.addCommand({
|
|
62
|
+
name: 'standup',
|
|
63
|
+
description: 'Post a one-line standup from a form pane',
|
|
64
|
+
handler: () => {
|
|
65
|
+
cmd.ui.openPane('standup', {focus: true});
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// Example mod: answer permission prompts with a team policy.
|
|
2
|
+
//
|
|
3
|
+
// hooks.onPermissionRequest fires ONLY where Command Code was about to ask you. Deny rules,
|
|
4
|
+
// plan mode, and dont-ask have already run, so a mod can never undo them. Return 'allow' or
|
|
5
|
+
// 'deny' to answer for the person, or undefined to let them decide. A prompt a safety check
|
|
6
|
+
// forced (isSafetyPrompt: a sensitive file, a risky command) can be denied but never allowed:
|
|
7
|
+
// an 'allow' there is ignored and you are asked as usual.
|
|
8
|
+
|
|
9
|
+
import type {ModApi} from '@commandcode/harness';
|
|
10
|
+
|
|
11
|
+
// Anything that can chain or redirect a second command goes to the person: without this,
|
|
12
|
+
// `git status && npm publish` would pass as a safe `git status`.
|
|
13
|
+
const SHELL_CONTROL = /[;&|`<>\n]|\$\(/;
|
|
14
|
+
|
|
15
|
+
// Anchored at both ends, so the whole command must be one of these.
|
|
16
|
+
const SAFE_SHELL = [
|
|
17
|
+
/^git (status|diff|log)( .*)?$/,
|
|
18
|
+
/^pnpm (test|lint|typecheck)( .*)?$/,
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
export default function (cmd: ModApi): void {
|
|
22
|
+
cmd.hooks({
|
|
23
|
+
onPermissionRequest: ({toolName, input}) => {
|
|
24
|
+
if (toolName !== 'shell_command') return undefined;
|
|
25
|
+
const command =
|
|
26
|
+
typeof input.command === 'string' ? input.command : '';
|
|
27
|
+
if (/\bcurl\b.*\|\s*(ba)?sh\b/.test(command)) return 'deny';
|
|
28
|
+
if (SHELL_CONTROL.test(command)) return undefined;
|
|
29
|
+
if (SAFE_SHELL.some(pattern => pattern.test(command)))
|
|
30
|
+
return 'allow';
|
|
31
|
+
return undefined;
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// Example mod: redact secrets from everything the transcript keeps.
|
|
2
|
+
//
|
|
3
|
+
// hooks.transformMessage sees every message before it is committed: your prompt, each model
|
|
4
|
+
// reply, every tool result, steering and follow-up messages. What it returns is what the
|
|
5
|
+
// transcript stores AND what every later request sends, so a key a tool printed never reaches
|
|
6
|
+
// the model on the next turn. The role and every tool_use / tool_result id must stay as they
|
|
7
|
+
// were; a rewrite that changes them is rejected and the message is kept.
|
|
8
|
+
|
|
9
|
+
import type {ModApi} from '@commandcode/harness';
|
|
10
|
+
|
|
11
|
+
const SECRET_PATTERNS = [
|
|
12
|
+
/\bsk-[A-Za-z0-9_-]{16,}\b/g, // API keys
|
|
13
|
+
/\bgh[pousr]_[A-Za-z0-9]{20,}\b/g, // GitHub tokens
|
|
14
|
+
/\bAKIA[0-9A-Z]{16}\b/g, // AWS access key ids
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
function redact(text: string): string {
|
|
18
|
+
return SECRET_PATTERNS.reduce(
|
|
19
|
+
(current, pattern) => current.replace(pattern, '[REDACTED]'),
|
|
20
|
+
text,
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export default function (cmd: ModApi): void {
|
|
25
|
+
cmd.hooks({
|
|
26
|
+
transformMessage: ({message}) => {
|
|
27
|
+
if (message.role === 'assistant') return message;
|
|
28
|
+
return {
|
|
29
|
+
...message,
|
|
30
|
+
content: message.content.map(block => {
|
|
31
|
+
if (block.type === 'text')
|
|
32
|
+
return {...block, text: redact(block.text)};
|
|
33
|
+
if (block.type !== 'tool_result') return block;
|
|
34
|
+
return {
|
|
35
|
+
...block,
|
|
36
|
+
content: block.content.map(part =>
|
|
37
|
+
part.type === 'text'
|
|
38
|
+
? {...part, text: redact(part.text)}
|
|
39
|
+
: part,
|
|
40
|
+
),
|
|
41
|
+
};
|
|
42
|
+
}),
|
|
43
|
+
};
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// Example mod: restyle built-in rows with cmd.ui.override.
|
|
2
|
+
//
|
|
3
|
+
// `spinner` appends nodes after the working status line, so "tool calls: 3" sits beside the
|
|
4
|
+
// spinner while the agent works. `tool_row` draws a tool's row instead of the built-in one;
|
|
5
|
+
// returning undefined keeps the built-in row, and `next()` is what mods loaded after this one
|
|
6
|
+
// drew (undefined when nobody did). A throwing override falls back to the built-in drawing.
|
|
7
|
+
//
|
|
8
|
+
// The permission prompt is never overridable: what a person approves is always drawn by the host.
|
|
9
|
+
|
|
10
|
+
import type {ModApi, ModUiOverrideProps} from '@commandcode/harness';
|
|
11
|
+
|
|
12
|
+
function statusMark(status: ModUiOverrideProps['tool_row']['status']): string {
|
|
13
|
+
if (status === 'error') return '✗';
|
|
14
|
+
if (status === 'pending') return '…';
|
|
15
|
+
return '✓';
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export default function (cmd: ModApi): void {
|
|
19
|
+
const calls = (): number => cmd.state.get<number>('calls') ?? 0;
|
|
20
|
+
|
|
21
|
+
cmd.on('tool_completed', () => {
|
|
22
|
+
cmd.state.set('calls', calls() + 1);
|
|
23
|
+
});
|
|
24
|
+
// One count per run: reset when the agent starts working on a new prompt.
|
|
25
|
+
cmd.on('run_start', () => {
|
|
26
|
+
cmd.state.set('calls', 0);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
cmd.ui.override('spinner', (_props, next) => [
|
|
30
|
+
{type: 'text', text: `tool calls: ${calls()}`, color: 'cyan'},
|
|
31
|
+
...(next() ?? []),
|
|
32
|
+
]);
|
|
33
|
+
|
|
34
|
+
// Shell rows as one compact line; every other tool keeps its built-in row.
|
|
35
|
+
cmd.ui.override('tool_row', ({toolName, input, status}) => {
|
|
36
|
+
if (toolName !== 'shell_command') return undefined;
|
|
37
|
+
return [
|
|
38
|
+
{
|
|
39
|
+
type: 'row',
|
|
40
|
+
children: [
|
|
41
|
+
{type: 'text', text: statusMark(status), bold: true},
|
|
42
|
+
{type: 'text', text: `$ ${input}`, dim: true},
|
|
43
|
+
],
|
|
44
|
+
},
|
|
45
|
+
];
|
|
46
|
+
});
|
|
47
|
+
}
|