@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
package/src/ui/welcome.ts
DELETED
|
@@ -1,369 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Welcome extension — ASCII art banner + startup health checks.
|
|
3
|
-
*
|
|
4
|
-
* Renders a coloured π logo above the editor on session start.
|
|
5
|
-
* Runs checks in parallel while the banner is visible:
|
|
6
|
-
*
|
|
7
|
-
* · pi version — read installed version only
|
|
8
|
-
* · auth — at least one provider configured in modelRegistry
|
|
9
|
-
* · gitignore — auto-ignore Pi emissions (.ai/.pi-lens) in git repos
|
|
10
|
-
*
|
|
11
|
-
* Each check updates the banner live as results arrive.
|
|
12
|
-
* Banner auto-dismisses on the first user turn (turn_start).
|
|
13
|
-
*
|
|
14
|
-
* Layout:
|
|
15
|
-
*
|
|
16
|
-
* ██████╗ ██╗██╗ ██╗
|
|
17
|
-
* ██╔══██╗██║╚██╗██╔╝
|
|
18
|
-
* ██████╔╝██║ ╚███╔╝
|
|
19
|
-
* ██╔═══╝ ██║ ██╔██╗
|
|
20
|
-
* ██║ ██║██╔╝ ██╗
|
|
21
|
-
* ╚═╝ ╚═╝╚═╝ ╚═╝
|
|
22
|
-
*
|
|
23
|
-
* ✓ PI 0.78.0
|
|
24
|
-
* ✓ Auth connected
|
|
25
|
-
* ✓ Models 16 loaded
|
|
26
|
-
* ✓ Tools 24 loaded
|
|
27
|
-
* ✓ Ignore up to date
|
|
28
|
-
*/
|
|
29
|
-
|
|
30
|
-
import type {
|
|
31
|
-
ExtensionAPI,
|
|
32
|
-
ExtensionContext,
|
|
33
|
-
} from "@earendil-works/pi-coding-agent";
|
|
34
|
-
|
|
35
|
-
// ─── Theme shim (same pattern as footer.ts) ───────────────────────────────────
|
|
36
|
-
|
|
37
|
-
export type Theme = {
|
|
38
|
-
fg(color: string, text: string): string;
|
|
39
|
-
bold(text: string): string;
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
// ─── ASCII logo ───────────────────────────────────────────────────────────────
|
|
43
|
-
|
|
44
|
-
export type Tag = "heading" | "model" | "cwd" | "ready" | "";
|
|
45
|
-
|
|
46
|
-
export const LOGO_ROWS: [string, Tag][] = [
|
|
47
|
-
["██████╗ ██╗██╗ ██╗", ""],
|
|
48
|
-
["██╔══██╗██║╚██╗██╔╝", "heading"],
|
|
49
|
-
["██████╔╝██║ ╚███╔╝ ", ""],
|
|
50
|
-
["██╔═══╝ ██║ ██╔██╗ ", "model"],
|
|
51
|
-
["██║ ██║██╔╝ ██╗", "cwd"],
|
|
52
|
-
["╚═╝ ╚═╝╚═╝ ╚═╝", "ready"],
|
|
53
|
-
];
|
|
54
|
-
|
|
55
|
-
// ─── Check result ─────────────────────────────────────────────────────────────
|
|
56
|
-
|
|
57
|
-
export type CheckStatus = "pending" | "ok" | "warn" | "error";
|
|
58
|
-
|
|
59
|
-
export interface CheckResult {
|
|
60
|
-
label: string;
|
|
61
|
-
status: CheckStatus;
|
|
62
|
-
detail?: string;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
66
|
-
|
|
67
|
-
export const shortCwd = (cwd: string, home?: string): string => {
|
|
68
|
-
const h = home ?? process.env.HOME ?? "";
|
|
69
|
-
return h && cwd.startsWith(h) ? `~${cwd.slice(h.length)}` : cwd;
|
|
70
|
-
};
|
|
71
|
-
|
|
72
|
-
export const PI_IGNORE_RULES = [".ai/", ".pi-lens/"];
|
|
73
|
-
const PI_IGNORE_SECTION_HEADER = "# Pix Agent";
|
|
74
|
-
|
|
75
|
-
// ─── Individual checks ────────────────────────────────────────────────────────
|
|
76
|
-
|
|
77
|
-
async function checkPiVersion(pi: ExtensionAPI): Promise<CheckResult> {
|
|
78
|
-
try {
|
|
79
|
-
const localRes = await pi.exec("pi", ["--version"], { timeout: 2_000 });
|
|
80
|
-
const local = (localRes.stdout.trim() || localRes.stderr.trim()).replace(
|
|
81
|
-
/^v/,
|
|
82
|
-
"",
|
|
83
|
-
);
|
|
84
|
-
return { label: "PI", status: "ok", detail: local || "installed" };
|
|
85
|
-
} catch {
|
|
86
|
-
return { label: "PI", status: "warn", detail: "version unavailable" };
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
type ExecResult = {
|
|
91
|
-
stdout: string;
|
|
92
|
-
stderr: string;
|
|
93
|
-
exitCode?: number;
|
|
94
|
-
code?: number;
|
|
95
|
-
};
|
|
96
|
-
|
|
97
|
-
function exitCode(r: ExecResult): number {
|
|
98
|
-
return r.exitCode ?? r.code ?? 0;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
function execOpts(cwd: string, timeout: number): { timeout?: number } {
|
|
102
|
-
return { cwd, timeout } as unknown as { timeout?: number };
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
async function checkPiIgnore(
|
|
106
|
-
pi: ExtensionAPI,
|
|
107
|
-
cwd: string,
|
|
108
|
-
): Promise<CheckResult> {
|
|
109
|
-
try {
|
|
110
|
-
// Find repo root — avoids creating .gitignore in a subfolder
|
|
111
|
-
const rootRes = await pi.exec(
|
|
112
|
-
"git",
|
|
113
|
-
["rev-parse", "--show-toplevel"],
|
|
114
|
-
execOpts(cwd, 2_000),
|
|
115
|
-
);
|
|
116
|
-
if (exitCode(rootRes) !== 0) {
|
|
117
|
-
return { label: "Ignore", status: "ok", detail: "not git" };
|
|
118
|
-
}
|
|
119
|
-
const repoRoot = rootRes.stdout.trim();
|
|
120
|
-
if (!repoRoot) return { label: "Ignore", status: "ok", detail: "not git" };
|
|
121
|
-
|
|
122
|
-
// Determine which rules are missing
|
|
123
|
-
const missing: string[] = [];
|
|
124
|
-
for (const rule of PI_IGNORE_RULES) {
|
|
125
|
-
const hasRule = await pi.exec(
|
|
126
|
-
"grep",
|
|
127
|
-
["-qxF", rule, ".gitignore"],
|
|
128
|
-
execOpts(repoRoot, 1_000),
|
|
129
|
-
);
|
|
130
|
-
if (exitCode(hasRule) !== 0) missing.push(rule);
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
if (missing.length === 0) {
|
|
134
|
-
return { label: "Ignore", status: "ok", detail: "up to date" };
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
// Rewrite .gitignore — strip any existing Pix Agent section, then append
|
|
138
|
-
// a clean block with all rules under a single header.
|
|
139
|
-
const gitignorePath = `${repoRoot}/.gitignore`;
|
|
140
|
-
const allRules = PI_IGNORE_RULES;
|
|
141
|
-
const addRules = await pi.exec(
|
|
142
|
-
"node",
|
|
143
|
-
[
|
|
144
|
-
"-e",
|
|
145
|
-
[
|
|
146
|
-
"const fs = require('fs');",
|
|
147
|
-
`const p = ${JSON.stringify(gitignorePath)};`,
|
|
148
|
-
`const header = ${JSON.stringify(PI_IGNORE_SECTION_HEADER)};`,
|
|
149
|
-
`const rules = ${JSON.stringify(allRules)};`,
|
|
150
|
-
"const existing = fs.existsSync(p) ? fs.readFileSync(p, 'utf8') : '';",
|
|
151
|
-
// Strip existing Pix Agent section (header line + consecutive rule lines)
|
|
152
|
-
"const stripped = existing.replace(/(?:^|\\n)# Pix Agent\\n(?:[^\\n]*\\n)*/g, '\\n').trimEnd();",
|
|
153
|
-
"const block = [header, ...rules].join('\\n');",
|
|
154
|
-
"const content = (stripped ? stripped + '\\n\\n' : '') + block + '\\n';",
|
|
155
|
-
"fs.writeFileSync(p, content, 'utf8');",
|
|
156
|
-
].join("\n"),
|
|
157
|
-
],
|
|
158
|
-
execOpts(repoRoot, 5_000),
|
|
159
|
-
);
|
|
160
|
-
|
|
161
|
-
return exitCode(addRules) === 0
|
|
162
|
-
? {
|
|
163
|
-
label: "Ignore",
|
|
164
|
-
status: "ok",
|
|
165
|
-
detail: `added ${missing.length} rule${missing.length === 1 ? "" : "s"}`,
|
|
166
|
-
}
|
|
167
|
-
: { label: "Ignore", status: "warn", detail: "write failed" };
|
|
168
|
-
} catch {
|
|
169
|
-
return { label: "Ignore", status: "warn", detail: "check failed" };
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
interface ToolInfo {
|
|
174
|
-
sourceInfo?: { source?: string };
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
/**
|
|
178
|
-
* Summarise loaded tools by source. Counts everything in `getActiveTools()`
|
|
179
|
-
* and breaks out built-in vs. extension/custom tools for the detail line.
|
|
180
|
-
*/
|
|
181
|
-
export function summariseTools(tools: ToolInfo[]): CheckResult {
|
|
182
|
-
const total = tools.length;
|
|
183
|
-
if (total === 0) {
|
|
184
|
-
return { label: "Tools", status: "warn", detail: "none active" };
|
|
185
|
-
}
|
|
186
|
-
const builtin = tools.filter(
|
|
187
|
-
(t) => t.sourceInfo?.source === "builtin",
|
|
188
|
-
).length;
|
|
189
|
-
const extra = total - builtin;
|
|
190
|
-
const detail =
|
|
191
|
-
extra > 0 ? `${total} loaded (+${extra} ext)` : `${total} loaded`;
|
|
192
|
-
return { label: "Tools", status: "ok", detail };
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
function checkTools(pi: { getActiveTools?: () => ToolInfo[] }): CheckResult {
|
|
196
|
-
try {
|
|
197
|
-
const tools = pi.getActiveTools?.() ?? [];
|
|
198
|
-
return summariseTools(tools);
|
|
199
|
-
} catch {
|
|
200
|
-
return { label: "Tools", status: "warn", detail: "unavailable" };
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
function checkAuth(ctx: {
|
|
205
|
-
modelRegistry: { getAvailable(): unknown[] };
|
|
206
|
-
}): [CheckResult, CheckResult] {
|
|
207
|
-
try {
|
|
208
|
-
const models = ctx.modelRegistry.getAvailable();
|
|
209
|
-
const count = models.length;
|
|
210
|
-
if (count === 0) {
|
|
211
|
-
return [
|
|
212
|
-
{ label: "Auth", status: "warn", detail: "run /login" },
|
|
213
|
-
{ label: "Models", status: "warn", detail: "0 loaded" },
|
|
214
|
-
];
|
|
215
|
-
}
|
|
216
|
-
return [
|
|
217
|
-
{ label: "Auth", status: "ok", detail: "connected" },
|
|
218
|
-
{ label: "Models", status: "ok", detail: `${count} loaded` },
|
|
219
|
-
];
|
|
220
|
-
} catch {
|
|
221
|
-
return [
|
|
222
|
-
{ label: "Auth", status: "error", detail: "registry unavailable" },
|
|
223
|
-
{ label: "Models", status: "error", detail: "unavailable" },
|
|
224
|
-
];
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
// ─── Render ───────────────────────────────────────────────────────────────────
|
|
229
|
-
|
|
230
|
-
export function statusIcon(theme: Theme, status: CheckStatus): string {
|
|
231
|
-
switch (status) {
|
|
232
|
-
case "pending":
|
|
233
|
-
return theme.fg("muted", "○");
|
|
234
|
-
case "ok":
|
|
235
|
-
return theme.fg("success", "✓");
|
|
236
|
-
case "warn":
|
|
237
|
-
return theme.fg("warning", "⚠");
|
|
238
|
-
case "error":
|
|
239
|
-
return theme.fg("error", "✗");
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
export const LABEL_WIDTH = 6; // visual chars for label column
|
|
244
|
-
|
|
245
|
-
export function renderCheck(theme: Theme, c: CheckResult): string {
|
|
246
|
-
const icon = statusIcon(theme, c.status);
|
|
247
|
-
const labelColor =
|
|
248
|
-
c.status === "error" ? "error" : c.status === "warn" ? "warning" : "muted";
|
|
249
|
-
const label = theme.fg(labelColor, c.label.padEnd(LABEL_WIDTH));
|
|
250
|
-
const detail = c.detail ? theme.fg("text", c.detail) : "";
|
|
251
|
-
return `${icon} ${label} ${detail}`;
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
function buildLogoLines(theme: Theme, model: string, cwd: string): string[] {
|
|
255
|
-
const pad = " ";
|
|
256
|
-
return LOGO_ROWS.map(([logo, tag]) => {
|
|
257
|
-
const l = theme.fg("accent", logo);
|
|
258
|
-
switch (tag) {
|
|
259
|
-
case "heading":
|
|
260
|
-
return `${pad}${l} ${theme.fg("muted", "PIx")}`;
|
|
261
|
-
case "model":
|
|
262
|
-
return `${pad}${l} ${theme.fg("muted", "")} ${theme.fg("text", model)}`;
|
|
263
|
-
case "cwd":
|
|
264
|
-
return `${pad}${l} ${theme.fg("muted", "")} ${theme.fg("text", cwd)}`;
|
|
265
|
-
case "ready":
|
|
266
|
-
return `${pad}${l} ${theme.fg("success", "")} ${theme.bold(theme.fg("success", "ready"))}`;
|
|
267
|
-
default:
|
|
268
|
-
return `${pad}${l}`;
|
|
269
|
-
}
|
|
270
|
-
});
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
function buildCheckLines(theme: Theme, checks: CheckResult[]): string[] {
|
|
274
|
-
if (checks.length === 0) return [];
|
|
275
|
-
const pad = " ";
|
|
276
|
-
const lines: string[] = [""];
|
|
277
|
-
for (const c of checks) {
|
|
278
|
-
lines.push(`${pad}${renderCheck(theme, c)}`);
|
|
279
|
-
}
|
|
280
|
-
return lines;
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
// ─── Extension ────────────────────────────────────────────────────────────────
|
|
284
|
-
|
|
285
|
-
export default function (pi: ExtensionAPI) {
|
|
286
|
-
let dismissed = false;
|
|
287
|
-
let requestRender: (() => void) | null = null;
|
|
288
|
-
|
|
289
|
-
const dismiss = (ctx: ExtensionContext) => {
|
|
290
|
-
if (dismissed) return;
|
|
291
|
-
dismissed = true;
|
|
292
|
-
requestRender = null;
|
|
293
|
-
ctx.ui?.setWidget?.("welcome", undefined);
|
|
294
|
-
};
|
|
295
|
-
|
|
296
|
-
pi.on("session_start", (_event, ctx) => {
|
|
297
|
-
dismissed = false;
|
|
298
|
-
|
|
299
|
-
// cwd is static; model can change via /model so keep it mutable
|
|
300
|
-
const modelId = ctx.model?.id ?? "—";
|
|
301
|
-
const cwd = shortCwd(ctx.cwd);
|
|
302
|
-
|
|
303
|
-
// Pending placeholders — one per check
|
|
304
|
-
const CHECKS: CheckResult[] = [
|
|
305
|
-
{ label: "PI", status: "pending" },
|
|
306
|
-
{ label: "Auth", status: "pending" },
|
|
307
|
-
{ label: "Models", status: "pending" },
|
|
308
|
-
{ label: "Tools", status: "pending" },
|
|
309
|
-
{ label: "Ignore", status: "pending" },
|
|
310
|
-
];
|
|
311
|
-
|
|
312
|
-
// Auth + Models checks are synchronous — fill immediately
|
|
313
|
-
const [authResult, modelsResult] = checkAuth(ctx);
|
|
314
|
-
CHECKS[1] = authResult;
|
|
315
|
-
CHECKS[2] = modelsResult;
|
|
316
|
-
|
|
317
|
-
// Register widget
|
|
318
|
-
if (!ctx.ui.setWidget) return;
|
|
319
|
-
ctx.ui.setWidget(
|
|
320
|
-
"welcome",
|
|
321
|
-
(tui: { requestRender(): void }, theme: Theme) => {
|
|
322
|
-
requestRender = () => tui.requestRender();
|
|
323
|
-
|
|
324
|
-
return {
|
|
325
|
-
render: () => {
|
|
326
|
-
const t = theme as unknown as Theme;
|
|
327
|
-
// Re-read modelId each render so /model changes show live
|
|
328
|
-
const logoLines = buildLogoLines(t, modelId, cwd);
|
|
329
|
-
return [...logoLines, ...buildCheckLines(t, CHECKS), ""];
|
|
330
|
-
},
|
|
331
|
-
dispose() {
|
|
332
|
-
requestRender = null;
|
|
333
|
-
},
|
|
334
|
-
invalidate() {},
|
|
335
|
-
};
|
|
336
|
-
},
|
|
337
|
-
{ placement: "aboveEditor" },
|
|
338
|
-
);
|
|
339
|
-
|
|
340
|
-
// Run async checks in parallel; each updates CHECKS and re-renders
|
|
341
|
-
const update = (idx: number, result: CheckResult) => {
|
|
342
|
-
if (dismissed) return;
|
|
343
|
-
CHECKS[idx] = result;
|
|
344
|
-
requestRender?.();
|
|
345
|
-
};
|
|
346
|
-
|
|
347
|
-
void checkPiVersion(pi).then((r) => update(0, r));
|
|
348
|
-
void checkPiIgnore(pi, ctx.cwd).then((r) => update(4, r));
|
|
349
|
-
// auth already filled synchronously above; no async needed
|
|
350
|
-
|
|
351
|
-
// Tools register during session_start (incl. other extensions); read on
|
|
352
|
-
// next tick so dynamically registered tools are counted.
|
|
353
|
-
setTimeout(
|
|
354
|
-
() =>
|
|
355
|
-
update(
|
|
356
|
-
3,
|
|
357
|
-
checkTools(pi as unknown as { getActiveTools?: () => ToolInfo[] }),
|
|
358
|
-
),
|
|
359
|
-
0,
|
|
360
|
-
);
|
|
361
|
-
});
|
|
362
|
-
|
|
363
|
-
pi.on("turn_start", (_event, ctx) => {
|
|
364
|
-
dismiss(ctx);
|
|
365
|
-
});
|
|
366
|
-
pi.on("session_shutdown", (_event, ctx) => {
|
|
367
|
-
dismiss(ctx);
|
|
368
|
-
});
|
|
369
|
-
}
|