@vellumai/assistant 0.5.11 → 0.5.13

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 (209) hide show
  1. package/Dockerfile +42 -9
  2. package/docs/architecture/integrations.md +34 -32
  3. package/node_modules/@vellumai/ces-contracts/src/__tests__/grants.test.ts +7 -7
  4. package/node_modules/@vellumai/ces-contracts/src/handles.ts +5 -4
  5. package/node_modules/@vellumai/ces-contracts/src/index.ts +7 -0
  6. package/node_modules/@vellumai/ces-contracts/src/rpc.ts +5 -0
  7. package/node_modules/@vellumai/credential-storage/src/index.ts +1 -1
  8. package/openapi.yaml +87 -9
  9. package/package.json +1 -1
  10. package/src/__tests__/catalog-cache.test.ts +164 -0
  11. package/src/__tests__/catalog-search.test.ts +61 -0
  12. package/src/__tests__/cli-command-risk-guard.test.ts +181 -6
  13. package/src/__tests__/conversation-delete-schedule-cleanup.test.ts +396 -0
  14. package/src/__tests__/conversation-error.test.ts +3 -2
  15. package/src/__tests__/credential-security-invariants.test.ts +9 -15
  16. package/src/__tests__/credential-vault-unit.test.ts +32 -34
  17. package/src/__tests__/credential-vault.test.ts +25 -33
  18. package/src/__tests__/credentials-cli.test.ts +3 -3
  19. package/src/__tests__/daemon-credential-client.test.ts +2 -2
  20. package/src/__tests__/first-greeting.test.ts +7 -0
  21. package/src/__tests__/host-bash-proxy.test.ts +79 -0
  22. package/src/__tests__/host-cu-proxy.test.ts +90 -0
  23. package/src/__tests__/host-file-proxy.test.ts +89 -0
  24. package/src/__tests__/integration-status.test.ts +5 -5
  25. package/src/__tests__/list-messages-attachments.test.ts +171 -0
  26. package/src/__tests__/mcp-abort-signal.test.ts +205 -0
  27. package/src/__tests__/messaging-send-tool.test.ts +5 -5
  28. package/src/__tests__/navigate-settings-tab.test.ts +6 -2
  29. package/src/__tests__/notification-telegram-adapter.test.ts +125 -0
  30. package/src/__tests__/oauth-cli.test.ts +126 -119
  31. package/src/__tests__/oauth-provider-profiles.test.ts +55 -20
  32. package/src/__tests__/oauth-scope-policy.test.ts +4 -6
  33. package/src/__tests__/onboarding-template-contract.test.ts +2 -2
  34. package/src/__tests__/platform.test.ts +3 -168
  35. package/src/__tests__/secret-routes-managed-proxy.test.ts +78 -0
  36. package/src/__tests__/secure-keys-managed-failover.test.ts +73 -0
  37. package/src/__tests__/skill-feature-flags.test.ts +8 -0
  38. package/src/__tests__/skill-secret-handling-guard.test.ts +212 -0
  39. package/src/__tests__/skills-uninstall.test.ts +2 -2
  40. package/src/__tests__/slack-messaging-token-resolution.test.ts +22 -24
  41. package/src/__tests__/slack-share-routes.test.ts +5 -5
  42. package/src/__tests__/system-prompt.test.ts +39 -0
  43. package/src/__tests__/token-estimator-accuracy.benchmark.test.ts +1 -1
  44. package/src/__tests__/workspace-migration-backfill-installation-id.test.ts +5 -4
  45. package/src/cli/AGENTS.md +47 -7
  46. package/src/cli/commands/browser-relay.ts +2 -17
  47. package/src/cli/commands/contacts.ts +6 -4
  48. package/src/cli/commands/conversations.ts +13 -1
  49. package/src/cli/commands/credential-execution.ts +16 -1
  50. package/src/cli/commands/credentials.ts +2 -8
  51. package/src/cli/commands/oauth/__tests__/connect.test.ts +29 -108
  52. package/src/cli/commands/oauth/__tests__/disconnect.test.ts +13 -87
  53. package/src/cli/commands/oauth/__tests__/mode.test.ts +22 -69
  54. package/src/cli/commands/oauth/__tests__/ping.test.ts +20 -79
  55. package/src/cli/commands/oauth/__tests__/providers-delete.test.ts +574 -0
  56. package/src/cli/commands/oauth/__tests__/providers-update.test.ts +416 -0
  57. package/src/cli/commands/oauth/__tests__/status.test.ts +12 -40
  58. package/src/cli/commands/oauth/__tests__/token.test.ts +3 -50
  59. package/src/cli/commands/oauth/apps.ts +63 -44
  60. package/src/cli/commands/oauth/connect.ts +187 -155
  61. package/src/cli/commands/oauth/disconnect.ts +27 -75
  62. package/src/cli/commands/oauth/index.ts +36 -46
  63. package/src/cli/commands/oauth/mode.ts +22 -34
  64. package/src/cli/commands/oauth/ping.ts +19 -45
  65. package/src/cli/commands/oauth/providers.ts +569 -62
  66. package/src/cli/commands/oauth/request.ts +36 -48
  67. package/src/cli/commands/oauth/shared.ts +1 -19
  68. package/src/cli/commands/oauth/status.ts +14 -25
  69. package/src/cli/commands/oauth/token.ts +25 -34
  70. package/src/cli/commands/platform/__tests__/connect.test.ts +224 -0
  71. package/src/cli/commands/platform/__tests__/disconnect.test.ts +237 -0
  72. package/src/cli/commands/platform/__tests__/status.test.ts +246 -0
  73. package/src/cli/commands/platform/connect.ts +104 -0
  74. package/src/cli/commands/platform/disconnect.ts +118 -0
  75. package/src/cli/commands/{platform.ts → platform/index.ts} +108 -38
  76. package/src/cli/commands/sequence.ts +5 -4
  77. package/src/cli/commands/shotgun.ts +16 -0
  78. package/src/cli/commands/skills.ts +173 -41
  79. package/src/cli/commands/usage.ts +5 -11
  80. package/src/cli/lib/daemon-credential-client.ts +22 -38
  81. package/src/cli/program.ts +1 -1
  82. package/src/config/assistant-feature-flags.ts +3 -7
  83. package/src/config/bundled-skills/contacts/tools/google-contacts.ts +1 -1
  84. package/src/config/bundled-skills/conversations/SKILL.md +20 -0
  85. package/src/config/bundled-skills/conversations/TOOLS.json +23 -0
  86. package/src/config/bundled-skills/conversations/tools/rename-conversation.ts +66 -0
  87. package/src/config/bundled-skills/gmail/SKILL.md +13 -13
  88. package/src/config/bundled-skills/gmail/tools/gmail-archive.ts +3 -3
  89. package/src/config/bundled-skills/gmail/tools/gmail-attachments.ts +2 -2
  90. package/src/config/bundled-skills/gmail/tools/gmail-draft.ts +1 -1
  91. package/src/config/bundled-skills/gmail/tools/gmail-filters.ts +1 -1
  92. package/src/config/bundled-skills/gmail/tools/gmail-follow-up.ts +1 -1
  93. package/src/config/bundled-skills/gmail/tools/gmail-forward.ts +1 -1
  94. package/src/config/bundled-skills/gmail/tools/gmail-label.ts +2 -2
  95. package/src/config/bundled-skills/gmail/tools/gmail-outreach-scan.ts +1 -1
  96. package/src/config/bundled-skills/gmail/tools/gmail-send-draft.ts +1 -1
  97. package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +1 -1
  98. package/src/config/bundled-skills/gmail/tools/gmail-trash.ts +1 -1
  99. package/src/config/bundled-skills/gmail/tools/gmail-unsubscribe.ts +1 -1
  100. package/src/config/bundled-skills/gmail/tools/gmail-vacation.ts +1 -1
  101. package/src/config/bundled-skills/google-calendar/SKILL.md +10 -4
  102. package/src/config/bundled-skills/google-calendar/tools/shared.ts +1 -1
  103. package/src/config/bundled-skills/messaging/SKILL.md +7 -7
  104. package/src/config/bundled-skills/messaging/tools/messaging-send.ts +5 -2
  105. package/src/config/bundled-skills/messaging/tools/shared.ts +5 -6
  106. package/src/config/bundled-skills/settings/TOOLS.json +5 -3
  107. package/src/config/bundled-skills/settings/tools/navigate-settings-tab.ts +4 -2
  108. package/src/config/bundled-tool-registry.ts +5 -0
  109. package/src/config/feature-flag-registry.json +2 -2
  110. package/src/credential-execution/client.ts +15 -3
  111. package/src/daemon/conversation-agent-loop.ts +2 -0
  112. package/src/daemon/conversation-error.ts +36 -6
  113. package/src/daemon/conversation-messaging.ts +9 -0
  114. package/src/daemon/conversation-runtime-assembly.ts +33 -0
  115. package/src/daemon/conversation-surfaces.ts +120 -14
  116. package/src/daemon/conversation.ts +5 -0
  117. package/src/daemon/first-greeting.ts +6 -1
  118. package/src/daemon/handlers/skills.ts +148 -3
  119. package/src/daemon/host-bash-proxy.ts +16 -0
  120. package/src/daemon/host-cu-proxy.ts +16 -0
  121. package/src/daemon/host-file-proxy.ts +16 -0
  122. package/src/daemon/lifecycle.ts +56 -5
  123. package/src/daemon/message-types/conversations.ts +1 -0
  124. package/src/daemon/message-types/guardian-actions.ts +2 -0
  125. package/src/daemon/message-types/host-bash.ts +6 -1
  126. package/src/daemon/message-types/host-cu.ts +6 -1
  127. package/src/daemon/message-types/host-file.ts +6 -1
  128. package/src/daemon/message-types/integrations.ts +0 -1
  129. package/src/daemon/server.ts +29 -2
  130. package/src/hooks/cli.ts +74 -0
  131. package/src/inbound/platform-callback-registration.ts +7 -12
  132. package/src/index.ts +0 -12
  133. package/src/mcp/client.ts +6 -1
  134. package/src/mcp/manager.ts +2 -1
  135. package/src/memory/conversation-crud.ts +92 -3
  136. package/src/memory/conversation-key-store.ts +26 -0
  137. package/src/memory/conversation-queries.ts +6 -6
  138. package/src/memory/db-init.ts +16 -0
  139. package/src/memory/journal-memory.ts +8 -2
  140. package/src/memory/migrations/196-messages-conversation-created-at-index.ts +9 -0
  141. package/src/memory/migrations/196-strip-integration-prefix-from-provider-keys.ts +186 -0
  142. package/src/memory/migrations/197-oauth-providers-behavior-columns.ts +29 -0
  143. package/src/memory/migrations/198-drop-setup-skill-id-column.ts +11 -0
  144. package/src/memory/migrations/index.ts +4 -0
  145. package/src/memory/migrations/registry.ts +8 -0
  146. package/src/memory/schema/oauth.ts +11 -0
  147. package/src/messaging/provider.ts +13 -12
  148. package/src/messaging/providers/gmail/adapter.ts +44 -35
  149. package/src/messaging/providers/slack/adapter.ts +63 -33
  150. package/src/messaging/providers/telegram-bot/adapter.ts +6 -8
  151. package/src/messaging/providers/whatsapp/adapter.ts +6 -8
  152. package/src/notifications/adapters/telegram.ts +78 -2
  153. package/src/oauth/__tests__/identity-verifier.test.ts +464 -0
  154. package/src/oauth/byo-connection.test.ts +22 -24
  155. package/src/oauth/connect-orchestrator.ts +37 -76
  156. package/src/oauth/connect-types.ts +7 -65
  157. package/src/oauth/connection-resolver.test.ts +13 -13
  158. package/src/oauth/connection-resolver.ts +3 -4
  159. package/src/oauth/identity-verifier.ts +177 -0
  160. package/src/oauth/oauth-store.ts +228 -3
  161. package/src/oauth/platform-connection.test.ts +56 -6
  162. package/src/oauth/platform-connection.ts +8 -1
  163. package/src/oauth/seed-providers.ts +247 -34
  164. package/src/permissions/checker.ts +127 -1
  165. package/src/prompts/journal-context.ts +4 -1
  166. package/src/prompts/system-prompt.ts +54 -9
  167. package/src/prompts/templates/BOOTSTRAP.md +16 -5
  168. package/src/providers/anthropic/client.ts +2 -33
  169. package/src/runtime/guardian-action-service.ts +7 -2
  170. package/src/runtime/http-server.ts +12 -18
  171. package/src/runtime/http-types.ts +8 -1
  172. package/src/runtime/migrations/rebind-secrets-screen.ts +2 -2
  173. package/src/runtime/routes/conversation-management-routes.ts +31 -0
  174. package/src/runtime/routes/conversation-routes.ts +79 -4
  175. package/src/runtime/routes/guardian-action-routes.ts +15 -2
  176. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +21 -8
  177. package/src/runtime/routes/integrations/slack/share.ts +1 -1
  178. package/src/runtime/routes/oauth-apps.ts +2 -1
  179. package/src/runtime/routes/secret-routes.ts +45 -15
  180. package/src/runtime/routes/settings-routes.ts +12 -19
  181. package/src/runtime/routes/skills-routes.ts +45 -4
  182. package/src/schedule/integration-status.ts +2 -2
  183. package/src/security/ces-rpc-credential-backend.ts +19 -16
  184. package/src/security/oauth-completion-page.ts +153 -0
  185. package/src/security/oauth2.ts +3 -17
  186. package/src/security/secure-keys.ts +207 -7
  187. package/src/security/token-manager.ts +3 -6
  188. package/src/signals/bash.ts +6 -1
  189. package/src/skills/catalog-cache.ts +44 -0
  190. package/src/skills/catalog-search.ts +18 -0
  191. package/src/tools/browser/browser-manager.ts +2 -2
  192. package/src/tools/credentials/post-connect-hooks.ts +1 -1
  193. package/src/tools/credentials/vault.ts +34 -45
  194. package/src/tools/host-terminal/host-shell.ts +16 -3
  195. package/src/tools/mcp/mcp-tool-factory.ts +2 -1
  196. package/src/tools/skills/sandbox-runner.ts +16 -3
  197. package/src/tools/terminal/shell.ts +16 -3
  198. package/src/util/logger.ts +11 -1
  199. package/src/util/platform.ts +1 -91
  200. package/src/util/sentry-log-stream.ts +51 -0
  201. package/src/watcher/providers/github.ts +2 -2
  202. package/src/watcher/providers/gmail.ts +1 -1
  203. package/src/watcher/providers/google-calendar.ts +1 -1
  204. package/src/watcher/providers/linear.ts +2 -2
  205. package/src/workspace/migrations/011-backfill-installation-id.ts +5 -3
  206. package/src/workspace/migrations/020-rename-oauth-skill-dirs.ts +119 -0
  207. package/src/workspace/migrations/registry.ts +2 -0
  208. package/src/cli/commands/oauth/connections.ts +0 -255
  209. package/src/oauth/provider-behaviors.ts +0 -634
