pi-vault-mind 0.14.2 → 0.16.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 (164) hide show
  1. package/CHANGELOG.md +293 -0
  2. package/README.md +5 -5
  3. package/agents/broadcaster.agent.md +2 -2
  4. package/agents/heavy-lifter.agent.md +1 -1
  5. package/agents/main.agent.md +29 -0
  6. package/agents/manager.agent.md +1 -1
  7. package/agents/miner.agent.md +1 -1
  8. package/dist/packages/obsidian/src/client.js +331 -0
  9. package/dist/packages/obsidian/src/types.js +1 -0
  10. package/dist/src/activity.d.ts +49 -0
  11. package/dist/src/activity.js +175 -0
  12. package/dist/src/agent-bus/Agent.d.ts +9 -1
  13. package/dist/src/agent-bus/Agent.js +18 -2
  14. package/dist/src/agent-bus/index.d.ts +5 -5
  15. package/dist/src/agent-bus/index.js +1 -1
  16. package/dist/src/agent-queue.js +12 -3
  17. package/dist/src/agents/BroadcasterAgent.d.ts +1 -1
  18. package/dist/src/agents/BroadcasterAgent.js +2 -2
  19. package/dist/src/agents/HeavyLifterAgent.d.ts +1 -1
  20. package/dist/src/agents/HeavyLifterAgent.js +2 -2
  21. package/dist/src/agents/ManagerAgent.js +1 -1
  22. package/dist/src/agents/MinerAgent.d.ts +1 -1
  23. package/dist/src/agents/MinerAgent.js +2 -2
  24. package/dist/src/agents/index.d.ts +2 -2
  25. package/dist/src/agents/index.js +2 -2
  26. package/dist/src/auth.d.ts +17 -15
  27. package/dist/src/auth.js +26 -37
  28. package/dist/src/autosync.js +2 -2
  29. package/dist/src/commands.d.ts +2 -19
  30. package/dist/src/commands.js +301 -231
  31. package/dist/src/config-keys.d.ts +16 -0
  32. package/dist/src/config-keys.js +45 -0
  33. package/dist/src/discover-schema.js +4 -4
  34. package/dist/src/edits.d.ts +21 -0
  35. package/dist/src/edits.js +75 -0
  36. package/dist/src/embedding-probe.d.ts +34 -0
  37. package/dist/src/embedding-probe.js +142 -0
  38. package/dist/src/embedding-providers.d.ts +133 -0
  39. package/dist/src/embedding-providers.js +173 -0
  40. package/dist/src/embedding-secrets.d.ts +38 -0
  41. package/dist/src/embedding-secrets.js +291 -0
  42. package/dist/src/engine.d.ts +6 -0
  43. package/dist/src/engine.js +78 -6
  44. package/dist/src/events.js +7 -10
  45. package/dist/src/extension-packages.d.ts +9 -0
  46. package/dist/src/extension-packages.js +14 -0
  47. package/dist/src/git.d.ts +19 -0
  48. package/dist/src/git.js +78 -0
  49. package/dist/src/graph.d.ts +2 -1
  50. package/dist/src/graph.js +47 -13
  51. package/dist/src/identities-config.d.ts +6 -0
  52. package/dist/src/identities-config.js +39 -0
  53. package/dist/src/identity-injector.js +1 -1
  54. package/dist/src/index.js +9 -26
  55. package/dist/src/lance.d.ts +9 -0
  56. package/dist/src/lance.js +143 -203
  57. package/dist/src/modal-client.d.ts +16 -0
  58. package/dist/src/modal-client.js +16 -0
  59. package/dist/src/modal-config.d.ts +37 -14
  60. package/dist/src/modal-config.js +59 -32
  61. package/dist/src/model-router.d.ts +27 -0
  62. package/dist/src/model-router.js +67 -0
  63. package/dist/src/models.d.ts +42 -0
  64. package/dist/src/models.js +78 -0
  65. package/dist/src/personalize.js +17 -3
  66. package/dist/src/scaffold.d.ts +48 -0
  67. package/dist/src/scaffold.js +246 -0
  68. package/dist/src/server.d.ts +47 -4
  69. package/dist/src/server.js +988 -221
  70. package/dist/src/session-search.d.ts +63 -0
  71. package/dist/src/session-search.js +379 -0
  72. package/dist/src/settings-ui.d.ts +2 -2
  73. package/dist/src/settings-ui.js +76 -135
  74. package/dist/src/sync.d.ts +3 -3
  75. package/dist/src/sync.js +19 -15
  76. package/dist/src/tool-catalog.d.ts +18 -0
  77. package/dist/src/tool-catalog.js +232 -0
  78. package/dist/src/tools/marksman.js +1 -1
  79. package/dist/src/tools.js +111 -65
  80. package/dist/src/types.d.ts +127 -71
  81. package/dist/src/utils.d.ts +57 -6
  82. package/dist/src/utils.js +114 -28
  83. package/dist/src/vault-tools.d.ts +34 -0
  84. package/dist/src/vault-tools.js +99 -0
  85. package/dist/src/vault-writer.js +4 -1
  86. package/dist/src/vm-handlers.js +140 -43
  87. package/dist/src/watcher.js +9 -9
  88. package/dist/test/activity-tool.test.js +54 -0
  89. package/dist/test/agent-bus/Agent.test.js +174 -0
  90. package/dist/test/agent-bus/AgentLoader.test.js +180 -0
  91. package/dist/test/agent-bus/AgentRegistry.test.js +86 -0
  92. package/dist/test/agent-bus/LLMProvider.test.js +162 -0
  93. package/dist/test/agent-bus/TaskQueue.test.js +256 -0
  94. package/dist/test/agent-dispatch-context.test.js +73 -0
  95. package/dist/test/agent-queue.test.js +1 -1
  96. package/dist/test/agents/Agents.test.js +282 -0
  97. package/dist/test/auth.test.js +57 -56
  98. package/dist/test/bridge-identity.test.js +34 -3
  99. package/dist/test/classify-search-mode.test.js +51 -0
  100. package/dist/test/commands.test.js +410 -0
  101. package/dist/test/config-merge.test.js +98 -0
  102. package/dist/test/context-capture.test.js +56 -0
  103. package/dist/test/discover-schema.test.js +8 -5
  104. package/dist/test/dispatch.test.js +45 -91
  105. package/dist/test/edits.test.js +144 -0
  106. package/dist/test/embedding-probe.test.js +424 -0
  107. package/dist/test/embedding-providers.test.js +157 -0
  108. package/dist/test/embedding-secrets.test.js +210 -0
  109. package/dist/test/engine-identity.test.js +1 -1
  110. package/dist/test/events.test.js +675 -0
  111. package/dist/test/git-bridge.test.js +160 -0
  112. package/dist/test/graph.test.js +464 -0
  113. package/dist/test/helpers/server.js +81 -0
  114. package/dist/test/identity-injector.test.js +197 -0
  115. package/dist/test/index.test.js +5 -7
  116. package/dist/test/lance-modal.test.js +67 -2
  117. package/dist/test/lance.test.js +429 -0
  118. package/dist/test/modal-client.test.js +1 -1
  119. package/dist/test/modal-config.test.js +92 -79
  120. package/dist/test/model-readiness.test.js +122 -0
  121. package/dist/test/model-router.test.js +99 -0
  122. package/dist/test/obsidian-client.test.js +42 -0
  123. package/dist/test/personalize.test.js +1 -1
  124. package/dist/test/rest-models.test.js +153 -0
  125. package/dist/test/rest-queue.test.js +135 -87
  126. package/dist/test/rest-setup.test.js +625 -115
  127. package/dist/test/rest-vm.test.js +355 -94
  128. package/dist/test/server-cors.test.js +48 -0
  129. package/dist/test/server-ws.test.js +61 -34
  130. package/dist/test/session-crud.test.js +323 -0
  131. package/dist/test/session-search.test.js +139 -0
  132. package/dist/test/settings-ui.test.js +113 -4
  133. package/dist/test/sync.test.js +12 -12
  134. package/dist/test/tombstone.test.js +8 -10
  135. package/dist/test/tool-catalog.test.js +86 -0
  136. package/dist/test/utils.test.js +588 -0
  137. package/dist/test/vault-tools.test.js +103 -12
  138. package/dist/test/vault-writer.test.js +371 -0
  139. package/dist/test/vm-handlers.test.js +620 -0
  140. package/extension-packages.json +9 -0
  141. package/package.json +107 -90
  142. package/pi-vault-mind.config.example.json +24 -2
  143. package/scripts/check-deps.sh +35 -32
  144. package/scripts/cli-e2e.mjs +215 -0
  145. package/scripts/configuration-e2e.mjs +783 -0
  146. package/scripts/e2e-commands.mjs +391 -0
  147. package/scripts/e2e-ws.mjs +3 -2
  148. package/scripts/fetch-modal-token.sh +4 -3
  149. package/scripts/generate-config-keys-json.mjs +45 -0
  150. package/scripts/generate-extension-packages-json.mjs +55 -0
  151. package/scripts/pi-session-query +0 -0
  152. package/scripts/publish-obsidian-plugin.sh +0 -0
  153. package/scripts/reset-test-vault.sh +2 -2
  154. package/scripts/setup-test-config.sh +0 -0
  155. package/scripts/setup-vault-pi.sh +48 -9
  156. package/scripts/test-slash-commands.mjs +2 -2
  157. package/scripts/vault-pi.sh +12 -3
  158. package/skills/github-release-notes-file/SKILL.md +42 -0
  159. package/skills/obsidian-guided-test-session/SKILL.md +110 -0
  160. package/skills/obsidian-interactive-test/SKILL.md +88 -0
  161. package/skills/obsidian-plugin-publish/SKILL.md +58 -0
  162. package/skills/pi-vault-mind-release/SKILL.md +141 -0
  163. package/skills/pre-test-release/SKILL.md +116 -0
  164. package/dist/test/auto-indexer.test.js +0 -2
