@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.
@@ -13323,11 +13323,376 @@ function rpiv_ask_user_question_default(pi) {
13323
13323
  registerAskUserQuestionReconciler(pi);
13324
13324
  }
13325
13325
 
13326
+ // src/modes.ts
13327
+ import {
13328
+ getAgentDir,
13329
+ parseFrontmatter as parseFrontmatter2
13330
+ } from "@earendil-works/pi-coding-agent";
13331
+ import { readFile as readFile2 } from "node:fs/promises";
13332
+ import { homedir as homedir3 } from "node:os";
13333
+ import { basename, extname, join as join5 } from "node:path";
13334
+
13335
+ // src/assets.ts
13336
+ import { existsSync as existsSync2 } from "node:fs";
13337
+ import { dirname as dirname2, join as join3 } from "node:path";
13338
+ import { fileURLToPath } from "node:url";
13339
+ function resolveBundledAgentsDir(moduleUrl = import.meta.url) {
13340
+ const moduleDir = dirname2(fileURLToPath(moduleUrl));
13341
+ const candidates = [
13342
+ join3(moduleDir, "agents"),
13343
+ join3(moduleDir, "..", "agents"),
13344
+ join3(moduleDir, "..", "..", "agents")
13345
+ ];
13346
+ return candidates.find((path) => existsSync2(path)) ?? candidates[1];
13347
+ }
13348
+
13349
+ // src/config.ts
13350
+ import { parseFrontmatter } from "@earendil-works/pi-coding-agent";
13351
+ import { z } from "zod";
13352
+ import { homedir as homedir2 } from "node:os";
13353
+ import { join as join4 } from "node:path";
13354
+
13355
+ // src/fsx.ts
13356
+ import { readdir, readFile } from "node:fs/promises";
13357
+ async function readDirectoryIfExists(path) {
13358
+ try {
13359
+ return await readdir(path, { withFileTypes: true });
13360
+ } catch (error) {
13361
+ if (isMissingPath(error))
13362
+ return [];
13363
+ throw error;
13364
+ }
13365
+ }
13366
+ async function readTextIfExists(path) {
13367
+ try {
13368
+ return await readFile(path, "utf8");
13369
+ } catch (error) {
13370
+ if (isMissingPath(error))
13371
+ return;
13372
+ throw error;
13373
+ }
13374
+ }
13375
+ function isMissingPath(error) {
13376
+ return error instanceof Error && "code" in error && error.code === "ENOENT";
13377
+ }
13378
+
13379
+ // src/config.ts
13380
+ var modelReferenceSchema = z.string().trim().min(1);
13381
+ var agentConfigSchema = z.object({
13382
+ models: z.array(modelReferenceSchema).optional()
13383
+ }).strict();
13384
+ var diffpiConfigSchema = z.object({
13385
+ agents: z.record(z.string(), agentConfigSchema).optional()
13386
+ }).strict();
13387
+ function diffpiConfigPaths(homeDir = homedir2()) {
13388
+ const directory = join4(homeDir, ".difflab", "diffpi");
13389
+ return {
13390
+ yaml: join4(directory, "config.yaml"),
13391
+ json: join4(directory, "config.json")
13392
+ };
13393
+ }
13394
+ async function loadDiffpiConfig(options = {}) {
13395
+ const paths = diffpiConfigPaths(options.homeDir);
13396
+ for (const [format, path] of [
13397
+ ["yaml", paths.yaml],
13398
+ ["json", paths.json]
13399
+ ]) {
13400
+ const content = await readTextIfExists(path);
13401
+ if (content === undefined)
13402
+ continue;
13403
+ try {
13404
+ const value = format === "yaml" ? parseYamlConfig(content) : JSON.parse(content);
13405
+ return { config: diffpiConfigSchema.parse(value ?? {}), path };
13406
+ } catch (error) {
13407
+ const reason = error instanceof Error ? error.message : String(error);
13408
+ throw new Error(`Invalid Diffpi config at ${path}: ${reason}`, { cause: error });
13409
+ }
13410
+ }
13411
+ return { config: {} };
13412
+ }
13413
+ function resolveAgentModelPreferences(agentId, profilePreferences, config) {
13414
+ const override = config.agents?.[agentId];
13415
+ if (override && Object.hasOwn(override, "models"))
13416
+ return [...override.models ?? []];
13417
+ return [...profilePreferences];
13418
+ }
13419
+ function findPreferredModel(models, preference) {
13420
+ const normalizedPreference = normalizeModelReference(preference);
13421
+ const exactReference = models.find((model) => normalizeModelReference(`${model.provider}/${model.id}`) === normalizedPreference);
13422
+ if (exactReference)
13423
+ return exactReference;
13424
+ const idPreference = preference.includes("/") ? preference.slice(preference.indexOf("/") + 1) : preference;
13425
+ const normalizedIdPreference = normalizeModelReference(idPreference);
13426
+ const exactId = models.find((model) => normalizeModelReference(model.id) === normalizedIdPreference);
13427
+ if (exactId)
13428
+ return exactId;
13429
+ const preferenceTokens = normalizedIdPreference.split("-").filter(Boolean);
13430
+ return models.find((model) => {
13431
+ const modelTokens = new Set(normalizeModelReference(model.id).split("-").filter(Boolean));
13432
+ return preferenceTokens.every((token) => modelTokens.has(token));
13433
+ });
13434
+ }
13435
+ function parseYamlConfig(content) {
13436
+ const document = content.replace(/^\uFEFF/, "").replace(/^---[^\S\r\n]*(?:#.*)?(?:\r?\n|$)/, "");
13437
+ return parseFrontmatter(`---
13438
+ ${document}
13439
+ ---
13440
+ `).frontmatter;
13441
+ }
13442
+ function normalizeModelReference(value) {
13443
+ return value.toLowerCase().replace(/^~/, "").replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
13444
+ }
13445
+
13446
+ // src/modes.ts
13447
+ async function discoverAgentModes(options) {
13448
+ const agentDir = options.agentDir ?? getAgentDir();
13449
+ const homeDir = options.homeDir ?? homedir3();
13450
+ const modes = new Map;
13451
+ const diagnostics = [];
13452
+ await loadAgentModes(options.bundledAgentsDir ?? BUNDLED_AGENTS_DIR, "diffpi agent", modes, diagnostics);
13453
+ await loadAgentModes(join5(agentDir, "agents"), "user agent", modes, diagnostics);
13454
+ if (options.includeSkills) {
13455
+ await loadSkillModes(join5(homeDir, ".agents", "skills"), "user skill", modes, diagnostics);
13456
+ await loadSkillModes(join5(agentDir, "skills"), "pi user skill", modes, diagnostics);
13457
+ }
13458
+ if (options.projectTrusted === true) {
13459
+ if (options.includeSkills) {
13460
+ await loadSkillModes(join5(options.cwd, ".agents", "skills"), "project skill", modes, diagnostics);
13461
+ await loadSkillModes(join5(options.cwd, ".pi", "skills"), "pi project skill", modes, diagnostics);
13462
+ }
13463
+ await loadAgentModes(join5(options.cwd, ".agents", "agents"), "project agent", modes, diagnostics);
13464
+ await loadAgentModes(join5(options.cwd, ".pi", "agents"), "pi project agent", modes, diagnostics);
13465
+ }
13466
+ const userConfig = await loadDiffpiConfig({ homeDir });
13467
+ const configuredModes = [...modes.values()].map((mode) => ({
13468
+ ...mode,
13469
+ modelPreferences: resolveAgentModelPreferences(mode.id, mode.modelPreferences, userConfig.config)
13470
+ }));
13471
+ return {
13472
+ modes: configuredModes.sort((left, right) => left.id.localeCompare(right.id)),
13473
+ diagnostics
13474
+ };
13475
+ }
13476
+ function resolveAgentMode(modes, requested) {
13477
+ const name = requested.trim();
13478
+ if (!name)
13479
+ return { ok: false, message: "Agent name is required." };
13480
+ const exact = modes.find((mode) => mode.id === name);
13481
+ if (exact)
13482
+ return { ok: true, active: exact, message: `Active inline agent: ${exact.id}.` };
13483
+ const lowerName = name.toLowerCase();
13484
+ const matches = modes.filter((mode) => mode.id.toLowerCase() === lowerName);
13485
+ if (matches.length === 1) {
13486
+ const active = matches[0];
13487
+ return { ok: true, active, message: `Active inline agent: ${active.id}.` };
13488
+ }
13489
+ if (matches.length > 1) {
13490
+ return {
13491
+ ok: false,
13492
+ message: `Inline agent "${name}" is ambiguous. Use one of: ${matches.map((mode) => mode.id).join(", ")}.`
13493
+ };
13494
+ }
13495
+ return { ok: false, message: `Unknown inline agent "${name}". Run /skill:mode or diffpi_modes_list.` };
13496
+ }
13497
+ function createModeController(pi, options = {}) {
13498
+ let active;
13499
+ let baseline;
13500
+ const updateStatus = (ctx) => {
13501
+ ctx.ui.setStatus(MODE_STATUS_KEY, active ? `mode: ${active.id}` : undefined);
13502
+ };
13503
+ const list = (ctx, listOptions = {}) => discoverAgentModes({
13504
+ cwd: ctx.cwd,
13505
+ agentDir: options.agentDir,
13506
+ bundledAgentsDir: options.bundledAgentsDir,
13507
+ homeDir: options.homeDir,
13508
+ projectTrusted: ctx.isProjectTrusted(),
13509
+ includeSkills: listOptions.includeSkills
13510
+ });
13511
+ return {
13512
+ list,
13513
+ async set(agent, ctx) {
13514
+ const catalog = await list(ctx, { includeSkills: agent.includes(":") });
13515
+ const result = resolveAgentMode(catalog.modes, agent);
13516
+ if (!result.ok || !result.active)
13517
+ return result;
13518
+ baseline ??= captureRuntime(pi, ctx);
13519
+ if (active && baseline)
13520
+ await restoreRuntime(pi, baseline, ctx);
13521
+ active = result.active;
13522
+ const runtimeMessage = await applyModeRuntime(pi, active, ctx);
13523
+ pi.appendEntry(MODE_STATE_ENTRY, { active, baseline });
13524
+ updateStatus(ctx);
13525
+ return { ...result, message: `${result.message} ${runtimeMessage}` };
13526
+ },
13527
+ async unset(ctx) {
13528
+ if (!active)
13529
+ return { ok: true, message: "Inline agent is already clear." };
13530
+ if (baseline)
13531
+ await restoreRuntime(pi, baseline, ctx);
13532
+ active = undefined;
13533
+ pi.appendEntry(MODE_STATE_ENTRY, { active: null });
13534
+ baseline = undefined;
13535
+ updateStatus(ctx);
13536
+ return { ok: true, message: "Inline agent cleared. The previous model, thinking, tools, and prompt resume." };
13537
+ },
13538
+ async restore(ctx) {
13539
+ const previousActive = active;
13540
+ const previousBaseline = baseline;
13541
+ const entry = [...ctx.sessionManager.getBranch()].reverse().find((candidate) => candidate.type === "custom" && candidate.customType === MODE_STATE_ENTRY);
13542
+ const restored = entry?.data?.active;
13543
+ const restoredBaseline = entry?.data?.baseline;
13544
+ if (isAgentModeSnapshot(restored)) {
13545
+ active = restored;
13546
+ baseline = isModeBaseline(restoredBaseline) ? restoredBaseline : previousBaseline;
13547
+ await applyModeRuntime(pi, active, ctx);
13548
+ } else {
13549
+ if (previousActive && previousBaseline)
13550
+ pi.setActiveTools(previousBaseline.tools);
13551
+ active = undefined;
13552
+ baseline = undefined;
13553
+ }
13554
+ updateStatus(ctx);
13555
+ },
13556
+ apply(systemPrompt) {
13557
+ if (!active)
13558
+ return systemPrompt;
13559
+ if (active.promptStrategy === "replace")
13560
+ return active.systemPrompt;
13561
+ return `${systemPrompt}
13562
+
13563
+ ## Active inline agent: ${active.label}
13564
+
13565
+ ${active.systemPrompt}`;
13566
+ },
13567
+ getActive() {
13568
+ return active;
13569
+ }
13570
+ };
13571
+ }
13572
+ var MODE_STATE_ENTRY = "diffpi-mode-state";
13573
+ var MODE_STATUS_KEY = "diffpi-mode";
13574
+ var MODE_CONTROL_TOOLS = ["ask_user_question", "diffpi_modes_list", "diffpi_modes_set", "diffpi_modes_unset"];
13575
+ var BUNDLED_AGENTS_DIR = resolveBundledAgentsDir();
13576
+ var THINKING_LEVELS = new Set(["off", "minimal", "low", "medium", "high", "xhigh", "max"]);
13577
+ async function applyModeRuntime(pi, mode, ctx) {
13578
+ let selectedModel;
13579
+ if (mode.modelPreferences.length > 0) {
13580
+ const scoped = ctx.scopedModels.length > 0 ? ctx.scopedModels.map((entry) => entry.model) : undefined;
13581
+ const availableModels = scoped ?? ctx.modelRegistry.getAvailable();
13582
+ for (const preference of mode.modelPreferences) {
13583
+ const model = findPreferredModel(availableModels, preference);
13584
+ if (model && await pi.setModel(model)) {
13585
+ selectedModel = `${model.provider}/${model.id}`;
13586
+ break;
13587
+ }
13588
+ }
13589
+ }
13590
+ if (mode.thinkingLevel)
13591
+ pi.setThinkingLevel(mode.thinkingLevel);
13592
+ if (mode.tools.length > 0) {
13593
+ const availableTools = new Set(pi.getAllTools().map((tool) => tool.name));
13594
+ const selectedTools = [...new Set([...mode.tools, ...MODE_CONTROL_TOOLS])].filter((tool) => availableTools.has(tool));
13595
+ if (selectedTools.length > 0)
13596
+ pi.setActiveTools(selectedTools);
13597
+ }
13598
+ const parts = [];
13599
+ if (mode.modelPreferences.length > 0) {
13600
+ parts.push(selectedModel ? `Model: ${selectedModel}.` : "No preferred model was available; kept the current model.");
13601
+ }
13602
+ if (mode.thinkingLevel)
13603
+ parts.push(`Thinking: ${mode.thinkingLevel}.`);
13604
+ if (mode.tools.length > 0)
13605
+ parts.push("Applied the profile tool set.");
13606
+ return parts.join(" ") || "The profile changes the prompt only.";
13607
+ }
13608
+ async function restoreRuntime(pi, state, ctx) {
13609
+ if (state.model) {
13610
+ const model = ctx.modelRegistry.find(state.model.provider, state.model.id);
13611
+ if (model)
13612
+ await pi.setModel(model);
13613
+ }
13614
+ pi.setThinkingLevel(state.thinkingLevel);
13615
+ pi.setActiveTools(state.tools);
13616
+ }
13617
+ function captureRuntime(pi, ctx) {
13618
+ return {
13619
+ model: ctx.model ? { provider: ctx.model.provider, id: ctx.model.id } : undefined,
13620
+ thinkingLevel: pi.getThinkingLevel(),
13621
+ tools: pi.getActiveTools()
13622
+ };
13623
+ }
13624
+ async function loadSkillModes(skillsDir, source, modes, diagnostics) {
13625
+ const entries = await readDirectoryIfExists(skillsDir);
13626
+ for (const entry of entries.filter((item) => item.isDirectory()).sort((a, b) => a.name.localeCompare(b.name))) {
13627
+ await loadAgentModes(join5(skillsDir, entry.name, "agents"), `${source} ${entry.name}`, modes, diagnostics, entry.name);
13628
+ }
13629
+ }
13630
+ async function loadAgentModes(directory, source, modes, diagnostics, skillName) {
13631
+ const entries = await readDirectoryIfExists(directory);
13632
+ for (const entry of entries.filter((item) => item.isFile() && item.name.endsWith(".md")).sort((a, b) => a.name.localeCompare(b.name))) {
13633
+ const path = join5(directory, entry.name);
13634
+ try {
13635
+ const content = await readFile2(path, "utf8");
13636
+ const { frontmatter, body } = parseFrontmatter2(content.startsWith("\uFEFF") ? content.slice(1) : content);
13637
+ if (frontmatter.enabled === false || frontmatter.inline === false)
13638
+ continue;
13639
+ const name = getFrontmatterText(frontmatter.name) ?? basename(path, extname(path));
13640
+ const systemPrompt = body.trim();
13641
+ if (!name || name.includes(":") || !systemPrompt) {
13642
+ diagnostics.push(`Skipped ${path}: agent name must not contain ":" and prompt body is required.`);
13643
+ continue;
13644
+ }
13645
+ const id = skillName ? `${skillName}:${name}` : name;
13646
+ modes.set(id, {
13647
+ id,
13648
+ label: getFrontmatterText(frontmatter.display_name) ?? name,
13649
+ description: getFrontmatterText(frontmatter.description) ?? `Inline agent from ${basename(path)}`,
13650
+ systemPrompt,
13651
+ promptStrategy: frontmatter.prompt_mode === "append" ? "append" : "replace",
13652
+ modelPreferences: [
13653
+ ...getFrontmatterList(frontmatter.model),
13654
+ ...getFrontmatterList(frontmatter.model_fallbacks)
13655
+ ],
13656
+ thinkingLevel: getThinkingLevel(frontmatter.thinking),
13657
+ tools: getFrontmatterList(frontmatter.tools),
13658
+ source,
13659
+ sourcePath: path
13660
+ });
13661
+ } catch (error) {
13662
+ diagnostics.push(`Skipped ${path}: ${error instanceof Error ? error.message : String(error)}`);
13663
+ }
13664
+ }
13665
+ }
13666
+ function getFrontmatterText(value) {
13667
+ return typeof value === "string" && value.trim() ? value.trim() : undefined;
13668
+ }
13669
+ function getFrontmatterList(value) {
13670
+ const values = Array.isArray(value) ? value : typeof value === "string" ? value.split(",") : [];
13671
+ return values.filter((item) => typeof item === "string").map((item) => item.trim()).filter(Boolean);
13672
+ }
13673
+ function getThinkingLevel(value) {
13674
+ const level = getFrontmatterText(value);
13675
+ return level && THINKING_LEVELS.has(level) ? level : undefined;
13676
+ }
13677
+ function isAgentModeSnapshot(value) {
13678
+ if (!value || typeof value !== "object")
13679
+ return false;
13680
+ const candidate = value;
13681
+ 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";
13682
+ }
13683
+ function isModeBaseline(value) {
13684
+ if (!value || typeof value !== "object")
13685
+ return false;
13686
+ const candidate = value;
13687
+ const model = candidate.model;
13688
+ 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");
13689
+ }
13690
+
13326
13691
  // src/tools/reload.ts
13327
13692
  import { defineTool } from "@earendil-works/pi-coding-agent";
13328
- import { z } from "zod";
13329
- var reloadParametersSchema = z.object({});
13330
- var reloadParameters = z.toJSONSchema(reloadParametersSchema, { io: "input" });
13693
+ import { z as z2 } from "zod";
13694
+ var reloadParametersSchema = z2.object({});
13695
+ var reloadParameters = z2.toJSONSchema(reloadParametersSchema, { io: "input" });
13331
13696
  function createDiffpiReloadTool(pi) {
13332
13697
  return defineTool({
13333
13698
  name: "diffpi_reload",
@@ -13350,21 +13715,122 @@ function createDiffpiReloadTool(pi) {
13350
13715
  });
13351
13716
  }
13352
13717
 
13353
- // src/tools/setup.ts
13718
+ // src/tools/modes.ts
13354
13719
  import { defineTool as defineTool2 } from "@earendil-works/pi-coding-agent";
13355
- import { z as z2 } from "zod";
13720
+ import { z as z3 } from "zod";
13721
+ var emptyParametersSchema = z3.object({});
13722
+ var emptyParameters = z3.toJSONSchema(emptyParametersSchema, { io: "input" });
13723
+ var listParametersSchema = z3.object({
13724
+ includeSkills: z3.boolean().optional().describe("Include skill-owned agents using skill:agent ids.")
13725
+ });
13726
+ var listParameters = z3.toJSONSchema(listParametersSchema, { io: "input" });
13727
+ var setParametersSchema = z3.object({
13728
+ agent: z3.string().trim().min(1).describe("Inline agent id from diffpi_modes_list.")
13729
+ });
13730
+ var setParameters = z3.toJSONSchema(setParametersSchema, { io: "input" });
13731
+ function createModeTools(controller) {
13732
+ return [
13733
+ defineTool2({
13734
+ name: "diffpi_modes_list",
13735
+ label: "diffpi modes list",
13736
+ description: "List inline agents shared with the subagent plugin, optionally including skill-owned agents.",
13737
+ promptSnippet: "List inline agents before selecting one when the requested agent is unclear",
13738
+ promptGuidelines: [
13739
+ "Call diffpi_modes_list when the user asks which inline agents are available.",
13740
+ "Set includeSkills to true only when the user asks for skill agents or runs /skill:mode --include-skills.",
13741
+ "Inline mode applies the profile prompt, first available preferred model, thinking level, and available tools."
13742
+ ],
13743
+ parameters: listParameters,
13744
+ executionMode: "parallel",
13745
+ async execute(_toolCallId, input, _signal, _onUpdate, ctx) {
13746
+ const params = listParametersSchema.parse(input);
13747
+ const catalog = await controller.list(ctx, { includeSkills: params.includeSkills });
13748
+ return {
13749
+ content: [{ type: "text", text: formatCatalog(catalog, controller.getActive()?.id) }],
13750
+ details: { active: controller.getActive()?.id, catalog }
13751
+ };
13752
+ }
13753
+ }),
13754
+ defineTool2({
13755
+ name: "diffpi_modes_set",
13756
+ label: "diffpi modes set",
13757
+ description: "Set a validated available agent as the inline behavioral agent for subsequent chat turns.",
13758
+ promptSnippet: "Set the inline behavioral agent only after the user chooses one",
13759
+ promptGuidelines: [
13760
+ "Call diffpi_modes_set only after the user explicitly selects an agent.",
13761
+ "Use the exact skill:agent id for a skill-owned agent.",
13762
+ "The selected prompt takes effect on the next model turn."
13763
+ ],
13764
+ parameters: setParameters,
13765
+ executionMode: "sequential",
13766
+ async execute(_toolCallId, input, _signal, _onUpdate, ctx) {
13767
+ const params = setParametersSchema.parse(input);
13768
+ const result = await controller.set(params.agent, ctx);
13769
+ if (!result.ok)
13770
+ throw new Error(result.message);
13771
+ return {
13772
+ content: [{ type: "text", text: `${result.message} The prompt takes effect on the next turn.` }],
13773
+ details: { active: result.active }
13774
+ };
13775
+ }
13776
+ }),
13777
+ defineTool2({
13778
+ name: "diffpi_modes_unset",
13779
+ label: "diffpi modes unset",
13780
+ description: "Clear the inline behavioral agent and restore default Pi prompting for subsequent turns.",
13781
+ promptSnippet: "Clear the inline agent when the user asks for default behavior",
13782
+ promptGuidelines: [
13783
+ "Call diffpi_modes_unset only when the user explicitly asks to clear the active inline agent."
13784
+ ],
13785
+ parameters: emptyParameters,
13786
+ executionMode: "sequential",
13787
+ async execute(_toolCallId, input, _signal, _onUpdate, ctx) {
13788
+ emptyParametersSchema.parse(input);
13789
+ const result = await controller.unset(ctx);
13790
+ return {
13791
+ content: [{ type: "text", text: result.message }],
13792
+ details: { active: controller.getActive()?.id }
13793
+ };
13794
+ }
13795
+ })
13796
+ ];
13797
+ }
13798
+ function formatCatalog(catalog, active) {
13799
+ const lines = [`Active inline agent: ${active ?? "default"}.`, "", "Available inline agents:"];
13800
+ for (const mode of catalog.modes) {
13801
+ const runtime = [mode.modelPreferences[0], mode.thinkingLevel].filter(Boolean).join(", ");
13802
+ lines.push(`- ${mode.id} [${mode.promptStrategy}${runtime ? `; ${runtime}` : ""}] — ${sanitize(mode.description)} (${mode.source})`);
13803
+ }
13804
+ if (catalog.diagnostics.length > 0) {
13805
+ lines.push("", "Skipped agent files:");
13806
+ for (const diagnostic of catalog.diagnostics)
13807
+ lines.push(`- ${sanitize(diagnostic)}`);
13808
+ }
13809
+ lines.push("", "Inline mode applies the profile prompt, preferred available model, thinking level, and tool set.");
13810
+ return lines.join(`
13811
+ `);
13812
+ }
13813
+ function sanitize(value) {
13814
+ return value.replace(/[\r\n\t]+/g, " ").replace(/\s+/g, " ").trim();
13815
+ }
13816
+
13817
+ // src/tools/setup.ts
13818
+ import { defineTool as defineTool3 } from "@earendil-works/pi-coding-agent";
13819
+ import { z as z4 } from "zod";
13356
13820
 
13357
13821
  // src/setup.ts
13358
- import { homedir as homedir5 } from "node:os";
13359
- import { join as join7 } from "node:path";
13822
+ import { parseFrontmatter as parseFrontmatter3 } from "@earendil-works/pi-coding-agent";
13823
+ import { readdir as readdir2, readFile as readFile5 } from "node:fs/promises";
13824
+ import { homedir as homedir7 } from "node:os";
13825
+ import { basename as basename3, join as join10 } from "node:path";
13360
13826
 
13361
13827
  // src/mcp.ts
13362
- import { mkdir, readFile, writeFile } from "node:fs/promises";
13363
- import { homedir as homedir2 } from "node:os";
13364
- import { dirname as dirname2, join as join3 } from "node:path";
13828
+ import { mkdir, readFile as readFile3, writeFile } from "node:fs/promises";
13829
+ import { homedir as homedir4 } from "node:os";
13830
+ import { dirname as dirname3, join as join6 } from "node:path";
13365
13831
  var mcp = {
13366
- globalConfigPath(homeDir = homedir2()) {
13367
- return join3(homeDir, ".config", "mcp", "mcp.json");
13832
+ globalConfigPath(homeDir = homedir4()) {
13833
+ return join6(homeDir, ".config", "mcp", "mcp.json");
13368
13834
  },
13369
13835
  async serversEnsure(servers, options = {}) {
13370
13836
  const path = options.path ?? mcp.globalConfigPath();
@@ -13377,7 +13843,7 @@ var mcp = {
13377
13843
  const next = { ...current, mcpServers: nextServers };
13378
13844
  const changed = JSON.stringify(current) !== JSON.stringify(next);
13379
13845
  if (changed && !options.dryRun) {
13380
- await mkdir(dirname2(path), { recursive: true });
13846
+ await mkdir(dirname3(path), { recursive: true });
13381
13847
  await writeFile(path, `${JSON.stringify(next, null, 2)}
13382
13848
  `, "utf8");
13383
13849
  }
@@ -13407,7 +13873,7 @@ function getParsedConfig(content, path) {
13407
13873
  }
13408
13874
  async function getOptionalFile(path) {
13409
13875
  try {
13410
- return await readFile(path, "utf8");
13876
+ return await readFile3(path, "utf8");
13411
13877
  } catch (error) {
13412
13878
  if (error instanceof Error && "code" in error && error.code === "ENOENT")
13413
13879
  return;
@@ -13419,14 +13885,14 @@ function isRecord(value) {
13419
13885
  }
13420
13886
 
13421
13887
  // src/mise.ts
13422
- import { mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2 } from "node:fs/promises";
13423
- import { homedir as homedir3 } from "node:os";
13424
- import { basename, dirname as dirname3, join as join5 } from "node:path";
13888
+ import { mkdir as mkdir2, readFile as readFile4, writeFile as writeFile2 } from "node:fs/promises";
13889
+ import { homedir as homedir5 } from "node:os";
13890
+ import { basename as basename2, dirname as dirname4, join as join8 } from "node:path";
13425
13891
 
13426
13892
  // src/process.ts
13427
13893
  import { constants } from "node:fs";
13428
13894
  import { access } from "node:fs/promises";
13429
- import { delimiter, join as join4 } from "node:path";
13895
+ import { delimiter, join as join7 } from "node:path";
13430
13896
  import { spawn as spawn2 } from "node:child_process";
13431
13897
  var MAX_CAPTURED_OUTPUT_LENGTH = 65536;
13432
13898
  async function findExecutable(name) {
@@ -13441,7 +13907,7 @@ async function findExecutable(name) {
13441
13907
  for (const directory of (process.env.PATH ?? "").split(delimiter)) {
13442
13908
  if (!directory)
13443
13909
  continue;
13444
- const candidate = join4(directory, name);
13910
+ const candidate = join7(directory, name);
13445
13911
  try {
13446
13912
  await access(candidate, constants.X_OK);
13447
13913
  return candidate;
@@ -13488,11 +13954,11 @@ var mise = {
13488
13954
  return findExecutable(name);
13489
13955
  },
13490
13956
  async install(options = {}) {
13491
- const homeDir = options.homeDir ?? homedir3();
13957
+ const homeDir = options.homeDir ?? homedir5();
13492
13958
  const platform = options.platform ?? process.platform;
13493
13959
  if (platform === "win32")
13494
13960
  throw new Error("Automatic mise installation supports macOS and Linux only.");
13495
- const installedPath = join5(homeDir, ".local", "bin", "mise");
13961
+ const installedPath = join8(homeDir, ".local", "bin", "mise");
13496
13962
  if (options.dryRun)
13497
13963
  return installedPath;
13498
13964
  await runChecked("sh", ["-c", "curl -fsSL https://mise.run | sh"]);
@@ -13502,8 +13968,8 @@ var mise = {
13502
13968
  return executable;
13503
13969
  },
13504
13970
  async hookEnsure(executable, options = {}) {
13505
- const homeDir = options.homeDir ?? homedir3();
13506
- const hook = getShellHook(basename(options.shell ?? process.env.SHELL ?? ""), executable, homeDir);
13971
+ const homeDir = options.homeDir ?? homedir5();
13972
+ const hook = getShellHook(basename2(options.shell ?? process.env.SHELL ?? ""), executable, homeDir);
13507
13973
  const current = await getOptionalFile2(hook.path);
13508
13974
  if (current.includes(MISE_HOOK_START))
13509
13975
  return { path: hook.path, changed: false, planned: false };
@@ -13512,7 +13978,7 @@ var mise = {
13512
13978
  const separator = current.length === 0 || current.endsWith(`
13513
13979
  `) ? "" : `
13514
13980
  `;
13515
- await mkdir2(dirname3(hook.path), { recursive: true });
13981
+ await mkdir2(dirname4(hook.path), { recursive: true });
13516
13982
  await writeFile2(hook.path, `${current}${separator}${hook.content}`, "utf8");
13517
13983
  return { path: hook.path, changed: true, planned: false };
13518
13984
  },
@@ -13530,7 +13996,7 @@ var mise = {
13530
13996
  async toolInstallLocal(executable, specification, cwd = process.cwd()) {
13531
13997
  await runChecked(executable, ["use", "--path", cwd, specification], { cwd });
13532
13998
  },
13533
- async toolUpdateAllGlobal(executable, homeDir = homedir3()) {
13999
+ async toolUpdateAllGlobal(executable, homeDir = homedir5()) {
13534
14000
  await runChecked(executable, ["upgrade"], { cwd: homeDir });
13535
14001
  }
13536
14002
  };
@@ -13539,7 +14005,7 @@ function getShellHook(shell, executable, homeDir) {
13539
14005
  switch (shell.toLowerCase()) {
13540
14006
  case "zsh":
13541
14007
  return {
13542
- path: join5(homeDir, ".zshrc"),
14008
+ path: join8(homeDir, ".zshrc"),
13543
14009
  content: `${MISE_HOOK_START}
13544
14010
  eval "$(${command} activate zsh)"
13545
14011
  ${MISE_HOOK_END}
@@ -13547,7 +14013,7 @@ ${MISE_HOOK_END}
13547
14013
  };
13548
14014
  case "fish":
13549
14015
  return {
13550
- path: join5(homeDir, ".config", "fish", "config.fish"),
14016
+ path: join8(homeDir, ".config", "fish", "config.fish"),
13551
14017
  content: `${MISE_HOOK_START}
13552
14018
  ${command} activate fish | source
13553
14019
  ${MISE_HOOK_END}
@@ -13556,7 +14022,7 @@ ${MISE_HOOK_END}
13556
14022
  case "nu":
13557
14023
  case "nushell":
13558
14024
  return {
13559
- path: join5(homeDir, ".config", "nushell", "config.nu"),
14025
+ path: join8(homeDir, ".config", "nushell", "config.nu"),
13560
14026
  content: `${MISE_HOOK_START}
13561
14027
  let mise_bin = ${command}
13562
14028
  let mise_path = $nu.default-config-dir | path join mise.nu
@@ -13567,7 +14033,7 @@ ${MISE_HOOK_END}
13567
14033
  };
13568
14034
  case "xonsh":
13569
14035
  return {
13570
- path: join5(homeDir, ".xonshrc"),
14036
+ path: join8(homeDir, ".xonshrc"),
13571
14037
  content: `${MISE_HOOK_START}
13572
14038
  execx($(${command} activate xonsh))
13573
14039
  ${MISE_HOOK_END}
@@ -13575,7 +14041,7 @@ ${MISE_HOOK_END}
13575
14041
  };
13576
14042
  case "elvish":
13577
14043
  return {
13578
- path: join5(homeDir, ".config", "elvish", "rc.elv"),
14044
+ path: join8(homeDir, ".config", "elvish", "rc.elv"),
13579
14045
  content: `${MISE_HOOK_START}
13580
14046
  var mise: = (ns [&])
13581
14047
  eval (${command} activate elvish | slurp) &ns=$mise: &on-end={|ns| set mise: = $ns }
@@ -13586,7 +14052,7 @@ ${MISE_HOOK_END}
13586
14052
  case "pwsh":
13587
14053
  case "powershell":
13588
14054
  return {
13589
- path: join5(homeDir, ".config", "powershell", "Microsoft.PowerShell_profile.ps1"),
14055
+ path: join8(homeDir, ".config", "powershell", "Microsoft.PowerShell_profile.ps1"),
13590
14056
  content: `${MISE_HOOK_START}
13591
14057
  (& ${command} activate pwsh) | Out-String | Invoke-Expression
13592
14058
  ${MISE_HOOK_END}
@@ -13595,7 +14061,7 @@ ${MISE_HOOK_END}
13595
14061
  case "bash":
13596
14062
  default:
13597
14063
  return {
13598
- path: join5(homeDir, ".bashrc"),
14064
+ path: join8(homeDir, ".bashrc"),
13599
14065
  content: `${MISE_HOOK_START}
13600
14066
  eval "$(${command} activate bash)"
13601
14067
  ${MISE_HOOK_END}
@@ -13634,7 +14100,7 @@ function isVersionAtLeast(version, minimumVersion) {
13634
14100
  }
13635
14101
  async function getOptionalFile2(path) {
13636
14102
  try {
13637
- return await readFile2(path, "utf8");
14103
+ return await readFile4(path, "utf8");
13638
14104
  } catch (error) {
13639
14105
  if (error instanceof Error && "code" in error && error.code === "ENOENT")
13640
14106
  return "";
@@ -13646,79 +14112,86 @@ function getShellQuoted(value) {
13646
14112
  }
13647
14113
 
13648
14114
  // src/pi.ts
13649
- import { mkdir as mkdir3, readFile as readFile3, writeFile as writeFile3 } from "node:fs/promises";
13650
- import { homedir as homedir4 } from "node:os";
13651
- import { dirname as dirname4, join as join6 } from "node:path";
14115
+ import { mkdir as mkdir3, writeFile as writeFile3 } from "node:fs/promises";
14116
+ import { homedir as homedir6 } from "node:os";
14117
+ import { dirname as dirname5, join as join9 } from "node:path";
13652
14118
  var pi = {
13653
- async executableCheck() {
13654
- return findExecutable("pi");
13655
- },
13656
- async packageList(executable) {
13657
- return (await runChecked(executable, ["list"])).stdout;
13658
- },
13659
- packageCheck(listOutput, source) {
13660
- if (listOutput.includes(source))
14119
+ executableCheck: findPiExecutable,
14120
+ packageList: listPiPackages,
14121
+ packageCheck: hasPiPackage,
14122
+ packageInstall: installPiPackage,
14123
+ agentDir: resolvePiAgentDir,
14124
+ agentEnsure: ensurePiAgent,
14125
+ skillCheckGlobal: checkGlobalPiSkill,
14126
+ skillInstallGlobal: installGlobalPiSkills,
14127
+ configEnsure: ensurePiConfig
14128
+ };
14129
+ async function ensurePiAgent(filename, content, agentDir = resolvePiAgentDir(), dryRun = false) {
14130
+ const path = join9(agentDir, "agents", filename);
14131
+ const currentText = await readTextIfExists(path);
14132
+ const changed = currentText !== content;
14133
+ if (changed && !dryRun) {
14134
+ await mkdir3(dirname5(path), { recursive: true });
14135
+ await writeFile3(path, content, "utf8");
14136
+ }
14137
+ return { path, changed, existed: currentText !== undefined, planned: changed && dryRun };
14138
+ }
14139
+ async function checkGlobalPiSkill(name, agentDir = resolvePiAgentDir(), sharedSkillsDir = join9(homedir6(), ".agents", "skills")) {
14140
+ const roots = [join9(agentDir, "skills"), sharedSkillsDir];
14141
+ for (const root of roots) {
14142
+ if (await readTextIfExists(join9(root, name, "SKILL.md")) !== undefined)
13661
14143
  return true;
13662
- return source.startsWith("https://") && listOutput.includes(source.slice("https://".length));
13663
- },
13664
- async packageInstall(executable, source) {
13665
- await runChecked(executable, ["install", source]);
13666
- },
13667
- agentDir(homeDir = homedir4()) {
13668
- return getAgentDir(homeDir);
13669
- },
13670
- async skillCheckGlobal(name, agentDir = getAgentDir(), sharedSkillsDir = join6(homedir4(), ".agents", "skills")) {
13671
- const roots = [join6(agentDir, "skills"), sharedSkillsDir];
13672
- for (const root of roots) {
13673
- if (await getOptionalFile3(join6(root, name, "SKILL.md")) !== undefined)
13674
- return true;
13675
- }
13676
- return false;
13677
- },
13678
- async skillInstallGlobal(miseExecutable, source, names) {
13679
- const selection = names.flatMap((name) => ["--skill", name]);
13680
- await runChecked(miseExecutable, [
13681
- "x",
13682
- "node@22",
13683
- "--",
13684
- "npx",
13685
- "-y",
13686
- "skills",
13687
- "add",
13688
- source,
13689
- ...selection,
13690
- "--global",
13691
- "--agent",
13692
- "pi",
13693
- "--yes"
13694
- ]);
13695
- },
13696
- async configEnsure(path, update, dryRun = false) {
13697
- const currentText = await getOptionalFile3(path);
13698
- const current = getParsedObject(currentText, path);
13699
- const next = update(current);
13700
- const changed = JSON.stringify(current) !== JSON.stringify(next);
13701
- if (changed && !dryRun) {
13702
- await mkdir3(dirname4(path), { recursive: true });
13703
- await writeFile3(path, `${JSON.stringify(next, null, 2)}
13704
- `, "utf8");
13705
- }
13706
- return { path, changed, existed: currentText !== undefined, planned: changed && dryRun };
13707
14144
  }
13708
- };
13709
- function getAgentDir(homeDir = homedir4()) {
13710
- return process.env.PI_CODING_AGENT_DIR ?? (process.env.XDG_CONFIG_HOME ? join6(process.env.XDG_CONFIG_HOME, "pi") : join6(homeDir, ".pi", "agent"));
14145
+ return false;
13711
14146
  }
13712
- async function getOptionalFile3(path) {
13713
- try {
13714
- return await readFile3(path, "utf8");
13715
- } catch (error) {
13716
- if (error instanceof Error && "code" in error && error.code === "ENOENT")
13717
- return;
13718
- throw error;
14147
+ async function installGlobalPiSkills(miseExecutable, source, names) {
14148
+ const selection = names.flatMap((name) => ["--skill", name]);
14149
+ await runChecked(miseExecutable, [
14150
+ "x",
14151
+ "node@22",
14152
+ "--",
14153
+ "npx",
14154
+ "-y",
14155
+ "skills",
14156
+ "add",
14157
+ source,
14158
+ ...selection,
14159
+ "--global",
14160
+ "--agent",
14161
+ "pi",
14162
+ "--yes"
14163
+ ]);
14164
+ }
14165
+ async function ensurePiConfig(path, update, dryRun = false) {
14166
+ const currentText = await readTextIfExists(path);
14167
+ const current = parseJsonObject(currentText, path);
14168
+ const next = update(current);
14169
+ const changed = JSON.stringify(current) !== JSON.stringify(next);
14170
+ if (changed && !dryRun) {
14171
+ await mkdir3(dirname5(path), { recursive: true });
14172
+ await writeFile3(path, `${JSON.stringify(next, null, 2)}
14173
+ `, "utf8");
13719
14174
  }
14175
+ return { path, changed, existed: currentText !== undefined, planned: changed && dryRun };
14176
+ }
14177
+ async function findPiExecutable() {
14178
+ return findExecutable("pi");
14179
+ }
14180
+ async function listPiPackages(executable) {
14181
+ return (await runChecked(executable, ["list"])).stdout;
13720
14182
  }
13721
- function getParsedObject(content, path) {
14183
+ function hasPiPackage(listOutput, source) {
14184
+ if (listOutput.includes(source))
14185
+ return true;
14186
+ return source.startsWith("https://") && listOutput.includes(source.slice("https://".length));
14187
+ }
14188
+ async function installPiPackage(executable, source) {
14189
+ await runChecked(executable, ["install", source]);
14190
+ }
14191
+ function resolvePiAgentDir(homeDir = homedir6()) {
14192
+ return process.env.PI_CODING_AGENT_DIR ?? (process.env.XDG_CONFIG_HOME ? join9(process.env.XDG_CONFIG_HOME, "pi") : join9(homeDir, ".pi", "agent"));
14193
+ }
14194
+ function parseJsonObject(content, path) {
13722
14195
  if (!content?.trim())
13723
14196
  return {};
13724
14197
  try {
@@ -13760,9 +14233,10 @@ var PI_SKILL_SOURCES = [
13760
14233
  { repository: "AminBlg/SimpleEnglish", skills: ["simple-english"] }
13761
14234
  ];
13762
14235
  var MCP_ADAPTER_PACKAGE = "npm:pi-mcp-adapter";
14236
+ var BUNDLED_AGENTS_DIR2 = resolveBundledAgentsDir();
13763
14237
  async function ensureMise(options = {}) {
13764
- const homeDir = options.homeDir ?? homedir5();
13765
- const current = await mise.executableCheck() ?? await mise.executableCheck(join7(homeDir, ".local", "bin", "mise"));
14238
+ const homeDir = options.homeDir ?? homedir7();
14239
+ const current = await mise.executableCheck() ?? await mise.executableCheck(join10(homeDir, ".local", "bin", "mise"));
13766
14240
  if (current)
13767
14241
  return { executable: current, action: createSetupAction("mise", "ready", current) };
13768
14242
  reportProgress(options, "Installing mise");
@@ -13807,18 +14281,33 @@ async function ensureMiseDeps(miseExecutable, options = {}) {
13807
14281
  async function ensurePiPlugins(options = {}) {
13808
14282
  const actions = await ensurePiPackages(PI_PACKAGES, options);
13809
14283
  const agentDir = options.agentDir ?? pi.agentDir(options.homeDir);
13810
- const webSearch = await pi.configEnsure(join7(agentDir, "web-search.json"), (config) => ({ ...config, workflow: "auto-summary" }), options.dryRun);
14284
+ const webSearch = await pi.configEnsure(join10(agentDir, "web-search.json"), (config) => ({ ...config, workflow: "auto-summary" }), options.dryRun);
13811
14285
  actions.push(getConfigSetupAction("web search settings", webSearch));
13812
- const lsp = await pi.configEnsure(join7(agentDir, "pi-lsp.json"), (config) => ({
14286
+ const lsp = await pi.configEnsure(join10(agentDir, "pi-lsp.json"), (config) => ({
13813
14287
  ...config,
13814
14288
  progressive: { ...getRecord(config.progressive), enabled: true, inject: "none" }
13815
14289
  }), options.dryRun);
13816
14290
  actions.push(getConfigSetupAction("pi-lsp settings", lsp));
13817
14291
  return actions;
13818
14292
  }
14293
+ async function ensurePiAgents(options = {}) {
14294
+ const agentDir = options.agentDir ?? pi.agentDir(options.homeDir);
14295
+ const bundledAgentsDir = options.bundledAgentsDir ?? BUNDLED_AGENTS_DIR2;
14296
+ const userConfig = await loadDiffpiConfig({ homeDir: options.homeDir });
14297
+ 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));
14298
+ const actions = [];
14299
+ for (const entry of entries) {
14300
+ const id = basename3(entry.name, ".md").replace(/^diffpi-/, "");
14301
+ const source = await readFile5(join10(bundledAgentsDir, entry.name), "utf8");
14302
+ const content = materializeAgentModels(source, id, userConfig.config, options.availableModels);
14303
+ const result = await pi.agentEnsure(entry.name, content, agentDir, options.dryRun);
14304
+ actions.push(getConfigSetupAction(`pi agent ${id}`, result));
14305
+ }
14306
+ return actions;
14307
+ }
13819
14308
  async function ensurePiSkills(miseExecutable, options = {}) {
13820
14309
  const agentDir = options.agentDir ?? pi.agentDir(options.homeDir);
13821
- const sharedSkillsDir = join7(options.homeDir ?? homedir5(), ".agents", "skills");
14310
+ const sharedSkillsDir = join10(options.homeDir ?? homedir7(), ".agents", "skills");
13822
14311
  const actions = [];
13823
14312
  for (const source of PI_SKILL_SOURCES) {
13824
14313
  const missing = [];
@@ -13875,13 +14364,55 @@ async function setupPi(options = {}) {
13875
14364
  actions.push(await ensureMiseHooks(miseResult.executable, options));
13876
14365
  actions.push(...await ensureMiseDeps(miseResult.executable, options));
13877
14366
  actions.push(...await ensurePiPlugins(options));
14367
+ actions.push(...await ensurePiAgents(options));
13878
14368
  actions.push(...await ensurePiSkills(miseResult.executable, options));
13879
14369
  actions.push(...await ensureMcpAdapters(miseResult.executable, options));
13880
14370
  return {
13881
14371
  actions,
13882
- 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"))
14372
+ restartPi: setupRequiresRestart(actions)
13883
14373
  };
13884
14374
  }
14375
+ function setupRequiresRestart(actions) {
14376
+ 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"));
14377
+ }
14378
+ function materializeAgentModels(content, agentId, config, availableModels) {
14379
+ const { frontmatter } = parseFrontmatter3(content.startsWith("\uFEFF") ? content.slice(1) : content);
14380
+ const profilePreferences = [...getTextList(frontmatter.model), ...getTextList(frontmatter.model_fallbacks)];
14381
+ const preferences = resolveAgentModelPreferences(agentId, profilePreferences, config);
14382
+ let selectedIndex = availableModels === undefined && preferences.length > 0 ? 0 : -1;
14383
+ let selectedModel = selectedIndex === 0 ? preferences[0] : undefined;
14384
+ if (availableModels) {
14385
+ for (const [index, preference] of preferences.entries()) {
14386
+ const match = findPreferredModel(availableModels, preference);
14387
+ if (!match)
14388
+ continue;
14389
+ selectedIndex = index;
14390
+ selectedModel = `${match.provider}/${match.id}`;
14391
+ break;
14392
+ }
14393
+ }
14394
+ const fallbacks = preferences.filter((_preference, index) => index !== selectedIndex);
14395
+ return replaceAgentModelFields(content, selectedModel, fallbacks);
14396
+ }
14397
+ function replaceAgentModelFields(content, model, fallbacks) {
14398
+ const newline = content.includes(`\r
14399
+ `) ? `\r
14400
+ ` : `
14401
+ `;
14402
+ const lines = content.replaceAll(`\r
14403
+ `, `
14404
+ `).split(`
14405
+ `);
14406
+ const closingDelimiter = lines.indexOf("---", 1);
14407
+ if (lines[0] !== "---" || closingDelimiter < 0)
14408
+ return content;
14409
+ const frontmatter = lines.slice(1, closingDelimiter).filter((line) => !/^model(?:_fallbacks)?:/.test(line));
14410
+ if (model)
14411
+ frontmatter.push(`model: ${model}`);
14412
+ if (fallbacks.length > 0)
14413
+ frontmatter.push(`model_fallbacks: ${fallbacks.join(", ")}`);
14414
+ return ["---", ...frontmatter, "---", ...lines.slice(closingDelimiter + 1)].join(newline);
14415
+ }
13885
14416
  async function ensurePiPackages(packages, options) {
13886
14417
  const executable = await pi.executableCheck();
13887
14418
  if (!executable && !options.dryRun)
@@ -13903,6 +14434,10 @@ ${source}`;
13903
14434
  }
13904
14435
  return actions;
13905
14436
  }
14437
+ function getTextList(value) {
14438
+ const values = Array.isArray(value) ? value : typeof value === "string" ? value.split(",") : [];
14439
+ return values.filter((item) => typeof item === "string").map((item) => item.trim()).filter(Boolean);
14440
+ }
13906
14441
  function getConfigSetupAction(name, result) {
13907
14442
  if (!result.changed)
13908
14443
  return createSetupAction(name, "ready", result.path);
@@ -13921,11 +14456,11 @@ function getRecord(value) {
13921
14456
  }
13922
14457
 
13923
14458
  // src/tools/setup.ts
13924
- var setupParametersSchema = z2.object({
13925
- 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.")
14459
+ var setupParametersSchema = z4.object({
14460
+ 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.")
13926
14461
  });
13927
- var setupParameters = z2.toJSONSchema(setupParametersSchema, { io: "input" });
13928
- var diffpiSetupTool = defineTool2({
14462
+ var setupParameters = z4.toJSONSchema(setupParametersSchema, { io: "input" });
14463
+ var diffpiSetupTool = defineTool3({
13929
14464
  name: "diffpi_setup",
13930
14465
  label: "diffpi setup",
13931
14466
  description: "Install or repair the @difflab/pi environment. This mutates user-level tool installations and configuration files.",
@@ -13938,11 +14473,12 @@ var diffpiSetupTool = defineTool2({
13938
14473
  ],
13939
14474
  parameters: setupParameters,
13940
14475
  executionMode: "sequential",
13941
- async execute(_toolCallId, input, _signal, onUpdate) {
14476
+ async execute(_toolCallId, input, _signal, onUpdate, ctx) {
13942
14477
  const params = setupParametersSchema.parse(input);
13943
14478
  const result = await setupPi({
13944
14479
  issueTracker: params.issueTracker,
13945
14480
  installMiseHook: true,
14481
+ availableModels: ctx.modelRegistry.getAvailable(),
13946
14482
  onProgress(message) {
13947
14483
  onUpdate?.({ content: [{ type: "text", text: message }], details: {} });
13948
14484
  }
@@ -13950,7 +14486,7 @@ var diffpiSetupTool = defineTool2({
13950
14486
  return formatResult(result, "Setup complete.");
13951
14487
  }
13952
14488
  });
13953
- var diffpiValidateTool = defineTool2({
14489
+ var diffpiValidateTool = defineTool3({
13954
14490
  name: "diffpi_validate",
13955
14491
  label: "diffpi validate",
13956
14492
  description: "Inspect the @difflab/pi environment without installing software or changing configuration files.",
@@ -13962,12 +14498,13 @@ var diffpiValidateTool = defineTool2({
13962
14498
  ],
13963
14499
  parameters: setupParameters,
13964
14500
  executionMode: "sequential",
13965
- async execute(_toolCallId, input) {
14501
+ async execute(_toolCallId, input, _signal, _onUpdate, ctx) {
13966
14502
  const params = setupParametersSchema.parse(input);
13967
14503
  const result = await setupPi({
13968
14504
  issueTracker: params.issueTracker,
13969
14505
  installMiseHook: true,
13970
- dryRun: true
14506
+ dryRun: true,
14507
+ availableModels: ctx.modelRegistry.getAvailable()
13971
14508
  });
13972
14509
  const incomplete = result.actions.some((item) => item.status === "planned");
13973
14510
  return formatResult(result, incomplete ? "Setup is incomplete." : "Setup is ready.");
@@ -13988,31 +14525,37 @@ ${lines.join(`
13988
14525
  }
13989
14526
 
13990
14527
  // src/tools/index.ts
13991
- function createPiTools(pi) {
13992
- return [diffpiSetupTool, diffpiValidateTool, createDiffpiReloadTool(pi)];
14528
+ function createPiTools(pi, modes) {
14529
+ return [diffpiSetupTool, diffpiValidateTool, createDiffpiReloadTool(pi), ...createModeTools(modes)];
13993
14530
  }
13994
14531
 
13995
14532
  // extensions/index.ts
13996
14533
  var RELOAD_COMMAND = "diffpi-reload";
13997
- var DOCS_ROUTING_GUIDANCE = `## Documentation routing
14534
+ var SKILL_ROUTING_GUIDANCE = `## Skill and tool routing
13998
14535
  Use the docs-search skill before web search for library or API documentation.
13999
14536
  Use docs-manage when the required documentation is absent or stale.
14000
- Use fetch-url for a one-time read that does not belong in the documentation index.`;
14537
+ Use fetch-url for a one-time read that does not belong in the documentation index.
14538
+ Use the context-mode skill for commands, tests, builds, logs, API responses, and other output that can be large.
14539
+ Use ctx_execute or ctx_execute_file to analyze that output, and use ctx_fetch_and_index with ctx_search for external documentation.`;
14001
14540
  function difflabPiExtension(pi) {
14002
14541
  rpiv_ask_user_question_default(pi);
14542
+ const modes = createModeController(pi);
14003
14543
  pi.registerCommand(RELOAD_COMMAND, {
14004
14544
  description: "Reload extensions, skills, prompts, themes, and context files",
14005
14545
  handler: async (_args, ctx) => {
14006
14546
  await ctx.reload();
14007
14547
  }
14008
14548
  });
14009
- for (const tool of createPiTools(pi))
14549
+ for (const tool of createPiTools(pi, modes))
14010
14550
  pi.registerTool(tool);
14011
- pi.on("before_agent_start", (event) => ({
14012
- systemPrompt: `${event.systemPrompt}
14551
+ pi.on("session_start", async (_event, ctx) => modes.restore(ctx));
14552
+ pi.on("session_tree", async (_event, ctx) => modes.restore(ctx));
14553
+ pi.on("before_agent_start", (event) => {
14554
+ const defaultPrompt = `${event.systemPrompt}
14013
14555
 
14014
- ${DOCS_ROUTING_GUIDANCE}`
14015
- }));
14556
+ ${SKILL_ROUTING_GUIDANCE}`;
14557
+ return { systemPrompt: modes.apply(defaultPrompt) };
14558
+ });
14016
14559
  }
14017
14560
  export {
14018
14561
  difflabPiExtension as default