@vellumai/assistant 0.4.35 → 0.4.37

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 (239) hide show
  1. package/AGENTS.md +1 -1
  2. package/ARCHITECTURE.md +44 -49
  3. package/README.md +32 -20
  4. package/docs/architecture/keychain-broker.md +186 -0
  5. package/docs/architecture/security.md +110 -116
  6. package/docs/runbook-trusted-contacts.md +2 -2
  7. package/docs/skills.md +25 -25
  8. package/package.json +5 -2
  9. package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +11 -2
  10. package/src/__tests__/actor-token-service.test.ts +1 -0
  11. package/src/__tests__/amazon-cdp-integration.test.ts +74 -0
  12. package/src/__tests__/assistant-feature-flags-integration.test.ts +38 -9
  13. package/src/__tests__/assistant-id-boundary-guard.test.ts +29 -0
  14. package/src/__tests__/browser-fill-credential.test.ts +1 -1
  15. package/src/__tests__/bundle-scanner.test.ts +1 -1
  16. package/src/__tests__/channel-guardian.test.ts +102 -102
  17. package/src/__tests__/channel-invite-transport.test.ts +155 -256
  18. package/src/__tests__/channel-readiness-routes.test.ts +336 -0
  19. package/src/__tests__/checker.test.ts +6 -6
  20. package/src/__tests__/chrome-cdp.test.ts +350 -0
  21. package/src/__tests__/computer-use-session-lifecycle.test.ts +3 -3
  22. package/src/__tests__/computer-use-session-working-dir.test.ts +86 -52
  23. package/src/__tests__/computer-use-skill-lifecycle-cleanup.test.ts +1 -1
  24. package/src/__tests__/config-loader-migration.test.ts +85 -0
  25. package/src/__tests__/conversation-pairing.test.ts +370 -5
  26. package/src/__tests__/credential-broker-browser-fill.test.ts +1 -10
  27. package/src/__tests__/credential-broker-server-use.test.ts +1 -10
  28. package/src/__tests__/credential-security-e2e.test.ts +7 -1
  29. package/src/__tests__/credential-security-invariants.test.ts +14 -20
  30. package/src/__tests__/credential-vault-unit.test.ts +1 -11
  31. package/src/__tests__/credential-vault.test.ts +5 -19
  32. package/src/__tests__/credentials-cli.test.ts +814 -0
  33. package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +23 -4
  34. package/src/__tests__/email-invite-adapter.test.ts +78 -0
  35. package/src/__tests__/email-service-config-fallback.test.ts +102 -0
  36. package/src/__tests__/encrypted-store.test.ts +6 -6
  37. package/src/__tests__/ephemeral-permissions.test.ts +3 -3
  38. package/src/__tests__/gateway-only-enforcement.test.ts +5 -1
  39. package/src/__tests__/guardian-actions-endpoint.test.ts +70 -12
  40. package/src/__tests__/guardian-outbound-http.test.ts +53 -47
  41. package/src/__tests__/handle-user-message-secret-resume.test.ts +23 -0
  42. package/src/__tests__/handlers-add-trust-rule-metadata.test.ts +32 -23
  43. package/src/__tests__/handlers-telegram-config.test.ts +8 -2
  44. package/src/__tests__/handlers-twitter-config.test.ts +2 -2
  45. package/src/__tests__/handlers-user-message-approval-consumption.test.ts +108 -7
  46. package/src/__tests__/ingress-reconcile.test.ts +6 -0
  47. package/src/__tests__/intent-routing.test.ts +23 -4
  48. package/src/__tests__/invite-routes-http.test.ts +12 -0
  49. package/src/__tests__/ipc-snapshot.test.ts +8 -2
  50. package/src/__tests__/keychain-broker-client.test.ts +543 -0
  51. package/src/__tests__/llm-usage-store.test.ts +344 -0
  52. package/src/__tests__/mcp-client-auth.test.ts +2 -2
  53. package/src/__tests__/media-reuse-story.e2e.test.ts +1 -1
  54. package/src/__tests__/migration-transport.test.ts +49 -0
  55. package/src/__tests__/notification-broadcaster.test.ts +205 -5
  56. package/src/__tests__/notification-deep-link.test.ts +365 -1
  57. package/src/__tests__/oauth-connect-handler.test.ts +2 -2
  58. package/src/__tests__/onboarding-starter-tasks.test.ts +17 -4
  59. package/src/__tests__/proxy-approval-callback.test.ts +1 -1
  60. package/src/__tests__/recording-handler.test.ts +1 -1
  61. package/src/__tests__/recording-intent-handler.test.ts +6 -1
  62. package/src/__tests__/recording-state-machine.test.ts +1 -1
  63. package/src/__tests__/relay-server.test.ts +9 -1
  64. package/src/__tests__/ride-shotgun-handler.test.ts +499 -0
  65. package/src/__tests__/runtime-attachment-metadata.test.ts +160 -1
  66. package/src/__tests__/script-proxy-injection-runtime.test.ts +299 -2
  67. package/src/__tests__/script-proxy-profile-template-fallback.test.ts +1 -1
  68. package/src/__tests__/secret-onetime-send.test.ts +8 -2
  69. package/src/__tests__/secure-keys.test.ts +175 -216
  70. package/src/__tests__/session-confirmation-signals.test.ts +1 -1
  71. package/src/__tests__/session-messaging-secret-redirect.test.ts +1 -1
  72. package/src/__tests__/session-queue.test.ts +2 -1
  73. package/src/__tests__/session-tool-setup-app-refresh.test.ts +2 -2
  74. package/src/__tests__/skill-feature-flags-integration.test.ts +29 -4
  75. package/src/__tests__/skill-feature-flags.test.ts +12 -9
  76. package/src/__tests__/skill-load-feature-flag.test.ts +26 -5
  77. package/src/__tests__/skill-projection.benchmark.test.ts +0 -1
  78. package/src/__tests__/skills.test.ts +34 -4
  79. package/src/__tests__/slack-channel-config.test.ts +2 -2
  80. package/src/__tests__/system-prompt.test.ts +26 -4
  81. package/src/__tests__/telegram-bot-username-resolution.test.ts +212 -0
  82. package/src/__tests__/telegram-invite-adapter.test.ts +164 -0
  83. package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +1 -1
  84. package/src/__tests__/tool-permission-simulate-handler.test.ts +8 -2
  85. package/src/__tests__/trusted-contact-approval-notifier.test.ts +9 -1
  86. package/src/__tests__/twitter-auth-handler.test.ts +2 -2
  87. package/src/__tests__/twitter-oauth-client.test.ts +1 -1
  88. package/src/__tests__/usage-routes.test.ts +339 -0
  89. package/src/__tests__/whatsapp-invite-adapter.test.ts +94 -0
  90. package/src/agent/loop.ts +3 -0
  91. package/src/amazon/checkout.ts +0 -1
  92. package/src/approvals/guardian-request-resolvers.ts +9 -1
  93. package/src/bundler/app-bundler.ts +28 -12
  94. package/src/bundler/bundle-scanner.ts +1 -1
  95. package/src/bundler/bundle-signer.ts +3 -3
  96. package/src/bundler/manifest.ts +1 -1
  97. package/src/bundler/signature-verifier.ts +3 -3
  98. package/src/channels/config.ts +1 -1
  99. package/src/cli/AGENTS.md +63 -0
  100. package/src/cli/__tests__/notifications.test.ts +470 -0
  101. package/src/cli/amazon.ts +344 -167
  102. package/src/cli/audit.ts +85 -0
  103. package/src/cli/autonomy.ts +369 -0
  104. package/src/cli/channels.ts +51 -0
  105. package/src/cli/completions.ts +208 -0
  106. package/src/cli/config.ts +220 -0
  107. package/src/cli/contacts.ts +471 -0
  108. package/src/cli/credentials.ts +564 -0
  109. package/src/cli/default-action.ts +14 -0
  110. package/src/cli/dev.ts +131 -0
  111. package/src/cli/doctor.ts +398 -0
  112. package/src/cli/email.ts +494 -0
  113. package/src/cli/influencer.ts +72 -0
  114. package/src/cli/integrations.ts +248 -57
  115. package/src/cli/keys.ts +114 -0
  116. package/src/cli/map.ts +46 -54
  117. package/src/cli/mcp.ts +111 -3
  118. package/src/cli/{config-commands.ts → memory.ts} +134 -245
  119. package/src/cli/notifications.ts +407 -0
  120. package/src/cli/program.ts +65 -0
  121. package/src/cli/reference.ts +48 -0
  122. package/src/cli/sequence.ts +154 -0
  123. package/src/cli/sessions.ts +262 -0
  124. package/src/cli/trust.ts +175 -0
  125. package/src/cli/twitter.ts +323 -106
  126. package/src/config/__tests__/build-cli-reference-section.test.ts +49 -0
  127. package/src/config/bundled-skills/amazon/SKILL.md +2 -2
  128. package/src/config/bundled-skills/app-builder/TOOLS.json +26 -0
  129. package/src/config/bundled-skills/app-builder/tools/app-generate-icon.ts +13 -0
  130. package/src/config/bundled-skills/contacts/SKILL.md +178 -10
  131. package/src/config/bundled-skills/doordash/doordash-cli.ts +23 -168
  132. package/src/config/bundled-skills/google-oauth-setup/SKILL.md +135 -34
  133. package/src/config/bundled-skills/messaging/tools/shared.ts +4 -1
  134. package/src/config/bundled-skills/twilio-setup/SKILL.md +70 -17
  135. package/src/config/bundled-tool-registry.ts +2 -0
  136. package/src/config/core-schema.ts +7 -0
  137. package/src/config/feature-flag-registry.json +16 -0
  138. package/src/config/loader.ts +26 -0
  139. package/src/config/schema.ts +4 -0
  140. package/src/config/skill-state.ts +0 -13
  141. package/src/config/system-prompt.ts +27 -0
  142. package/src/contacts/contact-store.ts +25 -0
  143. package/src/daemon/computer-use-session.ts +1 -1
  144. package/src/daemon/handlers/apps.ts +1 -0
  145. package/src/daemon/handlers/config-channels.ts +3 -3
  146. package/src/daemon/handlers/config-dispatch.ts +29 -0
  147. package/src/daemon/handlers/config-inbox.ts +4 -3
  148. package/src/daemon/handlers/config.ts +3 -43
  149. package/src/daemon/handlers/contacts.ts +34 -0
  150. package/src/daemon/handlers/index.ts +17 -3
  151. package/src/daemon/handlers/session-user-message.ts +7 -0
  152. package/src/daemon/handlers/sessions.ts +21 -2
  153. package/src/daemon/handlers/shared.ts +17 -0
  154. package/src/daemon/ipc-contract/apps.ts +2 -0
  155. package/src/daemon/ipc-contract/computer-use.ts +9 -0
  156. package/src/daemon/ipc-contract/contacts.ts +3 -3
  157. package/src/daemon/ipc-contract/inbox.ts +2 -0
  158. package/src/daemon/ipc-contract/messages.ts +4 -0
  159. package/src/daemon/ipc-contract/sessions.ts +8 -0
  160. package/src/daemon/ipc-contract-inventory.json +1 -0
  161. package/src/daemon/lifecycle.ts +0 -5
  162. package/src/daemon/ride-shotgun-handler.ts +139 -25
  163. package/src/daemon/session-agent-loop-handlers.ts +100 -0
  164. package/src/daemon/session-agent-loop.ts +72 -0
  165. package/src/daemon/session-tool-setup.ts +7 -0
  166. package/src/daemon/session.ts +23 -1
  167. package/src/daemon/tool-side-effects.ts +39 -1
  168. package/src/email/service.ts +59 -2
  169. package/src/index.ts +2 -60
  170. package/src/mcp/mcp-oauth-provider.ts +90 -8
  171. package/src/media/app-icon-generator.ts +86 -0
  172. package/src/memory/db-init.ts +11 -0
  173. package/src/memory/llm-usage-store.ts +186 -0
  174. package/src/memory/migrations/137-usage-dashboard-indexes.ts +26 -0
  175. package/src/memory/migrations/139-drop-usage-composite-indexes.ts +30 -0
  176. package/src/memory/migrations/index.ts +2 -0
  177. package/src/memory/schema-migration.ts +1 -0
  178. package/src/memory/shared-app-links-store.ts +1 -1
  179. package/src/messaging/registry.ts +27 -0
  180. package/src/notifications/README.md +79 -70
  181. package/src/notifications/broadcaster.ts +2 -1
  182. package/src/notifications/conversation-pairing.ts +147 -13
  183. package/src/notifications/copy-composer.ts +7 -3
  184. package/src/notifications/destination-resolver.ts +14 -1
  185. package/src/notifications/emit-signal.ts +3 -2
  186. package/src/notifications/signal.ts +105 -1
  187. package/src/notifications/types.ts +16 -0
  188. package/src/permissions/checker.ts +29 -3
  189. package/src/permissions/prompter.ts +11 -3
  190. package/src/runtime/access-request-helper.ts +2 -1
  191. package/src/runtime/auth/route-policy.ts +7 -1
  192. package/src/runtime/channel-invite-transport.ts +40 -63
  193. package/src/runtime/channel-invite-transports/email.ts +13 -39
  194. package/src/runtime/channel-invite-transports/slack.ts +5 -34
  195. package/src/runtime/channel-invite-transports/sms.ts +8 -29
  196. package/src/runtime/channel-invite-transports/telegram.ts +69 -28
  197. package/src/runtime/channel-invite-transports/voice.ts +0 -7
  198. package/src/runtime/channel-invite-transports/whatsapp.ts +43 -0
  199. package/src/runtime/channel-readiness-service.ts +202 -45
  200. package/src/runtime/confirmation-request-guardian-bridge.ts +2 -1
  201. package/src/runtime/guardian-outbound-actions.ts +8 -5
  202. package/src/runtime/http-server.ts +2 -0
  203. package/src/runtime/invite-instruction-generator.ts +178 -0
  204. package/src/runtime/invite-service.ts +22 -25
  205. package/src/runtime/migrations/migration-transport.ts +13 -0
  206. package/src/runtime/routes/app-routes.ts +1 -1
  207. package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +8 -7
  208. package/src/runtime/routes/channel-readiness-routes.ts +30 -11
  209. package/src/runtime/routes/contact-routes.ts +54 -26
  210. package/src/runtime/routes/inbound-stages/bootstrap-intercept.ts +1 -1
  211. package/src/runtime/routes/inbound-stages/escalation-intercept.ts +2 -1
  212. package/src/runtime/routes/inbound-stages/verification-intercept.ts +2 -1
  213. package/src/runtime/routes/integration-routes.ts +1 -1
  214. package/src/runtime/routes/invite-routes.ts +1 -1
  215. package/src/runtime/routes/secret-routes.ts +31 -7
  216. package/src/runtime/routes/twilio-routes.ts +32 -1
  217. package/src/runtime/routes/usage-routes.ts +114 -0
  218. package/src/runtime/tool-grant-request-helper.ts +2 -1
  219. package/src/security/encrypted-store.ts +9 -5
  220. package/src/security/keychain-broker-client.ts +393 -0
  221. package/src/security/secure-keys.ts +106 -321
  222. package/src/tools/apps/executors.ts +73 -0
  223. package/src/tools/browser/auto-navigate.ts +15 -6
  224. package/src/tools/browser/chrome-cdp.ts +211 -0
  225. package/src/tools/browser/network-recorder.test.ts +83 -0
  226. package/src/tools/browser/network-recorder.ts +8 -7
  227. package/src/tools/browser/x-auto-navigate.ts +12 -6
  228. package/src/tools/credentials/policy-types.ts +24 -0
  229. package/src/tools/credentials/vault.ts +22 -27
  230. package/src/tools/network/script-proxy/session-manager.ts +47 -3
  231. package/src/tools/permission-checker.ts +1 -0
  232. package/src/tools/types.ts +2 -0
  233. package/src/tools/ui-surface/definitions.ts +1 -2
  234. package/src/tools/watch/watch-state.ts +2 -0
  235. package/src/__tests__/key-migration.test.ts +0 -240
  236. package/src/__tests__/keychain.test.ts +0 -286
  237. package/src/cli/core-commands.ts +0 -899
  238. package/src/security/keychain-to-encrypted-migration.ts +0 -66
  239. package/src/security/keychain.ts +0 -490
