my-pi 0.0.3 → 0.0.4

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.
@@ -0,0 +1,1893 @@
1
+ import { InteractiveMode as InteractiveMode$1, SessionManager, SettingsManager, createAgentSessionFromServices, createAgentSessionRuntime, createAgentSessionServices, defineTool, getAgentDir, parseFrontmatter, runPrintMode as runPrintMode$1 } from "@mariozechner/pi-coding-agent";
2
+ import { cpSync, existsSync, globSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
3
+ import { basename, dirname, join, relative, resolve } from "node:path";
4
+ import { Type } from "@sinclair/typebox";
5
+ import { execFileSync, spawn } from "node:child_process";
6
+ import { homedir } from "node:os";
7
+ import { Container, SettingsList, Text } from "@mariozechner/pi-tui";
8
+ import { createHash } from "node:crypto";
9
+ //#region src/extensions/chain.ts
10
+ function parse_chain_yaml(raw) {
11
+ const chains = [];
12
+ let current = null;
13
+ let current_step = null;
14
+ for (const line of raw.split("\n")) {
15
+ const chain_match = line.match(/^(\S[^:]*):$/);
16
+ if (chain_match) {
17
+ if (current && current_step) {
18
+ current.steps.push(current_step);
19
+ current_step = null;
20
+ }
21
+ current = {
22
+ name: chain_match[1].trim(),
23
+ description: "",
24
+ steps: []
25
+ };
26
+ chains.push(current);
27
+ continue;
28
+ }
29
+ const desc_match = line.match(/^\s+description:\s+(.+)$/);
30
+ if (desc_match && current && !current_step) {
31
+ let desc = desc_match[1].trim();
32
+ if (desc.startsWith("\"") && desc.endsWith("\"") || desc.startsWith("'") && desc.endsWith("'")) desc = desc.slice(1, -1);
33
+ current.description = desc;
34
+ continue;
35
+ }
36
+ if (line.match(/^\s+steps:\s*$/) && current) continue;
37
+ const agent_match = line.match(/^\s+-\s+agent:\s+(.+)$/);
38
+ if (agent_match && current) {
39
+ if (current_step) current.steps.push(current_step);
40
+ current_step = {
41
+ agent: agent_match[1].trim(),
42
+ prompt: ""
43
+ };
44
+ continue;
45
+ }
46
+ const prompt_match = line.match(/^\s+prompt:\s+(.+)$/);
47
+ if (prompt_match && current_step) {
48
+ let prompt = prompt_match[1].trim();
49
+ if (prompt.startsWith("\"") && prompt.endsWith("\"") || prompt.startsWith("'") && prompt.endsWith("'")) prompt = prompt.slice(1, -1);
50
+ prompt = prompt.replace(/\\n/g, "\n");
51
+ current_step.prompt = prompt;
52
+ continue;
53
+ }
54
+ }
55
+ if (current && current_step) current.steps.push(current_step);
56
+ return chains;
57
+ }
58
+ function parse_agent_file(filePath) {
59
+ try {
60
+ const { frontmatter, body } = parseFrontmatter(readFileSync(filePath, "utf-8"));
61
+ if (!frontmatter?.name) return null;
62
+ return {
63
+ name: frontmatter.name,
64
+ description: frontmatter.description || "",
65
+ tools: frontmatter.tools || "read,grep,find,ls",
66
+ systemPrompt: body.trim()
67
+ };
68
+ } catch {
69
+ return null;
70
+ }
71
+ }
72
+ function scan_agent_dirs(cwd) {
73
+ const dirs = [
74
+ join(cwd, "agents"),
75
+ join(cwd, ".claude", "agents"),
76
+ join(cwd, ".pi", "agents")
77
+ ];
78
+ const agents = /* @__PURE__ */ new Map();
79
+ for (const dir of dirs) {
80
+ if (!existsSync(dir)) continue;
81
+ try {
82
+ for (const file of readdirSync(dir)) {
83
+ if (!file.endsWith(".md")) continue;
84
+ const def = parse_agent_file(resolve(dir, file));
85
+ if (def && !agents.has(def.name.toLowerCase())) agents.set(def.name.toLowerCase(), def);
86
+ }
87
+ } catch {}
88
+ }
89
+ return agents;
90
+ }
91
+ function run_agent_step(agent_def, task) {
92
+ const bin = process.argv[1];
93
+ const args = [
94
+ "--no-builtin",
95
+ "-P",
96
+ task
97
+ ];
98
+ const chunks = [];
99
+ return new Promise((res) => {
100
+ const proc = spawn(process.execPath, [bin, ...args], {
101
+ stdio: [
102
+ "ignore",
103
+ "pipe",
104
+ "pipe"
105
+ ],
106
+ env: {
107
+ ...process.env,
108
+ MY_PI_AGENT_SYSTEM_PROMPT: agent_def.systemPrompt
109
+ }
110
+ });
111
+ proc.stdout.setEncoding("utf-8");
112
+ proc.stdout.on("data", (chunk) => {
113
+ chunks.push(chunk);
114
+ });
115
+ proc.stderr.setEncoding("utf-8");
116
+ proc.stderr.on("data", () => {});
117
+ proc.on("close", (code) => {
118
+ res({
119
+ output: chunks.join("").trim(),
120
+ exitCode: code ?? 1
121
+ });
122
+ });
123
+ proc.on("error", (err) => {
124
+ res({
125
+ output: `Error spawning agent: ${err.message}`,
126
+ exitCode: 1
127
+ });
128
+ });
129
+ });
130
+ }
131
+ async function chain(pi) {
132
+ const cwd = process.cwd();
133
+ const agents = scan_agent_dirs(cwd);
134
+ let chains = [];
135
+ let active_chain = null;
136
+ const chain_paths = [
137
+ join(cwd, ".pi", "agents", "agent-chain.yaml"),
138
+ join(cwd, ".pi", "agents", "chains.yaml"),
139
+ join(cwd, ".claude", "agents", "chains.yaml")
140
+ ];
141
+ for (const path of chain_paths) if (existsSync(path)) try {
142
+ chains = parse_chain_yaml(readFileSync(path, "utf-8"));
143
+ break;
144
+ } catch {}
145
+ if (chains.length > 0) active_chain = chains[0];
146
+ pi.registerTool(defineTool({
147
+ name: "run_chain",
148
+ label: "Run Chain",
149
+ description: "Execute the active agent chain pipeline. Each step runs sequentially — output from one step feeds into the next as $INPUT. $ORIGINAL is always the user's initial prompt.",
150
+ parameters: Type.Object({ task: Type.String({ description: "The task/prompt for the chain to process" }) }),
151
+ execute: async (_id, params) => {
152
+ const { task } = params;
153
+ if (!active_chain) return {
154
+ content: [{
155
+ type: "text",
156
+ text: "No chain active. Use /chain to select one."
157
+ }],
158
+ details: {
159
+ chain: "",
160
+ steps: 0
161
+ }
162
+ };
163
+ let input = task;
164
+ const original = task;
165
+ const results = [];
166
+ for (let i = 0; i < active_chain.steps.length; i++) {
167
+ const step = active_chain.steps[i];
168
+ const agent_def = agents.get(step.agent.toLowerCase());
169
+ if (!agent_def) {
170
+ const msg = `Step ${i + 1}: agent "${step.agent}" not found. Available: ${Array.from(agents.keys()).join(", ")}`;
171
+ results.push(msg);
172
+ return {
173
+ content: [{
174
+ type: "text",
175
+ text: msg
176
+ }],
177
+ details: {
178
+ chain: active_chain.name,
179
+ steps: i
180
+ }
181
+ };
182
+ }
183
+ const result = await run_agent_step(agent_def, step.prompt.replace(/\$INPUT/g, input).replace(/\$ORIGINAL/g, original));
184
+ if (result.exitCode !== 0) {
185
+ const msg = `Step ${i + 1} (${step.agent}) failed:\n${result.output}`;
186
+ results.push(msg);
187
+ return {
188
+ content: [{
189
+ type: "text",
190
+ text: msg
191
+ }],
192
+ details: {
193
+ chain: active_chain.name,
194
+ steps: i + 1
195
+ }
196
+ };
197
+ }
198
+ results.push(`## Step ${i + 1}: ${step.agent}\n${result.output}`);
199
+ input = result.output;
200
+ }
201
+ return {
202
+ content: [{
203
+ type: "text",
204
+ text: results.join("\n\n---\n\n")
205
+ }],
206
+ details: {
207
+ chain: active_chain.name,
208
+ steps: active_chain.steps.length
209
+ }
210
+ };
211
+ }
212
+ }));
213
+ pi.registerCommand("chain", {
214
+ description: "Switch active chain or list chains (chain list)",
215
+ getArgumentCompletions: (prefix) => {
216
+ const parts = prefix.trim().split(/\s+/);
217
+ if (parts.length <= 1) return ["list", ...chains.map((c) => c.name)].filter((s) => s.startsWith(parts[0] || "")).map((s) => ({
218
+ value: s,
219
+ label: s
220
+ }));
221
+ return null;
222
+ },
223
+ handler: async (args, ctx) => {
224
+ const sub = args.trim();
225
+ if (!sub || sub === "list") {
226
+ if (chains.length === 0) {
227
+ ctx.ui.notify("No chains found. Add chains to .pi/agents/agent-chain.yaml", "warning");
228
+ return;
229
+ }
230
+ const lines = chains.map((c) => {
231
+ const active = c.name === active_chain?.name ? " *" : "";
232
+ const steps = c.steps.map((s) => s.agent).join(" -> ");
233
+ return `${c.name}${active}: ${c.description || steps}`;
234
+ });
235
+ ctx.ui.notify(lines.join("\n"));
236
+ return;
237
+ }
238
+ const found_chain = chains.find((c) => c.name.toLowerCase() === sub.toLowerCase());
239
+ if (!found_chain) {
240
+ ctx.ui.notify(`Unknown chain: ${sub}. Use /chain list.`, "warning");
241
+ return;
242
+ }
243
+ active_chain = found_chain;
244
+ const flow = found_chain.steps.map((s) => s.agent).join(" -> ");
245
+ ctx.ui.notify(`Active chain: ${found_chain.name} (${flow})`);
246
+ }
247
+ });
248
+ pi.on("before_agent_start", async (event) => {
249
+ if (!active_chain || chains.length === 0) return {};
250
+ const flow = active_chain.steps.map((s) => s.agent).join(" -> ");
251
+ const step_list = active_chain.steps.map((s, i) => {
252
+ const desc = agents.get(s.agent.toLowerCase())?.description || "unknown";
253
+ return `${i + 1}. **${s.agent}** — ${desc}`;
254
+ }).join("\n");
255
+ const chain_list = chains.map((c) => {
256
+ const active = c.name === active_chain?.name ? " (active)" : "";
257
+ return `- ${c.name}${active}: ${c.description}`;
258
+ }).join("\n");
259
+ return { systemPrompt: event.systemPrompt + `
260
+
261
+ ## Agent Chains
262
+
263
+ You have a run_chain tool that executes sequential agent pipelines.
264
+
265
+ ### Active Chain: ${active_chain.name}
266
+ ${active_chain.description}
267
+ Flow: ${flow}
268
+
269
+ ${step_list}
270
+
271
+ ### Available Chains
272
+ ${chain_list}
273
+
274
+ ### When to use run_chain
275
+ - Non-trivial work: features, refactors, multi-file changes
276
+ - Tasks that benefit from planning then building then reviewing
277
+ - When structured multi-agent collaboration helps
278
+
279
+ ### When to work directly
280
+ - Simple reads, quick lookups, small edits
281
+ - Answering questions about the codebase
282
+ - Anything you can handle in one step
283
+
284
+ Switch chains with /chain <name>.` };
285
+ });
286
+ pi.registerCommand("agents", {
287
+ description: "List discovered agent definitions",
288
+ handler: async (_args, ctx) => {
289
+ if (agents.size === 0) {
290
+ ctx.ui.notify("No agents found in agents/, .pi/agents/, or .claude/agents/", "warning");
291
+ return;
292
+ }
293
+ const lines = Array.from(agents.values()).map((a) => `${a.name}: ${a.description || "(no description)"} [${a.tools}]`);
294
+ ctx.ui.notify(lines.join("\n"));
295
+ }
296
+ });
297
+ }
298
+ //#endregion
299
+ //#region src/extensions/config.ts
300
+ const DEFAULT_CONFIG$1 = {
301
+ version: 1,
302
+ enabled: {}
303
+ };
304
+ const BUILTIN_EXTENSIONS = [
305
+ {
306
+ key: "mcp",
307
+ label: "MCP",
308
+ description: "MCP server integration and /mcp command",
309
+ cli_flag: "--no-mcp",
310
+ aliases: ["mcp"]
311
+ },
312
+ {
313
+ key: "skills",
314
+ label: "Skills",
315
+ description: "Managed pi-native skills and /skills command",
316
+ cli_flag: "--no-skills",
317
+ aliases: ["skills", "skill"]
318
+ },
319
+ {
320
+ key: "chain",
321
+ label: "Chain",
322
+ description: "Agent chain orchestration and /chain command",
323
+ cli_flag: "--no-chain",
324
+ aliases: ["chain", "chains"]
325
+ },
326
+ {
327
+ key: "filter-output",
328
+ label: "Filter output",
329
+ description: "Secret redaction for tool output",
330
+ cli_flag: "--no-filter",
331
+ aliases: [
332
+ "filter-output",
333
+ "filter_output",
334
+ "filter",
335
+ "redaction"
336
+ ]
337
+ },
338
+ {
339
+ key: "handoff",
340
+ label: "Handoff",
341
+ description: "Session handoff export and /handoff command",
342
+ cli_flag: "--no-handoff",
343
+ aliases: ["handoff"]
344
+ },
345
+ {
346
+ key: "recall",
347
+ label: "Recall",
348
+ description: "Past session recall guidance and /recall command",
349
+ cli_flag: "--no-recall",
350
+ aliases: ["recall"]
351
+ }
352
+ ];
353
+ function get_builtin_extensions_config_path() {
354
+ return join(process.env.XDG_CONFIG_HOME || join(homedir(), ".config"), "my-pi", "extensions.json");
355
+ }
356
+ function load_builtin_extensions_config() {
357
+ const path = get_builtin_extensions_config_path();
358
+ if (!existsSync(path)) return { ...DEFAULT_CONFIG$1 };
359
+ try {
360
+ const raw = readFileSync(path, "utf-8");
361
+ const parsed = JSON.parse(raw);
362
+ const enabled = {};
363
+ for (const extension of BUILTIN_EXTENSIONS) {
364
+ const value = parsed.enabled?.[extension.key];
365
+ if (typeof value === "boolean") enabled[extension.key] = value;
366
+ }
367
+ return {
368
+ version: parsed.version ?? 1,
369
+ enabled
370
+ };
371
+ } catch {
372
+ return { ...DEFAULT_CONFIG$1 };
373
+ }
374
+ }
375
+ function save_builtin_extensions_config(config) {
376
+ const path = get_builtin_extensions_config_path();
377
+ const dir = dirname(path);
378
+ if (!existsSync(dir)) mkdirSync(dir, {
379
+ recursive: true,
380
+ mode: 448
381
+ });
382
+ const tmp = `${path}.tmp-${Date.now()}`;
383
+ writeFileSync(tmp, JSON.stringify(config, null, " ") + "\n", { mode: 384 });
384
+ renameSync(tmp, path);
385
+ }
386
+ function is_builtin_extension_enabled(config, key) {
387
+ return config.enabled[key] ?? true;
388
+ }
389
+ function is_builtin_extension_active(config, key, force_disabled = /* @__PURE__ */ new Set()) {
390
+ return is_builtin_extension_enabled(config, key) && !force_disabled.has(key);
391
+ }
392
+ function resolve_builtin_extension_states(force_disabled = /* @__PURE__ */ new Set(), config = load_builtin_extensions_config()) {
393
+ return BUILTIN_EXTENSIONS.map((extension) => {
394
+ const saved_enabled = is_builtin_extension_enabled(config, extension.key);
395
+ const forced = force_disabled.has(extension.key);
396
+ return {
397
+ ...extension,
398
+ saved_enabled,
399
+ effective_enabled: saved_enabled && !forced,
400
+ forced_disabled: forced
401
+ };
402
+ });
403
+ }
404
+ function find_builtin_extension(query) {
405
+ const normalized = query.trim().toLowerCase();
406
+ if (!normalized) return void 0;
407
+ return BUILTIN_EXTENSIONS.find((extension) => [
408
+ extension.key,
409
+ extension.label,
410
+ ...extension.aliases
411
+ ].some((value) => value.toLowerCase() === normalized));
412
+ }
413
+ //#endregion
414
+ //#region src/extensions/extensions.ts
415
+ const ENABLED$1 = "[x]";
416
+ const DISABLED$1 = "[ ]";
417
+ function to_force_disabled_set(force_disabled) {
418
+ return new Set(force_disabled ?? []);
419
+ }
420
+ function format_effective_state(state) {
421
+ if (state.effective_enabled) return "enabled";
422
+ if (state.forced_disabled) return `disabled in this process by ${state.cli_flag}`;
423
+ return "disabled";
424
+ }
425
+ function format_extension_lines(states, options) {
426
+ const lines = [];
427
+ if (options?.heading) lines.push(options.heading, "");
428
+ const enabled_now = states.filter((state) => state.effective_enabled).length;
429
+ const disabled_now = states.length - enabled_now;
430
+ lines.push(`${states.length} built-in extensions (${enabled_now} enabled now, ${disabled_now} disabled now)`, "");
431
+ for (const state of states) {
432
+ lines.push(`${state.saved_enabled ? ENABLED$1 : DISABLED$1} ${state.label}`);
433
+ lines.push(` key: ${state.key}`);
434
+ lines.push(` saved config: ${state.saved_enabled ? "enabled" : "disabled"}`);
435
+ lines.push(` current process: ${format_effective_state(state)}`);
436
+ lines.push(` ${state.description}`);
437
+ }
438
+ return lines.join("\n");
439
+ }
440
+ function to_setting_item$1(state) {
441
+ const detail_lines = [
442
+ state.key,
443
+ state.description,
444
+ `current process: ${format_effective_state(state)}`,
445
+ `startup override: ${state.cli_flag}`
446
+ ];
447
+ return {
448
+ id: state.key,
449
+ label: state.label,
450
+ description: detail_lines.join("\n"),
451
+ currentValue: state.saved_enabled ? ENABLED$1 : DISABLED$1,
452
+ values: [ENABLED$1, DISABLED$1]
453
+ };
454
+ }
455
+ function sets_equal$1(a, b) {
456
+ if (a.size !== b.size) return false;
457
+ for (const value of a) if (!b.has(value)) return false;
458
+ return true;
459
+ }
460
+ function search_states(states, query) {
461
+ const normalized = query.trim().toLowerCase();
462
+ if (!normalized) return states;
463
+ return states.filter((state) => [
464
+ state.key,
465
+ state.label,
466
+ state.description,
467
+ ...state.aliases
468
+ ].some((value) => value.toLowerCase().includes(normalized)));
469
+ }
470
+ function save_extension_enabled(key, enabled) {
471
+ const config = load_builtin_extensions_config();
472
+ config.enabled[key] = enabled;
473
+ save_builtin_extensions_config(config);
474
+ }
475
+ function create_extensions_extension(options = {}) {
476
+ const force_disabled = to_force_disabled_set(options.force_disabled);
477
+ return async function extensions(pi) {
478
+ const subs = [
479
+ "list",
480
+ "enable",
481
+ "disable",
482
+ "toggle",
483
+ "search"
484
+ ];
485
+ pi.registerCommand("extensions", {
486
+ description: "Manage built-in my-pi extensions",
487
+ getArgumentCompletions: (prefix) => {
488
+ const parts = prefix.trim().split(/\s+/);
489
+ if (parts.length <= 1) return subs.filter((sub) => sub.startsWith(parts[0] || "")).map((sub) => ({
490
+ value: sub,
491
+ label: sub
492
+ }));
493
+ if ([
494
+ "enable",
495
+ "disable",
496
+ "toggle"
497
+ ].includes(parts[0])) {
498
+ const q = parts.slice(1).join(" ").toLowerCase();
499
+ return resolve_builtin_extension_states(force_disabled).filter((state) => state.key.toLowerCase().includes(q) || state.label.toLowerCase().includes(q)).slice(0, 20).map((state) => ({
500
+ value: `${parts[0]} ${state.key}`,
501
+ label: `${state.key} ${state.saved_enabled ? ENABLED$1 : DISABLED$1}`
502
+ }));
503
+ }
504
+ return null;
505
+ },
506
+ handler: async (args, ctx) => {
507
+ const trimmed = args.trim();
508
+ if (!trimmed && ctx.hasUI) {
509
+ const states = resolve_builtin_extension_states(force_disabled);
510
+ const initial_enabled = new Set(states.filter((state) => state.saved_enabled).map((state) => state.key));
511
+ const current_enabled = new Set(initial_enabled);
512
+ await ctx.ui.custom((tui, theme, _kb, done) => {
513
+ const items = states.map(to_setting_item$1);
514
+ const container = new Container();
515
+ container.addChild({
516
+ render: () => {
517
+ const saved_enabled = current_enabled.size;
518
+ const saved_disabled = states.length - saved_enabled;
519
+ const enabled_now = [...current_enabled].filter((key) => !force_disabled.has(key)).length;
520
+ const disabled_now = states.length - enabled_now;
521
+ return [
522
+ theme.fg("accent", theme.bold("Built-in extensions")),
523
+ theme.fg("muted", `${saved_enabled} saved enabled • ${saved_disabled} saved disabled • ${enabled_now} enabled now • ${disabled_now} disabled now`),
524
+ ""
525
+ ];
526
+ },
527
+ invalidate: () => {}
528
+ });
529
+ const settings_list = new SettingsList(items, Math.min(Math.max(items.length + 4, 8), 16), {
530
+ cursor: theme.fg("accent", "›"),
531
+ label: (text, selected) => selected ? theme.fg("accent", text) : text,
532
+ value: (text, selected) => {
533
+ const color = text === ENABLED$1 ? "success" : "dim";
534
+ const rendered = theme.fg(color, text);
535
+ return selected ? theme.bold(theme.fg("accent", rendered)) : rendered;
536
+ },
537
+ description: (text) => theme.fg("muted", text),
538
+ hint: (text) => theme.fg("dim", text)
539
+ }, (id, new_value) => {
540
+ const key = id;
541
+ const enabled = new_value === ENABLED$1;
542
+ if (enabled) current_enabled.add(key);
543
+ else current_enabled.delete(key);
544
+ save_extension_enabled(key, enabled);
545
+ }, () => done(void 0), { enableSearch: true });
546
+ container.addChild(settings_list);
547
+ container.addChild(new Text(theme.fg("dim", "esc close • search filters • changes save immediately • CLI --no-* flags still win in this process"), 0, 1));
548
+ return {
549
+ render(width) {
550
+ return container.render(width);
551
+ },
552
+ invalidate() {
553
+ container.invalidate();
554
+ },
555
+ handleInput(data) {
556
+ settings_list.handleInput(data);
557
+ tui.requestRender();
558
+ }
559
+ };
560
+ });
561
+ if (!sets_equal$1(initial_enabled, current_enabled)) {
562
+ ctx.ui.notify(force_disabled.size > 0 ? "Reloading to apply updated built-in extensions. CLI --no-* flags still force-disable some extensions in this process." : "Reloading to apply updated built-in extensions...", "info");
563
+ await ctx.reload();
564
+ return;
565
+ }
566
+ return;
567
+ }
568
+ const [sub, ...rest] = (trimmed || "list").split(/\s+/);
569
+ const arg = rest.join(" ");
570
+ const states = resolve_builtin_extension_states(force_disabled);
571
+ switch (sub) {
572
+ case "list":
573
+ ctx.ui.notify(format_extension_lines(states, { heading: "Built-in extensions" }));
574
+ break;
575
+ case "enable":
576
+ case "disable":
577
+ case "toggle": {
578
+ if (!arg) {
579
+ ctx.ui.notify(`Usage: /extensions ${sub} <key>`, "warning");
580
+ return;
581
+ }
582
+ const extension = find_builtin_extension(arg);
583
+ if (!extension) {
584
+ ctx.ui.notify(`Unknown extension: ${arg}. Use: ${BUILTIN_EXTENSIONS.map((item) => item.key).join(", ")}`, "warning");
585
+ return;
586
+ }
587
+ const current_state = states.find((state) => state.key === extension.key);
588
+ const next_enabled = sub === "enable" ? true : sub === "disable" ? false : !current_state?.saved_enabled;
589
+ save_extension_enabled(extension.key, next_enabled);
590
+ ctx.ui.notify(next_enabled && force_disabled.has(extension.key) ? `Enabled ${extension.key} in saved config. Still disabled in this process by ${extension.cli_flag}. /reload or restart without that flag to apply.` : `${extension.key} ${next_enabled ? "enabled" : "disabled"}. /reload to apply.`);
591
+ break;
592
+ }
593
+ case "search": {
594
+ if (!arg) {
595
+ ctx.ui.notify("Usage: /extensions search <query>", "warning");
596
+ return;
597
+ }
598
+ const results = search_states(states, arg);
599
+ if (results.length === 0) {
600
+ ctx.ui.notify(`No built-in extensions matching "${arg}"`);
601
+ return;
602
+ }
603
+ ctx.ui.notify(format_extension_lines(results, { heading: `Built-in extensions matching "${arg}"` }));
604
+ break;
605
+ }
606
+ default: ctx.ui.notify(`Unknown: ${sub}. Use: ${subs.join(", ")}`, "warning");
607
+ }
608
+ }
609
+ });
610
+ };
611
+ }
612
+ create_extensions_extension();
613
+ //#endregion
614
+ //#region src/extensions/filter-output.ts
615
+ const SECRET_PATTERNS = [
616
+ {
617
+ name: "AWS Access Key",
618
+ pattern: /AKIA[A-Z0-9]{16}/g
619
+ },
620
+ {
621
+ name: "AWS Secret Key",
622
+ pattern: /(?:SecretAccessKey|aws_secret_access_key)\s*[:=]\s*[A-Za-z0-9/+=]{40}/g
623
+ },
624
+ {
625
+ name: "Bearer Token",
626
+ pattern: /Bearer\s+[a-zA-Z0-9._-]{20,}/g
627
+ },
628
+ {
629
+ name: "OpenAI/Anthropic API Key",
630
+ pattern: /sk-[a-zA-Z0-9._-]{20,}/g
631
+ },
632
+ {
633
+ name: "Stripe Live Key",
634
+ pattern: /sk_live_[a-zA-Z0-9]{20,}/g
635
+ },
636
+ {
637
+ name: "Stripe Test Key",
638
+ pattern: /sk_test_[a-zA-Z0-9]{20,}/g
639
+ },
640
+ {
641
+ name: "Hetzner Token",
642
+ pattern: /(?:HCLOUD_TOKEN|hcloud_token|token)\s*[:=]\s*["']?[a-f0-9]{64}\b/g
643
+ },
644
+ {
645
+ name: "Private Key",
646
+ pattern: /-----BEGIN\s+[\w\s]*PRIVATE\s+KEY-----/g
647
+ },
648
+ {
649
+ name: "Connection String with Password",
650
+ pattern: /:\/\/[^:]+:[^@]+@/g
651
+ },
652
+ {
653
+ name: "Generic Password Field",
654
+ pattern: /(?:password|passwd|secret|token)\s*[:=]\s*["']?[^\s"']{8,}/gi
655
+ },
656
+ {
657
+ name: "Tavily API Key",
658
+ pattern: /tvly-[a-zA-Z0-9_-]{20,}/g
659
+ },
660
+ {
661
+ name: "Kagi API Key",
662
+ pattern: /[a-zA-Z0-9_-]{40,}\.[a-zA-Z0-9_-]{40,}/g
663
+ },
664
+ {
665
+ name: "Brave API Key",
666
+ pattern: /BSA[A-Z0-9]{20,}/g
667
+ },
668
+ {
669
+ name: "Firecrawl API Key",
670
+ pattern: /fc-[a-f0-9]{32}/g
671
+ },
672
+ {
673
+ name: "GitHub Token",
674
+ pattern: /gh[pousr]_[a-zA-Z0-9]{36,}/g
675
+ }
676
+ ];
677
+ function redact(text) {
678
+ let count = 0;
679
+ let result = text;
680
+ for (const sp of SECRET_PATTERNS) {
681
+ sp.pattern.lastIndex = 0;
682
+ result = result.replace(sp.pattern, (match) => {
683
+ count++;
684
+ return `${match.slice(0, 4)}${"*".repeat(Math.min(match.length - 4, 20))}[REDACTED:${sp.name}]`;
685
+ });
686
+ }
687
+ return {
688
+ redacted: result,
689
+ count
690
+ };
691
+ }
692
+ async function filter_output(pi) {
693
+ let totalRedacted = 0;
694
+ pi.on("tool_result", async (event) => {
695
+ if (!event.content) return;
696
+ let modified = false;
697
+ const newContent = event.content.map((item) => {
698
+ if (item.type !== "text" || !item.text) return item;
699
+ const { redacted, count } = redact(item.text);
700
+ if (count > 0) {
701
+ modified = true;
702
+ totalRedacted += count;
703
+ }
704
+ return {
705
+ ...item,
706
+ text: redacted
707
+ };
708
+ });
709
+ if (modified) return { content: newContent };
710
+ });
711
+ pi.registerCommand("redact-stats", {
712
+ description: "Show how many secrets have been redacted",
713
+ handler: async (_args, ctx) => {
714
+ ctx.ui.notify(`Secrets redacted this session: ${totalRedacted}`);
715
+ }
716
+ });
717
+ }
718
+ //#endregion
719
+ //#region src/extensions/handoff.ts
720
+ async function handoff(pi) {
721
+ const history = [];
722
+ pi.on("message_end", async (event) => {
723
+ const msg = event.message;
724
+ if (!msg) return;
725
+ const content = msg.content;
726
+ if (!Array.isArray(content)) return;
727
+ const text = content.filter((c) => c.type === "text").map((c) => c.text || "").join("\n");
728
+ if (!text) return;
729
+ const summary = text.length > 200 ? text.slice(0, 200) + "..." : text;
730
+ history.push({
731
+ role: msg.role || "unknown",
732
+ summary,
733
+ timestamp: Date.now()
734
+ });
735
+ });
736
+ pi.registerCommand("handoff", {
737
+ description: "Export session context as a handoff prompt for a new session",
738
+ handler: async (args, ctx) => {
739
+ const task = args.trim();
740
+ if (history.length === 0) {
741
+ ctx.ui.notify("No conversation history to hand off", "warning");
742
+ return;
743
+ }
744
+ const handoff = `## Handoff from Previous Session
745
+
746
+ ### Context
747
+ The previous session covered the following:
748
+
749
+ ${history.map((h) => `[${h.role}] ${h.summary}`).join("\n\n")}
750
+
751
+ ### Task
752
+ ${task || "Continue from where the previous session left off."}
753
+
754
+ ### Instructions
755
+ - Review the context above to understand what was done
756
+ - Do NOT repeat work that was already completed
757
+ - Focus on the task described above
758
+ `;
759
+ const filename = `handoff-${Date.now()}.md`;
760
+ writeFileSync(join(ctx.cwd, filename), handoff, "utf-8");
761
+ ctx.ui.notify(`Handoff written to ${filename}\n\nUse: my-pi < ${filename}`);
762
+ }
763
+ });
764
+ }
765
+ //#endregion
766
+ //#region src/mcp/client.ts
767
+ var McpClient = class {
768
+ #proc = null;
769
+ #config;
770
+ #nextId = 1;
771
+ #pending = /* @__PURE__ */ new Map();
772
+ #buffer = "";
773
+ constructor(config) {
774
+ this.#config = config;
775
+ }
776
+ async connect() {
777
+ const { command, args = [], env } = this.#config;
778
+ this.#proc = spawn(command, args, {
779
+ stdio: [
780
+ "pipe",
781
+ "pipe",
782
+ "pipe"
783
+ ],
784
+ env: {
785
+ ...process.env,
786
+ ...env
787
+ }
788
+ });
789
+ this.#proc.stdout.setEncoding("utf8");
790
+ this.#proc.stdout.on("data", (chunk) => {
791
+ this.#buffer += chunk;
792
+ const lines = this.#buffer.split("\n");
793
+ this.#buffer = lines.pop() || "";
794
+ for (const line of lines) {
795
+ if (!line.trim()) continue;
796
+ try {
797
+ const msg = JSON.parse(line);
798
+ if (msg.id != null && this.#pending.has(msg.id)) {
799
+ const p = this.#pending.get(msg.id);
800
+ this.#pending.delete(msg.id);
801
+ if (msg.error) p.reject(/* @__PURE__ */ new Error(`MCP error ${msg.error.code}: ${msg.error.message}`));
802
+ else p.resolve(msg.result);
803
+ }
804
+ } catch {}
805
+ }
806
+ });
807
+ await this.#request("initialize", {
808
+ protocolVersion: "2024-11-05",
809
+ capabilities: {},
810
+ clientInfo: {
811
+ name: "my-pi",
812
+ version: "0.0.1"
813
+ }
814
+ });
815
+ this.#send({
816
+ jsonrpc: "2.0",
817
+ method: "notifications/initialized"
818
+ });
819
+ }
820
+ async listTools() {
821
+ return (await this.#request("tools/list", {})).tools;
822
+ }
823
+ async callTool(name, args) {
824
+ return this.#request("tools/call", {
825
+ name,
826
+ arguments: args
827
+ });
828
+ }
829
+ async disconnect() {
830
+ if (this.#proc) {
831
+ this.#proc.kill();
832
+ this.#proc = null;
833
+ }
834
+ this.#pending.clear();
835
+ }
836
+ #request(method, params) {
837
+ return new Promise((resolve, reject) => {
838
+ const id = this.#nextId++;
839
+ this.#pending.set(id, {
840
+ resolve,
841
+ reject
842
+ });
843
+ this.#send({
844
+ jsonrpc: "2.0",
845
+ id,
846
+ method,
847
+ params
848
+ });
849
+ setTimeout(() => {
850
+ if (this.#pending.has(id)) {
851
+ this.#pending.delete(id);
852
+ reject(/* @__PURE__ */ new Error(`MCP request ${method} timed out`));
853
+ }
854
+ }, 3e4);
855
+ });
856
+ }
857
+ #send(msg) {
858
+ if (!this.#proc?.stdin?.writable) throw new Error("MCP server not connected");
859
+ this.#proc.stdin.write(JSON.stringify(msg) + "\n");
860
+ }
861
+ };
862
+ //#endregion
863
+ //#region src/mcp/config.ts
864
+ function read_config(path) {
865
+ if (!existsSync(path)) return {};
866
+ const raw = readFileSync(path, "utf-8");
867
+ return JSON.parse(raw).mcpServers || {};
868
+ }
869
+ function load_mcp_config(cwd) {
870
+ const global_servers = read_config(join(homedir(), ".pi", "agent", "mcp.json"));
871
+ const project_servers = read_config(join(cwd, "mcp.json"));
872
+ const merged = {
873
+ ...global_servers,
874
+ ...project_servers
875
+ };
876
+ return Object.entries(merged).map(([name, server]) => ({
877
+ name,
878
+ command: server.command,
879
+ args: server.args,
880
+ env: server.env
881
+ }));
882
+ }
883
+ //#endregion
884
+ //#region src/extensions/mcp.ts
885
+ async function mcp(pi) {
886
+ const cwd = process.cwd();
887
+ const servers = /* @__PURE__ */ new Map();
888
+ const configs = load_mcp_config(cwd);
889
+ const results = await Promise.allSettled(configs.map(async (config) => {
890
+ const client = new McpClient(config);
891
+ await client.connect();
892
+ return {
893
+ config,
894
+ client,
895
+ mcp_tools: await client.listTools()
896
+ };
897
+ }));
898
+ for (const result of results) {
899
+ if (result.status === "rejected") {
900
+ console.error(`MCP server failed: ${result.reason}`);
901
+ continue;
902
+ }
903
+ const { config, client, mcp_tools } = result.value;
904
+ const tool_names = [];
905
+ for (const mcp_tool of mcp_tools) {
906
+ const tool_name = `mcp__${config.name}__${mcp_tool.name}`;
907
+ tool_names.push(tool_name);
908
+ pi.registerTool(defineTool({
909
+ name: tool_name,
910
+ label: `${config.name}: ${mcp_tool.name}`,
911
+ description: mcp_tool.description || mcp_tool.name,
912
+ parameters: mcp_tool.inputSchema || {
913
+ type: "object",
914
+ properties: {}
915
+ },
916
+ execute: async (_id, params) => {
917
+ const result = await client.callTool(mcp_tool.name, params);
918
+ return {
919
+ content: [{
920
+ type: "text",
921
+ text: result?.content?.map((c) => c.text || "").join("\n") || JSON.stringify(result)
922
+ }],
923
+ details: {}
924
+ };
925
+ }
926
+ }));
927
+ }
928
+ servers.set(config.name, {
929
+ config,
930
+ client,
931
+ tool_names,
932
+ enabled: true
933
+ });
934
+ }
935
+ pi.registerCommand("mcp", {
936
+ description: "Manage MCP servers (list, enable, disable)",
937
+ getArgumentCompletions: (prefix) => {
938
+ const parts = prefix.split(" ");
939
+ if (parts.length <= 1) return [
940
+ "list",
941
+ "enable",
942
+ "disable"
943
+ ].filter((s) => s.startsWith(prefix)).map((s) => ({
944
+ value: s,
945
+ label: s
946
+ }));
947
+ if (parts[0] === "enable" || parts[0] === "disable") {
948
+ const name_prefix = parts[1] || "";
949
+ return Array.from(servers.keys()).filter((n) => n.startsWith(name_prefix)).map((n) => ({
950
+ value: `${parts[0]} ${n}`,
951
+ label: n
952
+ }));
953
+ }
954
+ return null;
955
+ },
956
+ handler: async (args, ctx) => {
957
+ const [sub, ...rest] = args.trim().split(/\s+/);
958
+ const name = rest.join(" ");
959
+ switch (sub || "list") {
960
+ case "list": {
961
+ if (servers.size === 0) {
962
+ ctx.ui.notify("No MCP servers configured");
963
+ return;
964
+ }
965
+ const lines = [];
966
+ for (const [sname, state] of servers.entries()) {
967
+ const status = state.enabled ? "enabled" : "disabled";
968
+ lines.push(`${sname} (${status}) — ${state.tool_names.length} tools`);
969
+ }
970
+ ctx.ui.notify(lines.join("\n"));
971
+ break;
972
+ }
973
+ case "enable": {
974
+ const server = servers.get(name);
975
+ if (!server) {
976
+ ctx.ui.notify(`Unknown server: ${name}`, "warning");
977
+ return;
978
+ }
979
+ if (server.enabled) {
980
+ ctx.ui.notify(`${name} already enabled`);
981
+ return;
982
+ }
983
+ server.enabled = true;
984
+ const active = pi.getActiveTools();
985
+ pi.setActiveTools([...active, ...server.tool_names]);
986
+ ctx.ui.notify(`Enabled ${name}`);
987
+ break;
988
+ }
989
+ case "disable": {
990
+ const server = servers.get(name);
991
+ if (!server) {
992
+ ctx.ui.notify(`Unknown server: ${name}`, "warning");
993
+ return;
994
+ }
995
+ if (!server.enabled) {
996
+ ctx.ui.notify(`${name} already disabled`);
997
+ return;
998
+ }
999
+ server.enabled = false;
1000
+ const tool_set = new Set(server.tool_names);
1001
+ pi.setActiveTools(pi.getActiveTools().filter((t) => !tool_set.has(t)));
1002
+ ctx.ui.notify(`Disabled ${name}`);
1003
+ break;
1004
+ }
1005
+ default: ctx.ui.notify(`Unknown subcommand: ${sub}. Use list, enable, or disable.`, "warning");
1006
+ }
1007
+ }
1008
+ });
1009
+ pi.on("session_shutdown", async () => {
1010
+ for (const server of servers.values()) await server.client.disconnect();
1011
+ });
1012
+ }
1013
+ //#endregion
1014
+ //#region src/extensions/recall.ts
1015
+ const DEFAULT_DB_PATH = join(process.env.HOME, ".pi", "pirecall.db");
1016
+ async function recall(pi) {
1017
+ if (existsSync(DEFAULT_DB_PATH)) try {
1018
+ execFileSync("npx", [
1019
+ "pirecall",
1020
+ "sync",
1021
+ "--json"
1022
+ ], {
1023
+ encoding: "utf-8",
1024
+ timeout: 3e4,
1025
+ stdio: [
1026
+ "pipe",
1027
+ "pipe",
1028
+ "pipe"
1029
+ ]
1030
+ });
1031
+ } catch {}
1032
+ pi.on("before_agent_start", async (event) => {
1033
+ return { systemPrompt: event.systemPrompt + `
1034
+
1035
+ ## Session Recall
1036
+
1037
+ You have access to past Pi session history via \`npx pirecall\`. Use it when:
1038
+ - The user references prior work ("what did we do", "last time", "remember when")
1039
+ - You need context from a previous session about this project
1040
+ - You want to avoid repeating work already done
1041
+
1042
+ Quick reference:
1043
+ - \`npx pirecall recall "<query>" --json\` — LLM-optimised context retrieval with surrounding messages
1044
+ - \`npx pirecall search "<query>" --json\` — full-text search (supports FTS5: AND, OR, NOT, "phrase", prefix*)
1045
+ - \`npx pirecall search "<query>" --json --project my-pi\` — filter by project
1046
+ - \`npx pirecall search "<query>" --json --after 2026-04-10\` — filter by date
1047
+ - \`npx pirecall sessions --json\` — list recent sessions
1048
+ - \`npx pirecall stats --json\` — database statistics
1049
+
1050
+ Always pass \`--json\` for structured output.` };
1051
+ });
1052
+ }
1053
+ //#endregion
1054
+ //#region src/skills/config.ts
1055
+ const DEFAULT_CONFIG = {
1056
+ version: 1,
1057
+ enabled: {},
1058
+ defaults: "all-disabled"
1059
+ };
1060
+ function get_config_path() {
1061
+ return join(process.env.XDG_CONFIG_HOME || join(homedir(), ".config"), "my-pi", "skills.json");
1062
+ }
1063
+ function load_skills_config() {
1064
+ const path = get_config_path();
1065
+ if (!existsSync(path)) return { ...DEFAULT_CONFIG };
1066
+ try {
1067
+ const raw = readFileSync(path, "utf-8");
1068
+ const parsed = JSON.parse(raw);
1069
+ return {
1070
+ version: parsed.version ?? 1,
1071
+ enabled: parsed.enabled ?? {},
1072
+ defaults: parsed.defaults ?? "all-enabled"
1073
+ };
1074
+ } catch {
1075
+ return { ...DEFAULT_CONFIG };
1076
+ }
1077
+ }
1078
+ function save_skills_config(config) {
1079
+ const path = get_config_path();
1080
+ const dir = dirname(path);
1081
+ if (!existsSync(dir)) mkdirSync(dir, {
1082
+ recursive: true,
1083
+ mode: 448
1084
+ });
1085
+ const tmp = `${path}.tmp-${Date.now()}`;
1086
+ writeFileSync(tmp, JSON.stringify(config, null, " ") + "\n", { mode: 384 });
1087
+ renameSync(tmp, path);
1088
+ }
1089
+ function make_skill_key(name, source) {
1090
+ return `${name}@${source}`;
1091
+ }
1092
+ function is_skill_enabled(config, key) {
1093
+ if (key in config.enabled) return config.enabled[key];
1094
+ return config.defaults === "all-enabled";
1095
+ }
1096
+ //#endregion
1097
+ //#region src/skills/scanner.ts
1098
+ const IMPORT_METADATA_FILE = ".my-pi-source.json";
1099
+ function read_installed_plugins() {
1100
+ const path = join(homedir(), ".claude", "plugins", "installed_plugins.json");
1101
+ if (!existsSync(path)) return null;
1102
+ try {
1103
+ return JSON.parse(readFileSync(path, "utf-8"));
1104
+ } catch {
1105
+ return null;
1106
+ }
1107
+ }
1108
+ function parse_skill_md(skill_path) {
1109
+ try {
1110
+ const { frontmatter } = parseFrontmatter(readFileSync(skill_path, "utf-8"));
1111
+ const description = frontmatter?.description;
1112
+ if (!description) return null;
1113
+ return {
1114
+ name: frontmatter?.name || basename(dirname(skill_path)),
1115
+ description: description.trim()
1116
+ };
1117
+ } catch {
1118
+ return null;
1119
+ }
1120
+ }
1121
+ function read_import_metadata(base_dir) {
1122
+ const metadata_path = join(base_dir, IMPORT_METADATA_FILE);
1123
+ if (!existsSync(metadata_path)) return void 0;
1124
+ try {
1125
+ return JSON.parse(readFileSync(metadata_path, "utf-8"));
1126
+ } catch {
1127
+ return;
1128
+ }
1129
+ }
1130
+ function scan_dir_for_skills(dir, options) {
1131
+ if (!existsSync(dir)) return [];
1132
+ const results = [];
1133
+ const direct = join(dir, "SKILL.md");
1134
+ if ((options.include_direct_root_skill ?? true) && existsSync(direct)) {
1135
+ const parsed = parse_skill_md(direct);
1136
+ if (parsed) results.push({
1137
+ ...parsed,
1138
+ skillPath: direct,
1139
+ baseDir: dir,
1140
+ source: options.source,
1141
+ kind: options.kind,
1142
+ plugin: options.plugin,
1143
+ import_meta: options.kind === "managed" ? read_import_metadata(dir) : void 0
1144
+ });
1145
+ return results;
1146
+ }
1147
+ try {
1148
+ const matches = globSync("*/SKILL.md", { cwd: dir });
1149
+ for (const match of matches) {
1150
+ const full_path = resolve(dir, match);
1151
+ const parsed = parse_skill_md(full_path);
1152
+ if (parsed) {
1153
+ const base_dir = dirname(full_path);
1154
+ results.push({
1155
+ ...parsed,
1156
+ skillPath: full_path,
1157
+ baseDir: base_dir,
1158
+ source: options.source,
1159
+ kind: options.kind,
1160
+ plugin: options.plugin,
1161
+ import_meta: options.kind === "managed" ? read_import_metadata(base_dir) : void 0
1162
+ });
1163
+ }
1164
+ }
1165
+ } catch {}
1166
+ return results;
1167
+ }
1168
+ function dedupe_by_skill_path(skills) {
1169
+ const seen = /* @__PURE__ */ new Set();
1170
+ const deduped = [];
1171
+ for (const skill of skills) {
1172
+ if (seen.has(skill.skillPath)) continue;
1173
+ seen.add(skill.skillPath);
1174
+ deduped.push(skill);
1175
+ }
1176
+ return deduped;
1177
+ }
1178
+ function scan_managed_skills() {
1179
+ const skills = [];
1180
+ for (const skill of scan_dir_for_skills(join(homedir(), ".claude", "skills"), {
1181
+ source: "user-local",
1182
+ kind: "managed"
1183
+ })) skills.push(skill);
1184
+ for (const skill of scan_dir_for_skills(join(homedir(), ".pi", "agent", "skills"), {
1185
+ source: "pi-native",
1186
+ kind: "managed",
1187
+ include_direct_root_skill: false
1188
+ })) skills.push(skill);
1189
+ return dedupe_by_skill_path(skills);
1190
+ }
1191
+ function scan_importable_skills() {
1192
+ const skills = [];
1193
+ const plugins = read_installed_plugins();
1194
+ if (!plugins?.plugins) return skills;
1195
+ for (const [plugin_id, entries] of Object.entries(plugins.plugins)) {
1196
+ const entry = entries[0];
1197
+ if (!entry?.installPath || !existsSync(entry.installPath)) continue;
1198
+ const source = `plugin:${plugin_id}`;
1199
+ const plugin = {
1200
+ pluginId: plugin_id,
1201
+ installPath: entry.installPath,
1202
+ version: entry.version,
1203
+ gitCommitSha: entry.gitCommitSha
1204
+ };
1205
+ for (const skill of scan_dir_for_skills(join(entry.installPath, "skills"), {
1206
+ source,
1207
+ kind: "external",
1208
+ plugin
1209
+ })) skills.push(skill);
1210
+ for (const skill of scan_dir_for_skills(join(entry.installPath, ".pi", "skills"), {
1211
+ source,
1212
+ kind: "external",
1213
+ plugin
1214
+ })) skills.push(skill);
1215
+ const direct_root_skill = join(entry.installPath, "SKILL.md");
1216
+ if (existsSync(direct_root_skill)) {
1217
+ const parsed = parse_skill_md(direct_root_skill);
1218
+ if (parsed) skills.push({
1219
+ ...parsed,
1220
+ skillPath: direct_root_skill,
1221
+ baseDir: entry.installPath,
1222
+ source,
1223
+ kind: "external",
1224
+ plugin
1225
+ });
1226
+ }
1227
+ }
1228
+ return dedupe_by_skill_path(skills);
1229
+ }
1230
+ //#endregion
1231
+ //#region src/skills/importer.ts
1232
+ const IMPORT_METADATA_VERSION = 1;
1233
+ function get_managed_skills_dir() {
1234
+ return join(homedir(), ".pi", "agent", "skills");
1235
+ }
1236
+ function ensure_dir(path) {
1237
+ mkdirSync(path, {
1238
+ recursive: true,
1239
+ mode: 448
1240
+ });
1241
+ }
1242
+ function list_files_recursively(dir) {
1243
+ const files = [];
1244
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
1245
+ const full_path = join(dir, entry.name);
1246
+ if (entry.name === ".my-pi-source.json") continue;
1247
+ if (entry.isDirectory()) {
1248
+ files.push(...list_files_recursively(full_path));
1249
+ continue;
1250
+ }
1251
+ if (entry.isFile()) files.push(full_path);
1252
+ }
1253
+ return files.sort((a, b) => a.localeCompare(b));
1254
+ }
1255
+ function hash_directory(dir) {
1256
+ const hash = createHash("sha256");
1257
+ for (const file of list_files_recursively(dir)) {
1258
+ hash.update(relative(dir, file));
1259
+ hash.update("\0");
1260
+ hash.update(readFileSync(file));
1261
+ hash.update("\0");
1262
+ }
1263
+ return hash.digest("hex");
1264
+ }
1265
+ function read_metadata(base_dir) {
1266
+ const path = join(base_dir, IMPORT_METADATA_FILE);
1267
+ if (!existsSync(path)) return void 0;
1268
+ try {
1269
+ return JSON.parse(readFileSync(path, "utf-8"));
1270
+ } catch {
1271
+ return;
1272
+ }
1273
+ }
1274
+ function write_metadata(base_dir, metadata) {
1275
+ writeFileSync(join(base_dir, IMPORT_METADATA_FILE), JSON.stringify(metadata, null, " ") + "\n", { mode: 384 });
1276
+ }
1277
+ function replace_directory(source_dir, dest_dir) {
1278
+ const parent_dir = dirname(dest_dir);
1279
+ ensure_dir(parent_dir);
1280
+ const tmp_dir = join(parent_dir, `.${resolve(dest_dir).split("/").pop()}.tmp-${Date.now()}`);
1281
+ rmSync(tmp_dir, {
1282
+ recursive: true,
1283
+ force: true
1284
+ });
1285
+ cpSync(source_dir, tmp_dir, {
1286
+ recursive: true,
1287
+ preserveTimestamps: true,
1288
+ verbatimSymlinks: false
1289
+ });
1290
+ rmSync(dest_dir, {
1291
+ recursive: true,
1292
+ force: true
1293
+ });
1294
+ cpSync(tmp_dir, dest_dir, {
1295
+ recursive: true,
1296
+ preserveTimestamps: true,
1297
+ verbatimSymlinks: false
1298
+ });
1299
+ rmSync(tmp_dir, {
1300
+ recursive: true,
1301
+ force: true
1302
+ });
1303
+ }
1304
+ function import_external_skill(skill) {
1305
+ if (skill.kind !== "external") throw new Error(`Skill ${skill.name} is not importable`);
1306
+ const managed_root = get_managed_skills_dir();
1307
+ ensure_dir(managed_root);
1308
+ const skill_dir = join(managed_root, skill.name);
1309
+ if (existsSync(skill_dir)) {
1310
+ if (!statSync(skill_dir).isDirectory()) throw new Error(`${skill_dir} exists and is not a directory`);
1311
+ if (!read_metadata(skill_dir)) throw new Error(`Refusing to overwrite existing unmanaged skill at ${skill_dir}`);
1312
+ }
1313
+ replace_directory(skill.baseDir, skill_dir);
1314
+ const upstream_hash = hash_directory(skill.baseDir);
1315
+ const imported_hash = hash_directory(skill_dir);
1316
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1317
+ const metadata = {
1318
+ version: IMPORT_METADATA_VERSION,
1319
+ source: skill.source,
1320
+ upstream_skill_path: skill.skillPath,
1321
+ upstream_base_dir: skill.baseDir,
1322
+ upstream_install_path: skill.plugin?.installPath,
1323
+ upstream_version: skill.plugin?.version,
1324
+ upstream_git_commit_sha: skill.plugin?.gitCommitSha,
1325
+ imported_at: now,
1326
+ last_synced_at: now,
1327
+ imported_hash,
1328
+ upstream_hash
1329
+ };
1330
+ write_metadata(skill_dir, metadata);
1331
+ return {
1332
+ skillDir: skill_dir,
1333
+ metadata
1334
+ };
1335
+ }
1336
+ function sync_imported_skill(skill) {
1337
+ if (skill.kind !== "managed" || !skill.import_meta) throw new Error(`Skill ${skill.name} is not managed by my-pi sync`);
1338
+ const metadata = skill.import_meta;
1339
+ if (!existsSync(metadata.upstream_base_dir)) throw new Error(`Upstream source no longer exists: ${metadata.upstream_base_dir}`);
1340
+ if (hash_directory(skill.baseDir) !== metadata.imported_hash) throw new Error(`Refusing to sync ${skill.name}; local changes detected in ${skill.baseDir}`);
1341
+ const upstream_hash = hash_directory(metadata.upstream_base_dir);
1342
+ if (upstream_hash === metadata.upstream_hash) return {
1343
+ skillDir: skill.baseDir,
1344
+ metadata,
1345
+ changed: false
1346
+ };
1347
+ replace_directory(metadata.upstream_base_dir, skill.baseDir);
1348
+ const imported_hash = hash_directory(skill.baseDir);
1349
+ const updated = {
1350
+ ...metadata,
1351
+ last_synced_at: (/* @__PURE__ */ new Date()).toISOString(),
1352
+ imported_hash,
1353
+ upstream_hash
1354
+ };
1355
+ write_metadata(skill.baseDir, updated);
1356
+ return {
1357
+ skillDir: skill.baseDir,
1358
+ metadata: updated,
1359
+ changed: true
1360
+ };
1361
+ }
1362
+ //#endregion
1363
+ //#region src/skills/manager.ts
1364
+ function resolve_skill_key(skill) {
1365
+ return make_skill_key(skill.name, skill.source);
1366
+ }
1367
+ function match_skill_by_key_or_name(skills, key_or_name) {
1368
+ const exact_key = skills.find((skill) => resolve_skill_key(skill) === key_or_name);
1369
+ if (exact_key) return exact_key;
1370
+ const by_name = skills.filter((skill) => skill.name === key_or_name);
1371
+ if (by_name.length === 1) return by_name[0];
1372
+ if (by_name.length > 1) throw new Error(`Multiple skills named ${key_or_name}. Use an exact key instead.`);
1373
+ throw new Error(`Unknown skill: ${key_or_name}`);
1374
+ }
1375
+ function create_skills_manager() {
1376
+ let config = load_skills_config();
1377
+ let managed_cache = null;
1378
+ let importable_cache = null;
1379
+ function get_managed() {
1380
+ if (!managed_cache) managed_cache = scan_managed_skills();
1381
+ return managed_cache;
1382
+ }
1383
+ function get_importable() {
1384
+ if (!importable_cache) importable_cache = scan_importable_skills();
1385
+ return importable_cache;
1386
+ }
1387
+ function to_managed(skill) {
1388
+ const key = resolve_skill_key(skill);
1389
+ return {
1390
+ ...skill,
1391
+ key,
1392
+ enabled: skill.kind === "managed" ? is_skill_enabled(config, key) : false
1393
+ };
1394
+ }
1395
+ function get_enabled_managed_skills() {
1396
+ return get_managed().filter((skill) => is_skill_enabled(config, resolve_skill_key(skill))).map(to_managed);
1397
+ }
1398
+ return {
1399
+ discover() {
1400
+ return get_managed().map(to_managed);
1401
+ },
1402
+ discover_importable() {
1403
+ return get_importable().map(to_managed);
1404
+ },
1405
+ is_enabled_by_skill(name, filePath) {
1406
+ const discovered = get_managed();
1407
+ const match = discovered.find((s) => s.skillPath === filePath);
1408
+ if (match) return is_skill_enabled(config, resolve_skill_key(match));
1409
+ const by_name = discovered.find((s) => s.name === name);
1410
+ if (by_name) return is_skill_enabled(config, resolve_skill_key(by_name));
1411
+ return true;
1412
+ },
1413
+ get_enabled_skill_paths() {
1414
+ return get_enabled_managed_skills().map((skill) => skill.baseDir);
1415
+ },
1416
+ enable(key) {
1417
+ config.enabled[key] = true;
1418
+ save_skills_config(config);
1419
+ return true;
1420
+ },
1421
+ disable(key) {
1422
+ config.enabled[key] = false;
1423
+ save_skills_config(config);
1424
+ return false;
1425
+ },
1426
+ toggle(key) {
1427
+ const current = is_skill_enabled(config, key);
1428
+ config.enabled[key] = !current;
1429
+ save_skills_config(config);
1430
+ return !current;
1431
+ },
1432
+ search(query) {
1433
+ const q = query.toLowerCase();
1434
+ return this.discover().filter((s) => s.name.toLowerCase().includes(q) || s.description.toLowerCase().includes(q) || s.source.toLowerCase().includes(q));
1435
+ },
1436
+ search_importable(query) {
1437
+ const q = query.toLowerCase();
1438
+ return this.discover_importable().filter((s) => s.name.toLowerCase().includes(q) || s.description.toLowerCase().includes(q) || s.source.toLowerCase().includes(q));
1439
+ },
1440
+ set_defaults(policy) {
1441
+ config.defaults = policy;
1442
+ save_skills_config(config);
1443
+ },
1444
+ import_skill(key_or_name) {
1445
+ const skill = match_skill_by_key_or_name(get_importable(), key_or_name);
1446
+ const result = import_external_skill(skill);
1447
+ const managed_key = make_skill_key(skill.name, "pi-native");
1448
+ config.enabled[managed_key] = true;
1449
+ save_skills_config(config);
1450
+ this.refresh();
1451
+ return {
1452
+ ...result,
1453
+ key: managed_key
1454
+ };
1455
+ },
1456
+ sync_skill(key_or_name) {
1457
+ const skill = match_skill_by_key_or_name(get_managed(), key_or_name);
1458
+ const result = sync_imported_skill(skill);
1459
+ this.refresh();
1460
+ return {
1461
+ ...result,
1462
+ key: resolve_skill_key(skill)
1463
+ };
1464
+ },
1465
+ refresh() {
1466
+ managed_cache = null;
1467
+ importable_cache = null;
1468
+ config = load_skills_config();
1469
+ }
1470
+ };
1471
+ }
1472
+ //#endregion
1473
+ //#region src/extensions/skills.ts
1474
+ const ENABLED = "[x]";
1475
+ const DISABLED = "[ ]";
1476
+ const SYNC = "[~]";
1477
+ const IMPORTED_LABEL = "[=]";
1478
+ function sort_skills(skills) {
1479
+ return [...skills].sort((a, b) => {
1480
+ const by_name = a.name.localeCompare(b.name);
1481
+ if (by_name !== 0) return by_name;
1482
+ const by_source = a.source.localeCompare(b.source);
1483
+ if (by_source !== 0) return by_source;
1484
+ return a.key.localeCompare(b.key);
1485
+ });
1486
+ }
1487
+ function find_matching_imported_skill(managed_skills, skill) {
1488
+ const exact_match = managed_skills.find((candidate) => candidate.import_meta?.source === skill.source && (candidate.import_meta.upstream_skill_path === skill.skillPath || candidate.import_meta.upstream_base_dir === skill.baseDir));
1489
+ if (exact_match) return exact_match;
1490
+ return managed_skills.find((candidate) => candidate.import_meta?.source === skill.source && candidate.name === skill.name);
1491
+ }
1492
+ function get_importable_state(managed_skills, skill) {
1493
+ const imported = find_matching_imported_skill(managed_skills, skill);
1494
+ if (imported?.import_meta) {
1495
+ const version_changed = Boolean(skill.plugin?.version && imported.import_meta.upstream_version && skill.plugin.version !== imported.import_meta.upstream_version);
1496
+ const sha_changed = Boolean(skill.plugin?.gitCommitSha && imported.import_meta.upstream_git_commit_sha && skill.plugin.gitCommitSha !== imported.import_meta.upstream_git_commit_sha);
1497
+ if (version_changed || sha_changed) return {
1498
+ label: "sync",
1499
+ detail: "Press Enter to sync the imported copy and reload",
1500
+ action: "sync"
1501
+ };
1502
+ return {
1503
+ label: "imported",
1504
+ detail: `Already imported to ${imported.baseDir}`,
1505
+ action: null
1506
+ };
1507
+ }
1508
+ const managed_conflict = managed_skills.find((candidate) => candidate.name === skill.name);
1509
+ if (managed_conflict) return {
1510
+ label: "managed",
1511
+ detail: `Already managed at ${managed_conflict.baseDir}`,
1512
+ action: null
1513
+ };
1514
+ return {
1515
+ label: "import",
1516
+ detail: "Press Enter to import into pi-native skills and reload",
1517
+ action: "import"
1518
+ };
1519
+ }
1520
+ function to_setting_item(skill) {
1521
+ const detail_lines = [
1522
+ `${skill.source} • ${skill.key}`,
1523
+ skill.description,
1524
+ skill.baseDir
1525
+ ];
1526
+ if (skill.import_meta?.upstream_version) detail_lines.push(`upstream: ${skill.import_meta.upstream_version}${skill.import_meta.upstream_git_commit_sha ? ` • ${skill.import_meta.upstream_git_commit_sha.slice(0, 12)}` : ""}`);
1527
+ return {
1528
+ id: skill.key,
1529
+ label: skill.name,
1530
+ description: detail_lines.join("\n"),
1531
+ currentValue: skill.enabled ? ENABLED : DISABLED,
1532
+ values: [ENABLED, DISABLED]
1533
+ };
1534
+ }
1535
+ function to_importable_setting_item(managed_skills, skill) {
1536
+ const state = get_importable_state(managed_skills, skill);
1537
+ const detail_lines = [
1538
+ `${skill.source} • ${skill.key}`,
1539
+ skill.description,
1540
+ skill.baseDir
1541
+ ];
1542
+ if (skill.plugin?.version) detail_lines.push(`plugin: ${skill.plugin.version}${skill.plugin.gitCommitSha ? ` • ${skill.plugin.gitCommitSha.slice(0, 12)}` : ""}`);
1543
+ if (state.action === "import") return {
1544
+ id: skill.key,
1545
+ label: skill.name,
1546
+ description: detail_lines.join("\n"),
1547
+ currentValue: DISABLED,
1548
+ values: [ENABLED, DISABLED]
1549
+ };
1550
+ if (state.action === "sync") {
1551
+ detail_lines.push("enter to sync");
1552
+ return {
1553
+ id: skill.key,
1554
+ label: skill.name,
1555
+ description: detail_lines.join("\n"),
1556
+ currentValue: SYNC,
1557
+ values: [SYNC]
1558
+ };
1559
+ }
1560
+ detail_lines.push(state.detail);
1561
+ return {
1562
+ id: skill.key,
1563
+ label: skill.name,
1564
+ description: detail_lines.join("\n"),
1565
+ currentValue: IMPORTED_LABEL
1566
+ };
1567
+ }
1568
+ function sets_equal(a, b) {
1569
+ if (a.size !== b.size) return false;
1570
+ for (const value of a) if (!b.has(value)) return false;
1571
+ return true;
1572
+ }
1573
+ async function skills(pi) {
1574
+ const mgr = create_skills_manager();
1575
+ const subs = [
1576
+ "import",
1577
+ "sync",
1578
+ "refresh",
1579
+ "defaults"
1580
+ ];
1581
+ pi.registerCommand("skills", {
1582
+ description: "Manage pi-native skills and import external skills",
1583
+ getArgumentCompletions: (prefix) => {
1584
+ const parts = prefix.trim().split(/\s+/);
1585
+ if (parts.length <= 1) return subs.filter((s) => s.startsWith(parts[0] || "")).map((s) => ({
1586
+ value: s,
1587
+ label: s
1588
+ }));
1589
+ if (parts[0] === "import") {
1590
+ const q = parts.slice(1).join(" ").toLowerCase();
1591
+ return sort_skills(mgr.discover_importable()).filter((s) => s.key.toLowerCase().includes(q) || s.name.toLowerCase().includes(q)).slice(0, 20).map((s) => ({
1592
+ value: `${parts[0]} ${s.key}`,
1593
+ label: s.key
1594
+ }));
1595
+ }
1596
+ if (parts[0] === "sync") {
1597
+ const q = parts.slice(1).join(" ").toLowerCase();
1598
+ return sort_skills(mgr.discover().filter((skill) => Boolean(skill.import_meta))).filter((s) => s.key.toLowerCase().includes(q) || s.name.toLowerCase().includes(q)).slice(0, 20).map((s) => ({
1599
+ value: `${parts[0]} ${s.key}`,
1600
+ label: s.key
1601
+ }));
1602
+ }
1603
+ return null;
1604
+ },
1605
+ handler: async (args, ctx) => {
1606
+ const trimmed = args.trim();
1607
+ if (!trimmed && ctx.hasUI) {
1608
+ const discovered = sort_skills(mgr.discover());
1609
+ const importable = sort_skills(mgr.discover_importable());
1610
+ if (discovered.length === 0 && importable.length === 0) {
1611
+ ctx.ui.notify("No managed or importable skills found");
1612
+ return;
1613
+ }
1614
+ const initial_enabled = new Set(discovered.filter((skill) => skill.enabled).map((skill) => skill.key));
1615
+ const current_enabled = new Set(initial_enabled);
1616
+ const queued_imports = /* @__PURE__ */ new Set();
1617
+ let reload_notice = null;
1618
+ const managed_items = discovered.map(to_setting_item);
1619
+ const importable_items = importable.map((skill) => to_importable_setting_item(discovered, skill));
1620
+ const all_items = [];
1621
+ if (managed_items.length > 0) {
1622
+ all_items.push({
1623
+ id: "__header_managed__",
1624
+ label: `── Managed (${managed_items.length}) ──`,
1625
+ description: "",
1626
+ currentValue: ""
1627
+ });
1628
+ all_items.push(...managed_items);
1629
+ }
1630
+ if (importable_items.length > 0) {
1631
+ all_items.push({
1632
+ id: "__header_importable__",
1633
+ label: `── Importable (${importable_items.length}) ──`,
1634
+ description: "",
1635
+ currentValue: ""
1636
+ });
1637
+ all_items.push(...importable_items);
1638
+ }
1639
+ const managed_keys = new Set(discovered.map((s) => s.key));
1640
+ const importable_map = new Map(importable.map((s) => [s.key, s]));
1641
+ await ctx.ui.custom((tui, theme, _kb, done) => {
1642
+ const list = new SettingsList(all_items, Math.min(Math.max(all_items.length + 4, 8), 22), {
1643
+ cursor: theme.fg("accent", "›"),
1644
+ label: (text, selected) => {
1645
+ if (text.startsWith("──") && text.endsWith("──")) return theme.fg("dim", theme.bold(text));
1646
+ return selected ? theme.fg("accent", text) : text;
1647
+ },
1648
+ value: (text, selected) => {
1649
+ const color = text === ENABLED ? "success" : text === SYNC ? "warning" : text === IMPORTED_LABEL ? "success" : "dim";
1650
+ const rendered = theme.fg(color, text);
1651
+ return selected ? theme.bold(theme.fg("accent", rendered)) : rendered;
1652
+ },
1653
+ description: (text) => theme.fg("muted", text),
1654
+ hint: (text) => theme.fg("dim", text)
1655
+ }, (id, new_value) => {
1656
+ if (id.startsWith("__header_")) return;
1657
+ if (managed_keys.has(id)) {
1658
+ if (new_value === ENABLED) {
1659
+ current_enabled.add(id);
1660
+ mgr.enable(id);
1661
+ } else {
1662
+ current_enabled.delete(id);
1663
+ mgr.disable(id);
1664
+ }
1665
+ return;
1666
+ }
1667
+ const import_skill = importable_map.get(id);
1668
+ if (!import_skill) return;
1669
+ const state = get_importable_state(discovered, import_skill);
1670
+ if (state.action === "import") {
1671
+ if (new_value === ENABLED) queued_imports.add(id);
1672
+ else queued_imports.delete(id);
1673
+ return;
1674
+ }
1675
+ if (state.action === "sync") {
1676
+ const imported_skill = find_matching_imported_skill(discovered, import_skill);
1677
+ if (!imported_skill) {
1678
+ ctx.ui.notify(`Imported copy for ${import_skill.name} was not found`, "warning");
1679
+ return;
1680
+ }
1681
+ try {
1682
+ if (mgr.sync_skill(imported_skill.key).changed) {
1683
+ reload_notice = `Synced ${import_skill.name}. Reloading...`;
1684
+ done(void 0);
1685
+ } else ctx.ui.notify(`${import_skill.name} is already up to date.`, "info");
1686
+ } catch (error) {
1687
+ ctx.ui.notify(error instanceof Error ? error.message : String(error), "warning");
1688
+ }
1689
+ }
1690
+ }, () => done(void 0), { enableSearch: true });
1691
+ const container = new Container();
1692
+ container.addChild({
1693
+ render: () => {
1694
+ const enabled = current_enabled.size;
1695
+ const disabled = discovered.length - enabled;
1696
+ const queued = queued_imports.size;
1697
+ const parts = [`${enabled} enabled`, `${disabled} disabled`];
1698
+ if (importable.length > 0) parts.push(`${importable.length} importable`);
1699
+ if (queued > 0) parts.push(`${queued} queued for import`);
1700
+ return [
1701
+ theme.fg("accent", theme.bold("Skills")),
1702
+ theme.fg("muted", parts.join(" • ")),
1703
+ ""
1704
+ ];
1705
+ },
1706
+ invalidate: () => {}
1707
+ });
1708
+ container.addChild({
1709
+ render(width) {
1710
+ return list.render(width);
1711
+ },
1712
+ invalidate() {
1713
+ list.invalidate();
1714
+ }
1715
+ });
1716
+ container.addChild(new Text(theme.fg("dim", "search filters • enter toggles • esc close"), 0, 1));
1717
+ return {
1718
+ render(width) {
1719
+ return container.render(width);
1720
+ },
1721
+ invalidate() {
1722
+ container.invalidate();
1723
+ },
1724
+ handleInput(data) {
1725
+ list.handleInput(data);
1726
+ tui.requestRender();
1727
+ }
1728
+ };
1729
+ });
1730
+ if (queued_imports.size > 0) {
1731
+ const imported_names = [];
1732
+ for (const key of queued_imports) try {
1733
+ mgr.import_skill(key);
1734
+ imported_names.push(key);
1735
+ } catch (error) {
1736
+ ctx.ui.notify(error instanceof Error ? error.message : String(error), "warning");
1737
+ }
1738
+ if (imported_names.length > 0) reload_notice = `Imported ${imported_names.length} skill(s). Reloading...`;
1739
+ }
1740
+ if (reload_notice) {
1741
+ ctx.ui.notify(reload_notice, "info");
1742
+ await ctx.reload();
1743
+ return;
1744
+ }
1745
+ if (!sets_equal(initial_enabled, current_enabled)) {
1746
+ ctx.ui.notify("Reloading to apply updated skills...", "info");
1747
+ await ctx.reload();
1748
+ return;
1749
+ }
1750
+ return;
1751
+ }
1752
+ const [sub, ...rest] = (trimmed || "list").split(/\s+/);
1753
+ const arg = rest.join(" ");
1754
+ switch (sub) {
1755
+ case "import":
1756
+ if (!arg) {
1757
+ ctx.ui.notify("Usage: /skills import <key|name>", "warning");
1758
+ return;
1759
+ }
1760
+ try {
1761
+ const result = mgr.import_skill(arg);
1762
+ ctx.ui.notify(`Imported ${arg} to ${result.skillDir}. Reloading...`, "info");
1763
+ await ctx.reload();
1764
+ return;
1765
+ } catch (error) {
1766
+ ctx.ui.notify(error instanceof Error ? error.message : String(error), "warning");
1767
+ return;
1768
+ }
1769
+ case "sync":
1770
+ if (!arg) {
1771
+ ctx.ui.notify("Usage: /skills sync <key|name>", "warning");
1772
+ return;
1773
+ }
1774
+ try {
1775
+ const result = mgr.sync_skill(arg);
1776
+ ctx.ui.notify(result.changed ? `Synced ${arg}. Reloading...` : `${arg} is already up to date.`, "info");
1777
+ if (result.changed) await ctx.reload();
1778
+ return;
1779
+ } catch (error) {
1780
+ ctx.ui.notify(error instanceof Error ? error.message : String(error), "warning");
1781
+ return;
1782
+ }
1783
+ case "refresh":
1784
+ mgr.refresh();
1785
+ ctx.ui.notify(`Rescanned: ${mgr.discover().length} managed skills, ${mgr.discover_importable().length} importable skills found`);
1786
+ break;
1787
+ case "defaults":
1788
+ if (arg !== "all-enabled" && arg !== "all-disabled") {
1789
+ ctx.ui.notify("Usage: /skills defaults <all-enabled|all-disabled>", "warning");
1790
+ return;
1791
+ }
1792
+ mgr.set_defaults(arg);
1793
+ ctx.ui.notify(`Default policy: ${arg}`);
1794
+ break;
1795
+ default: ctx.ui.notify(`Unknown: ${sub}. Use: ${subs.join(", ")}`, "warning");
1796
+ }
1797
+ }
1798
+ });
1799
+ }
1800
+ //#endregion
1801
+ //#region src/api.ts
1802
+ const BUILTIN_EXTENSION_FACTORIES = {
1803
+ mcp,
1804
+ skills,
1805
+ chain,
1806
+ "filter-output": filter_output,
1807
+ handoff,
1808
+ recall
1809
+ };
1810
+ function get_force_disabled_builtins(options) {
1811
+ const force_disabled = /* @__PURE__ */ new Set();
1812
+ if (!options.mcp) force_disabled.add("mcp");
1813
+ if (!options.skills) force_disabled.add("skills");
1814
+ if (!options.chain) force_disabled.add("chain");
1815
+ if (!options.filter_output) force_disabled.add("filter-output");
1816
+ if (!options.handoff) force_disabled.add("handoff");
1817
+ if (!options.recall) force_disabled.add("recall");
1818
+ return force_disabled;
1819
+ }
1820
+ function create_builtin_extension_factory(key, extension, force_disabled) {
1821
+ return async (pi) => {
1822
+ if (!is_builtin_extension_active(load_builtin_extensions_config(), key, force_disabled)) return;
1823
+ await extension(pi);
1824
+ };
1825
+ }
1826
+ function create_extensions_override(managed_inline_paths) {
1827
+ const managed_paths = new Set(managed_inline_paths);
1828
+ return (base) => {
1829
+ const managed = new Map(base.extensions.map((extension) => [extension.path, extension]));
1830
+ const ordered_managed = managed_inline_paths.map((path) => managed.get(path)).filter((extension) => Boolean(extension));
1831
+ const others = base.extensions.filter((extension) => !managed_paths.has(extension.path));
1832
+ return {
1833
+ ...base,
1834
+ extensions: [...ordered_managed, ...others]
1835
+ };
1836
+ };
1837
+ }
1838
+ async function create_my_pi(options = {}) {
1839
+ const { cwd = process.cwd(), extensions = [], extensionFactories: user_factories = [], mcp = true, skills = true, chain = true, filter_output = true, handoff = true, recall = true, model } = options;
1840
+ const resolved_extensions = extensions.map((p) => resolve(cwd, p));
1841
+ const force_disabled = get_force_disabled_builtins({
1842
+ mcp,
1843
+ skills,
1844
+ chain,
1845
+ filter_output,
1846
+ handoff,
1847
+ recall
1848
+ });
1849
+ const managed_extension_factories = [create_extensions_extension({ force_disabled }), ...BUILTIN_EXTENSIONS.map((extension) => create_builtin_extension_factory(extension.key, BUILTIN_EXTENSION_FACTORIES[extension.key], force_disabled))];
1850
+ const managed_inline_paths = managed_extension_factories.map((_, index) => `<inline:${index + 1}>`);
1851
+ const create_runtime = async ({ cwd: runtime_cwd, sessionManager, sessionStartEvent }) => {
1852
+ const settings_manager = model ? (() => {
1853
+ const sm = SettingsManager.create(runtime_cwd);
1854
+ sm.setDefaultModel(model);
1855
+ return sm;
1856
+ })() : void 0;
1857
+ const services = await createAgentSessionServices({
1858
+ cwd: runtime_cwd,
1859
+ ...settings_manager ? { settingsManager: settings_manager } : {},
1860
+ resourceLoaderOptions: {
1861
+ additionalExtensionPaths: [...resolved_extensions],
1862
+ extensionFactories: [...managed_extension_factories, ...user_factories],
1863
+ extensionsOverride: create_extensions_override(managed_inline_paths),
1864
+ skillsOverride: (base) => {
1865
+ if (!is_builtin_extension_active(load_builtin_extensions_config(), "skills", force_disabled)) return base;
1866
+ const skills_manager = create_skills_manager();
1867
+ return {
1868
+ ...base,
1869
+ skills: base.skills.filter((skill) => skills_manager.is_enabled_by_skill(skill.name, skill.filePath))
1870
+ };
1871
+ }
1872
+ }
1873
+ });
1874
+ return {
1875
+ ...await createAgentSessionFromServices({
1876
+ services,
1877
+ sessionManager,
1878
+ sessionStartEvent
1879
+ }),
1880
+ services,
1881
+ diagnostics: services.diagnostics
1882
+ };
1883
+ };
1884
+ return createAgentSessionRuntime(create_runtime, {
1885
+ cwd,
1886
+ agentDir: getAgentDir(),
1887
+ sessionManager: SessionManager.create(cwd)
1888
+ });
1889
+ }
1890
+ //#endregion
1891
+ export { create_my_pi as n, runPrintMode$1 as r, InteractiveMode$1 as t };
1892
+
1893
+ //# sourceMappingURL=api-B6KnhtN9.js.map