@xynogen/pix-core 0.1.0 → 0.1.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/package.json CHANGED
@@ -1,42 +1,51 @@
1
1
  {
2
- "name": "@xynogen/pix-core",
3
- "version": "0.1.0",
4
- "description": "Pi extension — core UI/UX bundle (welcome banner, footer, model picker, self-update)",
5
- "type": "module",
6
- "main": "src/index.ts",
7
- "scripts": {
8
- "test": "bun test"
9
- },
10
- "files": [
11
- "src",
12
- "skills",
13
- "README.md",
14
- "LICENSE"
15
- ],
16
- "pi": {
17
- "extensions": ["src/index.ts"],
18
- "skills": ["./skills"]
19
- },
20
- "keywords": ["pi", "pi-package", "pi-extension", "pix"],
21
- "author": "xynogen",
22
- "license": "MIT",
23
- "repository": {
24
- "type": "git",
25
- "url": "git+https://github.com/xynogen/pix-mono.git",
26
- "directory": "packages/pix-core"
27
- },
28
- "homepage": "https://github.com/xynogen/pix-mono/tree/main/packages/pix-core#readme",
29
- "bugs": {
30
- "url": "https://github.com/xynogen/pix-mono/issues"
31
- },
32
- "publishConfig": {
33
- "access": "public"
34
- },
35
- "dependencies": {
36
- "typebox": "^1.1.38"
37
- },
38
- "peerDependencies": {
39
- "@earendil-works/pi-coding-agent": "*",
40
- "@earendil-works/pi-tui": "*"
41
- }
2
+ "name": "@xynogen/pix-core",
3
+ "version": "0.1.1",
4
+ "description": "Pi extension — core UI/UX bundle (welcome banner, footer, model picker, self-update)",
5
+ "type": "module",
6
+ "main": "src/index.ts",
7
+ "scripts": {
8
+ "test": "bun test"
9
+ },
10
+ "files": [
11
+ "src",
12
+ "skills",
13
+ "README.md",
14
+ "LICENSE"
15
+ ],
16
+ "pi": {
17
+ "extensions": [
18
+ "src/index.ts"
19
+ ],
20
+ "skills": [
21
+ "./skills"
22
+ ]
23
+ },
24
+ "keywords": [
25
+ "pi",
26
+ "pi-package",
27
+ "pi-extension",
28
+ "pix"
29
+ ],
30
+ "author": "xynogen",
31
+ "license": "MIT",
32
+ "repository": {
33
+ "type": "git",
34
+ "url": "git+https://github.com/xynogen/pix-mono.git",
35
+ "directory": "packages/pix-core"
36
+ },
37
+ "homepage": "https://github.com/xynogen/pix-mono/tree/main/packages/pix-core#readme",
38
+ "bugs": {
39
+ "url": "https://github.com/xynogen/pix-mono/issues"
40
+ },
41
+ "publishConfig": {
42
+ "access": "public"
43
+ },
44
+ "dependencies": {
45
+ "typebox": "^1.1.38"
46
+ },
47
+ "peerDependencies": {
48
+ "@earendil-works/pi-coding-agent": "*",
49
+ "@earendil-works/pi-tui": "*"
50
+ }
42
51
  }
