command-code 1.82.4 → 1.83.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 +1 -0
- package/dist/bundled/command-code-knowledge/reference/models.md +3 -1
- package/dist/bundled/command-code-knowledge/reference/product-help.md +5 -1
- 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 +191 -19
- 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.83.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- feat: Planning and Implementation models follow the live model catalog
|
|
8
|
+
- feat: mods get panes, widgets, toasts, hot reload, permission answers, transcript redaction, per-request model routing
|
|
9
|
+
- feat: mods can use `cmd.state` / `cmd.store` / `cmd.clock` / `cmd.model` / `cmd.prompt`, sub-agent types, `/mods`, `cmd mods inspect`, and `--safe-mode`
|
|
10
|
+
- 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
|
|
11
|
+
- feat: mods can copy to the clipboard with `cmd.ui.copy` and play sounds with `cmd.audio.play` / `cmd.audio.bell`
|
|
12
|
+
- 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`
|
|
13
|
+
- feat: mods can restyle built-in TUI rows with `cmd.ui.override` (spinner, tool rows, user messages, command output), never the permission prompt
|
|
14
|
+
|
|
15
|
+
## 1.82.5
|
|
16
|
+
|
|
17
|
+
### Patch Changes
|
|
18
|
+
|
|
19
|
+
- feat: credit errors say when your renewal payment is still processing
|
|
20
|
+
|
|
3
21
|
## 1.82.4
|
|
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
|
|
|
@@ -52,10 +52,10 @@ The model catalog — every id `/model`, `--model`, `model:effort` shorthand, an
|
|
|
52
52
|
| `tencent/hy3-paid` | Tencent Hy3 | 262K | — | $0.14/$0.58 · cache $0.035 | Go and above | sparse-MoE reasoning & agentic tool use |
|
|
53
53
|
| `tencent/hy4-preview` | Tencent Hy4 Preview | 1.05M | low, medium, high | $0.834/$2.501 · cache $0.042 | Go and above | agentic coding & sustained multi-step tool use |
|
|
54
54
|
| `nvidia/nemotron-3-ultra-550b-a55b` | Nemotron 3 Ultra | 1M | — | $0.6/$2.4 · cache $0.12 | Go and above | open reasoning model for long-horizon autonomous agents |
|
|
55
|
+
| `nvidia/nemotron-3.5-super-vl` | Nemotron 3.5 Super VL | 262K | low, medium, high | $0.2/$0.5 · cache $0.04 | Go and above | open vision-language model for coding and documents |
|
|
55
56
|
| `thinkingmachines/inkling` | Inkling | 256K | — | $1/$4.05 · cache $0.17 | Go and above | multimodal MoE reasoning |
|
|
56
57
|
| `thinkingmachines/inkling-small` | Inkling Small | 1M | — | $0.5/$1.2 · cache $0.1 | Go and above | lightweight MoE reasoning at lower cost and latency |
|
|
57
58
|
| `poolside/laguna-s-2.1-free` | Laguna S 2.1 | 256K | — | $0/$0 · cache $0 | Go and above | open-weight agentic coding and long-horizon work |
|
|
58
|
-
| `inclusionai/ling-3.0-flash-sante:free` | Ling 3.0 Flash Sante | 262K | — | $0/$0 · cache $0 | Go and above | health & medicine tuned lightweight-MoE, still strong on code |
|
|
59
59
|
| `inclusionai/ling-3.1-flash` | Ling 3.1 Flash | 262K | low, medium, high | $0.3/$0.9 · cache $0.06 | Go and above | hybrid-reasoning MoE for coding & tool-using agents |
|
|
60
60
|
| `mistral/mistral-large-4` | Mistral Large 4 | 524K | low, medium, high | $1.36/$4.18 · cache $0.14 | Go and above | frontier open-weight multimodal model for coding & analysis |
|
|
61
61
|
## Stealth
|
|
@@ -63,6 +63,7 @@ The model catalog — every id `/model`, `--model`, `model:effort` shorthand, an
|
|
|
63
63
|
| Id (use EXACTLY this) | Name | Context | Efforts | $/1M in/out · cache read | Min plan | Best for |
|
|
64
64
|
|---|---|---|---|---|---|---|
|
|
65
65
|
| `stealth/glyph-cluster:free` | Glyph Cluster | 256K | low, medium, high, xhigh | $0/$0 · cache $0 | Go and above | stealth reasoning model in early access |
|
|
66
|
+
| `stealth/cat-jam:free` | Cat Jam | 1.05M | low, medium, high, xhigh, max | $0/$0 · cache $0 | Go and above | stealth reasoning model for coding and agentic work |
|
|
66
67
|
## Anthropic
|
|
67
68
|
|
|
68
69
|
| Id (use EXACTLY this) | Name | Context | Efforts | $/1M in/out · cache read | Min plan | Best for |
|
|
@@ -118,6 +119,7 @@ The model catalog — every id `/model`, `--model`, `model:effort` shorthand, an
|
|
|
118
119
|
| `meta/muse-spark-1.2-contributor` | Muse Spark 1.2 Contributor | 1.05M | low, medium, high, xhigh | $0.1/$0.2 · cache $0.002 | Go and above | Muse Spark 1.2 at ~95% off |
|
|
119
120
|
| `meta/muse-spark-1.3` | Muse Spark 1.3 | 1.05M | low, medium, high, xhigh, max | $1.25/$4.25 · cache $0.15 | GOAT and above | multimodal reasoning for long-horizon agentic and coding workflows |
|
|
120
121
|
| `meta/muse-spark-1.3-contributor` | Muse Spark 1.3 Contributor | 1.05M | low, medium, high, xhigh | $0.1/$0.2 · cache $0.002 | Go and above | Muse Spark 1.3 at up to 95% off |
|
|
122
|
+
| `meta/muse-spark-open` | Muse Spark Open | 1.05M | low, medium, high | $1.25/$4.25 · cache $0.15 | Go and above | open-weight Muse Spark for coding and agentic workflows |
|
|
121
123
|
| `meta/muse-wonder-1.0` | Muse Wonder | 1.05M | low, medium, high, xhigh | $4/$20 · cache $0.2 | Max | frontier coding, long-running agents and advanced computer use |
|
|
122
124
|
## xAI
|
|
123
125
|
|
|
@@ -73,6 +73,7 @@
|
|
|
73
73
|
- /extra: Buy extra usage at API cost, zero markup
|
|
74
74
|
- /update: Update Command Code to the latest version
|
|
75
75
|
- /reload: Restart Command Code and resume this session (applies a staged update)
|
|
76
|
+
- /mods: List loaded mods and panes; /mods reload [name] hot-reloads, /mods open|close <pane>
|
|
76
77
|
- /help: Display help information
|
|
77
78
|
- /exit: Exit Command Code
|
|
78
79
|
- /quit: Exit Command Code (alias of /exit)
|
|
@@ -139,6 +140,7 @@
|
|
|
139
140
|
- --base <branch>: Base branch for a new cloud session
|
|
140
141
|
- --mod <path>: Load a mod file or directory for this session (repeatable)
|
|
141
142
|
- --mod-option <name=value>: Set a mod-declared flag value (repeatable)
|
|
143
|
+
- --safe-mode: Load built-in mods only for this session (no installed mods, no --mod)
|
|
142
144
|
- --skill <path>: Load extra skills from a path (a skill directory or a directory of skills); repeatable
|
|
143
145
|
- --no-skills: Skip skill discovery; paths given via --skill still load
|
|
144
146
|
- --skip-onboarding: Skip taste onboarding (for automated runs)
|
|
@@ -329,17 +331,19 @@ Valid model ids (the /model catalog — use these EXACT ids):
|
|
|
329
331
|
- google/gemini-3.1-flash-lite
|
|
330
332
|
- sakana/fugu-ultra
|
|
331
333
|
- nvidia/nemotron-3-ultra-550b-a55b
|
|
334
|
+
- nvidia/nemotron-3.5-super-vl
|
|
332
335
|
- thinkingmachines/inkling
|
|
333
336
|
- thinkingmachines/inkling-small
|
|
334
337
|
- stealth/glyph-cluster:free
|
|
338
|
+
- stealth/cat-jam:free
|
|
335
339
|
- poolside/laguna-s-2.1-free
|
|
336
|
-
- inclusionai/ling-3.0-flash-sante:free
|
|
337
340
|
- inclusionai/ling-3.1-flash
|
|
338
341
|
- meta/muse-spark-1.1
|
|
339
342
|
- meta/muse-spark-1.2
|
|
340
343
|
- meta/muse-spark-1.2-contributor
|
|
341
344
|
- meta/muse-spark-1.3
|
|
342
345
|
- meta/muse-spark-1.3-contributor
|
|
346
|
+
- meta/muse-spark-open
|
|
343
347
|
- meta/muse-wonder-1.0
|
|
344
348
|
- xai/grok-4.5
|
|
345
349
|
- xai/grok-4.6
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// Example mod: filter the model's answer as it streams, and stream a process's output.
|
|
2
|
+
//
|
|
3
|
+
// hooks.transformTextDelta sees every chunk of the answer before it is shown. Return the chunk
|
|
4
|
+
// to show (rewritten or not), or '' to hold it back. When the answer is complete it is called
|
|
5
|
+
// once more with {delta: '', done: true}: return whatever you held back. The transcript keeps
|
|
6
|
+
// exactly what was shown. It must be synchronous and fast: it runs inside the stream.
|
|
7
|
+
//
|
|
8
|
+
// A key can arrive split across chunks ("sk-ab" then "cdef..."), so this filter holds back a
|
|
9
|
+
// tail that might be the start of one and decides when the next chunk (or the end) arrives.
|
|
10
|
+
//
|
|
11
|
+
// cmd.process.spawn runs a command and hands each stdout/stderr chunk to onOutput as it is
|
|
12
|
+
// produced; cmd.exec only answers once the process exits.
|
|
13
|
+
|
|
14
|
+
import type {ModApi} from '@commandcode/harness';
|
|
15
|
+
|
|
16
|
+
const KEY_PATTERN = /\bsk-[A-Za-z0-9_-]{16,}/g;
|
|
17
|
+
// A tail that could still grow into a key: "s", "sk", "sk-", "sk-abc...".
|
|
18
|
+
const MAYBE_KEY_TAIL = /(?:\bs|\bsk|\bsk-[A-Za-z0-9_-]*)$/;
|
|
19
|
+
|
|
20
|
+
function redact(text: string): string {
|
|
21
|
+
return text.replace(KEY_PATTERN, '[REDACTED]');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export default function (cmd: ModApi): void {
|
|
25
|
+
// Held-back text, for the stream it belongs to. A retry or a new turn starts a new stream.
|
|
26
|
+
let held = '';
|
|
27
|
+
let stream = '';
|
|
28
|
+
|
|
29
|
+
cmd.hooks({
|
|
30
|
+
transformTextDelta: ({delta, done, turnNumber, attempt}) => {
|
|
31
|
+
const id = `${turnNumber}:${attempt}`;
|
|
32
|
+
if (id !== stream) {
|
|
33
|
+
stream = id;
|
|
34
|
+
held = '';
|
|
35
|
+
}
|
|
36
|
+
const text = held + delta;
|
|
37
|
+
if (done) {
|
|
38
|
+
held = '';
|
|
39
|
+
return redact(text);
|
|
40
|
+
}
|
|
41
|
+
const tail = MAYBE_KEY_TAIL.exec(text)?.[0] ?? '';
|
|
42
|
+
held = tail;
|
|
43
|
+
return redact(text.slice(0, text.length - tail.length));
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
cmd.addCommand({
|
|
48
|
+
name: 'stream',
|
|
49
|
+
description: 'Run a command and show its latest output line as it runs',
|
|
50
|
+
argumentHint: '<command>',
|
|
51
|
+
handler: async ({args}) => {
|
|
52
|
+
if (!args.trim()) return {message: 'usage: /stream <command>'};
|
|
53
|
+
let lines = 0;
|
|
54
|
+
cmd.ui.setStatus(`running ${args}`);
|
|
55
|
+
try {
|
|
56
|
+
const {code} = await cmd.process.spawn({
|
|
57
|
+
command: args,
|
|
58
|
+
onOutput: (chunk, from) => {
|
|
59
|
+
const last = chunk.trimEnd().split('\n').at(-1) ?? '';
|
|
60
|
+
lines += chunk.split('\n').length - 1;
|
|
61
|
+
if (last) cmd.ui.setStatus(`${from}: ${last}`);
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
return {message: `${args} exited ${code} after ${lines} lines`};
|
|
65
|
+
} finally {
|
|
66
|
+
cmd.ui.setStatus(null);
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
}
|