@@ -8,6 +8,7 @@ import { setRelayBroadcast } from "../calls/relay-server.js";
8
8
  import { TwilioConversationRelayProvider } from "../calls/twilio-provider.js";
9
9
  import { setVoiceBridgeDeps } from "../calls/voice-session-bridge.js";
10
10
  import {
11
+ getPlatformAssistantId,
11
12
  getQdrantHttpPortEnv,
12
13
  getQdrantUrlEnv,
13
14
  getRuntimeHttpHost,
@@ -77,7 +78,11 @@ import {
77
78
  import { ensureVellumGuardianBinding } from "../runtime/guardian-vellum-migration.js";
78
79
  import { RuntimeHttpServer } from "../runtime/http-server.js";
79
80
  import { startScheduler } from "../schedule/scheduler.js";
80
- import { setCesClient } from "../security/secure-keys.js";
81
+ import {
82
+ onCesClientChanged,
83
+ setCesClient,
84
+ setCesReconnect,
85
+ } from "../security/secure-keys.js";
81
86
  import { seedCatalogSkillMemories } from "../skills/skill-memory.js";
82
87
  import { UsageTelemetryReporter } from "../telemetry/usage-telemetry-reporter.js";
83
88
  import { getDeviceId } from "../util/device-id.js";
@@ -194,11 +199,13 @@ export async function startCesProcess(
194
199
  // after hatch and stored in the credential store — CES can't read
195
200
  // the env var, so we pass it via the handshake.
196
201
  const proxyCtx = await resolveManagedProxyContext();
197
- const { accepted, reason } = await client.handshake(
198
- proxyCtx.assistantApiKey
202
+ const assistantId = getPlatformAssistantId();
203
+ const { accepted, reason } = await client.handshake({
204
+ ...(proxyCtx.assistantApiKey
199
205
  ? { assistantApiKey: proxyCtx.assistantApiKey }
200
- : undefined,
201
- );
206
+ : {}),
207
+ ...(assistantId ? { assistantId } : {}),
208
+ });
202
209
  if (abortController.signal.aborted) {
203
210
  client.close();
204
211
  throw new Error("CES initialization aborted during shutdown");
@@ -495,6 +502,43 @@ export async function runDaemon(): Promise<void> {
495
502
  setCesClient(client);
496
503
  }
497
504
  }
505
+
506
+ // Register CES reconnection callback so the credential layer can
507
+ // re-establish the connection when the transport dies, instead of
508
+ // falling back to the encrypted file store.
509
+ if (cesResult.processManager) {
510
+ const pm = cesResult.processManager;
511
+ setCesReconnect(async () => {
512
+ try {
513
+ await pm.stop();
514
+ const transport = await pm.start();
515
+ const newClient = createCesClient(transport);
516
+ const proxyCtx = await resolveManagedProxyContext();
517
+ const assistantId = getPlatformAssistantId();
518
+ const { accepted, reason } = await newClient.handshake({
519
+ ...(proxyCtx.assistantApiKey
520
+ ? { assistantApiKey: proxyCtx.assistantApiKey }
521
+ : {}),
522
+ ...(assistantId ? { assistantId } : {}),
523
+ });
524
+ if (accepted) {
525
+ log.info("CES reconnection handshake accepted");
526
+ return newClient;
527
+ }
528
+ log.warn({ reason }, "CES reconnection handshake rejected");
529
+ newClient.close();
530
+ await pm.stop().catch(() => {});
531
+ return undefined;
532
+ } catch (err) {
533
+ log.warn(
534
+ { error: err instanceof Error ? err.message : String(err) },
535
+ "CES reconnection attempt failed",
536
+ );
537
+ await pm.stop().catch(() => {});
538
+ return undefined;
539
+ }
540
+ });
541
+ }
498
542
  }
499
543
 
500
544
  await initializeProvidersAndTools(config);
@@ -504,6 +548,11 @@ export async function runDaemon(): Promise<void> {
504
548
  log.info("Daemon startup: starting DaemonServer");
505
549
  const server = new DaemonServer();
506
550
  server.setCes(await cesStartupPromise);
551
+
552
+ // Keep the server's CES client ref in sync after reconnection so that
553
+ // secret routes and new conversations use the fresh client.
554
+ onCesClientChanged((client) => server.updateCesClient(client));
555
+
507
556
  await server.start();
508
557
  log.info("Daemon startup: DaemonServer started");
509
558
 
@@ -851,6 +900,8 @@ export async function runDaemon(): Promise<void> {
851
900
  getHandlerContext: () => server.getHandlerContext(),
852
901
  }),
853
902
  getCesClient: () => server.getCesClient(),
903
+ onProviderCredentialsChanged: () =>
904
+ server.refreshConversationsForProviderChange(),
854
905
  getHeartbeatService: () => server.getHeartbeatService(),
855
906
  });