@@ -67,6 +67,22 @@ export interface EventHandlerState {
67
67
  firstThinkingDeltaEmitted: boolean;
68
68
  /** Name of the last completed tool, used to generate contextual statusText. */
69
69
  lastCompletedToolName: string | undefined;
70
+ /** Tracks tool_use_id → timing data for persisting on content blocks. */
71
+ readonly toolCallTimestamps: Map<
72
+ string,
73
+ { startedAt: number; completedAt?: number }
74
+ >;
75
+ /** The tool_use_id of the currently executing tool (set in handleToolUse, cleared in handleToolResult). */
76
+ currentToolUseId: string | undefined;
77
+ /** Maps confirmation requestId → tool_use_id for linking decisions to tools. */
78
+ readonly requestIdToToolUseId: Map<string, string>;
79
+ /** Stores confirmation outcomes keyed by tool_use_id. */
80
+ readonly toolConfirmationOutcomes: Map<
81
+ string,
82
+ { decision: string; label: string }
83
+ >;
84
+ /** tool_use_ids emitted in the current turn (populated in handleToolUse, cleared after annotation). */
85
+ currentTurnToolUseIds: string[];
70
86
  }
71
87
 
72
88
  /** Immutable context shared across event handlers within a single agent loop run. */
@@ -108,6 +124,11 @@ export function createEventHandlerState(): EventHandlerState {
108
124
  firstTextDeltaEmitted: false,
109
125
  firstThinkingDeltaEmitted: false,
110
126
  lastCompletedToolName: undefined,
127
+ toolCallTimestamps: new Map(),
128
+ currentToolUseId: undefined,
129
+ requestIdToToolUseId: new Map(),
130
+ toolConfirmationOutcomes: new Map(),
131
+ currentTurnToolUseIds: [],
111
132
  };
