little-coder 1.9.2 → 1.9.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.
- package/CHANGELOG.md +10 -0
- package/README.md +1 -0
- package/bin/extras.mjs +56 -0
- package/bin/extras.test.mjs +119 -0
- package/bin/little-coder.mjs +16 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,16 @@
|
|
|
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.9.3] — 2026-06-18
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- **`LITTLE_CODER_EXTRA_EXTENSIONS` env var: layer third-party pi extensions onto the bundled set without forking the installed package** ([#46](https://github.com/itayinbarr/little-coder/issues/46)). Path-delimited list (`:` on POSIX, `;` on Windows — `node:path.delimiter`) of extension paths. Each entry can be a direct file (e.g. a `pi-ponytail`-style `extensions/ponytail.js`) or a directory containing `index.ts` / `index.js` (the launcher prefers `.ts`). A leading `~/` is expanded; missing paths log a one-line warning to stderr and are skipped (a typo in the env var doesn't kill the session). Survives upgrades — drop the env var into your shell rc once and every `little-coder` run picks up the extras. Example: `LITTLE_CODER_EXTRA_EXTENSIONS=~/.local/lib/node_modules/pi-ponytail/extensions/ponytail.js little-coder`. Parsing rules live in `bin/extras.mjs` so they're unit-testable in isolation (9 cases covering direct-file / dir-index-resolution / `index.ts`-preference / missing-path warning / `~/` expansion / multiple entries / whitespace trimming). The launcher-level integration is exercised end-to-end (warning prints for a bad path; valid paths pass through silently to pi as `--extension <entry>` flags). Closest siblings — third-party skill bundles — are not yet covered; `skill-inject` still discovers only `<pkgRoot>/skills/tools/*.md`, and a follow-up will add the same kind of override.
|
|
9
|
+
|
|
10
|
+
### Notes for upgraders
|
|
11
|
+
- No CLI-flag or public-API changes. The new env var is opt-in: unset = identical behavior to v1.9.2. If you were carrying a custom wrapper extension inside the installed npm package (which gets wiped on upgrade), you can drop it and use the env var instead.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
5
15
|
## [v1.9.2] — 2026-06-18
|
|
6
16
|
|
|
7
17
|
### Fixed
|
package/README.md
CHANGED
|
@@ -67,6 +67,7 @@ The agent uses the directory you launched it from as its working directory — `
|
|
|
67
67
|
- **Sub-coders (`dispatch`)** — little-coder can spawn isolated child sessions to research a question (read the repo + browse online, read-only) and report back concisely, without cluttering the main conversation. A live panel above the input tracks them. Tune parallelism with `LITTLE_CODER_SUBCODER_CONCURRENCY` (default 2).
|
|
68
68
|
- **Sessions** — each session is auto-named from your first prompt (rename with `/name`) and shown in the terminal tab title. Use `/resume` to list and reopen past sessions for the current directory.
|
|
69
69
|
- **Read-before-edit** — editing a file requires reading it first, so edits match the file's exact current text.
|
|
70
|
+
- **Third-party extensions (`LITTLE_CODER_EXTRA_EXTENSIONS`)** — path-delimited list (`:` on POSIX, `;` on Windows) of extension paths to layer on top of the bundled set. Each entry can be a direct file (e.g. a `pi-ponytail`-style `extensions/ponytail.js`) or a directory containing `index.ts` / `index.js`. `~/` is expanded; missing paths log a warning and are skipped. Survives upgrades, no patching the installed package. Example: `LITTLE_CODER_EXTRA_EXTENSIONS=~/.local/lib/node_modules/pi-ponytail/extensions/ponytail.js little-coder`. (Single-file extensions can still use `little-coder -e <path>` for one-off loads.)
|
|
70
71
|
|
|
71
72
|
For local providers (llama.cpp, Ollama, LM Studio) pi expects *some* value in the API-key env even though local servers ignore it:
|
|
72
73
|
|
package/bin/extras.mjs
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// Helpers for layering third-party pi extensions onto little-coder's bundled
|
|
2
|
+
// set. Extracted from the launcher so the parsing rules — path-delimited list,
|
|
3
|
+
// `~/` expansion, directory-with-index resolution, missing-path warning — are
|
|
4
|
+
// directly unit-testable without spawning the whole CLI.
|
|
5
|
+
|
|
6
|
+
import { existsSync, statSync } from "node:fs";
|
|
7
|
+
import { homedir } from "node:os";
|
|
8
|
+
import { delimiter, join } from "node:path";
|
|
9
|
+
|
|
10
|
+
// Given the value of LITTLE_CODER_EXTRA_EXTENSIONS (path-delimited list of
|
|
11
|
+
// extension paths), return the resolved entry files that should be passed to pi
|
|
12
|
+
// as `--extension <entry>` flags. Skips empty segments, expands a leading `~/`,
|
|
13
|
+
// resolves a directory entry to its `index.ts` (preferred) or `index.js`, and
|
|
14
|
+
// records a one-line warning for each missing/unusable path so a typo in the
|
|
15
|
+
// env var doesn't kill the session — it just doesn't load that extension.
|
|
16
|
+
export function parseExtraExtensions(
|
|
17
|
+
envValue,
|
|
18
|
+
{ home = homedir(), exists = existsSync, stat = statSync } = {},
|
|
19
|
+
) {
|
|
20
|
+
const entries = [];
|
|
21
|
+
const warnings = [];
|
|
22
|
+
for (const raw of String(envValue ?? "").split(delimiter)) {
|
|
23
|
+
const trimmed = raw.trim();
|
|
24
|
+
if (!trimmed) continue;
|
|
25
|
+
const expanded = trimmed === "~"
|
|
26
|
+
? home
|
|
27
|
+
: trimmed.startsWith("~/")
|
|
28
|
+
? home + trimmed.slice(1)
|
|
29
|
+
: trimmed;
|
|
30
|
+
if (!exists(expanded)) {
|
|
31
|
+
warnings.push(
|
|
32
|
+
`little-coder: LITTLE_CODER_EXTRA_EXTENSIONS path not found, skipping: ${expanded}`,
|
|
33
|
+
);
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
let entry = expanded;
|
|
37
|
+
try {
|
|
38
|
+
if (stat(expanded).isDirectory()) {
|
|
39
|
+
const candidates = [join(expanded, "index.ts"), join(expanded, "index.js")];
|
|
40
|
+
const found = candidates.find((p) => exists(p));
|
|
41
|
+
if (!found) {
|
|
42
|
+
warnings.push(
|
|
43
|
+
`little-coder: LITTLE_CODER_EXTRA_EXTENSIONS dir has no index.ts/index.js, skipping: ${expanded}`,
|
|
44
|
+
);
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
entry = found;
|
|
48
|
+
}
|
|
49
|
+
} catch {
|
|
50
|
+
// unreadable / racing stat — skip silently
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
entries.push(entry);
|
|
54
|
+
}
|
|
55
|
+
return { entries, warnings };
|
|
56
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { mkdtempSync, writeFileSync, mkdirSync, rmSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { delimiter, join } from "node:path";
|
|
5
|
+
import { parseExtraExtensions } from "./extras.mjs";
|
|
6
|
+
|
|
7
|
+
function setupTmp() {
|
|
8
|
+
return mkdtempSync(join(tmpdir(), "lc-extras-"));
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
describe("parseExtraExtensions", () => {
|
|
12
|
+
it("returns no entries when env is unset / empty", () => {
|
|
13
|
+
expect(parseExtraExtensions(undefined).entries).toEqual([]);
|
|
14
|
+
expect(parseExtraExtensions("").entries).toEqual([]);
|
|
15
|
+
expect(parseExtraExtensions(delimiter + delimiter).entries).toEqual([]);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("forwards a direct file path verbatim", () => {
|
|
19
|
+
const dir = setupTmp();
|
|
20
|
+
try {
|
|
21
|
+
const file = join(dir, "ponytail.js");
|
|
22
|
+
writeFileSync(file, "export default function(){}");
|
|
23
|
+
const { entries, warnings } = parseExtraExtensions(file);
|
|
24
|
+
expect(entries).toEqual([file]);
|
|
25
|
+
expect(warnings).toEqual([]);
|
|
26
|
+
} finally {
|
|
27
|
+
rmSync(dir, { recursive: true });
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("resolves a directory entry to its index.ts (preferred)", () => {
|
|
32
|
+
const dir = setupTmp();
|
|
33
|
+
try {
|
|
34
|
+
const extDir = join(dir, "ponytail");
|
|
35
|
+
mkdirSync(extDir);
|
|
36
|
+
writeFileSync(join(extDir, "index.ts"), "");
|
|
37
|
+
writeFileSync(join(extDir, "index.js"), "");
|
|
38
|
+
const { entries } = parseExtraExtensions(extDir);
|
|
39
|
+
expect(entries).toEqual([join(extDir, "index.ts")]);
|
|
40
|
+
} finally {
|
|
41
|
+
rmSync(dir, { recursive: true });
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("falls back to index.js when index.ts is absent", () => {
|
|
46
|
+
const dir = setupTmp();
|
|
47
|
+
try {
|
|
48
|
+
const extDir = join(dir, "ponytail");
|
|
49
|
+
mkdirSync(extDir);
|
|
50
|
+
writeFileSync(join(extDir, "index.js"), "");
|
|
51
|
+
const { entries } = parseExtraExtensions(extDir);
|
|
52
|
+
expect(entries).toEqual([join(extDir, "index.js")]);
|
|
53
|
+
} finally {
|
|
54
|
+
rmSync(dir, { recursive: true });
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("warns and skips a directory without index.ts/index.js", () => {
|
|
59
|
+
const dir = setupTmp();
|
|
60
|
+
try {
|
|
61
|
+
const extDir = join(dir, "empty");
|
|
62
|
+
mkdirSync(extDir);
|
|
63
|
+
const { entries, warnings } = parseExtraExtensions(extDir);
|
|
64
|
+
expect(entries).toEqual([]);
|
|
65
|
+
expect(warnings).toHaveLength(1);
|
|
66
|
+
expect(warnings[0]).toContain("no index.ts/index.js");
|
|
67
|
+
} finally {
|
|
68
|
+
rmSync(dir, { recursive: true });
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("warns and skips a missing path (typo doesn't kill the session)", () => {
|
|
73
|
+
const { entries, warnings } = parseExtraExtensions("/tmp/does-not-exist-zzz-xyz-123");
|
|
74
|
+
expect(entries).toEqual([]);
|
|
75
|
+
expect(warnings).toHaveLength(1);
|
|
76
|
+
expect(warnings[0]).toContain("path not found");
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("expands a leading ~/ using the supplied home", () => {
|
|
80
|
+
const dir = setupTmp();
|
|
81
|
+
try {
|
|
82
|
+
const extDir = join(dir, "fake-home", "ext");
|
|
83
|
+
mkdirSync(extDir, { recursive: true });
|
|
84
|
+
writeFileSync(join(extDir, "index.js"), "");
|
|
85
|
+
const { entries } = parseExtraExtensions("~/ext", {
|
|
86
|
+
home: join(dir, "fake-home"),
|
|
87
|
+
});
|
|
88
|
+
expect(entries).toEqual([join(extDir, "index.js")]);
|
|
89
|
+
} finally {
|
|
90
|
+
rmSync(dir, { recursive: true });
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("layers multiple extensions from one path-delimited list", () => {
|
|
95
|
+
const dir = setupTmp();
|
|
96
|
+
try {
|
|
97
|
+
const a = join(dir, "a.js");
|
|
98
|
+
writeFileSync(a, "");
|
|
99
|
+
const b = join(dir, "b.js");
|
|
100
|
+
writeFileSync(b, "");
|
|
101
|
+
const { entries } = parseExtraExtensions([a, b].join(delimiter));
|
|
102
|
+
expect(entries).toEqual([a, b]);
|
|
103
|
+
} finally {
|
|
104
|
+
rmSync(dir, { recursive: true });
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("trims whitespace around list entries", () => {
|
|
109
|
+
const dir = setupTmp();
|
|
110
|
+
try {
|
|
111
|
+
const a = join(dir, "a.js");
|
|
112
|
+
writeFileSync(a, "");
|
|
113
|
+
const { entries } = parseExtraExtensions(` ${a} `);
|
|
114
|
+
expect(entries).toEqual([a]);
|
|
115
|
+
} finally {
|
|
116
|
+
rmSync(dir, { recursive: true });
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
});
|
package/bin/little-coder.mjs
CHANGED
|
@@ -17,6 +17,7 @@ import { homedir } from "node:os";
|
|
|
17
17
|
import { dirname, join, resolve } from "node:path";
|
|
18
18
|
import { fileURLToPath } from "node:url";
|
|
19
19
|
import { checkForUpdate } from "./update-check.mjs";
|
|
20
|
+
import { parseExtraExtensions } from "./extras.mjs";
|
|
20
21
|
|
|
21
22
|
// ---- 1. Node version preflight (>= 22.19.0, matching pi.dev) ----
|
|
22
23
|
const MIN_NODE = [22, 19, 0];
|
|
@@ -110,6 +111,21 @@ if (existsSync(extDir)) {
|
|
|
110
111
|
}
|
|
111
112
|
}
|
|
112
113
|
|
|
114
|
+
// ---- 4b. Third-party extensions via LITTLE_CODER_EXTRA_EXTENSIONS ----
|
|
115
|
+
// Path-delimited list (`:` on POSIX, `;` on Windows — node:path.delimiter)
|
|
116
|
+
// of extra extension paths to load alongside the bundled ones. Each entry can
|
|
117
|
+
// be either a direct file path (e.g. a pi-ponytail-style `extensions/ponytail.js`)
|
|
118
|
+
// or a directory containing `index.ts` / `index.js`. Survives upgrades and
|
|
119
|
+
// avoids the "fork the installed npm package" workaround that issue #46 hit.
|
|
120
|
+
// Parsing rules — ~/ expansion, directory-with-index resolution, one-line
|
|
121
|
+
// warning for missing/unusable entries — live in ./extras.mjs so they're
|
|
122
|
+
// unit-testable in isolation.
|
|
123
|
+
{
|
|
124
|
+
const { entries, warnings } = parseExtraExtensions(process.env.LITTLE_CODER_EXTRA_EXTENSIONS);
|
|
125
|
+
for (const w of warnings) console.error(w);
|
|
126
|
+
for (const entry of entries) extArgs.push("--extension", entry);
|
|
127
|
+
}
|
|
128
|
+
|
|
113
129
|
// ---- 5. Update check (best-effort, blocks on TTY prompt only) ----
|
|
114
130
|
let currentVersion = "0.0.0";
|
|
115
131
|
try {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "little-coder",
|
|
3
|
-
"version": "1.9.
|
|
3
|
+
"version": "1.9.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": {
|