@pellux/goodvibes-agent 0.1.117 → 1.0.1

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 (255) hide show
  1. package/CHANGELOG.md +20 -4
  2. package/README.md +35 -14
  3. package/bin/goodvibes-agent.ts +16 -2
  4. package/dist/package/main.js +176093 -171065
  5. package/docs/README.md +11 -5
  6. package/docs/channels-remote-and-api.md +50 -0
  7. package/docs/connected-host.md +3 -3
  8. package/docs/getting-started.md +29 -15
  9. package/docs/knowledge-artifacts-and-multimodal.md +91 -0
  10. package/docs/project-planning.md +79 -0
  11. package/docs/providers-and-routing.md +46 -0
  12. package/docs/release-and-publishing.md +44 -9
  13. package/docs/tools-and-commands.md +123 -0
  14. package/docs/voice-and-live-tts.md +51 -0
  15. package/package.json +2 -5
  16. package/src/agent/channel-delivery.ts +201 -0
  17. package/src/agent/media-generation.ts +159 -0
  18. package/src/agent/memory-prompt.ts +0 -1
  19. package/src/agent/note-registry.ts +329 -0
  20. package/src/agent/operator-actions.ts +343 -0
  21. package/src/agent/persona-registry.ts +15 -14
  22. package/src/agent/record-labels.ts +107 -0
  23. package/src/agent/reminder-schedule-format.ts +33 -24
  24. package/src/agent/reminder-schedule.ts +26 -25
  25. package/src/agent/routine-registry.ts +13 -12
  26. package/src/agent/routine-schedule-args.ts +2 -1
  27. package/src/agent/routine-schedule-format.ts +77 -53
  28. package/src/agent/routine-schedule-promotion.ts +34 -32
  29. package/src/agent/routine-schedule-receipts.ts +28 -26
  30. package/src/agent/runtime-profile-starters.ts +2 -2
  31. package/src/agent/runtime-profile.ts +18 -17
  32. package/src/agent/skill-registry.ts +25 -24
  33. package/src/cli/agent-knowledge-args.ts +5 -1
  34. package/src/cli/agent-knowledge-command.ts +39 -33
  35. package/src/cli/agent-knowledge-format.ts +80 -67
  36. package/src/cli/agent-knowledge-methods.ts +1 -1
  37. package/src/cli/agent-knowledge-runtime.ts +32 -26
  38. package/src/cli/bundle-command.ts +13 -8
  39. package/src/cli/config-overrides.ts +37 -36
  40. package/src/cli/external-runtime.ts +10 -4
  41. package/src/cli/help.ts +29 -11
  42. package/src/cli/local-library-command.ts +159 -73
  43. package/src/cli/management-commands.ts +98 -62
  44. package/src/cli/management.ts +52 -26
  45. package/src/cli/memory-command.ts +66 -32
  46. package/src/cli/parser.ts +37 -24
  47. package/src/cli/profiles-command.ts +52 -35
  48. package/src/cli/provider-auth-routes.ts +2 -1
  49. package/src/cli/routines-command.ts +59 -39
  50. package/src/cli/service-posture.ts +6 -6
  51. package/src/cli/status.ts +46 -121
  52. package/src/core/conversation-message-snapshot.ts +131 -0
  53. package/src/input/agent-workspace-activation.ts +33 -7
  54. package/src/input/agent-workspace-basic-command-editor-submission.ts +7 -3
  55. package/src/input/agent-workspace-basic-command-editors.ts +30 -10
  56. package/src/input/agent-workspace-categories.ts +276 -64
  57. package/src/input/agent-workspace-channel-command-editor-submission.ts +34 -0
  58. package/src/input/agent-workspace-channel-command-editors.ts +23 -5
  59. package/src/input/agent-workspace-channels.ts +35 -2
  60. package/src/input/agent-workspace-command-editor.ts +18 -2
  61. package/src/input/agent-workspace-config-reader.ts +16 -0
  62. package/src/input/agent-workspace-delegation-editor-submission.ts +1 -1
  63. package/src/input/agent-workspace-editors.ts +140 -2
  64. package/src/input/agent-workspace-knowledge-query-editor.ts +1 -1
  65. package/src/input/agent-workspace-knowledge-url-editor.ts +4 -11
  66. package/src/input/agent-workspace-learned-behavior.ts +2 -2
  67. package/src/input/agent-workspace-library-command-editor-submission.ts +1 -1
  68. package/src/input/agent-workspace-library-command-editors.ts +2 -2
  69. package/src/input/agent-workspace-local-operations.ts +218 -0
  70. package/src/input/agent-workspace-local-selection.ts +75 -0
  71. package/src/input/agent-workspace-media-command-editor-submission.ts +62 -0
  72. package/src/input/agent-workspace-media-command-editors.ts +27 -0
  73. package/src/input/agent-workspace-memory-command-editors.ts +1 -1
  74. package/src/input/agent-workspace-memory-editor.ts +2 -2
  75. package/src/input/agent-workspace-navigation.ts +38 -2
  76. package/src/input/agent-workspace-notify-editor-submission.ts +16 -2
  77. package/src/input/agent-workspace-operations-command-editor-submission.ts +63 -1
  78. package/src/input/agent-workspace-operations-command-editors.ts +80 -3
  79. package/src/input/agent-workspace-panel-route.ts +43 -0
  80. package/src/input/agent-workspace-reminder-schedule-editor.ts +1 -1
  81. package/src/input/agent-workspace-routine-schedule-editor.ts +2 -2
  82. package/src/input/agent-workspace-search.ts +169 -0
  83. package/src/input/agent-workspace-setup.ts +22 -11
  84. package/src/input/agent-workspace-skill-bundle-command-editor-submission.ts +6 -6
  85. package/src/input/agent-workspace-skill-bundle-command-editors.ts +10 -10
  86. package/src/input/agent-workspace-snapshot.ts +112 -13
  87. package/src/input/agent-workspace-task-command-editors.ts +4 -4
  88. package/src/input/agent-workspace-token.ts +18 -2
  89. package/src/input/agent-workspace-types.ts +92 -4
  90. package/src/input/agent-workspace-voice-media.ts +3 -6
  91. package/src/input/agent-workspace-web-research-editor.ts +104 -0
  92. package/src/input/agent-workspace.ts +136 -208
  93. package/src/input/command-registry.ts +4 -1
  94. package/src/input/commands/agent-local-library-args.ts +52 -0
  95. package/src/input/commands/agent-runtime-profile-runtime.ts +45 -41
  96. package/src/input/commands/agent-skills-runtime.ts +87 -99
  97. package/src/input/commands/agent-workspace-runtime.ts +23 -7
  98. package/src/input/commands/brief-runtime.ts +25 -24
  99. package/src/input/commands/channels-runtime.ts +145 -31
  100. package/src/input/commands/delegation-runtime.ts +29 -23
  101. package/src/input/commands/experience-runtime.ts +6 -4
  102. package/src/input/commands/guidance-runtime.ts +4 -4
  103. package/src/input/commands/health-runtime.ts +140 -115
  104. package/src/input/commands/knowledge.ts +57 -56
  105. package/src/input/commands/local-provider-runtime.ts +36 -18
  106. package/src/input/commands/local-runtime.ts +138 -16
  107. package/src/input/commands/local-setup-review.ts +7 -7
  108. package/src/input/commands/mcp-runtime.ts +118 -50
  109. package/src/input/commands/notify-runtime.ts +38 -9
  110. package/src/input/commands/operator-actions-runtime.ts +138 -0
  111. package/src/input/commands/operator-runtime.ts +6 -17
  112. package/src/input/commands/personas-runtime.ts +49 -59
  113. package/src/input/commands/planning-runtime.ts +1 -1
  114. package/src/input/commands/platform-access-runtime.ts +29 -29
  115. package/src/input/commands/provider-accounts-runtime.ts +60 -39
  116. package/src/input/commands/qrcode-runtime.ts +45 -6
  117. package/src/input/commands/recall-bundle.ts +11 -11
  118. package/src/input/commands/recall-capture.ts +13 -11
  119. package/src/input/commands/recall-query.ts +29 -21
  120. package/src/input/commands/recall-review.ts +2 -1
  121. package/src/input/commands/routines-runtime.ts +63 -72
  122. package/src/input/commands/schedule-runtime.ts +33 -20
  123. package/src/input/commands/security-runtime.ts +4 -4
  124. package/src/input/commands/session-content.ts +80 -78
  125. package/src/input/commands/session-workflow.ts +132 -93
  126. package/src/input/commands/session.ts +3 -174
  127. package/src/input/commands/shell-core.ts +32 -17
  128. package/src/input/commands/subscription-runtime.ts +53 -179
  129. package/src/input/commands/tasks-runtime.ts +20 -20
  130. package/src/input/commands/work-plan-runtime.ts +33 -8
  131. package/src/input/commands.ts +2 -2
  132. package/src/input/feed-context-factory.ts +2 -1
  133. package/src/input/file-picker.ts +3 -2
  134. package/src/input/handler-command-route.ts +4 -7
  135. package/src/input/handler-content-actions.ts +89 -1
  136. package/src/input/handler-feed-routes.ts +19 -12
  137. package/src/input/handler-feed.ts +6 -3
  138. package/src/input/handler-interactions.ts +7 -5
  139. package/src/input/handler-modal-stack.ts +3 -3
  140. package/src/input/handler-onboarding.ts +24 -80
  141. package/src/input/handler-shortcuts.ts +15 -4
  142. package/src/input/handler.ts +47 -17
  143. package/src/input/input-history.ts +5 -6
  144. package/src/input/keybindings.ts +22 -11
  145. package/src/input/onboarding/onboarding-wizard-apply.ts +13 -0
  146. package/src/input/onboarding/onboarding-wizard-constants.ts +1 -0
  147. package/src/input/onboarding/onboarding-wizard-operator-steps.ts +147 -57
  148. package/src/input/onboarding/onboarding-wizard-steps.ts +46 -29
  149. package/src/input/onboarding/onboarding-wizard-types.ts +3 -1
  150. package/src/input/onboarding/onboarding-wizard.ts +68 -0
  151. package/src/input/profile-picker-modal.ts +31 -12
  152. package/src/input/session-picker-modal.ts +22 -4
  153. package/src/input/settings-modal-behavior.ts +0 -3
  154. package/src/input/settings-modal-subscriptions.ts +3 -3
  155. package/src/input/settings-modal-types.ts +2 -13
  156. package/src/input/settings-modal.ts +6 -52
  157. package/src/input/submission-intent.ts +0 -1
  158. package/src/input/submission-router.ts +3 -3
  159. package/src/main.ts +18 -8
  160. package/src/panels/approval-panel.ts +8 -8
  161. package/src/panels/automation-control-panel.ts +2 -2
  162. package/src/panels/base-panel.ts +1 -1
  163. package/src/panels/builtin/agent.ts +1 -1
  164. package/src/panels/builtin/operations.ts +1 -10
  165. package/src/panels/builtin/session.ts +9 -9
  166. package/src/panels/builtin/shared.ts +2 -2
  167. package/src/panels/cost-tracker-panel.ts +1 -1
  168. package/src/panels/docs-panel.ts +5 -3
  169. package/src/panels/index.ts +0 -1
  170. package/src/panels/knowledge-panel.ts +6 -5
  171. package/src/panels/memory-panel.ts +7 -6
  172. package/src/panels/panel-list-panel.ts +36 -80
  173. package/src/panels/project-planning-panel.ts +18 -11
  174. package/src/panels/provider-account-snapshot.ts +51 -25
  175. package/src/panels/provider-accounts-panel.ts +33 -18
  176. package/src/panels/provider-health-domains.ts +45 -4
  177. package/src/panels/provider-health-panel.ts +5 -4
  178. package/src/panels/qr-panel.ts +1 -1
  179. package/src/panels/schedule-panel.ts +9 -17
  180. package/src/panels/security-panel.ts +8 -8
  181. package/src/panels/session-browser-panel.ts +10 -10
  182. package/src/panels/subscription-panel.ts +3 -3
  183. package/src/panels/system-messages-panel.ts +1 -1
  184. package/src/panels/tasks-panel.ts +20 -13
  185. package/src/panels/tool-inspector-panel.ts +19 -12
  186. package/src/panels/work-plan-panel.ts +5 -5
  187. package/src/planning/project-planning-coordinator.ts +1 -1
  188. package/src/provider-auth-route-display.ts +9 -0
  189. package/src/renderer/agent-workspace-style.ts +34 -0
  190. package/src/renderer/agent-workspace.ts +228 -52
  191. package/src/renderer/autocomplete-overlay.ts +25 -6
  192. package/src/renderer/bookmark-modal.ts +19 -4
  193. package/src/renderer/buffer.ts +4 -2
  194. package/src/renderer/context-inspector.ts +50 -13
  195. package/src/renderer/diff.ts +1 -1
  196. package/src/renderer/file-picker-overlay.ts +19 -6
  197. package/src/renderer/help-overlay.ts +106 -33
  198. package/src/renderer/history-search-overlay.ts +19 -4
  199. package/src/renderer/live-tail-modal.ts +27 -5
  200. package/src/renderer/mcp-workspace.ts +164 -50
  201. package/src/renderer/model-picker-overlay.ts +58 -2
  202. package/src/renderer/model-workspace.ts +104 -20
  203. package/src/renderer/process-modal.ts +27 -6
  204. package/src/renderer/profile-picker-modal.ts +20 -4
  205. package/src/renderer/search-overlay.ts +25 -5
  206. package/src/renderer/selection-modal-overlay.ts +46 -14
  207. package/src/renderer/session-picker-modal.ts +18 -1
  208. package/src/renderer/settings-modal-helpers.ts +2 -40
  209. package/src/renderer/settings-modal.ts +83 -50
  210. package/src/renderer/tool-call.ts +20 -11
  211. package/src/runtime/agent-runtime-events.ts +6 -6
  212. package/src/runtime/bootstrap-command-context.ts +7 -1
  213. package/src/runtime/bootstrap-command-parts.ts +9 -7
  214. package/src/runtime/bootstrap-core.ts +19 -1
  215. package/src/runtime/bootstrap-hook-bridge.ts +7 -18
  216. package/src/runtime/bootstrap-shell.ts +4 -4
  217. package/src/runtime/bootstrap.ts +31 -22
  218. package/src/runtime/connected-host-auth.ts +7 -2
  219. package/src/runtime/diagnostics/panels/index.ts +2 -2
  220. package/src/runtime/diagnostics/panels/policy.ts +7 -7
  221. package/src/runtime/index.ts +2 -1
  222. package/src/runtime/onboarding/apply-file-helpers.ts +66 -0
  223. package/src/runtime/onboarding/apply.ts +80 -79
  224. package/src/runtime/onboarding/derivation.ts +5 -8
  225. package/src/runtime/onboarding/snapshot.ts +0 -15
  226. package/src/runtime/onboarding/types.ts +8 -19
  227. package/src/runtime/onboarding/verify.ts +32 -10
  228. package/src/runtime/operator-token-cleanup.ts +1 -1
  229. package/src/runtime/services.ts +91 -22
  230. package/src/runtime/store/selectors/index.ts +3 -3
  231. package/src/runtime/store/state.ts +1 -4
  232. package/src/runtime/surface-feature-flags.ts +41 -6
  233. package/src/runtime/ui-read-models.ts +3 -4
  234. package/src/runtime/ui-services.ts +6 -6
  235. package/src/shell/blocking-input.ts +2 -1
  236. package/src/shell/ui-openers.ts +3 -13
  237. package/src/tools/agent-analysis-registry-policy.ts +0 -1
  238. package/src/tools/agent-channel-send-tool.ts +133 -0
  239. package/src/tools/agent-context-policy.ts +1 -1
  240. package/src/tools/agent-knowledge-ingest-tool.ts +405 -0
  241. package/src/tools/agent-knowledge-tool.ts +170 -0
  242. package/src/tools/agent-local-registry-tool.ts +269 -69
  243. package/src/tools/agent-media-generate-tool.ts +133 -0
  244. package/src/tools/agent-notify-tool.ts +143 -0
  245. package/src/tools/agent-operator-action-tool.ts +137 -0
  246. package/src/tools/agent-operator-briefing-tool.ts +217 -0
  247. package/src/tools/agent-reminder-schedule-tool.ts +237 -0
  248. package/src/tools/agent-tool-policy-guard.ts +8 -8
  249. package/src/tools/agent-work-plan-tool.ts +256 -0
  250. package/src/version.ts +1 -1
  251. package/src/input/commands/policy-dispatch.ts +0 -339
  252. package/src/input/commands/policy.ts +0 -13
  253. package/src/panels/panel-picker.ts +0 -105
  254. package/src/panels/policy-panel.ts +0 -308
  255. package/src/renderer/panel-picker-overlay.ts +0 -202
