pi-repl-py 0.1.0 → 0.2.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.
package/index.ts CHANGED
@@ -1,10 +1,10 @@
1
1
  // --- pi-repl: one execute tool over Python; everything else runs as functions inside it ---
2
2
 
3
3
  import { basename, join } from "node:path";
4
+ import { homedir } from "node:os";
4
5
  import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
5
6
  import { Type } from "typebox";
6
- import { EngineBusyError, EngineManager } from "./src/engine/index.js";
7
- import { loadConfig } from "./src/extension/config.js";
7
+ import { EngineManager } from "./src/engine/index.js";
8
8
  import { ExecuteCellComponent, type ExecuteDetails, type ExecuteRenderState } from "./src/extension/render.js";
9
9
  import { EngineLifecycle, summarizeNames } from "./src/extension/session-engine.js";
10
10
  import { EXECUTE_DESCRIPTION, buildExecutePromptGuidelines, EXECUTE_PROMPT_SNIPPET } from "./src/extension/tool-meta.js";
@@ -44,8 +44,6 @@ function composeErrorLines(error: { name: string; message: string; stack: string
44
44
  return stack[0]?.trim() === header ? stack : [header, ...stack];
45
45
  }
46
46
 
47
- const CFG = loadConfig();
48
-
49
47
  export default function (pi: ExtensionAPI) {
50
48
  pi.registerFlag("repl", {
51
49
  type: "boolean",
@@ -62,12 +60,10 @@ export default function (pi: ExtensionAPI) {
62
60
  create() {
63
61
  const { cwd, sessionFile } = location;
64
62
  const sessionKey = sessionFile ? basename(sessionFile).replace(/\.jsonl$/, "") : undefined;
65
- const stateDir = join(cwd, ".pi-repl", sessionKey ?? "ephemeral");
63
+ // --- kernel namespace state lives under ~/.pi/agent/pi-repl, keyed by session, so it never clutters the project ---
64
+ const stateDir = join(homedir(), ".pi", "agent", "pi-repl", "state", sessionKey ?? "ephemeral");
66
65
  return new EngineManager({
67
66
  cwd,
68
- pythonPath: CFG.pythonPath,
69
- timeoutMs: CFG.timeoutMs,
70
- toolboxDir: CFG.toolboxDir,
71
67
  // --- snapshots are keyed to a session file; ephemeral sessions get none ---
72
68
  snapshot: sessionKey ? { path: join(stateDir, "namespace.snapshot") } : undefined,
73
69
  });
@@ -81,11 +77,7 @@ export default function (pi: ExtensionAPI) {
81
77
  },
82
78
  });
83
79
 
84
- // --- no custom prompt: pi's default prompt stands. session_start collapses
85
- // the active set to just `execute`, so the default prompt's built-in
86
- // read/bash/edit/write never appear. All REPL knowledge (description,
87
- // promptSnippet, promptGuidelines) lives on the tool itself, not in a
88
- // prompt builder. ---
80
+ // --- no custom prompt: pi's default prompt stands; active tools collapse to execute, so REPL knowledge lives on the tool ---
89
81
 
90
82
  pi.on("session_start", async (_event, ctx) => {
91
83
  if (!active()) {
@@ -132,7 +124,7 @@ export default function (pi: ExtensionAPI) {
132
124
  label: "execute",
133
125
  description: EXECUTE_DESCRIPTION,
134
126
  promptSnippet: EXECUTE_PROMPT_SNIPPET,
135
- promptGuidelines: buildExecutePromptGuidelines(CFG.toolboxDir),
127
+ promptGuidelines: buildExecutePromptGuidelines(),
136
128
  parameters: executeSchema,
137
129
  renderShell: "self",
138
130
  renderCall(args, theme, context) {
@@ -191,25 +183,10 @@ export default function (pi: ExtensionAPI) {
191
183
  pendingErrorResults.set(toolCallId, { details });
192
184
  throw new Error(text || "(no output)");
193
185
  }
194
- if (r.status === "aborted") {
195
- // A cancelled cell's kernel may still be executing work the guest
196
- // single-threaded loop can't interrupt. Discard the engine so the
197
- // NEXT run gets a fresh kernel instead of queuing behind the
198
- // still-busy one (same class as the stalled-timeout recovery).
199
- await lifecycle.discard();
200
- }
186
+ // --- an aborted cell was interrupted, not wedged: the kernel keeps running, nothing to discard ---
201
187
  return result;
202
188
  } catch (error) {
203
- if (error instanceof EngineBusyError) {
204
- // --- discard the wedged engine; the next cell revives from the last snapshot ---
205
- await lifecycle.discard();
206
- throw new Error(
207
- "The evaluator was wedged by a previously interrupted cell and has been killed. " +
208
- "Run the next cell to get a fresh evaluator revived from the last snapshot; " +
209
- "anything newer than that snapshot is gone, so re-verify variables before reusing them.",
210
- );
211
- }
212
- // --- a guest that died leaves the engine shutdown; drop it so the next cell rebuilds fresh ---
189
+ // --- a kernel that died (or was killed as the abort backstop) is down; drop it so the next cell rebuilds ---
213
190
  if (m.isRunning === false) {
214
191
  await lifecycle.discard();
215
192
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-repl-py",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "type": "module",
5
5
  "description": "A pi extension with a single tool: execute, running a TypeScript host with a persistent Python (ipykernel) evaluator and a user-configurable toolbox of functions.",
6
6
  "keywords": [
@@ -11,6 +11,14 @@
11
11
  "repl"
12
12
  ],
13
13
  "license": "MIT",
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "git+https://github.com/k3-2o/pi-repl-py.git"
17
+ },
18
+ "homepage": "https://github.com/k3-2o/pi-repl-py",
19
+ "bugs": {
20
+ "url": "https://github.com/k3-2o/pi-repl-py/issues"
21
+ },
14
22
  "pi": {
15
23
  "extensions": [
16
24
  "./index.ts"
@@ -22,20 +30,18 @@
22
30
  "docs",
23
31
  "index.ts",
24
32
  "README.md",
25
- "ARCHITECTURE.md",
26
33
  "LICENSE"
27
34
  ],
28
35
  "engines": {
29
36
  "node": ">=22"
30
37
  },
31
- "scripts": {
38
+ "scripts": {
32
39
  "typecheck": "tsc --noEmit",
33
40
  "format": "biome format --write .",
34
41
  "lint": "biome check .",
35
42
  "knip": "knip",
36
- "check": "tsc --noEmit && biome check . && npm run test:ts && npm run test:py",
43
+ "check": "biome check . && npm run test:ts",
37
44
  "test:ts": "bun test test/units.test.ts test/preview-core.test.ts",
38
- "test:py": ".venv/bin/python -m pytest test/guest_contract.py -q",
39
45
  "test:integ": "bun test test/engine.integration.test.ts",
40
46
  "postinstall": "node scripts/setup-venv.mjs"
41
47
  },
@@ -54,4 +60,4 @@
54
60
  "typebox": "^1.3.11",
55
61
  "typescript": "^5.6.0"
56
62
  }
57
- }
63
+ }
@@ -1,18 +1,27 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * postinstall: build the stable per-user Python venv the guest evaluator needs.
3
+ * postinstall: build the stable per-user Python venv the evaluator needs.
4
4
  *
5
- * The guest (src/engine/guest.py) runs a real ipykernel, which requires
6
- * `ipykernel` + `jupyter_client` installed in a Python environment. When this
7
- * is installed as a pi package there is no repo-local `.venv` (it's gitignored
8
- * and excluded from the npm tarball), so we create one at a stable path that
9
- * the engine also knows about:
5
+ * The evaluator (src/engine/kernel.ts) drives a real ipykernel directly over
6
+ * the Jupyter protocol; `ipykernel` is the only hard runtime dependency. When
7
+ * this is installed as a pi package there is no repo-local `.venv` (gitignored
8
+ * and excluded from the npm tarball), so we create one at a stable path the
9
+ * engine also knows about:
10
10
  *
11
- * ~/.pi/agent/pi-repl-venv/bin/python3
11
+ * ~/.pi/agent/pi-repl/venv/bin/python3
12
12
  *
13
- * Failures are non-fatal: if there's no system python3 or no network, we print
14
- * a clear notice and let the engine fall back to '$PYTHON' or 'python3' at
15
- * runtime (where the user is told how to install the deps).
13
+ * HELPERS live ONLY in the user-owned config dir:
14
+ *
15
+ * ~/.pi/agent/pi-repl/helpers/
16
+ *
17
+ * There is no helper/config folder anywhere in this package (no
18
+ * src/engine/helpers, no templates/). The helpers dir is created if missing,
19
+ * but no default helpers are seeded — the REPL itself already provides shell
20
+ * (via `!cmd`, `%%bash`, `subprocess`) and file IO (via `open`, `pathlib`).
21
+ * Users add their own helper .py files freely; existing files are never clobbered.
22
+ *
23
+ * Failures are non-fatal: if there's no system python3 or no network we print a
24
+ * clear notice and let the engine fall back to '$PYTHON' or 'python3' at runtime.
16
25
  */
17
26
 
18
27
  import { execSync } from "node:child_process";
@@ -20,15 +29,16 @@ import { existsSync, mkdirSync } from "node:fs";
20
29
  import { homedir } from "node:os";
21
30
  import { join } from "node:path";
22
31
 
23
- const VENV_DIR = join(homedir(), ".pi", "agent", "pi-repl-venv");
32
+ const VENV_DIR = join(homedir(), ".pi", "agent", "pi-repl", "venv");
24
33
  const PY = join(VENV_DIR, "bin", "python3");
25
- const DEPS = ["ipykernel", "jupyter_client"];
34
+ const DEPS = ["ipykernel"];
35
+ const HELPERS_DIR = join(homedir(), ".pi", "agent", "pi-repl", "helpers");
26
36
 
27
- function log(msg) {
28
- process.stdout.write(`[pi-repl] ${msg}\n`);
37
+ function log(m) {
38
+ process.stdout.write(`[pi-repl] ${m}\n`);
29
39
  }
30
- function warn(msg) {
31
- process.stderr.write(`[pi-repl] warning: ${msg}\n`);
40
+ function warn(m) {
41
+ process.stderr.write(`[pi-repl] warning: ${m}\n`);
32
42
  }
33
43
 
34
44
  function findSystemPython() {
@@ -41,8 +51,20 @@ function findSystemPython() {
41
51
  return null;
42
52
  }
43
53
 
54
+ // ---------------------------------------------------------------- helpers dir
55
+ // The helpers dir is user-owned. We create it empty on install. The REPL
56
+ // provides shell and file IO natively; helpers are for things the user adds
57
+ // themselves (e.g. web_search, custom skills). Existing files are never clobbered.
58
+ function seedHelpersDir() {
59
+ try {
60
+ mkdirSync(HELPERS_DIR, { recursive: true });
61
+ } catch (e) {
62
+ warn(`could not create the helpers dir (${e?.message ?? e}); custom helpers won't preload.`);
63
+ }
64
+ }
65
+
44
66
  function main() {
45
- // Already built from a previous install/run.
67
+ seedHelpersDir();
46
68
  if (existsSync(PY)) {
47
69
  log(`venv already present at ${VENV_DIR}`);
48
70
  return;
@@ -51,7 +73,7 @@ function main() {
51
73
  if (!systemPython) {
52
74
  warn(
53
75
  `no python3 found on PATH; could not create the evaluator venv. ` +
54
- `Install python3 and run '${PY.slice(-60)} -m venv' manually, or set PI_REPL pythonPath.`
76
+ `Install python3 and run '${PY.slice(-60)} -m venv' manually, or set $PYTHON to point at one.`
55
77
  );
56
78
  return;
57
79
  }
@@ -64,7 +86,7 @@ function main() {
64
86
  log("done. The pi-repl evaluator will use this venv.");
65
87
  } catch (error) {
66
88
  warn(`could not build the evaluator venv (${error && error.message ? error.message : error}). `);
67
- warn("You may need to run npm rebuild pi-repl after fixing python/network, or install ipykernel manually.");
89
+ warn("You must install ipykernel in that venv before the evaluator runs.");
68
90
  }
69
91
  }
70
92