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
@@ -3,9 +3,10 @@ import * as fs from "node:fs";
3
3
  import * as os from "node:os";
4
4
  import * as path from "node:path";
5
5
  import { afterEach, describe, it } from "node:test";
6
+ import { registerCommands } from "../src/commands.js";
6
7
  import { modalUrl } from "../src/modal-config.js";
7
8
  import { buildPluginInstallGuidance, detectVaultFromCwd, getMissingOnboardingPlugins, setupWizard, } from "../src/settings-ui.js";
8
- import { getGlobalConfigPath } from "../src/utils.js";
9
+ import { deriveCollectionNameFromVaultPath, getConfigPath } from "../src/utils.js";
9
10
  const savedHome = process.env.HOME;
10
11
  const mkTmpDir = (prefix) => fs.mkdtempSync(path.join(os.tmpdir(), prefix));
11
12
  const makeCtx = (cwd) => ({
@@ -24,6 +25,19 @@ afterEach(() => {
24
25
  process.env.HOME = savedHome;
25
26
  }
26
27
  });
28
+ // registerCommands(pi) registers "vm" (and "intake"); capture the real handler so
29
+ // tests exercise the actual /vm command dispatch (args tokenizer, handleSetup
30
+ // ordering, etc.), not a hand-built cliArgs object that would bypass a bug entirely.
31
+ const captureVmHandler = () => {
32
+ const handlers = [];
33
+ const pi = {
34
+ registerCommand: (name, spec) => {
35
+ handlers.push({ name, handler: spec.handler });
36
+ },
37
+ getAllTools: () => [],
38
+ };
39
+ return { pi, handlers };
40
+ };
27
41
  describe("settings-ui vault detection", () => {
28
42
  it("detectVaultFromCwd returns cwd when .obsidian exists", () => {
29
43
  const vault = mkTmpDir("pvm-vault-detect-");
@@ -68,7 +82,7 @@ describe("setupWizard CLI defaults", () => {
68
82
  remoteUrl: "https://example.com/v1",
69
83
  model: "embeddinggemma",
70
84
  });
71
- const cfg = JSON.parse(fs.readFileSync(getGlobalConfigPath(), "utf-8"));
85
+ const cfg = JSON.parse(fs.readFileSync(getConfigPath(vault), "utf-8"));
72
86
  const stored = cfg.vaultMind?.vaults?.default?.path;
73
87
  assert.ok(stored, "default vault path should be written");
74
88
  assert.equal(expandMaybeHome(stored, home), vault);
@@ -84,9 +98,104 @@ describe("setupWizard CLI defaults", () => {
84
98
  workspace: "acme-workspace",
85
99
  model: "embeddinggemma",
86
100
  });
87
- const cfg = JSON.parse(fs.readFileSync(getGlobalConfigPath(), "utf-8"));
88
- assert.equal(cfg.vaultMind?.embedding?.modal?.workspace, "acme-workspace");
101
+ const cfg = JSON.parse(fs.readFileSync(getConfigPath(vault), "utf-8"));
102
+ assert.equal(cfg.vaultMind?.embedding?.workspace, "acme-workspace");
89
103
  assert.equal(cfg.vaultMind?.embedding?.remoteUrl, modalUrl("acme-workspace"));
90
104
  fs.rmSync(home, { recursive: true, force: true });
91
105
  });
92
106
  });
107
+ describe("/vm setup CLI arg tokenizer (regression)", () => {
108
+ it("captures --vault as the leading flag in the CLI-mode args string", async () => {
109
+ // cwd is deliberately a plain, non-Obsidian directory and distinct from the
110
+ // --vault target: if detectVaultFromCwd(cwd) could return a fallback vault,
111
+ // a broken tokenizer that drops --vault would be masked by that fallback.
112
+ const home = mkTmpDir("pvm-home-");
113
+ const cwd = mkTmpDir("pvm-cwd-");
114
+ const vault = mkTmpDir("pvm-vault-");
115
+ fs.mkdirSync(path.join(vault, ".obsidian"), { recursive: true });
116
+ process.env.HOME = home;
117
+ const { pi, handlers } = captureVmHandler();
118
+ registerCommands(pi);
119
+ const vm = handlers.find((h) => h.name === "vm");
120
+ assert.ok(vm, "/vm command should be registered");
121
+ // Exact invocation documented in docs/getting-started/CLI_ONLY_WALKTHROUGH.md —
122
+ // --vault has no leading whitespace before its `--`, so a tokenizer that only
123
+ // strips `--` when preceded by whitespace drops it from cliArgs entirely.
124
+ await vm?.handler(`setup --vault ${vault} --remoteUrl https://example.com/v1 --model embeddinggemma`, makeCtx(cwd));
125
+ const cfg = JSON.parse(fs.readFileSync(getConfigPath(cwd), "utf-8"));
126
+ const stored = cfg.vaultMind?.vaults?.default?.path;
127
+ assert.ok(stored, "leading --vault flag should populate vaults.default.path");
128
+ assert.equal(expandMaybeHome(stored, home), vault);
129
+ assert.equal(cfg.vaultMind?.embedding?.remoteUrl, "https://example.com/v1");
130
+ assert.equal(cfg.vaultMind?.embedding?.model, "embeddinggemma");
131
+ fs.rmSync(home, { recursive: true, force: true });
132
+ fs.rmSync(cwd, { recursive: true, force: true });
133
+ fs.rmSync(vault, { recursive: true, force: true });
134
+ });
135
+ it("parses whichever flag comes first, not only --vault", async () => {
136
+ const home = mkTmpDir("pvm-home-");
137
+ const cwd = mkTmpDir("pvm-cwd-");
138
+ const vault = mkTmpDir("pvm-vault-");
139
+ fs.mkdirSync(path.join(vault, ".obsidian"), { recursive: true });
140
+ process.env.HOME = home;
141
+ const { pi, handlers } = captureVmHandler();
142
+ registerCommands(pi);
143
+ const vm = handlers.find((h) => h.name === "vm");
144
+ assert.ok(vm, "/vm command should be registered");
145
+ // --model leads here instead of --vault; the fix strips leading `--` from
146
+ // whichever flag is first, so this must not regress to a --vault-only fix.
147
+ await vm?.handler(`setup --model embeddinggemma --vault ${vault} --remoteUrl https://example.com/v1`, makeCtx(cwd));
148
+ const cfg = JSON.parse(fs.readFileSync(getConfigPath(cwd), "utf-8"));
149
+ assert.equal(cfg.vaultMind?.embedding?.model, "embeddinggemma");
150
+ assert.equal(expandMaybeHome(cfg.vaultMind?.vaults?.default?.path, home), vault);
151
+ fs.rmSync(home, { recursive: true, force: true });
152
+ fs.rmSync(cwd, { recursive: true, force: true });
153
+ fs.rmSync(vault, { recursive: true, force: true });
154
+ });
155
+ });
156
+ describe("/vm setup collection scaffolding (regression)", () => {
157
+ it("populates collections.main/pending/context_events + draft-context injector on a fresh vault", async () => {
158
+ const vault = mkTmpDir("pvm-setup-scaffold-");
159
+ const { pi, handlers } = captureVmHandler();
160
+ registerCommands(pi);
161
+ const vm = handlers.find((h) => h.name === "vm");
162
+ assert.ok(vm, "/vm command should be registered");
163
+ await vm?.handler(`setup --vault ${vault} --remoteUrl https://example.test --model embeddinggemma --collection customname`, makeCtx(vault));
164
+ const cfg = JSON.parse(fs.readFileSync(getConfigPath(vault), "utf-8"));
165
+ // This is exactly the regression handleSetup shipped with: setupWizard() used to
166
+ // run before handleInit(), and setupWizard always writes the config file first —
167
+ // so handleInit's "file doesn't exist yet" branch (which populates these defaults)
168
+ // never fired, and every /vm setup run ended with "collections": {}.
169
+ assert.ok(cfg.collections?.main, "collections.main should be scaffolded");
170
+ assert.ok(cfg.collections.main.path.endsWith(path.join("collections", "customname.jsonl")), `expected collections.main.path to end with collections/customname.jsonl, got ${cfg.collections.main.path}`);
171
+ assert.ok(cfg.collections?.pending, "collections.pending should be scaffolded");
172
+ assert.ok(cfg.collections?.context_events, "collections.context_events should be scaffolded");
173
+ assert.ok((cfg.injectors ?? []).some((ij) => ij.name === "draft-context"), "draft-context injector should be scaffolded");
174
+ for (const rel of [
175
+ path.join("collections", "customname.jsonl"),
176
+ path.join("collections", "pending.jsonl"),
177
+ path.join("collections", "context_events.jsonl"),
178
+ path.join("collections", "artifact.md"),
179
+ ]) {
180
+ assert.ok(fs.existsSync(path.join(vault, rel)), `${rel} should exist on disk`);
181
+ }
182
+ assert.equal(cfg.vaultMind?.embedding?.remoteUrl, "https://example.test");
183
+ assert.equal(cfg.vaultMind?.embedding?.model, "embeddinggemma");
184
+ assert.ok(cfg.vaultMind?.vaults?.default?.path, "vaults.default.path should be written");
185
+ fs.rmSync(vault, { recursive: true, force: true });
186
+ });
187
+ it("derives the default collection name from the vault directory when --collection is omitted", async () => {
188
+ const vault = mkTmpDir("pvm-setup-scaffold-noflag-");
189
+ const { pi, handlers } = captureVmHandler();
190
+ registerCommands(pi);
191
+ const vm = handlers.find((h) => h.name === "vm");
192
+ assert.ok(vm, "/vm command should be registered");
193
+ await vm?.handler(`setup --vault ${vault} --remoteUrl https://example.test --model embeddinggemma`, makeCtx(vault));
194
+ const cfg = JSON.parse(fs.readFileSync(getConfigPath(vault), "utf-8"));
195
+ const expectedCollection = deriveCollectionNameFromVaultPath(vault);
196
+ assert.ok(cfg.collections?.main, "collections.main should be scaffolded");
197
+ assert.ok(cfg.collections.main.path.endsWith(path.join("collections", `${expectedCollection}.jsonl`)));
198
+ assert.ok(fs.existsSync(path.join(vault, "collections", `${expectedCollection}.jsonl`)), "derived collection file should exist on disk");
199
+ fs.rmSync(vault, { recursive: true, force: true });
200
+ });
201
+ });
@@ -23,8 +23,10 @@ const makeCfg = (dataDir) => ({
23
23
  dataDir,
24
24
  embedding: {
25
25
  remoteUrl: "https://pvm.modal.run",
26
- modal: { baseUrl: "https://pvm.modal.run", model: MODEL, dim: DIM },
26
+ model: MODEL,
27
+ dim: DIM,
27
28
  },
29
+ vaults: { default: { path: dataDir, collectionPrefix: "test" } },
28
30
  ftsEnabled: true,
29
31
  },
30
32
  });
