@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/README.md +13 -1
- package/agents/diffpi-copilot.md +22 -0
- package/agents/diffpi-orchestrator.md +23 -0
- package/agents/diffpi-tutor.md +20 -0
- package/agents/diffpi-worker.md +19 -0
- package/dist/assets.d.ts +2 -0
- package/dist/assets.d.ts.map +1 -0
- package/dist/config.d.ts +28 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/extensions/index.js +666 -123
- package/dist/fsx.d.ts +6 -0
- package/dist/fsx.d.ts.map +1 -0
- package/dist/index.d.ts +5 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +540 -98
- package/dist/modes.d.ts +59 -0
- package/dist/modes.d.ts.map +1 -0
- package/dist/pi.d.ts +21 -5
- package/dist/pi.d.ts.map +1 -1
- package/dist/setup.d.ts +7 -0
- package/dist/setup.d.ts.map +1 -1
- package/dist/tools/index.d.ts +4 -2
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +386 -111
- package/dist/tools/modes.d.ts +4 -0
- package/dist/tools/modes.d.ts.map +1 -0
- package/dist/tools/setup.d.ts.map +1 -1
- package/package.json +2 -1
- package/skills/diffpi-setup/SKILL.md +2 -0
- package/skills/mode/SKILL.md +38 -0
package/dist/tools/index.js
CHANGED
|
@@ -25,21 +25,224 @@ function createDiffpiReloadTool(pi) {
|
|
|
25
25
|
});
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
// src/tools/
|
|
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 {
|
|
34
|
-
import {
|
|
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/
|
|
37
|
-
import {
|
|
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 =
|
|
42
|
-
return
|
|
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(
|
|
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
|
|
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
|
|
98
|
-
import { homedir as
|
|
99
|
-
import { basename, dirname as
|
|
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
|
|
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 =
|
|
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 ??
|
|
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 =
|
|
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 ??
|
|
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(
|
|
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 =
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
|
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,
|
|
325
|
-
import { homedir as
|
|
326
|
-
import { dirname as
|
|
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
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
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
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
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
|
|
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 ??
|
|
440
|
-
const current = await mise.executableCheck() ?? await mise.executableCheck(
|
|
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(
|
|
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(
|
|
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 =
|
|
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
|
|
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 =
|
|
600
|
-
issueTracker:
|
|
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 =
|
|
603
|
-
var diffpiSetupTool =
|
|
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 =
|
|
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
|