pi-vault-mind 0.16.31 → 0.16.33

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/CHANGELOG.md CHANGED
@@ -1,5 +1,26 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.16.33 — 2026-08-29
4
+
5
+ ### Added
6
+
7
+ - **Content-writing-system runtime.** `pvm setup` now installs the private `content-writing-system` Pi package (`git:git@github.com:kylebrodeur/content-writing-system`) as a required extension, registering both its skill and the `/check-writing` command in the vault-local agent. Requires git/SSH access to the repository at setup time.
8
+
9
+ ### Changed
10
+
11
+ - **Idempotent extension install.** `pvm setup` now reads the vault-local `settings.json` and skips extensions already present at the recorded spec (including pinned versions), reinstalling only missing specs or changed version pins. Repeated `pvm setup` / `--resources-only` runs no longer re-clone or re-fetch unchanged extensions.
12
+
13
+ ## 0.16.32 — 2026-08-28
14
+
15
+ ### Changed
16
+
17
+ - **Concise setup progress.** `pvm setup` now reports one bounded progress line per extension and curated skill while preserving captured installer diagnostics on failure.
18
+
19
+ ### Fixed
20
+
21
+ - **Vault-isolated main sessions.** `pvm run` disables implicit skill and prompt-template discovery, then explicitly loads only curated and package skills rooted in the vault-local agent directory. Specialist skill availability can no longer select a specialist identity for the interactive main session.
22
+ - **Escaping package paths.** Setup removes local package paths that resolve outside the vault-local agent directory while preserving npm packages and in-runtime custom packages.
23
+
3
24
  ## 0.16.31 — 2026-08-28
4
25
 
5
26
  ### Added
package/README.md CHANGED
@@ -123,37 +123,32 @@ obsidian plugin:install id=obsidian42-brat enable
123
123
  # In Obsidian: Settings → BRAT → Beta Plugin List → Add
124
124
  ```
125
125
 
126
- ### Recommended: kepano/obsidian-skills (pi skills)
126
+ ### Curated vault-local skills
127
127
 
128
- Install via the official [`skills` CLI](https://github.com/vercel-labs/skills)
129
- (pi is a first-class target agent — auto-detected):
128
+ `pvm setup` installs the supported Obsidian and NotebookLM skills into the
129
+ vault-local Pi agent directory:
130
130
 
131
131
  ```bash
