little-coder 1.0.0 → 1.0.2
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 +19 -0
- package/README.md +31 -31
- package/bin/little-coder.mjs +22 -4
- package/bin/update-check.mjs +167 -0
- package/bin/update-check.test.mjs +164 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,25 @@
|
|
|
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.0.2] — 2026-04-28
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
- README order: **Install**, **Run**, and **Local model setup** now lead the doc (right after "How it relates to pi"), with **Paper / benchmark results** and **Roadmap** moved below. New users hit the install command first instead of scrolling past benchmark tables.
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## [v1.0.1] — 2026-04-28
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
- **Update check on startup.** When a newer `little-coder` is on npm, the launcher tells you and (in interactive mode) offers to update on the spot.
|
|
16
|
+
- **Interactive TTY:** prompt `Update now? [Y/n]` — Enter or `y` runs `npm install -g little-coder@latest` and asks you to re-run; `n` skips for this session.
|
|
17
|
+
- **Non-TTY (CI, scripts, pipes, `--print` pipelines):** prints a one-line stderr notice with the install command, never prompts.
|
|
18
|
+
- **Skipped automatically** for `--help`, `--version`, `--list-models`, `--export`, `--mode rpc`, `--mode json`, when `CI=true`, and for the new `--no-update-check` flag / `LITTLE_CODER_NO_UPDATE_CHECK=1` env opt-out.
|
|
19
|
+
- **Cached** at `${XDG_CACHE_HOME:-~/.cache}/little-coder/version-check.json` with a 12 h TTL — at most one network call per day.
|
|
20
|
+
- **Best-effort:** 2 s fetch timeout, all errors swallowed silently. Update check never blocks the agent if the registry is slow or unreachable.
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
5
24
|
## [v1.0.0] — 2026-04-28
|
|
6
25
|
|
|
7
26
|
Distribution + stability release. Hi everywhere, bye `./node_modules/.bin/pi`.
|
package/README.md
CHANGED
|
@@ -12,37 +12,6 @@ little-coder is **pi + 20 extensions + 30 skill markdown files + a Python benchm
|
|
|
12
12
|
|
|
13
13
|
If you've never used pi, it's useful to skim [pi.dev](https://pi.dev) first — the rest of this doc assumes pi's model of `--agent-import-path`, `--mode rpc`, and `.pi/extensions/` auto-discovery.
|
|
14
14
|
|
|
15
|
-
## Paper / benchmark results
|
|
16
|
-
|
|
17
|
-
| Release | Model | Benchmark | Result |
|
|
18
|
-
|---|---|---|---|
|
|
19
|
-
| [**v0.0.2**](https://github.com/itayinbarr/little-coder/releases/tag/v0.0.2) (commit `1d62bde`) — the paper | Qwen3.5-9B via Ollama | Aider Polyglot (225 exercises) | **45.56 %** mean of two runs; matched-model vanilla Aider baseline 19.11 %. Paper: [*Honey, I Shrunk the Coding Agent* on Substack](https://open.substack.com/pub/itayinbarr/p/honey-i-shrunk-the-coding-agent). |
|
|
20
|
-
| [**v0.0.5**](https://github.com/itayinbarr/little-coder/releases/tag/v0.0.5) — pre-pi Python | Qwen3.6-35B-A3B via llama.cpp | Aider Polyglot | **78.67 %**. [Full narrative](docs/benchmark-qwen3.6-35b-a3b.md). |
|
|
21
|
-
| [**v0.1.4**](https://github.com/itayinbarr/little-coder/releases/tag/v0.1.4) — on pi | Qwen3.6-35B-A3B via llama.cpp | Terminal-Bench-Core v0.1.1 (80 tasks) | **40.0 %** in 6 h 50 min. [Write-up](docs/benchmark-terminal-bench-v0.1.1.md). |
|
|
22
|
-
| [**v0.1.13**](https://github.com/itayinbarr/little-coder/releases/tag/v0.1.13) — on pi, TB 2.0 leaderboard | Qwen3.6-35B-A3B via llama.cpp | Terminal-Bench 2.0 (89 tasks × 5 trials = 445) | **23.82 %** (106 / 445). [PR #158](https://huggingface.co/datasets/harborframework/terminal-bench-2-leaderboard/discussions/158) — awaiting maintainer merge. |
|
|
23
|
-
| [**v0.1.24**](https://github.com/itayinbarr/little-coder/releases/tag/v0.1.24) — on pi, TB 2.0 leaderboard, smaller model | Qwen3.5-9B (Q4_K_M) via llama.cpp (5.3 GB on GPU, 2× faster per-token than the 35B-A3B) | Terminal-Bench 2.0 (89 tasks × 5 trials = 445) | **9.21 %** (41 / 445). [PR #163](https://huggingface.co/datasets/harborframework/terminal-bench-2-leaderboard/discussions/163) — awaiting maintainer merge. |
|
|
24
|
-
| [**v0.1.27**](https://github.com/itayinbarr/little-coder/releases/tag/v0.1.27) — on pi, GAIA validation | Qwen3.6-35B-A3B via llama.cpp | GAIA validation set (165 tasks) | **40.00 %** (66 / 165). L1 60.4 % / L2 37.2 % / L3 7.7 %. Test-split run pending. |
|
|
25
|
-
|
|
26
|
-
All runs used a consumer laptop: i9-14900HX, 32 GB RAM, **8 GB VRAM** on RTX 5070 Laptop (Blackwell). No cloud inference at any point.
|
|
27
|
-
|
|
28
|
-
---
|
|
29
|
-
|
|
30
|
-
## Roadmap
|
|
31
|
-
|
|
32
|
-
The near-term focus is **benchmarking**, not new features. The paper established that scaffold–model fit moves a 9.7 B model from 19 % to 45 % on Aider Polyglot. The open question is: **how wide is the impact radius?** Does the same set of adaptations — Write-vs-Edit invariant, per-turn skill injection, thinking-budget cap, output-repair, quality monitor — help on tasks that *aren't* self-contained coding exercises? What breaks? What compounds?
|
|
33
|
-
|
|
34
|
-
The plan is to establish a wide baseline before any further scaffolding changes:
|
|
35
|
-
|
|
36
|
-
1. **Aider Polyglot** — done. 45.56 % (paper, Qwen3.5-9B) and 78.67 % (v0.0.5, Qwen3.6-35B-A3B).
|
|
37
|
-
2. **Terminal-Bench-Core v0.1.1** — done. 40.0 % (v0.1.4).
|
|
38
|
-
3. **Terminal-Bench 2.0** — done. Qwen3.6-35B-A3B at **23.82 %** ([PR #158](https://huggingface.co/datasets/harborframework/terminal-bench-2-leaderboard/discussions/158)) and Qwen3.5-9B at **9.21 %** ([PR #163](https://huggingface.co/datasets/harborframework/terminal-bench-2-leaderboard/discussions/163)), both awaiting maintainer merge. The v0.1.24 prompt-repetition fix (re-add tool descriptions + concision guideline, validated by a 4 / 4 pilot on the previously-regressing `prove-plus-comm` task) was the prompt for both submissions.
|
|
39
|
-
4. **GAIA** — validation set done at v0.1.27: **40.00 %** (66 / 165) on Qwen3.6-35B-A3B. Per-level L1 60.4 % / L2 37.2 % / L3 7.7 %. Test-split run (301 tasks) pending → leaderboard submission to follow.
|
|
40
|
-
5. **SWE-bench Verified** — after GAIA. Multi-file real-world patches; the longest-horizon test of whether the scaffolding generalizes past exercise-scale tasks.
|
|
41
|
-
|
|
42
|
-
**After that baseline is in place**, the next phase starts: improvement experiments targeted at the specific failure patterns we've seen (thinking-budget / quality-monitor behavior on long-horizon tasks, deliberate.py-style parallel branches on failure, better shell-session recovery for interactive-process traps). No scaffold changes until the data says which ones are worth running.
|
|
43
|
-
|
|
44
|
-
---
|
|
45
|
-
|
|
46
15
|
## Install
|
|
47
16
|
|
|
48
17
|
One-line install (Node.js 20.6+ required):
|
|
@@ -122,6 +91,37 @@ All small-model-specific extensions auto-disable for large/cloud models so they
|
|
|
122
91
|
|
|
123
92
|
---
|
|
124
93
|
|
|
94
|
+
## Paper / benchmark results
|
|
95
|
+
|
|
96
|
+
| Release | Model | Benchmark | Result |
|
|
97
|
+
|---|---|---|---|
|
|
98
|
+
| [**v0.0.2**](https://github.com/itayinbarr/little-coder/releases/tag/v0.0.2) (commit `1d62bde`) — the paper | Qwen3.5-9B via Ollama | Aider Polyglot (225 exercises) | **45.56 %** mean of two runs; matched-model vanilla Aider baseline 19.11 %. Paper: [*Honey, I Shrunk the Coding Agent* on Substack](https://open.substack.com/pub/itayinbarr/p/honey-i-shrunk-the-coding-agent). |
|
|
99
|
+
| [**v0.0.5**](https://github.com/itayinbarr/little-coder/releases/tag/v0.0.5) — pre-pi Python | Qwen3.6-35B-A3B via llama.cpp | Aider Polyglot | **78.67 %**. [Full narrative](docs/benchmark-qwen3.6-35b-a3b.md). |
|
|
100
|
+
| [**v0.1.4**](https://github.com/itayinbarr/little-coder/releases/tag/v0.1.4) — on pi | Qwen3.6-35B-A3B via llama.cpp | Terminal-Bench-Core v0.1.1 (80 tasks) | **40.0 %** in 6 h 50 min. [Write-up](docs/benchmark-terminal-bench-v0.1.1.md). |
|
|
101
|
+
| [**v0.1.13**](https://github.com/itayinbarr/little-coder/releases/tag/v0.1.13) — on pi, TB 2.0 leaderboard | Qwen3.6-35B-A3B via llama.cpp | Terminal-Bench 2.0 (89 tasks × 5 trials = 445) | **23.82 %** (106 / 445). [PR #158](https://huggingface.co/datasets/harborframework/terminal-bench-2-leaderboard/discussions/158) — awaiting maintainer merge. |
|
|
102
|
+
| [**v0.1.24**](https://github.com/itayinbarr/little-coder/releases/tag/v0.1.24) — on pi, TB 2.0 leaderboard, smaller model | Qwen3.5-9B (Q4_K_M) via llama.cpp (5.3 GB on GPU, 2× faster per-token than the 35B-A3B) | Terminal-Bench 2.0 (89 tasks × 5 trials = 445) | **9.21 %** (41 / 445). [PR #163](https://huggingface.co/datasets/harborframework/terminal-bench-2-leaderboard/discussions/163) — awaiting maintainer merge. |
|
|
103
|
+
| [**v0.1.27**](https://github.com/itayinbarr/little-coder/releases/tag/v0.1.27) — on pi, GAIA validation | Qwen3.6-35B-A3B via llama.cpp | GAIA validation set (165 tasks) | **40.00 %** (66 / 165). L1 60.4 % / L2 37.2 % / L3 7.7 %. Test-split run pending. |
|
|
104
|
+
|
|
105
|
+
All runs used a consumer laptop: i9-14900HX, 32 GB RAM, **8 GB VRAM** on RTX 5070 Laptop (Blackwell). No cloud inference at any point.
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
## Roadmap
|
|
110
|
+
|
|
111
|
+
The near-term focus is **benchmarking**, not new features. The paper established that scaffold–model fit moves a 9.7 B model from 19 % to 45 % on Aider Polyglot. The open question is: **how wide is the impact radius?** Does the same set of adaptations — Write-vs-Edit invariant, per-turn skill injection, thinking-budget cap, output-repair, quality monitor — help on tasks that *aren't* self-contained coding exercises? What breaks? What compounds?
|
|
112
|
+
|
|
113
|
+
The plan is to establish a wide baseline before any further scaffolding changes:
|
|
114
|
+
|
|
115
|
+
1. **Aider Polyglot** — done. 45.56 % (paper, Qwen3.5-9B) and 78.67 % (v0.0.5, Qwen3.6-35B-A3B).
|
|
116
|
+
2. **Terminal-Bench-Core v0.1.1** — done. 40.0 % (v0.1.4).
|
|
117
|
+
3. **Terminal-Bench 2.0** — done. Qwen3.6-35B-A3B at **23.82 %** ([PR #158](https://huggingface.co/datasets/harborframework/terminal-bench-2-leaderboard/discussions/158)) and Qwen3.5-9B at **9.21 %** ([PR #163](https://huggingface.co/datasets/harborframework/terminal-bench-2-leaderboard/discussions/163)), both awaiting maintainer merge. The v0.1.24 prompt-repetition fix (re-add tool descriptions + concision guideline, validated by a 4 / 4 pilot on the previously-regressing `prove-plus-comm` task) was the prompt for both submissions.
|
|
118
|
+
4. **GAIA** — validation set done at v0.1.27: **40.00 %** (66 / 165) on Qwen3.6-35B-A3B. Per-level L1 60.4 % / L2 37.2 % / L3 7.7 %. Test-split run (301 tasks) pending → leaderboard submission to follow.
|
|
119
|
+
5. **SWE-bench Verified** — after GAIA. Multi-file real-world patches; the longest-horizon test of whether the scaffolding generalizes past exercise-scale tasks.
|
|
120
|
+
|
|
121
|
+
**After that baseline is in place**, the next phase starts: improvement experiments targeted at the specific failure patterns we've seen (thinking-budget / quality-monitor behavior on long-horizon tasks, deliberate.py-style parallel branches on failure, better shell-session recovery for interactive-process traps). No scaffold changes until the data says which ones are worth running.
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
125
125
|
## Troubleshooting
|
|
126
126
|
|
|
127
127
|
**`little-coder: command not found`** — npm's global bin directory isn't on your PATH. Run `npm config get prefix` to see where it installed; add `<prefix>/bin` to your PATH. Or reinstall with `sudo` if your prefix needs root.
|
package/bin/little-coder.mjs
CHANGED
|
@@ -4,9 +4,10 @@
|
|
|
4
4
|
// custom extension wired in — works from any working directory.
|
|
5
5
|
|
|
6
6
|
import { spawn } from "node:child_process";
|
|
7
|
-
import { existsSync, readdirSync, statSync } from "node:fs";
|
|
7
|
+
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
8
8
|
import { dirname, join, resolve } from "node:path";
|
|
9
9
|
import { fileURLToPath } from "node:url";
|
|
10
|
+
import { checkForUpdate } from "./update-check.mjs";
|
|
10
11
|
|
|
11
12
|
// ---- 1. Node version preflight (>= 20.6.0, matching pi.dev) ----
|
|
12
13
|
const MIN_NODE = [20, 6, 0];
|
|
@@ -54,20 +55,37 @@ if (existsSync(extDir)) {
|
|
|
54
55
|
}
|
|
55
56
|
}
|
|
56
57
|
|
|
57
|
-
// ---- 5.
|
|
58
|
+
// ---- 5. Update check (best-effort, blocks on TTY prompt only) ----
|
|
59
|
+
let currentVersion = "0.0.0";
|
|
60
|
+
try {
|
|
61
|
+
const pkgJson = JSON.parse(readFileSync(join(pkgRoot, "package.json"), "utf-8"));
|
|
62
|
+
if (typeof pkgJson?.version === "string") currentVersion = pkgJson.version;
|
|
63
|
+
} catch {
|
|
64
|
+
// ignore — update-check just won't fire if we can't read the version
|
|
65
|
+
}
|
|
66
|
+
const exitAfterCheck = await checkForUpdate(currentVersion);
|
|
67
|
+
if (exitAfterCheck) {
|
|
68
|
+
// Successful update happened; user needs to re-run the new binary.
|
|
69
|
+
process.exit(0);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ---- 6. Compose pi argv ----
|
|
58
73
|
// --no-context-files : ignore the user's AGENTS.md / CLAUDE.md so OURS wins
|
|
59
74
|
// --no-extensions : skip pi's auto-discovery from cwd; explicit -e flags still load
|
|
60
75
|
// --system-prompt : load <pkgRoot>/AGENTS.md regardless of cwd
|
|
76
|
+
//
|
|
77
|
+
// Strip our own flags before forwarding to pi so it doesn't reject them.
|
|
78
|
+
const userArgs = process.argv.slice(2).filter((a) => a !== "--no-update-check");
|
|
61
79
|
const agentsMd = join(pkgRoot, "AGENTS.md");
|
|
62
80
|
const piArgs = [
|
|
63
81
|
"--no-context-files",
|
|
64
82
|
"--no-extensions",
|
|
65
83
|
...(existsSync(agentsMd) ? ["--system-prompt", agentsMd] : []),
|
|
66
84
|
...extArgs,
|
|
67
|
-
...
|
|
85
|
+
...userArgs,
|
|
68
86
|
];
|
|
69
87
|
|
|
70
|
-
// ----
|
|
88
|
+
// ---- 7. Spawn pi in the user's cwd ----
|
|
71
89
|
const child = spawn(piBin, piArgs, {
|
|
72
90
|
stdio: "inherit",
|
|
73
91
|
cwd: process.cwd(),
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
// little-coder update check.
|
|
2
|
+
// Polls the npm registry for a newer published version and (in TTY mode)
|
|
3
|
+
// offers to install it before the agent starts. Cached so we don't call out
|
|
4
|
+
// on every invocation. Best-effort throughout: if anything fails, we skip
|
|
5
|
+
// silently — never block the agent over a version check.
|
|
6
|
+
|
|
7
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
8
|
+
import { homedir } from "node:os";
|
|
9
|
+
import { dirname, join } from "node:path";
|
|
10
|
+
import { spawnSync } from "node:child_process";
|
|
11
|
+
import { createInterface } from "node:readline";
|
|
12
|
+
|
|
13
|
+
const REGISTRY = "https://registry.npmjs.org/little-coder/latest";
|
|
14
|
+
const CACHE_TTL_MS = 12 * 60 * 60 * 1000; // 12 h
|
|
15
|
+
const FETCH_TIMEOUT_MS = 2000;
|
|
16
|
+
|
|
17
|
+
export function cachePath() {
|
|
18
|
+
const xdg = process.env.XDG_CACHE_HOME && process.env.XDG_CACHE_HOME.trim();
|
|
19
|
+
const base = xdg ? xdg : join(homedir(), ".cache");
|
|
20
|
+
return join(base, "little-coder", "version-check.json");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function readCache(now = Date.now()) {
|
|
24
|
+
try {
|
|
25
|
+
const path = cachePath();
|
|
26
|
+
if (!existsSync(path)) return null;
|
|
27
|
+
const data = JSON.parse(readFileSync(path, "utf-8"));
|
|
28
|
+
if (typeof data.checkedAt !== "number" || typeof data.latest !== "string") return null;
|
|
29
|
+
if (now - data.checkedAt > CACHE_TTL_MS) return null;
|
|
30
|
+
return data;
|
|
31
|
+
} catch {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function writeCache(latest, now = Date.now()) {
|
|
37
|
+
try {
|
|
38
|
+
const path = cachePath();
|
|
39
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
40
|
+
writeFileSync(path, JSON.stringify({ checkedAt: now, latest }));
|
|
41
|
+
} catch {
|
|
42
|
+
// best-effort; permission errors etc. are not fatal
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Compare semver strings. Only handles X.Y.Z[+pre]. Returns 1 if a > b,
|
|
47
|
+
// -1 if a < b, 0 if equal. Pre-release suffixes are treated as < release.
|
|
48
|
+
export function compareSemver(a, b) {
|
|
49
|
+
const parse = (v) => {
|
|
50
|
+
const [core, pre] = String(v).split("-", 2);
|
|
51
|
+
const parts = core.split(".").map((n) => parseInt(n, 10));
|
|
52
|
+
return {
|
|
53
|
+
major: parts[0] || 0,
|
|
54
|
+
minor: parts[1] || 0,
|
|
55
|
+
patch: parts[2] || 0,
|
|
56
|
+
pre: pre || "",
|
|
57
|
+
};
|
|
58
|
+
};
|
|
59
|
+
const pa = parse(a);
|
|
60
|
+
const pb = parse(b);
|
|
61
|
+
if (pa.major !== pb.major) return pa.major > pb.major ? 1 : -1;
|
|
62
|
+
if (pa.minor !== pb.minor) return pa.minor > pb.minor ? 1 : -1;
|
|
63
|
+
if (pa.patch !== pb.patch) return pa.patch > pb.patch ? 1 : -1;
|
|
64
|
+
// Equal core: a release beats a pre-release.
|
|
65
|
+
if (pa.pre === pb.pre) return 0;
|
|
66
|
+
if (pa.pre === "") return 1;
|
|
67
|
+
if (pb.pre === "") return -1;
|
|
68
|
+
return pa.pre > pb.pre ? 1 : -1;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function fetchLatest() {
|
|
72
|
+
const ctrl = new AbortController();
|
|
73
|
+
const t = setTimeout(() => ctrl.abort(), FETCH_TIMEOUT_MS);
|
|
74
|
+
try {
|
|
75
|
+
const res = await fetch(REGISTRY, { signal: ctrl.signal });
|
|
76
|
+
if (!res.ok) return null;
|
|
77
|
+
const json = await res.json();
|
|
78
|
+
return typeof json?.version === "string" ? json.version : null;
|
|
79
|
+
} catch {
|
|
80
|
+
return null;
|
|
81
|
+
} finally {
|
|
82
|
+
clearTimeout(t);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Decide whether to skip the check entirely. Errs toward NOT prompting in
|
|
87
|
+
// any context that smells programmatic.
|
|
88
|
+
export function shouldSkip(argv = process.argv.slice(2), env = process.env, stdout = process.stdout) {
|
|
89
|
+
if (env.LITTLE_CODER_NO_UPDATE_CHECK === "1") return true;
|
|
90
|
+
if (env.CI === "true" || env.CI === "1") return true;
|
|
91
|
+
for (let i = 0; i < argv.length; i++) {
|
|
92
|
+
const a = argv[i];
|
|
93
|
+
if (a === "--no-update-check") return true;
|
|
94
|
+
if (a === "--help" || a === "-h") return true;
|
|
95
|
+
if (a === "--version" || a === "-v") return true;
|
|
96
|
+
if (a === "--list-models") return true;
|
|
97
|
+
if (a === "--export") return true;
|
|
98
|
+
if (a === "--mode") {
|
|
99
|
+
const next = argv[i + 1];
|
|
100
|
+
if (next === "rpc" || next === "json") return true;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
// Non-TTY runs: scripts, pipes, --print pipelines. Notice only, no prompt.
|
|
104
|
+
if (!stdout.isTTY) return "notice-only";
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function promptYesNo(question) {
|
|
109
|
+
return new Promise((resolve) => {
|
|
110
|
+
if (!process.stdin.isTTY) {
|
|
111
|
+
resolve(false);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
115
|
+
rl.question(question, (answer) => {
|
|
116
|
+
rl.close();
|
|
117
|
+
const a = (answer ?? "").trim().toLowerCase();
|
|
118
|
+
resolve(a === "" || a === "y" || a === "yes");
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Returns `true` if the launcher should NOT proceed to spawn pi (because we
|
|
124
|
+
// updated and exited / the user opted out and we should re-run). Returns
|
|
125
|
+
// `false` to let the launcher continue.
|
|
126
|
+
export async function checkForUpdate(currentVersion, opts = {}) {
|
|
127
|
+
const skip = opts.skip ?? shouldSkip();
|
|
128
|
+
if (skip === true) return false;
|
|
129
|
+
|
|
130
|
+
let latest = readCache()?.latest;
|
|
131
|
+
if (!latest) {
|
|
132
|
+
latest = await fetchLatest();
|
|
133
|
+
if (latest) writeCache(latest);
|
|
134
|
+
}
|
|
135
|
+
if (!latest) return false;
|
|
136
|
+
if (compareSemver(latest, currentVersion) <= 0) return false;
|
|
137
|
+
|
|
138
|
+
const headline =
|
|
139
|
+
`\n📦 little-coder v${latest} is available (you have v${currentVersion}).`;
|
|
140
|
+
|
|
141
|
+
if (skip === "notice-only") {
|
|
142
|
+
process.stderr.write(`${headline}\n Update with: npm install -g little-coder\n\n`);
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
process.stderr.write(`${headline}\n`);
|
|
147
|
+
const wantsUpdate = await promptYesNo(" Update now? [Y/n] ");
|
|
148
|
+
if (!wantsUpdate) {
|
|
149
|
+
process.stderr.write(" Skipping update for this run.\n\n");
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
process.stderr.write(`\n Running: npm install -g little-coder@${latest}\n\n`);
|
|
154
|
+
const result = spawnSync("npm", ["install", "-g", `little-coder@${latest}`], {
|
|
155
|
+
stdio: "inherit",
|
|
156
|
+
});
|
|
157
|
+
if (result.status === 0) {
|
|
158
|
+
process.stderr.write(
|
|
159
|
+
`\n ✓ Updated to v${latest}. Re-run \`little-coder\` to use the new version.\n\n`,
|
|
160
|
+
);
|
|
161
|
+
return true;
|
|
162
|
+
}
|
|
163
|
+
process.stderr.write(
|
|
164
|
+
`\n ✗ Update failed (npm exit ${result.status}). Continuing with v${currentVersion}.\n\n`,
|
|
165
|
+
);
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { mkdtempSync, rmSync, existsSync, readFileSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import {
|
|
6
|
+
cachePath,
|
|
7
|
+
readCache,
|
|
8
|
+
writeCache,
|
|
9
|
+
compareSemver,
|
|
10
|
+
shouldSkip,
|
|
11
|
+
} from "./update-check.mjs";
|
|
12
|
+
|
|
13
|
+
describe("compareSemver", () => {
|
|
14
|
+
it("orders major / minor / patch correctly", () => {
|
|
15
|
+
expect(compareSemver("1.0.0", "1.0.0")).toBe(0);
|
|
16
|
+
expect(compareSemver("1.0.1", "1.0.0")).toBe(1);
|
|
17
|
+
expect(compareSemver("1.0.0", "1.0.1")).toBe(-1);
|
|
18
|
+
expect(compareSemver("1.1.0", "1.0.99")).toBe(1);
|
|
19
|
+
expect(compareSemver("2.0.0", "1.99.99")).toBe(1);
|
|
20
|
+
expect(compareSemver("0.99.99", "1.0.0")).toBe(-1);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("treats releases as greater than pre-releases of same core", () => {
|
|
24
|
+
expect(compareSemver("1.0.0", "1.0.0-rc.1")).toBe(1);
|
|
25
|
+
expect(compareSemver("1.0.0-rc.1", "1.0.0")).toBe(-1);
|
|
26
|
+
expect(compareSemver("1.0.0-rc.2", "1.0.0-rc.1")).toBe(1);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("tolerates short version strings", () => {
|
|
30
|
+
expect(compareSemver("1.0", "1.0.0")).toBe(0);
|
|
31
|
+
expect(compareSemver("1", "1.0.0")).toBe(0);
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
describe("cachePath", () => {
|
|
36
|
+
it("uses XDG_CACHE_HOME when set", () => {
|
|
37
|
+
const orig = process.env.XDG_CACHE_HOME;
|
|
38
|
+
process.env.XDG_CACHE_HOME = "/tmp/xdg-test";
|
|
39
|
+
try {
|
|
40
|
+
expect(cachePath()).toBe("/tmp/xdg-test/little-coder/version-check.json");
|
|
41
|
+
} finally {
|
|
42
|
+
if (orig !== undefined) process.env.XDG_CACHE_HOME = orig;
|
|
43
|
+
else delete process.env.XDG_CACHE_HOME;
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("falls back to ~/.cache when XDG is unset", () => {
|
|
48
|
+
const orig = process.env.XDG_CACHE_HOME;
|
|
49
|
+
delete process.env.XDG_CACHE_HOME;
|
|
50
|
+
try {
|
|
51
|
+
const p = cachePath();
|
|
52
|
+
expect(p).toMatch(/\.cache\/little-coder\/version-check\.json$/);
|
|
53
|
+
} finally {
|
|
54
|
+
if (orig !== undefined) process.env.XDG_CACHE_HOME = orig;
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe("read/writeCache", () => {
|
|
60
|
+
let tmp;
|
|
61
|
+
let origXdg;
|
|
62
|
+
beforeEach(() => {
|
|
63
|
+
tmp = mkdtempSync(join(tmpdir(), "lc-uc-test-"));
|
|
64
|
+
origXdg = process.env.XDG_CACHE_HOME;
|
|
65
|
+
process.env.XDG_CACHE_HOME = tmp;
|
|
66
|
+
});
|
|
67
|
+
afterEach(() => {
|
|
68
|
+
if (origXdg !== undefined) process.env.XDG_CACHE_HOME = origXdg;
|
|
69
|
+
else delete process.env.XDG_CACHE_HOME;
|
|
70
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("returns null when no cache exists", () => {
|
|
74
|
+
expect(readCache()).toBeNull();
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("round-trips a fresh entry", () => {
|
|
78
|
+
writeCache("1.0.5", 1000);
|
|
79
|
+
const cached = readCache(2000);
|
|
80
|
+
expect(cached?.latest).toBe("1.0.5");
|
|
81
|
+
expect(cached?.checkedAt).toBe(1000);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("returns null for stale entries past 12h TTL", () => {
|
|
85
|
+
writeCache("1.0.5", 0);
|
|
86
|
+
const stale = readCache(13 * 60 * 60 * 1000);
|
|
87
|
+
expect(stale).toBeNull();
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("returns the entry if exactly at TTL boundary", () => {
|
|
91
|
+
writeCache("1.0.5", 0);
|
|
92
|
+
const at = readCache(12 * 60 * 60 * 1000);
|
|
93
|
+
expect(at?.latest).toBe("1.0.5");
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("handles malformed cache files gracefully", () => {
|
|
97
|
+
writeCache("garbage", 1000);
|
|
98
|
+
const path = cachePath();
|
|
99
|
+
// Corrupt the file
|
|
100
|
+
const fs = readFileSync(path, "utf-8");
|
|
101
|
+
expect(fs).toContain("garbage");
|
|
102
|
+
// Now write actual garbage
|
|
103
|
+
require("node:fs").writeFileSync(path, "{not-json");
|
|
104
|
+
expect(readCache()).toBeNull();
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("creates the cache directory if missing", () => {
|
|
108
|
+
rmSync(join(tmp, "little-coder"), { recursive: true, force: true });
|
|
109
|
+
writeCache("1.2.3", 5000);
|
|
110
|
+
expect(existsSync(cachePath())).toBe(true);
|
|
111
|
+
expect(readCache(5000)?.latest).toBe("1.2.3");
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
describe("shouldSkip", () => {
|
|
116
|
+
function ttyStdout() { return { isTTY: true }; }
|
|
117
|
+
function pipeStdout() { return { isTTY: false }; }
|
|
118
|
+
const noEnv = {};
|
|
119
|
+
|
|
120
|
+
it("returns false in plain TTY interactive mode", () => {
|
|
121
|
+
expect(shouldSkip([], noEnv, ttyStdout())).toBe(false);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("skips when LITTLE_CODER_NO_UPDATE_CHECK=1", () => {
|
|
125
|
+
expect(shouldSkip([], { LITTLE_CODER_NO_UPDATE_CHECK: "1" }, ttyStdout())).toBe(true);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("skips on --no-update-check flag", () => {
|
|
129
|
+
expect(shouldSkip(["--no-update-check"], noEnv, ttyStdout())).toBe(true);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it("skips on --help / -h", () => {
|
|
133
|
+
expect(shouldSkip(["--help"], noEnv, ttyStdout())).toBe(true);
|
|
134
|
+
expect(shouldSkip(["-h"], noEnv, ttyStdout())).toBe(true);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("skips on --version / -v", () => {
|
|
138
|
+
expect(shouldSkip(["--version"], noEnv, ttyStdout())).toBe(true);
|
|
139
|
+
expect(shouldSkip(["-v"], noEnv, ttyStdout())).toBe(true);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("skips on --list-models and --export", () => {
|
|
143
|
+
expect(shouldSkip(["--list-models"], noEnv, ttyStdout())).toBe(true);
|
|
144
|
+
expect(shouldSkip(["--export", "session.jsonl"], noEnv, ttyStdout())).toBe(true);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("skips for --mode rpc / --mode json", () => {
|
|
148
|
+
expect(shouldSkip(["--mode", "rpc"], noEnv, ttyStdout())).toBe(true);
|
|
149
|
+
expect(shouldSkip(["--mode", "json"], noEnv, ttyStdout())).toBe(true);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it("does not skip for --mode text", () => {
|
|
153
|
+
expect(shouldSkip(["--mode", "text"], noEnv, ttyStdout())).toBe(false);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it("skips in CI environments", () => {
|
|
157
|
+
expect(shouldSkip([], { CI: "true" }, ttyStdout())).toBe(true);
|
|
158
|
+
expect(shouldSkip([], { CI: "1" }, ttyStdout())).toBe(true);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it("returns notice-only on non-TTY pipelines", () => {
|
|
162
|
+
expect(shouldSkip([], noEnv, pipeStdout())).toBe("notice-only");
|
|
163
|
+
});
|
|
164
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "little-coder",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
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": {
|