@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
@@ -1,631 +1,441 @@
1
1
  import { describe, expect, test } from "bun:test";
2
2
 
3
3
  import type { AppDefinition } from "../memory/app-store.js";
4
- import type { AppStore, ProxyResolver } from "../tools/apps/executors.js";
4
+ import type { AppStore } from "../tools/apps/executors.js";
5
5
  import {
6
6
  executeAppCreate,
7
- executeAppDelete,
8
7
  executeAppFileEdit,
9
8
  executeAppFileList,
10
9
  executeAppFileRead,
11
10
  executeAppFileWrite,
12
- executeAppList,
13
- executeAppQuery,
14
- executeAppUpdate,
11
+ resolveAppFilePath,
15
12
  } from "../tools/apps/executors.js";
16
- import type { EditEngineResult } from "../tools/shared/filesystem/edit-engine.js";
17
13
 
18
14
  // ---------------------------------------------------------------------------
19
- // Mock factory
15
+ // Helpers
20
16
  // ---------------------------------------------------------------------------
21
17
 
22
- function makeApp(overrides: Partial<AppDefinition> = {}): AppDefinition {
18
+ function makeLegacyApp(overrides?: Partial<AppDefinition>): AppDefinition {
23
19
  return {
24
- id: "app-1",
25
- name: "Test App",
26
- description: "A test app",
20
+ id: "legacy-app",
21
+ name: "Legacy App",
27
22
  schemaJson: "{}",
28
- htmlDefinition: "<h1>Hi</h1>",
29
- createdAt: 1000,
30
- updatedAt: 2000,
23
+ htmlDefinition: "<html></html>",
24
+ createdAt: Date.now(),
25
+ updatedAt: Date.now(),
31
26
  ...overrides,
32
27
  };
33
28
  }
34
29
 
35
- function makeMockStore(overrides: Partial<AppStore> = {}): AppStore {
30
+ function makeMultifileApp(overrides?: Partial<AppDefinition>): AppDefinition {
36
31
  return {
37
- getApp: () => makeApp(),
38
- listApps: () => [makeApp()],
32
+ id: "multi-app",
33
+ name: "Multifile App",
34
+ schemaJson: "{}",
35
+ htmlDefinition: "",
36
+ formatVersion: 2,
37
+ createdAt: Date.now(),
38
+ updatedAt: Date.now(),
39
+ ...overrides,
40
+ };
41
+ }
42
+
43
+ /**
44
+ * Builds a minimal mock AppStore that tracks writes/edits/reads
45
+ * against an in-memory file map.
46
+ */
47
+ function mockStore(
48
+ app: AppDefinition,
49
+ files: Record<string, string> = {},
50
+ ): AppStore {
51
+ return {
52
+ getApp: (id: string) => (id === app.id ? app : null),
53
+ listApps: () => [app],
39
54
  queryAppRecords: () => [],
40
- listAppFiles: () => ["index.html"],
41
- readAppFile: () => "<h1>Hi</h1>",
42
- createApp: (params) =>
43
- makeApp({ name: params.name, description: params.description }),
44
- updateApp: (id, updates) => makeApp({ id, ...updates }),
55
+ listAppFiles: () => Object.keys(files).sort(),
56
+ readAppFile: (_appId: string, path: string) => {
57
+ if (!(path in files)) throw new Error(`File not found: ${path}`);
58
+ return files[path];
59
+ },
60
+ createApp: () => app,
61
+ updateApp: () => app,
45
62
  deleteApp: () => {},
46
- writeAppFile: () => {},
47
- editAppFile: () =>
48
- ({
49
- ok: true,
50
- updatedContent: "new",
63
+ writeAppFile: (_appId: string, path: string, content: string) => {
64
+ files[path] = content;
65
+ },
66
+ editAppFile: (
67
+ _appId: string,
68
+ path: string,
69
+ oldStr: string,
70
+ newStr: string,
71
+ _replaceAll?: boolean,
72
+ ) => {
73
+ if (!(path in files)) throw new Error(`File not found: ${path}`);
74
+ const content = files[path];
75
+ if (!content.includes(oldStr)) {
76
+ return { ok: false as const, reason: "not_found" as const };
77
+ }
78
+ const updated = content.replace(oldStr, newStr);
79
+ files[path] = updated;
80
+ return {
81
+ ok: true as const,
82
+ updatedContent: updated,
51
83
  matchCount: 1,
52
- matchMethod: "exact",
84
+ matchMethod: "exact" as const,
53
85
  similarity: 1,
54
- actualOld: "old",
55
- actualNew: "new",
56
- }) as EditEngineResult,
57
- ...overrides,
86
+ actualOld: oldStr,
87
+ actualNew: newStr,
88
+ };
89
+ },
58
90
  };
59
91
  }
60
92
 
61
93
  // ---------------------------------------------------------------------------
62
- // app_create
94
+ // resolveAppFilePath
63
95
  // ---------------------------------------------------------------------------
64
96
 
65
- describe("executeAppCreate", () => {
66
- test("creates an app and returns its definition", async () => {
67
- const store = makeMockStore();
68
- const result = await executeAppCreate(
69
- { name: "My App", html: "<p>Hello</p>" },
70
- store,
71
- );
72
- expect(result.isError).toBe(false);
73
- const parsed = JSON.parse(result.content);
74
- expect(parsed.name).toBe("My App");
75
- });
76
-
77
- test('defaults schema_json to "{}" when not provided', async () => {
78
- let capturedSchema: string | undefined;
79
- const store = makeMockStore({
80
- createApp: (params) => {
81
- capturedSchema = params.schemaJson;
82
- return makeApp({ name: params.name });
83
- },
84
- });
85
- await executeAppCreate({ name: "App", html: "<p/>" }, store);
86
- expect(capturedSchema).toBe("{}");
97
+ describe("resolveAppFilePath", () => {
98
+ test("prepends src/ for multifile app with plain path", () => {
99
+ const app = makeMultifileApp();
100
+ expect(resolveAppFilePath(app, "main.tsx")).toBe("src/main.tsx");
87
101
  });
88
102
 
89
- test("passes schema_json through when provided", async () => {
90
- let capturedSchema: string | undefined;
91
- const store = makeMockStore({
92
- createApp: (params) => {
93
- capturedSchema = params.schemaJson;
94
- return makeApp({ name: params.name });
95
- },
96
- });
97
- await executeAppCreate(
98
- { name: "App", html: "<p/>", schema_json: '{"type":"object"}' },
99
- store,
103
+ test("prepends src/ for nested path in multifile app", () => {
104
+ const app = makeMultifileApp();
105
+ expect(resolveAppFilePath(app, "components/Header.tsx")).toBe(
106
+ "src/components/Header.tsx",
100
107
  );
101
- expect(capturedSchema).toBe('{"type":"object"}');
102
108
  });
103
109
 
104
- test("auto-opens the app when proxyToolResolver is provided", async () => {
105
- const store = makeMockStore();
106
- const proxy: ProxyResolver = async () => ({
107
- content: "opened",
108
- isError: false,
109
- });
110
- const result = await executeAppCreate(
111
- { name: "Auto", html: "<p/>" },
112
- store,
113
- proxy,
114
- );
115
- expect(result.isError).toBe(false);
116
- const parsed = JSON.parse(result.content);
117
- expect(parsed.auto_opened).toBe(true);
118
- expect(parsed.open_result).toBe("opened");
110
+ test("passes through src/ prefix unchanged for multifile app", () => {
111
+ const app = makeMultifileApp();
112
+ expect(resolveAppFilePath(app, "src/main.tsx")).toBe("src/main.tsx");
119
113
  });
120
114
 
121
- test("returns auto_opened=false when proxy resolver throws", async () => {
122
- const store = makeMockStore();
123
- const proxy: ProxyResolver = async () => {
124
- throw new Error("no client");
125
- };
126
- const result = await executeAppCreate(
127
- { name: "Fail Open", html: "<p/>" },
128
- store,
129
- proxy,
130
- );
131
- expect(result.isError).toBe(false);
132
- const parsed = JSON.parse(result.content);
133
- expect(parsed.auto_opened).toBe(false);
134
- expect(parsed.auto_open_error).toBe(
135
- "Failed to auto-open app. Use app_open to open it manually.",
136
- );
115
+ test("passes through dist/ prefix unchanged for multifile app", () => {
116
+ const app = makeMultifileApp();
117
+ expect(resolveAppFilePath(app, "dist/bundle.js")).toBe("dist/bundle.js");
137
118
  });
138
119
 
139
- test("skips auto-open when auto_open is false", async () => {
140
- let proxyCalled = false;
141
- const store = makeMockStore();
142
- const proxy: ProxyResolver = async () => {
143
- proxyCalled = true;
144
- return { content: "opened", isError: false };
145
- };
146
- const result = await executeAppCreate(
147
- { name: "No Open", html: "<p/>", auto_open: false },
148
- store,
149
- proxy,
120
+ test("passes through records/ prefix unchanged for multifile app", () => {
121
+ const app = makeMultifileApp();
122
+ expect(resolveAppFilePath(app, "records/data.json")).toBe(
123
+ "records/data.json",
150
124
  );
151
- expect(proxyCalled).toBe(false);
152
- expect(result.isError).toBe(false);
153
- const parsed = JSON.parse(result.content);
154
- expect(parsed.auto_opened).toBeUndefined();
155
125
  });
156
126
 
157
- test("skips auto-open when no proxyToolResolver", async () => {
158
- const store = makeMockStore();
159
- const result = await executeAppCreate(
160
- { name: "No Proxy", html: "<p/>" },
161
- store,
162
- );
163
- expect(result.isError).toBe(false);
164
- const parsed = JSON.parse(result.content);
165
- expect(parsed.auto_opened).toBeUndefined();
127
+ test("does not modify path for legacy app", () => {
128
+ const app = makeLegacyApp();
129
+ expect(resolveAppFilePath(app, "main.tsx")).toBe("main.tsx");
166
130
  });
167
131
 
168
- test("passes pages through to store.createApp", async () => {
169
- let capturedPages: Record<string, string> | undefined;
170
- const store = makeMockStore({
171
- createApp: (params) => {
172
- capturedPages = params.pages;
173
- return makeApp({ name: params.name });
174
- },
175
- });
176
- await executeAppCreate(
177
- { name: "Multi", html: "<p/>", pages: { "settings.html": "<div/>" } },
178
- store,
179
- );
180
- expect(capturedPages).toEqual({ "settings.html": "<div/>" });
132
+ test("does not modify path for legacy app (formatVersion undefined)", () => {
133
+ const app = makeLegacyApp({ formatVersion: undefined });
134
+ expect(resolveAppFilePath(app, "styles.css")).toBe("styles.css");
181
135
  });
182
136
 
183
- test("defaults html to minimal scaffold when omitted", async () => {
184
- let capturedHtml: string | undefined;
185
- const store = makeMockStore({
186
- createApp: (params) => {
187
- capturedHtml = params.htmlDefinition;
188
- return makeApp({ name: params.name });
189
- },
190
- });
191
- await executeAppCreate({ name: "No HTML" }, store);
192
- expect(capturedHtml).toBe(
193
- "<!DOCTYPE html><html><head></head><body></body></html>",
194
- );
137
+ test("does not modify path for legacy app (formatVersion 1)", () => {
138
+ const app = makeLegacyApp({ formatVersion: 1 });
139
+ expect(resolveAppFilePath(app, "index.html")).toBe("index.html");
195
140
  });
196
- });
197
-
198
- // ---------------------------------------------------------------------------
199
- // app_list
200
- // ---------------------------------------------------------------------------
201
141
 
202
- describe("executeAppList", () => {
203
- test("returns mapped list of apps", () => {
204
- const store = makeMockStore({
205
- listApps: () => [
206
- makeApp({
207
- id: "a1",
208
- name: "First",
209
- description: "desc1",
210
- updatedAt: 100,
211
- }),
212
- makeApp({ id: "a2", name: "Second", updatedAt: 200 }),
213
- ],
214
- });
215
- const result = executeAppList(store);
216
- expect(result.isError).toBe(false);
217
- const parsed = JSON.parse(result.content);
218
- expect(parsed).toHaveLength(2);
219
- expect(parsed[0]).toEqual({
220
- id: "a1",
221
- name: "First",
222
- description: "desc1",
223
- updatedAt: 100,
224
- });
225
- expect(parsed[1].id).toBe("a2");
226
- // Should not include htmlDefinition or schemaJson
227
- expect(parsed[0].htmlDefinition).toBeUndefined();
228
- expect(parsed[0].schemaJson).toBeUndefined();
142
+ test("strips ./ prefix and prepends src/ for multifile app", () => {
143
+ const app = makeMultifileApp();
144
+ expect(resolveAppFilePath(app, "./main.tsx")).toBe("src/main.tsx");
229
145
  });
230
146
 
231
- test("returns empty array when no apps exist", () => {
232
- const store = makeMockStore({ listApps: () => [] });
233
- const result = executeAppList(store);
234
- expect(result.isError).toBe(false);
235
- expect(JSON.parse(result.content)).toEqual([]);
147
+ test("strips ./ prefix for known top-level dir in multifile app", () => {
148
+ const app = makeMultifileApp();
149
+ expect(resolveAppFilePath(app, "./src/main.tsx")).toBe("src/main.tsx");
150
+ expect(resolveAppFilePath(app, "./dist/bundle.js")).toBe("dist/bundle.js");
151
+ expect(resolveAppFilePath(app, "./records/data.json")).toBe(
152
+ "records/data.json",
153
+ );
236
154
  });
237
155
  });
238
156
 
239
157
  // ---------------------------------------------------------------------------
240
- // app_query
158
+ // executeAppFileWrite
241
159
  // ---------------------------------------------------------------------------
242
160
 
243
- describe("executeAppQuery", () => {
244
- test("returns records for a given app", () => {
245
- const records = [{ id: "r1", appId: "app-1", data: { x: 1 } }];
246
- const store = makeMockStore({ queryAppRecords: () => records });
247
- const result = executeAppQuery({ app_id: "app-1" }, store);
248
- expect(result.isError).toBe(false);
249
- expect(JSON.parse(result.content)).toEqual(records);
250
- });
251
-
252
- test("returns empty array when no records", () => {
253
- const store = makeMockStore({ queryAppRecords: () => [] });
254
- const result = executeAppQuery({ app_id: "app-1" }, store);
255
- expect(result.isError).toBe(false);
256
- expect(JSON.parse(result.content)).toEqual([]);
257
- });
258
- });
259
-
260
- // ---------------------------------------------------------------------------
261
- // app_update
262
- // ---------------------------------------------------------------------------
161
+ describe("executeAppFileWrite", () => {
162
+ test("resolves plain path to src/ for multifile app", () => {
163
+ const files: Record<string, string> = {};
164
+ const app = makeMultifileApp();
165
+ const store = mockStore(app, files);
263
166
 
264
- describe("executeAppUpdate", () => {
265
- test("passes update fields through to store", () => {
266
- let capturedUpdates: Record<string, unknown> = {};
267
- const store = makeMockStore({
268
- updateApp: (_id, updates) => {
269
- capturedUpdates = updates;
270
- return makeApp({ id: _id, ...updates });
271
- },
272
- });
273
- const result = executeAppUpdate(
274
- {
275
- app_id: "app-1",
276
- name: "New Name",
277
- description: "New desc",
278
- schema_json: '{"a":1}',
279
- html: "<div/>",
280
- pages: { "about.html": "<p/>" },
281
- },
167
+ const result = executeAppFileWrite(
168
+ { app_id: app.id, path: "main.tsx", content: "export default 1;" },
282
169
  store,
283
170
  );
171
+
284
172
  expect(result.isError).toBe(false);
285
- expect(capturedUpdates).toEqual({
286
- name: "New Name",
287
- description: "New desc",
288
- schemaJson: '{"a":1}',
289
- htmlDefinition: "<div/>",
290
- pages: { "about.html": "<p/>" },
291
- });
173
+ expect(JSON.parse(result.content).path).toBe("src/main.tsx");
174
+ expect(files["src/main.tsx"]).toBe("export default 1;");
292
175
  });
293
176
 
294
- test("only includes provided fields in updates", () => {
295
- let capturedUpdates: Record<string, unknown> = {};
296
- const store = makeMockStore({
297
- updateApp: (_id, updates) => {
298
- capturedUpdates = updates;
299
- return makeApp({ id: _id, ...updates });
300
- },
301
- });
302
- executeAppUpdate({ app_id: "app-1", name: "Only Name" }, store);
303
- expect(capturedUpdates).toEqual({ name: "Only Name" });
304
- // html, description, schema_json, pages should NOT be in the updates
305
- expect("htmlDefinition" in capturedUpdates).toBe(false);
306
- expect("description" in capturedUpdates).toBe(false);
307
- expect("schemaJson" in capturedUpdates).toBe(false);
308
- expect("pages" in capturedUpdates).toBe(false);
309
- });
177
+ test("passes through src/ path unchanged for multifile app", () => {
178
+ const files: Record<string, string> = {};
179
+ const app = makeMultifileApp();
180
+ const store = mockStore(app, files);
310
181
 
311
- test("propagates store errors", () => {
312
- const store = makeMockStore({
313
- updateApp: () => {
314
- throw new Error("App not found: bad-id");
315
- },
316
- });
317
- expect(() => executeAppUpdate({ app_id: "bad-id" }, store)).toThrow(
318
- "App not found: bad-id",
182
+ const result = executeAppFileWrite(
183
+ { app_id: app.id, path: "src/main.tsx", content: "export default 2;" },
184
+ store,
319
185
  );
320
- });
321
- });
322
-
323
- // ---------------------------------------------------------------------------
324
- // app_delete
325
- // ---------------------------------------------------------------------------
326
186
 
327
- describe("executeAppDelete", () => {
328
- test("deletes the app and returns confirmation", () => {
329
- let deletedId: string | undefined;
330
- const store = makeMockStore({
331
- deleteApp: (id) => {
332
- deletedId = id;
333
- },
334
- });
335
- const result = executeAppDelete({ app_id: "app-1" }, store);
336
187
  expect(result.isError).toBe(false);
337
- expect(JSON.parse(result.content)).toEqual({
338
- deleted: true,
339
- appId: "app-1",
340
- });
341
- expect(deletedId).toBe("app-1");
188
+ expect(JSON.parse(result.content).path).toBe("src/main.tsx");
189
+ expect(files["src/main.tsx"]).toBe("export default 2;");
342
190
  });
343
- });
344
191
 
345
- // ---------------------------------------------------------------------------
346
- // app_file_list
347
- // ---------------------------------------------------------------------------
192
+ test("does not modify path for legacy app", () => {
193
+ const files: Record<string, string> = {};
194
+ const app = makeLegacyApp();
195
+ const store = mockStore(app, files);
348
196
 
349
- describe("executeAppFileList", () => {
350
- test("returns list of files", () => {
351
- const store = makeMockStore({
352
- listAppFiles: () => ["index.html", "styles.css", "js/app.js"],
353
- });
354
- const result = executeAppFileList({ app_id: "app-1" }, store);
355
- expect(result.isError).toBe(false);
356
- expect(JSON.parse(result.content)).toEqual([
357
- "index.html",
358
- "styles.css",
359
- "js/app.js",
360
- ]);
361
- });
197
+ const result = executeAppFileWrite(
198
+ { app_id: app.id, path: "index.html", content: "<html></html>" },
199
+ store,
200
+ );
362
201
 
363
- test("returns empty array when app has no files", () => {
364
- const store = makeMockStore({ listAppFiles: () => [] });
365
- const result = executeAppFileList({ app_id: "app-1" }, store);
366
202
  expect(result.isError).toBe(false);
367
- expect(JSON.parse(result.content)).toEqual([]);
203
+ expect(JSON.parse(result.content).path).toBe("index.html");
204
+ expect(files["index.html"]).toBe("<html></html>");
368
205
  });
369
206
  });
370
207
 
371
208
  // ---------------------------------------------------------------------------
372
- // app_file_read
209
+ // executeAppFileRead
373
210
  // ---------------------------------------------------------------------------
374
211
 
375
212
  describe("executeAppFileRead", () => {
376
- test("returns formatted content with line numbers", () => {
377
- const store = makeMockStore({
378
- readAppFile: () => "line1\nline2\nline3",
379
- });
380
- const result = executeAppFileRead(
381
- { app_id: "app-1", path: "index.html" },
382
- store,
383
- );
384
- expect(result.isError).toBe(false);
385
- expect(result.content).toBe(" 1\tline1\n 2\tline2\n 3\tline3");
386
- });
213
+ test("resolves plain path to src/ for multifile app", () => {
214
+ const app = makeMultifileApp();
215
+ const store = mockStore(app, { "src/main.tsx": "line1\nline2" });
387
216
 
388
- test("applies offset parameter (1-based)", () => {
389
- const store = makeMockStore({
390
- readAppFile: () => "a\nb\nc\nd\ne",
391
- });
392
217
  const result = executeAppFileRead(
393
- { app_id: "app-1", path: "f.txt", offset: 3 },
218
+ { app_id: app.id, path: "main.tsx" },
394
219
  store,
395
220
  );
396
- expect(result.isError).toBe(false);
397
- // Lines 3, 4, 5
398
- expect(result.content).toBe(" 3\tc\n 4\td\n 5\te");
399
- });
400
221
 
401
- test("applies limit parameter", () => {
402
- const store = makeMockStore({
403
- readAppFile: () => "a\nb\nc\nd\ne",
404
- });
405
- const result = executeAppFileRead(
406
- { app_id: "app-1", path: "f.txt", limit: 2 },
407
- store,
408
- );
409
222
  expect(result.isError).toBe(false);
410
- expect(result.content).toBe(" 1\ta\n 2\tb");
223
+ expect(result.content).toContain("line1");
411
224
  });
412
225
 
413
- test("applies both offset and limit", () => {
414
- const store = makeMockStore({
415
- readAppFile: () => "a\nb\nc\nd\ne",
416
- });
226
+ test("can read dist/ files explicitly for multifile app", () => {
227
+ const app = makeMultifileApp();
228
+ const store = mockStore(app, { "dist/bundle.js": "bundled code" });
229
+
417
230
  const result = executeAppFileRead(
418
- { app_id: "app-1", path: "f.txt", offset: 2, limit: 2 },
231
+ { app_id: app.id, path: "dist/bundle.js" },
419
232
  store,
420
233
  );
234
+
421
235
  expect(result.isError).toBe(false);
422
- expect(result.content).toBe(" 2\tb\n 3\tc");
236
+ expect(result.content).toContain("bundled code");
423
237
  });
424
238
 
425
- test("defaults offset to 1 when not provided", () => {
426
- const store = makeMockStore({
427
- readAppFile: () => "only",
428
- });
239
+ test("does not modify path for legacy app", () => {
240
+ const app = makeLegacyApp();
241
+ const store = mockStore(app, { "index.html": "<html>hello</html>" });
242
+
429
243
  const result = executeAppFileRead(
430
- { app_id: "app-1", path: "f.txt" },
244
+ { app_id: app.id, path: "index.html" },
431
245
  store,
432
246
  );
433
- expect(result.content).toBe(" 1\tonly");
434
- });
435
247
 
436
- test("propagates store errors (e.g. file not found)", () => {
437
- const store = makeMockStore({
438
- readAppFile: () => {
439
- throw new Error("File not found: missing.txt");
440
- },
441
- });
442
- expect(() =>
443
- executeAppFileRead({ app_id: "app-1", path: "missing.txt" }, store),
444
- ).toThrow("File not found: missing.txt");
248
+ expect(result.isError).toBe(false);
249
+ expect(result.content).toContain("<html>hello</html>");
445
250
  });
446
251
  });
447
252
 
448
253
  // ---------------------------------------------------------------------------
449
- // app_file_edit
254
+ // executeAppFileEdit
450
255
  // ---------------------------------------------------------------------------
451
256
 
452
257
  describe("executeAppFileEdit", () => {
453
- test("returns edit result from store", () => {
454
- const editResult: EditEngineResult = {
455
- ok: true,
456
- updatedContent: "updated",
457
- matchCount: 1,
458
- matchMethod: "exact" as const,
459
- similarity: 1,
460
- actualOld: "old",
461
- actualNew: "new",
462
- };
463
- const store = makeMockStore({ editAppFile: () => editResult });
258
+ test("resolves plain path to src/ for multifile app", () => {
259
+ const files = { "src/main.tsx": "const x = 1;" };
260
+ const app = makeMultifileApp();
261
+ const store = mockStore(app, files);
262
+
464
263
  const result = executeAppFileEdit(
465
264
  {
466
- app_id: "app-1",
467
- path: "index.html",
468
- old_string: "old",
469
- new_string: "new",
265
+ app_id: app.id,
266
+ path: "main.tsx",
267
+ old_string: "const x = 1;",
268
+ new_string: "const x = 2;",
470
269
  },
471
270
  store,
472
271
  );
272
+
473
273
  expect(result.isError).toBe(false);
474
- expect(JSON.parse(result.content)).toEqual(editResult);
274
+ expect(files["src/main.tsx"]).toBe("const x = 2;");
475
275
  });
476
276
 
477
- test("returns error when old_string is empty", () => {
478
- const store = makeMockStore();
277
+ test("does not modify path for legacy app", () => {
278
+ const files = { "index.html": "<p>old</p>" };
279
+ const app = makeLegacyApp();
280
+ const store = mockStore(app, files);
281
+
479
282
  const result = executeAppFileEdit(
480
283
  {
481
- app_id: "app-1",
284
+ app_id: app.id,
482
285
  path: "index.html",
483
- old_string: "",
484
- new_string: "new",
286
+ old_string: "<p>old</p>",
287
+ new_string: "<p>new</p>",
485
288
  },
486
289
  store,
487
290
  );
488
- expect(result.isError).toBe(true);
489
- expect(JSON.parse(result.content)).toEqual({
490
- error: "old_string must not be empty",
491
- });
291
+
292
+ expect(result.isError).toBe(false);
293
+ expect(files["index.html"]).toBe("<p>new</p>");
492
294
  });
295
+ });
493
296
 
494
- test("passes replace_all through to store", () => {
495
- let capturedReplaceAll: boolean | undefined;
496
- const store = makeMockStore({
497
- editAppFile: (_appId, _path, _old, _new, replaceAll) => {
498
- capturedReplaceAll = replaceAll;
499
- return {
500
- ok: true,
501
- updatedContent: "",
502
- matchCount: 1,
503
- matchMethod: "exact" as const,
504
- similarity: 1,
505
- actualOld: "",
506
- actualNew: "",
507
- };
508
- },
297
+ // ---------------------------------------------------------------------------
298
+ // executeAppFileList
299
+ // ---------------------------------------------------------------------------
300
+
301
+ describe("executeAppFileList", () => {
302
+ test("returns clean file paths and separate buildOutput for multifile app", () => {
303
+ const app = makeMultifileApp();
304
+ const store = mockStore(app, {
305
+ "src/main.tsx": "",
306
+ "src/components/Header.tsx": "",
307
+ "dist/index.html": "",
509
308
  });
510
- executeAppFileEdit(
511
- {
512
- app_id: "app-1",
513
- path: "f.txt",
514
- old_string: "x",
515
- new_string: "y",
516
- replace_all: true,
517
- },
518
- store,
519
- );
520
- expect(capturedReplaceAll).toBe(true);
309
+
310
+ const result = executeAppFileList({ app_id: app.id }, store);
311
+ const parsed = JSON.parse(result.content) as {
312
+ files: string[];
313
+ buildOutput: string[];
314
+ };
315
+
316
+ // File paths must be clean — no annotations appended
317
+ expect(parsed.files).toContain("src/main.tsx");
318
+ expect(parsed.files).toContain("src/components/Header.tsx");
319
+ expect(parsed.files).toContain("dist/index.html");
320
+ expect(
321
+ parsed.files.every((f: string) => !f.includes("[build output]")),
322
+ ).toBe(true);
323
+
324
+ // Build output files listed separately
325
+ expect(parsed.buildOutput).toEqual(["dist/index.html"]);
521
326
  });
522
327
 
523
- test("defaults replace_all to false", () => {
524
- let capturedReplaceAll: boolean | undefined;
525
- const store = makeMockStore({
526
- editAppFile: (_appId, _path, _old, _new, replaceAll) => {
527
- capturedReplaceAll = replaceAll;
528
- return {
529
- ok: true,
530
- updatedContent: "",
531
- matchCount: 1,
532
- matchMethod: "exact" as const,
533
- similarity: 1,
534
- actualOld: "",
535
- actualNew: "",
536
- };
537
- },
328
+ test("does not annotate files for legacy app", () => {
329
+ const app = makeLegacyApp();
330
+ const store = mockStore(app, {
331
+ "index.html": "",
332
+ "styles.css": "",
538
333
  });
539
- executeAppFileEdit(
540
- {
541
- app_id: "app-1",
542
- path: "f.txt",
543
- old_string: "x",
544
- new_string: "y",
545
- },
546
- store,
547
- );
548
- expect(capturedReplaceAll).toBe(false);
549
- });
550
334
 
551
- test("passes status through to result", () => {
552
- const store = makeMockStore();
553
- const result = executeAppFileEdit(
554
- {
555
- app_id: "app-1",
556
- path: "f.txt",
557
- old_string: "x",
558
- new_string: "y",
559
- status: "updating styles",
560
- },
561
- store,
335
+ const result = executeAppFileList({ app_id: app.id }, store);
336
+ const parsed = JSON.parse(result.content) as string[];
337
+
338
+ expect(parsed).toContain("index.html");
339
+ expect(parsed).toContain("styles.css");
340
+ expect(parsed.every((f: string) => !f.includes("[build output]"))).toBe(
341
+ true,
562
342
  );
563
- expect(result.status).toBe("updating styles");
564
343
  });
565
344
  });
566
345
 
567
346
  // ---------------------------------------------------------------------------
568
- // app_file_write
347
+ // executeAppCreate
569
348
  // ---------------------------------------------------------------------------
570
349
 
571
- describe("executeAppFileWrite", () => {
572
- test("writes file and returns confirmation", () => {
573
- let writtenPath: string | undefined;
574
- let writtenContent: string | undefined;
575
- const store = makeMockStore({
576
- writeAppFile: (_appId, path, content) => {
577
- writtenPath = path;
578
- writtenContent = content;
350
+ describe("executeAppCreate", () => {
351
+ test("flag off: creates legacy app with root index.html", async () => {
352
+ const files: Record<string, string> = {};
353
+ let createdParams: Record<string, unknown> | undefined;
354
+ const app = makeLegacyApp();
355
+ const store: AppStore = {
356
+ ...mockStore(app, files),
357
+ createApp: (params) => {
358
+ createdParams = params as unknown as Record<string, unknown>;
359
+ return app;
360
+ },
361
+ };
362
+
363
+ const result = await executeAppCreate(
364
+ {
365
+ name: "Test App",
366
+ html: "<html><body>Hello</body></html>",
579
367
  },
580
- });
581
- const result = executeAppFileWrite(
582
- { app_id: "app-1", path: "new.html", content: "<div/>" },
583
368
  store,
584
369
  );
370
+
585
371
  expect(result.isError).toBe(false);
586
- expect(JSON.parse(result.content)).toEqual({
587
- written: true,
588
- path: "new.html",
589
- });
590
- expect(writtenPath).toBe("new.html");
591
- expect(writtenContent).toBe("<div/>");
372
+ // Legacy path: no formatVersion set, htmlDefinition is the provided html
373
+ expect(createdParams?.formatVersion).toBeUndefined();
374
+ expect(createdParams?.htmlDefinition).toBe(
375
+ "<html><body>Hello</body></html>",
376
+ );
377
+ // No src/ files should be written
378
+ expect(files["src/index.html"]).toBeUndefined();
379
+ expect(files["src/main.tsx"]).toBeUndefined();
592
380
  });
593
381
 
594
- test("returns error when app is not found", () => {
595
- const store = makeMockStore({ getApp: () => null });
596
- const result = executeAppFileWrite(
597
- { app_id: "missing", path: "f.txt", content: "hi" },
382
+ test("flag on: creates multifile app with src/ scaffold", async () => {
383
+ const files: Record<string, string> = {};
384
+ let createdParams: Record<string, unknown> | undefined;
385
+ const app = makeMultifileApp({ name: "New App" });
386
+ const store: AppStore = {
387
+ ...mockStore(app, files),
388
+ createApp: (params) => {
389
+ createdParams = params as unknown as Record<string, unknown>;
390
+ return app;
391
+ },
392
+ };
393
+
394
+ const result = await executeAppCreate(
395
+ {
396
+ name: "New App",
397
+ featureFlags: { multifileEnabled: true },
398
+ },
598
399
  store,
599
400
  );
600
- expect(result.isError).toBe(true);
601
- expect(JSON.parse(result.content)).toEqual({
602
- error: "App 'missing' not found",
603
- });
604
- });
605
401
 
606
- test("passes status through to result", () => {
607
- const store = makeMockStore();
608
- const result = executeAppFileWrite(
402
+ expect(result.isError).toBe(false);
403
+ // formatVersion 2 passed to createApp
404
+ expect(createdParams?.formatVersion).toBe(2);
405
+ // htmlDefinition should be empty for multifile apps
406
+ expect(createdParams?.htmlDefinition).toBe("");
407
+ // Scaffold files should be written
408
+ expect(files["src/index.html"]).toBeDefined();
409
+ expect(files["src/index.html"]).toContain("<title>New App</title>");
410
+ expect(files["src/index.html"]).toContain('<div id="app"></div>');
411
+ expect(files["src/main.tsx"]).toBeDefined();
412
+ expect(files["src/main.tsx"]).toContain("import { render } from 'preact'");
413
+ expect(files["src/main.tsx"]).toContain('{"Hello, New App!"}');
414
+ });
415
+
416
+ test("flag on with explicit html: uses provided html as src/index.html", async () => {
417
+ const files: Record<string, string> = {};
418
+ const app = makeMultifileApp({ name: "Custom App" });
419
+ const store: AppStore = {
420
+ ...mockStore(app, files),
421
+ createApp: () => app,
422
+ };
423
+
424
+ const customHtml =
425
+ '<!DOCTYPE html><html><head></head><body><div id="root"></div></body></html>';
426
+ const result = await executeAppCreate(
609
427
  {
610
- app_id: "app-1",
611
- path: "f.txt",
612
- content: "hi",
613
- status: "adding dark mode styles",
428
+ name: "Custom App",
429
+ html: customHtml,
430
+ featureFlags: { multifileEnabled: true },
614
431
  },
615
432
  store,
616
433
  );
617
- expect(result.status).toBe("adding dark mode styles");
618
- });
619
434
 
620
- test("does not call writeAppFile when app not found", () => {
621
- let writeCalled = false;
622
- const store = makeMockStore({
623
- getApp: () => null,
624
- writeAppFile: () => {
625
- writeCalled = true;
626
- },
627
- });
628
- executeAppFileWrite({ app_id: "bad", path: "f.txt", content: "x" }, store);
629
- expect(writeCalled).toBe(false);
435
+ expect(result.isError).toBe(false);
436
+ // Explicit HTML should be used instead of scaffold
437
+ expect(files["src/index.html"]).toBe(customHtml);
438
+ // main.tsx scaffold should still be written
439
+ expect(files["src/main.tsx"]).toBeDefined();
630
440
  });
631
441
  });