132
- # Non-interactive: install all 5 globally for pi
133
- DISABLE_TELEMETRY=1 yes y | npx -y skills add https://github.com/kepano/obsidian-skills \
134
- --skill obsidian-markdown --skill obsidian-bases \
135
- --skill json-canvas --skill obsidian-cli --skill defuddle \
136
- -g -a pi --copy -y
137
- ```
138
-
139
- This puts them at `~/.pi/agent/skills/<name>/SKILL.md` where pi
140
- auto-discovers them. The CLI handles agent detection, symlink/copy,
141
- and security risk confirmations.
142
-
143
- **Verify they're discoverable:**
144
-
145
- ```bash
146
- for s in obsidian-markdown obsidian-bases json-canvas obsidian-cli defuddle; do
147
- [ -f ~/.pi/agent/skills/$s/SKILL.md ] && echo "✅ $s" || echo "❌ $s"
148
- done
132
+ pvm setup /path/to/your/vault
149
133
  ```
150
- - `obsidian-markdown` — agent learns Obsidian-flavored markdown syntax
151
- - `obsidian-bases` — agent learns to generate valid `.base` files (database views)
152
- - `json-canvas` — agent learns Canvas JSON format (used for our graph sync)
153
- - `obsidian-cli` — agent learns to use the Obsidian CLI
154
- - `defuddle` — cleaner markdown extraction from web pages
155
134
 
156
- These skills let the **Miner, Broadcaster, and Heavy-Lifter agents** produce valid Obsidian content.
135
+ The skills live under
136
+ `<vault>/.vault-mind/.pi/agent/skills/<name>/SKILL.md`. `pvm run` disables
137
+ Pi's implicit global/project skill discovery and explicitly loads only these
138
+ curated skills plus skills provided by packages installed in the same
139
+ vault-local agent directory. Do not install these skills globally for a Vault
140
+ Mind session; global copies defeat vault isolation and can create skill-name
141
+ conflicts.
142
+
143
+ - `obsidian-markdown` — Obsidian-flavored Markdown syntax
144
+ - `obsidian-bases` — valid `.base` database views
145
+ - `json-canvas` — Canvas JSON used by graph sync
146
+ - `obsidian-cli` — Obsidian CLI operation
147
+ - `nlm-skill` — NotebookLM CLI and MCP workflows
148
+
149
+ These skills let the **Miner, Broadcaster, and Heavy-Lifter agents** produce
150
+ valid Obsidian content without changing the identity of the interactive main
151
+ agent.
157
152
 
158
153
  ### Built-in Obsidian features used
159
154
 
@@ -1,9 +1,9 @@
1
1
  /**
2
- * Canonical list of pi extensions required/optional for vault-mind.
2
+ * Canonical extension list for the legacy `/vm setup` and Obsidian setup
3
+ * surfaces. The standalone `pvm` CLI has a broader package-managed runtime
4
+ * contract in `runtime/pvm-extensions.json`.
3
5
  *
4
- * This is the SINGLE SOURCE OF TRUTH. Shell scripts and the Obsidian plugin
5
- * consume generated JSON (see scripts/generate-extension-packages-json.mjs).
6
- * When adding/removing a package, edit ONLY this file and regenerate.
6
+ * Generate this surface with scripts/generate-extension-packages-json.mjs.
7
7
  */
8
8
  export declare const PI_REQUIRED_EXTENSIONS: readonly ["npm:pi-vault-mind", "npm:pi-context", "npm:@kylebrodeur/pi-model-discovery", "npm:@kylebrodeur/pi-model-router"];
9
9
  export declare const PI_OPTIONAL_EXTENSIONS: readonly ["npm:pi-1password"];
@@ -1,9 +1,9 @@
1
1
  /**
2
- * Canonical list of pi extensions required/optional for vault-mind.
2
+ * Canonical extension list for the legacy `/vm setup` and Obsidian setup
3
+ * surfaces. The standalone `pvm` CLI has a broader package-managed runtime
4
+ * contract in `runtime/pvm-extensions.json`.
3
5
  *
4
- * This is the SINGLE SOURCE OF TRUTH. Shell scripts and the Obsidian plugin
5
- * consume generated JSON (see scripts/generate-extension-packages-json.mjs).
6
- * When adding/removing a package, edit ONLY this file and regenerate.
6
+ * Generate this surface with scripts/generate-extension-packages-json.mjs.
7
7
  */
8
8
  export const PI_REQUIRED_EXTENSIONS = [
9
9
  "npm:pi-vault-mind",
@@ -14,6 +14,7 @@ const requiredExtensions = [
14
14
  "npm:@kylebrodeur/pi-model-router",
15
15
  "npm:@juicesharp/rpiv-todo@2.7.1",
16
16
  "npm:pi-subagents@0.58.0",
17
+ "git:git@github.com:kylebrodeur/content-writing-system",
17
18
  ];
18
19
  const runtimeSkills = [
19
20
  "obsidian-cli",
@@ -47,6 +48,8 @@ describe("pvm setup", () => {
47
48
  fs.writeFileSync(path.join(binDir, "pi"), `#!/usr/bin/env node
48
49
  const fs = require("node:fs");
49
50
  fs.appendFileSync(process.env.PVM_TEST_PI_LOG, JSON.stringify(process.argv.slice(2)) + "\\n");
51
+ process.stdout.write("NOISY INSTALL STDOUT\\n");
52
+ process.stderr.write("NOISY INSTALL STDERR\\n");
50
53
  `, { mode: 0o755 });
51
54
  fs.writeFileSync(path.join(binDir, "uv"), `#!/usr/bin/env node
52
55
  const fs = require("node:fs");
@@ -121,6 +124,37 @@ fs.appendFileSync(process.env.PVM_TEST_UV_LOG, JSON.stringify(process.argv.slice
121
124
  ]) {
122
125
  assert.ok(fs.existsSync(path.join(nlmSkillDir, "references", ref)), `nlm-skill reference missing: ${ref}`);
123
126
  }
