pi-vault-mind 0.8.0 → 0.8.3

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.
Files changed (119) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.md +44 -44
  3. package/agents/broadcaster.agent.md +30 -0
  4. package/agents/heavy-lifter.agent.md +30 -0
  5. package/agents/manager.agent.md +29 -0
  6. package/agents/miner.agent.md +29 -0
  7. package/dist/src/agent-bus/Agent.d.ts +49 -0
  8. package/dist/src/agent-bus/Agent.js +97 -0
  9. package/dist/src/agent-bus/AgentLoader.d.ts +33 -0
  10. package/dist/src/agent-bus/AgentLoader.js +102 -0
  11. package/dist/src/agent-bus/AgentRegistry.d.ts +17 -0
  12. package/dist/src/agent-bus/AgentRegistry.js +20 -0
  13. package/dist/src/agent-bus/LLMProvider.d.ts +36 -0
  14. package/dist/src/agent-bus/LLMProvider.js +125 -0
  15. package/dist/src/agent-bus/MessageBus.d.ts +47 -0
  16. package/dist/src/agent-bus/MessageBus.js +81 -0
  17. package/dist/src/agent-bus/TaskQueue.d.ts +27 -0
  18. package/dist/src/agent-bus/TaskQueue.js +57 -0
  19. package/dist/src/agent-bus/index.d.ts +6 -0
  20. package/dist/src/agent-bus/index.js +6 -0
  21. package/dist/src/agents/BroadcasterAgent.d.ts +19 -0
  22. package/dist/src/agents/BroadcasterAgent.js +70 -0
  23. package/dist/src/agents/HeavyLifterAgent.d.ts +26 -0
  24. package/dist/src/agents/HeavyLifterAgent.js +73 -0
  25. package/dist/src/agents/ManagerAgent.d.ts +20 -0
  26. package/dist/src/agents/ManagerAgent.js +71 -0
  27. package/dist/src/agents/MinerAgent.d.ts +19 -0
  28. package/dist/src/agents/MinerAgent.js +70 -0
  29. package/dist/src/agents/index.d.ts +4 -0
  30. package/dist/src/agents/index.js +4 -0
  31. package/dist/src/auth.d.ts +49 -2
  32. package/dist/src/auth.js +149 -7
  33. package/dist/src/autosync.d.ts +1 -1
  34. package/dist/src/autosync.js +3 -3
  35. package/dist/src/bridge.d.ts +45 -0
  36. package/dist/src/bridge.js +189 -0
  37. package/dist/src/commands.d.ts +1 -1
  38. package/dist/src/commands.js +419 -295
  39. package/dist/src/context-capture.d.ts +22 -0
  40. package/dist/src/context-capture.js +41 -0
  41. package/dist/src/discover-schema.d.ts +1 -1
  42. package/dist/src/discover-schema.js +4 -4
  43. package/dist/src/embed-queue.d.ts +1 -1
  44. package/dist/src/embed-queue.js +1 -1
  45. package/dist/src/engine.d.ts +65 -0
  46. package/dist/src/engine.js +244 -0
  47. package/dist/src/events.js +1 -1
  48. package/dist/src/graph.d.ts +3 -3
  49. package/dist/src/identity-injector.d.ts +15 -0
  50. package/dist/src/identity-injector.js +102 -0
  51. package/dist/src/index.js +88 -49
  52. package/dist/src/intake.js +4 -4
  53. package/dist/src/lance.d.ts +11 -37
  54. package/dist/src/lance.js +183 -240
  55. package/dist/src/modal-client.d.ts +14 -4
  56. package/dist/src/modal-client.js +50 -34
  57. package/dist/src/modal-config.d.ts +26 -10
  58. package/dist/src/modal-config.js +36 -5
  59. package/dist/src/personalize.d.ts +47 -0
  60. package/dist/src/personalize.js +352 -0
  61. package/dist/src/server.d.ts +30 -4
  62. package/dist/src/server.js +146 -64
  63. package/dist/src/settings-ui.d.ts +3 -1
  64. package/dist/src/settings-ui.js +84 -77
  65. package/dist/src/status.d.ts +25 -0
  66. package/dist/src/status.js +49 -0
  67. package/dist/src/sync.d.ts +1 -1
  68. package/dist/src/sync.js +19 -19
  69. package/dist/src/tombstone.js +2 -2
  70. package/dist/src/tools.js +112 -130
  71. package/dist/src/types.d.ts +86 -18
  72. package/dist/src/types.js +7 -4
  73. package/dist/src/utils.d.ts +5 -1
  74. package/dist/src/utils.js +24 -8
  75. package/dist/src/vault-tools.d.ts +29 -0
  76. package/dist/src/vault-tools.js +314 -0
  77. package/dist/src/vault-writer.d.ts +4 -5
  78. package/dist/src/vault-writer.js +13 -13
  79. package/dist/src/vm-handlers.d.ts +15 -0
  80. package/dist/src/vm-handlers.js +469 -0
  81. package/dist/src/watcher.d.ts +18 -4
  82. package/dist/src/watcher.js +109 -98
  83. package/dist/test/auth.test.js +57 -7
  84. package/dist/test/bridge-identity.test.js +388 -0
  85. package/dist/test/discover-schema.test.js +2 -2
  86. package/dist/test/engine-identity.test.js +119 -0
  87. package/dist/test/index.test.js +5 -6
  88. package/dist/test/lance-modal.test.js +7 -7
  89. package/dist/test/modal-config.test.js +51 -29
  90. package/dist/test/personalize.test.js +212 -0
  91. package/dist/test/rest-queue.test.js +1 -1
  92. package/dist/test/rest-setup.test.js +21 -21
  93. package/dist/test/{rest-wiki.test.js → rest-vm.test.js} +27 -23
  94. package/dist/test/server-ws.test.js +4 -4
  95. package/dist/test/sync.test.js +2 -2
  96. package/dist/test/tombstone.test.js +1 -1
  97. package/dist/test/vault-tools.test.js +224 -0
  98. package/package.json +2 -1
  99. package/scripts/check-deps.sh +207 -0
  100. package/scripts/e2e-ws.mjs +4 -4
  101. package/scripts/modal-e2e-smoke.mjs +1 -1
  102. package/scripts/setup-test-config.sh +1 -5
  103. package/scripts/test-slash-commands.mjs +197 -0
  104. package/skills/.agents/broadcaster.agent.md +31 -0
  105. package/skills/.agents/heavy-lifter.agent.md +31 -0
  106. package/skills/.agents/manager.agent.md +30 -0
  107. package/skills/.agents/miner.agent.md +30 -0
  108. package/skills/vault-mind/SKILL.md +50 -50
  109. package/skills/vault-mind/references/tool-reference.md +25 -25
  110. package/skills/vault-mind-broadcaster/SKILL.md +1 -1
  111. package/skills/vault-mind-heavy-lifter/SKILL.md +3 -3
  112. package/skills/vault-mind-manager/SKILL.md +7 -7
  113. package/skills/vault-mind-miner/SKILL.md +11 -11
  114. package/skills/vault-mind-setup/SKILL.md +65 -265
  115. package/skills/vault-mind-setup/references/obsidian-vault-structure.md +6 -6
  116. package/skills/vault-mind-setup/references/pi-extension-wiring.md +24 -33
  117. package/skills/vault-mind-setup/references/troubleshooting-tree.md +25 -26
  118. package/dist/src/auto-index.d.ts +0 -47
  119. package/dist/src/auto-index.js +0 -122
