@oh-my-pi/pi-coding-agent 14.9.5 → 14.9.7

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 (44) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/package.json +7 -7
  3. package/src/cli/setup-cli.ts +14 -161
  4. package/src/cli/stats-cli.ts +56 -2
  5. package/src/cli.ts +0 -1
  6. package/src/config/settings-schema.ts +0 -10
  7. package/src/eval/eval.lark +30 -10
  8. package/src/eval/js/context-manager.ts +334 -564
  9. package/src/eval/js/shared/helpers.ts +237 -0
  10. package/src/eval/js/shared/indirect-eval.ts +30 -0
  11. package/src/eval/js/shared/rewrite-imports.ts +211 -0
  12. package/src/eval/js/shared/runtime.ts +168 -0
  13. package/src/eval/js/shared/types.ts +18 -0
  14. package/src/eval/js/tool-bridge.ts +2 -4
  15. package/src/eval/js/worker-core.ts +146 -0
  16. package/src/eval/js/worker-entry.ts +24 -0
  17. package/src/eval/js/worker-protocol.ts +41 -0
  18. package/src/eval/parse.ts +218 -49
  19. package/src/eval/py/display.ts +71 -0
  20. package/src/eval/py/executor.ts +74 -89
  21. package/src/eval/py/index.ts +1 -2
  22. package/src/eval/py/kernel.ts +472 -900
  23. package/src/eval/py/prelude.py +95 -7
  24. package/src/eval/py/runner.py +879 -0
  25. package/src/eval/py/runtime.ts +3 -16
  26. package/src/eval/py/tool-bridge.ts +137 -0
  27. package/src/export/html/template.generated.ts +1 -1
  28. package/src/export/html/template.js +93 -5
  29. package/src/internal-urls/docs-index.generated.ts +3 -3
  30. package/src/modes/controllers/command-controller.ts +0 -23
  31. package/src/prompts/tools/eval.md +14 -27
  32. package/src/session/agent-session.ts +0 -1
  33. package/src/session/history-storage.ts +77 -19
  34. package/src/tools/browser/tab-protocol.ts +4 -0
  35. package/src/tools/browser/tab-supervisor.ts +86 -5
  36. package/src/tools/browser/tab-worker.ts +104 -58
  37. package/src/tools/eval.ts +1 -1
  38. package/src/web/search/index.ts +6 -4
  39. package/src/cli/jupyter-cli.ts +0 -106
  40. package/src/commands/jupyter.ts +0 -32
  41. package/src/eval/py/cancellation.ts +0 -28
  42. package/src/eval/py/gateway-coordinator.ts +0 -424
  43. /package/src/eval/js/{prelude.ts → shared/prelude.ts} +0 -0
  44. /package/src/eval/js/{prelude.txt → shared/prelude.txt} +0 -0
@@ -160,19 +160,6 @@ export function resolveVenvPath(cwd: string): string | undefined {
160
160
  return undefined;
161
161
  }
162
162
 
163
- /**
164
- * Resolve the windowless Python executable (pythonw.exe) on Windows.
165
- * Falls back to the regular Python path if pythonw.exe is not available.
166
- */
167
- function resolveWindowlessPython(pythonPath: string): string {
168
- if (process.platform !== "win32") return pythonPath;
169
- const pythonwPath = pythonPath.replace(/python\.exe$/i, "pythonw.exe");
170
- if (pythonwPath !== pythonPath && fs.existsSync(pythonwPath)) {
171
- return pythonwPath;
172
- }
173
- return pythonPath;
174
- }
175
-
176
163
  /**
177
164
  * Resolve Python runtime including executable path, environment, and venv detection.
178
165
  */
