little-coder 1.4.1 → 1.4.3

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.
@@ -1,4 +1,4 @@
1
- import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
2
  import { existsSync, readFileSync } from "node:fs";
3
3
  import { dirname, join } from "node:path";
4
4
  import { fileURLToPath } from "node:url";
@@ -1,4 +1,4 @@
1
- import type { ExtensionAPI, Theme } from "@mariozechner/pi-coding-agent";
1
+ import type { ExtensionAPI, Theme } from "@earendil-works/pi-coding-agent";
2
2
  import { readFileSync } from "node:fs";
3
3
  import { basename, dirname, join } from "node:path";
4
4
  import { fileURLToPath } from "node:url";
@@ -16,7 +16,7 @@ import { fileURLToPath } from "node:url";
16
16
  // override quietStartup and see the resource list.
17
17
  //
18
18
  // Implementation pattern follows the bundled pi example at
19
- // `node_modules/@mariozechner/pi-coding-agent/examples/extensions/custom-header.ts` —
19
+ // `node_modules/@earendil-works/pi-coding-agent/examples/extensions/custom-header.ts` —
20
20
  // the factory returns a duck-typed Component (`render(width): string[]` +
21
21
  // `invalidate()`), so no deep imports from pi-tui are needed.
22
22
 
@@ -1,4 +1,4 @@
1
- import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
2
  import { Type } from "@sinclair/typebox";
3
3
 
4
4
  // Port of local/tools/browser.py. Playwright-powered Browser* tools with
@@ -1,4 +1,4 @@
1
- import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
2
  import { getSessionStore } from "../evidence/index.ts";
3
3
 
4
4
  // Post-turn pruning of BrowserExtract tool-result messages.
@@ -2,7 +2,7 @@ import { describe, it, expect } from "vitest";
2
2
  import { buildPlaceholder, pruneMessages } from "./index.ts";
3
3
 
4
4
  // Canned message shapes mirror pi's AgentMessage / ToolResultMessage.
5
- // See node_modules/@mariozechner/pi-ai/dist/types.d.ts for the real types.
5
+ // See node_modules/@earendil-works/pi-ai/dist/types.d.ts for the real types.
6
6
 
