aboutsummaryrefslogtreecommitdiffstats
path: root/pi-ext
diff options
context:
space:
mode:
authoruser <user@clank>2026-07-22 20:33:39 +0200
committeruser <user@clank>2026-08-12 19:34:35 +0200
commit1e34da3f4819ecc3d460e52a3a022d6f195ebb96 (patch)
tree017358f75feace52133ad2805ae7a93f787d0fab /pi-ext
parentslop-tool-boxes: pulse the border of in-progress (pending) tools (diff)
downloadsl0ppi-1e34da3f4819ecc3d460e52a3a022d6f195ebb96.tar.gz
sl0ppi-1e34da3f4819ecc3d460e52a3a022d6f195ebb96.tar.xz
sl0ppi-1e34da3f4819ecc3d460e52a3a022d6f195ebb96.zip
dev.sh: also load globally-installed pi packages (subagents, goal, ...)
-ne disables package extension discovery, so pi-subagents' subagent tool never loaded under dev.sh. Add scripts/resolve-package-extensions.mjs to resolve each settings.json package's extension entry (file/index-dir/discovery-dir, mirroring pi's loader) and re-add them via -e. Package skills/prompts already load under -ne. Opt out per package with PI_DEV_SKIP_PACKAGES.
Diffstat (limited to 'pi-ext')
-rwxr-xr-xpi-ext/dev.sh11
-rwxr-xr-xpi-ext/scripts/resolve-package-extensions.mjs106
2 files changed, 117 insertions, 0 deletions
diff --git a/pi-ext/dev.sh b/pi-ext/dev.sh
index dddc026..08ff321 100755
--- a/pi-ext/dev.sh
+++ b/pi-ext/dev.sh
@@ -22,4 +22,15 @@ if [ "${#args[@]}" -eq 1 ]; then
exit 1
fi
+# -ne disables package discovery too, so globally-installed pi packages
+# (pi-subagents, pi-goal, pi-autoresearch, ...) wouldn't load their tools. Re-add
+# each package's extension entry via -e so they work alongside our dev copies.
+# Their skills/prompts still load (separate -ns/-np gates, which we don't pass).
+# Skip some with e.g. PI_DEV_SKIP_PACKAGES="pi-goal,pi-autoresearch".
+if command -v node >/dev/null 2>&1; then
+ while IFS= read -r extpath; do
+ [ -n "$extpath" ] && args+=(-e "$extpath")
+ done < <(node "$(dirname "$0")/scripts/resolve-package-extensions.mjs" 2>/dev/null || true)
+fi
+
exec pi "${args[@]}" "$@"
diff --git a/pi-ext/scripts/resolve-package-extensions.mjs b/pi-ext/scripts/resolve-package-extensions.mjs
new file mode 100755
index 0000000..4ba070d
--- /dev/null
+++ b/pi-ext/scripts/resolve-package-extensions.mjs
@@ -0,0 +1,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);