@vellumai/assistant 0.4.43 → 0.4.44

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/ARCHITECTURE.md +13 -14
  2. package/README.md +11 -12
  3. package/docs/architecture/integrations.md +75 -93
  4. package/package.json +1 -1
  5. package/src/__tests__/approval-routes-http.test.ts +0 -2
  6. package/src/__tests__/bundled-asset.test.ts +1 -1
  7. package/src/__tests__/checker.test.ts +31 -28
  8. package/src/__tests__/conversation-routes-guardian-reply.test.ts +6 -6
  9. package/src/__tests__/credential-security-invariants.test.ts +2 -1
  10. package/src/__tests__/error-handler-friendly-messages.test.ts +46 -0
  11. package/src/__tests__/managed-twitter-guardrails.test.ts +5 -1
  12. package/src/__tests__/onboarding-template-contract.test.ts +0 -10
  13. package/src/__tests__/provider-fail-open-selection.test.ts +12 -2
  14. package/src/__tests__/send-endpoint-busy.test.ts +0 -3
  15. package/src/__tests__/session-confirmation-signals.test.ts +7 -45
  16. package/src/__tests__/starter-task-flow.test.ts +9 -19
  17. package/src/__tests__/system-prompt.test.ts +3 -4
  18. package/src/__tests__/trust-store.test.ts +4 -4
  19. package/src/__tests__/twitter-platform-proxy-client.test.ts +43 -18
  20. package/src/cli/commands/amazon/index.ts +4 -39
  21. package/src/cli/commands/amazon/session.ts +18 -26
  22. package/src/cli/commands/twitter/__tests__/cli-read-routing.test.ts +58 -196
  23. package/src/cli/commands/twitter/__tests__/cli-routing.test.ts +26 -186
  24. package/src/cli/commands/twitter/__tests__/oauth-client.test.ts +1 -47
  25. package/src/cli/commands/twitter/index.ts +95 -835
  26. package/src/cli/commands/twitter/oauth-client.ts +1 -35
  27. package/src/cli/commands/twitter/router.ts +70 -115
  28. package/src/cli/commands/twitter/types.ts +30 -0
  29. package/src/cli/reference.ts +2 -2
  30. package/src/config/bundled-skills/amazon/SKILL.md +0 -1
  31. package/src/config/bundled-skills/app-builder/SKILL.md +0 -6
  32. package/src/config/bundled-skills/app-builder/TOOLS.json +0 -4
  33. package/src/config/bundled-skills/doordash/SKILL.md +0 -1
  34. package/src/config/bundled-skills/doordash/__tests__/doordash-session.test.ts +1 -82
  35. package/src/config/bundled-skills/doordash/doordash-cli.ts +17 -28
  36. package/src/config/bundled-skills/doordash/lib/session.ts +21 -17
  37. package/src/config/bundled-skills/twitter/SKILL.md +53 -166
  38. package/src/config/feature-flag-registry.json +8 -0
  39. package/src/daemon/handlers/session-history.ts +41 -9
  40. package/src/daemon/lifecycle.ts +4 -17
  41. package/src/daemon/message-types/apps.ts +0 -25
  42. package/src/daemon/message-types/integrations.ts +1 -7
  43. package/src/daemon/message-types/sessions.ts +6 -1
  44. package/src/daemon/message-types/surfaces.ts +2 -0
  45. package/src/daemon/ride-shotgun-handler.ts +33 -1
  46. package/src/daemon/seed-files.ts +3 -27
  47. package/src/daemon/server.ts +2 -18
  48. package/src/daemon/session-agent-loop-handlers.ts +24 -2
  49. package/src/daemon/session-runtime-assembly.ts +0 -7
  50. package/src/daemon/session-surfaces.ts +185 -33
  51. package/src/daemon/session.ts +2 -28
  52. package/src/memory/app-store.ts +0 -18
  53. package/src/memory/schema/infrastructure.ts +0 -8
  54. package/src/permissions/defaults.ts +3 -3
  55. package/src/prompts/system-prompt.ts +4 -5
  56. package/src/prompts/templates/BOOTSTRAP.md +0 -3
  57. package/src/providers/registry.ts +2 -4
  58. package/src/runtime/auth/__tests__/guard-tests.test.ts +1 -0
  59. package/src/runtime/auth/__tests__/scopes.test.ts +2 -1
  60. package/src/runtime/auth/route-policy.ts +0 -4
  61. package/src/runtime/auth/scopes.ts +1 -0
  62. package/src/runtime/auth/token-service.ts +1 -1
  63. package/src/runtime/http-types.ts +10 -0
  64. package/src/runtime/middleware/error-handler.ts +14 -1
  65. package/src/runtime/routes/app-management-routes.ts +61 -64
  66. package/src/runtime/routes/brain-graph/brain-graph.html +1845 -0
  67. package/src/runtime/routes/brain-graph-routes.ts +4 -42
  68. package/src/runtime/routes/conversation-routes.ts +9 -6
  69. package/src/runtime/routes/diagnostics-routes.ts +91 -14
  70. package/src/runtime/routes/settings-routes.ts +3 -93
  71. package/src/tools/AGENTS.md +38 -0
  72. package/src/tools/apps/executors.ts +0 -6
  73. package/src/tools/document/editor-template.ts +10 -8
  74. package/src/twitter/platform-proxy-client.ts +6 -3
  75. package/src/util/errors.ts +12 -0
  76. package/src/__tests__/home-base-bootstrap.test.ts +0 -84
  77. package/src/__tests__/prebuilt-home-base-seed.test.ts +0 -79
  78. package/src/cli/commands/twitter/__tests__/cli-error-shaping.test.ts +0 -265
  79. package/src/cli/commands/twitter/client.ts +0 -989
  80. package/src/cli/commands/twitter/session.ts +0 -121
  81. package/src/home-base/app-link-store.ts +0 -78
  82. package/src/home-base/bootstrap.ts +0 -74
  83. package/src/home-base/prebuilt/brain-graph.html +0 -1483
  84. package/src/home-base/prebuilt/index.html +0 -702
  85. package/src/home-base/prebuilt/seed-metadata.json +0 -21
  86. package/src/home-base/prebuilt/seed.ts +0 -122
  87. package/src/home-base/prebuilt-home-base-updater.ts +0 -36
  88. package/src/util/cookie-session.ts +0 -98
