pi-repl-py 0.2.8 → 0.3.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.
@@ -57,15 +57,17 @@ That path is stable across updates because it sits outside the package's own dir
57
57
  which npm replaces on each update. If `python3` or the network is missing at install time,
58
58
  `postinstall` prints a clear notice and the host falls back at runtime.
59
59
 
60
- At spawn, `resolvePythonPath` picks the interpreter in this order:
60
+ At spawn, `resolvePythonPath` uses exactly one interpreter, the install venv:
61
61
 
62
- 1. the repo's own `.venv` (development)
63
- 2. a venv in the current directory (per-project)
64
- 3. `~/.pi/agent/pi-repl/venv` (package install)
65
- 4. `$PYTHON`, then `python3` (the fallback)
62
+ 1. `~/.pi/agent/pi-repl/venv` (the package install)
63
+ 2. `$PYTHON`, then `python3` (only if the install venv is missing)
66
64
 
67
- The first one that exists wins. The tool's prompt tells the model it runs in a project-local
68
- venv, not the system interpreter, so it does not leak the wrong assumption into commands.
65
+ It deliberately does NOT auto-pick the repo's or current directory's `.venv`: a per-project
66
+ venv is not guaranteed to have `ipykernel`, so preferring it (as earlier versions did) made
67
+ the kernel die from a `ModuleNotFoundError` whenever cwd happened to contain such a venv. The
68
+ kernel therefore always runs in the stable install environment. The kernel starts in the
69
+ session's cwd, falling back to the host's own cwd if that directory no longer exists (a
70
+ deleted project dir), so a stale cwd can never prevent the kernel from coming up.
69
71
 
70
72
  ## The kernel client
71
73
 
package/index.ts CHANGED
@@ -6,7 +6,7 @@ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
6
6
  import { Type } from "typebox";
7
7
  import { EngineManager } from "./src/engine/index.js";
8
8
  import { ExecuteCellComponent, type ExecuteDetails, type ExecuteRenderState } from "./src/extension/render.js";
9
- import { EngineLifecycle, summarizeNames } from "./src/extension/session-engine.js";
9
+ import { EngineLifecycle } from "./src/extension/session-engine.js";
10
10
  import { EXECUTE_DESCRIPTION, buildExecutePromptGuidelines, EXECUTE_PROMPT_SNIPPET } from "./src/extension/tool-meta.js";
11
11
 
12
12
  const executeSchema = Type.Object({
@@ -87,23 +87,13 @@ export default function (pi: ExtensionAPI) {
87
87
  }
88
88
  // --- active: the whole surface collapses to the one tool ---
89
89
  pi.setActiveTools(["execute"]);
90
- // --- revive the previous run; the engine also self-revives if session_start was skipped ---
90
+ // --- warm the engine (and its revive) in the background; no popup. ---
91
+ // --- acquire() dedupes, so the first execute awaits this same in-flight boot ---
91
92
  location = { cwd: ctx.cwd, sessionFile: ctx.sessionManager.getSessionFile() ?? undefined };
92
- const { restore } = await lifecycle.acquire("startup");
93
- if (restore && restore.restored.length > 0) {
94
- pi.sendMessage({
95
- customType: "pi-repl-restore",
96
- content: `Revived ${restore.restored.length} variable(s) from the previous run: ${summarizeNames(restore.restored, 8)}${
97
- restore.failed.length > 0
98
- ? `. Failed: ${summarizeNames(
99
- restore.failed.map((f) => f.name),
100
- 8,
101
- )}`
102
- : ""
103
- }`,
104
- display: true,
105
- });
106
- }
93
+ void lifecycle.acquire("startup").catch(() => {
94
+ // --- boot/revive handled on the execute path; swallow so a background warm can never
95
+ // --- surface an unhandled rejection and the model never needs the restore notice ---
96
+ });
107
97
  });
108
98
 
