@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
@@ -4,9 +4,6 @@ import {
4
4
  type CloudflareComponentSelection,
5
5
  type CloudflareDaemonClient,
6
6
  type CloudflareDiscoverResult,
7
- type CloudflareOperationalTokenResult,
8
- type CloudflareProvisionRequest,
9
- type CloudflareProvisionResult,
10
7
  type CloudflareTokenRequirementsResult,
11
8
  type CloudflareValidateResult,
12
9
  type CloudflareVerifyResult,
@@ -16,7 +13,6 @@ import type { InputHandler } from './handler.ts';
16
13
  import type { OnboardingWizardAction, OnboardingWizardApplyFeedback } from './onboarding/onboarding-wizard.ts';
17
14
  import {
18
15
  buildCloudflareApiTokenRef,
19
- buildCloudflareProvisionRequest,
20
16
  getCloudflareBatchMode,
21
17
  getCloudflareComponentSelection,
22
18
  getCloudflareSetupSource,
@@ -113,26 +109,6 @@ function formatCloudflareDiscovery(result: CloudflareDiscoverResult): string[] {
113
109
  ];
114
110
  }
115
111
 
116
- function formatCloudflareTokenCreate(result: CloudflareOperationalTokenResult): string[] {
117
- return [
118
- `token: ${result.tokenName}${result.tokenId ? ` (${result.tokenId})` : ''}`,
119
- `account: ${result.accountId}`,
120
- `stored ref: ${result.apiTokenRef ?? 'not stored'}`,
121
- `permissions: ${result.permissions.length}`,
122
- 'Delete or expire the temporary bootstrap token in Cloudflare after confirming the operational token works.',
123
- ];
124
- }
125
-
126
- function formatCloudflareProvision(result: CloudflareProvisionResult): string[] {
127
- return [
128
- `result: ${result.ok ? 'ok' : 'needs attention'}`,
129
- ...(result.worker ? [`worker: ${result.worker.name}${result.worker.baseUrl ? ` at ${result.worker.baseUrl}` : ''}`] : []),
130
- ...(result.queues ? [`queue: ${result.queues.queueName}; DLQ: ${result.queues.deadLetterQueueName}`] : []),
131
- ...result.steps.map((step) => `${step.status}: ${step.name}${step.message ? ` - ${step.message}` : ''}`),
132
- ...(result.verification ? formatCloudflareVerify(result.verification).map((line) => `verify ${line}`) : []),
133
- ];
134
- }
135
-
136
112
  function formatCloudflareVerify(result: CloudflareVerifyResult): string[] {
137
113
  return [
138
114
  `worker health: ${result.workerHealth.ok ? 'ok' : 'failed'} (HTTP ${result.workerHealth.status})${result.workerHealth.error ? ` - ${result.workerHealth.error}` : ''}`,
@@ -171,40 +147,6 @@ function getCloudflareApiTokenRefFromWizard(handler: InputHandler): string {
171
147
  return wizard.runtimeSnapshot?.config.cloudflare.apiTokenRef ?? '';
172
148
  }
173
149
 
174
- async function createCloudflareOperationalTokenForHandler(handler: InputHandler): Promise<CloudflareOperationalTokenResult> {
175
- const wizard = handler.onboardingWizard;
176
- const bootstrapToken = getCloudflareBootstrapTokenFromWizard(handler);
177
- if (!bootstrapToken) {
178
- throw new Error('A bootstrap token is required. Paste it in the wizard or select an environment variable that is set in this TUI process.');
179
- }
180
- const accountId = wizard.getStringFieldValue('cloudflare.account-id', wizard.runtimeSnapshot?.config.cloudflare.accountId ?? '');
181
- const zoneId = wizard.getStringFieldValue('cloudflare.zone-id', wizard.runtimeSnapshot?.config.cloudflare.zoneId ?? '');
182
- const zoneName = wizard.getStringFieldValue('cloudflare.zone-name', wizard.runtimeSnapshot?.config.cloudflare.zoneName ?? '');
183
- return await getCloudflareDaemonClientForHandler(handler).createOperationalToken({
184
- components: getCloudflareComponentSelection(wizard),
185
- bootstrapToken,
186
- ...(accountId ? { accountId } : {}),
187
- ...(zoneId ? { zoneId } : {}),
188
- ...(zoneName ? { zoneName } : {}),
189
- storeApiToken: true,
190
- persistConfig: true,
191
- });
192
- }
193
-
194
- async function buildCloudflareProvisionInputForHandler(handler: InputHandler): Promise<CloudflareProvisionRequest> {
195
- const input = buildCloudflareProvisionRequest(handler.onboardingWizard, { includeTransientSecrets: true });
196
- const setupSource = getCloudflareSetupSource(handler.onboardingWizard);
197
- if (setupSource === 'bootstrap-token' || setupSource === 'bootstrap-env') {
198
- const tokenResult = await createCloudflareOperationalTokenForHandler(handler);
199
- if (tokenResult.apiTokenRef) {
200
- const withoutInlineToken = { ...input };
201
- delete withoutInlineToken.apiToken;
202
- return { ...withoutInlineToken, apiTokenRef: tokenResult.apiTokenRef };
203
- }
204
- }
205
- return input;
206
- }
207
-
208
150
  function buildCloudflareDiscoveryInputForHandler(handler: InputHandler): Parameters<CloudflareDaemonClient['discover']>[0] {
209
151
  const wizard = handler.onboardingWizard;
210
152
  const accountId = wizard.getStringFieldValue('cloudflare.account-id', wizard.runtimeSnapshot?.config.cloudflare.accountId ?? '');
@@ -223,6 +165,34 @@ function buildCloudflareDiscoveryInputForHandler(handler: InputHandler): Paramet
223
165
  };
224
166
  }
225
167
 
168
+ function blockedCloudflareMutationLines(action: CloudflareOnboardingAction): string[] {
169
+ switch (action) {
170
+ case 'cloudflare-create-operational-token':
171
+ return [
172
+ 'Creating and storing Cloudflare tokens is a side-effecting operation.',
173
+ 'Run /cloudflare create-token [flags] --yes from the main prompt when you explicitly want that mutation.',
174
+ ];
175
+ case 'cloudflare-provision':
176
+ return [
177
+ 'Provisioning creates or updates Cloudflare resources.',
178
+ 'Run /cloudflare provision [flags] --yes from the main prompt when you explicitly want that mutation.',
179
+ ];
180
+ case 'cloudflare-disable':
181
+ return [
182
+ 'Disabling Cloudflare changes persisted daemon integration config.',
183
+ 'Run /cloudflare disable [flags] --yes from the main prompt when you explicitly want that mutation.',
184
+ ];
185
+ default:
186
+ return [];
187
+ }
188
+ }
189
+
190
+ function isBlockedCloudflareMutation(action: CloudflareOnboardingAction): boolean {
191
+ return action === 'cloudflare-create-operational-token'
192
+ || action === 'cloudflare-provision'
193
+ || action === 'cloudflare-disable';
194
+ }
195
+
226
196
  function buildCloudflareValidateInputForHandler(handler: InputHandler): Parameters<CloudflareDaemonClient['validate']>[0] {
227
197
  const wizard = handler.onboardingWizard;
228
198
  const accountId = wizard.getStringFieldValue('cloudflare.account-id', wizard.runtimeSnapshot?.config.cloudflare.accountId ?? '');
@@ -244,6 +214,16 @@ export async function handleCloudflareOnboardingActionForHandler(
244
214
  handler.onboardingWizard.clearApplyFeedback();
245
215
  handler.requestRender();
246
216
  try {
217
+ if (isBlockedCloudflareMutation(action)) {
218
+ setCloudflareWizardStatusForHandler(
219
+ handler,
220
+ 'Cloudflare mutation requires explicit command',
221
+ blockedCloudflareMutationLines(action),
222
+ 'warning',
223
+ );
224
+ return;
225
+ }
226
+
247
227
  const client = getCloudflareDaemonClientForHandler(handler);
248
228
  if (action === 'cloudflare-token-requirements') {
249
229
  const result = await client.tokenRequirements({
@@ -254,13 +234,6 @@ export async function handleCloudflareOnboardingActionForHandler(
254
234
  return;
255
235
  }
256
236
 
257
- if (action === 'cloudflare-create-operational-token') {
258
- const result = await createCloudflareOperationalTokenForHandler(handler);
259
- setCloudflareWizardStatusForHandler(handler, 'Cloudflare operational token created', formatCloudflareTokenCreate(result));
260
- await handler.refreshOnboardingHydration({ preserveValues: true, targetStepId: 'cloudflare' });
261
- return;
262
- }
263
-
264
237
  if (action === 'cloudflare-discover') {
265
238
  const result = await client.discover(buildCloudflareDiscoveryInputForHandler(handler));
266
239
  if (result.selectedAccount && !handler.onboardingWizard.getStringFieldValue('cloudflare.account-id', '')) {
@@ -293,19 +266,6 @@ export async function handleCloudflareOnboardingActionForHandler(
293
266
  return;
294
267
  }
295
268
 
296
- if (action === 'cloudflare-provision') {
297
- const input = await buildCloudflareProvisionInputForHandler(handler);
298
- const result = await client.provision(input);
299
- setCloudflareWizardStatusForHandler(
300
- handler,
301
- result.ok ? 'Cloudflare provisioning completed' : 'Cloudflare provisioning needs attention',
302
- formatCloudflareProvision(result),
303
- result.ok ? 'info' : 'warning',
304
- );
305
- await handler.refreshOnboardingHydration({ preserveValues: true, targetStepId: 'cloudflare' });
306
- return;
307
- }
308
-
309
269
  if (action === 'cloudflare-verify') {
310
270
  const result = await client.verify({
311
271
  workerBaseUrl: handler.onboardingWizard.getStringFieldValue('cloudflare.worker-base-url', handler.onboardingWizard.runtimeSnapshot?.config.cloudflare.workerBaseUrl ?? ''),
@@ -319,22 +279,6 @@ export async function handleCloudflareOnboardingActionForHandler(
319
279
  );
320
280
  return;
321
281
  }
322
-
323
- if (action === 'cloudflare-disable') {
324
- const result = await client.disable({
325
- accountId: handler.onboardingWizard.getStringFieldValue('cloudflare.account-id', handler.onboardingWizard.runtimeSnapshot?.config.cloudflare.accountId ?? ''),
326
- apiTokenRef: getCloudflareApiTokenRefFromWizard(handler),
327
- workerName: handler.onboardingWizard.getStringFieldValue('cloudflare.worker-name', handler.onboardingWizard.runtimeSnapshot?.config.cloudflare.workerName ?? 'goodvibes-batch-worker'),
328
- persistConfig: true,
329
- });
330
- setCloudflareWizardStatusForHandler(
331
- handler,
332
- result.ok ? 'Cloudflare integration disabled' : 'Cloudflare disable needs attention',
333
- result.steps.map((step) => `${step.status}: ${step.name}${step.message ? ` - ${step.message}` : ''}`),
334
- result.ok ? 'info' : 'warning',
335
- );
336
- await handler.refreshOnboardingHydration({ preserveValues: true, targetStepId: 'cloudflare' });
337
- }
338
282
  } catch (error) {
339
283
  setCloudflareWizardStatusForHandler(handler, 'Cloudflare action failed', [normalizeCloudflareActionError(error)], 'error');
340
284
  } finally {
@@ -365,27 +309,14 @@ export async function maybeProvisionCloudflareOnFinalApplyForHandler(handler: In
365
309
  }];
366
310
  }
367
311
 
368
- try {
369
- const client = getCloudflareDaemonClientForHandler(handler);
370
- const result = await client.provision(await buildCloudflareProvisionInputForHandler(handler));
371
- handler.onboardingWizard.textState.set('cloudflare.action-status', [
372
- result.ok ? 'Cloudflare provisioning completed during final apply.' : 'Cloudflare provisioning needs attention after final apply.',
373
- ...formatCloudflareProvision(result),
374
- ].join('\n'));
375
- return [{
376
- id: 'cloudflare:provision',
377
- status: result.ok ? 'pass' : 'warn',
378
- message: result.ok
379
- ? 'Cloudflare resources were provisioned and verified through the daemon SDK route.'
380
- : 'Cloudflare provisioning returned warnings or failed verification. Settings were saved; rerun the Cloudflare wizard action after correcting token/resource issues.',
381
- target: 'cloudflare',
382
- }];
383
- } catch (error) {
384
- return [{
385
- id: 'cloudflare:provision',
386
- status: 'warn',
387
- message: `Cloudflare provisioning did not complete: ${normalizeCloudflareActionError(error)} Settings were saved; retry from the Cloudflare wizard or /cloudflare command.`,
388
- target: 'cloudflare',
389
- }];
390
- }
312
+ handler.onboardingWizard.textState.set('cloudflare.action-status', [
313
+ 'Cloudflare provisioning was not run during final apply.',
314
+ 'GoodVibes Agent requires an explicit /cloudflare provision [flags] --yes command for Cloudflare resource mutations.',
315
+ ].join('\n'));
316
+ return [{
317
+ id: 'cloudflare:provision',
318
+ status: 'warn',
319
+ message: 'Cloudflare settings were saved, but provisioning was blocked because Agent onboarding cannot create/update Cloudflare resources. Run /cloudflare provision [flags] --yes explicitly.',
320
+ target: 'cloudflare',
321
+ }];
391
322
  }
@@ -68,7 +68,7 @@ export const ACTION_DESCRIPTIONS: Record<KeyAction, string> = {
68
68
  'search': 'Toggle conversation search',
69
69
  'block-copy': 'Copy nearest block to clipboard',
70
70
  'bookmark': 'Bookmark / unbookmark nearest block',
71
- 'block-save': 'Save nearest block to file',
71
+ 'block-save': 'Block file save blocked; use /share --yes',
72
72
  'delete-word': 'Delete word backward',
73
73
  'apply-diff-line-start': 'Apply nearest diff / move to line start',
74
74
  'next-error-line-end': 'Navigate to next error / move to line end',
@@ -214,7 +214,7 @@ export class McpWorkspace {
214
214
  public formIndex = 0;
215
215
  public form: McpWorkspaceForm = serverConfigToForm();
216
216
  public editingServerName: string | null = null;
217
- public status = 'Ready. Add, edit, remove, reload, and inspect MCP servers without restarting the TUI.';
217
+ public status = 'Ready. Inspect MCP servers and tools. Config writes/reloads require explicit /mcp ... --yes commands.';
218
218
  public tools: readonly RegisteredTool[] = [];
219
219
  public loadingTools = false;
220
220
  public lastError: string | null = null;
@@ -234,7 +234,7 @@ export class McpWorkspace {
234
234
  this.formIndex = 0;
235
235
  this.lastError = null;
236
236
  this.refreshSnapshot();
237
- void this.refreshTools();
237
+ void this.refreshTools(false);
238
238
  }
239
239
 
240
240
  reopen(): void {
@@ -257,8 +257,8 @@ export class McpWorkspace {
257
257
  get rows(): readonly McpWorkspaceRow[] {
258
258
  return [
259
259
  ...this.snapshot.servers.map((server): McpWorkspaceRow => ({ type: 'server', server })),
260
- { type: 'action', id: 'add', label: 'Add server', detail: `Write a server through the SDK config manager. Default scope: ${this.form.scope}.` },
261
- { type: 'action', id: 'reload', label: 'Reload runtime', detail: 'Reconnect all MCP servers from global, Claude, and project config files.' },
260
+ { type: 'action', id: 'add', label: 'Add server preview', detail: `Draft server details here, then run /mcp add ... --scope ${this.form.scope} --yes to write config.` },
261
+ { type: 'action', id: 'reload', label: 'Reload guidance', detail: 'Runtime reload is blocked from the workspace; run /mcp reload --yes explicitly.' },
262
262
  { type: 'action', id: 'refresh-tools', label: 'Refresh tools', detail: 'Fetch the currently available MCP tool list from connected servers.' },
263
263
  { type: 'action', id: 'config', label: 'Config locations', detail: 'Show SDK-scanned config files and writable project/global paths.' },
264
264
  ];
@@ -284,7 +284,7 @@ export class McpWorkspace {
284
284
  { id: 'env', label: 'Environment', value: this.form.env, help: 'Comma-separated KEY=VALUE entries. Prefer env var references or secure secrets for sensitive values.', editable: true },
285
285
  { id: 'allowedPaths', label: 'Allowed paths', value: this.form.allowedPaths, help: 'Comma-separated path prefixes for filesystem-oriented servers.', editable: true },
286
286
  { id: 'allowedHosts', label: 'Allowed hosts', value: this.form.allowedHosts, help: 'Comma-separated hostnames for network-oriented servers.', editable: true },
287
- { id: 'save', label: 'Save and reload', value: '', help: 'Write the selected scope config and reconnect the live MCP runtime.', editable: false },
287
+ { id: 'save', label: 'Show save command', value: '', help: 'No workspace write. Shows the explicit /mcp add ... --yes command to run from the prompt.', editable: false },
288
288
  { id: 'cancel', label: 'Cancel', value: '', help: 'Return to the MCP server browser without changing config.', editable: false },
289
289
  ];
290
290
  }
@@ -306,14 +306,14 @@ export class McpWorkspace {
306
306
  this.selectedIndex = Math.max(0, Math.min(this.selectedIndex, this.rows.length - 1));
307
307
  }
308
308
 
309
- async refreshTools(): Promise<void> {
309
+ async refreshTools(updateStatus = true): Promise<void> {
310
310
  if (!this.context) return;
311
311
  this.loadingTools = true;
312
312
  this.lastError = null;
313
313
  try {
314
314
  const api = requireMcpApi(this.context);
315
315
  this.tools = await api.listAllTools();
316
- this.status = `Tool list refreshed: ${this.tools.length} tool(s) available.`;
316
+ if (updateStatus) this.status = `Tool list refreshed: ${this.tools.length} tool(s) available.`;
317
317
  } catch (error) {
318
318
  this.lastError = summarizeError(error);
319
319
  this.status = `Tool refresh failed: ${this.lastError}`;
@@ -326,20 +326,8 @@ export class McpWorkspace {
326
326
  async reloadRuntime(): Promise<void> {
327
327
  if (!this.context) return;
328
328
  this.lastError = null;
329
- try {
330
- const api = requireMcpApi(this.context);
331
- const roots = requireShellPaths(this.context);
332
- const result = await api.reload(roots);
333
- this.refreshSnapshot();
334
- const connected = this.snapshot.servers.filter((server) => server.connected).length;
335
- this.status = `Reloaded MCP runtime: ${connected}/${this.snapshot.servers.length} server(s) connected. Result: +${result.added} ~${result.changed} -${result.removed}, unchanged ${result.unchanged}.`;
336
- void this.refreshTools();
337
- } catch (error) {
338
- this.lastError = summarizeError(error);
339
- this.status = `Reload failed: ${this.lastError}`;
340
- } finally {
341
- this.context?.renderRequest();
342
- }
329
+ this.status = 'MCP runtime reload is blocked in the workspace. Run /mcp reload --yes from the prompt to explicitly reload.';
330
+ this.context.renderRequest();
343
331
  }
344
332
 
345
333
  openAddForm(): void {
@@ -347,7 +335,7 @@ export class McpWorkspace {
347
335
  this.formIndex = 0;
348
336
  this.editingServerName = null;
349
337
  this.form = serverConfigToForm();
350
- this.status = 'Add an MCP server. Choose project or global scope, then save and reload.';
338
+ this.status = 'Draft an MCP server. Saving from this workspace is blocked; use the shown /mcp add ... --yes command.';
351
339
  }
352
340
 
353
341
  openEditForm(serverName: string): void {
@@ -357,8 +345,8 @@ export class McpWorkspace {
357
345
  this.editingServerName = serverName;
358
346
  this.form = { ...serverConfigToForm(entry?.server), scope: entry?.source.scope === 'global' ? 'global' : 'project' };
359
347
  this.status = entry
360
- ? `Editing ${serverName}. Saving writes a ${this.form.scope} config entry and reloads the live runtime.`
361
- : `Editing ${serverName}. Runtime status exists, but no launch config was found; enter command details before saving.`;
348
+ ? `Viewing ${serverName}. Workspace saves are blocked; copy the generated /mcp add ... --yes command if you intend to change it.`
349
+ : `Viewing ${serverName}. Runtime status exists, but no launch config was found.`;
362
350
  }
363
351
 
364
352
  requestDelete(serverName: string): void {
@@ -366,24 +354,23 @@ export class McpWorkspace {
366
354
  this.editingServerName = serverName;
367
355
  const entry = this.snapshot.effectiveConfig.servers.find((configEntry) => configEntry.server.name === serverName);
368
356
  const scope = entry?.source.scope === 'global' ? 'global' : 'project';
369
- this.status = `Remove ${scope} server "${serverName}"? Press y to confirm or n/Esc to cancel.`;
357
+ this.status = `MCP server removal is blocked in the workspace. Run /mcp remove ${serverName} --scope ${scope} --yes from the prompt.`;
370
358
  }
371
359
 
372
360
  async saveForm(): Promise<void> {
373
361
  if (!this.context) return;
374
362
  try {
375
363
  const server = formToServerConfig(this.form);
376
- this.status = `Saving ${server.name} to ${this.form.scope} MCP config and reloading runtime...`;
377
364
  this.mode = 'browse';
378
365
  this.editingServerName = null;
379
- const api = requireMcpApi(this.context);
380
- const result = await api.upsertServerConfig(requireShellPaths(this.context), this.form.scope, server);
381
- this.refreshSnapshot();
382
- this.status = `Saved ${server.name} to ${result.path}. Reload result: +${result.reload.added} ~${result.reload.changed} -${result.reload.removed}, unchanged ${result.reload.unchanged}.`;
383
- void this.refreshTools();
366
+ const args = server.args?.length ? ` ${server.args.join(' ')}` : '';
367
+ const role = server.role ? ` --role ${server.role}` : '';
368
+ const trust = server.trustMode ? ` --trust ${server.trustMode}` : '';
369
+ this.status = `MCP config write blocked here. Run: /mcp add ${server.name} ${server.command}${args} --scope ${this.form.scope}${role}${trust} --yes`;
370
+ this.context.renderRequest();
384
371
  } catch (error) {
385
372
  this.lastError = summarizeError(error);
386
- this.status = `Save failed: ${this.lastError}`;
373
+ this.status = `Save command preview failed: ${this.lastError}`;
387
374
  this.context.renderRequest();
388
375
  }
389
376
  }
@@ -391,23 +378,12 @@ export class McpWorkspace {
391
378
  async confirmDelete(): Promise<void> {
392
379
  if (!this.context || !this.editingServerName) return;
393
380
  const name = this.editingServerName;
394
- try {
395
- const server = this.snapshot.effectiveConfig.servers.find((entry) => entry.server.name === name);
396
- const scope = server?.source.scope === 'global' ? 'global' : 'project';
397
- const api = requireMcpApi(this.context);
398
- const result = await api.removeServerConfig(requireShellPaths(this.context), scope, name);
399
- this.mode = 'browse';
400
- this.editingServerName = null;
401
- this.refreshSnapshot();
402
- this.status = result.removed
403
- ? `Removed ${scope} server "${name}" from ${result.path}. Reload result: +${result.reload.added} ~${result.reload.changed} -${result.reload.removed}.`
404
- : `No ${scope} MCP server named "${name}" exists in ${result.path}.`;
405
- void this.refreshTools();
406
- } catch (error) {
407
- this.lastError = summarizeError(error);
408
- this.status = `Remove failed: ${this.lastError}`;
409
- this.context.renderRequest();
410
- }
381
+ const server = this.snapshot.effectiveConfig.servers.find((entry) => entry.server.name === name);
382
+ const scope = server?.source.scope === 'global' ? 'global' : 'project';
383
+ this.mode = 'browse';
384
+ this.editingServerName = null;
385
+ this.status = `MCP removal blocked here. Run: /mcp remove ${name} --scope ${scope} --yes`;
386
+ this.context.renderRequest();
411
387
  }
412
388
 
413
389
  cancelForm(): void {
@@ -380,8 +380,8 @@ export function buildCloudflareStep(controller: OnboardingWizardController): Onb
380
380
  {
381
381
  kind: 'radio',
382
382
  id: 'cloudflare.provision-on-apply',
383
- label: 'Provision Cloudflare on final apply',
384
- hint: 'If yes, final Apply calls SDK daemon routes to create/update resources and verify them. Failure is reported as a warning; settings still save.',
383
+ label: 'Final apply Cloudflare provisioning',
384
+ hint: 'Agent onboarding saves config only. Run /cloudflare provision [flags] --yes explicitly for resource changes.',
385
385
  options: CLOUDFLARE_PROVISION_OPTIONS,
386
386
  defaultValue: 'no',
387
387
  },
@@ -404,8 +404,8 @@ export function buildCloudflareStep(controller: OnboardingWizardController): Onb
404
404
  kind: 'action',
405
405
  id: 'cloudflare.create-token',
406
406
  action: 'cloudflare-create-operational-token',
407
- label: 'Create operational token from bootstrap token',
408
- hint: 'Uses a pasted or environment bootstrap token once. The SDK stores the generated operational token as a goodvibes:// secret.',
407
+ label: 'Show create-token command',
408
+ hint: 'Token creation is side-effecting. The wizard shows the explicit /cloudflare create-token ... --yes path instead of running it.',
409
409
  defaultValue: 'Action',
410
410
  },
411
411
  {
@@ -428,8 +428,8 @@ export function buildCloudflareStep(controller: OnboardingWizardController): Onb
428
428
  kind: 'action',
429
429
  id: 'cloudflare.provision',
430
430
  action: 'cloudflare-provision',
431
- label: 'Provision and verify now',
432
- hint: 'Calls the daemon SDK route immediately with the current wizard values. This creates/updates selected Cloudflare resources.',
431
+ label: 'Show provision command',
432
+ hint: 'Provisioning is side-effecting. The wizard shows the explicit /cloudflare provision ... --yes path instead of running it.',
433
433
  defaultValue: 'Action',
434
434
  },
435
435
  {
@@ -444,8 +444,8 @@ export function buildCloudflareStep(controller: OnboardingWizardController): Onb
444
444
  kind: 'action',
445
445
  id: 'cloudflare.disable',
446
446
  action: 'cloudflare-disable',
447
- label: 'Disable Cloudflare integration',
448
- hint: 'Calls the daemon SDK route to disable local Cloudflare usage and return the batch queue backend to local behavior.',
447
+ label: 'Show disable command',
448
+ hint: 'Disabling persists config changes. The wizard shows the explicit /cloudflare disable ... --yes path instead of running it.',
449
449
  defaultValue: 'Action',
450
450
  },
451
451
  );
@@ -66,12 +66,7 @@ export const CLOUDFLARE_PROVISION_OPTIONS: readonly OnboardingWizardRadioOption[
66
66
  {
67
67
  id: 'no',
68
68
  label: 'No, save configuration only',
69
- hint: 'Final Apply saves the settings. Use the Cloudflare command or this wizard later to provision resources.',
70
- },
71
- {
72
- id: 'yes',
73
- label: 'Yes, create or update Cloudflare resources',
74
- hint: 'Final Apply asks the daemon SDK route to create/update selected Cloudflare resources and verify the Worker when possible.',
69
+ hint: 'Final Apply saves the settings. Run /cloudflare provision [flags] --yes explicitly when you want resource changes.',
75
70
  },
76
71
  ];
77
72
 
@@ -2,7 +2,7 @@
2
2
  * ProfilePickerModal — state management for the /profiles picker modal.
3
3
  *
4
4
  * Lists profiles from ProfileManager.list(), tracks selected index,
5
- * and handles load/delete/save actions.
5
+ * and handles load actions.
6
6
  */
7
7
 
8
8
  import type { ProfileInfo, ProfileData, ProfileManager } from '@pellux/goodvibes-sdk/platform/profiles';
@@ -141,39 +141,12 @@ export class ProfilePickerModal {
141
141
  }
142
142
  }
143
143
 
144
- /**
145
- * Delete the selected profile from disk.
146
- * Refreshes the list after deletion.
147
- */
148
144
  deleteSelected(): boolean {
149
145
  const profile = this.getSelected();
150
146
  if (!profile) return false;
151
- if (this.deleteConfirmationTarget !== profile.name) {
152
- this.deleteConfirmationTarget = profile.name;
153
- this.statusMessage = `Press delete again to remove profile: ${profile.name}`;
154
- return false;
155
- }
156
-
157
- try {
158
- const deleted = this.profileManager.delete(profile.name);
159
- if (!deleted) {
160
- this.statusMessage = `Profile not found: ${profile.name}`;
161
- this.deleteConfirmationTarget = null;
162
- return false;
163
- }
164
- this.profiles = this.profileManager.list();
165
- if (this.selectedIndex >= this.profiles.length) {
166
- this.selectedIndex = Math.max(0, this.profiles.length - 1);
167
- }
168
- this._clampScroll();
169
- this.deleteConfirmationTarget = null;
170
- this.statusMessage = `Deleted: ${profile.name}`;
171
- return true;
172
- } catch (e) {
173
- this.deleteConfirmationTarget = null;
174
- this.statusMessage = `Error: ${summarizeError(e)}`;
175
- return false;
176
- }
147
+ this.deleteConfirmationTarget = null;
148
+ this.statusMessage = `Deletion requires an explicit command: /profiles delete ${profile.name} --yes`;
149
+ return false;
177
150
  }
178
151
 
179
152
  /**
@@ -184,7 +157,16 @@ export class ProfilePickerModal {
184
157
  this.statusMessage = 'Profile name cannot be empty';
185
158
  return false;
186
159
  }
160
+ void configManager;
161
+ this.statusMessage = `Saving requires an explicit command: /profiles save ${name} --yes`;
162
+ return false;
163
+ }
187
164
 
165
+ public saveCurrentAsConfirmed(name: string, configManager: ConfigManager): boolean {
166
+ if (!name || !name.trim()) {
167
+ this.statusMessage = 'Profile name cannot be empty';
168
+ return false;
169
+ }
188
170
  try {
189
171
  const all = configManager.getAll();
190
172
  const data: ProfileData = {
@@ -2,10 +2,9 @@
2
2
  * SessionPickerModal — state management for the /sessions picker modal.
3
3
  *
4
4
  * Lists sessions from SessionManager.list(), tracks selected index,
5
- * and handles load/delete actions.
5
+ * and handles load actions.
6
6
  */
7
7
 
8
- import { unlinkSync } from 'node:fs';
9
8
  import type { SessionInfo, SessionManager } from '@pellux/goodvibes-sdk/platform/sessions';
10
9
  import type { ConversationManager } from '../core/conversation';
11
10
  import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
@@ -90,37 +89,12 @@ export class SessionPickerModal {
90
89
  }
91
90
  }
92
91
 
93
- /**
94
- * Delete the currently selected session from disk.
95
- * Refreshes the list after deletion.
96
- */
97
92
  deleteSelected(): boolean {
98
93
  const session = this.getSelected();
99
94
  if (!session) return false;
100
- if (this.deleteConfirmationTarget !== session.name) {
101
- this.deleteConfirmationTarget = session.name;
102
- this.statusMessage = `Press d again to delete ${session.name}.`;
103
- return false;
104
- }
105
-
106
- try {
107
- // Delete directly via filePath so it works with any session directory
108
- unlinkSync(session.filePath);
109
- // Reload list from the global session manager (removes the deleted entry)
110
- this.sessions = this.sessionManager.list();
111
- // Adjust selection
112
- if (this.selectedIndex >= this.sessions.length) {
113
- this.selectedIndex = Math.max(0, this.sessions.length - 1);
114
- }
115
- this._clampScroll();
116
- this.deleteConfirmationTarget = null;
117
- this.statusMessage = `Deleted: ${session.name}`;
118
- return true;
119
- } catch (e) {
120
- this.deleteConfirmationTarget = null;
121
- this.statusMessage = `Error: ${summarizeError(e)}`;
122
- return false;
123
- }
95
+ this.deleteConfirmationTarget = null;
96
+ this.statusMessage = `Deletion requires an explicit command: /session delete ${session.name} --yes`;
97
+ return false;
124
98
  }
125
99
 
126
100
  private _clampScroll(): void {
@@ -23,7 +23,7 @@ export function buildSubscriptionEntries(
23
23
  activeRoute: 'unconfigured',
24
24
  authFreshness: 'unconfigured',
25
25
  routeReason: 'Built-in subscription adapter is available, but no active subscription session is stored yet.',
26
- nextActions: [`Use /subscription login ${provider} start to begin browser sign-in.`],
26
+ nextActions: [`Use /subscription login ${provider} start --yes to begin browser sign-in.`],
27
27
  });
28
28
  }
29
29
 
@@ -38,7 +38,7 @@ export function buildSubscriptionEntries(
38
38
  activeRoute: providers.get(provider)?.activeRoute ?? 'unconfigured',
39
39
  authFreshness: providers.get(provider)?.authFreshness ?? 'unconfigured',
40
40
  routeReason: providers.get(provider)?.routeReason ?? 'OAuth metadata is configured for this provider.',
41
- nextActions: providers.get(provider)?.nextActions ?? [`Use /subscription login ${provider} start to begin browser sign-in.`],
41
+ nextActions: providers.get(provider)?.nextActions ?? [`Use /subscription login ${provider} start --yes to begin browser sign-in.`],
42
42
  });
43
43
  }
44
44
 
@@ -51,7 +51,7 @@ export function buildSubscriptionEntries(
51
51
  activeRoute: 'unconfigured',
52
52
  authFreshness: 'pending',
53
53
  routeReason: 'OAuth login is pending completion for this provider.',
54
- nextActions: [`Finish /subscription login ${pending.provider} finish <code> to activate this session.`],
54
+ nextActions: [`Finish /subscription login ${pending.provider} finish <code> --yes to activate this session.`],
55
55
  });
56
56
  }
57
57
 
@@ -185,7 +185,7 @@ export class IncidentReviewPanel extends ScrollableListPanel<FailureReport> {
185
185
  }
186
186
  }
187
187
  footerLines.push(buildPanelLine(width, [[' Action Rail', C.label]]));
188
- footerLines.push(buildPanelLine(width, [[` /incident latest /incident export ${selected.id} /recall capture incident ${selected.id}`, C.info]]));
188
+ footerLines.push(buildPanelLine(width, [[` /incident latest /incident export ${selected.id} <path> --yes /incident capture ${selected.id} --yes`, C.info]]));
189
189
  footerLines.push(buildGuidanceLine(width, '/security', 'open the broader trust and incident posture control room', C));
190
190
 
191
191
  return this.renderList(width, height, {
@@ -81,7 +81,7 @@ export class LocalAuthPanel extends ScrollableListPanel<LocalAuthUser> {
81
81
  ...(issueMessages.length > 0
82
82
  ? issueMessages.map((issue) => buildPanelLine(width, [[` issue: ${issue}`.slice(0, Math.max(0, width)), C.warn]]))
83
83
  : [buildPanelLine(width, [[' local auth posture looks healthy.', C.good]])]),
84
- buildGuidanceLine(width, '/auth local rotate-password <user> <password>', 'rotate bootstrap/default credentials and revoke older sessions as needed', C),
84
+ buildGuidanceLine(width, '/auth local rotate-password <user> <password> --yes', 'rotate bootstrap/default credentials and revoke older sessions as needed', C),
85
85
  ], C),
86
86
  ];
87
87
 
@@ -104,8 +104,8 @@ export class LocalAuthPanel extends ScrollableListPanel<LocalAuthUser> {
104
104
  footerLines.push(
105
105
  ...buildDetailBlock(width, 'Selected user', [
106
106
  buildPanelLine(width, [[' username ', C.label], [selected.username, C.value], [' roles ', C.label], [formatRoles(selected.roles).slice(0, Math.max(0, width - 23)), C.info]]),
107
- buildPanelLine(width, [[` next: /auth local rotate-password ${selected.username} <password>`.slice(0, Math.max(0, width)), C.dim]]),
108
- buildPanelLine(width, [[` next: /auth local delete-user ${selected.username}`.slice(0, Math.max(0, width)), C.dim]]),
107
+ buildPanelLine(width, [[` next: /auth local rotate-password ${selected.username} <password> --yes`.slice(0, Math.max(0, width)), C.dim]]),
108
+ buildPanelLine(width, [[` next: /auth local delete-user ${selected.username} --yes`.slice(0, Math.max(0, width)), C.dim]]),
109
109
  ], C),
110
110
  );
111
111
  }
@@ -119,7 +119,7 @@ export class LocalAuthPanel extends ScrollableListPanel<LocalAuthUser> {
119
119
  ])),
120
120
  );
121
121
  }
122
- footerLines.push(buildPanelLine(width, [[' /auth local review /auth local add-user /auth local rotate-password /auth local revoke-session ', C.dim]]));
122
+ footerLines.push(buildPanelLine(width, [[' /auth local review mutations require --yes: add-user rotate-password revoke-session ', C.dim]]));
123
123
 
124
124
  return this.renderList(width, height, {
125
125
  title: 'Local Auth Control Room',