112
133
  }
113
134
 
@@ -253,6 +274,9 @@ export function handleToolUse(
253
274
  ): void {
254
275
  state.toolUseIdToName.set(event.id, event.name);
255
276
  state.currentTurnToolNames.push(event.name);
277
+ state.toolCallTimestamps.set(event.id, { startedAt: Date.now() });
278
+ state.currentToolUseId = event.id;
279
+ state.currentTurnToolUseIds.push(event.id);
256
280
  const statusText = `Running ${friendlyToolName(event.name)}`;
257
281
  deps.ctx.emitActivityState(
258
282
  "tool_running",
@@ -266,6 +290,7 @@ export function handleToolUse(
266
290
  toolName: event.name,
267
291
  input: event.input,
268
292
  sessionId: deps.ctx.conversationId,
293
+ toolUseId: event.id,
269
294
  });
270
295
  }
271
296
 
@@ -392,6 +417,11 @@ export function handleToolResult(
392
417
  contentBlocks: event.contentBlocks,
393
418
  });
394
419
 
420
+ // Record tool completion timestamp
421
+ const ts = state.toolCallTimestamps.get(event.toolUseId);
422
+ if (ts) ts.completedAt = Date.now();
423
+ state.currentToolUseId = undefined;
424
+
395
425
  const toolName = state.toolUseIdToName.get(event.toolUseId);
