@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.
@@ -25,21 +25,224 @@ function createDiffpiReloadTool(pi) {
25
25
  });
26
26
  }
27
27
 
28
- // src/tools/setup.ts
28
+ // src/tools/modes.ts
29
29
  import { defineTool as defineTool2 } from "@earendil-works/pi-coding-agent";
30
30
  import { z as z2 } from "zod";
31
+ var emptyParametersSchema = z2.object({});
32
+ var emptyParameters = z2.toJSONSchema(emptyParametersSchema, { io: "input" });
33
+ var listParametersSchema = z2.object({
34
+ includeSkills: z2.boolean().optional().describe("Include skill-owned agents using skill:agent ids.")
35
+ });
36
+ var listParameters = z2.toJSONSchema(listParametersSchema, { io: "input" });
37
+ var setParametersSchema = z2.object({
38
+ agent: z2.string().trim().min(1).describe("Inline agent id from diffpi_modes_list.")
39
+ });
40
+ var setParameters = z2.toJSONSchema(setParametersSchema, { io: "input" });
41
+ function createModeTools(controller) {
42
+ return [
43
+ defineTool2({
44
+ name: "diffpi_modes_list",
45
+ label: "diffpi modes list",
46
+ description: "List inline agents shared with the subagent plugin, optionally including skill-owned agents.",
47
+ promptSnippet: "List inline agents before selecting one when the requested agent is unclear",
48
+ promptGuidelines: [
49
+ "Call diffpi_modes_list when the user asks which inline agents are available.",
50
+ "Set includeSkills to true only when the user asks for skill agents or runs /skill:mode --include-skills.",
51
+ "Inline mode applies the profile prompt, first available preferred model, thinking level, and available tools."
52
+ ],
53
+ parameters: listParameters,
54
+ executionMode: "parallel",
55
+ async execute(_toolCallId, input, _signal, _onUpdate, ctx) {
56
+ const params = listParametersSchema.parse(input);
57
+ const catalog = await controller.list(ctx, { includeSkills: params.includeSkills });
58
+ return {
59
+ content: [{ type: "text", text: formatCatalog(catalog, controller.getActive()?.id) }],
60
+ details: { active: controller.getActive()?.id, catalog }
61
+ };
62
+ }
63
+ }),
64
+ defineTool2({
65
+ name: "diffpi_modes_set",
66
+ label: "diffpi modes set",
67
+ description: "Set a validated available agent as the inline behavioral agent for subsequent chat turns.",
68
+ promptSnippet: "Set the inline behavioral agent only after the user chooses one",
69
+ promptGuidelines: [
70
+ "Call diffpi_modes_set only after the user explicitly selects an agent.",
71
+ "Use the exact skill:agent id for a skill-owned agent.",
72
+ "The selected prompt takes effect on the next model turn."
73
+ ],
74
+ parameters: setParameters,
75
+ executionMode: "sequential",
76
+ async execute(_toolCallId, input, _signal, _onUpdate, ctx) {
77
+ const params = setParametersSchema.parse(input);
78
+ const result = await controller.set(params.agent, ctx);
79
+ if (!result.ok)
80
+ throw new Error(result.message);
81
+ return {
82
+ content: [{ type: "text", text: `${result.message} The prompt takes effect on the next turn.` }],
83
+ details: { active: result.active }
84
+ };
85
+ }
86
+ }),
87
+ defineTool2({
88
+ name: "diffpi_modes_unset",
89
+ label: "diffpi modes unset",
90
+ description: "Clear the inline behavioral agent and restore default Pi prompting for subsequent turns.",
91
+ promptSnippet: "Clear the inline agent when the user asks for default behavior",
92
+ promptGuidelines: [
93
+ "Call diffpi_modes_unset only when the user explicitly asks to clear the active inline agent."
94
+ ],
95
+ parameters: emptyParameters,
96
+ executionMode: "sequential",
97
+ async execute(_toolCallId, input, _signal, _onUpdate, ctx) {
98
+ emptyParametersSchema.parse(input);
99
+ const result = await controller.unset(ctx);
100
+ return {
101
+ content: [{ type: "text", text: result.message }],
102
+ details: { active: controller.getActive()?.id }
103
+ };
104
+ }
105
+ })
106
+ ];
107
+ }
108
+ function formatCatalog(catalog, active) {
109
+ const lines = [`Active inline agent: ${active ?? "default"}.`, "", "Available inline agents:"];
110
+ for (const mode of catalog.modes) {
111
+ const runtime = [mode.modelPreferences[0], mode.thinkingLevel].filter(Boolean).join(", ");
112
+ lines.push(`- ${mode.id} [${mode.promptStrategy}${runtime ? `; ${runtime}` : ""}] — ${sanitize(mode.description)} (${mode.source})`);
113
+ }
114
+ if (catalog.diagnostics.length > 0) {
115
+ lines.push("", "Skipped agent files:");
116
+ for (const diagnostic of catalog.diagnostics)
117
+ lines.push(`- ${sanitize(diagnostic)}`);
118
+ }
119
+ lines.push("", "Inline mode applies the profile prompt, preferred available model, thinking level, and tool set.");
120
+ return lines.join(`
121
+ `);
122
+ }
123
+ function sanitize(value) {
124
+ return value.replace(/[\r\n\t]+/g, " ").replace(/\s+/g, " ").trim();
125
+ }
126
+
127
+ // src/tools/setup.ts
128
+ import { defineTool as defineTool3 } from "@earendil-works/pi-coding-agent";
129
+ import { z as z4 } from "zod";
31
130
 
