@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.
Files changed (169) hide show
  1. package/ARCHITECTURE.md +3 -3
  2. package/README.md +13 -13
  3. package/bun.lock +80 -24
  4. package/docs/architecture/integrations.md +126 -128
  5. package/docs/runbook-trusted-contacts.md +1 -1
  6. package/docs/trusted-contact-access.md +12 -12
  7. package/package.json +3 -1
  8. package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +0 -14
  9. package/src/__tests__/app-bundler.test.ts +209 -0
  10. package/src/__tests__/app-compiler.test.ts +279 -0
  11. package/src/__tests__/app-executors.test.ts +293 -483
  12. package/src/__tests__/app-migration.test.ts +148 -0
  13. package/src/__tests__/app-routes-csp.test.ts +202 -0
  14. package/src/__tests__/avatar-e2e.test.ts +452 -0
  15. package/src/__tests__/avatar-generator.test.ts +193 -0
  16. package/src/__tests__/avatar-router.test.ts +186 -0
  17. package/src/__tests__/browser-download-timeout.test.ts +28 -0
  18. package/src/__tests__/bundled-skill-retrieval-guard.test.ts +9 -9
  19. package/src/__tests__/call-domain.test.ts +3 -7
  20. package/src/__tests__/credential-security-e2e.test.ts +19 -12
  21. package/src/__tests__/credentials-cli.test.ts +30 -4
  22. package/src/__tests__/guardian-verify-setup-skill-regression.test.ts +1 -1
  23. package/src/__tests__/handlers-slack-config.test.ts +0 -72
  24. package/src/__tests__/handlers-telegram-config.test.ts +19 -12
  25. package/src/__tests__/handlers-twitter-config.test.ts +105 -48
  26. package/src/__tests__/inbound-invite-redemption.test.ts +4 -4
  27. package/src/__tests__/integration-status.test.ts +15 -5
  28. package/src/__tests__/integrations-cli.test.ts +1 -1
  29. package/src/__tests__/invite-redemption-service.test.ts +62 -7
  30. package/src/__tests__/ipc-snapshot.test.ts +0 -8
  31. package/src/__tests__/managed-avatar-client.test.ts +280 -0
  32. package/src/__tests__/mcp-cli.test.ts +3 -3
  33. package/src/__tests__/oauth-cli.test.ts +203 -0
  34. package/src/__tests__/relay-server.test.ts +3 -3
  35. package/src/__tests__/secret-onetime-send.test.ts +19 -12
  36. package/src/__tests__/secure-keys.test.ts +78 -0
  37. package/src/__tests__/session-messaging-secret-redirect.test.ts +3 -0
  38. package/src/__tests__/slack-channel-config.test.ts +23 -16
  39. package/src/__tests__/slack-share-routes.test.ts +263 -0
  40. package/src/__tests__/sms-messaging-provider.test.ts +3 -1
  41. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +7 -7
  42. package/src/__tests__/trusted-contact-multichannel.test.ts +3 -3
  43. package/src/__tests__/trusted-contact-verification.test.ts +10 -10
  44. package/src/__tests__/twilio-config.test.ts +15 -36
  45. package/src/__tests__/twilio-provider.test.ts +4 -0
  46. package/src/__tests__/twitter-auth-handler.test.ts +27 -14
  47. package/src/__tests__/twitter-cli-error-shaping.test.ts +1 -1
  48. package/src/__tests__/twitter-cli-routing.test.ts +38 -53
  49. package/src/__tests__/twitter-oauth-client.test.ts +18 -47
  50. package/src/__tests__/voice-invite-redemption.test.ts +27 -3
  51. package/src/amazon/cart.ts +1 -1
  52. package/src/amazon/client.ts +89 -7
  53. package/src/approvals/guardian-request-resolvers.ts +2 -2
  54. package/src/bundler/app-bundler.ts +77 -32
  55. package/src/bundler/app-compiler.ts +195 -0
  56. package/src/bundler/manifest.ts +1 -1
  57. package/src/bundler/package-resolver.ts +185 -0
  58. package/src/calls/call-domain.ts +4 -14
  59. package/src/calls/relay-server.ts +2 -2
  60. package/src/calls/twilio-config.ts +5 -24
  61. package/src/calls/twilio-rest.ts +19 -5
  62. package/src/cli/amazon.ts +74 -249
  63. package/src/cli/audit.ts +2 -2
  64. package/src/cli/autonomy.ts +9 -9
  65. package/src/cli/channels.ts +5 -5
  66. package/src/cli/completions.ts +27 -27
  67. package/src/cli/config.ts +14 -14
  68. package/src/cli/contacts.ts +27 -27
  69. package/src/cli/credentials.ts +28 -28
  70. package/src/cli/dev.ts +2 -2
  71. package/src/cli/doctor.ts +2 -2
  72. package/src/cli/email.ts +82 -82
  73. package/src/cli/influencer.ts +13 -13
  74. package/src/cli/integrations.ts +19 -144
  75. package/src/cli/keys.ts +10 -10
  76. package/src/cli/map.ts +4 -4
  77. package/src/cli/mcp.ts +17 -17
  78. package/src/cli/memory.ts +18 -18
  79. package/src/cli/notifications.ts +13 -13
  80. package/src/cli/oauth.ts +77 -0
  81. package/src/cli/program.ts +2 -0
  82. package/src/cli/sequence.ts +27 -27
  83. package/src/cli/sessions.ts +12 -12
  84. package/src/cli/trust.ts +8 -8
  85. package/src/cli/twitter.ts +124 -70
  86. package/src/config/bundled-skills/_shared/CLI_RETRIEVAL_PATTERN.md +1 -1
  87. package/src/config/bundled-skills/agentmail/SKILL.md +34 -34
  88. package/src/config/bundled-skills/amazon/SKILL.md +54 -54
  89. package/src/config/bundled-skills/app-builder/SKILL.md +137 -3
  90. package/src/config/bundled-skills/app-builder/tools/app-create.ts +10 -4
  91. package/src/config/bundled-skills/configure-settings/SKILL.md +18 -18
  92. package/src/config/bundled-skills/contacts/SKILL.md +12 -12
  93. package/src/config/bundled-skills/doordash/lib/client.ts +7 -9
  94. package/src/config/bundled-skills/email-setup/SKILL.md +4 -4
  95. package/src/config/bundled-skills/frontend-design/icon.svg +16 -0
  96. package/src/config/bundled-skills/google-oauth-setup/SKILL.md +143 -162
  97. package/src/config/bundled-skills/guardian-verify-setup/SKILL.md +4 -4
  98. package/src/config/bundled-skills/influencer/SKILL.md +13 -13
  99. package/src/config/bundled-skills/mcp-setup/SKILL.md +11 -11
  100. package/src/config/bundled-skills/phone-calls/SKILL.md +48 -54
  101. package/src/config/bundled-skills/public-ingress/SKILL.md +6 -6
  102. package/src/config/bundled-skills/slack-app-setup/SKILL.md +1 -1
  103. package/src/config/bundled-skills/sms-setup/SKILL.md +3 -3
  104. package/src/config/bundled-skills/telegram-setup/SKILL.md +2 -2
  105. package/src/config/bundled-skills/twilio-setup/SKILL.md +136 -225
  106. package/src/config/bundled-skills/twitter/SKILL.md +68 -44
  107. package/src/config/bundled-skills/voice-setup/SKILL.md +2 -2
  108. package/src/config/core-schema.ts +26 -0
  109. package/src/config/env.ts +4 -0
  110. package/src/config/feature-flag-registry.json +9 -1
  111. package/src/config/schema.ts +8 -0
  112. package/src/config/system-prompt.ts +6 -3
  113. package/src/config/templates/BOOTSTRAP.md +7 -5
  114. package/src/contacts/contacts-write.ts +5 -1
  115. package/src/daemon/handlers/apps.ts +31 -4
  116. package/src/daemon/handlers/config-ingress.ts +3 -3
  117. package/src/daemon/handlers/config-integrations.ts +120 -49
  118. package/src/daemon/handlers/config-slack-channel.ts +26 -7
  119. package/src/daemon/handlers/config-slack.ts +1 -54
  120. package/src/daemon/handlers/config-telegram.ts +28 -10
  121. package/src/daemon/handlers/config.ts +1 -4
  122. package/src/daemon/handlers/twitter-auth.ts +11 -4
  123. package/src/daemon/ipc-contract/apps.ts +0 -13
  124. package/src/daemon/ipc-contract-inventory.json +0 -2
  125. package/src/daemon/lifecycle.ts +8 -1
  126. package/src/daemon/session-messaging.ts +2 -2
  127. package/src/daemon/tool-side-effects.ts +30 -0
  128. package/src/email/providers/agentmail.ts +1 -1
  129. package/src/email/providers/index.ts +1 -1
  130. package/src/email/service.ts +1 -1
  131. package/src/gallery/default-gallery.ts +538 -0
  132. package/src/gallery/gallery-manifest.ts +5 -1
  133. package/src/influencer/client.ts +8 -6
  134. package/src/mcp/client.ts +1 -1
  135. package/src/media/avatar-router.ts +99 -0
  136. package/src/media/avatar-types.ts +60 -0
  137. package/src/media/managed-avatar-client.ts +189 -0
  138. package/src/memory/app-migration.ts +114 -0
  139. package/src/memory/app-store.ts +11 -0
  140. package/src/memory/qdrant-client.ts +1 -1
  141. package/src/messaging/providers/slack/client.ts +12 -2
  142. package/src/messaging/providers/sms/adapter.ts +6 -10
  143. package/src/migrations/data-layout.ts +8 -1
  144. package/src/oauth/token-persistence.ts +9 -6
  145. package/src/runtime/assistant-scope.ts +5 -0
  146. package/src/runtime/auth/route-policy.ts +4 -0
  147. package/src/runtime/channel-readiness-service.ts +9 -4
  148. package/src/runtime/gateway-internal-client.ts +11 -3
  149. package/src/runtime/http-server.ts +2 -0
  150. package/src/runtime/invite-redemption-service.ts +23 -13
  151. package/src/runtime/middleware/twilio-validation.ts +2 -2
  152. package/src/runtime/routes/app-routes.ts +131 -3
  153. package/src/runtime/routes/inbound-stages/verification-intercept.ts +3 -3
  154. package/src/runtime/routes/integration-routes.ts +2 -2
  155. package/src/runtime/routes/slack-share-routes.ts +235 -0
  156. package/src/runtime/routes/twilio-routes.ts +47 -34
  157. package/src/schedule/integration-status.ts +2 -3
  158. package/src/security/token-manager.ts +11 -3
  159. package/src/tools/apps/executors.ts +116 -8
  160. package/src/tools/browser/browser-manager.ts +30 -2
  161. package/src/tools/browser/chrome-cdp.ts +31 -3
  162. package/src/tools/credentials/vault.ts +9 -7
  163. package/src/tools/executor.ts +4 -0
  164. package/src/tools/system/avatar-generator.ts +55 -34
  165. package/src/twitter/client.ts +1 -1
  166. package/src/twitter/oauth-client.ts +31 -43
  167. package/src/twitter/router.ts +25 -23
  168. package/src/util/platform.ts +5 -0
  169. package/src/slack/slack-webhook.ts +0 -66
