pi-vault-mind 0.7.0

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 (46) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +428 -0
  3. package/dist/index.d.ts +1 -0
  4. package/dist/index.js +1 -0
  5. package/dist/src/commands.d.ts +9 -0
  6. package/dist/src/commands.js +813 -0
  7. package/dist/src/events.d.ts +13 -0
  8. package/dist/src/events.js +236 -0
  9. package/dist/src/graph.d.ts +3 -0
  10. package/dist/src/graph.js +234 -0
  11. package/dist/src/index.d.ts +2 -0
  12. package/dist/src/index.js +61 -0
  13. package/dist/src/lance.d.ts +40 -0
  14. package/dist/src/lance.js +409 -0
  15. package/dist/src/server.d.ts +25 -0
  16. package/dist/src/server.js +180 -0
  17. package/dist/src/settings-ui.d.ts +9 -0
  18. package/dist/src/settings-ui.js +313 -0
  19. package/dist/src/state.d.ts +2 -0
  20. package/dist/src/state.js +16 -0
  21. package/dist/src/tools.d.ts +2 -0
  22. package/dist/src/tools.js +772 -0
  23. package/dist/src/types.d.ts +103 -0
  24. package/dist/src/types.js +51 -0
  25. package/dist/src/utils.d.ts +17 -0
  26. package/dist/src/utils.js +102 -0
  27. package/dist/src/vault-writer.d.ts +17 -0
  28. package/dist/src/vault-writer.js +141 -0
  29. package/dist/src/watcher.d.ts +91 -0
  30. package/dist/src/watcher.js +411 -0
  31. package/dist/src/widget.d.ts +3 -0
  32. package/dist/src/widget.js +12 -0
  33. package/dist/test/index.test.d.ts +1 -0
  34. package/dist/test/index.test.js +368 -0
  35. package/package.json +83 -0
  36. package/skills/vault-mind/SKILL.md +260 -0
  37. package/skills/vault-mind/references/tool-reference.md +53 -0
  38. package/skills/vault-mind-broadcaster/SKILL.md +112 -0
  39. package/skills/vault-mind-heavy-lifter/SKILL.md +34 -0
  40. package/skills/vault-mind-manager/SKILL.md +35 -0
  41. package/skills/vault-mind-miner/SKILL.md +40 -0
  42. package/skills/vault-mind-setup/SKILL.md +385 -0
  43. package/skills/vault-mind-setup/references/obsidian-cli-and-plugins.md +269 -0
  44. package/skills/vault-mind-setup/references/obsidian-vault-structure.md +106 -0
  45. package/skills/vault-mind-setup/references/pi-extension-wiring.md +236 -0
  46. package/skills/vault-mind-setup/references/troubleshooting-tree.md +147 -0