@@ -156,47 +156,14 @@ export function handleGetBrainGraph(): Response {
156
156
  }
157
157
  }
158
158
 
159
- export function handleServeHomeBaseUI(bearerToken?: string): Response {
160
- try {
161
- const prebuiltDir = resolveBundledDir(
162
- import.meta.dirname ?? __dirname,
163
- "../../home-base/prebuilt",
164
- "prebuilt",
165
- );
166
- let html = readFileSync(join(prebuiltDir, "index.html"), "utf-8");
167
- if (bearerToken) {
168
- const escapedToken = bearerToken
169
- .replace(/&/g, "&")
170
- .replace(/"/g, """)
171
- .replace(/</g, "&lt;")
172
- .replace(/>/g, "&gt;");
173
- html = html.replace(
174
- "</head>",
175
- ` <meta name="api-token" content="${escapedToken}">\n</head>`,
176
- );
177
- }
178
- return new Response(html, {
179
- headers: { "Content-Type": "text/html; charset=utf-8" },
180
- });
181
- } catch (err) {
182
- return Response.json(
183
- {
184
- error: "Home Base UI not available",
185
- detail: err instanceof Error ? err.message : String(err),
186
- },
187
- { status: 500 },
188
- );
189
- }
190
- }
191
-
192
159
  export function handleServeBrainGraphUI(bearerToken?: string): Response {
193
160
  try {
194
- const prebuiltDir = resolveBundledDir(
161
+ const brainGraphDir = resolveBundledDir(
195
162
  import.meta.dirname ?? __dirname,
196
- "../../home-base/prebuilt",
197
- "prebuilt",
163
+ "./brain-graph",
164
+ "brain-graph",
198
165
  );
199
- let html = readFileSync(join(prebuiltDir, "brain-graph.html"), "utf-8");
166
+ let html = readFileSync(join(brainGraphDir, "brain-graph.html"), "utf-8");
200
167
  if (bearerToken) {
201
168
  // Inject token as a meta tag for client-side fetch authentication.
202
169
  // HTML-escape the token value to guard against injection if the token
@@ -256,10 +223,5 @@ export function brainGraphRouteDefinitions(deps: {
256
223
  method: "GET",
257
224
  handler: () => handleServeBrainGraphUI(deps.mintUiPageToken()),
258
225
  },
259
- {
260
- endpoint: "home-base-ui",
261
- method: "GET",
262
- handler: () => handleServeHomeBaseUI(deps.mintUiPageToken()),
263
- },
264
226
  ];
265
227
  }
@@ -331,6 +331,9 @@ export function handleListMessages(
331
331
  attachments: msgAttachments,
332
332
  ...(m.toolCalls.length > 0 ? { toolCalls: m.toolCalls } : {}),
333
333
  ...(interfaces ? { interfaces } : {}),
334
+ ...(m.surfaces.length > 0 ? { surfaces: m.surfaces } : {}),
335
+ ...(m.textSegments.length > 0 ? { textSegments: m.textSegments } : {}),
336
+ ...(m.contentOrder.length > 0 ? { contentOrder: m.contentOrder } : {}),
334
337
  };
335
338
  });
336
339
 
@@ -554,11 +557,11 @@ export async function handleSendMessage(
554
557
  }
555
558
 
556
559
  const onEvent = makeHubPublisher(smDeps, mapping.conversationId, session);
557
- // Route server-authoritative state signals (confirmation_state_changed,
558
- // assistant_activity_state) to the SSE hub. Without this, these signals
559
- // only travel through session.sendToClient, which is a no-op for
560
- // socketless HTTP sessions.
561
- session.setStateSignalListener(onEvent);
560
+ // Wire sendToClient to the SSE hub so all subsystems (prompter, surface
561
+ // resolver, notifiers, trace emitter) can reach the HTTP client. The
562
+ // IPC socket removal (PR #14431) left sendToClient as a no-op; this
563
+ // restores the delivery path using the SSE hub instead.
564
+ session.updateClient(onEvent, true);
562
565
 
563
566
  const attachments = hasAttachments
564
567
  ? smDeps.resolveAttachments(attachmentIds)
@@ -610,7 +613,7 @@ export async function handleSendMessage(
610
613
  // can finish the current turn and process this queued message.
611
614
  if (session.hasAnyPendingConfirmation()) {
612
615
  // Emit authoritative denial state for each pending request.
613
- // The onStateSignal listener routes these to the SSE hub automatically.
616
+ // sendToClient (wired to the SSE hub) delivers these to the client.
614
617
  for (const interaction of pendingInteractions.getByConversation(
615
618
  mapping.conversationId,
616
619
  )) {
@@ -7,9 +7,17 @@
7
7
  */
8
8
 
9
9
  import { randomBytes } from "node:crypto";
10
- import { createWriteStream, mkdirSync, rmSync, writeFileSync } from "node:fs";
10
+ import {
11
+ createWriteStream,
12
+ mkdirSync,
13
+ readdirSync,
14
+ readFileSync,
15
+ rmSync,
16
+ statSync,
17
+ writeFileSync,
18
+ } from "node:fs";
11
19
  import { homedir, tmpdir } from "node:os";
12
- import { join } from "node:path";
20
+ import { basename, join } from "node:path";
13
21
 
14
22
  import archiver from "archiver";
15
23
  import { and, desc, eq, gte, lte } from "drizzle-orm";
@@ -104,6 +112,58 @@ function redactDeep(value: unknown): unknown {
104
112
  return value;
105
113
  }
106
114
 
115
+ // ---------------------------------------------------------------------------
116
+ // Crash report discovery
117
+ // ---------------------------------------------------------------------------
118
+
119
+ const CRASH_REPORT_EXTENSIONS = new Set([".crash", ".ips", ".diag"]);
120
+ const CRASH_REPORT_TAR_GZ = ".tar.gz";
121
+ const CRASH_REPORT_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000; // 7 days
122
+
123
+ function findRecentCrashReports(): string[] {
124
+ const diagnosticReportsDir = join(
125
+ homedir(),
126
+ "Library",
127
+ "Logs",
128
+ "DiagnosticReports",
129
+ );
130
+
131
+ try {
132
+ const entries = readdirSync(diagnosticReportsDir);
133
+ const now = Date.now();
134
+ const results: string[] = [];
135
+
136
+ for (const entry of entries) {
137
+ // Case-insensitive prefix match for "vellum-assistant"
138
+ if (!entry.toLowerCase().startsWith("vellum-assistant")) continue;
139
+
140
+ // Check extension
141
+ const lowerEntry = entry.toLowerCase();
142
+ const hasValidExt =
143
+ CRASH_REPORT_EXTENSIONS.has(
144
+ lowerEntry.slice(lowerEntry.lastIndexOf(".")),
145
+ ) || lowerEntry.endsWith(CRASH_REPORT_TAR_GZ);
146
+
147
+ if (!hasValidExt) continue;
148
+
149
+ const filePath = join(diagnosticReportsDir, entry);
150
+ try {
151
+ const stat = statSync(filePath);
152
+ if (!stat.isFile()) continue;
153
+ if (now - stat.mtimeMs > CRASH_REPORT_MAX_AGE_MS) continue;
154
+ results.push(filePath);
155
+ } catch {
156
+ // Skip files we can't stat
157
+ }
158
+ }
159
+
160
+ return results;
161
+ } catch {
162
+ // Directory doesn't exist or can't be read — not an error
163
+ return [];
164
+ }
165
+ }
166
+
107
167
  // ---------------------------------------------------------------------------
108
168
  // Diagnostics export handler
109
169
  // ---------------------------------------------------------------------------
@@ -344,6 +404,28 @@ async function handleDiagnosticsExport(body: {
344
404
 
345
405
  archive.pipe(output);
346
406
  archive.directory(tempDir, false);
407
+
408
+ // Add recent crash report files under crash-reports/.
409
+ // Text-based crash files (.crash, .ips, .diag) are redacted using the
410
+ // same patterns as conversation data. Binary archives (.tar.gz) are
411
+ // added as-is since they can't be meaningfully text-redacted.
412
+ const crashReportFiles = findRecentCrashReports();
413
+ for (const filePath of crashReportFiles) {
414
+ try {
415
+ const fileName = basename(filePath);
416
+ if (fileName.toLowerCase().endsWith(CRASH_REPORT_TAR_GZ)) {
417
+ archive.file(filePath, { name: "crash-reports/" + fileName });
418
+ } else {
419
+ const content = readFileSync(filePath, "utf-8");
420
+ archive.append(redact(content), {
421
+ name: "crash-reports/" + fileName,
422
+ });
423
+ }
424
+ } catch {
425
+ // Skip files that can't be read
426
+ }
427
+ }
428
+
347
429
  archive.finalize();
348
430
  });
349
431
 
@@ -382,9 +464,7 @@ const MAX_WINDOW_TITLE_LENGTH = 100;
382
464
 
383
465
  function sanitizeWindowTitle(title: string | undefined): string {
384
466
  if (!title) return "";
385
- return title
386
- .replace(/[<>]/g, "")
387
- .slice(0, MAX_WINDOW_TITLE_LENGTH);
467
+ return title.replace(/[<>]/g, "").slice(0, MAX_WINDOW_TITLE_LENGTH);
388
468
  }
389
469
 
390
470
  interface DictationBody {
@@ -469,10 +549,7 @@ function buildCombinedDictationPrompt(
469
549
  return sections.join("\n");
470
550
  }
471
551
 
472
- function buildCommandPrompt(
473
- body: DictationBody,
474
- stylePrompt?: string,
475
- ): string {
552
+ function buildCommandPrompt(body: DictationBody, stylePrompt?: string): string {
476
553
  const sections = [
477
554
  "You are a text transformation assistant. The user has selected text and given a voice command to transform it.",
478
555
  "",
@@ -559,7 +636,10 @@ async function handleDictation(body: DictationBody): Promise<Response> {
559
636
  const stylePrompt = profile.stylePrompt || undefined;
560
637
 
561
638
  // Command mode: selected text present
562
- if (body.context.selectedText && body.context.selectedText.trim().length > 0) {
639
+ if (
640
+ body.context.selectedText &&
641
+ body.context.selectedText.trim().length > 0
642
+ ) {
563
643
  log.info({ mode: "command" }, "Command mode (selected text present)");
564
644
  return handleCommandMode(body, profile, profileMeta, stylePrompt);
565
645
  }
@@ -669,10 +749,7 @@ async function handleDictation(body: DictationBody): Promise<Response> {
669
749
  });
670
750
  }
671
751
  const cleanedText = input.text?.trim() || transcription;
672
- const normalizedText = applyDictionary(
673
- cleanedText,
674
- profile.dictionary,
675
- );
752
+ const normalizedText = applyDictionary(cleanedText, profile.dictionary);
676
753
  return Response.json({
677
754
  text: normalizedText,
678
755
  mode: "dictation",
@@ -1,13 +1,12 @@
1
1
  /**
2
2
  * HTTP route handlers for settings, identity/avatar, voice config,
3
- * OAuth connect, Twitter auth, home base, and workspace files.
3
+ * OAuth connect, Twitter auth, and workspace files.
4
4
  *
5
5
  * Migrated from IPC handlers:
6
6
  * - handlers/config-voice.ts (voice_config_update)
7
7
  * - handlers/avatar.ts (generate_avatar)
8
8
  * - handlers/oauth-connect.ts (oauth_connect_start)
9
9
  * - handlers/twitter-auth.ts (twitter_auth_start, twitter_auth_status)
10
- * - handlers/home-base.ts (home_base_get)
11
10
  * - handlers/workspace-files.ts (workspace_files_list, workspace_file_read)
12
11
  * - handlers/config-tools.ts (tool_names_list, tool_permission_simulate, env_vars_request)
13
12
  */
@@ -29,17 +28,7 @@ import {
29
28
  } from "../../config/loader.js";
30
29
  import { loadSkillCatalog } from "../../config/skills.js";
31
30
  import { normalizeActivationKey } from "../../daemon/handlers/config-voice.js";
32
- import { getHomeBaseAppLink } from "../../home-base/app-link-store.js";
33
- import {
34
- bootstrapHomeBaseAppLink,
35
- resolveHomeBaseAppId,
36
- } from "../../home-base/bootstrap.js";
37
- import {
38
- getPrebuiltHomeBasePreview,
39
- getPrebuiltHomeBaseTaskPayload,
40
- } from "../../home-base/prebuilt/seed.js";
41
31
  import { getPublicBaseUrl } from "../../inbound/public-ingress-urls.js";
42
- import { getApp } from "../../memory/app-store.js";
43
32
  import { orchestrateOAuthConnect } from "../../oauth/connect-orchestrator.js";
44
33
  import {
45
34
  getProviderProfile,
@@ -452,7 +441,8 @@ function handleTwitterAuthStatus(): Response {
452
441
  )?.baseUrl as string | undefined;
453
442
  const platformAssistantIdResolvable = !!(
454
443
  (platformBaseUrlFromEnv || platformBaseUrlFromConfig) &&
455
- getPlatformAssistantId()
444
+ (getSecureKey("credential:vellum:platform_assistant_id") ||
445
+ getPlatformAssistantId())
456
446
  );
457
447
 
458
448
  const managedPrerequisites = {
@@ -470,13 +460,6 @@ function handleTwitterAuthStatus(): Response {
470
460
  "credential:integration:twitter:client_id",
471
461
  );
472
462
 
473
- // Strategy
474
- const strategyRaw = getNestedValue(raw, "twitter.strategy") as
475
- | string
476
- | undefined;
477
- const strategy = strategyRaw ?? "auto";
478
- const strategyConfigured = strategyRaw !== undefined;
479
-
480
463
  // In managed mode, connected is always false (auth goes through platform)
481
464
  const connected = mode === "managed" ? false : !!accessToken;
482
465
 
@@ -487,8 +470,6 @@ function handleTwitterAuthStatus(): Response {
487
470
  managedAvailable,
488
471
  managedPrerequisites,
489
472
  localClientConfigured,
490
- strategy,
491
- strategyConfigured,
492
473
  });
493
474
  } catch (err) {
494
475
  const message = err instanceof Error ? err.message : String(err);
@@ -497,66 +478,6 @@ function handleTwitterAuthStatus(): Response {
497
478
  }
498
479
  }
499
480
 
500
- // ---------------------------------------------------------------------------
501
- // Home base
502
- // ---------------------------------------------------------------------------
503
-
504
- function handleHomeBaseGet(ensureLinked: boolean): Response {
505
- try {
506
- if (ensureLinked !== false) {
507
- bootstrapHomeBaseAppLink();
508
- }
509
-
510
- const appId = resolveHomeBaseAppId();
511
- if (!appId) {
512
- return Response.json({ homeBase: null });
513
- }
514
-
515
- const link = getHomeBaseAppLink();
516
- const source = link?.source ?? "prebuilt_seed";
517
-
518
- let preview: {
519
- title: string;
520
- subtitle: string;
521
- description: string;
522
- icon: string;
523
- metrics: Array<{ label: string; value: string }>;
524
- };
525
-
526
- if (source === "personalized") {
527
- const app = getApp(appId);
528
- if (app) {
529
- preview = {
530
- title: app.name,
531
- subtitle: "Dashboard",
532
- description: app.description ?? "",
533
- icon: app.icon ?? "🏠",
534
- metrics: [],
535
- };
536
- } else {
537
- preview = getPrebuiltHomeBasePreview();
538
- }
539
- } else {
540
- preview = getPrebuiltHomeBasePreview();
541
- }
542
-
543
- const tasks = getPrebuiltHomeBaseTaskPayload();
544
-
545
- return Response.json({
546
- homeBase: {
547
- appId,
548
- source,
549
- starterTasks: tasks.starterTasks,
550
- onboardingTasks: tasks.onboardingTasks,
551
- preview,
552
- },
553
- });
554
- } catch (err) {
555
- log.error({ err }, "Failed to resolve home base metadata");
556
- return Response.json({ homeBase: null });
557
- }
558
- }
559
-
560
481
  // ---------------------------------------------------------------------------
561
482
  // Workspace files (IPC-style list/read)
562
483
  // ---------------------------------------------------------------------------
@@ -906,17 +827,6 @@ export function settingsRouteDefinitions(): RouteDefinition[] {
906
827
  handler: () => handleTwitterAuthStatus(),
907
828
  },
908
829
 
909
- // Home base
910
- {
911
- endpoint: "home-base",
912
- method: "GET",
913
- policyKey: "home-base",
914
- handler: ({ url }) => {
915
- const ensureLinked = url.searchParams.get("ensureLinked") !== "false";
916
- return handleHomeBaseGet(ensureLinked);
917
- },
918
- },
919
-
920
830
  // Workspace files (IPC-style list/read -- distinct from workspace-routes.ts tree/file)
921
831
  {
922
832
  endpoint: "workspace-files",
@@ -0,0 +1,38 @@
1
+ # Tools — Agent Instructions
2
+
3
+ ## No New Tools Policy
4
+
5
+ **New tool registrations require approval from Team Jarvis.**
6
+
7
+ The tool registration system (`class ... implements Tool` + `registerTool()`) is being phased out in favor of skill-based approaches. Before adding a new tool, contact Team Jarvis for approval.
8
+
9
+ ## Why This Policy Exists
10
+
11
+ 1. **Skills are preferred** — The project direction is to teach the assistant CLI tools via skills rather than hardcoding tool implementations. Skills are progressively disclosed into context, are more portable, and are often self-contained.
12
+
13
+ 2. **Context overhead** — Each registered tool adds to the system prompt and increases token usage for every conversation.
14
+
15
+ 3. **Maintenance burden** — Tools require ongoing maintenance, testing, and security review. Skills can be iterated on independently.
16
+
17
+ ## What To Do Instead
18
+
19
+ Instead of creating a new tool, consider:
20
+
21
+ 1. **Create a skill**
22
+
23
+ 2. **Use existing tools** — Many capabilities can be achieved by combining existing tools (bash, file operations, network tools) with skill instructions.
24
+
25
+ 3. **External CLI tools** — If you need new functionality, consider whether it can be exposed as a CLI tool that the assistant can invoke via bash.
26
+
27
+ ## If You Have Approval
28
+
29
+ If Team Jarvis has approved your new tool:
30
+
31
+ 1. The pre-commit hook will block your commit by default
32
+ 2. Use `git commit --no-verify` to bypass the hook
33
+ 3. Include the approval context in your PR description
34
+
35
+
36
+ ## Questions?
37
+
38
+ Contact Team Jarvis before shipping a new tool.
@@ -9,7 +9,6 @@
9
9
  */
10
10
 
11
11
  import { compileApp } from "../../bundler/app-compiler.js";
12
- import { setHomeBaseAppLink } from "../../home-base/app-link-store.js";
13
12
  import { generateAppIcon } from "../../media/app-icon-generator.js";
14
13
  import type {
15
14
  AppDefinition,
@@ -115,7 +114,6 @@ export interface AppCreateInput {
115
114
  html?: string;
116
115
  pages?: Record<string, string>;
117
116
  auto_open?: boolean;
118
- set_as_home_base?: boolean;
119
117
  preview?: Record<string, unknown>;
120
118
  /** When provided, controls multifile scaffold behavior. */
121
119
  featureFlags?: { multifileEnabled: boolean };
@@ -240,10 +238,6 @@ render(<App />, document.getElementById('app')!);
240
238
  }
241
239
  }
242
240
 
243
- if (input.set_as_home_base) {
244
- setHomeBaseAppLink(app.id, "personalized");
245
- }
246
-
247
241
  // Emit the inline preview card via the proxy without opening a workspace panel.
248
242
  // open_mode: "preview" signals to the client that this should be shown inline only.
249
243
  if (autoOpen && proxyToolResolver) {
@@ -39,17 +39,19 @@ export function generateEditorHTML(
39
39
  --v-text: #1D1D1F;
40
40
  --v-text-secondary: #86868B;
41
41
  --v-text-muted: #AEAEB2;
42
- --v-accent: #8A5BE0;
42
+ --v-accent: #657D5B;
43
+ --v-accent-hover: #516748;
43
44
  }
44
45
  @media (prefers-color-scheme: dark) {
45
46
  :root {
46
- --v-bg: #070D19;
47
- --v-surface: #1E293B;
48
- --v-surface-border: #334155;
49
- --v-text: #F8FAFC;
50
- --v-text-secondary: #94A3B8;
51
- --v-text-muted: #64748B;
52
- --v-accent: #8A5BE0;
47
+ --v-bg: #20201E;
48
+ --v-surface: #3A3A37;
49
+ --v-surface-border: #4A4A46;
50
+ --v-text: #F5F3EB;
51
+ --v-text-secondary: #A1A096;
52
+ --v-text-muted: #6B6B65;
53
+ --v-accent: #657D5B;
54
+ --v-accent-hover: #516748;
53
55
  }
54
56
  }
55
57
 
@@ -9,7 +9,7 @@
9
9
  * Prerequisites:
10
10
  * - Platform base URL (env `PLATFORM_BASE_URL` or config `platform.baseUrl`)
11
11
  * - Auth token (secure key `credential:vellum:assistant_api_key`)
12
- * - Platform assistant ID (env `PLATFORM_ASSISTANT_ID`)
12
+ * - Platform assistant ID (secure key store or env `PLATFORM_ASSISTANT_ID`)
13
13
  */
14
14
 
15
15
  import { getPlatformAssistantId, getPlatformBaseUrl } from "../config/env.js";
@@ -64,10 +64,13 @@ export function resolveAuthToken(): string | undefined {
64
64
  }
65
65
 
66
66
  /**
67
- * Resolve the platform assistant ID from environment.
67
+ * Resolve the platform assistant ID from secure storage (local) or environment (containerized).
68
68
  */
69
69
  export function resolvePlatformAssistantId(): string {
70
- return getPlatformAssistantId();
70
+ return (
71
+ getSecureKey("credential:vellum:platform_assistant_id") ||
72
+ getPlatformAssistantId()
73
+ );
71
74
  }
72
75
 
73
76
  /**
@@ -151,6 +151,18 @@ export class ConfigError extends AssistantError {
151
151
  }
152
152
  }
153
153
 
154
+ export class ProviderNotConfiguredError extends ConfigError {
155
+ constructor(
156
+ public readonly requestedProvider: string,
157
+ public readonly registeredProviders: string[],
158
+ ) {
159
+ super(
160
+ `No providers available. Requested: "${requestedProvider}". Registered: ${registeredProviders.join(", ") || "none"}`,
161
+ );
162
+ this.name = "ProviderNotConfiguredError";
163
+ }
164
+ }
165
+
154
166
  export class DaemonError extends AssistantError {
155
167
  constructor(message: string, options?: { cause?: unknown }) {
156
168
  super(message, ErrorCode.DAEMON_ERROR, options);
@@ -1,84 +0,0 @@
1
- import { mkdirSync, mkdtempSync, rmSync } from "node:fs";
2
- import { tmpdir } from "node:os";
3
- import { join } from "node:path";
4
- import { afterAll, beforeEach, describe, expect, mock, test } from "bun:test";
5
-
6
- const testRoot = mkdtempSync(join(tmpdir(), "home-base-bootstrap-test-"));
7
- const dataDir = join(testRoot, "data");
8
- const dbPath = join(testRoot, "assistant.db");
9
-
10
- mock.module("../util/platform.js", () => ({
11
- getDataDir: () => dataDir,
12
- getDbPath: () => dbPath,
13
- ensureDataDir: () => {
14
- mkdirSync(dataDir, { recursive: true });
15
- },
16
- isMacOS: () => process.platform === "darwin",
17
- isLinux: () => process.platform === "linux",
18
- isWindows: () => process.platform === "win32",
19
- getSocketPath: () => join(testRoot, "test.sock"),
20
- getPidPath: () => join(testRoot, "test.pid"),
21
- getLogPath: () => join(testRoot, "test.log"),
22
- }));
23
-
24
- mock.module("../util/logger.js", () => ({
25
- getLogger: () =>
26
- new Proxy({} as Record<string, unknown>, {
27
- get: () => () => {},
28
- }),
29
- }));
30
-
31
- import { getHomeBaseAppLink } from "../home-base/app-link-store.js";
32
- import {
33
- bootstrapHomeBaseAppLink,
34
- resolveHomeBaseAppId,
35
- } from "../home-base/bootstrap.js";
36
- import { deleteApp } from "../memory/app-store.js";
37
- import { initializeDb, resetDb } from "../memory/db.js";
38
-
39
- describe("home base bootstrap", () => {
40
- beforeEach(() => {
41
- resetDb();
42
- rmSync(testRoot, { recursive: true, force: true });
43
- mkdirSync(testRoot, { recursive: true });
44
- initializeDb();
45
- });
46
-
47
- afterAll(() => {
48
- resetDb();
49
- rmSync(testRoot, { recursive: true, force: true });
50
- });
51
-
52
- test("creates a durable Home Base link on first bootstrap", () => {
53
- const result = bootstrapHomeBaseAppLink();
54
- expect(result).not.toBeNull();
55
- const link = getHomeBaseAppLink();
56
-
57
- expect(result!.linked).toBe(true);
58
- expect(link).not.toBeNull();
59
- expect(link?.appId).toBe(result!.appId);
60
- expect(resolveHomeBaseAppId()).toBe(result!.appId);
61
- });
62
-
63
- test("reuses existing link on repeated bootstrap calls", () => {
64
- const first = bootstrapHomeBaseAppLink();
65
- const second = bootstrapHomeBaseAppLink();
66
- expect(first).not.toBeNull();
67
- expect(second).not.toBeNull();
68
-
69
- expect(second!.appId).toBe(first!.appId);
70
- expect(second!.created).toBe(false);
71
- });
72
-
73
- test("relinks when stored app id is stale", () => {
74
- const first = bootstrapHomeBaseAppLink();
75
- expect(first).not.toBeNull();
76
- deleteApp(first!.appId);
77
-
78
- const second = bootstrapHomeBaseAppLink();
79
- expect(second).not.toBeNull();
80
-
81
- expect(second!.appId).not.toBe(first!.appId);
82
- expect(getHomeBaseAppLink()?.appId).toBe(second!.appId);
83
- });
84
- });