@namzu/cli 18.1.0 → 20.0.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 +5951 -0
- package/README.md +23 -24
- package/dist/bin.js +12 -1
- package/dist/bin.js.map +1 -1
- package/dist/checkpoints/store.d.ts +75 -0
- package/dist/checkpoints/store.d.ts.map +1 -0
- package/dist/checkpoints/store.js +177 -0
- package/dist/checkpoints/store.js.map +1 -0
- package/dist/checkpoints/wrap.d.ts +15 -0
- package/dist/checkpoints/wrap.d.ts.map +1 -0
- package/dist/checkpoints/wrap.js +37 -0
- package/dist/checkpoints/wrap.js.map +1 -0
- package/dist/cli.d.ts +6 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +12 -1
- package/dist/cli.js.map +1 -1
- package/dist/commands/acp.d.ts.map +1 -1
- package/dist/commands/acp.js +2 -0
- package/dist/commands/acp.js.map +1 -1
- package/dist/commands/drain.d.ts.map +1 -1
- package/dist/commands/drain.js +15 -2
- package/dist/commands/drain.js.map +1 -1
- package/dist/commands/provider-wait.d.ts +44 -0
- package/dist/commands/provider-wait.d.ts.map +1 -0
- package/dist/commands/provider-wait.js +65 -0
- package/dist/commands/provider-wait.js.map +1 -0
- package/dist/commands/run-flags.d.ts +13 -1
- package/dist/commands/run-flags.d.ts.map +1 -1
- package/dist/commands/run-flags.js +41 -0
- package/dist/commands/run-flags.js.map +1 -1
- package/dist/commands/run-stream.d.ts.map +1 -1
- package/dist/commands/run-stream.js +33 -13
- package/dist/commands/run-stream.js.map +1 -1
- package/dist/commands/run.d.ts.map +1 -1
- package/dist/commands/run.js +132 -78
- package/dist/commands/run.js.map +1 -1
- package/dist/commands/stdin.d.ts +17 -0
- package/dist/commands/stdin.d.ts.map +1 -0
- package/dist/commands/stdin.js +69 -0
- package/dist/commands/stdin.js.map +1 -0
- package/dist/config/load.d.ts.map +1 -1
- package/dist/config/load.js +107 -2
- package/dist/config/load.js.map +1 -1
- package/dist/config/schema.d.ts +80 -5
- package/dist/config/schema.d.ts.map +1 -1
- package/dist/config/schema.js.map +1 -1
- package/dist/context/doctrine.d.ts +16 -38
- package/dist/context/doctrine.d.ts.map +1 -1
- package/dist/context/doctrine.js +16 -75
- package/dist/context/doctrine.js.map +1 -1
- package/dist/context/environment.d.ts +2 -0
- package/dist/context/environment.d.ts.map +1 -1
- package/dist/context/environment.js +3 -0
- package/dist/context/environment.js.map +1 -1
- package/dist/integrations/mcp/servers.d.ts +19 -0
- package/dist/integrations/mcp/servers.d.ts.map +1 -1
- package/dist/integrations/mcp/servers.js +23 -2
- package/dist/integrations/mcp/servers.js.map +1 -1
- package/dist/integrations/plugins/runtime.d.ts +8 -2
- package/dist/integrations/plugins/runtime.d.ts.map +1 -1
- package/dist/integrations/plugins/runtime.js +24 -12
- package/dist/integrations/plugins/runtime.js.map +1 -1
- package/dist/integrations/providers/chain-capabilities.js +1 -1
- package/dist/integrations/providers/chain-capabilities.js.map +1 -1
- package/dist/integrations/providers/credential-store.d.ts +1 -1
- package/dist/integrations/providers/credential-store.js +1 -1
- package/dist/integrations/providers/harness-credentials.d.ts.map +1 -1
- package/dist/integrations/providers/harness-credentials.js +7 -0
- package/dist/integrations/providers/harness-credentials.js.map +1 -1
- package/dist/integrations/providers/registry.d.ts +2 -2
- package/dist/integrations/providers/registry.js +2 -2
- package/dist/integrations/sessions/store.d.ts +14 -22
- package/dist/integrations/sessions/store.d.ts.map +1 -1
- package/dist/integrations/sessions/store.js +78 -175
- package/dist/integrations/sessions/store.js.map +1 -1
- package/dist/integrations/sessions/transcript-export.d.ts.map +1 -1
- package/dist/integrations/sessions/transcript-export.js +6 -2
- package/dist/integrations/sessions/transcript-export.js.map +1 -1
- package/dist/integrations/state/identity.d.ts +21 -0
- package/dist/integrations/state/identity.d.ts.map +1 -0
- package/dist/integrations/state/identity.js +49 -0
- package/dist/integrations/state/identity.js.map +1 -0
- package/dist/integrations/state/immutable-json.d.ts +10 -0
- package/dist/integrations/state/immutable-json.d.ts.map +1 -0
- package/dist/integrations/state/immutable-json.js +38 -0
- package/dist/integrations/state/immutable-json.js.map +1 -0
- package/dist/integrations/state/project.d.ts +10 -0
- package/dist/integrations/state/project.d.ts.map +1 -0
- package/dist/integrations/state/project.js +15 -0
- package/dist/integrations/state/project.js.map +1 -0
- package/dist/integrations/state/report.d.ts.map +1 -1
- package/dist/integrations/state/report.js +42 -12
- package/dist/integrations/state/report.js.map +1 -1
- package/dist/integrations/subagents/definitions.d.ts +19 -0
- package/dist/integrations/subagents/definitions.d.ts.map +1 -0
- package/dist/integrations/subagents/definitions.js +26 -0
- package/dist/integrations/subagents/definitions.js.map +1 -0
- package/dist/integrations/subagents/parent.d.ts +13 -0
- package/dist/integrations/subagents/parent.d.ts.map +1 -0
- package/dist/integrations/subagents/parent.js +57 -0
- package/dist/integrations/subagents/parent.js.map +1 -0
- package/dist/integrations/subagents/runtime.d.ts +33 -16
- package/dist/integrations/subagents/runtime.d.ts.map +1 -1
- package/dist/integrations/subagents/runtime.js +450 -150
- package/dist/integrations/subagents/runtime.js.map +1 -1
- package/dist/memory/presentation.d.ts +9 -0
- package/dist/memory/presentation.d.ts.map +1 -0
- package/dist/memory/presentation.js +37 -0
- package/dist/memory/presentation.js.map +1 -0
- package/dist/memory/store.d.ts +35 -15
- package/dist/memory/store.d.ts.map +1 -1
- package/dist/memory/store.js +79 -25
- package/dist/memory/store.js.map +1 -1
- package/dist/permissions/mode.d.ts +15 -45
- package/dist/permissions/mode.d.ts.map +1 -1
- package/dist/permissions/mode.js +36 -13
- package/dist/permissions/mode.js.map +1 -1
- package/dist/release-notes.d.ts +20 -0
- package/dist/release-notes.d.ts.map +1 -0
- package/dist/release-notes.js +64 -0
- package/dist/release-notes.js.map +1 -0
- package/dist/tui/AgentExplorer.d.ts +1 -1
- package/dist/tui/AgentExplorer.d.ts.map +1 -1
- package/dist/tui/AgentExplorer.js +25 -21
- package/dist/tui/AgentExplorer.js.map +1 -1
- package/dist/tui/App.d.ts.map +1 -1
- package/dist/tui/App.js +701 -245
- package/dist/tui/App.js.map +1 -1
- package/dist/tui/BrandHeader.d.ts +12 -0
- package/dist/tui/BrandHeader.d.ts.map +1 -0
- package/dist/tui/BrandHeader.js +18 -0
- package/dist/tui/BrandHeader.js.map +1 -0
- package/dist/tui/ChoicePicker.d.ts +11 -1
- package/dist/tui/ChoicePicker.d.ts.map +1 -1
- package/dist/tui/ChoicePicker.js +46 -13
- package/dist/tui/ChoicePicker.js.map +1 -1
- package/dist/tui/Composer.d.ts +5 -2
- package/dist/tui/Composer.d.ts.map +1 -1
- package/dist/tui/Composer.js +6 -11
- package/dist/tui/Composer.js.map +1 -1
- package/dist/tui/ComposerFrame.d.ts +10 -0
- package/dist/tui/ComposerFrame.d.ts.map +1 -0
- package/dist/tui/ComposerFrame.js +93 -0
- package/dist/tui/ComposerFrame.js.map +1 -0
- package/dist/tui/EditPromptPicker.d.ts.map +1 -1
- package/dist/tui/EditPromptPicker.js +1 -1
- package/dist/tui/EditPromptPicker.js.map +1 -1
- package/dist/tui/LiveActivity.d.ts +12 -5
- package/dist/tui/LiveActivity.d.ts.map +1 -1
- package/dist/tui/LiveActivity.js +17 -32
- package/dist/tui/LiveActivity.js.map +1 -1
- package/dist/tui/PermissionOverlay.d.ts +26 -2
- package/dist/tui/PermissionOverlay.d.ts.map +1 -1
- package/dist/tui/PermissionOverlay.js +109 -16
- package/dist/tui/PermissionOverlay.js.map +1 -1
- package/dist/tui/Picker.d.ts +2 -2
- package/dist/tui/Picker.d.ts.map +1 -1
- package/dist/tui/Picker.js +82 -52
- package/dist/tui/Picker.js.map +1 -1
- package/dist/tui/StatusBar.d.ts.map +1 -1
- package/dist/tui/StatusBar.js +5 -2
- package/dist/tui/StatusBar.js.map +1 -1
- package/dist/tui/TaskList.d.ts +3 -1
- package/dist/tui/TaskList.d.ts.map +1 -1
- package/dist/tui/TaskList.js +13 -4
- package/dist/tui/TaskList.js.map +1 -1
- package/dist/tui/Transcript.d.ts +4 -12
- package/dist/tui/Transcript.d.ts.map +1 -1
- package/dist/tui/Transcript.js +45 -75
- package/dist/tui/Transcript.js.map +1 -1
- package/dist/tui/agent.d.ts +116 -51
- package/dist/tui/agent.d.ts.map +1 -1
- package/dist/tui/agent.js +539 -307
- package/dist/tui/agent.js.map +1 -1
- package/dist/tui/choice-selection.d.ts +17 -0
- package/dist/tui/choice-selection.d.ts.map +1 -0
- package/dist/tui/choice-selection.js +38 -0
- package/dist/tui/choice-selection.js.map +1 -0
- package/dist/tui/exit-summary.d.ts +7 -1
- package/dist/tui/exit-summary.d.ts.map +1 -1
- package/dist/tui/exit-summary.js +12 -3
- package/dist/tui/exit-summary.js.map +1 -1
- package/dist/tui/index.d.ts +3 -1
- package/dist/tui/index.d.ts.map +1 -1
- package/dist/tui/index.js +5 -3
- package/dist/tui/index.js.map +1 -1
- package/dist/tui/logo.d.ts +7 -22
- package/dist/tui/logo.d.ts.map +1 -1
- package/dist/tui/logo.js +8 -34
- package/dist/tui/logo.js.map +1 -1
- package/dist/tui/model-choices.d.ts.map +1 -1
- package/dist/tui/model-choices.js +21 -6
- package/dist/tui/model-choices.js.map +1 -1
- package/dist/tui/permission-review.d.ts +15 -4
- package/dist/tui/permission-review.d.ts.map +1 -1
- package/dist/tui/permission-review.js +48 -9
- package/dist/tui/permission-review.js.map +1 -1
- package/dist/tui/provider-selection.d.ts +4 -0
- package/dist/tui/provider-selection.d.ts.map +1 -0
- package/dist/tui/provider-selection.js +16 -0
- package/dist/tui/provider-selection.js.map +1 -0
- package/dist/tui/run-interruption.d.ts +3 -2
- package/dist/tui/run-interruption.d.ts.map +1 -1
- package/dist/tui/run-interruption.js +2 -1
- package/dist/tui/run-interruption.js.map +1 -1
- package/dist/tui/shell-escape.d.ts +42 -0
- package/dist/tui/shell-escape.d.ts.map +1 -0
- package/dist/tui/shell-escape.js +110 -0
- package/dist/tui/shell-escape.js.map +1 -0
- package/dist/tui/slashCommands.d.ts +82 -31
- package/dist/tui/slashCommands.d.ts.map +1 -1
- package/dist/tui/slashCommands.js +518 -306
- package/dist/tui/slashCommands.js.map +1 -1
- package/dist/tui/terminal-choice-text.d.ts +8 -0
- package/dist/tui/terminal-choice-text.d.ts.map +1 -0
- package/dist/tui/terminal-choice-text.js +37 -0
- package/dist/tui/terminal-choice-text.js.map +1 -0
- package/dist/tui/theme.d.ts +3 -4
- package/dist/tui/theme.d.ts.map +1 -1
- package/dist/tui/theme.js +16 -17
- package/dist/tui/theme.js.map +1 -1
- package/dist/tui/types.d.ts +9 -13
- package/dist/tui/types.d.ts.map +1 -1
- package/package.json +7 -5
|
@@ -15,17 +15,18 @@
|
|
|
15
15
|
* already carries. A command that had to compute its own answer would be a
|
|
16
16
|
* second source for a fact the kernel already owns, and the two would drift.
|
|
17
17
|
*
|
|
18
|
-
*
|
|
18
|
+
* Ctrl+O's reprint is the same discipline with the split drawn one step earlier. The
|
|
19
19
|
* lines it prints were captured when the tool ran and are held on the transcript
|
|
20
|
-
* row, which this module cannot see and must not:
|
|
21
|
-
*
|
|
22
|
-
*
|
|
20
|
+
* row, which this module cannot see and must not: App — the only thing that knows
|
|
21
|
+
* what rows exist — resolves it. Giving this module the transcript to search
|
|
22
|
+
* would put the answer
|
|
23
23
|
* to "does block 4 exist" in two places at once.
|
|
24
24
|
*/
|
|
25
25
|
import { kernelHostCommands, } from '@namzu/sdk';
|
|
26
26
|
import { renderConfigDebug } from '../config/debug.js';
|
|
27
|
-
import { isPermissionMode } from '../permissions/mode.js';
|
|
28
|
-
import {
|
|
27
|
+
import { effectivePermissionMode, isPermissionMode, permissionModeDescription, permissionModeLabel, } from '../permissions/mode.js';
|
|
28
|
+
import { readChangelog, renderReleaseNotes } from '../release-notes.js';
|
|
29
|
+
import { ARGUMENTS_TOKEN, expandCommand } from '../user-commands/store.js';
|
|
29
30
|
import { isCompletionArgument } from './login-prompt.js';
|
|
30
31
|
function renameConversationAction(_ctx, args) {
|
|
31
32
|
const text = args.join(' ').trim();
|
|
@@ -107,7 +108,7 @@ export class CommandNameCollisionError extends Error {
|
|
|
107
108
|
* {@link CommandNameCollisionError} exists to prevent; naming each one here
|
|
108
109
|
* keeps the refusal for every collision nobody decided about.
|
|
109
110
|
*/
|
|
110
|
-
export const HOST_OWNED_COMMAND_NAMES = ['skills'];
|
|
111
|
+
export const HOST_OWNED_COMMAND_NAMES = ['skills', 'agents', 'goal'];
|
|
111
112
|
export function mergeHostCommands(descriptors, locals = CLI_LOCAL_COMMANDS) {
|
|
112
113
|
const localNames = new Set(locals.map((c) => c.name));
|
|
113
114
|
const owned = new Set(HOST_OWNED_COMMAND_NAMES);
|
|
@@ -121,6 +122,11 @@ export function mergeHostCommands(descriptors, locals = CLI_LOCAL_COMMANDS) {
|
|
|
121
122
|
...fromKernel.map((descriptor) => ({
|
|
122
123
|
name: descriptor.name,
|
|
123
124
|
description: descriptor.description,
|
|
125
|
+
help: {
|
|
126
|
+
// An arbitrary JSON Schema does not declare positional CLI syntax.
|
|
127
|
+
usage: [`/${descriptor.name}${descriptor.args ? ' [arguments]' : ''}`],
|
|
128
|
+
...(descriptor.hint ? { details: [descriptor.hint] } : {}),
|
|
129
|
+
},
|
|
124
130
|
action: (_ctx, args) => ({
|
|
125
131
|
kind: 'host-command',
|
|
126
132
|
name: descriptor.name,
|
|
@@ -187,6 +193,62 @@ export function parseSlash(line) {
|
|
|
187
193
|
return null;
|
|
188
194
|
return { name, args };
|
|
189
195
|
}
|
|
196
|
+
/** Help reads metadata only: even a mutating or unavailable command is safe to inspect. */
|
|
197
|
+
function commandHelp(ctx, args) {
|
|
198
|
+
const name = args[0]?.replace(/^\//, '');
|
|
199
|
+
if (args.length !== 1 || !name) {
|
|
200
|
+
return {
|
|
201
|
+
kind: 'message',
|
|
202
|
+
role: 'system',
|
|
203
|
+
content: 'Usage: /help [command]. For example: /help permissions.',
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
const command = (ctx.builtins ?? CLI_LOCAL_COMMANDS).find((entry) => entry.name === name);
|
|
207
|
+
if (command) {
|
|
208
|
+
const reason = command.unavailable?.(ctx);
|
|
209
|
+
return {
|
|
210
|
+
kind: 'message',
|
|
211
|
+
role: 'system',
|
|
212
|
+
content: [
|
|
213
|
+
`/${command.name}`,
|
|
214
|
+
command.description,
|
|
215
|
+
'',
|
|
216
|
+
'Usage:',
|
|
217
|
+
...(command.help?.usage ?? [`/${command.name}`]).map((usage) => ` ${usage}`),
|
|
218
|
+
...(command.help?.details?.length ? ['', ...command.help.details] : []),
|
|
219
|
+
...(reason ? ['', `Unavailable: ${reason}`] : []),
|
|
220
|
+
].join('\n'),
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
const user = ctx.userCommands.find((entry) => entry.name === name);
|
|
224
|
+
if (user) {
|
|
225
|
+
const acceptsArguments = user.template.includes(ARGUMENTS_TOKEN);
|
|
226
|
+
return {
|
|
227
|
+
kind: 'message',
|
|
228
|
+
role: 'system',
|
|
229
|
+
content: [
|
|
230
|
+
`/${user.name}`,
|
|
231
|
+
user.description,
|
|
232
|
+
'',
|
|
233
|
+
...(user.problem
|
|
234
|
+
? []
|
|
235
|
+
: [
|
|
236
|
+
'Usage:',
|
|
237
|
+
` /${user.name}${acceptsArguments ? ' [arguments]' : ''}`,
|
|
238
|
+
'',
|
|
239
|
+
'Running this command sends its saved prompt to the model.',
|
|
240
|
+
]),
|
|
241
|
+
`${user.source === 'project' ? 'Project command' : 'User command (all projects)'}: ${user.path}`,
|
|
242
|
+
...(user.problem ? [`Unavailable: ${user.problem}`] : []),
|
|
243
|
+
].join('\n'),
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
return {
|
|
247
|
+
kind: 'message',
|
|
248
|
+
role: 'system',
|
|
249
|
+
content: `Unknown command: /${name}. Use /help to browse available commands.`,
|
|
250
|
+
};
|
|
251
|
+
}
|
|
190
252
|
/**
|
|
191
253
|
* The commands that are genuinely this host's.
|
|
192
254
|
*
|
|
@@ -196,10 +258,61 @@ export function parseSlash(line) {
|
|
|
196
258
|
* being restated in this file.
|
|
197
259
|
*/
|
|
198
260
|
export const CLI_LOCAL_COMMANDS = [
|
|
261
|
+
{
|
|
262
|
+
name: 'settings',
|
|
263
|
+
description: 'View current settings and change model, reasoning or permissions.',
|
|
264
|
+
action: () => ({ kind: 'settings-picker' }),
|
|
265
|
+
},
|
|
266
|
+
{
|
|
267
|
+
name: 'agents',
|
|
268
|
+
description: 'Inspect delegated agents; /agents available lists configured agents.',
|
|
269
|
+
help: { usage: ['/agents [running|available]'] },
|
|
270
|
+
action: (_ctx, args) => args.length === 0 || (args.length === 1 && args[0] === 'running')
|
|
271
|
+
? { kind: 'agent-cockpit' }
|
|
272
|
+
: args.length === 1 && args[0] === 'available'
|
|
273
|
+
? { kind: 'host-command', name: 'agents', args: [] }
|
|
274
|
+
: { kind: 'message', role: 'system', content: 'Usage: /agents [running|available]' },
|
|
275
|
+
},
|
|
276
|
+
{
|
|
277
|
+
name: 'goal',
|
|
278
|
+
description: 'Manage this conversation’s goal and automatic continuation.',
|
|
279
|
+
help: {
|
|
280
|
+
usage: [
|
|
281
|
+
'/goal',
|
|
282
|
+
'/goal status',
|
|
283
|
+
'/goal set [objective]',
|
|
284
|
+
'/goal edit [objective]',
|
|
285
|
+
'/goal pause|resume|clear',
|
|
286
|
+
],
|
|
287
|
+
details: [
|
|
288
|
+
'Set and edit open an editor when no objective is supplied. Direct /goal <objective> also creates a goal.',
|
|
289
|
+
'Creating or resuming a goal enables automatic work. Pausing or clearing stops future automatic turns.',
|
|
290
|
+
],
|
|
291
|
+
},
|
|
292
|
+
action: (_ctx, args) => {
|
|
293
|
+
if (args.length === 0)
|
|
294
|
+
return { kind: 'goal-picker' };
|
|
295
|
+
if (args.length === 1 && args[0] === 'status') {
|
|
296
|
+
return { kind: 'host-command', name: 'goal', args: [] };
|
|
297
|
+
}
|
|
298
|
+
if (args.length === 1 && (args[0] === 'set' || args[0] === 'edit')) {
|
|
299
|
+
return { kind: 'goal-editor', edit: args[0] === 'edit' };
|
|
300
|
+
}
|
|
301
|
+
return { kind: 'host-command', name: 'goal', args };
|
|
302
|
+
},
|
|
303
|
+
},
|
|
199
304
|
{
|
|
200
305
|
name: 'help',
|
|
201
|
-
description: '
|
|
202
|
-
|
|
306
|
+
description: 'Browse commands, or show usage with /help <command>.',
|
|
307
|
+
help: {
|
|
308
|
+
usage: ['/help', '/help <command>'],
|
|
309
|
+
details: [
|
|
310
|
+
'Without a name, opens the command menu. With a name, shows help without running the command.',
|
|
311
|
+
],
|
|
312
|
+
},
|
|
313
|
+
action: (ctx, args) => {
|
|
314
|
+
if (args.length > 0)
|
|
315
|
+
return commandHelp(ctx, args);
|
|
203
316
|
// Reads what this session OFFERS, not a module constant. A command the
|
|
204
317
|
// kernel registered and `/help` did not list is a command nobody
|
|
205
318
|
// discovers. Refused command files remain rows with their reason; App
|
|
@@ -212,6 +325,7 @@ export const CLI_LOCAL_COMMANDS = [
|
|
|
212
325
|
.map((command) => ({
|
|
213
326
|
name: command.name,
|
|
214
327
|
description: command.description,
|
|
328
|
+
...(command.unavailable?.(ctx) ? { problem: command.unavailable(ctx) } : {}),
|
|
215
329
|
})),
|
|
216
330
|
...ctx.userCommands.map((command) => ({
|
|
217
331
|
name: command.name,
|
|
@@ -222,14 +336,12 @@ export const CLI_LOCAL_COMMANDS = [
|
|
|
222
336
|
};
|
|
223
337
|
},
|
|
224
338
|
},
|
|
225
|
-
{
|
|
226
|
-
name: 'agent',
|
|
227
|
-
description: 'Compatibility alias for the automatic delegated-work panel.',
|
|
228
|
-
discoverable: false,
|
|
229
|
-
action: () => ({ kind: 'agent-cockpit' }),
|
|
230
|
-
},
|
|
231
339
|
{
|
|
232
340
|
name: 'feedback',
|
|
341
|
+
help: { usage: ['/feedback', '/feedback good|bad [note]'] },
|
|
342
|
+
unavailable: (ctx) => ctx.lastAssistantMessageId()
|
|
343
|
+
? undefined
|
|
344
|
+
: 'Nothing to rate yet. Wait for an assistant answer.',
|
|
233
345
|
description: 'Rate the last answer; choose good/bad or add an optional note.',
|
|
234
346
|
action: (ctx, args) => {
|
|
235
347
|
if (args.length === 0) {
|
|
@@ -282,73 +394,100 @@ export const CLI_LOCAL_COMMANDS = [
|
|
|
282
394
|
action: () => ({ kind: 'archive-picker' }),
|
|
283
395
|
},
|
|
284
396
|
{
|
|
285
|
-
name: '
|
|
286
|
-
description: 'Clear only the terminal transcript; keep this conversation context.',
|
|
287
|
-
action: () => ({ kind: 'clear-screen' }),
|
|
288
|
-
},
|
|
289
|
-
{
|
|
290
|
-
name: 'quit',
|
|
397
|
+
name: 'exit',
|
|
291
398
|
description: 'Exit namzu.',
|
|
292
399
|
action: () => ({ kind: 'exit' }),
|
|
293
400
|
},
|
|
294
401
|
{
|
|
295
|
-
name: '
|
|
296
|
-
|
|
297
|
-
|
|
402
|
+
name: 'rename',
|
|
403
|
+
help: { usage: ['/rename [name]', '/rename clear'] },
|
|
404
|
+
description: 'Rename this conversation; opens an editor when no name is supplied. /rename clear removes the saved name.',
|
|
405
|
+
action: renameConversationAction,
|
|
298
406
|
},
|
|
299
407
|
{
|
|
300
|
-
name: '
|
|
301
|
-
description: '
|
|
302
|
-
action: (
|
|
303
|
-
// Asked now, not when the session was built. Some of these register
|
|
304
|
-
// during the first turn, so a list captured earlier is a list of what
|
|
305
|
-
// was callable then — and this command is asked in the present tense.
|
|
306
|
-
const tools = ctx.availableTools();
|
|
307
|
-
return {
|
|
308
|
-
kind: 'message',
|
|
309
|
-
role: 'system',
|
|
310
|
-
content: tools.length === 0
|
|
311
|
-
? 'No tools registered yet — the agent session may still be connecting.'
|
|
312
|
-
: `Registered tools (${tools.length}):\n ${tools.join('\n ')}`,
|
|
313
|
-
};
|
|
314
|
-
},
|
|
408
|
+
name: 'fork',
|
|
409
|
+
description: 'Continue in a copy of this conversation, leaving the original where it is.',
|
|
410
|
+
action: () => ({ kind: 'fork' }),
|
|
315
411
|
},
|
|
316
412
|
{
|
|
317
|
-
name: '
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
413
|
+
name: 'add-dir',
|
|
414
|
+
help: {
|
|
415
|
+
usage: ['/add-dir [path]'],
|
|
416
|
+
details: ['Without a path, lists added directories. Paths may contain spaces.'],
|
|
417
|
+
},
|
|
418
|
+
description: 'Let the file tools reach another directory this session: /add-dir <path>. Alone, list them.',
|
|
419
|
+
action: (ctx, args) => {
|
|
420
|
+
const path = args.join(' ').trim();
|
|
421
|
+
if (path.length === 0) {
|
|
422
|
+
const dirs = ctx.directories?.() ?? [];
|
|
423
|
+
return {
|
|
323
424
|
kind: 'message',
|
|
324
425
|
role: 'system',
|
|
325
|
-
content:
|
|
326
|
-
|
|
327
|
-
|
|
426
|
+
content: dirs.length === 0
|
|
427
|
+
? 'No added directories. The file tools reach the working directory only. /add-dir <path> adds one for this session; `additionalDirectories` in the config file adds one for every session.'
|
|
428
|
+
: `Added directories (reachable by absolute path, bound into the sandbox):\n${dirs.map((d) => ` ${d}`).join('\n')}`,
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
return { kind: 'add-dir', path };
|
|
328
432
|
},
|
|
329
433
|
},
|
|
330
434
|
{
|
|
331
|
-
name: '
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
435
|
+
name: 'restore',
|
|
436
|
+
help: {
|
|
437
|
+
usage: ['/restore [turn]'],
|
|
438
|
+
details: ['Run /restore to list checkpoints before choosing a turn to restore.'],
|
|
439
|
+
},
|
|
440
|
+
description: 'Put files back to before a turn: /restore lists the checkpoints, /restore N restores.',
|
|
441
|
+
action: (_ctx, args) => {
|
|
442
|
+
const raw = args[0]?.trim();
|
|
443
|
+
if (!raw)
|
|
444
|
+
return { kind: 'restore' };
|
|
445
|
+
const turn = Number.parseInt(raw, 10);
|
|
446
|
+
if (!Number.isInteger(turn) || turn < 1 || String(turn) !== raw) {
|
|
447
|
+
return {
|
|
448
|
+
kind: 'message',
|
|
449
|
+
role: 'system',
|
|
450
|
+
content: `/restore takes a turn number from the list: /restore 3. Not: ${raw}`,
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
return { kind: 'restore', turn };
|
|
454
|
+
},
|
|
344
455
|
},
|
|
345
456
|
{
|
|
346
457
|
name: 'memory',
|
|
347
|
-
|
|
348
|
-
|
|
458
|
+
help: {
|
|
459
|
+
usage: ['/memory [show|list]', '/memory add <text>', '/memory --user add <text>'],
|
|
460
|
+
details: [
|
|
461
|
+
'Show and list read saved memory. Add saves a project note; --user saves a note for all projects.',
|
|
462
|
+
'Direct /memory <text> also saves a note. To save a reserved word as a note, use /memory add show.',
|
|
463
|
+
],
|
|
464
|
+
},
|
|
465
|
+
description: 'Show curated memory, or save a fact with /memory add <text>. Use /memory --user add <text> for every project.',
|
|
466
|
+
action: (_ctx, args) => {
|
|
467
|
+
const user = args[0] === '--user';
|
|
468
|
+
const memoryArgs = user ? args.slice(1) : args;
|
|
469
|
+
const text = memoryArgs.join(' ').trim();
|
|
470
|
+
// Reserve standalone inspection words while retaining multiword facts.
|
|
471
|
+
if (text.length === 0 || ['show', 'list'].includes(text.toLowerCase())) {
|
|
472
|
+
return { kind: 'show-memory' };
|
|
473
|
+
}
|
|
474
|
+
if (memoryArgs[0]?.toLowerCase() === 'add') {
|
|
475
|
+
const fact = memoryArgs.slice(1).join(' ').trim();
|
|
476
|
+
if (fact.length === 0) {
|
|
477
|
+
return {
|
|
478
|
+
kind: 'message',
|
|
479
|
+
role: 'system',
|
|
480
|
+
content: 'Usage: /memory add <text> or /memory --user add <text>. /memory show displays saved memory.',
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
return { kind: 'remember', text: fact, scope: user ? 'user' : 'project' };
|
|
484
|
+
}
|
|
485
|
+
return { kind: 'remember', text, scope: user ? 'user' : 'project' };
|
|
486
|
+
},
|
|
349
487
|
},
|
|
350
488
|
{
|
|
351
489
|
name: 'skills',
|
|
490
|
+
help: { usage: ['/skills', '/skills list', '/skills <name>'] },
|
|
352
491
|
description: 'Choose an available skill; use /skills list for the full roster.',
|
|
353
492
|
action: (_ctx, args) => {
|
|
354
493
|
const choice = args.join(' ').trim();
|
|
@@ -364,77 +503,33 @@ export const CLI_LOCAL_COMMANDS = [
|
|
|
364
503
|
description: 'Resume a past conversation in this folder.',
|
|
365
504
|
action: () => ({ kind: 'resume' }),
|
|
366
505
|
},
|
|
367
|
-
{
|
|
368
|
-
name: 'mention',
|
|
369
|
-
description: 'Insert @ so a project file can be mentioned in the next prompt.',
|
|
370
|
-
action: () => ({ kind: 'composer-draft', text: '@' }),
|
|
371
|
-
},
|
|
372
|
-
{
|
|
373
|
-
name: 'expand',
|
|
374
|
-
description: 'Print a collapsed tool output in full: /expand [n].',
|
|
375
|
-
action: (_ctx, args) => {
|
|
376
|
-
const arg = args.join(' ').trim();
|
|
377
|
-
// Bare `/expand` means the most recent one. That is what a person types
|
|
378
|
-
// the moment output truncates in front of them, and making them read a
|
|
379
|
-
// number back off the screen first would be a toll on the common case.
|
|
380
|
-
if (arg.length === 0)
|
|
381
|
-
return { kind: 'expand', which: 'last' };
|
|
382
|
-
// Matched as the literal decimal a hint can print, and nothing else.
|
|
383
|
-
//
|
|
384
|
-
// `parseInt` would read `2nd` as 2 and expand a block the operator did
|
|
385
|
-
// not name. `Number` plus `Number.isInteger` fixes that one and still
|
|
386
|
-
// admits `0x10`, `1e2`, `+3` and `3.0` — four spellings no hint has
|
|
387
|
-
// ever shown, each of which turns a typo into a valid reference to
|
|
388
|
-
// some OTHER block. What this accepts is exactly what the screen can
|
|
389
|
-
// produce, which is the only set with no silently-wrong answer in it.
|
|
390
|
-
if (!/^[1-9][0-9]*$/.test(arg)) {
|
|
391
|
-
return {
|
|
392
|
-
kind: 'message',
|
|
393
|
-
role: 'system',
|
|
394
|
-
content: 'Usage: /expand [n], where n is the number in the "… +N lines · /expand n" hint under a collapsed tool output. /expand on its own takes the most recent one.',
|
|
395
|
-
};
|
|
396
|
-
}
|
|
397
|
-
return { kind: 'expand', which: Number(arg) };
|
|
398
|
-
},
|
|
399
|
-
},
|
|
400
|
-
{
|
|
401
|
-
name: 'skill',
|
|
402
|
-
description: 'Alias for /skills; choose a skill or type its name.',
|
|
403
|
-
action: (_ctx, args) => {
|
|
404
|
-
const name = args.join(' ').trim();
|
|
405
|
-
return name.length === 0 ? { kind: 'skill-picker' } : { kind: 'load-skill', name };
|
|
406
|
-
},
|
|
407
|
-
},
|
|
408
|
-
{
|
|
409
|
-
name: 'provider',
|
|
410
|
-
description: 'Show the current provider + model.',
|
|
411
|
-
action: (ctx) => ({
|
|
412
|
-
kind: 'message',
|
|
413
|
-
role: 'system',
|
|
414
|
-
content: ctx.providerSummary === null
|
|
415
|
-
? 'No provider configured. Run /model to pick one, or set an LLM env var (ANTHROPIC_API_KEY / OPENAI_API_KEY / OPENROUTER_API_KEY) and restart namzu.'
|
|
416
|
-
: `Provider: ${ctx.providerSummary}${ctx.modelSummary ? `\nModel: ${ctx.modelSummary}` : ''}`,
|
|
417
|
-
}),
|
|
418
|
-
},
|
|
419
|
-
{
|
|
420
|
-
name: 'pwd',
|
|
421
|
-
description: 'Show the active working directory.',
|
|
422
|
-
action: (ctx) => ({ kind: 'message', role: 'system', content: ctx.cwd }),
|
|
423
|
-
},
|
|
424
506
|
{
|
|
425
507
|
name: 'model',
|
|
426
|
-
description: '
|
|
508
|
+
description: 'Choose a model for the current provider, or change providers.',
|
|
509
|
+
help: {
|
|
510
|
+
usage: ['/model'],
|
|
511
|
+
details: [
|
|
512
|
+
'Press p in the model picker to change providers. Normal selections are saved for future launches; temporary credentials keep the selection in this session.',
|
|
513
|
+
],
|
|
514
|
+
},
|
|
427
515
|
action: () => ({ kind: 'repick' }),
|
|
428
516
|
},
|
|
429
517
|
{
|
|
430
518
|
name: 'login',
|
|
431
519
|
description: 'Sign in with a Claude or Codex subscription.',
|
|
520
|
+
help: {
|
|
521
|
+
usage: ['/login', '/login <callback-address-or-code>'],
|
|
522
|
+
details: [
|
|
523
|
+
'Start sign-in with /login, then paste the callback address or code when requested.',
|
|
524
|
+
],
|
|
525
|
+
},
|
|
432
526
|
action: (_ctx, args) => isCompletionArgument(args)
|
|
433
527
|
? { kind: 'login', pasted: args.join(' ').trim() }
|
|
434
528
|
: { kind: 'login' },
|
|
435
529
|
},
|
|
436
530
|
{
|
|
437
531
|
name: 'logout',
|
|
532
|
+
help: { usage: ['/logout [claude|codex|all]'] },
|
|
438
533
|
description: 'Remove a Namzu-owned subscription credential: /logout [claude|codex|all].',
|
|
439
534
|
action: (_ctx, args) => {
|
|
440
535
|
const target = args.join(' ').trim().toLowerCase();
|
|
@@ -456,15 +551,52 @@ export const CLI_LOCAL_COMMANDS = [
|
|
|
456
551
|
},
|
|
457
552
|
{
|
|
458
553
|
name: 'cost',
|
|
459
|
-
|
|
554
|
+
help: { usage: ['/cost [details]'] },
|
|
555
|
+
description: 'Show usage and cost for the current or latest run.',
|
|
556
|
+
action: (ctx, args) => reportCommand('cost', args, (details) => renderCost(ctx.usage, ctx.compaction, details)),
|
|
557
|
+
},
|
|
558
|
+
{
|
|
559
|
+
name: 'jobs',
|
|
560
|
+
description: 'List background jobs started this session, running and ended.',
|
|
561
|
+
action: (ctx) => ({
|
|
562
|
+
kind: 'message',
|
|
563
|
+
role: 'system',
|
|
564
|
+
content: renderJobs(ctx.jobs()),
|
|
565
|
+
}),
|
|
566
|
+
},
|
|
567
|
+
{
|
|
568
|
+
name: 'release-notes',
|
|
569
|
+
help: { usage: ['/release-notes [version]'] },
|
|
570
|
+
description: 'Show what changed in the version that is running: /release-notes [version].',
|
|
571
|
+
action: (_ctx, args) => ({
|
|
572
|
+
kind: 'message',
|
|
573
|
+
role: 'system',
|
|
574
|
+
content: renderReleaseNotes(readChangelog(), args[0]?.trim() || undefined),
|
|
575
|
+
}),
|
|
576
|
+
},
|
|
577
|
+
{
|
|
578
|
+
name: 'hooks',
|
|
579
|
+
description: 'List the shell hooks this session runs, by event.',
|
|
460
580
|
action: (ctx) => ({
|
|
461
581
|
kind: 'message',
|
|
462
582
|
role: 'system',
|
|
463
|
-
content:
|
|
583
|
+
content: renderHooks(ctx.hooks?.()),
|
|
464
584
|
}),
|
|
465
585
|
},
|
|
586
|
+
{
|
|
587
|
+
name: 'context',
|
|
588
|
+
help: { usage: ['/context [details]'] },
|
|
589
|
+
description: 'Show context usage and automatic cleanup.',
|
|
590
|
+
action: (ctx, args) => reportCommand('context', args, (details) => renderContext(ctx.usage, ctx.compaction, details)),
|
|
591
|
+
},
|
|
466
592
|
{
|
|
467
593
|
name: 'review',
|
|
594
|
+
help: {
|
|
595
|
+
usage: ['/review [instructions]'],
|
|
596
|
+
details: [
|
|
597
|
+
'Without instructions, opens the review target menu. Starting a review uses the selected model.',
|
|
598
|
+
],
|
|
599
|
+
},
|
|
468
600
|
description: 'Choose a review target, or provide custom instructions: /review [instructions].',
|
|
469
601
|
action: (_ctx, args) => {
|
|
470
602
|
const instructions = args.join(' ').trim();
|
|
@@ -473,12 +605,9 @@ export const CLI_LOCAL_COMMANDS = [
|
|
|
473
605
|
},
|
|
474
606
|
{
|
|
475
607
|
name: 'mcp',
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
role: 'system',
|
|
480
|
-
content: renderMcp(ctx.mcp()),
|
|
481
|
-
}),
|
|
608
|
+
help: { usage: ['/mcp [tools|details]'] },
|
|
609
|
+
description: 'Show connected tool servers and connection problems.',
|
|
610
|
+
action: (ctx, args) => reportCommand('mcp', args, (details) => renderMcp(ctx.mcp(), details), 'tools'),
|
|
482
611
|
},
|
|
483
612
|
{
|
|
484
613
|
name: 'diff',
|
|
@@ -488,6 +617,10 @@ export const CLI_LOCAL_COMMANDS = [
|
|
|
488
617
|
{
|
|
489
618
|
name: 'compact',
|
|
490
619
|
description: 'Summarise the older half of this conversation to free up context.',
|
|
620
|
+
help: {
|
|
621
|
+
usage: ['/compact'],
|
|
622
|
+
details: ['Compaction uses a model call to summarise older context.'],
|
|
623
|
+
},
|
|
491
624
|
action: () => ({ kind: 'compact' }),
|
|
492
625
|
},
|
|
493
626
|
{
|
|
@@ -497,6 +630,7 @@ export const CLI_LOCAL_COMMANDS = [
|
|
|
497
630
|
},
|
|
498
631
|
{
|
|
499
632
|
name: 'raw',
|
|
633
|
+
help: { usage: ['/raw [on|off]'] },
|
|
500
634
|
description: 'Toggle copy-friendly plain transcript rendering: /raw [on|off].',
|
|
501
635
|
action: (_ctx, args) => {
|
|
502
636
|
const choice = args.join(' ').trim().toLowerCase();
|
|
@@ -515,6 +649,12 @@ export const CLI_LOCAL_COMMANDS = [
|
|
|
515
649
|
},
|
|
516
650
|
{
|
|
517
651
|
name: 'export',
|
|
652
|
+
help: {
|
|
653
|
+
usage: ['/export [path]'],
|
|
654
|
+
details: [
|
|
655
|
+
'Without a path, choose the clipboard or a Markdown file. Existing files are not overwritten. Paths may contain spaces.',
|
|
656
|
+
],
|
|
657
|
+
},
|
|
518
658
|
description: 'Export this verified conversation to the clipboard or a Markdown file.',
|
|
519
659
|
action: (_ctx, args) => {
|
|
520
660
|
const path = args.join(' ').trim();
|
|
@@ -523,40 +663,76 @@ export const CLI_LOCAL_COMMANDS = [
|
|
|
523
663
|
},
|
|
524
664
|
{
|
|
525
665
|
name: 'status',
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
666
|
+
help: { usage: ['/status [details|config|tools]'] },
|
|
667
|
+
description: 'Show the model, permissions, workspace and latest cost.',
|
|
668
|
+
action: (ctx, args) => {
|
|
669
|
+
const which = args.join(' ').trim().toLowerCase();
|
|
670
|
+
if (which === 'config') {
|
|
671
|
+
return {
|
|
672
|
+
kind: 'message',
|
|
673
|
+
role: 'system',
|
|
674
|
+
content: renderConfigDebug(ctx.configDebug),
|
|
675
|
+
};
|
|
676
|
+
}
|
|
677
|
+
if (which === 'tools') {
|
|
678
|
+
const tools = ctx.availableTools();
|
|
679
|
+
return {
|
|
680
|
+
kind: 'message',
|
|
681
|
+
role: 'system',
|
|
682
|
+
content: tools.length === 0
|
|
683
|
+
? 'No tools registered yet — the agent session may still be connecting.'
|
|
684
|
+
: `Registered tools (${tools.length}):\n ${tools.join('\n ')}`,
|
|
685
|
+
};
|
|
686
|
+
}
|
|
687
|
+
if (which.length > 0 && which !== 'details') {
|
|
688
|
+
return {
|
|
689
|
+
kind: 'message',
|
|
690
|
+
role: 'system',
|
|
691
|
+
content: 'Usage: /status [details|config|tools]',
|
|
692
|
+
};
|
|
693
|
+
}
|
|
694
|
+
return { kind: 'message', role: 'system', content: renderStatus(ctx, which === 'details') };
|
|
695
|
+
},
|
|
541
696
|
},
|
|
542
697
|
{
|
|
543
698
|
name: 'permissions',
|
|
544
|
-
description: 'Choose
|
|
545
|
-
|
|
699
|
+
description: 'Choose which actions need your approval.',
|
|
700
|
+
help: {
|
|
701
|
+
usage: ['/permissions', '/permissions details', '/permissions <mode>'],
|
|
702
|
+
details: [
|
|
703
|
+
'Choose a preset in the menu, or use a typed mode:',
|
|
704
|
+
...['prompt', 'accept-edits', 'plan', 'auto', 'strict'].map((mode) => ` ${mode}: ${permissionModeLabel(mode)}`),
|
|
705
|
+
'Changes apply to this session. Explicit deny rules and sandbox restrictions still apply.',
|
|
706
|
+
],
|
|
707
|
+
},
|
|
708
|
+
action: (ctx, args) => {
|
|
546
709
|
if (args.length === 0)
|
|
547
710
|
return { kind: 'permission-mode-picker' };
|
|
548
711
|
const mode = args.length === 1 ? args[0]?.toLowerCase() : undefined;
|
|
712
|
+
if (mode === 'details') {
|
|
713
|
+
return {
|
|
714
|
+
kind: 'message',
|
|
715
|
+
role: 'system',
|
|
716
|
+
content: renderPermissions(ctx.permissions, true),
|
|
717
|
+
};
|
|
718
|
+
}
|
|
549
719
|
if (isPermissionMode(mode))
|
|
550
720
|
return { kind: 'permission-mode', mode };
|
|
551
721
|
return {
|
|
552
722
|
kind: 'message',
|
|
553
723
|
role: 'system',
|
|
554
|
-
content: 'Usage: /permissions
|
|
724
|
+
content: 'Usage: /permissions opens the permission menu. Choose a preset there, or use /permissions details to view rules.',
|
|
555
725
|
};
|
|
556
726
|
},
|
|
557
727
|
},
|
|
558
728
|
{
|
|
559
729
|
name: 'effort',
|
|
730
|
+
help: {
|
|
731
|
+
usage: ['/effort [level|default]'],
|
|
732
|
+
details: [
|
|
733
|
+
'Run /effort to see the levels supported by the selected model and usable fallbacks. Changes affect future main-query turns in this session.',
|
|
734
|
+
],
|
|
735
|
+
},
|
|
560
736
|
description: 'Choose reasoning effort for future turns: /effort [level|default].',
|
|
561
737
|
action: (ctx, args) => {
|
|
562
738
|
if (args.length === 0) {
|
|
@@ -608,6 +784,12 @@ export const CLI_LOCAL_COMMANDS = [
|
|
|
608
784
|
{
|
|
609
785
|
name: 'init',
|
|
610
786
|
description: 'Write an AGENTS.md describing this project to future agents.',
|
|
787
|
+
help: {
|
|
788
|
+
usage: ['/init'],
|
|
789
|
+
details: [
|
|
790
|
+
'Asks the selected model to read the project and write instructions. Existing instructions are read before changes are proposed.',
|
|
791
|
+
],
|
|
792
|
+
},
|
|
611
793
|
action: (ctx) => {
|
|
612
794
|
if (!ctx.providerSummary) {
|
|
613
795
|
return {
|
|
@@ -667,53 +849,131 @@ export function initPrompt(instructionFiles) {
|
|
|
667
849
|
'future run, so a sentence that says nothing is not free.',
|
|
668
850
|
].join('\n');
|
|
669
851
|
}
|
|
670
|
-
/**
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
* by a guessed window, so it climbed with turn count and read FULL on a
|
|
676
|
-
* conversation with room to spare. This command prints the spend and says which
|
|
677
|
-
* quantity it is, so that nobody reads it as the other one.
|
|
678
|
-
*/
|
|
679
|
-
export function renderCost(usage) {
|
|
680
|
-
if (usage === null) {
|
|
681
|
-
return 'No usage reported yet. The kernel emits it as a turn runs, so this fills in after the first exchange.';
|
|
852
|
+
/** The session's shell hooks, by event, in the order the config file gave them. */
|
|
853
|
+
export function renderHooks(hooks) {
|
|
854
|
+
const events = Object.entries(hooks ?? {}).filter(([, entries]) => (entries?.length ?? 0) > 0);
|
|
855
|
+
if (events.length === 0) {
|
|
856
|
+
return 'No hooks. Add a `hooks` key to namzu.config.json or ~/.namzu/config.yaml: event → list of { command, matcher?, timeoutMs? }.';
|
|
682
857
|
}
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
858
|
+
const lines = [];
|
|
859
|
+
for (const [event, entries] of events) {
|
|
860
|
+
lines.push(`${event}`);
|
|
861
|
+
for (const entry of entries ?? []) {
|
|
862
|
+
const when = entry.matcher ? ` · matches ${entry.matcher}` : '';
|
|
863
|
+
const deadline = entry.timeoutMs !== undefined ? ` · ${Math.round(entry.timeoutMs / 1000)}s` : '';
|
|
864
|
+
lines.push(` ${entry.command}${when}${deadline}`);
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
return lines.join('\n');
|
|
868
|
+
}
|
|
869
|
+
/** The session's background jobs, as an operator would ask about them. */
|
|
870
|
+
export function renderJobs(jobs) {
|
|
871
|
+
if (jobs.length === 0) {
|
|
872
|
+
return 'No background jobs this session. The agent starts one with `run_in_background` on bash; under a sandbox none can be started.';
|
|
873
|
+
}
|
|
874
|
+
const lines = jobs.map((job) => {
|
|
875
|
+
const state = job.status === 'running'
|
|
876
|
+
? `running for ${Math.max(1, Math.round((Date.now() - job.startedAt) / 1000))}s`
|
|
877
|
+
: job.status === 'killed'
|
|
878
|
+
? 'stopped'
|
|
879
|
+
: `exited ${job.exitCode ?? '?'}`;
|
|
880
|
+
return `${job.id} ${state.padEnd(16)} ${job.command}`;
|
|
881
|
+
});
|
|
882
|
+
return [
|
|
883
|
+
`${jobs.length} background job${jobs.length === 1 ? '' : 's'} this session:`,
|
|
884
|
+
...lines,
|
|
705
885
|
'',
|
|
886
|
+
'The agent reads one with the job tool; ask it to stop one, or /exit stops them all.',
|
|
887
|
+
].join('\n');
|
|
888
|
+
}
|
|
889
|
+
/** Reports validate their subcommand instead of silently ignoring mistyped arguments. */
|
|
890
|
+
function reportCommand(name, args, render, detailName = 'details') {
|
|
891
|
+
const option = args.join(' ').trim().toLowerCase();
|
|
892
|
+
const details = option === detailName || option === 'details';
|
|
893
|
+
return {
|
|
894
|
+
kind: 'message',
|
|
895
|
+
role: 'system',
|
|
896
|
+
content: option.length === 0 || details ? render(details) : `Usage: /${name} [${detailName}]`,
|
|
897
|
+
};
|
|
898
|
+
}
|
|
899
|
+
function contextMeasurement(usage) {
|
|
900
|
+
const context = usage?.context;
|
|
901
|
+
if (!context || context.windowTokens <= 0)
|
|
902
|
+
return ['No context measurement yet.'];
|
|
903
|
+
const percent = Math.min(999, Math.round((context.tokens / context.windowTokens) * 100));
|
|
904
|
+
const approx = context.measured && !context.windowAssumed ? '' : '~';
|
|
905
|
+
return [
|
|
906
|
+
`Context: ${context.tokens.toLocaleString('en-US')} / ${context.windowTokens.toLocaleString('en-US')} tokens (${approx}${percent}%)`,
|
|
907
|
+
`${context.measured ? 'Counted by the provider' : 'Estimated by Namzu'}; window ${context.windowAssumed ? 'assumed from a table or default' : 'declared by the provider or config'}.`,
|
|
908
|
+
];
|
|
909
|
+
}
|
|
910
|
+
export function renderContext(usage, compaction, details = false) {
|
|
911
|
+
const lines = contextMeasurement(usage);
|
|
912
|
+
if (usage?.context)
|
|
913
|
+
lines.push('Latest reported context size; not cumulative token usage.');
|
|
914
|
+
if (!compaction) {
|
|
915
|
+
lines.push('Automatic cleanup: unavailable before a session starts.');
|
|
916
|
+
}
|
|
917
|
+
else {
|
|
918
|
+
lines.push(`Cleanup this session: ${compaction.passes} passes, ~${compaction.reclaimedTokens.toLocaleString('en-US')} tokens freed.`);
|
|
919
|
+
if (details) {
|
|
920
|
+
const pct = (fraction) => `${Math.round(fraction * 100)}%`;
|
|
921
|
+
lines.push(`Strategy: ${compaction.strategy}`);
|
|
922
|
+
if (compaction.strategy === 'salience') {
|
|
923
|
+
lines.push(`Older, less useful content is shortened from ${pct(compaction.softTarget)} of the window; older history is summarised at ${pct(compaction.triggerThreshold)}.`);
|
|
924
|
+
}
|
|
925
|
+
else {
|
|
926
|
+
lines.push(`Older tool results are cleared at ${pct(compaction.triggerThreshold)} of the window; history is summarised if needed.`);
|
|
927
|
+
}
|
|
928
|
+
lines.push(`Tool results cleared: ${compaction.clearedResults.toLocaleString('en-US')}`, `Messages shortened: ${compaction.stubbedNarrations.toLocaleString('en-US')}`, `Summaries written: ${compaction.summaries.toLocaleString('en-US')}`);
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
if (!details)
|
|
932
|
+
lines.push('/context details for cleanup history.');
|
|
933
|
+
return lines.join('\n');
|
|
934
|
+
}
|
|
935
|
+
/** Keep unknown prices distinct from measured zero, in status and cost alike. */
|
|
936
|
+
function costAmount(cost) {
|
|
937
|
+
if (cost.unpricedTokens > 0) {
|
|
938
|
+
return cost.totalCost > 0 ? `at least $${cost.totalCost.toFixed(4)}` : 'not known';
|
|
939
|
+
}
|
|
940
|
+
return `$${cost.totalCost.toFixed(4)}${cost.totalCost === 0 ? ' (measured zero)' : ''}`;
|
|
941
|
+
}
|
|
942
|
+
export function renderCost(usage, compaction = null, details = false) {
|
|
943
|
+
if (usage === null)
|
|
944
|
+
return 'No usage reported yet. Send a message to begin.';
|
|
945
|
+
const lines = [
|
|
946
|
+
'Current or latest run',
|
|
947
|
+
`Tokens: ${usage.totalTokens.toLocaleString('en-US')} (own model calls)`,
|
|
948
|
+
`Cost: ${costAmount(usage.cost)}`,
|
|
706
949
|
];
|
|
707
|
-
if (
|
|
708
|
-
lines.push(`${usage.cost.unpricedTokens.toLocaleString('en-US')} tokens
|
|
950
|
+
if (usage.cost.unpricedTokens > 0) {
|
|
951
|
+
lines.push(`${usage.cost.unpricedTokens.toLocaleString('en-US')} tokens have no known price and are excluded from the cost.`);
|
|
709
952
|
}
|
|
710
|
-
|
|
711
|
-
lines.push(
|
|
953
|
+
if (usage.budget) {
|
|
954
|
+
lines.push(`Including delegated agents: ${usage.budget.treeTokens.toLocaleString('en-US')} tokens; limit ${usage.budget.limit === 0 ? 'unlimited' : usage.budget.limit.toLocaleString('en-US')}.`);
|
|
955
|
+
if (usage.budget.poisoned)
|
|
956
|
+
lines.push('Further spending is blocked until unresolved request usage is reconciled.');
|
|
957
|
+
}
|
|
958
|
+
if (details) {
|
|
959
|
+
lines.push('Cost covers this run’s own model calls, excluding delegated calls and earlier runs. These are not conversation totals.');
|
|
960
|
+
if (usage.cost.unpricedTokens > 0) {
|
|
961
|
+
lines.push('Missing prices do not mean those tokens were free.');
|
|
962
|
+
}
|
|
963
|
+
else {
|
|
964
|
+
lines.push('All reported tokens have a known rate.');
|
|
965
|
+
}
|
|
966
|
+
if (usage.cost.cacheDiscount > 0)
|
|
967
|
+
lines.push(`Cache discount: $${usage.cost.cacheDiscount.toFixed(4)}`);
|
|
968
|
+
if (usage.context && usage.context.windowTokens > 0) {
|
|
969
|
+
lines.push('', ...contextMeasurement(usage), '/context details for automatic cleanup.');
|
|
970
|
+
}
|
|
971
|
+
if (compaction)
|
|
972
|
+
lines.push(`Cleanup passes this session: ${compaction.passes}.`);
|
|
712
973
|
}
|
|
713
974
|
else {
|
|
714
|
-
lines.push('
|
|
975
|
+
lines.push('/cost details for scope and pricing; /context for context size.');
|
|
715
976
|
}
|
|
716
|
-
lines.push('', 'Cumulative for this run, across every turn. Not a measure of how full', 'the context is — that is a different quantity and it goes down when the', 'conversation is compacted, while this only ever grows.');
|
|
717
977
|
return lines.join('\n');
|
|
718
978
|
}
|
|
719
979
|
/** What decides a tool call, in the order it actually decides it. */
|
|
@@ -797,146 +1057,93 @@ function reviewTargetPrompt(target) {
|
|
|
797
1057
|
'Say plainly what you did not examine.',
|
|
798
1058
|
].join('\n');
|
|
799
1059
|
}
|
|
800
|
-
export function renderMcp(mcp) {
|
|
1060
|
+
export function renderMcp(mcp, details = false) {
|
|
801
1061
|
if (mcp === null)
|
|
802
|
-
return 'No session yet
|
|
803
|
-
const lines = [];
|
|
1062
|
+
return 'No session yet. Tool servers have not been checked.';
|
|
804
1063
|
if (mcp.connected.length === 0 && mcp.failed.length === 0) {
|
|
805
|
-
return 'No tool servers configured. Add
|
|
1064
|
+
return 'No tool servers configured. Add mcpServers to namzu.config.json.';
|
|
806
1065
|
}
|
|
1066
|
+
const lines = [
|
|
1067
|
+
`Tool servers: ${mcp.connected.length} connected, ${mcp.failed.length} unavailable.`,
|
|
1068
|
+
];
|
|
807
1069
|
for (const server of mcp.connected) {
|
|
808
|
-
lines.push(`${server.name}
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
lines.push(` ${tool}`);
|
|
813
|
-
}
|
|
814
|
-
if (mcp.failed.length > 0) {
|
|
815
|
-
if (lines.length > 0)
|
|
816
|
-
lines.push('');
|
|
817
|
-
for (const server of mcp.failed) {
|
|
818
|
-
lines.push(`${server.name} — NOT available: ${server.reason}`);
|
|
819
|
-
}
|
|
1070
|
+
lines.push(`${server.name}: connected, ${server.tools.length} tools`);
|
|
1071
|
+
if (details)
|
|
1072
|
+
for (const tool of server.tools)
|
|
1073
|
+
lines.push(` ${tool}`);
|
|
820
1074
|
}
|
|
1075
|
+
for (const server of mcp.failed)
|
|
1076
|
+
lines.push(`${server.name}: unavailable — ${server.reason}`);
|
|
1077
|
+
if (!details && mcp.connected.length > 0)
|
|
1078
|
+
lines.push('/mcp tools to list available tools.');
|
|
821
1079
|
return lines.join('\n');
|
|
822
1080
|
}
|
|
823
|
-
export function renderStatus(ctx) {
|
|
824
|
-
const lines = [
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
1081
|
+
export function renderStatus(ctx, details = false) {
|
|
1082
|
+
const lines = [
|
|
1083
|
+
`Provider: ${ctx.providerSummary ?? 'none — run /model to choose one'}`,
|
|
1084
|
+
`Model: ${ctx.modelSummary ?? 'not selected'}`,
|
|
1085
|
+
`Working directory: ${ctx.cwd}`,
|
|
1086
|
+
...renderPermissions(ctx.permissions, details).split('\n'),
|
|
1087
|
+
];
|
|
829
1088
|
const sandbox = ctx.sandbox;
|
|
830
1089
|
if (!sandbox) {
|
|
831
|
-
lines.push('
|
|
1090
|
+
lines.push('Sandbox: not resolved yet.');
|
|
832
1091
|
}
|
|
833
1092
|
else if (sandbox.unconfined) {
|
|
834
|
-
|
|
835
|
-
// environment name: a tier that enforces nothing is not a weaker
|
|
836
|
-
// sandbox, it is the absence of one.
|
|
837
|
-
lines.push(' Anywhere this shell can. Commands are NOT confined.');
|
|
838
|
-
if (sandbox.environment) {
|
|
839
|
-
lines.push(` The sandbox is attached (${sandbox.environment}) and enforces nothing here.`);
|
|
840
|
-
}
|
|
841
|
-
lines.push(' Name what you need under `sandbox.requireIsolation` to be refused instead.');
|
|
1093
|
+
lines.push('Sandbox: commands are not confined; they have this shell’s access.');
|
|
842
1094
|
}
|
|
843
1095
|
else {
|
|
844
|
-
lines.push(`
|
|
845
|
-
lines.push(` Enforced here: ${sandbox.enforced.join(', ')}.`);
|
|
846
|
-
}
|
|
847
|
-
if (sandbox && sandbox.required.length > 0) {
|
|
848
|
-
// Worth its own line even when it matches what is enforced: a demand
|
|
849
|
-
// travels to the next machine and a coincidence does not.
|
|
850
|
-
lines.push(` Required by config: ${sandbox.required.join(', ')} — a host without them refuses to run.`);
|
|
1096
|
+
lines.push(`Sandbox: ${sandbox.environment ?? 'active'}; enforces ${sandbox.enforced.join(', ') || 'no reported restrictions'}.`);
|
|
851
1097
|
}
|
|
852
|
-
|
|
853
|
-
lines.push('
|
|
1098
|
+
if (sandbox?.workspace === 'ephemeral') {
|
|
1099
|
+
lines.push('Workspace: temporary files; removed when the run ends.');
|
|
854
1100
|
}
|
|
855
|
-
if (sandbox?.workspace === 'working-directory') {
|
|
856
|
-
lines.push('
|
|
1101
|
+
else if (sandbox?.workspace === 'working-directory' || sandbox?.workspace === 'host') {
|
|
1102
|
+
lines.push('Workspace: real project files; edits persist.');
|
|
857
1103
|
}
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
lines.push(line.length > 0 ? ` ${line}` : '');
|
|
868
|
-
}
|
|
869
|
-
if (ctx.usage) {
|
|
870
|
-
lines.push('');
|
|
871
|
-
lines.push(`Spend: ${renderCost(ctx.usage).split('\n')[0] ?? ''}`);
|
|
1104
|
+
if (details) {
|
|
1105
|
+
for (const dir of ctx.directories?.() ?? [])
|
|
1106
|
+
lines.push(`Additional directory: ${dir}`);
|
|
1107
|
+
if (sandbox) {
|
|
1108
|
+
lines.push(`Required isolation: ${sandbox.required.length > 0 ? sandbox.required.join(', ') : 'none'}.`);
|
|
1109
|
+
if (sandbox.required.length > 0)
|
|
1110
|
+
lines.push('A host missing required isolation cannot start the session.');
|
|
1111
|
+
}
|
|
1112
|
+
lines.push('/status config for setting sources; /status tools for available tools.');
|
|
872
1113
|
}
|
|
873
|
-
|
|
1114
|
+
if (ctx.usage)
|
|
1115
|
+
lines.push(`Spend (current or latest run, own calls): ${costAmount(ctx.usage.cost)}`);
|
|
1116
|
+
if (!details)
|
|
1117
|
+
lines.push('/status details for rules and workspace details.');
|
|
1118
|
+
return lines.join('\n');
|
|
874
1119
|
}
|
|
875
|
-
export function renderPermissions(permissions) {
|
|
876
|
-
const lines = [];
|
|
1120
|
+
export function renderPermissions(permissions, details = false) {
|
|
877
1121
|
const current = permissions.currentMode();
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
if (
|
|
886
|
-
lines.
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
lines.push('
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
lines.push('
|
|
896
|
-
lines.push('an earlier prompt. Run /permissions prompt to revoke that latch and ask again.');
|
|
897
|
-
}
|
|
898
|
-
else {
|
|
899
|
-
lines.push('Unreviewed calls: you are asked before they run.');
|
|
900
|
-
}
|
|
901
|
-
// Stated in every mode, because it is true in every mode and an operator
|
|
902
|
-
// cannot discover it by using namzu: these tools simply never appear at a
|
|
903
|
-
// prompt, so their absence reads as "the agent did not use any".
|
|
904
|
-
const neverPrompted = permissions.neverPrompted();
|
|
905
|
-
if (neverPrompted.length > 0) {
|
|
906
|
-
lines.push('');
|
|
907
|
-
lines.push(`Never prompted for (${neverPrompted.length}):`);
|
|
908
|
-
lines.push(` ${neverPrompted.join(', ')}`);
|
|
909
|
-
lines.push('Each of these declares itself read-only, or is a named exception');
|
|
910
|
-
lines.push("for the agent's own task list. A rule can still deny one, and any");
|
|
911
|
-
lines.push('call the kernel flags destructive is prompted for regardless.');
|
|
1122
|
+
const effective = effectivePermissionMode(current.mode, permissions.approvalLatched());
|
|
1123
|
+
const approvedAll = effective !== current.mode;
|
|
1124
|
+
const lines = [
|
|
1125
|
+
`Permissions: ${permissionModeLabel(effective)}. ${approvedAll ? 'Tools are approved automatically for this session.' : permissionModeDescription(effective)}`,
|
|
1126
|
+
];
|
|
1127
|
+
if (approvedAll)
|
|
1128
|
+
lines.push('Choose a preset in /permissions to change this.');
|
|
1129
|
+
if (!details)
|
|
1130
|
+
return lines.join('\n');
|
|
1131
|
+
if (current.source === 'launch-bypass')
|
|
1132
|
+
lines.push('Selected at launch with --dangerously-skip-permissions.');
|
|
1133
|
+
lines.push('Explicit deny rules and the built-in safety gate apply in every mode.');
|
|
1134
|
+
if (current.mode === 'plan')
|
|
1135
|
+
lines.push('Plan mode also blocks writes that an allow rule would otherwise permit.');
|
|
1136
|
+
const exempt = permissions.neverPrompted();
|
|
1137
|
+
if (exempt.length > 0) {
|
|
1138
|
+
lines.push(`Tools exempt from ordinary prompts: ${exempt.join(', ')}.`);
|
|
1139
|
+
lines.push('Destructive calls still require review; the selected mode decides whether to allow, ask or refuse. Plan mode still requires read-only tools.');
|
|
912
1140
|
}
|
|
913
|
-
if (permissions.rules.length
|
|
914
|
-
lines.push(
|
|
915
|
-
lines.push('No rules configured. Add a "permissions" object to namzu.config.json');
|
|
916
|
-
lines.push('to allow or deny tools by name without being asked each time.');
|
|
1141
|
+
if (permissions.rules.length > 0) {
|
|
1142
|
+
lines.push(`Rules (${permissions.rules.length}):`, ...permissions.rules.map((rule) => ` ${describeRule(rule)}`));
|
|
917
1143
|
}
|
|
918
1144
|
else {
|
|
919
|
-
lines.push('');
|
|
920
|
-
lines.push(`Rules (${permissions.rules.length}), from your config:`);
|
|
921
|
-
for (const rule of permissions.rules)
|
|
922
|
-
lines.push(` ${describeRule(rule)}`);
|
|
1145
|
+
lines.push('No custom rules. Configure permissions in namzu.config.json.');
|
|
923
1146
|
}
|
|
924
|
-
lines.push('');
|
|
925
|
-
// The order this function claims to describe starts here, and the page used
|
|
926
|
-
// to begin one step in. Omitting the gate is not a false statement, but it
|
|
927
|
-
// makes "a rule decides first" read as the whole story when something
|
|
928
|
-
// outranks the rules too — and a true-but-incomplete order is a wrong order
|
|
929
|
-
// for anyone reasoning about what can still get through.
|
|
930
|
-
lines.push('Before any of the below: a built-in safety gate hard-denies a narrow set');
|
|
931
|
-
lines.push('of catastrophic shell patterns (rm -rf /, mkfs, fork bombs, curl|sh, …).');
|
|
932
|
-
lines.push('It applies in every mode, including --dangerously-skip-permissions, and');
|
|
933
|
-
lines.push('nothing here can switch it off.');
|
|
934
|
-
lines.push('');
|
|
935
|
-
// Stated because the precedence is the part people get wrong, and getting it
|
|
936
|
-
// wrong in this direction is the dangerous one: assuming the flag lifts a
|
|
937
|
-
// `deny` they wrote.
|
|
938
|
-
lines.push('Then a rule decides. The approval setting above only reaches calls');
|
|
939
|
-
lines.push('no rule covered, so it can never reopen what a `deny` closed.');
|
|
940
1147
|
return lines.join('\n');
|
|
941
1148
|
}
|
|
942
1149
|
/** Render the exact session control without inventing a global effort menu. */
|
|
@@ -1044,8 +1251,13 @@ export function runSlash(line, ctx, builtins = CLI_LOCAL_COMMANDS) {
|
|
|
1044
1251
|
// a `problem` so their author is told, rather than leaving them to wonder
|
|
1045
1252
|
// why the file never ran.
|
|
1046
1253
|
const cmd = builtins.find((c) => c.name === parsed.name);
|
|
1047
|
-
if (cmd)
|
|
1048
|
-
|
|
1254
|
+
if (cmd) {
|
|
1255
|
+
const commandContext = { ...ctx, builtins };
|
|
1256
|
+
const reason = cmd.unavailable?.(commandContext);
|
|
1257
|
+
return reason
|
|
1258
|
+
? { kind: 'message', role: 'system', content: reason }
|
|
1259
|
+
: cmd.action(commandContext, parsed.args);
|
|
1260
|
+
}
|
|
1049
1261
|
const user = ctx.userCommands.find((c) => c.name === parsed.name);
|
|
1050
1262
|
if (user) {
|
|
1051
1263
|
const expanded = expandCommand(user, parsed.args.join(' '));
|