@@ -41,12 +43,10 @@ function exportPagesFetch(pages) {
41
43
  count: page.rows.length,
42
44
  done: page.done,
43
45
  };
44
- return {
45
- ok: true,
46
+ return new Response(JSON.stringify(body), {
46
47
  status: 200,
47
- json: async () => body,
48
- text: async () => "",
49
- };
48
+ headers: { "Content-Type": "application/json" },
49
+ });
50
50
  });
51
51
  }
52
52
  const row = (id, seq, vector) => ({
@@ -94,10 +94,10 @@ describe("Modal sync engine", () => {
94
94
  0: { rows: [row("1", 100, [1, 0, 0]), row("2", 150, [0, 1, 0])], next: 150, done: false },
95
95
  150: { rows: [row("3", 200, [0, 0, 1])], next: 200, done: true },
96
96
  });
97
- const res = await syncCollection(cfg, "main");
97
+ const res = await syncCollection(cfg, "main", dir);
98
98
  assert.equal(res.rows, 3);
99
99
  assert.equal(res.watermark, 200);
100
- assert.equal(getWatermark(cfg, "main", MODEL, DIM), 200);
100
+ assert.equal(getWatermark(cfg, "test_main", MODEL, DIM), 200);
101
101
  const conn = await connect(dir);
102
102
  const table = await conn.openTable(`col_main__${MODEL}__${DIM}`);
103
103
  assert.equal(await table.countRows(), 3);
@@ -109,8 +109,8 @@ describe("Modal sync engine", () => {
109
109
  100: { rows: [row("2", 200, [0, 1, 0])], next: 200, done: true },
110
110
  200: { rows: [], next: 200, done: true },
111
111
  });
112
- await syncCollection(cfg, "main");
113
- const second = await syncCollection(cfg, "main"); // since=200 → empty, done
112
+ await syncCollection(cfg, "main", dir);
113
+ const second = await syncCollection(cfg, "main", dir); // since=200 → empty, done
114
114
  assert.equal(second.rows, 0);
115
115
  assert.equal(second.watermark, 200);
116
116
  const conn = await connect(dir);
@@ -123,8 +123,8 @@ describe("Modal sync engine", () => {
123
123
  0: { rows: [row("1", 100, [1, 0, 0])], next: 100, done: false },
124
124
  100: { rows: [row("1", 200, [1, 0, 0])], next: 200, done: true },
125
125
  });
126
- await syncCollection(cfg, "main"); // inserts id=1
127
- await syncCollection(cfg, "main", { full: true }); // re-fetches id=1 → merge by id
126
+ await syncCollection(cfg, "main", dir); // inserts id=1
127
+ await syncCollection(cfg, "main", dir, { full: true }); // re-fetches id=1 → merge by id
128
128
  const conn = await connect(dir);
129
129
  const table = await conn.openTable(`col_main__${MODEL}__${DIM}`);
130
130
  assert.equal(await table.countRows(), 1);
@@ -5,7 +5,9 @@ import { tmpdir } from "node:os";
5
5
  import * as path from "node:path";
6
6
  import { afterEach, beforeEach, describe, it } from "node:test";
7
7
  const setupConfig = (dir) => {
8
- fs.writeFileSync(path.join(dir, "pi-vault-mind.config.json"), JSON.stringify({
8
+ const configDir = path.join(dir, ".vault-mind");
9
+ fs.mkdirSync(configDir, { recursive: true });
10
+ fs.writeFileSync(path.join(configDir, "vault-mind.config.json"), JSON.stringify({
9
11
  version: 2,
10
12
  collections: {
11
13
  main: {
@@ -17,7 +19,7 @@ const setupConfig = (dir) => {
17
19
  injectors: [],
18
20
  vaultMind: {
19
21
  dataDir: ".lancedb",
20
- embedding: { provider: "transformers" },
22
+ embedding: {},
21
23
  ftsEnabled: false,
22
24
  graph: { enabled: false },
23
25
  },
@@ -27,21 +29,17 @@ const setupConfig = (dir) => {
27
29
  describe("tombstone_entry", () => {
28
30
  let testDir;
29
31
  let collectionPath;
30
- let savedHome;
32
+ let savedCwd;
31
33
  beforeEach(() => {
32
34
  testDir = path.join(tmpdir(), `pi-vault-mind-tombstone-test-${randomUUID()}`);
33
35
  fs.mkdirSync(testDir, { recursive: true });
34
36
  setupConfig(testDir);
35
37
  collectionPath = path.join(testDir, "collections", "main.jsonl");
36
- savedHome = process.env.HOME;
37
- process.env.HOME = testDir;
38
- process.env.USERPROFILE = testDir;
38
+ savedCwd = process.cwd();
39
+ process.chdir(testDir);
39
40
  });
40
41
  afterEach(() => {
41
- if (savedHome !== undefined)
42
- process.env.HOME = savedHome;
43
- else
44
- process.env.HOME = undefined;
42
+ process.chdir(savedCwd);
45
43
  if (fs.existsSync(testDir))
46
44
  fs.rmSync(testDir, { recursive: true });
47
45
  });
@@ -0,0 +1,86 @@
1
+ import { strict as assert } from "node:assert";
2
+ import { describe, it } from "node:test";
3
+ import { listAllTools, listBuiltinTools, listExtensionTools } from "../src/tool-catalog.js";
4
+ import { registerTools } from "../src/tools.js";
5
+ describe("listAllTools", () => {
6
+ it("returns 30 entries", () => {
7
+ const tools = listAllTools();
8
+ assert.equal(tools.length, 30);
9
+ });
10
+ });
11
+ describe("listBuiltinTools", () => {
12
+ it("returns 7 entries, all kind: builtin", () => {
13
+ const tools = listBuiltinTools();
14
+ assert.equal(tools.length, 7);
15
+ for (const t of tools) {
16
+ assert.equal(t.kind, "builtin", `tool ${t.name} has kind ${t.kind}, expected builtin`);
17
+ }
18
+ });
19
+ });
20
+ describe("listExtensionTools", () => {
21
+ it("returns 23 entries, all kind starts with vm_", () => {
22
+ const tools = listExtensionTools();
23
+ assert.equal(tools.length, 23);
24
+ for (const t of tools) {
25
+ assert.ok(t.kind.startsWith("vm_"), `tool ${t.name} has kind ${t.kind}, expected vm_*`);
26
+ }
27
+ });
28
+ });
29
+ describe("tool catalog integrity", () => {
30
+ it("has no duplicate names across all tools", () => {
31
+ const tools = listAllTools();
32
+ const names = tools.map((t) => t.name);
33
+ const unique = new Set(names);
34
+ assert.equal(unique.size, names.length, `duplicate names: ${names}`);
35
+ });
36
+ it("every entry has a non-empty name and description", () => {
37
+ const tools = listAllTools();
38
+ for (const t of tools) {
39
+ assert.ok(t.name.length > 0, "tool has empty name");
40
+ assert.ok(t.description.length > 0, `tool ${t.name} has empty description`);
41
+ }
42
+ });
43
+ it("listAllTools equals listBuiltinTools + listExtensionTools", () => {
44
+ const all = listAllTools();
45
+ const builtin = listBuiltinTools();
46
+ const ext = listExtensionTools();
47
+ assert.equal(all.length, builtin.length + ext.length);
48
+ const allNames = new Set(all.map((t) => t.name));
49
+ const combinedNames = new Set([...builtin.map((t) => t.name), ...ext.map((t) => t.name)]);
50
+ assert.deepStrictEqual(allNames, combinedNames);
51
+ });
52
+ it("vm_search has mode parameter schema", () => {
53
+ const ext = listExtensionTools();
54
+ const vmSearch = ext.find((t) => t.name === "vm_search");
55
+ assert.ok(vmSearch, "vm_search not found");
56
+ assert.ok(vmSearch?.params, "vm_search missing params schema");
57
+ const params = vmSearch?.params;
58
+ const properties = params.properties;
59
+ assert.deepStrictEqual(properties.mode, {
60
+ type: "string",
61
+ enum: ["auto", "hybrid", "semantic", "fts", "graph", "vault", "full"],
62
+ default: "hybrid",
63
+ });
64
+ assert.deepStrictEqual(params.required, ["query"]);
65
+ });
66
+ });
67
+ describe("tool catalog registry drift guard", () => {
68
+ it("extension tools in the catalog exactly match registerTools output", () => {
69
+ const captured = new Set();
70
+ const capturedList = [];
71
+ const mock = {
72
+ registerTool(tool) {
73
+ captured.add(tool.name);
74
+ capturedList.push({ name: tool.name });
75
+ },
76
+ getAllTools() {
77
+ return capturedList;
78
+ },
79
+ };
80
+ registerTools(mock);
81
+ const catalogExtension = new Set(listAllTools()
82
+ .filter((t) => t.kind !== "builtin")
83
+ .map((t) => t.name));
84
+ assert.deepStrictEqual(captured, catalogExtension, "catalog must equal the registered tool set");
85
+ });
86
+ });