127
+ assert.doesNotMatch(result.stdout + result.stderr, /NOISY INSTALL/);
128
+ assert.match(result.stdout, /\[7\/7\] git:git@github\.com:kylebrodeur\/content-writing-system \.\.\. done/);
129
+ assert.match(result.stdout, /Curated skills \(5\)/);
130
+ });
131
+ it("removes package paths that escape the vault-local agent directory", () => {
132
+ const agentDir = path.join(vaultPath, ".vault-mind", ".pi", "agent");
133
+ const settingsPath = path.join(agentDir, "settings.json");
134
+ const insidePackage = path.join(agentDir, "packages", "custom-extension");
135
+ fs.mkdirSync(insidePackage, { recursive: true });
136
+ fs.writeFileSync(settingsPath, `${JSON.stringify({
137
+ packages: ["npm:pi-vault-mind", "../../../../content-writing-system", insidePackage],
138
+ theme: "dark",
139
+ }, null, 2)}\n`);
140
+ const result = runSetup(["--resources-only", vaultPath]);
141
+ assert.equal(result.status, 0, result.stderr || result.stdout);
142
+ const settings = JSON.parse(fs.readFileSync(settingsPath, "utf-8"));
143
+ assert.deepEqual(settings.packages, ["npm:pi-vault-mind", insidePackage]);
144
+ assert.equal(settings.theme, "dark");
145
+ assert.match(result.stdout, /Removed 1 external package path/);
146
+ });
147
+ it("prints captured installer diagnostics when an extension install fails", () => {
148
+ fs.writeFileSync(path.join(binDir, "pi"), `#!/usr/bin/env node
149
+ const args = process.argv.slice(2);
150
+ if (args[0] === "install" && args[1] === "npm:pi-context") {
151
+ process.stderr.write("PI_CONTEXT_INSTALL_FAILED\n");
152
+ process.exit(23);
153
+ }
154
+ `, { mode: 0o755 });
155
+ const result = runSetup(["--resources-only", vaultPath]);
156
+ assert.notEqual(result.status, 0);
157
+ assert.match(result.stderr + result.stdout, /PI_CONTEXT_INSTALL_FAILED/);
124
158
  });
