@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.
- package/Dockerfile +42 -9
- package/docs/architecture/integrations.md +34 -32
- package/node_modules/@vellumai/ces-contracts/src/__tests__/grants.test.ts +7 -7
- package/node_modules/@vellumai/ces-contracts/src/handles.ts +5 -4
- package/node_modules/@vellumai/ces-contracts/src/index.ts +7 -0
- package/node_modules/@vellumai/ces-contracts/src/rpc.ts +5 -0
- package/node_modules/@vellumai/credential-storage/src/index.ts +1 -1
- package/openapi.yaml +87 -9
- package/package.json +1 -1
- package/src/__tests__/catalog-cache.test.ts +164 -0
- package/src/__tests__/catalog-search.test.ts +61 -0
- package/src/__tests__/cli-command-risk-guard.test.ts +181 -6
- package/src/__tests__/conversation-delete-schedule-cleanup.test.ts +396 -0
- package/src/__tests__/conversation-error.test.ts +3 -2
- package/src/__tests__/credential-security-invariants.test.ts +9 -15
- package/src/__tests__/credential-vault-unit.test.ts +32 -34
- package/src/__tests__/credential-vault.test.ts +25 -33
- package/src/__tests__/credentials-cli.test.ts +3 -3
- package/src/__tests__/daemon-credential-client.test.ts +2 -2
- package/src/__tests__/first-greeting.test.ts +7 -0
- package/src/__tests__/host-bash-proxy.test.ts +79 -0
- package/src/__tests__/host-cu-proxy.test.ts +90 -0
- package/src/__tests__/host-file-proxy.test.ts +89 -0
- package/src/__tests__/integration-status.test.ts +5 -5
- package/src/__tests__/list-messages-attachments.test.ts +171 -0
- package/src/__tests__/mcp-abort-signal.test.ts +205 -0
- package/src/__tests__/messaging-send-tool.test.ts +5 -5
- package/src/__tests__/navigate-settings-tab.test.ts +6 -2
- package/src/__tests__/notification-telegram-adapter.test.ts +125 -0
- package/src/__tests__/oauth-cli.test.ts +126 -119
- package/src/__tests__/oauth-provider-profiles.test.ts +55 -20
- package/src/__tests__/oauth-scope-policy.test.ts +4 -6
- package/src/__tests__/onboarding-template-contract.test.ts +2 -2
- package/src/__tests__/platform.test.ts +3 -168
- package/src/__tests__/secret-routes-managed-proxy.test.ts +78 -0
- package/src/__tests__/secure-keys-managed-failover.test.ts +73 -0
- package/src/__tests__/skill-feature-flags.test.ts +8 -0
- package/src/__tests__/skill-secret-handling-guard.test.ts +212 -0
- package/src/__tests__/skills-uninstall.test.ts +2 -2
- package/src/__tests__/slack-messaging-token-resolution.test.ts +22 -24
- package/src/__tests__/slack-share-routes.test.ts +5 -5
- package/src/__tests__/system-prompt.test.ts +39 -0
- package/src/__tests__/token-estimator-accuracy.benchmark.test.ts +1 -1
- package/src/__tests__/workspace-migration-backfill-installation-id.test.ts +5 -4
- package/src/cli/AGENTS.md +47 -7
- package/src/cli/commands/browser-relay.ts +2 -17
- package/src/cli/commands/contacts.ts +6 -4
- package/src/cli/commands/conversations.ts +13 -1
- package/src/cli/commands/credential-execution.ts +16 -1
- package/src/cli/commands/credentials.ts +2 -8
- package/src/cli/commands/oauth/__tests__/connect.test.ts +29 -108
- package/src/cli/commands/oauth/__tests__/disconnect.test.ts +13 -87
- package/src/cli/commands/oauth/__tests__/mode.test.ts +22 -69
- package/src/cli/commands/oauth/__tests__/ping.test.ts +20 -79
- package/src/cli/commands/oauth/__tests__/providers-delete.test.ts +574 -0
- package/src/cli/commands/oauth/__tests__/providers-update.test.ts +416 -0
- package/src/cli/commands/oauth/__tests__/status.test.ts +12 -40
- package/src/cli/commands/oauth/__tests__/token.test.ts +3 -50
- package/src/cli/commands/oauth/apps.ts +63 -44
- package/src/cli/commands/oauth/connect.ts +187 -155
- package/src/cli/commands/oauth/disconnect.ts +27 -75
- package/src/cli/commands/oauth/index.ts +36 -46
- package/src/cli/commands/oauth/mode.ts +22 -34
- package/src/cli/commands/oauth/ping.ts +19 -45
- package/src/cli/commands/oauth/providers.ts +569 -62
- package/src/cli/commands/oauth/request.ts +36 -48
- package/src/cli/commands/oauth/shared.ts +1 -19
- package/src/cli/commands/oauth/status.ts +14 -25
- package/src/cli/commands/oauth/token.ts +25 -34
- package/src/cli/commands/platform/__tests__/connect.test.ts +224 -0
- package/src/cli/commands/platform/__tests__/disconnect.test.ts +237 -0
- package/src/cli/commands/platform/__tests__/status.test.ts +246 -0
- package/src/cli/commands/platform/connect.ts +104 -0
- package/src/cli/commands/platform/disconnect.ts +118 -0
- package/src/cli/commands/{platform.ts → platform/index.ts} +108 -38
- package/src/cli/commands/sequence.ts +5 -4
- package/src/cli/commands/shotgun.ts +16 -0
- package/src/cli/commands/skills.ts +173 -41
- package/src/cli/commands/usage.ts +5 -11
- package/src/cli/lib/daemon-credential-client.ts +22 -38
- package/src/cli/program.ts +1 -1
- package/src/config/assistant-feature-flags.ts +3 -7
- package/src/config/bundled-skills/contacts/tools/google-contacts.ts +1 -1
- package/src/config/bundled-skills/conversations/SKILL.md +20 -0
- package/src/config/bundled-skills/conversations/TOOLS.json +23 -0
- package/src/config/bundled-skills/conversations/tools/rename-conversation.ts +66 -0
- package/src/config/bundled-skills/gmail/SKILL.md +13 -13
- package/src/config/bundled-skills/gmail/tools/gmail-archive.ts +3 -3
- package/src/config/bundled-skills/gmail/tools/gmail-attachments.ts +2 -2
- package/src/config/bundled-skills/gmail/tools/gmail-draft.ts +1 -1
- package/src/config/bundled-skills/gmail/tools/gmail-filters.ts +1 -1
- package/src/config/bundled-skills/gmail/tools/gmail-follow-up.ts +1 -1
- package/src/config/bundled-skills/gmail/tools/gmail-forward.ts +1 -1
- package/src/config/bundled-skills/gmail/tools/gmail-label.ts +2 -2
- package/src/config/bundled-skills/gmail/tools/gmail-outreach-scan.ts +1 -1
- package/src/config/bundled-skills/gmail/tools/gmail-send-draft.ts +1 -1
- package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +1 -1
- package/src/config/bundled-skills/gmail/tools/gmail-trash.ts +1 -1
- package/src/config/bundled-skills/gmail/tools/gmail-unsubscribe.ts +1 -1
- package/src/config/bundled-skills/gmail/tools/gmail-vacation.ts +1 -1
- package/src/config/bundled-skills/google-calendar/SKILL.md +10 -4
- package/src/config/bundled-skills/google-calendar/tools/shared.ts +1 -1
- package/src/config/bundled-skills/messaging/SKILL.md +7 -7
- package/src/config/bundled-skills/messaging/tools/messaging-send.ts +5 -2
- package/src/config/bundled-skills/messaging/tools/shared.ts +5 -6
- package/src/config/bundled-skills/settings/TOOLS.json +5 -3
- package/src/config/bundled-skills/settings/tools/navigate-settings-tab.ts +4 -2
- package/src/config/bundled-tool-registry.ts +5 -0
- package/src/config/feature-flag-registry.json +2 -2
- package/src/credential-execution/client.ts +15 -3
- package/src/daemon/conversation-agent-loop.ts +2 -0
- package/src/daemon/conversation-error.ts +36 -6
- package/src/daemon/conversation-messaging.ts +9 -0
- package/src/daemon/conversation-runtime-assembly.ts +33 -0
- package/src/daemon/conversation-surfaces.ts +120 -14
- package/src/daemon/conversation.ts +5 -0
- package/src/daemon/first-greeting.ts +6 -1
- package/src/daemon/handlers/skills.ts +148 -3
- package/src/daemon/host-bash-proxy.ts +16 -0
- package/src/daemon/host-cu-proxy.ts +16 -0
- package/src/daemon/host-file-proxy.ts +16 -0
- package/src/daemon/lifecycle.ts +56 -5
- package/src/daemon/message-types/conversations.ts +1 -0
- package/src/daemon/message-types/guardian-actions.ts +2 -0
- package/src/daemon/message-types/host-bash.ts +6 -1
- package/src/daemon/message-types/host-cu.ts +6 -1
- package/src/daemon/message-types/host-file.ts +6 -1
- package/src/daemon/message-types/integrations.ts +0 -1
- package/src/daemon/server.ts +29 -2
- package/src/hooks/cli.ts +74 -0
- package/src/inbound/platform-callback-registration.ts +7 -12
- package/src/index.ts +0 -12
- package/src/mcp/client.ts +6 -1
- package/src/mcp/manager.ts +2 -1
- package/src/memory/conversation-crud.ts +92 -3
- package/src/memory/conversation-key-store.ts +26 -0
- package/src/memory/conversation-queries.ts +6 -6
- package/src/memory/db-init.ts +16 -0
- package/src/memory/journal-memory.ts +8 -2
- package/src/memory/migrations/196-messages-conversation-created-at-index.ts +9 -0
- package/src/memory/migrations/196-strip-integration-prefix-from-provider-keys.ts +186 -0
- package/src/memory/migrations/197-oauth-providers-behavior-columns.ts +29 -0
- package/src/memory/migrations/198-drop-setup-skill-id-column.ts +11 -0
- package/src/memory/migrations/index.ts +4 -0
- package/src/memory/migrations/registry.ts +8 -0
- package/src/memory/schema/oauth.ts +11 -0
- package/src/messaging/provider.ts +13 -12
- package/src/messaging/providers/gmail/adapter.ts +44 -35
- package/src/messaging/providers/slack/adapter.ts +63 -33
- package/src/messaging/providers/telegram-bot/adapter.ts +6 -8
- package/src/messaging/providers/whatsapp/adapter.ts +6 -8
- package/src/notifications/adapters/telegram.ts +78 -2
- package/src/oauth/__tests__/identity-verifier.test.ts +464 -0
- package/src/oauth/byo-connection.test.ts +22 -24
- package/src/oauth/connect-orchestrator.ts +37 -76
- package/src/oauth/connect-types.ts +7 -65
- package/src/oauth/connection-resolver.test.ts +13 -13
- package/src/oauth/connection-resolver.ts +3 -4
- package/src/oauth/identity-verifier.ts +177 -0
- package/src/oauth/oauth-store.ts +228 -3
- package/src/oauth/platform-connection.test.ts +56 -6
- package/src/oauth/platform-connection.ts +8 -1
- package/src/oauth/seed-providers.ts +247 -34
- package/src/permissions/checker.ts +127 -1
- package/src/prompts/journal-context.ts +4 -1
- package/src/prompts/system-prompt.ts +54 -9
- package/src/prompts/templates/BOOTSTRAP.md +16 -5
- package/src/providers/anthropic/client.ts +2 -33
- package/src/runtime/guardian-action-service.ts +7 -2
- package/src/runtime/http-server.ts +12 -18
- package/src/runtime/http-types.ts +8 -1
- package/src/runtime/migrations/rebind-secrets-screen.ts +2 -2
- package/src/runtime/routes/conversation-management-routes.ts +31 -0
- package/src/runtime/routes/conversation-routes.ts +79 -4
- package/src/runtime/routes/guardian-action-routes.ts +15 -2
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +21 -8
- package/src/runtime/routes/integrations/slack/share.ts +1 -1
- package/src/runtime/routes/oauth-apps.ts +2 -1
- package/src/runtime/routes/secret-routes.ts +45 -15
- package/src/runtime/routes/settings-routes.ts +12 -19
- package/src/runtime/routes/skills-routes.ts +45 -4
- package/src/schedule/integration-status.ts +2 -2
- package/src/security/ces-rpc-credential-backend.ts +19 -16
- package/src/security/oauth-completion-page.ts +153 -0
- package/src/security/oauth2.ts +3 -17
- package/src/security/secure-keys.ts +207 -7
- package/src/security/token-manager.ts +3 -6
- package/src/signals/bash.ts +6 -1
- package/src/skills/catalog-cache.ts +44 -0
- package/src/skills/catalog-search.ts +18 -0
- package/src/tools/browser/browser-manager.ts +2 -2
- package/src/tools/credentials/post-connect-hooks.ts +1 -1
- package/src/tools/credentials/vault.ts +34 -45
- package/src/tools/host-terminal/host-shell.ts +16 -3
- package/src/tools/mcp/mcp-tool-factory.ts +2 -1
- package/src/tools/skills/sandbox-runner.ts +16 -3
- package/src/tools/terminal/shell.ts +16 -3
- package/src/util/logger.ts +11 -1
- package/src/util/platform.ts +1 -91
- package/src/util/sentry-log-stream.ts +51 -0
- package/src/watcher/providers/github.ts +2 -2
- package/src/watcher/providers/gmail.ts +1 -1
- package/src/watcher/providers/google-calendar.ts +1 -1
- package/src/watcher/providers/linear.ts +2 -2
- package/src/workspace/migrations/011-backfill-installation-id.ts +5 -3
- package/src/workspace/migrations/020-rename-oauth-skill-dirs.ts +119 -0
- package/src/workspace/migrations/registry.ts +2 -0
- package/src/cli/commands/oauth/connections.ts +0 -255
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|
package/src/util/logger.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
}
|
package/src/util/platform.ts
CHANGED
|
@@ -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 `
|
|
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: "
|
|
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: "
|
|
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 = "
|
|
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 `
|
|
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: "
|
|
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
|
-
|
|
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(
|
|
45
|
-
join(
|
|
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
|
];
|