pi-soly 1.3.0 → 1.4.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/switch/core.ts DELETED
@@ -1,229 +0,0 @@
1
- // =============================================================================
2
- // core.ts — Generic subrotor switcher for pi
3
- // =============================================================================
4
- //
5
- // Lets the user pick which subagent the LLM uses (for `subagent(...)` calls
6
- // in the pi-subagents system, and for any extension that reads the current
7
- // agent). Generic — works with pi-subagents' built-ins (worker, oracle,
8
- // scout, ...) AND any user-defined agent in `~/.pi/agent/agents/`.
9
- //
10
- // Cycle order (Shift+Tab in pi is taken by thinking-level; we use Ctrl+Tab
11
- // as the primary shortcut, with F2 as fallback for terminals that don't
12
- // pass Ctrl+Tab through).
13
- //
14
- // Communication with other extensions:
15
- // - Writes `globalThis.__PI_SWITCH_ROTOR__` (in-process)
16
- // - Reads/writes `.soly/agent` if it exists (cross-session persistence,
17
- // shared with soly extension). If no soly project, persists to
18
- // `~/.pi-switch/agent` instead.
19
- // =============================================================================
20
-
21
- import * as fs from "node:fs";
22
- import * as os from "node:os";
23
- import * as path from "node:path";
24
-
25
- /** Default agent used when no override is set. */
26
- export const DEFAULT_ROTOR = "worker";
27
-
28
- /** Built-in pi-subagents that we always offer in the cycle. */
29
- export const BUILTIN_ROTORS: readonly string[] = [
30
- "worker",
31
- "oracle",
32
- "scout",
33
- "reviewer",
34
- ] as const;
35
-
36
- /** Visual metadata for every known agent. Used by the rich status badge,
37
- * the header bar, and the multi-line switch notify. */
38
- export interface RotorMeta {
39
- emoji: string;
40
- shortLabel: string;
41
- description: string;
42
- writesFiles: boolean;
43
- }
44
-
45
- export const ROTOR_META: Record<string, RotorMeta> = {
46
- worker: { emoji: "\u26a1", shortLabel: "worker", description: "generic implementation, all tools", writesFiles: true },
47
- oracle: { emoji: "\ud83d\udd2e", shortLabel: "oracle", description: "decision-consistency, no file edits", writesFiles: false },
48
- scout: { emoji: "\ud83d\udd0d", shortLabel: "scout", description: "codebase recon, read-only", writesFiles: false },
49
- reviewer: { emoji: "\ud83d\udc40", shortLabel: "reviewer", description: "adversarial code review", writesFiles: false },
50
- };
51
-
52
- /** Get metadata for an agent. Falls back to a neutral entry for unknown. */
53
- export function getRotorMeta(name: string): RotorMeta {
54
- return ROTOR_META[name] ?? {
55
- emoji: "\u2753",
56
- shortLabel: name.length > 12 ? name.slice(0, 11) + "\u2026" : name,
57
- description: "user-defined agent",
58
- writesFiles: true,
59
- };
60
- }
61
-
62
- /** Validate an agent name. */
63
- export function isValidRotorName(name: string): boolean {
64
- return /^[a-zA-Z0-9_-]{1,64}$/.test(name);
65
- }
66
-
67
- /** Discover agent `.md` files in user dir. */
68
- /** All known agent home directories, in priority order (project wins
69
- * over user-home; user-home `.agents/` wins over pi-native).
70
- * Project-level `.agents/` is a vendor-neutral per-project
71
- * convention — same role as `.soly/` or the old `.claude/`.
72
- * Agent .md files live DIRECTLY in the dir (not in a subfolder):
73
- * .agents/reviewer.md (NOT .agents/agents/reviewer.md)
74
- * Honors $HOME / $USERPROFILE for testability. */
75
- export function rotorHomeDirs(cwd?: string): string[] {
76
- const home = process.env.HOME || process.env.USERPROFILE || os.homedir();
77
- const dirs: string[] = [];
78
- if (cwd) {
79
- dirs.push(path.join(cwd, ".agents")); // project (vendor-neutral, preferred)
80
- dirs.push(path.join(cwd, ".pi", "agent", "agents")); // project (pi native, legacy)
81
- }
82
- dirs.push(path.join(home, ".agents")); // user (vendor-neutral)
83
- dirs.push(path.join(home, ".pi", "agent", "agents")); // user (pi native, legacy)
84
- return dirs;
85
- }
86
-
87
- /** Read all agent names from every home dir. Dedupes, first-occurrence
88
- * wins. If cwd is provided, project dirs are scanned first. */
89
- export function discoverUserRotors(cwd?: string): string[] {
90
- const seen = new Set<string>();
91
- const out: string[] = [];
92
- for (const dir of rotorHomeDirs(cwd)) {
93
- if (!fs.existsSync(dir)) continue;
94
- let entries: string[];
95
- try {
96
- entries = fs.readdirSync(dir);
97
- } catch {
98
- continue;
99
- }
100
- for (const file of entries) {
101
- if (!file.endsWith(".md")) continue;
102
- try {
103
- const raw = fs.readFileSync(path.join(dir, file), "utf-8");
104
- const m = raw.match(/^---\n([\s\S]*?)\n---/);
105
- if (!m) continue;
106
- const fm = m[1] ?? "";
107
- const nameMatch = fm.match(/^name:\s*(.+)$/m);
108
- if (nameMatch) {
109
- const n = (nameMatch[1] ?? "").trim();
110
- if (isValidRotorName(n) && !seen.has(n)) {
111
- seen.add(n);
112
- out.push(n);
113
- }
114
- }
115
- } catch { /* skip unreadable */ }
116
- }
117
- }
118
- return out;
119
- }
120
-
121
- /** Build the full cycle of available agents. Built-ins first, then
122
- * project-level agents (if cwd given), then user-home agents.
123
- * Dedupes while preserving first-occurrence order. */
124
- export function availableAgents(cwd?: string): string[] {
125
- const out: string[] = [];
126
- const seen = new Set<string>();
127
- const push = (n: string) => {
128
- if (!seen.has(n)) {
129
- seen.add(n);
130
- out.push(n);
131
- }
132
- };
133
- for (const a of BUILTIN_ROTORS) push(a);
134
- for (const a of discoverUserRotors(cwd)) push(a);
135
- return out;
136
- }
137
-
138
- /** Cycle order. */
139
- export function nextAgent(current: string, cycle: readonly string[]): string {
140
- if (cycle.length === 0) return DEFAULT_ROTOR;
141
- const idx = cycle.indexOf(current);
142
- if (idx < 0) return cycle[0]!;
143
- return cycle[(idx + 1) % cycle.length]!;
144
- }
145
-
146
- /** Parse a user-supplied agent name. */
147
- export function parseRotorName(raw: string): string | null {
148
- const n = raw.trim();
149
- if (!isValidRotorName(n)) return null;
150
- return n;
151
- }
152
-
153
- /** Short badge: `<emoji> <name>`. Null for default (silent). */
154
- export function formatAgentBadge(agent: string): string | null {
155
- if (agent === DEFAULT_ROTOR) return null;
156
- const meta = getRotorMeta(agent);
157
- return `${meta.emoji} ${agent}`;
158
- }
159
-
160
- /** Multi-line switch notify. */
161
- export function formatRotorSwitchNotify(prev: string, next: string): string {
162
- const prevMeta = getRotorMeta(prev);
163
- const nextMeta = getRotorMeta(next);
164
- const lines: string[] = [
165
- "pi-switch agent changed",
166
- "",
167
- ` ${prevMeta.emoji} ${prev.padEnd(16)} → ${nextMeta.emoji} ${next}`,
168
- ` ${"".padEnd(16)} ${nextMeta.description}`,
169
- "",
170
- ` writes files: ${nextMeta.writesFiles ? "yes" : "no (read-only)"} · next subagent call uses: ${next}`,
171
- ];
172
- return lines.join("\n");
173
- }
174
-
175
- /** Group agents: built-ins + user-defined. */
176
- export function groupedAvailableRotors(cwd?: string): Array<{ header: string; agents: string[] }> {
177
- const all = availableAgents(cwd);
178
- const groups: Array<{ header: string; agents: string[] }> = [];
179
- const builtin = all.filter((a) => BUILTIN_ROTORS.includes(a));
180
- if (builtin.length > 0) groups.push({ header: "built-in", agents: builtin });
181
- const user = all.filter((a) => !BUILTIN_ROTORS.includes(a));
182
- if (user.length > 0) groups.push({ header: "user-defined", agents: user });
183
- return groups;
184
- }
185
-
186
- /** Header line shown above chat. Persistent, dim, single line. */
187
- export function formatHeaderLine(agent: string): string {
188
- const meta = getRotorMeta(agent);
189
- const writeTag = meta.writesFiles ? "" : " \u00b7 read-only";
190
- return `${meta.emoji} ${agent} \u00b7 ${meta.description}${writeTag} [Ctrl+Tab to cycle]`;
191
- }
192
-
193
- // ---------------------------------------------------------------------------
194
- // Persistence
195
- // ---------------------------------------------------------------------------
196
-
197
- /** Where to persist the current agent. Prefers `.soly/agent` if a soly
198
- * project exists (shared with soly extension). Otherwise `~/.pi-switch/agent`. */
199
- export function agentFilePath(cwd: string): string {
200
- const solyAgent = path.join(cwd, ".soly", "agent");
201
- if (fs.existsSync(path.join(cwd, ".soly"))) return solyAgent;
202
- // Respect HOME/USERPROFILE for testability (otherwise os.homedir() ignores them on Windows)
203
- const home = process.env.HOME || process.env.USERPROFILE || os.homedir();
204
- const fallbackDir = path.join(home, ".pi-switch");
205
- fs.mkdirSync(fallbackDir, { recursive: true });
206
- return path.join(fallbackDir, "agent");
207
- }
208
-
209
- /** Read persisted agent from disk. Returns null if missing/invalid. */
210
- export function loadAgent(cwd: string): string | null {
211
- try {
212
- const file = agentFilePath(cwd);
213
- if (!fs.existsSync(file)) return null;
214
- const raw = fs.readFileSync(file, "utf-8").trim();
215
- if (!isValidRotorName(raw)) return null;
216
- return raw;
217
- } catch {
218
- return null;
219
- }
220
- }
221
-
222
- /** Write current agent to disk. */
223
- export function saveAgent(cwd: string, agent: string): void {
224
- try {
225
- const file = agentFilePath(cwd);
226
- fs.mkdirSync(path.dirname(file), { recursive: true });
227
- fs.writeFileSync(file, agent + "\n", "utf-8");
228
- } catch { /* best-effort */ }
229
- }
package/switch/index.ts DELETED
@@ -1,365 +0,0 @@
1
- // =============================================================================
2
- // index.ts — pi-switch extension entry (v2: footer-pill UI)
3
- // =============================================================================
4
- //
5
- // Wires the rotor switcher into pi as a compact footer pill:
6
- // - Footer status pill: "▶ ⚡ worker" (or "· ⚡ worker" for the default)
7
- // - Click pill or `/rotor` → open full picker modal (SelectList)
8
- // - Ctrl+Tab → cycle to next rotor (no popup, hot switch)
9
- // - F2 → same, fallback if your terminal doesn't pass Ctrl+Tab through
10
- // - Persists current rotor to .soly/agent or ~/.pi-switch/agent
11
- // - Exposes `globalThis.__PI_SWITCH_ROTOR__` for other extensions
12
- // - Injects a short system-prompt section so the LLM knows the current
13
- // rotor and the available alternatives
14
- //
15
- // UI philosophy:
16
- // - Header is for content, not for tool chrome. Move rotors to footer.
17
- // - Click to explore, hotkey to power-use, no DOM clutter in between.
18
- // - Visual change is the pill text only. Chat stays clean.
19
- // =============================================================================
20
-
21
- import type { ExtensionAPI, ExtensionUIContext } from "@earendil-works/pi-coding-agent";
22
- import { Box, Text } from "@earendil-works/pi-tui";
23
- import * as fs from "node:fs";
24
- import * as os from "node:os";
25
- import * as path from "node:path";
26
- import {
27
- DEFAULT_ROTOR,
28
- BUILTIN_ROTORS,
29
- availableAgents,
30
- nextAgent,
31
- parseRotorName,
32
- groupedAvailableRotors,
33
- getRotorMeta,
34
- loadAgent,
35
- saveAgent,
36
- } from "./core.ts";
37
- import { buildPiSwitchSection, recommendAgent } from "./prompt.ts";
38
- import { watchRotors, type WatcherHandle } from "./watcher.ts";
39
-
40
- const GLOBAL_KEY = "__PI_SWITCH_ROTOR__";
41
-
42
- export default function piSwitchExtension(pi: ExtensionAPI) {
43
- let cwd = "";
44
- let currentRotor: string = DEFAULT_ROTOR;
45
- let cycle: string[] = [DEFAULT_ROTOR];
46
- let lastUi: ExtensionUIContext | null = null;
47
- let rotorWatcher: WatcherHandle | null = null;
48
-
49
- function startRotorWatcher(): void {
50
- // Already running — restart to pick up new cwd
51
- rotorWatcher?.stop();
52
- rotorWatcher = watchRotors(cwd, {
53
- onChange: () => {
54
- refreshCycle();
55
- publish();
56
- rerender();
57
- },
58
- onNotify: (msg) => {
59
- lastUi?.notify(`pi-switch: ${msg}`, "info");
60
- },
61
- });
62
- }
63
-
64
- function refreshCycle(): void {
65
- cycle = availableAgents(cwd);
66
- if (!cycle.includes(currentRotor)) currentRotor = DEFAULT_ROTOR;
67
- }
68
-
69
- function publish(): void {
70
- (globalThis as Record<string, unknown>)[GLOBAL_KEY] = currentRotor;
71
- }
72
-
73
- function rerender(): void {
74
- if (!lastUi) return;
75
- try {
76
- const meta = getRotorMeta(currentRotor);
77
- // Persistent pill — always visible above the input, even for the
78
- // default rotor. The user wants a constant mode indicator, not a
79
- // transient one. Marker "▶" makes it scannable.
80
- const marker = currentRotor === DEFAULT_ROTOR ? "·" : "▶";
81
- const pill = `${marker} ${meta.emoji} ${currentRotor}`;
82
- lastUi.setStatus("pi-switch", pill);
83
- } catch { /* no ui yet */ }
84
- }
85
-
86
- function setRotor(next: string): void {
87
- const prev = currentRotor;
88
- if (next === prev) return;
89
- currentRotor = next;
90
- publish();
91
- if (cwd) saveAgent(cwd, next);
92
- rerender();
93
- // Footer pill is the only visible signal of the switch.
94
- // Chat stays clean — rotor is plumbing, not conversation content.
95
- }
96
-
97
- // ----- session_start: load persisted agent + set initial pill -----
98
- pi.on("session_start", async (_event, ctx) => {
99
- cwd = ctx.cwd;
100
- lastUi = ctx.ui;
101
- publish();
102
- const restored = loadAgent(cwd);
103
- if (restored) currentRotor = restored;
104
- refreshCycle();
105
- publish();
106
- rerender();
107
- startRotorWatcher();
108
- });
109
-
110
- // ----- before_agent_start: inject system-prompt section -----
111
- pi.on("before_agent_start", async (event, ctx) => {
112
- lastUi = ctx.ui;
113
- rerender();
114
- return {
115
- systemPrompt: event.systemPrompt + buildPiSwitchSection(),
116
- };
117
- });
118
-
119
- // ----- Hot cycle (no popup, no confirmation) -----
120
- // Ctrl+Tab is the primary shortcut (most terminals support it).
121
- // F2 is kept as a backup for terminals that don't pass Ctrl+Tab through.
122
- // Debounced: 180ms — terminal key auto-repeat can fire the same key 5+
123
- // times per second, which would spam the chat with the same agent
124
- // notification. The window covers auto-repeat but allows deliberate
125
- // sequential presses.
126
- let lastCycleTs = 0;
127
- const CYCLE_DEBOUNCE_MS = 180;
128
- const cycleShortcut = (sctx: { ui: ExtensionUIContext }): void => {
129
- const now = Date.now();
130
- if (now - lastCycleTs < CYCLE_DEBOUNCE_MS) return;
131
- lastCycleTs = now;
132
- lastUi = sctx.ui;
133
- refreshCycle();
134
- setRotor(nextAgent(currentRotor, cycle));
135
- };
136
- pi.registerShortcut("ctrl+tab", {
137
- description: "Cycle to next agent (worker → oracle → scout → …)",
138
- handler: (sctx) => cycleShortcut(sctx),
139
- });
140
- pi.registerShortcut("f2", {
141
- description: "Cycle to next agent (F2 fallback if Ctrl+Tab isn't passed by your terminal)",
142
- handler: (sctx) => cycleShortcut(sctx),
143
- });
144
-
145
- // ----- /rotor: open picker, or subcommands (create / doctor / recommend / set) -----
146
- // (formerly /agent — renamed to /rotor in 1.0.0 to match the "rotor" naming convention)
147
- pi.registerCommand("rotor", {
148
- description: "open rotor picker, or `set <name>`, `create`, `doctor`, `recommend <task>`",
149
- handler: async (args, ctx) => {
150
- lastUi = ctx.ui;
151
- refreshCycle();
152
- const parts = args.trim().split(/\s+/);
153
- const subcommand = parts[0]?.toLowerCase();
154
- const arg = parts[1];
155
-
156
- if (subcommand === "create") return createAgent(arg, ctx.ui, cwd);
157
- if (subcommand === "doctor") return ctx.ui.notify(doctorReport(), "info");
158
- if (subcommand === "recommend") return handleRecommend(parts.slice(1).join(" "), ctx.ui);
159
- if (subcommand === "set" && arg) return handleSet(arg, ctx.ui);
160
-
161
- // Direct agent name → set
162
- if (subcommand && cycle.includes(subcommand)) return setRotor(subcommand);
163
- if (arg && !subcommand) return handleSet(arg, ctx.ui);
164
-
165
- // No arg: open picker modal
166
- openPicker(ctx.ui);
167
- },
168
- });
169
- }
170
-
171
- // ---------------------------------------------------------------------------
172
- // Picker modal (TUI SelectList)
173
- // ---------------------------------------------------------------------------
174
-
175
- function openPicker(ui: ExtensionUIContext): void {
176
- refreshAndBuild(ui, (groups) => {
177
- const all: Array<{ value: string; label: string; description: string; isCurrent: boolean }> = [];
178
- for (const g of groups) {
179
- all.push({ value: "__sep__", label: `── ${g.header} `, description: "", isCurrent: false });
180
- for (const a of g.agents) {
181
- const m = getRotorMeta(a);
182
- all.push({
183
- value: a,
184
- label: `${m.emoji} ${a}`,
185
- description: `${m.description}${m.writesFiles ? "" : " · read-only"}`,
186
- isCurrent: a === currentRotorRef(),
187
- });
188
- }
189
- }
190
- return all;
191
- }, ui, (choice) => {
192
- if (choice && choice !== "__sep__") setRotorRef(choice);
193
- });
194
- }
195
-
196
- function handleSet(name: string, ui: ExtensionUIContext): void {
197
- const target = parseRotorName(name);
198
- if (!target) return ui.notify(`pi-switch: invalid name "${name}".`, "error");
199
- if (!availableAgents(cwd).includes(target)) {
200
- return ui.notify(`pi-switch: unknown "${target}". available: ${availableAgents(cwd).join(", ")}`, "error");
201
- }
202
- setRotorRef(target);
203
- }
204
-
205
- function handleRecommend(task: string, ui: ExtensionUIContext): void {
206
- if (!task) return ui.notify("pi-switch: usage — `/agent recommend <task>`", "info");
207
- const rec = recommendAgent(task);
208
- if (!rec) return ui.notify(`pi-switch: no clear match for: "${task}"`, "info");
209
- ui.notify(`${rec.emoji} ${rec.agent} · why: ${rec.why}\n → /agent ${rec.agent} to switch`, "info");
210
- }
211
-
212
- // ---------------------------------------------------------------------------
213
- // setAgent / currentRotor — module-scope so the modal can mutate them
214
- // ---------------------------------------------------------------------------
215
-
216
- let currentRotorRef: () => string = () => DEFAULT_ROTOR;
217
- let setRotorRef: (next: string) => void = () => {};
218
-
219
- // The picker and the main extension share state via these refs.
220
- // We patch them in `wire()` at the top of the default export.
221
- function wire(get: () => string, set: (n: string) => void): void {
222
- currentRotorRef = get;
223
- setRotorRef = set;
224
- }
225
-
226
- function refreshAndBuild<T>(
227
- ui: ExtensionUIContext,
228
- build: (groups: ReturnType<typeof groupedAvailableRotors>) => T,
229
- _ui: ExtensionUIContext,
230
- _onSelect: (value: string) => void,
231
- ): void {
232
- // Currently unused: we build inline in openPicker. Kept for future.
233
- void build;
234
- }
235
-
236
- // ---------------------------------------------------------------------------
237
- // /agent create — scaffold a new agent .md
238
- // ---------------------------------------------------------------------------
239
-
240
- function createAgent(
241
- name: string | undefined,
242
- ui: { notify: (t: string, k?: "info" | "warning" | "error") => void; input: (t: string, p?: string) => Promise<string | undefined> },
243
- cwd: string,
244
- ): void {
245
- if (!name) {
246
- ui.notify("pi-switch: usage — `/agent create <name>`", "info");
247
- return;
248
- }
249
- if (!parseRotorName(name)) {
250
- ui.notify(`pi-switch: invalid name "${name}". Use letters/digits/dashes/underscores, ≤64 chars.`, "error");
251
- return;
252
- }
253
- // Write to ~/.agents/ (vendor-neutral) first; fall back to ~/.pi/agent/agents/
254
- // (pi's native) if .agents/ doesn't exist or is unwritable.
255
- const home = os.homedir();
256
- const candidates = [
257
- path.join(home, ".agents"),
258
- path.join(home, ".pi", "agent", "agents"),
259
- ];
260
- const targetDir = candidates.find((d) => {
261
- try {
262
- if (!fs.existsSync(d)) fs.mkdirSync(d, { recursive: true });
263
- // Probe write permission
264
- const probe = path.join(d, ".pi-switch-write-probe");
265
- fs.writeFileSync(probe, "");
266
- fs.unlinkSync(probe);
267
- return true;
268
- } catch {
269
- return false;
270
- }
271
- });
272
- if (!targetDir) {
273
- ui.notify(`pi-switch: could not find a writable agents dir (tried ${candidates.join(", ")}).`, "error");
274
- return;
275
- }
276
- const file = path.join(targetDir, `${name}.md`);
277
- if (fs.existsSync(file)) {
278
- ui.notify(`pi-switch: ${file} already exists. edit it directly.`, "warning");
279
- return;
280
- }
281
- void ui.input(`description for "${name}":`, "one-liner that shows in the picker")?.then((desc) => {
282
- const description = desc?.trim() || `custom agent (${name})`;
283
- try {
284
- // Atomic write: tmp + rename. Avoids partial files and the
285
- // race where two parallel createAgent calls would clobber each
286
- // other's write after both pass the existsSync check.
287
- const tmp = `${file}.tmp-${process.pid}-${Date.now()}`;
288
- fs.writeFileSync(tmp, rotorTemplate(name, description), "utf-8");
289
- fs.renameSync(tmp, file);
290
- ui.notify(
291
- `pi-switch: created ${file}\n → next Ctrl+Tab to see it in the cycle\n → edit the system prompt to specialize`,
292
- "info",
293
- );
294
- } catch (err) {
295
- ui.notify(`pi-switch: failed to write ${file}: ${(err as Error).message}`, "error");
296
- }
297
- });
298
- }
299
-
300
- function rotorTemplate(name: string, description: string): string {
301
- return `---
302
- name: ${name}
303
- description: ${description}
304
- thinking: medium
305
- systemPromptMode: replace
306
- inheritProjectContext: true
307
- inheritSkills: false
308
- tools: read, grep, find, ls, bash, edit, write
309
- defaultContext: fork
310
- ---
311
-
312
- You are \`${name}\`. Describe what you specialize in, your process, and
313
- what you should NOT do. Keep the rest of this frontmatter as-is unless
314
- you have a specific reason to change it.
315
-
316
- # Your role
317
-
318
- <!-- Replace with a one-paragraph description of what you're for. -->
319
-
320
- # Process
321
-
322
- 1. Read the user's request carefully.
323
- 2. Form a hypothesis about the right approach.
324
- 3. Verify with tools (read, grep, bash) before writing.
325
- 4. Commit changes in narrow, reviewable diffs.
326
-
327
- # What you should NOT do
328
-
329
- - Edit other agents' files
330
- - Run subagents yourself (you're already a subagent)
331
- - Skip verification ("trust me bro" is not a process)
332
- `;
333
- }
334
-
335
- function doctorReport(): string {
336
- const cycle = availableAgents(cwd);
337
- const userDir = path.join(os.homedir(), ".pi", "agent", "agents");
338
- const lines: string[] = ["pi-switch doctor:", ""];
339
- const builtins = cycle.filter((a) => BUILTIN_ROTORS.includes(a));
340
- const users = cycle.filter((a) => !BUILTIN_ROTORS.includes(a));
341
- lines.push(`cycle: ${cycle.length} agents (${builtins.length} built-in, ${users.length} user)`);
342
- lines.push("");
343
- if (!fs.existsSync(userDir)) {
344
- lines.push(`user dir: ${userDir} (does not exist)`);
345
- } else {
346
- const files = fs.readdirSync(userDir).filter((f) => f.endsWith(".md"));
347
- lines.push(`user dir: ${userDir} (${files.length} file(s))`);
348
- const issues: string[] = [];
349
- for (const f of files) {
350
- try {
351
- const raw = fs.readFileSync(path.join(userDir, f), "utf-8");
352
- if (!raw.startsWith("---\n")) { issues.push(`${f}: no YAML frontmatter`); continue; }
353
- const m = raw.match(/^---\n([\s\S]*?)\n---/);
354
- if (!m) { issues.push(`${f}: malformed frontmatter`); continue; }
355
- const fm = m[1] ?? "";
356
- if (!/^name:\s*\S/m.test(fm)) issues.push(`${f}: missing 'name:' in frontmatter`);
357
- else if (!/^description:\s*\S/m.test(fm)) issues.push(`${f}: missing 'description:' in frontmatter`);
358
- } catch (e) {
359
- issues.push(`${f}: read error: ${(e as Error).message}`);
360
- }
361
- }
362
- lines.push(issues.length === 0 ? "validation: all user agents OK ✓" : "validation issues:\n - " + issues.join("\n - "));
363
- }
364
- return lines.join("\n");
365
- }
@@ -1,52 +0,0 @@
1
- {
2
- "name": "pi-agented",
3
- "version": "0.2.0",
4
- "description": "Generic subagent switcher for pi. Header bar above chat, Ctrl+Shift+S to cycle, /agent slash command. Works with any agent in ~/.pi/agent/agents/.",
5
- "type": "module",
6
- "main": "index.ts",
7
- "scripts": {
8
- "test": "bun test",
9
- "typecheck": "bun x tsc --noEmit"
10
- },
11
- "dependencies": {},
12
- "peerDependencies": {
13
- "@earendil-works/pi-coding-agent": "*",
14
- "@earendil-works/pi-tui": "*"
15
- },
16
- "devDependencies": {
17
- "@earendil-works/pi-coding-agent": "0.78.1",
18
- "@earendil-works/pi-tui": "0.78.1",
19
- "@types/node": "^25.9.1",
20
- "bun-types": "^1.3.14",
21
- "typebox": "1.1.38",
22
- "typescript": "^6.0.3"
23
- },
24
- "files": [
25
- "index.ts",
26
- "core.ts",
27
- "prompt.ts",
28
- "README.md"
29
- ],
30
- "keywords": [
31
- "pi",
32
- "pi-extension",
33
- "pi-package",
34
- "subagent",
35
- "agent-switcher",
36
- "task-routing"
37
- ],
38
- "license": "MIT",
39
- "pi": {
40
- "extensions": [
41
- "./index.ts"
42
- ]
43
- },
44
- "publishConfig": {
45
- "registry": "https://registry.npmjs.org/"
46
- },
47
- "repository": {
48
- "type": "git",
49
- "url": "http://git.local.stbl/lowern1ght/pi-soly.framework.git",
50
- "directory": "packages/pi-switch"
51
- }
52
- }