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,368 @@
1
+ import { strict as assert } from "node:assert";
2
+ import { randomUUID } from "node:crypto";
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
+ describe("pi-vault-mind", () => {
8
+ let testDir;
9
+ beforeEach(() => {
10
+ testDir = path.join(tmpdir(), `pi-vault-mind-test-${randomUUID()}`);
11
+ fs.mkdirSync(testDir, { recursive: true });
12
+ const collectionsDir = path.join(testDir, "collections");
13
+ fs.mkdirSync(collectionsDir, { recursive: true });
14
+ process.env.HOME = testDir;
15
+ process.env.USERPROFILE = testDir;
16
+ });
17
+ afterEach(() => {
18
+ // Preserve original env, don't delete (shared with build)
19
+ });
20
+ it("package exports a default function", async () => {
21
+ const cwd = process.cwd();
22
+ process.chdir(testDir);
23
+ try {
24
+ const mod = await import("../index.js");
25
+ assert.equal(typeof mod.default, "function");
26
+ }
27
+ finally {
28
+ process.chdir(cwd);
29
+ }
30
+ });
31
+ it("registerTools is exported from tools module", async () => {
32
+ const cwd = process.cwd();
33
+ process.chdir(testDir);
34
+ try {
35
+ const mod = await import("../src/tools.js");
36
+ assert.ok(mod.registerTools, "registerTools should be exported");
37
+ assert.equal(typeof mod.registerTools, "function");
38
+ }
39
+ finally {
40
+ process.chdir(cwd);
41
+ }
42
+ });
43
+ it("registerCommands is exported from commands module", async () => {
44
+ const cwd = process.cwd();
45
+ process.chdir(testDir);
46
+ try {
47
+ const mod = await import("../src/commands.js");
48
+ assert.ok(mod.registerCommands, "registerCommands should be exported");
49
+ assert.equal(typeof mod.registerCommands, "function");
50
+ }
51
+ finally {
52
+ process.chdir(cwd);
53
+ }
54
+ });
55
+ it("config is written and read correctly", async () => {
56
+ const cwd = process.cwd();
57
+ process.chdir(testDir);
58
+ try {
59
+ const configPath = path.join(testDir, "pi-vault-mind.config.json");
60
+ const collectionsDir = path.join(testDir, "collections");
61
+ fs.mkdirSync(collectionsDir, { recursive: true });
62
+ fs.writeFileSync(path.join(collectionsDir, "main.jsonl"), '{"id":"1","domain":"test","fact":"hello world","tag":"test"}\n');
63
+ fs.writeFileSync(configPath, JSON.stringify({
64
+ version: 2,
65
+ collections: {
66
+ main: {
67
+ path: "collections/main.jsonl",
68
+ schema: ["id", "domain", "fact", "tag"],
69
+ },
70
+ },
71
+ injectors: [],
72
+ wiki: {
73
+ dataDir: ".lancedb",
74
+ embedding: {
75
+ provider: "transformers",
76
+ ollamaModel: "embeddinggemma",
77
+ ollamaHost: "http://127.0.0.1:11434",
78
+ },
79
+ graph: { enabled: false, canvasSync: false },
80
+ ftsEnabled: false,
81
+ },
82
+ }));
83
+ const mod = await import("../src/utils.js");
84
+ const cfg = mod.loadConfig(testDir);
85
+ assert.ok(cfg, "config should be loaded");
86
+ assert.equal(cfg.version, 2);
87
+ assert.ok(cfg.collections.main, "should have main collection");
88
+ assert.equal(cfg.wiki.embedding.provider, "transformers");
89
+ }
90
+ finally {
91
+ process.chdir(cwd);
92
+ }
93
+ });
94
+ it("types module has DEFAULT_CONFIG with correct shape", async () => {
95
+ const cwd = process.cwd();
96
+ process.chdir(testDir);
97
+ try {
98
+ const types = await import("../src/types.js");
99
+ assert.ok(types.DEFAULT_CONFIG, "DEFAULT_CONFIG should exist");
100
+ assert.equal(types.DEFAULT_CONFIG.version, 2);
101
+ assert.ok(types.DEFAULT_CONFIG.collections, "collections should exist");
102
+ assert.ok(types.DEFAULT_CONFIG.collections.main, "should have main collection");
103
+ assert.equal(types.DEFAULT_CONFIG.wiki.embedding.provider, "transformers");
104
+ }
105
+ finally {
106
+ process.chdir(cwd);
107
+ }
108
+ });
109
+ it("lance module exports all expected functions", async () => {
110
+ const cwd = process.cwd();
111
+ process.chdir(testDir);
112
+ try {
113
+ const lance = await import("../src/lance.js");
114
+ assert.equal(typeof lance.connect, "function", "connect should be exported");
115
+ assert.equal(typeof lance.upsertEntry, "function", "upsertEntry should be exported");
116
+ assert.equal(typeof lance.searchHybrid, "function", "searchHybrid should be exported");
117
+ assert.equal(typeof lance.getStatus, "function", "getStatus should be exported");
118
+ }
119
+ finally {
120
+ process.chdir(cwd);
121
+ }
122
+ });
123
+ it("graph module exports expected functions", async () => {
124
+ const cwd = process.cwd();
125
+ process.chdir(testDir);
126
+ try {
127
+ const graph = await import("../src/graph.js");
128
+ assert.equal(typeof graph.graphUpsert, "function", "graphUpsert should be exported");
129
+ assert.equal(typeof graph.queryGraph, "function", "queryGraph should be exported");
130
+ }
131
+ finally {
132
+ process.chdir(cwd);
133
+ }
134
+ });
135
+ // ── Watcher e2e tests ──────────────────────────────────────────────────
136
+ it("watcher module exports expected functions", async () => {
137
+ const cwd = process.cwd();
138
+ process.chdir(testDir);
139
+ try {
140
+ const watcher = await import("../src/watcher.js");
141
+ assert.equal(typeof watcher.startWatcher, "function", "startWatcher should be exported");
142
+ assert.equal(typeof watcher.stopWatcher, "function", "stopWatcher should be exported");
143
+ assert.equal(typeof watcher.getWatcherStatus, "function", "getWatcherStatus should be exported");
144
+ assert.equal(typeof watcher.createWatcherState, "function", "createWatcherState should be exported");
145
+ assert.equal(typeof watcher.scanFile, "function", "scanFile should be exported");
146
+ assert.equal(typeof watcher.generateDispatchId, "function", "generateDispatchId should be exported");
147
+ }
148
+ finally {
149
+ process.chdir(cwd);
150
+ }
151
+ });
152
+ it("createWatcherState returns a valid state object", async () => {
153
+ const cwd = process.cwd();
154
+ process.chdir(testDir);
155
+ try {
156
+ const watcher = await import("../src/watcher.js");
157
+ const state = watcher.createWatcherState();
158
+ assert.equal(state.running, false);
159
+ assert.ok(state.watchers instanceof Map);
160
+ assert.ok(state.debounceTimers instanceof Map);
161
+ assert.ok(state.processing instanceof Set);
162
+ assert.ok(Array.isArray(state.pendingQueue));
163
+ assert.ok(state.activeDispatches_ instanceof Map);
164
+ assert.equal(state.maxConcurrent, 3);
165
+ assert.equal(state.activeDispatches, 0);
166
+ }
167
+ finally {
168
+ process.chdir(cwd);
169
+ }
170
+ });
171
+ it("scanFile parses @agent markers correctly", async () => {
172
+ const cwd = process.cwd();
173
+ process.chdir(testDir);
174
+ try {
175
+ const watcher = await import("../src/watcher.js");
176
+ const testFilePath = path.join(testDir, "test-note.md");
177
+ fs.writeFileSync(testFilePath, [
178
+ "# Research Notes",
179
+ "",
180
+ "Some context before the marker.",
181
+ "",
182
+ "@agent-miner Extract all technical claims from this section",
183
+ "",
184
+ "RLHF uses PPO to fine-tune language models based on human preferences.",
185
+ "The reward model is trained on pairwise comparison data.",
186
+ "",
187
+ "@agent-broadcaster Generate a study guide from these notes",
188
+ "",
189
+ "More content after the markers.",
190
+ ].join("\n"));
191
+ const groups = watcher.scanFile(testFilePath);
192
+ assert.equal(groups.length, 2, "should detect 2 groups (miner + broadcaster)");
193
+ const minerGroup = groups.find((g) => g.role === "miner");
194
+ assert.ok(minerGroup, "should have a miner group");
195
+ assert.equal(minerGroup.markers.length, 1);
196
+ assert.ok(minerGroup.markers[0].instruction.includes("Extract all technical claims"));
197
+ assert.ok(minerGroup.dispatchId.startsWith("miner-"), "dispatch ID should start with role");
198
+ assert.ok(minerGroup.combinedInstruction.includes("File:"), "combined instruction should include file path");
199
+ const broadcasterGroup = groups.find((g) => g.role === "broadcaster");
200
+ assert.ok(broadcasterGroup, "should have a broadcaster group");
201
+ assert.equal(broadcasterGroup.markers.length, 1);
202
+ assert.ok(broadcasterGroup.markers[0].instruction.includes("Generate a study guide"));
203
+ fs.unlinkSync(testFilePath);
204
+ }
205
+ finally {
206
+ process.chdir(cwd);
207
+ }
208
+ });
209
+ it("scanFile groups same-role markers together", async () => {
210
+ const cwd = process.cwd();
211
+ process.chdir(testDir);
212
+ try {
213
+ const watcher = await import("../src/watcher.js");
214
+ const testFilePath = path.join(testDir, "multi-miner.md");
215
+ fs.writeFileSync(testFilePath, [
216
+ "@agent-miner Extract entities from paragraph 1",
217
+ "",
218
+ "Paragraph 1 content.",
219
+ "",
220
+ "@agent-miner Extract entities from paragraph 2",
221
+ "",
222
+ "Paragraph 2 content.",
223
+ ].join("\n"));
224
+ const groups = watcher.scanFile(testFilePath);
225
+ assert.equal(groups.length, 1, "should bundle same-role markers into 1 group");
226
+ assert.equal(groups[0].markers.length, 2, "should have 2 markers in the group");
227
+ assert.equal(groups[0].role, "miner");
228
+ assert.ok(groups[0].combinedInstruction.includes("Multiple markers (2)"), "should note multiple markers");
229
+ fs.unlinkSync(testFilePath);
230
+ }
231
+ finally {
232
+ process.chdir(cwd);
233
+ }
234
+ });
235
+ it("scanFile isolates named IDs into separate groups", async () => {
236
+ const cwd = process.cwd();
237
+ process.chdir(testDir);
238
+ try {
239
+ const watcher = await import("../src/watcher.js");
240
+ const testFilePath = path.join(testDir, "named-ids.md");
241
+ fs.writeFileSync(testFilePath, [
242
+ "@agent-miner:security Audit the auth flow",
243
+ "",
244
+ "Auth flow details.",
245
+ "",
246
+ "@agent-miner:performance Check database queries",
247
+ "",
248
+ "Query details.",
249
+ ].join("\n"));
250
+ const groups = watcher.scanFile(testFilePath);
251
+ assert.equal(groups.length, 2, "should create separate groups for named IDs");
252
+ assert.equal(groups[0].markers.length, 1);
253
+ assert.equal(groups[1].markers.length, 1);
254
+ assert.notEqual(groups[0].dispatchId, groups[1].dispatchId, "dispatch IDs should be unique");
255
+ const ids = groups.map((g) => g.taskId);
256
+ assert.ok(ids.includes("miner:security") || ids.includes("security"), "should include security taskId");
257
+ assert.ok(ids.includes("miner:performance") || ids.includes("performance"), "should include performance taskId");
258
+ fs.unlinkSync(testFilePath);
259
+ }
260
+ finally {
261
+ process.chdir(cwd);
262
+ }
263
+ });
264
+ it("scanFile returns empty array for files without markers", async () => {
265
+ const cwd = process.cwd();
266
+ process.chdir(testDir);
267
+ try {
268
+ const watcher = await import("../src/watcher.js");
269
+ const testFilePath = path.join(testDir, "no-markers.md");
270
+ fs.writeFileSync(testFilePath, "# Just a regular note\n\nNo markers here.\n");
271
+ const groups = watcher.scanFile(testFilePath);
272
+ assert.equal(groups.length, 0, "should return empty array for marker-free files");
273
+ fs.unlinkSync(testFilePath);
274
+ }
275
+ finally {
276
+ process.chdir(cwd);
277
+ }
278
+ });
279
+ it("scanFile handles non-existent files gracefully", async () => {
280
+ const cwd = process.cwd();
281
+ process.chdir(testDir);
282
+ try {
283
+ const watcher = await import("../src/watcher.js");
284
+ const groups = watcher.scanFile("/nonexistent/file.md");
285
+ assert.equal(groups.length, 0, "should return empty array for missing files");
286
+ }
287
+ finally {
288
+ process.chdir(cwd);
289
+ }
290
+ });
291
+ it("generateDispatchId creates unique IDs with role prefix", async () => {
292
+ const cwd = process.cwd();
293
+ process.chdir(testDir);
294
+ try {
295
+ const watcher = await import("../src/watcher.js");
296
+ const id1 = watcher.generateDispatchId("miner");
297
+ assert.ok(id1.startsWith("miner-"), "should start with role prefix");
298
+ assert.ok(id1.length > 10, "should be reasonably long");
299
+ const id2 = watcher.generateDispatchId("miner");
300
+ assert.notEqual(id1, id2, "consecutive IDs should be unique");
301
+ const broadcaster = watcher.generateDispatchId("broadcaster");
302
+ assert.ok(broadcaster.startsWith("broadcaster-"));
303
+ }
304
+ finally {
305
+ process.chdir(cwd);
306
+ }
307
+ });
308
+ it("markProcessing replaces markers with dispatch comments", async () => {
309
+ const cwd = process.cwd();
310
+ process.chdir(testDir);
311
+ try {
312
+ const watcher = await import("../src/watcher.js");
313
+ const testFilePath = path.join(testDir, "writeback-test.md");
314
+ const originalContent = [
315
+ "# Test Note",
316
+ "",
317
+ "@agent-miner Extract claims",
318
+ "",
319
+ "Some content.",
320
+ "",
321
+ "@agent-broadcaster Make podcast",
322
+ "",
323
+ "More content.",
324
+ ].join("\n");
325
+ fs.writeFileSync(testFilePath, originalContent);
326
+ const groups = watcher.scanFile(testFilePath);
327
+ assert.equal(groups.length, 2, "should detect 2 groups");
328
+ for (const group of groups) {
329
+ assert.ok(group.dispatchId, "each group should have a dispatchId");
330
+ assert.ok(group.dispatchId.match(/^(miner|broadcaster)-\d+-[a-z0-9]+$/), `dispatchId format: ${group.dispatchId}`);
331
+ }
332
+ // Verify the original file still has @agent markers (scanFile doesn't modify)
333
+ const content = fs.readFileSync(testFilePath, "utf-8");
334
+ assert.ok(content.includes("@agent-miner"), "scanFile should not modify the file");
335
+ assert.ok(content.includes("@agent-broadcaster"), "scanFile should not modify the file");
336
+ fs.unlinkSync(testFilePath);
337
+ }
338
+ finally {
339
+ process.chdir(cwd);
340
+ }
341
+ });
342
+ it("getWatcherStatus reports state correctly", async () => {
343
+ const cwd = process.cwd();
344
+ process.chdir(testDir);
345
+ try {
346
+ const watcher = await import("../src/watcher.js");
347
+ const state = watcher.createWatcherState();
348
+ let status = watcher.getWatcherStatus(state);
349
+ assert.ok(status.includes("STOPPED"), "should show STOPPED initially");
350
+ assert.ok(status.includes("0"), "should show zero active vaults");
351
+ // Add a dispatch record
352
+ state.activeDispatches_.set("test-1", {
353
+ dispatchId: "test-1",
354
+ filePath: "/vault/notes/research.md",
355
+ role: "miner",
356
+ agentName: "vault-mind-miner",
357
+ dispatchedAt: new Date().toISOString(),
358
+ markerCount: 2,
359
+ });
360
+ status = watcher.getWatcherStatus(state);
361
+ assert.ok(status.includes("test-1"), "should show dispatch record");
362
+ assert.ok(status.includes("vault-mind-miner"), "should show agent name");
363
+ }
364
+ finally {
365
+ process.chdir(cwd);
366
+ }
367
+ });
368
+ });
package/package.json ADDED
@@ -0,0 +1,83 @@
1
+ {
2
+ "name": "pi-vault-mind",
3
+ "version": "0.7.0",
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
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "type": "module",
8
+ "files": [
9
+ "dist",
10
+ "skills",
11
+ "CHANGELOG.md",
12
+ "README.md",
13
+ "LICENSE"
14
+ ],
15
+ "keywords": [
16
+ "pi-package",
17
+ "pi-extension",
18
+ "pi-skills",
19
+ "vault-mind",
20
+ "jsonl",
21
+ "lance",
22
+ "lancedb",
23
+ "search",
24
+ "knowledge-base",
25
+ "agent-tools",
26
+ "hitl",
27
+ "semantic-search",
28
+ "obsidian",
29
+ "obsidian-vault",
30
+ "subagent",
31
+ "subagents",
32
+ "passive",
33
+ "file-watcher",
34
+ "graph",
35
+ "knowledge-graph",
36
+ "notebooklm",
37
+ "pkm"
38
+ ],
39
+ "author": "Kyle Brodeur",
40
+ "license": "MIT",
41
+ "repository": {
42
+ "type": "git",
43
+ "url": "git+https://github.com/kylebrodeur/pi-vault-mind.git"
44
+ },
45
+ "bugs": {
46
+ "url": "https://github.com/kylebrodeur/pi-vault-mind/issues"
47
+ },
48
+ "homepage": "https://github.com/kylebrodeur/pi-vault-mind#readme",
49
+ "engines": {
50
+ "node": ">=20.6.0"
51
+ },
52
+ "pi": {
53
+ "extensions": [
54
+ "./dist/index.js"
55
+ ],
56
+ "skills": [
57
+ "./skills"
58
+ ]
59
+ },
60
+ "dependencies": {
61
+ "@lancedb/lancedb": "latest",
62
+ "@xenova/transformers": "latest",
63
+ "apache-arrow": "^21.1.0"
64
+ },
65
+ "peerDependencies": {
66
+ "@earendil-works/pi-coding-agent": ">=0.75.0 <1.0.0"
67
+ },
68
+ "devDependencies": {
69
+ "@biomejs/biome": "^1.9.0",
70
+ "@earendil-works/pi-coding-agent": "^0.79.0",
71
+ "@earendil-works/pi-tui": "^0.79.0",
72
+ "@types/node": "^20.0.0",
73
+ "husky": "^9.1.6",
74
+ "typebox": "^1.1.0",
75
+ "typescript": "^5.0.0"
76
+ },
77
+ "scripts": {
78
+ "check": "biome check --write . && tsc --noEmit",
79
+ "build": "tsc && tsc -p tsconfig.test.json",
80
+ "dev": "tsc --watch",
81
+ "test": "node --test dist/test/index.test.js"
82
+ }
83
+ }