@vellumai/assistant 0.4.34 → 0.4.36

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 (251) 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 +4 -1
  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 +91 -43
  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 +806 -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 +491 -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} +133 -242
  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 +177 -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 +175 -145
  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 +12 -1
  173. package/src/memory/llm-usage-store.ts +186 -0
  174. package/src/memory/migrations/026-guardian-verification-sessions.ts +28 -9
  175. package/src/memory/migrations/027a-guardian-bootstrap-token.ts +16 -3
  176. package/src/memory/migrations/038-actor-token-records.ts +8 -1
  177. package/src/memory/migrations/039-actor-refresh-token-records.ts +11 -2
  178. package/src/memory/migrations/110-channel-guardian.ts +27 -6
  179. package/src/memory/migrations/112-assistant-inbox.ts +39 -15
  180. package/src/memory/migrations/114-notifications.ts +37 -15
  181. package/src/memory/migrations/117-conversation-attention.ts +33 -9
  182. package/src/memory/migrations/137-usage-dashboard-indexes.ts +26 -0
  183. package/src/memory/migrations/139-drop-usage-composite-indexes.ts +30 -0
  184. package/src/memory/migrations/index.ts +2 -0
  185. package/src/memory/migrations/schema-introspection.ts +18 -0
  186. package/src/memory/schema-migration.ts +1 -0
  187. package/src/memory/shared-app-links-store.ts +1 -1
  188. package/src/messaging/registry.ts +27 -0
  189. package/src/notifications/README.md +79 -70
  190. package/src/notifications/broadcaster.ts +2 -1
  191. package/src/notifications/conversation-pairing.ts +147 -13
  192. package/src/notifications/copy-composer.ts +7 -3
  193. package/src/notifications/destination-resolver.ts +14 -1
  194. package/src/notifications/emit-signal.ts +3 -2
  195. package/src/notifications/signal.ts +105 -1
  196. package/src/notifications/types.ts +16 -0
  197. package/src/permissions/checker.ts +29 -3
  198. package/src/permissions/prompter.ts +11 -3
  199. package/src/runtime/access-request-helper.ts +2 -1
  200. package/src/runtime/auth/route-policy.ts +7 -1
  201. package/src/runtime/channel-invite-transport.ts +40 -63
  202. package/src/runtime/channel-invite-transports/email.ts +13 -39
  203. package/src/runtime/channel-invite-transports/slack.ts +5 -34
  204. package/src/runtime/channel-invite-transports/sms.ts +8 -29
  205. package/src/runtime/channel-invite-transports/telegram.ts +69 -28
  206. package/src/runtime/channel-invite-transports/voice.ts +0 -7
  207. package/src/runtime/channel-invite-transports/whatsapp.ts +43 -0
  208. package/src/runtime/channel-readiness-service.ts +202 -45
  209. package/src/runtime/confirmation-request-guardian-bridge.ts +2 -1
  210. package/src/runtime/guardian-outbound-actions.ts +8 -5
  211. package/src/runtime/http-server.ts +5 -9
  212. package/src/runtime/http-types.ts +13 -1
  213. package/src/runtime/invite-instruction-generator.ts +178 -0
  214. package/src/runtime/invite-service.ts +22 -25
  215. package/src/runtime/migrations/migration-transport.ts +13 -0
  216. package/src/runtime/routes/app-routes.ts +1 -1
  217. package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +8 -7
  218. package/src/runtime/routes/channel-readiness-routes.ts +30 -11
  219. package/src/runtime/routes/contact-routes.ts +54 -26
  220. package/src/runtime/routes/guardian-bootstrap-routes.ts +1 -1
  221. package/src/runtime/routes/inbound-stages/bootstrap-intercept.ts +1 -1
  222. package/src/runtime/routes/inbound-stages/escalation-intercept.ts +2 -1
  223. package/src/runtime/routes/inbound-stages/verification-intercept.ts +2 -1
  224. package/src/runtime/routes/integration-routes.ts +1 -1
  225. package/src/runtime/routes/invite-routes.ts +1 -1
  226. package/src/runtime/routes/secret-routes.ts +31 -7
  227. package/src/runtime/routes/surface-content-routes.ts +104 -0
  228. package/src/runtime/routes/twilio-routes.ts +32 -1
  229. package/src/runtime/routes/usage-routes.ts +114 -0
  230. package/src/runtime/tool-grant-request-helper.ts +2 -1
  231. package/src/security/encrypted-store.ts +9 -5
  232. package/src/security/keychain-broker-client.ts +393 -0
  233. package/src/security/secure-keys.ts +106 -321
  234. package/src/tools/apps/executors.ts +73 -0
  235. package/src/tools/browser/auto-navigate.ts +15 -6
  236. package/src/tools/browser/chrome-cdp.ts +211 -0
  237. package/src/tools/browser/network-recorder.test.ts +83 -0
  238. package/src/tools/browser/network-recorder.ts +8 -7
  239. package/src/tools/browser/x-auto-navigate.ts +12 -6
  240. package/src/tools/credentials/policy-types.ts +24 -0
  241. package/src/tools/credentials/vault.ts +22 -27
  242. package/src/tools/network/script-proxy/session-manager.ts +47 -3
  243. package/src/tools/permission-checker.ts +1 -0
  244. package/src/tools/types.ts +2 -0
  245. package/src/tools/ui-surface/definitions.ts +1 -2
  246. package/src/tools/watch/watch-state.ts +2 -0
  247. package/src/__tests__/key-migration.test.ts +0 -240
  248. package/src/__tests__/keychain.test.ts +0 -286
  249. package/src/cli/core-commands.ts +0 -899
  250. package/src/security/keychain-to-encrypted-migration.ts +0 -66
  251. package/src/security/keychain.ts +0 -490