@@ -0,0 +1,19 @@
1
+ /**
2
+ * MinerAgent — knowledge extraction and storage agent.
3
+ *
4
+ * Allowed tools: read, write, edit, grep, find, ls, vm_search, vm_fts_search, vm_append
5
+ * Write collections: main, research
6
+ * Cannot publish (vm_sync disabled).
7
+ */
8
+ import { Agent, type AgentConfig } from "../agent-bus/Agent.js";
9
+ import type { LLMProvider } from "../agent-bus/LLMProvider.js";
10
+ import type { MessageBus } from "../agent-bus/MessageBus.js";
11
+ import type { TaskQueue } from "../agent-bus/TaskQueue.js";
12
+ export declare class MinerAgent extends Agent {
13
+ private taskQueue;
14
+ constructor(config: AgentConfig, bus: MessageBus, llm?: LLMProvider);
15
+ setTaskQueue(taskQueue: TaskQueue): void;
16
+ private onJobAvailable;
17
+ private executeJob;
18
+ tick(): Promise<void>;
19
+ }
@@ -0,0 +1,70 @@
1
+ /**
2
+ * MinerAgent — knowledge extraction and storage agent.
3
+ *
4
+ * Allowed tools: read, write, edit, grep, find, ls, vm_search, vm_fts_search, vm_append
5
+ * Write collections: main, research
6
+ * Cannot publish (vm_sync disabled).
7
+ */
8
+ import { Agent } from "../agent-bus/Agent.js";
9
+ export class MinerAgent extends Agent {
10
+ taskQueue = null;
11
+ constructor(config, bus, llm) {
12
+ super(config, bus, llm);
13
+ this.bus.subscribe("JOB_AVAILABLE", this.onJobAvailable.bind(this));
14
+ }
15
+ setTaskQueue(taskQueue) {
16
+ this.taskQueue = taskQueue;
17
+ }
18
+ async onJobAvailable(message) {
19
+ if (!this.taskQueue)
20
+ return;
21
+ const job = message.payload;
22
+ if (job.role !== "miner")
23
+ return;
24
+ const taskId = this.taskQueue.add("agent-dispatch", job);
25
+ if (!this.taskQueue.claim(taskId, this.config.id))
26
+ return;
27
+ try {
28
+ await this.executeJob(job);
29
+ this.taskQueue.complete(taskId, this.config.id, { dispatchId: job.dispatchId });
30
+ await this.broadcast("JOB_COMPLETED", {
31
+ dispatchId: job.dispatchId,
32
+ agentId: this.config.id,
33
+ status: "done",
34
+ });
35
+ }
36
+ catch (err) {
37
+ const msg = err instanceof Error ? err.message : String(err);
38
+ this.taskQueue.fail(taskId, this.config.id, msg);
39
+ await this.broadcast("JOB_COMPLETED", {
40
+ dispatchId: job.dispatchId,
41
+ agentId: this.config.id,
42
+ status: "failed",
43
+ error: msg,
44
+ });
45
+ }
46
+ }
47
+ async executeJob(job) {
48
+ await this.invokePiTool("read", { path: job.filePath });
49
+ if (this.llm) {
50
+ const plan = await this.askLLM(`You are the Miner agent. Task: ${job.instruction}\n\nSource file: ${job.filePath}\n\nExtract entities and relationships, then store them via vm_append. Plan the tool calls needed. Return JSON: { "tools": [{"name": "...", "args": {...}}] }`);
51
+ if (plan?.tools) {
52
+ for (const tool of plan.tools) {
53
+ await this.invokePiTool(tool.name, tool.args);
54
+ }
55
+ }
56
+ }
57
+ await this.invokePiTool("edit", {
58
+ path: job.filePath,
59
+ edits: [
60
+ {
61
+ oldText: `<!-- pi-dispatch:${job.dispatchId} -->`,
62
+ newText: `> Miner extracted findings. See dispatch ${job.dispatchId}.`,
63
+ },
64
+ ],
65
+ });
66
+ }
67
+ async tick() {
68
+ // Event-driven — work arrives via JOB_AVAILABLE subscription
69
+ }
70
+ }
@@ -0,0 +1,4 @@
1
+ export { HeavyLifterAgent, type DispatchJob } from "./HeavyLifterAgent.js";
2
+ export { MinerAgent } from "./MinerAgent.js";
3
+ export { BroadcasterAgent } from "./BroadcasterAgent.js";
4
+ export { ManagerAgent } from "./ManagerAgent.js";
@@ -0,0 +1,4 @@
1
+ export { HeavyLifterAgent } from "./HeavyLifterAgent.js";
2
+ export { MinerAgent } from "./MinerAgent.js";
3
+ export { BroadcasterAgent } from "./BroadcasterAgent.js";
4
+ export { ManagerAgent } from "./ManagerAgent.js";
@@ -4,7 +4,7 @@
4
4
  * Token resolution order:
