@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
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Strategy router for avatar generation.
3
+ * Selects managed platform or local Gemini path based on config.
4
+ */
5
+
6
+ import { getConfig } from "../config/loader.js";
7
+ import { ConfigError, ProviderError } from "../util/errors.js";
8
+ import { getLogger } from "../util/logger.js";
9
+ import type {
10
+ AvatarGenerationResult,
11
+ AvatarGenerationStrategy,
12
+ } from "./avatar-types.js";
13
+ import { generateImage } from "./gemini-image-service.js";
14
+ import {
15
+ generateManagedAvatar,
16
+ isManagedAvailable,
17
+ } from "./managed-avatar-client.js";
18
+
19
+ const log = getLogger("avatar-router");
20
+
21
+ export function getAvatarStrategy(): AvatarGenerationStrategy {
22
+ return getConfig().avatar.generationStrategy;
23
+ }
24
+
25
+ async function generateLocal(
26
+ prompt: string,
27
+ correlationId?: string,
28
+ ): Promise<AvatarGenerationResult> {
29
+ const config = getConfig();
30
+ const geminiKey = config.apiKeys.gemini ?? process.env.GEMINI_API_KEY;
31
+ if (!geminiKey) {
32
+ throw new ConfigError(
33
+ "Gemini API key is not configured. Set it via `config set apiKeys.gemini <key>` or the GEMINI_API_KEY environment variable.",
34
+ );
35
+ }
36
+
37
+ const result = await generateImage(geminiKey, {
38
+ prompt,
39
+ mode: "generate",
40
+ model: config.imageGenModel,
41
+ });
42
+
43
+ const image = result.images[0];
44
+ if (!image) {
45
+ throw new ProviderError(
46
+ "Local Gemini image generation returned no images.",
47
+ "gemini",
48
+ );
49
+ }
50
+
51
+ return {
52
+ imageBase64: image.dataBase64,
53
+ mimeType: image.mimeType,
54
+ pathUsed: "local",
55
+ correlationId,
56
+ };
57
+ }
58
+
59
+ export async function routedGenerateAvatar(
60
+ prompt: string,
61
+ options?: { correlationId?: string },
62
+ ): Promise<AvatarGenerationResult> {
63
+ const strategy = getAvatarStrategy();
64
+ const correlationId = options?.correlationId;
65
+
66
+ if (strategy === "managed_required") {
67
+ const managed = await generateManagedAvatar(prompt, { correlationId });
68
+ return {
69
+ imageBase64: managed.image.data_base64,
70
+ mimeType: managed.image.mime_type,
71
+ pathUsed: "managed",
72
+ correlationId: managed.correlation_id,
73
+ };
74
+ }
75
+
76
+ if (strategy === "local_only") {
77
+ return generateLocal(prompt, correlationId);
78
+ }
79
+
80
+ // managed_prefer: try managed first if available, fall back to local
81
+ if (isManagedAvailable()) {
82
+ try {
83
+ const managed = await generateManagedAvatar(prompt, { correlationId });
84
+ return {
85
+ imageBase64: managed.image.data_base64,
86
+ mimeType: managed.image.mime_type,
87
+ pathUsed: "managed",
88
+ correlationId: managed.correlation_id,
89
+ };
90
+ } catch (err) {
91
+ log.warn(
92
+ { err: err instanceof Error ? err.message : String(err) },
93
+ "Managed avatar generation failed, falling back to local Gemini",
94
+ );
95
+ }
96
+ }
97
+
98
+ return generateLocal(prompt, correlationId);
99
+ }
@@ -0,0 +1,60 @@
1
+ export type AvatarGenerationStrategy = "managed_required" | "managed_prefer" | "local_only";
2
+
3
+ export interface ManagedAvatarImagePayload {
4
+ mime_type: string;
5
+ data_base64: string;
6
+ bytes: number;
7
+ sha256: string;
8
+ }
9
+
10
+ export interface ManagedAvatarResponse {
11
+ image: ManagedAvatarImagePayload;
12
+ usage: { billable: boolean; class_name: string };
13
+ generation_source: string;
14
+ profile: string;
15
+ correlation_id: string;
16
+ }
17
+
18
+ export interface ManagedAvatarErrorResponse {
19
+ code: string;
20
+ subcode: string;
21
+ detail: string;
22
+ retryable: boolean;
23
+ correlation_id: string;
24
+ }
25
+
26
+ export class ManagedAvatarError extends Error {
27
+ readonly code: string;
28
+ readonly subcode: string;
29
+ readonly retryable: boolean;
30
+ readonly correlationId: string;
31
+ readonly statusCode: number;
32
+
33
+ constructor(opts: {
34
+ code: string;
35
+ subcode: string;
36
+ detail: string;
37
+ retryable: boolean;
38
+ correlationId: string;
39
+ statusCode: number;
40
+ }) {
41
+ super(opts.detail);
42
+ this.name = "ManagedAvatarError";
43
+ this.code = opts.code;
44
+ this.subcode = opts.subcode;
45
+ this.retryable = opts.retryable;
46
+ this.correlationId = opts.correlationId;
47
+ this.statusCode = opts.statusCode;
48
+ }
49
+ }
50
+
51
+ export interface AvatarGenerationResult {
52
+ imageBase64: string;
53
+ mimeType: string;
54
+ pathUsed: "managed" | "local";
55
+ correlationId?: string;
56
+ }
57
+
58
+ export const AVATAR_MIME_ALLOWLIST = new Set(["image/png", "image/jpeg", "image/webp"]);
59
+ export const AVATAR_MAX_DECODED_BYTES = 10 * 1024 * 1024;
60
+ export const AVATAR_PROMPT_MAX_LENGTH = 2000;
@@ -0,0 +1,189 @@
1
+ import { getPlatformBaseUrl } from "../config/env.js";
2
+ import { getConfig } from "../config/loader.js";
3
+ import { getSecureKey } from "../security/secure-keys.js";
4
+ import { getLogger } from "../util/logger.js";
5
+ import {
6
+ AVATAR_MAX_DECODED_BYTES,
7
+ AVATAR_MIME_ALLOWLIST,
8
+ AVATAR_PROMPT_MAX_LENGTH,
9
+ ManagedAvatarError,
10
+ type ManagedAvatarErrorResponse,
11
+ type ManagedAvatarResponse,
12
+ } from "./avatar-types.js";
13
+
14
+ const log = getLogger("managed-avatar-client");
15
+
16
+ export function getAssistantApiKey(): string | undefined {
17
+ return getSecureKey("credential:vellum:assistant_api_key");
18
+ }
19
+
20
+ export function getManagedAvatarBaseUrl(): string {
21
+ const baseUrl = getConfig().platform.baseUrl || getPlatformBaseUrl();
22
+ return baseUrl.replace(/\/+$/, "");
23
+ }
24
+
25
+ export function isManagedAvailable(): boolean {
26
+ const apiKey = getAssistantApiKey();
27
+ const baseUrl = getManagedAvatarBaseUrl();
28
+ return !!apiKey && apiKey.length > 0 && !!baseUrl && baseUrl.length > 0;
29
+ }
30
+
31
+ export async function generateManagedAvatar(
32
+ prompt: string,
33
+ options?: { correlationId?: string; idempotencyKey?: string },
34
+ ): Promise<ManagedAvatarResponse> {
35
+ if (prompt.length > AVATAR_PROMPT_MAX_LENGTH) {
36
+ throw new ManagedAvatarError({
37
+ code: "validation_error",
38
+ subcode: "prompt_too_long",
39
+ detail: `Prompt exceeds maximum length of ${AVATAR_PROMPT_MAX_LENGTH} characters`,
40
+ retryable: false,
41
+ correlationId: options?.correlationId ?? crypto.randomUUID(),
42
+ statusCode: 0,
43
+ });
44
+ }
45
+
46
+ const apiKey = getAssistantApiKey();
47
+ if (!apiKey) {
48
+ throw new ManagedAvatarError({
49
+ code: "configuration_error",
50
+ subcode: "missing_api_key",
51
+ detail: "Assistant API key is not configured",
52
+ retryable: false,
53
+ correlationId: options?.correlationId ?? crypto.randomUUID(),
54
+ statusCode: 0,
55
+ });
56
+ }
57
+
58
+ const baseUrl = getManagedAvatarBaseUrl();
59
+ if (!baseUrl) {
60
+ throw new ManagedAvatarError({
61
+ code: "configuration_error",
62
+ subcode: "missing_base_url",
63
+ detail:
64
+ "Platform base URL is not configured. Set platform.baseUrl in config or PLATFORM_BASE_URL environment variable.",
65
+ retryable: false,
66
+ correlationId: options?.correlationId ?? crypto.randomUUID(),
67
+ statusCode: 0,
68
+ });
69
+ }
70
+
71
+ const url = `${baseUrl}/v1/assistants/avatar/generate/`;
72
+ const idempotencyKey = options?.idempotencyKey ?? crypto.randomUUID();
73
+ const correlationId = options?.correlationId ?? crypto.randomUUID();
74
+
75
+ const headers: Record<string, string> = {
76
+ Authorization: `Api-Key ${apiKey}`,
77
+ "Content-Type": "application/json",
78
+ "Idempotency-Key": idempotencyKey,
79
+ "X-Correlation-Id": correlationId,
80
+ };
81
+
82
+ log.debug({ url, correlationId }, "Requesting managed avatar generation");
83
+
84
+ let response: Response;
85
+ try {
86
+ response = await fetch(url, {
87
+ method: "POST",
88
+ body: JSON.stringify({ prompt }),
89
+ signal: AbortSignal.timeout(60_000),
90
+ headers,
91
+ });
92
+ } catch (err) {
93
+ const isTimeout =
94
+ err instanceof DOMException && err.name === "TimeoutError";
95
+ throw new ManagedAvatarError({
96
+ code: "avatar_generation_failed",
97
+ subcode: isTimeout ? "upstream_timeout" : "network_error",
98
+ detail: isTimeout
99
+ ? "Request to avatar generation service timed out"
100
+ : `Network error: ${err instanceof Error ? err.message : String(err)}`,
101
+ retryable: true,
102
+ correlationId,
103
+ statusCode: 0,
104
+ });
105
+ }
106
+
107
+ if (!response.ok) {
108
+ let errorBody: ManagedAvatarErrorResponse;
109
+ try {
110
+ errorBody = (await response.json()) as ManagedAvatarErrorResponse;
111
+ } catch {
112
+ throw new ManagedAvatarError({
113
+ code: "upstream_error",
114
+ subcode: "unparseable_response",
115
+ detail: `HTTP ${response.status}: unable to parse error response`,
116
+ retryable: response.status >= 500 || response.status === 429,
117
+ correlationId,
118
+ statusCode: response.status,
119
+ });
120
+ }
121
+
122
+ throw new ManagedAvatarError({
123
+ code: errorBody.code ?? "upstream_error",
124
+ subcode: errorBody.subcode ?? "unknown",
125
+ detail: errorBody.detail ?? `HTTP ${response.status}`,
126
+ retryable:
127
+ errorBody.retryable ??
128
+ (response.status >= 500 || response.status === 429),
129
+ correlationId: errorBody.correlation_id ?? correlationId,
130
+ statusCode: response.status,
131
+ });
132
+ }
133
+
134
+ let body: ManagedAvatarResponse;
135
+ try {
136
+ body = (await response.json()) as ManagedAvatarResponse;
137
+
138
+ if (!AVATAR_MIME_ALLOWLIST.has(body.image.mime_type)) {
139
+ throw new ManagedAvatarError({
140
+ code: "validation_error",
141
+ subcode: "disallowed_mime_type",
142
+ detail: `Response MIME type "${body.image.mime_type}" is not in the allowlist`,
143
+ retryable: false,
144
+ correlationId,
145
+ statusCode: 0,
146
+ });
147
+ }
148
+
149
+ const b64 = body.image.data_base64;
150
+ const padding = b64.endsWith("==") ? 2 : b64.endsWith("=") ? 1 : 0;
151
+ const estimatedDecodedBytes = Math.ceil((b64.length * 3) / 4) - padding;
152
+ if (
153
+ estimatedDecodedBytes > AVATAR_MAX_DECODED_BYTES ||
154
+ body.image.bytes > AVATAR_MAX_DECODED_BYTES
155
+ ) {
156
+ throw new ManagedAvatarError({
157
+ code: "validation_error",
158
+ subcode: "oversized_image",
159
+ detail: `Response image size ${Math.max(estimatedDecodedBytes, body.image.bytes)} exceeds maximum of ${AVATAR_MAX_DECODED_BYTES} bytes`,
160
+ retryable: false,
161
+ correlationId,
162
+ statusCode: 0,
163
+ });
164
+ }
165
+
166
+ log.debug(
167
+ {
168
+ correlationId,
169
+ mimeType: body.image.mime_type,
170
+ bytes: body.image.bytes,
171
+ },
172
+ "Managed avatar generation succeeded",
173
+ );
174
+
175
+ return body;
176
+ } catch (err) {
177
+ if (err instanceof ManagedAvatarError) {
178
+ throw err;
179
+ }
180
+ throw new ManagedAvatarError({
181
+ code: "upstream_error",
182
+ subcode: "unparseable_response",
183
+ detail: `Failed to parse avatar generation response: ${err instanceof Error ? err.message : String(err)}`,
184
+ retryable: false,
185
+ correlationId,
186
+ statusCode: 0,
187
+ });
188
+ }
189
+ }
@@ -0,0 +1,114 @@
1
+ /**
2
+ * One-way migration helper: converts legacy single-HTML apps to the
3
+ * multi-file src/ layout (formatVersion 2).
4
+ *
5
+ * Non-destructive — the root index.html is preserved as a legacy fallback.
6
+ */
7
+
8
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
9
+ import { join } from "node:path";
10
+
11
+ import { getApp, getAppsDir, isMultifileApp } from "./app-store.js";
12
+
13
+ export interface MigrationResult {
14
+ ok: boolean;
15
+ error?: string;
16
+ }
17
+
18
+ /**
19
+ * Extract inline `<style>` blocks from HTML, returning the extracted CSS
20
+ * and the HTML with those blocks replaced by a `<link>` tag.
21
+ */
22
+ function extractInlineStyles(html: string): {
23
+ css: string;
24
+ html: string;
25
+ } {
26
+ const styleRegex = /<style[^>]*>([\s\S]*?)<\/style>/gi;
27
+ const cssChunks: string[] = [];
28
+
29
+ for (const match of html.matchAll(styleRegex)) {
30
+ cssChunks.push(match[1].trim());
31
+ }
32
+
33
+ if (cssChunks.length === 0) {
34
+ return { css: "", html };
35
+ }
36
+
37
+ const css = cssChunks.join("\n\n");
38
+
39
+ // Replace the first <style> block with a <link> tag, remove the rest
40
+ let replaced = false;
41
+ const updatedHtml = html.replace(styleRegex, () => {
42
+ if (!replaced) {
43
+ replaced = true;
44
+ return `<link rel="stylesheet" href="styles.css">`;
45
+ }
46
+ return "";
47
+ });
48
+
49
+ return { css, html: updatedHtml };
50
+ }
51
+
52
+ /**
53
+ * Migrate a legacy single-HTML app to the multi-file src/ layout.
54
+ *
55
+ * Steps:
56
+ * 1. Read existing index.html
57
+ * 2. Create {appId}/src/ directory
58
+ * 3. Copy HTML content to src/index.html (with inline styles extracted)
59
+ * 4. Create src/main.tsx placeholder entry point
60
+ * 5. If inline styles found, write src/styles.css
61
+ * 6. Update app metadata to formatVersion: 2
62
+ * 7. Keep root index.html untouched (legacy fallback)
63
+ */
64
+ export function migrateAppToMultifile(appId: string): MigrationResult {
65
+ const app = getApp(appId);
66
+ if (!app) {
67
+ return { ok: false, error: `App not found: ${appId}` };
68
+ }
69
+
70
+ // Already migrated — treat as no-op
71
+ if (isMultifileApp(app)) {
72
+ return { ok: true };
73
+ }
74
+
75
+ const appsDir = getAppsDir();
76
+ const appDir = join(appsDir, appId);
77
+ const rootIndex = join(appDir, "index.html");
78
+
79
+ if (!existsSync(rootIndex)) {
80
+ return { ok: false, error: `Root index.html not found for app ${appId}` };
81
+ }
82
+
83
+ const originalHtml = readFileSync(rootIndex, "utf-8");
84
+
85
+ // Create src/ directory
86
+ const srcDir = join(appDir, "src");
87
+ mkdirSync(srcDir, { recursive: true });
88
+
89
+ // Extract inline styles if present
90
+ const { css, html: processedHtml } = extractInlineStyles(originalHtml);
91
+
92
+ // Write src/index.html
93
+ writeFileSync(join(srcDir, "index.html"), processedHtml, "utf-8");
94
+
95
+ // Write src/main.tsx placeholder
96
+ const styleImport = css ? "import './styles.css';\n" : "";
97
+ const mainTsx = `// Entry point — migrated from legacy single-file app\n${styleImport}\nconsole.log('App loaded');\n`;
98
+ writeFileSync(join(srcDir, "main.tsx"), mainTsx, "utf-8");
99
+
100
+ // Write src/styles.css if styles were extracted
101
+ if (css) {
102
+ writeFileSync(join(srcDir, "styles.css"), css, "utf-8");
103
+ }
104
+
105
+ // Update metadata to formatVersion 2
106
+ const metadataPath = join(appsDir, `${appId}.json`);
107
+ const rawMeta = readFileSync(metadataPath, "utf-8");
108
+ const metadata = JSON.parse(rawMeta);
109
+ metadata.formatVersion = 2;
110
+ metadata.updatedAt = Date.now();
111
+ writeFileSync(metadataPath, JSON.stringify(metadata, null, 2));
112
+
113
+ return { ok: true };
114
+ }
@@ -51,6 +51,15 @@ export interface AppDefinition {
51
51
  pages?: Record<string, string>;
52
52
  createdAt: number;
53
53
  updatedAt: number;
54
+ /** App format version. undefined or 1 = legacy single-HTML, 2 = multi-file TSX. */
55
+ formatVersion?: number;
56
+ }
57
+
58
+ /**
59
+ * Returns true if the app uses the multi-file TSX format (formatVersion 2).
60
+ */
61
+ export function isMultifileApp(app: AppDefinition): boolean {
62
+ return app.formatVersion === 2;
54
63
  }