@@ -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();
@@ -101,7 +101,17 @@ export class McpOAuthProvider implements OAuthClientProvider {
101
101
  }
102
102
 
103
103
  async saveTokens(tokens: OAuthTokens): Promise<void> {
104
- await setSecureKeyAsync(tokensKey(this.serverId), JSON.stringify(tokens));
104
+ const ok = await setSecureKeyAsync(
105
+ tokensKey(this.serverId),
106
+ JSON.stringify(tokens),
107
+ );
108
+ if (!ok) {
109
+ log.warn(
110
+ { serverId: this.serverId },
111
+ "Failed to persist OAuth tokens to secure storage",
112
+ );
113
+ return;
114
+ }
105
115
  log.info({ serverId: this.serverId }, "OAuth tokens saved");
106
116
  }
107
117
 
@@ -124,7 +134,17 @@ export class McpOAuthProvider implements OAuthClientProvider {
124
134
  async saveClientInformation(
125
135
  info: OAuthClientInformationMixed,
126
136
  ): Promise<void> {
127
- await setSecureKeyAsync(clientInfoKey(this.serverId), JSON.stringify(info));
137
+ const ok = await setSecureKeyAsync(
138
+ clientInfoKey(this.serverId),
139
+ JSON.stringify(info),
140
+ );
141
+ if (!ok) {
142
+ log.warn(
143
+ { serverId: this.serverId },
144
+ "Failed to persist OAuth client information to secure storage",
145
+ );
146
+ return;
147
+ }
128
148
  log.info({ serverId: this.serverId }, "OAuth client information saved");
129
149
  }
130
150
 
@@ -154,7 +174,16 @@ export class McpOAuthProvider implements OAuthClientProvider {
154
174
  }
155
175
 
156
176
  async saveDiscoveryState(state: OAuthDiscoveryState): Promise<void> {
157
- await setSecureKeyAsync(discoveryKey(this.serverId), JSON.stringify(state));
177
+ const ok = await setSecureKeyAsync(
178
+ discoveryKey(this.serverId),
179
+ JSON.stringify(state),
180
+ );
181
+ if (!ok) {
182
+ log.warn(
183
+ { serverId: this.serverId },
184
+ "Failed to persist OAuth discovery state to secure storage",
185
+ );
186
+ }
158
187
  }
159
188
 
160
189
  // --- Redirect to Authorization ---
@@ -214,16 +243,49 @@ export class McpOAuthProvider implements OAuthClientProvider {
214
243
  );
215
244
 
216
245
  if (scope === "all" || scope === "tokens") {
217
- await deleteSecureKeyAsync(tokensKey(this.serverId));
246
+ const result = await deleteSecureKeyAsync(tokensKey(this.serverId));
247
+ if (result === "error") {
248
+ log.warn(
249
+ { serverId: this.serverId },
250
+ "Failed to delete OAuth tokens from secure storage",
251
+ );
252
+ } else if (result === "not-found") {
253
+ log.debug(
254
+ { serverId: this.serverId },
255
+ "OAuth tokens key not found in secure storage (already removed)",
256
+ );
257
+ }
218
258
  }
219
259
  if (scope === "all" || scope === "client") {
220
- await deleteSecureKeyAsync(clientInfoKey(this.serverId));
260
+ const result = await deleteSecureKeyAsync(clientInfoKey(this.serverId));
261
+ if (result === "error") {
262
+ log.warn(
263
+ { serverId: this.serverId },
264
+ "Failed to delete OAuth client information from secure storage",
265
+ );
266
+ } else if (result === "not-found") {
267
+ log.debug(
268
+ { serverId: this.serverId },
269
+ "OAuth client information key not found in secure storage (already removed)",
270
+ );
271
+ }
221
272
  }
222
273
  if (scope === "all" || scope === "verifier") {
223
274
  this._codeVerifier = undefined;
224
275
  }
225
276
  if (scope === "all" || scope === "discovery") {
226
- await deleteSecureKeyAsync(discoveryKey(this.serverId));
277
+ const result = await deleteSecureKeyAsync(discoveryKey(this.serverId));
278
+ if (result === "error") {
279
+ log.warn(
280
+ { serverId: this.serverId },
281
+ "Failed to delete OAuth discovery state from secure storage",
282
+ );
283
+ } else if (result === "not-found") {
284
+ log.debug(
285
+ { serverId: this.serverId },
286
+ "OAuth discovery state key not found in secure storage (already removed)",
287
+ );
288
+ }
227
289
  }
228
290
  }
