command-code 0.52.5 → 1.0.1
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 +2547 -0
- package/dist/bundled/command-code-knowledge/SKILL.md +52 -0
- package/dist/bundled/command-code-knowledge/reference/checkpoints.md +366 -0
- package/dist/bundled/command-code-knowledge/reference/custom-agents.md +91 -0
- package/dist/bundled/command-code-knowledge/reference/custom-slash-commands.md +345 -0
- package/dist/bundled/command-code-knowledge/reference/headless.md +234 -0
- package/dist/bundled/command-code-knowledge/reference/hooks.md +1097 -0
- package/dist/bundled/command-code-knowledge/reference/mcp.md +626 -0
- package/dist/bundled/command-code-knowledge/reference/memory.md +98 -0
- package/dist/bundled/command-code-knowledge/reference/models.md +81 -0
- package/dist/bundled/command-code-knowledge/reference/permissions.md +412 -0
- package/dist/bundled/command-code-knowledge/reference/plan-mode.md +101 -0
- package/dist/bundled/command-code-knowledge/reference/product-help.md +421 -0
- package/dist/bundled/command-code-knowledge/reference/skills.md +993 -0
- package/dist/bundled/mod-builder/SKILL.md +128 -0
- package/dist/bundled/mod-builder/examples/block-dangerous-commands.ts +41 -0
- package/dist/bundled/mod-builder/examples/custom-entry-renderer.ts +32 -0
- package/dist/bundled/mod-builder/examples/custom-tool.ts +53 -0
- package/dist/bundled/mod-builder/examples/flags-and-options.ts +29 -0
- package/dist/bundled/mod-builder/examples/input-shortcuts.ts +41 -0
- package/dist/bundled/mod-builder/examples/kitchen-sink.ts +110 -0
- package/dist/bundled/mod-builder/examples/lifecycle-hooks.ts +64 -0
- package/dist/bundled/mod-builder/examples/observe-events.ts +30 -0
- package/dist/bundled/mod-builder/examples/slash-command.ts +33 -0
- package/dist/bundled/mod-builder/reference/api.md +81 -0
- package/dist/bundled/mod-builder/reference/hooks-and-events.md +308 -0
- package/dist/bundled/mod-builder/reference/overview.md +161 -0
- package/dist/bundled/mod-builder/reference/packaging.md +63 -0
- package/dist/bundled/mod-builder/reference/ui.md +63 -0
- package/dist/bundled/mod-builder/reference/verify.md +58 -0
- package/dist/bundled/skill-builder/SKILL.md +104 -0
- package/dist/cli.mjs +5 -5
- package/package.json +35 -26
- package/vsix/commandcode-vscode.vsix +0 -0
- /package/{skills → dist/bundled}/agent-browser/SKILL.md +0 -0
- /package/{skills → dist/bundled}/design/SKILL.md +0 -0
- /package/{skills → dist/bundled}/design/references/border.md +0 -0
- /package/{skills → dist/bundled}/design/references/button.md +0 -0
- /package/{skills → dist/bundled}/design/references/checkup.md +0 -0
- /package/{skills → dist/bundled}/design/references/color.md +0 -0
- /package/{skills → dist/bundled}/design/references/create.md +0 -0
- /package/{skills → dist/bundled}/design/references/design-html.md +0 -0
- /package/{skills → dist/bundled}/design/references/deslop.md +0 -0
- /package/{skills → dist/bundled}/design/references/finish.md +0 -0
- /package/{skills → dist/bundled}/design/references/interaction.md +0 -0
- /package/{skills → dist/bundled}/design/references/layout.md +0 -0
- /package/{skills → dist/bundled}/design/references/motion.md +0 -0
- /package/{skills → dist/bundled}/design/references/redesign.md +0 -0
- /package/{skills → dist/bundled}/design/references/refine.md +0 -0
- /package/{skills → dist/bundled}/design/references/relayout.md +0 -0
- /package/{skills → dist/bundled}/design/references/report-html.md +0 -0
- /package/{skills → dist/bundled}/design/references/responsive.md +0 -0
- /package/{skills → dist/bundled}/design/references/review.md +0 -0
- /package/{skills → dist/bundled}/design/references/setup.md +0 -0
- /package/{skills → dist/bundled}/design/references/shadow.md +0 -0
- /package/{skills → dist/bundled}/design/references/smell.md +0 -0
- /package/{skills → dist/bundled}/design/references/surface.md +0 -0
- /package/{skills → dist/bundled}/design/references/tokenize.md +0 -0
- /package/{skills → dist/bundled}/design/references/typeset.md +0 -0
- /package/{skills → dist/bundled}/design/references/voice.md +0 -0
- /package/{skills → dist/bundled}/design/references/writing.md +0 -0
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: mod-builder
|
|
3
|
+
description: Build a Command Code mod (a loadable plugin) end to end — a TypeScript file that registers tools, slash commands, lifecycle hooks, event observers, input interception, custom renderers, flags, or a model provider through the ModApi. Use when the user wants to create, build, generate, or scaffold a mod, extend Command Code with custom behavior, add a custom tool or slash command, intercept input, block or rewrite tool calls, or turn a workflow into a reusable plugin.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Mod Builder
|
|
7
|
+
|
|
8
|
+
You are building a **mod**: a TypeScript file that Command Code discovers on disk and loads
|
|
9
|
+
onto its agent loop. A mod is the plugin unit — it can add tools the model calls, slash
|
|
10
|
+
commands, mutating lifecycle hooks, event observers, typed-input interception, custom feed
|
|
11
|
+
rendering, configurable flags, and model providers. This is the same API Command Code's own
|
|
12
|
+
built-in features (providers, titling, update notice) are written against.
|
|
13
|
+
|
|
14
|
+
Two resources ship next to this file — use them instead of searching the codebase:
|
|
15
|
+
|
|
16
|
+
- `examples/` — runnable, single-file example mods, one per capability. Read the one
|
|
17
|
+
closest to the job and adapt it.
|
|
18
|
+
- `reference/` — the full mods documentation, generated from the public docs
|
|
19
|
+
(commandcode.ai/docs/mods). This is the authoritative API reference:
|
|
20
|
+
|
|
21
|
+
<!-- generated:reference-index:start -->
|
|
22
|
+
- `reference/overview.md` — Mods
|
|
23
|
+
- `reference/api.md` — ModApi reference
|
|
24
|
+
- `reference/hooks-and-events.md` — Hooks and events
|
|
25
|
+
- `reference/ui.md` — UI surface
|
|
26
|
+
- `reference/packaging.md` — Packaging and install
|
|
27
|
+
- `reference/verify.md` — Verify a mod
|
|
28
|
+
<!-- generated:reference-index:end -->
|
|
29
|
+
|
|
30
|
+
## 1. The shape of a mod
|
|
31
|
+
|
|
32
|
+
A mod is a file that default-exports a factory. The factory receives the API bound as `cmd`
|
|
33
|
+
and registers everything up front (it may be async):
|
|
34
|
+
|
|
35
|
+
```typescript
|
|
36
|
+
import type {ModApi} from '@commandcode/harness';
|
|
37
|
+
|
|
38
|
+
export default function (cmd: ModApi) {
|
|
39
|
+
cmd.addCommand({name: 'hello', handler: () => ({message: 'hi from a mod'})});
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Save it at `~/.commandcode/mods/hello.ts` (personal) or `<project>/.commandcode/mods/hello.ts`
|
|
44
|
+
(project — loads once the workspace is trusted). It loads next session, or test it now with
|
|
45
|
+
`cmd --mod ./hello.ts`. No build step — jiti compiles the TypeScript at load.
|
|
46
|
+
|
|
47
|
+
## 2. Pick the capability
|
|
48
|
+
|
|
49
|
+
Every registration verb is `add*` and returns a `Disposable` (`.dispose()` undoes it). Match
|
|
50
|
+
the job to the seam, then open the matching example file:
|
|
51
|
+
|
|
52
|
+
| The mod should… | Use | Example |
|
|
53
|
+
|---|---|---|
|
|
54
|
+
| add a `/command` | `cmd.addCommand({name, handler})` | `examples/slash-command.ts` |
|
|
55
|
+
| add a tool the model calls | `cmd.addTool({schema, run})` | `examples/custom-tool.ts` |
|
|
56
|
+
| block or rewrite a tool call | `cmd.hooks({beforeToolCall})` | `examples/block-dangerous-commands.ts` |
|
|
57
|
+
| rewrite or consume typed input | `cmd.hooks({transformInput})` | `examples/input-shortcuts.ts` |
|
|
58
|
+
| keep a finished run going / react to session start·end | `cmd.hooks({onStop, onSessionStart, onSessionEnd})` | `examples/lifecycle-hooks.ts` |
|
|
59
|
+
| react to what happens | `cmd.on(event, handler)` | `examples/observe-events.ts` |
|
|
60
|
+
| print custom styled feed rows | `cmd.addRenderer` + `cmd.showEntry` | `examples/custom-entry-renderer.ts` |
|
|
61
|
+
| take configurable options | `cmd.addFlag` + `cmd.getFlag` | `examples/flags-and-options.ts` |
|
|
62
|
+
| everything at once (a tour) | — | `examples/kitchen-sink.ts` |
|
|
63
|
+
|
|
64
|
+
## 3. The one rule: hooks mutate, `on` observes
|
|
65
|
+
|
|
66
|
+
- **`cmd.hooks({...})`** is the only place that can change behavior — block a tool
|
|
67
|
+
(`beforeToolCall`), rewrite a result (`afterToolCall`), add to the prompt
|
|
68
|
+
(`appendSystemPrompt`), rewrite typed input (`transformInput`), force a finished run to
|
|
69
|
+
keep going (`onStop`), react to session start/end (`onSessionStart`/`onSessionEnd`), or run
|
|
70
|
+
post-turn work (`onRunEnd`). See `reference/hooks-and-events.md` for the full set. Multiple
|
|
71
|
+
`hooks()` calls compose in registration order.
|
|
72
|
+
- **`cmd.on(event, ...)`** only observes — it cannot block or rewrite. Handlers are isolated
|
|
73
|
+
(a throw becomes a `mod_error` event, never a crash).
|
|
74
|
+
|
|
75
|
+
If you find yourself wanting an `on` handler to stop a tool, you want a hook instead.
|
|
76
|
+
|
|
77
|
+
## 4. Tools and commands, precisely
|
|
78
|
+
|
|
79
|
+
- **Tool `run`** receives `{input, runtime, signal}` and returns
|
|
80
|
+
`{ok: true, content: [{type: 'text', text}]}` or `{ok: false, error}`. Mark
|
|
81
|
+
`readOnly: true` when the tool never mutates (stays available in plan mode). A mod tool
|
|
82
|
+
whose name collides with an existing tool is skipped with a `mod_error` — pick a distinct,
|
|
83
|
+
namespaced name.
|
|
84
|
+
- **Command `handler`** returns DATA, never a live callback: `{prompt}` runs an automated
|
|
85
|
+
model turn, `{message}` renders an info row, nothing = pure side effect. It gets
|
|
86
|
+
`{args, ui, cwd, exec}`.
|
|
87
|
+
|
|
88
|
+
## 5. Verify what you built
|
|
89
|
+
|
|
90
|
+
Do NOT declare the mod done until it demonstrably loads and its surface works. The loop
|
|
91
|
+
(full detail in `reference/verify.md`):
|
|
92
|
+
|
|
93
|
+
1. **Load it without installing** — `cmd --mod ./your-mod.ts`. A file that fails to import,
|
|
94
|
+
exports no factory, or throws in its factory becomes a warning — never a crashed session —
|
|
95
|
+
so an unchecked mod can fail silently.
|
|
96
|
+
2. **Confirm it registered** — `cmd mods list` must show the mod with zero load warnings.
|
|
97
|
+
If it is missing, the printed warning says why (import error, no default-export factory,
|
|
98
|
+
factory threw, duplicate name).
|
|
99
|
+
3. **Exercise every registered surface** — a slash command must appear in `/` autocomplete
|
|
100
|
+
and run; a tool must be callable by the model by name; a hook must be triggered (ask for
|
|
101
|
+
the guarded action and watch the block/rewrite land); a widget/status must render around
|
|
102
|
+
the input panel.
|
|
103
|
+
4. **Iterate with `/reload`** — mods load once per process; `/reload` restarts and
|
|
104
|
+
re-imports every mod.
|
|
105
|
+
5. **Headless check when CI matters** — `cmd -p "…" --mod ./your-mod.ts`; dialogs resolve
|
|
106
|
+
deterministic defaults (confirm → false, select/input → undefined), so the mod must
|
|
107
|
+
behave sensibly under them.
|
|
108
|
+
|
|
109
|
+
Working inside the Command Code repo itself? The examples are validated by
|
|
110
|
+
`packages/harness/src/mod-host/__tests__/examples.test.ts` — run it after touching them, and
|
|
111
|
+
run `pnpm generate:knowledge` after editing the docs under `packages/docs/src/app/mods/`
|
|
112
|
+
(the `reference/` files here are generated from them).
|
|
113
|
+
|
|
114
|
+
## 6. Ship it (optional)
|
|
115
|
+
|
|
116
|
+
- Personal: leave it in `~/.commandcode/mods/`.
|
|
117
|
+
- A package: a `package.json` with `{"commandcode": {"mods": ["./index.ts"]}}`, installed via
|
|
118
|
+
`cmd mods add npm:<name>` / `cmd mods add owner/repo` / `cmd mods add ./local-dir`.
|
|
119
|
+
Full packaging/filtering detail: `reference/packaging.md`.
|
|
120
|
+
|
|
121
|
+
## Boundaries to state up front
|
|
122
|
+
|
|
123
|
+
- **No sandbox** — a mod is arbitrary code; install packages you trust. Project mods are
|
|
124
|
+
trust-gated like project skills.
|
|
125
|
+
- **Reload is `/reload`** — mods load once per process; `/reload` restarts and re-imports.
|
|
126
|
+
- **Providers extend, never replace** — `cmd.addProvider` adds to the provider set.
|
|
127
|
+
- **Rendering is line-based** — `cmd.addRenderer` returns styled text lines, not React
|
|
128
|
+
components, so a mod renders in any host and a broken renderer can't break the screen.
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// Example mod: a policy guard that can BLOCK a tool call.
|
|
2
|
+
//
|
|
3
|
+
// cmd.hooks are the mutating lifecycle. `beforeToolCall` runs before every tool executes
|
|
4
|
+
// and may:
|
|
5
|
+
// return { block: true, additionalContext } → the call never runs; the model is told why
|
|
6
|
+
// return { input: rewritten } → the call runs with rewritten input
|
|
7
|
+
// return undefined → allow unchanged
|
|
8
|
+
//
|
|
9
|
+
// This is where you'd enforce org policy, sandbox paths, or require confirmation. The guard
|
|
10
|
+
// below refuses `rm -rf` and force-pushes unless the user confirms in the TUI.
|
|
11
|
+
|
|
12
|
+
import type {ModApi} from '@commandcode/harness';
|
|
13
|
+
|
|
14
|
+
const DANGEROUS = [
|
|
15
|
+
/\brm\s+-rf\b/,
|
|
16
|
+
/\bgit\s+push\b.*--force\b/,
|
|
17
|
+
/\bgit\s+push\s+-f\b/,
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
export default function (cmd: ModApi): void {
|
|
21
|
+
cmd.hooks({
|
|
22
|
+
beforeToolCall: async ({toolName, input}) => {
|
|
23
|
+
if (toolName !== 'shell_command') return undefined;
|
|
24
|
+
const command =
|
|
25
|
+
typeof input.command === 'string' ? input.command : '';
|
|
26
|
+
if (!DANGEROUS.some(pattern => pattern.test(command)))
|
|
27
|
+
return undefined;
|
|
28
|
+
|
|
29
|
+
const allow = await cmd.ui.confirm({
|
|
30
|
+
title: 'Dangerous command',
|
|
31
|
+
message: `Allow: ${command}`,
|
|
32
|
+
});
|
|
33
|
+
if (allow) return undefined;
|
|
34
|
+
return {
|
|
35
|
+
block: true,
|
|
36
|
+
additionalContext:
|
|
37
|
+
'Blocked by the safety mod. Ask the user to run this manually if it is really intended.',
|
|
38
|
+
};
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// Example mod: render a custom entry into the feed with your own styling.
|
|
2
|
+
//
|
|
3
|
+
// Two pieces:
|
|
4
|
+
// cmd.addRenderer(type, data => lines) → registers how a custom entry type prints
|
|
5
|
+
// cmd.showEntry(type, data) → pushes an entry of that type into the live feed
|
|
6
|
+
//
|
|
7
|
+
// Rendering is line-based: the renderer returns styled terminal lines (use raw ansi escape
|
|
8
|
+
// codes, picocolors, or @commandcode/tui helpers). Unrendered types pretty-print as JSON.
|
|
9
|
+
// The TUI wires the feed sink; headless runs drop entries. A throwing renderer degrades to
|
|
10
|
+
// a warning notice, never a broken screen.
|
|
11
|
+
//
|
|
12
|
+
// Pair showEntry with cmd.session.appendCustomEntry when the data should also persist to
|
|
13
|
+
// the transcript.
|
|
14
|
+
|
|
15
|
+
import type {ModApi} from '@commandcode/harness';
|
|
16
|
+
|
|
17
|
+
const GREEN = '\x1b[32m';
|
|
18
|
+
const DIM = '\x1b[2m';
|
|
19
|
+
const RESET = '\x1b[0m';
|
|
20
|
+
|
|
21
|
+
export default function (cmd: ModApi): void {
|
|
22
|
+
cmd.addRenderer('score', data => {
|
|
23
|
+
const {label, value} = data as {label: string; value: number};
|
|
24
|
+
const bar = '█'.repeat(Math.max(0, Math.min(10, value)));
|
|
25
|
+
return [`${GREEN}${label}${RESET} ${bar} ${DIM}${value}/10${RESET}`];
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
// Show a scored entry whenever a run finishes (contrived — a real mod would compute it).
|
|
29
|
+
cmd.on('run_end', () => {
|
|
30
|
+
cmd.showEntry('score', {label: 'confidence', value: 8});
|
|
31
|
+
});
|
|
32
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// Example mod: a tool the MODEL can call.
|
|
2
|
+
//
|
|
3
|
+
// cmd.addTool registers a ToolModule — the same shape the built-in tools use. The model
|
|
4
|
+
// sees the schema and decides when to call it; `run` receives the parsed input plus the
|
|
5
|
+
// harness Runtime (filesystem + shell), and returns a result:
|
|
6
|
+
// { ok: true, content: [{type: 'text', text}] } → success, text goes back to the model
|
|
7
|
+
// { ok: false, error } → the model sees the error and can retry
|
|
8
|
+
//
|
|
9
|
+
// Mark `readOnly: true` if the tool never mutates — it then stays available in plan mode
|
|
10
|
+
// and takes the read-only permission fast-path.
|
|
11
|
+
|
|
12
|
+
import type {ModApi} from '@commandcode/harness';
|
|
13
|
+
|
|
14
|
+
export default function (cmd: ModApi): void {
|
|
15
|
+
cmd.addTool({
|
|
16
|
+
schema: {
|
|
17
|
+
name: 'count_todos',
|
|
18
|
+
description:
|
|
19
|
+
'Count TODO/FIXME markers across the repository and report the total.',
|
|
20
|
+
input_schema: {
|
|
21
|
+
type: 'object',
|
|
22
|
+
properties: {
|
|
23
|
+
path: {
|
|
24
|
+
type: 'string',
|
|
25
|
+
description:
|
|
26
|
+
'Directory to search (defaults to the repo root).',
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
required: [],
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
readOnly: true,
|
|
33
|
+
run: async ({input}) => {
|
|
34
|
+
const path = typeof input.path === 'string' ? input.path : '.';
|
|
35
|
+
const result = await cmd.exec({
|
|
36
|
+
command: 'grep',
|
|
37
|
+
args: ['-rIoE', 'TODO|FIXME', path],
|
|
38
|
+
});
|
|
39
|
+
const count = result.stdout
|
|
40
|
+
.split('\n')
|
|
41
|
+
.filter(line => line.length > 0).length;
|
|
42
|
+
return {
|
|
43
|
+
ok: true,
|
|
44
|
+
content: [
|
|
45
|
+
{
|
|
46
|
+
type: 'text',
|
|
47
|
+
text: `${count} TODO/FIXME markers in ${path}`,
|
|
48
|
+
},
|
|
49
|
+
],
|
|
50
|
+
};
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// Example mod: declare configurable flags.
|
|
2
|
+
//
|
|
3
|
+
// cmd.addFlag declares a named option with a type and default. The user sets it at launch
|
|
4
|
+
// with repeatable `--mod-option name=value`:
|
|
5
|
+
// cmd --mod-option verbose=true --mod-option reviewer=alice
|
|
6
|
+
//
|
|
7
|
+
// Read the live value with cmd.getFlag(name). Boolean-declared flags coerce "true"/"false"
|
|
8
|
+
// text to a boolean; string flags keep the text verbatim. A flag only belongs to the mod
|
|
9
|
+
// that declared it.
|
|
10
|
+
|
|
11
|
+
import type {ModApi} from '@commandcode/harness';
|
|
12
|
+
|
|
13
|
+
export default function (cmd: ModApi): void {
|
|
14
|
+
cmd.addFlag('verbose', {
|
|
15
|
+
type: 'boolean',
|
|
16
|
+
default: false,
|
|
17
|
+
description: 'Announce every finished turn.',
|
|
18
|
+
});
|
|
19
|
+
cmd.addFlag('reviewer', {
|
|
20
|
+
type: 'string',
|
|
21
|
+
default: 'the team',
|
|
22
|
+
description: 'Who to address review summaries to.',
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
cmd.on('run_end', () => {
|
|
26
|
+
if (cmd.getFlag('verbose') !== true) return;
|
|
27
|
+
cmd.ui.notify(`turn done — summary is for ${cmd.getFlag('reviewer')}`);
|
|
28
|
+
});
|
|
29
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// Example mod: intercept what the user types before the model sees it.
|
|
2
|
+
//
|
|
3
|
+
// hooks.transformInput runs on every typed user prompt. Return:
|
|
4
|
+
// { action: 'transform', text } → rewrite the prompt (handlers chain; the next
|
|
5
|
+
// mod sees your rewrite)
|
|
6
|
+
// { action: 'handled', message? } → consume the prompt entirely; no model turn runs
|
|
7
|
+
// (an optional info row renders in its place)
|
|
8
|
+
// { action: 'continue' } / undefined → pass through unchanged
|
|
9
|
+
//
|
|
10
|
+
// Automated turns, meta messages, slash commands, and image-carrying prompts never reach
|
|
11
|
+
// transformInput — it only sees text the USER typed. A throwing handler is skipped, never
|
|
12
|
+
// blocking the prompt.
|
|
13
|
+
|
|
14
|
+
import type {ModApi} from '@commandcode/harness';
|
|
15
|
+
|
|
16
|
+
const SHORTCUTS: Record<string, string> = {
|
|
17
|
+
'#ship':
|
|
18
|
+
'Run the test suite, then the linter, then summarize what is left before this can ship.',
|
|
19
|
+
'#review': 'Review the current git diff for bugs and risky changes.',
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export default function (cmd: ModApi): void {
|
|
23
|
+
cmd.hooks({
|
|
24
|
+
transformInput: ({text}) => {
|
|
25
|
+
const trimmed = text.trim();
|
|
26
|
+
|
|
27
|
+
// A local slash-like command handled entirely by the mod.
|
|
28
|
+
if (trimmed === '#help') {
|
|
29
|
+
return {
|
|
30
|
+
action: 'handled',
|
|
31
|
+
message: `Shortcuts: ${Object.keys(SHORTCUTS).join(', ')}`,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const expansion = SHORTCUTS[trimmed];
|
|
36
|
+
if (expansion) return {action: 'transform', text: expansion};
|
|
37
|
+
|
|
38
|
+
return {action: 'continue'};
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
// Example mod: the KITCHEN SINK — every ModApi capability in one file, so you can see how
|
|
2
|
+
// the pieces fit together. In practice a mod does one job; this one is a tour.
|
|
3
|
+
//
|
|
4
|
+
// A mod is a TypeScript file that default-exports a factory. The factory receives the API
|
|
5
|
+
// bound as `cmd` and registers everything up front. It may be async (await setup work
|
|
6
|
+
// before returning). Every registration returns a Disposable — call .dispose() to undo
|
|
7
|
+
// exactly that one registration.
|
|
8
|
+
|
|
9
|
+
import type {ModApi} from '@commandcode/harness';
|
|
10
|
+
|
|
11
|
+
export default function (cmd: ModApi): void {
|
|
12
|
+
// ── identity ──────────────────────────────────────────────────────────────────────
|
|
13
|
+
// cmd.name is this mod's name; cmd.cwd is the workspace root.
|
|
14
|
+
|
|
15
|
+
// ── flags: configurable options (cmd --mod-option strict=true) ──────────────────────
|
|
16
|
+
cmd.addFlag('strict', {type: 'boolean', default: false});
|
|
17
|
+
|
|
18
|
+
// ── hooks: the MUTATING lifecycle (compose in registration order) ───────────────────
|
|
19
|
+
cmd.hooks({
|
|
20
|
+
// Add to the system prompt every request.
|
|
21
|
+
appendSystemPrompt: () => 'Prefer small, reviewable diffs.',
|
|
22
|
+
|
|
23
|
+
// Rewrite or block a tool call before it runs.
|
|
24
|
+
beforeToolCall: async ({toolName, input}) => {
|
|
25
|
+
if (toolName !== 'shell_command') return undefined;
|
|
26
|
+
const command =
|
|
27
|
+
typeof input.command === 'string' ? input.command : '';
|
|
28
|
+
if (
|
|
29
|
+
cmd.getFlag('strict') === true &&
|
|
30
|
+
command.includes('--no-verify')
|
|
31
|
+
)
|
|
32
|
+
return {
|
|
33
|
+
block: true,
|
|
34
|
+
additionalContext: 'strict mode forbids --no-verify',
|
|
35
|
+
};
|
|
36
|
+
return undefined;
|
|
37
|
+
},
|
|
38
|
+
|
|
39
|
+
// Inspect or rewrite a tool result after it runs.
|
|
40
|
+
afterToolCall: async ({result}) => {
|
|
41
|
+
if (typeof result !== 'string' || !result.includes('DEPRECATED'))
|
|
42
|
+
return undefined;
|
|
43
|
+
return {
|
|
44
|
+
additionalContext:
|
|
45
|
+
'A tool result mentioned DEPRECATED — flag it.',
|
|
46
|
+
};
|
|
47
|
+
},
|
|
48
|
+
|
|
49
|
+
// Intercept typed input before the model sees it.
|
|
50
|
+
transformInput: ({text}) =>
|
|
51
|
+
text.trim() === '#note'
|
|
52
|
+
? {action: 'handled', message: 'noted.'}
|
|
53
|
+
: {action: 'continue'},
|
|
54
|
+
|
|
55
|
+
// Fires once the run completes (post-turn work).
|
|
56
|
+
onRunEnd: async () => {
|
|
57
|
+
cmd.showEntry('summary', {tools: 'see feed'});
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// ── a tool the model can call ───────────────────────────────────────────────────────
|
|
62
|
+
cmd.addTool({
|
|
63
|
+
schema: {
|
|
64
|
+
name: 'repo_size',
|
|
65
|
+
description: 'Report the number of tracked files.',
|
|
66
|
+
input_schema: {type: 'object', properties: {}, required: []},
|
|
67
|
+
},
|
|
68
|
+
readOnly: true,
|
|
69
|
+
run: async () => {
|
|
70
|
+
const {stdout} = await cmd.exec({
|
|
71
|
+
command: 'git',
|
|
72
|
+
args: ['ls-files'],
|
|
73
|
+
});
|
|
74
|
+
const count = stdout.split('\n').filter(Boolean).length;
|
|
75
|
+
return {
|
|
76
|
+
ok: true,
|
|
77
|
+
content: [{type: 'text', text: `${count} tracked files`}],
|
|
78
|
+
};
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// ── a slash command ─────────────────────────────────────────────────────────────────
|
|
83
|
+
cmd.addCommand({
|
|
84
|
+
name: 'sink-status',
|
|
85
|
+
description: 'Show mod status',
|
|
86
|
+
handler: () => ({message: `strict = ${cmd.getFlag('strict')}`}),
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// ── a custom feed renderer + the entry it renders ──────────────────────────────────
|
|
90
|
+
cmd.addRenderer('summary', data => [`◆ summary: ${JSON.stringify(data)}`]);
|
|
91
|
+
|
|
92
|
+
// ── observe events (never mutate here) ──────────────────────────────────────────────
|
|
93
|
+
cmd.on('tool_errored', () => cmd.ui.notify('a tool errored', 'warning'));
|
|
94
|
+
|
|
95
|
+
// ── live surfaces (usable any time after the harness binds) ─────────────────────────
|
|
96
|
+
// cmd.ui.notify / confirm / select / input — talk to the user.
|
|
97
|
+
// cmd.queueMessage({content, deliverAs}) — steer the running loop or queue a follow-up.
|
|
98
|
+
// cmd.session — durable custom-entry persistence (undefined until bound).
|
|
99
|
+
// cmd.sessions — compact(), tree(), leafId(), navigateTree(...), setLabel(...).
|
|
100
|
+
// cmd.setModel / setEffort / setSessionName.
|
|
101
|
+
// cmd.getAllTools / getActiveTools / setActiveTools — a mod-managed tool filter.
|
|
102
|
+
//
|
|
103
|
+
// Example: on the first run, greet once via a queued steering message.
|
|
104
|
+
let greeted = false;
|
|
105
|
+
cmd.on('run_start', () => {
|
|
106
|
+
if (greeted) return;
|
|
107
|
+
greeted = true;
|
|
108
|
+
cmd.queueMessage({content: 'Remember to keep the change focused.'});
|
|
109
|
+
});
|
|
110
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// Example mod: the cross-agent lifecycle hooks — Stop, SessionStart/End, PostToolUseFailure,
|
|
2
|
+
// and SubagentStart/Stop. Together with beforeToolCall (PreToolUse), afterToolCall (PostToolUse),
|
|
3
|
+
// and transformInput (UserPromptSubmit), a mod can register a handler for every lifecycle hook
|
|
4
|
+
// point the major coding agents expose. See harness/docs/mods-api.md#cross-agent-hook-parity.
|
|
5
|
+
|
|
6
|
+
import type {ModApi} from '@commandcode/harness';
|
|
7
|
+
|
|
8
|
+
export default function (cmd: ModApi): void {
|
|
9
|
+
// SessionStart / SessionEnd — once per session, with typed source/reason the bare
|
|
10
|
+
// `session_start` / `session_shutdown` events don't carry.
|
|
11
|
+
cmd.hooks({
|
|
12
|
+
onSessionStart: ({source}) => {
|
|
13
|
+
cmd.ui.notify(`session started (${source})`);
|
|
14
|
+
},
|
|
15
|
+
onSessionEnd: ({reason}) => {
|
|
16
|
+
cmd.ui.notify(`session ending (${reason})`);
|
|
17
|
+
},
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
// PostToolUse vs PostToolUseFailure — the afterToolCall hook's `isError` param tells you
|
|
21
|
+
// whether the tool's own execution failed, so you can react only to failures.
|
|
22
|
+
let toolFailures = 0;
|
|
23
|
+
cmd.hooks({
|
|
24
|
+
afterToolCall: async ({toolName, isError}) => {
|
|
25
|
+
if (isError) {
|
|
26
|
+
toolFailures += 1;
|
|
27
|
+
return {
|
|
28
|
+
additionalContext: `(${toolName} failed — ${toolFailures} failure(s) so far this run)`,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
return undefined;
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
// The Stop hook — force-continue a run that would otherwise finish. Here: keep going until
|
|
36
|
+
// the model says it is done, but never more than a couple of nudges (the loop also caps this).
|
|
37
|
+
let nudges = 0;
|
|
38
|
+
cmd.hooks({
|
|
39
|
+
onStop: async ({lastAssistantText}) => {
|
|
40
|
+
if (/\bdone\b/i.test(lastAssistantText) || nudges >= 2) {
|
|
41
|
+
return {continue: false};
|
|
42
|
+
}
|
|
43
|
+
nudges += 1;
|
|
44
|
+
return {
|
|
45
|
+
continue: true,
|
|
46
|
+
reason: 'If the task is complete, say "done". Otherwise keep working.',
|
|
47
|
+
};
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// SubagentStart / SubagentStop — observe the `task` tool bracketing a nested sub-agent run.
|
|
52
|
+
cmd.on('subagent_start', event => {
|
|
53
|
+
if (event.type === 'subagent_start') {
|
|
54
|
+
cmd.ui.notify(`sub-agent started: ${event.subagentType}`);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
cmd.on('subagent_stop', event => {
|
|
58
|
+
if (event.type === 'subagent_stop') {
|
|
59
|
+
cmd.ui.notify(
|
|
60
|
+
`sub-agent ${event.subagentType} done (~${event.tokensUsed} tokens)`,
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// Example mod: observe the event stream (never mutate).
|
|
2
|
+
//
|
|
3
|
+
// cmd.on subscribes to any AgentEvent type plus the host lifecycle events
|
|
4
|
+
// `session_start` / `session_shutdown`. Observers CANNOT block tools or rewrite context —
|
|
5
|
+
// that is what cmd.hooks is for. The boundary is deliberate: hooks mutate, `on` observes.
|
|
6
|
+
//
|
|
7
|
+
// Handlers are isolated: a throw becomes a `mod_error` event, never a crashed session.
|
|
8
|
+
// Every cmd.on returns a Disposable — call .dispose() to stop observing.
|
|
9
|
+
//
|
|
10
|
+
// Common event types: run_start, run_end, turn_start, turn_end, model_request_start,
|
|
11
|
+
// model_request_end, tool_running, tool_completed, tool_errored, notice.
|
|
12
|
+
|
|
13
|
+
import type {ModApi} from '@commandcode/harness';
|
|
14
|
+
|
|
15
|
+
export default function (cmd: ModApi): void {
|
|
16
|
+
let toolCalls = 0;
|
|
17
|
+
|
|
18
|
+
cmd.on('tool_completed', () => {
|
|
19
|
+
toolCalls += 1;
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
cmd.on('run_end', () => {
|
|
23
|
+
cmd.ui.notify(`turn finished — ${toolCalls} tool call(s) this session`);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
// The cross-mod bus lets mods talk to each other without knowing about one another.
|
|
27
|
+
cmd.on('tool_errored', () => {
|
|
28
|
+
cmd.events.emit('tool-failed', {at: toolCalls});
|
|
29
|
+
});
|
|
30
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// Example mod: a slash command.
|
|
2
|
+
//
|
|
3
|
+
// Drop this file at ~/.commandcode/mods/standup.ts and `/standup` appears in the TUI
|
|
4
|
+
// slash menu. A command handler returns DATA, never a live callback:
|
|
5
|
+
// { prompt } → submitted as an automated turn (the model runs it)
|
|
6
|
+
// { message } → rendered as an info row (no model turn)
|
|
7
|
+
// nothing → pure side effect (e.g. it only called cmd.ui / cmd.exec)
|
|
8
|
+
//
|
|
9
|
+
// The handler receives { args, ui, cwd, exec } — `args` is the raw text after the
|
|
10
|
+
// command name (`/standup since=yesterday` → args === "since=yesterday").
|
|
11
|
+
|
|
12
|
+
import type {ModApi} from '@commandcode/harness';
|
|
13
|
+
|
|
14
|
+
export default function (cmd: ModApi): void {
|
|
15
|
+
cmd.addCommand({
|
|
16
|
+
name: 'standup',
|
|
17
|
+
description: 'Summarize what changed today for standup',
|
|
18
|
+
argumentHint: '[since]',
|
|
19
|
+
handler: ({args}) => {
|
|
20
|
+
const since = args.trim() || 'this morning';
|
|
21
|
+
return {
|
|
22
|
+
prompt: `Summarize the git commits and working changes since ${since} as three standup bullets: done, in-progress, blockers.`,
|
|
23
|
+
};
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
// A second command that renders an info row instead of running the model.
|
|
28
|
+
cmd.addCommand({
|
|
29
|
+
name: 'where',
|
|
30
|
+
description: 'Print the current working directory',
|
|
31
|
+
handler: ({cwd}) => ({message: `You are in ${cwd}`}),
|
|
32
|
+
});
|
|
33
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
<!-- GENERATED FILE — do not edit. Source: packages/docs/src/app/mods/page.mdx. Regenerate: pnpm generate:knowledge -->
|
|
2
|
+
|
|
3
|
+
# ModApi reference
|
|
4
|
+
|
|
5
|
+
`export default function (cmd: ModApi)` — the factory. `ModFactory = (cmd: ModApi) => void | Promise<void>`. Every `add*` / `on` / `hooks` call returns `Disposable { dispose(): void }` — `.dispose()` undoes exactly that one registration (idempotent; a second call is a no-op). Disposables make a registration retractable and are the seam future hot-reload will recycle.
|
|
6
|
+
|
|
7
|
+
## Fields
|
|
8
|
+
|
|
9
|
+
| Member | Type | Notes |
|
|
10
|
+
|---|---|---|
|
|
11
|
+
| `cmd.name` | `string` | the mod's name (from filename / manifest) |
|
|
12
|
+
| `cmd.cwd` | `string` | workspace root |
|
|
13
|
+
| `cmd.session` | `ModSessionApi \| undefined` | persistence seam; undefined until bound |
|
|
14
|
+
| `cmd.events` | `ModEventBus` | `emit(channel, data?)` / `on(channel, handler)` cross-mod bus |
|
|
15
|
+
| `cmd.ui` | `ModUi` | `notify` / `confirm` / `select` / `input` (each with `{timeoutMs, timeoutValue}`) / `setStatus` / `widget` / `refreshWidgets` |
|
|
16
|
+
| `cmd.sessions` | `ModSessionControls` | `compact` / `tree` / `leafId` / `navigateTree` / `setLabel` |
|
|
17
|
+
|
|
18
|
+
## Registration (factory-time; each returns `Disposable`)
|
|
19
|
+
|
|
20
|
+
| Method | Signature |
|
|
21
|
+
|---|---|
|
|
22
|
+
| `cmd.hooks` | `(hooks: ModHooks) => Disposable` |
|
|
23
|
+
| `cmd.addTool` | `(tool: ToolModule) => Disposable` |
|
|
24
|
+
| `cmd.addCommand` | `(command: {name; description?; argumentHint?; handler}) => Disposable` |
|
|
25
|
+
| `cmd.addFlag` | `(name, {type: 'boolean'\|'string'; default?; description?}) => Disposable` |
|
|
26
|
+
| `cmd.addProvider` | `(module: ProviderModule) => Disposable` |
|
|
27
|
+
| `cmd.addRenderer` | `(customType: string, (data) => readonly string[]) => Disposable` |
|
|
28
|
+
| `cmd.on` | `(event: AgentEventType \| 'session_start' \| 'session_shutdown', handler) => Disposable` |
|
|
29
|
+
|
|
30
|
+
What each does:
|
|
31
|
+
|
|
32
|
+
- `cmd.hooks(hooks)` — the mutating lifecycle hooks: the `AgentMod` surface (transformContext, beforeToolCall, afterToolCall, onTurnStart/End, appendSystemPrompt, shouldStopAfterTurn, prepareNextTurn, onRunEnd, **onStop**) **plus `transformInput`, `onSessionStart`, and `onSessionEnd`**. Multiple calls compose in order with mod-runner semantics (threading, chaining, any-true, later-wins). See [the hook contracts](./hooks-and-events.md#the-hook-contracts) for the full contract of each hook.
|
|
33
|
+
- `cmd.addTool(toolModule)` — a `ToolModule` the model can call; same collision policy as `AgentMod.tools` (existing names win, collision emits `mod_error`). The tool's `run` receives `{input, runtime, signal}` and returns `{ok: true, content: [{type: 'text', text}]}` or `{ok: false, error}`. Mark `readOnly: true` when the tool never mutates (stays available in plan mode).
|
|
34
|
+
- `cmd.addCommand({name, description, handler})` — a `/name` slash command. The handler returns data, not callbacks: `{prompt}` submits an automated turn, `{message}` renders an info row, nothing = pure side effect. It gets `{args, ui, cwd, exec}`. First registration of a name wins across mods.
|
|
35
|
+
- `cmd.addFlag(name, {type, default})` / `cmd.getFlag(name)` — named options; values come from repeatable `--mod-option name=value` CLI flags (defaults apply otherwise).
|
|
36
|
+
- `cmd.on(event, handler)` — observe any `AgentEvent` plus the host lifecycle events `session_start` / `session_shutdown` (fired when a harness binds the host and when a session switch or dispose replaces it). Handlers are isolated: a throw becomes a `mod_error {hook: 'on:<event>'}` event, never a crash.
|
|
37
|
+
- `cmd.addProvider(providerModule)` — register a model provider through the same `ProviderModule` seam the built-ins use (id, transport, auth hooks, model list). The host appends mod providers to its set — mods extend the provider set, never replace it.
|
|
38
|
+
- `cmd.addRenderer(customType, data => lines)` — a renderer for a custom entry type; returns the lines to print (style them with ansi escapes — picocolors, `@commandcode/tui` helpers, or raw codes). First registration per type wins across mods.
|
|
39
|
+
|
|
40
|
+
## Live methods (any time after the harness binds)
|
|
41
|
+
|
|
42
|
+
| Method | Signature |
|
|
43
|
+
|---|---|
|
|
44
|
+
| `cmd.getFlag` | `(name) => boolean \| string \| undefined` |
|
|
45
|
+
| `cmd.showEntry` | `(customType, data?) => void` |
|
|
46
|
+
| `cmd.queueMessage` | `({content, deliverAs?: 'steer' \| 'follow-up'}) => void` |
|
|
47
|
+
| `cmd.exec` | `({command, args?, cwd?, signal?}) => Promise<{stdout, stderr, code}>` |
|
|
48
|
+
| `cmd.setSessionName` / `setModel` / `setEffort` | `(value) => void` (buffered pre-bind) |
|
|
49
|
+
| `cmd.getAllTools` / `getActiveTools` | `() => readonly string[]` |
|
|
50
|
+
| `cmd.setActiveTools` | `(names: readonly string[]) => void` |
|
|
51
|
+
|
|
52
|
+
Details:
|
|
53
|
+
|
|
54
|
+
- `cmd.ui` — `notify` (a `notice` feed row), `confirm` / `select` / `input` (the Interaction question modal in the TUI; deterministic defaults headless: false/undefined), `setStatus`, `widget`, `refreshWidgets`. The full dialog/status/widget contract is in [UI surface](./ui.md#ui-surface).
|
|
55
|
+
- `cmd.queueMessage({content, deliverAs})` — `steer` lands after the current tool batch (behind any queued user messages in the same poll), `follow-up` only when the run would stop (ahead of continuation nudges, behind queued user input).
|
|
56
|
+
- `cmd.session` — the `ModSessionApi` persistence seam (appendCustomEntry / appendCustomMessageEntry / getCustomEntries), undefined until bound. See [ModContext.session — the mods' persistence surface](./hooks-and-events.md#mod-context-session-the-mods-persistence-surface).
|
|
57
|
+
- `cmd.showEntry(customType, data)` — render a custom entry into the live feed through the renderer registered for that type (unrendered types pretty-print as JSON). The TUI wires the sink; headless runs drop entries. Pair with `cmd.session.appendCustomEntry` when the data should also persist.
|
|
58
|
+
- `cmd.sessions` — live session controls: `compact()`, `tree()` (the session branch tree as stable `{id, label, children}` nodes), `leafId()`, `navigateTree({targetId, summarize?, customInstructions?})`, and `setLabel({targetId, label})`. These read live harness state, so unlike the buffered setters they throw with a clear message when no session is bound yet.
|
|
59
|
+
- `cmd.exec({command, args})` — run a process through the harness Runtime (args are shell-quoted).
|
|
60
|
+
- `cmd.getAllTools()` / `cmd.getActiveTools()` / `cmd.setActiveTools(names)` — a mod-managed tool filter: disabled tools vanish from the model's schemas and refuse execution.
|
|
61
|
+
- `cmd.events` — a tiny cross-mod pub/sub bus (`emit` / `on`).
|
|
62
|
+
|
|
63
|
+
## `ModHooks` (the mutating lifecycle — all optional, all composable)
|
|
64
|
+
|
|
65
|
+
`transformContext`, `appendSystemPrompt`, `beforeToolCall`, `afterToolCall`, `onTurnStart`, `onTurnEnd`, `shouldStopAfterTurn`, `prepareNextTurn`, `onRunEnd`, `onStop`, plus the host-level `transformInput`, `onSessionStart`, and `onSessionEnd`.
|
|
66
|
+
|
|
67
|
+
- `beforeToolCall` returns `{block?, input?, additionalContext?, terminate?}`.
|
|
68
|
+
- `afterToolCall` receives an `isError` param (PostToolUse vs PostToolUseFailure) and returns `{content?, isError?, additionalContext?, terminate?, modState?}`.
|
|
69
|
+
- `onStop` receives `{state, stopReason, turnNumber, lastAssistantText}` and returns `{continue?, reason?}`.
|
|
70
|
+
- `transformInput` returns `{action:'continue'} \| {action:'transform', text} \| {action:'handled', message?}`.
|
|
71
|
+
- `onSessionStart`/`onSessionEnd` receive `{source}` / `{reason}`.
|
|
72
|
+
|
|
73
|
+
The per-hook contracts, ordering guarantees, and error policy are in [the hook contracts](./hooks-and-events.md#the-hook-contracts).
|
|
74
|
+
|
|
75
|
+
- `transformInput({text})` intercepts typed user prompts before they reach the model (the mods' **UserPromptSubmit** hook): return `{action: 'transform', text}` to rewrite (handlers chain — the next sees the rewrite), `{action: 'handled', message?}` to consume the prompt entirely (an optional info row renders in its place), or `undefined`/`{action: 'continue'}` to pass through. Only real typed input is intercepted — automated turns, meta messages, image-carrying prompts, and slash commands never route through it; a throwing handler is skipped (`mod_error {hook: 'transformInput'}`), never a swallowed prompt. It lives in `hooks` because it MUTATES what the agent sees.
|
|
76
|
+
- `onStop({state, stopReason, turnNumber, lastAssistantText})` is the **Stop** hook — return `{continue: true, reason?}` to force a run that would otherwise finish to keep going (the reason rides an automated turn). Fires only on natural completion, any-mod wins, and the loop caps consecutive continuations.
|
|
77
|
+
- `onSessionStart({source})` / `onSessionEnd({reason})` are the once-per-session **SessionStart** / **SessionEnd** hooks — fired when the host binds to / tears down a session (`source: 'startup' | 'resume'`, `reason: 'shutdown' | 'replaced'`). Act through the mod's captured `cmd`; they run fire-and-forget (a throw becomes `mod_error`), so a session hook never blocks bind/dispose. Pure observation is also available via `cmd.on('session_start' | 'session_shutdown')` — these named hooks add the typed metadata.
|
|
78
|
+
|
|
79
|
+
## `on` event types
|
|
80
|
+
|
|
81
|
+
Any `AgentEvent['type']` — including `run_start`, `run_end`, `turn_start`, `turn_end`, `model_request_start`, `model_request_end`, `tool_running`, `tool_completed`, `tool_errored`, `subagent_start`, `subagent_stop`, `compaction_start`, `compaction_done`, `notice`, `session_titled`, `mod_error` — plus the two host lifecycle events `session_start` / `session_shutdown`. (`subagent_start`/`subagent_stop` are SubagentStart/Stop; `compaction_start`/`compaction_done` are Pre/PostCompact; `notice` is Notification.) The full payload catalog is in [the AgentEvent catalog](./hooks-and-events.md#agent-event-catalog).
|