@difflab/pi 0.1.0-rc.202609140747.4d45e72.2
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 +14 -0
- package/dist/extensions/index.js +607 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +510 -0
- package/dist/mcp.d.ts +16 -0
- package/dist/mcp.d.ts.map +1 -0
- package/dist/mise.d.ts +26 -0
- package/dist/mise.d.ts.map +1 -0
- package/dist/pi.d.ts +19 -0
- package/dist/pi.d.ts.map +1 -0
- package/dist/process.d.ts +13 -0
- package/dist/process.d.ts.map +1 -0
- package/dist/setup.d.ts +33 -0
- package/dist/setup.d.ts.map +1 -0
- package/dist/tools/index.d.ts +9 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +594 -0
- package/dist/tools/setup.d.ts +15 -0
- package/dist/tools/setup.d.ts.map +1 -0
- package/package.json +80 -0
- package/skills/diffpi-setup/SKILL.md +39 -0
|
@@ -0,0 +1,594 @@
|
|
|
1
|
+
// src/tools/setup.ts
|
|
2
|
+
import { defineTool } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import { Type } from "typebox";
|
|
4
|
+
|
|
5
|
+
// src/setup.ts
|
|
6
|
+
import { homedir as homedir4 } from "node:os";
|
|
7
|
+
import { join as join5 } from "node:path";
|
|
8
|
+
|
|
9
|
+
// src/mcp.ts
|
|
10
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
11
|
+
import { homedir } from "node:os";
|
|
12
|
+
import { dirname, join } from "node:path";
|
|
13
|
+
var mcp = {
|
|
14
|
+
globalConfigPath(homeDir = homedir()) {
|
|
15
|
+
return join(homeDir, ".config", "mcp", "mcp.json");
|
|
16
|
+
},
|
|
17
|
+
async serversEnsure(servers, options = {}) {
|
|
18
|
+
const path = options.path ?? mcp.globalConfigPath();
|
|
19
|
+
const currentText = await readOptional(path);
|
|
20
|
+
const current = parseConfig(currentText, path);
|
|
21
|
+
const nextServers = { ...current.mcpServers };
|
|
22
|
+
for (const [name, entry] of Object.entries(servers)) {
|
|
23
|
+
nextServers[name] = mergeEntry(nextServers[name], entry);
|
|
24
|
+
}
|
|
25
|
+
const next = { ...current, mcpServers: nextServers };
|
|
26
|
+
const changed = JSON.stringify(current) !== JSON.stringify(next);
|
|
27
|
+
if (changed && !options.dryRun) {
|
|
28
|
+
await mkdir(dirname(path), { recursive: true });
|
|
29
|
+
await writeFile(path, `${JSON.stringify(next, null, 2)}
|
|
30
|
+
`, "utf8");
|
|
31
|
+
}
|
|
32
|
+
return { path, changed, existed: currentText !== undefined, planned: changed && options.dryRun === true };
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
function mergeEntry(current, required) {
|
|
36
|
+
const merged = { ...current, ...required };
|
|
37
|
+
if (current?.env || required.env)
|
|
38
|
+
merged.env = { ...current?.env, ...required.env };
|
|
39
|
+
return merged;
|
|
40
|
+
}
|
|
41
|
+
function parseConfig(content, path) {
|
|
42
|
+
if (!content?.trim())
|
|
43
|
+
return { mcpServers: {} };
|
|
44
|
+
try {
|
|
45
|
+
const value = JSON.parse(content);
|
|
46
|
+
if (!isRecord(value))
|
|
47
|
+
throw new Error("not an object");
|
|
48
|
+
const servers = value.mcpServers;
|
|
49
|
+
if (servers !== undefined && !isRecord(servers))
|
|
50
|
+
throw new Error("mcpServers is not an object");
|
|
51
|
+
return { ...value, mcpServers: servers ?? {} };
|
|
52
|
+
} catch {
|
|
53
|
+
throw new Error(`Expected a valid pi-mcp-adapter configuration in ${path}.`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
async function readOptional(path) {
|
|
57
|
+
try {
|
|
58
|
+
return await readFile(path, "utf8");
|
|
59
|
+
} catch (error) {
|
|
60
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT")
|
|
61
|
+
return;
|
|
62
|
+
throw error;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
function isRecord(value) {
|
|
66
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// src/mise.ts
|
|
70
|
+
import { mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2 } from "node:fs/promises";
|
|
71
|
+
import { homedir as homedir2 } from "node:os";
|
|
72
|
+
import { basename, dirname as dirname2, join as join3 } from "node:path";
|
|
73
|
+
|
|
74
|
+
// src/process.ts
|
|
75
|
+
import { constants } from "node:fs";
|
|
76
|
+
import { access } from "node:fs/promises";
|
|
77
|
+
import { delimiter, join as join2 } from "node:path";
|
|
78
|
+
import { spawn } from "node:child_process";
|
|
79
|
+
async function findExecutable(name) {
|
|
80
|
+
if (name.includes("/")) {
|
|
81
|
+
try {
|
|
82
|
+
await access(name, constants.X_OK);
|
|
83
|
+
return name;
|
|
84
|
+
} catch {
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
for (const directory of (process.env.PATH ?? "").split(delimiter)) {
|
|
89
|
+
if (!directory)
|
|
90
|
+
continue;
|
|
91
|
+
const candidate = join2(directory, name);
|
|
92
|
+
try {
|
|
93
|
+
await access(candidate, constants.X_OK);
|
|
94
|
+
return candidate;
|
|
95
|
+
} catch {}
|
|
96
|
+
}
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
function run(command, args, options = {}) {
|
|
100
|
+
return new Promise((resolve, reject) => {
|
|
101
|
+
const child = spawn(command, args, {
|
|
102
|
+
cwd: options.cwd,
|
|
103
|
+
env: options.env ?? process.env,
|
|
104
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
105
|
+
});
|
|
106
|
+
let stdout = "";
|
|
107
|
+
let stderr = "";
|
|
108
|
+
child.stdout.on("data", (chunk) => {
|
|
109
|
+
stdout = appendBounded(stdout, chunk.toString());
|
|
110
|
+
});
|
|
111
|
+
child.stderr.on("data", (chunk) => {
|
|
112
|
+
stderr = appendBounded(stderr, chunk.toString());
|
|
113
|
+
});
|
|
114
|
+
child.on("error", reject);
|
|
115
|
+
child.on("close", (code) => resolve({ code: code ?? 1, stdout, stderr }));
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
async function runChecked(command, args, options = {}) {
|
|
119
|
+
const result = await run(command, args, options);
|
|
120
|
+
if (result.code === 0)
|
|
121
|
+
return result;
|
|
122
|
+
const detail = result.stderr.trim() || result.stdout.trim() || `exit code ${result.code}`;
|
|
123
|
+
throw new Error(`${command} ${args.join(" ")} failed: ${detail}`);
|
|
124
|
+
}
|
|
125
|
+
function appendBounded(current, next) {
|
|
126
|
+
const combined = current + next;
|
|
127
|
+
return combined.length <= 65536 ? combined : combined.slice(-65536);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// src/mise.ts
|
|
131
|
+
var mise = {
|
|
132
|
+
async executableCheck(name = "mise") {
|
|
133
|
+
return findExecutable(name);
|
|
134
|
+
},
|
|
135
|
+
async install(options = {}) {
|
|
136
|
+
const homeDir = options.homeDir ?? homedir2();
|
|
137
|
+
const platform = options.platform ?? process.platform;
|
|
138
|
+
if (platform === "win32")
|
|
139
|
+
throw new Error("Automatic mise installation supports macOS and Linux only.");
|
|
140
|
+
const installedPath = join3(homeDir, ".local", "bin", "mise");
|
|
141
|
+
if (options.dryRun)
|
|
142
|
+
return installedPath;
|
|
143
|
+
await runChecked("sh", ["-c", "curl -fsSL https://mise.run | sh"]);
|
|
144
|
+
const executable = await findExecutable(installedPath) ?? await findExecutable("mise");
|
|
145
|
+
if (!executable)
|
|
146
|
+
throw new Error(`mise installation completed, but ${installedPath} was not found.`);
|
|
147
|
+
return executable;
|
|
148
|
+
},
|
|
149
|
+
async hookEnsure(executable, options = {}) {
|
|
150
|
+
const homeDir = options.homeDir ?? homedir2();
|
|
151
|
+
const hook = shellHook(basename(options.shell ?? process.env.SHELL ?? ""), executable, homeDir);
|
|
152
|
+
const current = await readOptional2(hook.path);
|
|
153
|
+
if (current.includes(MISE_HOOK_START))
|
|
154
|
+
return { path: hook.path, changed: false, planned: false };
|
|
155
|
+
if (options.dryRun)
|
|
156
|
+
return { path: hook.path, changed: true, planned: true };
|
|
157
|
+
const separator = current.length === 0 || current.endsWith(`
|
|
158
|
+
`) ? "" : `
|
|
159
|
+
`;
|
|
160
|
+
await mkdir2(dirname2(hook.path), { recursive: true });
|
|
161
|
+
await writeFile2(hook.path, `${current}${separator}${hook.content}`, "utf8");
|
|
162
|
+
return { path: hook.path, changed: true, planned: false };
|
|
163
|
+
},
|
|
164
|
+
async toolCheckGlobal(executable, tool, minimumMajor = 0) {
|
|
165
|
+
const result = await run(executable, ["ls", "--global", "--installed", tool, "--json"]);
|
|
166
|
+
return result.code === 0 && hasInstalledTool(result.stdout, minimumMajor);
|
|
167
|
+
},
|
|
168
|
+
async toolInstallGlobal(executable, specification) {
|
|
169
|
+
await runChecked(executable, ["use", "--global", specification]);
|
|
170
|
+
},
|
|
171
|
+
async toolCheckLocal(executable, tool, cwd = process.cwd()) {
|
|
172
|
+
const result = await run(executable, ["ls", "--local", "--installed", tool, "--json"], { cwd });
|
|
173
|
+
return result.code === 0 && hasInstalledTool(result.stdout);
|
|
174
|
+
},
|
|
175
|
+
async toolInstallLocal(executable, specification, cwd = process.cwd()) {
|
|
176
|
+
await runChecked(executable, ["use", "--path", cwd, specification], { cwd });
|
|
177
|
+
},
|
|
178
|
+
async toolUpdateAllGlobal(executable, homeDir = homedir2()) {
|
|
179
|
+
await runChecked(executable, ["upgrade"], { cwd: homeDir });
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
var MISE_HOOK_START = "# >>> @difflab/pi mise >>>";
|
|
183
|
+
var MISE_HOOK_END = "# <<< @difflab/pi mise <<<";
|
|
184
|
+
function shellHook(shell, executable, homeDir) {
|
|
185
|
+
const command = shellQuote(executable);
|
|
186
|
+
if (shell === "bash") {
|
|
187
|
+
return {
|
|
188
|
+
path: join3(homeDir, ".bashrc"),
|
|
189
|
+
content: `${MISE_HOOK_START}
|
|
190
|
+
eval "$(${command} activate bash)"
|
|
191
|
+
${MISE_HOOK_END}
|
|
192
|
+
`
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
if (shell === "zsh") {
|
|
196
|
+
return {
|
|
197
|
+
path: join3(homeDir, ".zshrc"),
|
|
198
|
+
content: `${MISE_HOOK_START}
|
|
199
|
+
eval "$(${command} activate zsh)"
|
|
200
|
+
${MISE_HOOK_END}
|
|
201
|
+
`
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
if (shell === "fish") {
|
|
205
|
+
return {
|
|
206
|
+
path: join3(homeDir, ".config", "fish", "config.fish"),
|
|
207
|
+
content: `${MISE_HOOK_START}
|
|
208
|
+
${command} activate fish | source
|
|
209
|
+
${MISE_HOOK_END}
|
|
210
|
+
`
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
throw new Error(`Unsupported shell "${shell || "unknown"}". Supported shells: bash, zsh, fish.`);
|
|
214
|
+
}
|
|
215
|
+
function hasInstalledTool(output, minimumMajor = 0) {
|
|
216
|
+
try {
|
|
217
|
+
const value = JSON.parse(output);
|
|
218
|
+
if (!Array.isArray(value))
|
|
219
|
+
return false;
|
|
220
|
+
return value.some((entry) => {
|
|
221
|
+
if (!entry || typeof entry !== "object" || !("installed" in entry) || entry.installed !== true)
|
|
222
|
+
return false;
|
|
223
|
+
if (minimumMajor === 0)
|
|
224
|
+
return true;
|
|
225
|
+
if (!("version" in entry) || typeof entry.version !== "string")
|
|
226
|
+
return false;
|
|
227
|
+
return Number.parseInt(entry.version, 10) >= minimumMajor;
|
|
228
|
+
});
|
|
229
|
+
} catch {
|
|
230
|
+
return false;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
async function readOptional2(path) {
|
|
234
|
+
try {
|
|
235
|
+
return await readFile2(path, "utf8");
|
|
236
|
+
} catch (error) {
|
|
237
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT")
|
|
238
|
+
return "";
|
|
239
|
+
throw error;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
function shellQuote(value) {
|
|
243
|
+
return `'${value.replaceAll("'", "'\\''")}'`;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// src/pi.ts
|
|
247
|
+
import { mkdir as mkdir3, readFile as readFile3, writeFile as writeFile3 } from "node:fs/promises";
|
|
248
|
+
import { homedir as homedir3 } from "node:os";
|
|
249
|
+
import { dirname as dirname3, join as join4 } from "node:path";
|
|
250
|
+
var pi = {
|
|
251
|
+
async executableCheck() {
|
|
252
|
+
return findExecutable("pi");
|
|
253
|
+
},
|
|
254
|
+
async packageList(executable) {
|
|
255
|
+
return (await runChecked(executable, ["list"])).stdout;
|
|
256
|
+
},
|
|
257
|
+
packageCheck(listOutput, source) {
|
|
258
|
+
if (listOutput.includes(source))
|
|
259
|
+
return true;
|
|
260
|
+
return source.startsWith("https://") && listOutput.includes(source.slice("https://".length));
|
|
261
|
+
},
|
|
262
|
+
async packageInstall(executable, source) {
|
|
263
|
+
await runChecked(executable, ["install", source]);
|
|
264
|
+
},
|
|
265
|
+
agentDir(homeDir = homedir3()) {
|
|
266
|
+
return resolveAgentDir(homeDir);
|
|
267
|
+
},
|
|
268
|
+
async skillCheckGlobal(name, agentDir = resolveAgentDir(), sharedSkillsDir = join4(homedir3(), ".agents", "skills")) {
|
|
269
|
+
const roots = [join4(agentDir, "skills"), sharedSkillsDir];
|
|
270
|
+
for (const root of roots) {
|
|
271
|
+
if (await readOptional3(join4(root, name, "SKILL.md")) !== undefined)
|
|
272
|
+
return true;
|
|
273
|
+
}
|
|
274
|
+
return false;
|
|
275
|
+
},
|
|
276
|
+
async skillInstallGlobal(miseExecutable, source, names) {
|
|
277
|
+
const selection = names.flatMap((name) => ["--skill", name]);
|
|
278
|
+
await runChecked(miseExecutable, [
|
|
279
|
+
"x",
|
|
280
|
+
"node@22",
|
|
281
|
+
"--",
|
|
282
|
+
"npx",
|
|
283
|
+
"-y",
|
|
284
|
+
"skills",
|
|
285
|
+
"add",
|
|
286
|
+
source,
|
|
287
|
+
...selection,
|
|
288
|
+
"--global",
|
|
289
|
+
"--agent",
|
|
290
|
+
"pi",
|
|
291
|
+
"--yes"
|
|
292
|
+
]);
|
|
293
|
+
},
|
|
294
|
+
async configEnsure(path, update, dryRun = false) {
|
|
295
|
+
const currentText = await readOptional3(path);
|
|
296
|
+
const current = parseObject(currentText, path);
|
|
297
|
+
const next = update(current);
|
|
298
|
+
const changed = JSON.stringify(current) !== JSON.stringify(next);
|
|
299
|
+
if (changed && !dryRun) {
|
|
300
|
+
await mkdir3(dirname3(path), { recursive: true });
|
|
301
|
+
await writeFile3(path, `${JSON.stringify(next, null, 2)}
|
|
302
|
+
`, "utf8");
|
|
303
|
+
}
|
|
304
|
+
return { path, changed, existed: currentText !== undefined, planned: changed && dryRun };
|
|
305
|
+
}
|
|
306
|
+
};
|
|
307
|
+
function resolveAgentDir(homeDir = homedir3()) {
|
|
308
|
+
return process.env.PI_CODING_AGENT_DIR ?? (process.env.XDG_CONFIG_HOME ? join4(process.env.XDG_CONFIG_HOME, "pi") : join4(homeDir, ".pi", "agent"));
|
|
309
|
+
}
|
|
310
|
+
async function readOptional3(path) {
|
|
311
|
+
try {
|
|
312
|
+
return await readFile3(path, "utf8");
|
|
313
|
+
} catch (error) {
|
|
314
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT")
|
|
315
|
+
return;
|
|
316
|
+
throw error;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
function parseObject(content, path) {
|
|
320
|
+
if (!content?.trim())
|
|
321
|
+
return {};
|
|
322
|
+
try {
|
|
323
|
+
const value = JSON.parse(content);
|
|
324
|
+
if (value && typeof value === "object" && !Array.isArray(value))
|
|
325
|
+
return value;
|
|
326
|
+
} catch {}
|
|
327
|
+
throw new Error(`Expected valid JSON object in ${path}.`);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// src/setup.ts
|
|
331
|
+
async function ensureMise(options = {}) {
|
|
332
|
+
const homeDir = options.homeDir ?? homedir4();
|
|
333
|
+
const current = await mise.executableCheck() ?? await mise.executableCheck(join5(homeDir, ".local", "bin", "mise"));
|
|
334
|
+
if (current)
|
|
335
|
+
return { executable: current, action: action("mise", "ready", current) };
|
|
336
|
+
progress(options, "Installing mise");
|
|
337
|
+
const executable = await mise.install({
|
|
338
|
+
dryRun: options.dryRun,
|
|
339
|
+
homeDir: options.homeDir,
|
|
340
|
+
platform: options.platform
|
|
341
|
+
});
|
|
342
|
+
return { executable, action: action("mise", options.dryRun ? "planned" : "installed", executable) };
|
|
343
|
+
}
|
|
344
|
+
async function ensureMiseHooks(miseExecutable, options = {}) {
|
|
345
|
+
if (options.installMiseHook === false)
|
|
346
|
+
return action("mise shell hook", "skipped", "disabled");
|
|
347
|
+
const result = await mise.hookEnsure(miseExecutable, {
|
|
348
|
+
dryRun: options.dryRun,
|
|
349
|
+
homeDir: options.homeDir,
|
|
350
|
+
shell: options.shell
|
|
351
|
+
});
|
|
352
|
+
if (!result.changed)
|
|
353
|
+
return action("mise shell hook", "ready", result.path);
|
|
354
|
+
return action("mise shell hook", result.planned ? "planned" : "installed", result.path);
|
|
355
|
+
}
|
|
356
|
+
async function ensureMiseDeps(miseExecutable, options = {}) {
|
|
357
|
+
const canRunMise = Boolean(await mise.executableCheck(miseExecutable));
|
|
358
|
+
const actions = [];
|
|
359
|
+
for (const dependency of MISE_DEPENDENCIES) {
|
|
360
|
+
const installed = canRunMise && await mise.toolCheckGlobal(miseExecutable, dependency.tool, dependency.minimumMajor);
|
|
361
|
+
if (installed) {
|
|
362
|
+
actions.push(action(dependency.name, "ready", dependency.spec));
|
|
363
|
+
continue;
|
|
364
|
+
}
|
|
365
|
+
progress(options, `Installing ${dependency.name} with mise`);
|
|
366
|
+
if (!options.dryRun)
|
|
367
|
+
await mise.toolInstallGlobal(miseExecutable, dependency.spec);
|
|
368
|
+
actions.push(action(dependency.name, options.dryRun ? "planned" : "installed", dependency.spec));
|
|
369
|
+
}
|
|
370
|
+
return actions;
|
|
371
|
+
}
|
|
372
|
+
async function ensurePiPlugins(options = {}) {
|
|
373
|
+
const actions = await ensurePiPackages(PI_PACKAGES, options);
|
|
374
|
+
const agentDir = options.agentDir ?? pi.agentDir(options.homeDir);
|
|
375
|
+
const webSearch = await pi.configEnsure(join5(agentDir, "web-search.json"), (config) => ({ ...config, workflow: "auto-summary" }), options.dryRun);
|
|
376
|
+
actions.push(configAction("web search settings", webSearch));
|
|
377
|
+
const lsp = await pi.configEnsure(join5(agentDir, "pi-lsp.json"), (config) => ({
|
|
378
|
+
...config,
|
|
379
|
+
progressive: { ...record(config.progressive), enabled: true, inject: "none" }
|
|
380
|
+
}), options.dryRun);
|
|
381
|
+
actions.push(configAction("pi-lsp settings", lsp));
|
|
382
|
+
return actions;
|
|
383
|
+
}
|
|
384
|
+
async function ensurePiSkills(miseExecutable, options = {}) {
|
|
385
|
+
const agentDir = options.agentDir ?? pi.agentDir(options.homeDir);
|
|
386
|
+
const sharedSkillsDir = join5(options.homeDir ?? homedir4(), ".agents", "skills");
|
|
387
|
+
const actions = [];
|
|
388
|
+
for (const source of PI_SKILL_SOURCES) {
|
|
389
|
+
const missing = [];
|
|
390
|
+
for (const name of source.skills) {
|
|
391
|
+
if (await pi.skillCheckGlobal(name, agentDir, sharedSkillsDir))
|
|
392
|
+
actions.push(action(`pi skill ${name}`, "ready", source.repository));
|
|
393
|
+
else
|
|
394
|
+
missing.push(name);
|
|
395
|
+
}
|
|
396
|
+
if (missing.length === 0)
|
|
397
|
+
continue;
|
|
398
|
+
progress(options, `Installing skills from ${source.repository}`);
|
|
399
|
+
if (!options.dryRun)
|
|
400
|
+
await pi.skillInstallGlobal(miseExecutable, source.repository, missing);
|
|
401
|
+
for (const name of missing) {
|
|
402
|
+
actions.push(action(`pi skill ${name}`, options.dryRun ? "planned" : "installed", source.repository));
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
return actions;
|
|
406
|
+
}
|
|
407
|
+
async function ensureMcpAdapters(miseExecutable, options = {}) {
|
|
408
|
+
const actions = await ensurePiPackages([MCP_ADAPTER_PACKAGE], options);
|
|
409
|
+
const projectDir = options.projectDir ?? process.cwd();
|
|
410
|
+
const servers = {
|
|
411
|
+
"docs-mcp-server": {
|
|
412
|
+
command: miseExecutable,
|
|
413
|
+
args: ["x", "node@22", "--", "npx", "-y", "@arabold/docs-mcp-server@latest"]
|
|
414
|
+
},
|
|
415
|
+
mise: {
|
|
416
|
+
command: miseExecutable,
|
|
417
|
+
args: ["--cd", projectDir, "mcp"],
|
|
418
|
+
env: { MISE_EXPERIMENTAL: "1" }
|
|
419
|
+
},
|
|
420
|
+
"context-mode": {
|
|
421
|
+
command: miseExecutable,
|
|
422
|
+
args: ["x", "npm:context-mode@latest", "--", "context-mode"]
|
|
423
|
+
}
|
|
424
|
+
};
|
|
425
|
+
if (options.issueTracker === "linear") {
|
|
426
|
+
servers.linear = { url: "https://mcp.linear.app/mcp", auth: "oauth", protocolVersion: "auto" };
|
|
427
|
+
} else if (options.issueTracker === "jira") {
|
|
428
|
+
servers.atlassian = { url: "https://mcp.atlassian.com/v1/mcp", auth: "oauth", protocolVersion: "auto" };
|
|
429
|
+
}
|
|
430
|
+
const result = await mcp.serversEnsure(servers, {
|
|
431
|
+
dryRun: options.dryRun,
|
|
432
|
+
path: mcp.globalConfigPath(options.homeDir)
|
|
433
|
+
});
|
|
434
|
+
actions.push(configAction("MCP configuration", result));
|
|
435
|
+
return actions;
|
|
436
|
+
}
|
|
437
|
+
async function setupPi(options = {}) {
|
|
438
|
+
const miseResult = await ensureMise(options);
|
|
439
|
+
const actions = [miseResult.action];
|
|
440
|
+
actions.push(await ensureMiseHooks(miseResult.executable, options));
|
|
441
|
+
actions.push(...await ensureMiseDeps(miseResult.executable, options));
|
|
442
|
+
actions.push(...await ensurePiPlugins(options));
|
|
443
|
+
actions.push(...await ensurePiSkills(miseResult.executable, options));
|
|
444
|
+
actions.push(...await ensureMcpAdapters(miseResult.executable, options));
|
|
445
|
+
return {
|
|
446
|
+
actions,
|
|
447
|
+
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"))
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
var MISE_DEPENDENCIES = [
|
|
451
|
+
{ name: "node", tool: "node", spec: "node@22", minimumMajor: 22 },
|
|
452
|
+
{ name: "zellij", tool: "zellij", spec: "zellij@latest", minimumMajor: 0 },
|
|
453
|
+
{ name: "helix", tool: "helix", spec: "helix@latest", minimumMajor: 0 },
|
|
454
|
+
{ name: "tuicr", tool: "github:agavra/tuicr", spec: "github:agavra/tuicr@latest", minimumMajor: 0 },
|
|
455
|
+
{ name: "context-mode", tool: "npm:context-mode", spec: "npm:context-mode@latest", minimumMajor: 0 }
|
|
456
|
+
];
|
|
457
|
+
var PI_PACKAGES = [
|
|
458
|
+
"npm:@tintinweb/pi-subagents",
|
|
459
|
+
"npm:pi-schedule-prompt",
|
|
460
|
+
"npm:@narumitw/pi-btw",
|
|
461
|
+
"npm:pi-web-access",
|
|
462
|
+
"npm:@gitawego/pi-lsp",
|
|
463
|
+
"npm:@juicesharp/rpiv-ask-user-question",
|
|
464
|
+
"npm:context-mode"
|
|
465
|
+
];
|
|
466
|
+
var PI_SKILL_SOURCES = [
|
|
467
|
+
{ repository: "arabold/docs-mcp-server", skills: ["docs-manage", "docs-search", "fetch-url"] },
|
|
468
|
+
{ repository: "AminBlg/SimpleEnglish", skills: ["simple-english"] }
|
|
469
|
+
];
|
|
470
|
+
var MCP_ADAPTER_PACKAGE = "npm:pi-mcp-adapter";
|
|
471
|
+
async function ensurePiPackages(packages, options) {
|
|
472
|
+
const executable = await pi.executableCheck();
|
|
473
|
+
if (!executable && !options.dryRun)
|
|
474
|
+
throw new Error("Install pi before you run diffpi_setup.");
|
|
475
|
+
let installed = executable ? await pi.packageList(executable) : "";
|
|
476
|
+
const actions = [];
|
|
477
|
+
for (const source of packages) {
|
|
478
|
+
if (pi.packageCheck(installed, source)) {
|
|
479
|
+
actions.push(action(`pi package ${source}`, "ready", source));
|
|
480
|
+
continue;
|
|
481
|
+
}
|
|
482
|
+
progress(options, `Installing pi package ${source}`);
|
|
483
|
+
if (!options.dryRun) {
|
|
484
|
+
await pi.packageInstall(executable, source);
|
|
485
|
+
installed += `
|
|
486
|
+
${source}`;
|
|
487
|
+
}
|
|
488
|
+
actions.push(action(`pi package ${source}`, options.dryRun ? "planned" : "installed", source));
|
|
489
|
+
}
|
|
490
|
+
return actions;
|
|
491
|
+
}
|
|
492
|
+
function configAction(name, result) {
|
|
493
|
+
if (!result.changed)
|
|
494
|
+
return action(name, "ready", result.path);
|
|
495
|
+
if (result.planned)
|
|
496
|
+
return action(name, "planned", result.path);
|
|
497
|
+
return action(name, result.existed ? "updated" : "installed", result.path);
|
|
498
|
+
}
|
|
499
|
+
function action(name, status, detail) {
|
|
500
|
+
return { name, status, detail };
|
|
501
|
+
}
|
|
502
|
+
function progress(options, message) {
|
|
503
|
+
options.onProgress?.(message);
|
|
504
|
+
}
|
|
505
|
+
function record(value) {
|
|
506
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// src/tools/setup.ts
|
|
510
|
+
var parameters = Type.Object({
|
|
511
|
+
issueTracker: Type.Optional(Type.String({
|
|
512
|
+
description: "Issue tracker MCP server to configure. Use none unless the user explicitly selects Linear or Jira.",
|
|
513
|
+
enum: ["none", "linear", "jira"],
|
|
514
|
+
default: "none"
|
|
515
|
+
})),
|
|
516
|
+
installMiseHook: Type.Optional(Type.Boolean({
|
|
517
|
+
description: "Add the mise activation hook to the current shell configuration. Set false only when the user declines.",
|
|
518
|
+
default: true
|
|
519
|
+
}))
|
|
520
|
+
});
|
|
521
|
+
var diffpiSetupTool = defineTool({
|
|
522
|
+
name: "diffpi_setup",
|
|
523
|
+
label: "diffpi setup",
|
|
524
|
+
description: "Install or repair the @difflab/pi environment. This mutates user-level tool installations and configuration files.",
|
|
525
|
+
promptSnippet: "Install or repair @difflab/pi only after the user approves the setup choices",
|
|
526
|
+
promptGuidelines: [
|
|
527
|
+
"Call this tool only when the user explicitly asks to install, configure, or repair the environment.",
|
|
528
|
+
"Use ask_user_question for unspecified setup choices before calling this tool.",
|
|
529
|
+
'Use issueTracker="none" unless the user explicitly selects Linear or Jira.',
|
|
530
|
+
"Use diffpi_validate instead when the user asks only to inspect or verify setup."
|
|
531
|
+
],
|
|
532
|
+
parameters,
|
|
533
|
+
executionMode: "sequential",
|
|
534
|
+
async execute(_toolCallId, params, _signal, onUpdate) {
|
|
535
|
+
const result = await setupPi({
|
|
536
|
+
issueTracker: parseIssueTracker(params.issueTracker),
|
|
537
|
+
installMiseHook: params.installMiseHook ?? true,
|
|
538
|
+
onProgress(message) {
|
|
539
|
+
onUpdate?.({ content: [{ type: "text", text: message }], details: {} });
|
|
540
|
+
}
|
|
541
|
+
});
|
|
542
|
+
return formatResult(result, "Setup complete.");
|
|
543
|
+
}
|
|
544
|
+
});
|
|
545
|
+
var diffpiValidateTool = defineTool({
|
|
546
|
+
name: "diffpi_validate",
|
|
547
|
+
label: "diffpi validate",
|
|
548
|
+
description: "Inspect the @difflab/pi environment without installing software or changing configuration files.",
|
|
549
|
+
promptSnippet: "Validate @difflab/pi safely before setup or when the user asks for an environment check",
|
|
550
|
+
promptGuidelines: [
|
|
551
|
+
"Prefer this tool before diffpi_setup when the requested action is unclear.",
|
|
552
|
+
"This tool is read-only. Do not describe planned actions as completed changes.",
|
|
553
|
+
'Use issueTracker="none" unless the user explicitly asks to validate Linear or Jira configuration.'
|
|
554
|
+
],
|
|
555
|
+
parameters,
|
|
556
|
+
executionMode: "sequential",
|
|
557
|
+
async execute(_toolCallId, params) {
|
|
558
|
+
const result = await setupPi({
|
|
559
|
+
issueTracker: parseIssueTracker(params.issueTracker),
|
|
560
|
+
installMiseHook: params.installMiseHook ?? true,
|
|
561
|
+
dryRun: true
|
|
562
|
+
});
|
|
563
|
+
const incomplete = result.actions.some((item) => item.status === "planned");
|
|
564
|
+
return formatResult(result, incomplete ? "Setup is incomplete." : "Setup is ready.");
|
|
565
|
+
}
|
|
566
|
+
});
|
|
567
|
+
function parseIssueTracker(value) {
|
|
568
|
+
if (!value || value === "none")
|
|
569
|
+
return "none";
|
|
570
|
+
if (value === "linear" || value === "jira")
|
|
571
|
+
return value;
|
|
572
|
+
throw new Error(`Unknown issue tracker: ${value}`);
|
|
573
|
+
}
|
|
574
|
+
function formatResult(result, heading) {
|
|
575
|
+
const changed = result.actions.some((item) => item.status === "installed" || item.status === "updated" || item.status === "planned");
|
|
576
|
+
const lines = result.actions.map((item) => `${item.status.padEnd(9)} ${item.name}: ${item.detail}`);
|
|
577
|
+
if (result.restartPi)
|
|
578
|
+
lines.push("Restart pi to load package, skill, and MCP changes.");
|
|
579
|
+
return {
|
|
580
|
+
content: [{ type: "text", text: `${heading}
|
|
581
|
+
|
|
582
|
+
${lines.join(`
|
|
583
|
+
`)}` }],
|
|
584
|
+
details: { changed, result }
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
// src/tools/index.ts
|
|
589
|
+
var piTools = [diffpiSetupTool, diffpiValidateTool];
|
|
590
|
+
export {
|
|
591
|
+
diffpiSetupTool,
|
|
592
|
+
diffpiValidateTool,
|
|
593
|
+
piTools
|
|
594
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type ToolDefinition } from '@earendil-works/pi-coding-agent';
|
|
2
|
+
import { Type } from 'typebox';
|
|
3
|
+
import { type SetupResult } from '../setup';
|
|
4
|
+
declare const parameters: Type.TObject<{
|
|
5
|
+
issueTracker: Type.TOptional<Type.TString>;
|
|
6
|
+
installMiseHook: Type.TOptional<Type.TBoolean>;
|
|
7
|
+
}>;
|
|
8
|
+
export interface ToolDetails {
|
|
9
|
+
changed?: boolean;
|
|
10
|
+
result?: SetupResult;
|
|
11
|
+
}
|
|
12
|
+
export declare const diffpiSetupTool: ToolDefinition<typeof parameters, ToolDetails>;
|
|
13
|
+
export declare const diffpiValidateTool: ToolDefinition<typeof parameters, ToolDetails>;
|
|
14
|
+
export {};
|
|
15
|
+
//# sourceMappingURL=setup.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"setup.d.ts","sourceRoot":"","sources":["../../src/tools/setup.ts"],"names":[],"mappings":"AAAA,OAAO,EAAc,KAAK,cAAc,EAAE,MAAM,iCAAiC,CAAC;AAClF,OAAO,EAAE,IAAI,EAAE,MAAM,SAAS,CAAC;AAC/B,OAAO,EAAW,KAAK,WAAW,EAAE,MAAM,UAAU,CAAC;AAErD,QAAA,MAAM,UAAU;;;EAed,CAAC;AAEH,MAAM,WAAW,WAAW;IAC1B,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAED,eAAO,MAAM,eAAe,EAAE,cAAc,CAAC,OAAO,UAAU,EAAE,WAAW,CAyBzE,CAAC;AAEH,eAAO,MAAM,kBAAkB,EAAE,cAAc,CAAC,OAAO,UAAU,EAAE,WAAW,CAqB5E,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@difflab/pi",
|
|
3
|
+
"version": "0.1.0-rc.202609140747.4d45e72.2",
|
|
4
|
+
"description": "Tools and skills for the pi coding agent",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/difflab-io/diffpi.git",
|
|
8
|
+
"directory": "packages/pi"
|
|
9
|
+
},
|
|
10
|
+
"homepage": "https://github.com/difflab-io/diffpi#readme",
|
|
11
|
+
"bugs": {
|
|
12
|
+
"url": "https://github.com/difflab-io/diffpi/issues"
|
|
13
|
+
},
|
|
14
|
+
"publishConfig": {
|
|
15
|
+
"access": "public"
|
|
16
|
+
},
|
|
17
|
+
"type": "module",
|
|
18
|
+
"main": "./dist/index.js",
|
|
19
|
+
"types": "./dist/index.d.ts",
|
|
20
|
+
"exports": {
|
|
21
|
+
".": {
|
|
22
|
+
"types": "./dist/index.d.ts",
|
|
23
|
+
"default": "./dist/index.js"
|
|
24
|
+
},
|
|
25
|
+
"./tools": {
|
|
26
|
+
"types": "./dist/tools/index.d.ts",
|
|
27
|
+
"default": "./dist/tools/index.js"
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
"files": [
|
|
31
|
+
"dist/",
|
|
32
|
+
"skills/"
|
|
33
|
+
],
|
|
34
|
+
"pi": {
|
|
35
|
+
"extensions": [
|
|
36
|
+
"./dist/extensions/index.js"
|
|
37
|
+
],
|
|
38
|
+
"skills": [
|
|
39
|
+
"./skills"
|
|
40
|
+
]
|
|
41
|
+
},
|
|
42
|
+
"keywords": [
|
|
43
|
+
"pi-package",
|
|
44
|
+
"pi",
|
|
45
|
+
"developer-tools"
|
|
46
|
+
],
|
|
47
|
+
"author": "",
|
|
48
|
+
"license": "MIT",
|
|
49
|
+
"packageManager": "bun@1.4.0",
|
|
50
|
+
"engines": {
|
|
51
|
+
"node": ">=22.5.0"
|
|
52
|
+
},
|
|
53
|
+
"dependencies": {
|
|
54
|
+
"pi-mcp-adapter": "^2.33.0"
|
|
55
|
+
},
|
|
56
|
+
"devDependencies": {
|
|
57
|
+
"@eslint/js": "^9.0.0",
|
|
58
|
+
"@semantic-release/changelog": "^6.0.0",
|
|
59
|
+
"@semantic-release/exec": "^7.1.0",
|
|
60
|
+
"@semantic-release/git": "^10.0.0",
|
|
61
|
+
"@semantic-release/github": "^10.0.0",
|
|
62
|
+
"@earendil-works/pi-coding-agent": "^0.84.4",
|
|
63
|
+
"@types/bun": "^1.3.10",
|
|
64
|
+
"@types/node": "^22.0.0",
|
|
65
|
+
"@typescript-eslint/eslint-plugin": "^8.0.0",
|
|
66
|
+
"@typescript-eslint/parser": "^8.0.0",
|
|
67
|
+
"conventional-changelog-conventionalcommits": "^8.0.0",
|
|
68
|
+
"eslint": "^9.0.0",
|
|
69
|
+
"globals": "^15.0.0",
|
|
70
|
+
"prettier": "^3.0.0",
|
|
71
|
+
"semantic-release": "^24.0.0",
|
|
72
|
+
"typebox": "^1.0.0",
|
|
73
|
+
"typescript": "^5.0.0",
|
|
74
|
+
"typescript-eslint": "^8.0.0"
|
|
75
|
+
},
|
|
76
|
+
"peerDependencies": {
|
|
77
|
+
"@earendil-works/pi-coding-agent": "*",
|
|
78
|
+
"typebox": "*"
|
|
79
|
+
}
|
|
80
|
+
}
|