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
@@ -1,95 +1,121 @@
1
1
  import { strict as assert } from "node:assert";
2
+ import * as fs from "node:fs";
3
+ import * as os from "node:os";
4
+ import * as path from "node:path";
2
5
  import { afterEach, beforeEach, describe, it } from "node:test";
3
- import { MODAL_TOKEN_ENV, MODAL_TOKEN_ENV_PATH, createModalClient, isModalConfigured, modalUrl, namespacedTableName, resolveBaseUrl, resolveDim, resolveModalToken, resolveModel, resolveWorkspace, } from "../src/modal-config.js";
6
+ import { createModalClient, getModalToken, isModalConfigured, modalTokenEnvPath, modalUrl, namespacedTableName, resolveBaseUrl, resolveDim, resolveModalToken, resolveModel, resolveWorkspace, } from "../src/modal-config.js";
4
7
  const makeCfg = (over = {}) => ({
5
8
  dataDir: ".lancedb",
6
9
  embedding: { remoteUrl: "https://pvm.modal.run" },
7
10
  ...over,
8
11
  });
12
+ const REMOTE_ENV = "PVM_REMOTE_EMBEDDING_API_KEY";
13
+ const BRIDGE_ENV = "PVM_API_TOKEN";
14
+ const PROVIDER_ENV_KEYS = [
15
+ REMOTE_ENV,
16
+ BRIDGE_ENV,
17
+ "PVM_MODAL_READ_TOKEN",
18
+ "PVM_MODAL_WRITE_TOKEN",
19
+ ];
9
20
  describe("modal-config", () => {
10
- let origToken;
11
- let dotenvExisted = false;
12
- let dotenvBak = "";
13
- beforeEach(async () => {
14
- origToken = process.env[MODAL_TOKEN_ENV];
15
- // Also guard against the dotenv file (~/.pi/agent/vault-mind.env)
16
- // which resolveModalToken reads as a fallback.
17
- const fs = await import("node:fs");
18
- if (fs.existsSync(MODAL_TOKEN_ENV_PATH)) {
19
- dotenvExisted = true;
20
- dotenvBak = `${MODAL_TOKEN_ENV_PATH}.bak`;
21
- fs.renameSync(MODAL_TOKEN_ENV_PATH, dotenvBak);
21
+ let originalCwd;
22
+ let testDir;
23
+ let savedEnvironment;
24
+ beforeEach(() => {
25
+ originalCwd = process.cwd();
26
+ testDir = fs.mkdtempSync(path.join(os.tmpdir(), "pvm-modal-config-"));
27
+ process.chdir(testDir);
28
+ savedEnvironment = {};
29
+ for (const key of PROVIDER_ENV_KEYS) {
30
+ savedEnvironment[key] = process.env[key];
31
+ delete process.env[key];
22
32
  }
23
33
  });
24
- afterEach(async () => {
25
- if (origToken === undefined)
26
- delete process.env[MODAL_TOKEN_ENV];
27
- else
28
- process.env[MODAL_TOKEN_ENV] = origToken;
29
- if (dotenvExisted) {
30
- const fs = await import("node:fs");
31
- fs.renameSync(dotenvBak, MODAL_TOKEN_ENV_PATH);
32
- dotenvExisted = false;
34
+ afterEach(() => {
35
+ for (const key of PROVIDER_ENV_KEYS) {
36
+ const value = savedEnvironment[key];
37
+ if (value === undefined)
38
+ delete process.env[key];
39
+ else
40
+ process.env[key] = value;
33
41
  }
42
+ process.chdir(originalCwd);
43
+ fs.rmSync(testDir, { recursive: true, force: true });
34
44
  });
35
- it("resolveModalToken: env wins over config apiToken", () => {
36
- process.env[MODAL_TOKEN_ENV] = "env-token";
45
+ it("resolveModalToken: remote provider env wins over legacy config", () => {
46
+ process.env[REMOTE_ENV] = "REMOTE_ENV_SENTINEL";
37
47
  const cfg = makeCfg({
38
48
  embedding: {
39
49
  remoteUrl: "https://pvm.modal.run",
40
- modal: { baseUrl: "https://x", apiToken: "cfg-token" },
50
+ remoteApiKey: "REMOTE_LEGACY_SENTINEL",
41
51
  },
42
52
  });
43
- assert.equal(resolveModalToken(cfg), "env-token");
53
+ assert.equal(resolveModalToken(cfg), "REMOTE_ENV_SENTINEL");
44
54
  });
45
- it("resolveModalToken: config apiToken is the fallback", () => {
46
- delete process.env[MODAL_TOKEN_ENV];
55
+ it("resolveModalToken: legacy config is the fallback", () => {
47
56
  const cfg = makeCfg({
48
57
  embedding: {
49
58
  remoteUrl: "https://pvm.modal.run",
50
- modal: { baseUrl: "https://x", apiToken: "cfg-token" },
59
+ remoteApiKey: "REMOTE_LEGACY_SENTINEL",
51
60
  },
52
61
  });
53
- assert.equal(resolveModalToken(cfg), "cfg-token");
54
- });
55
- it("resolveModalToken: undefined when neither set", () => {
56
- delete process.env[MODAL_TOKEN_ENV];
57
- const cfg = makeCfg({
58
- embedding: { remoteUrl: "https://pvm.modal.run", modal: { baseUrl: "https://x" } },
62
+ assert.equal(resolveModalToken(cfg), "REMOTE_LEGACY_SENTINEL");
63
+ });
64
+ it("PVM_API_TOKEN never resolves a remote provider credential", () => {
65
+ const cfg = makeCfg();
66
+ process.env[BRIDGE_ENV] = "BRIDGE_ONLY_SENTINEL";
67
+ const fromProcess = [
68
+ resolveModalToken(cfg),
69
+ getModalToken(cfg, "read"),
70
+ getModalToken(cfg, "write"),
71
+ ];
72
+ delete process.env[BRIDGE_ENV];
73
+ fs.mkdirSync(path.dirname(modalTokenEnvPath()), { recursive: true });
74
+ fs.writeFileSync(modalTokenEnvPath(), 'PVM_API_TOKEN="BRIDGE_ONLY_SENTINEL"\n', "utf-8");
75
+ const fromStore = [
76
+ resolveModalToken(cfg),
77
+ getModalToken(cfg, "read"),
78
+ getModalToken(cfg, "write"),
79
+ ];
80
+ const result = { fromProcess, fromStore };
81
+ assert.deepEqual(result, {
82
+ fromProcess: [undefined, undefined, undefined],
83
+ fromStore: [undefined, undefined, undefined],
59
84
  });
60
- assert.equal(resolveModalToken(cfg), undefined);
85
+ assert.equal(JSON.stringify(result).includes("BRIDGE_ONLY_SENTINEL"), false);
86
+ assert.equal(JSON.stringify(result).includes("REMOTE_PROVIDER_SENTINEL"), false);
61
87
  });
62
88
  it("createModalClient: null when not configured (no baseUrl or token)", () => {
63
- delete process.env[MODAL_TOKEN_ENV];
89
+ delete process.env[REMOTE_ENV];
64
90
  assert.equal(createModalClient(makeCfg()), null);
65
91
  assert.equal(createModalClient(makeCfg({
66
- embedding: { remoteUrl: "https://pvm.modal.run", modal: { baseUrl: "https://x" } },
92
+ embedding: { remoteUrl: "https://pvm.modal.run" },
67
93
  })), null);
68
94
  });
69
95
  it("createModalClient: builds a client when baseUrl + token present", () => {
70
- process.env[MODAL_TOKEN_ENV] = "tok";
96
+ process.env[REMOTE_ENV] = "tok";
71
97
  const c = createModalClient(makeCfg({
72
- embedding: { remoteUrl: "https://pvm.modal.run", modal: { baseUrl: "https://x/" } },
98
+ embedding: { remoteUrl: "https://pvm.modal.run" },
73
99
  }));
74
100
  assert.ok(c);
75
101
  });
76
102
  it("isModalConfigured: requires provider modal + baseUrl + token", () => {
77
- delete process.env[MODAL_TOKEN_ENV];
103
+ delete process.env[REMOTE_ENV];
78
104
  assert.equal(isModalConfigured(makeCfg()), false);
79
- process.env[MODAL_TOKEN_ENV] = "tok";
105
+ process.env[REMOTE_ENV] = "tok";
80
106
  assert.equal(isModalConfigured(makeCfg({
81
- embedding: { remoteUrl: "https://pvm.modal.run", modal: { baseUrl: "https://x" } },
107
+ embedding: { remoteUrl: "https://pvm.modal.run" },
82
108
  })), true);
83
109
  // ollama provider is not modal-configured
84
110
  assert.equal(isModalConfigured(makeCfg({
85
- embedding: { localUrl: "http://127.0.0.1:11434/v1", modal: { baseUrl: "https://x" } },
111
+ embedding: { localUrl: "http://127.0.0.1:11434/v1" },
86
112
  })), false);
87
113
  });
88
- it("resolveModel: per-collection override wins, then modal.model, default embeddinggemma", () => {
114
+ it("resolveModel: per-collection override wins, then embedding.model, default embeddinggemma", () => {
89
115
  const cfg = makeCfg({
90
116
  embedding: {
91
117
  remoteUrl: "https://pvm.modal.run",
92
- modal: { baseUrl: "https://x", model: "global-model" },
118
+ model: "global-model",
93
119
  collectionModels: { special: { model: "special-model", dim: 128 } },
94
120
  },
95
121
  });
@@ -98,11 +124,11 @@ describe("modal-config", () => {
98
124
  assert.equal(resolveModel(cfg, "other"), "global-model");
99
125
  assert.equal(resolveModel(makeCfg()), "embeddinggemma");
100
126
  });
101
- it("resolveDim: per-collection override wins, then modal.dim", () => {
127
+ it("resolveDim: per-collection override wins, then embedding.dim", () => {
102
128
  const cfg = makeCfg({
103
129
  embedding: {
104
130
  remoteUrl: "https://pvm.modal.run",
105
- modal: { baseUrl: "https://x", dim: 512 },
131
+ dim: 512,
106
132
  collectionModels: { special: { model: "m", dim: 128 } },
107
133
  },
108
134
  });
@@ -110,63 +136,50 @@ describe("modal-config", () => {
110
136
  assert.equal(resolveDim(cfg, "special"), 128);
111
137
  assert.equal(resolveDim(makeCfg()), undefined);
112
138
  });
113
- it("resolveModalToken: dotenv file is a fallback when env is unset", async () => {
114
- delete process.env[MODAL_TOKEN_ENV];
115
- const fs = await import("node:fs");
116
- const path = await import("node:path");
117
- const dir = path.dirname(MODAL_TOKEN_ENV_PATH);
118
- if (!fs.existsSync(dir))
119
- fs.mkdirSync(dir, { recursive: true });
120
- fs.writeFileSync(MODAL_TOKEN_ENV_PATH, 'PVM_API_TOKEN="dotenv-token"\n', "utf-8");
121
- try {
122
- const cfg = makeCfg({
123
- embedding: { remoteUrl: "https://pvm.modal.run", modal: { baseUrl: "https://x" } },
124
- });
125
- assert.equal(resolveModalToken(cfg), "dotenv-token");
126
- }
127
- finally {
128
- fs.rmSync(MODAL_TOKEN_ENV_PATH, { force: true });
129
- }
139
+ it("resolveModalToken: dotenv file is a fallback when env is unset", () => {
140
+ fs.mkdirSync(path.dirname(modalTokenEnvPath()), { recursive: true });
141
+ fs.writeFileSync(modalTokenEnvPath(), 'PVM_REMOTE_EMBEDDING_API_KEY="dotenv-token"\n', "utf-8");
142
+ const cfg = makeCfg({
143
+ embedding: { remoteUrl: "https://pvm.modal.run" },
144
+ });
145
+ assert.equal(resolveModalToken(cfg), "dotenv-token");
130
146
  });
131
147
  it("modalUrl derives the canonical service URL from workspace", () => {
132
148
  assert.equal(modalUrl("kylebrodeur"), "https://kylebrodeur--pi-vault-mind-embed-embeddingservice-fastapi-app.modal.run");
133
149
  });
134
- it("resolveBaseUrl: explicit baseUrl wins over derived workspace URL", () => {
150
+ it("resolveBaseUrl: explicit remoteUrl wins over derived workspace URL", () => {
135
151
  const cfg = makeCfg({
136
152
  embedding: {
137
- remoteUrl: "https://pvm.modal.run",
138
- modal: {
139
- baseUrl: "https://explicit.example.com",
140
- workspace: "kylebrodeur",
141
- },
153
+ remoteUrl: "https://explicit.example.com",
154
+ workspace: "kylebrodeur",
142
155
  },
143
156
  });
144
157
  assert.equal(resolveBaseUrl(cfg), "https://explicit.example.com");
145
158
  });
146
- it("resolveBaseUrl: derives URL from workspace when baseUrl is missing", () => {
159
+ it("resolveBaseUrl: derives URL from workspace when remoteUrl is missing", () => {
147
160
  const cfg = makeCfg({
148
- embedding: { remoteUrl: "https://pvm.modal.run", modal: { workspace: "kylebrodeur" } },
161
+ embedding: { workspace: "kylebrodeur" },
149
162
  });
150
163
  assert.equal(resolveBaseUrl(cfg), modalUrl("kylebrodeur"));
151
164
  });
152
165
  it("resolveWorkspace returns the configured workspace slug", () => {
153
166
  const cfg = makeCfg({
154
- embedding: { remoteUrl: "https://pvm.modal.run", modal: { workspace: "kylebrodeur" } },
167
+ embedding: { remoteUrl: "https://pvm.modal.run", workspace: "kylebrodeur" },
155
168
  });
156
169
  assert.equal(resolveWorkspace(cfg), "kylebrodeur");
157
170
  assert.equal(resolveWorkspace(makeCfg()), undefined);
158
171
  });
159
172
  it("createModalClient: builds a client from workspace-derived URL", () => {
160
- process.env[MODAL_TOKEN_ENV] = "tok";
173
+ process.env[REMOTE_ENV] = "tok";
161
174
  const c = createModalClient(makeCfg({
162
- embedding: { remoteUrl: "https://pvm.modal.run", modal: { workspace: "kylebrodeur" } },
175
+ embedding: { workspace: "kylebrodeur" },
163
176
  }));
164
177
  assert.ok(c);
165
178
  });
166
- it("isModalConfigured: true when workspace + token are present (no explicit baseUrl)", () => {
167
- process.env[MODAL_TOKEN_ENV] = "tok";
179
+ it("isModalConfigured: true when remoteUrl + token are present (no explicit workspace)", () => {
180
+ process.env[REMOTE_ENV] = "tok";
168
181
  assert.equal(isModalConfigured(makeCfg({
169
- embedding: { remoteUrl: "https://pvm.modal.run", modal: { workspace: "kylebrodeur" } },
182
+ embedding: { remoteUrl: "https://pvm.modal.run", workspace: "kylebrodeur" },
170
183
  })), true);
171
184
  });
172
185
  it("namespacedTableName mirrors the server's ADR-3 scheme", () => {
@@ -0,0 +1,122 @@
1
+ import { strict as assert } from "node:assert";
2
+ import * as fs from "node:fs";
3
+ import * as path from "node:path";
4
+ import { after, before, beforeEach, describe, it } from "node:test";
5
+ import { _resetAuthCache } from "../src/auth.js";
6
+ import { fakePi, fetchJson, makeVault, startTestServer, TEST_TOKEN, } from "./helpers/server.js";
7
+ const envVar = "PVM_API_TOKEN";
8
+ const readyResponse = {
9
+ status: "ready",
10
+ model: { id: "test-model-id", name: "Test Model Name", provider: "test-provider" },
11
+ };
12
+ const allStatuses = [
13
+ { status: "model-registry-error" },
14
+ { status: "model-registry-empty" },
15
+ { status: "provider-authentication-required", authSource: "models_json_key" },
16
+ { status: "configured-provider-unavailable", providerId: "test-provider-down" },
17
+ { status: "model-selection-required" },
18
+ { status: "current-model-unavailable", modelId: "test-current-model" },
19
+ readyResponse,
20
+ ];
21
+ describe("GET /vault-mind/model-readiness", () => {
22
+ let vaultPath;
23
+ let port;
24
+ let srv;
25
+ let savedEnv;
26
+ let savedCwd;
27
+ let callbackCalls;
28
+ const responseSentinel = "CALLBACK_RESPONSE_SENTINEL_42";
29
+ const baseCallback = async () => {
30
+ callbackCalls++;
31
+ return { ...readyResponse, model: { ...readyResponse.model, name: responseSentinel } };
32
+ };
33
+ before(async () => {
34
+ savedEnv = process.env[envVar];
35
+ savedCwd = process.cwd();
36
+ process.env[envVar] = TEST_TOKEN;
37
+ vaultPath = makeVault("model-readiness");
38
+ process.chdir(vaultPath);
39
+ fs.mkdirSync(path.join(vaultPath, ".vault-mind"), { recursive: true });
40
+ srv = await startTestServer(fakePi, baseCallback);
41
+ port = srv.port;
42
+ });
43
+ after(() => {
44
+ try {
45
+ srv.stop();
46
+ }
47
+ catch {
48
+ // ignore double-stop
49
+ }
50
+ if (savedEnv === undefined)
51
+ delete process.env[envVar];
52
+ else
53
+ process.env[envVar] = savedEnv;
54
+ process.chdir(savedCwd);
55
+ if (fs.existsSync(vaultPath))
56
+ fs.rmSync(vaultPath, { recursive: true, force: true });
57
+ _resetAuthCache();
58
+ });
59
+ beforeEach(() => {
60
+ callbackCalls = 0;
61
+ });
62
+ it("requires bearer auth and rejects requests without a token", async () => {
63
+ const { status, body } = await fetchJson(port, "GET", "/vault-mind/model-readiness");
64
+ assert.equal(status, 401);
65
+ assert.ok(typeof body === "object" && body !== null);
66
+ assert.equal(body.error, "unauthorized");
67
+ assert.equal(callbackCalls, 0, "callback must not run for unauthenticated request");
68
+ assert.equal(JSON.stringify(body).includes(responseSentinel), false, "sentinel must not leak");
69
+ });
70
+ it("rejects non-GET methods with 405", async () => {
71
+ const { status, body } = await fetchJson(port, "POST", "/vault-mind/model-readiness", {
72
+ token: TEST_TOKEN,
73
+ });
74
+ assert.equal(status, 405);
75
+ assert.ok(typeof body === "object" && body !== null);
76
+ assert.equal(body.error, "Method not allowed");
77
+ assert.equal(callbackCalls, 0, "callback must not run for disallowed method");
78
+ assert.equal(JSON.stringify(body).includes(responseSentinel), false, "sentinel must not leak");
79
+ });
80
+ it("returns 500 when the readiness callback is not installed", async () => {
81
+ const previous = srv.serverState.getModelReadiness;
82
+ srv.serverState.getModelReadiness = undefined;
83
+ try {
84
+ const { status, body } = await fetchJson(port, "GET", "/vault-mind/model-readiness", {
85
+ token: TEST_TOKEN,
86
+ });
87
+ assert.equal(status, 500);
88
+ assert.ok(typeof body === "object" && body !== null);
89
+ assert.equal(body.error, "Readiness callback not installed");
90
+ }
91
+ finally {
92
+ srv.serverState.getModelReadiness = previous;
93
+ }
94
+ });
95
+ it("returns every valid readiness status unchanged", async () => {
96
+ for (const expected of allStatuses) {
97
+ srv.serverState.getModelReadiness = async () => expected;
98
+ const { status, body } = await fetchJson(port, "GET", "/vault-mind/model-readiness", {
99
+ token: TEST_TOKEN,
100
+ });
101
+ assert.equal(status, 200, `expected 200 for status ${expected.status}`);
102
+ assert.deepEqual(body, expected, `status ${expected.status} must round-trip unchanged`);
103
+ }
104
+ });
105
+ it("redacts callback failures to model-registry-error without leaking exception details", async () => {
106
+ const sentinel = "SENTINEL_EXCEPTION_TEXT_98765";
107
+ srv.serverState.getModelReadiness = async () => {
108
+ throw new Error(sentinel);
109
+ };
110
+ const { status, body } = await fetchJson(port, "GET", "/vault-mind/model-readiness", {
111
+ token: TEST_TOKEN,
112
+ });
113
+ assert.equal(status, 200);
114
+ assert.deepEqual(body, { status: "model-registry-error" });
115
+ assert.equal(JSON.stringify(body).includes(sentinel), false, "sentinel must not leak");
116
+ });
117
+ it("clears the readiness callback on server stop", () => {
118
+ assert.equal(typeof srv.serverState.getModelReadiness, "function");
119
+ srv.stop();
120
+ assert.equal(srv.serverState.getModelReadiness, undefined);
121
+ });
122
+ });
@@ -0,0 +1,99 @@
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 { defaultModelRouterChoices, scaffoldModelRouterConfig } from "../src/model-router.js";
8
+ import { resolveVaultMindPaths } from "../src/utils.js";
9
+ const savedAgentDir = process.env.PI_CODING_AGENT_DIR;
10
+ const savedHome = process.env.HOME;
11
+ describe("defaultModelRouterChoices", () => {
12
+ it("returns the tuned primary model and a 4-entry fallback sequence", () => {
13
+ const { primary, fallbackSequence } = defaultModelRouterChoices();
14
+ assert.equal(primary, "ollama/gemma4:31b-cloud");
15
+ assert.ok(Array.isArray(fallbackSequence));
16
+ assert.equal(fallbackSequence.length, 4);
17
+ });
18
+ it("includes the common baseline entries regardless of platform", () => {
19
+ const { fallbackSequence } = defaultModelRouterChoices();
20
+ for (const entry of ["ollama/gemma4:e4b", "ollama/gemma3:4b", "ollama/*"]) {
21
+ assert.ok(fallbackSequence.includes(entry), `expected fallbackSequence to include ${entry}`);
22
+ }
23
+ });
24
+ it("includes exactly one of the platform-specific mid-tier variants", () => {
25
+ const { fallbackSequence } = defaultModelRouterChoices();
26
+ const macVariant = fallbackSequence.includes("ollama/gemma4:12b-mlx");
27
+ const otherVariant = fallbackSequence.includes("ollama/gemma4:12b");
28
+ // Exactly one of the two platform-specific entries is present — never both,
29
+ // never neither — without hardcoding which platform CI happens to run on.
30
+ assert.notEqual(macVariant, otherVariant);
31
+ });
32
+ });
33
+ describe("scaffoldModelRouterConfig", () => {
34
+ let testDir;
35
+ beforeEach(() => {
36
+ testDir = path.join(tmpdir(), `model-router-test-${randomUUID()}`);
37
+ fs.mkdirSync(testDir, { recursive: true });
38
+ // Isolate the resolved agent dir under the temp vault so this test never
39
+ // touches the real ~/.pi/agent (getAgentDir() falls back to homedir()
40
+ // when PI_CODING_AGENT_DIR is unset).
41
+ process.env.PI_CODING_AGENT_DIR = path.join(testDir, ".vault-mind", ".pi", "agent");
42
+ process.env.HOME = testDir;
43
+ });
44
+ afterEach(() => {
45
+ if (savedAgentDir === undefined) {
46
+ Reflect.deleteProperty(process.env, "PI_CODING_AGENT_DIR");
47
+ }
48
+ else {
49
+ process.env.PI_CODING_AGENT_DIR = savedAgentDir;
50
+ }
51
+ if (savedHome === undefined) {
52
+ Reflect.deleteProperty(process.env, "HOME");
53
+ }
54
+ else {
55
+ process.env.HOME = savedHome;
56
+ }
57
+ try {
58
+ fs.rmSync(testDir, { recursive: true, force: true });
59
+ }
60
+ catch {
61
+ /* ignore */
62
+ }
63
+ });
64
+ it("resolves to resolveVaultMindPaths(cwd).modelRouter", () => {
65
+ const expectedPath = resolveVaultMindPaths(testDir).modelRouter;
66
+ const result = scaffoldModelRouterConfig(testDir);
67
+ assert.equal(result.path, expectedPath);
68
+ });
69
+ it("creates model-router.json with the tuned defaults on first call", () => {
70
+ const expectedPath = resolveVaultMindPaths(testDir).modelRouter;
71
+ const result = scaffoldModelRouterConfig(testDir);
72
+ assert.deepEqual(result, { created: true, path: expectedPath });
73
+ assert.ok(fs.existsSync(expectedPath), "expected model-router.json to be written");
74
+ const parsed = JSON.parse(fs.readFileSync(expectedPath, "utf-8"));
75
+ assert.equal(parsed.defaultProfile, "auto");
76
+ assert.equal(typeof parsed.features, "object");
77
+ assert.ok(parsed.features);
78
+ assert.equal(typeof parsed.rateLimitFallback, "object");
79
+ assert.ok(Array.isArray(parsed.rateLimitFallback.fallbackSequence));
80
+ for (const tier of ["high", "medium", "low"]) {
81
+ const profile = parsed.profiles.auto[tier];
82
+ assert.ok(profile, `expected profiles.auto.${tier} to be defined`);
83
+ assert.equal(typeof profile.model, "string");
84
+ assert.equal(typeof profile.thinking, "string");
85
+ }
86
+ });
87
+ it("never overwrites an existing model-router.json (idempotent)", () => {
88
+ const expectedPath = resolveVaultMindPaths(testDir).modelRouter;
89
+ const first = scaffoldModelRouterConfig(testDir);
90
+ assert.equal(first.created, true);
91
+ const written = JSON.parse(fs.readFileSync(expectedPath, "utf-8"));
92
+ written.defaultProfile = "custom";
93
+ fs.writeFileSync(expectedPath, `${JSON.stringify(written, null, 2)}\n`, "utf-8");
94
+ const second = scaffoldModelRouterConfig(testDir);
95
+ assert.deepEqual(second, { created: false, path: expectedPath });
96
+ const unchanged = JSON.parse(fs.readFileSync(expectedPath, "utf-8"));
97
+ assert.equal(unchanged.defaultProfile, "custom");
98
+ });
99
+ });
@@ -0,0 +1,42 @@
1
+ import { strict as assert } from "node:assert";
2
+ import { afterEach, beforeEach, describe, it } from "node:test";
3
+ import { VaultMindClient } from "../packages/obsidian/src/client.js";
4
+ describe("VaultMindClient", () => {
5
+ let originalFetch;
6
+ beforeEach(() => {
7
+ originalFetch = globalThis.fetch;
8
+ });
9
+ afterEach(() => {
10
+ globalThis.fetch = originalFetch;
11
+ });
12
+ it("listActivity encodes filters and returns activity events", async () => {
13
+ const jobId = "job/with spaces&symbols";
14
+ const responseBody = [
15
+ {
16
+ id: 1,
17
+ jobId,
18
+ ts: "2026-07-12T00:00:00.000Z",
19
+ kind: "status",
20
+ label: "status changed",
21
+ tone: "neutral",
22
+ },
23
+ ];
24
+ let captured;
25
+ globalThis.fetch = async (input, init) => {
26
+ const rawUrl = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
27
+ captured = { url: new URL(rawUrl), init: init ?? {} };
28
+ return new Response(JSON.stringify(responseBody), {
29
+ status: 200,
30
+ headers: { "Content-Type": "application/json" },
31
+ });
32
+ };
33
+ const client = new VaultMindClient({ host: "127.0.0.1", port: 11435, token: "client-token" });
34
+ const result = await client.listActivity({ jobId, limit: 7 });
35
+ assert.deepEqual(result, responseBody);
36
+ assert.ok(captured);
37
+ assert.equal(captured.url.pathname, "/agent/activity");
38
+ assert.equal(captured.url.searchParams.get("jobId"), jobId);
39
+ assert.equal(captured.url.searchParams.get("limit"), "7");
40
+ assert.equal(captured.init.method, "GET");
41
+ });
42
+ });
@@ -17,7 +17,7 @@ const setupConfig = (dir) => {
17
17
  injectors: [],
18
18
  vaultMind: {
19
19
  dataDir: ".lancedb",
20
- embedding: { provider: "transformers" },
20
+ embedding: {},
21
21
  ftsEnabled: false,
22
22
  graph: { enabled: false },
23
23
  },