@vellumai/assistant 0.4.37 → 0.4.41
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/ARCHITECTURE.md +3 -3
- package/README.md +13 -13
- package/bun.lock +80 -24
- package/docs/architecture/integrations.md +126 -128
- package/docs/runbook-trusted-contacts.md +1 -1
- package/docs/trusted-contact-access.md +12 -12
- package/package.json +3 -1
- package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +0 -14
- package/src/__tests__/app-bundler.test.ts +209 -0
- package/src/__tests__/app-compiler.test.ts +279 -0
- package/src/__tests__/app-executors.test.ts +293 -483
- package/src/__tests__/app-migration.test.ts +148 -0
- package/src/__tests__/app-routes-csp.test.ts +202 -0
- package/src/__tests__/avatar-e2e.test.ts +452 -0
- package/src/__tests__/avatar-generator.test.ts +193 -0
- package/src/__tests__/avatar-router.test.ts +186 -0
- package/src/__tests__/browser-download-timeout.test.ts +28 -0
- package/src/__tests__/bundled-skill-retrieval-guard.test.ts +9 -9
- package/src/__tests__/call-domain.test.ts +3 -7
- package/src/__tests__/credential-security-e2e.test.ts +19 -12
- package/src/__tests__/credentials-cli.test.ts +30 -4
- package/src/__tests__/guardian-verify-setup-skill-regression.test.ts +1 -1
- package/src/__tests__/handlers-slack-config.test.ts +0 -72
- package/src/__tests__/handlers-telegram-config.test.ts +19 -12
- package/src/__tests__/handlers-twitter-config.test.ts +105 -48
- package/src/__tests__/inbound-invite-redemption.test.ts +4 -4
- package/src/__tests__/integration-status.test.ts +15 -5
- package/src/__tests__/integrations-cli.test.ts +1 -1
- package/src/__tests__/invite-redemption-service.test.ts +62 -7
- package/src/__tests__/ipc-snapshot.test.ts +0 -8
- package/src/__tests__/managed-avatar-client.test.ts +280 -0
- package/src/__tests__/mcp-cli.test.ts +3 -3
- package/src/__tests__/oauth-cli.test.ts +203 -0
- package/src/__tests__/relay-server.test.ts +3 -3
- package/src/__tests__/secret-onetime-send.test.ts +19 -12
- package/src/__tests__/secure-keys.test.ts +78 -0
- package/src/__tests__/session-messaging-secret-redirect.test.ts +3 -0
- package/src/__tests__/slack-channel-config.test.ts +23 -16
- package/src/__tests__/slack-share-routes.test.ts +263 -0
- package/src/__tests__/sms-messaging-provider.test.ts +3 -1
- package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +7 -7
- package/src/__tests__/trusted-contact-multichannel.test.ts +3 -3
- package/src/__tests__/trusted-contact-verification.test.ts +10 -10
- package/src/__tests__/twilio-config.test.ts +15 -36
- package/src/__tests__/twilio-provider.test.ts +4 -0
- package/src/__tests__/twitter-auth-handler.test.ts +27 -14
- package/src/__tests__/twitter-cli-error-shaping.test.ts +1 -1
- package/src/__tests__/twitter-cli-routing.test.ts +38 -53
- package/src/__tests__/twitter-oauth-client.test.ts +18 -47
- package/src/__tests__/voice-invite-redemption.test.ts +27 -3
- package/src/amazon/cart.ts +1 -1
- package/src/amazon/client.ts +89 -7
- package/src/approvals/guardian-request-resolvers.ts +2 -2
- package/src/bundler/app-bundler.ts +77 -32
- package/src/bundler/app-compiler.ts +195 -0
- package/src/bundler/manifest.ts +1 -1
- package/src/bundler/package-resolver.ts +185 -0
- package/src/calls/call-domain.ts +4 -14
- package/src/calls/relay-server.ts +2 -2
- package/src/calls/twilio-config.ts +5 -24
- package/src/calls/twilio-rest.ts +19 -5
- package/src/cli/amazon.ts +74 -249
- package/src/cli/audit.ts +2 -2
- package/src/cli/autonomy.ts +9 -9
- package/src/cli/channels.ts +5 -5
- package/src/cli/completions.ts +27 -27
- package/src/cli/config.ts +14 -14
- package/src/cli/contacts.ts +27 -27
- package/src/cli/credentials.ts +28 -28
- package/src/cli/dev.ts +2 -2
- package/src/cli/doctor.ts +2 -2
- package/src/cli/email.ts +82 -82
- package/src/cli/influencer.ts +13 -13
- package/src/cli/integrations.ts +19 -144
- package/src/cli/keys.ts +10 -10
- package/src/cli/map.ts +4 -4
- package/src/cli/mcp.ts +17 -17
- package/src/cli/memory.ts +18 -18
- package/src/cli/notifications.ts +13 -13
- package/src/cli/oauth.ts +77 -0
- package/src/cli/program.ts +2 -0
- package/src/cli/sequence.ts +27 -27
- package/src/cli/sessions.ts +12 -12
- package/src/cli/trust.ts +8 -8
- package/src/cli/twitter.ts +124 -70
- package/src/config/bundled-skills/_shared/CLI_RETRIEVAL_PATTERN.md +1 -1
- package/src/config/bundled-skills/agentmail/SKILL.md +34 -34
- package/src/config/bundled-skills/amazon/SKILL.md +54 -54
- package/src/config/bundled-skills/app-builder/SKILL.md +137 -3
- package/src/config/bundled-skills/app-builder/tools/app-create.ts +10 -4
- package/src/config/bundled-skills/configure-settings/SKILL.md +18 -18
- package/src/config/bundled-skills/contacts/SKILL.md +12 -12
- package/src/config/bundled-skills/doordash/lib/client.ts +7 -9
- package/src/config/bundled-skills/email-setup/SKILL.md +4 -4
- package/src/config/bundled-skills/frontend-design/icon.svg +16 -0
- package/src/config/bundled-skills/google-oauth-setup/SKILL.md +143 -162
- package/src/config/bundled-skills/guardian-verify-setup/SKILL.md +4 -4
- package/src/config/bundled-skills/influencer/SKILL.md +13 -13
- package/src/config/bundled-skills/mcp-setup/SKILL.md +11 -11
- package/src/config/bundled-skills/phone-calls/SKILL.md +48 -54
- package/src/config/bundled-skills/public-ingress/SKILL.md +6 -6
- package/src/config/bundled-skills/slack-app-setup/SKILL.md +1 -1
- package/src/config/bundled-skills/sms-setup/SKILL.md +3 -3
- package/src/config/bundled-skills/telegram-setup/SKILL.md +2 -2
- package/src/config/bundled-skills/twilio-setup/SKILL.md +136 -225
- package/src/config/bundled-skills/twitter/SKILL.md +68 -44
- package/src/config/bundled-skills/voice-setup/SKILL.md +2 -2
- package/src/config/core-schema.ts +26 -0
- package/src/config/env.ts +4 -0
- package/src/config/feature-flag-registry.json +9 -1
- package/src/config/schema.ts +8 -0
- package/src/config/system-prompt.ts +6 -3
- package/src/config/templates/BOOTSTRAP.md +7 -5
- package/src/contacts/contacts-write.ts +5 -1
- package/src/daemon/handlers/apps.ts +31 -4
- package/src/daemon/handlers/config-ingress.ts +3 -3
- package/src/daemon/handlers/config-integrations.ts +120 -49
- package/src/daemon/handlers/config-slack-channel.ts +26 -7
- package/src/daemon/handlers/config-slack.ts +1 -54
- package/src/daemon/handlers/config-telegram.ts +28 -10
- package/src/daemon/handlers/config.ts +1 -4
- package/src/daemon/handlers/twitter-auth.ts +11 -4
- package/src/daemon/ipc-contract/apps.ts +0 -13
- package/src/daemon/ipc-contract-inventory.json +0 -2
- package/src/daemon/lifecycle.ts +8 -1
- package/src/daemon/session-messaging.ts +2 -2
- package/src/daemon/tool-side-effects.ts +30 -0
- package/src/email/providers/agentmail.ts +1 -1
- package/src/email/providers/index.ts +1 -1
- package/src/email/service.ts +1 -1
- package/src/gallery/default-gallery.ts +538 -0
- package/src/gallery/gallery-manifest.ts +5 -1
- package/src/influencer/client.ts +8 -6
- package/src/mcp/client.ts +1 -1
- package/src/media/avatar-router.ts +99 -0
- package/src/media/avatar-types.ts +60 -0
- package/src/media/managed-avatar-client.ts +189 -0
- package/src/memory/app-migration.ts +114 -0
- package/src/memory/app-store.ts +11 -0
- package/src/memory/qdrant-client.ts +1 -1
- package/src/messaging/providers/slack/client.ts +12 -2
- package/src/messaging/providers/sms/adapter.ts +6 -10
- package/src/migrations/data-layout.ts +8 -1
- package/src/oauth/token-persistence.ts +9 -6
- package/src/runtime/assistant-scope.ts +5 -0
- package/src/runtime/auth/route-policy.ts +4 -0
- package/src/runtime/channel-readiness-service.ts +9 -4
- package/src/runtime/gateway-internal-client.ts +11 -3
- package/src/runtime/http-server.ts +2 -0
- package/src/runtime/invite-redemption-service.ts +23 -13
- package/src/runtime/middleware/twilio-validation.ts +2 -2
- package/src/runtime/routes/app-routes.ts +131 -3
- package/src/runtime/routes/inbound-stages/verification-intercept.ts +3 -3
- package/src/runtime/routes/integration-routes.ts +2 -2
- package/src/runtime/routes/slack-share-routes.ts +235 -0
- package/src/runtime/routes/twilio-routes.ts +47 -34
- package/src/schedule/integration-status.ts +2 -3
- package/src/security/token-manager.ts +11 -3
- package/src/tools/apps/executors.ts +116 -8
- package/src/tools/browser/browser-manager.ts +30 -2
- package/src/tools/browser/chrome-cdp.ts +31 -3
- package/src/tools/credentials/vault.ts +9 -7
- package/src/tools/executor.ts +4 -0
- package/src/tools/system/avatar-generator.ts +55 -34
- package/src/twitter/client.ts +1 -1
- package/src/twitter/oauth-client.ts +31 -43
- package/src/twitter/router.ts +25 -23
- package/src/util/platform.ts +5 -0
- package/src/slack/slack-webhook.ts +0 -66
package/src/amazon/client.ts
CHANGED
|
@@ -53,10 +53,14 @@ import type {
|
|
|
53
53
|
ExtensionResponse,
|
|
54
54
|
} from "../browser-extension-relay/protocol.js";
|
|
55
55
|
import { extensionRelayServer } from "../browser-extension-relay/server.js";
|
|
56
|
-
import {
|
|
56
|
+
import {
|
|
57
|
+
initAuthSigningKey,
|
|
58
|
+
isSigningKeyInitialized,
|
|
59
|
+
loadOrCreateSigningKey,
|
|
60
|
+
} from "../runtime/auth/token-service.js";
|
|
57
61
|
import { gatewayPost } from "../runtime/gateway-internal-client.js";
|
|
58
62
|
import type { ExtractedCredential } from "../tools/browser/network-recording-types.js";
|
|
59
|
-
import { type AmazonSession, loadSession } from "./session.js";
|
|
63
|
+
import { type AmazonSession, loadSession, saveSession } from "./session.js";
|
|
60
64
|
|
|
61
65
|
export const AMAZON_BASE = "https://www.amazon.com";
|
|
62
66
|
|
|
@@ -81,12 +85,10 @@ export async function sendRelayCommand(
|
|
|
81
85
|
|
|
82
86
|
// Fall back to HTTP relay endpoint via the gateway.
|
|
83
87
|
// The gateway validates edge JWTs (aud=vellum-gateway) and mints an
|
|
84
|
-
// exchange token for the runtime.
|
|
85
|
-
//
|
|
88
|
+
// exchange token for the runtime. In CLI out-of-process contexts the
|
|
89
|
+
// signing key may not be initialized yet — load it from disk.
|
|
86
90
|
if (!isSigningKeyInitialized()) {
|
|
87
|
-
|
|
88
|
-
"Auth signing key not initialized — browser-relay commands require the daemon to be running",
|
|
89
|
-
);
|
|
91
|
+
initAuthSigningKey(loadOrCreateSigningKey());
|
|
90
92
|
}
|
|
91
93
|
|
|
92
94
|
const { data } = await gatewayPost<ExtensionResponse>(
|
|
@@ -358,6 +360,86 @@ export interface PlaceOrderResult {
|
|
|
358
360
|
estimatedDelivery?: string;
|
|
359
361
|
}
|
|
360
362
|
|
|
363
|
+
// ---------------------------------------------------------------------------
|
|
364
|
+
// Session refresh via browser extension relay
|
|
365
|
+
// ---------------------------------------------------------------------------
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Refresh the Amazon session by grabbing cookies directly from Chrome
|
|
369
|
+
* via the browser extension relay's `get_cookies` action.
|
|
370
|
+
*
|
|
371
|
+
* Much faster than the CDP-based Ride Shotgun flow — no separate Chrome
|
|
372
|
+
* instance, no recording. Requires the extension to be loaded and connected.
|
|
373
|
+
*/
|
|
374
|
+
export async function refreshSessionFromExtension(): Promise<AmazonSession> {
|
|
375
|
+
const resp = await sendRelayCommand({
|
|
376
|
+
action: "get_cookies",
|
|
377
|
+
domain: "amazon.com",
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
if (!resp.success) {
|
|
381
|
+
throw new Error(
|
|
382
|
+
`Failed to get cookies from browser extension: ${resp.error ?? "unknown error"}`,
|
|
383
|
+
);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
const chromeCookies = resp.result as Array<{
|
|
387
|
+
name: string;
|
|
388
|
+
value: string;
|
|
389
|
+
domain: string;
|
|
390
|
+
path: string;
|
|
391
|
+
httpOnly: boolean;
|
|
392
|
+
secure: boolean;
|
|
393
|
+
expirationDate?: number;
|
|
394
|
+
}>;
|
|
395
|
+
|
|
396
|
+
if (!chromeCookies?.length) {
|
|
397
|
+
throw new Error(
|
|
398
|
+
"No Amazon cookies found in Chrome. " +
|
|
399
|
+
"Make sure you are signed into Amazon in Chrome.",
|
|
400
|
+
);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
const cookies: ExtractedCredential[] = chromeCookies.map((c) => ({
|
|
404
|
+
name: c.name,
|
|
405
|
+
value: c.value,
|
|
406
|
+
domain: c.domain,
|
|
407
|
+
path: c.path || "/",
|
|
408
|
+
httpOnly: c.httpOnly,
|
|
409
|
+
secure: c.secure,
|
|
410
|
+
expires: c.expirationDate ? Math.floor(c.expirationDate) : undefined,
|
|
411
|
+
}));
|
|
412
|
+
|
|
413
|
+
// Validate required cookies
|
|
414
|
+
const cookieNames = new Set(cookies.map((c) => c.name));
|
|
415
|
+
if (!cookieNames.has("session-id")) {
|
|
416
|
+
throw new Error(
|
|
417
|
+
"Chrome cookies are missing required Amazon cookie: session-id. " +
|
|
418
|
+
"Make sure you are signed into Amazon in Chrome.",
|
|
419
|
+
);
|
|
420
|
+
}
|
|
421
|
+
if (!cookieNames.has("ubid-main")) {
|
|
422
|
+
throw new Error(
|
|
423
|
+
"Chrome cookies are missing required Amazon cookie: ubid-main. " +
|
|
424
|
+
"Make sure you are signed into Amazon in Chrome.",
|
|
425
|
+
);
|
|
426
|
+
}
|
|
427
|
+
if (!cookieNames.has("at-main") && !cookieNames.has("x-main")) {
|
|
428
|
+
throw new Error(
|
|
429
|
+
"Chrome cookies are missing required Amazon auth cookie (at-main or x-main). " +
|
|
430
|
+
"Make sure you are fully signed into Amazon in Chrome.",
|
|
431
|
+
);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
const session: AmazonSession = {
|
|
435
|
+
cookies,
|
|
436
|
+
importedAt: new Date().toISOString(),
|
|
437
|
+
};
|
|
438
|
+
|
|
439
|
+
saveSession(session);
|
|
440
|
+
return session;
|
|
441
|
+
}
|
|
442
|
+
|
|
361
443
|
// ---------------------------------------------------------------------------
|
|
362
444
|
// Re-export public API from submodules
|
|
363
445
|
// ---------------------------------------------------------------------------
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
|
|
14
14
|
import { answerCall } from "../calls/call-domain.js";
|
|
15
15
|
import { getGatewayInternalBaseUrl } from "../config/env.js";
|
|
16
|
-
import {
|
|
16
|
+
import { upsertContactChannel } from "../contacts/contacts-write.js";
|
|
17
17
|
import {
|
|
18
18
|
type CanonicalGuardianRequest,
|
|
19
19
|
getCanonicalGuardianRequest,
|
|
@@ -459,7 +459,7 @@ const accessRequestResolver: GuardianRequestResolver = {
|
|
|
459
459
|
// relay server's in-call wait loop will detect the approved status.
|
|
460
460
|
if (channel === "voice") {
|
|
461
461
|
try {
|
|
462
|
-
|
|
462
|
+
upsertContactChannel({
|
|
463
463
|
sourceChannel: "voice",
|
|
464
464
|
externalUserId: requesterExternalUserId,
|
|
465
465
|
externalChatId: requesterChatId,
|
|
@@ -14,9 +14,10 @@ import { extname, join } from "node:path";
|
|
|
14
14
|
import archiver from "archiver";
|
|
15
15
|
import JSZip from "jszip";
|
|
16
16
|
|
|
17
|
-
import { getApp, getAppsDir } from "../memory/app-store.js";
|
|
17
|
+
import { getApp, getAppsDir, isMultifileApp } from "../memory/app-store.js";
|
|
18
18
|
import { computeContentId } from "../util/content-id.js";
|
|
19
19
|
import { getLogger } from "../util/logger.js";
|
|
20
|
+
import { compileApp } from "./app-compiler.js";
|
|
20
21
|
import type { SigningCallback } from "./bundle-signer.js";
|
|
21
22
|
import { signBundle } from "./bundle-signer.js";
|
|
22
23
|
import type { AppManifest } from "./manifest.js";
|
|
@@ -194,13 +195,15 @@ export async function packageApp(
|
|
|
194
195
|
throw new Error(`App not found: ${appId}`);
|
|
195
196
|
}
|
|
196
197
|
|
|
198
|
+
const multifile = isMultifileApp(app);
|
|
199
|
+
|
|
197
200
|
// Build manifest
|
|
198
201
|
const createdBy = `vellum-assistant/${PACKAGE_VERSION}`;
|
|
199
202
|
const version = app.version ?? "1.0.0";
|
|
200
203
|
const contentId = computeContentId(app.name);
|
|
201
204
|
|
|
202
205
|
const manifest: AppManifest = {
|
|
203
|
-
format_version: 1,
|
|
206
|
+
format_version: multifile ? 2 : 1,
|
|
204
207
|
name: app.name,
|
|
205
208
|
...(app.description ? { description: app.description } : {}),
|
|
206
209
|
...(app.icon ? { icon: app.icon } : {}),
|
|
@@ -213,28 +216,63 @@ export async function packageApp(
|
|
|
213
216
|
content_id: contentId,
|
|
214
217
|
};
|
|
215
218
|
|
|
216
|
-
//
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
// Also materialize assets in additional pages
|
|
219
|
+
// For multifile apps, compile first then bundle the dist/ output.
|
|
220
|
+
// For legacy apps, materialize remote assets and bundle the HTML directly.
|
|
221
|
+
let rewrittenHtml = "";
|
|
222
|
+
let allAssets: FetchedAsset[] = [];
|
|
222
223
|
const rewrittenPages: Record<string, string> = {};
|
|
223
|
-
const
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
224
|
+
const compiledFiles: { name: string; data: Buffer }[] = [];
|
|
225
|
+
|
|
226
|
+
if (multifile) {
|
|
227
|
+
const appDir = join(getAppsDir(), appId);
|
|
228
|
+
const compileResult = await compileApp(appDir);
|
|
229
|
+
if (!compileResult.ok) {
|
|
230
|
+
const messages = compileResult.errors
|
|
231
|
+
.map((e) => {
|
|
232
|
+
const loc = e.location
|
|
233
|
+
? ` (${e.location.file}:${e.location.line}:${e.location.column})`
|
|
234
|
+
: "";
|
|
235
|
+
return `${e.text}${loc}`;
|
|
236
|
+
})
|
|
237
|
+
.join("\n");
|
|
238
|
+
throw new Error(`Compilation failed for app "${app.name}":\n${messages}`);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const distDir = join(appDir, "dist");
|
|
242
|
+
const indexHtml = await readFile(join(distDir, "index.html"), "utf-8");
|
|
243
|
+
const mainJs = await readFile(join(distDir, "main.js"));
|
|
244
|
+
|
|
245
|
+
compiledFiles.push({ name: "index.html", data: Buffer.from(indexHtml) });
|
|
246
|
+
compiledFiles.push({ name: "main.js", data: mainJs });
|
|
247
|
+
|
|
248
|
+
// main.css is optional — only produced when the app imports CSS
|
|
249
|
+
const cssPath = join(distDir, "main.css");
|
|
250
|
+
if (existsSync(cssPath)) {
|
|
251
|
+
compiledFiles.push({ name: "main.css", data: await readFile(cssPath) });
|
|
252
|
+
}
|
|
253
|
+
} else {
|
|
254
|
+
// Legacy path: fetch remote assets and rewrite HTML
|
|
255
|
+
const materialized = await materializeAssets(app.htmlDefinition);
|
|
256
|
+
rewrittenHtml = materialized.rewrittenHtml;
|
|
257
|
+
const fetchedAssets = materialized.assets;
|
|
258
|
+
|
|
259
|
+
// Also materialize assets in additional pages
|
|
260
|
+
const pageAssets: FetchedAsset[] = [];
|
|
261
|
+
if (app.pages) {
|
|
262
|
+
for (const [filename, content] of Object.entries(app.pages)) {
|
|
263
|
+
const result = await materializeAssets(content);
|
|
264
|
+
rewrittenPages[filename] = result.rewrittenHtml;
|
|
265
|
+
pageAssets.push(...result.assets);
|
|
266
|
+
}
|
|
229
267
|
}
|
|
230
|
-
}
|
|
231
268
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
269
|
+
// Deduplicate assets by archive path
|
|
270
|
+
const allAssetsMap = new Map<string, FetchedAsset>();
|
|
271
|
+
for (const asset of [...fetchedAssets, ...pageAssets]) {
|
|
272
|
+
allAssetsMap.set(asset.archivePath, asset);
|
|
273
|
+
}
|
|
274
|
+
allAssets = [...allAssetsMap.values()];
|
|
236
275
|
}
|
|
237
|
-
const allAssets = [...allAssetsMap.values()];
|
|
238
276
|
|
|
239
277
|
// Create the zip archive
|
|
240
278
|
const safeName = app.name.replace(/[/\\:*?"<>|]/g, "_").trim() || "App";
|
|
@@ -264,20 +302,27 @@ export async function packageApp(
|
|
|
264
302
|
// Add manifest.json at root level
|
|
265
303
|
archive.append(serializeManifest(manifest), { name: "manifest.json" });
|
|
266
304
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
305
|
+
if (multifile) {
|
|
306
|
+
// Add compiled dist/ files
|
|
307
|
+
for (const file of compiledFiles) {
|
|
308
|
+
archive.append(file.data, { name: file.name });
|
|
309
|
+
}
|
|
310
|
+
} else {
|
|
311
|
+
// Add index.html at root level
|
|
312
|
+
archive.append(rewrittenHtml, { name: "index.html" });
|
|
313
|
+
|
|
314
|
+
// Add additional pages alongside index.html (with rewritten asset URLs)
|
|
315
|
+
if (app.pages) {
|
|
316
|
+
for (const filename of Object.keys(app.pages)) {
|
|
317
|
+
const content = rewrittenPages[filename] ?? app.pages[filename];
|
|
318
|
+
archive.append(content, { name: filename });
|
|
319
|
+
}
|
|
275
320
|
}
|
|
276
|
-
}
|
|
277
321
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
322
|
+
// Add fetched remote assets
|
|
323
|
+
for (const asset of allAssets) {
|
|
324
|
+
archive.append(asset.data, { name: asset.archivePath });
|
|
325
|
+
}
|
|
281
326
|
}
|
|
282
327
|
|
|
283
328
|
// Include app icon if one was generated
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* esbuild wrapper for compiling multi-file TSX apps.
|
|
3
|
+
*
|
|
4
|
+
* Compiles src/main.tsx → dist/main.js, copies index.html with
|
|
5
|
+
* script/style tag injection, and returns structured diagnostics.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { existsSync, rmSync } from "node:fs";
|
|
9
|
+
import { mkdir, readdir, readFile, writeFile } from "node:fs/promises";
|
|
10
|
+
import { join, resolve } from "node:path";
|
|
11
|
+
|
|
12
|
+
import { build, type Message, type Plugin } from "esbuild";
|
|
13
|
+
|
|
14
|
+
import { getLogger } from "../util/logger.js";
|
|
15
|
+
import {
|
|
16
|
+
ALLOWED_PACKAGES,
|
|
17
|
+
getCacheDir,
|
|
18
|
+
isBareImport,
|
|
19
|
+
packageName,
|
|
20
|
+
resolvePackage,
|
|
21
|
+
} from "./package-resolver.js";
|
|
22
|
+
|
|
23
|
+
const log = getLogger("app-compiler");
|
|
24
|
+
|
|
25
|
+
export interface CompileDiagnostic {
|
|
26
|
+
text: string;
|
|
27
|
+
location?: { file: string; line: number; column: number };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface CompileResult {
|
|
31
|
+
ok: boolean;
|
|
32
|
+
errors: CompileDiagnostic[];
|
|
33
|
+
warnings: CompileDiagnostic[];
|
|
34
|
+
durationMs: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function mapDiagnostics(messages: Message[]): CompileDiagnostic[] {
|
|
38
|
+
return messages.map((msg) => ({
|
|
39
|
+
text: msg.text,
|
|
40
|
+
...(msg.location
|
|
41
|
+
? {
|
|
42
|
+
location: {
|
|
43
|
+
file: msg.location.file,
|
|
44
|
+
line: msg.location.line,
|
|
45
|
+
column: msg.location.column,
|
|
46
|
+
},
|
|
47
|
+
}
|
|
48
|
+
: {}),
|
|
49
|
+
}));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Compile a TSX app from appDir/src/ into appDir/dist/.
|
|
54
|
+
*
|
|
55
|
+
* Expects appDir/src/main.tsx as the entry point and appDir/src/index.html
|
|
56
|
+
* as the HTML shell. Produces appDir/dist/main.js and appDir/dist/index.html
|
|
57
|
+
* (with script and optional stylesheet tags injected).
|
|
58
|
+
*/
|
|
59
|
+
export async function compileApp(appDir: string): Promise<CompileResult> {
|
|
60
|
+
const start = performance.now();
|
|
61
|
+
const srcDir = join(appDir, "src");
|
|
62
|
+
const distDir = join(appDir, "dist");
|
|
63
|
+
const entryPoint = join(srcDir, "main.tsx");
|
|
64
|
+
|
|
65
|
+
// Clear stale dist/ output so removed assets (e.g. CSS) don't persist
|
|
66
|
+
if (existsSync(distDir)) {
|
|
67
|
+
rmSync(distDir, { recursive: true, force: true });
|
|
68
|
+
}
|
|
69
|
+
await mkdir(distDir, { recursive: true });
|
|
70
|
+
|
|
71
|
+
// Resolve preact from the assistant's own node_modules so per-app
|
|
72
|
+
// directories don't need their own copy.
|
|
73
|
+
const preactDir = resolve(
|
|
74
|
+
import.meta.dirname ?? __dirname,
|
|
75
|
+
"../../node_modules/preact",
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
// Plugin that resolves bare third-party imports against the allowlist
|
|
79
|
+
const resolvePlugin: Plugin = {
|
|
80
|
+
name: "vellum-package-resolver",
|
|
81
|
+
setup(pluginBuild) {
|
|
82
|
+
pluginBuild.onResolve({ filter: /.*/ }, async (args) => {
|
|
83
|
+
// Only intercept bare specifiers (not relative, not preact/react aliases)
|
|
84
|
+
if (
|
|
85
|
+
args.kind !== "import-statement" &&
|
|
86
|
+
args.kind !== "dynamic-import"
|
|
87
|
+
) {
|
|
88
|
+
return undefined;
|
|
89
|
+
}
|
|
90
|
+
if (!isBareImport(args.path)) return undefined;
|
|
91
|
+
|
|
92
|
+
const pkg = packageName(args.path);
|
|
93
|
+
const nodeModulesDir = await resolvePackage(pkg);
|
|
94
|
+
|
|
95
|
+
if (nodeModulesDir) {
|
|
96
|
+
// Let esbuild resolve normally — nodePaths will pick it up
|
|
97
|
+
return undefined;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Not allowed — produce a clear error
|
|
101
|
+
return {
|
|
102
|
+
errors: [
|
|
103
|
+
{
|
|
104
|
+
text: `Package '${pkg}' is not in the allowed list. Allowed: ${ALLOWED_PACKAGES.join(", ")}`,
|
|
105
|
+
},
|
|
106
|
+
],
|
|
107
|
+
};
|
|
108
|
+
});
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const cacheNodeModules = join(getCacheDir(), "node_modules");
|
|
113
|
+
|
|
114
|
+
let result;
|
|
115
|
+
try {
|
|
116
|
+
result = await build({
|
|
117
|
+
entryPoints: [entryPoint],
|
|
118
|
+
bundle: true,
|
|
119
|
+
minify: true,
|
|
120
|
+
sourcemap: false,
|
|
121
|
+
outdir: distDir,
|
|
122
|
+
format: "esm",
|
|
123
|
+
target: ["es2022"],
|
|
124
|
+
jsx: "automatic",
|
|
125
|
+
jsxImportSource: "preact",
|
|
126
|
+
loader: {
|
|
127
|
+
".tsx": "tsx",
|
|
128
|
+
".ts": "ts",
|
|
129
|
+
".jsx": "jsx",
|
|
130
|
+
".js": "js",
|
|
131
|
+
".css": "css",
|
|
132
|
+
},
|
|
133
|
+
alias: {
|
|
134
|
+
react: "preact/compat",
|
|
135
|
+
"react-dom": "preact/compat",
|
|
136
|
+
},
|
|
137
|
+
plugins: [resolvePlugin],
|
|
138
|
+
// Point esbuild at assistant's preact and at the shared package cache
|
|
139
|
+
nodePaths: [resolve(preactDir, ".."), cacheNodeModules],
|
|
140
|
+
logLevel: "silent",
|
|
141
|
+
});
|
|
142
|
+
} catch (err: unknown) {
|
|
143
|
+
// esbuild throws on hard failures (e.g. syntax errors) with .errors/.warnings
|
|
144
|
+
const esbuildErr = err as {
|
|
145
|
+
errors?: Message[];
|
|
146
|
+
warnings?: Message[];
|
|
147
|
+
};
|
|
148
|
+
const durationMs = Math.round(performance.now() - start);
|
|
149
|
+
const errors = mapDiagnostics(esbuildErr.errors ?? []);
|
|
150
|
+
const warnings = mapDiagnostics(esbuildErr.warnings ?? []);
|
|
151
|
+
log.info({ durationMs, errorCount: errors.length }, "Build failed");
|
|
152
|
+
return { ok: false, errors, warnings, durationMs };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const errors = mapDiagnostics(result.errors);
|
|
156
|
+
const warnings = mapDiagnostics(result.warnings);
|
|
157
|
+
|
|
158
|
+
if (errors.length > 0) {
|
|
159
|
+
const durationMs = Math.round(performance.now() - start);
|
|
160
|
+
log.info({ durationMs, errorCount: errors.length }, "Build failed");
|
|
161
|
+
return { ok: false, errors, warnings, durationMs };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Copy index.html and inject script/style tags
|
|
165
|
+
const htmlSrc = join(srcDir, "index.html");
|
|
166
|
+
if (existsSync(htmlSrc)) {
|
|
167
|
+
let html = await readFile(htmlSrc, "utf-8");
|
|
168
|
+
|
|
169
|
+
// Check if CSS output was produced
|
|
170
|
+
const distFiles = await readdir(distDir);
|
|
171
|
+
const hasCss = distFiles.some((f) => f.endsWith(".css"));
|
|
172
|
+
|
|
173
|
+
// Inject stylesheet link into <head> if CSS exists and not already present
|
|
174
|
+
if (hasCss && !html.includes('href="main.css"')) {
|
|
175
|
+
html = html.replace(
|
|
176
|
+
"</head>",
|
|
177
|
+
' <link rel="stylesheet" href="main.css">\n </head>',
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Inject script tag before </body> if not already present
|
|
182
|
+
if (!html.includes('src="main.js"')) {
|
|
183
|
+
html = html.replace(
|
|
184
|
+
"</body>",
|
|
185
|
+
' <script type="module" src="main.js"></script>\n </body>',
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
await writeFile(join(distDir, "index.html"), html);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const durationMs = Math.round(performance.now() - start);
|
|
193
|
+
log.info({ durationMs }, "Build succeeded");
|
|
194
|
+
return { ok: true, errors, warnings, durationMs };
|
|
195
|
+
}
|
package/src/bundler/manifest.ts
CHANGED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Third-party package resolver with an allowlist for app builds.
|
|
3
|
+
*
|
|
4
|
+
* Maintains a shared cache at ~/.vellum/package-cache/ so packages are
|
|
5
|
+
* installed once and reused across all app compilations.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { existsSync } from "node:fs";
|
|
9
|
+
import { mkdir, stat } from "node:fs/promises";
|
|
10
|
+
import { homedir } from "node:os";
|
|
11
|
+
import { join } from "node:path";
|
|
12
|
+
|
|
13
|
+
import { getLogger } from "../util/logger.js";
|
|
14
|
+
|
|
15
|
+
const log = getLogger("package-resolver");
|
|
16
|
+
|
|
17
|
+
/** Packages the model is likely to use and that we trust in sandboxed apps. */
|
|
18
|
+
export const ALLOWED_PACKAGES: readonly string[] = [
|
|
19
|
+
"date-fns",
|
|
20
|
+
"chart.js",
|
|
21
|
+
"lodash-es",
|
|
22
|
+
"zod",
|
|
23
|
+
"clsx",
|
|
24
|
+
"lucide",
|
|
25
|
+
] as const;
|
|
26
|
+
|
|
27
|
+
const INSTALL_TIMEOUT_MS = 10_000;
|
|
28
|
+
const MAX_PACKAGE_SIZE_BYTES = 5 * 1024 * 1024; // 5 MB
|
|
29
|
+
|
|
30
|
+
/** In-flight install promises keyed by package name, to deduplicate concurrent requests. */
|
|
31
|
+
const inflight = new Map<string, Promise<string | null>>();
|
|
32
|
+
|
|
33
|
+
/** Where all cached packages live on disk. */
|
|
34
|
+
export function getCacheDir(): string {
|
|
35
|
+
return join(homedir(), ".vellum", "package-cache");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Return true when `name` is a bare specifier that our plugin should handle
|
|
40
|
+
* (i.e. not a relative/absolute path, not preact/react which are aliased).
|
|
41
|
+
*/
|
|
42
|
+
export function isBareImport(name: string): boolean {
|
|
43
|
+
if (name.startsWith(".") || name.startsWith("/")) return false;
|
|
44
|
+
if (
|
|
45
|
+
name.startsWith("preact") ||
|
|
46
|
+
name.startsWith("react") ||
|
|
47
|
+
name.startsWith("react-dom")
|
|
48
|
+
) {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Get the top-level package name from a specifier (handles scoped pkgs). */
|
|
55
|
+
export function packageName(specifier: string): string {
|
|
56
|
+
if (specifier.startsWith("@")) {
|
|
57
|
+
const parts = specifier.split("/");
|
|
58
|
+
return parts.slice(0, 2).join("/");
|
|
59
|
+
}
|
|
60
|
+
return specifier.split("/")[0];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Resolve a third-party package from the shared cache.
|
|
65
|
+
*
|
|
66
|
+
* Returns the path to the package inside node_modules, or null if the
|
|
67
|
+
* package is not allowed or installation failed.
|
|
68
|
+
*/
|
|
69
|
+
export async function resolvePackage(name: string): Promise<string | null> {
|
|
70
|
+
const pkg = packageName(name);
|
|
71
|
+
|
|
72
|
+
if (!ALLOWED_PACKAGES.includes(pkg)) {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const cacheDir = getCacheDir();
|
|
77
|
+
const nodeModulesDir = join(cacheDir, "node_modules");
|
|
78
|
+
const pkgDir = join(nodeModulesDir, pkg);
|
|
79
|
+
|
|
80
|
+
// Already cached — skip install
|
|
81
|
+
if (existsSync(pkgDir)) {
|
|
82
|
+
return nodeModulesDir;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Deduplicate concurrent install requests for the same package
|
|
86
|
+
const existing = inflight.get(pkg);
|
|
87
|
+
if (existing) {
|
|
88
|
+
return existing;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const promise = installPackage(pkg, cacheDir, nodeModulesDir, pkgDir);
|
|
92
|
+
inflight.set(pkg, promise);
|
|
93
|
+
try {
|
|
94
|
+
return await promise;
|
|
95
|
+
} finally {
|
|
96
|
+
inflight.delete(pkg);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function installPackage(
|
|
101
|
+
pkg: string,
|
|
102
|
+
cacheDir: string,
|
|
103
|
+
nodeModulesDir: string,
|
|
104
|
+
pkgDir: string,
|
|
105
|
+
): Promise<string | null> {
|
|
106
|
+
// Ensure cache directory exists with a package.json so bun install works
|
|
107
|
+
await mkdir(cacheDir, { recursive: true });
|
|
108
|
+
const pkgJsonPath = join(cacheDir, "package.json");
|
|
109
|
+
if (!existsSync(pkgJsonPath)) {
|
|
110
|
+
const { writeFile } = await import("node:fs/promises");
|
|
111
|
+
await writeFile(
|
|
112
|
+
pkgJsonPath,
|
|
113
|
+
JSON.stringify({ name: "vellum-pkg-cache", private: true }),
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
log.info({ pkg }, "Installing package into shared cache");
|
|
118
|
+
|
|
119
|
+
try {
|
|
120
|
+
const proc = Bun.spawn(["bun", "install", "--no-save", pkg], {
|
|
121
|
+
cwd: cacheDir,
|
|
122
|
+
stdout: "pipe",
|
|
123
|
+
stderr: "pipe",
|
|
124
|
+
env: {
|
|
125
|
+
...process.env,
|
|
126
|
+
PATH: `${homedir()}/.bun/bin:${process.env.PATH}`,
|
|
127
|
+
},
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
// Race against timeout
|
|
131
|
+
const exited = proc.exited;
|
|
132
|
+
let timer: ReturnType<typeof setTimeout>;
|
|
133
|
+
const timeout = new Promise<"timeout">((resolve) => {
|
|
134
|
+
timer = setTimeout(() => resolve("timeout"), INSTALL_TIMEOUT_MS);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
const raceResult = await Promise.race([exited, timeout]);
|
|
138
|
+
clearTimeout(timer!);
|
|
139
|
+
|
|
140
|
+
if (raceResult === "timeout") {
|
|
141
|
+
proc.kill();
|
|
142
|
+
log.warn({ pkg }, "Package install timed out");
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (raceResult !== 0) {
|
|
147
|
+
const stderr = await new Response(proc.stderr).text();
|
|
148
|
+
log.warn({ pkg, stderr }, "Package install failed");
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Enforce max size
|
|
153
|
+
if (existsSync(pkgDir)) {
|
|
154
|
+
const size = await dirSize(pkgDir);
|
|
155
|
+
if (size > MAX_PACKAGE_SIZE_BYTES) {
|
|
156
|
+
log.warn({ pkg, size }, "Package exceeds size limit, removing");
|
|
157
|
+
const { rm } = await import("node:fs/promises");
|
|
158
|
+
await rm(pkgDir, { recursive: true, force: true });
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return existsSync(pkgDir) ? nodeModulesDir : null;
|
|
164
|
+
} catch (err) {
|
|
165
|
+
log.warn({ pkg, err }, "Package resolution failed");
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** Recursively sum file sizes under a directory. */
|
|
171
|
+
async function dirSize(dir: string): Promise<number> {
|
|
172
|
+
const { readdir } = await import("node:fs/promises");
|
|
173
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
174
|
+
let total = 0;
|
|
175
|
+
for (const entry of entries) {
|
|
176
|
+
const full = join(dir, entry.name);
|
|
177
|
+
if (entry.isDirectory()) {
|
|
178
|
+
total += await dirSize(full);
|
|
179
|
+
} else {
|
|
180
|
+
const s = await stat(full);
|
|
181
|
+
total += s.size;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return total;
|
|
185
|
+
}
|