109
99
  pi.on("session_shutdown", async () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-repl-py",
3
- "version": "0.2.8",
3
+ "version": "0.3.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": [
@@ -5,21 +5,16 @@
5
5
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
6
6
  import { homedir } from "node:os";
7
7
  import { dirname, join } from "node:path";
8
- import { fileURLToPath } from "node:url";
9
8
  import { KernelClient } from "./kernel.js";
10
9
 
11
- const GUEST_REL = fileURLToPath(new URL("./kernel.js", import.meta.url));
12
-
13
10
  function installVenvPython(): string {
14
11
  return join(homedir(), ".pi", "agent", "pi-repl", "venv", "bin", "python3");
15
12
  }
16
13
 
17
14
  /** Prefer a venv with ipykernel; else $PYTHON or python3. */
18
- function resolvePythonPath(cwd: string | undefined): string {
19
- const repoVenv = join(dirname(GUEST_REL), "..", "..", ".venv", "bin", "python3");
20
- if (existsSync(repoVenv)) return repoVenv;
21
- const cwdVenv = cwd ? join(cwd, ".venv", "bin", "python3") : "";
22
- if (cwdVenv && existsSync(cwdVenv)) return cwdVenv;
15
+ function resolvePythonPath(_cwd: string | undefined): string {
16
+ // Only ever use the install venv: a project or repo `.venv` may lack ipykernel and
17
+ // shadow the good environment, killing the kernel. No auto-picking.
23
18
  const installVenv = installVenvPython();
24
19
  if (existsSync(installVenv)) return installVenv;
25
20
  return process.env.PYTHON ?? "python3";
@@ -54,7 +54,14 @@ export interface SnapshotReply {
54
54
  // --- boot preload: exec each helper; ls()/help() are gone, discovery is globals() ---
55
55
 
56
56
  /** Read the helpers dir (same skip rules as the extension's prompt loader). */
57
- export function readHelperSources(dir?: string): { name: string; source: string }[] {
57
+ /** A directory for the kernel to start in; if the requested cwd is gone, fall back to the
58
+ * evaluator's own cwd rather than letting spawn() die with ENOENT. A deleted project dir is
59
+ * a real resume case (pi guards it too) — the kernel must still come up. */
60
+ function resolveCwd(requested?: string): string {
61
+ if (requested && existsSync(requested)) return requested;
62
+ return process.cwd();
63
+ }
64
+ function readHelperSources(dir?: string): { name: string; source: string }[] {
58
65
  // --- one fixed dir, resolved like the prompt side (helpers.ts) so both always agree ---
59
66
  const d = dir ?? join(homedir(), ".pi", "agent", "pi-repl", "helpers");
60
67
  if (!existsSync(d)) return [];
@@ -179,7 +186,7 @@ export class KernelClient {
179
186
  static async start(pythonPath: string, opts: KernelOptions = {}): Promise<KernelClient> {
180
187
  const connPath = join(tmpdir(), `pi-repl-kernel-${randomUUID()}.json`);
181
188
  const child = spawn(pythonPath, ["-m", "ipykernel", "-f", connPath, "--no-stdout"], {
182
- cwd: opts.cwd,
189
+ cwd: resolveCwd(opts.cwd),
183
190
  env: { ...process.env, ...(opts.env ?? {}) },
184
191
  stdio: ["ignore", "pipe", "pipe"],
185
192
  });
@@ -3,7 +3,7 @@
3
3
  import type { RestoreResult } from "../engine/index.js";
4
4
 
5
5
  /** Show enough names to orient, then count the rest (a revive can carry hundreds). */
6
- export function summarizeNames(names: readonly string[], limit: number): string {
6
+ function summarizeNames(names: readonly string[], limit: number): string {
7
7
  if (names.length <= limit) return names.join(", ");
8
8
  return `${names.slice(0, limit).join(", ")} … and ${names.length - limit} more`;
9
9
  }