@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
@@ -204,20 +204,33 @@ class HostShellTool implements Tool {
204
204
  cwd: workingDir,
205
205
  env: hostEnv,
206
206
  stdio: ["ignore", "pipe", "pipe"],
207
+ detached: true,
207
208
  });
208
209
 
209
210
  const timer = setTimeout(() => {
210
211
  timedOut = true;
211
- child.kill("SIGKILL");
212
+ try {
213
+ process.kill(-child.pid!, "SIGKILL");
214
+ } catch {
215
+ // Process group may have already exited.
216
+ }
212
217
  }, timeoutMs);
213
218
 
214
219
  // Cooperative cancellation via AbortSignal
215
220
  const onAbort = () => {
216
- child.kill("SIGKILL");
221
+ try {
222
+ process.kill(-child.pid!, "SIGKILL");
223
+ } catch {
224
+ // Process group may have already exited.
225
+ }
217
226
  };
218
227
  if (context.signal) {
219
228
  if (context.signal.aborted) {
220
- child.kill("SIGKILL");
229
+ try {
230
+ process.kill(-child.pid!, "SIGKILL");
231
+ } catch {
232
+ // Process group may have already exited.
233
+ }
221
234
  } else {
222
235
  context.signal.addEventListener("abort", onAbort, { once: true });
223
236
  }
@@ -62,7 +62,7 @@ export function createMcpTool(
62
62
 
63
63
  async execute(
64
64
  input: Record<string, unknown>,
65
- _context: ToolContext,
65
+ context: ToolContext,
66
66
  ): Promise<ToolExecutionResult> {
67
67
  try {
68
68
  // Strip injected activity before sending to MCP server
@@ -77,6 +77,7 @@ export function createMcpTool(
77
77
  serverId,
78
78
  metadata.name,
79
79
  forwardInput,
80
+ context.signal,
80
81
  );
81
82
  return {
82
83
  content: result.content,
@@ -159,20 +159,33 @@ function spawnRunner(
159
159
  cwd: runDir,
160
160
  env,
161
161
  stdio: ["ignore", "pipe", "pipe"],
162
+ detached: true,
162
163
  });
163
164
 
164
165
  const timer = setTimeout(() => {
165
166
  timedOut = true;
166
- child.kill("SIGKILL");
167
+ try {
168
+ process.kill(-child.pid!, "SIGKILL");
169
+ } catch {
170
+ // Process group may have already exited.
171
+ }
167
172
  }, timeoutMs);
168
173
 
169
174
  // Cooperative cancellation via AbortSignal
170
175
  const onAbort = () => {
171
- child.kill("SIGKILL");
176
+ try {
177
+ process.kill(-child.pid!, "SIGKILL");
178
+ } catch {
179
+ // Process group may have already exited.
180
+ }
172
181
  };
173
182
  if (context.signal) {
174
183
  if (context.signal.aborted) {
175
- child.kill("SIGKILL");
184
+ try {
185
+ process.kill(-child.pid!, "SIGKILL");
186
+ } catch {
187
+ // Process group may have already exited.
188
+ }
176
189
  } else {
177
190
  context.signal.addEventListener("abort", onAbort, { once: true });
178
191
  }
@@ -329,20 +329,33 @@ class ShellTool implements Tool {
329
329
  cwd: context.workingDir,
330
330
  env,
331
331
  stdio: ["ignore", "pipe", "pipe"],
332
+ detached: true,
332
333
  });
333
334
 
334
335
  const timer = setTimeout(() => {
335
336
  timedOut = true;
336
- child.kill("SIGKILL");
337
+ try {
338
+ process.kill(-child.pid!, "SIGKILL");
339
+ } catch {
340
+ // Process group may have already exited.
341
+ }
337
342
  }, timeoutMs);
338
343
 
339
344
  // Cooperative cancellation via AbortSignal
340
345
  const onAbort = () => {
341
- child.kill("SIGKILL");
346
+ try {
347
+ process.kill(-child.pid!, "SIGKILL");
348
+ } catch {
349
+ // Process group may have already exited.
350
+ }
342
351
  };
343
352
  if (context.signal) {
344
353
  if (context.signal.aborted) {
345
- child.kill("SIGKILL");
354
+ try {
355
+ process.kill(-child.pid!, "SIGKILL");
356
+ } catch {
357
+ // Process group may have already exited.
358
+ }
346
359
  } else {
347
360
  context.signal.addEventListener("abort", onAbort, { once: true });
348
361
  }
@@ -18,6 +18,7 @@ import {
18
18
  } from "../config/env-registry.js";
19
19
  import { logSerializers } from "./log-redact.js";
20
20
  import { getLogPath } from "./platform.js";
21
+ import { createSentryLogStream } from "./sentry-log-stream.js";
21
22
 
22
23
  /** Common pino-pretty options that inline [module] into the message prefix. */
23
24
  function prettyOpts(extra?: PrettyOptions): PrettyOptions {
@@ -133,6 +134,11 @@ function buildRotatingLogger(config: LogFileConfig): pino.Logger {
133
134
  activeLogDate = today;
134
135
  activeLogFileConfig = { ...config, dir };
135
136
 
137
+ const sentryStream = {
138
+ stream: createSentryLogStream(),
139
+ level: "error" as const,
140
+ };
141
+
136
142
  // When stdout is not a TTY (e.g. desktop app redirects to a hatch log file),
137
143
  // write to the rotating file only — the hatch log already captured early
138
144
  // startup output and echoing pino output there is unnecessary duplication.
@@ -141,7 +147,10 @@ function buildRotatingLogger(config: LogFileConfig): pino.Logger {
141
147
  if (!process.stdout.isTTY && !getIsContainerized()) {
142
148
  return pino(
143
149
  { name: "assistant", level: "info", serializers: logSerializers },
144
- fileStream,
150
+ pino.multistream([
151
+ { stream: fileStream, level: "info" as const },
152
+ sentryStream,
153
+ ]),
145
154
  );
146
155
  }
147
156
 
@@ -153,6 +162,7 @@ function buildRotatingLogger(config: LogFileConfig): pino.Logger {
153
162
  stream: pinoPretty(prettyOpts({ destination: 1 })),
154
163
  level: "info" as const,
155
164
  },
165
+ sentryStream,
156
166
  ]),
157
167
  );
158
168
  }
@@ -1,9 +1,4 @@
1
- import {
2
- chmodSync,
3
- existsSync,
4
- mkdirSync,
5
- readFileSync,
6
- } from "node:fs";
1
+ import { chmodSync, existsSync, mkdirSync, readFileSync } from "node:fs";
7
2
  import { homedir } from "node:os";
8
3
  import { join } from "node:path";
9
4
 
@@ -44,90 +39,6 @@ export function getClipboardCommand(): string | null {
44
39
  return null;
45
40
  }
46
41
 
47
-
48
- /**
49
- * Resolve the instance data directory from the lockfile.
50
- *
51
- * Checks both ~/.vellum.lock.json (current) and ~/.vellum.lockfile.json
52
- * (legacy) to support installs that haven't migrated the filename.
53
- *
54
- * Reads the lockfile from homedir() directly (NOT via readLockfile() which
55
- * depends on BASE_DATA_DIR) to avoid circular dependency — this function is
56
- * called at CLI bootstrap before BASE_DATA_DIR is set.
57
- *
58
- * Resolution:
59
- * - If activeAssistant matches a local assistant, returns its instanceDir.
60
- * - If there is exactly one local assistant and no activeAssistant, returns
61
- * its instanceDir (auto-select).
62
- * - Returns undefined in all other cases (no lockfile, no local assistants,
63
- * multiple local assistants with no active selection, malformed JSON).
64
- *
65
- * Synchronous (uses readFileSync) since it runs at bootstrap before any async
66
- * context. Never throws — catches all errors and returns undefined for
67
- * graceful degradation.
68
- */
69
- export function resolveInstanceDataDir(): string | undefined {
70
- try {
71
- const home = homedir();
72
- const candidates = [
73
- join(home, ".vellum.lock.json"),
74
- join(home, ".vellum.lockfile.json"),
75
- ];
76
-
77
- let raw: unknown;
78
- for (const lockPath of candidates) {
79
- if (!existsSync(lockPath)) continue;
80
- try {
81
- const parsed = JSON.parse(readFileSync(lockPath, "utf-8"));
82
- if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
83
- raw = parsed;
84
- break;
85
- }
86
- } catch {
87
- // Malformed JSON; try next candidate
88
- }
89
- }
90
- if (!raw || typeof raw !== "object" || Array.isArray(raw)) return undefined;
91
- const lockData = raw as Record<string, unknown>;
92
-
93
- const assistants = lockData.assistants as
94
- | Array<Record<string, unknown>>
95
- | undefined;
96
- if (!Array.isArray(assistants)) return undefined;
97
-
98
- const localAssistants = assistants.filter(
99
- (a) => a.cloud === "local" || a.cloud === undefined,
100
- );
101
- if (localAssistants.length === 0) return undefined;
102
-
103
- const activeAssistant = lockData.activeAssistant as string | undefined;
104
-
105
- if (activeAssistant) {
106
- const match = localAssistants.find(
107
- (a) => a.assistantId === activeAssistant,
108
- );
109
- if (match) {
110
- const resources = match.resources as
111
- | Record<string, unknown>
112
- | undefined;
113
- return resources?.instanceDir as string | undefined;
114
- }
115
- return undefined;
116
- }
117
-
118
- if (localAssistants.length === 1) {
119
- const resources = localAssistants[0].resources as
120
- | Record<string, unknown>
121
- | undefined;
122
- return resources?.instanceDir as string | undefined;
123
- }
124
-
125
- return undefined;
126
- } catch {
127
- return undefined;
128
- }
129
- }
130
-
131
42
  /**
132
43
  * Normalize an assistant ID to its canonical form for DB operations.
133
44
  *
@@ -147,7 +58,6 @@ export function normalizeAssistantId(assistantId: string): string {
147
58
  return assistantId;
148
59
  }
149
60
 
150
-
151
61
  /**
152
62
  * Returns the root ~/.vellum directory. User-facing files (config, prompt
153
63
  * files, skills) and runtime files (socket, PID) live here.
@@ -0,0 +1,51 @@
1
+ import { Writable } from "node:stream";
2
+
3
+ import * as Sentry from "@sentry/node";
4
+
5
+ /**
6
+ * Pino-compatible writable stream that forwards error/fatal log messages
7
+ * to Sentry as captured events. Add this stream to a pino multistream at
8
+ * the "error" level so that every `log.error(…)` and `log.fatal(…)` call
9
+ * automatically creates a Sentry issue.
10
+ *
11
+ * If the log entry contains an `err` field (serialised error object), the
12
+ * error is captured via `Sentry.captureException`; otherwise the message
13
+ * text is captured via `Sentry.captureMessage`.
14
+ */
15
+ export function createSentryLogStream(): Writable {
16
+ return new Writable({
17
+ write(chunk, _encoding, callback) {
18
+ try {
19
+ const entry = JSON.parse(chunk.toString());
20
+ const module: string = entry.module ?? "unknown";
21
+ const msg: string = entry.msg ?? "";
22
+
23
+ if (entry.err && typeof entry.err === "object") {
24
+ // Reconstruct an Error so Sentry gets a proper stack trace.
25
+ const errObj = entry.err;
26
+ const error = new Error(errObj.message ?? msg);
27
+ error.name = errObj.type ?? errObj.name ?? "Error";
28
+ if (errObj.stack) error.stack = errObj.stack;
29
+
30
+ Sentry.withScope((scope) => {
31
+ scope.setTag("source", "error_log");
32
+ scope.setTag("log_module", module);
33
+ scope.setLevel(entry.level >= 60 ? "fatal" : "error");
34
+ if (msg) scope.setExtra("log_message", msg);
35
+ Sentry.captureException(error);
36
+ });
37
+ } else {
38
+ Sentry.withScope((scope) => {
39
+ scope.setTag("source", "error_log");
40
+ scope.setTag("log_module", module);
41
+ scope.setLevel(entry.level >= 60 ? "fatal" : "error");
42
+ Sentry.captureMessage(`[${module}] ${msg}`);
43
+ });
44
+ }
45
+ } catch {
46
+ // Never block logging if Sentry capture fails.
47
+ }
48
+ callback();
49
+ },
50
+ });
51
+ }
@@ -6,7 +6,7 @@
6
6
  * start from "now" and don't replay historical notifications.
7
7
  *
8
8
  * The credential service expects a GitHub Personal Access Token (or fine-grained
9
- * token) stored under `integration:github`. The token needs at minimum the
9
+ * token) stored under `github`. The token needs at minimum the
10
10
  * `notifications` scope (classic) or Notification read permission (fine-grained).
11
11
  */
12
12
 
@@ -117,7 +117,7 @@ async function fetchNotificationsPage(
117
117
  export const githubProvider: WatcherProvider = {
118
118
  id: "github",
119
119
  displayName: "GitHub",
120
- requiredCredentialService: "integration:github",
120
+ requiredCredentialService: "github",
121
121
 
122
122
  async getInitialWatermark(_credentialService: string): Promise<string> {
123
123
  // Start from "now" so we don't replay all existing notifications
@@ -115,7 +115,7 @@ class HistoryExpiredError extends Error {
115
115
  export const gmailProvider: WatcherProvider = {
116
116
  id: "gmail",
117
117
  displayName: "Gmail",
118
- requiredCredentialService: "integration:google",
118
+ requiredCredentialService: "google",
119
119
 
120
120
  async getInitialWatermark(credentialService: string): Promise<string> {
121
121
  const connection = await resolveOAuthConnection(credentialService);
@@ -25,7 +25,7 @@ import type {
25
25
  const log = getLogger("watcher:google-calendar");
26
26
 
27
27
  /** The credential service — calendar shares OAuth tokens with Gmail. */
28
- const CREDENTIAL_SERVICE = "integration:google";
28
+ const CREDENTIAL_SERVICE = "google";
29
29
 
30
30
  function eventToItem(event: CalendarEvent, eventType: string): WatcherItem {
31
31
  const start = event.start?.dateTime ?? event.start?.date ?? "";
@@ -9,7 +9,7 @@
9
9
  * for issues assigned to the authenticated user.
10
10
  *
11
11
  * The credential service expects a Linear API key (personal or OAuth access token)
12
- * stored under `integration:linear`. The token only needs read access to notifications
12
+ * stored under `linear`. The token only needs read access to notifications
13
13
  * and issues.
14
14
  */
15
15
 
@@ -495,7 +495,7 @@ function issueToStatusChangeItem(
495
495
  export const linearProvider: WatcherProvider = {
496
496
  id: "linear",
497
497
  displayName: "Linear",
498
- requiredCredentialService: "integration:linear",
498
+ requiredCredentialService: "linear",
499
499
 
500
500
  async getInitialWatermark(_credentialService: string): Promise<string> {
501
501
  // Start from "now" so we don't replay all existing notifications
@@ -39,10 +39,12 @@ export const backfillInstallationIdMigration: WorkspaceMigration = {
39
39
 
40
40
  // b. Read the lockfile — check both the current and legacy lockfile paths
41
41
  // to support installs that haven't migrated the filename yet.
42
- const base = process.env.BASE_DATA_DIR?.trim() || homedir();
42
+ // Always reads from homedir(), matching resolveInstanceDataDir() in
43
+ // platform.ts — the lockfile is a per-user file, not per-instance.
44
+ const home = homedir();
43
45
  const lockCandidates = [
44
- join(base, ".vellum.lock.json"),
45
- join(base, ".vellum.lockfile.json"),
46
+ join(home, ".vellum.lock.json"),
47
+ join(home, ".vellum.lockfile.json"),
46
48
  ];
47
49
 
48
50
  let lockPath: string | undefined;
@@ -0,0 +1,119 @@
1
+ /**
2
+ * Workspace migration 020: Rename OAuth skill directories and update SKILLS.md
3
+ * index entries to match the new `<domain>-oauth-app-setup` naming convention.
4
+ *
5
+ * Also removes deleted skills (collaborative-oauth-flow, oauth-setup) that
6
+ * were superseded by vellum-oauth-integrations.
7
+ *
8
+ * Idempotent: safe to re-run after interruption at any point.
9
+ */
10
+
11
+ import {
12
+ existsSync,
13
+ readFileSync,
14
+ renameSync,
15
+ rmSync,
16
+ writeFileSync,
17
+ } from "node:fs";
18
+ import { join } from "node:path";
19
+
20
+ import type { WorkspaceMigration } from "./types.js";
21
+
22
+ // ---------------------------------------------------------------------------
23
+ // Rename map: old directory name -> new directory name
24
+ // ---------------------------------------------------------------------------
25
+
26
+ const RENAMES: [oldName: string, newName: string][] = [
27
+ ["google-oauth-applescript", "google-oauth-app-setup"],
28
+ ["airtable-oauth-setup", "airtable-oauth-app-setup"],
29
+ ["asana-oauth-setup", "asana-oauth-app-setup"],
30
+ ["discord-oauth-setup", "discord-oauth-app-setup"],
31
+ ["dropbox-oauth-setup", "dropbox-oauth-app-setup"],
32
+ ["figma-oauth-setup", "figma-oauth-app-setup"],
33
+ ["github-oauth-setup", "github-oauth-app-setup"],
34
+ ["hubspot-oauth-setup", "hubspot-oauth-app-setup"],
35
+ ["linear-oauth-setup", "linear-oauth-app-setup"],
36
+ ["notion-oauth-setup", "notion-oauth-app-setup"],
37
+ ["spotify-oauth-setup", "spotify-oauth-app-setup"],
38
+ ["todoist-oauth-setup", "todoist-oauth-app-setup"],
39
+ ["twitter-oauth-setup", "twitter-oauth-app-setup"],
40
+ ];
41
+
42
+ /** Skills that were deleted and should be removed from the workspace. */
43
+ const DELETED_SKILLS = ["collaborative-oauth-flow", "oauth-setup"];
44
+
45
+ // ---------------------------------------------------------------------------
46
+ // Migration
47
+ // ---------------------------------------------------------------------------
48
+
49
+ export const renameOauthSkillDirsMigration: WorkspaceMigration = {
50
+ id: "020-rename-oauth-skill-dirs",
51
+ description:
52
+ "Rename OAuth skill directories to <domain>-oauth-app-setup convention and remove deleted skills",
53
+
54
+ run(workspaceDir: string): void {
55
+ const skillsDir = join(workspaceDir, "skills");
56
+ if (!existsSync(skillsDir)) return;
57
+
58
+ // 1. Rename skill directories
59
+ for (const [oldName, newName] of RENAMES) {
60
+ const oldDir = join(skillsDir, oldName);
61
+ const newDir = join(skillsDir, newName);
62
+ if (existsSync(oldDir) && !existsSync(newDir)) {
63
+ renameSync(oldDir, newDir);
64
+ }
65
+ }
66
+
67
+ // 2. Remove deleted skill directories
68
+ for (const name of DELETED_SKILLS) {
69
+ const dir = join(skillsDir, name);
70
+ if (existsSync(dir)) {
71
+ rmSync(dir, { recursive: true, force: true });
72
+ }
73
+ }
74
+
75
+ // 3. Update SKILLS.md index entries
76
+ const indexPath = join(skillsDir, "SKILLS.md");
77
+ if (existsSync(indexPath)) {
78
+ let content = readFileSync(indexPath, "utf-8");
79
+ for (const [oldName, newName] of RENAMES) {
80
+ content = content.replaceAll(oldName, newName);
81
+ }
82
+ for (const name of DELETED_SKILLS) {
83
+ // Remove lines referencing deleted skills (e.g., "- collaborative-oauth-flow\n")
84
+ content = content.replace(
85
+ new RegExp(`^[\\t ]*-\\s*${name}\\s*\\n?`, "gm"),
86
+ "",
87
+ );
88
+ }
89
+ writeFileSync(indexPath, content, "utf-8");
90
+ }
91
+ },
92
+
93
+ down(workspaceDir: string): void {
94
+ const skillsDir = join(workspaceDir, "skills");
95
+ if (!existsSync(skillsDir)) return;
96
+
97
+ // Reverse renames
98
+ for (const [oldName, newName] of RENAMES) {
99
+ const oldDir = join(skillsDir, oldName);
100
+ const newDir = join(skillsDir, newName);
101
+ if (existsSync(newDir) && !existsSync(oldDir)) {
102
+ renameSync(newDir, oldDir);
103
+ }
104
+ }
105
+
106
+ // Reverse SKILLS.md index entries
107
+ const indexPath = join(skillsDir, "SKILLS.md");
108
+ if (existsSync(indexPath)) {
109
+ let content = readFileSync(indexPath, "utf-8");
110
+ for (const [oldName, newName] of RENAMES) {
111
+ content = content.replaceAll(newName, oldName);
112
+ }
113
+ writeFileSync(indexPath, content, "utf-8");
114
+ }
115
+
116
+ // Note: deleted skills cannot be restored by down() since they were
117
+ // removed from the repo. Users would need to reinstall them.
118
+ },
119
+ };
@@ -16,6 +16,7 @@ import { migrateCredentialsFromKeychainMigration } from "./016-migrate-credentia
16
16
  import { seedPersonaDirsMigration } from "./017-seed-persona-dirs.js";
17
17
  import { rekeyCompoundCredentialKeysMigration } from "./018-rekey-compound-credential-keys.js";
18
18
  import { scopeJournalToGuardianMigration } from "./019-scope-journal-to-guardian.js";
19
+ import { renameOauthSkillDirsMigration } from "./020-rename-oauth-skill-dirs.js";
19
20
  import { migrateToWorkspaceVolumeMigration } from "./migrate-to-workspace-volume.js";
20
21
  import type { WorkspaceMigration } from "./types.js";
21
22
 
@@ -43,4 +44,5 @@ export const WORKSPACE_MIGRATIONS: WorkspaceMigration[] = [
43
44
  extractFeatureFlagsToProtectedMigration,
44
45
  rekeyCompoundCredentialKeysMigration,
45
46
  scopeJournalToGuardianMigration,
47
+ renameOauthSkillDirsMigration,
46
48
  ];