@@ -3,15 +3,25 @@ import type { McpConfigScope, McpReloadResult, McpServerConfig } from '@pellux/g
3
3
  import { requireMcpApi, requireShellPaths } from './runtime-services.ts';
4
4
  import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
5
5
  import { requireYesFlag, stripYesFlag } from './confirmation.ts';
6
+ import { quoteSlashCommandArg } from '../slash-command-parser.ts';
6
7
 
7
8
  const MCP_ROLES = ['general', 'docs', 'filesystem', 'git', 'database', 'browser', 'automation', 'ops', 'remote'] as const;
8
9
  const MCP_TRUST_MODES = ['constrained', 'ask-on-risk', 'allow-all', 'blocked'] as const;
10
+ const MCP_COMMAND_TRUST_MODES = ['constrained', 'ask-on-risk', 'blocked'] as const;
11
+ const MCP_ADD_COMMAND_USAGE = '/mcp add <name> <command> ... --yes; options: --scope project|global, --role <role>, --trust constrained|ask-on-risk|blocked, --env KEY=VALUE, --path <path>, --host <host>';
12
+ const MCP_TRUST_COMMAND_USAGE = '/mcp trust <server> <constrained|ask-on-risk|blocked> --yes';
13
+ const MCP_ROLE_COMMAND_USAGE = '/mcp role <server> <general|docs|filesystem|git|database|browser|automation|ops|remote> --yes';
9
14
 
