@pellux/goodvibes-agent 0.1.117 → 1.0.0

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 (253) hide show
  1. package/CHANGELOG.md +13 -4
  2. package/README.md +35 -14
  3. package/bin/goodvibes-agent.ts +16 -2
  4. package/dist/package/main.js +176073 -170980
  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 +134 -68
  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 +44 -36
  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-learned-behavior.ts +2 -2
  66. package/src/input/agent-workspace-library-command-editor-submission.ts +1 -1
  67. package/src/input/agent-workspace-library-command-editors.ts +2 -2
  68. package/src/input/agent-workspace-local-operations.ts +218 -0
  69. package/src/input/agent-workspace-local-selection.ts +75 -0
  70. package/src/input/agent-workspace-media-command-editor-submission.ts +62 -0
  71. package/src/input/agent-workspace-media-command-editors.ts +27 -0
  72. package/src/input/agent-workspace-memory-command-editors.ts +1 -1
  73. package/src/input/agent-workspace-memory-editor.ts +2 -2
  74. package/src/input/agent-workspace-navigation.ts +38 -2
  75. package/src/input/agent-workspace-notify-editor-submission.ts +16 -2
  76. package/src/input/agent-workspace-operations-command-editor-submission.ts +63 -1
  77. package/src/input/agent-workspace-operations-command-editors.ts +80 -3
  78. package/src/input/agent-workspace-panel-route.ts +43 -0
  79. package/src/input/agent-workspace-reminder-schedule-editor.ts +1 -1
  80. package/src/input/agent-workspace-routine-schedule-editor.ts +2 -2
  81. package/src/input/agent-workspace-search.ts +169 -0
  82. package/src/input/agent-workspace-setup.ts +22 -11
  83. package/src/input/agent-workspace-skill-bundle-command-editor-submission.ts +6 -6
  84. package/src/input/agent-workspace-skill-bundle-command-editors.ts +10 -10
  85. package/src/input/agent-workspace-snapshot.ts +112 -13
  86. package/src/input/agent-workspace-task-command-editors.ts +4 -4
  87. package/src/input/agent-workspace-token.ts +18 -2
  88. package/src/input/agent-workspace-types.ts +92 -4
  89. package/src/input/agent-workspace-voice-media.ts +3 -6
  90. package/src/input/agent-workspace-web-research-editor.ts +104 -0
  91. package/src/input/agent-workspace.ts +136 -208
  92. package/src/input/command-registry.ts +4 -1
  93. package/src/input/commands/agent-runtime-profile-runtime.ts +45 -41
  94. package/src/input/commands/agent-skills-runtime.ts +83 -70
  95. package/src/input/commands/agent-workspace-runtime.ts +23 -7
  96. package/src/input/commands/brief-runtime.ts +25 -24
  97. package/src/input/commands/channels-runtime.ts +145 -31
  98. package/src/input/commands/delegation-runtime.ts +29 -23
  99. package/src/input/commands/experience-runtime.ts +6 -4
  100. package/src/input/commands/guidance-runtime.ts +4 -4
  101. package/src/input/commands/health-runtime.ts +140 -115
  102. package/src/input/commands/knowledge.ts +57 -56
  103. package/src/input/commands/local-provider-runtime.ts +36 -18
  104. package/src/input/commands/local-runtime.ts +138 -16
  105. package/src/input/commands/local-setup-review.ts +7 -7
  106. package/src/input/commands/mcp-runtime.ts +56 -35
  107. package/src/input/commands/notify-runtime.ts +38 -9
  108. package/src/input/commands/operator-actions-runtime.ts +138 -0
  109. package/src/input/commands/operator-runtime.ts +6 -17
  110. package/src/input/commands/personas-runtime.ts +45 -30
  111. package/src/input/commands/planning-runtime.ts +1 -1
  112. package/src/input/commands/platform-access-runtime.ts +29 -29
  113. package/src/input/commands/provider-accounts-runtime.ts +60 -39
  114. package/src/input/commands/qrcode-runtime.ts +45 -6
  115. package/src/input/commands/recall-bundle.ts +11 -11
  116. package/src/input/commands/recall-capture.ts +13 -11
  117. package/src/input/commands/recall-query.ts +29 -21
  118. package/src/input/commands/recall-review.ts +2 -1
  119. package/src/input/commands/routines-runtime.ts +59 -43
  120. package/src/input/commands/schedule-runtime.ts +33 -20
  121. package/src/input/commands/security-runtime.ts +4 -4
  122. package/src/input/commands/session-content.ts +80 -78
  123. package/src/input/commands/session-workflow.ts +132 -93
  124. package/src/input/commands/session.ts +3 -174
  125. package/src/input/commands/shell-core.ts +32 -17
  126. package/src/input/commands/subscription-runtime.ts +53 -179
  127. package/src/input/commands/tasks-runtime.ts +20 -20
  128. package/src/input/commands/work-plan-runtime.ts +33 -8
  129. package/src/input/commands.ts +2 -2
  130. package/src/input/feed-context-factory.ts +2 -1
  131. package/src/input/file-picker.ts +3 -2
  132. package/src/input/handler-command-route.ts +4 -7
  133. package/src/input/handler-content-actions.ts +89 -1
  134. package/src/input/handler-feed-routes.ts +19 -12
  135. package/src/input/handler-feed.ts +6 -3
  136. package/src/input/handler-interactions.ts +7 -5
  137. package/src/input/handler-modal-stack.ts +3 -3
  138. package/src/input/handler-onboarding.ts +24 -80
  139. package/src/input/handler-shortcuts.ts +15 -4
  140. package/src/input/handler.ts +47 -17
  141. package/src/input/input-history.ts +5 -6
  142. package/src/input/keybindings.ts +22 -11
  143. package/src/input/onboarding/onboarding-wizard-apply.ts +13 -0
  144. package/src/input/onboarding/onboarding-wizard-constants.ts +1 -0
  145. package/src/input/onboarding/onboarding-wizard-operator-steps.ts +147 -57
  146. package/src/input/onboarding/onboarding-wizard-steps.ts +46 -29
  147. package/src/input/onboarding/onboarding-wizard-types.ts +3 -1
  148. package/src/input/onboarding/onboarding-wizard.ts +68 -0
  149. package/src/input/profile-picker-modal.ts +31 -12
  150. package/src/input/session-picker-modal.ts +21 -4
  151. package/src/input/settings-modal-behavior.ts +0 -3
  152. package/src/input/settings-modal-subscriptions.ts +3 -3
  153. package/src/input/settings-modal-types.ts +2 -13
  154. package/src/input/settings-modal.ts +6 -52
  155. package/src/input/submission-intent.ts +0 -1
  156. package/src/input/submission-router.ts +3 -3
  157. package/src/main.ts +18 -8
  158. package/src/panels/approval-panel.ts +8 -8
  159. package/src/panels/automation-control-panel.ts +2 -2
  160. package/src/panels/base-panel.ts +1 -1
  161. package/src/panels/builtin/agent.ts +1 -1
  162. package/src/panels/builtin/operations.ts +1 -10
  163. package/src/panels/builtin/session.ts +9 -9
  164. package/src/panels/builtin/shared.ts +2 -2
  165. package/src/panels/cost-tracker-panel.ts +1 -1
  166. package/src/panels/docs-panel.ts +5 -3
  167. package/src/panels/index.ts +0 -1
  168. package/src/panels/knowledge-panel.ts +6 -5
  169. package/src/panels/memory-panel.ts +7 -6
  170. package/src/panels/panel-list-panel.ts +36 -80
  171. package/src/panels/project-planning-panel.ts +18 -11
  172. package/src/panels/provider-account-snapshot.ts +51 -25
  173. package/src/panels/provider-accounts-panel.ts +33 -18
  174. package/src/panels/provider-health-domains.ts +45 -4
  175. package/src/panels/provider-health-panel.ts +5 -4
  176. package/src/panels/qr-panel.ts +1 -1
  177. package/src/panels/schedule-panel.ts +9 -17
  178. package/src/panels/security-panel.ts +8 -8
  179. package/src/panels/session-browser-panel.ts +10 -10
  180. package/src/panels/subscription-panel.ts +3 -3
  181. package/src/panels/system-messages-panel.ts +1 -1
  182. package/src/panels/tasks-panel.ts +20 -13
  183. package/src/panels/tool-inspector-panel.ts +19 -12
  184. package/src/panels/work-plan-panel.ts +5 -5
  185. package/src/planning/project-planning-coordinator.ts +1 -1
  186. package/src/provider-auth-route-display.ts +9 -0
  187. package/src/renderer/agent-workspace-style.ts +34 -0
  188. package/src/renderer/agent-workspace.ts +228 -52
  189. package/src/renderer/autocomplete-overlay.ts +25 -6
  190. package/src/renderer/bookmark-modal.ts +19 -4
  191. package/src/renderer/buffer.ts +4 -2
  192. package/src/renderer/context-inspector.ts +50 -13
  193. package/src/renderer/diff.ts +1 -1
  194. package/src/renderer/file-picker-overlay.ts +19 -6
  195. package/src/renderer/help-overlay.ts +106 -33
  196. package/src/renderer/history-search-overlay.ts +19 -4
  197. package/src/renderer/live-tail-modal.ts +27 -5
  198. package/src/renderer/mcp-workspace.ts +164 -50
  199. package/src/renderer/model-picker-overlay.ts +58 -2
  200. package/src/renderer/model-workspace.ts +104 -20
  201. package/src/renderer/process-modal.ts +27 -6
  202. package/src/renderer/profile-picker-modal.ts +20 -4
  203. package/src/renderer/search-overlay.ts +25 -5
  204. package/src/renderer/selection-modal-overlay.ts +46 -14
  205. package/src/renderer/session-picker-modal.ts +18 -1
  206. package/src/renderer/settings-modal-helpers.ts +2 -40
  207. package/src/renderer/settings-modal.ts +83 -50
  208. package/src/renderer/tool-call.ts +20 -11
  209. package/src/runtime/agent-runtime-events.ts +6 -6
  210. package/src/runtime/bootstrap-command-context.ts +7 -1
  211. package/src/runtime/bootstrap-command-parts.ts +9 -7
  212. package/src/runtime/bootstrap-core.ts +19 -1
  213. package/src/runtime/bootstrap-hook-bridge.ts +7 -18
  214. package/src/runtime/bootstrap-shell.ts +4 -4
  215. package/src/runtime/bootstrap.ts +31 -22
  216. package/src/runtime/connected-host-auth.ts +7 -2
  217. package/src/runtime/diagnostics/panels/index.ts +2 -2
  218. package/src/runtime/diagnostics/panels/policy.ts +7 -7
  219. package/src/runtime/index.ts +2 -1
  220. package/src/runtime/onboarding/apply-file-helpers.ts +66 -0
  221. package/src/runtime/onboarding/apply.ts +80 -79
  222. package/src/runtime/onboarding/derivation.ts +5 -8
  223. package/src/runtime/onboarding/snapshot.ts +0 -15
  224. package/src/runtime/onboarding/types.ts +8 -19
  225. package/src/runtime/onboarding/verify.ts +32 -10
  226. package/src/runtime/operator-token-cleanup.ts +1 -1
  227. package/src/runtime/services.ts +91 -22
  228. package/src/runtime/store/selectors/index.ts +3 -3
  229. package/src/runtime/store/state.ts +1 -4
  230. package/src/runtime/surface-feature-flags.ts +41 -6
  231. package/src/runtime/ui-read-models.ts +3 -4
  232. package/src/runtime/ui-services.ts +6 -6
  233. package/src/shell/blocking-input.ts +2 -1
  234. package/src/shell/ui-openers.ts +3 -13
  235. package/src/tools/agent-analysis-registry-policy.ts +0 -1
  236. package/src/tools/agent-channel-send-tool.ts +133 -0
  237. package/src/tools/agent-context-policy.ts +1 -1
  238. package/src/tools/agent-knowledge-ingest-tool.ts +405 -0
  239. package/src/tools/agent-knowledge-tool.ts +170 -0
  240. package/src/tools/agent-local-registry-tool.ts +269 -69
  241. package/src/tools/agent-media-generate-tool.ts +133 -0
  242. package/src/tools/agent-notify-tool.ts +143 -0
  243. package/src/tools/agent-operator-action-tool.ts +137 -0
  244. package/src/tools/agent-operator-briefing-tool.ts +217 -0
  245. package/src/tools/agent-reminder-schedule-tool.ts +237 -0
  246. package/src/tools/agent-tool-policy-guard.ts +8 -8
  247. package/src/tools/agent-work-plan-tool.ts +256 -0
  248. package/src/version.ts +1 -1
  249. package/src/input/commands/policy-dispatch.ts +0 -339
  250. package/src/input/commands/policy.ts +0 -13
  251. package/src/panels/panel-picker.ts +0 -105
  252. package/src/panels/policy-panel.ts +0 -308
  253. package/src/renderer/panel-picker-overlay.ts +0 -202