55
64
 
56
65
  export interface AppRecord {
@@ -195,6 +204,7 @@ export function createApp(params: {
195
204
  htmlDefinition: string;
196
205
  version?: string;
197
206
  pages?: Record<string, string>;
207
+ formatVersion?: number;
198
208
  }): AppDefinition {
199
209
  const dir = getAppsDir();
200
210
  const now = Date.now();
@@ -209,6 +219,7 @@ export function createApp(params: {
209
219
  version: params.version,
210
220
  createdAt: now,
211
221
  updatedAt: now,
222
+ formatVersion: params.formatVersion,
212
223
  };
213
224
 
214
225
  // Write htmlDefinition to {appId}/index.html on disk
@@ -365,7 +365,7 @@ export class VellumQdrantClient {
365
365
  /**
366
366
  * Detect "collection not found" errors from Qdrant so callers can
367
367
  * reset collectionReady and retry after an external deletion
368
- * (e.g. `vellum sessions clear`).
368
+ * (e.g. `assistant sessions clear`).
369
369
  */
370
370
  private isCollectionMissing(err: unknown): boolean {
371
371
  if (
@@ -231,14 +231,24 @@ export async function userInfo(
231
231
  return request<SlackUserInfoResponse>(token, "users.info", { user: userId });
232
232
  }
233
233
 
234
+ export interface PostMessageOptions {
235
+ threadTs?: string;
236
+ blocks?: unknown[];
237
+ }
238
+
234
239
  export async function postMessage(
235
240
  token: string,
236
241
  channel: string,
237
242
  text: string,
238
- threadTs?: string,
243
+ optionsOrThreadTs?: PostMessageOptions | string,
239
244
  ): Promise<SlackPostMessageResponse> {
245
+ const opts: PostMessageOptions =
246
+ typeof optionsOrThreadTs === "string"
247
+ ? { threadTs: optionsOrThreadTs }
248
+ : (optionsOrThreadTs ?? {});
240
249
  const body: Record<string, unknown> = { channel, text };
241
- if (threadTs) body.thread_ts = threadTs;
250
+ if (opts.threadTs) body.thread_ts = opts.threadTs;
251
+ if (opts.blocks) body.blocks = opts.blocks;
242
252
  return request<SlackPostMessageResponse>(
243
253
  token,
244
254
  "chat.postMessage",
@@ -14,6 +14,10 @@
14
14
  * a per-user OAuth token.
15
15
  */
16
16
 
17
+ import {
18
+ getTwilioCredentials,
19
+ hasTwilioCredentials,
20
+ } from "../../../calls/twilio-rest.js";
17
21
  import {
18
22
  getGatewayInternalBaseUrl,
19
23
  getTwilioPhoneNumberEnv,
@@ -48,14 +52,6 @@ function getBearerToken(): string {
48
52
  return mintDaemonDeliveryToken();
49
53
  }
50
54
 
51
- /** Check whether Twilio credentials are stored. */
52
- function hasTwilioCredentials(): boolean {
53
- return (
54
- !!getSecureKey("credential:twilio:account_sid") &&
55
- !!getSecureKey("credential:twilio:auth_token")
56
- );
57
- }
58
-
59
55
  /** Resolve the configured SMS phone number. */
60
56
  function getPhoneNumber(): string | undefined {
61
57
  const fromEnv = getTwilioPhoneNumberEnv();
@@ -118,7 +114,7 @@ export const smsMessagingProvider: MessagingProvider = {
118
114
  | Record<string, string>
119
115
  | undefined;
120
116
  if (mappings && Object.keys(mappings).length > 0) {
121
- const accountSid = getSecureKey("credential:twilio:account_sid")!;
117
+ const accountSid = getTwilioCredentials().accountSid;
122
118
  return {
123
119
  connected: true,
124
120
  user: "assistant-scoped",
@@ -143,7 +139,7 @@ export const smsMessagingProvider: MessagingProvider = {
143
139
  };
144
140
  }
145
141
 
146
- const accountSid = getSecureKey("credential:twilio:account_sid")!;
142
+ const accountSid = getTwilioCredentials().accountSid;
147
143
 
148
144
  return {
149
145
  connected: true,
@@ -51,7 +51,14 @@ export function migrateToDataLayout(): void {
51
51
  migrateItem(join(root, "qdrant.pid"), join(data, "qdrant", "qdrant.pid"));
52
52
 
53
53
  // Qdrant binary: ~/.vellum/bin/ → ~/.vellum/data/qdrant/bin/
54
- migrateItem(join(root, "bin"), join(data, "qdrant", "bin"));
54
+ // Only migrate if the directory actually contains a qdrant binary.
55
+ // After the CLI-launcher feature landed, ~/.vellum/bin/ is used for
56
+ // launcher scripts (doordash, map, etc.), not qdrant, so moving it
57
+ // blindly would break CLI launchers on every fresh hatch.
58
+ const legacyBinDir = join(root, "bin");
59
+ if (existsSync(join(legacyBinDir, "qdrant"))) {
60
+ migrateItem(legacyBinDir, join(data, "qdrant", "bin"));
61
+ }
55
62
 
56
63
  // Logs: ~/.vellum/logs/ → ~/.vellum/data/logs/
57
64
  migrateItem(join(root, "logs"), join(data, "logs"));
@@ -10,7 +10,10 @@ import type {
10
10
  OAuth2FlowResult,
11
11
  TokenEndpointAuthMethod,
12
12
  } from "../security/oauth2.js";
13
- import { deleteSecureKey, setSecureKey } from "../security/secure-keys.js";
13
+ import {
14
+ deleteSecureKeyAsync,
15
+ setSecureKeyAsync,
16
+ } from "../security/secure-keys.js";
14
17
  import {
15
18
  deleteCredentialMetadata,
16
19
  upsertCredentialMetadata,
@@ -67,7 +70,7 @@ export async function storeOAuth2Tokens(
67
70
  wellKnownInjectionTemplates,
68
71
  } = params;
69
72
 
70
- const tokenStored = setSecureKey(
73
+ const tokenStored = await setSecureKeyAsync(
71
74
  `credential:${service}:access_token`,
72
75
  tokens.accessToken,
73
76
  );
@@ -95,7 +98,7 @@ export async function storeOAuth2Tokens(
95
98
  }
96
99
 
97
100
  // Persist client credentials in keychain for defense in depth
98
- const clientIdStored = setSecureKey(
101
+ const clientIdStored = await setSecureKeyAsync(
99
102
  `credential:${service}:client_id`,
100
103
  clientId,
101
104
  );
@@ -103,7 +106,7 @@ export async function storeOAuth2Tokens(
103
106
  throw new Error("Failed to store client_id in secure storage");
104
107
  }
105
108
  if (clientSecret) {
106
- const clientSecretStored = setSecureKey(
109
+ const clientSecretStored = await setSecureKeyAsync(
107
110
  `credential:${service}:client_secret`,
108
111
  clientSecret,
109
112
  );
@@ -129,7 +132,7 @@ export async function storeOAuth2Tokens(
129
132
  });
130
133
 
131
134
  if (tokens.refreshToken) {
132
- const refreshStored = setSecureKey(
135
+ const refreshStored = await setSecureKeyAsync(
133
136
  `credential:${service}:refresh_token`,
134
137
  tokens.refreshToken,
135
138
  );
@@ -140,7 +143,7 @@ export async function storeOAuth2Tokens(
140
143
  // Re-auth grants that omit refresh_token must clear any stale stored
141
144
  // token — otherwise withValidToken() will attempt refresh with invalid
142
145
  // credentials.
143
- deleteSecureKey(`credential:${service}:refresh_token`);
146
+ await deleteSecureKeyAsync(`credential:${service}:refresh_token`);
144
147
  deleteCredentialMetadata(service, "refresh_token");
145
148
  }
146
149
 
@@ -6,5 +6,10 @@
6
6
  * gateway and platform layers (hatch, invite links, etc.). Daemon code
7
7
  * should never derive scoping decisions from externally-provided assistant
8
8
  * IDs — use this constant instead.
9
+ *
10
+ * Multi-instance invariant: each daemon process is single-tenant within
11
+ * its own BASE_DATA_DIR. The fixed "self" value works across multiple
12
+ * local instances because each instance has isolated storage — there is
13
+ * no cross-instance data sharing that would require disambiguating IDs.
9
14
  */
10
15
  export const DAEMON_INTERNAL_ASSISTANT_ID = "self" as const;
@@ -244,6 +244,10 @@ const ACTOR_ENDPOINTS: Array<{ endpoint: string; scopes: Scope[] }> = [
244
244
  { endpoint: "integrations/twilio/sms/test", scopes: ["settings.write"] },
245
245
  { endpoint: "integrations/twilio/sms/doctor", scopes: ["settings.write"] },
246
246
 
247
+ // Slack share
248
+ { endpoint: "slack/channels", scopes: ["settings.read"] },
249
+ { endpoint: "slack/share", scopes: ["settings.write"] },
250
+
247
251
  // Channel readiness
248
252
  { endpoint: "channels/readiness", scopes: ["settings.read"] },
249
253
  { endpoint: "channels/readiness/refresh", scopes: ["settings.write"] },