229
291
 
@@ -373,12 +435,32 @@ export class McpOAuthProvider implements OAuthClientProvider {
373
435
  export async function deleteMcpOAuthCredentials(
374
436
  serverId: string,
375
437
  ): Promise<void> {
376
- await Promise.all([
438
+ const [tokensResult, clientResult, discoveryResult] = await Promise.all([
377
439
  deleteSecureKeyAsync(tokensKey(serverId)),
378
440
  deleteSecureKeyAsync(clientInfoKey(serverId)),
379
441
  deleteSecureKeyAsync(discoveryKey(serverId)),
380
442
  ]);
381
- log.info({ serverId }, "OAuth credentials deleted");
443
+ const results = [
444
+ { key: "tokens", result: tokensResult },
445
+ { key: "client_info", result: clientResult },
446
+ { key: "discovery", result: discoveryResult },
447
+ ];
448
+ const errors = results
449
+ .filter((r) => r.result === "error")
450
+ .map((r) => r.key);
451
+ if (errors.length > 0) {
452
+ log.warn(
453
+ { serverId, failedKeys: errors },
454
+ "Some OAuth credentials could not be deleted from secure storage",
455
+ );
456
+ }
457
+ const hasErrors = errors.length > 0;
458
+ log.info(
459
+ { serverId },
460
+ hasErrors
461
+ ? "OAuth credential deletion completed with errors"
462
+ : "OAuth credentials deleted",
463
+ );
382
464
  }
383
465
 
384
466
  // --- HTML rendering ---
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Generates app icons using the Gemini image generation service.
3
+ *
4
+ * Called as an async side-effect after app creation — never blocks
5
+ * the main app_create flow. Icons are saved to the app's directory
6
+ * as `icon.png` and included in .vellum bundles.
7
+ */
8
+
9
+ import { existsSync, mkdirSync, writeFileSync } from "node:fs";
10
+ import { join } from "node:path";
11
+
12
+ import { getConfig } from "../config/loader.js";
13
+ import { getAppsDir } from "../memory/app-store.js";
14
+ import { getLogger } from "../util/logger.js";
15
+ import { generateImage, mapGeminiError } from "./gemini-image-service.js";
16
+
17
+ const log = getLogger("app-icon-generator");
18
+
19
+ /**
20
+ * Generate an app icon and save it to `~/.vellum/apps/{appId}/icon.png`.
21
+ *
22
+ * Uses Gemini image generation when an API key is available.
23
+ * Silently no-ops if no key is configured or generation fails.
24
+ */
25
+ export async function generateAppIcon(
26
+ appId: string,
27
+ appName: string,
28
+ appDescription?: string,
29
+ ): Promise<void> {
30
+ const config = getConfig();
31
+ const apiKey = config.apiKeys.gemini ?? process.env.GEMINI_API_KEY;
32
+ if (!apiKey) {
33
+ log.debug("No Gemini API key — skipping app icon generation");
34
+ return;
35
+ }
36
+
37
+ const appDir = join(getAppsDir(), appId);
38
+ const iconPath = join(appDir, "icon.png");
39
+
40
+ // Don't regenerate if icon already exists
41
+ if (existsSync(iconPath)) {
42
+ return;
43
+ }
44
+
45
+ const descPart = appDescription ? ` Description: ${appDescription}.` : "";
46
+
47
+ const prompt =
48
+ `Design a beautiful, minimal app icon for "${appName}".${descPart}\n\n` +
49
+ "Style requirements:\n" +
50
+ "- Square app icon with rounded corners (like macOS/iOS app icons)\n" +
51
+ "- Clean, flat design with a single bold symbol or glyph in the center\n" +
52
+ "- Rich gradient background using 2-3 harmonious colors\n" +
53
+ "- The symbol should be white or very light colored for contrast\n" +
54
+ "- No text, no letters, no words — only a symbolic glyph\n" +
55
+ "- Professional quality, recognizable at small sizes (32px)\n" +
56
+ "- Modern aesthetic similar to Apple's design language";
57
+
58
+ try {
59
+ log.info({ appId, appName }, "Generating app icon via Gemini");
60
+
61
+ const result = await generateImage(apiKey, {
62
+ prompt,
63
+ mode: "generate",
64
+ model: config.imageGenModel,
65
+ });
66
+
67
+ if (result.images.length === 0) {
68
+ log.warn({ appId }, "Gemini returned no image for app icon");
69
+ return;
70
+ }
71
+
72
+ const image = result.images[0];
73
+ const pngBuffer = Buffer.from(image.dataBase64, "base64");
74
+
75
+ mkdirSync(appDir, { recursive: true });
76
+ writeFileSync(iconPath, pngBuffer);
77
+
78
+ log.info({ appId, iconPath }, "App icon saved");
79
+ } catch (error) {
80
+ const message = mapGeminiError(error);
81
+ log.warn(
82
+ { appId, error: message },
83
+ "App icon generation failed — skipping",
84
+ );
85
+ }
86
+ }
@@ -36,7 +36,6 @@ import {
36
36
  migrateBackfillContactInteractionStats,
37
37
  migrateBackfillGuardianPrincipalId,
38
38
  migrateCallSessionMode,
39
- migrateDropAssistantIdColumns,
40
39
  migrateCanonicalGuardianDeliveriesDestinationIndex,
41
40
  migrateCanonicalGuardianRequesterChatId,
42
41
  migrateChannelInboundDeliveredSegments,
@@ -46,7 +45,9 @@ import {
46
45
  migrateContactsNotesColumn,
47
46
  migrateContactsRolePrincipal,
48
47
  migrateConversationsThreadTypeIndex,
48
+ migrateDropAssistantIdColumns,
49
49
  migrateDropLegacyMemberGuardianTables,
50
+ migrateDropUsageCompositeIndexes,
50
51
  migrateFkCascadeRebuilds,
51
52
  migrateGuardianActionFollowup,
52
53
  migrateGuardianActionSupersession,
@@ -63,6 +64,7 @@ import {
63
64
  migrateNotificationDeliveryThreadDecision,
64
65
  migrateReminderRoutingIntent,
65
66
  migrateSchemaIndexesAndColumns,
67
+ migrateUsageDashboardIndexes,
66
68
  migrateVoiceInviteColumns,
67
69
  migrateVoiceInviteDisplayMetadata,
68
70
  recoverCrashedMigrations,
@@ -293,6 +295,15 @@ export function initializeDb(): void {
293
295
  // 40. Drop assistant_id columns from all 16 daemon tables
294
296
  migrateDropAssistantIdColumns(database);
295
297
 
298
+ // 41. Indexes on llm_usage_events for usage dashboard time-range and breakdown queries
299
+ migrateUsageDashboardIndexes(database);
300
+
301
+ // 42. (skipped) migrateReorderUsageDashboardIndexes — superseded by 43 which drops
302
+ // all composite indexes that 42 would create, so running it is wasted work.
303
+
304
+ // 43. Drop all composite usage indexes — they don't eliminate temp B-trees for GROUP BY
305
+ migrateDropUsageCompositeIndexes(database);
306
+
296
307
  validateMigrationState(database);
297
308
 
298
309
  if (process.env.BUN_TEST === "1") {