@@ -9,6 +9,10 @@ import { BUILTIN_SECRET_PROVIDER_SOURCES, describeSecretRef, isSecretRefInput, r
9
9
  import { requireBookmarkManager, requireProviderApi, requireSecretsManager } from './runtime-services.ts';
10
10
  import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
11
11
  import { requireYesFlag, stripYesFlag } from './confirmation.ts';
12
+ import {
13
+ formatAgentMediaGenerationResult,
14
+ generateAgentMedia,
15
+ } from '../../agent/media-generation.ts';
12
16
 
13
17
  function isGoodVibesSecretRefInput(value: string): boolean {
14
18
  const normalized = value.trim();
@@ -23,7 +27,7 @@ function isMalformedGoodVibesSecretRefInput(value: string): boolean {
23
27
  function toggleBlocks(typeFilter: string, collapsed: boolean, ctx: CommandContext): void {
24
28
  const VALID_TYPES = ['all', 'thinking', 'tool', 'code'] as const;
25
29
  if (!VALID_TYPES.includes(typeFilter as typeof VALID_TYPES[number])) {
26
- ctx.print(`Unknown type: ${typeFilter}\nValid types: ${VALID_TYPES.join(', ')}`);
30
+ ctx.print(`Unknown type ${typeFilter}\nValid types ${VALID_TYPES.join(', ')}`);
27
31
  return;
28
32
  }
29
33
  const blockRegistry = ctx.session.conversationManager.getBlockRegistry();
@@ -49,6 +53,60 @@ function toggleBlocks(typeFilter: string, collapsed: boolean, ctx: CommandContex
49
53
  ctx.renderRequest();
50
54
  }
51
55
 
56
+ interface MediaGenerateArgs {
57
+ readonly prompt: string;
58
+ readonly providerId?: string;
59
+ readonly modelId?: string;
60
+ readonly outputMimeType?: string;
61
+ readonly yes: boolean;
62
+ readonly errors: readonly string[];
63
+ }
64
+
65
+ function readFlagValue(args: readonly string[], index: number, flag: string, errors: string[]): string | null {
66
+ const value = args[index + 1];
67
+ if (!value || value.startsWith('--')) {
68
+ errors.push(`${flag} requires a value.`);
69
+ return null;
70
+ }
71
+ return value;
72
+ }
73
+
74
+ function parseMediaGenerateArgs(args: readonly string[]): MediaGenerateArgs {
75
+ const parsed = stripYesFlag([...args]);
76
+ const errors: string[] = [];
77
+ const promptParts: string[] = [];
78
+ let providerId: string | undefined;
79
+ let modelId: string | undefined;
80
+ let outputMimeType: string | undefined;
81
+ for (let index = 0; index < parsed.rest.length; index += 1) {
82
+ const arg = parsed.rest[index];
83
+ if (arg === '--provider') {
84
+ providerId = readFlagValue(parsed.rest, index, arg, errors) ?? providerId;
85
+ index += 1;
86
+ } else if (arg === '--model') {
87
+ modelId = readFlagValue(parsed.rest, index, arg, errors) ?? modelId;
88
+ index += 1;
89
+ } else if (arg === '--mime') {
90
+ outputMimeType = readFlagValue(parsed.rest, index, arg, errors) ?? outputMimeType;
91
+ index += 1;
92
+ } else if (arg?.startsWith('--')) {
93
+ errors.push(`Unknown media generation flag ${arg}`);
94
+ } else if (arg) {
95
+ promptParts.push(arg);
96
+ }
97
+ }
98
+ const prompt = promptParts.join(' ').trim();
99
+ if (!prompt) errors.push('Media generation prompt is required.');
100
+ return {
101
+ prompt,
102
+ ...(providerId ? { providerId } : {}),
103
+ ...(modelId ? { modelId } : {}),
104
+ ...(outputMimeType ? { outputMimeType } : {}),
105
+ yes: parsed.yes,
106
+ errors,
107
+ };
108
+ }
109
+
52
110
  export function registerLocalRuntimeCommands(registry: CommandRegistry): void {
53
111
  registry.register({ name: 'expand', description: 'Expand blocks by type', usage: '[all|thinking|tool|code]', argsHint: '[all|thinking|tool|code]', handler(args, ctx) { toggleBlocks(args[0] || 'all', false, ctx); } });
54
112
  registry.register({ name: 'collapse', description: 'Collapse blocks by type', usage: '[all|thinking|tool|code]', argsHint: '[all|thinking|tool|code]', handler(args, ctx) { toggleBlocks(args[0] || 'all', true, ctx); } });
@@ -130,9 +188,12 @@ export function registerLocalRuntimeCommands(registry: CommandRegistry): void {
130
188
  }
131
189
  try {
132
190
  const resolved = await resolveSecretRef(refText, { resolveLocalSecret: (key) => mgr.get(key) });
133
- ctx.print(`[secrets] ${describeSecretRef(refText)}: ${resolved.value ? 'resolved <redacted>' : 'missing'}`);
191
+ ctx.print([
192
+ `[secrets] ${describeSecretRef(refText)}`,
193
+ ` status ${resolved.value ? 'resolved <redacted>' : 'missing'}`,
194
+ ].join('\n'));
134
195
  } catch (error) {
135
- ctx.print(`[secrets] ${describeSecretRef(refText)} failed: ${summarizeError(error)}`);
196
+ ctx.print(`[secrets] ${describeSecretRef(refText)} failed ${summarizeError(error)}`);
136
197
  }
137
198
  return;
138
199
  }
@@ -190,7 +251,10 @@ export function registerLocalRuntimeCommands(registry: CommandRegistry): void {
190
251
  scope: flags.has('--user') ? 'user' : flags.has('--project') ? 'project' : undefined,
191
252
  medium: flags.has('--plaintext') ? 'plaintext' : flags.has('--secure') ? 'secure' : undefined,
192
253
  });
193
- ctx.print(`[secrets] Deleted: ${key}`);
254
+ ctx.print([
255
+ '[secrets] Deleted',
256
+ ` key ${key}`,
257
+ ].join('\n'));
194
258
  return;
195
259
  }
196
260
  ctx.print('[secrets] Usage: /secrets set <KEY> <value> [--user|--project] [--secure|--plaintext] --yes | link <KEY> <secret-ref> [--user|--project] [--secure|--plaintext] --yes | get <KEY> | test <secret-ref> | providers | list | delete <KEY> [--user|--project] [--secure|--plaintext] --yes');
@@ -212,36 +276,42 @@ export function registerLocalRuntimeCommands(registry: CommandRegistry): void {
212
276
  const promptText = args.slice(1).join(' ') || `Attached image: ${rawPath.split('/').pop() ?? rawPath}`;
213
277
  const projectRoot = ctx.workspace.shellPaths?.workingDirectory ?? ctx.platform.configManager.getWorkingDirectory();
214
278
  if (!projectRoot) {
215
- ctx.print('Error: working directory is unavailable.');
279
+ ctx.print([
280
+ 'Error',
281
+ ' message working directory is unavailable.',
282
+ ].join('\n'));
216
283
  return;
217
284
  }
218
285
  let resolvedPath: string;
219
286
  try {
220
287
  resolvedPath = resolveAndValidatePath(rawPath, projectRoot);
221
288
  } catch (err) {
222
- ctx.print(`Error: ${summarizeError(err)}`);
289
+ ctx.print([
290
+ 'Error',
291
+ ` message ${summarizeError(err)}`,
292
+ ].join('\n'));
223
293
  return;
224
294
  }
225
295
  if (!existsSync(resolvedPath)) {
226
- ctx.print(`File not found: ${rawPath}`);
296
+ ctx.print(`File not found ${rawPath}`);
227
297
  return;
228
298
  }
229
299
  const ext = resolvedPath.slice(resolvedPath.lastIndexOf('.')).toLowerCase();
230
300
  const mediaType = ({ '.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.webp': 'image/webp', '.gif': 'image/gif' } as Record<string, string>)[ext];
231
301
  if (!mediaType) {
232
- ctx.print(`Unsupported image format: ${ext}\nSupported: .png, .jpg, .jpeg, .webp, .gif`);
302
+ ctx.print(`Unsupported image format ${ext}\nSupported .png, .jpg, .jpeg, .webp, .gif`);
233
303
  return;
234
304
  }
235
305
  const stat = statSync(resolvedPath);
236
306
  if (stat.size > 20 * 1024 * 1024) {
237
- ctx.print(`Image too large (${(stat.size / 1024 / 1024).toFixed(1)}MB). Maximum: 20MB`);
307
+ ctx.print(`Image too large (${(stat.size / 1024 / 1024).toFixed(1)}MB). Maximum 20MB`);
238
308
  return;
239
309
  }
240
310
  let data: string;
241
311
  try {
242
312
  data = (await readFile(resolvedPath)).toString('base64');
243
313
  } catch (err) {
244
- ctx.print(`Failed to read image: ${summarizeError(err)}`);
314
+ ctx.print(`Failed to read image ${summarizeError(err)}`);
245
315
  return;
246
316
  }
247
317
  const currentModel = await requireProviderApi(ctx).getCurrentModel();
@@ -253,6 +323,53 @@ export function registerLocalRuntimeCommands(registry: CommandRegistry): void {
253
323
  },
254
324
  });
255
325
 
326
+ registry.register({
327
+ name: 'media',
328
+ description: 'Inspect media providers or generate media through configured providers',
329
+ usage: 'providers | generate [--provider <id>] [--model <id>] [--mime <mime>] <prompt> --yes',
330
+ argsHint: '<providers|generate>',
331
+ async handler(args, ctx) {
332
+ const subcommand = args[0]?.toLowerCase() || 'providers';
333
+ const registry = ctx.platform.mediaProviderRegistry;
334
+ if (!registry) {
335
+ ctx.print('[media] Media providers are not available in this runtime.');
336
+ return;
337
+ }
338
+ if (subcommand === 'providers' || subcommand === 'status' || subcommand === 'list') {
339
+ const statuses = await registry.status();
340
+ ctx.print([
341
+ '[media] Providers:',
342
+ ...statuses.map((provider) => ` ${provider.id}: ${provider.state}${provider.configured ? '' : ' (setup needed)'} - ${provider.capabilities.join(', ')}${provider.detail ? ` - ${provider.detail}` : ''}`),
343
+ ].join('\n'));
344
+ return;
345
+ }
346
+ if (subcommand !== 'generate') {
347
+ ctx.print('[media] Usage: /media providers | /media generate [--provider <id>] [--model <id>] [--mime <mime>] <prompt> --yes');
348
+ return;
349
+ }
350
+ const artifactStore = ctx.platform.artifactStore;
351
+ if (!artifactStore) {
352
+ ctx.print('[media] Artifact storage is not available in this runtime.');
353
+ return;
354
+ }
355
+ const parsed = parseMediaGenerateArgs(args.slice(1));
356
+ if (parsed.errors.length > 0) {
357
+ ctx.print(`[media] ${parsed.errors.join('\n[media] ')}`);
358
+ return;
359
+ }
360
+ if (!parsed.yes) {
361
+ requireYesFlag(ctx, `generate media for "${parsed.prompt}"`, '/media generate [--provider <id>] [--model <id>] [--mime <mime>] <prompt> --yes');
362
+ return;
363
+ }
364
+ try {
365
+ const result = await generateAgentMedia(registry, artifactStore, parsed);
366
+ ctx.print(formatAgentMediaGenerationResult(result));
367
+ } catch (error) {
368
+ ctx.print(`[media] Generation failed ${summarizeError(error)}`);
369
+ }
370
+ },
371
+ });
372
+
256
373
  registry.register({
257
374
  name: 'refresh-models',
258
375
  description: 'Refresh model catalog, benchmarks, and token limits',
@@ -267,15 +384,17 @@ export function registerLocalRuntimeCommands(registry: CommandRegistry): void {
267
384
  catalogOk = true;
268
385
  ctx.print(`Model catalog refreshed: ${catalog.modelCount} models from ${catalog.providerCount} providers`);
269
386
  } catch (e) {
270
- ctx.print(`Catalog refresh failed: ${summarizeError(e)}`);
387
+ ctx.print(`Catalog refresh failed ${summarizeError(e)}`);
271
388
  }
272
389
  ctx.print('Refreshing benchmarks...');
273
390
  try {
274
391
  const benchmarkCount = await providerApi.refreshBenchmarks();
275
392
  benchmarksOk = true;
276
- ctx.print(`Benchmarks refreshed${benchmarkCount > 0 ? `: ${benchmarkCount} model records available.` : '.'}`);
393
+ ctx.print(benchmarkCount > 0
394
+ ? `Benchmarks refreshed: ${benchmarkCount} model records available.`
395
+ : 'Benchmarks refreshed.');
277
396
  } catch (e) {
278
- ctx.print(`Benchmarks refresh failed: ${summarizeError(e)}`);
397
+ ctx.print(`Benchmarks refresh failed ${summarizeError(e)}`);
279
398
  }
280
399
  ctx.print('Refreshing token limits...');
281
400
  try {
@@ -283,7 +402,7 @@ export function registerLocalRuntimeCommands(registry: CommandRegistry): void {
283
402
  limitsOk = true;
284
403
  ctx.print(`Token limits refreshed: ${count} models updated.`);
285
404
  } catch (e) {
286
- ctx.print(`Token limits refresh failed: ${summarizeError(e)}`);
405
+ ctx.print(`Token limits refresh failed ${summarizeError(e)}`);
287
406
  }
288
407
  if (!catalogOk || !benchmarksOk || !limitsOk) ctx.print('Some refreshes failed — see messages above.');
289
408
  },
@@ -308,14 +427,17 @@ export function registerLocalRuntimeCommands(registry: CommandRegistry): void {
308
427
  }
309
428
  const favorites = await providerApi.getFavorites();
310
429
  if (favorites.pinned.some((entry) => entry.modelId === modelId || entry.registryKey === modelId)) {
311
- ctx.print(`Model already pinned: ${modelId}`);
430
+ ctx.print(`Model already pinned ${modelId}`);
312
431
  return;
313
432
  }
314
433
  try {
315
434
  await providerApi.pinModel(modelId);
316
435
  ctx.print(`Pinned: ${modelId}`);
317
436
  } catch (e) {
318
- ctx.print(`Error: ${summarizeError(e)}`);
437
+ ctx.print([
438
+ 'Error',
439
+ ` message ${summarizeError(e)}`,
440
+ ].join('\n'));
319
441
  }
320
442
  },
321
443
  });
