@chat-js/cli 0.4.0 → 0.6.0

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 (124) hide show
  1. package/dist/index.js +1163 -959
  2. package/package.json +1 -1
  3. package/templates/chat-app/app/(auth)/device-login/page.tsx +37 -0
  4. package/templates/chat-app/app/(auth)/login/page.tsx +26 -2
  5. package/templates/chat-app/app/(auth)/register/page.tsx +0 -12
  6. package/templates/chat-app/app/(chat)/api/chat/filter-reasoning-parts.ts +1 -1
  7. package/templates/chat-app/app/(chat)/api/chat/route.ts +13 -5
  8. package/templates/chat-app/app/(chat)/layout.tsx +4 -1
  9. package/templates/chat-app/app/api/trpc/[trpc]/route.ts +1 -0
  10. package/templates/chat-app/app/globals.css +9 -9
  11. package/templates/chat-app/app/layout.tsx +4 -2
  12. package/templates/chat-app/biome.jsonc +3 -3
  13. package/templates/chat-app/chat.config.ts +144 -141
  14. package/templates/chat-app/components/ai-elements/prompt-input.tsx +1 -1
  15. package/templates/chat-app/components/anonymous-session-init.tsx +10 -6
  16. package/templates/chat-app/components/artifact-actions.tsx +81 -18
  17. package/templates/chat-app/components/artifact-panel.tsx +142 -41
  18. package/templates/chat-app/components/attachment-list.tsx +1 -1
  19. package/templates/chat-app/components/{social-auth-providers.tsx → auth-providers.tsx} +49 -4
  20. package/templates/chat-app/components/chat/chat-welcome.tsx +3 -3
  21. package/templates/chat-app/components/chat-menu-items.tsx +1 -1
  22. package/templates/chat-app/components/chat-sync.tsx +3 -8
  23. package/templates/chat-app/components/console.tsx +9 -9
  24. package/templates/chat-app/components/context-usage.tsx +2 -2
  25. package/templates/chat-app/components/create-artifact.tsx +15 -5
  26. package/templates/chat-app/components/data-stream-handler.tsx +57 -16
  27. package/templates/chat-app/components/device-login-page.tsx +191 -0
  28. package/templates/chat-app/components/diffview.tsx +8 -2
  29. package/templates/chat-app/components/electron-auth-handler.tsx +184 -0
  30. package/templates/chat-app/components/electron-auth-ui.tsx +121 -0
  31. package/templates/chat-app/components/favicon-group.tsx +1 -1
  32. package/templates/chat-app/components/feedback-actions.tsx +1 -1
  33. package/templates/chat-app/components/greeting.tsx +1 -1
  34. package/templates/chat-app/components/interactive-chart-impl.tsx +3 -4
  35. package/templates/chat-app/components/interactive-charts.tsx +1 -1
  36. package/templates/chat-app/components/login-form.tsx +52 -10
  37. package/templates/chat-app/components/message-editor.tsx +4 -5
  38. package/templates/chat-app/components/model-selector.tsx +661 -655
  39. package/templates/chat-app/components/multimodal-input.tsx +13 -10
  40. package/templates/chat-app/components/parallel-response-cards.tsx +53 -35
  41. package/templates/chat-app/components/part/code-execution.tsx +8 -2
  42. package/templates/chat-app/components/part/document-common.tsx +1 -1
  43. package/templates/chat-app/components/part/document-preview.tsx +5 -5
  44. package/templates/chat-app/components/part/retrieve-url.tsx +12 -12
  45. package/templates/chat-app/components/part/text-message-part.tsx +13 -9
  46. package/templates/chat-app/components/project-chat-item.tsx +1 -1
  47. package/templates/chat-app/components/project-menu-items.tsx +1 -1
  48. package/templates/chat-app/components/research-task.tsx +1 -1
  49. package/templates/chat-app/components/research-tasks.tsx +1 -1
  50. package/templates/chat-app/components/retry-button.tsx +1 -1
  51. package/templates/chat-app/components/sandbox.tsx +1 -1
  52. package/templates/chat-app/components/sheet-editor.tsx +7 -7
  53. package/templates/chat-app/components/sidebar-chats-list.tsx +1 -1
  54. package/templates/chat-app/components/sidebar-toggle.tsx +15 -2
  55. package/templates/chat-app/components/sidebar-top-row.tsx +27 -12
  56. package/templates/chat-app/components/sidebar-user-nav.tsx +10 -1
  57. package/templates/chat-app/components/signup-form.tsx +49 -10
  58. package/templates/chat-app/components/sources.tsx +4 -4
  59. package/templates/chat-app/components/text-editor.tsx +5 -2
  60. package/templates/chat-app/components/toolbar.tsx +3 -3
  61. package/templates/chat-app/components/ui/sidebar.tsx +0 -1
  62. package/templates/chat-app/components/upgrade-cta/limit-display.tsx +1 -1
  63. package/templates/chat-app/components/user-message.tsx +135 -134
  64. package/templates/chat-app/electron.d.ts +41 -0
  65. package/templates/chat-app/evals/my-eval.eval.ts +3 -1
  66. package/templates/chat-app/hooks/use-artifact.tsx +13 -13
  67. package/templates/chat-app/lib/ai/gateways/provider-types.ts +19 -10
  68. package/templates/chat-app/lib/ai/stream-errors.test.ts +72 -0
  69. package/templates/chat-app/lib/ai/stream-errors.ts +94 -0
  70. package/templates/chat-app/lib/ai/tools/code-execution.javascript.ts +171 -0
  71. package/templates/chat-app/lib/ai/tools/code-execution.python.ts +336 -0
  72. package/templates/chat-app/lib/ai/tools/code-execution.shared.test.ts +71 -0
  73. package/templates/chat-app/lib/ai/tools/code-execution.shared.ts +59 -0
  74. package/templates/chat-app/lib/ai/tools/code-execution.ts +62 -391
  75. package/templates/chat-app/lib/ai/tools/code-execution.types.ts +24 -0
  76. package/templates/chat-app/lib/ai/tools/steps/multi-query-web-search.ts +3 -2
  77. package/templates/chat-app/lib/anonymous-session-client.ts +0 -3
  78. package/templates/chat-app/lib/artifacts/code/client.tsx +35 -5
  79. package/templates/chat-app/lib/artifacts/sheet/client.tsx +11 -3
  80. package/templates/chat-app/lib/auth-client.ts +23 -1
  81. package/templates/chat-app/lib/auth.ts +18 -1
  82. package/templates/chat-app/lib/blob.ts +1 -1
  83. package/templates/chat-app/lib/clone-messages.ts +1 -1
  84. package/templates/chat-app/lib/config-schema.ts +13 -1
  85. package/templates/chat-app/lib/constants.ts +3 -4
  86. package/templates/chat-app/lib/db/migrations/meta/0044_snapshot.json +42 -129
  87. package/templates/chat-app/lib/db/migrations/meta/_journal.json +1 -1
  88. package/templates/chat-app/lib/editor/config.ts +4 -4
  89. package/templates/chat-app/lib/electron-auth.ts +96 -0
  90. package/templates/chat-app/lib/env-schema.ts +33 -4
  91. package/templates/chat-app/lib/message-conversion.ts +1 -1
  92. package/templates/chat-app/lib/playwright-test-environment.ts +18 -0
  93. package/templates/chat-app/lib/social-auth.ts +5 -0
  94. package/templates/chat-app/lib/stores/hooks-threads.ts +2 -1
  95. package/templates/chat-app/lib/stores/with-threads.test.ts +1 -1
  96. package/templates/chat-app/lib/stores/with-threads.ts +5 -6
  97. package/templates/chat-app/lib/stores/with-tracing.ts +1 -1
  98. package/templates/chat-app/lib/thread-utils.ts +19 -21
  99. package/templates/chat-app/lib/utils/download-assets.ts +6 -7
  100. package/templates/chat-app/lib/utils/rate-limit.ts +9 -3
  101. package/templates/chat-app/package.json +20 -18
  102. package/templates/chat-app/playwright.config.ts +0 -19
  103. package/templates/chat-app/providers/chat-input-provider.tsx +1 -1
  104. package/templates/chat-app/proxy.ts +28 -3
  105. package/templates/chat-app/scripts/check-env.ts +10 -0
  106. package/templates/chat-app/trpc/server.tsx +7 -2
  107. package/templates/chat-app/tsconfig.json +2 -1
  108. package/templates/chat-app/vercel.json +0 -10
  109. package/templates/electron/CHANGELOG.md +7 -0
  110. package/templates/electron/README.md +54 -0
  111. package/templates/electron/entitlements.mac.plist +10 -0
  112. package/templates/electron/forge.config.ts +157 -0
  113. package/templates/electron/icon.png +0 -0
  114. package/templates/electron/package.json +53 -0
  115. package/templates/electron/scripts/generate-icons.test.js +37 -0
  116. package/templates/electron/scripts/generate-icons.ts +29 -0
  117. package/templates/electron/scripts/run-forge.cjs +28 -0
  118. package/templates/electron/scripts/write-branding.ts +18 -0
  119. package/templates/electron/src/config.ts +16 -0
  120. package/templates/electron/src/lib/auth-client.ts +64 -0
  121. package/templates/electron/src/main.ts +670 -0
  122. package/templates/electron/src/preload.d.ts +27 -0
  123. package/templates/electron/src/preload.ts +25 -0
  124. package/templates/electron/tsconfig.json +18 -0