7
7
  function userMsg(text: string) {
8
8
  return { role: "user", content: text };
@@ -1,4 +1,4 @@
1
- import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
2
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
3
3
  import { join } from "node:path";
4
4
  import { homedir } from "node:os";
@@ -1,4 +1,4 @@
1
- import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
2
  import { Type } from "@sinclair/typebox";
3
3
  import { randomBytes } from "node:crypto";
4
4
 
@@ -1,4 +1,4 @@
1
- import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
2
  import { getSessionStore } from "../evidence/index.ts";
3
3
 
4
4
  // Port of compaction.py's Evidence-preservation contract.
@@ -1,4 +1,4 @@
1
- import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
2
  import { Type } from "@sinclair/typebox";
3
3
  import { glob as globSync } from "node:fs/promises";
4
4
 
@@ -1,4 +1,4 @@
1
- import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
2
 
3
3
  // Pre-cap finalize-warn: when the agent has WARN_REMAINING turns left
4
4
  // (this turn included), inject a follow-up user message reminding it to
@@ -1,4 +1,4 @@
1
- import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
2
 
3
3
  export default function (pi: ExtensionAPI) {
4
4
  pi.on("session_start", async (_event, ctx) => {
@@ -1,4 +1,4 @@
1
- import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
2
  import { readdirSync, readFileSync, existsSync } from "node:fs";
3
3
  import { dirname, join } from "node:path";
4
4
  import { fileURLToPath } from "node:url";
@@ -1,6 +1,6 @@
1
1
  import { dirname, resolve } from "node:path";
2
2
  import { fileURLToPath } from "node:url";
3
- import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
3
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
4
4
  import { loadProviders } from "./config.ts";
5
5
 
6
6
  // Data-driven provider registration. Reads:
@@ -1,4 +1,4 @@
1
- import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
2
  import { parseTextToolCalls } from "./parser.ts";
3
3
 
4
4
  // Detects malformed/fenced tool calls in assistant text and nudges the model
@@ -1,4 +1,4 @@
1
- import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
2
 
3
3
  // Port of tools.py::_SAFE_PREFIXES + agent.py::_check_permission. Bash
4
4
  // commands not matching the whitelist are blocked in "auto" mode. In
@@ -1,4 +1,4 @@
1
- import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
2
  import { assessResponse, buildCorrectionMessage, type ToolCall } from "./quality.ts";
3
3
 
4
4
  // Port of local/quality.py. Hooks turn_end, inspects the assistant message
@@ -1,4 +1,4 @@
1
- import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
2
  import { Type } from "@sinclair/typebox";
3
3
  import { execSync } from "node:child_process";
4
4
  import { formatOutput, DEFAULT_TIMEOUT } from "./helpers.ts";
@@ -1,4 +1,4 @@
1
- import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
2
  import { readdirSync, readFileSync, existsSync } from "node:fs";
3
3
  import { dirname, join } from "node:path";
4
4
  import { fileURLToPath } from "node:url";
@@ -1,4 +1,4 @@
1
- import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
2
 
3
3
  // Port of the thinking-budget cap + partial-trace reuse logic from
4
4
  // providers.py. little-coder's Python implementation aborts the stream
@@ -1,4 +1,4 @@
1
- import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
2
 
3
3
  // Port of agent.py's _allowed_tools gate. When LITTLE_CODER_ALLOWED_TOOLS
4
4
  // is set (comma-separated), any tool_call not in the list is blocked with
@@ -1,4 +1,4 @@
1
- import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
2
 
3
3
  // Port of agent.py's max_turns early-break. Counts turn_start events per
4
4
  // agent_start span; when the count exceeds LITTLE_CODER_MAX_TURNS (or the
@@ -1,4 +1,4 @@
1
- import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
2
  import { Type } from "@sinclair/typebox";
3
3
  import { existsSync, mkdirSync, writeFileSync } from "node:fs";
4
4
  import { dirname, isAbsolute, join } from "node:path";
package/CHANGELOG.md CHANGED
@@ -2,6 +2,37 @@
2
2
 
3
3
  All notable changes to little-coder are documented here. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and little-coder's public interface (CLI, providers, tools, skills) follows semver starting at `v0.0.1` post-rename.
4
4
 
5
+ ## [v1.4.3] — 2026-05-19
6
+
7
+ Follow-up to v1.4.2: clean up two cosmetic regressions that the @earendil-works scope migration surfaced.
8
+
9
+ ### Fixed
10
+ - **Pi's `What's New` block no longer appears inside little-coder's TUI after a version bump.** Root cause: pi's interactive mode reads its own bundled `CHANGELOG.md` on startup and renders every entry strictly newer than the `lastChangelogVersion` field in `~/.pi/agent/settings.json` (`interactive-mode.js:getChangelogForDisplay`). v1.4.2 jumped the bundled pi from 0.68.1 to 0.75.3, so users who had previously launched any older little-coder saw pi's full 0.68 → 0.75 upstream changelog dumped *underneath* little-coder's own startup banner. That's wrong because little-coder is the surface and pi is the substrate — the chrome above shouldn't suddenly start advertising the substrate's release notes. The launcher (`bin/little-coder.mjs`) now pre-stamps `lastChangelogVersion` to the currently bundled pi version (resolved from `node_modules/@earendil-works/pi-coding-agent/package.json#version`, the same file we already read to find pi's cli.js, so there's no second source of truth) *before* pi starts. Pi then sees "user already saw this changelog" and the block never renders. The merge into `~/.pi/agent/settings.json` is non-destructive — `quietStartup: true` and every other existing key are preserved. Users who genuinely want pi's upstream changelog can still pull it up with `/changelog` inside the TUI.
11
+ - **`npm install -g little-coder` no longer prints `node-domexception@1.0.0` deprecation warning.** Root cause: a 5-hop transitive — `@earendil-works/pi-ai` → `@google/genai` → `google-auth-library` → `gaxios` → `node-fetch@3` → `fetch-blob@3` → `node-domexception@1.0.0`. The `node-domexception` package is just a 16-line shim that sets `globalThis.DOMException` when undefined, and native `DOMException` has been built into Node since 18 — so on our `Node >= 22.19` floor, the entire shim is dead code. Replaced it via `package.json#overrides` pointing at a bundled stub at `./vendor/node-domexception/` that exports `module.exports = globalThis.DOMException` directly. The stub ships in the npm tarball (`files` array now includes `vendor/`). Since npm's `overrides` field is honored when little-coder is the install root (which it is for `npm install -g little-coder`), the deprecated upstream package never reaches the user's tree, and npm prints no warning. Functional behavior is identical because the only call site (`fetch-blob/from.js:import DOMException from 'node-domexception'`) sees the same `globalThis.DOMException` it would have gotten from the upstream shim.
12
+
13
+ ### Notes for upgraders
14
+ - The bundled stub lives at `vendor/node-domexception/` inside the published package — it's listed under `files` in `package.json`. If you'd added your own `overrides` field that touches `node-domexception` in a hand-rolled fork of little-coder, our entry will take precedence when you publish; in the unlikely case that breaks something for you, override it back in your fork's root `package.json`.
15
+ - The `lastChangelogVersion` pre-stamp is one-directional: it writes the *currently bundled* pi version into settings on every launch. If you'd like to see pi's upstream changelog for a future bump, `/changelog` inside the TUI is the unconditional path — it doesn't consult `lastChangelogVersion`.
16
+ - No CLI flag, models.json shape, skill-pack, extension API, or per-model profile changes. Little-coder's own startup banner, tagline, and keybind hints (the branding extension at `.pi/extensions/branding/`) are byte-for-byte unchanged from v1.4.2.
17
+
18
+ ---
19
+
20
+ ## [v1.4.2] — 2026-05-19
21
+
22
+ Bundled-pi maintenance release. Closes [#22](https://github.com/itayinbarr/little-coder/issues/22), [#23](https://github.com/itayinbarr/little-coder/issues/23), [#25](https://github.com/itayinbarr/little-coder/issues/25). The pi runtime moves from `@mariozechner/pi-coding-agent@^0.68.1` to `@earendil-works/pi-coding-agent@^0.75.3` — same author, same project, new npm scope — which makes the deprecation warnings disappear, pulls in pi's recent Windows / undici / cmd-shim fixes, and (because pi 0.75 raised its floor) bumps the supported Node range to ≥ 22.19. No CLI flag, settings, extension API, or skill-pack changes.
23
+
24
+ ### Fixed
25
+ - **`npm install -g little-coder` no longer emits `@mariozechner/pi-*` deprecation warnings ([#25](https://github.com/itayinbarr/little-coder/issues/25)).** Upstream pi published the new scope as `@earendil-works/pi-coding-agent` (the `@mariozechner/*` packages remain on npm only to print the migration notice). The little-coder `package.json` dependency entry, all 21 extension import statements under `.pi/extensions/*/index.ts`, the retention-test doc comment, and the README attribution have been migrated. The public `ExtensionAPI`, `Theme`, hook event names (`before_agent_start`, `context`, `before_provider_request`, `tool_call`, `tool_result`, `turn_end`, `session_compact`, `session_start`), `pi.ui.setHeader()` / `pi.ui.setTitle()`, `registerProvider()`, and CLI flags (`--no-context-files`, `--no-extensions`, `--system-prompt`, `--extension`, `--mode rpc`, `--list-models`, `--verbose`, `--offline`) all keep their previous signatures — the rename is purely the npm scope. Full `npm run typecheck` + 152-test vitest suite passes against the new scope.
26
+ - **Windows startup no longer fails with `'C:\Program' is not recognized as an internal or external command` ([#23](https://github.com/itayinbarr/little-coder/issues/23)).** Root cause: `bin/little-coder.mjs` was invoking `node_modules/.bin/pi.cmd` via `cmd.exe /c …`. When npm's prefix or Node's install path contained spaces (the default Windows location is `C:\Program Files\nodejs\`), the chain of nested `.cmd` shims could tokenize on the first space and execute `C:\Program` as a command name. The launcher now resolves pi's JS entry by reading `node_modules/@earendil-works/pi-coding-agent/package.json#bin.pi` and spawns `process.execPath` (the same Node that's already running) with that absolute path as an argv element. Node's `child_process.spawn` handles Windows argv quoting itself, so there is no shell tokenization at any layer — and the same spawn line works on Linux, macOS, and Windows (the previous `isWindows ? cmd.exe : piBin` branch is gone). This also picks up pi 0.75.2's own Windows fixes for cross-spawn / npm-family commands.
27
+ - **Node version requirement bumped to ≥ 22.19.0 ([#22](https://github.com/itayinbarr/little-coder/issues/22)).** The `glob`-cannot-be-used error users hit on Node 20.x came from the bundled pi runtime depending on `glob@^13`, which itself requires Node ≥ 20.19 / 22 to run correctly. Upstream pi 0.75.0 raised its hard minimum to 22.19.0, so the bundled-pi update forces this floor onto us regardless. The launcher's `MIN_NODE` preflight, `package.json#engines.node`, `install.sh`'s version check, and the README install / troubleshooting prose are all moved together. Easiest fix: `nvm install 22 && nvm use 22`.
28
+
29
+ ### Notes for upgraders
30
+ - **You must be on Node ≥ 22.19.0 to upgrade.** If `node --version` is below that, `npm install -g little-coder@1.4.2` will still install but the launcher's preflight will refuse to start pi and print the nvm hint. `npm install -g little-coder@1.4.1` keeps working on Node 20.6+ if you genuinely cannot move yet.
31
+ - The model list, `models.json` shape, `.pi/settings.json` keys (`quietStartup`, per-model context/thinking-budget/temperature profiles, benchmark_overrides), and skill-pack are untouched. Existing `LITTLE_CODER_BASH_ALLOW`, `LITTLE_CODER_PERMISSION_MODE`, `LITTLE_CODER_MODELS_FILE`, `LLAMACPP_BASE_URL` / `OLLAMA_BASE_URL` / `LMSTUDIO_BASE_URL` / `*_API_KEY` env vars all keep their meaning.
32
+ - If you'd hand-written an extension under your local `.pi/extensions/` that imports `from "@mariozechner/pi-coding-agent"`, change the import to `from "@earendil-works/pi-coding-agent"` and re-run. The old scope's last published version was 0.73.1 — it works against an installed `@earendil-works/pi-coding-agent` only via the legacy `@sinclair/typebox` alias that pi 0.69+ keeps for compatibility.
33
+
34
+ ---
35
+
5
36
  ## [v1.4.1] — 2026-05-16
6
37
 
7
38
  Wire fix for the v1.4.0 startup rebrand. The `[Extensions]` block was still showing for users running from outside the repo root.
package/README.md CHANGED
@@ -16,7 +16,7 @@ If you've never used pi, it's useful to skim [pi.dev](https://pi.dev) first —
16
16
 
17
17
  ## Install
18
18
 
19
- One-line install (Node.js 20.6+ required):
19
+ One-line install (Node.js 22.19+ required):
20
20
 
21
21
  ```bash
22
22
  curl -fsSL https://raw.githubusercontent.com/itayinbarr/little-coder/main/install.sh | bash
@@ -36,7 +36,7 @@ bun add -g little-coder
36
36
 
37
37
  That's the whole install. No clone, no `npm install` in a workspace, no PATH fiddling. `little-coder` is now on your PATH and works from any directory.
38
38
 
39
- > **Note for `bun add -g` users.** The launcher (`bin/little-coder.mjs`) is a Node.js script with `#!/usr/bin/env node` at the top, so Node ≥ 20.6 still has to be on your PATH for the binary to start — bun is fine for installing/updating the package, but the runtime is Node. If you want a fully node-less setup, replace the shebang in `$(bun pm bin -g)/little-coder` with `#!/usr/bin/env bun`.
39
+ > **Note for `bun add -g` users.** The launcher (`bin/little-coder.mjs`) is a Node.js script with `#!/usr/bin/env node` at the top, so Node ≥ 22.19 still has to be on your PATH for the binary to start — bun is fine for installing/updating the package, but the runtime is Node. If you want a fully node-less setup, replace the shebang in `$(bun pm bin -g)/little-coder` with `#!/usr/bin/env bun`.
40
40
 
41
41
  ## Run
42
42
 
@@ -264,7 +264,7 @@ That spans short coding exercises (Polyglot), interactive shell-bound tasks (Ter
264
264
 
265
265
  **Extension load failures on startup** — run `little-coder --list-models --verbose`; extension errors surface there. If the install looks corrupt: `npm uninstall -g little-coder && npm install -g little-coder`.
266
266
 
267
- **Node version too old** — little-coder needs Node ≥ 20.6.0. Check with `node --version`. Easiest fix: `nvm install 20 && nvm use 20`.
267
+ **Node version too old** — little-coder needs Node ≥ 22.19.0 (matching the minimum of the bundled `@earendil-works/pi-coding-agent` v0.75+). Check with `node --version`. Easiest fix: `nvm install 22 && nvm use 22`.
268
268
 
269
269
  ---
270
270
 
@@ -367,7 +367,7 @@ The paper ran `ollama/qwen3.5` through the Python little-coder at commit **`1d62
367
367
 
368
368
  little-coder v0.0.x was a derivative work of [CheetahClaws / ClawSpring](https://github.com/SafeRL-Lab/clawspring) by SafeRL-Lab, Apache 2.0. That upstream provided the Python agent substrate, tool system, multi-provider support, and REPL.
369
369
 
370
- little-coder v0.1.0+ replaces that substrate with **[pi](https://github.com/badlogic/pi-mono)** (`@mariozechner/pi-coding-agent`) by Mario Zechner Apache 2.0 / MIT. pi provides the agent loop, provider abstraction, TUI, and extension model. little-coder rebuilds its small-model adaptations on top of pi as extensions.
370
+ little-coder v0.1.0+ replaces that substrate with **[pi](https://pi.dev)** by Mario Zechner — Apache 2.0 / MIT. The npm package was renamed from `@mariozechner/pi-coding-agent` to `@earendil-works/pi-coding-agent` in upstream's 0.74 release; little-coder v1.4.2+ ships with the new package. pi provides the agent loop, provider abstraction, TUI, and extension model. little-coder rebuilds its small-model adaptations on top of pi as extensions.
371
371
 
372
372
  All little-coder-specific mechanisms — Write-vs-Edit invariant, skill / knowledge injection, thinking-budget cap, output-parser, quality-monitor, per-model profiles, per-benchmark overrides, ShellSession / Browser / Evidence tool families, evidence-aware compaction — are preserved across versions.
373
373
 
@@ -17,8 +17,8 @@ import { dirname, join, resolve } from "node:path";
17
17
  import { fileURLToPath } from "node:url";
18
18
  import { checkForUpdate } from "./update-check.mjs";
19
19
 
20
- // ---- 1. Node version preflight (>= 20.6.0, matching pi.dev) ----
21
- const MIN_NODE = [20, 6, 0];
20
+ // ---- 1. Node version preflight (>= 22.19.0, matching pi.dev) ----
21
+ const MIN_NODE = [22, 19, 0];
22
22
  const cur = process.versions.node.split(".").map((n) => parseInt(n, 10));
23
23
  const tooOld =
24
24
  cur[0] < MIN_NODE[0] ||
@@ -27,7 +27,7 @@ const tooOld =
27
27
  if (tooOld) {
28
28
  console.error(
29
29
  `little-coder requires Node.js >= ${MIN_NODE.join(".")} (you have ${process.versions.node}).\n` +
30
- `Install a newer Node from https://nodejs.org or via nvm: 'nvm install 20'.`,
30
+ `Install a newer Node from https://nodejs.org or via nvm: 'nvm install 22'.`,
31
31
  );
32
32
  process.exit(1);
33
33
  }
@@ -36,13 +36,38 @@ if (tooOld) {
36
36
  const here = dirname(fileURLToPath(import.meta.url));
37
37
  const pkgRoot = resolve(here, "..");
38
38
 
39
- // ---- 3. Resolve the bundled pi binary ----
40
- const isWindows = process.platform === "win32";
41
- const piBinBase = join(pkgRoot, "node_modules", ".bin", "pi");
42
- const piBin = isWindows && existsSync(`${piBinBase}.cmd`) ? `${piBinBase}.cmd` : piBinBase;
43
- if (!existsSync(piBin)) {
39
+ // ---- 3. Resolve the bundled pi CLI entry point ----
40
+ // We invoke pi's JS entry directly under the current Node binary instead of
41
+ // the `node_modules/.bin/pi` shim. Two reasons:
42
+ // 1. On Windows, `.bin/pi.cmd` is an npm-generated batch shim. When it (or
43
+ // anything it transitively invokes) is launched from a path containing
44
+ // spaces — most notably the default Node install location
45
+ // `C:\Program Files\nodejs\` — cmd's whitespace tokenization can split
46
+ // the path at the first space and produce errors like
47
+ // `'C:\Program' is not recognized as an internal or external command`
48
+ // (see issue #23). Spawning `process.execPath` with the resolved cli.js
49
+ // path as an argv element sidesteps cmd entirely — Node's spawn handles
50
+ // Windows argv quoting itself.
51
+ // 2. We no longer need a separate `cmd.exe /c …` branch, so the same
52
+ // spawn path works identically on Linux, macOS, and Windows.
53
+ const piPkgRoot = join(pkgRoot, "node_modules", "@earendil-works", "pi-coding-agent");
54
+ let piEntry;
55
+ try {
56
+ const piPkgJson = JSON.parse(readFileSync(join(piPkgRoot, "package.json"), "utf-8"));
57
+ const binRel = typeof piPkgJson?.bin === "string" ? piPkgJson.bin : piPkgJson?.bin?.pi;
58
+ if (typeof binRel !== "string") throw new Error("pi package.json has no bin.pi entry");
59
+ piEntry = resolve(piPkgRoot, binRel);
60
+ } catch (err) {
44
61
  console.error(
45
- `little-coder: cannot find pi at ${piBin}.\n` +
62
+ `little-coder: cannot resolve pi cli entry under ${piPkgRoot}.\n` +
63
+ `Underlying error: ${err?.message ?? err}\n` +
64
+ `Try reinstalling: npm install -g little-coder`,
65
+ );
66
+ process.exit(1);
67
+ }
68
+ if (!existsSync(piEntry)) {
69
+ console.error(
70
+ `little-coder: cannot find pi at ${piEntry}.\n` +
46
71
  `Try reinstalling: npm install -g little-coder`,
47
72
  );
48
73
  process.exit(1);
@@ -106,15 +131,30 @@ if (process.env.PI_SKIP_VERSION_CHECK === undefined) {
106
131
  process.env.PI_SKIP_VERSION_CHECK = "1";
107
132
  }
108
133
 
109
- // ---- 8. Force pi's global quietStartup so the loaded-resources block stays hidden ----
110
- // Pi's interactive mode dumps an [Extensions] / [Skills] / [Prompts] block on
111
- // every launch unless `quietStartup: true` is set in its global settings
112
- // (~/.pi/agent/settings.json). Our shipped .pi/settings.json doesn't reach pi
113
- // because pi reads from <cwd>/.pi/settings.json (project) or <agentDir>/settings.json
114
- // (global), neither of which is our npm-installed package dir. So the launcher
115
- // non-destructively merges quietStartup: true into the user's actual global
116
- // settings file. Existing keys are preserved. To see the full inventory, run
117
- // `little-coder --verbose` pi's verbose flag overrides quietStartup.
134
+ // ---- 8. Force pi's global quietStartup + pin lastChangelogVersion ----
135
+ // Two non-destructive merges into ~/.pi/agent/settings.json (or the dir pointed
136
+ // to by PI_CODING_AGENT_DIR):
137
+ //
138
+ // 1. quietStartup: true
139
+ // Pi's interactive mode otherwise dumps an [Extensions] / [Skills] /
140
+ // [Prompts] inventory on every launch. Pi reads global settings from
141
+ // <agentDir>/settings.json NOT from our npm-installed package dir
142
+ // so our shipped .pi/settings.json doesn't reach it. To see the
143
+ // inventory anyway, run `little-coder --verbose`.
144
+ //
145
+ // 2. lastChangelogVersion: <currently installed pi version>
146
+ // Pi reads its own bundled CHANGELOG.md on startup and renders a
147
+ // "What's New" block for every entry strictly newer than this stored
148
+ // version (interactive-mode.js:getChangelogForDisplay). That makes pi's
149
+ // upstream changelog show up inside little-coder's TUI every time we
150
+ // bump the bundled pi dep — which is jarring because little-coder is
151
+ // the surface, not pi. We pre-stamp this field to the version we just
152
+ // bundled BEFORE pi starts, so pi sees "user already saw this", and
153
+ // the block never renders. Users who genuinely want to read pi's
154
+ // upstream changelog can still do so with `/changelog` inside the TUI.
155
+ //
156
+ // Existing keys are preserved. We only write when the desired value differs
157
+ // from what's already on disk, so this is a no-op on warm launches.
118
158
  try {
119
159
  const agentDirEnv = process.env.PI_CODING_AGENT_DIR;
120
160
  let agentDir;
@@ -139,8 +179,32 @@ try {
139
179
  globalSettings = {};
140
180
  }
141
181
  }
182
+
183
+ // Read the bundled pi version. We resolve via the same package.json we used
184
+ // to find piEntry, so this stays consistent with whichever pi we actually
185
+ // spawn — no second source of truth.
186
+ let bundledPiVersion;
187
+ try {
188
+ const piPkgJson = JSON.parse(
189
+ readFileSync(join(piPkgRoot, "package.json"), "utf-8"),
190
+ );
191
+ if (typeof piPkgJson?.version === "string") bundledPiVersion = piPkgJson.version;
192
+ } catch {
193
+ // If we can't read pi's version, fall back to leaving lastChangelogVersion
194
+ // alone — pi will then show its own changelog on the next launch. Better
195
+ // than writing garbage into the user's settings.
196
+ }
197
+
198
+ let mutated = false;
142
199
  if (globalSettings.quietStartup !== true) {
143
200
  globalSettings.quietStartup = true;
201
+ mutated = true;
202
+ }
203
+ if (bundledPiVersion && globalSettings.lastChangelogVersion !== bundledPiVersion) {
204
+ globalSettings.lastChangelogVersion = bundledPiVersion;
205
+ mutated = true;
206
+ }
207
+ if (mutated) {
144
208
  writeFileSync(globalSettingsPath, JSON.stringify(globalSettings, null, 2));
145
209
  }
146
210
  } catch {
@@ -150,11 +214,11 @@ try {
150
214
  }
151
215
 
152
216
  // ---- 9. Spawn pi in the user's cwd ----
153
- const [spawnCmd, spawnArgs] = isWindows
154
- ? ["cmd.exe", ["/c", piBin, ...piArgs]]
155
- : [piBin, piArgs];
156
-
157
- const child = spawn(spawnCmd, spawnArgs, {
217
+ // `process.execPath` is the same Node binary that's running this launcher, so
218
+ // pi inherits the exact runtime that already passed our >= 22.19.0 preflight.
219
+ // Passing piEntry as an argv element (not a shell string) avoids any
220
+ // shell-injection / space-in-path classes on every platform.
221
+ const child = spawn(process.execPath, [piEntry, ...piArgs], {
158
222
  stdio: "inherit",
159
223
  cwd: process.cwd(),
160
224
  env: process.env,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "little-coder",
3
- "version": "1.4.1",
3
+ "version": "1.4.3",
4
4
  "description": "A pi-based coding agent optimized for small local language models. Reproduces the whitepaper's scaffold-model-fit adaptations as pi extensions.",
5
5
  "homepage": "https://github.com/itayinbarr/little-coder",
6
6
  "repository": {
@@ -14,7 +14,7 @@
14
14
  "little-coder": "bin/little-coder.mjs"
15
15
  },
16
16
  "engines": {
17
- "node": ">=20.6.0"
17
+ "node": ">=22.19.0"
18
18
  },
19
19
  "files": [
20
20
  "bin/",
@@ -23,6 +23,7 @@
23
23
  ".pi/extensions/",
24
24
  ".pi/settings.json",
25
25
  "models.json",
26
+ "vendor/",
26
27
  "LICENSE",
27
28
  "NOTICE",
28
29
  "README.md",
@@ -35,10 +36,13 @@
35
36
  "typecheck": "tsc --noEmit"
36
37
  },
37
38
  "dependencies": {
38
- "@mariozechner/pi-coding-agent": "^0.68.1",
39
+ "@earendil-works/pi-coding-agent": "^0.75.3",
39
40
  "@sinclair/typebox": "^0.34.49",
40
41
  "playwright": "^1.59.1"
41
42
  },
43
+ "overrides": {
44
+ "node-domexception": "file:./vendor/node-domexception"
45
+ },
42
46
  "devDependencies": {
43
47
  "typescript": "^5.6.0",
44
48
  "vitest": "^2.1.0"
@@ -0,0 +1,9 @@
1
+ // Drop-in replacement for the deprecated `node-domexception@1.0.0` shim.
2
+ // Bundled with little-coder; wired in via `package.json#overrides` so the
3
+ // transitive `fetch-blob -> node-domexception` chain (pulled in via
4
+ // @earendil-works/pi-ai -> @google/genai -> google-auth-library -> gaxios ->
5
+ // node-fetch -> fetch-blob) doesn't emit a deprecation warning during
6
+ // `npm install -g little-coder`. Native `globalThis.DOMException` has been
7
+ // available since Node 18, and little-coder requires Node >= 22.19, so this
8
+ // is always defined at import time.
9
+ module.exports = globalThis.DOMException;
@@ -0,0 +1,10 @@
1
+ {
2
+ "name": "node-domexception",
3
+ "version": "1.0.1",
4
+ "description": "Bundled with little-coder. Returns Node's native globalThis.DOMException (built-in since Node 18). Replaces the deprecated upstream node-domexception@1.0.0 via npm overrides so `npm install -g little-coder` no longer prints its deprecation warning. We pin engines.node to >= 18 because that's where native DOMException landed; the launcher's own preflight enforces >= 22.19, so this is satisfied transitively.",
5
+ "main": "index.js",
6
+ "engines": {
7
+ "node": ">=18.0.0"
8
+ },
9
+ "license": "MIT"
10
+ }