396
426
  if (toolName === "file_write" || toolName === "bash") {
397
427
  deps.ctx.markWorkspaceTopLevelDirty();
@@ -433,6 +463,68 @@ export function handleToolResult(
433
463
  deps.reqId,
434
464
  statusText,
435
465
  );
466
+
467
+ // Once all tools for this turn have completed, annotate the persisted
468
+ // assistant message with timing and confirmation metadata.
469
+ const allToolsDone = state.currentTurnToolUseIds.every((id) => {
470
+ const ts = state.toolCallTimestamps.get(id);
471
+ return ts && ts.completedAt != null;
472
+ });
473
+ if (allToolsDone && state.currentTurnToolUseIds.length > 0) {
474
+ annotatePersistedAssistantMessage(state);
475
+ }
476
+ }
477
+
478
+ /**
479
+ * After all tools for the current turn complete, fetch the persisted assistant
480
+ * message, annotate its tool_use blocks with timing and confirmation metadata,
481
+ * and update the DB. This runs post-tool-execution so the metadata maps are
482
+ * fully populated (unlike message_complete which fires before tools run).
483
+ */
484
+ function annotatePersistedAssistantMessage(state: EventHandlerState): void {
485
+ const messageId = state.lastAssistantMessageId;
486
+ if (!messageId) return;
487
+
488
+ const row = conversationStore.getMessageById(messageId);
489
+ if (!row) return;
490
+
491
+ let content: ContentBlock[];
492
+ try {
493
+ content = JSON.parse(row.content) as ContentBlock[];
494
+ } catch {
495
+ return;
496
+ }
497
+
498
+ let modified = false;
499
+ for (const block of content) {
500
+ if (block.type === "tool_use") {
501
+ const rec = block as unknown as Record<string, unknown>;
502
+ const id = rec.id as string | undefined;
503
+ if (!id) continue;
504
+
505
+ const ts = state.toolCallTimestamps.get(id);
506
+ if (ts) {
507
+ rec._startedAt = ts.startedAt;
508
+ if (ts.completedAt != null) {
509
+ rec._completedAt = ts.completedAt;
510
+ }
511
+ modified = true;
512
+ }
513
+ const confirmation = state.toolConfirmationOutcomes.get(id);
514
+ if (confirmation) {
515
+ rec._confirmationDecision = confirmation.decision;
516
+ rec._confirmationLabel = confirmation.label;
517
+ modified = true;
518
+ }
519
+ }
520
+ }
521
+
522
+ if (modified) {
523
+ conversationStore.updateMessageContent(messageId, JSON.stringify(content));
524
+ }
525
+
526
+ // Clear for the next turn
527
+ state.currentTurnToolUseIds = [];
436
528
  }
