@vellumai/assistant 0.4.34 → 0.4.36
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +1 -1
- package/ARCHITECTURE.md +44 -49
- package/README.md +32 -20
- package/docs/architecture/keychain-broker.md +186 -0
- package/docs/architecture/security.md +110 -116
- package/docs/runbook-trusted-contacts.md +2 -2
- package/docs/skills.md +25 -25
- package/package.json +4 -1
- package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +11 -2
- package/src/__tests__/actor-token-service.test.ts +1 -0
- package/src/__tests__/amazon-cdp-integration.test.ts +74 -0
- package/src/__tests__/assistant-feature-flags-integration.test.ts +38 -9
- package/src/__tests__/assistant-id-boundary-guard.test.ts +91 -43
- package/src/__tests__/browser-fill-credential.test.ts +1 -1
- package/src/__tests__/bundle-scanner.test.ts +1 -1
- package/src/__tests__/channel-guardian.test.ts +102 -102
- package/src/__tests__/channel-invite-transport.test.ts +155 -256
- package/src/__tests__/channel-readiness-routes.test.ts +336 -0
- package/src/__tests__/checker.test.ts +6 -6
- package/src/__tests__/chrome-cdp.test.ts +350 -0
- package/src/__tests__/computer-use-session-lifecycle.test.ts +3 -3
- package/src/__tests__/computer-use-session-working-dir.test.ts +86 -52
- package/src/__tests__/computer-use-skill-lifecycle-cleanup.test.ts +1 -1
- package/src/__tests__/config-loader-migration.test.ts +85 -0
- package/src/__tests__/conversation-pairing.test.ts +370 -5
- package/src/__tests__/credential-broker-browser-fill.test.ts +1 -10
- package/src/__tests__/credential-broker-server-use.test.ts +1 -10
- package/src/__tests__/credential-security-e2e.test.ts +7 -1
- package/src/__tests__/credential-security-invariants.test.ts +14 -20
- package/src/__tests__/credential-vault-unit.test.ts +1 -11
- package/src/__tests__/credential-vault.test.ts +5 -19
- package/src/__tests__/credentials-cli.test.ts +806 -0
- package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +23 -4
- package/src/__tests__/email-invite-adapter.test.ts +78 -0
- package/src/__tests__/email-service-config-fallback.test.ts +102 -0
- package/src/__tests__/encrypted-store.test.ts +6 -6
- package/src/__tests__/ephemeral-permissions.test.ts +3 -3
- package/src/__tests__/gateway-only-enforcement.test.ts +5 -1
- package/src/__tests__/guardian-actions-endpoint.test.ts +70 -12
- package/src/__tests__/guardian-outbound-http.test.ts +53 -47
- package/src/__tests__/handle-user-message-secret-resume.test.ts +23 -0
- package/src/__tests__/handlers-add-trust-rule-metadata.test.ts +32 -23
- package/src/__tests__/handlers-telegram-config.test.ts +8 -2
- package/src/__tests__/handlers-twitter-config.test.ts +2 -2
- package/src/__tests__/handlers-user-message-approval-consumption.test.ts +108 -7
- package/src/__tests__/ingress-reconcile.test.ts +6 -0
- package/src/__tests__/intent-routing.test.ts +23 -4
- package/src/__tests__/invite-routes-http.test.ts +12 -0
- package/src/__tests__/ipc-snapshot.test.ts +8 -2
- package/src/__tests__/keychain-broker-client.test.ts +543 -0
- package/src/__tests__/llm-usage-store.test.ts +344 -0
- package/src/__tests__/mcp-client-auth.test.ts +2 -2
- package/src/__tests__/media-reuse-story.e2e.test.ts +1 -1
- package/src/__tests__/migration-transport.test.ts +49 -0
- package/src/__tests__/notification-broadcaster.test.ts +205 -5
- package/src/__tests__/notification-deep-link.test.ts +365 -1
- package/src/__tests__/oauth-connect-handler.test.ts +2 -2
- package/src/__tests__/onboarding-starter-tasks.test.ts +17 -4
- package/src/__tests__/proxy-approval-callback.test.ts +1 -1
- package/src/__tests__/recording-handler.test.ts +1 -1
- package/src/__tests__/recording-intent-handler.test.ts +6 -1
- package/src/__tests__/recording-state-machine.test.ts +1 -1
- package/src/__tests__/relay-server.test.ts +9 -1
- package/src/__tests__/ride-shotgun-handler.test.ts +499 -0
- package/src/__tests__/runtime-attachment-metadata.test.ts +160 -1
- package/src/__tests__/script-proxy-injection-runtime.test.ts +299 -2
- package/src/__tests__/script-proxy-profile-template-fallback.test.ts +1 -1
- package/src/__tests__/secret-onetime-send.test.ts +8 -2
- package/src/__tests__/secure-keys.test.ts +175 -216
- package/src/__tests__/session-confirmation-signals.test.ts +1 -1
- package/src/__tests__/session-messaging-secret-redirect.test.ts +1 -1
- package/src/__tests__/session-queue.test.ts +2 -1
- package/src/__tests__/session-tool-setup-app-refresh.test.ts +2 -2
- package/src/__tests__/skill-feature-flags-integration.test.ts +29 -4
- package/src/__tests__/skill-feature-flags.test.ts +12 -9
- package/src/__tests__/skill-load-feature-flag.test.ts +26 -5
- package/src/__tests__/skill-projection.benchmark.test.ts +0 -1
- package/src/__tests__/skills.test.ts +34 -4
- package/src/__tests__/slack-channel-config.test.ts +2 -2
- package/src/__tests__/system-prompt.test.ts +26 -4
- package/src/__tests__/telegram-bot-username-resolution.test.ts +212 -0
- package/src/__tests__/telegram-invite-adapter.test.ts +164 -0
- package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +1 -1
- package/src/__tests__/tool-permission-simulate-handler.test.ts +8 -2
- package/src/__tests__/trusted-contact-approval-notifier.test.ts +9 -1
- package/src/__tests__/twitter-auth-handler.test.ts +2 -2
- package/src/__tests__/twitter-oauth-client.test.ts +1 -1
- package/src/__tests__/usage-routes.test.ts +339 -0
- package/src/__tests__/whatsapp-invite-adapter.test.ts +94 -0
- package/src/agent/loop.ts +3 -0
- package/src/amazon/checkout.ts +0 -1
- package/src/approvals/guardian-request-resolvers.ts +9 -1
- package/src/bundler/app-bundler.ts +28 -12
- package/src/bundler/bundle-scanner.ts +1 -1
- package/src/bundler/bundle-signer.ts +3 -3
- package/src/bundler/manifest.ts +1 -1
- package/src/bundler/signature-verifier.ts +3 -3
- package/src/channels/config.ts +1 -1
- package/src/cli/AGENTS.md +63 -0
- package/src/cli/__tests__/notifications.test.ts +470 -0
- package/src/cli/amazon.ts +344 -167
- package/src/cli/audit.ts +85 -0
- package/src/cli/autonomy.ts +369 -0
- package/src/cli/channels.ts +51 -0
- package/src/cli/completions.ts +208 -0
- package/src/cli/config.ts +220 -0
- package/src/cli/contacts.ts +471 -0
- package/src/cli/credentials.ts +564 -0
- package/src/cli/default-action.ts +14 -0
- package/src/cli/dev.ts +131 -0
- package/src/cli/doctor.ts +398 -0
- package/src/cli/email.ts +491 -0
- package/src/cli/influencer.ts +72 -0
- package/src/cli/integrations.ts +248 -57
- package/src/cli/keys.ts +114 -0
- package/src/cli/map.ts +46 -54
- package/src/cli/mcp.ts +111 -3
- package/src/cli/{config-commands.ts → memory.ts} +133 -242
- package/src/cli/notifications.ts +407 -0
- package/src/cli/program.ts +65 -0
- package/src/cli/reference.ts +48 -0
- package/src/cli/sequence.ts +154 -0
- package/src/cli/sessions.ts +262 -0
- package/src/cli/trust.ts +177 -0
- package/src/cli/twitter.ts +323 -106
- package/src/config/__tests__/build-cli-reference-section.test.ts +49 -0
- package/src/config/bundled-skills/amazon/SKILL.md +2 -2
- package/src/config/bundled-skills/app-builder/TOOLS.json +26 -0
- package/src/config/bundled-skills/app-builder/tools/app-generate-icon.ts +13 -0
- package/src/config/bundled-skills/contacts/SKILL.md +178 -10
- package/src/config/bundled-skills/doordash/doordash-cli.ts +23 -168
- package/src/config/bundled-skills/google-oauth-setup/SKILL.md +175 -145
- package/src/config/bundled-skills/messaging/tools/shared.ts +4 -1
- package/src/config/bundled-skills/twilio-setup/SKILL.md +70 -17
- package/src/config/bundled-tool-registry.ts +2 -0
- package/src/config/core-schema.ts +7 -0
- package/src/config/feature-flag-registry.json +16 -0
- package/src/config/loader.ts +26 -0
- package/src/config/schema.ts +4 -0
- package/src/config/skill-state.ts +0 -13
- package/src/config/system-prompt.ts +27 -0
- package/src/contacts/contact-store.ts +25 -0
- package/src/daemon/computer-use-session.ts +1 -1
- package/src/daemon/handlers/apps.ts +1 -0
- package/src/daemon/handlers/config-channels.ts +3 -3
- package/src/daemon/handlers/config-dispatch.ts +29 -0
- package/src/daemon/handlers/config-inbox.ts +4 -3
- package/src/daemon/handlers/config.ts +3 -43
- package/src/daemon/handlers/contacts.ts +34 -0
- package/src/daemon/handlers/index.ts +17 -3
- package/src/daemon/handlers/session-user-message.ts +7 -0
- package/src/daemon/handlers/sessions.ts +21 -2
- package/src/daemon/handlers/shared.ts +17 -0
- package/src/daemon/ipc-contract/apps.ts +2 -0
- package/src/daemon/ipc-contract/computer-use.ts +9 -0
- package/src/daemon/ipc-contract/contacts.ts +3 -3
- package/src/daemon/ipc-contract/inbox.ts +2 -0
- package/src/daemon/ipc-contract/messages.ts +4 -0
- package/src/daemon/ipc-contract/sessions.ts +8 -0
- package/src/daemon/ipc-contract-inventory.json +1 -0
- package/src/daemon/lifecycle.ts +0 -5
- package/src/daemon/ride-shotgun-handler.ts +139 -25
- package/src/daemon/session-agent-loop-handlers.ts +100 -0
- package/src/daemon/session-agent-loop.ts +72 -0
- package/src/daemon/session-tool-setup.ts +7 -0
- package/src/daemon/session.ts +23 -1
- package/src/daemon/tool-side-effects.ts +39 -1
- package/src/email/service.ts +59 -2
- package/src/index.ts +2 -60
- package/src/mcp/mcp-oauth-provider.ts +90 -8
- package/src/media/app-icon-generator.ts +86 -0
- package/src/memory/db-init.ts +12 -1
- package/src/memory/llm-usage-store.ts +186 -0
- package/src/memory/migrations/026-guardian-verification-sessions.ts +28 -9
- package/src/memory/migrations/027a-guardian-bootstrap-token.ts +16 -3
- package/src/memory/migrations/038-actor-token-records.ts +8 -1
- package/src/memory/migrations/039-actor-refresh-token-records.ts +11 -2
- package/src/memory/migrations/110-channel-guardian.ts +27 -6
- package/src/memory/migrations/112-assistant-inbox.ts +39 -15
- package/src/memory/migrations/114-notifications.ts +37 -15
- package/src/memory/migrations/117-conversation-attention.ts +33 -9
- package/src/memory/migrations/137-usage-dashboard-indexes.ts +26 -0
- package/src/memory/migrations/139-drop-usage-composite-indexes.ts +30 -0
- package/src/memory/migrations/index.ts +2 -0
- package/src/memory/migrations/schema-introspection.ts +18 -0
- package/src/memory/schema-migration.ts +1 -0
- package/src/memory/shared-app-links-store.ts +1 -1
- package/src/messaging/registry.ts +27 -0
- package/src/notifications/README.md +79 -70
- package/src/notifications/broadcaster.ts +2 -1
- package/src/notifications/conversation-pairing.ts +147 -13
- package/src/notifications/copy-composer.ts +7 -3
- package/src/notifications/destination-resolver.ts +14 -1
- package/src/notifications/emit-signal.ts +3 -2
- package/src/notifications/signal.ts +105 -1
- package/src/notifications/types.ts +16 -0
- package/src/permissions/checker.ts +29 -3
- package/src/permissions/prompter.ts +11 -3
- package/src/runtime/access-request-helper.ts +2 -1
- package/src/runtime/auth/route-policy.ts +7 -1
- package/src/runtime/channel-invite-transport.ts +40 -63
- package/src/runtime/channel-invite-transports/email.ts +13 -39
- package/src/runtime/channel-invite-transports/slack.ts +5 -34
- package/src/runtime/channel-invite-transports/sms.ts +8 -29
- package/src/runtime/channel-invite-transports/telegram.ts +69 -28
- package/src/runtime/channel-invite-transports/voice.ts +0 -7
- package/src/runtime/channel-invite-transports/whatsapp.ts +43 -0
- package/src/runtime/channel-readiness-service.ts +202 -45
- package/src/runtime/confirmation-request-guardian-bridge.ts +2 -1
- package/src/runtime/guardian-outbound-actions.ts +8 -5
- package/src/runtime/http-server.ts +5 -9
- package/src/runtime/http-types.ts +13 -1
- package/src/runtime/invite-instruction-generator.ts +178 -0
- package/src/runtime/invite-service.ts +22 -25
- package/src/runtime/migrations/migration-transport.ts +13 -0
- package/src/runtime/routes/app-routes.ts +1 -1
- package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +8 -7
- package/src/runtime/routes/channel-readiness-routes.ts +30 -11
- package/src/runtime/routes/contact-routes.ts +54 -26
- package/src/runtime/routes/guardian-bootstrap-routes.ts +1 -1
- package/src/runtime/routes/inbound-stages/bootstrap-intercept.ts +1 -1
- package/src/runtime/routes/inbound-stages/escalation-intercept.ts +2 -1
- package/src/runtime/routes/inbound-stages/verification-intercept.ts +2 -1
- package/src/runtime/routes/integration-routes.ts +1 -1
- package/src/runtime/routes/invite-routes.ts +1 -1
- package/src/runtime/routes/secret-routes.ts +31 -7
- package/src/runtime/routes/surface-content-routes.ts +104 -0
- package/src/runtime/routes/twilio-routes.ts +32 -1
- package/src/runtime/routes/usage-routes.ts +114 -0
- package/src/runtime/tool-grant-request-helper.ts +2 -1
- package/src/security/encrypted-store.ts +9 -5
- package/src/security/keychain-broker-client.ts +393 -0
- package/src/security/secure-keys.ts +106 -321
- package/src/tools/apps/executors.ts +73 -0
- package/src/tools/browser/auto-navigate.ts +15 -6
- package/src/tools/browser/chrome-cdp.ts +211 -0
- package/src/tools/browser/network-recorder.test.ts +83 -0
- package/src/tools/browser/network-recorder.ts +8 -7
- package/src/tools/browser/x-auto-navigate.ts +12 -6
- package/src/tools/credentials/policy-types.ts +24 -0
- package/src/tools/credentials/vault.ts +22 -27
- package/src/tools/network/script-proxy/session-manager.ts +47 -3
- package/src/tools/permission-checker.ts +1 -0
- package/src/tools/types.ts +2 -0
- package/src/tools/ui-surface/definitions.ts +1 -2
- package/src/tools/watch/watch-state.ts +2 -0
- package/src/__tests__/key-migration.test.ts +0 -240
- package/src/__tests__/keychain.test.ts +0 -286
- package/src/cli/core-commands.ts +0 -899
- package/src/security/keychain-to-encrypted-migration.ts +0 -66
- package/src/security/keychain.ts +0 -490
|
@@ -135,6 +135,7 @@ export function createToolExecutor(
|
|
|
135
135
|
name: string,
|
|
136
136
|
input: Record<string, unknown>,
|
|
137
137
|
onOutput?: (chunk: string) => void,
|
|
138
|
+
toolUseId?: string,
|
|
138
139
|
) => Promise<ToolExecutionResult> {
|
|
139
140
|
// Register the session's sendToClient for browser screencast surface messages
|
|
140
141
|
registerSessionSender(ctx.conversationId, (msg) => ctx.sendToClient(msg));
|
|
@@ -143,6 +144,7 @@ export function createToolExecutor(
|
|
|
143
144
|
name: string,
|
|
144
145
|
input: Record<string, unknown>,
|
|
145
146
|
onOutput?: (chunk: string) => void,
|
|
147
|
+
toolUseId?: string,
|
|
146
148
|
) => {
|
|
147
149
|
if (isDoordashCommand(name, input)) {
|
|
148
150
|
markDoordashStepInProgress(ctx, input);
|
|
@@ -172,6 +174,7 @@ export function createToolExecutor(
|
|
|
172
174
|
allowedToolNames: ctx.allowedToolNames,
|
|
173
175
|
memoryScopeId: ctx.memoryPolicy.scopeId,
|
|
174
176
|
forcePromptSideEffects: ctx.memoryPolicy.strictSideEffects,
|
|
177
|
+
toolUseId,
|
|
175
178
|
onToolLifecycleEvent: handleToolLifecycleEvent,
|
|
176
179
|
sendToClient: (msg) => {
|
|
177
180
|
// Tool context's sendToClient uses a loose { type: string; [key: string]: unknown }
|
|
@@ -255,6 +258,10 @@ export function createToolExecutor(
|
|
|
255
258
|
undefined,
|
|
256
259
|
ctx.conversationId,
|
|
257
260
|
req.executionTarget,
|
|
261
|
+
undefined,
|
|
262
|
+
undefined,
|
|
263
|
+
undefined,
|
|
264
|
+
toolUseId,
|
|
258
265
|
);
|
|
259
266
|
if (
|
|
260
267
|
(response.decision === "always_allow" ||
|
package/src/daemon/session.ts
CHANGED
|
@@ -222,6 +222,13 @@ export class Session {
|
|
|
222
222
|
* no-op for socketless sessions.
|
|
223
223
|
*/
|
|
224
224
|
private onStateSignal?: (msg: ServerMessage) => void;
|
|
225
|
+
/** Set by the agent loop to track confirmation outcomes for persistence. */
|
|
226
|
+
onConfirmationOutcome?: (
|
|
227
|
+
requestId: string,
|
|
228
|
+
state: string,
|
|
229
|
+
toolName?: string,
|
|
230
|
+
toolUseId?: string,
|
|
231
|
+
) => void;
|
|
225
232
|
|
|
226
233
|
constructor(
|
|
227
234
|
conversationId: string,
|
|
@@ -243,7 +250,7 @@ export class Session {
|
|
|
243
250
|
: { ...DEFAULT_MEMORY_POLICY };
|
|
244
251
|
this.traceEmitter = new TraceEmitter(conversationId, sendToClient);
|
|
245
252
|
this.prompter = new PermissionPrompter(sendToClient);
|
|
246
|
-
this.prompter.setOnStateChanged((requestId, state, source) => {
|
|
253
|
+
this.prompter.setOnStateChanged((requestId, state, source, toolUseId) => {
|
|
247
254
|
// Route through emitConfirmationStateChanged so the onStateSignal
|
|
248
255
|
// listener publishes to the SSE hub for HTTP/SSE consumers.
|
|
249
256
|
this.emitConfirmationStateChanged({
|
|
@@ -251,7 +258,11 @@ export class Session {
|
|
|
251
258
|
requestId,
|
|
252
259
|
state,
|
|
253
260
|
source,
|
|
261
|
+
toolUseId,
|
|
254
262
|
});
|
|
263
|
+
// Notify the agent loop so it can track requestId → toolUseId mappings
|
|
264
|
+
// and record confirmation outcomes for persistence.
|
|
265
|
+
this.onConfirmationOutcome?.(requestId, state, undefined, toolUseId);
|
|
255
266
|
// Emit activity state transitions for confirmation lifecycle
|
|
256
267
|
if (state === "pending") {
|
|
257
268
|
this.emitActivityState(
|
|
@@ -523,6 +534,9 @@ export class Session {
|
|
|
523
534
|
return;
|
|
524
535
|
}
|
|
525
536
|
|
|
537
|
+
// Capture toolUseId before resolving (resolution deletes the pending entry)
|
|
538
|
+
const toolUseId = this.prompter.getToolUseId(requestId);
|
|
539
|
+
|
|
526
540
|
this.prompter.resolveConfirmation(
|
|
527
541
|
requestId,
|
|
528
542
|
decision,
|
|
@@ -547,6 +561,7 @@ export class Session {
|
|
|
547
561
|
requestId,
|
|
548
562
|
state: resolvedState,
|
|
549
563
|
source: emissionContext?.source ?? "button",
|
|
564
|
+
toolUseId,
|
|
550
565
|
...(emissionContext?.causedByRequestId
|
|
551
566
|
? { causedByRequestId: emissionContext.causedByRequestId }
|
|
552
567
|
: {}),
|
|
@@ -554,6 +569,13 @@ export class Session {
|
|
|
554
569
|
? { decisionText: emissionContext.decisionText }
|
|
555
570
|
: {}),
|
|
556
571
|
});
|
|
572
|
+
// Notify the agent loop of the confirmation outcome for persistence
|
|
573
|
+
this.onConfirmationOutcome?.(
|
|
574
|
+
requestId,
|
|
575
|
+
resolvedState,
|
|
576
|
+
undefined,
|
|
577
|
+
toolUseId,
|
|
578
|
+
);
|
|
557
579
|
this.emitActivityState(
|
|
558
580
|
"thinking",
|
|
559
581
|
"confirmation_resolved",
|
|
@@ -9,14 +9,18 @@
|
|
|
9
9
|
|
|
10
10
|
import { join } from "node:path";
|
|
11
11
|
|
|
12
|
+
import { generateAppIcon } from "../media/app-icon-generator.js";
|
|
12
13
|
import { updatePublishedAppDeployment } from "../services/published-app-updater.js";
|
|
13
14
|
import type { ToolExecutionResult } from "../tools/types.js";
|
|
15
|
+
import { getLogger } from "../util/logger.js";
|
|
14
16
|
import { getWorkspaceDir } from "../util/platform.js";
|
|
15
17
|
import { isDoordashCommand, updateDoordashProgress } from "./doordash-steps.js";
|
|
16
18
|
import type { ServerMessage } from "./ipc-protocol.js";
|
|
17
19
|
import { refreshSurfacesForApp } from "./session-surfaces.js";
|
|
18
20
|
import type { ToolSetupContext } from "./session-tool-setup.js";
|
|
19
21
|
|
|
22
|
+
const log = getLogger("tool-side-effects");
|
|
23
|
+
|
|
20
24
|
// ── Types ────────────────────────────────────────────────────────────
|
|
21
25
|
|
|
22
26
|
export interface SideEffectContext {
|
|
@@ -65,13 +69,36 @@ function registerHook(
|
|
|
65
69
|
|
|
66
70
|
// Broadcast app_files_changed when a new app is created so clients
|
|
67
71
|
// (e.g. macOS "Things" sidebar) refresh their app list immediately.
|
|
72
|
+
// Also kicks off async icon generation via Gemini.
|
|
68
73
|
registerHook(
|
|
69
74
|
"app_create",
|
|
70
75
|
(_name, _input, result, { ctx, broadcastToAllClients }) => {
|
|
71
76
|
try {
|
|
72
|
-
const parsed = JSON.parse(result.content) as {
|
|
77
|
+
const parsed = JSON.parse(result.content) as {
|
|
78
|
+
id?: string;
|
|
79
|
+
name?: string;
|
|
80
|
+
description?: string;
|
|
81
|
+
};
|
|
73
82
|
if (parsed.id) {
|
|
74
83
|
handleAppChange(ctx, parsed.id, broadcastToAllClients);
|
|
84
|
+
|
|
85
|
+
// Fire-and-forget: generate an app icon in the background.
|
|
86
|
+
// When complete, broadcast again so clients pick up the new icon.
|
|
87
|
+
if (parsed.name) {
|
|
88
|
+
void generateAppIcon(parsed.id, parsed.name, parsed.description)
|
|
89
|
+
.then(() => {
|
|
90
|
+
broadcastToAllClients?.({
|
|
91
|
+
type: "app_files_changed",
|
|
92
|
+
appId: parsed.id!,
|
|
93
|
+
});
|
|
94
|
+
})
|
|
95
|
+
.catch((err) => {
|
|
96
|
+
log.warn(
|
|
97
|
+
{ err, appId: parsed.id },
|
|
98
|
+
"Background icon generation failed",
|
|
99
|
+
);
|
|
100
|
+
});
|
|
101
|
+
}
|
|
75
102
|
}
|
|
76
103
|
} catch {
|
|
77
104
|
// Result wasn't valid JSON — skip the broadcast.
|
|
@@ -79,6 +106,17 @@ registerHook(
|
|
|
79
106
|
},
|
|
80
107
|
);
|
|
81
108
|
|
|
109
|
+
// Broadcast app_files_changed when an icon is (re)generated so clients refresh.
|
|
110
|
+
registerHook(
|
|
111
|
+
"app_generate_icon",
|
|
112
|
+
(_name, input, _result, { broadcastToAllClients }) => {
|
|
113
|
+
const appId = input.app_id as string | undefined;
|
|
114
|
+
if (appId) {
|
|
115
|
+
broadcastToAllClients?.({ type: "app_files_changed", appId });
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
);
|
|
119
|
+
|
|
82
120
|
// Auto-refresh workspace surfaces when a persisted app is updated.
|
|
83
121
|
registerHook(
|
|
84
122
|
"app_update",
|
package/src/email/service.ts
CHANGED
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
setOutboundPaused,
|
|
18
18
|
} from "../cli/email-guardrails.js";
|
|
19
19
|
import {
|
|
20
|
+
getNestedValue,
|
|
20
21
|
loadRawConfig,
|
|
21
22
|
saveRawConfig,
|
|
22
23
|
setNestedValue,
|
|
@@ -74,6 +75,8 @@ export class EmailService {
|
|
|
74
75
|
/** Force re-creation of the provider (e.g. after `provider set`). */
|
|
75
76
|
resetProvider(): void {
|
|
76
77
|
this.providerInstance = null;
|
|
78
|
+
this.primaryAddressResolved = false;
|
|
79
|
+
this.cachedPrimaryAddress = undefined;
|
|
77
80
|
}
|
|
78
81
|
|
|
79
82
|
// =========================================================================
|
|
@@ -109,6 +112,54 @@ export class EmailService {
|
|
|
109
112
|
};
|
|
110
113
|
}
|
|
111
114
|
|
|
115
|
+
// =========================================================================
|
|
116
|
+
// Primary inbox address (cached)
|
|
117
|
+
// =========================================================================
|
|
118
|
+
|
|
119
|
+
private primaryAddressResolved = false;
|
|
120
|
+
private cachedPrimaryAddress: string | undefined;
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Return the assistant's primary inbox email address, caching the result
|
|
124
|
+
* for the lifetime of this service instance. Returns `undefined` when no
|
|
125
|
+
* inboxes are configured or the provider is unavailable.
|
|
126
|
+
*/
|
|
127
|
+
async getPrimaryInboxAddress(): Promise<string | undefined> {
|
|
128
|
+
if (this.primaryAddressResolved) {
|
|
129
|
+
return this.cachedPrimaryAddress;
|
|
130
|
+
}
|
|
131
|
+
try {
|
|
132
|
+
const p = await this.provider();
|
|
133
|
+
const health = await p.health();
|
|
134
|
+
this.cachedPrimaryAddress =
|
|
135
|
+
health.inboxes.length > 0 ? health.inboxes[0].address : undefined;
|
|
136
|
+
} catch {
|
|
137
|
+
this.cachedPrimaryAddress = undefined;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Only cache positive results from the provider so a missing inbox is
|
|
141
|
+
// retried on next call (e.g. user sets up email after initial miss).
|
|
142
|
+
if (this.cachedPrimaryAddress !== undefined) {
|
|
143
|
+
this.primaryAddressResolved = true;
|
|
144
|
+
return this.cachedPrimaryAddress;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Fall back to the statically configured email address in workspace config
|
|
148
|
+
// when the provider can't list inboxes (e.g. provider temporarily unavailable).
|
|
149
|
+
// Intentionally NOT setting primaryAddressResolved so the provider is retried
|
|
150
|
+
// on the next call — the fallback is a best-effort stopgap, not authoritative.
|
|
151
|
+
try {
|
|
152
|
+
const raw = loadRawConfig();
|
|
153
|
+
const configured = getNestedValue(raw, "email.address");
|
|
154
|
+
if (typeof configured === "string" && configured.length > 0) {
|
|
155
|
+
return configured;
|
|
156
|
+
}
|
|
157
|
+
} catch {
|
|
158
|
+
// Config unavailable — leave as undefined
|
|
159
|
+
}
|
|
160
|
+
return undefined;
|
|
161
|
+
}
|
|
162
|
+
|
|
112
163
|
// =========================================================================
|
|
113
164
|
// Domain setup
|
|
114
165
|
// =========================================================================
|
|
@@ -138,7 +189,10 @@ export class EmailService {
|
|
|
138
189
|
displayName?: string,
|
|
139
190
|
): Promise<EmailInbox> {
|
|
140
191
|
const p = await this.provider();
|
|
141
|
-
|
|
192
|
+
const inbox = await p.createInbox({ username, domain, displayName });
|
|
193
|
+
this.primaryAddressResolved = false;
|
|
194
|
+
this.cachedPrimaryAddress = undefined;
|
|
195
|
+
return inbox;
|
|
142
196
|
}
|
|
143
197
|
|
|
144
198
|
async listInboxes(): Promise<EmailInbox[]> {
|
|
@@ -148,7 +202,10 @@ export class EmailService {
|
|
|
148
202
|
|
|
149
203
|
async ensureInboxes(domain: string): Promise<EmailInbox[]> {
|
|
150
204
|
const p = await this.provider();
|
|
151
|
-
|
|
205
|
+
const inboxes = await p.ensureInboxes({ domain });
|
|
206
|
+
this.primaryAddressResolved = false;
|
|
207
|
+
this.cachedPrimaryAddress = undefined;
|
|
208
|
+
return inboxes;
|
|
152
209
|
}
|
|
153
210
|
|
|
154
211
|
// =========================================================================
|
package/src/index.ts
CHANGED
|
@@ -1,63 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { buildCliProgram } from "./cli/program.js";
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
const require = createRequire(import.meta.url);
|
|
8
|
-
const { version } = require("../package.json") as { version: string };
|
|
9
|
-
|
|
10
|
-
import { registerAmazonCommand } from "./cli/amazon.js";
|
|
11
|
-
import {
|
|
12
|
-
registerConfigCommand,
|
|
13
|
-
registerKeysCommand,
|
|
14
|
-
registerMemoryCommand,
|
|
15
|
-
registerTrustCommand,
|
|
16
|
-
} from "./cli/config-commands.js";
|
|
17
|
-
import {
|
|
18
|
-
registerAuditCommand,
|
|
19
|
-
registerCompletionsCommand,
|
|
20
|
-
registerDefaultAction,
|
|
21
|
-
registerDevCommand,
|
|
22
|
-
registerDoctorCommand,
|
|
23
|
-
registerSessionsCommand,
|
|
24
|
-
} from "./cli/core-commands.js";
|
|
25
|
-
import { registerEmailCommand } from "./cli/email.js";
|
|
26
|
-
import { registerInfluencerCommand } from "./cli/influencer.js";
|
|
27
|
-
import {
|
|
28
|
-
registerContactsCommand,
|
|
29
|
-
registerIntegrationsCommand,
|
|
30
|
-
} from "./cli/integrations.js";
|
|
31
|
-
import { registerMapCommand } from "./cli/map.js";
|
|
32
|
-
import { registerMcpCommand } from "./cli/mcp.js";
|
|
33
|
-
import { registerSequenceCommand } from "./cli/sequence.js";
|
|
34
|
-
import { registerTwitterCommand } from "./cli/twitter.js";
|
|
35
|
-
import { registerHooksCommand } from "./hooks/cli.js";
|
|
36
|
-
|
|
37
|
-
const program = new Command();
|
|
38
|
-
|
|
39
|
-
program.name("vellum").description("Local AI assistant").version(version);
|
|
40
|
-
|
|
41
|
-
registerDefaultAction(program);
|
|
42
|
-
registerDevCommand(program);
|
|
43
|
-
registerSessionsCommand(program);
|
|
44
|
-
registerConfigCommand(program);
|
|
45
|
-
registerKeysCommand(program);
|
|
46
|
-
registerTrustCommand(program);
|
|
47
|
-
registerMemoryCommand(program);
|
|
48
|
-
registerAuditCommand(program);
|
|
49
|
-
registerDoctorCommand(program);
|
|
50
|
-
registerHooksCommand(program);
|
|
51
|
-
registerMcpCommand(program);
|
|
52
|
-
registerEmailCommand(program);
|
|
53
|
-
registerIntegrationsCommand(program);
|
|
54
|
-
registerContactsCommand(program);
|
|
55
|
-
registerAmazonCommand(program);
|
|
56
|
-
registerCompletionsCommand(program);
|
|
57
|
-
|
|
58
|
-
registerTwitterCommand(program);
|
|
59
|
-
registerMapCommand(program);
|
|
60
|
-
registerInfluencerCommand(program);
|
|
61
|
-
registerSequenceCommand(program);
|
|
62
|
-
|
|
63
|
-
program.parse();
|
|
5
|
+
buildCliProgram().parse();
|
|
@@ -101,7 +101,17 @@ export class McpOAuthProvider implements OAuthClientProvider {
|
|
|
101
101
|
}
|
|
102
102
|
|
|
103
103
|
async saveTokens(tokens: OAuthTokens): Promise<void> {
|
|
104
|
-
await setSecureKeyAsync(
|
|
104
|
+
const ok = await setSecureKeyAsync(
|
|
105
|
+
tokensKey(this.serverId),
|
|
106
|
+
JSON.stringify(tokens),
|
|
107
|
+
);
|
|
108
|
+
if (!ok) {
|
|
109
|
+
log.warn(
|
|
110
|
+
{ serverId: this.serverId },
|
|
111
|
+
"Failed to persist OAuth tokens to secure storage",
|
|
112
|
+
);
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
105
115
|
log.info({ serverId: this.serverId }, "OAuth tokens saved");
|
|
106
116
|
}
|
|
107
117
|
|
|
@@ -124,7 +134,17 @@ export class McpOAuthProvider implements OAuthClientProvider {
|
|
|
124
134
|
async saveClientInformation(
|
|
125
135
|
info: OAuthClientInformationMixed,
|
|
126
136
|
): Promise<void> {
|
|
127
|
-
await setSecureKeyAsync(
|
|
137
|
+
const ok = await setSecureKeyAsync(
|
|
138
|
+
clientInfoKey(this.serverId),
|
|
139
|
+
JSON.stringify(info),
|
|
140
|
+
);
|
|
141
|
+
if (!ok) {
|
|
142
|
+
log.warn(
|
|
143
|
+
{ serverId: this.serverId },
|
|
144
|
+
"Failed to persist OAuth client information to secure storage",
|
|
145
|
+
);
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
128
148
|
log.info({ serverId: this.serverId }, "OAuth client information saved");
|
|
129
149
|
}
|
|
130
150
|
|
|
@@ -154,7 +174,16 @@ export class McpOAuthProvider implements OAuthClientProvider {
|
|
|
154
174
|
}
|
|
155
175
|
|
|
156
176
|
async saveDiscoveryState(state: OAuthDiscoveryState): Promise<void> {
|
|
157
|
-
await setSecureKeyAsync(
|
|
177
|
+
const ok = await setSecureKeyAsync(
|
|
178
|
+
discoveryKey(this.serverId),
|
|
179
|
+
JSON.stringify(state),
|
|
180
|
+
);
|
|
181
|
+
if (!ok) {
|
|
182
|
+
log.warn(
|
|
183
|
+
{ serverId: this.serverId },
|
|
184
|
+
"Failed to persist OAuth discovery state to secure storage",
|
|
185
|
+
);
|
|
186
|
+
}
|
|
158
187
|
}
|
|
159
188
|
|
|
160
189
|
// --- Redirect to Authorization ---
|
|
@@ -214,16 +243,49 @@ export class McpOAuthProvider implements OAuthClientProvider {
|
|
|
214
243
|
);
|
|
215
244
|
|
|
216
245
|
if (scope === "all" || scope === "tokens") {
|
|
217
|
-
await deleteSecureKeyAsync(tokensKey(this.serverId));
|
|
246
|
+
const result = await deleteSecureKeyAsync(tokensKey(this.serverId));
|
|
247
|
+
if (result === "error") {
|
|
248
|
+
log.warn(
|
|
249
|
+
{ serverId: this.serverId },
|
|
250
|
+
"Failed to delete OAuth tokens from secure storage",
|
|
251
|
+
);
|
|
252
|
+
} else if (result === "not-found") {
|
|
253
|
+
log.debug(
|
|
254
|
+
{ serverId: this.serverId },
|
|
255
|
+
"OAuth tokens key not found in secure storage (already removed)",
|
|
256
|
+
);
|
|
257
|
+
}
|
|
218
258
|
}
|
|
219
259
|
if (scope === "all" || scope === "client") {
|
|
220
|
-
await deleteSecureKeyAsync(clientInfoKey(this.serverId));
|
|
260
|
+
const result = await deleteSecureKeyAsync(clientInfoKey(this.serverId));
|
|
261
|
+
if (result === "error") {
|
|
262
|
+
log.warn(
|
|
263
|
+
{ serverId: this.serverId },
|
|
264
|
+
"Failed to delete OAuth client information from secure storage",
|
|
265
|
+
);
|
|
266
|
+
} else if (result === "not-found") {
|
|
267
|
+
log.debug(
|
|
268
|
+
{ serverId: this.serverId },
|
|
269
|
+
"OAuth client information key not found in secure storage (already removed)",
|
|
270
|
+
);
|
|
271
|
+
}
|
|
221
272
|
}
|
|
222
273
|
if (scope === "all" || scope === "verifier") {
|
|
223
274
|
this._codeVerifier = undefined;
|
|
224
275
|
}
|
|
225
276
|
if (scope === "all" || scope === "discovery") {
|
|
226
|
-
await deleteSecureKeyAsync(discoveryKey(this.serverId));
|
|
277
|
+
const result = await deleteSecureKeyAsync(discoveryKey(this.serverId));
|
|
278
|
+
if (result === "error") {
|
|
279
|
+
log.warn(
|
|
280
|
+
{ serverId: this.serverId },
|
|
281
|
+
"Failed to delete OAuth discovery state from secure storage",
|
|
282
|
+
);
|
|
283
|
+
} else if (result === "not-found") {
|
|
284
|
+
log.debug(
|
|
285
|
+
{ serverId: this.serverId },
|
|
286
|
+
"OAuth discovery state key not found in secure storage (already removed)",
|
|
287
|
+
);
|
|
288
|
+
}
|
|
227
289
|
}
|
|
228
290
|
}
|
|
229
291
|
|
|
@@ -373,12 +435,32 @@ export class McpOAuthProvider implements OAuthClientProvider {
|
|
|
373
435
|
export async function deleteMcpOAuthCredentials(
|
|
374
436
|
serverId: string,
|
|
375
437
|
): Promise<void> {
|
|
376
|
-
await Promise.all([
|
|
438
|
+
const [tokensResult, clientResult, discoveryResult] = await Promise.all([
|
|
377
439
|
deleteSecureKeyAsync(tokensKey(serverId)),
|
|
378
440
|
deleteSecureKeyAsync(clientInfoKey(serverId)),
|
|
379
441
|
deleteSecureKeyAsync(discoveryKey(serverId)),
|
|
380
442
|
]);
|
|
381
|
-
|
|
443
|
+
const results = [
|
|
444
|
+
{ key: "tokens", result: tokensResult },
|
|
445
|
+
{ key: "client_info", result: clientResult },
|
|
446
|
+
{ key: "discovery", result: discoveryResult },
|
|
447
|
+
];
|
|
448
|
+
const errors = results
|
|
449
|
+
.filter((r) => r.result === "error")
|
|
450
|
+
.map((r) => r.key);
|
|
451
|
+
if (errors.length > 0) {
|
|
452
|
+
log.warn(
|
|
453
|
+
{ serverId, failedKeys: errors },
|
|
454
|
+
"Some OAuth credentials could not be deleted from secure storage",
|
|
455
|
+
);
|
|
456
|
+
}
|
|
457
|
+
const hasErrors = errors.length > 0;
|
|
458
|
+
log.info(
|
|
459
|
+
{ serverId },
|
|
460
|
+
hasErrors
|
|
461
|
+
? "OAuth credential deletion completed with errors"
|
|
462
|
+
: "OAuth credentials deleted",
|
|
463
|
+
);
|
|
382
464
|
}
|
|
383
465
|
|
|
384
466
|
// --- HTML rendering ---
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generates app icons using the Gemini image generation service.
|
|
3
|
+
*
|
|
4
|
+
* Called as an async side-effect after app creation — never blocks
|
|
5
|
+
* the main app_create flow. Icons are saved to the app's directory
|
|
6
|
+
* as `icon.png` and included in .vellum bundles.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
|
|
12
|
+
import { getConfig } from "../config/loader.js";
|
|
13
|
+
import { getAppsDir } from "../memory/app-store.js";
|
|
14
|
+
import { getLogger } from "../util/logger.js";
|
|
15
|
+
import { generateImage, mapGeminiError } from "./gemini-image-service.js";
|
|
16
|
+
|
|
17
|
+
const log = getLogger("app-icon-generator");
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Generate an app icon and save it to `~/.vellum/apps/{appId}/icon.png`.
|
|
21
|
+
*
|
|
22
|
+
* Uses Gemini image generation when an API key is available.
|
|
23
|
+
* Silently no-ops if no key is configured or generation fails.
|
|
24
|
+
*/
|
|
25
|
+
export async function generateAppIcon(
|
|
26
|
+
appId: string,
|
|
27
|
+
appName: string,
|
|
28
|
+
appDescription?: string,
|
|
29
|
+
): Promise<void> {
|
|
30
|
+
const config = getConfig();
|
|
31
|
+
const apiKey = config.apiKeys.gemini ?? process.env.GEMINI_API_KEY;
|
|
32
|
+
if (!apiKey) {
|
|
33
|
+
log.debug("No Gemini API key — skipping app icon generation");
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const appDir = join(getAppsDir(), appId);
|
|
38
|
+
const iconPath = join(appDir, "icon.png");
|
|
39
|
+
|
|
40
|
+
// Don't regenerate if icon already exists
|
|
41
|
+
if (existsSync(iconPath)) {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const descPart = appDescription ? ` Description: ${appDescription}.` : "";
|
|
46
|
+
|
|
47
|
+
const prompt =
|
|
48
|
+
`Design a beautiful, minimal app icon for "${appName}".${descPart}\n\n` +
|
|
49
|
+
"Style requirements:\n" +
|
|
50
|
+
"- Square app icon with rounded corners (like macOS/iOS app icons)\n" +
|
|
51
|
+
"- Clean, flat design with a single bold symbol or glyph in the center\n" +
|
|
52
|
+
"- Rich gradient background using 2-3 harmonious colors\n" +
|
|
53
|
+
"- The symbol should be white or very light colored for contrast\n" +
|
|
54
|
+
"- No text, no letters, no words — only a symbolic glyph\n" +
|
|
55
|
+
"- Professional quality, recognizable at small sizes (32px)\n" +
|
|
56
|
+
"- Modern aesthetic similar to Apple's design language";
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
log.info({ appId, appName }, "Generating app icon via Gemini");
|
|
60
|
+
|
|
61
|
+
const result = await generateImage(apiKey, {
|
|
62
|
+
prompt,
|
|
63
|
+
mode: "generate",
|
|
64
|
+
model: config.imageGenModel,
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
if (result.images.length === 0) {
|
|
68
|
+
log.warn({ appId }, "Gemini returned no image for app icon");
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const image = result.images[0];
|
|
73
|
+
const pngBuffer = Buffer.from(image.dataBase64, "base64");
|
|
74
|
+
|
|
75
|
+
mkdirSync(appDir, { recursive: true });
|
|
76
|
+
writeFileSync(iconPath, pngBuffer);
|
|
77
|
+
|
|
78
|
+
log.info({ appId, iconPath }, "App icon saved");
|
|
79
|
+
} catch (error) {
|
|
80
|
+
const message = mapGeminiError(error);
|
|
81
|
+
log.warn(
|
|
82
|
+
{ appId, error: message },
|
|
83
|
+
"App icon generation failed — skipping",
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
}
|
package/src/memory/db-init.ts
CHANGED
|
@@ -36,7 +36,6 @@ import {
|
|
|
36
36
|
migrateBackfillContactInteractionStats,
|
|
37
37
|
migrateBackfillGuardianPrincipalId,
|
|
38
38
|
migrateCallSessionMode,
|
|
39
|
-
migrateDropAssistantIdColumns,
|
|
40
39
|
migrateCanonicalGuardianDeliveriesDestinationIndex,
|
|
41
40
|
migrateCanonicalGuardianRequesterChatId,
|
|
42
41
|
migrateChannelInboundDeliveredSegments,
|
|
@@ -46,7 +45,9 @@ import {
|
|
|
46
45
|
migrateContactsNotesColumn,
|
|
47
46
|
migrateContactsRolePrincipal,
|
|
48
47
|
migrateConversationsThreadTypeIndex,
|
|
48
|
+
migrateDropAssistantIdColumns,
|
|
49
49
|
migrateDropLegacyMemberGuardianTables,
|
|
50
|
+
migrateDropUsageCompositeIndexes,
|
|
50
51
|
migrateFkCascadeRebuilds,
|
|
51
52
|
migrateGuardianActionFollowup,
|
|
52
53
|
migrateGuardianActionSupersession,
|
|
@@ -63,6 +64,7 @@ import {
|
|
|
63
64
|
migrateNotificationDeliveryThreadDecision,
|
|
64
65
|
migrateReminderRoutingIntent,
|
|
65
66
|
migrateSchemaIndexesAndColumns,
|
|
67
|
+
migrateUsageDashboardIndexes,
|
|
66
68
|
migrateVoiceInviteColumns,
|
|
67
69
|
migrateVoiceInviteDisplayMetadata,
|
|
68
70
|
recoverCrashedMigrations,
|
|
@@ -293,6 +295,15 @@ export function initializeDb(): void {
|
|
|
293
295
|
// 40. Drop assistant_id columns from all 16 daemon tables
|
|
294
296
|
migrateDropAssistantIdColumns(database);
|
|
295
297
|
|
|
298
|
+
// 41. Indexes on llm_usage_events for usage dashboard time-range and breakdown queries
|
|
299
|
+
migrateUsageDashboardIndexes(database);
|
|
300
|
+
|
|
301
|
+
// 42. (skipped) migrateReorderUsageDashboardIndexes — superseded by 43 which drops
|
|
302
|
+
// all composite indexes that 42 would create, so running it is wasted work.
|
|
303
|
+
|
|
304
|
+
// 43. Drop all composite usage indexes — they don't eliminate temp B-trees for GROUP BY
|
|
305
|
+
migrateDropUsageCompositeIndexes(database);
|
|
306
|
+
|
|
296
307
|
validateMigrationState(database);
|
|
297
308
|
|
|
298
309
|
if (process.env.BUN_TEST === "1") {
|