@@ -13,7 +13,7 @@ export interface SetupReviewSnapshot {
13
13
  readonly builtinSubscriptionProviderCount: number;
14
14
  readonly activeSubscriptionCount: number;
15
15
  readonly pendingSubscriptionCount: number;
16
- readonly serviceIssues: string[];
16
+ readonly hostIntegrationIssues: string[];
17
17
  readonly skillCount: number;
18
18
  readonly pluginCount: number;
19
19
  readonly quarantinedPluginCount: number;
@@ -33,11 +33,11 @@ export async function buildSetupReviewSnapshot(ctx: CommandContext): Promise<Set
33
33
  const serviceRegistry = requireServiceRegistry(ctx);
34
34
  const services = Object.keys(serviceRegistry.getAll()).sort((a, b) => a.localeCompare(b));
35
35
  const serviceConfigs = serviceRegistry.getAll();
36
- const serviceIssues: string[] = [];
36
+ const hostIntegrationIssues: string[] = [];
37
37
  for (const name of services) {
38
38
  const inspection = await serviceRegistry.inspect(name);
39
39
  if (!inspection?.hasPrimaryCredential) {
40
- serviceIssues.push(`${name}: missing primary credential`);
40
+ hostIntegrationIssues.push(`${name}: missing primary credential`);
41
41
  }
42
42
  }
43
43
 
@@ -79,13 +79,13 @@ export async function buildSetupReviewSnapshot(ctx: CommandContext): Promise<Set
79
79
  message: providerCount > 0 ? `${providerCount} model(s) available` : 'no models available',
80
80
  },