@@ -0,0 +1,71 @@
1
+ import { beforeEach, describe, expect, it, vi } from "vitest";
2
+
3
+ const envMock: {
4
+ VERCEL_SANDBOX_RUNTIME: string | undefined;
5
+ VERCEL_SANDBOX_RUNTIME_PYTHON: string | undefined;
6
+ VERCEL_SANDBOX_RUNTIME_JAVASCRIPT: string | undefined;
7
+ } = {
8
+ VERCEL_SANDBOX_RUNTIME: undefined,
9
+ VERCEL_SANDBOX_RUNTIME_PYTHON: undefined,
10
+ VERCEL_SANDBOX_RUNTIME_JAVASCRIPT: undefined,
11
+ };
12
+
13
+ vi.mock("@/lib/env", () => ({
14
+ env: envMock,
15
+ }));
16
+
17
+ describe("getSandboxRuntime", () => {
18
+ beforeEach(() => {
19
+ envMock.VERCEL_SANDBOX_RUNTIME = undefined;
20
+ envMock.VERCEL_SANDBOX_RUNTIME_PYTHON = undefined;
21
+ envMock.VERCEL_SANDBOX_RUNTIME_JAVASCRIPT = undefined;
22
+ });
23
+
24
+ it("uses Python defaults when no override is set", async () => {
25
+ const { getSandboxRuntime } = await import("./code-execution.shared");
26
+
27
+ expect(getSandboxRuntime("python")).toBe("python3.13");
28
+ });
29
+
30
+ it("uses JavaScript defaults when no override is set", async () => {
31
+ const { getSandboxRuntime } = await import("./code-execution.shared");
32
+
33
+ expect(getSandboxRuntime("javascript")).toBe("node22");
34
+ });
35
+
36
+ it("honors VERCEL_SANDBOX_RUNTIME_PYTHON override for python", async () => {
37
+ envMock.VERCEL_SANDBOX_RUNTIME_PYTHON = "python3.12";
38
+ const { getSandboxRuntime } = await import("./code-execution.shared");
39
+
40
+ expect(getSandboxRuntime("python")).toBe("python3.12");
41
+ });
42
+
43
+ it("honors VERCEL_SANDBOX_RUNTIME_JAVASCRIPT override for javascript", async () => {
44
+ envMock.VERCEL_SANDBOX_RUNTIME_JAVASCRIPT = "node20";
45
+ const { getSandboxRuntime } = await import("./code-execution.shared");
46
+
47
+ expect(getSandboxRuntime("javascript")).toBe("node20");
48
+ });
49
+
50
+ it("falls back to legacy VERCEL_SANDBOX_RUNTIME for python", async () => {
51
+ envMock.VERCEL_SANDBOX_RUNTIME = "python3.11";
52
+ const { getSandboxRuntime } = await import("./code-execution.shared");
53
+
54
+ expect(getSandboxRuntime("python")).toBe("python3.11");
55
+ });
56
+
57
+ it("prefers VERCEL_SANDBOX_RUNTIME_PYTHON over legacy VERCEL_SANDBOX_RUNTIME", async () => {
58
+ envMock.VERCEL_SANDBOX_RUNTIME_PYTHON = "python3.12";
59
+ envMock.VERCEL_SANDBOX_RUNTIME = "python3.11";
60
+ const { getSandboxRuntime } = await import("./code-execution.shared");
61
+
62
+ expect(getSandboxRuntime("python")).toBe("python3.12");
63
+ });
64
+
65
+ it("does not use legacy VERCEL_SANDBOX_RUNTIME for javascript", async () => {
66
+ envMock.VERCEL_SANDBOX_RUNTIME = "python3.11";
67
+ const { getSandboxRuntime } = await import("./code-execution.shared");
68
+
69
+ expect(getSandboxRuntime("javascript")).toBe("node22");
70
+ });
71
+ });
@@ -0,0 +1,59 @@
1
+ import { Sandbox } from "@vercel/sandbox";
2
+ import { env } from "@/lib/env";
3
+ import type { createModuleLogger } from "@/lib/logger";
4
+ import type { SupportedExecutionLanguage } from "./code-execution.types";
5
+
6
+ export function getTokenAuth(): Record<string, string> {
7
+ const { VERCEL_TEAM_ID, VERCEL_PROJECT_ID, VERCEL_TOKEN } = env;
8
+ if (VERCEL_TEAM_ID && VERCEL_PROJECT_ID && VERCEL_TOKEN) {
9
+ return {
10
+ teamId: VERCEL_TEAM_ID,
11
+ projectId: VERCEL_PROJECT_ID,
12
+ token: VERCEL_TOKEN,
13
+ };
14
+ }
15
+ return {};
16
+ }
17
+
18
+ export function getSandboxRuntime(
19
+ language: SupportedExecutionLanguage
20
+ ): string {
21
+ if (language === "javascript") {
22
+ return env.VERCEL_SANDBOX_RUNTIME_JAVASCRIPT ?? "node22";
23
+ }
24
+
25
+ return (
26
+ env.VERCEL_SANDBOX_RUNTIME_PYTHON ??
27
+ env.VERCEL_SANDBOX_RUNTIME ??
28
+ "python3.13"
29
+ );
30
+ }
31
+
32
+ export function createSandbox(runtime: string): Promise<Sandbox> {
33
+ return Sandbox.create({
34
+ runtime,
35
+ timeout: 5 * 60 * 1000,
36
+ resources: { vcpus: 2 },
37
+ ...getTokenAuth(),
38
+ });
39
+ }
40
+
41
+ export async function cleanupSandbox(
42
+ sandbox: Sandbox | undefined,
43
+ log: ReturnType<typeof createModuleLogger>,
44
+ requestId: string
45
+ ): Promise<void> {
46
+ if (!sandbox) {
47
+ return;
48
+ }
49
+ try {
50
+ await sandbox.stop();
51
+ log.info({ requestId }, "sandbox closed");
52
+ } catch (closeErr) {
53
+ log.warn({ requestId, closeErr }, "failed to close sandbox");
54
+ }
55
+ }
56
+
57
+ export function getErrorMessage(err: unknown): string {
58
+ return err instanceof Error ? err.message : "Unknown error";
59
+ }
@@ -1,373 +1,25 @@
1
- import { Sandbox } from "@vercel/sandbox";
1
+ import type { Sandbox } from "@vercel/sandbox";
2
2
  import { tool } from "ai";