@@ -0,0 +1,411 @@
1
+ /**
2
+ * Vault Watcher — passive file-system observer for @agent markers.
3
+ *
4
+ * Inspired by pi-piqo but redesigned for the pi-vault-mind multi-agent
5
+ * architecture. Instead of sending prompts directly to the LLM, the
6
+ * Watcher groups markers by target agent role and dispatches them as
7
+ * well-structured subagent tasks into the active pi session. The
8
+ * Manager agent then uses the standard subagent() tool to fork
9
+ * isolated worker sessions.
10
+ *
11
+ * Key differences from pi-piqo:
12
+ * - Multi-role grouping (one bundled task per agent per file)
13
+ * - Named IDs force isolation (e.g., @agent-Miner:customId)
14
+ * - Concurrency limits prevent explosion of subagent processes
15
+ * - Debounced watching (1000ms per file) groups rapid saves
16
+ */
17
+ import * as fs from "node:fs";
18
+ import * as path from "node:path";
19
+ // ── Constants ────────────────────────────────────────────────────────────────────
20
+ const MARKER_REGEX = /@agent-(\w+)(?::(\S+))?\s*(.*)/;
21
+ const WRITEBACK_MARKER_REGEX = /<!--\s*pi-dispatch:(\S+)\s*-->/;
22
+ const DEBOUNCE_MS = 1000;
23
+ const MAX_CONCURRENT = 3;
24
+ const CONTEXT_LINES = 15;
25
+ const WATCHED_EXTENSIONS = new Set([
26
+ ".txt",
27
+ ".md",
28
+ ".markdown",
29
+ ".mdx",
30
+ ".js",
31
+ ".ts",
32
+ ".jsx",
33
+ ".tsx",
34
+ ".py",
35
+ ".rb",
36
+ ".rs",
37
+ ".go",
38
+ ".java",
39
+ ".c",
40
+ ".cpp",
41
+ ".h",
42
+ ".hpp",
43
+ ".css",
44
+ ".html",
45
+ ".xml",
46
+ ".json",
47
+ ".yaml",
48
+ ".yml",
49
+ ".toml",
50
+ ".sh",
51
+ ".bash",
52
+ ".zsh",
53
+ ".sql",
54
+ ".r",
55
+ ".swift",
56
+ ".kt",
57
+ ".lua",
58
+ ".vim",
59
+ ".el",
60
+ ".clj",
61
+ ".hs",
62
+ ".ml",
63
+ ".ex",
64
+ ".exs",
65
+ ".erl",
66
+ ".dart",
67
+ ".cs",
68
+ ".php",
69
+ ".pl",
70
+ ".pm",
71
+ ".svelte",
72
+ ".vue",
73
+ ".astro",
74
+ ]);
75
+ const IGNORE_DIRS = new Set([
76
+ ".git",
77
+ ".obsidian",
78
+ "node_modules",
79
+ ".trash",
80
+ "__pycache__",
81
+ ".DS_Store",
82
+ ".pi",
83
+ ".lancedb",
84
+ ]);
85
+ // ── Helper Functions ─────────────────────────────────────────────────────────────
86
+ function isWatchableFile(filePath) {
87
+ const ext = path.extname(filePath).toLowerCase();
88
+ if (!WATCHED_EXTENSIONS.has(ext))
89
+ return false;
90
+ const dirParts = filePath.split(path.sep);
91
+ for (const part of dirParts) {
92
+ if (IGNORE_DIRS.has(part))
93
+ return false;
94
+ }
95
+ return true;
96
+ }
97
+ /**
98
+ * Scan a file for @agent markers and return grouped tasks.
99
+ * Exported for testing.
100
+ *
101
+ * Grouping strategy:
102
+ * 1. Same role markers in a single file are bundled into one task.
103
+ * 2. Different role markers create separate groups.
104
+ * 3. Named IDs (e.g., @agent-Miner:customId) force isolation regardless of role.
105
+ */
106
+ export function scanFile(filePath) {
107
+ let content;
108
+ try {
109
+ content = fs.readFileSync(filePath, "utf-8");
110
+ }
111
+ catch {
112
+ return [];
113
+ }
114
+ const lines = content.split("\n");
115
+ const markers = [];
116
+ for (let i = 0; i < lines.length; i++) {
117
+ const line = lines[i];
118
+ const match = line.match(MARKER_REGEX);
119
+ if (!match)
120
+ continue;
121
+ const role = match[1].toLowerCase();
122
+ const taskId = match[2] || undefined;
123
+ const instruction = match[3]?.trim() || "";
124
+ // Build context: surrounding lines
125
+ const contextStart = Math.max(0, i - CONTEXT_LINES);
126
+ const contextEnd = Math.min(lines.length, i + CONTEXT_LINES + 1);
127
+ const contextLines = lines.slice(contextStart, contextEnd);
128
+ const context = contextLines.map((l, idx) => `${contextStart + idx + 1}: ${l}`).join("\n");
129
+ markers.push({
130
+ rawLine: line,
131
+ lineNumber: i + 1,
132
+ role,
133
+ taskId,
134
+ instruction,
135
+ context,
136
+ });
137
+ }
138
+ if (markers.length === 0)
139
+ return [];
140
+ // Group markers by (role, taskId) pair
141
+ const groupMap = new Map();
142
+ for (const marker of markers) {
143
+ // Named IDs always get their own group; unnamed share by role
144
+ const groupKey = marker.taskId ? `${marker.role}:${marker.taskId}` : marker.role;
145
+ if (!groupMap.has(groupKey)) {
146
+ groupMap.set(groupKey, []);
147
+ }
148
+ groupMap.get(groupKey)?.push(marker);
149
+ }
150
+ const groups = [];
151
+ for (const [groupKey, groupMarkers] of groupMap.entries()) {
152
+ const firstMarker = groupMarkers[0];
153
+ const role = firstMarker.role;
154
+ const taskId = firstMarker.taskId || groupKey;
155
+ // Combine instructions
156
+ const instructions = groupMarkers
157
+ .map((m) => `@line ${m.lineNumber}: ${m.instruction}`)
158
+ .join("\n");
159
+ const combinedInstruction = [
160
+ `File: ${filePath}`,
161
+ `Role: ${role}`,
162
+ groupMarkers.length > 1 ? `Multiple markers (${groupMarkers.length}):` : "Single marker:",
163
+ "",
164
+ instructions,
165
+ "",
166
+ "Context:",
167
+ "```",
168
+ groupMarkers.map((m) => m.context).join("\n---\n"),
169
+ "```",
170
+ ].join("\n");
171
+ groups.push({
172
+ role,
173
+ taskId,
174
+ markers: groupMarkers,
175
+ filePath,
176
+ combinedInstruction,
177
+ dispatchId: generateDispatchId(role),
178
+ });
179
+ }
180
+ return groups;
181
+ }
182
+ /**
183
+ * Generate a unique dispatch ID for tracking and writeback targeting.
184
+ * Exported for testing.
185
+ */
186
+ export function generateDispatchId(role) {
187
+ const ts = Date.now();
188
+ const rand = Math.random().toString(36).slice(2, 8);
189
+ return `${role}-${ts}-${rand}`;
190
+ }
191
+ /**
192
+ * Replace @agent markers in the file with a dispatch marker comment.
193
+ * The comment embeds the dispatch ID so the subagent can find and replace it
194
+ * with results when it completes.
195
+ */
196
+ function markProcessing(filePath, groups, dispatchRecords) {
197
+ try {
198
+ let content = fs.readFileSync(filePath, "utf-8");
199
+ for (const group of groups) {
200
+ const indicator = `<!-- pi-dispatch:${group.dispatchId} -->`;
201
+ // Replace all markers in this group with the same dispatch indicator
202
+ for (const marker of group.markers) {
203
+ content = content.replace(marker.rawLine, indicator);
204
+ }
205
+ // Store the dispatch record for status tracking
206
+ dispatchRecords.set(group.dispatchId, {
207
+ dispatchId: group.dispatchId,
208
+ filePath,
209
+ role: group.role,
210
+ agentName: group.role, // will be set later in dispatchGroup
211
+ dispatchedAt: new Date().toISOString(),
212
+ markerCount: group.markers.length,
213
+ });
214
+ }
215
+ fs.writeFileSync(filePath, content, "utf-8");
216
+ }
217
+ catch {
218
+ // Best-effort; don't block processing
219
+ }
220
+ }
221
+ // ── Core Watcher Logic ────────────────────────────────────────────────────────────
222
+ export function startWatcher(pi, vaults, state) {
223
+ if (state.running) {
224
+ pi.events?.emit?.("wiki:log", { level: "warn", message: "Watcher already running." });
225
+ return;
226
+ }
227
+ state.running = true;
228
+ const vaultEntries = Object.entries(vaults);
229
+ if (vaultEntries.length === 0) {
230
+ console.warn("[pi-vault-mind] No vaults configured. Watcher requires at least one vault in wiki.vaults.");
231
+ return;
232
+ }
233
+ for (const [name, vault] of vaultEntries) {
234
+ const vaultPath = vault.path;
235
+ if (!fs.existsSync(vaultPath)) {
236
+ console.warn(`[pi-vault-mind] Vault "${name}" path does not exist: ${vaultPath}`);
237
+ continue;
238
+ }
239
+ console.log(`[pi-vault-mind] Watching vault "${name}" at ${vaultPath}`);
240
+ const watcher = fs.watch(vaultPath, { recursive: true }, (eventType, filename) => {
241
+ if (!filename || !state.running)
242
+ return;
243
+ const filePath = path.join(vaultPath, filename);
244
+ // Skip non-watchable files
245
+ if (!isWatchableFile(filePath))
246
+ return;
247
+ if (!fs.existsSync(filePath))
248
+ return;
249
+ // Debounce: clear existing timer, set a new one
250
+ const existingTimer = state.debounceTimers.get(filePath);
251
+ if (existingTimer)
252
+ clearTimeout(existingTimer);
253
+ const timer = setTimeout(() => {
254
+ state.debounceTimers.delete(filePath);
255
+ handleFileChange(pi, filePath, state);
256
+ }, DEBOUNCE_MS);
257
+ state.debounceTimers.set(filePath, timer);
258
+ });
259
+ watcher.on("error", (err) => {
260
+ console.error(`[pi-vault-mind] Watcher error for vault "${name}":`, err);
261
+ });
262
+ state.watchers.set(name, watcher);
263
+ }
264
+ console.log(`[pi-vault-mind] Watcher started. Monitoring ${state.watchers.size} vault(s).`);
265
+ }
266
+ export function stopWatcher(state) {
267
+ state.running = false;
268
+ // Clear debounce timers
269
+ for (const [, timer] of state.debounceTimers) {
270
+ clearTimeout(timer);
271
+ }
272
+ state.debounceTimers.clear();
273
+ // Close all file watchers
274
+ for (const [name, watcher] of state.watchers) {
275
+ watcher.close();
276
+ console.log(`[pi-vault-mind] Stopped watching vault "${name}".`);
277
+ }
278
+ state.watchers.clear();
279
+ state.processing.clear();
280
+ console.log("[pi-vault-mind] Watcher stopped.");
281
+ }
282
+ function handleFileChange(pi, filePath, state) {
283
+ if (state.processing.has(filePath))
284
+ return;
285
+ const groups = scanFile(filePath);
286
+ if (groups.length === 0)
287
+ return;
288
+ state.processing.add(filePath);
289
+ console.log(`[pi-vault-mind] Detected ${groups.length} task group(s) in ${filePath}`);
290
+ // Replace markers with dispatch comments (embeds dispatch ID for writeback)
291
+ markProcessing(filePath, groups, state.activeDispatches_);
292
+ // Queue the groups for dispatch
293
+ for (const group of groups) {
294
+ state.pendingQueue.push(group);
295
+ }
296
+ // Process the queue
297
+ processQueue(pi, state);
298
+ }
299
+ export function processQueue(pi, state) {
300
+ while (state.pendingQueue.length > 0 && state.activeDispatches < state.maxConcurrent) {
301
+ const group = state.pendingQueue.shift();
302
+ if (!group)
303
+ break;
304
+ dispatchGroup(pi, group, state);
305
+ }
306
+ }
307
+ function dispatchGroup(pi, group, state) {
308
+ state.activeDispatches++;
309
+ const role = group.role;
310
+ const agentMap = {
311
+ miner: "vault-mind-miner",
312
+ manager: "vault-mind-manager",
313
+ broadcaster: "vault-mind-broadcaster",
314
+ "heavy-lifter": "vault-mind-heavy-lifter",
315
+ delegator: "vault-mind-heavy-lifter",
316
+ researcher: "vault-mind-miner",
317
+ podcast: "vault-mind-broadcaster",
318
+ };
319
+ const agentName = agentMap[role] || `vault-mind-${role}`;
320
+ // Update the dispatch record with the resolved agent name
321
+ const record = state.activeDispatches_.get(group.dispatchId);
322
+ if (record)
323
+ record.agentName = agentName;
324
+ // Build writeback instructions: tell the subagent exactly how to replace its marker
325
+ const writebackInstructions = [
326
+ "## Result Writeback",
327
+ "After completing your work, replace the dispatch marker in the source file with a summary:",
328
+ "",
329
+ `1. Read the file: \`${group.filePath}\``,
330
+ `2. Find the comment: \`<!-- pi-dispatch:${group.dispatchId} -->\``,
331
+ "3. Replace it with a brief summary of your findings (1-3 lines), prefixed with `> ` to render as a blockquote",
332
+ "",
333
+ "Example replacement:",
334
+ "> Extracted 3 entities (PPO, Reward Model, RLHF) and 2 relationships. Stored in LanceDB. See [[Agent/Inbox/2026-06-06-rlhf-claims]].",
335
+ "",
336
+ "This ensures results are visible directly in the source note.",
337
+ ].join("\n");
338
+ // Build the task prompt for the Manager agent
339
+ const taskDescription = [
340
+ `The vault watcher detected an @agent-${role} marker in ${group.filePath}.`,
341
+ `Dispatch ID: ${group.dispatchId}`,
342
+ "",
343
+ "Please dispatch a subagent to handle it:",
344
+ "",
345
+ "```",
346
+ "subagent({",
347
+ ` agent: "${agentName}",`,
348
+ ` task: ${JSON.stringify(`${group.combinedInstruction}\n\n${writebackInstructions}`)},`,
349
+ ` context: "fork",`,
350
+ " async: true,",
351
+ "})",
352
+ "```",
353
+ "",
354
+ `Role: ${agentName}`,
355
+ `Source file: ${group.filePath}`,
356
+ `Dispatch ID: ${group.dispatchId}`,
357
+ `Group: ${role}${group.taskId ? ` (id: ${group.taskId})` : ""} (${group.markers.length} marker(s))`,
358
+ ].join("\n");
359
+ // Send as a user message to trigger an agent turn
360
+ try {
361
+ pi.sendUserMessage(taskDescription, { deliverAs: "followUp" });
362
+ console.log(`[pi-vault-mind] Dispatched ${agentName} task for ${group.filePath}`);
363
+ }
364
+ catch (err) {
365
+ console.error(`[pi-vault-mind] Failed to dispatch ${agentName}:`, err.message);
366
+ }
367
+ finally {
368
+ state.activeDispatches--;
369
+ // Check for more work
370
+ processQueue(pi, state);
371
+ }
372
+ }
373
+ export function getWatcherStatus(state) {
374
+ const lines = [
375
+ `Watcher: ${state.running ? "RUNNING" : "STOPPED"}`,
376
+ `Active vaults: ${state.watchers.size}`,
377
+ `Active dispatches: ${state.activeDispatches}`,
378
+ `Pending queue: ${state.pendingQueue.length}`,
379
+ `Max concurrent: ${state.maxConcurrent}`,
380
+ `Dispatch records: ${state.activeDispatches_.size}`,
381
+ ];
382
+ if (state.watchers.size > 0) {
383
+ lines.push("");
384
+ lines.push("Watched vaults:");
385
+ for (const [name] of state.watchers) {
386
+ lines.push(` - ${name}`);
387
+ }
388
+ }
389
+ if (state.activeDispatches_.size > 0) {
390
+ lines.push("");
391
+ lines.push("Recent dispatches:");
392
+ const recent = [...state.activeDispatches_.values()].slice(-10);
393
+ for (const d of recent) {
394
+ const relPath = d.filePath.split("/").slice(-3).join("/");
395
+ lines.push(` ${d.dispatchId} → ${d.agentName} (${relPath}, ${d.markerCount} markers)`);
396
+ }
397
+ }
398
+ return lines.join("\n");
399
+ }
400
+ export function createWatcherState() {
401
+ return {
402
+ watchers: new Map(),
403
+ debounceTimers: new Map(),
404
+ processing: new Set(),
405
+ running: false,
406
+ maxConcurrent: MAX_CONCURRENT,
407
+ activeDispatches: 0,
408
+ pendingQueue: [],
409
+ activeDispatches_: new Map(),
410
+ };
411
+ }
@@ -0,0 +1,3 @@
1
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+ export declare const WIDGET_KEY = "pi-vault-mind-active";
3
+ export declare const updateActiveCollectionWidget: (ctx: ExtensionContext) => void;
@@ -0,0 +1,12 @@
1
+ import { getActiveCollection } from "./state.js";
2
+ export const WIDGET_KEY = "pi-vault-mind-active";
3
+ export const updateActiveCollectionWidget = (ctx) => {
4
+ const active = getActiveCollection(ctx.cwd);
5
+ // We use a component-based widget to style it nicely above the editor
6
+ // If ctx.ui.setWidget doesn't support the callback in older versions,
7
+ // it will just fall back, but we pass strings.
8
+ // However, the doc says string arrays are supported for RPC and function callbacks for TUI.
9
+ // The doc also says: ctx.ui.setWidget("my-widget", ["Line 1", "Line 2"], { placement: "aboveEditor" });
10
+ const lines = [`[Active Collection: ${active}]`];
11
+ ctx.ui.setWidget(WIDGET_KEY, lines, { placement: "aboveEditor" });
12
+ };
@@ -0,0 +1 @@
1
+ export {};