@chat-js/cli 0.4.0 → 0.6.1

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 +1548 -969
  2. package/package.json +4 -3
  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 +22 -19
  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 +152 -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,72 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import {
3
+ getStreamErrorMessage,
4
+ getStreamErrorToastContent,
5
+ } from "./stream-errors";
6
+
7
+ describe("getStreamErrorMessage", () => {
8
+ it("returns actionable gateway billing guidance", () => {
9
+ expect(
10
+ getStreamErrorMessage(
11
+ new Error("AI Gateway requires a valid credit card to run this model.")
12
+ )
13
+ ).toBe(
14
+ "AI Gateway requires a valid credit card. Add one in your Vercel dashboard and try again."
15
+ );
16
+ });
17
+
18
+ it("returns a context-window specific message", () => {
19
+ expect(
20
+ getStreamErrorMessage(
21
+ new Error("This model exceeded its maximum context length.")
22
+ )
23
+ ).toBe(
24
+ "This conversation is too long for the selected model. Start a new chat or shorten the message and try again."
25
+ );
26
+ });
27
+
28
+ it("falls back to a generic message for unknown errors", () => {
29
+ expect(getStreamErrorMessage(new Error("Provider exploded"))).toBe(
30
+ "An error occurred while generating a response. Please try again."
31
+ );
32
+ });
33
+ });
34
+
35
+ describe("getStreamErrorToastContent", () => {
36
+ it("uses the streamed cause as description when the message is truncated", () => {
37
+ const error = new Error("O", {
38
+ cause: "Oops, the provider returned a 429.",
39
+ });
40
+
41
+ expect(getStreamErrorToastContent(error)).toEqual({
42
+ message:
43
+ "An error occurred while generating a response. Please try again.",
44
+ description: "Oops, the provider returned a 429.",
45
+ });
46
+ });
47
+
48
+ it("extracts message from an Error object used as cause", () => {
49
+ const error = new Error("An error occurred, please try again!", {
50
+ cause: new Error("Rate limit exceeded. Try again in 30 seconds."),
51
+ });
52
+
53
+ expect(getStreamErrorToastContent(error)).toEqual({
54
+ message: "Rate limit exceeded. Please wait a moment and try again.",
55
+ description: "Rate limit exceeded. Try again in 30 seconds.",
56
+ });
57
+ });
58
+
59
+ it("keeps a specific message and moves the cause to the description", () => {
60
+ const error = new Error(
61
+ "Rate limit exceeded. Please wait a moment and try again.",
62
+ {
63
+ cause: "Provider returned HTTP 429",
64
+ }
65
+ );
66
+
67
+ expect(getStreamErrorToastContent(error)).toEqual({
68
+ message: "Rate limit exceeded. Please wait a moment and try again.",
69
+ description: "Provider returned HTTP 429",
70
+ });
71
+ });
72
+ });
@@ -0,0 +1,94 @@
1
+ const FALLBACK_STREAM_ERROR_MESSAGE =
2
+ "An error occurred while generating a response. Please try again.";
3
+
4
+ const genericErrorMessages = new Set([
5
+ "",
6
+ "An error occurred, please try again!",
7
+ "Something went wrong. Please try again later.",
8
+ FALLBACK_STREAM_ERROR_MESSAGE,
9
+ ]);
10
+
11
+ function getErrorText(error: unknown): string | null {
12
+ if (typeof error === "string") {
13
+ const trimmed = error.trim();
14
+ return trimmed.length > 0 ? trimmed : null;
15
+ }
16
+
17
+ if (
18
+ typeof error === "object" &&
19
+ error !== null &&
20
+ "message" in error &&
21
+ typeof error.message === "string"
22
+ ) {
23
+ const trimmed = error.message.trim();
24
+ return trimmed.length > 0 ? trimmed : null;
25
+ }
26
+
27
+ return null;
28
+ }
29
+
30
+ function mapKnownStreamErrorMessage(message: string): string {
31
+ const normalized = message.toLowerCase();
32
+
33
+ if (
34
+ normalized.includes("ai gateway requires a valid credit card") ||
35
+ (normalized.includes("credit card") && normalized.includes("gateway"))
36
+ ) {
37
+ return "AI Gateway requires a valid credit card. Add one in your Vercel dashboard and try again.";
38
+ }
39
+
40
+ if (
41
+ normalized.includes("context window") ||
42
+ normalized.includes("maximum context length") ||
43
+ normalized.includes("prompt is too long") ||
44
+ normalized.includes("too many tokens")
45
+ ) {
46
+ return "This conversation is too long for the selected model. Start a new chat or shorten the message and try again.";
47
+ }
48
+
49
+ if (
50
+ normalized.includes("rate limit") ||
51
+ normalized.includes("too many requests")
52
+ ) {
53
+ return "Rate limit exceeded. Please wait a moment and try again.";
54
+ }
55
+
56
+ if (
57
+ normalized.includes("model not found") ||
58
+ normalized.includes("model is not available") ||
59
+ normalized.includes("no such model")
60
+ ) {
61
+ return "The selected model is not available right now. Choose another model and try again.";
62
+ }
63
+
64
+ return FALLBACK_STREAM_ERROR_MESSAGE;
65
+ }
66
+
67
+ export function getStreamErrorMessage(error: unknown): string {
68
+ return mapKnownStreamErrorMessage(
69
+ getErrorText(error) ?? FALLBACK_STREAM_ERROR_MESSAGE
70
+ );
71
+ }
72
+
73
+ export function getStreamErrorToastContent(error: Error): {
74
+ description?: string;
75
+ message: string;
76
+ } {
77
+ const rawMessage =
78
+ typeof error.message === "string" ? error.message.trim() : "";
79
+ const rawCause =
80
+ error.cause == null ? undefined : (getErrorText(error.cause) ?? undefined);
81
+
82
+ const rawResolved =
83
+ (rawMessage.length <= 1 || genericErrorMessages.has(rawMessage)) && rawCause
84
+ ? rawCause
85
+ : rawMessage || rawCause || FALLBACK_STREAM_ERROR_MESSAGE;
86
+
87
+ const message = mapKnownStreamErrorMessage(rawResolved);
88
+
89
+ if (rawCause && rawCause !== message && !genericErrorMessages.has(rawCause)) {
90
+ return { message, description: rawCause };
91
+ }
92
+
93
+ return { message };
94
+ }
@@ -0,0 +1,171 @@
1
+ import type {
2
+ CodeExecutionContext,
3
+ CodeExecutionResult,
4
+ } from "./code-execution.types";
5
+
6
+ const EXECUTION_STATUS_PREFIX = "__EXECUTION_STATUS__:";
7
+
8
+ function createWrappedCode(code: string): string {
9
+ // Inject user code as a string literal so backticks / ${} in user code
10
+ // cannot break out of the wrapper template.
11
+ const userCodeLiteral = JSON.stringify(code);
12
+ return `
13
+ import { inspect } from "node:util";
14
+
15
+ const __formatOutput = (value) => {
16
+ if (typeof value === "string") {
17
+ return value;
18
+ }
19
+
20
+ return inspect(value, {
21
+ depth: 4,
22
+ colors: false,
23
+ maxArrayLength: 100,
24
+ breakLength: 120,
25
+ });
26
+ };
27
+
28
+ const __run = async () => {
29
+ try {
30
+ const __userCode = ${userCodeLiteral};
31
+ const __execution = await (0, eval)(
32
+ "(async () => {\\n" +
33
+ __userCode + "\\n" +
34
+ "return { __kind: \\"locals\\"," +
35
+ " result: typeof result !== \\"undefined\\" ? result : undefined," +
36
+ " results: typeof results !== \\"undefined\\" ? results : undefined };\\n" +
37
+ "})()"
38
+ );
39
+
40
+ const __result =
41
+ __execution &&
42
+ typeof __execution === "object" &&
43
+ __execution.__kind === "locals"
44
+ ? typeof __execution.result !== "undefined"
45
+ ? __execution.result
46
+ : typeof __execution.results !== "undefined"
47
+ ? __execution.results
48
+ : undefined
49
+ : __execution;
50
+
51
+ if (typeof __result !== "undefined") {
52
+ console.log(__formatOutput(__result));
53
+ }
54
+
55
+ console.log("${EXECUTION_STATUS_PREFIX}" + JSON.stringify({ success: true }));
56
+ } catch (error) {
57
+ const execError = error instanceof Error ? error : new Error(String(error));
58
+ console.log(
59
+ "${EXECUTION_STATUS_PREFIX}" +
60
+ JSON.stringify({
61
+ success: false,
62
+ error: {
63
+ name: execError.name,
64
+ value: execError.message,
65
+ traceback: execError.stack ?? execError.message,
66
+ },
67
+ })
68
+ );
69
+ process.exitCode = 1;
70
+ }
71
+ };
72
+
73
+ await __run();
74
+ `;
75
+ }
76
+
77
+ type JsExecInfo = {
78
+ success: boolean;
79
+ error?: { name: string; value: string; traceback: string };
80
+ };
81
+
82
+ function execInfoFromExitCode(exitCode: number): JsExecInfo {
83
+ if (exitCode === 0) {
84
+ return { success: true };
85
+ }
86
+ return {
87
+ success: false,
88
+ error: {
89
+ name: "SandboxExecutionError",
90
+ value: "Execution completed without a valid status trailer",
91
+ traceback: "",
92
+ },
93
+ };
94
+ }
95
+
96
+ async function parseExecutionOutput(execResult: {
97
+ stdout: () => Promise<string>;
98
+ exitCode: number;
99
+ }): Promise<{
100
+ outputText: string;
101
+ execInfo: JsExecInfo;
102
+ }> {
103
+ const stdout = await execResult.stdout();
104
+ const lines = (stdout ?? "").split("\n");
105
+ // Search from the end so a user console.log of the prefix cannot be
106
+ // mistaken for the real status trailer emitted last by the wrapper.
107
+ const statusLineIndex = lines.findLastIndex((line) =>
108
+ line.startsWith(EXECUTION_STATUS_PREFIX)
109
+ );
110
+
111
+ if (statusLineIndex === -1) {
112
+ return {
113
+ outputText: stdout ?? "",
114
+ execInfo: execInfoFromExitCode(execResult.exitCode),
115
+ };
116
+ }
117
+
118
+ const execInfoRaw = lines[statusLineIndex].slice(
119
+ EXECUTION_STATUS_PREFIX.length
120
+ );
121
+ let execInfo: JsExecInfo;
122
+ try {
123
+ execInfo = JSON.parse(execInfoRaw) as JsExecInfo;
124
+ } catch {
125
+ return {
126
+ outputText: stdout ?? "",
127
+ execInfo: execInfoFromExitCode(execResult.exitCode),
128
+ };
129
+ }
130
+ lines.splice(statusLineIndex, 1);
131
+
132
+ return {
133
+ outputText: lines.join("\n").trim(),
134
+ execInfo,
135
+ };
136
+ }
137
+
138
+ export async function executeJavaScriptInSandbox({
139
+ sandbox,
140
+ code,
141
+ log,
142
+ requestId,
143
+ }: CodeExecutionContext): Promise<CodeExecutionResult> {
144
+ const execResult = await sandbox.runCommand({
145
+ cmd: "node",
146
+ args: ["--input-type=module", "-e", createWrappedCode(code)],
147
+ });
148
+
149
+ const { outputText, execInfo } = await parseExecutionOutput(execResult);
150
+ const stderrTrimmed = (await execResult.stderr())?.trim();
151
+ let message = "";
152
+
153
+ if (outputText) {
154
+ message += `${outputText}\n`;
155
+ }
156
+ if (stderrTrimmed) {
157
+ message += `${stderrTrimmed}\n`;
158
+ }
159
+ if (execInfo.error) {
160
+ message += `Error: ${execInfo.error.name}: ${execInfo.error.value}\n`;
161
+ log.error(
162
+ { requestId, error: execInfo.error },
163
+ "javascript execution error"
164
+ );
165
+ }
166
+
167
+ return {
168
+ message: message.trim(),
169
+ chart: "",
170
+ };
171
+ }
@@ -0,0 +1,336 @@
1
+ import type { Sandbox } from "@vercel/sandbox";
2
+ import type {
3
+ CodeExecutionContext,
4
+ CodeExecutionResult,
5
+ } from "./code-execution.types";
6
+
7
+ const WHITESPACE_REGEX = /\s+/;
8
+ const PACKAGE_SPEC_SPLIT_RE = /[=<>![\s]/;
9
+ const CHART_JSON_PREFIX = "__CHART_JSON__:";
10
+
11
+ function packageName(spec: string): string {
12
+ return spec.split(PACKAGE_SPEC_SPLIT_RE)[0].toLowerCase();
13
+ }
14
+
15
+ async function installBasePackages(
16
+ sandbox: Sandbox,
17
+ basePackages: readonly string[],
18
+ requestId: string,
19
+ log: CodeExecutionContext["log"]
20
+ ): Promise<{
21
+ success: boolean;
22
+ result?: CodeExecutionResult;
23
+ }> {
24
+ const installStep = await sandbox.runCommand({
25
+ cmd: "pip",
26
+ args: ["install", ...basePackages],
27
+ });
28
+ if (installStep.exitCode !== 0) {
29
+ const installStderr = await installStep.stderr();
30
+ log.error(
31
+ { requestId, stderr: installStderr },
32
+ "base package installation failed"
33
+ );
34
+ return {
35
+ success: false,
36
+ result: {
37
+ message: `Failed to install base packages: ${installStderr}`,
38
+ chart: "",
39
+ },
40
+ };
41
+ }
42
+ log.info({ requestId }, "base packages installed");
43
+ return { success: true };
44
+ }
45
+
46
+ async function processExtraPackages(
47
+ code: string,
48
+ basePackages: readonly string[],
49
+ sandbox: Sandbox,
50
+ requestId: string,
51
+ log: CodeExecutionContext["log"]
52
+ ): Promise<{
53
+ codeToRun: string;
54
+ installResult: {
55
+ success: boolean;
56
+ result?: CodeExecutionResult;
57
+ };
58
+ }> {
59
+ const basePackageNames = new Set(basePackages.map((p) => p.toLowerCase()));
60
+ const lines = code.split("\n");
61
+ const pipLines = lines.filter((line) =>
62
+ line.trim().startsWith("!pip install ")
63
+ );
64
+ const extraPackages = pipLines
65
+ .flatMap((line) =>
66
+ line
67
+ .trim()
68
+ .slice("!pip install ".length)
69
+ .split(WHITESPACE_REGEX)
70
+ .filter(Boolean)
71
+ )
72
+ .filter((spec) => !basePackageNames.has(packageName(spec)));
73
+
74
+ const codeWithoutPipLines = lines
75
+ .filter((line) => !line.trim().startsWith("!pip install "))
76
+ .join("\n");
77
+
78
+ if (extraPackages.length === 0) {
79
+ return { codeToRun: codeWithoutPipLines, installResult: { success: true } };
80
+ }
81
+
82
+ log.info({ requestId, extraPackages }, "installing extra packages");
83
+ const dynamicInstall = await sandbox.runCommand({
84
+ cmd: "pip",
85
+ args: ["install", ...extraPackages],
86
+ });
87
+ if (dynamicInstall.exitCode !== 0) {
88
+ const stderr = await dynamicInstall.stderr();
89
+ log.error({ requestId, stderr }, "dynamic package installation failed");
90
+ return {
91
+ codeToRun: code,
92
+ installResult: {
93
+ success: false,
94
+ result: {
95
+ message: `Failed to install packages: ${stderr}`,
96
+ chart: "",
97
+ },
98
+ },
99
+ };
100
+ }
101
+
102
+ return {
103
+ codeToRun: codeWithoutPipLines,
104
+ installResult: { success: true },
105
+ };
106
+ }
107
+
108
+ function createWrappedCode(codeToRun: string, chartPath: string): string {
109
+ return `
110
+ import sys
111
+ import json
112
+ import traceback
113
+
114
+ try:
115
+ import matplotlib.pyplot as _plt_module
116
+ _orig_savefig = _plt_module.savefig
117
+ def _intercepted_savefig(*args, **kwargs):
118
+ _orig_savefig('${chartPath}', format='png', bbox_inches='tight', dpi=100)
119
+ _user_target = args[0] if args else kwargs.get('fname')
120
+ if _user_target not in (None, '${chartPath}'):
121
+ return _orig_savefig(*args, **kwargs)
122
+ _plt_module.savefig = _intercepted_savefig
123
+ except ImportError:
124
+ pass
125
+
126
+ try:
127
+ exec(${JSON.stringify(codeToRun)})
128
+ try:
129
+ _locals = locals()
130
+ _globals = globals()
131
+ _chart_var = _locals.get("chart") or _globals.get("chart")
132
+ if (isinstance(_chart_var, dict)
133
+ and isinstance(_chart_var.get("type"), str)
134
+ and isinstance(_chart_var.get("elements"), list)):
135
+ print("__CHART_JSON__:" + json.dumps(_chart_var))
136
+ else:
137
+ if "result" in _locals:
138
+ print(_locals["result"])
139
+ elif "result" in _globals:
140
+ print(_globals["result"])
141
+ elif "results" in _locals:
142
+ print(_locals["results"])
143
+ elif "results" in _globals:
144
+ print(_globals["results"])
145
+ except Exception:
146
+ pass
147
+ try:
148
+ import matplotlib.pyplot as plt
149
+ if plt.get_fignums():
150
+ plt.savefig('${chartPath}', format='png', bbox_inches='tight', dpi=100)
151
+ plt.close('all')
152
+ except ImportError:
153
+ pass
154
+ print(json.dumps({"success": True}))
155
+ except Exception as e:
156
+ error_info = {"success": False, "error": {"name": type(e).__name__, "value": str(e), "traceback": traceback.format_exc()}}
157
+ print(json.dumps(error_info))
158
+ sys.exit(1)
159
+ `;
160
+ }
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((line) =>
188
+ line.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 from the sandboxed snippet.
196
+ }
197
+ outLines.splice(chartLineIdx, 1);
198
+ }
199
+
200
+ outputText = outLines.join("\n");
201
+ } catch {
202
+ outputText = stdout ?? "";
203
+ if (execResult.exitCode !== 0) {
204
+ execInfo = {
205
+ success: false,
206
+ error: {
207
+ name: "SandboxExecutionError",
208
+ value: "Execution completed without a parsable status trailer",
209
+ traceback: "",
210
+ },
211
+ };
212
+ }
213
+ }
214
+
215
+ return { outputText, chartData, execInfo };
216
+ }
217
+
218
+ async function checkForChart(
219
+ sandbox: Sandbox,
220
+ chartPath: string,
221
+ requestId: string,
222
+ log: CodeExecutionContext["log"]
223
+ ): Promise<{ base64: string; format: string } | undefined> {
224
+ const chartCheck = await sandbox.runCommand({
225
+ cmd: "test",
226
+ args: ["-f", chartPath],
227
+ });
228
+ if (chartCheck.exitCode === 0) {
229
+ const b64 = await (
230
+ await sandbox.runCommand({
231
+ cmd: "base64",
232
+ args: ["-w", "0", chartPath],
233
+ })
234
+ ).stdout();
235
+ log.info({ requestId }, "chart generated");
236
+ return { base64: (b64 ?? "").trim(), format: "png" };
237
+ }
238
+ return;
239
+ }
240
+
241
+ function buildResponseMessage({
242
+ outputText,
243
+ stderr,
244
+ execInfo,
245
+ log,
246
+ requestId,
247
+ }: {
248
+ outputText: string;
249
+ stderr: string;
250
+ execInfo: {
251
+ success: boolean;
252
+ error?: { name: string; value: string; traceback: string };
253
+ };
254
+ log: CodeExecutionContext["log"];
255
+ requestId: string;
256
+ }): string {
257
+ let message = "";
258
+
259
+ if (outputText) {
260
+ message += `${outputText}\n`;
261
+ }
262
+ if (stderr && stderr.trim().length > 0) {
263
+ message += `${stderr}\n`;
264
+ }
265
+ if (execInfo.error) {
266
+ message += `Error: ${execInfo.error.name}: ${execInfo.error.value}\n`;
267
+ log.error({ requestId, error: execInfo.error }, "python execution error");
268
+ }
269
+
270
+ return message;
271
+ }
272
+
273
+ export async function executePythonInSandbox({
274
+ sandbox,
275
+ code,
276
+ log,
277
+ requestId,
278
+ }: CodeExecutionContext): Promise<CodeExecutionResult> {
279
+ const basePackages = [
280
+ "matplotlib",
281
+ "pandas",
282
+ "numpy",
283
+ "sympy",
284
+ "yfinance",
285
+ ] as const;
286
+ const chartPath = "/tmp/chart.png";
287
+
288
+ const baseInstallResult = await installBasePackages(
289
+ sandbox,
290
+ basePackages,
291
+ requestId,
292
+ log
293
+ );
294
+ if (!baseInstallResult.success) {
295
+ return baseInstallResult.result ?? { message: "Unknown error", chart: "" };
296
+ }
297
+
298
+ const { codeToRun, installResult } = await processExtraPackages(
299
+ code,
300
+ basePackages,
301
+ sandbox,
302
+ requestId,
303
+ log
304
+ );
305
+ if (!installResult.success) {
306
+ return installResult.result ?? { message: "Unknown error", chart: "" };
307
+ }
308
+
309
+ const wrappedCode = createWrappedCode(codeToRun, chartPath);
310
+ const execResult = await sandbox.runCommand({
311
+ cmd: "python3",
312
+ args: ["-c", wrappedCode],
313
+ });
314
+
315
+ const { outputText, chartData, execInfo } =
316
+ await parseExecutionOutput(execResult);
317
+
318
+ const message = buildResponseMessage({
319
+ outputText,
320
+ stderr: await execResult.stderr(),
321
+ execInfo,
322
+ log,
323
+ requestId,
324
+ });
325
+
326
+ if (chartData) {
327
+ log.info({ requestId }, "interactive chart data returned");
328
+ return { message: message.trim(), chart: chartData };
329
+ }
330
+
331
+ const chartOut = await checkForChart(sandbox, chartPath, requestId, log);
332
+ return {
333
+ message: message.trim(),
334
+ chart: chartOut ?? "",
335
+ };
336
+ }