@difflab/pi 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,10 +1,106 @@
1
- // src/mcp.ts
2
- import { mkdir, readFile, writeFile } from "node:fs/promises";
1
+ // src/config.ts
2
+ import { parseFrontmatter } from "@earendil-works/pi-coding-agent";
3
+ import { z } from "zod";
3
4
  import { homedir } from "node:os";
4
- import { dirname, join } from "node:path";
5
+ import { join } from "node:path";
6
+
7
+ // src/fsx.ts
8
+ import { readdir, readFile } from "node:fs/promises";
9
+ async function readDirectoryIfExists(path) {
10
+ try {
11
+ return await readdir(path, { withFileTypes: true });
12
+ } catch (error) {
13
+ if (isMissingPath(error))
14
+ return [];
15
+ throw error;
16
+ }
17
+ }
18
+ async function readTextIfExists(path) {
19
+ try {
20
+ return await readFile(path, "utf8");
21
+ } catch (error) {
22
+ if (isMissingPath(error))
23
+ return;
24
+ throw error;
25
+ }
26
+ }
27
+ function isMissingPath(error) {
28
+ return error instanceof Error && "code" in error && error.code === "ENOENT";
29
+ }
30
+
31
+ // src/config.ts
32
+ var modelReferenceSchema = z.string().trim().min(1);
33
+ var agentConfigSchema = z.object({
34
+ models: z.array(modelReferenceSchema).optional()
35
+ }).strict();
36
+ var diffpiConfigSchema = z.object({
37
+ agents: z.record(z.string(), agentConfigSchema).optional()
38
+ }).strict();
39
+ function diffpiConfigPaths(homeDir = homedir()) {
40
+ const directory = join(homeDir, ".difflab", "diffpi");
41
+ return {
42
+ yaml: join(directory, "config.yaml"),
43
+ json: join(directory, "config.json")
44
+ };
45
+ }
46
+ async function loadDiffpiConfig(options = {}) {
47
+ const paths = diffpiConfigPaths(options.homeDir);
48
+ for (const [format, path] of [
49
+ ["yaml", paths.yaml],
50
+ ["json", paths.json]
51
+ ]) {
52
+ const content = await readTextIfExists(path);
53
+ if (content === undefined)
54
+ continue;
55
+ try {
56
+ const value = format === "yaml" ? parseYamlConfig(content) : JSON.parse(content);
57
+ return { config: diffpiConfigSchema.parse(value ?? {}), path };
58
+ } catch (error) {
59
+ const reason = error instanceof Error ? error.message : String(error);
60
+ throw new Error(`Invalid Diffpi config at ${path}: ${reason}`, { cause: error });
61
+ }
62
+ }
63
+ return { config: {} };
64
+ }
65
+ function resolveAgentModelPreferences(agentId, profilePreferences, config) {
66
+ const override = config.agents?.[agentId];
67
+ if (override && Object.hasOwn(override, "models"))
68
+ return [...override.models ?? []];
69
+ return [...profilePreferences];
70
+ }
71
+ function findPreferredModel(models, preference) {
72
+ const normalizedPreference = normalizeModelReference(preference);
73
+ const exactReference = models.find((model) => normalizeModelReference(`${model.provider}/${model.id}`) === normalizedPreference);
74
+ if (exactReference)
75
+ return exactReference;
76
+ const idPreference = preference.includes("/") ? preference.slice(preference.indexOf("/") + 1) : preference;
77
+ const normalizedIdPreference = normalizeModelReference(idPreference);
78
+ const exactId = models.find((model) => normalizeModelReference(model.id) === normalizedIdPreference);
79
+ if (exactId)
80
+ return exactId;
81
+ const preferenceTokens = normalizedIdPreference.split("-").filter(Boolean);
82
+ return models.find((model) => {
83
+ const modelTokens = new Set(normalizeModelReference(model.id).split("-").filter(Boolean));
84
+ return preferenceTokens.every((token) => modelTokens.has(token));
85
+ });
86
+ }
87
+ function parseYamlConfig(content) {
88
+ const document = content.replace(/^\uFEFF/, "").replace(/^---[^\S\r\n]*(?:#.*)?(?:\r?\n|$)/, "");
89
+ return parseFrontmatter(`---
90
+ ${document}
91
+ ---
92
+ `).frontmatter;
93
+ }
94
+ function normalizeModelReference(value) {
95
+ return value.toLowerCase().replace(/^~/, "").replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
96
+ }
97
+ // src/mcp.ts
98
+ import { mkdir, readFile as readFile2, writeFile } from "node:fs/promises";
99
+ import { homedir as homedir2 } from "node:os";
100
+ import { dirname, join as join2 } from "node:path";
5
101
  var mcp = {
6
- globalConfigPath(homeDir = homedir()) {
7
- return join(homeDir, ".config", "mcp", "mcp.json");
102
+ globalConfigPath(homeDir = homedir2()) {
103
+ return join2(homeDir, ".config", "mcp", "mcp.json");
8
104
  },
9
105
  async serversEnsure(servers, options = {}) {
10
106
  const path = options.path ?? mcp.globalConfigPath();
@@ -47,7 +143,7 @@ function getParsedConfig(content, path) {
47
143
  }
48
144
  async function getOptionalFile(path) {
49
145
  try {
50
- return await readFile(path, "utf8");
146
+ return await readFile2(path, "utf8");
51
147
  } catch (error) {
52
148
  if (error instanceof Error && "code" in error && error.code === "ENOENT")
53
149
  return;
@@ -58,14 +154,14 @@ function isRecord(value) {
58
154
  return value !== null && typeof value === "object" && !Array.isArray(value);
59
155
  }
60
156
  // src/mise.ts
61
- import { mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2 } from "node:fs/promises";
62
- import { homedir as homedir2 } from "node:os";
63
- import { basename, dirname as dirname2, join as join3 } from "node:path";
157
+ import { mkdir as mkdir2, readFile as readFile3, writeFile as writeFile2 } from "node:fs/promises";
158
+ import { homedir as homedir3 } from "node:os";
159
+ import { basename, dirname as dirname2, join as join4 } from "node:path";
64
160
 
65
161
  // src/process.ts
66
162
  import { constants } from "node:fs";
67
163
  import { access } from "node:fs/promises";
68
- import { delimiter, join as join2 } from "node:path";
164
+ import { delimiter, join as join3 } from "node:path";
69
165
  import { spawn } from "node:child_process";
70
166
  var MAX_CAPTURED_OUTPUT_LENGTH = 65536;
71
167
  async function findExecutable(name) {
@@ -80,7 +176,7 @@ async function findExecutable(name) {
80
176
  for (const directory of (process.env.PATH ?? "").split(delimiter)) {
81
177
  if (!directory)
82
178
  continue;
83
- const candidate = join2(directory, name);
179
+ const candidate = join3(directory, name);
84
180
  try {
85
181
  await access(candidate, constants.X_OK);
86
182
  return candidate;
@@ -127,11 +223,11 @@ var mise = {
127
223
  return findExecutable(name);
128
224
  },
129
225
  async install(options = {}) {
130
- const homeDir = options.homeDir ?? homedir2();
226
+ const homeDir = options.homeDir ?? homedir3();
131
227
  const platform = options.platform ?? process.platform;
132
228
  if (platform === "win32")
133
229
  throw new Error("Automatic mise installation supports macOS and Linux only.");
134
- const installedPath = join3(homeDir, ".local", "bin", "mise");
230
+ const installedPath = join4(homeDir, ".local", "bin", "mise");
135
231
  if (options.dryRun)
136
232
  return installedPath;
137
233
  await runChecked("sh", ["-c", "curl -fsSL https://mise.run | sh"]);
@@ -141,7 +237,7 @@ var mise = {
141
237
  return executable;
142
238
  },
143
239
  async hookEnsure(executable, options = {}) {
144
- const homeDir = options.homeDir ?? homedir2();
240
+ const homeDir = options.homeDir ?? homedir3();
145
241
  const hook = getShellHook(basename(options.shell ?? process.env.SHELL ?? ""), executable, homeDir);
146
242
  const current = await getOptionalFile2(hook.path);
147
243
  if (current.includes(MISE_HOOK_START))
@@ -169,7 +265,7 @@ var mise = {
169
265
  async toolInstallLocal(executable, specification, cwd = process.cwd()) {
170
266
  await runChecked(executable, ["use", "--path", cwd, specification], { cwd });
171
267
  },
172
- async toolUpdateAllGlobal(executable, homeDir = homedir2()) {
268
+ async toolUpdateAllGlobal(executable, homeDir = homedir3()) {
173
269
  await runChecked(executable, ["upgrade"], { cwd: homeDir });
174
270
  }
175
271
  };
@@ -178,7 +274,7 @@ function getShellHook(shell, executable, homeDir) {
178
274
  switch (shell.toLowerCase()) {
179
275
  case "zsh":
180
276
  return {
181
- path: join3(homeDir, ".zshrc"),
277
+ path: join4(homeDir, ".zshrc"),
182
278
  content: `${MISE_HOOK_START}
183
279
  eval "$(${command} activate zsh)"
184
280
  ${MISE_HOOK_END}
@@ -186,7 +282,7 @@ ${MISE_HOOK_END}
186
282
  };
187
283
  case "fish":
188
284
  return {
189
- path: join3(homeDir, ".config", "fish", "config.fish"),
285
+ path: join4(homeDir, ".config", "fish", "config.fish"),
190
286
  content: `${MISE_HOOK_START}
191
287
  ${command} activate fish | source
192
288
  ${MISE_HOOK_END}
@@ -195,7 +291,7 @@ ${MISE_HOOK_END}
195
291
  case "nu":
196
292
  case "nushell":
197
293
  return {
198
- path: join3(homeDir, ".config", "nushell", "config.nu"),
294
+ path: join4(homeDir, ".config", "nushell", "config.nu"),
199
295
  content: `${MISE_HOOK_START}
200
296
  let mise_bin = ${command}
201
297
  let mise_path = $nu.default-config-dir | path join mise.nu
@@ -206,7 +302,7 @@ ${MISE_HOOK_END}
206
302
  };
207
303
  case "xonsh":
208
304
  return {
209
- path: join3(homeDir, ".xonshrc"),
305
+ path: join4(homeDir, ".xonshrc"),
210
306
  content: `${MISE_HOOK_START}
211
307
  execx($(${command} activate xonsh))
212
308
  ${MISE_HOOK_END}
@@ -214,7 +310,7 @@ ${MISE_HOOK_END}
214
310
  };
215
311
  case "elvish":
216
312
  return {
217
- path: join3(homeDir, ".config", "elvish", "rc.elv"),
313
+ path: join4(homeDir, ".config", "elvish", "rc.elv"),
218
314
  content: `${MISE_HOOK_START}
219
315
  var mise: = (ns [&])
220
316
  eval (${command} activate elvish | slurp) &ns=$mise: &on-end={|ns| set mise: = $ns }
@@ -225,7 +321,7 @@ ${MISE_HOOK_END}
225
321
  case "pwsh":
226
322
  case "powershell":
227
323
  return {
228
- path: join3(homeDir, ".config", "powershell", "Microsoft.PowerShell_profile.ps1"),
324
+ path: join4(homeDir, ".config", "powershell", "Microsoft.PowerShell_profile.ps1"),
229
325
  content: `${MISE_HOOK_START}
230
326
  (& ${command} activate pwsh) | Out-String | Invoke-Expression
231
327
  ${MISE_HOOK_END}
@@ -234,7 +330,7 @@ ${MISE_HOOK_END}
234
330
  case "bash":
235
331
  default:
236
332
  return {
237
- path: join3(homeDir, ".bashrc"),
333
+ path: join4(homeDir, ".bashrc"),
238
334
  content: `${MISE_HOOK_START}
239
335
  eval "$(${command} activate bash)"
240
336
  ${MISE_HOOK_END}
@@ -273,7 +369,7 @@ function isVersionAtLeast(version, minimumVersion) {
273
369
  }
274
370
  async function getOptionalFile2(path) {
275
371
  try {
276
- return await readFile2(path, "utf8");
372
+ return await readFile3(path, "utf8");
277
373
  } catch (error) {
278
374
  if (error instanceof Error && "code" in error && error.code === "ENOENT")
279
375
  return "";
@@ -283,80 +379,354 @@ async function getOptionalFile2(path) {
283
379
  function getShellQuoted(value) {
284
380
  return `'${value.replaceAll("'", "'\\''")}'`;
285
381
  }
286
- // src/pi.ts
287
- import { mkdir as mkdir3, readFile as readFile3, writeFile as writeFile3 } from "node:fs/promises";
288
- import { homedir as homedir3 } from "node:os";
289
- import { dirname as dirname3, join as join4 } from "node:path";
290
- var pi = {
291
- async executableCheck() {
292
- return findExecutable("pi");
293
- },
294
- async packageList(executable) {
295
- return (await runChecked(executable, ["list"])).stdout;
296
- },
297
- packageCheck(listOutput, source) {
298
- if (listOutput.includes(source))
299
- return true;
300
- return source.startsWith("https://") && listOutput.includes(source.slice("https://".length));
301
- },
302
- async packageInstall(executable, source) {
303
- await runChecked(executable, ["install", source]);
304
- },
305
- agentDir(homeDir = homedir3()) {
306
- return getAgentDir(homeDir);
307
- },
308
- async skillCheckGlobal(name, agentDir = getAgentDir(), sharedSkillsDir = join4(homedir3(), ".agents", "skills")) {
309
- const roots = [join4(agentDir, "skills"), sharedSkillsDir];
310
- for (const root of roots) {
311
- if (await getOptionalFile3(join4(root, name, "SKILL.md")) !== undefined)
312
- return true;
382
+ // src/modes.ts
383
+ import {
384
+ getAgentDir,
385
+ parseFrontmatter as parseFrontmatter2
386
+ } from "@earendil-works/pi-coding-agent";
387
+ import { readFile as readFile4 } from "node:fs/promises";
388
+ import { homedir as homedir4 } from "node:os";
389
+ import { basename as basename2, extname, join as join6 } from "node:path";
390
+
391
+ // src/assets.ts
392
+ import { existsSync } from "node:fs";
393
+ import { dirname as dirname3, join as join5 } from "node:path";
394
+ import { fileURLToPath } from "node:url";
395
+ function resolveBundledAgentsDir(moduleUrl = import.meta.url) {
396
+ const moduleDir = dirname3(fileURLToPath(moduleUrl));
397
+ const candidates = [
398
+ join5(moduleDir, "agents"),
399
+ join5(moduleDir, "..", "agents"),
400
+ join5(moduleDir, "..", "..", "agents")
401
+ ];
402
+ return candidates.find((path) => existsSync(path)) ?? candidates[1];
403
+ }
404
+
405
+ // src/modes.ts
406
+ async function discoverAgentModes(options) {
407
+ const agentDir = options.agentDir ?? getAgentDir();
408
+ const homeDir = options.homeDir ?? homedir4();
409
+ const modes = new Map;
410
+ const diagnostics = [];
411
+ await loadAgentModes(options.bundledAgentsDir ?? BUNDLED_AGENTS_DIR, "diffpi agent", modes, diagnostics);
412
+ await loadAgentModes(join6(agentDir, "agents"), "user agent", modes, diagnostics);
413
+ if (options.includeSkills) {
414
+ await loadSkillModes(join6(homeDir, ".agents", "skills"), "user skill", modes, diagnostics);
415
+ await loadSkillModes(join6(agentDir, "skills"), "pi user skill", modes, diagnostics);
416
+ }
417
+ if (options.projectTrusted === true) {
418
+ if (options.includeSkills) {
419
+ await loadSkillModes(join6(options.cwd, ".agents", "skills"), "project skill", modes, diagnostics);
420
+ await loadSkillModes(join6(options.cwd, ".pi", "skills"), "pi project skill", modes, diagnostics);
313
421
  }
314
- return false;
315
- },
316
- async skillInstallGlobal(miseExecutable, source, names) {
317
- const selection = names.flatMap((name) => ["--skill", name]);
318
- await runChecked(miseExecutable, [
319
- "x",
320
- "node@22",
321
- "--",
322
- "npx",
323
- "-y",
324
- "skills",
325
- "add",
326
- source,
327
- ...selection,
328
- "--global",
329
- "--agent",
330
- "pi",
331
- "--yes"
332
- ]);
333
- },
334
- async configEnsure(path, update, dryRun = false) {
335
- const currentText = await getOptionalFile3(path);
336
- const current = getParsedObject(currentText, path);
337
- const next = update(current);
338
- const changed = JSON.stringify(current) !== JSON.stringify(next);
339
- if (changed && !dryRun) {
340
- await mkdir3(dirname3(path), { recursive: true });
341
- await writeFile3(path, `${JSON.stringify(next, null, 2)}
342
- `, "utf8");
422
+ await loadAgentModes(join6(options.cwd, ".agents", "agents"), "project agent", modes, diagnostics);
423
+ await loadAgentModes(join6(options.cwd, ".pi", "agents"), "pi project agent", modes, diagnostics);
424
+ }
425
+ const userConfig = await loadDiffpiConfig({ homeDir });
426
+ const configuredModes = [...modes.values()].map((mode) => ({
427
+ ...mode,
428
+ modelPreferences: resolveAgentModelPreferences(mode.id, mode.modelPreferences, userConfig.config)
429
+ }));
430
+ return {
431
+ modes: configuredModes.sort((left, right) => left.id.localeCompare(right.id)),
432
+ diagnostics
433
+ };
434
+ }
435
+ function resolveAgentMode(modes, requested) {
436
+ const name = requested.trim();
437
+ if (!name)
438
+ return { ok: false, message: "Agent name is required." };
439
+ const exact = modes.find((mode) => mode.id === name);
440
+ if (exact)
441
+ return { ok: true, active: exact, message: `Active inline agent: ${exact.id}.` };
442
+ const lowerName = name.toLowerCase();
443
+ const matches = modes.filter((mode) => mode.id.toLowerCase() === lowerName);
444
+ if (matches.length === 1) {
445
+ const active = matches[0];
446
+ return { ok: true, active, message: `Active inline agent: ${active.id}.` };
447
+ }
448
+ if (matches.length > 1) {
449
+ return {
450
+ ok: false,
451
+ message: `Inline agent "${name}" is ambiguous. Use one of: ${matches.map((mode) => mode.id).join(", ")}.`
452
+ };
453
+ }
454
+ return { ok: false, message: `Unknown inline agent "${name}". Run /skill:mode or diffpi_modes_list.` };
455
+ }
456
+ function createModeController(pi, options = {}) {
457
+ let active;
458
+ let baseline;
459
+ const updateStatus = (ctx) => {
460
+ ctx.ui.setStatus(MODE_STATUS_KEY, active ? `mode: ${active.id}` : undefined);
461
+ };
462
+ const list = (ctx, listOptions = {}) => discoverAgentModes({
463
+ cwd: ctx.cwd,
464
+ agentDir: options.agentDir,
465
+ bundledAgentsDir: options.bundledAgentsDir,
466
+ homeDir: options.homeDir,
467
+ projectTrusted: ctx.isProjectTrusted(),
468
+ includeSkills: listOptions.includeSkills
469
+ });
470
+ return {
471
+ list,
472
+ async set(agent, ctx) {
473
+ const catalog = await list(ctx, { includeSkills: agent.includes(":") });
474
+ const result = resolveAgentMode(catalog.modes, agent);
475
+ if (!result.ok || !result.active)
476
+ return result;
477
+ baseline ??= captureRuntime(pi, ctx);
478
+ if (active && baseline)
479
+ await restoreRuntime(pi, baseline, ctx);
480
+ active = result.active;
481
+ const runtimeMessage = await applyModeRuntime(pi, active, ctx);
482
+ pi.appendEntry(MODE_STATE_ENTRY, { active, baseline });
483
+ updateStatus(ctx);
484
+ return { ...result, message: `${result.message} ${runtimeMessage}` };
485
+ },
486
+ async unset(ctx) {
487
+ if (!active)
488
+ return { ok: true, message: "Inline agent is already clear." };
489
+ if (baseline)
490
+ await restoreRuntime(pi, baseline, ctx);
491
+ active = undefined;
492
+ pi.appendEntry(MODE_STATE_ENTRY, { active: null });
493
+ baseline = undefined;
494
+ updateStatus(ctx);
495
+ return { ok: true, message: "Inline agent cleared. The previous model, thinking, tools, and prompt resume." };
496
+ },
497
+ async restore(ctx) {
498
+ const previousActive = active;
499
+ const previousBaseline = baseline;
500
+ const entry = [...ctx.sessionManager.getBranch()].reverse().find((candidate) => candidate.type === "custom" && candidate.customType === MODE_STATE_ENTRY);
501
+ const restored = entry?.data?.active;
502
+ const restoredBaseline = entry?.data?.baseline;
503
+ if (isAgentModeSnapshot(restored)) {
504
+ active = restored;
505
+ baseline = isModeBaseline(restoredBaseline) ? restoredBaseline : previousBaseline;
506
+ await applyModeRuntime(pi, active, ctx);
507
+ } else {
508
+ if (previousActive && previousBaseline)
509
+ pi.setActiveTools(previousBaseline.tools);
510
+ active = undefined;
511
+ baseline = undefined;
512
+ }
513
+ updateStatus(ctx);
514
+ },
515
+ apply(systemPrompt) {
516
+ if (!active)
517
+ return systemPrompt;
518
+ if (active.promptStrategy === "replace")
519
+ return active.systemPrompt;
520
+ return `${systemPrompt}
521
+
522
+ ## Active inline agent: ${active.label}
523
+
524
+ ${active.systemPrompt}`;
525
+ },
526
+ getActive() {
527
+ return active;
343
528
  }
344
- return { path, changed, existed: currentText !== undefined, planned: changed && dryRun };
529
+ };
530
+ }
531
+ var MODE_STATE_ENTRY = "diffpi-mode-state";
532
+ var MODE_STATUS_KEY = "diffpi-mode";
533
+ var MODE_CONTROL_TOOLS = ["ask_user_question", "diffpi_modes_list", "diffpi_modes_set", "diffpi_modes_unset"];
534
+ var BUNDLED_AGENTS_DIR = resolveBundledAgentsDir();
535
+ var THINKING_LEVELS = new Set(["off", "minimal", "low", "medium", "high", "xhigh", "max"]);
536
+ async function applyModeRuntime(pi, mode, ctx) {
537
+ let selectedModel;
538
+ if (mode.modelPreferences.length > 0) {
539
+ const scoped = ctx.scopedModels.length > 0 ? ctx.scopedModels.map((entry) => entry.model) : undefined;
540
+ const availableModels = scoped ?? ctx.modelRegistry.getAvailable();
541
+ for (const preference of mode.modelPreferences) {
542
+ const model = findPreferredModel(availableModels, preference);
543
+ if (model && await pi.setModel(model)) {
544
+ selectedModel = `${model.provider}/${model.id}`;
545
+ break;
546
+ }
547
+ }
548
+ }
549
+ if (mode.thinkingLevel)
550
+ pi.setThinkingLevel(mode.thinkingLevel);
551
+ if (mode.tools.length > 0) {
552
+ const availableTools = new Set(pi.getAllTools().map((tool) => tool.name));
553
+ const selectedTools = [...new Set([...mode.tools, ...MODE_CONTROL_TOOLS])].filter((tool) => availableTools.has(tool));
554
+ if (selectedTools.length > 0)
555
+ pi.setActiveTools(selectedTools);
345
556
  }
557
+ const parts = [];
558
+ if (mode.modelPreferences.length > 0) {
559
+ parts.push(selectedModel ? `Model: ${selectedModel}.` : "No preferred model was available; kept the current model.");
560
+ }
561
+ if (mode.thinkingLevel)
562
+ parts.push(`Thinking: ${mode.thinkingLevel}.`);
563
+ if (mode.tools.length > 0)
564
+ parts.push("Applied the profile tool set.");
565
+ return parts.join(" ") || "The profile changes the prompt only.";
566
+ }
567
+ async function restoreRuntime(pi, state, ctx) {
568
+ if (state.model) {
569
+ const model = ctx.modelRegistry.find(state.model.provider, state.model.id);
570
+ if (model)
571
+ await pi.setModel(model);
572
+ }
573
+ pi.setThinkingLevel(state.thinkingLevel);
574
+ pi.setActiveTools(state.tools);
575
+ }
576
+ function captureRuntime(pi, ctx) {
577
+ return {
578
+ model: ctx.model ? { provider: ctx.model.provider, id: ctx.model.id } : undefined,
579
+ thinkingLevel: pi.getThinkingLevel(),
580
+ tools: pi.getActiveTools()
581
+ };
582
+ }
583
+ async function loadSkillModes(skillsDir, source, modes, diagnostics) {
584
+ const entries = await readDirectoryIfExists(skillsDir);
585
+ for (const entry of entries.filter((item) => item.isDirectory()).sort((a, b) => a.name.localeCompare(b.name))) {
586
+ await loadAgentModes(join6(skillsDir, entry.name, "agents"), `${source} ${entry.name}`, modes, diagnostics, entry.name);
587
+ }
588
+ }
589
+ async function loadAgentModes(directory, source, modes, diagnostics, skillName) {
590
+ const entries = await readDirectoryIfExists(directory);
591
+ for (const entry of entries.filter((item) => item.isFile() && item.name.endsWith(".md")).sort((a, b) => a.name.localeCompare(b.name))) {
592
+ const path = join6(directory, entry.name);
593
+ try {
594
+ const content = await readFile4(path, "utf8");
595
+ const { frontmatter, body } = parseFrontmatter2(content.startsWith("\uFEFF") ? content.slice(1) : content);
596
+ if (frontmatter.enabled === false || frontmatter.inline === false)
597
+ continue;
598
+ const name = getFrontmatterText(frontmatter.name) ?? basename2(path, extname(path));
599
+ const systemPrompt = body.trim();
600
+ if (!name || name.includes(":") || !systemPrompt) {
601
+ diagnostics.push(`Skipped ${path}: agent name must not contain ":" and prompt body is required.`);
602
+ continue;
603
+ }
604
+ const id = skillName ? `${skillName}:${name}` : name;
605
+ modes.set(id, {
606
+ id,
607
+ label: getFrontmatterText(frontmatter.display_name) ?? name,
608
+ description: getFrontmatterText(frontmatter.description) ?? `Inline agent from ${basename2(path)}`,
609
+ systemPrompt,
610
+ promptStrategy: frontmatter.prompt_mode === "append" ? "append" : "replace",
611
+ modelPreferences: [
612
+ ...getFrontmatterList(frontmatter.model),
613
+ ...getFrontmatterList(frontmatter.model_fallbacks)
614
+ ],
615
+ thinkingLevel: getThinkingLevel(frontmatter.thinking),
616
+ tools: getFrontmatterList(frontmatter.tools),
617
+ source,
618
+ sourcePath: path
619
+ });
620
+ } catch (error) {
621
+ diagnostics.push(`Skipped ${path}: ${error instanceof Error ? error.message : String(error)}`);
622
+ }
623
+ }
624
+ }
625
+ function getFrontmatterText(value) {
626
+ return typeof value === "string" && value.trim() ? value.trim() : undefined;
627
+ }
628
+ function getFrontmatterList(value) {
629
+ const values = Array.isArray(value) ? value : typeof value === "string" ? value.split(",") : [];
630
+ return values.filter((item) => typeof item === "string").map((item) => item.trim()).filter(Boolean);
631
+ }
632
+ function getThinkingLevel(value) {
633
+ const level = getFrontmatterText(value);
634
+ return level && THINKING_LEVELS.has(level) ? level : undefined;
635
+ }
636
+ function isAgentModeSnapshot(value) {
637
+ if (!value || typeof value !== "object")
638
+ return false;
639
+ const candidate = value;
640
+ return typeof candidate.id === "string" && typeof candidate.label === "string" && typeof candidate.description === "string" && typeof candidate.systemPrompt === "string" && (candidate.promptStrategy === "append" || candidate.promptStrategy === "replace") && Array.isArray(candidate.modelPreferences) && (candidate.thinkingLevel === undefined || THINKING_LEVELS.has(candidate.thinkingLevel)) && Array.isArray(candidate.tools) && typeof candidate.source === "string" && typeof candidate.sourcePath === "string";
641
+ }
642
+ function isModeBaseline(value) {
643
+ if (!value || typeof value !== "object")
644
+ return false;
645
+ const candidate = value;
646
+ const model = candidate.model;
647
+ return (model === undefined || typeof model.provider === "string" && typeof model.id === "string") && candidate.thinkingLevel !== undefined && THINKING_LEVELS.has(candidate.thinkingLevel) && Array.isArray(candidate.tools) && candidate.tools.every((tool) => typeof tool === "string");
648
+ }
649
+ // src/pi.ts
650
+ import { mkdir as mkdir3, writeFile as writeFile3 } from "node:fs/promises";
651
+ import { homedir as homedir5 } from "node:os";
652
+ import { dirname as dirname4, join as join7 } from "node:path";
653
+ var pi = {
654
+ executableCheck: findPiExecutable,
655
+ packageList: listPiPackages,
656
+ packageCheck: hasPiPackage,
657
+ packageInstall: installPiPackage,
658
+ agentDir: resolvePiAgentDir,
659
+ agentEnsure: ensurePiAgent,
660
+ skillCheckGlobal: checkGlobalPiSkill,
661
+ skillInstallGlobal: installGlobalPiSkills,
662
+ configEnsure: ensurePiConfig
346
663
  };
347
- function getAgentDir(homeDir = homedir3()) {
348
- return process.env.PI_CODING_AGENT_DIR ?? (process.env.XDG_CONFIG_HOME ? join4(process.env.XDG_CONFIG_HOME, "pi") : join4(homeDir, ".pi", "agent"));
664
+ async function ensurePiAgent(filename, content, agentDir = resolvePiAgentDir(), dryRun = false) {
665
+ const path = join7(agentDir, "agents", filename);
666
+ const currentText = await readTextIfExists(path);
667
+ const changed = currentText !== content;
668
+ if (changed && !dryRun) {
669
+ await mkdir3(dirname4(path), { recursive: true });
670
+ await writeFile3(path, content, "utf8");
671
+ }
672
+ return { path, changed, existed: currentText !== undefined, planned: changed && dryRun };
349
673
  }
350
- async function getOptionalFile3(path) {
351
- try {
352
- return await readFile3(path, "utf8");
353
- } catch (error) {
354
- if (error instanceof Error && "code" in error && error.code === "ENOENT")
355
- return;
356
- throw error;
674
+ async function checkGlobalPiSkill(name, agentDir = resolvePiAgentDir(), sharedSkillsDir = join7(homedir5(), ".agents", "skills")) {
675
+ const roots = [join7(agentDir, "skills"), sharedSkillsDir];
676
+ for (const root of roots) {
677
+ if (await readTextIfExists(join7(root, name, "SKILL.md")) !== undefined)
678
+ return true;
679
+ }
680
+ return false;
681
+ }
682
+ async function installGlobalPiSkills(miseExecutable, source, names) {
683
+ const selection = names.flatMap((name) => ["--skill", name]);
684
+ await runChecked(miseExecutable, [
685
+ "x",
686
+ "node@22",
687
+ "--",
688
+ "npx",
689
+ "-y",
690
+ "skills",
691
+ "add",
692
+ source,
693
+ ...selection,
694
+ "--global",
695
+ "--agent",
696
+ "pi",
697
+ "--yes"
698
+ ]);
699
+ }
700
+ async function ensurePiConfig(path, update, dryRun = false) {
701
+ const currentText = await readTextIfExists(path);
702
+ const current = parseJsonObject(currentText, path);
703
+ const next = update(current);
704
+ const changed = JSON.stringify(current) !== JSON.stringify(next);
705
+ if (changed && !dryRun) {
706
+ await mkdir3(dirname4(path), { recursive: true });
707
+ await writeFile3(path, `${JSON.stringify(next, null, 2)}
708
+ `, "utf8");
357
709
  }
710
+ return { path, changed, existed: currentText !== undefined, planned: changed && dryRun };
711
+ }
712
+ async function findPiExecutable() {
713
+ return findExecutable("pi");
714
+ }
715
+ async function listPiPackages(executable) {
716
+ return (await runChecked(executable, ["list"])).stdout;
717
+ }
718
+ function hasPiPackage(listOutput, source) {
719
+ if (listOutput.includes(source))
720
+ return true;
721
+ return source.startsWith("https://") && listOutput.includes(source.slice("https://".length));
358
722
  }
359
- function getParsedObject(content, path) {
723
+ async function installPiPackage(executable, source) {
724
+ await runChecked(executable, ["install", source]);
725
+ }
726
+ function resolvePiAgentDir(homeDir = homedir5()) {
727
+ return process.env.PI_CODING_AGENT_DIR ?? (process.env.XDG_CONFIG_HOME ? join7(process.env.XDG_CONFIG_HOME, "pi") : join7(homeDir, ".pi", "agent"));
728
+ }
729
+ function parseJsonObject(content, path) {
360
730
  if (!content?.trim())
361
731
  return {};
362
732
  try {
@@ -367,8 +737,10 @@ function getParsedObject(content, path) {
367
737
  throw new Error(`Expected valid JSON object in ${path}.`);
368
738
  }
369
739
  // src/setup.ts
370
- import { homedir as homedir4 } from "node:os";
371
- import { join as join5 } from "node:path";
740
+ import { parseFrontmatter as parseFrontmatter3 } from "@earendil-works/pi-coding-agent";
741
+ import { readdir as readdir2, readFile as readFile5 } from "node:fs/promises";
742
+ import { homedir as homedir6 } from "node:os";
743
+ import { basename as basename3, join as join8 } from "node:path";
372
744
  var MISE_DEPENDENCIES = [
373
745
  { name: "node", tool: "node", spec: "node@22", minimumVersion: "22.19.0" },
374
746
  { name: "zellij", tool: "zellij", spec: "zellij@latest", minimumVersion: undefined },
@@ -399,9 +771,10 @@ var PI_SKILL_SOURCES = [
399
771
  { repository: "AminBlg/SimpleEnglish", skills: ["simple-english"] }
400
772
  ];
401
773
  var MCP_ADAPTER_PACKAGE = "npm:pi-mcp-adapter";
774
+ var BUNDLED_AGENTS_DIR2 = resolveBundledAgentsDir();
402
775
  async function ensureMise(options = {}) {
403
- const homeDir = options.homeDir ?? homedir4();
404
- const current = await mise.executableCheck() ?? await mise.executableCheck(join5(homeDir, ".local", "bin", "mise"));
776
+ const homeDir = options.homeDir ?? homedir6();
777
+ const current = await mise.executableCheck() ?? await mise.executableCheck(join8(homeDir, ".local", "bin", "mise"));
405
778
  if (current)
406
779
  return { executable: current, action: createSetupAction("mise", "ready", current) };
407
780
  reportProgress(options, "Installing mise");
@@ -446,18 +819,33 @@ async function ensureMiseDeps(miseExecutable, options = {}) {
446
819
  async function ensurePiPlugins(options = {}) {
447
820
  const actions = await ensurePiPackages(PI_PACKAGES, options);
448
821
  const agentDir = options.agentDir ?? pi.agentDir(options.homeDir);
449
- const webSearch = await pi.configEnsure(join5(agentDir, "web-search.json"), (config) => ({ ...config, workflow: "auto-summary" }), options.dryRun);
822
+ const webSearch = await pi.configEnsure(join8(agentDir, "web-search.json"), (config) => ({ ...config, workflow: "auto-summary" }), options.dryRun);
450
823
  actions.push(getConfigSetupAction("web search settings", webSearch));
451
- const lsp = await pi.configEnsure(join5(agentDir, "pi-lsp.json"), (config) => ({
824
+ const lsp = await pi.configEnsure(join8(agentDir, "pi-lsp.json"), (config) => ({
452
825
  ...config,
453
826
  progressive: { ...getRecord(config.progressive), enabled: true, inject: "none" }
454
827
  }), options.dryRun);
455
828
  actions.push(getConfigSetupAction("pi-lsp settings", lsp));
456
829
  return actions;
457
830
  }
831
+ async function ensurePiAgents(options = {}) {
832
+ const agentDir = options.agentDir ?? pi.agentDir(options.homeDir);
833
+ const bundledAgentsDir = options.bundledAgentsDir ?? BUNDLED_AGENTS_DIR2;
834
+ const userConfig = await loadDiffpiConfig({ homeDir: options.homeDir });
835
+ const entries = (await readdir2(bundledAgentsDir, { withFileTypes: true })).filter((entry) => entry.isFile() && entry.name.startsWith("diffpi-") && entry.name.endsWith(".md")).sort((left, right) => left.name.localeCompare(right.name));
836
+ const actions = [];
837
+ for (const entry of entries) {
838
+ const id = basename3(entry.name, ".md").replace(/^diffpi-/, "");
839
+ const source = await readFile5(join8(bundledAgentsDir, entry.name), "utf8");
840
+ const content = materializeAgentModels(source, id, userConfig.config, options.availableModels);
841
+ const result = await pi.agentEnsure(entry.name, content, agentDir, options.dryRun);
842
+ actions.push(getConfigSetupAction(`pi agent ${id}`, result));
843
+ }
844
+ return actions;
845
+ }
458
846
  async function ensurePiSkills(miseExecutable, options = {}) {
459
847
  const agentDir = options.agentDir ?? pi.agentDir(options.homeDir);
460
- const sharedSkillsDir = join5(options.homeDir ?? homedir4(), ".agents", "skills");
848
+ const sharedSkillsDir = join8(options.homeDir ?? homedir6(), ".agents", "skills");
461
849
  const actions = [];
462
850
  for (const source of PI_SKILL_SOURCES) {
463
851
  const missing = [];
@@ -514,13 +902,55 @@ async function setupPi(options = {}) {
514
902
  actions.push(await ensureMiseHooks(miseResult.executable, options));
515
903
  actions.push(...await ensureMiseDeps(miseResult.executable, options));
516
904
  actions.push(...await ensurePiPlugins(options));
905
+ actions.push(...await ensurePiAgents(options));
517
906
  actions.push(...await ensurePiSkills(miseResult.executable, options));
518
907
  actions.push(...await ensureMcpAdapters(miseResult.executable, options));
519
908
  return {
520
909
  actions,
521
- restartPi: actions.some((item) => (item.status === "installed" || item.status === "updated") && (item.name.startsWith("pi package ") || item.name.startsWith("pi skill ") || item.name === "MCP configuration" || item.name === "web search settings" || item.name === "pi-lsp settings"))
910
+ restartPi: setupRequiresRestart(actions)
522
911
  };
523
912
  }
913
+ function setupRequiresRestart(actions) {
914
+ return actions.some((item) => (item.status === "installed" || item.status === "updated") && (item.name.startsWith("pi package ") || item.name.startsWith("pi agent ") || item.name.startsWith("pi skill ") || item.name === "MCP configuration" || item.name === "web search settings" || item.name === "pi-lsp settings"));
915
+ }
916
+ function materializeAgentModels(content, agentId, config, availableModels) {
917
+ const { frontmatter } = parseFrontmatter3(content.startsWith("\uFEFF") ? content.slice(1) : content);
918
+ const profilePreferences = [...getTextList(frontmatter.model), ...getTextList(frontmatter.model_fallbacks)];
919
+ const preferences = resolveAgentModelPreferences(agentId, profilePreferences, config);
920
+ let selectedIndex = availableModels === undefined && preferences.length > 0 ? 0 : -1;
921
+ let selectedModel = selectedIndex === 0 ? preferences[0] : undefined;
922
+ if (availableModels) {
923
+ for (const [index, preference] of preferences.entries()) {
924
+ const match = findPreferredModel(availableModels, preference);
925
+ if (!match)
926
+ continue;
927
+ selectedIndex = index;
928
+ selectedModel = `${match.provider}/${match.id}`;
929
+ break;
930
+ }
931
+ }
932
+ const fallbacks = preferences.filter((_preference, index) => index !== selectedIndex);
933
+ return replaceAgentModelFields(content, selectedModel, fallbacks);
934
+ }
935
+ function replaceAgentModelFields(content, model, fallbacks) {
936
+ const newline = content.includes(`\r
937
+ `) ? `\r
938
+ ` : `
939
+ `;
940
+ const lines = content.replaceAll(`\r
941
+ `, `
942
+ `).split(`
943
+ `);
944
+ const closingDelimiter = lines.indexOf("---", 1);
945
+ if (lines[0] !== "---" || closingDelimiter < 0)
946
+ return content;
947
+ const frontmatter = lines.slice(1, closingDelimiter).filter((line) => !/^model(?:_fallbacks)?:/.test(line));
948
+ if (model)
949
+ frontmatter.push(`model: ${model}`);
950
+ if (fallbacks.length > 0)
951
+ frontmatter.push(`model_fallbacks: ${fallbacks.join(", ")}`);
952
+ return ["---", ...frontmatter, "---", ...lines.slice(closingDelimiter + 1)].join(newline);
953
+ }
524
954
  async function ensurePiPackages(packages, options) {
525
955
  const executable = await pi.executableCheck();
526
956
  if (!executable && !options.dryRun)
@@ -542,6 +972,10 @@ ${source}`;
542
972
  }
543
973
  return actions;
544
974
  }
975
+ function getTextList(value) {
976
+ const values = Array.isArray(value) ? value : typeof value === "string" ? value.split(",") : [];
977
+ return values.filter((item) => typeof item === "string").map((item) => item.trim()).filter(Boolean);
978
+ }
545
979
  function getConfigSetupAction(name, result) {
546
980
  if (!result.changed)
547
981
  return createSetupAction(name, "ready", result.path);
@@ -559,14 +993,22 @@ function getRecord(value) {
559
993
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : {};
560
994
  }
561
995
  export {
996
+ createModeController,
997
+ diffpiConfigPaths,
998
+ discoverAgentModes,
562
999
  ensureMcpAdapters,
563
1000
  ensureMise,
564
1001
  ensureMiseDeps,
565
1002
  ensureMiseHooks,
1003
+ ensurePiAgents,
566
1004
  ensurePiPlugins,
567
1005
  ensurePiSkills,
1006
+ findPreferredModel,
1007
+ loadDiffpiConfig,
568
1008
  mcp,
569
1009
  mise,
570
1010
  pi,
1011
+ resolveAgentMode,
1012
+ resolveAgentModelPreferences,
571
1013
  setupPi
572
1014
  };