856
907
 
@@ -372,6 +372,7 @@ export type ConversationErrorCode =
372
372
  | "PROVIDER_ORDERING"
373
373
  | "PROVIDER_WEB_SEARCH"
374
374
  | "PROVIDER_NOT_CONFIGURED"
375
+ | "MANAGED_KEY_INVALID"
375
376
  | "CONTEXT_TOO_LARGE"
376
377
  | "CONVERSATION_ABORTED"
377
378
  | "CONVERSATION_PROCESSING_FAILED"
@@ -47,6 +47,8 @@ export interface GuardianActionDecisionResponse {
47
47
  resolverFailureReason?: string;
48
48
  requestId?: string;
49
49
  userText?: string;
50
+ /** Resolver reply text for the guardian (e.g. verification code for access requests). */
51
+ replyText?: string;
50
52
  }
51
53
 
52
54
  // --- Domain-level union aliases (consumed by the barrel file) ---
@@ -15,6 +15,11 @@ export interface HostBashRequest {
15
15
  env?: Record<string, string>;
16
16
  }
17
17
 
18
+ export interface HostBashCancelRequest {
19
+ type: "host_bash_cancel";
20
+ requestId: string;
21
+ }
22
+
18
23
  // --- Domain-level union aliases (consumed by the barrel file) ---