5
5
  * 1. `process.env.PVM_API_TOKEN`
6
6
  * 2. `<homedir>/.pi/agent/vault-mind.env` (key=value, # comments)
7
- * 3. `wiki.embedding.modal.apiToken` in the loaded config
7
+ * 3. `vaultMind.embedding.modal.apiToken` in the loaded config
8
8
  *
9
9
  * Returns `null` if no token is configured (auth is off until set).
10
10
  *
@@ -15,6 +15,21 @@
15
15
  import type { UniversalConfig } from "./types.js";
16
16
  /** Resolve the bearer token. `cfg` is optional; pass the loaded config to fall back to it. */
17
17
  export declare const resolveAuthToken: (cfg?: UniversalConfig) => string | null;
18
+ /** Resolved identity for a per-role bearer token. */
19
+ export interface TokenIdentity {
20
+ token: string;
21
+ /** Lowercase role name, e.g. "manager", "miner". */
22
+ identity: string;
23
+ /** Same as identity; reserved for future role-vs-identity distinction. */
24
+ role: string;
25
+ }
26
+ /**
27
+ * Scan `PVM_TOKEN_<ROLE>` env vars and return the identity whose token
28
+ * matches `token` via constant-time compare. Returns `undefined` when
29
+ * no per-role token matches (caller should fall back to the legacy
30
+ * `PVM_API_TOKEN` path).
31
+ */
32
+ export declare const resolveTokenIdentity: (token: string) => TokenIdentity | undefined;
18
33
  export interface IncomingLike {
19
34
  headers: {
20
35
  authorization?: string | string[] | undefined;
@@ -23,8 +38,40 @@ export interface IncomingLike {
23
38
  export interface AuthResult {
24
39
  ok: boolean;
25
40
  reason?: "missing" | "malformed" | "mismatch";
41
+ /** Resolved per-role identity when a `PVM_TOKEN_*` token matched. */
42
+ identity?: TokenIdentity;
26
43
  }
27
- /** Authorize a request against the resolved token. Returns `ok: true` if auth is off (no token configured). */
44
+ /**
45
+ * Authorize a request against configured tokens.
46
+ *
47
+ * Resolution order:
48
+ * 1. Per-role `PVM_TOKEN_<ROLE>` env vars — returns the matched identity.
49
+ * 2. Legacy `PVM_API_TOKEN` — returns `ok: true` with no identity (full access).
50
+ * 3. No token configured → `ok: true` (auth is off).
51
+ */
28
52
  export declare const authoriseRequest: (req: IncomingLike, cfg?: UniversalConfig) => AuthResult;
29
53
  /** Test-only: forget the cached token so tests get a fresh resolve on each call. */
30
54
  export declare const _resetAuthCache: () => void;
55
+ /** Set an in-memory token override for the current process. Invalidates any cached value. */
56
+ export declare const setToken: (value: string) => void;
57
+ /** Clear the in-memory token cache. Equivalent to `_resetAuthCache` but with a public name. */
58
+ export declare const clearToken: () => void;
59
+ /**
60
+ * Revoke a role-specific bearer token by removing `PVM_TOKEN_<IDENTITY>`
61
+ * from `~/.pi/agent/vault-mind.env`. Preserves all other lines and comments.
62
+ * Returns `true` if the variable was found and removed.
63
+ */
64
+ export declare const revokeToken: (identity: string) => boolean;
65
+ /**
66
+ * Generate a fresh 32-byte base64url token, persist it to
67
+ * `<os.homedir()>/.pi/agent/vault-mind.env` (replacing any existing
68
+ * `PVM_API_TOKEN` line while preserving all other lines and comments), create
69
+ * the directory + file if missing, apply `chmod 600` (best-effort; no-op on
70
+ * win32), update the in-process cache, and return the new token.
71
+ *
72
+ * **Env-var precedence note:** if `PVM_API_TOKEN` is exported as an environment
73
+ * variable it takes precedence over the dotenv file at resolution time, so the
74
+ * rotated token only becomes active once the env override is unset and the
75
+ * process is restarted (or the in-process cache is refreshed via `setToken`).
76
+ */
77
+ export declare const rotateToken: () => string;
package/dist/src/auth.js CHANGED
@@ -4,7 +4,7 @@
4
4
  * Token resolution order:
5
5
  * 1. `process.env.PVM_API_TOKEN`
6
6
  * 2. `<homedir>/.pi/agent/vault-mind.env` (key=value, # comments)
7
- * 3. `wiki.embedding.modal.apiToken` in the loaded config
7
+ * 3. `vaultMind.embedding.modal.apiToken` in the loaded config
8
8
  *
9
9
  * Returns `null` if no token is configured (auth is off until set).
10
10
  *
@@ -12,6 +12,7 @@
12
12
  * dotenv file is the caller's responsibility and is a no-op on
13
13
  * win32 (the file system doesn't honor POSIX modes there).
14
14
  */
15
+ import * as crypto from "node:crypto";
15
16
  import * as fs from "node:fs";
16
17
  import * as os from "node:os";
17
18
  import * as path from "node:path";
@@ -41,7 +42,7 @@ const readDotenvToken = () => {
41
42
  return null;
42
43
  };
43
44
  const readConfigToken = (cfg) => {
44
- return cfg?.wiki?.embedding?.modal?.apiToken;
45
+ return cfg?.vaultMind?.embedding?.modal?.apiToken;
45
46
  };
46
47
  /** Resolve the bearer token. `cfg` is optional; pass the loaded config to fall back to it. */
47
48
  export const resolveAuthToken = (cfg) => {
@@ -61,6 +62,32 @@ export const resolveAuthToken = (cfg) => {
61
62
  }
62
63
  return null;
63
64
  };
65
+ /**
66
+ * Scan `PVM_TOKEN_<ROLE>` env vars and return the identity whose token
67
+ * matches `token` via constant-time compare. Returns `undefined` when
68
+ * no per-role token matches (caller should fall back to the legacy
69
+ * `PVM_API_TOKEN` path).
70
+ */
71
+ export const resolveTokenIdentity = (token) => {
72
+ for (const [key, value] of Object.entries(process.env)) {
73
+ const m = key.match(/^PVM_TOKEN_([A-Z][A-Z0-9_]*)$/);
74
+ if (!m || !value)
75
+ continue;
76
+ if (safeEqual(value, token)) {
77
+ const role = m[1].toLowerCase();
78
+ return { token: value, identity: role, role };
79
+ }
80
+ }
81
+ return undefined;
82
+ };
83
+ /** True when any PVM_TOKEN_<ROLE> env var is set. */
84
+ const hasAnyPerRoleToken = () => {
85
+ for (const key of Object.keys(process.env)) {
86
+ if (/^PVM_TOKEN_[A-Z][A-Z0-9_]*$/.test(key))
87
+ return true;
88
+ }
89
+ return false;
90
+ };
64
91
  /** Constant-time compare to avoid timing attacks on the bearer token. */
65
92
  const safeEqual = (a, b) => {
66
93
  if (a.length !== b.length)
@@ -71,19 +98,40 @@ const safeEqual = (a, b) => {
71
98
  }
72
99
  return mismatch === 0;
73
100
  };
74
- /** Authorize a request against the resolved token. Returns `ok: true` if auth is off (no token configured). */
101
+ /**
102
+ * Authorize a request against configured tokens.
103
+ *
104
+ * Resolution order:
105
+ * 1. Per-role `PVM_TOKEN_<ROLE>` env vars — returns the matched identity.
106
+ * 2. Legacy `PVM_API_TOKEN` — returns `ok: true` with no identity (full access).
107
+ * 3. No token configured → `ok: true` (auth is off).
108
+ */
75
109
  export const authoriseRequest = (req, cfg) => {
76
- const expected = resolveAuthToken(cfg);
77
- if (!expected)
78
- return { ok: true };
79
110
  const raw = req.headers.authorization;
80
111
  const header = Array.isArray(raw) ? raw[0] : raw;
112
+ // Auth off: no tokens configured at all → allow everything
113
+ const hasPerRole = resolveTokenIdentity("") !== undefined || true; // check env vars
114
+ const hasLegacy = resolveAuthToken(cfg) !== null;
115
+ if (!hasLegacy && !hasAnyPerRoleToken()) {
116
+ // No tokens configured — auth is off, allow all requests
117
+ if (!header)
118
+ return { ok: true };
119
+ }
81
120
  if (!header)
82
121
  return { ok: false, reason: "missing" };
83
122
  const m = header.match(/^Bearer\s+(.+)$/i);
84
123
  if (!m)
85
124
  return { ok: false, reason: "malformed" };
86
- if (!safeEqual(m[1], expected))
125
+ const token = m[1];
126
+ // 1. Per-role token match
127
+ const identity = resolveTokenIdentity(token);
128
+ if (identity)
129
+ return { ok: true, identity };
130
+ // 2. Legacy single-token fallback
131
+ const expected = resolveAuthToken(cfg);
132
+ if (!expected)
133
+ return { ok: true }; // auth off — no tokens configured
134
+ if (!safeEqual(token, expected))
87
135
  return { ok: false, reason: "mismatch" };
88
136
  return { ok: true };
89
137
  };
@@ -91,3 +139,97 @@ export const authoriseRequest = (req, cfg) => {
91
139
  export const _resetAuthCache = () => {
92
140
  cached = null;
93
141
  };
142
+ /** Set an in-memory token override for the current process. Invalidates any cached value. */
143
+ export const setToken = (value) => {
144
+ cached = { value };
145
+ };
146
+ /** Clear the in-memory token cache. Equivalent to `_resetAuthCache` but with a public name. */
147
+ export const clearToken = () => {
148
+ cached = null;
149
+ };
150
+ /**
151
+ * Revoke a role-specific bearer token by removing `PVM_TOKEN_<IDENTITY>`
152
+ * from `~/.pi/agent/vault-mind.env`. Preserves all other lines and comments.
153
+ * Returns `true` if the variable was found and removed.
154
+ */
155
+ export const revokeToken = (identity) => {
156
+ const varName = `PVM_TOKEN_${identity.toUpperCase()}`;
157
+ const dotenvPath = path.join(os.homedir(), DOTENV_REL);
158
+ let lines = [];
159
+ try {
160
+ lines = fs.readFileSync(dotenvPath, "utf-8").split(/\r?\n/);
161
+ }
162
+ catch {
163
+ return false;
164
+ }
165
+ const lineRe = /^\s*([A-Z_][A-Z0-9_]*)\s*=\s*("([^"]*)"|'([^']*)'|[^\s#]+)/;
166
+ let removed = false;
167
+ const updated = [];
168
+ for (const line of lines) {
169
+ const m = line.match(lineRe);
170
+ if (!removed && m && m[1] === varName) {
171
+ removed = true;
172
+ continue;
173
+ }
174
+ updated.push(line);
175
+ }
176
+ if (!removed)
177
+ return false;
178
+ let content = updated.join("\n");
179
+ if (!content.endsWith("\n"))
180
+ content += "\n";
181
+ fs.writeFileSync(dotenvPath, content, "utf-8");
182
+ return true;
183
+ };
184
+ /**
185
+ * Generate a fresh 32-byte base64url token, persist it to
186
+ * `<os.homedir()>/.pi/agent/vault-mind.env` (replacing any existing
187
+ * `PVM_API_TOKEN` line while preserving all other lines and comments), create
188
+ * the directory + file if missing, apply `chmod 600` (best-effort; no-op on
189
+ * win32), update the in-process cache, and return the new token.
190
+ *
191
+ * **Env-var precedence note:** if `PVM_API_TOKEN` is exported as an environment
192
+ * variable it takes precedence over the dotenv file at resolution time, so the
193
+ * rotated token only becomes active once the env override is unset and the
194
+ * process is restarted (or the in-process cache is refreshed via `setToken`).
195
+ */
196
+ export const rotateToken = () => {
197
+ const newToken = crypto.randomBytes(32).toString("base64url");
198
+ const dotenvPath = path.join(os.homedir(), DOTENV_REL);
199
+ const dir = path.dirname(dotenvPath);
200
+ if (!fs.existsSync(dir))
201
+ fs.mkdirSync(dir, { recursive: true });
202
+ let existingLines = [];
203
+ try {
204
+ existingLines = fs.readFileSync(dotenvPath, "utf-8").split(/\r?\n/);
205
+ }
206
+ catch {
207
+ // file missing — start fresh
208
+ }
209
+ const tokenLine = `PVM_API_TOKEN="${newToken}"`;
210
+ let replaced = false;
211
+ const updatedLines = [];
212
+ for (const line of existingLines) {
213
+ if (!replaced && /^\s*PVM_API_TOKEN\s*=/.test(line)) {
214
+ updatedLines.push(tokenLine);
215
+ replaced = true;
216
+ }
217
+ else {
218
+ updatedLines.push(line);
219
+ }
220
+ }
221
+ if (!replaced)
222
+ updatedLines.push(tokenLine);
223
+ let content = updatedLines.join("\n");
224
+ if (!content.endsWith("\n"))
225
+ content += "\n";
226
+ fs.writeFileSync(dotenvPath, content, "utf-8");
227
+ try {
228
+ fs.chmodSync(dotenvPath, 0o600);
229
+ }
230
+ catch {
231
+ // no-op on win32 or when chmod is unavailable
232
+ }
233
+ cached = { value: newToken };
234
+ return newToken;
235
+ };
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Optional background vector sync (off by default).
3
3
  *
4
- * When `wiki.embedding.modal.sync.autoSync` is true, a background interval pulls
4
+ * When `vaultMind.embedding.modal.sync.autoSync` is true, a background interval pulls
5
5
  * new server-side vectors into the local LanceDB on a configurable cadence so
6
6
  * search keeps working with the latest embeddings. It is guarded by config and
7
7
  * safe to leave disabled — if Modal is not configured or auto-sync is off, the
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Optional background vector sync (off by default).
3
3
  *
4
- * When `wiki.embedding.modal.sync.autoSync` is true, a background interval pulls
4
+ * When `vaultMind.embedding.modal.sync.autoSync` is true, a background interval pulls
5
5
  * new server-side vectors into the local LanceDB on a configurable cadence so
6
6
  * search keeps working with the latest embeddings. It is guarded by config and
7
7
  * safe to leave disabled — if Modal is not configured or auto-sync is off, the
@@ -20,10 +20,10 @@ export const startAutoSync = (intervalMs = 300_000) => {
20
20
  const tick = async () => {
21
21
  try {
22
22
  const cfg = loadConfig(process.cwd());
23
- const sync = cfg.wiki.embedding.modal?.sync;
23
+ const sync = cfg.vaultMind.embedding.modal?.sync;
24
24
  if (!sync?.autoSync)
25
25
  return; // off by default
26
- if (!isModalConfigured(cfg.wiki))
26
+ if (!isModalConfigured(cfg.vaultMind))
27
27
  return; // nothing to sync
28
28
  const ms = sync.autoSyncIntervalMs ?? intervalMs;
29
29
  void ms; // cadence is fixed at start; tick honors config toggle
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Embedded pi-agent-bus bridge for pi-vault-mind.
3
+ *
4
+ * Adapted from pi-agent-bus-bridge/index.ts. Provides:
5
+ * - Per-agent tool allowlist enforcement (not global exposedPiTools)
6
+ * - Synchronous execution of both built-in SDK tools and vm_* extension tools
7
+ * - Tool cache scoped by agent working directory
8
+ * - MessageBus integration for agent→tool→agent communication
9
+ *
10
+ * Vendored from kylebrodeur/pi-agent-bus (MIT).
11
+ */
12
+ import { type ExtensionAPI } from "@earendil-works/pi-coding-agent";
13
+ import type { MessageBus } from "./agent-bus/MessageBus.js";
14
+ export interface AgentIdentity {
15
+ id: string;
16
+ role: string;
17
+ allowedTools: string[];
18
+ writeCollections: string[];
19
+ canPublish: boolean;
20
+ workingDir?: string;
21
+ /** Vault-relative path prefixes this identity may write/edit files within. Empty or absent = no restriction. */
22
+ allowedVaultPaths?: string[];
23
+ /** Allowed outbound hosts for bash egress. Undefined/empty = allow all (backwards compat). */
24
+ egressAllowlist?: string[];
25
+ /** Visibility for entries written by this identity. "vault" = private to this vault, "public" = visible to all. Defaults to "vault". */
26
+ visibility?: string;
27
+ /** Collections this role may read from. Empty or absent = no restriction. */
28
+ readCollections?: string[];
29
+ }
30
+ /** Register an agent's identity for tool gating. Called by the engine after loading agents. */
31
+ export declare function registerAgentIdentity(identity: AgentIdentity): void;
32
+ /** Remove an agent's identity. */
33
+ export declare function unregisterAgentIdentity(agentId: string): void;
34
+ /** Check whether a tool is allowed for a given agent. */
35
+ export declare function isToolAllowed(agentId: string, toolName: string): boolean;
36
+ /** Get an agent's identity. */
37
+ export declare function getAgentIdentity(agentId: string): AgentIdentity | undefined;
38
+ export declare function createBridge(pi: ExtensionAPI, bus: MessageBus): void;
39
+ type ExtensionToolHandler = (args: Record<string, unknown>, identity: AgentIdentity | undefined, isAutopilot?: boolean) => Promise<unknown>;
40
+ /**
41
+ * Register a handler for an extension tool (vm_*).
42
+ * Call this in index.ts after registering tools with pi.
43
+ */
44
+ export declare function registerExtensionToolHandler(toolName: string, handler: ExtensionToolHandler): void;
45
+ export {};
@@ -0,0 +1,189 @@
1
+ /**
2
+ * Embedded pi-agent-bus bridge for pi-vault-mind.
3
+ *
4
+ * Adapted from pi-agent-bus-bridge/index.ts. Provides:
5
+ * - Per-agent tool allowlist enforcement (not global exposedPiTools)
6
+ * - Synchronous execution of both built-in SDK tools and vm_* extension tools
7
+ * - Tool cache scoped by agent working directory
8
+ * - MessageBus integration for agent→tool→agent communication
9
+ *
10
+ * Vendored from kylebrodeur/pi-agent-bus (MIT).
11
+ */
12
+ import * as path from "node:path";
13
+ import { createBashTool, createEditTool, createFindTool, createGrepTool, createLsTool, createReadTool, createWriteTool, } from "@earendil-works/pi-coding-agent";
14
+ // ── Path-scope helpers ───────────────────────────────────────────────────────
15
+ /**
16
+ * Return true if `filePath` (absolute or relative to `cwd`) is within at
17
+ * least one of the `allowedPrefixes`. Handles `..` escapes via path.resolve.
18
+ */
19
+ function isPathWithinAllowed(filePath, cwd, allowedPrefixes) {
20
+ const resolved = path.resolve(cwd, filePath);
21
+ return allowedPrefixes.some((prefix) => {
22
+ const base = path.resolve(cwd, prefix);
23
+ return resolved === base || resolved.startsWith(base + path.sep);
24
+ });
25
+ }
26
+ // ── State ────────────────────────────────────────────────────────────────────
27
+ const toolCache = new Map();
28
+ const agentIdentities = new Map();
29
+ // ── Tool factories ───────────────────────────────────────────────────────────
30
+ function createBuiltInTools(cwd) {
31
+ return {
32
+ read: createReadTool(cwd),
33
+ bash: createBashTool(cwd),
34
+ edit: createEditTool(cwd),
35
+ write: createWriteTool(cwd),
36
+ find: createFindTool(cwd),
37
+ grep: createGrepTool(cwd),
38
+ ls: createLsTool(cwd),
39
+ };
40
+ }
41
+ function getBuiltInTools(cwd) {
42
+ let tools = toolCache.get(cwd);
43
+ if (!tools) {
44
+ tools = createBuiltInTools(cwd);
45
+ toolCache.set(cwd, tools);
46
+ }
47
+ return tools;
48
+ }
49
+ // ── Agent identity registry ──────────────────────────────────────────────────
50
+ /** Register an agent's identity for tool gating. Called by the engine after loading agents. */
51
+ export function registerAgentIdentity(identity) {
52
+ agentIdentities.set(identity.id, identity);
53
+ }
54
+ /** Remove an agent's identity. */
55
+ export function unregisterAgentIdentity(agentId) {
56
+ agentIdentities.delete(agentId);
57
+ }
58
+ /** Check whether a tool is allowed for a given agent. */
59
+ export function isToolAllowed(agentId, toolName) {
60
+ const identity = agentIdentities.get(agentId);
61
+ if (!identity)
62
+ return false;
63
+ return identity.allowedTools.includes(toolName);
64
+ }
65
+ /** Get an agent's identity. */
66
+ export function getAgentIdentity(agentId) {
67
+ return agentIdentities.get(agentId);
68
+ }
69
+ // ── Bridge ───────────────────────────────────────────────────────────────────
70
+ /**
71
+ * Create the tool bridge on a MessageBus.
72
+ *
73
+ * Subscribes to `pi_tool_bridge_requests`, validates against per-agent
74
+ * allowlists, executes tools synchronously, and publishes results back.
75
+ *
76
+ * Extension tools (vm_*) are executed via a handler registry — call
77
+ * `registerExtensionToolHandler()` to wire them up.
78
+ */
79
+ // ── Egress host parsing ──────────────────────────────────────────────────────
80
+ /**
81
+ * Extract outbound hostnames from a bash command string.
82
+ * Handles URLs (http/https/ftp), git SSH-style (git@host:path),
83
+ * and scp-style (user@host:path) patterns.
84
+ * Returns unique, lowercased hostnames.
85
+ */
86
+ function parseHostsFromCommand(command) {
87
+ const hosts = new Set();
88
+ // Standard URLs: scheme://host[:port][/path]
89
+ const urlRe = /[a-zA-Z][a-zA-Z0-9+.-]*:\/\/([^/\s"'`$<>|&;()]+)/g;
90
+ for (const m of command.matchAll(urlRe)) {
91
+ // Strip trailing port if present
92
+ const host = m[1].replace(/:\d+$/, "");
93
+ if (host.length > 0)
94
+ hosts.add(host.toLowerCase());
95
+ }
96
+ // Git SSH-style: git@host:path
97
+ const gitSshRe = /git@([a-zA-Z0-9][a-zA-Z0-9.-]*[a-zA-Z0-9]):/g;
98
+ for (const m of command.matchAll(gitSshRe)) {
99
+ hosts.add(m[1].toLowerCase());
100
+ }
101
+ // scp-style: user@host:path (not email addresses — requires path-like after colon)
102
+ const scpRe = /(?:^|\s)([a-zA-Z0-9._%+-]+)@([a-zA-Z0-9][a-zA-Z0-9.-]*[a-zA-Z0-9]):(?:\/|~|[a-zA-Z0-9])/g;
103
+ for (const m of command.matchAll(scpRe)) {
104
+ hosts.add(m[2].toLowerCase());
105
+ }
106
+ return [...hosts];
107
+ }
108
+ export function createBridge(pi, bus) {
109
+ bus.subscribe("pi_tool_bridge_requests", async (message) => {
110
+ const payload = message.payload;
111
+ const { requestId, agentId, toolName, args, responseTopic } = payload;
112
+ let result;
113
+ let error;
114
+ // 1. Check per-agent allowlist
115
+ if (!isToolAllowed(agentId, toolName)) {
116
+ error = `Tool '${toolName}' is not allowed for agent '${agentId}'.`;
117
+ }
118
+ else {
119
+ try {
120
+ const identity = agentIdentities.get(agentId);
121
+ const cwd = identity?.workingDir ?? process.cwd();
122
+ // 2. Try built-in SDK tools first
123
+ const builtInTools = getBuiltInTools(cwd);
124
+ const builtIn = builtInTools[toolName];
125
+ if (builtIn) {
126
+ // 2a. Enforce writable-path scope for write/edit tools.
127
+ // read/bash/grep/find/ls are unaffected.
128
+ if ((toolName === "write" || toolName === "edit") &&
129
+ identity?.allowedVaultPaths &&
130
+ identity.allowedVaultPaths.length > 0) {
131
+ const targetPath = typeof args.path === "string" ? args.path : undefined;
132
+ if (!targetPath || !isPathWithinAllowed(targetPath, cwd, identity.allowedVaultPaths)) {
133
+ error = `Write to '${targetPath ?? "(no path)"}' denied for agent '${agentId}': path is not within allowed vault paths [${identity.allowedVaultPaths.join(", ")}].`;
134
+ }
135
+ }
136
+ // 2b. Enforce egress allowlist for Heavy-Lifter bash calls.
137
+ if (!error &&
138
+ toolName === "bash" &&
139
+ identity?.role === "heavy-lifter" &&
140
+ identity?.egressAllowlist &&
141
+ identity.egressAllowlist.length > 0) {
142
+ const command = typeof args.command === "string" ? args.command : "";
143
+ const hosts = parseHostsFromCommand(command);
144
+ for (const host of hosts) {
145
+ if (!identity.egressAllowlist.includes(host)) {
146
+ error = `Heavy-Lifter cannot reach '${host}'. Allowed: ${identity.egressAllowlist.join(", ")}`;
147
+ break;
148
+ }
149
+ }
150
+ }
151
+ // 2c. Gate Heavy-Lifter bash for autopilot forks (destructive operation).
152
+ if (!error && toolName === "bash" && identity?.role === "heavy-lifter") {
153
+ error =
154
+ "This action requires approval. Reason: autopilot forks cannot perform destructive operations without human authorization.";
155
+ }
156
+ if (!error) {
157
+ result = await builtIn.execute(`bridge_${requestId}`, args, new AbortController().signal, () => { });
158
+ }
159
+ }
160
+ else {
161
+ // 3. Try extension tool handlers
162
+ const handler = extensionToolHandlers.get(toolName);
163
+ if (handler) {
164
+ result = await handler(args, identity, true);
165
+ }
166
+ else {
167
+ // 4. Fallback: queue as user message (for tools we haven't wired yet)
168
+ pi.sendUserMessage(`Please run tool ${toolName} with args: ${JSON.stringify(args)}`, {
169
+ deliverAs: "followUp",
170
+ });
171
+ result = { queued: true };
172
+ }
173
+ }
174
+ }
175
+ catch (e) {
176
+ error = e instanceof Error ? e.message : String(e);
177
+ }
178
+ }
179
+ await bus.publish(responseTopic, "pi-tool-bridge", { requestId, result, error });
180
+ });
181
+ }
182
+ const extensionToolHandlers = new Map();
183
+ /**
184
+ * Register a handler for an extension tool (vm_*).
185
+ * Call this in index.ts after registering tools with pi.
186
+ */
187
+ export function registerExtensionToolHandler(toolName, handler) {
188
+ extensionToolHandlers.set(toolName, handler);
189
+ }
@@ -21,7 +21,7 @@ export declare const planGitignore: (existing: string | null) => {
21
21
  content: string;
22
22
  };
23
23
  export declare const watcherState: import("./watcher.js").WatcherState;
24
- /** Module-level server state, referenced by the /wiki server command.
24
+ /** Module-level server state, referenced by the /vm server command.
25
25
  * Port is set when the server starts in index.ts. */
26
26
  export declare const serverState: ServerState;
27
27
  export declare const registerCommands: (pi: ExtensionAPI) => void;