@@ -189,7 +176,7 @@ export function resolvePythonRuntime(cwd: string, baseEnv: Record<string, string
189
176
  const currentPath = env[pathKey];
190
177
  env[pathKey] = currentPath ? `${binDir}${path.delimiter}${currentPath}` : binDir;
191
178
  return {
192
- pythonPath: resolveWindowlessPython(pythonCandidate),
179
+ pythonPath: pythonCandidate,
193
180
  env,
194
181
  venvPath,
195
182
  };
@@ -205,7 +192,7 @@ export function resolvePythonRuntime(cwd: string, baseEnv: Record<string, string
205
192
  process.platform === "win32" ? path.join(managed.venvPath, "Scripts") : path.join(managed.venvPath, "bin");
206
193
  env[pathKey] = currentPath ? `${managedBin}${path.delimiter}${currentPath}` : managedBin;
207
194
  return {
208
- pythonPath: resolveWindowlessPython(managed.pythonPath),
195
+ pythonPath: managed.pythonPath,
209
196
  env,
210
197
  venvPath: managed.venvPath,
211
198
  };
@@ -216,7 +203,7 @@ export function resolvePythonRuntime(cwd: string, baseEnv: Record<string, string
216
203
  throw new Error("Python executable not found on PATH");
217
204
  }
218
205
  return {
219
- pythonPath: resolveWindowlessPython(pythonPath),
206
+ pythonPath,
220
207
  env,
221
208
  };
222
209
  }
@@ -0,0 +1,137 @@
1
+ /**
2
+ * HTTP loopback bridge that lets the Python kernel synchronously invoke
3
+ * host-side tools by name, mirroring the JS worker's `tool.<name>(args)` proxy.
4
+ *
5
+ * The Python prelude builds a `tool` proxy that POSTs to `/v1/tool` over a
6
+ * 127.0.0.1 loopback socket; the host resolves the request against the
7
+ * `ToolSession` registered for the current execution and forwards to the same
8
+ * `callSessionTool` implementation the JS bridge uses.
9
+ */
10
+ import { logger } from "@oh-my-pi/pi-utils";
11
+ import type { ToolSession } from "../../tools";
12
+ import { callSessionTool, type JsStatusEvent } from "../js/tool-bridge";
13
+
14
+ export interface PyToolBridgeEntry {
15
+ toolSession: ToolSession;
16
+ signal?: AbortSignal;
17
+ emitStatus?: (event: JsStatusEvent) => void;
18
+ }
19
+
20
+ export interface PyToolBridgeInfo {
21
+ url: string;
22
+ token: string;
23
+ }
24
+
25
+ interface BridgeServer {
26
+ info: PyToolBridgeInfo;
27
+ stop: () => Promise<void>;
28
+ }
29
+
30
+ const registrations = new Map<string, PyToolBridgeEntry>();
31
+ let serverPromise: Promise<BridgeServer> | null = null;
32
+
33
+ async function startServer(): Promise<BridgeServer> {
34
+ const token = crypto.randomUUID();
35
+ const server = Bun.serve({
36
+ hostname: "127.0.0.1",
37
+ port: 0,
38
+ async fetch(req) {
39
+ const url = new URL(req.url);
40
+ if (req.method !== "POST" || url.pathname !== "/v1/tool") {
41
+ return new Response("Not Found", { status: 404 });
42
+ }
43
+ if (req.headers.get("authorization") !== `Bearer ${token}`) {
44
+ return new Response("Forbidden", { status: 403 });
45
+ }
46
+
47
+ let body: { session?: unknown; name?: unknown; args?: unknown };
48
+ try {
49
+ body = (await req.json()) as { session?: unknown; name?: unknown; args?: unknown };
50
+ } catch {
51
+ return Response.json({ ok: false, error: "Invalid JSON body" }, { status: 400 });
52
+ }
53
+ const sessionId = typeof body.session === "string" ? body.session : "";
54
+ const name = typeof body.name === "string" ? body.name : "";
55
+ if (!sessionId || !name) {
56
+ return Response.json({ ok: false, error: "Missing session/name" }, { status: 400 });
57
+ }
58
+ const entry = registrations.get(sessionId);
59
+ if (!entry) {
60
+ return Response.json(
61
+ { ok: false, error: `No active Python tool bridge session: ${sessionId}` },
62
+ { status: 200 },
63
+ );
64
+ }
65
+
66
+ try {
67
+ const value = await callSessionTool(name, body.args, {
68
+ session: entry.toolSession,
69
+ signal: entry.signal,
70
+ emitStatus: entry.emitStatus,
71
+ });
72
+ return Response.json({ ok: true, value });
73
+ } catch (err) {
74
+ return Response.json({
75
+ ok: false,
76
+ error: err instanceof Error ? err.message : String(err),
77
+ });
78
+ }
79
+ },
80
+ });
81
+
82
+ const info: PyToolBridgeInfo = {
83
+ url: `http://${server.hostname}:${server.port}`,
84
+ token,
85
+ };
86
+ logger.debug("Python tool bridge listening", { url: info.url });
87
+
88
+ return {
89
+ info,
90
+ stop: async () => {
91
+ await server.stop(true);
92
+ },
93
+ };
94
+ }
95
+
96
+ /** Starts the bridge server lazily and returns its connection info. */
97
+ export async function ensurePyToolBridge(): Promise<PyToolBridgeInfo> {
98
+ if (!serverPromise) {
99
+ serverPromise = startServer();
100
+ }
101
+ try {
102
+ const server = await serverPromise;
103
+ return server.info;
104
+ } catch (err) {
105
+ serverPromise = null;
106
+ throw err;
107
+ }
108
+ }
109
+
110
+ /**
111
+ * Register a tool session for the duration of one execution. The returned
112
+ * function MUST be called to remove the entry once execution finishes.
113
+ */
114
+ export function registerPyToolBridge(sessionId: string, entry: PyToolBridgeEntry): () => void {
115
+ registrations.set(sessionId, entry);
116
+ return () => {
117
+ if (registrations.get(sessionId) === entry) {
118
+ registrations.delete(sessionId);
119
+ }
120
+ };
121
+ }
122
+
123
+ /** Stop the bridge and clear registrations. Test-only / shutdown helper. */
124
+ export async function disposePyToolBridge(): Promise<void> {
125
+ registrations.clear();
126
+ const pending = serverPromise;
127
+ serverPromise = null;
128
+ if (!pending) return;
129
+ try {
130
+ const server = await pending;
131
+ await server.stop();
132
+ } catch (err) {
133
+ logger.debug("Failed to stop Python tool bridge", {
134
+ error: err instanceof Error ? err.message : String(err),
135
+ });
136
+ }
137
+ }