1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
|
#!/usr/bin/env node
/**
* Print absolute extension entry files for the globally-installed pi packages
* listed in ~/.pi/agent/settings.json `packages`, one per line.
*
* dev.sh runs pi with -ne (--no-extensions), which disables ALL extension
* discovery — including npm packages like pi-subagents — so their tools/commands
* don't load. (Package *skills* and *prompts* still load, since those use the
* separate -ns/-np gates.) This resolver lets dev.sh re-add just the package
* extension modules via -e, without also pulling in the global-dir copies of our
* own in-development extensions (which we deliberately isolate).
*
* Skip specific packages with PI_DEV_SKIP_PACKAGES="pi-goal,pi-autoresearch".
*
* Resolution mirrors pi's loader: a package.json "pi.extensions" entry may point
* at a file, an index.ts/js directory, or a discovery directory of *.ts/*.js
* files and index-bearing subdirs.
*/
import { readFileSync, existsSync, statSync, readdirSync } from "node:fs";
import { join, resolve } from "node:path";
import { homedir } from "node:os";
const AGENT = join(homedir(), ".pi", "agent");
const NODE_MODULES = join(AGENT, "npm", "node_modules");
const SETTINGS = join(AGENT, "settings.json");
const readJSON = (p) => {
try {
return JSON.parse(readFileSync(p, "utf8"));
} catch {
return null;
}
};
const isExtFile = (n) => n.endsWith(".ts") || n.endsWith(".js");
const out = new Set();
function piField(pkg) {
if (!pkg) return null;
// Extensions live under "pi.extensions" (a couple of packages nest under a
// key matching their own name); we only need the extensions array.
return pkg.pi ?? null;
}
function addDir(dir) {
const field = piField(readJSON(join(dir, "package.json")));
if (field && Array.isArray(field.extensions)) {
for (const e of field.extensions) addEntry(dir, e);
return;
}
for (const idx of ["index.ts", "index.js"]) {
if (existsSync(join(dir, idx))) {
out.add(join(dir, idx));
return;
}
}
// Discovery directory: direct *.ts/*.js files + index-bearing subdirs.
let entries = [];
try {
entries = readdirSync(dir, { withFileTypes: true });
} catch {
return;
}
for (const ent of entries) {
const p = join(dir, ent.name);
if ((ent.isFile() || ent.isSymbolicLink()) && isExtFile(ent.name)) {
out.add(p);
} else if (ent.isDirectory()) {
for (const idx of ["index.ts", "index.js"]) {
if (existsSync(join(p, idx))) {
out.add(join(p, idx));
break;
}
}
}
}
}
function addEntry(base, entry) {
const abs = resolve(base, entry);
if (!existsSync(abs)) return;
if (statSync(abs).isDirectory()) addDir(abs);
else if (isExtFile(abs)) out.add(abs);
}
const settings = readJSON(SETTINGS) ?? {};
const packages = Array.isArray(settings.packages) ? settings.packages : [];
const skip = new Set(
(process.env.PI_DEV_SKIP_PACKAGES ?? "")
.split(",")
.map((s) => s.trim())
.filter(Boolean),
);
for (const spec of packages) {
if (!spec.startsWith("npm:")) continue; // git: packages resolve elsewhere
const name = spec.slice(4).replace(/@[^/@]+$/, ""); // strip trailing @version
if (skip.has(name)) continue;
const dir = join(NODE_MODULES, name);
if (!existsSync(dir)) continue;
const field = piField(readJSON(join(dir, "package.json")));
const entries = field && Array.isArray(field.extensions) ? field.extensions : ["."];
for (const e of entries) addEntry(dir, e);
}
for (const p of out) console.log(p);
|