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.
- package/CHANGELOG.md +293 -0
- package/README.md +5 -5
- package/agents/broadcaster.agent.md +2 -2
- package/agents/heavy-lifter.agent.md +1 -1
- package/agents/main.agent.md +29 -0
- package/agents/manager.agent.md +1 -1
- package/agents/miner.agent.md +1 -1
- package/dist/packages/obsidian/src/client.js +331 -0
- package/dist/packages/obsidian/src/types.js +1 -0
- package/dist/src/activity.d.ts +49 -0
- package/dist/src/activity.js +175 -0
- package/dist/src/agent-bus/Agent.d.ts +9 -1
- package/dist/src/agent-bus/Agent.js +18 -2
- package/dist/src/agent-bus/index.d.ts +5 -5
- package/dist/src/agent-bus/index.js +1 -1
- package/dist/src/agent-queue.js +12 -3
- package/dist/src/agents/BroadcasterAgent.d.ts +1 -1
- package/dist/src/agents/BroadcasterAgent.js +2 -2
- package/dist/src/agents/HeavyLifterAgent.d.ts +1 -1
- package/dist/src/agents/HeavyLifterAgent.js +2 -2
- package/dist/src/agents/ManagerAgent.js +1 -1
- package/dist/src/agents/MinerAgent.d.ts +1 -1
- package/dist/src/agents/MinerAgent.js +2 -2
- package/dist/src/agents/index.d.ts +2 -2
- package/dist/src/agents/index.js +2 -2
- package/dist/src/auth.d.ts +17 -15
- package/dist/src/auth.js +26 -37
- package/dist/src/autosync.js +2 -2
- package/dist/src/commands.d.ts +2 -19
- package/dist/src/commands.js +301 -231
- package/dist/src/config-keys.d.ts +16 -0
- package/dist/src/config-keys.js +45 -0
- package/dist/src/discover-schema.js +4 -4
- package/dist/src/edits.d.ts +21 -0
- package/dist/src/edits.js +75 -0
- package/dist/src/embedding-probe.d.ts +34 -0
- package/dist/src/embedding-probe.js +142 -0
- package/dist/src/embedding-providers.d.ts +133 -0
- package/dist/src/embedding-providers.js +173 -0
- package/dist/src/embedding-secrets.d.ts +38 -0
- package/dist/src/embedding-secrets.js +291 -0
- package/dist/src/engine.d.ts +6 -0
- package/dist/src/engine.js +78 -6
- package/dist/src/events.js +7 -10
- package/dist/src/extension-packages.d.ts +9 -0
- package/dist/src/extension-packages.js +14 -0
- package/dist/src/git.d.ts +19 -0
- package/dist/src/git.js +78 -0
- package/dist/src/graph.d.ts +2 -1
- package/dist/src/graph.js +47 -13
- package/dist/src/identities-config.d.ts +6 -0
- package/dist/src/identities-config.js +39 -0
- package/dist/src/identity-injector.js +1 -1
- package/dist/src/index.js +9 -26
- package/dist/src/lance.d.ts +9 -0
- package/dist/src/lance.js +143 -203
- package/dist/src/modal-client.d.ts +16 -0
- package/dist/src/modal-client.js +16 -0
- package/dist/src/modal-config.d.ts +37 -14
- package/dist/src/modal-config.js +59 -32
- package/dist/src/model-router.d.ts +27 -0
- package/dist/src/model-router.js +67 -0
- package/dist/src/models.d.ts +42 -0
- package/dist/src/models.js +78 -0
- package/dist/src/personalize.js +17 -3
- package/dist/src/scaffold.d.ts +48 -0
- package/dist/src/scaffold.js +246 -0
- package/dist/src/server.d.ts +47 -4
- package/dist/src/server.js +988 -221
- package/dist/src/session-search.d.ts +63 -0
- package/dist/src/session-search.js +379 -0
- package/dist/src/settings-ui.d.ts +2 -2
- package/dist/src/settings-ui.js +76 -135
- package/dist/src/sync.d.ts +3 -3
- package/dist/src/sync.js +19 -15
- package/dist/src/tool-catalog.d.ts +18 -0
- package/dist/src/tool-catalog.js +232 -0
- package/dist/src/tools/marksman.js +1 -1
- package/dist/src/tools.js +111 -65
- package/dist/src/types.d.ts +127 -71
- package/dist/src/utils.d.ts +57 -6
- package/dist/src/utils.js +114 -28
- package/dist/src/vault-tools.d.ts +34 -0
- package/dist/src/vault-tools.js +99 -0
- package/dist/src/vault-writer.js +4 -1
- package/dist/src/vm-handlers.js +140 -43
- package/dist/src/watcher.js +9 -9
- package/dist/test/activity-tool.test.js +54 -0
- package/dist/test/agent-bus/Agent.test.js +174 -0
- package/dist/test/agent-bus/AgentLoader.test.js +180 -0
- package/dist/test/agent-bus/AgentRegistry.test.js +86 -0
- package/dist/test/agent-bus/LLMProvider.test.js +162 -0
- package/dist/test/agent-bus/TaskQueue.test.js +256 -0
- package/dist/test/agent-dispatch-context.test.js +73 -0
- package/dist/test/agent-queue.test.js +1 -1
- package/dist/test/agents/Agents.test.js +282 -0
- package/dist/test/auth.test.js +57 -56
- package/dist/test/bridge-identity.test.js +34 -3
- package/dist/test/classify-search-mode.test.js +51 -0
- package/dist/test/commands.test.js +410 -0
- package/dist/test/config-merge.test.js +98 -0
- package/dist/test/context-capture.test.js +56 -0
- package/dist/test/discover-schema.test.js +8 -5
- package/dist/test/dispatch.test.js +45 -91
- package/dist/test/edits.test.js +144 -0
- package/dist/test/embedding-probe.test.js +424 -0
- package/dist/test/embedding-providers.test.js +157 -0
- package/dist/test/embedding-secrets.test.js +210 -0
- package/dist/test/engine-identity.test.js +1 -1
- package/dist/test/events.test.js +675 -0
- package/dist/test/git-bridge.test.js +160 -0
- package/dist/test/graph.test.js +464 -0
- package/dist/test/helpers/server.js +81 -0
- package/dist/test/identity-injector.test.js +197 -0
- package/dist/test/index.test.js +5 -7
- package/dist/test/lance-modal.test.js +67 -2
- package/dist/test/lance.test.js +429 -0
- package/dist/test/modal-client.test.js +1 -1
- package/dist/test/modal-config.test.js +92 -79
- package/dist/test/model-readiness.test.js +122 -0
- package/dist/test/model-router.test.js +99 -0
- package/dist/test/obsidian-client.test.js +42 -0
- package/dist/test/personalize.test.js +1 -1
- package/dist/test/rest-models.test.js +153 -0
- package/dist/test/rest-queue.test.js +135 -87
- package/dist/test/rest-setup.test.js +625 -115
- package/dist/test/rest-vm.test.js +355 -94
- package/dist/test/server-cors.test.js +48 -0
- package/dist/test/server-ws.test.js +61 -34
- package/dist/test/session-crud.test.js +323 -0
- package/dist/test/session-search.test.js +139 -0
- package/dist/test/settings-ui.test.js +113 -4
- package/dist/test/sync.test.js +12 -12
- package/dist/test/tombstone.test.js +8 -10
- package/dist/test/tool-catalog.test.js +86 -0
- package/dist/test/utils.test.js +588 -0
- package/dist/test/vault-tools.test.js +103 -12
- package/dist/test/vault-writer.test.js +371 -0
- package/dist/test/vm-handlers.test.js +620 -0
- package/extension-packages.json +9 -0
- package/package.json +107 -90
- package/pi-vault-mind.config.example.json +24 -2
- package/scripts/check-deps.sh +35 -32
- package/scripts/cli-e2e.mjs +215 -0
- package/scripts/configuration-e2e.mjs +783 -0
- package/scripts/e2e-commands.mjs +391 -0
- package/scripts/e2e-ws.mjs +3 -2
- package/scripts/fetch-modal-token.sh +4 -3
- package/scripts/generate-config-keys-json.mjs +45 -0
- package/scripts/generate-extension-packages-json.mjs +55 -0
- package/scripts/pi-session-query +0 -0
- package/scripts/publish-obsidian-plugin.sh +0 -0
- package/scripts/reset-test-vault.sh +2 -2
- package/scripts/setup-test-config.sh +0 -0
- package/scripts/setup-vault-pi.sh +48 -9
- package/scripts/test-slash-commands.mjs +2 -2
- package/scripts/vault-pi.sh +12 -3
- package/skills/github-release-notes-file/SKILL.md +42 -0
- package/skills/obsidian-guided-test-session/SKILL.md +110 -0
- package/skills/obsidian-interactive-test/SKILL.md +88 -0
- package/skills/obsidian-plugin-publish/SKILL.md +58 -0
- package/skills/pi-vault-mind-release/SKILL.md +141 -0
- package/skills/pre-test-release/SKILL.md +116 -0
- 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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
60
|
-
process.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
});
|