@pellux/goodvibes-agent 0.1.10 → 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 (68) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/package.json +1 -1
  3. package/src/cli/agent-knowledge-command.ts +30 -3
  4. package/src/cli/help.ts +2 -2
  5. package/src/input/commands/cloudflare-runtime.ts +20 -5
  6. package/src/input/commands/confirmation.ts +24 -0
  7. package/src/input/commands/discovery-runtime.ts +16 -7
  8. package/src/input/commands/eval.ts +27 -14
  9. package/src/input/commands/experience-runtime.ts +65 -26
  10. package/src/input/commands/health-runtime.ts +1 -1
  11. package/src/input/commands/hooks-runtime.ts +50 -19
  12. package/src/input/commands/incident-runtime.ts +17 -6
  13. package/src/input/commands/integration-runtime.ts +93 -50
  14. package/src/input/commands/knowledge.ts +38 -12
  15. package/src/input/commands/local-auth-runtime.ts +36 -13
  16. package/src/input/commands/local-provider-runtime.ts +22 -11
  17. package/src/input/commands/local-runtime.ts +21 -11
  18. package/src/input/commands/local-setup.ts +35 -16
  19. package/src/input/commands/managed-runtime.ts +51 -20
  20. package/src/input/commands/marketplace-runtime.ts +31 -16
  21. package/src/input/commands/mcp-runtime.ts +65 -34
  22. package/src/input/commands/memory-product-runtime.ts +72 -35
  23. package/src/input/commands/memory.ts +9 -9
  24. package/src/input/commands/notify-runtime.ts +27 -8
  25. package/src/input/commands/operator-runtime.ts +85 -17
  26. package/src/input/commands/planning-runtime.ts +14 -2
  27. package/src/input/commands/platform-access-runtime.ts +88 -45
  28. package/src/input/commands/platform-services-runtime.ts +51 -25
  29. package/src/input/commands/product-runtime.ts +54 -27
  30. package/src/input/commands/profile-sync-runtime.ts +17 -6
  31. package/src/input/commands/recall-bundle.ts +38 -17
  32. package/src/input/commands/recall-query.ts +15 -4
  33. package/src/input/commands/recall-review.ts +9 -3
  34. package/src/input/commands/remote-runtime-setup.ts +45 -18
  35. package/src/input/commands/remote-runtime.ts +25 -9
  36. package/src/input/commands/replay-runtime.ts +9 -2
  37. package/src/input/commands/services-runtime.ts +21 -10
  38. package/src/input/commands/session-content.ts +53 -51
  39. package/src/input/commands/session-workflow.ts +10 -4
  40. package/src/input/commands/session.ts +1 -1
  41. package/src/input/commands/settings-sync-runtime.ts +40 -17
  42. package/src/input/commands/share-runtime.ts +12 -4
  43. package/src/input/commands/shell-core.ts +3 -3
  44. package/src/input/commands/subscription-runtime.ts +35 -20
  45. package/src/input/commands/teleport-runtime.ts +16 -5
  46. package/src/input/commands/work-plan-runtime.ts +23 -12
  47. package/src/input/handler-content-actions.ts +11 -62
  48. package/src/input/handler-interactions.ts +1 -1
  49. package/src/input/handler-onboarding-cloudflare.ts +48 -117
  50. package/src/input/keybindings.ts +1 -1
  51. package/src/input/mcp-workspace.ts +25 -49
  52. package/src/input/onboarding/onboarding-wizard-cloudflare-step.ts +8 -8
  53. package/src/input/onboarding/onboarding-wizard-cloudflare.ts +1 -6
  54. package/src/input/profile-picker-modal.ts +13 -31
  55. package/src/input/session-picker-modal.ts +4 -30
  56. package/src/input/settings-modal-subscriptions.ts +3 -3
  57. package/src/panels/incident-review-panel.ts +1 -1
  58. package/src/panels/local-auth-panel.ts +4 -4
  59. package/src/panels/provider-account-snapshot.ts +1 -1
  60. package/src/panels/provider-health-domains.ts +2 -2
  61. package/src/panels/settings-sync-panel.ts +2 -2
  62. package/src/panels/subscription-panel.ts +7 -7
  63. package/src/renderer/block-actions.ts +1 -1
  64. package/src/renderer/help-overlay.ts +2 -2
  65. package/src/renderer/mcp-workspace.ts +12 -12
  66. package/src/renderer/profile-picker-modal.ts +3 -11
  67. package/src/renderer/session-picker-modal.ts +2 -10
  68. package/src/version.ts +1 -1