125
159
  it("ignores npm exec's transient Pi binary when a stable Pi install is available", () => {
126
160
  const transientBin = path.join(testDir, "_npx", "cache-key", "node_modules", ".bin");
@@ -158,6 +192,28 @@ fs.appendFileSync(process.env.PVM_TEST_UV_LOG, JSON.stringify(process.argv.slice
158
192
  assert.ok(fs.existsSync(path.join(vaultPath, ".vault-mind", ".pi", "agent", "skills", skill, "SKILL.md")));
159
193
  }
160
194
  });
195
+ it("skips extensions already present in the vault settings", () => {
196
+ const agentDir = path.join(vaultPath, ".vault-mind", ".pi", "agent");
197
+ fs.mkdirSync(agentDir, { recursive: true });
198
+ fs.writeFileSync(path.join(agentDir, "settings.json"), `${JSON.stringify({
199
+ packages: ["npm:pi-vault-mind", "npm:pi-context", "npm:pi-subagents@0.58.0"],
200
+ })}\n`);
201
+ const result = runSetup(["--resources-only", vaultPath]);
202
+ assert.equal(result.status, 0, result.stderr || result.stdout);
203
+ const installed = installedExtensions();
204
+ for (const present of ["npm:pi-vault-mind", "npm:pi-context", "npm:pi-subagents@0.58.0"]) {
205
+ assert.ok(!installed.includes(present), `expected ${present} to be skipped`);
206
+ }
207
+ for (const missing of [
208
+ "npm:@kylebrodeur/pi-model-discovery",
209
+ "npm:@kylebrodeur/pi-model-router",
210
+ "npm:@juicesharp/rpiv-todo@2.7.1",
211
+ "git:git@github.com:kylebrodeur/content-writing-system",
212
+ ]) {
213
+ assert.ok(installed.includes(missing), `expected ${missing} to be installed`);
214
+ }
215
+ assert.match(result.stdout, /npm:pi-subagents@0\.58\.0 \.\.\. already installed/);
216
+ });
161
217
  it("preserves customized configuration across repeated full setup", () => {
162
218
  const first = runSetup([vaultPath]);
163
219
  assert.equal(first.status, 0, first.stderr || first.stdout);
@@ -19,11 +19,21 @@ describe("vault-pi launcher", () => {
19
19
  outputPath = path.join(testDir, "pi-output.txt");
20
20
  fs.mkdirSync(vaultPath, { recursive: true });
21
21
  fs.mkdirSync(binDir, { recursive: true });
22
+ const skillFiles = [
23
+ path.join(vaultPath, ".vault-mind", ".pi", "agent", "npm", "node_modules", "@scope", "package", "skills", "scoped-skill", "SKILL.md"),
24
+ path.join(vaultPath, ".vault-mind", ".pi", "agent", "npm", "node_modules", "pi-context", "skills", "context-management", "SKILL.md"),
25
+ path.join(vaultPath, ".vault-mind", ".pi", "agent", "skills", "obsidian-cli", "SKILL.md"),
26
+ ];
27
+ for (const skillFile of skillFiles) {
28
+ fs.mkdirSync(path.dirname(skillFile), { recursive: true });
29
+ fs.writeFileSync(skillFile, "---\nname: test\n---\n");
30
+ }
22
31
  fs.writeFileSync(path.join(binDir, "pi"), `#!/usr/bin/env bash
23
32
  set -euo pipefail
24
33
  pid_file="$PWD/.vault-mind/.pi/agent/daemon.pid"
25
- printf 'cwd=%s\nagent=%s\nargs=%s\npid=%s\n' \
26
- "$PWD" "$PI_CODING_AGENT_DIR" "$*" "$(cat "$pid_file")" > "$PVM_TEST_OUTPUT"
34
+ printf 'cwd=%s\nagent=%s\npid=%s\n' \
35
+ "$PWD" "$PI_CODING_AGENT_DIR" "$(cat "$pid_file")" > "$PVM_TEST_OUTPUT"
36
+ printf 'arg=%s\n' "$@" >> "$PVM_TEST_OUTPUT"
27
37
  `, { mode: 0o755 });
28
38
  for (const command of ["open", "osascript"]) {
29
39
  fs.writeFileSync(path.join(binDir, command), "#!/usr/bin/env bash\nexit 97\n", {
@@ -48,10 +58,42 @@ printf 'cwd=%s\nagent=%s\nargs=%s\npid=%s\n' \
48
58
  const output = fs.readFileSync(outputPath, "utf-8");
49
59
  assert.match(output, new RegExp(`^cwd=${vaultPath.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}$`, "m"));
50
60
  assert.match(output, new RegExp(`^agent=${path.join(vaultPath, ".vault-mind", ".pi", "agent").replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}$`, "m"));
51
- assert.match(output, /^args=$/m);
61
+ const args = [...output.matchAll(/^arg=(.*)$/gm)].map((match) => match[1]);
62
+ const agentDir = path.join(vaultPath, ".vault-mind", ".pi", "agent");
63
+ assert.deepEqual(args, [
64
+ "--no-skills",
65
+ "--no-prompt-templates",
66
+ "--skill",
67
+ path.join(agentDir, "npm", "node_modules", "@scope", "package", "skills", "scoped-skill", "SKILL.md"),
68
+ "--skill",
69
+ path.join(agentDir, "npm", "node_modules", "pi-context", "skills", "context-management", "SKILL.md"),
70
+ "--skill",
71
+ path.join(agentDir, "skills", "obsidian-cli", "SKILL.md"),
72
+ ]);
52
73
  assert.match(output, /^pid=\d+$/m);
53
74
  assert.equal(fs.existsSync(path.join(vaultPath, ".vault-mind", ".pi", "agent", "daemon.pid")), false);
54
75
  });
76
+ it("preserves the vault-local 1Password environment flag", () => {
77
+ const agentDir = path.join(vaultPath, ".vault-mind", ".pi", "agent");
78
+ fs.mkdirSync(path.join(agentDir, "npm", "node_modules", "pi-1password"), {
79
+ recursive: true,
80
+ });
81
+ fs.mkdirSync(path.join(vaultPath, ".vault-mind"), { recursive: true });
82
+ fs.writeFileSync(path.join(vaultPath, ".vault-mind", ".env.1pass"), "TOKEN=op://vault/item\n");
83
+ const result = spawnSync("bash", [launcher, "--same-terminal", vaultPath], {
84
+ cwd: testDir,
85
+ encoding: "utf-8",
86
+ env: {
87
+ ...process.env,
88
+ PATH: `${binDir}:${process.env.PATH ?? ""}`,
89
+ PVM_TEST_OUTPUT: outputPath,
90
+ },
91
+ });
92
+ assert.equal(result.status, 0, result.stderr || result.stdout);
93
+ const output = fs.readFileSync(outputPath, "utf-8");
94
+ const args = [...output.matchAll(/^arg=(.*)$/gm)].map((match) => match[1]);
95
+ assert.deepEqual(args.slice(-2), ["--op-env", ".vault-mind/.env.1pass"]);
96
+ });
55
97
  it("ignores npm exec's transient Pi binary when a stable Pi install is available", () => {
56
98
  const transientBin = path.join(testDir, "_npx", "cache-key", "node_modules", ".bin");
57
99
  fs.mkdirSync(transientBin, { recursive: true });
@@ -102,6 +144,30 @@ printf 'cwd=%s\nagent=%s\nargs=%s\npid=%s\n' \
102
144
  assert.equal(result.status, 97);
103
145
  assert.equal(fs.existsSync(outputPath), false);
104
146
  });
147
+ it("passes isolated skill arguments through new-window command construction", () => {
148
+ for (const command of ["open", "osascript"]) {
149
+ fs.writeFileSync(path.join(binDir, command), `#!/usr/bin/env node
150
+ const fs = require("node:fs");
151
+ fs.writeFileSync(process.env.PVM_TEST_OUTPUT, JSON.stringify(process.argv.slice(2)));
152
+ `, { mode: 0o755 });
153
+ }
154
+ const result = spawnSync("bash", [launcher, "--new-window", vaultPath], {
155
+ cwd: testDir,
156
+ encoding: "utf-8",
157
+ env: {
158
+ ...process.env,
159
+ PATH: `${binDir}:${process.env.PATH ?? ""}`,
160
+ PVM_TEST_OUTPUT: outputPath,
161
+ },
162
+ });
163
+ assert.equal(result.status, 0, result.stderr || result.stdout);
164
+ const commandText = JSON.parse(fs.readFileSync(outputPath, "utf-8")).join("\n");
165
+ assert.match(commandText, /--no-skills/);
166
+ assert.match(commandText, /--no-prompt-templates/);
167
+ assert.match(commandText, /--skill/);
168
+ assert.match(commandText, /obsidian-cli/);
169
+ assert.match(commandText, /SKILL\.md/);
170
+ });
105
171
  it("rejects an unknown launch flag with usage help", () => {
106
172
  const result = spawnSync("bash", [launcher, "--new-widnow"], {
107
173
  cwd: testDir,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-vault-mind",
3
- "version": "0.16.31",
3
+ "version": "0.16.33",
4
4
  "packageManager": "pnpm@10.34.5",
5
5
  "description": "Passive Obsidian vault extension for pi. Watches @agent markers, dispatches forked subagents (Miner, Broadcaster, Heavy-Lifter), stores in LanceDB with vector + FTS + graph. Multi-agent 'Drop & Forget' workflow for the pi agent ecosystem.",
6
6
  "main": "dist/index.js",
@@ -5,7 +5,8 @@
5
5
  "npm:@kylebrodeur/pi-model-discovery",
6
6
  "npm:@kylebrodeur/pi-model-router",
7
7
  "npm:@juicesharp/rpiv-todo@2.7.1",
8
- "npm:pi-subagents@0.58.0"
8
+ "npm:pi-subagents@0.58.0",
9
+ "git:git@github.com:kylebrodeur/content-writing-system"
9
10
  ],
10
11
  "optional": ["npm:pi-1password"]
11
12
  }
@@ -63,12 +63,18 @@ function commandAvailable(command) {
63
63
  return !result.error && result.status === 0;
64
64
  }
65
65
 
66
+ function printFailureOutput(result) {
67
+ if (result.stdout) process.stderr.write(result.stdout);
68
+ if (result.stderr) process.stderr.write(result.stderr);
69
+ }
70
+
66
71
  function installNotebookLM() {
72
+ console.log("==> NotebookLM CLI");
67
73
  const versionCheck = spawnSync("nlm", ["--version"], { encoding: "utf-8" });
68
74
  if (!versionCheck.error && versionCheck.status === 0) {
69
75
  const installed = (versionCheck.stdout + versionCheck.stderr).trim();
70
76
  if (installed.includes("0.9.13")) {
71
- console.log("==> notebooklm-mcp-cli==0.9.13 already installed, skipping");
77
+ console.log(" notebooklm-mcp-cli 0.9.13 ... already installed");
72
78
  return;
73
79
  }
74
80
  }
@@ -78,12 +84,15 @@ function installNotebookLM() {
78
84
  "Install uv: https://docs.astral.sh/uv/getting-started/installation/"
79
85
  );
80
86
  }
81
- console.log("==> Installing notebooklm-mcp-cli==0.9.13 via uv");
82
87
  const result = spawnSync("uv", ["tool", "install", "--force", "notebooklm-mcp-cli==0.9.13"], {
83
- stdio: "inherit",
88
+ encoding: "utf-8",
84
89
  });
85
90
  if (result.error) fail(`could not start uv: ${result.error.message}`);
86
- if (result.status !== 0) process.exit(result.status ?? 1);
91
+ if (result.status !== 0) {
92
+ printFailureOutput(result);
93
+ fail("notebooklm-mcp-cli installation failed", result.status ?? 1);
94
+ }
95
+ console.log(" notebooklm-mcp-cli 0.9.13 ... installed");
87
96
  }
88
97
 
89
98
  let piExecutable;
@@ -116,14 +125,17 @@ function resolvePiExecutable() {
116
125
  return piExecutable;
117
126
  }
118
127
 
119
- function installExtension(extension, agentDir) {
120
- console.log(` ${extension}`);
128
+ function installExtension(extension, agentDir, index, total) {
121
129
  const result = spawnSync(resolvePiExecutable(), ["install", extension], {
122
- stdio: "inherit",
130
+ encoding: "utf-8",
123
131
  env: { ...process.env, PI_CODING_AGENT_DIR: agentDir },
124
132
  });
125
133
  if (result.error) fail(`could not start pi: ${result.error.message}`);
126
- if (result.status !== 0) process.exit(result.status ?? 1);
134
+ if (result.status !== 0) {
135
+ printFailureOutput(result);
136
+ fail(`extension installation failed: ${extension}`, result.status ?? 1);
137
+ }
138
+ console.log(` [${index}/${total}] ${extension} ... done`);
127
139
  }
128
140
 
129
141
  function removeStaleExtension(extension, agentDir) {
@@ -146,16 +158,39 @@ function removeStaleExtensions(agentDir) {
146
158
  for (const extension of STALE_EXTENSIONS) removeStaleExtension(extension, agentDir);
147
159
  }
148
160
 
161
+ function readInstalledPackages(agentDir) {
162
+ const settingsPath = path.join(agentDir, "settings.json");
163
+ try {
164
+ const settings = JSON.parse(fs.readFileSync(settingsPath, "utf-8"));
165
+ if (!Array.isArray(settings.packages)) return new Set();
166
+ return new Set(
167
+ settings.packages
168
+ .map((entry) => (typeof entry === "string" ? entry : entry?.source))
169
+ .filter((source) => typeof source === "string")
170
+ );
171
+ } catch {
172
+ return new Set();
173
+ }
174
+ }
175
+
149
176
  function installExtensions(agentDir) {
150
177
  const manifest = readExtensionManifest();
151
- console.log("==> Installing required Pi extensions");
152
- for (const extension of manifest.required) installExtension(extension, agentDir);
178
+ const extensions = [...manifest.required];
179
+ if (process.env.PVM_SETUP_SKIP_OPTIONAL !== "1") {
180
+ for (const extension of manifest.optional) {
181
+ if (extension === "npm:pi-1password" && !commandAvailable("op")) continue;
182
+ extensions.push(extension);
183
+ }
184
+ }
153
185
 
154
- if (process.env.PVM_SETUP_SKIP_OPTIONAL === "1") return;
155
- for (const extension of manifest.optional) {
156
- if (extension === "npm:pi-1password" && !commandAvailable("op")) continue;
157
- console.log("==> Installing optional Pi extension");
158
- installExtension(extension, agentDir);
186
+ const installed = readInstalledPackages(agentDir);
187
+ console.log(`==> Pi extensions (${extensions.length})`);
188
+ for (const [index, extension] of extensions.entries()) {
189
+ if (installed.has(extension)) {
190
+ console.log(` [${index + 1}/${extensions.length}] ${extension} ... already installed`);
191
+ continue;
192
+ }
193
+ installExtension(extension, agentDir, index + 1, extensions.length);
159
194
  }
160
195
  }
161
196
 
@@ -163,8 +198,8 @@ function installRuntimeSkills(agentDir) {
163
198
  const sourceRoot = path.join(packageRoot, "runtime", "skills");
164
199
  const destinationRoot = path.join(agentDir, "skills");
165
200
  fs.mkdirSync(destinationRoot, { recursive: true });
166
- console.log("==> Installing curated runtime skills");
167
- for (const skill of runtimeSkills) {
201
+ console.log(`==> Curated skills (${runtimeSkills.length})`);
202
+ for (const [index, skill] of runtimeSkills.entries()) {
168
203
  const source = path.join(sourceRoot, skill);
169
204
  const destination = path.join(destinationRoot, skill);
170
205
  if (!fs.existsSync(path.join(source, "SKILL.md"))) {
@@ -172,8 +207,63 @@ function installRuntimeSkills(agentDir) {
172
207
  }
173
208
  fs.rmSync(destination, { recursive: true, force: true });
174
209
  fs.cpSync(source, destination, { recursive: true });
175
- console.log(` ${skill}`);
210
+ console.log(` [${index + 1}/${runtimeSkills.length}] ${skill} ... done`);
211
+ }
212
+ }
213
+ function isLocalPackagePath(packageSpec) {
214
+ return (
215
+ path.isAbsolute(packageSpec) ||
216
+ packageSpec.startsWith(".") ||
217
+ packageSpec === "~" ||
218
+ packageSpec.startsWith(`~${path.sep}`)
219
+ );
220
+ }
221
+
222
+ function resolvedPackagePath(packageSpec, agentDir) {
223
+ const expanded =
224
+ packageSpec === "~" || packageSpec.startsWith(`~${path.sep}`)
225
+ ? path.join(process.env.HOME ?? "", packageSpec.slice(2))
226
+ : packageSpec;
227
+ const resolved = path.resolve(agentDir, expanded);
228
+ try {
229
+ return fs.realpathSync(resolved);
230
+ } catch {
231
+ return resolved;
232
+ }
233
+ }
234
+
235
+ function isWithinDirectory(candidate, directory) {
236
+ const relative = path.relative(directory, candidate);
237
+ return relative === "" || (!relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative));
238
+ }
239
+
240
+ function sanitizeAgentSettings(agentDir) {
241
+ const settingsPath = path.join(agentDir, "settings.json");
242
+ if (!fs.existsSync(settingsPath)) return 0;
243
+
244
+ let settings;
245
+ try {
246
+ settings = JSON.parse(fs.readFileSync(settingsPath, "utf-8"));
247
+ } catch (error) {
248
+ fail(`cannot read ${settingsPath}: ${error instanceof Error ? error.message : String(error)}`);
249
+ }
250
+ if (!Array.isArray(settings.packages)) return 0;
251
+
252
+ const canonicalAgentDir = fs.realpathSync(agentDir);
253
+ let removed = 0;
254
+ settings.packages = settings.packages.filter((packageSpec) => {
255
+ if (typeof packageSpec !== "string" || !isLocalPackagePath(packageSpec)) return true;
256
+ const keep = isWithinDirectory(resolvedPackagePath(packageSpec, agentDir), canonicalAgentDir);
257
+ if (!keep) removed += 1;
258
+ return keep;
259
+ });
260
+ if (removed > 0) {
261
+ fs.writeFileSync(settingsPath, `${JSON.stringify(settings, null, 2)}\n`, "utf-8");
262
+ console.log(
263
+ `==> Removed ${removed} external package path${removed === 1 ? "" : "s"} from vault-local settings`
264
+ );
176
265
  }
266
+ return removed;
177
267
  }
178
268
 
179
269
  function scaffoldMainAgentSystem(agentDir, vaultRoot) {
@@ -197,6 +287,7 @@ fs.mkdirSync(agentDir, { recursive: true });
197
287
  console.log(`==> Setting up vault-local Pi runtime at ${agentDir}`);
198
288
  removeStaleExtensions(agentDir);
199
289
  installExtensions(agentDir);
290
+ sanitizeAgentSettings(agentDir);
200
291
  installNotebookLM();
201
292
  installRuntimeSkills(agentDir);
202
293
 
@@ -111,6 +111,28 @@ USE_OP_ENV="false"
111
111
  if [ -f "${VAULT}/.vault-mind/.env.1pass" ] && [ -d "${PI_DIR}/npm/node_modules/pi-1password" ]; then
112
112
  USE_OP_ENV="true"
113
113
  fi
114
+ # Pi discovers user and project skills even when PI_CODING_AGENT_DIR is vault-local.
115
+ # Disable implicit discovery, then load only curated and installed package skills
116
+ # rooted inside this vault's agent directory.
117
+ PI_ARGS=(--no-skills --no-prompt-templates)
118
+ while IFS= read -r skill_file; do
119
+ PI_ARGS+=(--skill "${skill_file}")
120
+ done < <(
121
+ for skills_root in \
122
+ "${PI_DIR}/skills" \
123
+ "${PI_DIR}/npm/node_modules"/*/skills \
124
+ "${PI_DIR}/npm/node_modules"/@*/*/skills; do
125
+ [ -d "${skills_root}" ] || continue
126
+ find "${skills_root}" -mindepth 2 -maxdepth 2 -type f -name SKILL.md
127
+ done | LC_ALL=C sort -u
128
+ )
129
+
130
+ PI_ARGS_Q=""
131
+ for pi_arg in "${PI_ARGS[@]}"; do
132
+ printf -v pi_arg_q '%q' "${pi_arg}"
133
+ PI_ARGS_Q="${PI_ARGS_Q} ${pi_arg_q}"
134
+ done
135
+
114
136
 
115
137
  # Auto mode keeps interactive callers, including Obsidian Terminal, in place.
116
138
  # Non-interactive runners retain the legacy new-window behavior.
@@ -128,9 +150,9 @@ if [ "${MODE}" = "same-terminal" ]; then
128
150
  cd "${VAULT}"
129
151
  echo "[vault-pi] Starting pi for ${VAULT} in the current terminal"
130
152
  if [ "${USE_OP_ENV}" = "true" ]; then
131
- PI_CODING_AGENT_DIR="${PI_DIR}" "${PI_EXECUTABLE}" --op-env .vault-mind/.env.1pass
153
+ PI_CODING_AGENT_DIR="${PI_DIR}" "${PI_EXECUTABLE}" "${PI_ARGS[@]}" --op-env .vault-mind/.env.1pass
132
154
  else
133
- PI_CODING_AGENT_DIR="${PI_DIR}" "${PI_EXECUTABLE}"
155
+ PI_CODING_AGENT_DIR="${PI_DIR}" "${PI_EXECUTABLE}" "${PI_ARGS[@]}"
134
156
  fi
135
157
  exit $?
136
158
  fi
@@ -140,7 +162,7 @@ OP_ENV_ARG=""
140
162
  if [ "${USE_OP_ENV}" = "true" ]; then
141
163
  OP_ENV_ARG="--op-env .vault-mind/.env.1pass"
142
164
  fi
143
- PI_CMD="PI_CODING_AGENT_DIR='${PI_DIR}' ${PI_EXECUTABLE_Q} ${OP_ENV_ARG}; rm -f '${PID_FILE}'"
165
+ PI_CMD="PI_CODING_AGENT_DIR='${PI_DIR}' ${PI_EXECUTABLE_Q}${PI_ARGS_Q} ${OP_ENV_ARG}; rm -f '${PID_FILE}'"
144
166
 
145
167
  if [ -d "/Applications/iTerm.app" ]; then
146
168
  open -a iTerm --args --detach bash -c "echo \$\$ > '${PID_FILE}'; cd '${VAULT}'; ${PI_CMD}"