@xynogen/pix-core 0.1.6 → 0.2.0
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 +1 -1
- package/src/commands/diff/diff.ts +17 -155
- package/src/commands/models/models.ts +5 -0
- package/src/commands/models/patch-builtin.test.ts +66 -0
- package/src/commands/models/patch-builtin.ts +60 -0
- package/src/commands/tools.test.ts +1 -1
- package/src/index.ts +1 -7
- package/src/commands/copy-all/copy-all.ts +0 -104
- package/src/commands/lg/lg.ts +0 -32
- package/src/commands/yeet/yeet.ts +0 -29
package/package.json
CHANGED
|
@@ -1,170 +1,32 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* /diff — explain unstaged git diff with per-file +/- counts.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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
|
|
12
|
+
const DIFF_PROMPT = `Run git status and inspect the unstaged git diff, then respond with only:
|
|
19
13
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
}
|
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
visibleWidth,
|
|
25
25
|
} from "@earendil-works/pi-tui";
|
|
26
26
|
import { lookupBenchmark, lookupModelsDev } from "../../lib/data";
|
|
27
|
+
import { patchOutBuiltinModelCommand } from "./patch-builtin";
|
|
27
28
|
|
|
28
29
|
// ─── Pure logic (exported for tests) ─────────────────────────────────────────
|
|
29
30
|
|
|
@@ -352,6 +353,10 @@ export async function showEnhancedPicker(
|
|
|
352
353
|
}
|
|
353
354
|
|
|
354
355
|
export default function modelPickerExtension(pi: ExtensionAPI) {
|
|
356
|
+
// Remove Pi's built-in /model so only the enhanced /models picker remains.
|
|
357
|
+
// Self-healing: re-applies on every load, so a Pi upgrade can't restore it.
|
|
358
|
+
patchOutBuiltinModelCommand();
|
|
359
|
+
|
|
355
360
|
const handler = async (_args: unknown, ctx: ExtensionContext) => {
|
|
356
361
|
await showEnhancedPicker(pi, ctx);
|
|
357
362
|
};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import { mkdtempSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
|
|
6
|
+
// Pure replacement tested in isolation (the exported fn resolves the host
|
|
7
|
+
// package, which isn't present in the test sandbox).
|
|
8
|
+
const MODEL_COMMAND_LINE =
|
|
9
|
+
'{ name: "model", description: "Select model (opens selector UI)" },';
|
|
10
|
+
|
|
11
|
+
function escapeRegExp(text: string): string {
|
|
12
|
+
return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function patchSource(source: string): string {
|
|
16
|
+
if (!source.includes(MODEL_COMMAND_LINE)) return source;
|
|
17
|
+
return source.replace(
|
|
18
|
+
new RegExp(`[ \\t]*${escapeRegExp(MODEL_COMMAND_LINE)}\\n?`),
|
|
19
|
+
"",
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const UNPATCHED = `export const BUILTIN_SLASH_COMMANDS = [
|
|
24
|
+
{ name: "settings", description: "Open settings menu" },
|
|
25
|
+
{ name: "model", description: "Select model (opens selector UI)" },
|
|
26
|
+
{ name: "login", description: "Configure provider authentication" },
|
|
27
|
+
];
|
|
28
|
+
`;
|
|
29
|
+
|
|
30
|
+
describe("patch-builtin /model removal", () => {
|
|
31
|
+
it("removes the built-in /model line and keeps neighbors", () => {
|
|
32
|
+
const out = patchSource(UNPATCHED);
|
|
33
|
+
expect(out).not.toContain('name: "model"');
|
|
34
|
+
expect(out).toContain('name: "settings"');
|
|
35
|
+
expect(out).toContain('name: "login"');
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("is idempotent — second pass is a no-op", () => {
|
|
39
|
+
const once = patchSource(UNPATCHED);
|
|
40
|
+
const twice = patchSource(once);
|
|
41
|
+
expect(twice).toBe(once);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("leaves an already-clean file untouched", () => {
|
|
45
|
+
const clean = `export const X = [\n { name: "login" },\n];\n`;
|
|
46
|
+
expect(patchSource(clean)).toBe(clean);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("does not strip the plural /models entry", () => {
|
|
50
|
+
const withPlural = `[
|
|
51
|
+
{ name: "models", description: "Enhanced picker" },
|
|
52
|
+
{ name: "model", description: "Select model (opens selector UI)" },
|
|
53
|
+
]`;
|
|
54
|
+
const out = patchSource(withPlural);
|
|
55
|
+
expect(out).toContain('name: "models"');
|
|
56
|
+
expect(out).not.toContain('{ name: "model", description');
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("round-trips through disk", () => {
|
|
60
|
+
const dir = mkdtempSync(join(tmpdir(), "pix-patch-"));
|
|
61
|
+
const file = join(dir, "slash-commands.js");
|
|
62
|
+
writeFileSync(file, UNPATCHED, "utf8");
|
|
63
|
+
writeFileSync(file, patchSource(readFileSync(file, "utf8")), "utf8");
|
|
64
|
+
expect(readFileSync(file, "utf8")).not.toContain('name: "model"');
|
|
65
|
+
});
|
|
66
|
+
});
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* patch-builtin.ts — strip Pi's built-in /model slash command at load time.
|
|
3
|
+
*
|
|
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
|
|
6
|
+
* self-healing across Pi upgrades, so no manual repatch is ever needed.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
10
|
+
import { createRequire } from "node:module";
|
|
11
|
+
import { dirname, resolve } from "node:path";
|
|
12
|
+
|
|
13
|
+
const HOST_PACKAGE = "@earendil-works/pi-coding-agent";
|
|
14
|
+
const MODEL_COMMAND_LINE =
|
|
15
|
+
'{ name: "model", description: "Select model (opens selector UI)" },';
|
|
16
|
+
|
|
17
|
+
/** Locate the host's compiled slash-commands.js, or null if it can't be found. */
|
|
18
|
+
function findSlashCommandsFile(): string | null {
|
|
19
|
+
try {
|
|
20
|
+
const require = createRequire(import.meta.url);
|
|
21
|
+
const entry = require.resolve(HOST_PACKAGE);
|
|
22
|
+
return resolve(dirname(entry), "core", "slash-commands.js");
|
|
23
|
+
} catch {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Remove the built-in /model command line from Pi's slash-commands.js.
|
|
30
|
+
* Idempotent: returns silently if the file is missing or already patched.
|
|
31
|
+
*/
|
|
32
|
+
export function patchOutBuiltinModelCommand(): void {
|
|
33
|
+
const file = findSlashCommandsFile();
|
|
34
|
+
if (!file) return;
|
|
35
|
+
|
|
36
|
+
let source: string;
|
|
37
|
+
try {
|
|
38
|
+
source = readFileSync(file, "utf8");
|
|
39
|
+
} catch {
|
|
40
|
+
return; // file not present (different Pi layout) — nothing to do
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (!source.includes(MODEL_COMMAND_LINE)) return; // already patched
|
|
44
|
+
|
|
45
|
+
const patched = source.replace(
|
|
46
|
+
new RegExp(`[ \\t]*${escapeRegExp(MODEL_COMMAND_LINE)}\\n?`),
|
|
47
|
+
"",
|
|
48
|
+
);
|
|
49
|
+
if (patched === source) return;
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
writeFileSync(file, patched, "utf8");
|
|
53
|
+
} catch {
|
|
54
|
+
// Read-only install — leave /model in place rather than crash.
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function escapeRegExp(text: string): string {
|
|
59
|
+
return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
60
|
+
}
|
|
@@ -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 ["
|
|
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
|
-
*
|
|
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);
|
|
@@ -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
|
-
}
|
package/src/commands/lg/lg.ts
DELETED
|
@@ -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
|
-
}
|