@xynogen/pix-core 0.2.3 → 0.3.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 +11 -17
- package/skills/ask-user/SKILL.md +0 -48
- package/src/commands/agent-sop/agent-sop.ts +0 -58
- package/src/commands/clear/clear.ts +0 -32
- package/src/commands/diff/diff.ts +0 -32
- package/src/commands/models/models.test.ts +0 -95
- package/src/commands/models/models.ts +0 -367
- package/src/commands/models/patch-builtin.test.ts +0 -66
- package/src/commands/models/patch-builtin.ts +0 -120
- package/src/commands/tools.test.ts +0 -15
- package/src/commands/update/update.test.ts +0 -112
- package/src/commands/update/update.ts +0 -271
- package/src/index.ts +0 -45
- package/src/lib/data.ts +0 -33
- package/src/nudge/capability.test.ts +0 -258
- package/src/nudge/capability.ts +0 -189
- package/src/nudge/index.ts +0 -17
- package/src/nudge/tools.test.ts +0 -157
- package/src/nudge/tools.ts +0 -212
- package/src/tool/ask/ask.test.ts +0 -243
- package/src/tool/ask/components.ts +0 -55
- package/src/tool/ask/helpers.ts +0 -77
- package/src/tool/ask/index.ts +0 -130
- package/src/tool/ask/questionnaire.ts +0 -693
- package/src/tool/ask/rpc.ts +0 -84
- package/src/tool/ask/schema.ts +0 -69
- package/src/tool/ask/single-select-layout.test.ts +0 -124
- package/src/tool/ask/single-select-layout.ts +0 -237
- package/src/tool/ask/types.ts +0 -17
- package/src/tool/todo/todo.test.ts +0 -646
- package/src/tool/todo/todo.ts +0 -218
- package/src/tool/toolbox/toolbox.test.ts +0 -314
- package/src/tool/toolbox/toolbox.ts +0 -570
- package/src/ui/diagnostics.ts +0 -145
- package/src/ui/footer.ts +0 -512
- package/src/ui/welcome.test.ts +0 -124
- package/src/ui/welcome.ts +0 -369
|
@@ -1,66 +0,0 @@
|
|
|
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
|
-
});
|
|
@@ -1,120 +0,0 @@
|
|
|
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
|
-
* 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).
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
import { execSync } from "node:child_process";
|
|
17
|
-
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
18
|
-
import { createRequire } from "node:module";
|
|
19
|
-
import { homedir } from "node:os";
|
|
20
|
-
import { dirname, join, resolve } from "node:path";
|
|
21
|
-
|
|
22
|
-
const MODEL_COMMAND_LINE =
|
|
23
|
-
'{ name: "model", description: "Select model (opens selector UI)" },';
|
|
24
|
-
|
|
25
|
-
/** Candidate slash-commands.js paths, most-specific first. */
|
|
26
|
-
function candidatePaths(): string[] {
|
|
27
|
-
const paths: string[] = [];
|
|
28
|
-
|
|
29
|
-
// 1. Resolve via the running `pi` binary → its realpath gives the dist dir.
|
|
30
|
-
try {
|
|
31
|
-
const piReal = execSync("realpath $(which pi)", {
|
|
32
|
-
encoding: "utf8",
|
|
33
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
34
|
-
}).trim();
|
|
35
|
-
if (piReal) {
|
|
36
|
-
// piReal = /.../pi-coding-agent/dist/cli.js → dist/ → ../dist/core/
|
|
37
|
-
const distCore = resolve(dirname(piReal), "core");
|
|
38
|
-
paths.push(join(distCore, "slash-commands.js"));
|
|
39
|
-
}
|
|
40
|
-
} catch {
|
|
41
|
-
// `pi` not on PATH or `which`/`realpath` unavailable — skip
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
// 2. Well-known global install locations.
|
|
45
|
-
const home = homedir();
|
|
46
|
-
const globalRoots = [
|
|
47
|
-
join(home, ".bun", "install", "global", "node_modules"),
|
|
48
|
-
join(home, ".npm-global", "lib", "node_modules"),
|
|
49
|
-
"/usr/local/lib/node_modules",
|
|
50
|
-
"/usr/lib/node_modules",
|
|
51
|
-
];
|
|
52
|
-
for (const root of globalRoots) {
|
|
53
|
-
paths.push(
|
|
54
|
-
join(
|
|
55
|
-
root,
|
|
56
|
-
"@earendil-works",
|
|
57
|
-
"pi-coding-agent",
|
|
58
|
-
"dist",
|
|
59
|
-
"core",
|
|
60
|
-
"slash-commands.js",
|
|
61
|
-
),
|
|
62
|
-
);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
// 3. Fallback: createRequire from this file (works when extension is co-installed).
|
|
66
|
-
try {
|
|
67
|
-
const require = createRequire(import.meta.url);
|
|
68
|
-
const entry = require.resolve("@earendil-works/pi-coding-agent");
|
|
69
|
-
paths.push(resolve(dirname(entry), "core", "slash-commands.js"));
|
|
70
|
-
} catch {
|
|
71
|
-
// local resolution failed — skip
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
return paths;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/** Locate the host's compiled slash-commands.js, or null if not found. */
|
|
78
|
-
function findSlashCommandsFile(): string | null {
|
|
79
|
-
for (const p of candidatePaths()) {
|
|
80
|
-
if (existsSync(p)) return p;
|
|
81
|
-
}
|
|
82
|
-
return null;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* Remove the built-in /model command line from Pi's slash-commands.js.
|
|
87
|
-
* Idempotent: returns silently if the file is missing or already patched.
|
|
88
|
-
*/
|
|
89
|
-
export function patchOutBuiltinModelCommand(): void {
|
|
90
|
-
const file = findSlashCommandsFile();
|
|
91
|
-
if (!file) return;
|
|
92
|
-
|
|
93
|
-
let source: string;
|
|
94
|
-
try {
|
|
95
|
-
source = readFileSync(file, "utf8");
|
|
96
|
-
} catch {
|
|
97
|
-
return;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
if (!source.includes(MODEL_COMMAND_LINE)) return; // already patched
|
|
101
|
-
|
|
102
|
-
const patched = source.replace(
|
|
103
|
-
new RegExp(`[ \\t]*${escapeRegExp(MODEL_COMMAND_LINE)}\\n?`),
|
|
104
|
-
"",
|
|
105
|
-
);
|
|
106
|
-
if (patched === source) return;
|
|
107
|
-
|
|
108
|
-
try {
|
|
109
|
-
writeFileSync(file, patched, "utf8");
|
|
110
|
-
} catch {
|
|
111
|
-
// Read-only install — leave /model in place rather than crash.
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
function escapeRegExp(text: string): string {
|
|
116
|
-
return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
// Export for tests
|
|
120
|
-
export { candidatePaths, findSlashCommandsFile };
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Smoke tests for the command extensions merged from pix-tools.
|
|
3
|
-
* Each default export must be a registrable (pi) => void function.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { describe, expect, it } from "bun:test";
|
|
7
|
-
|
|
8
|
-
describe("merged pix-tools commands", () => {
|
|
9
|
-
for (const name of ["diff"]) {
|
|
10
|
-
it(`${name} exports a register function`, async () => {
|
|
11
|
-
const mod = await import(`./${name}/${name}.ts`);
|
|
12
|
-
expect(mod.default).toBeFunction();
|
|
13
|
-
});
|
|
14
|
-
}
|
|
15
|
-
});
|
|
@@ -1,112 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "bun:test";
|
|
2
|
-
import {
|
|
3
|
-
commandFor,
|
|
4
|
-
formatUpdateSummary,
|
|
5
|
-
type InstallMethod,
|
|
6
|
-
isTransient,
|
|
7
|
-
PACKAGE_NAME,
|
|
8
|
-
} from "./update.ts";
|
|
9
|
-
|
|
10
|
-
describe("isTransient", () => {
|
|
11
|
-
it("matches network errors", () => {
|
|
12
|
-
expect(isTransient("ETIMEDOUT")).toBe(true);
|
|
13
|
-
expect(isTransient("ECONNRESET")).toBe(true);
|
|
14
|
-
expect(isTransient("ECONNREFUSED")).toBe(true);
|
|
15
|
-
expect(isTransient("socket hang up")).toBe(true);
|
|
16
|
-
expect(isTransient("network error occurred")).toBe(true);
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
it("matches HTTP status codes", () => {
|
|
20
|
-
expect(isTransient("Error 429: Too many requests")).toBe(true);
|
|
21
|
-
expect(isTransient("502 Bad Gateway")).toBe(true);
|
|
22
|
-
expect(isTransient("503 Service Unavailable")).toBe(true);
|
|
23
|
-
expect(isTransient("504 Gateway Timeout")).toBe(true);
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
it("matches timeout/temporary", () => {
|
|
27
|
-
expect(isTransient("Request timeout after 30s")).toBe(true);
|
|
28
|
-
expect(isTransient("temporary failure")).toBe(true);
|
|
29
|
-
expect(isTransient("EAI_AGAIN")).toBe(true);
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
it("returns false for permanent errors", () => {
|
|
33
|
-
expect(isTransient("permission denied")).toBe(false);
|
|
34
|
-
expect(isTransient("command not found")).toBe(false);
|
|
35
|
-
expect(isTransient("syntax error")).toBe(false);
|
|
36
|
-
expect(isTransient("")).toBe(false);
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
it("is case-insensitive", () => {
|
|
40
|
-
expect(isTransient("NETWORK FAILURE")).toBe(true);
|
|
41
|
-
expect(isTransient("Timeout after 30s")).toBe(true);
|
|
42
|
-
});
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
describe("commandFor", () => {
|
|
46
|
-
it("returns correct command for each method", () => {
|
|
47
|
-
const methods: InstallMethod[] = ["vp", "bun", "npm", "brew"];
|
|
48
|
-
for (const m of methods) {
|
|
49
|
-
const spec = commandFor(m);
|
|
50
|
-
expect(spec).toBeDefined();
|
|
51
|
-
expect(spec?.command).toBeTruthy();
|
|
52
|
-
expect(spec?.label).toBeTruthy();
|
|
53
|
-
}
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
it("vp uses vp add -g", () => {
|
|
57
|
-
const spec = commandFor("vp")!;
|
|
58
|
-
expect(spec.command).toBe("vp");
|
|
59
|
-
expect(spec.args).toContain(`${PACKAGE_NAME}@latest`);
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
it("bun uses bun add -g", () => {
|
|
63
|
-
const spec = commandFor("bun")!;
|
|
64
|
-
expect(spec.command).toBe("bun");
|
|
65
|
-
expect(spec.args).toContain(`${PACKAGE_NAME}@latest`);
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
it("npm uses npm install -g", () => {
|
|
69
|
-
const spec = commandFor("npm")!;
|
|
70
|
-
expect(spec.command).toBe("npm");
|
|
71
|
-
expect(spec.args).toContain("-g");
|
|
72
|
-
expect(spec.args).toContain(`${PACKAGE_NAME}@latest`);
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
it("brew uses sh -lc", () => {
|
|
76
|
-
const spec = commandFor("brew")!;
|
|
77
|
-
expect(spec.command).toBe("/bin/sh");
|
|
78
|
-
expect(spec.label).toContain("brew upgrade");
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
it("native returns undefined", () => {
|
|
82
|
-
expect(commandFor("native")).toBeUndefined();
|
|
83
|
-
});
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
describe("formatUpdateSummary", () => {
|
|
87
|
-
it("shows updated message when version changed", () => {
|
|
88
|
-
const msg = formatUpdateSummary("0.75.0", "0.76.0", 1);
|
|
89
|
-
expect(msg).toContain("0.75.0 → 0.76.0");
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
it("shows up-to-date when version unchanged", () => {
|
|
93
|
-
const msg = formatUpdateSummary("0.76.0", "0.76.0", 1);
|
|
94
|
-
expect(msg).toContain("up to date");
|
|
95
|
-
expect(msg).toContain("0.76.0");
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
it("shows retry count when attempts > 1", () => {
|
|
99
|
-
const msg = formatUpdateSummary("0.75.0", "0.76.0", 3);
|
|
100
|
-
expect(msg).toContain("Retried 2 transient failure");
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
it("no retry mention when attempts = 1", () => {
|
|
104
|
-
const msg = formatUpdateSummary("0.75.0", "0.76.0", 1);
|
|
105
|
-
expect(msg).not.toContain("Retried");
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
it("handles unknown versions gracefully", () => {
|
|
109
|
-
const msg = formatUpdateSummary("unknown", "unknown", 1);
|
|
110
|
-
expect(msg).toContain("up to date");
|
|
111
|
-
});
|
|
112
|
-
});
|
|
@@ -1,271 +0,0 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
ExtensionAPI,
|
|
3
|
-
ExtensionCommandContext,
|
|
4
|
-
} from "@earendil-works/pi-coding-agent";
|
|
5
|
-
// ─── Pure logic (exported for tests) ─────────────────────────────────────────
|
|
6
|
-
|
|
7
|
-
export const PACKAGE_NAME = "@earendil-works/pi-coding-agent";
|
|
8
|
-
|
|
9
|
-
export const TRANSIENT_PATTERNS = [
|
|
10
|
-
/eai_again/i,
|
|
11
|
-
/etimedout/i,
|
|
12
|
-
/econnreset/i,
|
|
13
|
-
/econnrefused/i,
|
|
14
|
-
/socket hang up/i,
|
|
15
|
-
/network/i,
|
|
16
|
-
/timeout/i,
|
|
17
|
-
/temporar/i,
|
|
18
|
-
/too many requests/i,
|
|
19
|
-
/\b429\b/,
|
|
20
|
-
/\b502\b/,
|
|
21
|
-
/\b503\b/,
|
|
22
|
-
/\b504\b/,
|
|
23
|
-
];
|
|
24
|
-
|
|
25
|
-
export type InstallMethod = "vp" | "bun" | "npm" | "brew" | "native";
|
|
26
|
-
|
|
27
|
-
export type CommandSpec = {
|
|
28
|
-
command: string;
|
|
29
|
-
args: string[];
|
|
30
|
-
label: string;
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
export function isTransient(output: string): boolean {
|
|
34
|
-
return TRANSIENT_PATTERNS.some((pattern) => pattern.test(output));
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export function commandFor(method: InstallMethod): CommandSpec | undefined {
|
|
38
|
-
switch (method) {
|
|
39
|
-
case "vp":
|
|
40
|
-
return {
|
|
41
|
-
command: "vp",
|
|
42
|
-
args: ["add", "-g", `${PACKAGE_NAME}@latest`],
|
|
43
|
-
label: `vp add -g ${PACKAGE_NAME}@latest`,
|
|
44
|
-
};
|
|
45
|
-
case "bun":
|
|
46
|
-
return {
|
|
47
|
-
command: "bun",
|
|
48
|
-
args: ["add", "-g", `${PACKAGE_NAME}@latest`],
|
|
49
|
-
label: `bun add -g ${PACKAGE_NAME}@latest`,
|
|
50
|
-
};
|
|
51
|
-
case "npm":
|
|
52
|
-
return {
|
|
53
|
-
command: "npm",
|
|
54
|
-
args: ["install", "-g", `${PACKAGE_NAME}@latest`],
|
|
55
|
-
label: `npm install -g ${PACKAGE_NAME}@latest`,
|
|
56
|
-
};
|
|
57
|
-
case "brew":
|
|
58
|
-
return {
|
|
59
|
-
command: "/bin/sh",
|
|
60
|
-
args: ["-lc", "brew upgrade pi-coding-agent || brew upgrade pi"],
|
|
61
|
-
label: "brew upgrade pi-coding-agent || brew upgrade pi",
|
|
62
|
-
};
|
|
63
|
-
case "native":
|
|
64
|
-
return undefined;
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
export function formatUpdateSummary(
|
|
69
|
-
before: string,
|
|
70
|
-
after: string,
|
|
71
|
-
attempts: number,
|
|
72
|
-
): string {
|
|
73
|
-
const changed =
|
|
74
|
-
before !== after && before !== "unknown" && after !== "unknown";
|
|
75
|
-
const summary = changed
|
|
76
|
-
? `Pi updated: ${before} → ${after}`
|
|
77
|
-
: `Pi is up to date (${after}).`;
|
|
78
|
-
return attempts > 1
|
|
79
|
-
? `${summary} Retried ${attempts - 1} transient failure(s).`
|
|
80
|
-
: summary;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
async function resolveCommand(command: string, pi: ExtensionAPI) {
|
|
84
|
-
const result = await pi.exec(
|
|
85
|
-
"/bin/sh",
|
|
86
|
-
["-lc", `command -v ${command} || true`],
|
|
87
|
-
{ timeout: 10_000 },
|
|
88
|
-
);
|
|
89
|
-
return result.stdout.trim().split("\n")[0] || undefined;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
async function currentVersion(pi: ExtensionAPI) {
|
|
93
|
-
const result = await pi.exec("pi", ["--version"], { timeout: 10_000 });
|
|
94
|
-
return result.stdout.trim() || result.stderr.trim() || "unknown";
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
async function detectInstallMethod(pi: ExtensionAPI): Promise<InstallMethod> {
|
|
98
|
-
const piPath = await resolveCommand("pi", pi);
|
|
99
|
-
const realPiPath = piPath
|
|
100
|
-
? (
|
|
101
|
-
await pi.exec(
|
|
102
|
-
"/bin/sh",
|
|
103
|
-
["-lc", `realpath ${piPath} 2>/dev/null || printf %s ${piPath}`],
|
|
104
|
-
{ timeout: 10_000 },
|
|
105
|
-
)
|
|
106
|
-
).stdout.trim()
|
|
107
|
-
: undefined;
|
|
108
|
-
|
|
109
|
-
if (piPath?.includes("/.vite-plus/") || realPiPath?.includes("/.vite-plus/"))
|
|
110
|
-
return "vp";
|
|
111
|
-
if (piPath?.includes("/.bun/") || realPiPath?.includes("/.bun/"))
|
|
112
|
-
return "bun";
|
|
113
|
-
if (
|
|
114
|
-
piPath?.includes("/Homebrew/") ||
|
|
115
|
-
piPath?.includes("/homebrew/") ||
|
|
116
|
-
realPiPath?.includes("/Homebrew/") ||
|
|
117
|
-
realPiPath?.includes("/homebrew/")
|
|
118
|
-
)
|
|
119
|
-
return "brew";
|
|
120
|
-
|
|
121
|
-
if (piPath) {
|
|
122
|
-
const hasGlobalNpm = await pi.exec(
|
|
123
|
-
"/bin/sh",
|
|
124
|
-
[
|
|
125
|
-
"-lc",
|
|
126
|
-
`p=${piPath}; i=0; while [ $i -lt 5 ]; do d=$(dirname "$p"); [ -d "$d/node_modules/${PACKAGE_NAME}" ] && exit 0; p=$d; i=$((i+1)); done; exit 1`,
|
|
127
|
-
],
|
|
128
|
-
{ timeout: 10_000 },
|
|
129
|
-
);
|
|
130
|
-
if ((hasGlobalNpm.code ?? 1) === 0) return "npm";
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
if (await resolveCommand("vp", pi)) return "vp";
|
|
134
|
-
if (await resolveCommand("bun", pi)) return "bun";
|
|
135
|
-
if (await resolveCommand("npm", pi)) return "npm";
|
|
136
|
-
if (await resolveCommand("brew", pi)) return "brew";
|
|
137
|
-
return "native";
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
async function runWithRetry(pi: ExtensionAPI, spec: CommandSpec) {
|
|
141
|
-
let lastOutput = "";
|
|
142
|
-
for (let attempt = 1; attempt <= 3; attempt++) {
|
|
143
|
-
const result = await pi.exec(spec.command, spec.args, { timeout: 180_000 });
|
|
144
|
-
lastOutput = [result.stdout, result.stderr]
|
|
145
|
-
.filter(Boolean)
|
|
146
|
-
.join("\n")
|
|
147
|
-
.trim();
|
|
148
|
-
if ((result.code ?? 0) === 0)
|
|
149
|
-
return { ok: true, output: lastOutput, attempts: attempt };
|
|
150
|
-
if (attempt === 3 || !isTransient(lastOutput))
|
|
151
|
-
return { ok: false, output: lastOutput, attempts: attempt };
|
|
152
|
-
await new Promise((resolve) => setTimeout(resolve, attempt * 1500));
|
|
153
|
-
}
|
|
154
|
-
return { ok: false, output: lastOutput, attempts: 3 };
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
async function updatePi(pi: ExtensionAPI, ctx: ExtensionCommandContext) {
|
|
158
|
-
await (
|
|
159
|
-
ctx as ExtensionCommandContext & { waitForIdle?: () => Promise<void> }
|
|
160
|
-
).waitForIdle?.();
|
|
161
|
-
|
|
162
|
-
const before = await currentVersion(pi).catch(() => "unknown");
|
|
163
|
-
const method = await detectInstallMethod(pi);
|
|
164
|
-
const spec = commandFor(method);
|
|
165
|
-
|
|
166
|
-
if (!spec) {
|
|
167
|
-
ctx.ui.notify(
|
|
168
|
-
`Pi ${before}; install method appears native. Please update the native binary manually.`,
|
|
169
|
-
"warning",
|
|
170
|
-
);
|
|
171
|
-
return;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
ctx.ui.notify(`Updating Pi via ${method}: ${spec.label}`, "info");
|
|
175
|
-
const result = await runWithRetry(pi, spec);
|
|
176
|
-
const after = await currentVersion(pi).catch(() => "unknown");
|
|
177
|
-
|
|
178
|
-
if (!result.ok) {
|
|
179
|
-
ctx.ui.notify(
|
|
180
|
-
`Pi update failed after ${result.attempts} attempt(s). ${result.output || "No output."}`,
|
|
181
|
-
"error",
|
|
182
|
-
);
|
|
183
|
-
return;
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
ctx.ui.notify(formatUpdateSummary(before, after, result.attempts), "info");
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
async function updateExtensions(
|
|
190
|
-
pi: ExtensionAPI,
|
|
191
|
-
ctx: ExtensionCommandContext,
|
|
192
|
-
) {
|
|
193
|
-
ctx.ui.notify("Updating Pi extensions from dotfiles setup", "info");
|
|
194
|
-
const result = await pi.exec(
|
|
195
|
-
"/bin/sh",
|
|
196
|
-
["-lc", '"$HOME/dotfiles/ai_config/pi/setup.sh"'],
|
|
197
|
-
{ timeout: 240_000 },
|
|
198
|
-
);
|
|
199
|
-
const output = [result.stdout, result.stderr]
|
|
200
|
-
.filter(Boolean)
|
|
201
|
-
.join("\n")
|
|
202
|
-
.trim();
|
|
203
|
-
if ((result.code ?? 0) !== 0) {
|
|
204
|
-
ctx.ui.notify(
|
|
205
|
-
`Pi extensions update failed. ${output || "No output."}`,
|
|
206
|
-
"error",
|
|
207
|
-
);
|
|
208
|
-
return;
|
|
209
|
-
}
|
|
210
|
-
ctx.ui.notify(
|
|
211
|
-
"Pi extensions updated. Please run /reload to apply changes.",
|
|
212
|
-
"warning",
|
|
213
|
-
);
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
async function updatePackages(pi: ExtensionAPI, ctx: ExtensionCommandContext) {
|
|
217
|
-
ctx.ui.notify("Updating pi packages (pi update --extensions)", "info");
|
|
218
|
-
const result = await pi.exec("pi", ["update", "--extensions"], {
|
|
219
|
-
timeout: 240_000,
|
|
220
|
-
});
|
|
221
|
-
const output = [result.stdout, result.stderr]
|
|
222
|
-
.filter(Boolean)
|
|
223
|
-
.join("\n")
|
|
224
|
-
.trim();
|
|
225
|
-
if ((result.code ?? 0) !== 0) {
|
|
226
|
-
ctx.ui.notify(
|
|
227
|
-
`Pi package update failed. ${output || "No output."}`,
|
|
228
|
-
"error",
|
|
229
|
-
);
|
|
230
|
-
return;
|
|
231
|
-
}
|
|
232
|
-
ctx.ui.notify(
|
|
233
|
-
"Pi packages updated. Please run /reload to apply changes.",
|
|
234
|
-
"warning",
|
|
235
|
-
);
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
async function updateAll(pi: ExtensionAPI, ctx: ExtensionCommandContext) {
|
|
239
|
-
await updatePi(pi, ctx);
|
|
240
|
-
await updateExtensions(pi, ctx);
|
|
241
|
-
await updatePackages(pi, ctx);
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
export default function (pi: ExtensionAPI) {
|
|
245
|
-
(
|
|
246
|
-
pi as ExtensionAPI & {
|
|
247
|
-
registerFlag: (name: string, opts: unknown) => void;
|
|
248
|
-
}
|
|
249
|
-
).registerFlag("update", {
|
|
250
|
-
description: "Update Pi, dotfiles extensions, and pi packages",
|
|
251
|
-
type: "boolean",
|
|
252
|
-
default: false,
|
|
253
|
-
});
|
|
254
|
-
|
|
255
|
-
pi.registerCommand("update", {
|
|
256
|
-
description: "Update Pi, dotfiles extensions, and pi packages",
|
|
257
|
-
handler: async (_args, ctx) => {
|
|
258
|
-
await updateAll(pi, ctx);
|
|
259
|
-
},
|
|
260
|
-
});
|
|
261
|
-
|
|
262
|
-
pi.on("session_start", async (_event, ctx) => {
|
|
263
|
-
const flags = pi as ExtensionAPI & {
|
|
264
|
-
getFlag?: (name: string) => boolean;
|
|
265
|
-
sendUserMessage?: (message: string, opts?: unknown) => void;
|
|
266
|
-
};
|
|
267
|
-
if (!flags.getFlag?.("update")) return;
|
|
268
|
-
flags.sendUserMessage?.("/update", { deliverAs: "followUp" });
|
|
269
|
-
ctx.ui.notify("Queued /update from --update", "info");
|
|
270
|
-
});
|
|
271
|
-
}
|
package/src/index.ts
DELETED
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* pix-core — Pi extension bundle
|
|
3
|
-
*
|
|
4
|
-
* Layout (grouped by concern):
|
|
5
|
-
* - ui/ — welcome (π banner + health checks), footer (status bar)
|
|
6
|
-
* - commands/ — models (/models picker), update (/update self-update),
|
|
7
|
-
* diff (/diff)
|
|
8
|
-
* - tool/ — todo (durable execution checklist),
|
|
9
|
-
* toolbox (/toolbox command — user toggles tools on/off),
|
|
10
|
-
* lazy (lazy tool exposure — gates schemas out of the prompt)
|
|
11
|
-
* - nudge/ — model-steering reminders (tools / capability+skills)
|
|
12
|
-
* - lib/ — shared data layer (models.dev + BenchLM)
|
|
13
|
-
*
|
|
14
|
-
* Depends on pix-data (github.com/xynogen/pix-data) for shared
|
|
15
|
-
* models.dev + BenchLM cache at ~/.cache/pi/.
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
19
|
-
import registerAgentSop from "./commands/agent-sop/agent-sop.ts";
|
|
20
|
-
import registerClear from "./commands/clear/clear.ts";
|
|
21
|
-
import registerDiff from "./commands/diff/diff.ts";
|
|
22
|
-
import registerModels from "./commands/models/models.ts";
|
|
23
|
-
import registerUpdate from "./commands/update/update.ts";
|
|
24
|
-
import registerNudges from "./nudge/index.ts";
|
|
25
|
-
import registerAsk from "./tool/ask/index.ts";
|
|
26
|
-
import registerTodo from "./tool/todo/todo.ts";
|
|
27
|
-
import registerToolbox from "./tool/toolbox/toolbox.ts";
|
|
28
|
-
import registerDiagnostics from "./ui/diagnostics.ts";
|
|
29
|
-
import registerFooter from "./ui/footer.ts";
|
|
30
|
-
import registerWelcome from "./ui/welcome.ts";
|
|
31
|
-
|
|
32
|
-
export default function (pi: ExtensionAPI): void {
|
|
33
|
-
registerAgentSop(pi);
|
|
34
|
-
registerWelcome(pi);
|
|
35
|
-
registerFooter(pi);
|
|
36
|
-
registerDiagnostics(pi);
|
|
37
|
-
registerModels(pi);
|
|
38
|
-
registerUpdate(pi);
|
|
39
|
-
registerDiff(pi);
|
|
40
|
-
registerClear(pi);
|
|
41
|
-
registerTodo(pi);
|
|
42
|
-
registerAsk(pi);
|
|
43
|
-
registerToolbox(pi);
|
|
44
|
-
registerNudges(pi);
|
|
45
|
-
}
|
package/src/lib/data.ts
DELETED
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* data.ts — model data layer (shim)
|
|
3
|
-
*
|
|
4
|
-
* Thin re-export of the shared data layer from @xynogen/pix-data
|
|
5
|
-
* (github.com/xynogen/pix-mono/tree/main/packages/pix-data). Cache lives at
|
|
6
|
-
* ~/.cache/pi/ and is shared across all Pi extensions — pix-data warms it on
|
|
7
|
-
* session start; this extension reads from it.
|
|
8
|
-
*
|
|
9
|
-
* Consumers in this extension dir:
|
|
10
|
-
* footer.ts — lookupModelsDev, lookupBenchmark, ModelsDevModel
|
|
11
|
-
* models.ts — lookupModelsDev, lookupBenchmark
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
export type {
|
|
15
|
-
BenchmarkEntry,
|
|
16
|
-
ModelsDevApi,
|
|
17
|
-
ModelsDevModel,
|
|
18
|
-
} from "../../../pix-data/src/index.ts";
|
|
19
|
-
export {
|
|
20
|
-
benchmark,
|
|
21
|
-
buildModelsDevIndex,
|
|
22
|
-
CACHE_DIR,
|
|
23
|
-
DataSource,
|
|
24
|
-
fetchModelsDevIndex,
|
|
25
|
-
lookupBenchmark,
|
|
26
|
-
lookupInIndex,
|
|
27
|
-
lookupModelsDev,
|
|
28
|
-
modelsDev,
|
|
29
|
-
} from "../../../pix-data/src/index.ts";
|
|
30
|
-
|
|
31
|
-
export default function (_pi: unknown): void {
|
|
32
|
-
// pix-data warms this cache on startup — nothing to do here.
|
|
33
|
-
}
|