aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore1
-rw-r--r--scripts/check-manifest.ts40
-rw-r--r--skills/pi-browser-harness/SKILL.md18
-rw-r--r--test/util/manifest-test.ts49
4 files changed, 108 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
index 0c71f0a..d871976 100644
--- a/.gitignore
+++ b/.gitignore
@@ -9,6 +9,7 @@ docs/
scripts/*
!scripts/check-boundaries.ts
!scripts/boundary-allowlist.ts
+!scripts/check-manifest.ts
data/
.worktrees/
.harness/ \ No newline at end of file
diff --git a/scripts/check-manifest.ts b/scripts/check-manifest.ts
new file mode 100644
index 0000000..437cd90
--- /dev/null
+++ b/scripts/check-manifest.ts
@@ -0,0 +1,40 @@
+// Mirrors HOST_PROVIDED_EXTENSION_PACKAGES in pi-coding-agent's
+// resource-loader.js and the list in its docs/packages.md. Pi provides these
+// packages to extensions at runtime; declaring them in dependencies makes pi
+// emit an extension warning and risks duplicate runtime modules.
+export const HOST_PROVIDED_EXTENSION_PACKAGES: ReadonlySet<string> = new Set([
+ "@earendil-works/pi-agent-core",
+ "@earendil-works/pi-ai",
+ "@earendil-works/pi-coding-agent",
+ "@earendil-works/pi-tui",
+ "@mariozechner/pi-agent-core",
+ "@mariozechner/pi-ai",
+ "@mariozechner/pi-coding-agent",
+ "@mariozechner/pi-tui",
+ "@sinclair/typebox",
+ "typebox",
+]);
+
+export type Manifest = {
+ readonly dependencies?: unknown;
+ readonly peerDependencies?: unknown;
+ readonly devDependencies?: unknown;
+};
+
+// A host-provided package belongs in peerDependencies with a "*" range only.
+export function findHostProvidedDependencies(
+ manifest: Manifest
+): ReadonlyArray<string> {
+ const dependencies = manifest.dependencies;
+ if (
+ typeof dependencies !== "object" ||
+ dependencies === null ||
+ Array.isArray(dependencies)
+ ) {
+ return [];
+ }
+ const names = Object.keys(dependencies).filter((name) =>
+ HOST_PROVIDED_EXTENSION_PACKAGES.has(name)
+ );
+ return names.sort();
+}
diff --git a/skills/pi-browser-harness/SKILL.md b/skills/pi-browser-harness/SKILL.md
index c1dcef7..f0a3324 100644
--- a/skills/pi-browser-harness/SKILL.md
+++ b/skills/pi-browser-harness/SKILL.md
@@ -100,3 +100,21 @@ When a workflow repeats 3+ times or needs Node.js APIs, write a script to disk a
**Don't:**
- Use scripts for one-off actions — call `browser_*` tools directly.
- Call `browser_*` tools from inside a script — sequence them as separate tool calls outside.
+
+## Troubleshooting: "Could not start the browser daemon"
+
+`browser_setup` spawns the daemon as `node <tsx cli> <pkg>/src/daemon/index.ts` with `stdio: "ignore"` (tsx is resolved from the package directory upward, so in pi's hoisted install it sits beside the package rather than inside it). A crash before the socket binds is completely silent and all you see is `Could not start the browser daemon. Check /tmp/pi-browser-daemon.sock.`
+
+Run it in the foreground to see the real error (it prints whether it reached Chrome):
+
+ cd <pkg> && node "$(node -p "require.resolve('tsx/cli')")" src/daemon/index.ts
+
+Known cause (package 0.11.1): `Error: Cannot find module 'typebox'` at `src/cdp/discovery.ts`. The package declares `typebox` in `peerDependencies` and `devDependencies`, and pi installs packages with dev and peer dependencies omitted, so the tree has no physical copy. It still resolves inside the pi extension host, which maps `typebox` to pi's bundled copy, but not in the spawned daemon process, which resolves modules from the package directory upward. The daemon dies, no socket appears, and every browser tool reports `not_connected`.
+
+Fix it without touching the manifest, which must keep `typebox` out of `dependencies` (pi warns on it and `npm run check` rejects it):
+
+ cd <pkg> && npm install --no-save typebox --no-audit --no-fund
+
+or restore the full tree with `npm ci`, which installs the `devDependencies` copy. Then confirm with `node -e "console.log(require.resolve('typebox'))"` inside the package directory, and start the daemon with `NODE_PATH` unset to prove no environment workaround is in play. A fresh clone or update reinstalls the tree, so the missing copy can come back; the manifest itself is correct upstream.
+
+Do not work around it with `npm install typebox --save` (that writes `typebox` into `dependencies`, tripping pi's extension warning and the manifest check), a global `NODE_PATH`, or a hand-made symlink: the spawn inherits the extension host environment, so a shell-level export may not reach it, and the underlying problem is an incomplete install, not an undeclared dependency.
diff --git a/test/util/manifest-test.ts b/test/util/manifest-test.ts
new file mode 100644
index 0000000..6fcf136
--- /dev/null
+++ b/test/util/manifest-test.ts
@@ -0,0 +1,49 @@
+import { test, describe } from "node:test";
+import assert from "node:assert/strict";
+import { readFileSync } from "node:fs";
+import { findHostProvidedDependencies } from "../../scripts/check-manifest";
+
+describe("host-provided manifest contract", () => {
+ test("flags host-provided packages listed in dependencies", () => {
+ const manifest = { dependencies: { ws: "^8.20.0", typebox: "^1.3.34" } };
+ assert.deepEqual(findHostProvidedDependencies(manifest), ["typebox"]);
+ });
+
+ test("recognises every pi-provided package", () => {
+ const manifest = {
+ dependencies: {
+ typebox: "*",
+ "@sinclair/typebox": "*",
+ "@mariozechner/pi-tui": "*",
+ "@earendil-works/pi-coding-agent": "*",
+ },
+ };
+ assert.deepEqual(findHostProvidedDependencies(manifest), [
+ "@earendil-works/pi-coding-agent",
+ "@mariozechner/pi-tui",
+ "@sinclair/typebox",
+ "typebox",
+ ]);
+ });
+
+ test("ignores peer and devDependencies", () => {
+ const manifest = {
+ peerDependencies: { typebox: "*" },
+ devDependencies: { typebox: "*" },
+ };
+ assert.deepEqual(findHostProvidedDependencies(manifest), []);
+ });
+
+ test("tolerates a missing or malformed dependencies field", () => {
+ assert.deepEqual(findHostProvidedDependencies({}), []);
+ assert.deepEqual(findHostProvidedDependencies({ dependencies: null }), []);
+ assert.deepEqual(findHostProvidedDependencies({ dependencies: [] }), []);
+ });
+
+ test("this package declares no host-provided dependencies", () => {
+ const manifest = JSON.parse(
+ readFileSync(new URL("../../package.json", import.meta.url), "utf8")
+ );
+ assert.deepEqual(findHostProvidedDependencies(manifest), []);
+ });
+});