3
3
  import z from "zod";
4
4
  import type { CostAccumulator } from "@/lib/credits/cost-accumulator";
5
- import { env } from "@/lib/env";
6
5
  import { createModuleLogger } from "@/lib/logger";
6
+ import { executeJavaScriptInSandbox } from "./code-execution.javascript";
7
+ import { executePythonInSandbox } from "./code-execution.python";
8
+ import {
9
+ cleanupSandbox,
10
+ createSandbox,
11
+ getErrorMessage,
12
+ getSandboxRuntime,
13
+ } from "./code-execution.shared";
14
+ import {
15
+ type SupportedExecutionLanguage,
16
+ supportedExecutionLanguages,
17
+ } from "./code-execution.types";
7
18
  import { toolsDefinitions } from "./tools-definitions";
8
19
 
9
- const WHITESPACE_REGEX = /\s+/;
10
- const PACKAGE_SPEC_SPLIT_RE = /[=<>![\s]/;
20
+ const languageSchema = z.enum(supportedExecutionLanguages);
11
21
 
12
- async function installBasePackages(
13
- sandbox: Sandbox,
14
- basePackages: readonly string[],
15
- log: ReturnType<typeof createModuleLogger>,
16
- requestId: string
17
- ): Promise<{
18
- success: boolean;
19
- result?: { message: string; chart: string };
20
- }> {
21
- const installStep = await sandbox.runCommand({
22
- cmd: "pip",
23
- args: ["install", ...basePackages],
24
- });
25
- if (installStep.exitCode !== 0) {
26
- const installStderr = await installStep.stderr();
27
- log.error(
28
- { requestId, stderr: installStderr },
29
- "base package installation failed"
30
- );
31
- return {
32
- success: false,
33
- result: {
34
- message: `Failed to install base packages: ${installStderr}`,
35
- chart: "",
36
- },
37
- };
38
- }
39
- log.info({ requestId }, "base packages installed");
40
- return { success: true };
41
- }
42
-
43
- function packageName(spec: string): string {
44
- return spec.split(PACKAGE_SPEC_SPLIT_RE)[0].toLowerCase();
45
- }
46
-
47
- async function processExtraPackages(
48
- code: string,
49
- basePackages: readonly string[],
50
- sandbox: Sandbox,
51
- log: ReturnType<typeof createModuleLogger>,
52
- requestId: string
53
- ): Promise<{
54
- codeToRun: string;
55
- installResult: {
56
- success: boolean;
57
- result?: { message: string; chart: string };
58
- };
59
- }> {
60
- const basePackageNames = new Set(basePackages.map((p) => p.toLowerCase()));
61
- const lines = code.split("\n");
62
- const pipLines = lines.filter((l) => l.trim().startsWith("!pip install "));
63
- const extraPackages = pipLines
64
- .flatMap((l) =>
65
- l
66
- .trim()
67
- .slice("!pip install ".length)
68
- .split(WHITESPACE_REGEX)
69
- .filter(Boolean)
70
- )
71
- .filter((spec) => !basePackageNames.has(packageName(spec)));
72
-
73
- const codeWithoutPipLines = lines
74
- .filter((l) => !l.trim().startsWith("!pip install "))
75
- .join("\n");
76
-
77
- if (extraPackages.length === 0) {
78
- return { codeToRun: codeWithoutPipLines, installResult: { success: true } };
79
- }
80
-
81
- log.info({ requestId, extraPackages }, "installing extra packages");
82
- const dynamicInstall = await sandbox.runCommand({
83
- cmd: "pip",
84
- args: ["install", ...extraPackages],
85
- });
86
- if (dynamicInstall.exitCode !== 0) {
87
- const stderr = await dynamicInstall.stderr();
88
- log.error({ requestId, stderr }, "dynamic package installation failed");
89
- return {
90
- codeToRun: code,
91
- installResult: {
92
- success: false,
93
- result: {
94
- message: `Failed to install packages: ${stderr}`,
95
- chart: "",
96
- },
97
- },
98
- };
99
- }
100
-
101
- return {
102
- codeToRun: codeWithoutPipLines,
103
- installResult: { success: true },
104
- };
105
- }
106
-
107
- function createWrappedCode(codeToRun: string, chartPath: string): string {
108
- return `
109
- import sys
110
- import json
111
- import traceback
112
-
113
- try:
114
- import matplotlib.pyplot as _plt_module
115
- _orig_savefig = _plt_module.savefig
116
- def _intercepted_savefig(*args, **kwargs):
117
- _orig_savefig('${chartPath}', format='png', bbox_inches='tight', dpi=100)
118
- if args or kwargs.get('fname') not in (None, '${chartPath}'):
119
- return _orig_savefig(*args, **kwargs)
120
- _plt_module.savefig = _intercepted_savefig
121
- except ImportError:
122
- pass
123
-
124
- try:
125
- exec(${JSON.stringify(codeToRun)})
126
- try:
127
- _locals = locals()
128
- _globals = globals()
129
- _chart_var = _locals.get("chart") or _globals.get("chart")
130
- if (isinstance(_chart_var, dict)
131
- and isinstance(_chart_var.get("type"), str)
132
- and isinstance(_chart_var.get("elements"), list)):
133
- print("__CHART_JSON__:" + json.dumps(_chart_var))
134
- else:
135
- if "result" in _locals:
136
- print(_locals["result"])
137
- elif "result" in _globals:
138
- print(_globals["result"])
139
- elif "results" in _locals:
140
- print(_locals["results"])
141
- elif "results" in _globals:
142
- print(_globals["results"])
143
- except Exception:
144
- pass
145
- try:
146
- import matplotlib.pyplot as plt
147
- if plt.get_fignums():
148
- plt.savefig('${chartPath}', format='png', bbox_inches='tight', dpi=100)
149
- plt.close('all')
150
- except ImportError:
151
- pass
152
- print(json.dumps({"success": True}))
153
- except Exception as e:
154
- error_info = {"success": False, "error": {"name": type(e).__name__, "value": str(e), "traceback": traceback.format_exc()}}
155
- print(json.dumps(error_info))
156
- sys.exit(1)
157
- `;
158
- }
159
-
160
- const CHART_JSON_PREFIX = "__CHART_JSON__:";
161
-
162
- async function parseExecutionOutput(execResult: {
163
- stdout: () => Promise<string>;
164
- exitCode: number;
165
- }): Promise<{
166
- outputText: string;
167
- chartData: Record<string, unknown> | null;
168
- execInfo: {
169
- success: boolean;
170
- error?: { name: string; value: string; traceback: string };
171
- };
172
- }> {
173
- const stdout = await execResult.stdout();
174
- let execInfo: {
175
- success: boolean;
176
- error?: { name: string; value: string; traceback: string };
177
- } = { success: true };
178
- let outputText = "";
179
- let chartData: Record<string, unknown> | null = null;
180
-
181
- try {
182
- const outLines = (stdout ?? "").trim().split("\n");
183
- const lastLine = outLines.at(-1);
184
- execInfo = JSON.parse(lastLine ?? "{}");
185
- outLines.pop();
186
-
187
- const chartLineIdx = outLines.findIndex((l) =>
188
- l.startsWith(CHART_JSON_PREFIX)
189
- );
190
- if (chartLineIdx !== -1) {
191
- const raw = outLines[chartLineIdx].slice(CHART_JSON_PREFIX.length);
192
- try {
193
- chartData = JSON.parse(raw) as Record<string, unknown>;
194
- } catch {
195
- // ignore malformed chart JSON
196
- }
197
- outLines.splice(chartLineIdx, 1);
198
- }
199
-
200
- outputText = outLines.join("\n");
201
- } catch {
202
- outputText = stdout ?? "";
203
- }
204
-
205
- return { outputText, chartData, execInfo };
206
- }
207
-
208
- async function checkForChart(
209
- sandbox: Sandbox,
210
- chartPath: string,
211
- log: ReturnType<typeof createModuleLogger>,
212
- requestId: string
213
- ): Promise<{ base64: string; format: string } | undefined> {
214
- const chartCheck = await sandbox.runCommand({
215
- cmd: "test",
216
- args: ["-f", chartPath],
217
- });
218
- if (chartCheck.exitCode === 0) {
219
- const b64 = await (
220
- await sandbox.runCommand({
221
- cmd: "base64",
222
- args: ["-w", "0", chartPath],
223
- })
224
- ).stdout();
225
- log.info({ requestId }, "chart generated");
226
- return { base64: (b64 ?? "").trim(), format: "png" };
227
- }
228
- return;
229
- }
230
-
231
- function buildResponseMessage({
232
- outputText,
233
- stderr,
234
- execInfo,
235
- log,
236
- requestId,
237
- }: {
238
- outputText: string;
239
- stderr: string;
240
- execInfo: {
241
- success: boolean;
242
- error?: { name: string; value: string; traceback: string };
243
- };
244
- log: ReturnType<typeof createModuleLogger>;
245
- requestId: string;
246
- }): string {
247
- let message = "";
248
-
249
- if (outputText) {
250
- message += `${outputText}\n`;
251
- }
252
- if (stderr && stderr.trim().length > 0) {
253
- message += `${stderr}\n`;
254
- }
255
- if (execInfo.error) {
256
- message += `Error: ${execInfo.error.name}: ${execInfo.error.value}\n`;
257
- log.error({ requestId, error: execInfo.error }, "python execution error");
258
- }
259
-
260
- return message;
261
- }
262
-
263
- function getTokenAuth(): Record<string, string> {
264
- const { VERCEL_TEAM_ID, VERCEL_PROJECT_ID, VERCEL_TOKEN } = env;
265
- if (VERCEL_TEAM_ID && VERCEL_PROJECT_ID && VERCEL_TOKEN) {
266
- return {
267
- teamId: VERCEL_TEAM_ID,
268
- projectId: VERCEL_PROJECT_ID,
269
- token: VERCEL_TOKEN,
270
- };
271
- }
272
- return {};
273
- }
274
-
275
- function createSandbox(runtime: string): Promise<Sandbox> {
276
- return Sandbox.create({
277
- runtime,
278
- timeout: 5 * 60 * 1000,
279
- resources: { vcpus: 2 },
280
- ...getTokenAuth(),
281
- });
282
- }
283
-
284
- async function executeInSandbox({
285
- sandbox,
286
- code,
287
- basePackages,
288
- chartPath,
289
- log,
290
- requestId,
291
- }: {
292
- sandbox: Sandbox;
293
- code: string;
294
- basePackages: readonly string[];
295
- chartPath: string;
296
- log: ReturnType<typeof createModuleLogger>;
297
- requestId: string;
298
- }): Promise<{
299
- message: string;
300
- chart: string | { base64: string; format: string } | Record<string, unknown>;
301
- }> {
302
- const baseInstallResult = await installBasePackages(
303
- sandbox,
304
- basePackages,
305
- log,
306
- requestId
307
- );
308
- if (!baseInstallResult.success) {
309
- return baseInstallResult.result ?? { message: "Unknown error", chart: "" };
310
- }
311
-
312
- const { codeToRun, installResult } = await processExtraPackages(
313
- code,
314
- basePackages,
315
- sandbox,
316
- log,
317
- requestId
318
- );
319
- if (!installResult.success) {
320
- return installResult.result ?? { message: "Unknown error", chart: "" };
321
- }
322
-
323
- const wrappedCode = createWrappedCode(codeToRun, chartPath);
324
- const execResult = await sandbox.runCommand({
325
- cmd: "python3",
326
- args: ["-c", wrappedCode],
327
- });
328
-
329
- const { outputText, chartData, execInfo } =
330
- await parseExecutionOutput(execResult);
331
-
332
- const message = buildResponseMessage({
333
- outputText,
334
- stderr: await execResult.stderr(),
335
- execInfo,
336
- log,
337
- requestId,
338
- });
339
-
340
- if (chartData) {
341
- log.info({ requestId }, "interactive chart data returned");
342
- return { message: message.trim(), chart: chartData };
343
- }
344
-
345
- const chartOut = await checkForChart(sandbox, chartPath, log, requestId);
346
- return {
347
- message: message.trim(),
348
- chart: chartOut ?? "",
349
- };
350
- }
351
-
352
- async function cleanupSandbox(
353
- sandbox: Sandbox | undefined,
354
- log: ReturnType<typeof createModuleLogger>,
355
- requestId: string
356
- ): Promise<void> {
357
- if (!sandbox) {
358
- return;
359
- }
360
- try {
361
- await sandbox.stop();
362
- log.info({ requestId }, "sandbox closed");
363
- } catch (closeErr) {
364
- log.warn({ requestId, closeErr }, "failed to close sandbox");
365
- }
366
- }
367
-
368
- function getErrorMessage(err: unknown): string {
369
- return err instanceof Error ? err.message : "Unknown error";
370
- }
22
+ const defaultExecutionLanguage: SupportedExecutionLanguage = "python";
371
23
 
372
24
  export const codeExecution = ({
373
25
  costAccumulator,
@@ -375,14 +27,23 @@ export const codeExecution = ({
375
27
  costAccumulator?: CostAccumulator;
376
28
  }) =>
377
29
  tool({
378
- description: `Python-only sandbox for calculations, data analysis & visualisations.
30
+ description: `Sandboxed code execution for Python and JavaScript.
379
31
 
380
32
  Use for:
381
- - Execute Python (matplotlib, pandas, numpy, sympy, yfinance pre-installed — do NOT reinstall them)
33
+ - Execute Python for calculations, data analysis, and visualisations
34
+ - Execute JavaScript for scripting, transformations, async fetches, and general runtime checks
35
+
36
+ Python support:
37
+ - matplotlib, pandas, numpy, sympy, yfinance pre-installed — do NOT reinstall them
382
38
  - Produce interactive line / scatter / bar charts OR matplotlib PNG charts
383
39
  - Install extra libs by adding lines like: '!pip install <pkg> [<pkg2> ...]' (we auto-install and strip these lines; pre-installed packages are ignored)
384
40
 
385
- Chart output — choose ONE:
41
+ JavaScript support:
42
+ - Runs in a sandboxed Node.js runtime, never in the browser thread
43
+ - Use console.log(...) to print output
44
+ - You can also assign 'result' or 'results', or return a value from the snippet
45
+
46
+ Chart output — Python only:
386
47
  1. Interactive chart (preferred for line/scatter/bar): assign a 'chart' variable matching this schema:
387
48
  chart = {
388
49
  "type": "line" | "scatter" | "bar",
@@ -402,47 +63,57 @@ Restrictions:
402
63
  - Interactive chart: only line / scatter / bar types
403
64
 
404
65
  Output rules:
405
- - Assign 'chart' dict for interactive charts (takes priority over matplotlib PNG)
406
- - Assign 'result' or 'results' for other computed values (auto-printed)
407
- - Or print explicitly: print(answer)
66
+ - Set language to 'python' or 'javascript'
67
+ - Python charts: assign 'chart' dict for interactive charts (takes priority over matplotlib PNG)
68
+ - Python values: assign 'result' or 'results', or print explicitly
69
+ - JavaScript values: assign 'result' or 'results', return a value, or print explicitly
408
70
  - Don't rely on implicit REPL last-expression output`,
409
71
  inputSchema: z.object({
410
72
  title: z.string().describe("The title of the code snippet."),
73
+ language: languageSchema
74
+ .default(defaultExecutionLanguage)
75
+ .describe("The language to execute: 'python' or 'javascript'."),
411
76
  code: z
412
77
  .string()
413
78
  .describe(
414
- "The Python code to execute. Print anything you want to return. Optionally assign to 'result' or 'results' to auto-print."
79
+ "The code to execute in the selected sandbox language. Print anything you want to return, or assign to 'result'/'results'."
415
80
  ),
416
81
  }),
417
- execute: async ({ code, title }: { code: string; title: string }) => {
82
+ execute: async ({
83
+ code,
84
+ title,
85
+ language,
86
+ }: {
87
+ code: string;
88
+ title: string;
89
+ language: SupportedExecutionLanguage;
90
+ }) => {
418
91
  const log = createModuleLogger("code-execution");
419
92
  const requestId = `ci-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
420
- const runtime = env.VERCEL_SANDBOX_RUNTIME ?? "python3.13";
421
- const basePackages = [
422
- "matplotlib",
423
- "pandas",
424
- "numpy",
425
- "sympy",
426
- "yfinance",
427
- ] as const;
428
- const chartPath = "/tmp/chart.png";
93
+ const runtime = getSandboxRuntime(language);
429
94
 
430
95
  let sandbox: Sandbox | undefined;
431
96
 
432
97
  try {
433
- log.info({ requestId, title, runtime }, "creating sandbox");
98
+ log.info({ requestId, title, runtime, language }, "creating sandbox");
434
99
  sandbox = await createSandbox(runtime);
435
100
  log.debug({ requestId }, "sandbox created");
436
101
 
437
- log.info({ requestId, title }, "executing python code");
438
- const result = await executeInSandbox({
439
- sandbox,
440
- code,
441
- basePackages,
442
- chartPath,
443
- log,
444
- requestId,
445
- });
102
+ log.info({ requestId, title, language }, "executing code");
103
+ const result =
104
+ language === "javascript"
105
+ ? await executeJavaScriptInSandbox({
106
+ sandbox,
107
+ code,
108
+ log,
109
+ requestId,
110
+ })
111
+ : await executePythonInSandbox({
112
+ sandbox,
113
+ code,
114
+ log,
115
+ requestId,
116
+ });
446
117
 
447
118
  costAccumulator?.addAPICost(
448
119
  "codeExecution",
@@ -451,7 +122,7 @@ Output rules:
451
122
 
452
123
  return result;
453
124
  } catch (err) {
454
- log.error({ err, requestId }, "code execution failed");
125
+ log.error({ err, requestId, language }, "code execution failed");
455
126
  return {
456
127
  message: `Sandbox execution failed: ${getErrorMessage(err)}`,
457
128
  chart: "",
@@ -0,0 +1,24 @@
1
+ import type { Sandbox } from "@vercel/sandbox";
2
+ import type { createModuleLogger } from "@/lib/logger";
3
+
4
+ export const supportedExecutionLanguages = ["python", "javascript"] as const;
5
+
6
+ export type SupportedExecutionLanguage =
7
+ (typeof supportedExecutionLanguages)[number];
8
+
9
+ export type CodeExecutionChart =
10
+ | string
11
+ | { base64: string; format: string }
12
+ | Record<string, unknown>;
13
+
14
+ export interface CodeExecutionResult {
15
+ chart: CodeExecutionChart;
16
+ message: string;
17
+ }
18
+
19
+ export interface CodeExecutionContext {
20
+ code: string;
21
+ log: ReturnType<typeof createModuleLogger>;
22
+ requestId: string;
23
+ sandbox: Sandbox;
24
+ }
@@ -118,8 +118,9 @@ export async function multiQueryWebSearchStep({
118
118
  return {
119
119
  searches: searchResults,
120
120
  };
121
- } catch (error: any) {
122
- const errorMessage = error?.message || "Unknown error occurred";
121
+ } catch (error: unknown) {
122
+ const errorMessage =
123
+ error instanceof Error ? error.message : "Unknown error occurred";
123
124
 
124
125
  // Send error annotation
125
126
  dataStream.write({
@@ -26,7 +26,6 @@ function setCookie(name: string, value: string, maxAge: number): void {
26
26
  const secure = window.location.protocol === "https:" ? "; Secure" : "";
27
27
  const encodedValue = encodeURIComponent(value);
28
28
  if ("cookieStore" in window) {
29
- // @ts-expect-error cookieStore is not yet in TS lib.dom
30
29
  window.cookieStore
31
30
  .set({
32
31
  name,
@@ -34,7 +33,6 @@ function setCookie(name: string, value: string, maxAge: number): void {
34
33
  path: "/",
35
34
  expires: Date.now() + maxAge * 1000,
36
35
  sameSite: "lax",
37
- secure: window.location.protocol === "https:",
38
36
  })
39
37
  .catch(() => {
40
38
  // Fail silently if Cookie Store API fails
@@ -50,7 +48,6 @@ function deleteCookie(name: string): void {
50
48
  return;
51
49
  }
52
50
  if ("cookieStore" in window) {
53
- // @ts-expect-error cookieStore is not yet in TS lib.dom
54
51
  window.cookieStore.delete(name).catch(() => {
55
52
  // Fail silently if Cookie Store API fails
56
53
  });