@@ -9,7 +9,9 @@
9
9
 
10
10
  import { join } from "node:path";
11
11
 
12
+ import { compileApp } from "../bundler/app-compiler.js";
12
13
  import { generateAppIcon } from "../media/app-icon-generator.js";
14
+ import { getApp, getAppsDir, isMultifileApp } from "../memory/app-store.js";
13
15
  import { updatePublishedAppDeployment } from "../services/published-app-updater.js";
14
16
  import type { ToolExecutionResult } from "../tools/types.js";
15
17
  import { getLogger } from "../util/logger.js";
@@ -44,6 +46,34 @@ function handleAppChange(
44
46
  broadcastToAllClients: ((msg: ServerMessage) => void) | undefined,
45
47
  opts?: { fileChange?: boolean; status?: string },
46
48
  ): void {
49
+ const app = getApp(appId);
50
+
51
+ // Multifile apps need a recompile before refreshing surfaces so the
52
+ // WebView picks up the latest compiled output.
53
+ if (app && isMultifileApp(app)) {
54
+ const appDir = join(getAppsDir(), appId);
55
+ void compileApp(appDir)
56
+ .then((result) => {
57
+ if (!result.ok) {
58
+ log.warn(
59
+ { appId, errors: result.errors },
60
+ "Recompile failed on app change, serving stale dist/",
61
+ );
62
+ }
63
+ refreshSurfacesForApp(ctx, appId, opts);
64
+ broadcastToAllClients?.({ type: "app_files_changed", appId });
65
+ void updatePublishedAppDeployment(appId);
66
+ })
67
+ .catch((err) => {
68
+ log.warn({ appId, err }, "Recompile threw on app change");
69
+ // Still refresh surfaces with stale output
70
+ refreshSurfacesForApp(ctx, appId, opts);
71
+ broadcastToAllClients?.({ type: "app_files_changed", appId });
72
+ void updatePublishedAppDeployment(appId);
73
+ });
74
+ return;
75
+ }
76
+
47
77
  refreshSurfacesForApp(ctx, appId, opts);
48
78
  broadcastToAllClients?.({ type: "app_files_changed", appId });
49
79
  void updatePublishedAppDeployment(appId);
@@ -236,7 +236,7 @@ export class AgentMailProvider implements EmailProvider {
236
236
  return inboxes.inboxes[0].inboxId;
237
237
  }
238
238
  throw new ConfigError(
239
- "No inboxes found. Run: vellum email setup inboxes --domain <domain>",
239
+ "No inboxes found. Run: assistant email setup inboxes --domain <domain>",
240
240
  );
241
241
  }