32
131
  // src/setup.ts
33
- import { homedir as homedir4 } from "node:os";
34
- import { join as join5 } from "node:path";
132
+ import { parseFrontmatter as parseFrontmatter2 } from "@earendil-works/pi-coding-agent";
133
+ import { readdir as readdir2, readFile as readFile4 } from "node:fs/promises";
134
+ import { homedir as homedir5 } from "node:os";
135
+ import { basename as basename2, join as join7 } from "node:path";
35
136
 
36
- // src/mcp.ts
37
- import { mkdir, readFile, writeFile } from "node:fs/promises";
38
- import { homedir } from "node:os";
137
+ // src/assets.ts
138
+ import { existsSync } from "node:fs";
39
139
  import { dirname, join } from "node:path";
140
+ import { fileURLToPath } from "node:url";
141
+ function resolveBundledAgentsDir(moduleUrl = import.meta.url) {
142
+ const moduleDir = dirname(fileURLToPath(moduleUrl));
143
+ const candidates = [
144
+ join(moduleDir, "agents"),
145
+ join(moduleDir, "..", "agents"),
146
+ join(moduleDir, "..", "..", "agents")
147
+ ];
148
+ return candidates.find((path) => existsSync(path)) ?? candidates[1];
149
+ }
150
+
151
+ // src/config.ts
152
+ import { parseFrontmatter } from "@earendil-works/pi-coding-agent";
153
+ import { z as z3 } from "zod";
154
+ import { homedir } from "node:os";
155
+ import { join as join2 } from "node:path";
156
+
157
+ // src/fsx.ts
158
+ import { readdir, readFile } from "node:fs/promises";
159
+ async function readTextIfExists(path) {
160
+ try {
161
+ return await readFile(path, "utf8");
162
+ } catch (error) {
163
+ if (isMissingPath(error))
164
+ return;
165
+ throw error;
166
+ }
167
+ }
168
+ function isMissingPath(error) {
169
+ return error instanceof Error && "code" in error && error.code === "ENOENT";
170
+ }
171
+
172
+ // src/config.ts
173
+ var modelReferenceSchema = z3.string().trim().min(1);
174
+ var agentConfigSchema = z3.object({
175
+ models: z3.array(modelReferenceSchema).optional()
176
+ }).strict();
177
+ var diffpiConfigSchema = z3.object({
178
+ agents: z3.record(z3.string(), agentConfigSchema).optional()
179
+ }).strict();
180
+ function diffpiConfigPaths(homeDir = homedir()) {
181
+ const directory = join2(homeDir, ".difflab", "diffpi");
182
+ return {
183
+ yaml: join2(directory, "config.yaml"),
184
+ json: join2(directory, "config.json")
185
+ };
186
+ }
187
+ async function loadDiffpiConfig(options = {}) {
188
+ const paths = diffpiConfigPaths(options.homeDir);
189
+ for (const [format, path] of [
190
+ ["yaml", paths.yaml],
191
+ ["json", paths.json]
192
+ ]) {
193
+ const content = await readTextIfExists(path);
194
+ if (content === undefined)
195
+ continue;
196
+ try {
197
+ const value = format === "yaml" ? parseYamlConfig(content) : JSON.parse(content);
198
+ return { config: diffpiConfigSchema.parse(value ?? {}), path };
199
+ } catch (error) {
200
+ const reason = error instanceof Error ? error.message : String(error);
201
+ throw new Error(`Invalid Diffpi config at ${path}: ${reason}`, { cause: error });
202
+ }
203
+ }
204
+ return { config: {} };
205
+ }
206
+ function resolveAgentModelPreferences(agentId, profilePreferences, config) {
207
+ const override = config.agents?.[agentId];
208
+ if (override && Object.hasOwn(override, "models"))
209
+ return [...override.models ?? []];
210
+ return [...profilePreferences];
211
+ }
212
+ function findPreferredModel(models, preference) {
213
+ const normalizedPreference = normalizeModelReference(preference);
214
+ const exactReference = models.find((model) => normalizeModelReference(`${model.provider}/${model.id}`) === normalizedPreference);
215
+ if (exactReference)
216
+ return exactReference;
217
+ const idPreference = preference.includes("/") ? preference.slice(preference.indexOf("/") + 1) : preference;
218
+ const normalizedIdPreference = normalizeModelReference(idPreference);
219
+ const exactId = models.find((model) => normalizeModelReference(model.id) === normalizedIdPreference);
220
+ if (exactId)
221
+ return exactId;
222
+ const preferenceTokens = normalizedIdPreference.split("-").filter(Boolean);
223
+ return models.find((model) => {
224
+ const modelTokens = new Set(normalizeModelReference(model.id).split("-").filter(Boolean));
225
+ return preferenceTokens.every((token) => modelTokens.has(token));
226
+ });
227
+ }
228
+ function parseYamlConfig(content) {
229
+ const document = content.replace(/^\uFEFF/, "").replace(/^---[^\S\r\n]*(?:#.*)?(?:\r?\n|$)/, "");
230
+ return parseFrontmatter(`---
231
+ ${document}
232
+ ---
233
+ `).frontmatter;
234
+ }
235
+ function normalizeModelReference(value) {
236
+ return value.toLowerCase().replace(/^~/, "").replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
237
+ }
238
+
239
+ // src/mcp.ts
240
+ import { mkdir, readFile as readFile2, writeFile } from "node:fs/promises";
241
+ import { homedir as homedir2 } from "node:os";
242
+ import { dirname as dirname2, join as join3 } from "node:path";
40
243
  var mcp = {
41
- globalConfigPath(homeDir = homedir()) {
42
- return join(homeDir, ".config", "mcp", "mcp.json");
244
+ globalConfigPath(homeDir = homedir2()) {
245
+ return join3(homeDir, ".config", "mcp", "mcp.json");
43
246
  },
44
247
  async serversEnsure(servers, options = {}) {
45
248
  const path = options.path ?? mcp.globalConfigPath();
@@ -52,7 +255,7 @@ var mcp = {
52
255
  const next = { ...current, mcpServers: nextServers };
53
256
  const changed = JSON.stringify(current) !== JSON.stringify(next);
54
257
  if (changed && !options.dryRun) {
55
- await mkdir(dirname(path), { recursive: true });
258
+ await mkdir(dirname2(path), { recursive: true });
56
259
  await writeFile(path, `${JSON.stringify(next, null, 2)}
57
260
  `, "utf8");
58
261
  }
@@ -82,7 +285,7 @@ function getParsedConfig(content, path) {
82
285
  }
83
286
  async function getOptionalFile(path) {
84
287
  try {
85
- return await readFile(path, "utf8");
288
+ return await readFile2(path, "utf8");
86
289
  } catch (error) {
87
290
  if (error instanceof Error && "code" in error && error.code === "ENOENT")
88
291
  return;
@@ -94,14 +297,14 @@ function isRecord(value) {
94
297
  }
95
298
 
96
299
  // src/mise.ts
97
- import { mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2 } from "node:fs/promises";
98
- import { homedir as homedir2 } from "node:os";
99
- import { basename, dirname as dirname2, join as join3 } from "node:path";
300
+ import { mkdir as mkdir2, readFile as readFile3, writeFile as writeFile2 } from "node:fs/promises";
301
+ import { homedir as homedir3 } from "node:os";
302
+ import { basename, dirname as dirname3, join as join5 } from "node:path";
100
303
 
101
304
  // src/process.ts
102
305
  import { constants } from "node:fs";
103
306
  import { access } from "node:fs/promises";
104
- import { delimiter, join as join2 } from "node:path";
307
+ import { delimiter, join as join4 } from "node:path";
105
308
  import { spawn } from "node:child_process";
106
309
  var MAX_CAPTURED_OUTPUT_LENGTH = 65536;
107
310
  async function findExecutable(name) {
@@ -116,7 +319,7 @@ async function findExecutable(name) {
116
319
  for (const directory of (process.env.PATH ?? "").split(delimiter)) {
117
320
  if (!directory)
118
321
  continue;
119
- const candidate = join2(directory, name);
322
+ const candidate = join4(directory, name);
120
323
  try {
121
324
  await access(candidate, constants.X_OK);
122
325
  return candidate;
@@ -163,11 +366,11 @@ var mise = {
163
366
  return findExecutable(name);
164
367
  },
165
368
  async install(options = {}) {
166
- const homeDir = options.homeDir ?? homedir2();
369
+ const homeDir = options.homeDir ?? homedir3();
167
370
  const platform = options.platform ?? process.platform;
168
371
  if (platform === "win32")
169
372
  throw new Error("Automatic mise installation supports macOS and Linux only.");
170
- const installedPath = join3(homeDir, ".local", "bin", "mise");
373
+ const installedPath = join5(homeDir, ".local", "bin", "mise");
171
374
  if (options.dryRun)
172
375
  return installedPath;
173
376
  await runChecked("sh", ["-c", "curl -fsSL https://mise.run | sh"]);
@@ -177,7 +380,7 @@ var mise = {
177
380
  return executable;
178
381
  },
179
382
  async hookEnsure(executable, options = {}) {
180
- const homeDir = options.homeDir ?? homedir2();
383
+ const homeDir = options.homeDir ?? homedir3();
181
384
  const hook = getShellHook(basename(options.shell ?? process.env.SHELL ?? ""), executable, homeDir);
182
385
  const current = await getOptionalFile2(hook.path);
183
386
  if (current.includes(MISE_HOOK_START))
@@ -187,7 +390,7 @@ var mise = {
187
390
  const separator = current.length === 0 || current.endsWith(`
188
391
  `) ? "" : `
189
392
  `;
190
- await mkdir2(dirname2(hook.path), { recursive: true });
393
+ await mkdir2(dirname3(hook.path), { recursive: true });
191
394
  await writeFile2(hook.path, `${current}${separator}${hook.content}`, "utf8");
192
395
  return { path: hook.path, changed: true, planned: false };
193
396
  },
@@ -205,7 +408,7 @@ var mise = {
205
408
  async toolInstallLocal(executable, specification, cwd = process.cwd()) {
206
409
  await runChecked(executable, ["use", "--path", cwd, specification], { cwd });
207
410
  },
208
- async toolUpdateAllGlobal(executable, homeDir = homedir2()) {
411
+ async toolUpdateAllGlobal(executable, homeDir = homedir3()) {
209
412
  await runChecked(executable, ["upgrade"], { cwd: homeDir });
210
413
  }
211
414
  };
@@ -214,7 +417,7 @@ function getShellHook(shell, executable, homeDir) {
214
417
  switch (shell.toLowerCase()) {
215
418
  case "zsh":
216
419
  return {
217
- path: join3(homeDir, ".zshrc"),
420
+ path: join5(homeDir, ".zshrc"),
218
421
  content: `${MISE_HOOK_START}
219
422
  eval "$(${command} activate zsh)"
220
423
  ${MISE_HOOK_END}
@@ -222,7 +425,7 @@ ${MISE_HOOK_END}
222
425
  };
223
426
  case "fish":
224
427
  return {
225
- path: join3(homeDir, ".config", "fish", "config.fish"),
428
+ path: join5(homeDir, ".config", "fish", "config.fish"),
226
429
  content: `${MISE_HOOK_START}
227
430
  ${command} activate fish | source
228
431
  ${MISE_HOOK_END}
@@ -231,7 +434,7 @@ ${MISE_HOOK_END}
231
434
  case "nu":
232
435
  case "nushell":
233
436
  return {
234
- path: join3(homeDir, ".config", "nushell", "config.nu"),
437
+ path: join5(homeDir, ".config", "nushell", "config.nu"),
235
438
  content: `${MISE_HOOK_START}
236
439
  let mise_bin = ${command}
237
440
  let mise_path = $nu.default-config-dir | path join mise.nu
@@ -242,7 +445,7 @@ ${MISE_HOOK_END}
242
445
  };
243
446
  case "xonsh":
244
447
  return {
245
- path: join3(homeDir, ".xonshrc"),
448
+ path: join5(homeDir, ".xonshrc"),
246
449
  content: `${MISE_HOOK_START}
247
450
  execx($(${command} activate xonsh))
248
451
  ${MISE_HOOK_END}
@@ -250,7 +453,7 @@ ${MISE_HOOK_END}
250
453
  };
251
454
  case "elvish":
252
455
  return {
253
- path: join3(homeDir, ".config", "elvish", "rc.elv"),
456
+ path: join5(homeDir, ".config", "elvish", "rc.elv"),
254
457
  content: `${MISE_HOOK_START}
255
458
  var mise: = (ns [&])
256
459
  eval (${command} activate elvish | slurp) &ns=$mise: &on-end={|ns| set mise: = $ns }
@@ -261,7 +464,7 @@ ${MISE_HOOK_END}
261
464
  case "pwsh":
262
465
  case "powershell":
263
466
  return {
264
- path: join3(homeDir, ".config", "powershell", "Microsoft.PowerShell_profile.ps1"),
467
+ path: join5(homeDir, ".config", "powershell", "Microsoft.PowerShell_profile.ps1"),
265
468
  content: `${MISE_HOOK_START}
266
469
  (& ${command} activate pwsh) | Out-String | Invoke-Expression
267
470
  ${MISE_HOOK_END}
@@ -270,7 +473,7 @@ ${MISE_HOOK_END}
270
473
  case "bash":
271
474
  default:
272
475
  return {
273
- path: join3(homeDir, ".bashrc"),
476
+ path: join5(homeDir, ".bashrc"),
274
477
  content: `${MISE_HOOK_START}
275
478
  eval "$(${command} activate bash)"
276
479
  ${MISE_HOOK_END}
@@ -309,7 +512,7 @@ function isVersionAtLeast(version, minimumVersion) {
309
512
  }
310
513
  async function getOptionalFile2(path) {
311
514
  try {
312
- return await readFile2(path, "utf8");
515
+ return await readFile3(path, "utf8");
313
516
  } catch (error) {
314
517
  if (error instanceof Error && "code" in error && error.code === "ENOENT")
315
518
  return "";
@@ -321,79 +524,86 @@ function getShellQuoted(value) {
321
524
  }
322
525
 
323
526
  // src/pi.ts
324
- import { mkdir as mkdir3, readFile as readFile3, writeFile as writeFile3 } from "node:fs/promises";
325
- import { homedir as homedir3 } from "node:os";
326
- import { dirname as dirname3, join as join4 } from "node:path";
527
+ import { mkdir as mkdir3, writeFile as writeFile3 } from "node:fs/promises";
528
+ import { homedir as homedir4 } from "node:os";
529
+ import { dirname as dirname4, join as join6 } from "node:path";
327
530
  var pi = {
328
- async executableCheck() {
329
- return findExecutable("pi");
330
- },
331
- async packageList(executable) {
332
- return (await runChecked(executable, ["list"])).stdout;
333
- },
334
- packageCheck(listOutput, source) {
335
- if (listOutput.includes(source))
531
+ executableCheck: findPiExecutable,
532
+ packageList: listPiPackages,
533
+ packageCheck: hasPiPackage,
534
+ packageInstall: installPiPackage,
535
+ agentDir: resolvePiAgentDir,
536
+ agentEnsure: ensurePiAgent,
537
+ skillCheckGlobal: checkGlobalPiSkill,
538
+ skillInstallGlobal: installGlobalPiSkills,
539
+ configEnsure: ensurePiConfig
540
+ };
541
+ async function ensurePiAgent(filename, content, agentDir = resolvePiAgentDir(), dryRun = false) {
542
+ const path = join6(agentDir, "agents", filename);
543
+ const currentText = await readTextIfExists(path);
544
+ const changed = currentText !== content;
545
+ if (changed && !dryRun) {
546
+ await mkdir3(dirname4(path), { recursive: true });
547
+ await writeFile3(path, content, "utf8");
548
+ }
549
+ return { path, changed, existed: currentText !== undefined, planned: changed && dryRun };
550
+ }
551
+ async function checkGlobalPiSkill(name, agentDir = resolvePiAgentDir(), sharedSkillsDir = join6(homedir4(), ".agents", "skills")) {
552
+ const roots = [join6(agentDir, "skills"), sharedSkillsDir];
553
+ for (const root of roots) {
554
+ if (await readTextIfExists(join6(root, name, "SKILL.md")) !== undefined)
336
555
  return true;
337
- return source.startsWith("https://") && listOutput.includes(source.slice("https://".length));
338
- },
339
- async packageInstall(executable, source) {
340
- await runChecked(executable, ["install", source]);
341
- },
342
- agentDir(homeDir = homedir3()) {
343
- return getAgentDir(homeDir);
344
- },
345
- async skillCheckGlobal(name, agentDir = getAgentDir(), sharedSkillsDir = join4(homedir3(), ".agents", "skills")) {
346
- const roots = [join4(agentDir, "skills"), sharedSkillsDir];
347
- for (const root of roots) {
348
- if (await getOptionalFile3(join4(root, name, "SKILL.md")) !== undefined)
349
- return true;
350
- }
351
- return false;
352
- },
353
- async skillInstallGlobal(miseExecutable, source, names) {
354
- const selection = names.flatMap((name) => ["--skill", name]);
355
- await runChecked(miseExecutable, [
356
- "x",
357
- "node@22",
358
- "--",
359
- "npx",
360
- "-y",
361
- "skills",
362
- "add",
363
- source,
364
- ...selection,
365
- "--global",
366
- "--agent",
367
- "pi",
368
- "--yes"
369
- ]);
370
- },
371
- async configEnsure(path, update, dryRun = false) {
372
- const currentText = await getOptionalFile3(path);
373
- const current = getParsedObject(currentText, path);
374
- const next = update(current);
375
- const changed = JSON.stringify(current) !== JSON.stringify(next);
376
- if (changed && !dryRun) {
377
- await mkdir3(dirname3(path), { recursive: true });
378
- await writeFile3(path, `${JSON.stringify(next, null, 2)}
379
- `, "utf8");
380
- }
381
- return { path, changed, existed: currentText !== undefined, planned: changed && dryRun };
382
556
  }
383
- };
384
- function getAgentDir(homeDir = homedir3()) {
385
- return process.env.PI_CODING_AGENT_DIR ?? (process.env.XDG_CONFIG_HOME ? join4(process.env.XDG_CONFIG_HOME, "pi") : join4(homeDir, ".pi", "agent"));
557
+ return false;
386
558
  }
387
- async function getOptionalFile3(path) {
388
- try {
389
- return await readFile3(path, "utf8");
390
- } catch (error) {
391
- if (error instanceof Error && "code" in error && error.code === "ENOENT")
392
- return;
393
- throw error;
559
+ async function installGlobalPiSkills(miseExecutable, source, names) {
560
+ const selection = names.flatMap((name) => ["--skill", name]);
561
+ await runChecked(miseExecutable, [
562
+ "x",
563
+ "node@22",
564
+ "--",
565
+ "npx",
566
+ "-y",
567
+ "skills",
568
+ "add",
569
+ source,
570
+ ...selection,
571
+ "--global",
572
+ "--agent",
573
+ "pi",
574
+ "--yes"
575
+ ]);
576
+ }
577
+ async function ensurePiConfig(path, update, dryRun = false) {
578
+ const currentText = await readTextIfExists(path);
579
+ const current = parseJsonObject(currentText, path);
580
+ const next = update(current);
581
+ const changed = JSON.stringify(current) !== JSON.stringify(next);
582
+ if (changed && !dryRun) {
583
+ await mkdir3(dirname4(path), { recursive: true });
584
+ await writeFile3(path, `${JSON.stringify(next, null, 2)}
585
+ `, "utf8");
394
586
  }
587
+ return { path, changed, existed: currentText !== undefined, planned: changed && dryRun };
395
588
  }
396
- function getParsedObject(content, path) {
589
+ async function findPiExecutable() {
590
+ return findExecutable("pi");
591
+ }
592
+ async function listPiPackages(executable) {
593
+ return (await runChecked(executable, ["list"])).stdout;
594
+ }
595
+ function hasPiPackage(listOutput, source) {
596
+ if (listOutput.includes(source))
597
+ return true;
598
+ return source.startsWith("https://") && listOutput.includes(source.slice("https://".length));
599
+ }
600
+ async function installPiPackage(executable, source) {
601
+ await runChecked(executable, ["install", source]);
602
+ }
603
+ function resolvePiAgentDir(homeDir = homedir4()) {
604
+ return process.env.PI_CODING_AGENT_DIR ?? (process.env.XDG_CONFIG_HOME ? join6(process.env.XDG_CONFIG_HOME, "pi") : join6(homeDir, ".pi", "agent"));
605
+ }
606
+ function parseJsonObject(content, path) {
397
607
  if (!content?.trim())
398
608
  return {};
399
609
  try {
@@ -435,9 +645,10 @@ var PI_SKILL_SOURCES = [
435
645
  { repository: "AminBlg/SimpleEnglish", skills: ["simple-english"] }
436
646
  ];
437
647
  var MCP_ADAPTER_PACKAGE = "npm:pi-mcp-adapter";
648
+ var BUNDLED_AGENTS_DIR = resolveBundledAgentsDir();
438
649
  async function ensureMise(options = {}) {
439
- const homeDir = options.homeDir ?? homedir4();
440
- const current = await mise.executableCheck() ?? await mise.executableCheck(join5(homeDir, ".local", "bin", "mise"));
650
+ const homeDir = options.homeDir ?? homedir5();
651
+ const current = await mise.executableCheck() ?? await mise.executableCheck(join7(homeDir, ".local", "bin", "mise"));
441
652
  if (current)
442
653
  return { executable: current, action: createSetupAction("mise", "ready", current) };
443
654
  reportProgress(options, "Installing mise");
@@ -482,18 +693,33 @@ async function ensureMiseDeps(miseExecutable, options = {}) {
482
693
  async function ensurePiPlugins(options = {}) {
483
694
  const actions = await ensurePiPackages(PI_PACKAGES, options);
484
695
  const agentDir = options.agentDir ?? pi.agentDir(options.homeDir);
485
- const webSearch = await pi.configEnsure(join5(agentDir, "web-search.json"), (config) => ({ ...config, workflow: "auto-summary" }), options.dryRun);
696
+ const webSearch = await pi.configEnsure(join7(agentDir, "web-search.json"), (config) => ({ ...config, workflow: "auto-summary" }), options.dryRun);
486
697
  actions.push(getConfigSetupAction("web search settings", webSearch));
487
- const lsp = await pi.configEnsure(join5(agentDir, "pi-lsp.json"), (config) => ({
698
+ const lsp = await pi.configEnsure(join7(agentDir, "pi-lsp.json"), (config) => ({
488
699
  ...config,
489
700
  progressive: { ...getRecord(config.progressive), enabled: true, inject: "none" }
490
701
  }), options.dryRun);
491
702
  actions.push(getConfigSetupAction("pi-lsp settings", lsp));
492
703
  return actions;
493
704
  }
705
+ async function ensurePiAgents(options = {}) {
706
+ const agentDir = options.agentDir ?? pi.agentDir(options.homeDir);
707
+ const bundledAgentsDir = options.bundledAgentsDir ?? BUNDLED_AGENTS_DIR;
708
+ const userConfig = await loadDiffpiConfig({ homeDir: options.homeDir });
709
+ 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));
710
+ const actions = [];
711
+ for (const entry of entries) {
712
+ const id = basename2(entry.name, ".md").replace(/^diffpi-/, "");
713
+ const source = await readFile4(join7(bundledAgentsDir, entry.name), "utf8");
714
+ const content = materializeAgentModels(source, id, userConfig.config, options.availableModels);
715
+ const result = await pi.agentEnsure(entry.name, content, agentDir, options.dryRun);
716
+ actions.push(getConfigSetupAction(`pi agent ${id}`, result));
717
+ }
718
+ return actions;
719
+ }
494
720
  async function ensurePiSkills(miseExecutable, options = {}) {
495
721
  const agentDir = options.agentDir ?? pi.agentDir(options.homeDir);
496
- const sharedSkillsDir = join5(options.homeDir ?? homedir4(), ".agents", "skills");
722
+ const sharedSkillsDir = join7(options.homeDir ?? homedir5(), ".agents", "skills");
497
723
  const actions = [];
498
724
  for (const source of PI_SKILL_SOURCES) {
499
725
  const missing = [];
@@ -550,13 +776,55 @@ async function setupPi(options = {}) {
550
776
  actions.push(await ensureMiseHooks(miseResult.executable, options));
551
777
  actions.push(...await ensureMiseDeps(miseResult.executable, options));
552
778
  actions.push(...await ensurePiPlugins(options));
779
+ actions.push(...await ensurePiAgents(options));
553
780
  actions.push(...await ensurePiSkills(miseResult.executable, options));
554
781
  actions.push(...await ensureMcpAdapters(miseResult.executable, options));
555
782
  return {
556
783
  actions,
557
- 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"))
784
+ restartPi: setupRequiresRestart(actions)
558
785
  };
559
786
  }
787
+ function setupRequiresRestart(actions) {
788
+ 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"));
789
+ }
790
+ function materializeAgentModels(content, agentId, config, availableModels) {
791
+ const { frontmatter } = parseFrontmatter2(content.startsWith("\uFEFF") ? content.slice(1) : content);
792
+ const profilePreferences = [...getTextList(frontmatter.model), ...getTextList(frontmatter.model_fallbacks)];
793
+ const preferences = resolveAgentModelPreferences(agentId, profilePreferences, config);
794
+ let selectedIndex = availableModels === undefined && preferences.length > 0 ? 0 : -1;
795
+ let selectedModel = selectedIndex === 0 ? preferences[0] : undefined;
796
+ if (availableModels) {
797
+ for (const [index, preference] of preferences.entries()) {
798
+ const match = findPreferredModel(availableModels, preference);
799
+ if (!match)
800
+ continue;
801
+ selectedIndex = index;
802
+ selectedModel = `${match.provider}/${match.id}`;
803
+ break;
804
+ }
805
+ }
806
+ const fallbacks = preferences.filter((_preference, index) => index !== selectedIndex);
807
+ return replaceAgentModelFields(content, selectedModel, fallbacks);
808
+ }
809
+ function replaceAgentModelFields(content, model, fallbacks) {
810
+ const newline = content.includes(`\r
811
+ `) ? `\r
812
+ ` : `
813
+ `;
814
+ const lines = content.replaceAll(`\r
815
+ `, `
816
+ `).split(`
817
+ `);
818
+ const closingDelimiter = lines.indexOf("---", 1);
819
+ if (lines[0] !== "---" || closingDelimiter < 0)
820
+ return content;
821
+ const frontmatter = lines.slice(1, closingDelimiter).filter((line) => !/^model(?:_fallbacks)?:/.test(line));
822
+ if (model)
823
+ frontmatter.push(`model: ${model}`);
824
+ if (fallbacks.length > 0)
825
+ frontmatter.push(`model_fallbacks: ${fallbacks.join(", ")}`);
826
+ return ["---", ...frontmatter, "---", ...lines.slice(closingDelimiter + 1)].join(newline);
827
+ }
560
828
  async function ensurePiPackages(packages, options) {
561
829
  const executable = await pi.executableCheck();
562
830
  if (!executable && !options.dryRun)
@@ -578,6 +846,10 @@ ${source}`;
578
846
  }
579
847
  return actions;
580
848
  }
849
+ function getTextList(value) {
850
+ const values = Array.isArray(value) ? value : typeof value === "string" ? value.split(",") : [];
851
+ return values.filter((item) => typeof item === "string").map((item) => item.trim()).filter(Boolean);
852
+ }
581
853
  function getConfigSetupAction(name, result) {
582
854
  if (!result.changed)
583
855
  return createSetupAction(name, "ready", result.path);
@@ -596,11 +868,11 @@ function getRecord(value) {
596
868
  }
597
869
 
598
870
  // src/tools/setup.ts
599
- var setupParametersSchema = z2.object({
600
- issueTracker: z2.enum(["none", "linear", "jira"]).default("none").describe("Issue tracker MCP server to configure. Use none unless the user explicitly selects Linear or Jira.")
871
+ var setupParametersSchema = z4.object({
872
+ issueTracker: z4.enum(["none", "linear", "jira"]).default("none").describe("Issue tracker MCP server to configure. Use none unless the user explicitly selects Linear or Jira.")
601
873
  });
602
- var setupParameters = z2.toJSONSchema(setupParametersSchema, { io: "input" });
603
- var diffpiSetupTool = defineTool2({
874
+ var setupParameters = z4.toJSONSchema(setupParametersSchema, { io: "input" });
875
+ var diffpiSetupTool = defineTool3({
604
876
  name: "diffpi_setup",
605
877
  label: "diffpi setup",
606
878
  description: "Install or repair the @difflab/pi environment. This mutates user-level tool installations and configuration files.",
@@ -613,11 +885,12 @@ var diffpiSetupTool = defineTool2({
613
885
  ],
614
886
  parameters: setupParameters,
615
887
  executionMode: "sequential",
616
- async execute(_toolCallId, input, _signal, onUpdate) {
888
+ async execute(_toolCallId, input, _signal, onUpdate, ctx) {
617
889
  const params = setupParametersSchema.parse(input);
618
890
  const result = await setupPi({
619
891
  issueTracker: params.issueTracker,
620
892
  installMiseHook: true,
893
+ availableModels: ctx.modelRegistry.getAvailable(),
621
894
  onProgress(message) {
622
895
  onUpdate?.({ content: [{ type: "text", text: message }], details: {} });
623
896
  }
@@ -625,7 +898,7 @@ var diffpiSetupTool = defineTool2({
625
898
  return formatResult(result, "Setup complete.");
626
899
  }
627
900
  });
628
- var diffpiValidateTool = defineTool2({
901
+ var diffpiValidateTool = defineTool3({
629
902
  name: "diffpi_validate",
630
903
  label: "diffpi validate",
631
904
  description: "Inspect the @difflab/pi environment without installing software or changing configuration files.",
@@ -637,12 +910,13 @@ var diffpiValidateTool = defineTool2({
637
910
  ],
638
911
  parameters: setupParameters,
639
912
  executionMode: "sequential",
640
- async execute(_toolCallId, input) {
913
+ async execute(_toolCallId, input, _signal, _onUpdate, ctx) {
641
914
  const params = setupParametersSchema.parse(input);
642
915
  const result = await setupPi({
643
916
  issueTracker: params.issueTracker,
644
917
  installMiseHook: true,
645
- dryRun: true
918
+ dryRun: true,
919
+ availableModels: ctx.modelRegistry.getAvailable()
646
920
  });
647
921
  const incomplete = result.actions.some((item) => item.status === "planned");
648
922
  return formatResult(result, incomplete ? "Setup is incomplete." : "Setup is ready.");
@@ -663,11 +937,12 @@ ${lines.join(`
663
937
  }
664
938
 
665
939
  // src/tools/index.ts
666
- function createPiTools(pi) {
667
- return [diffpiSetupTool, diffpiValidateTool, createDiffpiReloadTool(pi)];
940
+ function createPiTools(pi, modes) {
941
+ return [diffpiSetupTool, diffpiValidateTool, createDiffpiReloadTool(pi), ...createModeTools(modes)];
668
942
  }
669
943
  export {
670
944
  createDiffpiReloadTool,
945
+ createModeTools,
671
946
  createPiTools,
672
947
  diffpiSetupTool,
673
948
  diffpiValidateTool