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 +21 -0
- package/README.md +21 -26
- package/dist/src/extension-packages.d.ts +4 -4
- package/dist/src/extension-packages.js +4 -4
- package/dist/test/pvm-setup.test.js +56 -0
- package/dist/test/vault-pi-script.test.js +69 -3
- package/package.json +1 -1
- package/runtime/pvm-extensions.json +2 -1
- package/scripts/pvm-setup.mjs +109 -18
- package/scripts/vault-pi.sh +25 -3
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
|
-
###
|
|
126
|
+
### Curated vault-local skills
|
|
127
127
|
|
|
128
|
-
|
|
129
|
-
|
|
128
|
+
`pvm setup` installs the supported Obsidian and NotebookLM skills into the
|
|
129
|
+
vault-local Pi agent directory:
|
|
130
130
|
|
|
131
131
|
```bash
|
|
132
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
*
|
|
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
|
|
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
|
-
*
|
|
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\
|
|
26
|
-
"$PWD" "$PI_CODING_AGENT_DIR" "
|
|
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
|
-
|
|
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.
|
|
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
|
}
|
package/scripts/pvm-setup.mjs
CHANGED
|
@@ -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("
|
|
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
|
-
|
|
88
|
+
encoding: "utf-8",
|
|
84
89
|
});
|
|
85
90
|
if (result.error) fail(`could not start uv: ${result.error.message}`);
|
|
86
|
-
if (result.status !== 0)
|
|
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
|
-
|
|
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)
|
|
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
|
-
|
|
152
|
-
|
|
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
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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(
|
|
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
|
|
package/scripts/vault-pi.sh
CHANGED
|
@@ -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}"
|