pi-vault-mind 0.16.29 → 0.16.30
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 +16 -0
- package/README.md +184 -1
- package/dist/test/pvm-cli.test.js +88 -0
- package/dist/test/vault-pi-script.test.js +101 -0
- package/package.json +4 -1
- package/scripts/pvm.mjs +80 -0
- package/scripts/vault-pi.sh +76 -12
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.16.30 — 2026-08-27
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- **First-class `pvm` executable.** The npm package now exposes `pvm run [vault-path]`; it uses the current directory when the vault path is omitted and supports both global installation and `npx pi-vault-mind run`.
|
|
8
|
+
- **Explicit launch modes.** `pvm run` stays in the current terminal by default, with `--same-terminal`, `--new-window`, and `--auto` overrides.
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- **Portable vault launcher.** `vault-pi.sh` now handles interactive terminals directly, normalizes relative vault paths before deriving runtime state, removes inline-session PID files on exit, and rejects unknown flags instead of treating them as vault paths.
|
|
13
|
+
|
|
14
|
+
### Verification
|
|
15
|
+
|
|
16
|
+
- Root and Obsidian builds passed; full test suite: 910/910 passing.
|
|
17
|
+
- `npm pack` includes `scripts/pvm.mjs` as an executable, and `npm exec` resolved the packed tarball's `pvm` command successfully.
|
|
18
|
+
|
|
3
19
|
## 0.16.29 — 2026-08-27
|
|
4
20
|
|
|
5
21
|
### Fixed
|
package/README.md
CHANGED
|
@@ -194,10 +194,25 @@ The Broadcaster agent can generate podcasts, study guides, and slide decks from
|
|
|
194
194
|
|
|
195
195
|
```bash
|
|
196
196
|
pi install npm:pi-vault-mind # latest
|
|
197
|
-
pi install npm:pi-vault-mind@0.
|
|
197
|
+
pi install npm:pi-vault-mind@0.16.30 # pinned release
|
|
198
198
|
pi -e npm:pi-vault-mind # try without installing
|
|
199
199
|
```
|
|
200
200
|
|
|
201
|
+
`pi install` loads the extension but does not expose its npm executable on your
|
|
202
|
+
shell `PATH`. Install the first-class launcher once when you want the bare
|
|
203
|
+
`pvm` command:
|
|
204
|
+
|
|
205
|
+
```bash
|
|
206
|
+
npm install --global pi-vault-mind
|
|
207
|
+
pvm run /path/to/your/vault
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
Or run it without a global installation:
|
|
211
|
+
|
|
212
|
+
```bash
|
|
213
|
+
npx --yes pi-vault-mind run /path/to/your/vault
|
|
214
|
+
```
|
|
215
|
+
|
|
201
216
|
Or from git:
|
|
202
217
|
|
|
203
218
|
```bash
|
|
@@ -269,6 +284,174 @@ Edit `<vault>/.vault-mind/vault-mind.config.json` to match your domain:
|
|
|
269
284
|
}
|
|
270
285
|
```
|
|
271
286
|
|
|
287
|
+
## Using Vault Mind agent identities
|
|
288
|
+
|
|
289
|
+
Vault Mind keeps the interactive Pi session on the unrestricted `main` persona
|
|
290
|
+
unless you explicitly select a specialist boundary. Bundled specialist skills
|
|
291
|
+
are available resources; merely loading them does not change the active
|
|
292
|
+
persona.
|
|
293
|
+
|
|
294
|
+
### Start the vault-local main agent
|
|
295
|
+
|
|
296
|
+
From the vault directory, start Pi in the current terminal:
|
|
297
|
+
|
|
298
|
+
```bash
|
|
299
|
+
pvm run
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
You can launch a different vault from anywhere or use the package without a
|
|
303
|
+
global install:
|
|
304
|
+
|
|
305
|
+
```bash
|
|
306
|
+
pvm run /path/to/your/vault
|
|
307
|
+
npx --yes pi-vault-mind run /path/to/your/vault
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
`pvm run` defaults to the current terminal. This is the recommended Obsidian
|
|
311
|
+
Terminal profile command. Use a vault-path placeholder supplied by the plugin,
|
|
312
|
+
or provide the vault's absolute path. Override the launch mode when needed:
|
|
313
|
+
|
|
314
|
+
```bash
|
|
315
|
+
pvm run --same-terminal /path/to/your/vault
|
|
316
|
+
pvm run --new-window /path/to/your/vault
|
|
317
|
+
pvm run --auto /path/to/your/vault
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
`--new-window` opens a dedicated Terminal/iTerm window. `--auto` uses the
|
|
321
|
+
current terminal when attached to a TTY and falls back to a new window
|
|
322
|
+
otherwise. The packaged shell launcher remains available for direct use as
|
|
323
|
+
`scripts/vault-pi.sh`.
|
|
324
|
+
|
|
325
|
+
In every mode, the launcher runs Pi inside the target vault with its
|
|
326
|
+
configuration isolated under `<vault>/.vault-mind/.pi/agent/`. It also prevents
|
|
327
|
+
two launcher-managed Pi processes from running for the same vault and removes
|
|
328
|
+
the PID file when an inline session exits.
|
|
329
|
+
|
|
330
|
+
Check the active persona after startup:
|
|
331
|
+
|
|
332
|
+
```text
|
|
333
|
+
/vm identity status
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
A normal main-agent session reports:
|
|
337
|
+
|
|
338
|
+
```text
|
|
339
|
+
Persona: main
|
|
340
|
+
Effective boundary: none (main agent)
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
`Injection` may report `enabled` or `disabled`. An enabled injector still
|
|
344
|
+
applies no boundary while the persona is `main`.
|
|
345
|
+
|
|
346
|
+
### Use specialist agents without changing the main session
|
|
347
|
+
|
|
348
|
+
Normally, leave the interactive session on `main` and dispatch specialist work
|
|
349
|
+
through the passive watcher:
|
|
350
|
+
|
|
351
|
+
```text
|
|
352
|
+
@agent-Miner
|
|
353
|
+
Research and extract the durable facts from this note.
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
The watcher runs that work as an isolated specialist job. It does not turn the
|
|
357
|
+
interactive terminal into the Miner persona. The same distinction applies to
|
|
358
|
+
`@agent-Broadcaster`, `@agent-Manager`, and `@agent-Heavy-Lifter`.
|
|
359
|
+
|
|
360
|
+
### Explicitly change the interactive persona
|
|
361
|
+
|
|
362
|
+
Use an identity boundary only when the current interactive session itself
|
|
363
|
+
should follow a specialist capability contract:
|
|
364
|
+
|
|
365
|
+
```text
|
|
366
|
+
/vm identity set miner
|
|
367
|
+
/vm identity set broadcaster
|
|
368
|
+
/vm identity set manager
|
|
369
|
+
/vm identity set heavy-lifter
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
| Persona | Intended interactive boundary |
|
|
373
|
+
|---|---|
|
|
374
|
+
| `main` | Unrestricted primary agent with the full configured toolset |
|
|
375
|
+
| `manager` | Orchestrates collections, publishing, and subagents; does not run Bash |
|
|
376
|
+
| `miner` | Extracts and stores knowledge; does not run Bash or publish |
|
|
377
|
+
| `broadcaster` | Produces presentation artifacts; does not run Bash or write durable knowledge |
|
|
378
|
+
| `heavy-lifter` | Performs file and Bash work in isolation; does not write durable knowledge or publish |
|
|
379
|
+
|
|
380
|
+
`/vm identity set <role>` persists the selected persona, enables boundary
|
|
381
|
+
injection, and applies on the next agent turn.
|
|
382
|
+
|
|
383
|
+
Use the remaining controls to inspect or suspend that selection:
|
|
384
|
+
|
|
385
|
+
```text
|
|
386
|
+
/vm identity status
|
|
387
|
+
/vm identity off
|
|
388
|
+
/vm identity on
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
- `off` disables boundary injection without discarding the selected persona.
|
|
392
|
+
- `on` re-enables the configured persona boundary on the next agent turn.
|
|
393
|
+
- `status` shows the configured persona, injection switch, effective boundary,
|
|
394
|
+
and any environment overrides.
|
|
395
|
+
|
|
396
|
+
Restore the safe main-agent default with:
|
|
397
|
+
|
|
398
|
+
```text
|
|
399
|
+
/vm identity set main
|
|
400
|
+
/vm identity off
|
|
401
|
+
/vm identity status
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
The final status should show `Effective boundary: none (main agent)`.
|
|
405
|
+
|
|
406
|
+
### Configuration and environment overrides
|
|
407
|
+
|
|
408
|
+
Identity controls are stored in the vault-local configuration:
|
|
409
|
+
|
|
410
|
+
```json
|
|
411
|
+
{
|
|
412
|
+
"vaultMind": {
|
|
413
|
+
"identities": {
|
|
414
|
+
"activeRole": "main",
|
|
415
|
+
"injectBoundaries": false
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
The canonical path is
|
|
422
|
+
`<vault>/.vault-mind/vault-mind.config.json` under
|
|
423
|
+
`vaultMind.identities`. Identity commands preserve any existing
|
|
424
|
+
`vaultMind.identities.roles` allowlists.
|
|
425
|
+
|
|
426
|
+
Launch automation and explicit specialist forks can override the saved values
|
|
427
|
+
for one process:
|
|
428
|
+
|
|
429
|
+
```bash
|
|
430
|
+
VAULT_MIND_AGENT_ROLE=miner VAULT_MIND_IDENTITY_INJECTION=on pi
|
|
431
|
+
VAULT_MIND_IDENTITY_INJECTION=off pi
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
`VAULT_MIND_AGENT_ROLE` takes precedence over `activeRole`.
|
|
435
|
+
`VAULT_MIND_IDENTITY_INJECTION=off` suppresses every boundary, while `on`
|
|
436
|
+
enables the explicitly selected role. Missing, `main`, `none`, and unknown
|
|
437
|
+
roles all resolve safely to the unrestricted main agent.
|
|
438
|
+
|
|
439
|
+
### Troubleshoot an unexpected specialist boundary
|
|
440
|
+
|
|
441
|
+
1. Run `/vm identity status` and check both the configured persona and any
|
|
442
|
+
reported environment overrides.
|
|
443
|
+
2. If an environment override is present, exit Pi, unset
|
|
444
|
+
`VAULT_MIND_AGENT_ROLE` and `VAULT_MIND_IDENTITY_INJECTION`, then relaunch.
|
|
445
|
+
3. Run `/vm identity set main`, followed by `/vm identity off`.
|
|
446
|
+
4. Send the next agent turn and re-run `/vm identity status`; boundary changes
|
|
447
|
+
take effect on the next turn.
|
|
448
|
+
5. Confirm Pi was started for the intended vault and uses
|
|
449
|
+
`<vault>/.vault-mind/.pi/agent/` as `PI_CODING_AGENT_DIR`.
|
|
450
|
+
|
|
451
|
+
Do not edit `SYSTEM.md` or an agent skill to change the interactive identity.
|
|
452
|
+
Those files provide operating instructions and resources; persona selection
|
|
453
|
+
belongs in `vaultMind.identities` or an explicit environment override.
|
|
454
|
+
|
|
272
455
|
## Example domains
|
|
273
456
|
|
|
274
457
|
### Research project
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { strict as assert } from "node:assert";
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
import * as fs from "node:fs";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import * as path from "node:path";
|
|
6
|
+
import { afterEach, beforeEach, describe, it } from "node:test";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
|
|
9
|
+
const pvmCli = path.join(repoRoot, "scripts", "pvm.mjs");
|
|
10
|
+
const vaultLauncher = path.join(repoRoot, "scripts", "vault-pi.sh");
|
|
11
|
+
describe("pvm CLI", () => {
|
|
12
|
+
let testDir;
|
|
13
|
+
let vaultPath;
|
|
14
|
+
let binDir;
|
|
15
|
+
let outputPath;
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
testDir = path.join(tmpdir(), `pvm-cli-${process.pid}-${Date.now()}`);
|
|
18
|
+
vaultPath = path.join(testDir, "Test Vault");
|
|
19
|
+
binDir = path.join(testDir, "bin");
|
|
20
|
+
outputPath = path.join(testDir, "bash-args.txt");
|
|
21
|
+
fs.mkdirSync(vaultPath, { recursive: true });
|
|
22
|
+
fs.mkdirSync(binDir, { recursive: true });
|
|
23
|
+
fs.writeFileSync(path.join(binDir, "bash"), `#!/bin/bash
|
|
24
|
+
set -euo pipefail
|
|
25
|
+
printf '%s\n' "$@" > "$PVM_TEST_OUTPUT"
|
|
26
|
+
exit "\${PVM_TEST_BASH_EXIT:-0}"
|
|
27
|
+
`, { mode: 0o755 });
|
|
28
|
+
});
|
|
29
|
+
afterEach(() => {
|
|
30
|
+
fs.rmSync(testDir, { recursive: true, force: true });
|
|
31
|
+
});
|
|
32
|
+
function runCli(args, cwd = vaultPath, env = {}) {
|
|
33
|
+
return spawnSync(process.execPath, [pvmCli, ...args], {
|
|
34
|
+
cwd,
|
|
35
|
+
encoding: "utf-8",
|
|
36
|
+
env: {
|
|
37
|
+
...process.env,
|
|
38
|
+
PATH: `${binDir}:${process.env.PATH ?? ""}`,
|
|
39
|
+
PVM_TEST_OUTPUT: outputPath,
|
|
40
|
+
...env,
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
function capturedBashArgs() {
|
|
45
|
+
return fs.readFileSync(outputPath, "utf-8").trimEnd().split("\n");
|
|
46
|
+
}
|
|
47
|
+
it("runs the current vault in the same terminal by default", () => {
|
|
48
|
+
const result = runCli(["run"]);
|
|
49
|
+
assert.equal(result.status, 0, result.stderr || result.stdout);
|
|
50
|
+
assert.deepEqual(capturedBashArgs(), [
|
|
51
|
+
vaultLauncher,
|
|
52
|
+
"--same-terminal",
|
|
53
|
+
fs.realpathSync(vaultPath),
|
|
54
|
+
]);
|
|
55
|
+
});
|
|
56
|
+
it("passes an explicit vault and launch mode to the packaged launcher", () => {
|
|
57
|
+
const result = runCli(["run", "--new-window", vaultPath], testDir);
|
|
58
|
+
assert.equal(result.status, 0, result.stderr || result.stdout);
|
|
59
|
+
assert.deepEqual(capturedBashArgs(), [vaultLauncher, "--new-window", vaultPath]);
|
|
60
|
+
});
|
|
61
|
+
it("preserves the launcher's automatic TTY mode when requested", () => {
|
|
62
|
+
const result = runCli(["run", vaultPath, "--auto"], testDir);
|
|
63
|
+
assert.equal(result.status, 0, result.stderr || result.stdout);
|
|
64
|
+
assert.deepEqual(capturedBashArgs(), [vaultLauncher, vaultPath]);
|
|
65
|
+
});
|
|
66
|
+
it("propagates the packaged launcher's exit status", () => {
|
|
67
|
+
const result = runCli(["run"], vaultPath, { PVM_TEST_BASH_EXIT: "23" });
|
|
68
|
+
assert.equal(result.status, 23);
|
|
69
|
+
});
|
|
70
|
+
it("reports when the shell launcher cannot be started", () => {
|
|
71
|
+
const emptyBinDir = path.join(testDir, "empty-bin");
|
|
72
|
+
fs.mkdirSync(emptyBinDir);
|
|
73
|
+
const result = runCli(["run"], vaultPath, { PATH: emptyBinDir });
|
|
74
|
+
assert.equal(result.status, 1);
|
|
75
|
+
assert.match(result.stderr, /pvm: failed to start launcher:/);
|
|
76
|
+
});
|
|
77
|
+
it("rejects unknown commands with usage help", () => {
|
|
78
|
+
const result = runCli(["launch"]);
|
|
79
|
+
assert.equal(result.status, 2);
|
|
80
|
+
assert.match(result.stderr, /Usage: pvm run/);
|
|
81
|
+
assert.equal(fs.existsSync(outputPath), false);
|
|
82
|
+
});
|
|
83
|
+
it("publishes pvm as an executable package bin", () => {
|
|
84
|
+
const packageJson = JSON.parse(fs.readFileSync(path.join(repoRoot, "package.json"), "utf-8"));
|
|
85
|
+
assert.equal(packageJson.bin?.pvm, "scripts/pvm.mjs");
|
|
86
|
+
assert.notEqual(fs.statSync(pvmCli).mode & 0o111, 0);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { strict as assert } from "node:assert";
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
import * as fs from "node:fs";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import * as path from "node:path";
|
|
6
|
+
import { afterEach, beforeEach, describe, it } from "node:test";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
|
|
9
|
+
const launcher = path.join(repoRoot, "scripts", "vault-pi.sh");
|
|
10
|
+
describe("vault-pi launcher", () => {
|
|
11
|
+
let testDir;
|
|
12
|
+
let vaultPath;
|
|
13
|
+
let binDir;
|
|
14
|
+
let outputPath;
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
testDir = path.join(tmpdir(), `pvm-vault-pi-${process.pid}-${Date.now()}`);
|
|
17
|
+
vaultPath = path.join(testDir, "Test Vault");
|
|
18
|
+
binDir = path.join(testDir, "bin");
|
|
19
|
+
outputPath = path.join(testDir, "pi-output.txt");
|
|
20
|
+
fs.mkdirSync(vaultPath, { recursive: true });
|
|
21
|
+
fs.mkdirSync(binDir, { recursive: true });
|
|
22
|
+
fs.writeFileSync(path.join(binDir, "pi"), `#!/usr/bin/env bash
|
|
23
|
+
set -euo pipefail
|
|
24
|
+
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"
|
|
27
|
+
`, { mode: 0o755 });
|
|
28
|
+
for (const command of ["open", "osascript"]) {
|
|
29
|
+
fs.writeFileSync(path.join(binDir, command), "#!/usr/bin/env bash\nexit 97\n", {
|
|
30
|
+
mode: 0o755,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
afterEach(() => {
|
|
35
|
+
fs.rmSync(testDir, { recursive: true, force: true });
|
|
36
|
+
});
|
|
37
|
+
it("runs Pi in the current terminal when same-terminal mode is requested", () => {
|
|
38
|
+
const result = spawnSync("bash", [launcher, "--same-terminal", vaultPath], {
|
|
39
|
+
cwd: testDir,
|
|
40
|
+
encoding: "utf-8",
|
|
41
|
+
env: {
|
|
42
|
+
...process.env,
|
|
43
|
+
PATH: `${binDir}:${process.env.PATH ?? ""}`,
|
|
44
|
+
PVM_TEST_OUTPUT: outputPath,
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
assert.equal(result.status, 0, result.stderr || result.stdout);
|
|
48
|
+
const output = fs.readFileSync(outputPath, "utf-8");
|
|
49
|
+
assert.match(output, new RegExp(`^cwd=${vaultPath.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}$`, "m"));
|
|
50
|
+
assert.match(output, new RegExp(`^agent=${path.join(vaultPath, ".vault-mind", ".pi", "agent").replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}$`, "m"));
|
|
51
|
+
assert.match(output, /^args=$/m);
|
|
52
|
+
assert.match(output, /^pid=\d+$/m);
|
|
53
|
+
assert.equal(fs.existsSync(path.join(vaultPath, ".vault-mind", ".pi", "agent", "daemon.pid")), false);
|
|
54
|
+
});
|
|
55
|
+
it("normalizes a relative vault path before changing directories", () => {
|
|
56
|
+
const relativeVaultPath = path.relative(testDir, vaultPath);
|
|
57
|
+
const result = spawnSync("bash", [launcher, "--same-terminal", relativeVaultPath], {
|
|
58
|
+
cwd: testDir,
|
|
59
|
+
encoding: "utf-8",
|
|
60
|
+
env: {
|
|
61
|
+
...process.env,
|
|
62
|
+
PATH: `${binDir}:${process.env.PATH ?? ""}`,
|
|
63
|
+
PVM_TEST_OUTPUT: outputPath,
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
assert.equal(result.status, 0, result.stderr || result.stdout);
|
|
67
|
+
const output = fs.readFileSync(outputPath, "utf-8");
|
|
68
|
+
const cwd = output.match(/^cwd=(.*)$/m)?.[1];
|
|
69
|
+
const agentDir = output.match(/^agent=(.*)$/m)?.[1];
|
|
70
|
+
assert.ok(cwd, "fake Pi did not report its working directory");
|
|
71
|
+
assert.equal(agentDir, path.join(cwd, ".vault-mind", ".pi", "agent"));
|
|
72
|
+
assert.equal(fs.existsSync(path.join(vaultPath, ".vault-mind", ".pi", "agent", "daemon.pid")), false);
|
|
73
|
+
});
|
|
74
|
+
it("falls back to a new window for non-interactive automatic launches", () => {
|
|
75
|
+
const result = spawnSync("bash", [launcher, vaultPath], {
|
|
76
|
+
cwd: testDir,
|
|
77
|
+
encoding: "utf-8",
|
|
78
|
+
env: {
|
|
79
|
+
...process.env,
|
|
80
|
+
PATH: `${binDir}:${process.env.PATH ?? ""}`,
|
|
81
|
+
PVM_TEST_OUTPUT: outputPath,
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
assert.equal(result.status, 97);
|
|
85
|
+
assert.equal(fs.existsSync(outputPath), false);
|
|
86
|
+
});
|
|
87
|
+
it("rejects an unknown launch flag with usage help", () => {
|
|
88
|
+
const result = spawnSync("bash", [launcher, "--new-widnow"], {
|
|
89
|
+
cwd: testDir,
|
|
90
|
+
encoding: "utf-8",
|
|
91
|
+
env: {
|
|
92
|
+
...process.env,
|
|
93
|
+
PATH: `${binDir}:${process.env.PATH ?? ""}`,
|
|
94
|
+
PVM_TEST_OUTPUT: outputPath,
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
assert.equal(result.status, 2);
|
|
98
|
+
assert.match(result.stderr, /Usage: vault-pi\.sh/);
|
|
99
|
+
assert.equal(fs.existsSync(path.join(testDir, "--new-widnow")), false);
|
|
100
|
+
});
|
|
101
|
+
});
|
package/package.json
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-vault-mind",
|
|
3
|
-
"version": "0.16.
|
|
3
|
+
"version": "0.16.30",
|
|
4
4
|
"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.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
7
7
|
"type": "module",
|
|
8
|
+
"bin": {
|
|
9
|
+
"pvm": "scripts/pvm.mjs"
|
|
10
|
+
},
|
|
8
11
|
"files": [
|
|
9
12
|
"dist",
|
|
10
13
|
"agents",
|
package/scripts/pvm.mjs
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { spawnSync } from "node:child_process";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
|
|
6
|
+
const USAGE = `Usage: pvm run [--same-terminal | --new-window | --auto] [vault-path]
|
|
7
|
+
|
|
8
|
+
Commands:
|
|
9
|
+
run Start Pi with the vault-local Vault Mind agent environment.
|
|
10
|
+
|
|
11
|
+
Run options:
|
|
12
|
+
--same-terminal Run Pi in the current terminal (default).
|
|
13
|
+
--new-window Open Pi in a new Terminal/iTerm window.
|
|
14
|
+
--auto Use the current terminal when attached to a TTY; otherwise open a new window.
|
|
15
|
+
|
|
16
|
+
When vault-path is omitted, pvm uses the current working directory.`;
|
|
17
|
+
|
|
18
|
+
function fail(message) {
|
|
19
|
+
if (message) console.error(`pvm: ${message}\n`);
|
|
20
|
+
console.error(USAGE);
|
|
21
|
+
process.exitCode = 2;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const [command, ...args] = process.argv.slice(2);
|
|
25
|
+
|
|
26
|
+
if (!command || command === "-h" || command === "--help" || command === "help") {
|
|
27
|
+
console.log(USAGE);
|
|
28
|
+
} else if (command !== "run") {
|
|
29
|
+
fail(`unknown command: ${command}`);
|
|
30
|
+
} else {
|
|
31
|
+
let mode = "same-terminal";
|
|
32
|
+
let vaultPath;
|
|
33
|
+
let invalid = false;
|
|
34
|
+
|
|
35
|
+
for (const arg of args) {
|
|
36
|
+
if (arg === "-h" || arg === "--help") {
|
|
37
|
+
console.log(USAGE);
|
|
38
|
+
process.exit(0);
|
|
39
|
+
}
|
|
40
|
+
if (arg === "--same-terminal") {
|
|
41
|
+
mode = "same-terminal";
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
if (arg === "--new-window") {
|
|
45
|
+
mode = "new-window";
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
if (arg === "--auto") {
|
|
49
|
+
mode = "auto";
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
if (arg.startsWith("-")) {
|
|
53
|
+
fail(`unknown option: ${arg}`);
|
|
54
|
+
invalid = true;
|
|
55
|
+
break;
|
|
56
|
+
}
|
|
57
|
+
if (vaultPath !== undefined) {
|
|
58
|
+
fail("run accepts at most one vault path");
|
|
59
|
+
invalid = true;
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
vaultPath = arg;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (!invalid) {
|
|
66
|
+
const launcher = fileURLToPath(new URL("./vault-pi.sh", import.meta.url));
|
|
67
|
+
const launchMode = mode === "auto" ? [] : [`--${mode}`];
|
|
68
|
+
const result = spawnSync("bash", [launcher, ...launchMode, vaultPath ?? process.cwd()], {
|
|
69
|
+
stdio: "inherit",
|
|
70
|
+
env: process.env,
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
if (result.error) {
|
|
74
|
+
console.error(`pvm: failed to start launcher: ${result.error.message}`);
|
|
75
|
+
process.exitCode = 1;
|
|
76
|
+
} else {
|
|
77
|
+
process.exitCode = result.status ?? 1;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
package/scripts/vault-pi.sh
CHANGED
|
@@ -2,20 +2,59 @@
|
|
|
2
2
|
# vault-pi.sh — Start pi for a vault, ensuring only one instance runs.
|
|
3
3
|
#
|
|
4
4
|
# Usage:
|
|
5
|
-
# bash scripts/vault-pi.sh /path/to/vault
|
|
5
|
+
# bash scripts/vault-pi.sh [--same-terminal | --new-window] /path/to/vault
|
|
6
6
|
#
|
|
7
|
-
# Obsidian
|
|
8
|
-
#
|
|
7
|
+
# Obsidian Terminal integration:
|
|
8
|
+
# bash /path/to/pi-vault-mind/scripts/vault-pi.sh {{vault_path}}
|
|
9
9
|
#
|
|
10
10
|
# The script:
|
|
11
11
|
# 1. Checks for an existing PID file at <vault>/.vault-mind/.pi/agent/daemon.pid
|
|
12
12
|
# 2. If the PID is alive → exits silently (already running)
|
|
13
13
|
# 3. If stale/dead → cleans up and starts fresh
|
|
14
|
-
# 4.
|
|
14
|
+
# 4. Runs in the current terminal when attached to a TTY
|
|
15
|
+
# 5. Falls back to a new Terminal/iTerm window for non-interactive callers
|
|
15
16
|
|
|
16
17
|
set -euo pipefail
|
|
17
18
|
|
|
18
|
-
|
|
19
|
+
usage() {
|
|
20
|
+
cat <<'EOF'
|
|
21
|
+
Usage: vault-pi.sh [--same-terminal | --new-window] <vault-path>
|
|
22
|
+
|
|
23
|
+
Launch modes:
|
|
24
|
+
auto Use the current terminal when attached to a TTY; otherwise open a new window.
|
|
25
|
+
--same-terminal Force Pi to run in the current terminal.
|
|
26
|
+
--new-window Force Pi to open in a new Terminal/iTerm window.
|
|
27
|
+
EOF
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
MODE="auto"
|
|
31
|
+
case "${1:-}" in
|
|
32
|
+
--same-terminal)
|
|
33
|
+
MODE="same-terminal"
|
|
34
|
+
shift
|
|
35
|
+
;;
|
|
36
|
+
--new-window)
|
|
37
|
+
MODE="new-window"
|
|
38
|
+
shift
|
|
39
|
+
;;
|
|
40
|
+
-h | --help)
|
|
41
|
+
usage
|
|
42
|
+
exit 0
|
|
43
|
+
;;
|
|
44
|
+
-*)
|
|
45
|
+
usage >&2
|
|
46
|
+
exit 2
|
|
47
|
+
;;
|
|
48
|
+
esac
|
|
49
|
+
|
|
50
|
+
if [ "$#" -ne 1 ]; then
|
|
51
|
+
usage >&2
|
|
52
|
+
exit 2
|
|
53
|
+
fi
|
|
54
|
+
|
|
55
|
+
VAULT="$1"
|
|
56
|
+
mkdir -p "${VAULT}"
|
|
57
|
+
VAULT="$(cd "${VAULT}" && pwd)"
|
|
19
58
|
PI_DIR="${VAULT}/.vault-mind/.pi/agent"
|
|
20
59
|
PID_FILE="${PI_DIR}/daemon.pid"
|
|
21
60
|
|
|
@@ -37,14 +76,39 @@ fi
|
|
|
37
76
|
# extension isn't installed makes pi reject the whole invocation with
|
|
38
77
|
# "Unknown option: --op-env" — only pass it when both the env file exists
|
|
39
78
|
# AND pi-1password is actually installed in the vault-local agent dir.
|
|
40
|
-
|
|
79
|
+
USE_OP_ENV="false"
|
|
41
80
|
if [ -f "${VAULT}/.vault-mind/.env.1pass" ] && [ -d "${PI_DIR}/npm/node_modules/pi-1password" ]; then
|
|
42
|
-
|
|
81
|
+
USE_OP_ENV="true"
|
|
43
82
|
fi
|
|
44
83
|
|
|
45
|
-
#
|
|
46
|
-
#
|
|
47
|
-
|
|
84
|
+
# Auto mode keeps interactive callers, including Obsidian Terminal, in place.
|
|
85
|
+
# Non-interactive runners retain the legacy new-window behavior.
|
|
86
|
+
if [ "${MODE}" = "auto" ]; then
|
|
87
|
+
if [ -t 0 ] && [ -t 1 ]; then
|
|
88
|
+
MODE="same-terminal"
|
|
89
|
+
else
|
|
90
|
+
MODE="new-window"
|
|
91
|
+
fi
|
|
92
|
+
fi
|
|
93
|
+
|
|
94
|
+
if [ "${MODE}" = "same-terminal" ]; then
|
|
95
|
+
echo "$$" > "${PID_FILE}"
|
|
96
|
+
trap 'rm -f "${PID_FILE}"' EXIT
|
|
97
|
+
cd "${VAULT}"
|
|
98
|
+
echo "[vault-pi] Starting pi for ${VAULT} in the current terminal"
|
|
99
|
+
if [ "${USE_OP_ENV}" = "true" ]; then
|
|
100
|
+
PI_CODING_AGENT_DIR="${PI_DIR}" pi --op-env .vault-mind/.env.1pass
|
|
101
|
+
else
|
|
102
|
+
PI_CODING_AGENT_DIR="${PI_DIR}" pi
|
|
103
|
+
fi
|
|
104
|
+
exit $?
|
|
105
|
+
fi
|
|
106
|
+
|
|
107
|
+
# New-window mode is retained for non-interactive launchers and explicit use.
|
|
108
|
+
OP_ENV_ARG=""
|
|
109
|
+
if [ "${USE_OP_ENV}" = "true" ]; then
|
|
110
|
+
OP_ENV_ARG="--op-env .vault-mind/.env.1pass"
|
|
111
|
+
fi
|
|
48
112
|
PI_CMD="PI_CODING_AGENT_DIR='${PI_DIR}' pi ${OP_ENV_ARG}; rm -f '${PID_FILE}'"
|
|
49
113
|
|
|
50
114
|
if [ -d "/Applications/iTerm.app" ]; then
|
|
@@ -52,8 +116,8 @@ if [ -d "/Applications/iTerm.app" ]; then
|
|
|
52
116
|
elif [ -d "/Applications/Utilities/Terminal.app" ] || [ -d "/System/Applications/Utilities/Terminal.app" ]; then
|
|
53
117
|
osascript -e "tell application \"Terminal\" to do script \"echo \$\$ > '${PID_FILE}'; cd '${VAULT}'; ${PI_CMD}\""
|
|
54
118
|
else
|
|
55
|
-
echo "[vault-pi] No supported terminal found (iTerm or Terminal.app)"
|
|
119
|
+
echo "[vault-pi] No supported terminal found (iTerm or Terminal.app)" >&2
|
|
56
120
|
exit 1
|
|
57
121
|
fi
|
|
58
122
|
|
|
59
|
-
echo "[vault-pi] Started pi for ${VAULT}"
|
|
123
|
+
echo "[vault-pi] Started pi for ${VAULT} in a new terminal window"
|