@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
@@ -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
  }
@@ -238,6 +238,7 @@ export class InputHandler {
238
238
  agentManager: uiServices.agents.agentManager,
239
239
  processManager: uiServices.shell.processManager,
240
240
  wrfcController: uiServices.agents.wrfcController,
241
+ agentEntries: 'hidden',
241
242
  });
242
243
  this.liveTailModal = new LiveTailModal({
243
244
  agentManager: uiServices.agents.agentManager,
@@ -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 {
@@ -19,24 +19,24 @@ export function runtimePortDiagnostic(
19
19
  if (status) {
20
20
  const reason = status.reason ? ` ${status.reason}` : '';
21
21
  if (status.mode === 'blocked') {
22
- return `The configured endpoint ${status.baseUrl} is occupied but was not usable by this TUI instance.${reason}`;
22
+ return `The configured endpoint ${status.baseUrl} is occupied but was not usable by this Agent instance.${reason}`;
23
23
  }
24
24
  if (status.mode === 'disabled') {
25
25
  return `The configured endpoint ${status.baseUrl} is disabled in the runtime service configuration.${reason}`;
26
26
  }
27
27
  if (status.mode === 'unavailable') {
28
- return `The configured endpoint ${status.baseUrl} is unavailable after startup or restart.${reason}`;
28
+ return `The configured endpoint ${status.baseUrl} is unavailable to Agent.${reason}`;
29
29
  }
30
30
  if (status.mode === 'external') {
31
31
  const version = status.version ? ` version ${status.version}` : '';
32
32
  return `An existing GoodVibes service was verified at ${status.baseUrl}${version}.`;
33
33
  }
34
- return `An embedded GoodVibes service is running at ${status.baseUrl}.`;
34
+ return `A GoodVibes service reports embedded mode at ${status.baseUrl}; Agent still treats daemon lifecycle as external.`;
35
35
  }
36
36
  if (portInUse) {
37
- return `The configured port ${binding.host}:${binding.port} is occupied after restart; another GoodVibes process, an overlapping restart, or another service may still own it.`;
37
+ return `The configured port ${binding.host}:${binding.port} is occupied; another GoodVibes process or another service may own it.`;
38
38
  }
39
- return `No process is listening on ${binding.host}:${binding.port} after restart.`;
39
+ return `No process is listening on ${binding.host}:${binding.port}.`;
40
40
  }
41
41
 
42
42
  export function getRuntimeEndpointStatus(
@@ -79,9 +79,9 @@ export function formatRuntimeActiveSuccessMessage(
79
79
  return `${label} is already running as a verified external GoodVibes service at ${status.baseUrl}${version}.`;
80
80
  }
81
81
  if (status?.mode === 'embedded') {
82
- return `${label} is running as an embedded service at ${status.baseUrl}.`;
82
+ return `${label} reports embedded mode at ${status.baseUrl}; Agent does not own that service lifecycle.`;
83
83
  }
84
84
  return endpoint === 'daemon'
85
- ? 'The GoodVibes daemon is running with the applied onboarding settings.'
86
- : 'The HTTP listener is running with the applied onboarding settings.';
85
+ ? 'The GoodVibes daemon is reachable to Agent.'
86
+ : 'The HTTP listener is reachable to Agent.';
87
87
  }
@@ -15,7 +15,7 @@ import {
15
15
  getExternalSurfaceAutoStartFieldId,
16
16
  isExternalSurfaceSelectedByDefault,
17
17
  } from './onboarding-wizard-external-surfaces.ts';
18
- import { buildGoodVibesSecretKey, buildGoodVibesSecretRef, isLoopbackAddress, isSecretReferenceValue } from './onboarding-wizard-helpers.ts';
18
+ import { buildGoodVibesSecretKey, buildGoodVibesSecretRef, isSecretReferenceValue } from './onboarding-wizard-helpers.ts';
19
19
  import type { OnboardingWizardController } from './onboarding-wizard.ts';
20
20
 
21
21
  export function buildOnboardingApplyRequest(controller: OnboardingWizardController): OnboardingApplyRequest {
@@ -203,7 +203,7 @@ function addCloudflareOperations(
203
203
  setConfig('cloudflare.workerCron', controller.getStringFieldValue('cloudflare.worker-cron', config?.workerCron ?? '*/5 * * * *'));
204
204
  setConfig('cloudflare.queueName', controller.getStringFieldValue('cloudflare.queue-name', config?.queueName ?? 'goodvibes-batch'));
205
205
  setConfig('cloudflare.deadLetterQueueName', controller.getStringFieldValue('cloudflare.dead-letter-queue-name', config?.deadLetterQueueName ?? 'goodvibes-batch-dlq'));
206
- setConfig('cloudflare.tunnelName', controller.getStringFieldValue('cloudflare.tunnel-name', config?.tunnelName ?? 'goodvibes-daemon'));
206
+ setConfig('cloudflare.tunnelName', controller.getStringFieldValue('cloudflare.tunnel-name', config?.tunnelName ?? 'goodvibes-agent-daemon'));
207
207
  setConfig('cloudflare.tunnelId', controller.getStringFieldValue('cloudflare.tunnel-id', config?.tunnelId ?? ''));
208
208
  setConfig('cloudflare.tunnelTokenRef', controller.getStringFieldValue('cloudflare.tunnel-token-ref', config?.tunnelTokenRef ?? ''));
209
209
  setConfig('cloudflare.accessAppId', controller.getStringFieldValue('cloudflare.access-app-id', config?.accessAppId ?? ''));
@@ -222,56 +222,16 @@ function addCloudflareOperations(
222
222
  }
223
223
 
224
224
  export function addNetworkOperations(
225
- controller: OnboardingWizardController,
226
- operations: OnboardingApplyOperation[],
227
- customNetwork: boolean,
228
- enabled: {
229
- readonly controlPlane: boolean;
230
- readonly controlPlaneRemote: boolean;
231
- readonly httpListener: boolean;
232
- readonly web: boolean;
233
- },
225
+ _controller: OnboardingWizardController,
226
+ _operations: OnboardingApplyOperation[],
227
+ _customNetwork: boolean,
228
+ _enabled: {
229
+ readonly controlPlane: boolean;
230
+ readonly controlPlaneRemote: boolean;
231
+ readonly httpListener: boolean;
232
+ readonly web: boolean;
233
+ },
234
234
  ): void {
235
- const setConfig = (
236
- key: Extract<OnboardingApplyOperation, { kind: 'set-config' }>['key'],
237
- value: unknown,
238
- ): void => {
239
- operations.push({ kind: 'set-config', key, value });
240
- };
241
- const networkFacingEnabled = {
242
- controlPlane: enabled.controlPlaneRemote,
243
- httpListener: enabled.httpListener,
244
- web: enabled.web,
245
- };
246
- const sharedIpDefault = controller.getSharedIpDefault(networkFacingEnabled);
247
- const sharedIp = controller.getBooleanFieldValue('network.shared-ip', sharedIpDefault);
248
- const sharedHost = controller.getStringFieldValue('network.shared-ip-address', controller.getSharedIpHostDefault(networkFacingEnabled)) || '0.0.0.0';
249
- const controlPlaneHost = sharedIp
250
- ? sharedHost
251
- : controller.getStringFieldValue('network.service-ip', controller.runtimeSnapshot?.bindSettings.controlPlane.host ?? '0.0.0.0');
252
- const httpListenerHost = sharedIp
253
- ? sharedHost
254
- : controller.getStringFieldValue('network.webhook-ip', controller.runtimeSnapshot?.bindSettings.httpListener.host ?? '0.0.0.0');
255
- const webHost = sharedIp
256
- ? sharedHost
257
- : controller.getStringFieldValue('network.browser-ip', controller.runtimeSnapshot?.bindSettings.web.host ?? '0.0.0.0');
258
-
259
- if (enabled.controlPlane) {
260
- setConfig('controlPlane.hostMode', enabled.controlPlaneRemote ? (customNetwork ? 'custom' : 'network') : 'local');
261
- setConfig('controlPlane.host', enabled.controlPlaneRemote ? (customNetwork ? controlPlaneHost : '0.0.0.0') : '127.0.0.1');
262
- setConfig('controlPlane.port', controller.getPortFieldValue('network.service-port', controller.runtimeSnapshot?.bindSettings.controlPlane.port ?? 3421));
263
- setConfig('controlPlane.allowRemote', enabled.controlPlaneRemote && (customNetwork ? !isLoopbackAddress(controlPlaneHost) : true));
264
- }
265
-
266
- if (enabled.httpListener) {
267
- setConfig('httpListener.hostMode', customNetwork ? 'custom' : 'network');
268
- setConfig('httpListener.host', customNetwork ? httpListenerHost : '0.0.0.0');
269
- setConfig('httpListener.port', controller.getPortFieldValue('network.webhook-port', controller.runtimeSnapshot?.bindSettings.httpListener.port ?? 3422));
270
- }
271
-
272
- if (enabled.web) {
273
- setConfig('web.hostMode', customNetwork ? 'custom' : 'network');
274
- setConfig('web.host', customNetwork ? webHost : '0.0.0.0');
275
- setConfig('web.port', controller.getPortFieldValue('network.browser-port', controller.runtimeSnapshot?.bindSettings.web.port ?? 3423));
276
- }
235
+ // Agent onboarding intentionally never mutates daemon/listener/web bind posture.
236
+ // Network fields are advisory for the external daemon owner.
277
237
  }
@@ -26,7 +26,7 @@ export function buildCloudflareStep(controller: OnboardingWizardController): Onb
26
26
  const bind = controller.runtimeSnapshot?.bindSettings.controlPlane;
27
27
  const defaultDaemonBaseUrl = normalizeText(config?.daemonBaseUrl)
28
28
  || `http://${bind?.host && bind.host !== '0.0.0.0' && bind.host !== '::' ? bind.host : '127.0.0.1'}:${bind?.port ?? 3421}`;
29
- const resultMessage = controller.textState.get('cloudflare.action-status') ?? 'No Cloudflare daemon action has run in this wizard session.';
29
+ const resultMessage = controller.textState.get('cloudflare.action-status') ?? 'No Cloudflare action has run in this wizard session.';
30
30
  const fields: OnboardingWizardFieldDefinition[] = [
31
31
  {
32
32
  kind: 'checklist',
@@ -96,7 +96,7 @@ export function buildCloudflareStep(controller: OnboardingWizardController): Onb
96
96
  kind: 'text',
97
97
  id: 'cloudflare.bootstrap-env-name',
98
98
  label: 'Bootstrap token environment variable',
99
- hint: 'The TUI reads this environment variable once and passes the value to the SDK token-create route. It is not persisted.',
99
+ hint: 'Agent reads this environment variable once and passes the value to the SDK token-create route. It is not persisted.',
100
100
  placeholder: 'GOODVIBES_CLOUDFLARE_BOOTSTRAP_TOKEN',
101
101
  defaultValue: 'GOODVIBES_CLOUDFLARE_BOOTSTRAP_TOKEN',
102
102
  });
@@ -227,8 +227,8 @@ export function buildCloudflareStep(controller: OnboardingWizardController): Onb
227
227
  id: 'cloudflare.tunnel-name',
228
228
  label: 'Tunnel name',
229
229
  hint: 'Cloudflare Tunnel name to create or reuse.',
230
- placeholder: 'goodvibes-daemon',
231
- defaultValue: config?.tunnelName || 'goodvibes-daemon',
230
+ placeholder: 'goodvibes-agent-daemon',
231
+ defaultValue: config?.tunnelName || 'goodvibes-agent-daemon',
232
232
  },
233
233
  {
234
234
  kind: 'text',
@@ -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
 
@@ -176,7 +171,7 @@ export function buildCloudflareProvisionRequest(controller: OnboardingWizardCont
176
171
  daemonHostname: controller.getStringFieldValue('cloudflare.daemon-hostname', controller.runtimeSnapshot?.config.cloudflare.daemonHostname ?? ''),
177
172
  queueName: controller.getStringFieldValue('cloudflare.queue-name', controller.runtimeSnapshot?.config.cloudflare.queueName ?? 'goodvibes-batch'),
178
173
  deadLetterQueueName: controller.getStringFieldValue('cloudflare.dead-letter-queue-name', controller.runtimeSnapshot?.config.cloudflare.deadLetterQueueName ?? 'goodvibes-batch-dlq'),
179
- tunnelName: controller.getStringFieldValue('cloudflare.tunnel-name', controller.runtimeSnapshot?.config.cloudflare.tunnelName ?? 'goodvibes-daemon'),
174
+ tunnelName: controller.getStringFieldValue('cloudflare.tunnel-name', controller.runtimeSnapshot?.config.cloudflare.tunnelName ?? 'goodvibes-agent-daemon'),
180
175
  tunnelId: controller.getStringFieldValue('cloudflare.tunnel-id', controller.runtimeSnapshot?.config.cloudflare.tunnelId ?? ''),
181
176
  tunnelServiceUrl: controller.getStringFieldValue('cloudflare.tunnel-service-url', ''),
182
177
  tunnelTokenRef: controller.getStringFieldValue('cloudflare.tunnel-token-ref', controller.runtimeSnapshot?.config.cloudflare.tunnelTokenRef ?? ''),