@namzu/cli 18.1.0 → 19.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 +5898 -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 +24 -13
- package/dist/integrations/subagents/runtime.d.ts.map +1 -1
- package/dist/integrations/subagents/runtime.js +282 -122
- package/dist/integrations/subagents/runtime.js.map +1 -1
- 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 +12 -45
- package/dist/permissions/mode.d.ts.map +1 -1
- package/dist/permissions/mode.js +18 -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.map +1 -1
- package/dist/tui/AgentExplorer.js +14 -18
- package/dist/tui/AgentExplorer.js.map +1 -1
- package/dist/tui/App.d.ts.map +1 -1
- package/dist/tui/App.js +606 -207
- 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 +17 -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 +4 -1
- package/dist/tui/Composer.d.ts.map +1 -1
- package/dist/tui/Composer.js +4 -4
- 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 +108 -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 +79 -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 +3 -3
- package/dist/tui/Transcript.d.ts.map +1 -1
- package/dist/tui/Transcript.js +16 -34
- package/dist/tui/Transcript.js.map +1 -1
- package/dist/tui/agent.d.ts +113 -50
- package/dist/tui/agent.d.ts.map +1 -1
- package/dist/tui/agent.js +512 -293
- 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 +77 -31
- package/dist/tui/slashCommands.d.ts.map +1 -1
- package/dist/tui/slashCommands.js +343 -303
- 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 +6 -1
- package/dist/tui/types.d.ts.map +1 -1
- package/package.json +6 -4
|
@@ -15,16 +15,17 @@
|
|
|
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';
|
|
27
|
+
import { PERMISSION_MODES, isPermissionMode, permissionModeDescription, } from '../permissions/mode.js';
|
|
28
|
+
import { readChangelog, renderReleaseNotes } from '../release-notes.js';
|
|
28
29
|
import { expandCommand } from '../user-commands/store.js';
|
|
29
30
|
import { isCompletionArgument } from './login-prompt.js';
|
|
30
31
|
function renameConversationAction(_ctx, args) {
|
|
@@ -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);
|
|
@@ -196,6 +197,35 @@ export function parseSlash(line) {
|
|
|
196
197
|
* being restated in this file.
|
|
197
198
|
*/
|
|
198
199
|
export const CLI_LOCAL_COMMANDS = [
|
|
200
|
+
{
|
|
201
|
+
name: 'settings',
|
|
202
|
+
description: 'View current settings and change model, reasoning or permissions.',
|
|
203
|
+
action: () => ({ kind: 'settings-picker' }),
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
name: 'agents',
|
|
207
|
+
description: 'Inspect delegated agents; /agents available lists configured agents.',
|
|
208
|
+
action: (_ctx, args) => args.length === 0 || (args.length === 1 && args[0] === 'running')
|
|
209
|
+
? { kind: 'agent-cockpit' }
|
|
210
|
+
: args.length === 1 && args[0] === 'available'
|
|
211
|
+
? { kind: 'host-command', name: 'agents', args: [] }
|
|
212
|
+
: { kind: 'message', role: 'system', content: 'Usage: /agents [running|available]' },
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
name: 'goal',
|
|
216
|
+
description: 'Manage this conversation’s goal and automatic continuation.',
|
|
217
|
+
action: (_ctx, args) => {
|
|
218
|
+
if (args.length === 0)
|
|
219
|
+
return { kind: 'goal-picker' };
|
|
220
|
+
if (args.length === 1 && args[0] === 'status') {
|
|
221
|
+
return { kind: 'host-command', name: 'goal', args: [] };
|
|
222
|
+
}
|
|
223
|
+
if (args.length === 1 && (args[0] === 'set' || args[0] === 'edit')) {
|
|
224
|
+
return { kind: 'goal-editor', edit: args[0] === 'edit' };
|
|
225
|
+
}
|
|
226
|
+
return { kind: 'host-command', name: 'goal', args };
|
|
227
|
+
},
|
|
228
|
+
},
|
|
199
229
|
{
|
|
200
230
|
name: 'help',
|
|
201
231
|
description: 'Choose and run an available slash command.',
|
|
@@ -212,6 +242,7 @@ export const CLI_LOCAL_COMMANDS = [
|
|
|
212
242
|
.map((command) => ({
|
|
213
243
|
name: command.name,
|
|
214
244
|
description: command.description,
|
|
245
|
+
...(command.unavailable?.(ctx) ? { problem: command.unavailable(ctx) } : {}),
|
|
215
246
|
})),
|
|
216
247
|
...ctx.userCommands.map((command) => ({
|
|
217
248
|
name: command.name,
|
|
@@ -222,14 +253,11 @@ export const CLI_LOCAL_COMMANDS = [
|
|
|
222
253
|
};
|
|
223
254
|
},
|
|
224
255
|
},
|
|
225
|
-
{
|
|
226
|
-
name: 'agent',
|
|
227
|
-
description: 'Compatibility alias for the automatic delegated-work panel.',
|
|
228
|
-
discoverable: false,
|
|
229
|
-
action: () => ({ kind: 'agent-cockpit' }),
|
|
230
|
-
},
|
|
231
256
|
{
|
|
232
257
|
name: 'feedback',
|
|
258
|
+
unavailable: (ctx) => ctx.lastAssistantMessageId()
|
|
259
|
+
? undefined
|
|
260
|
+
: 'Nothing to rate yet. Wait for an assistant answer.',
|
|
233
261
|
description: 'Rate the last answer; choose good/bad or add an optional note.',
|
|
234
262
|
action: (ctx, args) => {
|
|
235
263
|
if (args.length === 0) {
|
|
@@ -282,70 +310,66 @@ export const CLI_LOCAL_COMMANDS = [
|
|
|
282
310
|
action: () => ({ kind: 'archive-picker' }),
|
|
283
311
|
},
|
|
284
312
|
{
|
|
285
|
-
name: '
|
|
286
|
-
description: 'Clear only the terminal transcript; keep this conversation context.',
|
|
287
|
-
action: () => ({ kind: 'clear-screen' }),
|
|
288
|
-
},
|
|
289
|
-
{
|
|
290
|
-
name: 'quit',
|
|
313
|
+
name: 'exit',
|
|
291
314
|
description: 'Exit namzu.',
|
|
292
315
|
action: () => ({ kind: 'exit' }),
|
|
293
316
|
},
|
|
294
317
|
{
|
|
295
|
-
name: '
|
|
296
|
-
description: '
|
|
297
|
-
action:
|
|
318
|
+
name: 'rename',
|
|
319
|
+
description: 'Rename this conversation; opens an editor when no name is supplied. /rename clear removes the saved name.',
|
|
320
|
+
action: renameConversationAction,
|
|
298
321
|
},
|
|
299
322
|
{
|
|
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
|
-
},
|
|
323
|
+
name: 'fork',
|
|
324
|
+
description: 'Continue in a copy of this conversation, leaving the original where it is.',
|
|
325
|
+
action: () => ({ kind: 'fork' }),
|
|
315
326
|
},
|
|
316
327
|
{
|
|
317
|
-
name: '
|
|
318
|
-
description: '
|
|
319
|
-
action: (
|
|
320
|
-
const
|
|
321
|
-
|
|
322
|
-
|
|
328
|
+
name: 'add-dir',
|
|
329
|
+
description: 'Let the file tools reach another directory this session: /add-dir <path>. Alone, list them.',
|
|
330
|
+
action: (ctx, args) => {
|
|
331
|
+
const path = args.join(' ').trim();
|
|
332
|
+
if (path.length === 0) {
|
|
333
|
+
const dirs = ctx.directories?.() ?? [];
|
|
334
|
+
return {
|
|
323
335
|
kind: 'message',
|
|
324
336
|
role: 'system',
|
|
325
|
-
content:
|
|
326
|
-
|
|
327
|
-
|
|
337
|
+
content: dirs.length === 0
|
|
338
|
+
? '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.'
|
|
339
|
+
: `Added directories (reachable by absolute path, bound into the sandbox):\n${dirs.map((d) => ` ${d}`).join('\n')}`,
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
return { kind: 'add-dir', path };
|
|
328
343
|
},
|
|
329
344
|
},
|
|
330
345
|
{
|
|
331
|
-
name: '
|
|
332
|
-
description: '
|
|
333
|
-
action:
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
346
|
+
name: 'restore',
|
|
347
|
+
description: 'Put files back to before a turn: /restore lists the checkpoints, /restore N restores.',
|
|
348
|
+
action: (_ctx, args) => {
|
|
349
|
+
const raw = args[0]?.trim();
|
|
350
|
+
if (!raw)
|
|
351
|
+
return { kind: 'restore' };
|
|
352
|
+
const turn = Number.parseInt(raw, 10);
|
|
353
|
+
if (!Number.isInteger(turn) || turn < 1 || String(turn) !== raw) {
|
|
354
|
+
return {
|
|
355
|
+
kind: 'message',
|
|
356
|
+
role: 'system',
|
|
357
|
+
content: `/restore takes a turn number from the list: /restore 3. Not: ${raw}`,
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
return { kind: 'restore', turn };
|
|
361
|
+
},
|
|
344
362
|
},
|
|
345
363
|
{
|
|
346
364
|
name: 'memory',
|
|
347
|
-
description: 'Show what namzu remembers
|
|
348
|
-
action: () =>
|
|
365
|
+
description: 'Show what namzu remembers, or save a fact about this project: /memory [text]. /memory --user [text] saves it for every project.',
|
|
366
|
+
action: (_ctx, args) => {
|
|
367
|
+
const user = args[0] === '--user';
|
|
368
|
+
const text = (user ? args.slice(1) : args).join(' ').trim();
|
|
369
|
+
return text.length === 0
|
|
370
|
+
? { kind: 'show-memory' }
|
|
371
|
+
: { kind: 'remember', text, scope: user ? 'user' : 'project' };
|
|
372
|
+
},
|
|
349
373
|
},
|
|
350
374
|
{
|
|
351
375
|
name: 'skills',
|
|
@@ -364,66 +388,9 @@ export const CLI_LOCAL_COMMANDS = [
|
|
|
364
388
|
description: 'Resume a past conversation in this folder.',
|
|
365
389
|
action: () => ({ kind: 'resume' }),
|
|
366
390
|
},
|
|
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
391
|
{
|
|
425
392
|
name: 'model',
|
|
426
|
-
description: '
|
|
393
|
+
description: 'Choose a model for the current provider, or change providers.',
|
|
427
394
|
action: () => ({ kind: 'repick' }),
|
|
428
395
|
},
|
|
429
396
|
{
|
|
@@ -456,13 +423,41 @@ export const CLI_LOCAL_COMMANDS = [
|
|
|
456
423
|
},
|
|
457
424
|
{
|
|
458
425
|
name: 'cost',
|
|
459
|
-
description: 'Show
|
|
426
|
+
description: 'Show usage and cost for the current or latest run.',
|
|
427
|
+
action: (ctx, args) => reportCommand('cost', args, (details) => renderCost(ctx.usage, ctx.compaction, details)),
|
|
428
|
+
},
|
|
429
|
+
{
|
|
430
|
+
name: 'jobs',
|
|
431
|
+
description: 'List background jobs started this session, running and ended.',
|
|
432
|
+
action: (ctx) => ({
|
|
433
|
+
kind: 'message',
|
|
434
|
+
role: 'system',
|
|
435
|
+
content: renderJobs(ctx.jobs()),
|
|
436
|
+
}),
|
|
437
|
+
},
|
|
438
|
+
{
|
|
439
|
+
name: 'release-notes',
|
|
440
|
+
description: 'Show what changed in the version that is running: /release-notes [version].',
|
|
441
|
+
action: (_ctx, args) => ({
|
|
442
|
+
kind: 'message',
|
|
443
|
+
role: 'system',
|
|
444
|
+
content: renderReleaseNotes(readChangelog(), args[0]?.trim() || undefined),
|
|
445
|
+
}),
|
|
446
|
+
},
|
|
447
|
+
{
|
|
448
|
+
name: 'hooks',
|
|
449
|
+
description: 'List the shell hooks this session runs, by event.',
|
|
460
450
|
action: (ctx) => ({
|
|
461
451
|
kind: 'message',
|
|
462
452
|
role: 'system',
|
|
463
|
-
content:
|
|
453
|
+
content: renderHooks(ctx.hooks?.()),
|
|
464
454
|
}),
|
|
465
455
|
},
|
|
456
|
+
{
|
|
457
|
+
name: 'context',
|
|
458
|
+
description: 'Show context usage and automatic cleanup.',
|
|
459
|
+
action: (ctx, args) => reportCommand('context', args, (details) => renderContext(ctx.usage, ctx.compaction, details)),
|
|
460
|
+
},
|
|
466
461
|
{
|
|
467
462
|
name: 'review',
|
|
468
463
|
description: 'Choose a review target, or provide custom instructions: /review [instructions].',
|
|
@@ -473,12 +468,8 @@ export const CLI_LOCAL_COMMANDS = [
|
|
|
473
468
|
},
|
|
474
469
|
{
|
|
475
470
|
name: 'mcp',
|
|
476
|
-
description: 'Show
|
|
477
|
-
action: (ctx) => (
|
|
478
|
-
kind: 'message',
|
|
479
|
-
role: 'system',
|
|
480
|
-
content: renderMcp(ctx.mcp()),
|
|
481
|
-
}),
|
|
471
|
+
description: 'Show connected tool servers and connection problems.',
|
|
472
|
+
action: (ctx, args) => reportCommand('mcp', args, (details) => renderMcp(ctx.mcp(), details), 'tools'),
|
|
482
473
|
},
|
|
483
474
|
{
|
|
484
475
|
name: 'diff',
|
|
@@ -523,35 +514,56 @@ export const CLI_LOCAL_COMMANDS = [
|
|
|
523
514
|
},
|
|
524
515
|
{
|
|
525
516
|
name: 'status',
|
|
526
|
-
description: 'Show
|
|
527
|
-
action: (ctx) =>
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
517
|
+
description: 'Show the model, permissions, workspace and latest cost.',
|
|
518
|
+
action: (ctx, args) => {
|
|
519
|
+
const which = args.join(' ').trim().toLowerCase();
|
|
520
|
+
if (which === 'config') {
|
|
521
|
+
return {
|
|
522
|
+
kind: 'message',
|
|
523
|
+
role: 'system',
|
|
524
|
+
content: renderConfigDebug(ctx.configDebug),
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
if (which === 'tools') {
|
|
528
|
+
const tools = ctx.availableTools();
|
|
529
|
+
return {
|
|
530
|
+
kind: 'message',
|
|
531
|
+
role: 'system',
|
|
532
|
+
content: tools.length === 0
|
|
533
|
+
? 'No tools registered yet — the agent session may still be connecting.'
|
|
534
|
+
: `Registered tools (${tools.length}):\n ${tools.join('\n ')}`,
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
if (which.length > 0 && which !== 'details') {
|
|
538
|
+
return {
|
|
539
|
+
kind: 'message',
|
|
540
|
+
role: 'system',
|
|
541
|
+
content: 'Usage: /status [details|config|tools]',
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
return { kind: 'message', role: 'system', content: renderStatus(ctx, which === 'details') };
|
|
545
|
+
},
|
|
541
546
|
},
|
|
542
547
|
{
|
|
543
548
|
name: 'permissions',
|
|
544
|
-
description: 'Choose
|
|
545
|
-
action: (
|
|
549
|
+
description: 'Choose which actions need your approval.',
|
|
550
|
+
action: (ctx, args) => {
|
|
546
551
|
if (args.length === 0)
|
|
547
552
|
return { kind: 'permission-mode-picker' };
|
|
548
553
|
const mode = args.length === 1 ? args[0]?.toLowerCase() : undefined;
|
|
554
|
+
if (mode === 'details') {
|
|
555
|
+
return {
|
|
556
|
+
kind: 'message',
|
|
557
|
+
role: 'system',
|
|
558
|
+
content: renderPermissions(ctx.permissions, true),
|
|
559
|
+
};
|
|
560
|
+
}
|
|
549
561
|
if (isPermissionMode(mode))
|
|
550
562
|
return { kind: 'permission-mode', mode };
|
|
551
563
|
return {
|
|
552
564
|
kind: 'message',
|
|
553
565
|
role: 'system',
|
|
554
|
-
content:
|
|
566
|
+
content: `Usage: /permissions [details|${PERMISSION_MODES.join('|')}]`,
|
|
555
567
|
};
|
|
556
568
|
},
|
|
557
569
|
},
|
|
@@ -667,53 +679,131 @@ export function initPrompt(instructionFiles) {
|
|
|
667
679
|
'future run, so a sentence that says nothing is not free.',
|
|
668
680
|
].join('\n');
|
|
669
681
|
}
|
|
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.';
|
|
682
|
+
/** The session's shell hooks, by event, in the order the config file gave them. */
|
|
683
|
+
export function renderHooks(hooks) {
|
|
684
|
+
const events = Object.entries(hooks ?? {}).filter(([, entries]) => (entries?.length ?? 0) > 0);
|
|
685
|
+
if (events.length === 0) {
|
|
686
|
+
return 'No hooks. Add a `hooks` key to namzu.config.json or ~/.namzu/config.yaml: event → list of { command, matcher?, timeoutMs? }.';
|
|
682
687
|
}
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
688
|
+
const lines = [];
|
|
689
|
+
for (const [event, entries] of events) {
|
|
690
|
+
lines.push(`${event}`);
|
|
691
|
+
for (const entry of entries ?? []) {
|
|
692
|
+
const when = entry.matcher ? ` · matches ${entry.matcher}` : '';
|
|
693
|
+
const deadline = entry.timeoutMs !== undefined ? ` · ${Math.round(entry.timeoutMs / 1000)}s` : '';
|
|
694
|
+
lines.push(` ${entry.command}${when}${deadline}`);
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
return lines.join('\n');
|
|
698
|
+
}
|
|
699
|
+
/** The session's background jobs, as an operator would ask about them. */
|
|
700
|
+
export function renderJobs(jobs) {
|
|
701
|
+
if (jobs.length === 0) {
|
|
702
|
+
return 'No background jobs this session. The agent starts one with `run_in_background` on bash; under a sandbox none can be started.';
|
|
703
|
+
}
|
|
704
|
+
const lines = jobs.map((job) => {
|
|
705
|
+
const state = job.status === 'running'
|
|
706
|
+
? `running for ${Math.max(1, Math.round((Date.now() - job.startedAt) / 1000))}s`
|
|
707
|
+
: job.status === 'killed'
|
|
708
|
+
? 'stopped'
|
|
709
|
+
: `exited ${job.exitCode ?? '?'}`;
|
|
710
|
+
return `${job.id} ${state.padEnd(16)} ${job.command}`;
|
|
711
|
+
});
|
|
712
|
+
return [
|
|
713
|
+
`${jobs.length} background job${jobs.length === 1 ? '' : 's'} this session:`,
|
|
714
|
+
...lines,
|
|
705
715
|
'',
|
|
716
|
+
'The agent reads one with the job tool; ask it to stop one, or /exit stops them all.',
|
|
717
|
+
].join('\n');
|
|
718
|
+
}
|
|
719
|
+
/** Reports validate their subcommand instead of silently ignoring mistyped arguments. */
|
|
720
|
+
function reportCommand(name, args, render, detailName = 'details') {
|
|
721
|
+
const option = args.join(' ').trim().toLowerCase();
|
|
722
|
+
const details = option === detailName || option === 'details';
|
|
723
|
+
return {
|
|
724
|
+
kind: 'message',
|
|
725
|
+
role: 'system',
|
|
726
|
+
content: option.length === 0 || details ? render(details) : `Usage: /${name} [${detailName}]`,
|
|
727
|
+
};
|
|
728
|
+
}
|
|
729
|
+
function contextMeasurement(usage) {
|
|
730
|
+
const context = usage?.context;
|
|
731
|
+
if (!context || context.windowTokens <= 0)
|
|
732
|
+
return ['No context measurement yet.'];
|
|
733
|
+
const percent = Math.min(999, Math.round((context.tokens / context.windowTokens) * 100));
|
|
734
|
+
const approx = context.measured && !context.windowAssumed ? '' : '~';
|
|
735
|
+
return [
|
|
736
|
+
`Context: ${context.tokens.toLocaleString('en-US')} / ${context.windowTokens.toLocaleString('en-US')} tokens (${approx}${percent}%)`,
|
|
737
|
+
`${context.measured ? 'Counted by the provider' : 'Estimated by Namzu'}; window ${context.windowAssumed ? 'assumed from a table or default' : 'declared by the provider or config'}.`,
|
|
738
|
+
];
|
|
739
|
+
}
|
|
740
|
+
export function renderContext(usage, compaction, details = false) {
|
|
741
|
+
const lines = contextMeasurement(usage);
|
|
742
|
+
if (usage?.context)
|
|
743
|
+
lines.push('Latest reported context size; not cumulative token usage.');
|
|
744
|
+
if (!compaction) {
|
|
745
|
+
lines.push('Automatic cleanup: unavailable before a session starts.');
|
|
746
|
+
}
|
|
747
|
+
else {
|
|
748
|
+
lines.push(`Cleanup this session: ${compaction.passes} passes, ~${compaction.reclaimedTokens.toLocaleString('en-US')} tokens freed.`);
|
|
749
|
+
if (details) {
|
|
750
|
+
const pct = (fraction) => `${Math.round(fraction * 100)}%`;
|
|
751
|
+
lines.push(`Strategy: ${compaction.strategy}`);
|
|
752
|
+
if (compaction.strategy === 'salience') {
|
|
753
|
+
lines.push(`Older, less useful content is shortened from ${pct(compaction.softTarget)} of the window; older history is summarised at ${pct(compaction.triggerThreshold)}.`);
|
|
754
|
+
}
|
|
755
|
+
else {
|
|
756
|
+
lines.push(`Older tool results are cleared at ${pct(compaction.triggerThreshold)} of the window; history is summarised if needed.`);
|
|
757
|
+
}
|
|
758
|
+
lines.push(`Tool results cleared: ${compaction.clearedResults.toLocaleString('en-US')}`, `Messages shortened: ${compaction.stubbedNarrations.toLocaleString('en-US')}`, `Summaries written: ${compaction.summaries.toLocaleString('en-US')}`);
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
if (!details)
|
|
762
|
+
lines.push('/context details for cleanup history.');
|
|
763
|
+
return lines.join('\n');
|
|
764
|
+
}
|
|
765
|
+
/** Keep unknown prices distinct from measured zero, in status and cost alike. */
|
|
766
|
+
function costAmount(cost) {
|
|
767
|
+
if (cost.unpricedTokens > 0) {
|
|
768
|
+
return cost.totalCost > 0 ? `at least $${cost.totalCost.toFixed(4)}` : 'not known';
|
|
769
|
+
}
|
|
770
|
+
return `$${cost.totalCost.toFixed(4)}${cost.totalCost === 0 ? ' (measured zero)' : ''}`;
|
|
771
|
+
}
|
|
772
|
+
export function renderCost(usage, compaction = null, details = false) {
|
|
773
|
+
if (usage === null)
|
|
774
|
+
return 'No usage reported yet. Send a message to begin.';
|
|
775
|
+
const lines = [
|
|
776
|
+
'Current or latest run',
|
|
777
|
+
`Tokens: ${usage.totalTokens.toLocaleString('en-US')} (own model calls)`,
|
|
778
|
+
`Cost: ${costAmount(usage.cost)}`,
|
|
706
779
|
];
|
|
707
|
-
if (
|
|
708
|
-
lines.push(`${usage.cost.unpricedTokens.toLocaleString('en-US')} tokens
|
|
780
|
+
if (usage.cost.unpricedTokens > 0) {
|
|
781
|
+
lines.push(`${usage.cost.unpricedTokens.toLocaleString('en-US')} tokens have no known price and are excluded from the cost.`);
|
|
782
|
+
}
|
|
783
|
+
if (usage.budget) {
|
|
784
|
+
lines.push(`Including delegated agents: ${usage.budget.treeTokens.toLocaleString('en-US')} tokens; limit ${usage.budget.limit === 0 ? 'unlimited' : usage.budget.limit.toLocaleString('en-US')}.`);
|
|
785
|
+
if (usage.budget.poisoned)
|
|
786
|
+
lines.push('Further spending is blocked until unresolved request usage is reconciled.');
|
|
709
787
|
}
|
|
710
|
-
|
|
711
|
-
lines.push('
|
|
788
|
+
if (details) {
|
|
789
|
+
lines.push('Cost covers this run’s own model calls, excluding delegated calls and earlier runs. These are not conversation totals.');
|
|
790
|
+
if (usage.cost.unpricedTokens > 0) {
|
|
791
|
+
lines.push('Missing prices do not mean those tokens were free.');
|
|
792
|
+
}
|
|
793
|
+
else {
|
|
794
|
+
lines.push('All reported tokens have a known rate.');
|
|
795
|
+
}
|
|
796
|
+
if (usage.cost.cacheDiscount > 0)
|
|
797
|
+
lines.push(`Cache discount: $${usage.cost.cacheDiscount.toFixed(4)}`);
|
|
798
|
+
if (usage.context && usage.context.windowTokens > 0) {
|
|
799
|
+
lines.push('', ...contextMeasurement(usage), '/context details for automatic cleanup.');
|
|
800
|
+
}
|
|
801
|
+
if (compaction)
|
|
802
|
+
lines.push(`Cleanup passes this session: ${compaction.passes}.`);
|
|
712
803
|
}
|
|
713
804
|
else {
|
|
714
|
-
lines.push('
|
|
805
|
+
lines.push('/cost details for scope and pricing; /context for context size.');
|
|
715
806
|
}
|
|
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
807
|
return lines.join('\n');
|
|
718
808
|
}
|
|
719
809
|
/** What decides a tool call, in the order it actually decides it. */
|
|
@@ -797,146 +887,92 @@ function reviewTargetPrompt(target) {
|
|
|
797
887
|
'Say plainly what you did not examine.',
|
|
798
888
|
].join('\n');
|
|
799
889
|
}
|
|
800
|
-
export function renderMcp(mcp) {
|
|
890
|
+
export function renderMcp(mcp, details = false) {
|
|
801
891
|
if (mcp === null)
|
|
802
|
-
return 'No session yet
|
|
803
|
-
const lines = [];
|
|
892
|
+
return 'No session yet. Tool servers have not been checked.';
|
|
804
893
|
if (mcp.connected.length === 0 && mcp.failed.length === 0) {
|
|
805
|
-
return 'No tool servers configured. Add
|
|
894
|
+
return 'No tool servers configured. Add mcpServers to namzu.config.json.';
|
|
806
895
|
}
|
|
896
|
+
const lines = [
|
|
897
|
+
`Tool servers: ${mcp.connected.length} connected, ${mcp.failed.length} unavailable.`,
|
|
898
|
+
];
|
|
807
899
|
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
|
-
}
|
|
900
|
+
lines.push(`${server.name}: connected, ${server.tools.length} tools`);
|
|
901
|
+
if (details)
|
|
902
|
+
for (const tool of server.tools)
|
|
903
|
+
lines.push(` ${tool}`);
|
|
820
904
|
}
|
|
905
|
+
for (const server of mcp.failed)
|
|
906
|
+
lines.push(`${server.name}: unavailable — ${server.reason}`);
|
|
907
|
+
if (!details && mcp.connected.length > 0)
|
|
908
|
+
lines.push('/mcp tools to list available tools.');
|
|
821
909
|
return lines.join('\n');
|
|
822
910
|
}
|
|
823
|
-
export function renderStatus(ctx) {
|
|
824
|
-
const lines = [
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
911
|
+
export function renderStatus(ctx, details = false) {
|
|
912
|
+
const lines = [
|
|
913
|
+
`Provider: ${ctx.providerSummary ?? 'none — run /model to choose one'}`,
|
|
914
|
+
`Model: ${ctx.modelSummary ?? 'not selected'}`,
|
|
915
|
+
`Working directory: ${ctx.cwd}`,
|
|
916
|
+
...renderPermissions(ctx.permissions, details).split('\n'),
|
|
917
|
+
];
|
|
829
918
|
const sandbox = ctx.sandbox;
|
|
830
919
|
if (!sandbox) {
|
|
831
|
-
lines.push('
|
|
920
|
+
lines.push('Sandbox: not resolved yet.');
|
|
832
921
|
}
|
|
833
922
|
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.');
|
|
923
|
+
lines.push('Sandbox: commands are not confined; they have this shell’s access.');
|
|
842
924
|
}
|
|
843
925
|
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.`);
|
|
851
|
-
}
|
|
852
|
-
else if (sandbox) {
|
|
853
|
-
lines.push(' Required by config: nothing — this host decides what you get.');
|
|
854
|
-
}
|
|
855
|
-
if (sandbox?.workspace === 'working-directory') {
|
|
856
|
-
lines.push(' Workspace: real project files — changes persist across turns.');
|
|
926
|
+
lines.push(`Sandbox: ${sandbox.environment ?? 'active'}; enforces ${sandbox.enforced.join(', ') || 'no reported restrictions'}.`);
|
|
857
927
|
}
|
|
858
|
-
|
|
859
|
-
lines.push('
|
|
928
|
+
if (sandbox?.workspace === 'ephemeral') {
|
|
929
|
+
lines.push('Workspace: temporary files; removed when the run ends.');
|
|
860
930
|
}
|
|
861
|
-
else if (sandbox?.workspace === 'host') {
|
|
862
|
-
lines.push('
|
|
931
|
+
else if (sandbox?.workspace === 'working-directory' || sandbox?.workspace === 'host') {
|
|
932
|
+
lines.push('Workspace: real project files; edits persist.');
|
|
863
933
|
}
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
934
|
+
if (details) {
|
|
935
|
+
for (const dir of ctx.directories?.() ?? [])
|
|
936
|
+
lines.push(`Additional directory: ${dir}`);
|
|
937
|
+
if (sandbox) {
|
|
938
|
+
lines.push(`Required isolation: ${sandbox.required.length > 0 ? sandbox.required.join(', ') : 'none'}.`);
|
|
939
|
+
if (sandbox.required.length > 0)
|
|
940
|
+
lines.push('A host missing required isolation cannot start the session.');
|
|
941
|
+
}
|
|
942
|
+
lines.push('/status config for setting sources; /status tools for available tools.');
|
|
872
943
|
}
|
|
873
|
-
|
|
944
|
+
if (ctx.usage)
|
|
945
|
+
lines.push(`Spend (current or latest run, own calls): ${costAmount(ctx.usage.cost)}`);
|
|
946
|
+
if (!details)
|
|
947
|
+
lines.push('/status details for rules and workspace details.');
|
|
948
|
+
return lines.join('\n');
|
|
874
949
|
}
|
|
875
|
-
export function renderPermissions(permissions) {
|
|
876
|
-
const lines = [];
|
|
950
|
+
export function renderPermissions(permissions, details = false) {
|
|
877
951
|
const current = permissions.currentMode();
|
|
878
|
-
|
|
879
|
-
lines
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
lines.push('Unreviewed calls: approved automatically — "approve all" was chosen at');
|
|
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.');
|
|
952
|
+
const approvedAll = (current.mode === 'prompt' || current.mode === 'accept-edits') && permissions.approvalLatched();
|
|
953
|
+
const lines = [
|
|
954
|
+
`Permissions: ${current.mode}. ${approvedAll ? 'Tools are approved automatically because “approve all” is active.' : permissionModeDescription(current.mode)}`,
|
|
955
|
+
];
|
|
956
|
+
if (approvedAll)
|
|
957
|
+
lines.push('Use /permissions prompt to ask again.');
|
|
958
|
+
if (!details)
|
|
959
|
+
return lines.join('\n');
|
|
960
|
+
if (current.source === 'launch-bypass')
|
|
961
|
+
lines.push('Selected at launch with --dangerously-skip-permissions.');
|
|
962
|
+
lines.push('Explicit deny rules and the built-in safety gate apply in every mode.');
|
|
963
|
+
if (current.mode === 'plan')
|
|
964
|
+
lines.push('Plan mode also blocks writes that an allow rule would otherwise permit.');
|
|
965
|
+
const exempt = permissions.neverPrompted();
|
|
966
|
+
if (exempt.length > 0) {
|
|
967
|
+
lines.push(`Tools exempt from ordinary prompts: ${exempt.join(', ')}.`);
|
|
968
|
+
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
969
|
}
|
|
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.');
|
|
970
|
+
if (permissions.rules.length > 0) {
|
|
971
|
+
lines.push(`Rules (${permissions.rules.length}):`, ...permissions.rules.map((rule) => ` ${describeRule(rule)}`));
|
|
917
972
|
}
|
|
918
973
|
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)}`);
|
|
974
|
+
lines.push('No custom rules. Configure permissions in namzu.config.json.');
|
|
923
975
|
}
|
|
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
976
|
return lines.join('\n');
|
|
941
977
|
}
|
|
942
978
|
/** Render the exact session control without inventing a global effort menu. */
|
|
@@ -1044,8 +1080,12 @@ export function runSlash(line, ctx, builtins = CLI_LOCAL_COMMANDS) {
|
|
|
1044
1080
|
// a `problem` so their author is told, rather than leaving them to wonder
|
|
1045
1081
|
// why the file never ran.
|
|
1046
1082
|
const cmd = builtins.find((c) => c.name === parsed.name);
|
|
1047
|
-
if (cmd)
|
|
1048
|
-
|
|
1083
|
+
if (cmd) {
|
|
1084
|
+
const reason = cmd.unavailable?.(ctx);
|
|
1085
|
+
return reason
|
|
1086
|
+
? { kind: 'message', role: 'system', content: reason }
|
|
1087
|
+
: cmd.action(ctx, parsed.args);
|
|
1088
|
+
}
|
|
1049
1089
|
const user = ctx.userCommands.find((c) => c.name === parsed.name);
|
|
1050
1090
|
if (user) {
|
|
1051
1091
|
const expanded = expandCommand(user, parsed.args.join(' '));
|