242
242
  }
@@ -51,7 +51,7 @@ export async function createProvider(
51
51
  }
52
52
  if (!apiKey) {
53
53
  throw new ConfigError(
54
- "No AgentMail API key configured. Run: vellum keys set agentmail <key>",
54
+ "No AgentMail API key configured. Run: assistant keys set agentmail <key>",
55
55
  );
56
56
  }
57
57
  const { AgentMailClient } = await import("agentmail");
@@ -227,7 +227,7 @@ export class EmailService {
227
227
  const health = await p.health();
228
228
  if (health.inboxes.length === 0) {
229
229
  throw new ConfigError(
230
- "No inboxes found. Run: vellum email setup inboxes --domain <domain>",
230
+ "No inboxes found. Run: assistant email setup inboxes --domain <domain>",
231
231
  );
232
232
  }
233
233
  const inbox = health.inboxes.find(
@@ -752,6 +752,540 @@ const expenseTrackerHtml = `<!DOCTYPE html>
752
752
  </body>
753
753
  </html>`;
754
754
 
755
+ // -- Multi-file source files for Focus Timer (formatVersion 2) --
756
+
757
+ const focusTimerSourceFiles: Record<string, string> = {
758
+ "src/index.html": `<!DOCTYPE html>
759
+ <html lang="en">
760
+ <head>
761
+ <meta charset="UTF-8">
762
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
763
+ <title>Focus Timer</title>
764
+ </head>
765
+ <body>
766
+ <div id="app"></div>
767
+ </body>
768
+ </html>`,
769
+
770
+ "src/main.tsx": `import { render } from "preact";
771
+ import { Timer } from "./components/Timer.js";
772
+ import "./styles.css";
773
+
774
+ function App() {
775
+ return <Timer workMinutes={25} breakMinutes={5} />;
776
+ }
777
+
778
+ render(<App />, document.getElementById("app")!);
779
+ `,
780
+
781
+ "src/components/Timer.tsx": `import { useCallback, useEffect, useRef, useState } from "preact/hooks";
782
+
783
+ interface TimerProps {
784
+ workMinutes: number;
785
+ breakMinutes: number;
786
+ }
787
+
788
+ export function Timer({ workMinutes, breakMinutes }: TimerProps) {
789
+ const [secondsLeft, setSecondsLeft] = useState(workMinutes * 60);
790
+ const [isRunning, setIsRunning] = useState(false);
791
+ const [isBreak, setIsBreak] = useState(false);
792
+ const [sessions, setSessions] = useState(0);
793
+ const [totalMinutes, setTotalMinutes] = useState(0);
794
+ const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
795
+
796
+ const clearTimer = useCallback(() => {
797
+ if (intervalRef.current !== null) {
798
+ clearInterval(intervalRef.current);
799
+ intervalRef.current = null;
800
+ }
801
+ }, []);
802
+
803
+ // Tick effect: runs when the timer is active
804
+ useEffect(() => {
805
+ if (!isRunning) return;
806
+
807
+ intervalRef.current = setInterval(() => {
808
+ setSecondsLeft((prev) => {
809
+ if (prev <= 1) {
810
+ clearTimer();
811
+ setIsRunning(false);
812
+ if (!isBreak) {
813
+ setSessions((s) => s + 1);
814
+ setTotalMinutes((t) => t + workMinutes);
815
+ setIsBreak(true);
816
+ return breakMinutes * 60;
817
+ } else {
818
+ setIsBreak(false);
819
+ return workMinutes * 60;
820
+ }
821
+ }
822
+ return prev - 1;
823
+ });
824
+ }, 1000);
825
+
826
+ return clearTimer;
827
+ }, [isRunning, isBreak, workMinutes, breakMinutes, clearTimer]);
828
+
829
+ const toggle = () => setIsRunning((r) => !r);
830
+
831
+ const reset = () => {
832
+ clearTimer();
833
+ setIsRunning(false);
834
+ setIsBreak(false);
835
+ setSecondsLeft(workMinutes * 60);
836
+ };
837
+
838
+ const minutes = Math.floor(secondsLeft / 60);
839
+ const seconds = secondsLeft % 60;
840
+ const display =
841
+ String(minutes).padStart(2, "0") + ":" + String(seconds).padStart(2, "0");
842
+
843
+ return (
844
+ <div class="container">
845
+ <h1>Focus Timer</h1>
846
+ <div class={\`mode-label\${isBreak ? " break" : ""}\`}>
847
+ {isBreak ? "Break Time" : "Work Session"}
848
+ </div>
849
+ <div class="timer-display">{display}</div>
850
+ <div class="controls">
851
+ <button class="btn-primary" onClick={toggle}>
852
+ {isRunning ? "Pause" : "Start"}
853
+ </button>
854
+ <button class="btn-secondary" onClick={reset}>
855
+ Reset
856
+ </button>
857
+ </div>
858
+ <div class="stats">
859
+ <div class="stat-item">
860
+ <div class="stat-value">{sessions}</div>
861
+ <div class="stat-label">Sessions</div>
862
+ </div>
863
+ <div class="stat-item">
864
+ <div class="stat-value">{totalMinutes}</div>
865
+ <div class="stat-label">Minutes</div>
866
+ </div>
867
+ </div>
868
+ </div>
869
+ );
870
+ }
871
+ `,
872
+
873
+ "src/styles.css": `:root {
874
+ --bg: #1a1a2e;
875
+ --surface: #16213e;
876
+ --primary: #e94560;
877
+ --primary-hover: #c73e54;
878
+ --text: #eee;
879
+ --text-secondary: #aaa;
880
+ --break-color: #0f9b58;
881
+ --radius: 12px;
882
+ }
883
+ * { margin: 0; padding: 0; box-sizing: border-box; }
884
+ body {
885
+ font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", sans-serif;
886
+ background: var(--bg);
887
+ color: var(--text);
888
+ display: flex;
889
+ justify-content: center;
890
+ align-items: center;
891
+ min-height: 100vh;
892
+ padding: 20px;
893
+ }
894
+ .container {
895
+ text-align: center;
896
+ max-width: 400px;
897
+ width: 100%;
898
+ }
899
+ h1 {
900
+ font-size: 1.4rem;
901
+ font-weight: 600;
902
+ margin-bottom: 8px;
903
+ }
904
+ .mode-label {
905
+ font-size: 0.9rem;
906
+ color: var(--text-secondary);
907
+ margin-bottom: 32px;
908
+ text-transform: uppercase;
909
+ letter-spacing: 1px;
910
+ }
911
+ .mode-label.break { color: var(--break-color); }
912
+ .timer-display {
913
+ font-size: 5rem;
914
+ font-weight: 200;
915
+ font-variant-numeric: tabular-nums;
916
+ letter-spacing: 2px;
917
+ margin-bottom: 40px;
918
+ }
919
+ .controls {
920
+ display: flex;
921
+ gap: 12px;
922
+ justify-content: center;
923
+ margin-bottom: 32px;
924
+ }
925
+ button {
926
+ font-family: inherit;
927
+ font-size: 0.95rem;
928
+ font-weight: 500;
929
+ padding: 10px 28px;
930
+ border: none;
931
+ border-radius: var(--radius);
932
+ cursor: pointer;
933
+ transition: background 0.2s;
934
+ }
935
+ .btn-primary {
936
+ background: var(--primary);
937
+ color: white;
938
+ }
939
+ .btn-primary:hover { background: var(--primary-hover); }
940
+ .btn-secondary {
941
+ background: var(--surface);
942
+ color: var(--text);
943
+ border: 1px solid #333;
944
+ }
945
+ .btn-secondary:hover { background: #1e2d4f; }
946
+ .stats {
947
+ display: flex;
948
+ justify-content: center;
949
+ gap: 32px;
950
+ }
951
+ .stat-item {
952
+ text-align: center;
953
+ }
954
+ .stat-value {
955
+ font-size: 1.6rem;
956
+ font-weight: 600;
957
+ }
958
+ .stat-label {
959
+ font-size: 0.75rem;
960
+ color: var(--text-secondary);
961
+ text-transform: uppercase;
962
+ letter-spacing: 0.5px;
963
+ margin-top: 2px;
964
+ }
965
+ `,
966
+ };
967
+
968
+ // -- Multi-file source files for Habit Tracker (formatVersion 2) --
969
+
970
+ const habitTrackerSourceFiles: Record<string, string> = {
971
+ "src/index.html": `<!DOCTYPE html>
972
+ <html lang="en">
973
+ <head>
974
+ <meta charset="UTF-8">
975
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
976
+ <title>Habit Tracker</title>
977
+ </head>
978
+ <body>
979
+ <div id="app"></div>
980
+ </body>
981
+ </html>`,
982
+
983
+ "src/main.tsx": `import { render } from "preact";
984
+ import { HabitTracker } from "./components/HabitTracker.js";
985
+ import "./styles.css";
986
+
987
+ render(<HabitTracker />, document.getElementById("app")!);
988
+ `,
989
+
990
+ "src/components/HabitTracker.tsx": `import { useCallback, useEffect, useState } from "preact/hooks";
991
+ import { HabitRow } from "./HabitRow.js";
992
+
993
+ declare const vellum: {
994
+ data: {
995
+ query(): Promise<Array<{ id: string; data: Record<string, string> }>>;
996
+ create(data: Record<string, string>): Promise<void>;
997
+ update(id: string, data: Record<string, string>): Promise<void>;
998
+ delete(id: string): Promise<void>;
999
+ };
1000
+ };
1001
+
1002
+ function getDates(): string[] {
1003
+ const dates: string[] = [];
1004
+ for (let i = 6; i >= 0; i--) {
1005
+ const d = new Date();
1006
+ d.setDate(d.getDate() - i);
1007
+ dates.push(d.toISOString().slice(0, 10));
1008
+ }
1009
+ return dates;
1010
+ }
1011
+
1012
+ function getDayNames(dates: string[]): string[] {
1013
+ const names = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
1014
+ return dates.map((d) => names[new Date(d + "T12:00:00").getDay()]);
1015
+ }
1016
+
1017
+ interface HabitRecord {
1018
+ id: string;
1019
+ data: { name: string; completedDates: string };
1020
+ }
1021
+
1022
+ export function HabitTracker() {
1023
+ const [habits, setHabits] = useState<HabitRecord[]>([]);
1024
+ const [input, setInput] = useState("");
1025
+ const dates = getDates();
1026
+ const dayNames = getDayNames(dates);
1027
+
1028
+ const loadHabits = useCallback(() => {
1029
+ vellum.data.query().then((records) => {
1030
+ setHabits(records as unknown as HabitRecord[]);
1031
+ });
1032
+ }, []);
1033
+
1034
+ useEffect(() => {
1035
+ loadHabits();
1036
+ }, [loadHabits]);
1037
+
1038
+ const addHabit = () => {
1039
+ const name = input.trim();
1040
+ if (!name) return;
1041
+ setInput("");
1042
+ vellum.data.create({ name, completedDates: "[]" }).then(loadHabits);
1043
+ };
1044
+
1045
+ const toggleDate = (recordId: string, date: string) => {
1046
+ const record = habits.find((h) => h.id === recordId);
1047
+ if (!record) return;
1048
+ let completed: string[] = [];
1049
+ try {
1050
+ completed = JSON.parse(record.data.completedDates || "[]");
1051
+ } catch {
1052
+ // ignore parse errors
1053
+ }
1054
+ const idx = completed.indexOf(date);
1055
+ if (idx === -1) completed.push(date);
1056
+ else completed.splice(idx, 1);
1057
+ vellum.data
1058
+ .update(recordId, {
1059
+ name: record.data.name,
1060
+ completedDates: JSON.stringify(completed),
1061
+ })
1062
+ .then(loadHabits);
1063
+ };
1064
+
1065
+ const deleteHabit = (recordId: string) => {
1066
+ vellum.data.delete(recordId).then(loadHabits);
1067
+ };
1068
+
1069
+ return (
1070
+ <div>
1071
+ <div class="header">
1072
+ <h1>Habit Tracker</h1>
1073
+ </div>
1074
+ <div class="add-form">
1075
+ <input
1076
+ type="text"
1077
+ value={input}
1078
+ onInput={(e) => setInput((e.target as HTMLInputElement).value)}
1079
+ onKeyDown={(e) => e.key === "Enter" && addHabit()}
1080
+ placeholder="Add a new habit..."
1081
+ />
1082
+ <button class="btn-primary" onClick={addHabit}>
1083
+ Add
1084
+ </button>
1085
+ </div>
1086
+ <div class="days-header">
1087
+ <div />
1088
+ {dayNames.map((name, i) => (
1089
+ <div key={i} class="day-label">
1090
+ {name}
1091
+ </div>
1092
+ ))}
1093
+ </div>
1094
+ <div>
1095
+ {habits.length === 0 ? (
1096
+ <div class="empty-state">No habits yet. Add one above!</div>
1097
+ ) : (
1098
+ habits.map((record) => (
1099
+ <HabitRow
1100
+ key={record.id}
1101
+ record={record}
1102
+ dates={dates}
1103
+ onToggle={toggleDate}
1104
+ onDelete={deleteHabit}
1105
+ />
1106
+ ))
1107
+ )}
1108
+ </div>
1109
+ </div>
1110
+ );
1111
+ }
1112
+ `,
1113
+
1114
+ "src/components/HabitRow.tsx": `interface HabitRowProps {
1115
+ record: { id: string; data: { name: string; completedDates: string } };
1116
+ dates: string[];
1117
+ onToggle: (id: string, date: string) => void;
1118
+ onDelete: (id: string) => void;
1119
+ }
1120
+
1121
+ export function HabitRow({ record, dates, onToggle, onDelete }: HabitRowProps) {
1122
+ let completed: string[] = [];
1123
+ try {
1124
+ completed = JSON.parse(record.data.completedDates || "[]");
1125
+ } catch {
1126
+ // ignore parse errors
1127
+ }
1128
+
1129
+ return (
1130
+ <div class="habit-row">
1131
+ <div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
1132
+ <span class="habit-name">{record.data.name}</span>
1133
+ <button class="delete-btn" onClick={() => onDelete(record.id)}>
1134
+ x
1135
+ </button>
1136
+ </div>
1137
+ {dates.map((date) => {
1138
+ const checked = completed.includes(date);
1139
+ return (
1140
+ <div key={date} class="check-cell">
1141
+ <button
1142
+ class={\`check-btn\${checked ? " checked" : ""}\`}
1143
+ onClick={() => onToggle(record.id, date)}
1144
+ >
1145
+ {checked ? "\\u2713" : ""}
1146
+ </button>
1147
+ </div>
1148
+ );
1149
+ })}
1150
+ </div>
1151
+ );
1152
+ }
1153
+ `,
1154
+
1155
+ "src/styles.css": `:root {
1156
+ --bg: #0f172a;
1157
+ --surface: #1e293b;
1158
+ --surface-hover: #263348;
1159
+ --primary: #6366f1;
1160
+ --primary-hover: #5558e6;
1161
+ --success: #22c55e;
1162
+ --text: #f1f5f9;
1163
+ --text-secondary: #94a3b8;
1164
+ --border: #334155;
1165
+ --radius: 10px;
1166
+ }
1167
+ * { margin: 0; padding: 0; box-sizing: border-box; }
1168
+ body {
1169
+ font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", sans-serif;
1170
+ background: var(--bg);
1171
+ color: var(--text);
1172
+ padding: 24px;
1173
+ min-height: 100vh;
1174
+ }
1175
+ .header {
1176
+ display: flex;
1177
+ justify-content: space-between;
1178
+ align-items: center;
1179
+ margin-bottom: 24px;
1180
+ }
1181
+ h1 { font-size: 1.4rem; font-weight: 600; }
1182
+ .add-form {
1183
+ display: flex;
1184
+ gap: 8px;
1185
+ margin-bottom: 24px;
1186
+ }
1187
+ .add-form input {
1188
+ flex: 1;
1189
+ padding: 10px 14px;
1190
+ background: var(--surface);
1191
+ border: 1px solid var(--border);
1192
+ border-radius: var(--radius);
1193
+ color: var(--text);
1194
+ font-family: inherit;
1195
+ font-size: 0.9rem;
1196
+ outline: none;
1197
+ }
1198
+ .add-form input:focus { border-color: var(--primary); }
1199
+ .add-form input::placeholder { color: var(--text-secondary); }
1200
+ button {
1201
+ font-family: inherit;
1202
+ font-size: 0.85rem;
1203
+ font-weight: 500;
1204
+ padding: 10px 18px;
1205
+ border: none;
1206
+ border-radius: var(--radius);
1207
+ cursor: pointer;
1208
+ transition: background 0.2s;
1209
+ }
1210
+ .btn-primary {
1211
+ background: var(--primary);
1212
+ color: white;
1213
+ }
1214
+ .btn-primary:hover { background: var(--primary-hover); }
1215
+ .days-header {
1216
+ display: grid;
1217
+ grid-template-columns: 1fr repeat(7, 40px);
1218
+ gap: 4px;
1219
+ margin-bottom: 8px;
1220
+ padding: 0 4px;
1221
+ }
1222
+ .day-label {
1223
+ text-align: center;
1224
+ font-size: 0.7rem;
1225
+ color: var(--text-secondary);
1226
+ text-transform: uppercase;
1227
+ }
1228
+ .habit-row {
1229
+ display: grid;
1230
+ grid-template-columns: 1fr repeat(7, 40px);
1231
+ gap: 4px;
1232
+ padding: 10px 4px;
1233
+ border-radius: var(--radius);
1234
+ margin-bottom: 4px;
1235
+ align-items: center;
1236
+ }
1237
+ .habit-row:hover { background: var(--surface); }
1238
+ .habit-name {
1239
+ font-size: 0.9rem;
1240
+ font-weight: 500;
1241
+ overflow: hidden;
1242
+ text-overflow: ellipsis;
1243
+ white-space: nowrap;
1244
+ }
1245
+ .check-cell {
1246
+ display: flex;
1247
+ justify-content: center;
1248
+ align-items: center;
1249
+ }
1250
+ .check-btn {
1251
+ width: 28px;
1252
+ height: 28px;
1253
+ border-radius: 6px;
1254
+ border: 2px solid var(--border);
1255
+ background: transparent;
1256
+ cursor: pointer;
1257
+ padding: 0;
1258
+ display: flex;
1259
+ align-items: center;
1260
+ justify-content: center;
1261
+ transition: all 0.15s;
1262
+ color: transparent;
1263
+ font-size: 14px;
1264
+ }
1265
+ .check-btn.checked {
1266
+ background: var(--success);
1267
+ border-color: var(--success);
1268
+ color: white;
1269
+ }
1270
+ .check-btn:hover { border-color: var(--success); }
1271
+ .delete-btn {
1272
+ background: transparent;
1273
+ color: var(--text-secondary);
1274
+ border: none;
1275
+ padding: 4px 8px;
1276
+ font-size: 0.8rem;
1277
+ cursor: pointer;
1278
+ border-radius: 4px;
1279
+ }
1280
+ .delete-btn:hover { color: #ef4444; background: rgba(239,68,68,0.1); }
1281
+ .empty-state {
1282
+ text-align: center;
1283
+ padding: 48px 0;
1284
+ color: var(--text-secondary);
1285
+ }
1286
+ `,
1287
+ };
1288
+
755
1289
  export const defaultGallery: GalleryManifest = {
756
1290
  version: 1,
757
1291
  updatedAt: "2026-02-15T00:00:00Z",
@@ -776,6 +1310,8 @@ export const defaultGallery: GalleryManifest = {
776
1310
  additionalProperties: false,
777
1311
  }),
778
1312
  htmlDefinition: focusTimerHtml,
1313
+ formatVersion: 2,
1314
+ sourceFiles: focusTimerSourceFiles,
779
1315
  },
780
1316
  {
781
1317
  id: "gallery-habit-tracker",
@@ -795,6 +1331,8 @@ export const defaultGallery: GalleryManifest = {
795
1331
  required: ["name", "completedDates"],
796
1332
  }),
797
1333
  htmlDefinition: habitTrackerHtml,
1334
+ formatVersion: 2,
1335
+ sourceFiles: habitTrackerSourceFiles,
798
1336
  },
