@oh-my-pi/pi-coding-agent 14.9.5 → 14.9.8
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 +69 -0
- package/package.json +7 -7
- package/scripts/generate-template.ts +4 -3
- package/src/cli/setup-cli.ts +14 -161
- package/src/cli/stats-cli.ts +56 -2
- package/src/cli.ts +0 -1
- package/src/config/settings-schema.ts +0 -10
- package/src/eval/eval.lark +30 -10
- package/src/eval/js/context-manager.ts +334 -564
- package/src/eval/js/shared/helpers.ts +237 -0
- package/src/eval/js/shared/indirect-eval.ts +30 -0
- package/src/eval/js/shared/rewrite-imports.ts +211 -0
- package/src/eval/js/shared/runtime.ts +168 -0
- package/src/eval/js/shared/types.ts +18 -0
- package/src/eval/js/tool-bridge.ts +2 -4
- package/src/eval/js/worker-core.ts +146 -0
- package/src/eval/js/worker-entry.ts +24 -0
- package/src/eval/js/worker-protocol.ts +41 -0
- package/src/eval/parse.ts +218 -49
- package/src/eval/py/display.ts +71 -0
- package/src/eval/py/executor.ts +74 -89
- package/src/eval/py/index.ts +1 -2
- package/src/eval/py/kernel.ts +472 -900
- package/src/eval/py/prelude.py +95 -7
- package/src/eval/py/runner.py +879 -0
- package/src/eval/py/runtime.ts +3 -16
- package/src/eval/py/tool-bridge.ts +137 -0
- package/src/export/html/index.ts +5 -2
- package/src/export/html/template.generated.ts +1 -1
- package/src/export/html/template.js +93 -5
- package/src/export/html/template.macro.ts +4 -3
- package/src/internal-urls/docs-index.generated.ts +3 -3
- package/src/modes/components/read-tool-group.ts +9 -0
- package/src/modes/controllers/command-controller.ts +0 -23
- package/src/prompts/tools/eval.md +14 -27
- package/src/prompts/tools/read.md +1 -0
- package/src/session/agent-session.ts +0 -1
- package/src/session/history-storage.ts +77 -19
- package/src/tools/browser/tab-protocol.ts +4 -0
- package/src/tools/browser/tab-supervisor.ts +86 -5
- package/src/tools/browser/tab-worker.ts +104 -58
- package/src/tools/conflict-detect.ts +661 -0
- package/src/tools/eval.ts +1 -1
- package/src/tools/index.ts +6 -0
- package/src/tools/path-utils.ts +1 -1
- package/src/tools/read.ts +130 -0
- package/src/tools/write.ts +204 -0
- package/src/web/search/index.ts +6 -4
- package/src/cli/jupyter-cli.ts +0 -106
- package/src/commands/jupyter.ts +0 -32
- package/src/eval/py/cancellation.ts +0 -28
- package/src/eval/py/gateway-coordinator.ts +0 -424
- /package/src/eval/js/{prelude.ts → shared/prelude.ts} +0 -0
- /package/src/eval/js/{prelude.txt → shared/prelude.txt} +0 -0
package/src/eval/py/runtime.ts
CHANGED
|
@@ -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:
|
|
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:
|
|
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
|
|
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
|
+
}
|
package/src/export/html/index.ts
CHANGED
|
@@ -103,9 +103,12 @@ async function generateHtml(sessionData: SessionData, themeName?: string): Promi
|
|
|
103
103
|
const themeVars = await generateThemeVars(themeName);
|
|
104
104
|
const sessionDataBase64 = Buffer.from(JSON.stringify(sessionData)).toBase64();
|
|
105
105
|
|
|
106
|
-
|
|
106
|
+
// Use function replacements so `$'`, `$&`, `$$`, `$n`, etc. in the
|
|
107
|
+
// substituted CSS/base64 are not interpreted as substitution patterns
|
|
108
|
+
// (see https://mdn.io/String.replace).
|
|
109
|
+
return TEMPLATE.replace("<theme-vars/>", () => `<style>:root { ${themeVars} }</style>`).replace(
|
|
107
110
|
"{{SESSION_DATA}}",
|
|
108
|
-
sessionDataBase64,
|
|
111
|
+
() => sessionDataBase64,
|
|
109
112
|
);
|
|
110
113
|
}
|
|
111
114
|
|