#!/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);