10
15
  interface ParsedMcpAddArgs {
11
16
  readonly scope: McpConfigScope;
12
17
  readonly server: McpServerConfig;
13
18
  }
14
19
 
20
+ type McpSecurityServer = ReturnType<NonNullable<NonNullable<CommandContext['clients']>['mcpApi']>['listServerSecurity']>[number];
21
+ type McpDisplayTrustMode = NonNullable<McpServerConfig['trustMode']> | McpSecurityServer['trustMode'];
22
+ type McpDisplayRole = NonNullable<McpServerConfig['role']> | McpSecurityServer['role'];
23
+ type McpDisplayFreshness = McpSecurityServer['schemaFreshness'];
24
+
15
25
  function isMcpRole(value: string): value is NonNullable<McpServerConfig['role']> {
16
26
  return MCP_ROLES.includes(value as NonNullable<McpServerConfig['role']>);
17
27
  }
@@ -20,10 +30,52 @@ function isMcpTrustMode(value: string): value is NonNullable<McpServerConfig['tr
20
30
  return MCP_TRUST_MODES.includes(value as NonNullable<McpServerConfig['trustMode']>);
21
31
  }
22
32
 
33
+ function isMcpCommandTrustMode(value: string): value is Exclude<NonNullable<McpServerConfig['trustMode']>, 'allow-all'> {
34
+ return MCP_COMMAND_TRUST_MODES.includes(value as Exclude<NonNullable<McpServerConfig['trustMode']>, 'allow-all'>);
35
+ }
36
+
23
37
  function isMcpScope(value: string): value is McpConfigScope {
24
38
  return value === 'project' || value === 'global';
25
39
  }
26
40
 