19
24
 
20
- export type _HostBashServerMessages = HostBashRequest;
25
+ export type _HostBashServerMessages = HostBashRequest | HostBashCancelRequest;
@@ -14,6 +14,11 @@ export interface HostCuRequest {
14
14
  reasoning?: string;
15
15
  }
16
16
 
17
+ export interface HostCuCancelRequest {
18
+ type: "host_cu_cancel";
19
+ requestId: string;
20
+ }
21
+
17
22
  // --- Domain-level union aliases (consumed by the barrel file) ---
18
23
 
19
- export type _HostCuServerMessages = HostCuRequest;
24
+ export type _HostCuServerMessages = HostCuRequest | HostCuCancelRequest;
@@ -39,6 +39,11 @@ export type HostFileRequest =
39
39
  | HostFileWriteRequest
40
40
  | HostFileEditRequest;
41
41
 
42
+ export interface HostFileCancelRequest {
43
+ type: "host_file_cancel";
44
+ requestId: string;
45
+ }
46
+
42
47
  // --- Domain-level union aliases (consumed by the barrel file) ---
43
48
 
44
- export type _HostFileServerMessages = HostFileRequest;
49
+ export type _HostFileServerMessages = HostFileRequest | HostFileCancelRequest;
@@ -188,7 +188,6 @@ export interface IntegrationConnectResult {
188
188
  accountInfo?: string | null;
189
189
  error?: string | null;
190
190
  setupRequired?: boolean;
191
- setupSkillId?: string;
192
191
  setupHint?: string;
193
192
  }