81
81
  {
82
- severity: (services.length === 0 && oauthProviderCount === 0 && builtinSubscriptionProviderCount === 0) ? 'warn' : serviceIssues.length === 0 ? 'pass' : 'warn',
82
+ severity: (services.length === 0 && oauthProviderCount === 0 && builtinSubscriptionProviderCount === 0) ? 'warn' : hostIntegrationIssues.length === 0 ? 'pass' : 'warn',
83
83
  area: 'host',
84
84
  message: (services.length === 0 && oauthProviderCount === 0 && builtinSubscriptionProviderCount === 0)
85
85
  ? 'no connected host integrations configured'
86
- : serviceIssues.length === 0
86
+ : hostIntegrationIssues.length === 0
87
87
  ? `${services.length} host integration(s), ${oauthProviderCount + builtinSubscriptionProviderCount} oauth provider(s), ${activeSubscriptionCount} active subscription override(s)`
88
- : `${serviceIssues.length} host integration configuration issue(s)`,
88
+ : `${hostIntegrationIssues.length} host integration configuration issue(s)`,
89
89
  },
90
90
  {
91
91
  severity: quarantinedPluginCount === 0 ? 'pass' : 'warn',
@@ -111,7 +111,7 @@ export async function buildSetupReviewSnapshot(ctx: CommandContext): Promise<Set
111
111
  builtinSubscriptionProviderCount,
112
112
  activeSubscriptionCount,
113
113
  pendingSubscriptionCount,
114
- serviceIssues,
114
+ hostIntegrationIssues,
115
115
  skillCount: skills.length,
116
116
  pluginCount: plugins.length,
117
117
  quarantinedPluginCount,
@@ -12,6 +12,11 @@ interface ParsedMcpAddArgs {
12
12
  readonly server: McpServerConfig;
13
13
  }
14
14
 
15
+ type McpSecurityServer = ReturnType<NonNullable<NonNullable<CommandContext['clients']>['mcpApi']>['listServerSecurity']>[number];
16
+ type McpDisplayTrustMode = NonNullable<McpServerConfig['trustMode']> | McpSecurityServer['trustMode'];
17
+ type McpDisplayRole = NonNullable<McpServerConfig['role']> | McpSecurityServer['role'];
18
+ type McpDisplayFreshness = McpSecurityServer['schemaFreshness'];
19
+
15
20
  function isMcpRole(value: string): value is NonNullable<McpServerConfig['role']> {
16
21
  return MCP_ROLES.includes(value as NonNullable<McpServerConfig['role']>);
17
22
  }
@@ -24,6 +29,21 @@ function isMcpScope(value: string): value is McpConfigScope {
24
29
  return value === 'project' || value === 'global';
25
30
  }
26
31
 
32
+ function formatMcpTrustMode(mode: McpDisplayTrustMode): string {
33
+ if (mode === 'ask-on-risk') return 'ask on risky actions';
34
+ if (mode === 'allow-all') return 'allow all actions';
35
+ return mode.replace(/[_-]+/g, ' ');
36
+ }
37
+
38
+ function formatMcpFreshness(value: McpDisplayFreshness): string {
39
+ if (value === 'quarantined') return 'needs review';
40
+ return value.replace(/[_-]+/g, ' ');
41
+ }
42
+
43
+ function formatMcpRole(value: McpDisplayRole): string {
44
+ return value.replace(/[_-]+/g, ' ');
45
+ }
46
+
27
47
  function validateServerName(name: string): string | null {
28
48
  if (!name.trim()) return 'MCP server name is required.';
29
49
  if (!/^[a-zA-Z0-9._-]+$/.test(name)) {
@@ -71,7 +91,7 @@ function parseAddServerArgs(args: string[]): ParsedMcpAddArgs {
71
91
  }
72
92
  if (token === '--role') {
73
93
  const value = readFlagValue(tokens, index, token);
74
- if (!isMcpRole(value)) throw new Error(`Invalid MCP role "${value}". Expected one of: ${MCP_ROLES.join(', ')}`);
94
+ if (!isMcpRole(value)) throw new Error(`Invalid MCP role "${value}". Expected one of ${MCP_ROLES.join(', ')}.`);
75
95
  role = value;
76
96
  index += 1;
77
97
  continue;
@@ -85,7 +105,7 @@ function parseAddServerArgs(args: string[]): ParsedMcpAddArgs {
85
105
  }
86
106
  if (token === '--trust') {
87
107
  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(', ')}`);
108
+ if (!isMcpTrustMode(value)) throw new Error(`Invalid MCP trust mode "${value}". Expected one of ${MCP_TRUST_MODES.join(', ')}.`);
89
109
  trustMode = value;
90
110
  index += 1;
91
111
  continue;
@@ -157,7 +177,7 @@ export function registerMcpRuntimeCommands(registry: CommandRegistry): void {
157
177
  ` servers: ${servers.length}`,
158
178
  ` connected: ${servers.filter((server) => server.connected).length}`,
159
179
  ` 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'}`),
180
+ ...servers.map((server) => ` ${server.name} trust=${formatMcpTrustMode(server.trustMode)} role=${formatMcpRole(server.role)} status=${formatMcpFreshness(server.schemaFreshness)} connected=${server.connected ? 'yes' : 'no'}`),
161
181
  ' next: /mcp auth-review',
162
182
  ' next: /mcp repair <server>',
163
183
  ].join('\n'));
@@ -218,7 +238,7 @@ export function registerMcpRuntimeCommands(registry: CommandRegistry): void {
218
238
  ? [
219
239
  'MCP Auth Review',
220
240
  ...needingAttention.map((server) => (
221
- ` ${server.name} connected=${server.connected ? 'yes' : 'no'} freshness=${server.schemaFreshness} trust=${server.trustMode}`
241
+ ` ${server.name} connected ${server.connected ? 'yes' : 'no'} status ${formatMcpFreshness(server.schemaFreshness)} trust ${formatMcpTrustMode(server.trustMode)}`
222
242
  )),
223
243
  ' next: /auth review',
224
244
  ' next: /mcp repair <server>',
@@ -233,7 +253,7 @@ export function registerMcpRuntimeCommands(registry: CommandRegistry): void {
233
253
  const selected = serverName ? servers.find((server) => server.name === serverName) : servers.find((server) => !server.connected || server.schemaFreshness === 'quarantined');
234
254
  if (!selected) {
235
255
  ctx.print(serverName
236
- ? `Unknown MCP server: ${serverName}`
256
+ ? `Unknown MCP server ${serverName}`
237
257
  : 'MCP Repair\n No MCP server currently needs repair.');
238
258
  return;
239
259
  }
@@ -248,9 +268,9 @@ export function registerMcpRuntimeCommands(registry: CommandRegistry): void {
248
268
  ctx.print([
249
269
  `MCP Repair: ${selected.name}`,
250
270
  ` connected: ${selected.connected ? 'yes' : 'no'}`,
251
- ` trust: ${selected.trustMode}`,
252
- ` role: ${selected.role}`,
253
- ` freshness: ${selected.schemaFreshness}`,
271
+ ` trust: ${formatMcpTrustMode(selected.trustMode)}`,
272
+ ` role: ${formatMcpRole(selected.role)}`,
273
+ ` status: ${formatMcpFreshness(selected.schemaFreshness)}`,
254
274
  ...(selected.quarantineReason ? [` quarantine: ${selected.quarantineReason}`] : []),
255
275
  ...(selected.quarantineDetail ? [` detail: ${selected.quarantineDetail}`] : []),
256
276
  ' next:',
@@ -273,7 +293,7 @@ export function registerMcpRuntimeCommands(registry: CommandRegistry): void {
273
293
  return;
274
294
  }
275
295
  mcpApi.setServerTrustMode(serverName, mode);
276
- ctx.print(`Updated MCP trust mode for ${serverName} to ${mode}.`);
296
+ ctx.print(`Updated MCP trust mode for ${serverName} to ${formatMcpTrustMode(mode)}.`);
277
297
  return;
278
298
  }
279
299
  if (serverName || mode) {
@@ -291,7 +311,7 @@ export function registerMcpRuntimeCommands(registry: CommandRegistry): void {
291
311
  return;
292
312
  }
293
313
  mcpApi.setServerRole(serverName, role);
294
- ctx.print(`Updated MCP role for ${serverName} to ${role}.`);
314
+ ctx.print(`Updated MCP role for ${serverName} to ${formatMcpRole(role)}.`);
295
315
  return;
296
316
  }
297
317
  if (serverName || role) {
@@ -317,13 +337,16 @@ export function registerMcpRuntimeCommands(registry: CommandRegistry): void {
317
337
  const result = await mcpApi.upsertServerConfig(shellPaths, parsedAdd.scope, parsedAdd.server);
318
338
  const connected = listServerSecurity().find((entry) => entry.name === parsedAdd.server.name)?.connected ?? false;
319
339
  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',
340
+ `MCP server "${parsedAdd.server.name}" saved`,
341
+ ` scope ${parsedAdd.scope}`,
342
+ ` path ${result.path}`,
343
+ ...(parsedAdd.scope === 'global' ? [' global config'] : []),
344
+ ` Runtime reload: ${connected ? 'connected' : 'server saved; connection needs attention'} (+${result.reload.added} ~${result.reload.changed} -${result.reload.removed}, unchanged ${result.reload.unchanged})`,
345
+ ` command ${parsedAdd.server.command}${parsedAdd.server.args?.length ? ` ${parsedAdd.server.args.join(' ')}` : ''}`,
346
+ ' next /mcp tools',
324
347
  ].join('\n'));
325
348
  } catch (error) {
326
- ctx.print(`MCP add failed: ${summarizeError(error)}`);
349
+ ctx.print(`MCP add failed ${summarizeError(error)}`);
327
350
  }
328
351
  return;
329
352
  }
@@ -359,10 +382,10 @@ export function registerMcpRuntimeCommands(registry: CommandRegistry): void {
359
382
  try {
360
383
  const result = await mcpApi.removeServerConfig(shellPaths, scope, serverName);
361
384
  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}.`
385
+ ? `Removed MCP server "${serverName}" from ${scope} config ${result.path}. Reload +${result.reload.added} ~${result.reload.changed} -${result.reload.removed}, unchanged ${result.reload.unchanged}.`
363
386
  : `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
387
  } catch (error) {
365
- ctx.print(`MCP remove failed: ${summarizeError(error)}`);
388
+ ctx.print(`MCP remove failed ${summarizeError(error)}`);
366
389
  }
367
390
  return;
368
391
  }
@@ -375,9 +398,9 @@ export function registerMcpRuntimeCommands(registry: CommandRegistry): void {
375
398
  try {
376
399
  const result = await reloadMcpRuntime(ctx);
377
400
  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}.`);
401
+ 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
402
  } catch (error) {
380
- ctx.print(`MCP reload failed: ${summarizeError(error)}`);
403
+ ctx.print(`MCP reload failed ${summarizeError(error)}`);
381
404
  }
382
405
  return;
383
406
  }
@@ -388,22 +411,22 @@ export function registerMcpRuntimeCommands(registry: CommandRegistry): void {
388
411
  const effective = mcpApi.getEffectiveConfig(shellPaths);
389
412
  ctx.print([
390
413
  'MCP Config',
391
- ' locations:',
414
+ ' locations',
392
415
  ...effective.locations.map((location) => ` ${location.scope}/${location.kind}${location.writable ? ' writable' : ' read-only'} ${location.path}`),
393
- ` effective servers: ${effective.servers.length}`,
416
+ ` effective servers ${effective.servers.length}`,
394
417
  ...effective.servers.map((entry) => {
395
418
  const server = entry.server;
396
419
  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(',')}` : ''}`;
420
+ return ` - ${server.name}: ${server.command}${server.args?.length ? ` ${server.args.join(' ')}` : ''} origin ${entry.source.scope}/${entry.source.kind}${envKeys.length ? ` envKeys=${envKeys.join(',')}` : ''}`;
398
421
  }),
399
422
  '',
400
- 'Add or update from inside Agent with explicit confirmation:',
423
+ 'Add or update from inside Agent with explicit confirmation',
401
424
  ' Open /mcp and choose Add or update server, or use Agent Workspace -> Tools & MCP -> Add MCP server.',
402
- 'Automation equivalent:',
425
+ 'Automation equivalent',
403
426
  ' /mcp add <name> <command> [args...] [--scope project|global] [--role <role>] [--trust <mode>] --yes',
404
427
  ].join('\n'));
405
428
  } catch (error) {
406
- ctx.print(`MCP config read failed: ${summarizeError(error)}`);
429
+ ctx.print(`MCP config read failed ${summarizeError(error)}`);
407
430
  }
408
431
  return;
409
432
  }
@@ -417,12 +440,12 @@ export function registerMcpRuntimeCommands(registry: CommandRegistry): void {
417
440
  }
418
441
  if (action === 'approve') {
419
442
  if (!confirmation.yes) {
420
- requireYesFlag(ctx, `approve MCP schema quarantine override for ${serverName}`, '/mcp quarantine <server> approve [operatorId] --yes');
443
+ requireYesFlag(ctx, `approve MCP tool-definition quarantine override for ${serverName}`, '/mcp quarantine <server> approve [operatorId] --yes');
421
444
  return;
422
445
  }
423
446
  const operatorId = commandArgs[3] || 'operator';
424
447
  mcpApi.approveSchemaQuarantine(serverName, operatorId);
425
- ctx.print(`Approved MCP schema quarantine override for ${serverName} as ${operatorId}. Refresh is still recommended.`);
448
+ ctx.print(`Approved MCP tool-definition quarantine override for ${serverName} as ${operatorId}. Refresh is still recommended.`);
426
449
  return;
427
450
  }
428
451
  if (!confirmation.yes) {
@@ -431,7 +454,7 @@ export function registerMcpRuntimeCommands(registry: CommandRegistry): void {
431
454
  }
432
455
  const detail = commandArgs.slice(2).join(' ') || 'quarantined by operator';
433
456
  mcpApi.quarantineSchema(serverName, 'operator_flagged', detail);
434
- ctx.print(`Quarantined MCP schema for ${serverName}.\nReason: ${detail}`);
457
+ ctx.print(`Quarantined MCP tool definitions for ${serverName}.\nReason: ${detail}`);
435
458
  return;
436
459
  }
437
460
 
@@ -456,18 +479,16 @@ export function registerMcpRuntimeCommands(registry: CommandRegistry): void {
456
479
  });
457
480
  }
458
481
 
459
- type McpSecurityServer = ReturnType<NonNullable<NonNullable<CommandContext['clients']>['mcpApi']>['listServerSecurity']>[number];
460
-
461
482
  function formatMcpServerList(servers: readonly McpSecurityServer[]): string {
462
483
  const connected = servers.filter(s => s.connected);
463
484
  const disconnected = servers.filter(s => !s.connected);
464
485
  const lines: string[] = [`MCP Servers (${connected.length}/${servers.length} connected):`];
465
486
  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}`);
487
+ const pathScope = s.allowedPaths.length > 0 ? ` paths ${s.allowedPaths.length}` : '';
488
+ const hostScope = s.allowedHosts.length > 0 ? ` hosts ${s.allowedHosts.length}` : '';
489
+ const freshness = ` status ${formatMcpFreshness(s.schemaFreshness)}`;
490
+ const quarantine = s.schemaFreshness === 'quarantined' ? ` review reason ${s.quarantineReason ?? 'unknown'}` : '';
491
+ lines.push(` ${s.connected ? '[connected] ' : '[disconnected]'} ${s.name} trust=${formatMcpTrustMode(s.trustMode)} role=${formatMcpRole(s.role)}${freshness}${quarantine}${pathScope}${hostScope}`);
471
492
  }
472
493
  if (connected.length > 0) {
473
494
  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
  }