@xynogen/pix-models 0.2.0 → 0.2.1

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/README.md CHANGED
@@ -4,7 +4,14 @@ Pi extension — enhanced `/models` picker with coding score/rank.
4
4
 
5
5
  ## What it does
6
6
 
7
- Registers a `/models` slash command that replaces Pi's built-in `/model` selector with a richer TUI picker. Each row shows the model id, context window, per-million-token cost, and a coding-focused score/rank (with star bar) when available. The list is sorted by coding score (best first), then alphabetically for unscored models. Fuzzy search filters the list as you type. Left/right changes the thinking level (`off` → `minimal` → `low` → `medium` → `high` → `xhigh`), with the effective level shown live in the picker header. Selecting a model switches the active model for the session. Model metadata is sourced from `~/.cache/pi/` via `pix-data`; the coding score/rank is computed locally from the modelgrep catalog (best = #1). Requires `@xynogen/pix-data` as a dependency.
7
+ Registers a `/models` slash command a richer TUI picker replacing Pi's built-in `/model` selector.
8
+
9
+ - **Each row** — model id, context window, per-M-token cost, and a coding score/rank (star bar) when available.
10
+ - **Sorting** — by coding score (best first), then alphabetically for unscored models. Fuzzy search filters as you type.
11
+ - **Thinking level** — left/right cycles `off` → `minimal` → `low` → `medium` → `high` → `xhigh`, shown live in the header.
12
+ - **Select** — switches the active model for the session.
13
+
14
+ Model metadata comes from `~/.cache/pi/` via `pix-data`; the coding score/rank is computed locally from the modelgrep catalog (best = #1).
8
15
 
9
16
  ## Install
10
17
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xynogen/pix-models",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Pi extension — enhanced /models picker with BenchLM ranks",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -80,6 +80,17 @@ export const BUILTIN_SLASH_COMMANDS = [
80
80
  expect(out).toContain('name: "login"');
81
81
  });
82
82
 