@@ -20,6 +20,7 @@ import {
20
20
  } from '@/runtime/index.ts';
21
21
  import { requireProfileManager, requireShellPaths } from './runtime-services.ts';
22
22
  import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
23
+ import { requireYesFlag, stripYesFlag } from './confirmation.ts';
23
24
 
24
25
  function buildConfigSnapshot(
25
26
  manager: { get: (key: ConfigKey) => unknown },
@@ -39,11 +40,13 @@ export function registerManagedRuntimeCommands(registry: CommandRegistry): void
39
40
  registry.register({
40
41
  name: 'managed',
41
42
  description: 'Export, inspect, and apply managed settings bundles',
42
- usage: '[review|staged|rollback-history|export <profile> <path>|inspect <path>|stage <path>|apply <path> [key ...]|apply-staged [key ...]|rollback <token>|lock <key> <source> <reason...>|unlock <key>]',
43
+ usage: '[review|staged|rollback-history|export <profile> <path> --yes|inspect <path>|stage <path> --yes|apply <path> [key ...] --yes|apply-staged [key ...] --yes|rollback <token> --yes|lock <key> <source> <reason...> --yes|unlock <key> --yes]',
43
44
  handler(args, ctx) {
45
+ const parsed = stripYesFlag(args);
46
+ const commandArgs = [...parsed.rest];
44
47
  const shellPaths = requireShellPaths(ctx);
45
48
  const controlPlaneConfigDir = ctx.platform.configManager.getControlPlaneConfigDir();
46
- const sub = args[0] ?? 'review';
49
+ const sub = commandArgs[0] ?? 'review';
47
50
  const pm = requireProfileManager(ctx);
48
51
  if (sub === 'review') {
49
52
  const profiles = pm.list();
@@ -78,11 +81,15 @@ export function registerManagedRuntimeCommands(registry: CommandRegistry): void
78
81
  }
79
82
 
80
83
  if (sub === 'lock') {
81
- const key = args[1] as ConfigKey | undefined;
82
- const source = args[2];
83
- const reason = args.slice(3).join(' ').trim();
84
+ const key = commandArgs[1] as ConfigKey | undefined;
85
+ const source = commandArgs[2];
86
+ const reason = commandArgs.slice(3).join(' ').trim();
84
87
  if (!key || !source || !reason) {
85
- ctx.print('Usage: /managed lock <key> <source> <reason...>');
88
+ ctx.print('Usage: /managed lock <key> <source> <reason...> --yes');
89
+ return;
90
+ }
91
+ if (!parsed.yes) {
92
+ requireYesFlag(ctx, `lock managed setting ${key}`, '/managed lock <key> <source> <reason...> --yes');
86
93
  return;
87
94
  }
88
95
  setManagedSettingLock(key, source, reason, controlPlaneConfigDir);
@@ -91,9 +98,13 @@ export function registerManagedRuntimeCommands(registry: CommandRegistry): void
91
98
  }
92
99
 
93
100
  if (sub === 'unlock') {
94
- const key = args[1] as ConfigKey | undefined;
101
+ const key = commandArgs[1] as ConfigKey | undefined;
95
102
  if (!key) {
96
- ctx.print('Usage: /managed unlock <key>');
103
+ ctx.print('Usage: /managed unlock <key> --yes');
104
+ return;
105
+ }
106
+ if (!parsed.yes) {
107
+ requireYesFlag(ctx, `unlock managed setting ${key}`, '/managed unlock <key> --yes');
97
108
  return;
98
109
  }
99
110
  ctx.print(clearManagedSettingLock(key, controlPlaneConfigDir) ? `Managed lock cleared for ${key}.` : `No managed lock found for ${key}.`);
@@ -101,10 +112,14 @@ export function registerManagedRuntimeCommands(registry: CommandRegistry): void
101
112
  }
102
113
 
103
114
  if (sub === 'export') {
104
- const profileName = args[1];
105
- const pathArg = args[2];
115
+ const profileName = commandArgs[1];
116
+ const pathArg = commandArgs[2];
106
117
  if (!profileName || !pathArg) {
107
- ctx.print('Usage: /managed export <profile> <path>');
118
+ ctx.print('Usage: /managed export <profile> <path> --yes');
119
+ return;
120
+ }
121
+ if (!parsed.yes) {
122
+ requireYesFlag(ctx, `export managed settings profile ${profileName} to ${pathArg}`, '/managed export <profile> <path> --yes');
108
123
  return;
109
124
  }
110
125
  const loaded = pm.load(profileName);
@@ -129,12 +144,16 @@ export function registerManagedRuntimeCommands(registry: CommandRegistry): void
129
144
  }
130
145
 
131
146
  if (sub === 'apply-staged') {
132
- const requestedKeys = args.slice(1).filter((value): value is ConfigKey => CONFIG_KEYS.has(value as ConfigKey));
133
- const invalidKeys = args.slice(1).filter((value) => !CONFIG_KEYS.has(value as ConfigKey));
147
+ const requestedKeys = commandArgs.slice(1).filter((value): value is ConfigKey => CONFIG_KEYS.has(value as ConfigKey));
148
+ const invalidKeys = commandArgs.slice(1).filter((value) => !CONFIG_KEYS.has(value as ConfigKey));
134
149
  if (invalidKeys.length > 0) {
135
150
  ctx.print(`Unknown config key(s): ${invalidKeys.join(', ')}`);
136
151
  return;
137
152
  }
153
+ if (!parsed.yes) {
154
+ requireYesFlag(ctx, 'apply staged managed settings', '/managed apply-staged [key ...] --yes');
155
+ return;
156
+ }
138
157
  try {
139
158
  const result = applyStagedManagedBundle(ctx.platform.configManager, requestedKeys);
140
159
  ctx.session.runtime.model = String(ctx.platform.configManager.get('provider.model'));
@@ -149,9 +168,13 @@ export function registerManagedRuntimeCommands(registry: CommandRegistry): void
149
168
  }
150
169
 
151
170
  if (sub === 'rollback') {
152
- const token = args[1];
171
+ const token = commandArgs[1];
153
172
  if (!token) {
154
- ctx.print('Usage: /managed rollback <token>');
173
+ ctx.print('Usage: /managed rollback <token> --yes');
174
+ return;
175
+ }
176
+ if (!parsed.yes) {
177
+ requireYesFlag(ctx, `rollback managed settings token ${token}`, '/managed rollback <token> --yes');
155
178
  return;
156
179
  }
157
180
  try {
@@ -167,9 +190,9 @@ export function registerManagedRuntimeCommands(registry: CommandRegistry): void
167
190
  return;
168
191
  }
169
192
 
170
- const pathArg = args[1];
193
+ const pathArg = commandArgs[1];
171
194
  if (!pathArg) {
172
- ctx.print(`Usage: /managed ${sub} <path>`);
195
+ ctx.print(`Usage: /managed ${sub} <path>${sub === 'stage' || sub === 'apply' ? ' --yes' : ''}`);
173
196
  return;
174
197
  }
175
198
  const sourcePath = shellPaths.resolveWorkspacePath(pathArg);
@@ -181,18 +204,26 @@ export function registerManagedRuntimeCommands(registry: CommandRegistry): void
181
204
  }
182
205
 
183
206
  if (sub === 'stage') {
207
+ if (!parsed.yes) {
208
+ requireYesFlag(ctx, `stage managed settings bundle from ${pathArg}`, '/managed stage <path> --yes');
209
+ return;
210
+ }
184
211
  const stage = stageManagedSettingsBundle(ctx.platform.configManager, bundle, sourcePath);
185
212
  ctx.print(`Managed settings bundle staged from ${sourcePath} (${stage.changeCount} changes, risk=${stage.risk}).`);
186
213
  return;
187
214
  }
188
215
 
189
216
  if (sub === 'apply') {
190
- const requestedKeys = args.slice(2).filter((value): value is ConfigKey => CONFIG_KEYS.has(value as ConfigKey));
191
- const invalidKeys = args.slice(2).filter((value) => !CONFIG_KEYS.has(value as ConfigKey));
217
+ const requestedKeys = commandArgs.slice(2).filter((value): value is ConfigKey => CONFIG_KEYS.has(value as ConfigKey));
218
+ const invalidKeys = commandArgs.slice(2).filter((value) => !CONFIG_KEYS.has(value as ConfigKey));
192
219
  if (invalidKeys.length > 0) {
193
220
  ctx.print(`Unknown config key(s): ${invalidKeys.join(', ')}`);
194
221
  return;
195
222
  }
223
+ if (!parsed.yes) {
224
+ requireYesFlag(ctx, `apply managed settings bundle from ${pathArg}`, '/managed apply <path> [key ...] --yes');
225
+ return;
226
+ }
196
227
  stageManagedSettingsBundle(ctx.platform.configManager, bundle, sourcePath);
197
228
  const result = applyStagedManagedBundle(ctx.platform.configManager, requestedKeys);
198
229
  ctx.session.runtime.model = String(ctx.platform.configManager.get('provider.model'));
@@ -203,7 +234,7 @@ export function registerManagedRuntimeCommands(registry: CommandRegistry): void
203
234
  }
204
235
 
205
236
  recordSettingsSyncFailure('managed', `unsupported subcommand: ${sub}`, controlPlaneConfigDir);
206
- ctx.print('Usage: /managed [review|staged|rollback-history|export <profile> <path>|inspect <path>|stage <path>|apply <path> [key ...]|apply-staged [key ...]|rollback <token>|lock <key> <source> <reason...>|unlock <key>]');
237
+ ctx.print('Usage: /managed [review|staged|rollback-history|export <profile> <path> --yes|inspect <path>|stage <path> --yes|apply <path> [key ...] --yes|apply-staged [key ...] --yes|rollback <token> --yes|lock <key> <source> <reason...> --yes|unlock <key> --yes]');
207
238
  },
208
239
  });
209
240
  }
@@ -20,6 +20,7 @@ import {
20
20
  type EcosystemEntryKind,
21
21
  } from '@/runtime/index.ts';
22
22
  import { openCommandPanel, requireEcosystemCatalogPaths, requireReadModels, requireShellPaths } from './runtime-services.ts';
23
+ import { requireYesFlag, stripYesFlag } from './confirmation.ts';
23
24
 
24
25
  function resolveMarketplaceEntry(
25
26
  kind: EcosystemEntryKind,
@@ -39,11 +40,13 @@ export function registerMarketplaceRuntimeCommands(registry: CommandRegistry): v
39
40
  name: 'marketplace',
40
41
  aliases: ['catalog'],
41
42
  description: 'Browse the unified plugin and skill marketplace',
42
- usage: '[open|overview|recommend|browse [query]|review <plugin|skill|hook-pack|policy-pack> <id>|provenance <plugin|skill|hook-pack|policy-pack> <id>|install-hint <plugin|skill|hook-pack|policy-pack> <id>|install <plugin|skill|hook-pack|policy-pack> <id> [project|user]|update <plugin|skill|hook-pack|policy-pack> <id> [project|user]|rollback <plugin|skill|hook-pack|policy-pack> <id> [project|user] [backupId]|history <plugin|skill|hook-pack|policy-pack> <id> [project|user]|uninstall <plugin|skill|hook-pack|policy-pack> <id> [project|user]|receipt <plugin|skill|hook-pack|policy-pack> <id> [project|user]|bundle export <path> [project|user]|bundle inspect <path>|bundle import <path> [project|user]|installed]',
43
+ usage: '[open|overview|recommend|browse [query]|review <plugin|skill|hook-pack|policy-pack> <id>|provenance <plugin|skill|hook-pack|policy-pack> <id>|install-hint <plugin|skill|hook-pack|policy-pack> <id>|install <plugin|skill|hook-pack|policy-pack> <id> [project|user] --yes|update <plugin|skill|hook-pack|policy-pack> <id> [project|user] --yes|rollback <plugin|skill|hook-pack|policy-pack> <id> [project|user] [backupId] --yes|history <plugin|skill|hook-pack|policy-pack> <id> [project|user]|uninstall <plugin|skill|hook-pack|policy-pack> <id> [project|user] --yes|receipt <plugin|skill|hook-pack|policy-pack> <id> [project|user]|bundle export <path> [project|user] --yes|bundle inspect <path>|bundle import <path> [project|user] --yes|installed]',
43
44
  handler(args, ctx) {
45
+ const parsed = stripYesFlag(args);
46
+ const commandArgs = [...parsed.rest];
44
47
  const shellPaths = requireShellPaths(ctx);
45
48
  const ecosystemPaths = requireEcosystemCatalogPaths(ctx);
46
- const sub = args[0] ?? 'open';
49
+ const sub = commandArgs[0] ?? 'open';
47
50
  if (sub === 'open' || sub === 'panel') {
48
51
  openCommandPanel(ctx, 'marketplace');
49
52
  return;
@@ -71,7 +74,7 @@ export function registerMarketplaceRuntimeCommands(registry: CommandRegistry): v
71
74
  return;
72
75
  }
73
76
  if (sub === 'browse') {
74
- const query = args.slice(1).join(' ');
77
+ const query = commandArgs.slice(1).join(' ');
75
78
  const pluginEntries = query ? searchEcosystemCatalog('plugin', query, ecosystemPaths) : loadEcosystemCatalog('plugin', ecosystemPaths);
76
79
  const skillEntries = query ? searchEcosystemCatalog('skill', query, ecosystemPaths) : loadEcosystemCatalog('skill', ecosystemPaths);
77
80
  const hookPackEntries = query ? searchEcosystemCatalog('hook-pack', query, ecosystemPaths) : loadEcosystemCatalog('hook-pack', ecosystemPaths);
@@ -113,14 +116,18 @@ export function registerMarketplaceRuntimeCommands(registry: CommandRegistry): v
113
116
  return;
114
117
  }
115
118
  if (sub === 'bundle') {
116
- const mode = args[1];
117
- const target = args[2];
118
- const scope = args[3] === 'user' ? 'user' : 'project';
119
+ const mode = commandArgs[1];
120
+ const target = commandArgs[2];
121
+ const scope = commandArgs[3] === 'user' ? 'user' : 'project';
119
122
  if ((mode === 'export' || mode === 'inspect' || mode === 'import') && !target) {
120
- ctx.print(`Usage: /marketplace bundle ${mode} <path>${mode === 'export' || mode === 'import' ? ' [project|user]' : ''}`);
123
+ ctx.print(`Usage: /marketplace bundle ${mode} <path>${mode === 'export' || mode === 'import' ? ' [project|user] --yes' : ''}`);
121
124
  return;
122
125
  }
123
126
  if (mode === 'export') {
127
+ if (!parsed.yes) {
128
+ requireYesFlag(ctx, `export marketplace bundle to ${target}`, '/marketplace bundle export <path> [project|user] --yes');
129
+ return;
130
+ }
124
131
  const bundle = exportEcosystemCatalogBundle(scope, ecosystemPaths);
125
132
  const targetPath = shellPaths.resolveWorkspacePath(target!);
126
133
  mkdirSync(dirname(targetPath), { recursive: true });
@@ -141,6 +148,10 @@ export function registerMarketplaceRuntimeCommands(registry: CommandRegistry): v
141
148
  return;
142
149
  }
143
150
  if (mode === 'import') {
151
+ if (!parsed.yes) {
152
+ requireYesFlag(ctx, `import marketplace bundle from ${target}`, '/marketplace bundle import <path> [project|user] --yes');
153
+ return;
154
+ }
144
155
  const bundle = JSON.parse(readFileSync(shellPaths.resolveWorkspacePath(target!), 'utf-8')) as EcosystemCatalogBundle;
145
156
  const result = importEcosystemCatalogBundle(bundle, { ...ecosystemPaths, scope });
146
157
  ctx.print([
@@ -150,14 +161,14 @@ export function registerMarketplaceRuntimeCommands(registry: CommandRegistry): v
150
161
  ].join('\n'));
151
162
  return;
152
163
  }
153
- ctx.print('Usage: /marketplace bundle <export|inspect|import> <path> [project|user]');
164
+ ctx.print('Usage: /marketplace bundle <export|inspect|import> <path> [project|user] --yes');
154
165
  return;
155
166
  }
156
167
 
157
- const kind = args[1] as EcosystemEntryKind | undefined;
158
- const entryId = args[2];
168
+ const kind = commandArgs[1] as EcosystemEntryKind | undefined;
169
+ const entryId = commandArgs[2];
159
170
  if ((sub === 'review' || sub === 'provenance' || sub === 'install-hint' || sub === 'install' || sub === 'update' || sub === 'rollback' || sub === 'history' || sub === 'uninstall' || sub === 'receipt') && (!kind || !entryId || !['plugin', 'skill', 'hook-pack', 'policy-pack'].includes(kind))) {
160
- ctx.print(`Usage: /marketplace ${sub} <plugin|skill|hook-pack|policy-pack> <id>${sub === 'install' || sub === 'update' || sub === 'rollback' || sub === 'history' || sub === 'uninstall' || sub === 'receipt' ? ' [project|user]' : ''}`);
171
+ ctx.print(`Usage: /marketplace ${sub} <plugin|skill|hook-pack|policy-pack> <id>${sub === 'install' || sub === 'update' || sub === 'rollback' || sub === 'history' || sub === 'uninstall' || sub === 'receipt' ? ' [project|user]' : ''}${sub === 'install' || sub === 'update' || sub === 'rollback' || sub === 'uninstall' ? ' --yes' : ''}`);
161
172
  return;
162
173
  }
163
174
  if (sub === 'review') {
@@ -216,7 +227,7 @@ export function registerMarketplaceRuntimeCommands(registry: CommandRegistry): v
216
227
  return;
217
228
  }
218
229
  if (sub === 'receipt') {
219
- const scope = args[3] === 'user' ? 'user' : 'project';
230
+ const scope = commandArgs[3] === 'user' ? 'user' : 'project';
220
231
  const result = inspectInstalledEcosystemEntry(kind!, entryId!, { ...ecosystemPaths, scope });
221
232
  if (!result.ok) {
222
233
  ctx.print(`Error: ${result.error}`);
@@ -236,7 +247,7 @@ export function registerMarketplaceRuntimeCommands(registry: CommandRegistry): v
236
247
  return;
237
248
  }
238
249
  if (sub === 'history') {
239
- const scope = args[3] === 'user' ? 'user' : 'project';
250
+ const scope = commandArgs[3] === 'user' ? 'user' : 'project';
240
251
  const backups = listEcosystemInstallBackups(kind!, entryId!, { ...ecosystemPaths, scope });
241
252
  ctx.print(backups.length > 0
242
253
  ? [
@@ -247,7 +258,11 @@ export function registerMarketplaceRuntimeCommands(registry: CommandRegistry): v
247
258
  return;
248
259
  }
249
260
  if (sub === 'install' || sub === 'update' || sub === 'rollback' || sub === 'uninstall') {
250
- const scope = args[3] === 'user' ? 'user' : 'project';
261
+ const scope = commandArgs[3] === 'user' ? 'user' : 'project';
262
+ if (!parsed.yes) {
263
+ requireYesFlag(ctx, `${sub} curated ${kind} ${entryId}`, `/marketplace ${sub} <plugin|skill|hook-pack|policy-pack> <id> [project|user]${sub === 'rollback' ? ' [backupId]' : ''} --yes`);
264
+ return;
265
+ }
251
266
  if (sub === 'install') {
252
267
  const result = installEcosystemCatalogEntry(kind!, entryId!, { ...ecosystemPaths, scope });
253
268
  if (!result.ok) {
@@ -267,7 +282,7 @@ export function registerMarketplaceRuntimeCommands(registry: CommandRegistry): v
267
282
  return;
268
283
  }
269
284
  if (sub === 'rollback') {
270
- const backupId = args[4];
285
+ const backupId = commandArgs[4];
271
286
  const result = rollbackInstalledEcosystemEntry(kind!, entryId!, { ...ecosystemPaths, scope, backupId });
272
287
  if (!result.ok) {
273
288
  ctx.print(`Error: ${result.error}`);
@@ -284,7 +299,7 @@ export function registerMarketplaceRuntimeCommands(registry: CommandRegistry): v
284
299
  ctx.print(`Uninstalled curated ${kind} ${entryId} from ${result.removedPath}`);
285
300
  return;
286
301
  }
287
- ctx.print('Usage: /marketplace [open|overview|recommend|browse [query]|review <plugin|skill|hook-pack|policy-pack> <id>|provenance <plugin|skill|hook-pack|policy-pack> <id>|install-hint <plugin|skill|hook-pack|policy-pack> <id>|install <plugin|skill|hook-pack|policy-pack> <id> [project|user]|update <plugin|skill|hook-pack|policy-pack> <id> [project|user]|rollback <plugin|skill|hook-pack|policy-pack> <id> [project|user] [backupId]|history <plugin|skill|hook-pack|policy-pack> <id> [project|user]|uninstall <plugin|skill|hook-pack|policy-pack> <id> [project|user]|receipt <plugin|skill|hook-pack|policy-pack> <id> [project|user]|bundle export <path> [project|user]|bundle inspect <path>|bundle import <path> [project|user]|installed]');
302
+ ctx.print('Usage: /marketplace [open|overview|recommend|browse [query]|review <plugin|skill|hook-pack|policy-pack> <id>|provenance <plugin|skill|hook-pack|policy-pack> <id>|install-hint <plugin|skill|hook-pack|policy-pack> <id>|install <plugin|skill|hook-pack|policy-pack> <id> [project|user] --yes|update <plugin|skill|hook-pack|policy-pack> <id> [project|user] --yes|rollback <plugin|skill|hook-pack|policy-pack> <id> [project|user] [backupId] --yes|history <plugin|skill|hook-pack|policy-pack> <id> [project|user]|uninstall <plugin|skill|hook-pack|policy-pack> <id> [project|user] --yes|receipt <plugin|skill|hook-pack|policy-pack> <id> [project|user]|bundle export <path> [project|user] --yes|bundle inspect <path>|bundle import <path> [project|user] --yes|installed]');
288
303
  },
289
304
  });
290
305
  }
@@ -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) {