41
+ function stripMcpAddYesFlag(args: readonly string[]): { readonly rest: readonly string[]; readonly yes: boolean } {
42
+ const rest: string[] = [];
43
+ let yes = false;
44
+ let passthrough = false;
45
+ for (const token of args) {
46
+ if (passthrough) {
47
+ rest.push(token);
48
+ continue;
49
+ }
50
+ if (token === '--') {
51
+ passthrough = true;
52
+ rest.push(token);
53
+ continue;
54
+ }
55
+ if (token === '--yes') {
56
+ yes = true;
57
+ continue;
58
+ }
59
+ rest.push(token);
60
+ }
61
+ return { rest, yes };
62
+ }
63
+
64
+ function formatMcpTrustMode(mode: McpDisplayTrustMode): string {
65
+ if (mode === 'ask-on-risk') return 'ask on risky actions';
66
+ if (mode === 'allow-all') return 'allow all actions';
67
+ return mode.replace(/[_-]+/g, ' ');
68
+ }
69
+
70
+ function formatMcpFreshness(value: McpDisplayFreshness): string {
71
+ if (value === 'quarantined') return 'needs review';
72
+ return value.replace(/[_-]+/g, ' ');
73
+ }
74
+
75
+ function formatMcpRole(value: McpDisplayRole): string {
76
+ return value.replace(/[_-]+/g, ' ');
77
+ }
78
+
27
79
  function validateServerName(name: string): string | null {
28
80
  if (!name.trim()) return 'MCP server name is required.';
29
81
  if (!/^[a-zA-Z0-9._-]+$/.test(name)) {
@@ -44,7 +96,7 @@ function parseAddServerArgs(args: string[]): ParsedMcpAddArgs {
44
96
  const name = args[1]?.trim();
45
97
  const command = args[2]?.trim();
46
98
  if (!name || !command) {
47
- throw new Error('Usage: /mcp add <name> <command> [args...] [--scope project|global] [--role <role>] [--trust <mode>] [--env KEY=VALUE] [--path <path>] [--host <host>]');
99
+ throw new Error(`Usage: ${MCP_ADD_COMMAND_USAGE}`);
48
100
  }
49
101
  const nameError = validateServerName(name);
50
102
  if (nameError) throw new Error(nameError);
@@ -71,7 +123,7 @@ function parseAddServerArgs(args: string[]): ParsedMcpAddArgs {
71
123
  }
72
124
  if (token === '--role') {
73
125
  const value = readFlagValue(tokens, index, token);
74
- if (!isMcpRole(value)) throw new Error(`Invalid MCP role "${value}". Expected one of: ${MCP_ROLES.join(', ')}`);
126
+ if (!isMcpRole(value)) throw new Error(`Invalid MCP role "${value}". Expected one of ${MCP_ROLES.join(', ')}.`);
75
127
  role = value;
76
128
  index += 1;
77
129
  continue;
@@ -85,7 +137,10 @@ function parseAddServerArgs(args: string[]): ParsedMcpAddArgs {
85
137
  }
86
138
  if (token === '--trust') {
87
139
  const value = readFlagValue(tokens, index, token);
88
- if (!isMcpTrustMode(value)) throw new Error(`Invalid MCP trust mode "${value}". Expected one of: ${MCP_TRUST_MODES.join(', ')}`);
140
+ if (!isMcpTrustMode(value)) throw new Error(`Invalid MCP trust mode "${value}". Expected one of ${MCP_TRUST_MODES.join(', ')}.`);
141
+ if (!isMcpCommandTrustMode(value)) {
142
+ throw new Error(`Use /settings -> MCP to explicitly enable allow-all.\nUsage: ${MCP_ADD_COMMAND_USAGE}`);
143
+ }
89
144
  trustMode = value;
90
145
  index += 1;
91
146
  continue;
@@ -142,7 +197,7 @@ export function registerMcpRuntimeCommands(registry: CommandRegistry): void {
142
197
  async handler(args, ctx) {
143
198
  const mcpApi = requireMcpApi(ctx);
144
199
  const listServerSecurity = () => mcpApi.listServerSecurity();
145
- const confirmation = stripYesFlag(args);
200
+ const confirmation = args[0] === 'add' ? stripMcpAddYesFlag(args) : stripYesFlag(args);
146
201
  const commandArgs = [...confirmation.rest];
147
202
  const subcommand = commandArgs[0];
148
203
  if (!subcommand && ctx.openMcpWorkspace) {
@@ -157,7 +212,7 @@ export function registerMcpRuntimeCommands(registry: CommandRegistry): void {
157
212
  ` servers: ${servers.length}`,
158
213
  ` connected: ${servers.filter((server) => server.connected).length}`,
159
214
  ` auth or repair attention: ${authRequired.length}`,
160
- ...servers.map((server) => ` ${server.name} trust=${server.trustMode} role=${server.role} freshness=${server.schemaFreshness} connected=${server.connected ? 'yes' : 'no'}`),
215
+ ...servers.map((server) => ` ${server.name} trust=${formatMcpTrustMode(server.trustMode)} role=${formatMcpRole(server.role)} status=${formatMcpFreshness(server.schemaFreshness)} connected=${server.connected ? 'yes' : 'no'}`),
161
216
  ' next: /mcp auth-review',
162
217
  ' next: /mcp repair <server>',
163
218
  ].join('\n'));
@@ -218,7 +273,7 @@ export function registerMcpRuntimeCommands(registry: CommandRegistry): void {
218
273
  ? [
219
274
  'MCP Auth Review',
220
275
  ...needingAttention.map((server) => (
221
- ` ${server.name} connected=${server.connected ? 'yes' : 'no'} freshness=${server.schemaFreshness} trust=${server.trustMode}`
276
+ ` ${server.name} connected ${server.connected ? 'yes' : 'no'} status ${formatMcpFreshness(server.schemaFreshness)} trust ${formatMcpTrustMode(server.trustMode)}`
222
277
  )),
223
278
  ' next: /auth review',
224
279
  ' next: /mcp repair <server>',
@@ -233,13 +288,13 @@ export function registerMcpRuntimeCommands(registry: CommandRegistry): void {
233
288
  const selected = serverName ? servers.find((server) => server.name === serverName) : servers.find((server) => !server.connected || server.schemaFreshness === 'quarantined');
234
289
  if (!selected) {
235
290
  ctx.print(serverName
236
- ? `Unknown MCP server: ${serverName}`
291
+ ? `Unknown MCP server ${serverName}`
237
292
  : 'MCP Repair\n No MCP server currently needs repair.');
238
293
  return;
239
294
  }
240
295
  const nextSteps = [
241
296
  selected.schemaFreshness === 'quarantined'
242
- ? `/mcp quarantine ${selected.name} approve operator --yes`
297
+ ? `/mcp quarantine ${quoteSlashCommandArg(selected.name)} approve operator --yes`
243
298
  : null,
244
299
  !selected.connected ? '/auth review' : null,
245
300
  '/mcp review',
@@ -248,9 +303,9 @@ export function registerMcpRuntimeCommands(registry: CommandRegistry): void {
248
303
  ctx.print([
249
304
  `MCP Repair: ${selected.name}`,
250
305
  ` connected: ${selected.connected ? 'yes' : 'no'}`,
251
- ` trust: ${selected.trustMode}`,
252
- ` role: ${selected.role}`,
253
- ` freshness: ${selected.schemaFreshness}`,
306
+ ` trust: ${formatMcpTrustMode(selected.trustMode)}`,
307
+ ` role: ${formatMcpRole(selected.role)}`,
308
+ ` status: ${formatMcpFreshness(selected.schemaFreshness)}`,
254
309
  ...(selected.quarantineReason ? [` quarantine: ${selected.quarantineReason}`] : []),
255
310
  ...(selected.quarantineDetail ? [` detail: ${selected.quarantineDetail}`] : []),
256
311
  ' next:',
@@ -261,48 +316,58 @@ export function registerMcpRuntimeCommands(registry: CommandRegistry): void {
261
316
 
262
317
  if (subcommand === 'trust') {
263
318
  const serverName = commandArgs[1];
264
- const mode = commandArgs[2] as 'constrained' | 'ask-on-risk' | 'allow-all' | 'blocked' | undefined;
265
- if (serverName && mode) {
319
+ const requestedMode = commandArgs[2];
320
+ if (serverName && requestedMode) {
321
+ if (!isMcpTrustMode(requestedMode)) {
322
+ ctx.print(`Invalid MCP trust mode "${requestedMode}". Expected constrained, ask-on-risk, blocked, or allow-all.\nUsage: ${MCP_TRUST_COMMAND_USAGE}\nUse /settings -> MCP to explicitly enable allow-all.`);
323
+ return;
324
+ }
325
+ const mode = requestedMode;
266
326
  if (mode === 'allow-all') {
267
327
  ctx.print(`Use /settings → MCP to explicitly enable allow-all for ${serverName}. Direct command escalation is blocked.`);
268
328
  ctx.openSettingsModal?.();
269
329
  return;
270
330
  }
271
331
  if (!confirmation.yes) {
272
- requireYesFlag(ctx, `change MCP trust mode for ${serverName}`, '/mcp trust <server> <constrained|ask-on-risk|blocked> --yes');
332
+ requireYesFlag(ctx, `change MCP trust mode for ${serverName}`, MCP_TRUST_COMMAND_USAGE);
273
333
  return;
274
334
  }
275
335
  mcpApi.setServerTrustMode(serverName, mode);
276
- ctx.print(`Updated MCP trust mode for ${serverName} to ${mode}.`);
336
+ ctx.print(`Updated MCP trust mode for ${serverName} to ${formatMcpTrustMode(mode)}.`);
277
337
  return;
278
338
  }
279
- if (serverName || mode) {
280
- ctx.print('Usage: /mcp trust <server> <constrained|ask-on-risk|blocked>\nUse /settings MCP to explicitly enable allow-all.');
339
+ if (serverName || requestedMode) {
340
+ ctx.print(`Usage: ${MCP_TRUST_COMMAND_USAGE}\nUse /settings -> MCP to explicitly enable allow-all.`);
281
341
  return;
282
342
  }
283
343
  }
284
344
 
285
345
  if (subcommand === 'role') {
286
346
  const serverName = commandArgs[1];
287
- const role = commandArgs[2] as 'general' | 'docs' | 'filesystem' | 'git' | 'database' | 'browser' | 'automation' | 'ops' | 'remote' | undefined;
288
- if (serverName && role) {
347
+ const requestedRole = commandArgs[2];
348
+ if (serverName && requestedRole) {
349
+ if (!isMcpRole(requestedRole)) {
350
+ ctx.print(`Invalid MCP role "${requestedRole}". Expected one of ${MCP_ROLES.join(', ')}.\nUsage: ${MCP_ROLE_COMMAND_USAGE}`);
351
+ return;
352
+ }
353
+ const role = requestedRole;
289
354
  if (!confirmation.yes) {
290
- requireYesFlag(ctx, `change MCP role for ${serverName}`, '/mcp role <server> <general|docs|filesystem|git|database|browser|automation|ops|remote> --yes');
355
+ requireYesFlag(ctx, `change MCP role for ${serverName}`, MCP_ROLE_COMMAND_USAGE);
291
356
  return;
292
357
  }
293
358
  mcpApi.setServerRole(serverName, role);
294
- ctx.print(`Updated MCP role for ${serverName} to ${role}.`);
359
+ ctx.print(`Updated MCP role for ${serverName} to ${formatMcpRole(role)}.`);
295
360
  return;
296
361
  }
297
- if (serverName || role) {
298
- ctx.print('Usage: /mcp role <server> <general|docs|filesystem|git|database|browser|automation|ops|remote>');
362
+ if (serverName || requestedRole) {
363
+ ctx.print(`Usage: ${MCP_ROLE_COMMAND_USAGE}`);
299
364
  return;
300
365
  }
301
366
  }
302
367
 
303
368
  if (subcommand === 'add') {
304
369
  if (!confirmation.yes) {
305
- requireYesFlag(ctx, 'add or update an MCP server config', '/mcp add <name> <command> [args...] [--scope project|global] [--role <role>] [--trust <mode>] --yes');
370
+ requireYesFlag(ctx, 'add or update an MCP server config', MCP_ADD_COMMAND_USAGE);
306
371
  return;
307
372
  }
308
373
  let parsedAdd: ParsedMcpAddArgs;
@@ -317,13 +382,16 @@ export function registerMcpRuntimeCommands(registry: CommandRegistry): void {
317
382
  const result = await mcpApi.upsertServerConfig(shellPaths, parsedAdd.scope, parsedAdd.server);
318
383
  const connected = listServerSecurity().find((entry) => entry.name === parsedAdd.server.name)?.connected ?? false;
319
384
  ctx.print([
320
- `MCP server "${parsedAdd.server.name}" saved to ${parsedAdd.scope} config: ${result.path}.`,
321
- `Runtime reload: ${connected ? 'connected' : 'server saved; connection needs attention'} (+${result.reload.added} ~${result.reload.changed} -${result.reload.removed}, unchanged ${result.reload.unchanged}).`,
322
- `Command: ${parsedAdd.server.command}${parsedAdd.server.args?.length ? ` ${parsedAdd.server.args.join(' ')}` : ''}`,
323
- 'Next: /mcp tools',
385
+ `MCP server "${parsedAdd.server.name}" saved`,
386
+ ` scope ${parsedAdd.scope}`,
387
+ ` path ${result.path}`,
388
+ ...(parsedAdd.scope === 'global' ? [' global config'] : []),
389
+ ` Runtime reload: ${connected ? 'connected' : 'server saved; connection needs attention'} (+${result.reload.added} ~${result.reload.changed} -${result.reload.removed}, unchanged ${result.reload.unchanged})`,
390
+ ` command ${parsedAdd.server.command}${parsedAdd.server.args?.length ? ` ${parsedAdd.server.args.join(' ')}` : ''}`,
391
+ ' next /mcp tools',
324
392
  ].join('\n'));
325
393
  } catch (error) {
326
- ctx.print(`MCP add failed: ${summarizeError(error)}`);
394
+ ctx.print(`MCP add failed ${summarizeError(error)}`);
327
395
  }
328
396
  return;
329
397
  }
@@ -359,10 +427,10 @@ export function registerMcpRuntimeCommands(registry: CommandRegistry): void {
359
427
  try {
360
428
  const result = await mcpApi.removeServerConfig(shellPaths, scope, serverName);
361
429
  ctx.print(result.removed
362
- ? `Removed MCP server "${serverName}" from ${scope} config ${result.path}. Reload: +${result.reload.added} ~${result.reload.changed} -${result.reload.removed}, unchanged ${result.reload.unchanged}.`
430
+ ? `Removed MCP server "${serverName}" from ${scope} config ${result.path}. Reload +${result.reload.added} ~${result.reload.changed} -${result.reload.removed}, unchanged ${result.reload.unchanged}.`
363
431
  : `No ${scope} MCP server named "${serverName}" exists in ${result.path}.\nIf it still appears, it is coming from another config scope or external MCP config.`);
364
432
  } catch (error) {
365
- ctx.print(`MCP remove failed: ${summarizeError(error)}`);
433
+ ctx.print(`MCP remove failed ${summarizeError(error)}`);
366
434
  }
367
435
  return;
368
436
  }
@@ -375,9 +443,9 @@ export function registerMcpRuntimeCommands(registry: CommandRegistry): void {
375
443
  try {
376
444
  const result = await reloadMcpRuntime(ctx);
377
445
  const servers = listServerSecurity();
378
- ctx.print(`Reloaded MCP runtime from config. ${servers.filter((server) => server.connected).length}/${servers.length} server(s) connected. Result: +${result.added} ~${result.changed} -${result.removed}, unchanged ${result.unchanged}.`);
446
+ ctx.print(`Reloaded MCP runtime from config. ${servers.filter((server) => server.connected).length}/${servers.length} server(s) connected. Result +${result.added} ~${result.changed} -${result.removed}, unchanged ${result.unchanged}.`);
379
447
  } catch (error) {
380
- ctx.print(`MCP reload failed: ${summarizeError(error)}`);
448
+ ctx.print(`MCP reload failed ${summarizeError(error)}`);
381
449
  }
382
450
  return;
383
451
  }
@@ -388,22 +456,24 @@ export function registerMcpRuntimeCommands(registry: CommandRegistry): void {
388
456
  const effective = mcpApi.getEffectiveConfig(shellPaths);
389
457
  ctx.print([
390
458
  'MCP Config',
391
- ' locations:',
459
+ ' locations',
392
460
  ...effective.locations.map((location) => ` ${location.scope}/${location.kind}${location.writable ? ' writable' : ' read-only'} ${location.path}`),
393
- ` effective servers: ${effective.servers.length}`,
461
+ ` effective servers ${effective.servers.length}`,
394
462
  ...effective.servers.map((entry) => {
395
463
  const server = entry.server;
396
464
  const envKeys = Object.keys(server.env ?? {});
397
- return ` - ${server.name}: ${server.command}${server.args?.length ? ` ${server.args.join(' ')}` : ''} source=${entry.source.scope}/${entry.source.kind}${envKeys.length ? ` envKeys=${envKeys.join(',')}` : ''}`;
465
+ return ` - ${server.name}: ${server.command}${server.args?.length ? ` ${server.args.join(' ')}` : ''} origin ${entry.source.scope}/${entry.source.kind}${envKeys.length ? ` envKeys=${envKeys.join(',')}` : ''}`;
398
466
  }),
399
467
  '',
400
- 'Add or update from inside Agent with explicit confirmation:',
468
+ 'Add or update from inside Agent with explicit confirmation',
401
469
  ' Open /mcp and choose Add or update server, or use Agent Workspace -> Tools & MCP -> Add MCP server.',
402
- 'Automation equivalent:',
403
- ' /mcp add <name> <command> [args...] [--scope project|global] [--role <role>] [--trust <mode>] --yes',
470
+ ' Use Settings -> MCP for allow-all decisions.',
471
+ ' Use -- before server args that look like Agent flags.',
472
+ 'Automation equivalent',
473
+ ` ${MCP_ADD_COMMAND_USAGE}`,
404
474
  ].join('\n'));
405
475
  } catch (error) {
406
- ctx.print(`MCP config read failed: ${summarizeError(error)}`);
476
+ ctx.print(`MCP config read failed ${summarizeError(error)}`);
407
477
  }
408
478
  return;
409
479
  }
@@ -417,12 +487,12 @@ export function registerMcpRuntimeCommands(registry: CommandRegistry): void {
417
487
  }
418
488
  if (action === 'approve') {
419
489
  if (!confirmation.yes) {
420
- requireYesFlag(ctx, `approve MCP schema quarantine override for ${serverName}`, '/mcp quarantine <server> approve [operatorId] --yes');
490
+ requireYesFlag(ctx, `approve MCP tool-definition quarantine override for ${serverName}`, '/mcp quarantine <server> approve [operatorId] --yes');
421
491
  return;
422
492
  }
423
493
  const operatorId = commandArgs[3] || 'operator';
424
494
  mcpApi.approveSchemaQuarantine(serverName, operatorId);
425
- ctx.print(`Approved MCP schema quarantine override for ${serverName} as ${operatorId}. Refresh is still recommended.`);
495
+ ctx.print(`Approved MCP tool-definition quarantine override for ${serverName} as ${operatorId}. Refresh is still recommended.`);
426
496
  return;
427
497
  }
428
498
  if (!confirmation.yes) {
@@ -431,7 +501,7 @@ export function registerMcpRuntimeCommands(registry: CommandRegistry): void {
431
501
  }
432
502
  const detail = commandArgs.slice(2).join(' ') || 'quarantined by operator';
433
503
  mcpApi.quarantineSchema(serverName, 'operator_flagged', detail);
434
- ctx.print(`Quarantined MCP schema for ${serverName}.\nReason: ${detail}`);
504
+ ctx.print(`Quarantined MCP tool definitions for ${serverName}.\nReason: ${detail}`);
435
505
  return;
436
506
  }
437
507
 
@@ -456,18 +526,16 @@ export function registerMcpRuntimeCommands(registry: CommandRegistry): void {
456
526
  });
457
527
  }
458
528
 
459
- type McpSecurityServer = ReturnType<NonNullable<NonNullable<CommandContext['clients']>['mcpApi']>['listServerSecurity']>[number];
460
-
461
529
  function formatMcpServerList(servers: readonly McpSecurityServer[]): string {
462
530
  const connected = servers.filter(s => s.connected);
463
531
  const disconnected = servers.filter(s => !s.connected);
464
532
  const lines: string[] = [`MCP Servers (${connected.length}/${servers.length} connected):`];
465
533
  for (const s of servers) {
466
- const pathScope = s.allowedPaths.length > 0 ? ` paths=${s.allowedPaths.length}` : '';
467
- const hostScope = s.allowedHosts.length > 0 ? ` hosts=${s.allowedHosts.length}` : '';
468
- const freshness = ` freshness=${s.schemaFreshness}`;
469
- const quarantine = s.schemaFreshness === 'quarantined' ? ` quarantine=${s.quarantineReason ?? 'unknown'}` : '';
470
- lines.push(` ${s.connected ? '[connected] ' : '[disconnected]'} ${s.name} trust=${s.trustMode} role=${s.role}${freshness}${quarantine}${pathScope}${hostScope}`);
534
+ const pathScope = s.allowedPaths.length > 0 ? ` paths ${s.allowedPaths.length}` : '';
535
+ const hostScope = s.allowedHosts.length > 0 ? ` hosts ${s.allowedHosts.length}` : '';
536
+ const freshness = ` status ${formatMcpFreshness(s.schemaFreshness)}`;
537
+ const quarantine = s.schemaFreshness === 'quarantined' ? ` review reason ${s.quarantineReason ?? 'unknown'}` : '';
538
+ lines.push(` ${s.connected ? '[connected] ' : '[disconnected]'} ${s.name} trust=${formatMcpTrustMode(s.trustMode)} role=${formatMcpRole(s.role)}${freshness}${quarantine}${pathScope}${hostScope}`);
471
539
  }
472
540
  if (connected.length > 0) {
473
541
  lines.push('');
@@ -6,9 +6,9 @@ export function registerNotifyRuntimeCommands(registry: CommandRegistry): void {
6
6
  registry.register({
7
7
  name: 'notify',
8
8
  aliases: [],
9
- description: 'Manage webhook notification URLs (ntfy.sh format)',
10
- usage: 'add <url> --yes | remove <url> --yes | list | clear --yes | test --yes',
11
- argsHint: 'list|add --yes|remove --yes|test --yes',
9
+ description: 'Manage and send configured Agent webhook notifications',
10
+ usage: 'add <url> --yes | remove <url> --yes | list | clear --yes | test --yes | send <message> --yes',
11
+ argsHint: 'list|add --yes|remove --yes|test --yes|send --yes',
12
12
  async handler(args, ctx) {
13
13
  const parsed = stripYesFlag(args);
14
14
  const commandArgs = [...parsed.rest];
@@ -18,8 +18,8 @@ export function registerNotifyRuntimeCommands(registry: CommandRegistry): void {
18
18
  const sub = commandArgs[0];
19
19
 
20
20
  if (!sub || sub === 'list') {
21
- if (urls.length === 0) ctx.print('No webhook URLs configured.\nUse: /notify add <url>');
22
- else ctx.print(`Webhook URLs (${urls.length}):\n${urls.map((u, i) => ` ${i + 1}. ${u}`).join('\n')}`);
21
+ if (urls.length === 0) ctx.print('No webhook URLs configured.\nUse /notify add <url>');
22
+ else ctx.print(`Webhook URLs (${urls.length})\n${urls.map((u, i) => ` ${i + 1}. ${u}`).join('\n')}`);
23
23
  return;
24
24
  }
25
25
 
@@ -34,11 +34,11 @@ export function registerNotifyRuntimeCommands(registry: CommandRegistry): void {
34
34
  return;
35
35
  }
36
36
  try { new URL(url); } catch {
37
- ctx.print(`Invalid URL: ${url}`);
37
+ ctx.print(`Invalid URL ${url}`);
38
38
  return;
39
39
  }
40
40
  if (urls.includes(url)) {
41
- ctx.print(`Already configured: ${url}`);
41
+ ctx.print(`Already configured ${url}`);
42
42
  return;
43
43
  }
44
44
  urls.push(url);
@@ -86,7 +86,7 @@ export function registerNotifyRuntimeCommands(registry: CommandRegistry): void {
86
86
  return;
87
87
  }
88
88
  if (urls.length === 0) {
89
- ctx.print('No webhook URLs configured. Use: /notify add <url>');
89
+ ctx.print('No webhook URLs configured. Use /notify add <url>');
90
90
  return;
91
91
  }
92
92
  ctx.print(`Testing ${urls.length} webhook${urls.length !== 1 ? 's' : ''}...`);
@@ -96,7 +96,36 @@ export function registerNotifyRuntimeCommands(registry: CommandRegistry): void {
96
96
  return;
97
97
  }
98
98
 
99
- ctx.print('Usage: /notify add <url> --yes | remove <url> --yes | list | clear --yes | test --yes');
99
+ if (sub === 'send') {
100
+ const message = commandArgs.slice(1).join(' ').trim();
101
+ if (!message) {
102
+ ctx.print('Usage: /notify send <message> --yes');
103
+ return;
104
+ }
105
+ if (!parsed.yes) {
106
+ requireYesFlag(ctx, 'send a notification to configured webhook targets', '/notify send <message> --yes');
107
+ return;
108
+ }
109
+ if (urls.length === 0) {
110
+ ctx.print('No webhook URLs configured. Use /notify add <url>');
111
+ return;
112
+ }
113
+ notifier.setUrls(urls);
114
+ const result = await notifier.send(message);
115
+ ctx.print([
116
+ 'Notification sent',
117
+ ` attempted ${result.attempted}`,
118
+ ` delivered ${result.delivered}`,
119
+ ` failed ${result.failed}`,
120
+ ...result.results.slice(0, 20).map((delivery, index) => (
121
+ ` target ${index + 1} ${delivery.ok ? 'ok' : `failed${delivery.error ? ` (${delivery.error})` : ''}`}`
122
+ )),
123
+ ...(result.results.length > 20 ? [` ${result.results.length - 20} more target(s) omitted.`] : []),
124
+ ].join('\n'));
125
+ return;
126
+ }
127
+
128
+ ctx.print('Usage: /notify add <url> --yes | remove <url> --yes | list | clear --yes | test --yes | send <message> --yes');
100
129
  },
101
130
  });
102
131
  }
@@ -0,0 +1,138 @@
1
+ import type { CommandContext, CommandRegistry } from '../command-registry.ts';
2
+ import {
3
+ buildOperatorActionRequest,
4
+ formatOperatorActionFailure,
5
+ formatOperatorActionPreview,
6
+ formatOperatorActionSuccess,
7
+ postOperatorAction,
8
+ type OperatorActionId,
9
+ } from '../../agent/operator-actions.ts';
10
+ import { resolveAgentConnectedHostConnection } from '../../agent/routine-schedule-promotion.ts';
11
+ import { requireShellPaths } from './runtime-services.ts';
12
+ import { stripYesFlag } from './confirmation.ts';
13
+
14
+ interface OperatorActionOptionParse {
15
+ readonly note?: string;
16
+ readonly remember?: boolean;
17
+ readonly errors: readonly string[];
18
+ }
19
+
20
+ function parseOperatorActionOptions(args: readonly string[]): OperatorActionOptionParse {
21
+ const errors: string[] = [];
22
+ let note: string | undefined;
23
+ let remember: boolean | undefined;
24
+ for (let index = 0; index < args.length; index += 1) {
25
+ const token = args[index];
26
+ if (token === '--note') {
27
+ const value = args[index + 1];
28
+ if (!value) {
29
+ errors.push('--note requires text');
30
+ continue;
31
+ }
32
+ note = value;
33
+ index += 1;
34
+ continue;
35
+ }
36
+ if (token === '--remember') {
37
+ remember = true;
38
+ continue;
39
+ }
40
+ if (token === '--no-remember') {
41
+ remember = false;
42
+ continue;
43
+ }
44
+ errors.push(`Unknown option ${token}`);
45
+ }
46
+ return { note, remember, errors };
47
+ }
48
+
49
+ export async function executeConfirmedOperatorAction(
50
+ ctx: CommandContext,
51
+ action: OperatorActionId,
52
+ targetField: 'approvalId' | 'jobId' | 'runId' | 'scheduleId',
53
+ targetId: string,
54
+ rawArgs: readonly string[],
55
+ usage: string,
56
+ ): Promise<void> {
57
+ const parsed = stripYesFlag(rawArgs);
58
+ const options = parseOperatorActionOptions(parsed.rest);
59
+ if (options.errors.length > 0) {
60
+ ctx.print([`Usage: ${usage}`, ...options.errors.map((error) => ` ${error}`)].join('\n'));
61
+ return;
62
+ }
63
+ const request = buildOperatorActionRequest({
64
+ action,
65
+ [targetField]: targetId,
66
+ note: options.note,
67
+ remember: options.remember,
68
+ });
69
+ if (!request.ok) {
70
+ ctx.print(request.error);
71
+ return;
72
+ }
73
+ const requestSummary = `/${action} ${targetId}`;
74
+ if (!parsed.yes) {
75
+ ctx.print(`${formatOperatorActionPreview(request, requestSummary)}\nUsage: ${usage}`);
76
+ return;
77
+ }
78
+ const shellPaths = requireShellPaths(ctx);
79
+ const connection = resolveAgentConnectedHostConnection(ctx.platform.configManager, shellPaths.homeDirectory);
80
+ const result = await postOperatorAction(connection, request);
81
+ ctx.print(result.ok ? formatOperatorActionSuccess(connection.baseUrl, result) : formatOperatorActionFailure(result));
82
+ }
83
+
84
+ function approvalActionFromSubcommand(sub: string): OperatorActionId | null {
85
+ if (sub === 'approve') return 'approvals.approve';
86
+ if (sub === 'deny') return 'approvals.deny';
87
+ if (sub === 'cancel') return 'approvals.cancel';
88
+ return null;
89
+ }
90
+
91
+ function automationActionFromParts(scope: string, verb: string): {
92
+ readonly action: OperatorActionId;
93
+ readonly targetField: 'jobId' | 'runId' | 'scheduleId';
94
+ } | null {
95
+ if ((scope === 'job' || scope === 'jobs') && verb === 'run') return { action: 'automation.jobs.run', targetField: 'jobId' };
96
+ if ((scope === 'job' || scope === 'jobs') && verb === 'pause') return { action: 'automation.jobs.pause', targetField: 'jobId' };
97
+ if ((scope === 'job' || scope === 'jobs') && verb === 'resume') return { action: 'automation.jobs.resume', targetField: 'jobId' };
98
+ if ((scope === 'run' || scope === 'runs') && verb === 'cancel') return { action: 'automation.runs.cancel', targetField: 'runId' };
99
+ if ((scope === 'run' || scope === 'runs') && verb === 'retry') return { action: 'automation.runs.retry', targetField: 'runId' };
100
+ if ((scope === 'schedule' || scope === 'schedules') && verb === 'run') return { action: 'schedules.run', targetField: 'scheduleId' };
101
+ return null;
102
+ }
103
+
104
+ export async function handleApprovalOperatorAction(args: readonly string[], ctx: CommandContext): Promise<boolean> {
105
+ const sub = (args[0] ?? '').toLowerCase();
106
+ const action = approvalActionFromSubcommand(sub);
107
+ if (!action) return false;
108
+ const approvalId = args[1] ?? '';
109
+ const usage = `/approval ${sub} <approval-id> [--note <text>] [--remember|--no-remember] --yes`;
110
+ if (!approvalId) {
111
+ ctx.print(`Usage: ${usage}`);
112
+ return true;
113
+ }
114
+ await executeConfirmedOperatorAction(ctx, action, 'approvalId', approvalId, args.slice(2), usage);
115
+ return true;
116
+ }
117
+
118
+ export function registerOperatorActionRuntimeCommands(registry: CommandRegistry): void {
119
+ registry.register({
120
+ name: 'automation',
121
+ aliases: ['auto'],
122
+ description: 'Run confirmed connected-host automation actions from the Agent TUI',
123
+ usage: 'job <run|pause|resume> <job-id> --yes | run <cancel|retry> <run-id> --yes | schedule run <schedule-id> --yes',
124
+ argsHint: 'job run <id> --yes | run cancel <id> --yes | schedule run <id> --yes',
125
+ async handler(args, ctx) {
126
+ const scope = (args[0] ?? '').toLowerCase();
127
+ const verb = (args[1] ?? '').toLowerCase();
128
+ const targetId = args[2] ?? '';
129
+ const mapping = automationActionFromParts(scope, verb);
130
+ const usage = '/automation job <run|pause|resume> <job-id> --yes | /automation run <cancel|retry> <run-id> --yes | /automation schedule run <schedule-id> --yes';
131
+ if (!mapping || !targetId) {
132
+ ctx.print(`Usage: ${usage}`);
133
+ return;
134
+ }
135
+ await executeConfirmedOperatorAction(ctx, mapping.action, mapping.targetField, targetId, args.slice(3), usage);
136
+ },
137
+ });
138
+ }
@@ -57,21 +57,6 @@ export function registerOperatorRuntimeCommands(registry: CommandRegistry): void
57
57
  else ctx.scrollToLine?.(nextLine);
58
58
  },
59
59
  });
60
-
61
- registry.register({
62
- name: 'profiles',
63
- aliases: ['profile'],
64
- description: 'Blocked copied config-profile command; Agent uses isolated profile homes',
65
- usage: 'disabled',
66
- argsHint: 'disabled',
67
- handler(_args, ctx) {
68
- ctx.print([
69
- 'Copied config profiles are disabled in GoodVibes Agent.',
70
- 'Use /agent profiles or /agent-profile to create and manage isolated Agent profile homes.',
71
- ].join('\n'));
72
- },
73
- });
74
-
75
60
  registry.register({
76
61
  name: 'prev-error',
77
62
  aliases: ['pe'],
@@ -153,7 +138,7 @@ export function registerOperatorRuntimeCommands(registry: CommandRegistry): void
153
138
  return;
154
139
  }
155
140
  if (verbosity !== 'minimal' && verbosity !== 'normal' && verbosity !== 'verbose') {
156
- ctx.print(`Invalid verbosity "${verbosity}". Valid values: minimal, normal, verbose`);
141
+ ctx.print(`Invalid verbosity "${verbosity}". Valid values minimal, normal, verbose.`);
157
142
  return;
158
143
  }
159
144
  if (!parsed.yes) {
@@ -161,7 +146,11 @@ export function registerOperatorRuntimeCommands(registry: CommandRegistry): void
161
146
  return;
162
147
  }
163
148
  mgr.setDomainVerbosity(domain, verbosity as 'minimal' | 'normal' | 'verbose');
164
- ctx.print(`Domain "${domain}" verbosity set to: ${verbosity}`);
149
+ ctx.print([
150
+ 'Domain verbosity set',
151
+ ` domain ${domain}`,
152
+ ` verbosity ${verbosity}`,
153
+ ].join('\n'));
165
154
  return;
166
155
  }
167
156