@@ -6,9 +6,11 @@ import * as path from "node:path";
6
6
  import { afterEach, beforeEach, describe, it } from "node:test";
7
7
  import { resetConnection, upsertPrecomputed } from "../src/lance.js";
8
8
  import { MODAL_TOKEN_ENV } from "../src/modal-config.js";
9
- import { registerVaultTools, vaultAppendTool, vaultListTool, vaultReadTool, vaultSearchTool, vaultTagsTool, vaultWriteTool, } from "../src/vault-tools.js";
9
+ import { registerVaultTools, searchVaultNative, vaultAppendTool, vaultListTool, vaultReadTool, vaultSearchTool, vaultTagsTool, vaultWriteTool, } from "../src/vault-tools.js";
10
10
  const setupConfig = (dir, vaultPath) => {
11
- fs.writeFileSync(path.join(dir, "pi-vault-mind.config.json"), JSON.stringify({
11
+ const configDir = path.join(dir, ".vault-mind");
12
+ fs.mkdirSync(configDir, { recursive: true });
13
+ fs.writeFileSync(path.join(configDir, "vault-mind.config.json"), JSON.stringify({
12
14
  version: 2,
13
15
  collections: {
14
16
  main: {
@@ -22,7 +24,7 @@ const setupConfig = (dir, vaultPath) => {
22
24
  dataDir: ".lancedb",
23
25
  embedding: {
24
26
  remoteUrl: "https://pvm.modal.run",
25
- modal: { baseUrl: "https://pvm.modal.run", model: "testmodel" },
27
+ model: "testmodel",
26
28
  },
27
29
  ftsEnabled: true,
28
30
  graph: { enabled: false },
@@ -49,25 +51,21 @@ const makeMockPi = () => {
49
51
  describe("vault tools", () => {
50
52
  let testDir;
51
53
  let vaultPath;
52
- let savedHome;
54
+ let savedCwd;
53
55
  let origToken;
54
56
  beforeEach(() => {
55
57
  testDir = path.join(tmpdir(), `pi-vault-mind-vault-tools-${randomUUID()}`);
56
58
  vaultPath = path.join(testDir, "vault");
57
59
  fs.mkdirSync(vaultPath, { recursive: true });
58
60
  setupConfig(testDir, vaultPath);
59
- savedHome = process.env.HOME;
60
- process.env.HOME = testDir;
61
- process.env.USERPROFILE = testDir;
61
+ savedCwd = process.cwd();
62
+ process.chdir(testDir);
62
63
  origToken = process.env[MODAL_TOKEN_ENV];
63
64
  process.env[MODAL_TOKEN_ENV] = "tok";
64
65
  resetConnection();
65
66
  });
66
67
  afterEach(() => {
67
- if (savedHome !== undefined)
68
- process.env.HOME = savedHome;
69
- else
70
- process.env.HOME = undefined;
68
+ process.chdir(savedCwd);
71
69
  if (origToken === undefined)
72
70
  delete process.env[MODAL_TOKEN_ENV];
73
71
  else
@@ -161,7 +159,7 @@ describe("vault tools", () => {
161
159
  dataDir: path.join(testDir, ".lancedb"),
162
160
  embedding: {
163
161
  remoteUrl: "https://pvm.modal.run",
164
- modal: { baseUrl: "https://pvm.modal.run", model: "testmodel" },
162
+ model: "testmodel",
165
163
  },
166
164
  ftsEnabled: true,
167
165
  graph: { enabled: false },
@@ -221,4 +219,97 @@ describe("vault tools", () => {
221
219
  assert.equal(pi.tools.length, 6);
222
220
  });
223
221
  });
222
+ describe("searchVaultNative", () => {
223
+ const writeNote = (rel, opts = {}) => {
224
+ const full = path.join(vaultPath, rel);
225
+ fs.mkdirSync(path.dirname(full), { recursive: true });
226
+ const lines = [];
227
+ if (opts.title !== undefined || opts.tags !== undefined) {
228
+ lines.push("---");
229
+ if (opts.title !== undefined)
230
+ lines.push(`title: ${opts.title}`);
231
+ if (opts.tags !== undefined)
232
+ lines.push(`tags: [${opts.tags.join(", ")}]`);
233
+ lines.push("---");
234
+ }
235
+ if (opts.body !== undefined)
236
+ lines.push(opts.body);
237
+ fs.writeFileSync(full, lines.join("\n"));
238
+ };
239
+ it("ranks exact title > title substring > path-only and drops zero-score files", async () => {
240
+ writeNote("alpha.md", { title: "Widget" }); // exact title +10
241
+ writeNote("bravo.md", { title: "Widget Toolkit" }); // title substring +5
242
+ writeNote("widget/charlie.md", { title: "Notes" }); // path-only via subdir +2
243
+ writeNote("unrelated.md", { title: "Something Else" }); // score 0 -> excluded
244
+ const hits = await searchVaultNative(vaultPath, "widget", 10);
245
+ assert.deepEqual(hits.map((h) => h.source), ["alpha.md", "bravo.md", "widget/charlie.md"]);
246
+ });
247
+ it("uses frontmatter title over basename for scoring and the title field", async () => {
248
+ writeNote("random-file-name.md", { title: "Kubernetes" }); // fm title exact +10
249
+ writeNote("kubernetes.md", { title: "Networking" }); // fm masks basename; only path +2
250
+ const hits = await searchVaultNative(vaultPath, "kubernetes", 10);
251
+ assert.deepEqual(hits.map((h) => h.source), ["random-file-name.md", "kubernetes.md"]);
252
+ assert.equal(hits[0].title, "Kubernetes");
253
+ assert.equal(hits.find((h) => h.source === "kubernetes.md")?.title, "Networking");
254
+ });
255
+ it("scores a file whose tag equals the query with no title or path match", async () => {
256
+ writeNote("journal.md", { title: "Journal", tags: ["devops", "misc"] }); // frontmatter tag
257
+ writeNote("notes.md", { body: "See #devops for the pipeline" }); // inline tag
258
+ writeNote("misc.md", { title: "Misc", body: "nothing relevant here" }); // no match
259
+ const hits = await searchVaultNative(vaultPath, "devops", 10);
260
+ assert.deepEqual(hits.map((h) => h.source), ["journal.md", "notes.md"]);
261
+ for (const hit of hits)
262
+ assert.ok(hit.tags.includes("devops"));
263
+ });
264
+ it("includes and boosts files carrying tags-param tags, tolerating a leading #", async () => {
265
+ writeNote("a.md", { title: "A", tags: ["project", "urgent"] }); // +3 +3
266
+ writeNote("b.md", { title: "B", tags: ["project"] }); // +3
267
+ writeNote("c.md", { title: "C", tags: ["other"] }); // score 0 -> excluded
268
+ const hits = await searchVaultNative(vaultPath, "no-query-match", 10, ["#project", "urgent"]);
269
+ assert.deepEqual(hits.map((h) => h.source), ["a.md", "b.md"]);
270
+ });
271
+ it("breaks score ties by source ascending", async () => {
272
+ writeNote("zebra.md", { title: "Report" });
273
+ writeNote("apple.md", { title: "Report" });
274
+ const hits = await searchVaultNative(vaultPath, "report", 10);
275
+ assert.deepEqual(hits.map((h) => h.source), ["apple.md", "zebra.md"]);
276
+ });
277
+ it("slices the ranked results down to limit", async () => {
278
+ writeNote("n1.md", { title: "Note" });
279
+ writeNote("n2.md", { title: "Note" });
280
+ writeNote("n3.md", { title: "Note" });
281
+ writeNote("n4.md", { title: "Note" });
282
+ const hits = await searchVaultNative(vaultPath, "note", 2);
283
+ assert.deepEqual(hits.map((h) => h.source), ["n1.md", "n2.md"]);
284
+ });
285
+ it("returns [] for empty query, whitespace query, empty path, or missing path", async () => {
286
+ writeNote("match.md", { title: "Match" });
287
+ assert.deepEqual(await searchVaultNative(vaultPath, "", 10), []);
288
+ assert.deepEqual(await searchVaultNative(vaultPath, " ", 10), []);
289
+ assert.deepEqual(await searchVaultNative("", "match", 10), []);
290
+ assert.deepEqual(await searchVaultNative(path.join(testDir, "does-not-exist"), "match", 10), []);
291
+ });
292
+ it("ignores non-.md files", async () => {
293
+ writeNote("config.md", { title: "Config" });
294
+ fs.writeFileSync(path.join(vaultPath, "config.txt"), "config config config");
295
+ fs.writeFileSync(path.join(vaultPath, "config.json"), '{"config": true}');
296
+ const hits = await searchVaultNative(vaultPath, "config", 10);
297
+ assert.deepEqual(hits.map((h) => h.source), ["config.md"]);
298
+ });
299
+ it("shapes hits as { source, title, tags[], _source } with union tags and forward-slash source", async () => {
300
+ writeNote("sub/dir/deep.md", {
301
+ title: "Deep",
302
+ tags: ["alpha", "beta"],
303
+ body: "content #gamma",
304
+ });
305
+ const hits = await searchVaultNative(vaultPath, "deep", 10);
306
+ assert.equal(hits.length, 1);
307
+ const hit = hits[0];
308
+ assert.equal(hit.source, "sub/dir/deep.md");
309
+ assert.equal(hit.title, "Deep");
310
+ assert.equal(hit._source, "vault");
311
+ assert.ok(Array.isArray(hit.tags));
312
+ assert.deepEqual([...hit.tags].sort(), ["alpha", "beta", "gamma"]);
313
+ });
314
+ });
224
315
  });
@@ -0,0 +1,371 @@
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
+ import { connect, resetConnection } from "../src/lance.js";
8
+ import { VaultWriter } from "../src/vault-writer.js";
9
+ // ---------------------------------------------------------------------------
10
+ // helpers
11
+ // ---------------------------------------------------------------------------
12
+ const makeCfg = (vaultPath) => ({
13
+ dataDir: path.join(vaultPath, ".lancedb"),
14
+ embedding: {},
15
+ folders: { inbox: "inbox", library: "library" },
16
+ });
17
+ const setupConfigFile = (vaultPath, collections) => {
18
+ const configDir = path.join(vaultPath, ".vault-mind");
19
+ fs.mkdirSync(configDir, { recursive: true });
20
+ fs.writeFileSync(path.join(configDir, "vault-mind.config.json"), JSON.stringify({ collections }), "utf-8");
21
+ };
22
+ // ---------------------------------------------------------------------------
23
+ // constructor
24
+ // ---------------------------------------------------------------------------
25
+ describe("VaultWriter constructor", () => {
26
+ it("stores vaultPath and cfg (exercised via writeEntry)", async () => {
27
+ const vaultPath = path.join(tmpdir(), `vw-ctor-${randomUUID()}`);
28
+ const cfg = makeCfg(vaultPath);
29
+ const writer = new VaultWriter(vaultPath, cfg);
30
+ const filepath = await writer.writeEntry("test", {
31
+ id: "ctor-test",
32
+ fact: "constructor stores state",
33
+ });
34
+ assert.ok(fs.existsSync(filepath));
35
+ const content = fs.readFileSync(filepath, "utf-8");
36
+ assert.ok(content.includes('id: "ctor-test"'));
37
+ assert.ok(content.includes("collection: test"));
38
+ assert.ok(content.includes("constructor stores state"));
39
+ fs.rmSync(vaultPath, { recursive: true });
40
+ });
41
+ });
42
+ // ---------------------------------------------------------------------------
43
+ // writeEntry
44
+ // ---------------------------------------------------------------------------
45
+ describe("writeEntry", () => {
46
+ let vaultPath;
47
+ let cfg;
48
+ let writer;
49
+ beforeEach(() => {
50
+ vaultPath = path.join(tmpdir(), `vw-write-${randomUUID()}`);
51
+ cfg = makeCfg(vaultPath);
52
+ writer = new VaultWriter(vaultPath, cfg);
53
+ });
54
+ afterEach(() => {
55
+ if (fs.existsSync(vaultPath))
56
+ fs.rmSync(vaultPath, { recursive: true });
57
+ });
58
+ it("creates a .md file in the inbox with frontmatter and body", async () => {
59
+ const filepath = await writer.writeEntry("notes", {
60
+ id: "abc-123",
61
+ fact: "The quick brown fox jumps over the lazy dog",
62
+ domain: "biology",
63
+ tag: "animals",
64
+ source: "observation",
65
+ created_at: "2025-01-15T10:00:00.000Z",
66
+ artifact: "[[fox-sighting.png]]",
67
+ });
68
+ assert.ok(fs.existsSync(filepath));
69
+ assert.ok(filepath.endsWith("abc-123.md"));
70
+ assert.ok(filepath.includes(path.join("inbox")));
71
+ const content = fs.readFileSync(filepath, "utf-8");
72
+ // frontmatter
73
+ assert.ok(content.includes("---"));
74
+ assert.ok(content.includes('id: "abc-123"'));
75
+ assert.ok(content.includes("collection: notes"));
76
+ assert.ok(content.includes("domain: biology"));
77
+ assert.ok(content.includes("tag: animals"));
78
+ assert.ok(content.includes("source: observation"));
79
+ assert.ok(content.includes("created: 2025-01-15T10:00:00.000Z"));
80
+ // body
81
+ assert.ok(content.includes("# The quick brown fox jumps over the lazy dog"));
82
+ assert.ok(content.includes("The quick brown fox jumps over the lazy dog"));
83
+ assert.ok(content.includes("**See also:** [[fox-sighting.png]]"));
84
+ });
85
+ it("generates filename from entry.id", async () => {
86
+ const filepath = await writer.writeEntry("test", {
87
+ id: "my-custom-id",
88
+ fact: "some fact",
89
+ });
90
+ assert.ok(filepath.endsWith("my-custom-id.md"));
91
+ });
92
+ it("generates filename from entry.fact when id is missing", async () => {
93
+ const filepath = await writer.writeEntry("test", {
94
+ fact: "Hello World! This is a test fact with special chars: @#$%",
95
+ });
96
+ // fact sliced to 40 chars, non-alphanumeric replaced with "-"
97
+ assert.ok(filepath.endsWith("Hello-World--This-is-a-test-fact-with-sp.md"));
98
+ });
99
+ it('falls back to "entry" when neither id nor fact', async () => {
100
+ const filepath = await writer.writeEntry("test", {
101
+ domain: "general",
102
+ });
103
+ assert.ok(filepath.endsWith("entry.md"));
104
+ });
105
+ it("writes identity in frontmatter when present", async () => {
106
+ const filepath = await writer.writeEntry("test", {
107
+ id: "with-identity",
108
+ fact: "fact with identity",
109
+ identity: "alice",
110
+ });
111
+ const content = fs.readFileSync(filepath, "utf-8");
112
+ assert.ok(content.includes('identity: "alice"'));
113
+ });
114
+ it("omits identity line when identity is absent", async () => {
115
+ const filepath = await writer.writeEntry("test", {
116
+ id: "no-identity",
117
+ fact: "fact without identity",
118
+ });
119
+ const content = fs.readFileSync(filepath, "utf-8");
120
+ assert.ok(!content.includes("identity:"));
121
+ });
122
+ it("omits artifact line when artifact is absent", async () => {
123
+ const filepath = await writer.writeEntry("test", {
124
+ id: "no-artifact",
125
+ fact: "fact without artifact",
126
+ });
127
+ const content = fs.readFileSync(filepath, "utf-8");
128
+ assert.ok(!content.includes("**See also:**"));
129
+ });
130
+ it("uses current timestamp when created_at is missing", async () => {
131
+ const before = new Date().toISOString();
132
+ const filepath = await writer.writeEntry("test", {
133
+ id: "no-created",
134
+ fact: "fact without created_at",
135
+ });
136
+ const after = new Date().toISOString();
137
+ const content = fs.readFileSync(filepath, "utf-8");
138
+ const match = content.match(/created: (.+)/);
139
+ assert.ok(match, "created field missing");
140
+ const created = match[1];
141
+ assert.ok(created >= before && created <= after, `created ${created} not in [${before}, ${after}]`);
142
+ });
143
+ it("writes multiple entries without collision", async () => {
144
+ const a = await writer.writeEntry("test", { id: "entry-a", fact: "Fact A" });
145
+ const b = await writer.writeEntry("test", { id: "entry-b", fact: "Fact B" });
146
+ assert.ok(fs.existsSync(a));
147
+ assert.ok(fs.existsSync(b));
148
+ assert.notEqual(a, b);
149
+ });
150
+ });
151
+ // ---------------------------------------------------------------------------
152
+ // syncEntries
153
+ // ---------------------------------------------------------------------------
154
+ describe("syncEntries", () => {
155
+ let vaultPath;
156
+ let cfg;
157
+ let writer;
158
+ let savedCwd;
159
+ beforeEach(() => {
160
+ vaultPath = path.join(tmpdir(), `vw-sync-${randomUUID()}`);
161
+ fs.mkdirSync(vaultPath, { recursive: true });
162
+ cfg = makeCfg(vaultPath);
163
+ writer = new VaultWriter(vaultPath, cfg);
164
+ // syncEntries calls loadConfig(vaultPath) which reads
165
+ // <vaultPath>/.vault-mind/vault-mind.config.json, then reads
166
+ // the JSONL file at def.path (relative to cwd).
167
+ savedCwd = process.cwd();
168
+ process.chdir(vaultPath);
169
+ });
170
+ afterEach(() => {
171
+ process.chdir(savedCwd);
172
+ if (fs.existsSync(vaultPath))
173
+ fs.rmSync(vaultPath, { recursive: true });
174
+ });
175
+ it("syncs entries from a JSONL collection file", async () => {
176
+ const jsonlPath = path.join(vaultPath, "collections", "main.jsonl");
177
+ fs.mkdirSync(path.dirname(jsonlPath), { recursive: true });
178
+ fs.writeFileSync(jsonlPath, `${[
179
+ JSON.stringify({ id: "e1", fact: "First entry", tag: "alpha" }),
180
+ JSON.stringify({ id: "e2", fact: "Second entry", tag: "beta" }),
181
+ JSON.stringify({ id: "e3", fact: "Third entry", tag: "alpha" }),
182
+ ].join("\n")}\n`, "utf-8");
183
+ setupConfigFile(vaultPath, {
184
+ main: { path: "collections/main.jsonl", schema: ["id", "fact", "tag"] },
185
+ });
186
+ const count = await writer.syncEntries("main", "", 10);
187
+ assert.equal(count, 3);
188
+ // all three should be written to inbox
189
+ const inboxDir = path.join(vaultPath, "inbox");
190
+ const files = fs.readdirSync(inboxDir);
191
+ assert.equal(files.length, 3);
192
+ });
193
+ it("filters entries by substring query", async () => {
194
+ const jsonlPath = path.join(vaultPath, "collections", "main.jsonl");
195
+ fs.mkdirSync(path.dirname(jsonlPath), { recursive: true });
196
+ fs.writeFileSync(jsonlPath, `${[
197
+ JSON.stringify({ id: "e1", fact: "Apple pie recipe", tag: "food" }),
198
+ JSON.stringify({ id: "e2", fact: "Banana bread", tag: "food" }),
199
+ JSON.stringify({ id: "e3", fact: "Carrot cake", tag: "dessert" }),
200
+ ].join("\n")}\n`, "utf-8");
201
+ setupConfigFile(vaultPath, {
202
+ main: { path: "collections/main.jsonl", schema: ["id", "fact", "tag"] },
203
+ });
204
+ const count = await writer.syncEntries("main", "apple", 10);
205
+ assert.equal(count, 1);
206
+ const inboxDir = path.join(vaultPath, "inbox");
207
+ const files = fs.readdirSync(inboxDir);
208
+ assert.equal(files.length, 1);
209
+ assert.ok(files[0].startsWith("e1"));
210
+ });
211
+ it("filters entries by tag: prefix", async () => {
212
+ const jsonlPath = path.join(vaultPath, "collections", "main.jsonl");
213
+ fs.mkdirSync(path.dirname(jsonlPath), { recursive: true });
214
+ fs.writeFileSync(jsonlPath, `${[
215
+ JSON.stringify({ id: "e1", fact: "Fact one", tag: "alpha" }),
216
+ JSON.stringify({ id: "e2", fact: "Fact two", tag: "beta" }),
217
+ JSON.stringify({ id: "e3", fact: "Fact three", tag: "alpha" }),
218
+ ].join("\n")}\n`, "utf-8");
219
+ setupConfigFile(vaultPath, {
220
+ main: { path: "collections/main.jsonl", schema: ["id", "fact", "tag"] },
221
+ });
222
+ const count = await writer.syncEntries("main", "tag:alpha", 10);
223
+ assert.equal(count, 2);
224
+ const inboxDir = path.join(vaultPath, "inbox");
225
+ const files = fs.readdirSync(inboxDir);
226
+ assert.equal(files.length, 2);
227
+ });
228
+ it("respects the limit parameter", async () => {
229
+ const jsonlPath = path.join(vaultPath, "collections", "main.jsonl");
230
+ fs.mkdirSync(path.dirname(jsonlPath), { recursive: true });
231
+ const lines = [];
232
+ for (let i = 0; i < 10; i++) {
233
+ lines.push(JSON.stringify({ id: `e${i}`, fact: `Entry ${i}`, tag: "common" }));
234
+ }
235
+ fs.writeFileSync(jsonlPath, `${lines.join("\n")}\n`, "utf-8");
236
+ setupConfigFile(vaultPath, {
237
+ main: { path: "collections/main.jsonl", schema: ["id", "fact", "tag"] },
238
+ });
239
+ const count = await writer.syncEntries("main", "", 3);
240
+ assert.equal(count, 3);
241
+ const inboxDir = path.join(vaultPath, "inbox");
242
+ assert.equal(fs.readdirSync(inboxDir).length, 3);
243
+ });
244
+ it("returns 0 when collection is not in config", async () => {
245
+ setupConfigFile(vaultPath, {});
246
+ const count = await writer.syncEntries("nonexistent", "", 10);
247
+ assert.equal(count, 0);
248
+ });
249
+ it("returns 0 when collection file does not exist", async () => {
250
+ setupConfigFile(vaultPath, {
251
+ main: { path: "collections/missing.jsonl", schema: ["id"] },
252
+ });
253
+ const count = await writer.syncEntries("main", "", 10);
254
+ assert.equal(count, 0);
255
+ });
256
+ it("skips malformed JSON lines", async () => {
257
+ const jsonlPath = path.join(vaultPath, "collections", "main.jsonl");
258
+ fs.mkdirSync(path.dirname(jsonlPath), { recursive: true });
259
+ fs.writeFileSync(jsonlPath, `${[
260
+ JSON.stringify({ id: "good", fact: "valid entry" }),
261
+ "not valid json {{{",
262
+ JSON.stringify({ id: "also-good", fact: "another valid entry" }),
263
+ ].join("\n")}\n`, "utf-8");
264
+ setupConfigFile(vaultPath, {
265
+ main: { path: "collections/main.jsonl", schema: ["id", "fact"] },
266
+ });
267
+ const count = await writer.syncEntries("main", "", 10);
268
+ assert.equal(count, 2);
269
+ const inboxDir = path.join(vaultPath, "inbox");
270
+ assert.equal(fs.readdirSync(inboxDir).length, 2);
271
+ });
272
+ });
273
+ // ---------------------------------------------------------------------------
274
+ // syncGraphToCanvas
275
+ // ---------------------------------------------------------------------------
276
+ describe("syncGraphToCanvas", () => {
277
+ let vaultPath;
278
+ let cfg;
279
+ let writer;
280
+ beforeEach(() => {
281
+ vaultPath = path.join(tmpdir(), `vw-graph-${randomUUID()}`);
282
+ cfg = makeCfg(vaultPath);
283
+ writer = new VaultWriter(vaultPath, cfg);
284
+ resetConnection();
285
+ });
286
+ afterEach(() => {
287
+ resetConnection();
288
+ if (fs.existsSync(vaultPath))
289
+ fs.rmSync(vaultPath, { recursive: true });
290
+ });
291
+ it("generates a .canvas file from entities and relations", async () => {
292
+ const db = await connect(cfg.dataDir);
293
+ await db.createTable("entities", [
294
+ { id: "1", name: "Entity One" },
295
+ { id: "2", name: "Entity Two" },
296
+ ]);
297
+ await db.createTable("relations", [
298
+ { from_entity_id: "1", to_entity_id: "2", relation_type: "related_to" },
299
+ ]);
300
+ const edgeCount = await writer.syncGraphToCanvas();
301
+ assert.equal(edgeCount, 1);
302
+ const canvasPath = path.join(vaultPath, "library", "Library-Graph.canvas");
303
+ assert.ok(fs.existsSync(canvasPath));
304
+ const canvas = JSON.parse(fs.readFileSync(canvasPath, "utf-8"));
305
+ assert.ok(Array.isArray(canvas.nodes));
306
+ assert.ok(Array.isArray(canvas.edges));
307
+ assert.equal(canvas.nodes.length, 2);
308
+ assert.equal(canvas.edges.length, 1);
309
+ // verify node structure
310
+ assert.equal(canvas.nodes[0].id, "e-1");
311
+ assert.equal(canvas.nodes[0].type, "text");
312
+ assert.equal(canvas.nodes[0].file, "Entity-One.md");
313
+ // verify edge structure
314
+ assert.equal(canvas.edges[0].id, "edge-0");
315
+ assert.equal(canvas.edges[0].fromNode, "e-1");
316
+ assert.equal(canvas.edges[0].toNode, "e-2");
317
+ assert.equal(canvas.edges[0].label, "related_to");
318
+ });
319
+ it("uses custom canvasPath from graph config", async () => {
320
+ const customCfg = {
321
+ ...makeCfg(vaultPath),
322
+ graph: { canvasPath: "Custom/MyGraph.canvas" },
323
+ };
324
+ const customWriter = new VaultWriter(vaultPath, customCfg);
325
+ const db = await connect(cfg.dataDir);
326
+ await db.createTable("entities", [{ id: "1", name: "Solo" }]);
327
+ await customWriter.syncGraphToCanvas();
328
+ const canvasPath = path.join(vaultPath, "Custom", "MyGraph.canvas");
329
+ assert.ok(fs.existsSync(canvasPath));
330
+ });
331
+ it("returns 0 when no tables exist", async () => {
332
+ // Connect to create the data dir but don't create any tables
333
+ await connect(cfg.dataDir);
334
+ const edgeCount = await writer.syncGraphToCanvas();
335
+ assert.equal(edgeCount, 0);
336
+ const canvasPath = path.join(vaultPath, "library", "Library-Graph.canvas");
337
+ assert.ok(fs.existsSync(canvasPath));
338
+ const canvas = JSON.parse(fs.readFileSync(canvasPath, "utf-8"));
339
+ assert.equal(canvas.nodes.length, 0);
340
+ assert.equal(canvas.edges.length, 0);
341
+ });
342
+ it("handles entities without a name field", async () => {
343
+ const db = await connect(cfg.dataDir);
344
+ await db.createTable("entities", [{ id: "99" }]);
345
+ await writer.syncGraphToCanvas();
346
+ const canvasPath = path.join(vaultPath, "library", "Library-Graph.canvas");
347
+ const canvas = JSON.parse(fs.readFileSync(canvasPath, "utf-8"));
348
+ assert.equal(canvas.nodes.length, 1);
349
+ assert.equal(canvas.nodes[0].id, "e-99");
350
+ // no name → no file property
351
+ assert.equal(canvas.nodes[0].file, undefined);
352
+ });
353
+ it("handles relations without a relation_type", async () => {
354
+ const db = await connect(cfg.dataDir);
355
+ await db.createTable("entities", [{ id: "1" }, { id: "2" }]);
356
+ await db.createTable("relations", [{ from_entity_id: "1", to_entity_id: "2" }]);
357
+ await writer.syncGraphToCanvas();
358
+ const canvasPath = path.join(vaultPath, "library", "Library-Graph.canvas");
359
+ const canvas = JSON.parse(fs.readFileSync(canvasPath, "utf-8"));
360
+ assert.equal(canvas.edges.length, 1);
361
+ // no relation_type → label is undefined
362
+ assert.equal(canvas.edges[0].label, undefined);
363
+ });
364
+ it("creates the library directory if it does not exist", async () => {
365
+ await connect(cfg.dataDir);
366
+ const libDir = path.join(vaultPath, "library");
367
+ assert.ok(!fs.existsSync(libDir), "library dir should not exist before call");
368
+ await writer.syncGraphToCanvas();
369
+ assert.ok(fs.existsSync(libDir), "library dir should be created");
370
+ });
371
+ });