83
+ it("removes /model from Pi's minified bundle chunk", () => {
84
+ // Real form from dist/bundle/chunks/*.js: no `export const`, no spaces,
85
+ // array followed immediately by other bundled statements.
86
+ const minified = `var x=1;BUILTIN_SLASH_COMMANDS=[{name:"settings",description:"Open settings menu"},{name:"model",description:"Select model (opens selector UI)",argumentHint:"<provider/model>"},{name:"scoped-models",description:"Enable/disable models for Ctrl+P cycling"}];import path11 from"node:path";`;
87
+ const out = stripBuiltinModelCommand(minified);
88
+ expect(out).not.toContain('name:"model"');
89
+ expect(out).toContain('name:"settings"');
90
+ expect(out).toContain('name:"scoped-models"');
91
+ expect(out).toContain('import path11 from"node:path"');
92
+ });
93
+
83
94
  it("round-trips through disk", () => {
84
95
  const dir = mkdtempSync(join(tmpdir(), "pix-patch-"));
85
96
  const file = join(dir, "slash-commands.js");
@@ -2,79 +2,115 @@
2
2
  * patch-builtin.ts — strip Pi's built-in /model slash command at load time.
3
3
  *
4
4
  * Built-in commands can't be removed via the extension API, so we edit Pi's
5
- * compiled slash-commands.js directly. Done on every load: idempotent and
5
+ * compiled slash-command source directly. Done on every load: idempotent and
6
6
  * self-healing across Pi upgrades, so no manual repatch is ever needed.
7
7
  *
8
- * Resolution strategy (in order):
9
- * 1. Locate the `pi` binary via PATH → infer package root from its realpath.
10
- * The binary is always at <pkg>/dist/cli.js so ../../ is the package root.
11
- * 2. Probe well-known global install locations (bun, npm).
12
- * 3. Fall back to createRequire against the extension's own node_modules
13
- * (works when pi and the extension share the same install tree).
8
+ * Pi's build layout has moved over time. Two forms are in the wild:
9
+ * - legacy: <pkg>/dist/core/slash-commands.js (readable, one entry/line)
10
+ * - bundled: <pkg>/dist/bundle/chunks/chunk-*.js (minified, hash-named)
11
+ * The bundled chunk is what the runtime actually executes; the legacy
12
+ * dist/core/ file may still exist as dead output. We therefore prefer the
13
+ * chunk that really contains the `BUILTIN_SLASH_COMMANDS=[...]` assignment and
14
+ * only fall back to dist/core/ for older hosts that lack a bundle dir.
15
+ *
16
+ * Package-root resolution (in order):
17
+ * 1. `pi` binary via PATH → realpath → split at /dist/ to get the pkg root.
18
+ * 2. Well-known global install locations (bun, npm).
19
+ * 3. createRequire against the extension's own node_modules.
14
20
  */
15
21
 
16
22
  import { execSync } from "node:child_process";
17
- import { existsSync, readFileSync, writeFileSync } from "node:fs";
23
+ import { existsSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
18
24
  import { createRequire } from "node:module";
19
25
  import { homedir } from "node:os";
20
- import { dirname, join, resolve } from "node:path";
21
-
22
- // Pi has added fields to this object over time (for example, `argumentHint` in
23
- // v0.80). Match the command entry by its stable `name`, rather than an exact
24
- // serialized line, while limiting the match to a single non-nested object.
25
- const BUILTIN_COMMANDS_ARRAY = /export\s+const\s+BUILTIN_SLASH_COMMANDS[^=]*=\s*\[/;
26
- const BUILTIN_MODEL_COMMAND =
27
- /^[ \t]*\{(?=[^{}]*\bname\s*:\s*["']model["'])[^{}]*\},?[ \t]*(?:\r?\n|$)/gm;
28
-
29
- /** Candidate slash-commands.js paths, most-specific first. */
30
- function candidatePaths(): string[] {
31
- const paths: string[] = [];
32
-
33
- // 1. Resolve via the running `pi` binary → its realpath gives the dist dir.
26
+ import { join, sep } from "node:path";
27
+
28
+ // The assignment appears once per build. `\s*=\s*\[` avoids the `.map(...)`
29
+ // references and tolerates both `= [` (pretty) and `=[` (minified) spacing.
30
+ const BUILTIN_COMMANDS_ARRAY = /BUILTIN_SLASH_COMMANDS\s*=\s*\[/;
31
+ // Match one flat (non-nested) command object whose stable `name` is exactly
32
+ // "model", plus an optional trailing comma. No line anchors, so it works for
33
+ // both minified inline entries and pretty multi-line ones. The `["']model["']`
34
+ // requires a closing quote right after `model`, so `models`/`scoped-models`
35
+ // never match.
36
+ const BUILTIN_MODEL_COMMAND = /\{(?=[^{}]*\bname\s*:\s*["']model["'])[^{}]*\},?/g;
37
+
38
+ /** Infer pi-coding-agent package roots, most-specific first. */
39
+ function packageRoots(): string[] {
40
+ const roots: string[] = [];
41
+ const pushRoot = (r: string | undefined) => {
42
+ if (r && !roots.includes(r)) roots.push(r);
43
+ };
44
+
45
+ // 1. Resolve via the running `pi` binary → realpath → strip at /dist/.
34
46
  try {
35
47
  const piReal = execSync("realpath $(which pi)", {
36
48
  encoding: "utf8",
37
49
  stdio: ["pipe", "pipe", "pipe"],
38
50
  }).trim();
39
- if (piReal) {
40
- // piReal = /.../pi-coding-agent/dist/cli.js → dist/ → ../dist/core/
41
- const distCore = resolve(dirname(piReal), "core");
42
- paths.push(join(distCore, "slash-commands.js"));
43
- }
51
+ const idx = piReal.indexOf(`${sep}dist${sep}`);
52
+ if (idx >= 0) pushRoot(piReal.slice(0, idx));
44
53
  } catch {
45
54
  // `pi` not on PATH or `which`/`realpath` unavailable — skip
46
55
  }
47
56
 
48
57
  // 2. Well-known global install locations.
49
58
  const home = homedir();
50
- const globalRoots = [
59
+ for (const root of [
51
60
  join(home, ".bun", "install", "global", "node_modules"),
52
61
  join(home, ".npm-global", "lib", "node_modules"),
53
62
  "/usr/local/lib/node_modules",
54
63
  "/usr/lib/node_modules",
55
- ];
56
- for (const root of globalRoots) {
57
- paths.push(
58
- join(root, "@earendil-works", "pi-coding-agent", "dist", "core", "slash-commands.js"),
59
- );
64
+ ]) {
65
+ pushRoot(join(root, "@earendil-works", "pi-coding-agent"));
60
66
  }
61
67
 
62
- // 3. Fallback: createRequire from this file (works when extension is co-installed).
68
+ // 3. Fallback: createRequire from this file (co-installed extension).
63
69
  try {
64
70
  const require = createRequire(import.meta.url);
65
71
  const entry = require.resolve("@earendil-works/pi-coding-agent");
66
- paths.push(resolve(dirname(entry), "core", "slash-commands.js"));
72
+ const idx = entry.indexOf(`${sep}dist${sep}`);
73
+ if (idx >= 0) pushRoot(entry.slice(0, idx));
67
74
  } catch {
68
75
  // local resolution failed — skip
69
76
  }
70
77
 
71
- return paths;
78
+ return roots;
79
+ }
80
+
81
+ /** Candidate source files for a package root — live bundle chunks first. */
82
+ function candidateFiles(root: string): string[] {
83
+ const files: string[] = [];
84
+ const chunkDir = join(root, "dist", "bundle", "chunks");
85
+ try {
86
+ for (const name of readdirSync(chunkDir)) {
87
+ if (name.endsWith(".js")) files.push(join(chunkDir, name));
88
+ }
89
+ } catch {
90
+ // no bundle dir on this host — legacy layout below
91
+ }
92
+ // Legacy readable output, kept as a fallback for older Pi builds.
93
+ files.push(join(root, "dist", "core", "slash-commands.js"));
94
+ return files;
95
+ }
96
+
97
+ /** All candidate paths across all roots (exported for tests). */
98
+ function candidatePaths(): string[] {
99
+ return packageRoots().flatMap(candidateFiles);
72
100
  }
73
101
 
74
- /** Locate the host's compiled slash-commands.js, or null if not found. */
102
+ /**
103
+ * Locate the source file that actually declares BUILTIN_SLASH_COMMANDS.
104
+ * Chunk filenames are content hashes, so we confirm by contents, not by name.
105
+ */
75
106
  function findSlashCommandsFile(): string | null {
76
107
  for (const p of candidatePaths()) {
77
- if (existsSync(p)) return p;
108
+ if (!existsSync(p)) continue;
109
+ try {
110
+ if (BUILTIN_COMMANDS_ARRAY.test(readFileSync(p, "utf8"))) return p;
111
+ } catch {
112
+ // unreadable — skip
113
+ }
78
114
  }
79
115
  return null;
80
116
  }