@@ -12,7 +12,7 @@ async function clearCache(pi: ExtensionAPI, ctx: ExtensionCommandContext) {
12
12
  .filter(Boolean)
13
13
  .join("\n")
14
14
  .trim();
15
- if ((result.exitCode ?? 0) !== 0) {
15
+ if ((result.code ?? 0) !== 0) {
16
16
  ctx.ui.notify(`Cache clear failed. ${output || "No output."}`, "error");
17
17
  return;
18
18
  }
@@ -16,7 +16,12 @@ function textFromContent(content: unknown): string {
16
16
  return content
17
17
  .map((block) => {
18
18
  if (!block || typeof block !== "object" || !("type" in block)) return "";
19
- if (block.type === "text" && "text" in block && typeof block.text === "string") return block.text;
19
+ if (
20
+ block.type === "text" &&
21
+ "text" in block &&
22
+ typeof block.text === "string"
23
+ )
24
+ return block.text;
20
25
  if (block.type === "image") return "[image]";
21
26
  return "";
22
27
  })
@@ -43,11 +48,14 @@ function copyToClipboard(text: string): Promise<void> {
43
48
  }
44
49
  const child = spawn(c.cmd, c.args);
45
50
  let stderr = "";
46
- child.stderr.on("data", (chunk) => { stderr += String(chunk); });
51
+ child.stderr.on("data", (chunk) => {
52
+ stderr += String(chunk);
53
+ });
47
54
  child.on("error", reject);
48
55
  child.on("close", (code) => {
49
56
  if (code === 0) resolve();
50
- else reject(new Error(stderr.trim() || `${c.cmd} exited with code ${code}`));
57
+ else
58
+ reject(new Error(stderr.trim() || `${c.cmd} exited with code ${code}`));
51
59
  });
52
60
  child.stdin.end(text);
53
61
  });
@@ -55,7 +63,8 @@ function copyToClipboard(text: string): Promise<void> {
55
63
 
56
64
  export default function (pi: ExtensionAPI) {
57
65
  pi.registerCommand("copy-all", {
58
- description: "Copy all user/assistant messages in this thread to the clipboard",
66
+ description:
67
+ "Copy all user/assistant messages in this thread to the clipboard",
59
68
  handler: async (_args, ctx) => {
60
69
  await ctx.waitForIdle();
61
70
 
@@ -80,9 +89,15 @@ export default function (pi: ExtensionAPI) {
80
89
 
81
90
  try {
82
91
  await copyToClipboard(text);
83
- ctx.ui.notify(`📋 Copied ${messages.length} messages to clipboard`, "info");
92
+ ctx.ui.notify(
93
+ `📋 Copied ${messages.length} messages to clipboard`,
94
+ "info",
95
+ );
84
96
  } catch (err) {
85
- ctx.ui.notify(`Clipboard copy failed: ${err instanceof Error ? err.message : String(err)}`, "error");
97
+ ctx.ui.notify(
98
+ `Clipboard copy failed: ${err instanceof Error ? err.message : String(err)}`,
99
+ "error",
100
+ );
86
101
  }
87
102
  },
88
103
  });
@@ -18,7 +18,8 @@ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
18
18
  const COMMAND = "diff";
19
19
 
20
20
  function getStringPath(input: unknown): string | undefined {
21
- if (!input || typeof input !== "object" || !("path" in input)) return undefined;
21
+ if (!input || typeof input !== "object" || !("path" in input))
22
+ return undefined;
22
23
  const p = (input as { path?: unknown }).path;
23
24
  return typeof p === "string" ? p : undefined;
24
25
  }
@@ -45,8 +46,15 @@ function parseGitStatus(output: string, cwd: string): Set<string> {
45
46
  return files;
46
47
  }
47
48
 
48
- async function getGitChanged(pi: ExtensionAPI, cwd: string): Promise<Set<string>> {
49
- const r = await pi.exec("git", ["status", "--porcelain", "--untracked-files=all"], { cwd, timeout: 5000 });
49
+ async function getGitChanged(
50
+ pi: ExtensionAPI,
51
+ cwd: string,
52
+ ): Promise<Set<string>> {
53
+ const r = await pi.exec(
54
+ "git",
55
+ ["status", "--porcelain", "--untracked-files=all"],
56
+ { cwd, timeout: 5000 },
57
+ );
50
58
  if (r.code !== 0) return new Set();
51
59
  return parseGitStatus(r.stdout, cwd);
52
60
  }
@@ -56,7 +64,8 @@ function diff(current: Set<string>, baseline: Set<string>): Set<string> {
56
64
  }
57
65
 
58
66
  function pickEditor(): { cmd: string; args: (file: string) => string[] } {
59
- const env = process.env.PI_DIFF_EDITOR || process.env.VISUAL || process.env.EDITOR;
67
+ const env =
68
+ process.env.PI_DIFF_EDITOR || process.env.VISUAL || process.env.EDITOR;
60
69
  if (env) {
61
70
  const parts = env.split(/\s+/);
62
71
  const cmd = parts[0];
@@ -88,12 +97,16 @@ export default function (pi: ExtensionAPI) {
88
97
  const now = await getGitChanged(pi, ctx.cwd);
89
98
  changed = new Set([...diff(now, baseline), ...touched]);
90
99
  if (changed.size > 0) {
91
- ctx.ui.notify(`📝 ${changed.size} changed file(s). Run /${COMMAND} to view/open.`, "info");
100
+ ctx.ui.notify(
101
+ `📝 ${changed.size} changed file(s). Run /${COMMAND} to view/open.`,
102
+ "info",
103
+ );
92
104
  }
93
105
  });
94
106
 
95
107
  pi.registerCommand(COMMAND, {
96
- description: "Show files changed by the last agent run and open one in your editor",
108
+ description:
109
+ "Show files changed by the last agent run and open one in your editor",
97
110
  handler: async (args, ctx) => {
98
111
  await ctx.waitForIdle();
99
112
  const arg = (args ?? "").trim();
@@ -106,19 +119,30 @@ export default function (pi: ExtensionAPI) {
106
119
  return;
107
120
  }
108
121
 
109
- const files = [...changed].sort((a, b) => toRel(ctx.cwd, a).localeCompare(toRel(ctx.cwd, b)));
122
+ const files = [...changed].sort((a, b) =>
123
+ toRel(ctx.cwd, a).localeCompare(toRel(ctx.cwd, b)),
124
+ );
110
125
  if (files.length === 0) {
111
- ctx.ui.notify("No changed files tracked from the last agent run", "info");
126
+ ctx.ui.notify(
127
+ "No changed files tracked from the last agent run",
128
+ "info",
129
+ );
112
130
  return;
113
131
  }
114
132
 
115
133
  if (arg === "list") {
116
- ctx.ui.notify(`Changed files:\n${files.map((f) => `- ${toRel(ctx.cwd, f)}`).join("\n")}`, "info");
134
+ ctx.ui.notify(
135
+ `Changed files:\n${files.map((f) => `- ${toRel(ctx.cwd, f)}`).join("\n")}`,
136
+ "info",
137
+ );
117
138
  return;
118
139
  }
119
140
 
120
141
  if (arg) {
121
- ctx.ui.notify(`Unknown /${COMMAND} argument: ${arg}. Try /${COMMAND}, /${COMMAND} list, /${COMMAND} clear.`, "warning");
142
+ ctx.ui.notify(
143
+ `Unknown /${COMMAND} argument: ${arg}. Try /${COMMAND}, /${COMMAND} list, /${COMMAND} clear.`,
144
+ "warning",
145
+ );
122
146
  return;
123
147
  }
124
148
 
@@ -130,9 +154,17 @@ export default function (pi: ExtensionAPI) {
130
154
  if (!file) return;
131
155
 
132
156
  const ed = pickEditor();
133
- const r = await pi.exec(ed.cmd, ed.args(file), { cwd: ctx.cwd, timeout: 5000 });
134
- if (r.code === 0) ctx.ui.notify(`Opened ${selected} in ${ed.cmd}`, "info");
135
- else ctx.ui.notify(r.stderr.trim() || `Failed to open ${selected} in ${ed.cmd}`, "error");
157
+ const r = await pi.exec(ed.cmd, ed.args(file), {
158
+ cwd: ctx.cwd,
159
+ timeout: 5000,
160
+ });
161
+ if (r.code === 0)
162
+ ctx.ui.notify(`Opened ${selected} in ${ed.cmd}`, "info");
163
+ else
164
+ ctx.ui.notify(
165
+ r.stderr.trim() || `Failed to open ${selected} in ${ed.cmd}`,
166
+ "error",
167
+ );
136
168
  },
137
169
  });
138
170
  }
@@ -1,5 +1,5 @@
1
- import { describe, it, expect } from "bun:test";
2
- import { fmtCtx, fmtCost, benchStars, sortModels } from "./models.ts";
1
+ import { describe, expect, it } from "bun:test";
2
+ import { benchStars, fmtCost, fmtCtx, sortModels } from "./models.ts";
3
3
 
4
4
  describe("fmtCtx", () => {
5
5
  it("formats 0 as 0", () => expect(fmtCtx(0)).toBe("0"));
@@ -23,7 +23,7 @@ import {
23
23
  Text,
24
24
  visibleWidth,
25
25
  } from "@earendil-works/pi-tui";
26
- import { lookupModelsDev, lookupBenchmark } from "../../lib/data";
26
+ import { lookupBenchmark, lookupModelsDev } from "../../lib/data";
27
27
 
28
28
  // ─── Pure logic (exported for tests) ─────────────────────────────────────────
29
29
 
@@ -1,10 +1,10 @@
1
- import { describe, it, expect } from "bun:test";
1
+ import { describe, expect, it } from "bun:test";
2
2
  import {
3
- isTransient,
4
3
  commandFor,
5
4
  formatUpdateSummary,
6
- PACKAGE_NAME,
7
5
  type InstallMethod,
6
+ isTransient,
7
+ PACKAGE_NAME,
8
8
  } from "./update.ts";
9
9
 
10
10
  describe("isTransient", () => {
@@ -48,28 +48,28 @@ describe("commandFor", () => {
48
48
  for (const m of methods) {
49
49
  const spec = commandFor(m);
50
50
  expect(spec).toBeDefined();
51
- expect(spec!.command).toBeTruthy();
52
- expect(spec!.label).toBeTruthy();
51
+ expect(spec?.command).toBeTruthy();
52
+ expect(spec?.label).toBeTruthy();
53
53
  }
54
54
  });
55
55
 
56
56
  it("vp uses vp add -g", () => {
57
57
  const spec = commandFor("vp")!;
58
58
  expect(spec.command).toBe("vp");
59
- expect(spec.args).toContain(PACKAGE_NAME + "@latest");
59
+ expect(spec.args).toContain(`${PACKAGE_NAME}@latest`);
60
60
  });
61
61
 
62
62
  it("bun uses bun add -g", () => {
63
63
  const spec = commandFor("bun")!;
64
64
  expect(spec.command).toBe("bun");
65
- expect(spec.args).toContain(PACKAGE_NAME + "@latest");
65
+ expect(spec.args).toContain(`${PACKAGE_NAME}@latest`);
66
66
  });
67
67
 
68
68
  it("npm uses npm install -g", () => {
69
69
  const spec = commandFor("npm")!;
70
70
  expect(spec.command).toBe("npm");
71
71
  expect(spec.args).toContain("-g");
72
- expect(spec.args).toContain(PACKAGE_NAME + "@latest");
72
+ expect(spec.args).toContain(`${PACKAGE_NAME}@latest`);
73
73
  });
74
74
 
75
75
  it("brew uses sh -lc", () => {
@@ -127,7 +127,7 @@ async function detectInstallMethod(pi: ExtensionAPI): Promise<InstallMethod> {
127
127
  ],
128
128
  { timeout: 10_000 },
129
129
  );
130
- if ((hasGlobalNpm.exitCode ?? 1) === 0) return "npm";
130
+ if ((hasGlobalNpm.code ?? 1) === 0) return "npm";
131
131
  }
132
132
 
133
133
  if (await resolveCommand("vp", pi)) return "vp";
@@ -145,7 +145,7 @@ async function runWithRetry(pi: ExtensionAPI, spec: CommandSpec) {
145
145
  .filter(Boolean)
146
146
  .join("\n")
147
147
  .trim();
148
- if ((result.exitCode ?? 0) === 0)
148
+ if ((result.code ?? 0) === 0)
149
149
  return { ok: true, output: lastOutput, attempts: attempt };
150
150
  if (attempt === 3 || !isTransient(lastOutput))
151
151
  return { ok: false, output: lastOutput, attempts: attempt };
@@ -200,7 +200,7 @@ async function updateExtensions(
200
200
  .filter(Boolean)
201
201
  .join("\n")
202
202
  .trim();
203
- if ((result.exitCode ?? 0) !== 0) {
203
+ if ((result.code ?? 0) !== 0) {
204
204
  ctx.ui.notify(
205
205
  `Pi extensions update failed. ${output || "No output."}`,
206
206
  "error",
@@ -222,7 +222,7 @@ async function updatePackages(pi: ExtensionAPI, ctx: ExtensionCommandContext) {
222
222
  .filter(Boolean)
223
223
  .join("\n")
224
224
  .trim();
225
- if ((result.exitCode ?? 0) !== 0) {
225
+ if ((result.code ?? 0) !== 0) {
226
226
  ctx.ui.notify(
227
227
  `Pi package update failed. ${output || "No output."}`,
228
228
  "error",
package/src/index.ts CHANGED
@@ -16,20 +16,20 @@
16
16
  */
17
17
 
18
18
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
19
- import registerWelcome from "./ui/welcome.ts";
20
- import registerFooter from "./ui/footer.ts";
21
- import registerDiagnostics from "./ui/diagnostics.ts";
19
+ import registerClear from "./commands/clear/clear.ts";
20
+ import registerCopyAll from "./commands/copy-all/copy-all.ts";
21
+ import registerDiff from "./commands/diff/diff.ts";
22
+ import registerLg from "./commands/lg/lg.ts";
22
23
  import registerModels from "./commands/models/models.ts";
23
24
  import registerUpdate from "./commands/update/update.ts";
24
- import registerLg from "./commands/lg/lg.ts";
25
25
  import registerYeet from "./commands/yeet/yeet.ts";
26
- import registerCopyAll from "./commands/copy-all/copy-all.ts";
27
- import registerDiff from "./commands/diff/diff.ts";
28
- import registerClear from "./commands/clear/clear.ts";
29
- import registerTodo from "./tool/todo/todo.ts";
26
+ import registerNudges from "./nudge/index.ts";
30
27
  import registerAsk from "./tool/ask/ask.ts";
28
+ import registerTodo from "./tool/todo/todo.ts";
31
29
  import registerToolbox from "./tool/toolbox/toolbox.ts";
32
- import registerNudges from "./nudge/index.ts";
30
+ import registerDiagnostics from "./ui/diagnostics.ts";
31
+ import registerFooter from "./ui/footer.ts";
32
+ import registerWelcome from "./ui/welcome.ts";
33
33
 
34
34
  export default function (pi: ExtensionAPI): void {
35
35
  registerWelcome(pi);
package/src/lib/data.ts CHANGED
@@ -9,8 +9,8 @@
9
9
  * models.ts — lookupModelsDev, lookupBenchmark
10
10
  */
11
11
 
12
- import { mkdir, readFile, writeFile } from "node:fs/promises";
13
12
  import { existsSync, readFileSync } from "node:fs";
13
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
14
14
  import { homedir } from "node:os";
15
15
  import { dirname, join } from "node:path";
16
16
 
@@ -18,7 +18,6 @@ const skill = (
18
18
  disableModelInvocation,
19
19
  filePath: "",
20
20
  baseDir: "",
21
- // biome-ignore lint: test fixture
22
21
  sourceInfo: {} as never,
23
22
  }) as never;
24
23
 
@@ -29,7 +28,6 @@ const tool = (name: string, source: string) =>
29
28
  description: `${name} desc.`,
30
29
  parameters: {},
31
30
  sourceInfo: { source, path: "", scope: "user", origin: "package" },
32
- // biome-ignore lint: test fixture
33
31
  }) as never;
34
32
 
35
33
  describe("CAPABILITY_REMINDER", () => {
@@ -176,18 +174,17 @@ describe("buildOrientation", () => {
176
174
  test("lists invocable skill names, sorted, excluding user-only", () => {
177
175
  const out = buildOrientation(
178
176
  [],
179
- [
180
- skill("zebra", "z."),
181
- skill("alpha", "a."),
182
- skill("hidden", "h.", true),
183
- ],
177
+ [skill("zebra", "z."), skill("alpha", "a."), skill("hidden", "h.", true)],
184
178
  );
185
179
  expect(out).toContain("Skills: alpha, zebra.");
186
180
  expect(out).not.toContain("hidden");
187
181
  });
188
182
 
189
183
  test("omits the skills line when no invocable skills", () => {
190
- const out = buildOrientation([tool("read", "builtin")], [skill("x", ".", true)]);
184
+ const out = buildOrientation(
185
+ [tool("read", "builtin")],
186
+ [skill("x", ".", true)],
187
+ );
191
188
  expect(out).not.toContain("Skills:");
192
189
  });
193
190
 
@@ -32,7 +32,9 @@ export const CAPABILITY_REMINDER =
32
32
  "All tools are always callable via function definitions.";
33
33
 
34
34
  /** Count model-invocable skills (excludes user-only /skill:name entries). */
35
- export function countInvocableSkills(skills: LoadedSkill[] | undefined): number {
35
+ export function countInvocableSkills(
36
+ skills: LoadedSkill[] | undefined,
37
+ ): number {
36
38
  return (skills ?? []).filter((s) => !s.disableModelInvocation).length;
37
39
  }
38
40
 
@@ -8,8 +8,8 @@
8
8
  */
9
9
 
10
10
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
11
- import registerToolsNudge from "./tools.ts";
12
11
  import registerCapabilityNudge from "./capability.ts";
12
+ import registerToolsNudge from "./tools.ts";
13
13
 
14
14
  export default function registerNudges(pi: ExtensionAPI): void {
15
15
  registerToolsNudge(pi);
@@ -114,15 +114,23 @@ describe("classifyCompound", () => {
114
114
  });
115
115
 
116
116
  describe("nudgeReason", () => {
117
- test("active tool: point straight at it, no toolbox", () => {
118
- const msg = nudgeReason("Searching file contents via bash grep/rg.", "grep", true);
117
+ test("active tool: point straight at it, no toolbox", () => {
118
+ const msg = nudgeReason(
119
+ "Searching file contents via bash grep/rg.",
120
+ "grep",
121
+ true,
122
+ );
119
123
  expect(msg).toContain("Use `grep` instead");
120
124
  expect(msg).toContain("function definitions");
121
125
  expect(msg).not.toContain("toolbox");
122
126
  });
123
127
 
124
- test("gated tool: route through toolbox enable, not a direct call", () => {
125
- const msg = nudgeReason("Listing a directory via bash ls/tree.", "ls", false);
128
+ test("gated tool: route through toolbox enable, not a direct call", () => {
129
+ const msg = nudgeReason(
130
+ "Listing a directory via bash ls/tree.",
131
+ "ls",
132
+ false,
133
+ );
126
134
  expect(msg).toContain('toolbox(action:"enable", name:"ls")');
127
135
  expect(msg).toContain("prompt-hidden");
128
136
  expect(msg).toContain("function definitions");
@@ -131,13 +139,17 @@ describe("nudgeReason", () => {
131
139
  });
132
140
 
133
141
  test("gated find tool names itself in the enable hint", () => {
134
- expect(nudgeReason("Locating files via bash find/fd.", "find", false)).toContain(
135
- 'toolbox(action:"enable", name:"find")',
136
- );
142
+ expect(
143
+ nudgeReason("Locating files via bash find/fd.", "find", false),
144
+ ).toContain('toolbox(action:"enable", name:"find")');
137
145
  });
138
146
 
139
- test("is a single short line — no inventory dump, no newlines", () => {
140
- const msg = nudgeReason("Listing a directory via bash ls/tree.", "ls", false);
147
+ test("is a single short line — no inventory dump, no newlines", () => {
148
+ const msg = nudgeReason(
149
+ "Listing a directory via bash ls/tree.",
150
+ "ls",
151
+ false,
152
+ );
141
153
  expect(msg).not.toContain("\n");
142
154
  expect(msg).not.toContain("Available tools");
143
155
  expect(msg.length).toBeLessThan(400);
@@ -28,7 +28,6 @@ import {
28
28
  isToolCallEventType,
29
29
  } from "@earendil-works/pi-coding-agent";
30
30
 
31
-
32
31
  /** Categories that map a raw shell command to a dedicated Pi tool. */
33
32
  type Category = "read" | "ls" | "grep" | "find" | "edit";
34
33
 
@@ -116,7 +115,6 @@ export function splitSegments(command: string): Segment[] {
116
115
  const re = /(\|\||&&|[|;&\n])/g;
117
116
  let last = 0;
118
117
  let m: RegExpExecArray | null;
119
- // biome-ignore lint/suspicious/noAssignInExpressions: standard regex walk
120
118
  while ((m = re.exec(command)) !== null) {
121
119
  out.push({ text: command.slice(last, m.index), followedBy: m[1] });
122
120
  last = m.index + m[1].length;