437
529
 
438
530
  export function handleError(
@@ -465,6 +557,9 @@ export async function handleMessageComplete(
465
557
  deps: EventHandlerDeps,
466
558
  event: Extract<AgentEvent, { type: "message_complete" }>,
467
559
  ): Promise<void> {
560
+ // Reset per-turn tool tracking for the new turn.
561
+ state.currentTurnToolUseIds = [];
562
+
468
563
  // Flush any remaining directive display buffer
469
564
  if (state.pendingDirectiveDisplayBuffer.length > 0) {
470
565
  deps.onEvent({
@@ -533,6 +628,11 @@ export async function handleMessageComplete(
533
628
  );
534
629
  }
535
630
 
631
+ // NOTE: Tool timing/confirmation annotations are NOT applied here because
632
+ // message_complete fires BEFORE tool_use/tool_result events. The annotations
633
+ // are applied in handleToolResult after all tools for the turn complete,
634
+ // then the persisted message is updated via updateMessageContent.
635
+
536
636
  // Build content with UI surfaces
537
637
  const contentWithSurfaces: ContentBlock[] = [...cleanedBlocks];
538
638
  for (const surface of deps.ctx.currentTurnSurfaces) {
@@ -118,6 +118,27 @@ import type { TraceEmitter } from "./trace-emitter.js";
118
118
 
119
119
  const log = getLogger("session-agent-loop");
120
120
 
121
+ /** Title-cased friendly labels for tool names, used in confirmation chips. */
122
+ const TOOL_FRIENDLY_LABEL: Record<string, string> = {
123
+ bash: "Run Command",
124
+ web_search: "Web Search",
125
+ web_fetch: "Web Fetch",
126
+ file_read: "Read File",
127
+ file_write: "Write File",
128
+ file_edit: "Edit File",
129
+ browser_navigate: "Browser",
130
+ browser_click: "Browser",
131
+ browser_type: "Browser",
132
+ browser_screenshot: "Browser",
133
+ browser_scroll: "Browser",
134
+ browser_wait: "Browser",
135
+ app_create: "Create App",
136
+ app_update: "Update App",
137
+ skill_load: "Load Skill",
138
+ app_file_edit: "Edit App File",
139
+ app_file_write: "Write App File",
140
+ };
141
+
121
142
  type GitServiceInitializer = {
122
143
  ensureInitialized(): Promise<void>;
123
144
  };
@@ -222,6 +243,18 @@ export interface AgentLoopSessionContext {
222
243
  : never,
223
244
  ): void;
224
245
 
246
+ /**
247
+ * Optional callback invoked by the Session when a confirmation state changes.
248
+ * The agent loop registers this to track requestId → toolUseId mappings
249
+ * and record confirmation outcomes for persistence.
250
+ */
251
+ onConfirmationOutcome?: (
252
+ requestId: string,
253
+ state: string,
254
+ toolName?: string,
255
+ toolUseId?: string,
256
+ ) => void;
257
+
225
258
  getWorkspaceGitService?: (workspaceDir: string) => GitServiceInitializer;
226
259
  commitTurnChanges?: typeof commitTurnChanges;
227
260
 
@@ -432,6 +465,44 @@ export async function runAgentLoopImpl(
432
465
  }
433
466
 
434
467
  const state = createEventHandlerState();
468
+
469
+ // Register confirmation outcome tracker so the agent loop can link
470
+ // confirmation decisions to tool_use_ids for persistence.
471
+ ctx.onConfirmationOutcome = (
472
+ requestId,
473
+ confirmationState,
474
+ toolName,
475
+ toolUseId,
476
+ ) => {
477
+ if (confirmationState === "pending") {
478
+ // Use the toolUseId passed from the prompter (which knows which tool
479
+ // requested confirmation) instead of the ambient state.currentToolUseId,
480
+ // which is unreliable when multiple tools execute in parallel.
481
+ const resolvedToolUseId = toolUseId ?? state.currentToolUseId;
482
+ if (resolvedToolUseId) {
483
+ state.requestIdToToolUseId.set(requestId, resolvedToolUseId);
484
+ }
485
+ } else if (
486
+ confirmationState === "approved" ||
487
+ confirmationState === "denied" ||
488
+ confirmationState === "timed_out"
489
+ ) {
490
+ const resolvedId =
491
+ state.requestIdToToolUseId.get(requestId) ?? toolUseId;
492
+ if (resolvedId) {
493
+ const name = state.toolUseIdToName.get(resolvedId) ?? toolName ?? "";
494
+ // Build a friendly label from the tool name
495
+ const label =
496
+ TOOL_FRIENDLY_LABEL[name] ??
497
+ name.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
498
+ state.toolConfirmationOutcomes.set(resolvedId, {
499
+ decision: confirmationState,
500
+ label,
501
+ });
502
+ }
503
+ }
504
+ };
505
+
435
506
  let runMessages = ctx.messages;
436
507
 
437
508
  const memoryResult = await prepareMemoryContext(
@@ -1347,6 +1418,7 @@ export async function runAgentLoopImpl(
1347
1418
 
1348
1419
  ctx.abortController = null;
1349
1420
  ctx.processing = false;
1421
+ ctx.onConfirmationOutcome = undefined;
1350
1422
  ctx.surfaceActionRequestIds.delete(ctx.currentRequestId ?? "");
1351
1423
  ctx.currentRequestId = undefined;
1352
1424
  ctx.currentActiveSurfaceId = undefined;
@@ -135,6 +135,7 @@ export function createToolExecutor(
135
135
  name: string,
136
136
  input: Record<string, unknown>,
137
137
  onOutput?: (chunk: string) => void,
138
+ toolUseId?: string,
138
139
  ) => Promise<ToolExecutionResult> {
139
140
  // Register the session's sendToClient for browser screencast surface messages
140
141
  registerSessionSender(ctx.conversationId, (msg) => ctx.sendToClient(msg));
@@ -143,6 +144,7 @@ export function createToolExecutor(
143
144
  name: string,
144
145
  input: Record<string, unknown>,
145
146
  onOutput?: (chunk: string) => void,
147
+ toolUseId?: string,
146
148
  ) => {
147
149
  if (isDoordashCommand(name, input)) {
148
150
  markDoordashStepInProgress(ctx, input);
@@ -172,6 +174,7 @@ export function createToolExecutor(
172
174
  allowedToolNames: ctx.allowedToolNames,
173
175
  memoryScopeId: ctx.memoryPolicy.scopeId,
174
176
  forcePromptSideEffects: ctx.memoryPolicy.strictSideEffects,
177
+ toolUseId,
175
178
  onToolLifecycleEvent: handleToolLifecycleEvent,
176
179
  sendToClient: (msg) => {
177
180
  // Tool context's sendToClient uses a loose { type: string; [key: string]: unknown }
@@ -255,6 +258,10 @@ export function createToolExecutor(
255
258
  undefined,
256
259
  ctx.conversationId,
257
260
  req.executionTarget,
261
+ undefined,
262
+ undefined,
263
+ undefined,
264
+ toolUseId,
258
265
  );
259
266
  if (
260
267
  (response.decision === "always_allow" ||
@@ -222,6 +222,13 @@ export class Session {
222
222
  * no-op for socketless sessions.
223
223
  */
224
224
  private onStateSignal?: (msg: ServerMessage) => void;
225
+ /** Set by the agent loop to track confirmation outcomes for persistence. */
226
+ onConfirmationOutcome?: (
227
+ requestId: string,
228
+ state: string,
229
+ toolName?: string,
230
+ toolUseId?: string,
231
+ ) => void;
225
232
 
226
233
  constructor(
227
234
  conversationId: string,
@@ -243,7 +250,7 @@ export class Session {
243
250
  : { ...DEFAULT_MEMORY_POLICY };
244
251
  this.traceEmitter = new TraceEmitter(conversationId, sendToClient);
245
252
  this.prompter = new PermissionPrompter(sendToClient);
246
- this.prompter.setOnStateChanged((requestId, state, source) => {
253
+ this.prompter.setOnStateChanged((requestId, state, source, toolUseId) => {
247
254
  // Route through emitConfirmationStateChanged so the onStateSignal
248
255
  // listener publishes to the SSE hub for HTTP/SSE consumers.
249
256
  this.emitConfirmationStateChanged({
@@ -251,7 +258,11 @@ export class Session {
251
258
  requestId,
252
259
  state,
253
260
  source,
261
+ toolUseId,
254
262
  });
263
+ // Notify the agent loop so it can track requestId → toolUseId mappings
264
+ // and record confirmation outcomes for persistence.
265
+ this.onConfirmationOutcome?.(requestId, state, undefined, toolUseId);
255
266
  // Emit activity state transitions for confirmation lifecycle
256
267
  if (state === "pending") {
257
268
  this.emitActivityState(
@@ -523,6 +534,9 @@ export class Session {
523
534
  return;
524
535
  }
525
536
 
537
+ // Capture toolUseId before resolving (resolution deletes the pending entry)
538
+ const toolUseId = this.prompter.getToolUseId(requestId);
539
+
526
540
  this.prompter.resolveConfirmation(
527
541
  requestId,
528
542
  decision,
@@ -547,6 +561,7 @@ export class Session {
547
561
  requestId,
548
562
  state: resolvedState,
549
563
  source: emissionContext?.source ?? "button",
564
+ toolUseId,
550
565
  ...(emissionContext?.causedByRequestId
551
566
  ? { causedByRequestId: emissionContext.causedByRequestId }
552
567
  : {}),
@@ -554,6 +569,13 @@ export class Session {
554
569
  ? { decisionText: emissionContext.decisionText }
555
570
  : {}),
556
571
  });
572
+ // Notify the agent loop of the confirmation outcome for persistence
573
+ this.onConfirmationOutcome?.(
574
+ requestId,
575
+ resolvedState,
576
+ undefined,
577
+ toolUseId,
578
+ );
557
579
  this.emitActivityState(
558
580
  "thinking",
559
581
  "confirmation_resolved",
@@ -9,14 +9,18 @@
9
9
 
10
10
  import { join } from "node:path";
11
11
 
12
+ import { generateAppIcon } from "../media/app-icon-generator.js";
12
13
  import { updatePublishedAppDeployment } from "../services/published-app-updater.js";
13
14
  import type { ToolExecutionResult } from "../tools/types.js";
15
+ import { getLogger } from "../util/logger.js";
14
16
  import { getWorkspaceDir } from "../util/platform.js";
15
17
  import { isDoordashCommand, updateDoordashProgress } from "./doordash-steps.js";
16
18
  import type { ServerMessage } from "./ipc-protocol.js";
17
19
  import { refreshSurfacesForApp } from "./session-surfaces.js";
18
20
  import type { ToolSetupContext } from "./session-tool-setup.js";
19
21
 
22
+ const log = getLogger("tool-side-effects");
23
+
20
24
  // ── Types ────────────────────────────────────────────────────────────
21
25
 
22
26
  export interface SideEffectContext {
@@ -65,13 +69,36 @@ function registerHook(
65
69
 
66
70
  // Broadcast app_files_changed when a new app is created so clients
67
71
  // (e.g. macOS "Things" sidebar) refresh their app list immediately.
72
+ // Also kicks off async icon generation via Gemini.
68
73
  registerHook(
69
74
  "app_create",
70
75
  (_name, _input, result, { ctx, broadcastToAllClients }) => {
71
76
  try {
72
- const parsed = JSON.parse(result.content) as { id?: string };
77
+ const parsed = JSON.parse(result.content) as {
78
+ id?: string;
79
+ name?: string;
80
+ description?: string;
81
+ };
73
82
  if (parsed.id) {
74
83
  handleAppChange(ctx, parsed.id, broadcastToAllClients);
84
+
85
+ // Fire-and-forget: generate an app icon in the background.
86
+ // When complete, broadcast again so clients pick up the new icon.
87
+ if (parsed.name) {
88
+ void generateAppIcon(parsed.id, parsed.name, parsed.description)
89
+ .then(() => {
90
+ broadcastToAllClients?.({
91
+ type: "app_files_changed",
92
+ appId: parsed.id!,
93
+ });
94
+ })
95
+ .catch((err) => {
96
+ log.warn(
97
+ { err, appId: parsed.id },
98
+ "Background icon generation failed",
99
+ );
100
+ });
101
+ }
75
102
  }
76
103
  } catch {
77
104
  // Result wasn't valid JSON — skip the broadcast.
@@ -79,6 +106,17 @@ registerHook(
79
106
  },
80
107
  );
81
108
 
109
+ // Broadcast app_files_changed when an icon is (re)generated so clients refresh.
110
+ registerHook(
111
+ "app_generate_icon",
112
+ (_name, input, _result, { broadcastToAllClients }) => {
113
+ const appId = input.app_id as string | undefined;
114
+ if (appId) {
115
+ broadcastToAllClients?.({ type: "app_files_changed", appId });
116
+ }
117
+ },
118
+ );
119
+
82
120
  // Auto-refresh workspace surfaces when a persisted app is updated.
83
121
  registerHook(
84
122
  "app_update",
@@ -17,6 +17,7 @@ import {
17
17
  setOutboundPaused,
18
18
  } from "../cli/email-guardrails.js";
19
19
  import {
20
+ getNestedValue,
20
21
  loadRawConfig,
21
22
  saveRawConfig,
22
23
  setNestedValue,
@@ -74,6 +75,8 @@ export class EmailService {
74
75
  /** Force re-creation of the provider (e.g. after `provider set`). */
75
76
  resetProvider(): void {
76
77
  this.providerInstance = null;
78
+ this.primaryAddressResolved = false;
79
+ this.cachedPrimaryAddress = undefined;
77
80
  }
78
81
 
79
82
  // =========================================================================
@@ -109,6 +112,54 @@ export class EmailService {
109
112
  };
110
113
  }
111
114
 
115
+ // =========================================================================
116
+ // Primary inbox address (cached)
117
+ // =========================================================================
118
+
119
+ private primaryAddressResolved = false;
120
+ private cachedPrimaryAddress: string | undefined;
121
+
122
+ /**
123
+ * Return the assistant's primary inbox email address, caching the result
124
+ * for the lifetime of this service instance. Returns `undefined` when no
125
+ * inboxes are configured or the provider is unavailable.
126
+ */
127
+ async getPrimaryInboxAddress(): Promise<string | undefined> {
128
+ if (this.primaryAddressResolved) {
129
+ return this.cachedPrimaryAddress;
130
+ }
131
+ try {
132
+ const p = await this.provider();
133
+ const health = await p.health();
134
+ this.cachedPrimaryAddress =
135
+ health.inboxes.length > 0 ? health.inboxes[0].address : undefined;
136
+ } catch {
137
+ this.cachedPrimaryAddress = undefined;
138
+ }
139
+
140
+ // Only cache positive results from the provider so a missing inbox is
141
+ // retried on next call (e.g. user sets up email after initial miss).
142
+ if (this.cachedPrimaryAddress !== undefined) {
143
+ this.primaryAddressResolved = true;
144
+ return this.cachedPrimaryAddress;
145
+ }
146
+
147
+ // Fall back to the statically configured email address in workspace config
148
+ // when the provider can't list inboxes (e.g. provider temporarily unavailable).
149
+ // Intentionally NOT setting primaryAddressResolved so the provider is retried
150
+ // on the next call — the fallback is a best-effort stopgap, not authoritative.
151
+ try {
152
+ const raw = loadRawConfig();
153
+ const configured = getNestedValue(raw, "email.address");
154
+ if (typeof configured === "string" && configured.length > 0) {
155
+ return configured;
156
+ }
157
+ } catch {
158
+ // Config unavailable — leave as undefined
159
+ }
160
+ return undefined;
161
+ }
162
+
112
163
  // =========================================================================
113
164
  // Domain setup
114
165
  // =========================================================================
@@ -138,7 +189,10 @@ export class EmailService {
138
189
  displayName?: string,
139
190
  ): Promise<EmailInbox> {
140
191
  const p = await this.provider();
141
- return p.createInbox({ username, domain, displayName });
192
+ const inbox = await p.createInbox({ username, domain, displayName });
193
+ this.primaryAddressResolved = false;
194
+ this.cachedPrimaryAddress = undefined;
195
+ return inbox;
142
196
  }
143
197
 
144
198
  async listInboxes(): Promise<EmailInbox[]> {
@@ -148,7 +202,10 @@ export class EmailService {
148
202
 
149
203
  async ensureInboxes(domain: string): Promise<EmailInbox[]> {
150
204
  const p = await this.provider();
151
- return p.ensureInboxes({ domain });
205
+ const inboxes = await p.ensureInboxes({ domain });
206
+ this.primaryAddressResolved = false;
207
+ this.cachedPrimaryAddress = undefined;
208
+ return inboxes;
152
209
  }
153
210
 
154
211
  // =========================================================================
package/src/index.ts CHANGED
@@ -1,63 +1,5 @@
1
1
  #!/usr/bin/env bun
2
2
 
3
- import { createRequire } from "node:module";
3
+ import { buildCliProgram } from "./cli/program.js";
4
4
 
5
- import { Command } from "commander";
6
-
7
- const require = createRequire(import.meta.url);
8
- const { version } = require("../package.json") as { version: string };
9
-
10
- import { registerAmazonCommand } from "./cli/amazon.js";
11
- import {
12
- registerConfigCommand,
13
- registerKeysCommand,
14
- registerMemoryCommand,
15
- registerTrustCommand,
16
- } from "./cli/config-commands.js";
17
- import {
18
- registerAuditCommand,
19
- registerCompletionsCommand,
20
- registerDefaultAction,
21
- registerDevCommand,
22
- registerDoctorCommand,
23
- registerSessionsCommand,
24
- } from "./cli/core-commands.js";
25
- import { registerEmailCommand } from "./cli/email.js";
26
- import { registerInfluencerCommand } from "./cli/influencer.js";
27
- import {
28
- registerContactsCommand,
29
- registerIntegrationsCommand,
30
- } from "./cli/integrations.js";
31
- import { registerMapCommand } from "./cli/map.js";
32
- import { registerMcpCommand } from "./cli/mcp.js";
33
- import { registerSequenceCommand } from "./cli/sequence.js";
34
- import { registerTwitterCommand } from "./cli/twitter.js";
35
- import { registerHooksCommand } from "./hooks/cli.js";
36
-
37
- const program = new Command();
38
-
39
- program.name("vellum").description("Local AI assistant").version(version);
40
-
41
- registerDefaultAction(program);
42
- registerDevCommand(program);
43
- registerSessionsCommand(program);
44
- registerConfigCommand(program);
45
- registerKeysCommand(program);
46
- registerTrustCommand(program);
47
- registerMemoryCommand(program);
48
- registerAuditCommand(program);
49
- registerDoctorCommand(program);
50
- registerHooksCommand(program);
51
- registerMcpCommand(program);
52
- registerEmailCommand(program);
53
- registerIntegrationsCommand(program);
54
- registerContactsCommand(program);
55
- registerAmazonCommand(program);
56
- registerCompletionsCommand(program);
57
-
58
- registerTwitterCommand(program);
59
- registerMapCommand(program);
60
- registerInfluencerCommand(program);
61
- registerSequenceCommand(program);
62
-
63
- program.parse();
5
+ buildCliProgram().parse();