799
1337
  {
800
1338
  id: "gallery-expense-tracker",
@@ -20,5 +20,9 @@ export interface GalleryApp {
20
20
  version: string; // e.g. "1.0.0"
21
21
  featured?: boolean;
22
22
  schemaJson: string; // JSON schema for app records
23
- htmlDefinition: string; // Complete HTML app
23
+ htmlDefinition: string; // Complete HTML app (also serves as compiled fallback)
24
+ /** 2 = multi-file TSX format with sourceFiles */
25
+ formatVersion?: number;
26
+ /** Maps relative path to file content, e.g. { "src/main.tsx": "...", "src/index.html": "..." } */
27
+ sourceFiles?: Record<string, string>;
24
28
  }
@@ -50,7 +50,11 @@ import type {
50
50
  ExtensionResponse,
51
51
  } from "../browser-extension-relay/protocol.js";
52
52
  import { extensionRelayServer } from "../browser-extension-relay/server.js";
53
- import { isSigningKeyInitialized } from "../runtime/auth/token-service.js";
53
+ import {
54
+ initAuthSigningKey,
55
+ isSigningKeyInitialized,
56
+ loadOrCreateSigningKey,
57
+ } from "../runtime/auth/token-service.js";
54
58
  import { gatewayPost } from "../runtime/gateway-internal-client.js";
55
59
 
56
60
  // ---------------------------------------------------------------------------
@@ -135,12 +139,10 @@ async function sendRelayCommand(
135
139
 
136
140
  // Fall back to HTTP relay endpoint via the gateway.
137
141
  // The gateway validates edge JWTs (aud=vellum-gateway) and mints an
138
- // exchange token for the runtime. Without the signing key (CLI
139
- // out-of-process), we cannot mint JWTs at all.
142
+ // exchange token for the runtime. In CLI out-of-process contexts the
143
+ // signing key may not be initialized yet — load it from disk.
140
144
  if (!isSigningKeyInitialized()) {
141
- throw new Error(
142
- "Auth signing key not initialized — browser-relay commands require the daemon to be running",
143
- );
145
+ initAuthSigningKey(loadOrCreateSigningKey());
144
146
  }
145
147
 
146
148
  const { data } = await gatewayPost<ExtensionResponse>(
package/src/mcp/client.ts CHANGED
@@ -107,7 +107,7 @@ export class McpClient {
107
107
  (err.code === 401 || err.code === 403));
108
108
 
109
109
  if (isAuthError) {
110
- // Auth-related — user can run `vellum mcp auth <name>` to authenticate.
110
+ // Auth-related — user can run `assistant mcp auth <name>` to authenticate.
111
111
  log.info(
112
112
  { serverId: this.serverId, err },
113
113
  "MCP server requires authentication",