194
193
 
@@ -256,6 +256,8 @@ export class DaemonServer {
256
256
  private cesClientPromise?: Promise<CesClient | undefined>;
257
257
  private cesInitAbortController?: AbortController;
258
258
  private cesClientRef?: CesClient;
259
+ /** Monotonically increasing counter to detect stale client updates. */
260
+ private cesClientGeneration = 0;
259
261
 
260
262
  /**
261
263
  * Logical assistant identifier used when publishing to the assistant-events hub.
@@ -279,9 +281,14 @@ export class DaemonServer {
279
281
  // Wrap the external promise so that cesClientRef stays in sync once the
280
282
  // handshake completes — the async work runs in lifecycle.ts but the
281
283
  // server needs the resolved client reference for getCesClient().
284
+ // Use a generation snapshot so a late-resolving promise doesn't overwrite
285
+ // a newer client set by updateCesClient().
282
286
  if (result.clientPromise) {
287
+ const gen = this.cesClientGeneration;
283
288
  this.cesClientPromise = result.clientPromise.then((client) => {
284
- this.cesClientRef = client;
289
+ if (this.cesClientGeneration === gen) {
290
+ this.cesClientRef = client;
291
+ }
285
292
  return client;
286
293
  });
287
294
  }
@@ -295,6 +302,17 @@ export class DaemonServer {
295
302
  return this.cesClientRef;
296
303
  }
297
304
 
305
+ /**
306
+ * Update the CES client reference after a successful reconnection.
307
+ * Called via the `onCesClientChanged` listener registered in lifecycle.ts.
308
+ * Bumps the generation counter so any pending setCes().then() callback
309
+ * won't overwrite this newer client.
310
+ */
311
+ updateCesClient(client: CesClient | undefined): void {
312
+ this.cesClientGeneration++;
313
+ this.cesClientRef = client;
314
+ }
315
+
298
316
  /** Optional heartbeat service reference for "Run Now" from the UI. */
299
317
  private _heartbeatService?: HeartbeatService;
300
318
 
@@ -319,7 +337,7 @@ export class DaemonServer {
319
337
  }
320
338
 
321
339
  private applyTransportMetadata(
322
- _conversation: Conversation,
340
+ conversation: Conversation,
323
341
  options: ConversationCreateOptions | undefined,
324
342
  ): void {
325
343
  const transport = options?.transport;
@@ -328,6 +346,7 @@ export class DaemonServer {
328
346
  { channelId: transport.channelId },
329
347
  "Transport metadata received",
330
348
  );
349
+ conversation.setTransportHints(transport.hints);
331
350
  }
332
351
 
333
352
  constructor() {
@@ -683,6 +702,14 @@ export class DaemonServer {
683
702
  return changed;
684
703
  }
685
704
 
705
+ /**
706
+ * Provider instances are captured when conversations are created, so a key
707
+ * change must evict or mark them stale before the next turn.
708
+ */
709
+ refreshConversationsForProviderChange(): void {
710
+ this.evictConversationsForReload();
711
+ }
712
+
686
713
  private async getOrCreateConversation(
687
714
  conversationId: string,
688
715
  options?: ConversationCreateOptions,
package/src/hooks/cli.ts CHANGED
@@ -14,9 +14,33 @@ const log = getCliLogger("hooks");
14
14
  export function registerHooksCommand(program: Command): void {
15
15
  const hooks = program.command("hooks").description("Manage hooks");
16
16
 
17
+ hooks.addHelpText(
18
+ "after",
19
+ `
20
+ Hooks are user-installed scripts that run in response to assistant lifecycle
21
+ events (e.g. tool invocations, message sends). Each hook is a directory
22
+ containing a hook.json manifest and a script file. Hooks are stored in
23
+ ~/.vellum/hooks/ and must be explicitly enabled after installation.
24
+
25
+ Examples:
26
+ $ assistant hooks list
27
+ $ assistant hooks install ./my-hook
28
+ $ assistant hooks enable my-hook
29
+ $ assistant hooks disable my-hook`,
30
+ );
31
+
17
32
  hooks
18
33
  .command("list")
19
34
  .description("List all installed hooks")
35
+ .addHelpText(
36
+ "after",
37
+ `
38
+ Displays a table of all installed hooks with their name, subscribed events,
39
+ enabled status, and version.
40
+
41
+ Examples:
42
+ $ assistant hooks list`,
43
+ )
20
44
  .action(() => {
21
45
  const discovered = discoverHooks();
22
46
  if (discovered.length === 0) {
@@ -53,6 +77,17 @@ export function registerHooksCommand(program: Command): void {
53
77
  hooks
54
78
  .command("enable <name>")
55
79
  .description("Enable a hook")
80
+ .addHelpText(
81
+ "after",
82
+ `
83
+ Arguments:
84
+ name Hook name as shown by 'assistant hooks list'
85
+
86
+ Enables a previously installed hook so it runs on matching events.
87
+
88
+ Examples:
89
+ $ assistant hooks enable my-hook`,
90
+ )
56
91
  .action((name: string) => {
57
92
  const discovered = discoverHooks();
58
93
  const hook = discovered.find((h) => h.name === name);
@@ -67,6 +102,18 @@ export function registerHooksCommand(program: Command): void {
67
102
  hooks
68
103
  .command("disable <name>")
69
104
  .description("Disable a hook")
105
+ .addHelpText(
106
+ "after",
107
+ `
108
+ Arguments:
109
+ name Hook name as shown by 'assistant hooks list'
110
+
111
+ Disables a hook so it no longer runs on events. The hook remains installed
112
+ and can be re-enabled later.
113
+
114
+ Examples:
115
+ $ assistant hooks disable my-hook`,
116
+ )
70
117
  .action((name: string) => {
71
118
  const discovered = discoverHooks();
72
119
  const hook = discovered.find((h) => h.name === name);
@@ -81,6 +128,21 @@ export function registerHooksCommand(program: Command): void {
81
128
  hooks
82
129
  .command("install <path>")
83
130
  .description("Install a hook from a directory")
131
+ .addHelpText(
132
+ "after",
133
+ `
134
+ Arguments:
135
+ path Path to a directory containing a hook.json manifest and a script file.
136
+ The manifest must have name, script, description, version, and at
137
+ least one valid event.
138
+
139
+ Copies the hook directory into ~/.vellum/hooks/<name>/ and registers it as
140
+ disabled by default. Run 'assistant hooks enable <name>' to activate.
141
+
142
+ Examples:
143
+ $ assistant hooks install ./my-hook
144
+ $ assistant hooks install /path/to/custom-hook`,
145
+ )
84
146
  .action((hookPath: string) => {
85
147
  const srcDir = resolve(hookPath);
86
148
  if (!pathExists(srcDir)) {
@@ -146,6 +208,18 @@ export function registerHooksCommand(program: Command): void {
146
208
  hooks
147
209
  .command("remove <name>")
148
210
  .description("Remove an installed hook")
211
+ .addHelpText(
212
+ "after",
213
+ `
214
+ Arguments:
215
+ name Hook name as shown by 'assistant hooks list'
216
+
217
+ Permanently deletes the hook directory and removes it from configuration.
218
+ Prompts for confirmation before proceeding.
219
+
220
+ Examples:
221
+ $ assistant hooks remove my-hook`,
222
+ )
149
223
  .action(async (name: string) => {
150
224
  const discovered = discoverHooks();
151
225
  const hook = discovered.find((h) => h.name === name);
@@ -204,18 +204,13 @@ export async function resolveCallbackUrl(
204
204
  }
205
205
  return url;
206
206
  } catch (err) {
207
- log.warn(
208
- { err, callbackPath, type },
209
- "Failed to register platform callback route, falling back to direct URL",
207
+ // In managed/containerized mode there is no local-ingress fallback and
208
+ // ngrok is not applicable. Surface a clear error so callers (and the
209
+ // user) understand this is a platform-side issue, not a tunnel problem.
210
+ const detail = err instanceof Error ? err.message : String(err);
211
+ throw new Error(
212
+ `Managed callback route registration failed: ${detail}. ` +
213
+ `Please contact support if this problem persists.`,
210
214
  );
211
- try {
212
- return directUrl();
213
- } catch (fallbackErr) {
214
- log.error(
215
- { fallbackErr, callbackPath, type },
216
- "Direct URL fallback also failed after platform registration failure",
217
- );
218
- throw err;
219
- }
220
215
  }
221
216
  }
package/src/index.ts CHANGED
@@ -1,17 +1,5 @@
1
1
  #!/usr/bin/env bun
2
2
 
3
3
  import { buildCliProgram } from "./cli/program.js";
4
- import { resolveInstanceDataDir } from "./util/platform.js";
5
-
6
- // Auto-resolve BASE_DATA_DIR from the lockfile when running as a standalone CLI.
7
- // The daemon always has BASE_DATA_DIR set by the launcher (cli/src/lib/local.ts),
8
- // but the CLI process doesn't — so credential commands and other path-dependent
9
- // operations would read from ~/.vellum instead of the instance-scoped directory.
10
- if (!process.env.BASE_DATA_DIR) {
11
- const instanceDir = resolveInstanceDataDir();
12
- if (instanceDir) {
13
- process.env.BASE_DATA_DIR = instanceDir;
14
- }
15
- }
16
4
 
17
5
  buildCliProgram().parse();
package/src/mcp/client.ts CHANGED
@@ -159,12 +159,17 @@ export class McpClient {
159
159
  async callTool(
160
160
  name: string,
161
161
  args: Record<string, unknown>,
162
+ signal?: AbortSignal,
162
163
  ): Promise<McpCallResult> {
163
164
  if (!this.connected) {
164
165
  throw new Error(`MCP client "${this.serverId}" is not connected`);
165
166
  }
166
167
 
167
- const result = await this.client.callTool({ name, arguments: args });
168
+ const result = await this.client.callTool(
169
+ { name, arguments: args },
170
+ undefined,
171
+ signal ? { signal } : undefined,
172
+ );
168
173
  const isError = result.isError === true;
169
174
 
170
175
  // Handle structuredContent if present
@@ -127,12 +127,13 @@ export class McpServerManager {
127
127
  serverId: string,
128
128
  toolName: string,
129
129
  args: Record<string, unknown>,
130
+ signal?: AbortSignal,
130
131
  ) {
131
132
  const client = this.clients.get(serverId);
132
133
  if (!client) {
133
134
  throw new Error(`MCP server "${serverId}" not found`);
134
135
  }
135
- return client.callTool(toolName, args);
136
+ return client.callTool(toolName, args, signal);
136
137
  }
137
138
 
138
139
  getClient(serverId: string): McpClient | undefined {
@@ -10,6 +10,7 @@ import {
10
10
  gte,
11
11
  inArray,
12
12
  isNull,
13
+ lt,
13
14
  lte,
14
15
  sql,
15
16
  } from "drizzle-orm";
@@ -314,6 +315,22 @@ export function getConversation(id: string): ConversationRow | null {
314
315
  return row ? parseConversation(row) : null;
315
316
  }
316
317
 
318
+ /**
319
+ * Count conversations that reference a given schedule job ID.
320
+ * Useful for determining whether a schedule can be safely deleted
321
+ * (i.e. no other conversations still reference it).
322
+ */
323
+ export function countConversationsByScheduleJobId(
324
+ scheduleJobId: string,
325
+ ): number {
326
+ return (
327
+ rawGet<{ c: number }>(
328
+ "SELECT COUNT(*) AS c FROM conversations WHERE schedule_job_id = ?",
329
+ scheduleJobId,
330
+ )?.c ?? 0
331
+ );
332
+ }
333
+
317
334
  export function getConversationType(
318
335
  conversationId: string,
319
336
  ): "standard" | "private" {
@@ -1096,6 +1113,77 @@ export function getMessages(conversationId: string): MessageRow[] {
1096
1113
  .map(parseMessage);
1097
1114
  }
1098
1115
 
1116
+ export interface PaginatedMessagesResult {
1117
+ messages: MessageRow[];
1118
+ hasMore: boolean;
1119
+ }
1120
+
1121
+ export function getMessagesPaginated(
1122
+ conversationId: string,
1123
+ limit: number | undefined,
1124
+ beforeTimestamp?: number,
1125
+ ): PaginatedMessagesResult {
1126
+ const db = getDb();
1127
+
1128
+ if (limit === undefined) {
1129
+ const conditions = [eq(messages.conversationId, conversationId)];
1130
+ if (beforeTimestamp !== undefined) {
1131
+ conditions.push(lt(messages.createdAt, beforeTimestamp));
1132
+ }
1133
+ const rows = db
1134
+ .select()
1135
+ .from(messages)
1136
+ .where(and(...conditions))
1137
+ .orderBy(asc(messages.createdAt))
1138
+ .all()
1139
+ .map(parseMessage);
1140
+ return { messages: rows, hasMore: false };
1141
+ }
1142
+
1143
+ const conditions = [eq(messages.conversationId, conversationId)];
1144
+ if (beforeTimestamp !== undefined) {
1145
+ conditions.push(lt(messages.createdAt, beforeTimestamp));
1146
+ }
1147
+
1148
+ const rows = db
1149
+ .select()
1150
+ .from(messages)
1151
+ .where(and(...conditions))
1152
+ .orderBy(desc(messages.createdAt))
1153
+ .limit(limit + 1)
1154
+ .all()
1155
+ .map(parseMessage);
1156
+
1157
+ const hasMore = rows.length > limit;
1158
+ if (hasMore) {
1159
+ rows.splice(limit);
1160
+ }
1161
+ rows.reverse();
1162
+
1163
+ return { messages: rows, hasMore };
1164
+ }
1165
+
1166
+ export function getLastAssistantTimestampBefore(
1167
+ conversationId: string,
1168
+ beforeTimestamp: number,
1169
+ ): number {
1170
+ const db = getDb();
1171
+ const row = db
1172
+ .select({ createdAt: messages.createdAt })
1173
+ .from(messages)
1174
+ .where(
1175
+ and(
1176
+ eq(messages.conversationId, conversationId),
1177
+ eq(messages.role, "assistant"),
1178
+ lt(messages.createdAt, beforeTimestamp),
1179
+ ),
1180
+ )
1181
+ .orderBy(desc(messages.createdAt))
1182
+ .limit(1)
1183
+ .get();
1184
+ return row?.createdAt ?? 0;
1185
+ }
1186
+
1099
1187
  /** Fetch a single message by ID, optionally scoped to a specific conversation. */
1100
1188
  export function getMessageById(
1101
1189
  messageId: string,
@@ -1745,9 +1833,10 @@ export function getTurnTimeBounds(
1745
1833
  // beyond any surviving message. Cap at 30 minutes to avoid sweeping in
1746
1834
  // logs from a much later turn.
1747
1835
  const MAX_TURN_DURATION_MS = 30 * 60 * 1000;
1748
- const hardCeiling = nextTurnStart != null && nextTurnStart > startTime
1749
- ? nextTurnStart - 1
1750
- : startTime + MAX_TURN_DURATION_MS;
1836
+ const hardCeiling =
1837
+ nextTurnStart != null && nextTurnStart > startTime
1838
+ ? nextTurnStart - 1
1839
+ : startTime + MAX_TURN_DURATION_MS;
1751
1840
 
1752
1841
  if (hardCeiling > endTime) {
1753
1842
  const latestLog = db
@@ -7,14 +7,23 @@
7
7
  * first contact.
8
8
  */
9
9
 
10
+ import { existsSync, unlinkSync } from "node:fs";
11
+
10
12
  import { eq } from "drizzle-orm";
11
13
  import { v4 as uuid } from "uuid";
12
14
 
15
+ import { getLogger } from "../util/logger.js";
16
+ import { getWorkspacePromptPath } from "../util/platform.js";
13
17
  import { initConversationDir } from "./conversation-disk-view.js";
14
18
  import { GENERATING_TITLE } from "./conversation-title-service.js";
15
19
  import { getDb } from "./db.js";
16
20
  import { conversationKeys, conversations } from "./schema.js";
17
21
 
22
+ const log = getLogger("conversation-key-store");
23
+
24
+ /** Set after the first conversation is created so BOOTSTRAP.md is deleted on the second. */
25
+ let firstConversationSeen = false;
26
+
18
27
  export interface ConversationKeyMapping {
19
28
  id: string;
20
29
  conversationKey: string;
@@ -190,6 +199,23 @@ export function getOrCreateConversation(
190
199
  };
191
200
  }
192
201
 
202
+ // Delete BOOTSTRAP.md when a non-first conversation is created.
203
+ // The first conversation is the onboarding one; keep BOOTSTRAP.md
204
+ // for its entire duration. Any subsequent conversation means
205
+ // onboarding is over.
206
+ if (firstConversationSeen) {
207
+ const bp = getWorkspacePromptPath("BOOTSTRAP.md");
208
+ if (existsSync(bp)) {
209
+ try {
210
+ unlinkSync(bp);
211
+ log.info("Deleted BOOTSTRAP.md — onboarding conversation ended");
212
+ } catch {
213
+ // Best-effort
214
+ }
215
+ }
216
+ }
217
+ firstConversationSeen = true;
218
+
193
219
  const now = Date.now();
194
220
  const conversationId = uuid();
195
221
  const title = GENERATING_TITLE;
@@ -26,13 +26,13 @@ function buildFtsMatchQuery(text: string): string | null {
26
26
 
27
27
  export function listConversations(
28
28
  limit?: number,
29
- includeBackground = false,
29
+ backgroundOnly = false,
30
30
  offset = 0,
31
31
  ): ConversationRow[] {
32
32
  ensureDisplayOrderMigration();
33
33
  const db = getDb();
34
- const where = includeBackground
35
- ? undefined
34
+ const where = backgroundOnly
35
+ ? sql`${conversations.conversationType} = 'background'`
36
36
  : sql`${conversations.conversationType} NOT IN ('background', 'private')`;
37
37
  const query = db
38
38
  .select()
@@ -44,10 +44,10 @@ export function listConversations(
44
44
  return query.all().map(parseConversation);
45
45
  }
46
46
 
47
- export function countConversations(includeBackground = false): number {
47
+ export function countConversations(backgroundOnly = false): number {
48
48
  const db = getDb();
49
- const where = includeBackground
50
- ? undefined
49
+ const where = backgroundOnly
50
+ ? sql`${conversations.conversationType} = 'background'`
51
51
  : sql`${conversations.conversationType} NOT IN ('background', 'private')`;
52
52
  const [{ total }] = db
53
53
  .select({ total: count() })
@@ -71,6 +71,7 @@ import {
71
71
  migrateDropMemorySegmentFts,
72
72
  migrateDropOrphanedMediaTables,
73
73
  migrateDropRemindersTable,
74
+ migrateDropSetupSkillIdColumn,
74
75
  migrateDropSimplifiedMemory,
75
76
  migrateDropUsageCompositeIndexes,
76
77
  migrateFkCascadeRebuilds,
@@ -89,10 +90,12 @@ import {
89
90
  migrateLlmRequestLogMessageId,
90
91
  migrateLlmRequestLogProvider,
91
92
  migrateMemoryItemSupersession,
93
+ migrateMessagesConversationCreatedAtIndex,
92
94
  migrateMessagesFtsBackfill,
93
95
  migrateNormalizePhoneIdentities,
94
96
  migrateNotificationDeliveryThreadDecision,
95
97
  migrateOAuthAppsClientSecretPath,
98
+ migrateOAuthProvidersBehaviorColumns,
96
99
  migrateOAuthProvidersDisplayMetadata,
97
100
  migrateOAuthProvidersManagedServiceConfigKey,
98
101
  migrateOAuthProvidersPingConfig,
@@ -117,6 +120,7 @@ import {
117
120
  migrateScheduleOneShotRouting,
118
121
  migrateScheduleQuietFlag,
119
122
  migrateSchemaIndexesAndColumns,
123
+ migrateStripIntegrationPrefixFromProviderKeys,
120
124
  migrateUsageDashboardIndexes,
121
125
  migrateVoiceInviteColumns,
122
126
  migrateVoiceInviteDisplayMetadata,
@@ -516,6 +520,18 @@ export function initializeDb(): void {
516
520
  // 92. Add ping_method, ping_headers, ping_body columns to oauth_providers
517
521
  migrateOAuthProvidersPingConfig(database);
518
522
 
523
+ // 93. Strip `integration:` prefix from provider_key across OAuth tables
524
+ migrateStripIntegrationPrefixFromProviderKeys(database);
525
+
526
+ // 94. Composite index on messages(conversation_id, created_at) for paginated history queries
527
+ migrateMessagesConversationCreatedAtIndex(database);
528
+
529
+ // 95. Add behavioral config columns to oauth_providers (loopback port, injection templates, setup metadata, identity verification)
530
+ migrateOAuthProvidersBehaviorColumns(database);
531
+
532
+ // 96. Drop the setup_skill_id column from oauth_providers (concept removed)
533
+ migrateDropSetupSkillIdColumn(database);
534
+
519
535
  validateMigrationState(database);
520
536
 
521
537
  if (process.env.BUN_TEST === "1") {