@oh-my-pi/pi-coding-agent 5.5.0 → 5.6.70
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.
- package/CHANGELOG.md +105 -0
- package/docs/python-repl.md +77 -0
- package/examples/hooks/snake.ts +7 -7
- package/package.json +5 -5
- package/src/bun-imports.d.ts +6 -0
- package/src/cli/args.ts +7 -0
- package/src/cli/setup-cli.ts +231 -0
- package/src/cli.ts +2 -0
- package/src/core/agent-session.ts +118 -15
- package/src/core/bash-executor.ts +3 -84
- package/src/core/compaction/compaction.ts +10 -5
- package/src/core/extensions/index.ts +2 -0
- package/src/core/extensions/loader.ts +13 -1
- package/src/core/extensions/runner.ts +50 -2
- package/src/core/extensions/types.ts +67 -2
- package/src/core/keybindings.ts +51 -1
- package/src/core/prompt-templates.ts +15 -0
- package/src/core/python-executor-display.test.ts +42 -0
- package/src/core/python-executor-lifecycle.test.ts +99 -0
- package/src/core/python-executor-mapping.test.ts +41 -0
- package/src/core/python-executor-per-call.test.ts +49 -0
- package/src/core/python-executor-session.test.ts +103 -0
- package/src/core/python-executor-streaming.test.ts +77 -0
- package/src/core/python-executor-timeout.test.ts +35 -0
- package/src/core/python-executor.lifecycle.test.ts +139 -0
- package/src/core/python-executor.result.test.ts +49 -0
- package/src/core/python-executor.test.ts +180 -0
- package/src/core/python-executor.ts +313 -0
- package/src/core/python-gateway-coordinator.ts +832 -0
- package/src/core/python-kernel-display.test.ts +54 -0
- package/src/core/python-kernel-env.test.ts +138 -0
- package/src/core/python-kernel-session.test.ts +87 -0
- package/src/core/python-kernel-ws.test.ts +104 -0
- package/src/core/python-kernel.lifecycle.test.ts +249 -0
- package/src/core/python-kernel.test.ts +461 -0
- package/src/core/python-kernel.ts +1182 -0
- package/src/core/python-modules.test.ts +102 -0
- package/src/core/python-modules.ts +110 -0
- package/src/core/python-prelude.py +889 -0
- package/src/core/python-prelude.test.ts +140 -0
- package/src/core/python-prelude.ts +3 -0
- package/src/core/sdk.ts +24 -6
- package/src/core/session-manager.ts +174 -82
- package/src/core/settings-manager-python.test.ts +23 -0
- package/src/core/settings-manager.ts +202 -0
- package/src/core/streaming-output.test.ts +26 -0
- package/src/core/streaming-output.ts +100 -0
- package/src/core/system-prompt.python.test.ts +17 -0
- package/src/core/system-prompt.ts +3 -1
- package/src/core/timings.ts +1 -1
- package/src/core/tools/bash.ts +13 -2
- package/src/core/tools/edit-diff.ts +9 -1
- package/src/core/tools/index.test.ts +50 -23
- package/src/core/tools/index.ts +83 -1
- package/src/core/tools/python-execution.test.ts +68 -0
- package/src/core/tools/python-fallback.test.ts +72 -0
- package/src/core/tools/python-renderer.test.ts +36 -0
- package/src/core/tools/python-tool-mode.test.ts +43 -0
- package/src/core/tools/python.test.ts +121 -0
- package/src/core/tools/python.ts +760 -0
- package/src/core/tools/renderers.ts +2 -0
- package/src/core/tools/schema-validation.test.ts +1 -0
- package/src/core/tools/task/executor.ts +146 -3
- package/src/core/tools/task/worker-protocol.ts +32 -2
- package/src/core/tools/task/worker.ts +182 -15
- package/src/index.ts +6 -0
- package/src/main.ts +136 -40
- package/src/modes/interactive/components/custom-editor.ts +16 -31
- package/src/modes/interactive/components/extensions/extension-dashboard.ts +5 -16
- package/src/modes/interactive/components/extensions/extension-list.ts +5 -13
- package/src/modes/interactive/components/history-search.ts +5 -8
- package/src/modes/interactive/components/hook-editor.ts +3 -4
- package/src/modes/interactive/components/hook-input.ts +3 -3
- package/src/modes/interactive/components/hook-selector.ts +5 -15
- package/src/modes/interactive/components/index.ts +1 -0
- package/src/modes/interactive/components/keybinding-hints.ts +66 -0
- package/src/modes/interactive/components/model-selector.ts +53 -66
- package/src/modes/interactive/components/oauth-selector.ts +5 -5
- package/src/modes/interactive/components/session-selector.ts +29 -23
- package/src/modes/interactive/components/settings-defs.ts +404 -196
- package/src/modes/interactive/components/settings-selector.ts +14 -10
- package/src/modes/interactive/components/status-line-segment-editor.ts +7 -7
- package/src/modes/interactive/components/tool-execution.ts +8 -0
- package/src/modes/interactive/components/tree-selector.ts +29 -23
- package/src/modes/interactive/components/user-message-selector.ts +6 -17
- package/src/modes/interactive/controllers/command-controller.ts +86 -37
- package/src/modes/interactive/controllers/event-controller.ts +8 -0
- package/src/modes/interactive/controllers/extension-ui-controller.ts +51 -0
- package/src/modes/interactive/controllers/input-controller.ts +42 -6
- package/src/modes/interactive/interactive-mode.ts +56 -30
- package/src/modes/interactive/theme/theme-schema.json +2 -2
- package/src/modes/interactive/types.ts +6 -1
- package/src/modes/interactive/utils/ui-helpers.ts +2 -1
- package/src/modes/print-mode.ts +23 -0
- package/src/modes/rpc/rpc-mode.ts +21 -0
- package/src/prompts/agents/reviewer.md +1 -1
- package/src/prompts/system/system-prompt.md +32 -1
- package/src/prompts/tools/python.md +91 -0
package/src/core/tools/index.ts
CHANGED
|
@@ -24,6 +24,7 @@ export {
|
|
|
24
24
|
} from "./lsp/index";
|
|
25
25
|
export { createNotebookTool, type NotebookToolDetails } from "./notebook";
|
|
26
26
|
export { createOutputTool, type OutputToolDetails } from "./output";
|
|
27
|
+
export { createPythonTool, type PythonToolDetails } from "./python";
|
|
27
28
|
export { createReadTool, type ReadToolDetails } from "./read";
|
|
28
29
|
export { reportFindingTool, type SubmitReviewDetails } from "./review";
|
|
29
30
|
export { createSshTool, type SSHToolDetails } from "./ssh";
|
|
@@ -63,6 +64,9 @@ export { createWriteTool, type WriteToolDetails } from "./write";
|
|
|
63
64
|
|
|
64
65
|
import type { AgentTool } from "@oh-my-pi/pi-agent-core";
|
|
65
66
|
import type { EventBus } from "../event-bus";
|
|
67
|
+
import { logger } from "../logger";
|
|
68
|
+
import { getPreludeDocs, warmPythonEnvironment } from "../python-executor";
|
|
69
|
+
import { checkPythonKernelAvailability } from "../python-kernel";
|
|
66
70
|
import type { BashInterceptorRule } from "../settings-manager";
|
|
67
71
|
import { createAskTool } from "./ask";
|
|
68
72
|
import { createBashTool } from "./bash";
|
|
@@ -76,6 +80,7 @@ import { createLsTool } from "./ls";
|
|
|
76
80
|
import { createLspTool } from "./lsp/index";
|
|
77
81
|
import { createNotebookTool } from "./notebook";
|
|
78
82
|
import { createOutputTool } from "./output";
|
|
83
|
+
import { createPythonTool } from "./python";
|
|
79
84
|
import { createReadTool } from "./read";
|
|
80
85
|
import { reportFindingTool } from "./review";
|
|
81
86
|
import { createSshTool } from "./ssh";
|
|
@@ -129,6 +134,9 @@ export interface ToolSession {
|
|
|
129
134
|
getBashInterceptorEnabled(): boolean;
|
|
130
135
|
getBashInterceptorSimpleLsEnabled(): boolean;
|
|
131
136
|
getBashInterceptorRules(): BashInterceptorRule[];
|
|
137
|
+
getPythonToolMode?(): "ipy-only" | "bash-only" | "both";
|
|
138
|
+
getPythonKernelMode?(): "session" | "per-call";
|
|
139
|
+
getPythonSharedGateway?(): boolean;
|
|
132
140
|
};
|
|
133
141
|
}
|
|
134
142
|
|
|
@@ -137,6 +145,7 @@ type ToolFactory = (session: ToolSession) => Tool | null | Promise<Tool | null>;
|
|
|
137
145
|
export const BUILTIN_TOOLS: Record<string, ToolFactory> = {
|
|
138
146
|
ask: createAskTool,
|
|
139
147
|
bash: createBashTool,
|
|
148
|
+
python: createPythonTool,
|
|
140
149
|
calc: createCalculatorTool,
|
|
141
150
|
ssh: createSshTool,
|
|
142
151
|
edit: createEditTool,
|
|
@@ -162,6 +171,36 @@ export const HIDDEN_TOOLS: Record<string, ToolFactory> = {
|
|
|
162
171
|
|
|
163
172
|
export type ToolName = keyof typeof BUILTIN_TOOLS;
|
|
164
173
|
|
|
174
|
+
export type PythonToolMode = "ipy-only" | "bash-only" | "both";
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Parse OMP_PY environment variable to determine Python tool mode.
|
|
178
|
+
* Returns null if not set or invalid.
|
|
179
|
+
*
|
|
180
|
+
* Values:
|
|
181
|
+
* - "0" or "bash" → bash-only
|
|
182
|
+
* - "1" or "py" → ipy-only
|
|
183
|
+
* - "mix" or "both" → both
|
|
184
|
+
*/
|
|
185
|
+
function getPythonModeFromEnv(): PythonToolMode | null {
|
|
186
|
+
const value = process.env.OMP_PY?.toLowerCase();
|
|
187
|
+
if (!value) return null;
|
|
188
|
+
|
|
189
|
+
switch (value) {
|
|
190
|
+
case "0":
|
|
191
|
+
case "bash":
|
|
192
|
+
return "bash-only";
|
|
193
|
+
case "1":
|
|
194
|
+
case "py":
|
|
195
|
+
return "ipy-only";
|
|
196
|
+
case "mix":
|
|
197
|
+
case "both":
|
|
198
|
+
return "both";
|
|
199
|
+
default:
|
|
200
|
+
return null;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
165
204
|
/**
|
|
166
205
|
* Create tools from BUILTIN_TOOLS registry.
|
|
167
206
|
*/
|
|
@@ -169,8 +208,51 @@ export async function createTools(session: ToolSession, toolNames?: string[]): P
|
|
|
169
208
|
const includeComplete = session.requireCompleteTool === true;
|
|
170
209
|
const enableLsp = session.enableLsp ?? true;
|
|
171
210
|
const requestedTools = toolNames && toolNames.length > 0 ? [...new Set(toolNames)] : undefined;
|
|
211
|
+
const pythonMode = getPythonModeFromEnv() ?? session.settings?.getPythonToolMode?.() ?? "ipy-only";
|
|
212
|
+
let pythonAvailable = true;
|
|
213
|
+
const shouldCheckPython =
|
|
214
|
+
pythonMode !== "bash-only" &&
|
|
215
|
+
(requestedTools === undefined || requestedTools.includes("python") || pythonMode === "ipy-only");
|
|
216
|
+
const isTestEnv = process.env.BUN_ENV === "test" || process.env.NODE_ENV === "test";
|
|
217
|
+
if (shouldCheckPython) {
|
|
218
|
+
const availability = await checkPythonKernelAvailability(session.cwd);
|
|
219
|
+
pythonAvailable = availability.ok;
|
|
220
|
+
if (!availability.ok) {
|
|
221
|
+
logger.warn("Python kernel unavailable, falling back to bash", {
|
|
222
|
+
reason: availability.reason,
|
|
223
|
+
});
|
|
224
|
+
} else if (!isTestEnv && getPreludeDocs().length === 0) {
|
|
225
|
+
const sessionFile = session.getSessionFile?.() ?? undefined;
|
|
226
|
+
const warmSessionId = sessionFile ? `session:${sessionFile}:workdir:${session.cwd}` : `cwd:${session.cwd}`;
|
|
227
|
+
void warmPythonEnvironment(session.cwd, warmSessionId, session.settings?.getPythonSharedGateway?.()).catch(
|
|
228
|
+
(err) => {
|
|
229
|
+
logger.warn("Failed to warm Python environment", {
|
|
230
|
+
error: err instanceof Error ? err.message : String(err),
|
|
231
|
+
});
|
|
232
|
+
},
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const effectiveMode = pythonAvailable ? pythonMode : "bash-only";
|
|
238
|
+
const allowBash = effectiveMode !== "ipy-only";
|
|
239
|
+
const allowPython = effectiveMode !== "bash-only";
|
|
240
|
+
if (
|
|
241
|
+
requestedTools &&
|
|
242
|
+
allowBash &&
|
|
243
|
+
!allowPython &&
|
|
244
|
+
requestedTools.includes("python") &&
|
|
245
|
+
!requestedTools.includes("bash")
|
|
246
|
+
) {
|
|
247
|
+
requestedTools.push("bash");
|
|
248
|
+
}
|
|
172
249
|
const allTools: Record<string, ToolFactory> = { ...BUILTIN_TOOLS, ...HIDDEN_TOOLS };
|
|
173
|
-
const isToolAllowed = (name: string) =>
|
|
250
|
+
const isToolAllowed = (name: string) => {
|
|
251
|
+
if (name === "lsp") return enableLsp;
|
|
252
|
+
if (name === "bash") return allowBash;
|
|
253
|
+
if (name === "python") return allowPython;
|
|
254
|
+
return true;
|
|
255
|
+
};
|
|
174
256
|
if (includeComplete && requestedTools && !requestedTools.includes("complete")) {
|
|
175
257
|
requestedTools.push("complete");
|
|
176
258
|
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "bun:test";
|
|
2
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import * as pythonExecutor from "../python-executor";
|
|
6
|
+
import type { ToolSession } from "./index";
|
|
7
|
+
import { createPythonTool } from "./python";
|
|
8
|
+
|
|
9
|
+
function createSession(cwd: string): ToolSession {
|
|
10
|
+
return {
|
|
11
|
+
cwd,
|
|
12
|
+
hasUI: false,
|
|
13
|
+
getSessionFile: () => "session-file",
|
|
14
|
+
getSessionSpawns: () => "*",
|
|
15
|
+
settings: {
|
|
16
|
+
getImageAutoResize: () => true,
|
|
17
|
+
getLspFormatOnWrite: () => true,
|
|
18
|
+
getLspDiagnosticsOnWrite: () => true,
|
|
19
|
+
getLspDiagnosticsOnEdit: () => false,
|
|
20
|
+
getEditFuzzyMatch: () => true,
|
|
21
|
+
getGitToolEnabled: () => true,
|
|
22
|
+
getBashInterceptorEnabled: () => true,
|
|
23
|
+
getBashInterceptorSimpleLsEnabled: () => true,
|
|
24
|
+
getBashInterceptorRules: () => [],
|
|
25
|
+
getPythonToolMode: () => "ipy-only",
|
|
26
|
+
getPythonKernelMode: () => "per-call",
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
describe("python tool execution", () => {
|
|
32
|
+
it("passes kernel options from settings and args", async () => {
|
|
33
|
+
const tempDir = mkdtempSync(join(tmpdir(), "python-tool-"));
|
|
34
|
+
const executeSpy = vi.spyOn(pythonExecutor, "executePython").mockResolvedValue({
|
|
35
|
+
output: "ok",
|
|
36
|
+
exitCode: 0,
|
|
37
|
+
cancelled: false,
|
|
38
|
+
truncated: false,
|
|
39
|
+
displayOutputs: [],
|
|
40
|
+
stdinRequested: false,
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const tool = createPythonTool(createSession(tempDir));
|
|
44
|
+
const result = await tool.execute(
|
|
45
|
+
"call-id",
|
|
46
|
+
{ code: "print('hi')", timeout: 5, workdir: tempDir, reset: true },
|
|
47
|
+
undefined,
|
|
48
|
+
undefined,
|
|
49
|
+
undefined,
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
expect(executeSpy).toHaveBeenCalledWith(
|
|
53
|
+
"print('hi')",
|
|
54
|
+
expect.objectContaining({
|
|
55
|
+
cwd: tempDir,
|
|
56
|
+
timeout: 5000,
|
|
57
|
+
sessionId: `session:session-file:workdir:${tempDir}`,
|
|
58
|
+
kernelMode: "per-call",
|
|
59
|
+
reset: true,
|
|
60
|
+
}),
|
|
61
|
+
);
|
|
62
|
+
const text = result.content.find((item) => item.type === "text")?.text;
|
|
63
|
+
expect(text).toBe("ok");
|
|
64
|
+
|
|
65
|
+
executeSpy.mockRestore();
|
|
66
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
67
|
+
});
|
|
68
|
+
});
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "bun:test";
|
|
2
|
+
import * as pythonKernelModule from "../python-kernel";
|
|
3
|
+
import type { ToolSession } from "./index";
|
|
4
|
+
import { createTools } from "./index";
|
|
5
|
+
|
|
6
|
+
function createTestSession(overrides: Partial<ToolSession> = {}): ToolSession {
|
|
7
|
+
return {
|
|
8
|
+
cwd: "/tmp/test",
|
|
9
|
+
hasUI: false,
|
|
10
|
+
getSessionFile: () => null,
|
|
11
|
+
getSessionSpawns: () => "*",
|
|
12
|
+
...overrides,
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function createBaseSettings(overrides: Partial<NonNullable<ToolSession["settings"]>> = {}) {
|
|
17
|
+
return {
|
|
18
|
+
getImageAutoResize: () => true,
|
|
19
|
+
getLspFormatOnWrite: () => true,
|
|
20
|
+
getLspDiagnosticsOnWrite: () => true,
|
|
21
|
+
getLspDiagnosticsOnEdit: () => false,
|
|
22
|
+
getEditFuzzyMatch: () => true,
|
|
23
|
+
getGitToolEnabled: () => true,
|
|
24
|
+
getBashInterceptorEnabled: () => true,
|
|
25
|
+
getBashInterceptorSimpleLsEnabled: () => true,
|
|
26
|
+
getBashInterceptorRules: () => [],
|
|
27
|
+
...overrides,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
describe("createTools python fallback", () => {
|
|
32
|
+
it("falls back to bash-only when kernel unavailable", async () => {
|
|
33
|
+
const availabilitySpy = vi
|
|
34
|
+
.spyOn(pythonKernelModule, "checkPythonKernelAvailability")
|
|
35
|
+
.mockResolvedValue({ ok: false, reason: "unavailable" });
|
|
36
|
+
|
|
37
|
+
const session = createTestSession({
|
|
38
|
+
settings: createBaseSettings({
|
|
39
|
+
getPythonToolMode: () => "ipy-only",
|
|
40
|
+
getPythonKernelMode: () => "session",
|
|
41
|
+
}),
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const tools = await createTools(session, ["python"]);
|
|
45
|
+
const names = tools.map((tool) => tool.name).sort();
|
|
46
|
+
|
|
47
|
+
expect(names).toEqual(["bash"]);
|
|
48
|
+
|
|
49
|
+
availabilitySpy.mockRestore();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("keeps bash when python mode is both but unavailable", async () => {
|
|
53
|
+
const availabilitySpy = vi
|
|
54
|
+
.spyOn(pythonKernelModule, "checkPythonKernelAvailability")
|
|
55
|
+
.mockResolvedValue({ ok: false, reason: "unavailable" });
|
|
56
|
+
|
|
57
|
+
const session = createTestSession({
|
|
58
|
+
settings: createBaseSettings({
|
|
59
|
+
getPythonToolMode: () => "both",
|
|
60
|
+
getPythonKernelMode: () => "session",
|
|
61
|
+
}),
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
const tools = await createTools(session);
|
|
65
|
+
const names = tools.map((tool) => tool.name);
|
|
66
|
+
|
|
67
|
+
expect(names).toContain("bash");
|
|
68
|
+
expect(names).not.toContain("python");
|
|
69
|
+
|
|
70
|
+
availabilitySpy.mockRestore();
|
|
71
|
+
});
|
|
72
|
+
});
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import stripAnsi from "strip-ansi";
|
|
3
|
+
import { getThemeByName } from "../../modes/interactive/theme/theme";
|
|
4
|
+
import { pythonToolRenderer } from "./python";
|
|
5
|
+
import { truncateTail } from "./truncate";
|
|
6
|
+
|
|
7
|
+
describe("pythonToolRenderer", () => {
|
|
8
|
+
it("renders truncated output when collapsed and full output when expanded", () => {
|
|
9
|
+
const theme = getThemeByName("dark");
|
|
10
|
+
expect(theme).toBeDefined();
|
|
11
|
+
const uiTheme = theme!;
|
|
12
|
+
|
|
13
|
+
const fullOutput = ["line 1", "line 2", "line 3", "line 4"].join("\n");
|
|
14
|
+
const truncation = truncateTail(fullOutput, { maxLines: 2, maxBytes: 128 });
|
|
15
|
+
|
|
16
|
+
const result = {
|
|
17
|
+
content: [{ type: "text", text: truncation.content }],
|
|
18
|
+
details: {
|
|
19
|
+
truncation,
|
|
20
|
+
fullOutput,
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const collapsed = pythonToolRenderer.renderResult(result, { expanded: false, isPartial: false }, uiTheme);
|
|
25
|
+
const collapsedLines = stripAnsi(collapsed.render(80).join("\n"));
|
|
26
|
+
expect(collapsedLines).toContain("line 4");
|
|
27
|
+
expect(collapsedLines).not.toContain("line 1");
|
|
28
|
+
expect(collapsedLines).toContain("Truncated:");
|
|
29
|
+
|
|
30
|
+
const expanded = pythonToolRenderer.renderResult(result, { expanded: true, isPartial: false }, uiTheme);
|
|
31
|
+
const expandedLines = stripAnsi(expanded.render(80).join("\n"));
|
|
32
|
+
expect(expandedLines).toContain("line 1");
|
|
33
|
+
expect(expandedLines).toContain("line 4");
|
|
34
|
+
expect(expandedLines).not.toContain("Truncated:");
|
|
35
|
+
});
|
|
36
|
+
});
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import { createTools, type ToolSession } from "./index";
|
|
3
|
+
|
|
4
|
+
function createSession(overrides: Partial<ToolSession> = {}): ToolSession {
|
|
5
|
+
return {
|
|
6
|
+
cwd: "/tmp/test",
|
|
7
|
+
hasUI: false,
|
|
8
|
+
getSessionFile: () => null,
|
|
9
|
+
getSessionSpawns: () => "*",
|
|
10
|
+
settings: {
|
|
11
|
+
getImageAutoResize: () => true,
|
|
12
|
+
getLspFormatOnWrite: () => true,
|
|
13
|
+
getLspDiagnosticsOnWrite: () => true,
|
|
14
|
+
getLspDiagnosticsOnEdit: () => false,
|
|
15
|
+
getEditFuzzyMatch: () => true,
|
|
16
|
+
getGitToolEnabled: () => true,
|
|
17
|
+
getBashInterceptorEnabled: () => true,
|
|
18
|
+
getBashInterceptorSimpleLsEnabled: () => true,
|
|
19
|
+
getBashInterceptorRules: () => [],
|
|
20
|
+
getPythonToolMode: () => "bash-only",
|
|
21
|
+
getPythonKernelMode: () => "session",
|
|
22
|
+
},
|
|
23
|
+
...overrides,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
describe("createTools python fallback", () => {
|
|
28
|
+
it("falls back to bash when python is requested but disabled", async () => {
|
|
29
|
+
const previous = process.env.OMP_PYTHON_SKIP_CHECK;
|
|
30
|
+
process.env.OMP_PYTHON_SKIP_CHECK = "1";
|
|
31
|
+
const session = createSession();
|
|
32
|
+
const tools = await createTools(session, ["python"]);
|
|
33
|
+
const names = tools.map((tool) => tool.name);
|
|
34
|
+
|
|
35
|
+
expect(names).toEqual(["bash"]);
|
|
36
|
+
|
|
37
|
+
if (previous === undefined) {
|
|
38
|
+
delete process.env.OMP_PYTHON_SKIP_CHECK;
|
|
39
|
+
} else {
|
|
40
|
+
process.env.OMP_PYTHON_SKIP_CHECK = previous;
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
});
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { afterAll, beforeAll, describe, expect, it, vi } from "bun:test";
|
|
2
|
+
import * as pythonExecutor from "../python-executor";
|
|
3
|
+
import { createTools, type ToolSession } from "./index";
|
|
4
|
+
import { createPythonTool } from "./python";
|
|
5
|
+
|
|
6
|
+
let previousSkipCheck: string | undefined;
|
|
7
|
+
|
|
8
|
+
beforeAll(() => {
|
|
9
|
+
previousSkipCheck = process.env.OMP_PYTHON_SKIP_CHECK;
|
|
10
|
+
process.env.OMP_PYTHON_SKIP_CHECK = "1";
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
afterAll(() => {
|
|
14
|
+
if (previousSkipCheck === undefined) {
|
|
15
|
+
delete process.env.OMP_PYTHON_SKIP_CHECK;
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
process.env.OMP_PYTHON_SKIP_CHECK = previousSkipCheck;
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
function createSession(overrides: Partial<ToolSession> = {}): ToolSession {
|
|
22
|
+
return {
|
|
23
|
+
cwd: "/tmp/test",
|
|
24
|
+
hasUI: false,
|
|
25
|
+
getSessionFile: () => null,
|
|
26
|
+
getSessionSpawns: () => "*",
|
|
27
|
+
...overrides,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function createSettings(toolMode: "ipy-only" | "bash-only" | "both") {
|
|
32
|
+
return {
|
|
33
|
+
getImageAutoResize: () => true,
|
|
34
|
+
getLspFormatOnWrite: () => true,
|
|
35
|
+
getLspDiagnosticsOnWrite: () => true,
|
|
36
|
+
getLspDiagnosticsOnEdit: () => false,
|
|
37
|
+
getEditFuzzyMatch: () => true,
|
|
38
|
+
getGitToolEnabled: () => true,
|
|
39
|
+
getBashInterceptorEnabled: () => true,
|
|
40
|
+
getBashInterceptorSimpleLsEnabled: () => true,
|
|
41
|
+
getBashInterceptorRules: () => [],
|
|
42
|
+
getPythonToolMode: () => toolMode,
|
|
43
|
+
getPythonKernelMode: () => "session" as const,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
describe("python tool schema", () => {
|
|
48
|
+
it("exposes expected parameters", () => {
|
|
49
|
+
const tool = createPythonTool(createSession());
|
|
50
|
+
const schema = tool.parameters as {
|
|
51
|
+
type: string;
|
|
52
|
+
properties: Record<string, { type: string; description?: string }>;
|
|
53
|
+
required?: string[];
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
expect(schema.type).toBe("object");
|
|
57
|
+
expect(schema.properties.code.type).toBe("string");
|
|
58
|
+
expect(schema.properties.timeout.type).toBe("number");
|
|
59
|
+
expect(schema.properties.workdir.type).toBe("string");
|
|
60
|
+
expect(schema.properties.reset.type).toBe("boolean");
|
|
61
|
+
expect(schema.required).toEqual(["code"]);
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
describe("python tool docs template", () => {
|
|
66
|
+
it("renders dynamic helper docs", () => {
|
|
67
|
+
const docs = [
|
|
68
|
+
{
|
|
69
|
+
name: "read",
|
|
70
|
+
signature: "(path)",
|
|
71
|
+
docstring: "Read file contents.",
|
|
72
|
+
category: "File I/O",
|
|
73
|
+
},
|
|
74
|
+
];
|
|
75
|
+
const spy = vi.spyOn(pythonExecutor, "getPreludeDocs").mockReturnValue(docs);
|
|
76
|
+
|
|
77
|
+
const tool = createPythonTool(createSession());
|
|
78
|
+
|
|
79
|
+
expect(tool.description).toContain("### File I/O");
|
|
80
|
+
expect(tool.description).toContain("read(path)");
|
|
81
|
+
expect(tool.description).toContain("Read file contents.");
|
|
82
|
+
|
|
83
|
+
spy.mockRestore();
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("renders fallback when docs are unavailable", () => {
|
|
87
|
+
const spy = vi.spyOn(pythonExecutor, "getPreludeDocs").mockReturnValue([]);
|
|
88
|
+
|
|
89
|
+
const tool = createPythonTool(createSession());
|
|
90
|
+
|
|
91
|
+
expect(tool.description).toContain("Documentation unavailable — Python kernel failed to start");
|
|
92
|
+
|
|
93
|
+
spy.mockRestore();
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
describe("python tool exposure", () => {
|
|
98
|
+
it("includes python only in ipy-only mode", async () => {
|
|
99
|
+
const session = createSession({ settings: createSettings("ipy-only") });
|
|
100
|
+
const tools = await createTools(session);
|
|
101
|
+
const names = tools.map((tool) => tool.name);
|
|
102
|
+
expect(names).toContain("python");
|
|
103
|
+
expect(names).not.toContain("bash");
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("includes bash only in bash-only mode", async () => {
|
|
107
|
+
const session = createSession({ settings: createSettings("bash-only") });
|
|
108
|
+
const tools = await createTools(session);
|
|
109
|
+
const names = tools.map((tool) => tool.name);
|
|
110
|
+
expect(names).toContain("bash");
|
|
111
|
+
expect(names).not.toContain("python");
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("includes bash and python in both mode", async () => {
|
|
115
|
+
const session = createSession({ settings: createSettings("both") });
|
|
116
|
+
const tools = await createTools(session);
|
|
117
|
+
const names = tools.map((tool) => tool.name);
|
|
118
|
+
expect(names).toContain("bash");
|
|
119
|
+
expect(names).toContain("python");
|
|
120
|
+
});
|
|
121
|
+
});
|