@vellumai/assistant 0.4.35 → 0.4.37
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 +5 -2
- 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 +29 -0
- 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 +814 -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 +494 -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} +134 -245
- 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 +175 -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 +135 -34
- 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 +11 -0
- package/src/memory/llm-usage-store.ts +186 -0
- 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/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 +2 -0
- 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/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/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
|
@@ -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
|
@@ -47,6 +47,7 @@ import {
|
|
|
47
47
|
migrateConversationsThreadTypeIndex,
|
|
48
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") {
|
|
@@ -7,8 +7,13 @@ import type {
|
|
|
7
7
|
UsageEventInput,
|
|
8
8
|
} from "../usage/types.js";
|
|
9
9
|
import { getDb } from "./db.js";
|
|
10
|
+
import { rawAll } from "./raw-query.js";
|
|
10
11
|
import { llmUsageEvents } from "./schema.js";
|
|
11
12
|
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
// Write
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
|
|
12
17
|
export function recordUsageEvent(
|
|
13
18
|
input: UsageEventInput,
|
|
14
19
|
pricing: PricingResult,
|
|
@@ -43,6 +48,10 @@ export function recordUsageEvent(
|
|
|
43
48
|
return event;
|
|
44
49
|
}
|
|
45
50
|
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
// Read — single-event listing
|
|
53
|
+
// ---------------------------------------------------------------------------
|
|
54
|
+
|
|
46
55
|
export function listUsageEvents(options?: { limit?: number }): UsageEvent[] {
|
|
47
56
|
const db = getDb();
|
|
48
57
|
const rows = db
|
|
@@ -68,3 +77,180 @@ export function listUsageEvents(options?: { limit?: number }): UsageEvent[] {
|
|
|
68
77
|
pricingStatus: row.pricingStatus as "priced" | "unpriced",
|
|
69
78
|
}));
|
|
70
79
|
}
|
|
80
|
+
|
|
81
|
+
// ---------------------------------------------------------------------------
|
|
82
|
+
// Aggregation — time-range queries for the usage dashboard
|
|
83
|
+
// ---------------------------------------------------------------------------
|
|
84
|
+
|
|
85
|
+
/** Epoch-millis time range (inclusive on both ends). */
|
|
86
|
+
export interface UsageTimeRange {
|
|
87
|
+
from: number;
|
|
88
|
+
to: number;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Aggregate totals across a time range. */
|
|
92
|
+
export interface UsageTotals {
|
|
93
|
+
totalInputTokens: number;
|
|
94
|
+
totalOutputTokens: number;
|
|
95
|
+
totalCacheCreationTokens: number;
|
|
96
|
+
totalCacheReadTokens: number;
|
|
97
|
+
totalEstimatedCostUsd: number;
|
|
98
|
+
eventCount: number;
|
|
99
|
+
pricedEventCount: number;
|
|
100
|
+
unpricedEventCount: number;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** A single day bucket with its aggregate totals. */
|
|
104
|
+
export interface UsageDayBucket {
|
|
105
|
+
/** ISO date string (YYYY-MM-DD) in UTC. */
|
|
106
|
+
date: string;
|
|
107
|
+
totalInputTokens: number;
|
|
108
|
+
totalOutputTokens: number;
|
|
109
|
+
totalEstimatedCostUsd: number;
|
|
110
|
+
eventCount: number;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** A grouped breakdown row (by actor, provider, or model). */
|
|
114
|
+
export interface UsageGroupBreakdown {
|
|
115
|
+
group: string;
|
|
116
|
+
totalInputTokens: number;
|
|
117
|
+
totalOutputTokens: number;
|
|
118
|
+
totalEstimatedCostUsd: number;
|
|
119
|
+
eventCount: number;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// -- raw row shapes returned by SQLite aggregation queries --
|
|
123
|
+
|
|
124
|
+
interface TotalsRow {
|
|
125
|
+
total_input_tokens: number;
|
|
126
|
+
total_output_tokens: number;
|
|
127
|
+
total_cache_creation_tokens: number;
|
|
128
|
+
total_cache_read_tokens: number;
|
|
129
|
+
total_estimated_cost_usd: number | null;
|
|
130
|
+
event_count: number;
|
|
131
|
+
priced_event_count: number;
|
|
132
|
+
unpriced_event_count: number;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
interface DayBucketRow {
|
|
136
|
+
date: string;
|
|
137
|
+
total_input_tokens: number;
|
|
138
|
+
total_output_tokens: number;
|
|
139
|
+
total_estimated_cost_usd: number | null;
|
|
140
|
+
event_count: number;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
interface GroupRow {
|
|
144
|
+
group_key: string;
|
|
145
|
+
total_input_tokens: number;
|
|
146
|
+
total_output_tokens: number;
|
|
147
|
+
total_estimated_cost_usd: number | null;
|
|
148
|
+
event_count: number;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Return aggregate totals for all usage events within the given time range.
|
|
153
|
+
*/
|
|
154
|
+
export function getUsageTotals(range: UsageTimeRange): UsageTotals {
|
|
155
|
+
const rows = rawAll<TotalsRow>(
|
|
156
|
+
/*sql*/ `
|
|
157
|
+
SELECT
|
|
158
|
+
COALESCE(SUM(input_tokens), 0) AS total_input_tokens,
|
|
159
|
+
COALESCE(SUM(output_tokens), 0) AS total_output_tokens,
|
|
160
|
+
COALESCE(SUM(cache_creation_input_tokens), 0) AS total_cache_creation_tokens,
|
|
161
|
+
COALESCE(SUM(cache_read_input_tokens), 0) AS total_cache_read_tokens,
|
|
162
|
+
COALESCE(SUM(estimated_cost_usd), 0) AS total_estimated_cost_usd,
|
|
163
|
+
COUNT(*) AS event_count,
|
|
164
|
+
COUNT(CASE WHEN pricing_status = 'priced' THEN 1 END) AS priced_event_count,
|
|
165
|
+
COUNT(CASE WHEN pricing_status = 'unpriced' THEN 1 END) AS unpriced_event_count
|
|
166
|
+
FROM llm_usage_events
|
|
167
|
+
WHERE created_at >= ?1 AND created_at <= ?2
|
|
168
|
+
`,
|
|
169
|
+
range.from,
|
|
170
|
+
range.to,
|
|
171
|
+
);
|
|
172
|
+
const row = rows[0];
|
|
173
|
+
return {
|
|
174
|
+
totalInputTokens: row.total_input_tokens,
|
|
175
|
+
totalOutputTokens: row.total_output_tokens,
|
|
176
|
+
totalCacheCreationTokens: row.total_cache_creation_tokens,
|
|
177
|
+
totalCacheReadTokens: row.total_cache_read_tokens,
|
|
178
|
+
totalEstimatedCostUsd: row.total_estimated_cost_usd ?? 0,
|
|
179
|
+
eventCount: row.event_count,
|
|
180
|
+
pricedEventCount: row.priced_event_count,
|
|
181
|
+
unpricedEventCount: row.unpriced_event_count,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Return per-day aggregates (UTC) within the given time range, ordered by date ascending.
|
|
187
|
+
*
|
|
188
|
+
* Each bucket key is a YYYY-MM-DD string derived by dividing the epoch-millis
|
|
189
|
+
* timestamp by 86400000 and formatting as a date.
|
|
190
|
+
*/
|
|
191
|
+
export function getUsageDayBuckets(range: UsageTimeRange): UsageDayBucket[] {
|
|
192
|
+
const rows = rawAll<DayBucketRow>(
|
|
193
|
+
/*sql*/ `
|
|
194
|
+
SELECT
|
|
195
|
+
strftime('%Y-%m-%d', created_at / 1000, 'unixepoch') AS date,
|
|
196
|
+
COALESCE(SUM(input_tokens), 0) AS total_input_tokens,
|
|
197
|
+
COALESCE(SUM(output_tokens), 0) AS total_output_tokens,
|
|
198
|
+
COALESCE(SUM(estimated_cost_usd), 0) AS total_estimated_cost_usd,
|
|
199
|
+
COUNT(*) AS event_count
|
|
200
|
+
FROM llm_usage_events
|
|
201
|
+
WHERE created_at >= ?1 AND created_at <= ?2
|
|
202
|
+
GROUP BY date
|
|
203
|
+
ORDER BY date ASC
|
|
204
|
+
`,
|
|
205
|
+
range.from,
|
|
206
|
+
range.to,
|
|
207
|
+
);
|
|
208
|
+
return rows.map((r) => ({
|
|
209
|
+
date: r.date,
|
|
210
|
+
totalInputTokens: r.total_input_tokens,
|
|
211
|
+
totalOutputTokens: r.total_output_tokens,
|
|
212
|
+
totalEstimatedCostUsd: r.total_estimated_cost_usd ?? 0,
|
|
213
|
+
eventCount: r.event_count,
|
|
214
|
+
}));
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
type GroupByDimension = "actor" | "provider" | "model";
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Return grouped breakdowns across the given time range, ordered by total
|
|
221
|
+
* estimated cost descending (most expensive group first).
|
|
222
|
+
*/
|
|
223
|
+
export function getUsageGroupBreakdown(
|
|
224
|
+
range: UsageTimeRange,
|
|
225
|
+
groupBy: GroupByDimension,
|
|
226
|
+
): UsageGroupBreakdown[] {
|
|
227
|
+
// Runtime allowlist — defense-in-depth against SQL injection via type assertions.
|
|
228
|
+
const ALLOWED_COLUMNS = new Set<string>(["actor", "provider", "model"]);
|
|
229
|
+
if (!ALLOWED_COLUMNS.has(groupBy)) {
|
|
230
|
+
throw new Error(`Invalid groupBy column: ${groupBy}`);
|
|
231
|
+
}
|
|
232
|
+
const column = groupBy;
|
|
233
|
+
const rows = rawAll<GroupRow>(
|
|
234
|
+
/*sql*/ `
|
|
235
|
+
SELECT
|
|
236
|
+
${column} AS group_key,
|
|
237
|
+
COALESCE(SUM(input_tokens), 0) AS total_input_tokens,
|
|
238
|
+
COALESCE(SUM(output_tokens), 0) AS total_output_tokens,
|
|
239
|
+
COALESCE(SUM(estimated_cost_usd), 0) AS total_estimated_cost_usd,
|
|
240
|
+
COUNT(*) AS event_count
|
|
241
|
+
FROM llm_usage_events
|
|
242
|
+
WHERE created_at >= ?1 AND created_at <= ?2
|
|
243
|
+
GROUP BY ${column}
|
|
244
|
+
ORDER BY total_estimated_cost_usd DESC
|
|
245
|
+
`,
|
|
246
|
+
range.from,
|
|
247
|
+
range.to,
|
|
248
|
+
);
|
|
249
|
+
return rows.map((r) => ({
|
|
250
|
+
group: r.group_key,
|
|
251
|
+
totalInputTokens: r.total_input_tokens,
|
|
252
|
+
totalOutputTokens: r.total_output_tokens,
|
|
253
|
+
totalEstimatedCostUsd: r.total_estimated_cost_usd ?? 0,
|
|
254
|
+
eventCount: r.event_count,
|
|
255
|
+
}));
|
|
256
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { DrizzleDb } from "../db-connection.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Idempotent migration to add indexes on llm_usage_events for the
|
|
5
|
+
* time-range and breakdown queries the usage dashboard needs.
|
|
6
|
+
*
|
|
7
|
+
* - Covering index on (created_at) for efficient time-range scans.
|
|
8
|
+
* - Composite index on (actor, created_at) for per-actor breakdowns.
|
|
9
|
+
* - Composite index on (provider, model, created_at) for provider/model grouping.
|
|
10
|
+
*
|
|
11
|
+
* SUPERSEDED: The two composite indexes are dropped by migration 139.
|
|
12
|
+
* They don't accelerate grouped queries — SQLite still uses temp B-trees
|
|
13
|
+
* for GROUP BY regardless of index column order. Only the plain
|
|
14
|
+
* created_at index (kept) provides value for range scans.
|
|
15
|
+
*/
|
|
16
|
+
export function migrateUsageDashboardIndexes(database: DrizzleDb): void {
|
|
17
|
+
database.run(
|
|
18
|
+
/*sql*/ `CREATE INDEX IF NOT EXISTS idx_llm_usage_events_created_at ON llm_usage_events(created_at)`,
|
|
19
|
+
);
|
|
20
|
+
database.run(
|
|
21
|
+
/*sql*/ `CREATE INDEX IF NOT EXISTS idx_llm_usage_events_actor_created_at ON llm_usage_events(actor, created_at)`,
|
|
22
|
+
);
|
|
23
|
+
database.run(
|
|
24
|
+
/*sql*/ `CREATE INDEX IF NOT EXISTS idx_llm_usage_events_provider_model_created_at ON llm_usage_events(provider, model, created_at)`,
|
|
25
|
+
);
|
|
26
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { DrizzleDb } from "../db-connection.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Drop all composite indexes on llm_usage_events added by migrations 137
|
|
5
|
+
* and 138. EXPLAIN QUERY PLAN shows they provide no benefit: SQLite uses
|
|
6
|
+
* the created_at prefix for the range scan but still needs a temp B-tree
|
|
7
|
+
* for GROUP BY because the grouping column isn't contiguous after a range
|
|
8
|
+
* filter. For a local SQLite DB with typical usage volumes, the plain
|
|
9
|
+
* created_at index is sufficient and the temp B-tree overhead is negligible.
|
|
10
|
+
*
|
|
11
|
+
* The plain idx_llm_usage_events_created_at index (from migration 137)
|
|
12
|
+
* is intentionally kept — it genuinely helps range scans.
|
|
13
|
+
*/
|
|
14
|
+
export function migrateDropUsageCompositeIndexes(database: DrizzleDb): void {
|
|
15
|
+
// Migration 137 composites (may already be dropped by 138, hence IF EXISTS)
|
|
16
|
+
database.run(
|
|
17
|
+
/*sql*/ `DROP INDEX IF EXISTS idx_llm_usage_events_actor_created_at`,
|
|
18
|
+
);
|
|
19
|
+
database.run(
|
|
20
|
+
/*sql*/ `DROP INDEX IF EXISTS idx_llm_usage_events_provider_model_created_at`,
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
// Migration 138 composites
|
|
24
|
+
database.run(
|
|
25
|
+
/*sql*/ `DROP INDEX IF EXISTS idx_llm_usage_events_created_at_actor`,
|
|
26
|
+
);
|
|
27
|
+
database.run(
|
|
28
|
+
/*sql*/ `DROP INDEX IF EXISTS idx_llm_usage_events_created_at_provider_model`,
|
|
29
|
+
);
|
|
30
|
+
}
|
|
@@ -79,6 +79,8 @@ export { migrateAssistantContactMetadata } from "./133-assistant-contact-metadat
|
|
|
79
79
|
export { migrateContactsNotesColumn } from "./134-contacts-notes-column.js";
|
|
80
80
|
export { migrateBackfillContactInteractionStats } from "./135-backfill-contact-interaction-stats.js";
|
|
81
81
|
export { migrateDropAssistantIdColumns } from "./136-drop-assistant-id-columns.js";
|
|
82
|
+
export { migrateUsageDashboardIndexes } from "./137-usage-dashboard-indexes.js";
|
|
83
|
+
export { migrateDropUsageCompositeIndexes } from "./139-drop-usage-composite-indexes.js";
|
|
82
84
|
export {
|
|
83
85
|
MIGRATION_REGISTRY,
|
|
84
86
|
type MigrationRegistryEntry,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Store for cloud-shared app link records.
|
|
3
3
|
*
|
|
4
|
-
* Each record holds a .
|
|
4
|
+
* Each record holds a .vellum zip bundle keyed by a short, shareable token.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { randomBytes, randomUUID } from "node:crypto";
|
|
@@ -2,9 +2,21 @@
|
|
|
2
2
|
* Messaging provider registry — register/lookup providers by platform ID.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
import { isAssistantFeatureFlagEnabled } from "../config/assistant-feature-flags.js";
|
|
6
|
+
import { getConfig } from "../config/loader.js";
|
|
5
7
|
import { getSecureKey } from "../security/secure-keys.js";
|
|
6
8
|
import type { MessagingProvider } from "./provider.js";
|
|
7
9
|
|
|
10
|
+
/**
|
|
11
|
+
* Per-platform feature flag keys. Platforms not listed here are allowed
|
|
12
|
+
* by default (undeclared keys resolve to `true`).
|
|
13
|
+
*/
|
|
14
|
+
const PLATFORM_FLAG_KEYS: Record<string, string> = {
|
|
15
|
+
gmail: "feature_flags.messaging.gmail.enabled",
|
|
16
|
+
telegram: "feature_flags.messaging.telegram.enabled",
|
|
17
|
+
sms: "feature_flags.sms.enabled",
|
|
18
|
+
};
|
|
19
|
+
|
|
8
20
|
const providers = new Map<string, MessagingProvider>();
|
|
9
21
|
|
|
10
22
|
export function registerMessagingProvider(provider: MessagingProvider): void {
|
|
@@ -19,9 +31,24 @@ export function getMessagingProvider(id: string): MessagingProvider {
|
|
|
19
31
|
`Messaging provider "${id}" not found. Available: ${available}`,
|
|
20
32
|
);
|
|
21
33
|
}
|
|
34
|
+
assertPlatformEnabled(id);
|
|
22
35
|
return provider;
|
|
23
36
|
}
|
|
24
37
|
|
|
38
|
+
export function isPlatformEnabled(platformId: string): boolean {
|
|
39
|
+
const flagKey = PLATFORM_FLAG_KEYS[platformId];
|
|
40
|
+
if (!flagKey) return true;
|
|
41
|
+
return isAssistantFeatureFlagEnabled(flagKey, getConfig());
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function assertPlatformEnabled(platformId: string): void {
|
|
45
|
+
if (!isPlatformEnabled(platformId)) {
|
|
46
|
+
throw new Error(
|
|
47
|
+
`The ${platformId} platform is not enabled. Enable it in Settings > Features.`,
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
25
52
|
/** Return all registered providers that have stored credentials. */
|
|
26
53
|
export function getConnectedProviders(): MessagingProvider[] {
|
|
27
54
|
return Array.from(providers.values()).filter((p) => {
|