@pellux/goodvibes-agent 0.1.9 → 0.1.11

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.
Files changed (97) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/README.md +1 -1
  3. package/docs/getting-started.md +1 -1
  4. package/docs/release-and-publishing.md +2 -2
  5. package/package.json +4 -1
  6. package/src/cli/agent-knowledge-command.ts +46 -20
  7. package/src/cli/help.ts +15 -2
  8. package/src/cli/management-commands.ts +3 -3
  9. package/src/cli/management.ts +7 -1
  10. package/src/cli/parser.ts +3 -0
  11. package/src/cli/service-posture.ts +6 -6
  12. package/src/cli/status.ts +9 -9
  13. package/src/cli/surface-command.ts +3 -3
  14. package/src/cli/types.ts +2 -0
  15. package/src/input/commands/cloudflare-runtime.ts +20 -5
  16. package/src/input/commands/confirmation.ts +24 -0
  17. package/src/input/commands/discovery-runtime.ts +16 -7
  18. package/src/input/commands/eval.ts +27 -14
  19. package/src/input/commands/experience-runtime.ts +66 -27
  20. package/src/input/commands/health-runtime.ts +1 -1
  21. package/src/input/commands/hooks-runtime.ts +79 -20
  22. package/src/input/commands/incident-runtime.ts +17 -6
  23. package/src/input/commands/integration-runtime.ts +93 -50
  24. package/src/input/commands/knowledge.ts +38 -12
  25. package/src/input/commands/local-auth-runtime.ts +36 -13
  26. package/src/input/commands/local-provider-runtime.ts +22 -11
  27. package/src/input/commands/local-runtime.ts +21 -11
  28. package/src/input/commands/local-setup.ts +35 -16
  29. package/src/input/commands/managed-runtime.ts +51 -20
  30. package/src/input/commands/marketplace-runtime.ts +31 -16
  31. package/src/input/commands/mcp-runtime.ts +65 -34
  32. package/src/input/commands/memory-product-runtime.ts +72 -35
  33. package/src/input/commands/memory.ts +9 -9
  34. package/src/input/commands/notify-runtime.ts +27 -8
  35. package/src/input/commands/operator-runtime.ts +85 -17
  36. package/src/input/commands/planning-runtime.ts +14 -2
  37. package/src/input/commands/platform-access-runtime.ts +88 -45
  38. package/src/input/commands/platform-services-runtime.ts +51 -25
  39. package/src/input/commands/product-runtime.ts +54 -27
  40. package/src/input/commands/profile-sync-runtime.ts +17 -6
  41. package/src/input/commands/recall-bundle.ts +38 -17
  42. package/src/input/commands/recall-query.ts +15 -4
  43. package/src/input/commands/recall-review.ts +9 -3
  44. package/src/input/commands/remote-runtime-setup.ts +45 -18
  45. package/src/input/commands/remote-runtime.ts +25 -9
  46. package/src/input/commands/replay-runtime.ts +9 -2
  47. package/src/input/commands/services-runtime.ts +21 -10
  48. package/src/input/commands/session-content.ts +53 -51
  49. package/src/input/commands/session-workflow.ts +10 -4
  50. package/src/input/commands/session.ts +1 -1
  51. package/src/input/commands/settings-sync-runtime.ts +40 -17
  52. package/src/input/commands/share-runtime.ts +12 -4
  53. package/src/input/commands/shell-core.ts +3 -3
  54. package/src/input/commands/subscription-runtime.ts +35 -20
  55. package/src/input/commands/teleport-runtime.ts +16 -5
  56. package/src/input/commands/work-plan-runtime.ts +23 -12
  57. package/src/input/handler-content-actions.ts +11 -62
  58. package/src/input/handler-interactions.ts +1 -1
  59. package/src/input/handler-onboarding-cloudflare.ts +48 -117
  60. package/src/input/handler.ts +1 -0
  61. package/src/input/keybindings.ts +1 -1
  62. package/src/input/mcp-workspace.ts +25 -49
  63. package/src/input/onboarding/onboarding-runtime-status.ts +8 -8
  64. package/src/input/onboarding/onboarding-wizard-apply.ts +13 -53
  65. package/src/input/onboarding/onboarding-wizard-cloudflare-step.ts +12 -12
  66. package/src/input/onboarding/onboarding-wizard-cloudflare.ts +2 -7
  67. package/src/input/onboarding/onboarding-wizard-constants.ts +7 -7
  68. package/src/input/onboarding/onboarding-wizard-external-surface-extra-specs.ts +4 -4
  69. package/src/input/onboarding/onboarding-wizard-steps.ts +13 -13
  70. package/src/input/profile-picker-modal.ts +13 -31
  71. package/src/input/session-picker-modal.ts +4 -30
  72. package/src/input/settings-modal-agent-policy.ts +18 -0
  73. package/src/input/settings-modal-subscriptions.ts +3 -3
  74. package/src/input/settings-modal-types.ts +17 -0
  75. package/src/input/settings-modal.ts +30 -29
  76. package/src/main.ts +3 -26
  77. package/src/panels/incident-review-panel.ts +1 -1
  78. package/src/panels/local-auth-panel.ts +4 -4
  79. package/src/panels/provider-account-snapshot.ts +1 -1
  80. package/src/panels/provider-health-domains.ts +2 -2
  81. package/src/panels/settings-sync-panel.ts +2 -2
  82. package/src/panels/subscription-panel.ts +7 -7
  83. package/src/renderer/block-actions.ts +1 -1
  84. package/src/renderer/help-overlay.ts +2 -2
  85. package/src/renderer/mcp-workspace.ts +12 -12
  86. package/src/renderer/process-modal.ts +17 -8
  87. package/src/renderer/profile-picker-modal.ts +3 -11
  88. package/src/renderer/session-picker-modal.ts +2 -10
  89. package/src/renderer/settings-modal.ts +12 -8
  90. package/src/renderer/ui-factory.ts +4 -32
  91. package/src/runtime/bootstrap-shell.ts +0 -13
  92. package/src/runtime/bootstrap.ts +0 -10
  93. package/src/runtime/onboarding/derivation.ts +6 -6
  94. package/src/verification/live-verifier.ts +148 -13
  95. package/src/version.ts +10 -3
  96. package/src/input/commands/quit-shared.ts +0 -162
  97. package/src/renderer/git-status.ts +0 -89
