@xynogen/pix-core 0.1.7 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xynogen/pix-core",
3
- "version": "0.1.7",
3
+ "version": "0.2.1",
4
4
  "description": "Pi extension — core UI/UX bundle (welcome banner, footer, model picker, self-update)",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -42,8 +42,8 @@
42
42
  "access": "public"
43
43
  },
44
44
  "dependencies": {
45
- "@xynogen/pix-data": "^0.1.0",
46
- "@xynogen/pix-skills": "^0.1.1",
45
+ "@xynogen/pix-data": "*",
46
+ "@xynogen/pix-skills": "*",
47
47
  "typebox": "^1.1.38"
48
48
  },
49
49
  "peerDependencies": {
@@ -1,170 +1,32 @@
1
1
  /**
2
- * Diff Extension
2
+ * /diff — explain unstaged git diff with per-file +/- counts.
3
3
  *
4
- * Tracks files changed during the last agent run (git delta + tool-touched
5
- * paths from edit/write), and exposes /diff to list/open them.
6
- *
7
- * Subcommands:
8
- * /diff → interactive selector, opens choice in editor
9
- * /diff list → notify with the list of changed files
10
- * /diff clear → reset tracked set and re-baseline against git
11
- *
12
- * Editor: $PI_DIFF_EDITOR > $VISUAL > $EDITOR > zed > code > vim
4
+ * The agent runs `git status` + `git diff`, then replies with:
5
+ * 1. 1–2 sentence explanation of what changed
6
+ * 2. Per-file +/- line counts
7
+ * 3. Total +/- line count
13
8
  */
14
9
 
15
- import path from "node:path";
16
10
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
17
11
 
18
- const COMMAND = "diff";
12
+ const DIFF_PROMPT = `Run git status and inspect the unstaged git diff, then respond with only:
19
13
 
20
- function getStringPath(input: unknown): string | undefined {
21
- if (!input || typeof input !== "object" || !("path" in input))
22
- return undefined;
23
- const p = (input as { path?: unknown }).path;
24
- return typeof p === "string" ? p : undefined;
25
- }
14
+ 1. A short 1-2 sentence explanation of what changed and why it matters.
15
+ 2. A list of changed unstaged files with their +/- line counts.
16
+ 3. A total +/- line count at the bottom.
26
17
 
27
- function toAbs(cwd: string, p: string): string {
28
- return path.isAbsolute(p) ? path.normalize(p) : path.resolve(cwd, p);
29
- }
30
-
31
- function toRel(cwd: string, p: string): string {
32
- const r = path.relative(cwd, p);
33
- return r && !r.startsWith("..") && !path.isAbsolute(r) ? r : p;
34
- }
35
-
36
- function parseGitStatus(output: string, cwd: string): Set<string> {
37
- const files = new Set<string>();
38
- for (const line of output.split("\n")) {
39
- if (line.length < 4) continue;
40
- const raw = line.slice(3).trim();
41
- if (!raw) continue;
42
- const target = raw.includes(" -> ") ? raw.split(" -> ").at(-1) : raw;
43
- if (!target) continue;
44
- files.add(toAbs(cwd, target.replace(/^"|"$/g, "")));
45
- }
46
- return files;
47
- }
48
-
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
- );
58
- if (r.code !== 0) return new Set();
59
- return parseGitStatus(r.stdout, cwd);
60
- }
61
-
62
- function diff(current: Set<string>, baseline: Set<string>): Set<string> {
63
- return new Set([...current].filter((f) => !baseline.has(f)));
64
- }
65
-
66
- function pickEditor(): { cmd: string; args: (file: string) => string[] } {
67
- const env =
68
- process.env.PI_DIFF_EDITOR || process.env.VISUAL || process.env.EDITOR;
69
- if (env) {
70
- const parts = env.split(/\s+/);
71
- const cmd = parts[0];
72
- const rest = parts.slice(1);
73
- return { cmd, args: (f) => [...rest, f] };
74
- }
75
- return { cmd: "zed", args: (f) => ["-e", f] };
76
- }
18
+ Keep it concise. Use git commands to calculate the line counts. Base the summary on the actual diff, not only filenames. Do not include staged changes unless they also have unstaged modifications.`;
77
19
 
78
20
  export default function (pi: ExtensionAPI) {
79
- let baseline = new Set<string>();
80
- let changed = new Set<string>();
81
- let touched = new Set<string>();
82
-
83
- pi.on("agent_start", async (_event, ctx) => {
84
- touched = new Set();
85
- changed = new Set();
86
- baseline = await getGitChanged(pi, ctx.cwd);
87
- });
88
-
89
- pi.on("tool_result", (event, ctx) => {
90
- if (event.toolName !== "edit" && event.toolName !== "write") return;
91
- const p = getStringPath(event.input);
92
- if (!p) return;
93
- touched.add(toAbs(ctx.cwd, p));
94
- });
95
-
96
- pi.on("agent_end", async (_event, ctx) => {
97
- const now = await getGitChanged(pi, ctx.cwd);
98
- changed = new Set([...diff(now, baseline), ...touched]);
99
- if (changed.size > 0) {
100
- ctx.ui.notify(
101
- `📝 ${changed.size} changed file(s). Run /${COMMAND} to view/open.`,
102
- "info",
103
- );
104
- }
105
- });
106
-
107
- pi.registerCommand(COMMAND, {
108
- description:
109
- "Show files changed by the last agent run and open one in your editor",
110
- handler: async (args, ctx) => {
111
- await ctx.waitForIdle();
112
- const arg = (args ?? "").trim();
113
-
114
- if (arg === "clear") {
115
- changed = new Set();
116
- touched = new Set();
117
- baseline = await getGitChanged(pi, ctx.cwd);
118
- ctx.ui.notify("Cleared changed file list", "info");
21
+ pi.registerCommand("diff", {
22
+ description: "Explain unstaged git diff with per-file +/- counts",
23
+ handler: async (_args, ctx) => {
24
+ if (!ctx.isIdle()) {
25
+ pi.sendUserMessage(DIFF_PROMPT, { deliverAs: "followUp" });
26
+ ctx.ui.notify("Queued /diff after the current turn finishes.", "info");
119
27
  return;
120
28
  }
121
-
122
- const files = [...changed].sort((a, b) =>
123
- toRel(ctx.cwd, a).localeCompare(toRel(ctx.cwd, b)),
124
- );
125
- if (files.length === 0) {
126
- ctx.ui.notify(
127
- "No changed files tracked from the last agent run",
128
- "info",
129
- );
130
- return;
131
- }
132
-
133
- if (arg === "list") {
134
- ctx.ui.notify(
135
- `Changed files:\n${files.map((f) => `- ${toRel(ctx.cwd, f)}`).join("\n")}`,
136
- "info",
137
- );
138
- return;
139
- }
140
-
141
- if (arg) {
142
- ctx.ui.notify(
143
- `Unknown /${COMMAND} argument: ${arg}. Try /${COMMAND}, /${COMMAND} list, /${COMMAND} clear.`,
144
- "warning",
145
- );
146
- return;
147
- }
148
-
149
- const labels = files.map((f) => toRel(ctx.cwd, f));
150
- const selected = await ctx.ui.select("Open changed file", labels);
151
- if (!selected) return;
152
-
153
- const file = files[labels.indexOf(selected)];
154
- if (!file) return;
155
-
156
- const ed = pickEditor();
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
- );
29
+ pi.sendUserMessage(DIFF_PROMPT);
168
30
  },
169
31
  });
170
32
  }
@@ -6,7 +6,7 @@
6
6
  import { describe, expect, it } from "bun:test";
7
7
 
8
8
  describe("merged pix-tools commands", () => {
9
- for (const name of ["lg", "yeet", "copy-all", "diff"]) {
9
+ for (const name of ["diff"]) {
10
10
  it(`${name} exports a register function`, async () => {
11
11
  const mod = await import(`./${name}/${name}.ts`);
12
12
  expect(mod.default).toBeFunction();
package/src/index.ts CHANGED
@@ -4,7 +4,7 @@
4
4
  * Layout (grouped by concern):
5
5
  * - ui/ — welcome (π banner + health checks), footer (status bar)
6
6
  * - commands/ — models (/models picker), update (/update self-update),
7
- * lg (/lg), yeet (/yeet), copy-all (/copy-all), diff (/diff)
7
+ * diff (/diff)
8
8
  * - tool/ — todo (durable execution checklist),
9
9
  * toolbox (/toolbox command — user toggles tools on/off),
10
10
  * lazy (lazy tool exposure — gates schemas out of the prompt)
@@ -19,12 +19,9 @@ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
19
19
  import registerSkillLoader from "@xynogen/pix-skills";
20
20
  import registerAgentSop from "./commands/agent-sop/agent-sop.ts";
21
21
  import registerClear from "./commands/clear/clear.ts";
22
- import registerCopyAll from "./commands/copy-all/copy-all.ts";
23
22
  import registerDiff from "./commands/diff/diff.ts";
24
- import registerLg from "./commands/lg/lg.ts";
25
23
  import registerModels from "./commands/models/models.ts";
26
24
  import registerUpdate from "./commands/update/update.ts";
27
- import registerYeet from "./commands/yeet/yeet.ts";
28
25
  import registerNudges from "./nudge/index.ts";
29
26
  import registerAsk from "./tool/ask/index.ts";
30
27
  import registerTodo from "./tool/todo/todo.ts";
@@ -41,9 +38,6 @@ export default function (pi: ExtensionAPI): void {
41
38
  registerDiagnostics(pi);
42
39
  registerModels(pi);
43
40
  registerUpdate(pi);
44
- registerLg(pi);
45
- registerYeet(pi);
46
- registerCopyAll(pi);
47
41
  registerDiff(pi);
48
42
  registerClear(pi);
49
43
  registerTodo(pi);
@@ -100,7 +100,7 @@ describe("partitionTools", () => {
100
100
  test("without an active set, every tool counts as active (gated 0)", () => {
101
101
  const { active, gated } = partitionTools([
102
102
  tool("read", "builtin"),
103
- tool("ast_grep_search", "builtin"),
103
+ tool("find", "builtin"),
104
104
  ]);
105
105
  expect(active).toBe(2);
106
106
  expect(gated).toBe(0);
@@ -111,8 +111,8 @@ describe("partitionTools", () => {
111
111
  [
112
112
  tool("read", "builtin"),
113
113
  tool("grep", "builtin"),
114
- tool("ast_grep_search", "builtin"),
115
- tool("ctx_search", "builtin"),
114
+ tool("find", "builtin"),
115
+ tool("ls", "builtin"),
116
116
  ],
117
117
  ["read", "grep"],
118
118
  );
@@ -146,8 +146,8 @@ describe("buildOrientation", () => {
146
146
  [
147
147
  tool("read", "builtin"),
148
148
  tool("grep", "builtin"),
149
- tool("ast_grep_search", "builtin"),
150
- tool("ctx_search", "builtin"),
149
+ tool("find", "builtin"),
150
+ tool("ls", "builtin"),
151
151
  ],
152
152
  [],
153
153
  ["read", "grep"], // active set: 2 gated out
@@ -158,7 +158,7 @@ describe("buildOrientation", () => {
158
158
 
159
159
  test("singular phrasing when exactly one tool is gated", () => {
160
160
  const out = buildOrientation(
161
- [tool("read", "builtin"), tool("ast_grep_search", "builtin")],
161
+ [tool("read", "builtin"), tool("find", "builtin")],
162
162
  [],
163
163
  ["read"],
164
164
  );
@@ -177,7 +177,7 @@ describe("buildOrientation", () => {
177
177
 
178
178
  test("no gate line when active set is unknown", () => {
179
179
  const out = buildOrientation(
180
- [tool("read", "builtin"), tool("ast_grep_search", "builtin")],
180
+ [tool("read", "builtin"), tool("find", "builtin")],
181
181
  [],
182
182
  );
183
183
  expect(out).not.toContain("gated out of the prompt");
@@ -183,7 +183,9 @@ afterAll(() => {
183
183
  delete process.env.PI_CODING_AGENT_DIR;
184
184
  try {
185
185
  rmSync(tmpAgentDir, { recursive: true });
186
- } catch {}
186
+ } catch {
187
+ // temp dir may already be gone — safe to ignore
188
+ }
187
189
  });
188
190
 
189
191
  function makeHost(toolNames: string[]) {
@@ -245,7 +247,7 @@ function makeCtx() {
245
247
  }
246
248
 
247
249
  describe("/toolbox command", () => {
248
- const ALL = ["read", "write", "bash", "grep", "ast_grep_search"];
250
+ const ALL = ["read", "write", "bash", "grep", "find"];
249
251
 
250
252
  async function boot() {
251
253
  const host = makeHost(ALL);
@@ -267,7 +269,7 @@ describe("/toolbox command", () => {
267
269
  expect(notes.length).toBe(1);
268
270
  // only non-core tools shown, all start active
269
271
  expect(notes[0].text).toContain("✓ active grep");
270
- expect(notes[0].text).toContain("✓ active ast_grep_search");
272
+ expect(notes[0].text).toContain("✓ active find");
271
273
  // core tools excluded from toolbox
272
274
  expect(notes[0].text).not.toContain(" read");
273
275
  expect(notes[0].text).not.toContain(" bash");
@@ -278,15 +280,15 @@ describe("/toolbox command", () => {
278
280
  const { ctx, notes } = makeCtx();
279
281
  await host.command("toolbox")?.handler("list", ctx);
280
282
  expect(notes[0].text).toContain("grep");
281
- expect(notes[0].text).toContain("ast_grep_search");
283
+ expect(notes[0].text).toContain("find");
282
284
  expect(notes[0].text).not.toContain(" bash");
283
285
  });
284
286
 
285
287
  test("/toolbox list <query> filters", async () => {
286
288
  const host = await boot();
287
289
  const { ctx, notes } = makeCtx();
288
- await host.command("toolbox")?.handler("list ast", ctx);
289
- expect(notes[0].text).toContain("ast_grep_search");
290
+ await host.command("toolbox")?.handler("list fin", ctx);
291
+ expect(notes[0].text).toContain("find");
290
292
  expect(notes[0].text).not.toContain("✓ active read");
291
293
  });
292
294
 
@@ -1,104 +0,0 @@
1
- /**
2
- * Copy-All Extension
3
- *
4
- * /copy-all → copies the entire user+assistant conversation in the current
5
- * branch to the system clipboard. Uses pbcopy on macOS, xclip/xsel/wl-copy
6
- * on Linux, clip.exe on WSL/Windows.
7
- */
8
-
9
- import { spawn } from "node:child_process";
10
- import { platform } from "node:os";
11
- import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
12
-
13
- function textFromContent(content: unknown): string {
14
- if (typeof content === "string") return content;
15
- if (!Array.isArray(content)) return "";
16
- return content
17
- .map((block) => {
18
- if (!block || typeof block !== "object" || !("type" in block)) return "";
19
- if (
20
- block.type === "text" &&
21
- "text" in block &&
22
- typeof block.text === "string"
23
- )
24
- return block.text;
25
- if (block.type === "image") return "[image]";
26
- return "";
27
- })
28
- .filter(Boolean)
29
- .join("\n");
30
- }
31
-
32
- function pickClipboardCmd(): { cmd: string; args: string[] } | undefined {
33
- const p = platform();
34
- if (p === "darwin") return { cmd: "pbcopy", args: [] };
35
- if (p === "win32") return { cmd: "clip.exe", args: [] };
36
- // linux / wsl
37
- if (process.env.WSL_DISTRO_NAME) return { cmd: "clip.exe", args: [] };
38
- if (process.env.WAYLAND_DISPLAY) return { cmd: "wl-copy", args: [] };
39
- return { cmd: "xclip", args: ["-selection", "clipboard"] };
40
- }
41
-
42
- function copyToClipboard(text: string): Promise<void> {
43
- return new Promise((resolve, reject) => {
44
- const c = pickClipboardCmd();
45
- if (!c) {
46
- reject(new Error("No clipboard utility detected"));
47
- return;
48
- }
49
- const child = spawn(c.cmd, c.args);
50
- let stderr = "";
51
- child.stderr.on("data", (chunk) => {
52
- stderr += String(chunk);
53
- });
54
- child.on("error", reject);
55
- child.on("close", (code) => {
56
- if (code === 0) resolve();
57
- else
58
- reject(new Error(stderr.trim() || `${c.cmd} exited with code ${code}`));
59
- });
60
- child.stdin.end(text);
61
- });
62
- }
63
-
64
- export default function (pi: ExtensionAPI) {
65
- pi.registerCommand("copy-all", {
66
- description:
67
- "Copy all user/assistant messages in this thread to the clipboard",
68
- handler: async (_args, ctx) => {
69
- await ctx.waitForIdle();
70
-
71
- const messages = ctx.sessionManager
72
- .getBranch()
73
- .filter((entry) => entry.type === "message")
74
- .map((entry) => entry.message)
75
- .filter((m) => m.role === "user" || m.role === "assistant");
76
-
77
- const text = messages
78
- .map((m) => {
79
- const c = textFromContent(m.content).trim();
80
- return `${m.role.toUpperCase()}:\n${c}`;
81
- })
82
- .filter((s) => !s.endsWith(":\n"))
83
- .join("\n\n---\n\n");
84
-
85
- if (!text) {
86
- ctx.ui.notify("No user or assistant messages to copy", "info");
87
- return;
88
- }
89
-
90
- try {
91
- await copyToClipboard(text);
92
- ctx.ui.notify(
93
- `📋 Copied ${messages.length} messages to clipboard`,
94
- "info",
95
- );
96
- } catch (err) {
97
- ctx.ui.notify(
98
- `Clipboard copy failed: ${err instanceof Error ? err.message : String(err)}`,
99
- "error",
100
- );
101
- }
102
- },
103
- });
104
- }
@@ -1,32 +0,0 @@
1
- /**
2
- * /lg — summarize unstaged git changes with per-file +/- counts.
3
- *
4
- * The agent runs `git status` + line counts and replies with:
5
- * 1. 1–2 sentence summary of unstaged changes
6
- * 2. Per-file +/- line counts
7
- * 3. Total +/- line count
8
- */
9
-
10
- import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
11
-
12
- const LG_PROMPT = `Run git status, inspect what has changed, then respond with only:
13
-
14
- 1. A short 1-2 sentence summary of the unstaged changes.
15
- 2. A list of changed unstaged files with their +/- line counts.
16
- 3. A total +/- line count at the bottom.
17
-
18
- Keep it concise. Use git commands to calculate the line counts; do not include staged changes unless they also have unstaged modifications.`;
19
-
20
- export default function (pi: ExtensionAPI) {
21
- pi.registerCommand("lg", {
22
- description: "Summarize unstaged git changes with per-file +/- counts",
23
- handler: async (_args, ctx) => {
24
- if (!ctx.isIdle()) {
25
- pi.sendUserMessage(LG_PROMPT, { deliverAs: "followUp" });
26
- ctx.ui.notify("Queued /lg after the current turn finishes.", "info");
27
- return;
28
- }
29
- pi.sendUserMessage(LG_PROMPT);
30
- },
31
- });
32
- }
@@ -1,29 +0,0 @@
1
- import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
-
3
- const YEET_PROMPT = `Commit the current repository changes.
4
-
5
- Steps:
6
- 1. Add all unstaged changes with \`git add -A\`.
7
- 2. Inspect the staged changes and write a concise commit message that accurately summarizes them.
8
- 3. Commit the changes with that message.
9
- - If there are no staged changes, output "Nothing to commit" and stop.
10
-
11
- Keep the commit message concise. Do not push.`;
12
-
13
- export default function (pi: ExtensionAPI) {
14
- pi.registerCommand("yeet", {
15
- description: "Add and commit current repo changes (no push)",
16
- handler: async (args, ctx) => {
17
- const prompt = args?.trim()
18
- ? `${YEET_PROMPT}\n\nAdditional instructions from the user:\n${args.trim()}`
19
- : YEET_PROMPT;
20
-
21
- if (ctx.isIdle()) {
22
- pi.sendUserMessage(prompt);
23
- } else {
24
- pi.sendUserMessage(prompt, { deliverAs: "followUp" });
25
- ctx.ui.notify("Queued /yeet as a follow-up", "info");
26
- }
27
- },
28
- });
29
- }