@@ -2,6 +2,7 @@ import type { CommandContext, CommandRegistry } from '../command-registry.ts';
2
2
  import type { McpConfigScope, McpReloadResult, McpServerConfig } from '@pellux/goodvibes-sdk/platform/mcp';
3
3
  import { requireMcpApi, requireShellPaths } from './runtime-services.ts';
4
4
  import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
5
+ import { requireYesFlag, stripYesFlag } from './confirmation.ts';
5
6
 
6
7
  const MCP_ROLES = ['general', 'docs', 'filesystem', 'git', 'database', 'browser', 'automation', 'ops', 'remote'] as const;
7
8
  const MCP_TRUST_MODES = ['constrained', 'ask-on-risk', 'allow-all', 'blocked'] as const;
@@ -137,11 +138,13 @@ export function registerMcpRuntimeCommands(registry: CommandRegistry): void {
137
138
  aliases: [],
138
139
  description: 'Manage MCP servers and their tools',
139
140
  usage: '[add|remove|reload|config|review|tools [<server>]|auth-review|repair [server]]',
140
- argsHint: '[add|remove|reload|config|review|tools [server]]',
141
+ argsHint: '[review|tools|config|add --yes|remove --yes]',
141
142
  async handler(args, ctx) {
142
143
  const mcpApi = requireMcpApi(ctx);
143
144
  const listServerSecurity = () => mcpApi.listServerSecurity();
144
- const subcommand = args[0];
145
+ const confirmation = stripYesFlag(args);
146
+ const commandArgs = [...confirmation.rest];
147
+ const subcommand = commandArgs[0];
145
148
  if (!subcommand && ctx.openMcpWorkspace) {
146
149
  ctx.openMcpWorkspace();
147
150
  return;
@@ -161,7 +164,7 @@ export function registerMcpRuntimeCommands(registry: CommandRegistry): void {
161
164
  return;
162
165
  }
163
166
  if (subcommand === 'tools') {
164
- const filterServer = args[1];
167
+ const filterServer = commandArgs[1];
165
168
  ctx.print('Fetching MCP tool list...');
166
169
  let allTools;
167
170
  try {
@@ -207,7 +210,7 @@ export function registerMcpRuntimeCommands(registry: CommandRegistry): void {
207
210
  }
208
211
 
209
212
  if (subcommand === 'repair') {
210
- const serverName = args[1];
213
+ const serverName = commandArgs[1];
211
214
  const servers = listServerSecurity();
212
215
  const selected = serverName ? servers.find((server) => server.name === serverName) : servers.find((server) => !server.connected || server.schemaFreshness === 'quarantined');
213
216
  if (!selected) {
@@ -218,7 +221,7 @@ export function registerMcpRuntimeCommands(registry: CommandRegistry): void {
218
221
  }
219
222
  const nextSteps = [
220
223
  selected.schemaFreshness === 'quarantined'
221
- ? `/mcp quarantine ${selected.name} approve operator`
224
+ ? `/mcp quarantine ${selected.name} approve operator --yes`
222
225
  : null,
223
226
  !selected.connected ? '/services auth-review' : null,
224
227
  '/mcp review',
@@ -239,14 +242,18 @@ export function registerMcpRuntimeCommands(registry: CommandRegistry): void {
239
242
  }
240
243
 
241
244
  if (subcommand === 'trust') {
242
- const serverName = args[1];
243
- const mode = args[2] as 'constrained' | 'ask-on-risk' | 'allow-all' | 'blocked' | undefined;
245
+ const serverName = commandArgs[1];
246
+ const mode = commandArgs[2] as 'constrained' | 'ask-on-risk' | 'allow-all' | 'blocked' | undefined;
244
247
  if (serverName && mode) {
245
248
  if (mode === 'allow-all') {
246
249
  ctx.print(`Use /settings → MCP to explicitly enable allow-all for ${serverName}. Direct CLI escalation is blocked.`);
247
250
  ctx.openSettingsModal?.();
248
251
  return;
249
252
  }
253
+ if (!confirmation.yes) {
254
+ requireYesFlag(ctx, `change MCP trust mode for ${serverName}`, '/mcp trust <server> <constrained|ask-on-risk|blocked> --yes');
255
+ return;
256
+ }
250
257
  mcpApi.setServerTrustMode(serverName, mode);
251
258
  ctx.print(`Updated MCP trust mode for ${serverName} to ${mode}.`);
252
259
  return;
@@ -258,9 +265,13 @@ export function registerMcpRuntimeCommands(registry: CommandRegistry): void {
258
265
  }
259
266
 
260
267
  if (subcommand === 'role') {
261
- const serverName = args[1];
262
- const role = args[2] as 'general' | 'docs' | 'filesystem' | 'git' | 'database' | 'browser' | 'automation' | 'ops' | 'remote' | undefined;
268
+ const serverName = commandArgs[1];
269
+ const role = commandArgs[2] as 'general' | 'docs' | 'filesystem' | 'git' | 'database' | 'browser' | 'automation' | 'ops' | 'remote' | undefined;
263
270
  if (serverName && role) {
271
+ if (!confirmation.yes) {
272
+ requireYesFlag(ctx, `change MCP role for ${serverName}`, '/mcp role <server> <general|docs|filesystem|git|database|browser|automation|ops|remote> --yes');
273
+ return;
274
+ }
264
275
  mcpApi.setServerRole(serverName, role);
265
276
  ctx.print(`Updated MCP role for ${serverName} to ${role}.`);
266
277
  return;
@@ -272,21 +283,25 @@ export function registerMcpRuntimeCommands(registry: CommandRegistry): void {
272
283
  }
273
284
 
274
285
  if (subcommand === 'add') {
275
- let parsed: ParsedMcpAddArgs;
286
+ if (!confirmation.yes) {
287
+ requireYesFlag(ctx, 'add or update an MCP server config', '/mcp add <name> <command> [args...] [--scope project|global] [--role <role>] [--trust <mode>] --yes');
288
+ return;
289
+ }
290
+ let parsedAdd: ParsedMcpAddArgs;
276
291
  try {
277
- parsed = parseAddServerArgs(args);
292
+ parsedAdd = parseAddServerArgs(commandArgs);
278
293
  } catch (error) {
279
294
  ctx.print(summarizeError(error));
280
295
  return;
281
296
  }
282
297
  const shellPaths = requireShellPaths(ctx);
283
298
  try {
284
- const result = await mcpApi.upsertServerConfig(shellPaths, parsed.scope, parsed.server);
285
- const connected = listServerSecurity().find((entry) => entry.name === parsed.server.name)?.connected ?? false;
299
+ const result = await mcpApi.upsertServerConfig(shellPaths, parsedAdd.scope, parsedAdd.server);
300
+ const connected = listServerSecurity().find((entry) => entry.name === parsedAdd.server.name)?.connected ?? false;
286
301
  ctx.print([
287
- `MCP server "${parsed.server.name}" saved to ${parsed.scope} config: ${result.path}.`,
302
+ `MCP server "${parsedAdd.server.name}" saved to ${parsedAdd.scope} config: ${result.path}.`,
288
303
  `Runtime reload: ${connected ? 'connected' : 'server saved; connection needs attention'} (+${result.reload.added} ~${result.reload.changed} -${result.reload.removed}, unchanged ${result.reload.unchanged}).`,
289
- `Command: ${parsed.server.command}${parsed.server.args?.length ? ` ${parsed.server.args.join(' ')}` : ''}`,
304
+ `Command: ${parsedAdd.server.command}${parsedAdd.server.args?.length ? ` ${parsedAdd.server.args.join(' ')}` : ''}`,
290
305
  'Next: /mcp tools',
291
306
  ].join('\n'));
292
307
  } catch (error) {
@@ -296,16 +311,20 @@ export function registerMcpRuntimeCommands(registry: CommandRegistry): void {
296
311
  }
297
312
 
298
313
  if (subcommand === 'remove') {
299
- const serverName = args[1]?.trim();
314
+ const serverName = commandArgs[1]?.trim();
300
315
  if (!serverName) {
301
- ctx.print('Usage: /mcp remove <server> [--scope project|global]');
316
+ ctx.print('Usage: /mcp remove <server> [--scope project|global] --yes');
317
+ return;
318
+ }
319
+ if (!confirmation.yes) {
320
+ requireYesFlag(ctx, `remove MCP server ${serverName}`, '/mcp remove <server> [--scope project|global] --yes');
302
321
  return;
303
322
  }
304
323
  let scope: McpConfigScope = 'project';
305
324
  try {
306
- for (let index = 2; index < args.length; index += 1) {
307
- if (args[index] === '--scope') {
308
- const value = readFlagValue(args, index, '--scope');
325
+ for (let index = 2; index < commandArgs.length; index += 1) {
326
+ if (commandArgs[index] === '--scope') {
327
+ const value = readFlagValue(commandArgs, index, '--scope');
309
328
  if (!isMcpScope(value)) {
310
329
  ctx.print(`Invalid MCP scope "${value}". Expected project or global.`);
311
330
  return;
@@ -331,6 +350,10 @@ export function registerMcpRuntimeCommands(registry: CommandRegistry): void {
331
350
  }
332
351
 
333
352
  if (subcommand === 'reload') {
353
+ if (!confirmation.yes) {
354
+ requireYesFlag(ctx, 'reload the MCP runtime from config', '/mcp reload --yes');
355
+ return;
356
+ }
334
357
  try {
335
358
  const result = await reloadMcpRuntime(ctx);
336
359
  const servers = listServerSecurity();
@@ -356,10 +379,10 @@ export function registerMcpRuntimeCommands(registry: CommandRegistry): void {
356
379
  return ` - ${server.name}: ${server.command}${server.args?.length ? ` ${server.args.join(' ')}` : ''} source=${entry.source.scope}/${entry.source.kind}${envKeys.length ? ` envKeys=${envKeys.join(',')}` : ''}`;
357
380
  }),
358
381
  '',
359
- 'Add or update from inside the TUI:',
360
- ' /mcp add <name> <command> [args...] [--scope project|global] [--role <role>] [--trust <mode>]',
382
+ 'Add or update from inside Agent with explicit confirmation:',
383
+ ' /mcp add <name> <command> [args...] [--scope project|global] [--role <role>] [--trust <mode>] --yes',
361
384
  'Example:',
362
- ' /mcp add filesystem npx -y @modelcontextprotocol/server-filesystem . --scope project --role filesystem --trust constrained',
385
+ ' /mcp add filesystem npx -y @modelcontextprotocol/server-filesystem . --scope project --role filesystem --trust constrained --yes',
363
386
  ].join('\n'));
364
387
  } catch (error) {
365
388
  ctx.print(`MCP config read failed: ${summarizeError(error)}`);
@@ -368,19 +391,27 @@ export function registerMcpRuntimeCommands(registry: CommandRegistry): void {
368
391
  }
369
392
 
370
393
  if (subcommand === 'quarantine') {
371
- const serverName = args[1];
372
- const action = args[2];
394
+ const serverName = commandArgs[1];
395
+ const action = commandArgs[2];
373
396
  if (!serverName) {
374
- ctx.print('Usage: /mcp quarantine <server> [detail]\n /mcp quarantine <server> approve [operatorId]');
397
+ ctx.print('Usage: /mcp quarantine <server> [detail] --yes\n /mcp quarantine <server> approve [operatorId] --yes');
375
398
  return;
376
399
  }
377
400
  if (action === 'approve') {
378
- const operatorId = args[3] || 'operator';
401
+ if (!confirmation.yes) {
402
+ requireYesFlag(ctx, `approve MCP schema quarantine override for ${serverName}`, '/mcp quarantine <server> approve [operatorId] --yes');
403
+ return;
404
+ }
405
+ const operatorId = commandArgs[3] || 'operator';
379
406
  mcpApi.approveSchemaQuarantine(serverName, operatorId);
380
407
  ctx.print(`Approved MCP schema quarantine override for ${serverName} as ${operatorId}. Refresh is still recommended.`);
381
408
  return;
382
409
  }
383
- const detail = args.slice(2).join(' ') || 'quarantined by operator';
410
+ if (!confirmation.yes) {
411
+ requireYesFlag(ctx, `quarantine MCP server ${serverName}`, '/mcp quarantine <server> [detail] --yes');
412
+ return;
413
+ }
414
+ const detail = commandArgs.slice(2).join(' ') || 'quarantined by operator';
384
415
  mcpApi.quarantineSchema(serverName, 'operator_flagged', detail);
385
416
  ctx.print(`Quarantined MCP schema for ${serverName}.\nReason: ${detail}`);
386
417
  return;
@@ -396,8 +427,8 @@ export function registerMcpRuntimeCommands(registry: CommandRegistry): void {
396
427
  + ' ~/.config/claude/claude_desktop_config.json (Claude Desktop)\n'
397
428
  + ' .mcp/mcp.json (project-local)\n'
398
429
  + ' .goodvibes/mcp.json (goodvibes project)\n'
399
- + '\nAdd one from inside the TUI:\n'
400
- + ' /mcp add filesystem npx -y @modelcontextprotocol/server-filesystem . --scope project --role filesystem\n'
430
+ + '\nAdd one from inside Agent with explicit confirmation:\n'
431
+ + ' /mcp add filesystem npx -y @modelcontextprotocol/server-filesystem . --scope project --role filesystem --yes\n'
401
432
  + '\nFormat: { "servers": [{ "name": "my-server", "command": "npx", "args": ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"] }] }'
402
433
  );
403
434
  return;
@@ -416,10 +447,10 @@ export function registerMcpRuntimeCommands(registry: CommandRegistry): void {
416
447
  if (connected.length > 0) {
417
448
  lines.push('');
418
449
  lines.push('Run "/mcp tools" to list all tools, or "/mcp tools <server>" for a specific server.');
419
- lines.push('Run "/mcp" to open the fullscreen MCP workspace, or "/mcp add <name> <command> [args...] [--scope project|global]" to add/update without restarting.');
420
- lines.push('Run "/mcp reload" after editing MCP config outside the TUI.');
421
- lines.push('Run "/mcp trust <server> <mode>" to change trust mode, or "/mcp role <server> <role>" to change its coherence role.');
422
- lines.push('Run "/mcp quarantine <server> [detail]" to block a server, or "/mcp quarantine <server> approve [operatorId]" to approve a temporary override.');
450
+ lines.push('Run "/mcp" to open the fullscreen MCP workspace, or "/mcp add <name> <command> [args...] [--scope project|global] --yes" to add/update.');
451
+ lines.push('Run "/mcp reload --yes" after editing MCP config outside Agent.');
452
+ lines.push('Run "/mcp trust <server> <mode> --yes" to change trust mode, or "/mcp role <server> <role> --yes" to change its coherence role.');
453
+ lines.push('Run "/mcp quarantine <server> [detail] --yes" to block a server, or "/mcp quarantine <server> approve [operatorId] --yes" to approve a temporary override.');
423
454
  lines.push('Use /settings → MCP to explicitly enable allow-all for a server.');
424
455
  }
425
456
  if (disconnected.length > 0) {
@@ -1,111 +1,148 @@
1
1
  import type { CommandRegistry } from '../command-registry.ts';
2
+ import { requireYesFlag, stripYesFlag } from './confirmation.ts';
2
3
 
3
4
  export function registerMemoryProductRuntimeCommands(registry: CommandRegistry): void {
4
5
  registry.register({
5
6
  name: 'memory-sync',
6
7
  aliases: ['memsync'],
7
8
  description: 'Dedicated front-door for durable memory export/import and bundle exchange',
8
- usage: '[export <path> [scope] | import <path>]',
9
+ usage: '[export <path> [scope] --yes | import <path> --yes]',
9
10
  async handler(args, ctx) {
10
- const sub = (args[0] ?? '').toLowerCase();
11
+ const parsed = stripYesFlag(args);
12
+ const commandArgs = [...parsed.rest];
13
+ const sub = (commandArgs[0] ?? '').toLowerCase();
11
14
  if (!ctx.executeCommand) {
12
15
  ctx.print('Memory sync controls are not available in this runtime.');
13
16
  return;
14
17
  }
15
- if (sub === 'export' && args[1]) {
16
- const scope = args[2];
17
- const recallArgs = ['export', args[1], ...(scope ? ['--scope', scope] : [])];
18
+ if (sub === 'export' && commandArgs[1]) {
19
+ if (!parsed.yes) {
20
+ requireYesFlag(ctx, `export durable memory bundle to ${commandArgs[1]}`, '/memory-sync export <path> [scope] --yes');
21
+ return;
22
+ }
23
+ const scope = commandArgs[2];
24
+ const recallArgs = ['export', commandArgs[1], ...(scope ? ['--scope', scope] : []), '--yes'];
18
25
  await ctx.executeCommand('recall', recallArgs);
19
26
  return;
20
27
  }
21
- if (sub === 'import' && args[1]) {
22
- await ctx.executeCommand('recall', ['import', args[1]]);
28
+ if (sub === 'import' && commandArgs[1]) {
29
+ if (!parsed.yes) {
30
+ requireYesFlag(ctx, `import durable memory bundle from ${commandArgs[1]}`, '/memory-sync import <path> --yes');
31
+ return;
32
+ }
33
+ await ctx.executeCommand('recall', ['import', commandArgs[1], '--yes']);
23
34
  return;
24
35
  }
25
- ctx.print('Usage: /memory-sync [export <path> [scope] | import <path>]');
36
+ ctx.print('Usage: /memory-sync [export <path> [scope] --yes | import <path> --yes]');
26
37
  },
27
38
  });
28
39
 
29
40
  registry.register({
30
41
  name: 'handoff',
31
42
  description: 'Dedicated front-door for reviewable memory handoff bundles',
32
- usage: '[export <path> [scope] | inspect <path> | import <path>]',
43
+ usage: '[export <path> [scope] --yes | inspect <path> | import <path> --yes]',
33
44
  async handler(args, ctx) {
34
- const sub = (args[0] ?? '').toLowerCase();
45
+ const parsed = stripYesFlag(args);
46
+ const commandArgs = [...parsed.rest];
47
+ const sub = (commandArgs[0] ?? '').toLowerCase();
35
48
  if (!ctx.executeCommand) {
36
49
  ctx.print('Handoff controls are not available in this runtime.');
37
50
  return;
38
51
  }
39
- if (sub === 'export' && args[1]) {
40
- const scope = args[2];
41
- await ctx.executeCommand('recall', ['handoff-export', args[1], ...(scope ? ['--scope', scope] : [])]);
52
+ if (sub === 'export' && commandArgs[1]) {
53
+ if (!parsed.yes) {
54
+ requireYesFlag(ctx, `export memory handoff bundle to ${commandArgs[1]}`, '/handoff export <path> [scope] --yes');
55
+ return;
56
+ }
57
+ const scope = commandArgs[2];
58
+ await ctx.executeCommand('recall', ['handoff-export', commandArgs[1], ...(scope ? ['--scope', scope] : []), '--yes']);
42
59
  return;
43
60
  }
44
- if (sub === 'inspect' && args[1]) {
45
- await ctx.executeCommand('recall', ['handoff-inspect', args[1]]);
61
+ if (sub === 'inspect' && commandArgs[1]) {
62
+ await ctx.executeCommand('recall', ['handoff-inspect', commandArgs[1]]);
46
63
  return;
47
64
  }
48
- if (sub === 'import' && args[1]) {
49
- await ctx.executeCommand('recall', ['handoff-import', args[1]]);
65
+ if (sub === 'import' && commandArgs[1]) {
66
+ if (!parsed.yes) {
67
+ requireYesFlag(ctx, `import memory handoff bundle from ${commandArgs[1]}`, '/handoff import <path> --yes');
68
+ return;
69
+ }
70
+ await ctx.executeCommand('recall', ['handoff-import', commandArgs[1], '--yes']);
50
71
  return;
51
72
  }
52
- ctx.print('Usage: /handoff [export <path> [scope] | inspect <path> | import <path>]');
73
+ ctx.print('Usage: /handoff [export <path> [scope] --yes | inspect <path> | import <path> --yes]');
53
74
  },
54
75
  });
55
76
 
56
77
  registry.register({
57
78
  name: 'session-memory',
58
79
  description: 'Dedicated front-door for session-scoped memory capture and review',
59
- usage: '[queue [limit] | export <path> | add <class> <summary...>]',
80
+ usage: '[queue [limit] | export <path> --yes | add <class> <summary...>]',
60
81
  async handler(args, ctx) {
61
- const sub = (args[0] ?? 'queue').toLowerCase();
82
+ const parsed = stripYesFlag(args);
83
+ const commandArgs = [...parsed.rest];
84
+ const sub = (commandArgs[0] ?? 'queue').toLowerCase();
62
85
  if (!ctx.executeCommand) {
63
86
  ctx.print('Session memory controls are not available in this runtime.');
64
87
  return;
65
88
  }
66
89
  if (sub === 'queue') {
67
- await ctx.executeCommand('recall', ['queue', ...(args[1] ? [args[1]] : [])]);
90
+ await ctx.executeCommand('recall', ['queue', ...(commandArgs[1] ? [commandArgs[1]] : [])]);
68
91
  return;
69
92
  }
70
- if (sub === 'export' && args[1]) {
71
- await ctx.executeCommand('recall', ['export', args[1], '--scope', 'session']);
93
+ if (sub === 'export' && commandArgs[1]) {
94
+ if (!parsed.yes) {
95
+ requireYesFlag(ctx, `export session memory bundle to ${commandArgs[1]}`, '/session-memory export <path> --yes');
96
+ return;
97
+ }
98
+ await ctx.executeCommand('recall', ['export', commandArgs[1], '--scope', 'session', '--yes']);
72
99
  return;
73
100
  }
74
- if (sub === 'add' && args.length >= 3) {
75
- await ctx.executeCommand('recall', ['add', args[1], ...args.slice(2), '--scope', 'session']);
101
+ if (sub === 'add' && commandArgs.length >= 3) {
102
+ await ctx.executeCommand('recall', ['add', commandArgs[1], ...commandArgs.slice(2), '--scope', 'session']);
76
103
  return;
77
104
  }
78
- ctx.print('Usage: /session-memory [queue [limit] | export <path> | add <class> <summary...>]');
105
+ ctx.print('Usage: /session-memory [queue [limit] | export <path> --yes | add <class> <summary...>]');
79
106
  },
80
107
  });
81
108
 
82
109
  registry.register({
83
110
  name: 'team-memory',
84
111
  description: 'Dedicated front-door for team/shared memory review and exchange',
85
- usage: '[queue [limit] | export <path> | import <path> | capture policy]',
112
+ usage: '[queue [limit] | export <path> --yes | import <path> --yes | capture policy]',
86
113
  async handler(args, ctx) {
87
- const sub = (args[0] ?? 'queue').toLowerCase();
114
+ const parsed = stripYesFlag(args);
115
+ const commandArgs = [...parsed.rest];
116
+ const sub = (commandArgs[0] ?? 'queue').toLowerCase();
88
117
  if (!ctx.executeCommand) {
89
118
  ctx.print('Team memory controls are not available in this runtime.');
90
119
  return;
91
120
  }
92
121
  if (sub === 'queue') {
93
- await ctx.executeCommand('recall', ['queue', ...(args[1] ? [args[1]] : [])]);
122
+ await ctx.executeCommand('recall', ['queue', ...(commandArgs[1] ? [commandArgs[1]] : [])]);
94
123
  return;
95
124
  }
96
- if (sub === 'export' && args[1]) {
97
- await ctx.executeCommand('recall', ['handoff-export', args[1], '--scope', 'team']);
125
+ if (sub === 'export' && commandArgs[1]) {
126
+ if (!parsed.yes) {
127
+ requireYesFlag(ctx, `export team memory handoff bundle to ${commandArgs[1]}`, '/team-memory export <path> --yes');
128
+ return;
129
+ }
130
+ await ctx.executeCommand('recall', ['handoff-export', commandArgs[1], '--scope', 'team', '--yes']);
98
131
  return;
99
132
  }
100
- if (sub === 'import' && args[1]) {
101
- await ctx.executeCommand('recall', ['handoff-import', args[1]]);
133
+ if (sub === 'import' && commandArgs[1]) {
134
+ if (!parsed.yes) {
135
+ requireYesFlag(ctx, `import team memory handoff bundle from ${commandArgs[1]}`, '/team-memory import <path> --yes');
136
+ return;
137
+ }
138
+ await ctx.executeCommand('recall', ['handoff-import', commandArgs[1], '--yes']);
102
139
  return;
103
140
  }
104
- if (sub === 'capture' && args[1]?.toLowerCase() === 'policy') {
141
+ if (sub === 'capture' && commandArgs[1]?.toLowerCase() === 'policy') {
105
142
  await ctx.executeCommand('recall', ['capture', 'policy']);
106
143
  return;
107
144
  }
108
- ctx.print('Usage: /team-memory [queue [limit] | export <path> | import <path> | capture policy]');
145
+ ctx.print('Usage: /team-memory [queue [limit] | export <path> --yes | import <path> --yes | capture policy]');
109
146
  },
110
147
  });
111
148
  }
@@ -7,10 +7,10 @@
7
7
  * /recall add <class> <summary> --detail <text> --tags <tag,tag>
8
8
  * /recall search [query] — Search memory records
9
9
  * /recall search --cls <class> — Filter by class
10
- * /recall link <fromId> <toId> <relation> — Link two records
10
+ * /recall link <fromId> <toId> <relation> --yes — Link two records
11
11
  * /recall get <id> — Show a single record with provenance
12
12
  * /recall list [class] — List all records (optionally by class)
13
- * /recall remove <id> — Delete a record
13
+ * /recall remove <id> --yes — Delete a record
14
14
  */
15
15
 
16
16
  import type { SlashCommand, CommandContext } from '../command-registry.ts';
@@ -128,20 +128,20 @@ export const recallCommand: SlashCommand = {
128
128
  ' search [query] [--semantic] [--cls <class>] [--scope <scope>] [--limit <n>] — Full-text or sqlite-vec semantic search',
129
129
  ' vector [status|doctor|rebuild] — Inspect or rebuild the sqlite-vec memory index',
130
130
  ' get <id> — Show record with provenance + links',
131
- ' link <fromId> <toId> <relation> — Create a directed relation between records',
131
+ ' link <fromId> <toId> <relation> --yes — Create a directed relation between records',
132
132
  ' queue [limit] — Show the operator review queue',
133
133
  ' review <id> <state> [--confidence <n>] [--by <name>] [--reason <text>]',
134
134
  ' stale <id> [reason...] — Mark a record stale with an operator reason',
135
135
  ' contradict <id> [reason...] — Mark a record contradicted with an operator reason',
136
136
  ' explain <task...> [--scope <path> ...] — Show the knowledge records that would be injected for a task',
137
- ' promote <id> <scope> — Promote a memory record into session|project|team scope',
138
- ' export <path> [--scope <scope>] [--cls <class>] — Export a durable knowledge bundle',
139
- ' import <path> — Import a durable knowledge bundle',
140
- ' handoff-export <path> [--scope <scope>] — Export a reviewable handoff bundle for team/shared use',
137
+ ' promote <id> <scope> --yes — Promote a memory record into session|project|team scope',
138
+ ' export <path> [--scope <scope>] [--cls <class>] --yes — Export a durable knowledge bundle',
139
+ ' import <path> --yes — Import a durable knowledge bundle',
140
+ ' handoff-export <path> [--scope <scope>] --yes — Export a reviewable handoff bundle for team/shared use',
141
141
  ' handoff-inspect <path> — Inspect a handoff bundle before import',
142
- ' handoff-import <path> — Import a handoff bundle into durable memory',
142
+ ' handoff-import <path> --yes — Import a handoff bundle into durable memory',
143
143
  ' list [class] [--scope <scope>] — List all records grouped by class',
144
- ' remove <id> — Delete a record',
144
+ ' remove <id> --yes — Delete a record',
145
145
  ].join('\n');
146
146
  context.print(usage);
147
147
  break;
@@ -1,18 +1,21 @@
1
1
  import type { CommandRegistry } from '../command-registry.ts';
2
2
  import { requireWebhookNotifier } from './runtime-services.ts';
3
+ import { requireYesFlag, stripYesFlag } from './confirmation.ts';
3
4
 
4
5
  export function registerNotifyRuntimeCommands(registry: CommandRegistry): void {
5
6
  registry.register({
6
7
  name: 'notify',
7
8
  aliases: [],
8
9
  description: 'Manage webhook notification URLs (ntfy.sh format)',
9
- usage: 'add <url> | remove <url> | list | clear | test',
10
- argsHint: 'add|remove|list|clear|test',
10
+ usage: 'add <url> --yes | remove <url> --yes | list | clear --yes | test --yes',
11
+ argsHint: 'list|add --yes|remove --yes|test --yes',
11
12
  async handler(args, ctx) {
13
+ const parsed = stripYesFlag(args);
14
+ const commandArgs = [...parsed.rest];
12
15
  const notifications = ctx.platform.configManager.getCategory('notifications');
13
16
  const urls: string[] = Array.isArray(notifications.webhookUrls) ? [...notifications.webhookUrls] : [];
14
17
  const notifier = requireWebhookNotifier(ctx);
15
- const sub = args[0];
18
+ const sub = commandArgs[0];
16
19
 
17
20
  if (!sub || sub === 'list') {
18
21
  if (urls.length === 0) ctx.print('No webhook URLs configured.\nUse: /notify add <url>');
@@ -21,9 +24,13 @@ export function registerNotifyRuntimeCommands(registry: CommandRegistry): void {
21
24
  }
22
25
 
23
26
  if (sub === 'add') {
24
- const url = args[1];
27
+ const url = commandArgs[1];
25
28
  if (!url) {
26
- ctx.print('Usage: /notify add <url>\nExample: /notify add https://ntfy.sh/my-topic');
29
+ ctx.print('Usage: /notify add <url> --yes\nExample: /notify add https://ntfy.sh/my-topic --yes');
30
+ return;
31
+ }
32
+ if (!parsed.yes) {
33
+ requireYesFlag(ctx, `add webhook notification URL ${url}`, '/notify add <url> --yes');
27
34
  return;
28
35
  }
29
36
  try { new URL(url); } catch {
@@ -42,9 +49,13 @@ export function registerNotifyRuntimeCommands(registry: CommandRegistry): void {
42
49
  }
43
50
 
44
51
  if (sub === 'remove') {
45
- const url = args[1];
52
+ const url = commandArgs[1];
46
53
  if (!url) {
47
- ctx.print('Usage: /notify remove <url>');
54
+ ctx.print('Usage: /notify remove <url> --yes');
55
+ return;
56
+ }
57
+ if (!parsed.yes) {
58
+ requireYesFlag(ctx, `remove webhook notification URL ${url}`, '/notify remove <url> --yes');
48
59
  return;
49
60
  }
50
61
  const next = urls.filter((u) => u !== url);
@@ -59,6 +70,10 @@ export function registerNotifyRuntimeCommands(registry: CommandRegistry): void {
59
70
  }
60
71
 
61
72
  if (sub === 'clear') {
73
+ if (!parsed.yes) {
74
+ requireYesFlag(ctx, 'clear all webhook notification URLs', '/notify clear --yes');
75
+ return;
76
+ }
62
77
  ctx.platform.configManager.mergeCategory('notifications', { webhookUrls: [] });
63
78
  notifier.setUrls([]);
64
79
  ctx.print('All webhook URLs cleared.');
@@ -66,6 +81,10 @@ export function registerNotifyRuntimeCommands(registry: CommandRegistry): void {
66
81
  }
67
82
 
68
83
  if (sub === 'test') {
84
+ if (!parsed.yes) {
85
+ requireYesFlag(ctx, 'send webhook notification test requests', '/notify test --yes');
86
+ return;
87
+ }
69
88
  if (urls.length === 0) {
70
89
  ctx.print('No webhook URLs configured. Use: /notify add <url>');
71
90
  return;
@@ -77,7 +96,7 @@ export function registerNotifyRuntimeCommands(registry: CommandRegistry): void {
77
96
  return;
78
97
  }
79
98
 
80
- ctx.print('Usage: /notify add <url> | remove <url> | list | clear | test');
99
+ ctx.print('Usage: /notify add <url> --yes | remove <url> --yes | list | clear --yes | test --yes');
81
100
  },
82
101
  });
83
102
  }