pi-vault-mind 0.7.4 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/README.md +21 -21
  2. package/dist/src/agent-queue.d.ts +108 -0
  3. package/dist/src/agent-queue.js +225 -0
  4. package/dist/src/auth.d.ts +30 -0
  5. package/dist/src/auth.js +93 -0
  6. package/dist/src/auto-index.d.ts +47 -0
  7. package/dist/src/auto-index.js +122 -0
  8. package/dist/src/commands.js +265 -4
  9. package/dist/src/discover-schema.d.ts +27 -0
  10. package/dist/src/discover-schema.js +255 -0
  11. package/dist/src/index.js +2 -0
  12. package/dist/src/intake.d.ts +4 -0
  13. package/dist/src/intake.js +248 -0
  14. package/dist/src/server.d.ts +31 -0
  15. package/dist/src/server.js +561 -4
  16. package/dist/src/settings-ui.js +11 -11
  17. package/dist/src/tombstone.d.ts +13 -0
  18. package/dist/src/tombstone.js +103 -0
  19. package/dist/src/tools.d.ts +8 -0
  20. package/dist/src/tools.js +58 -4
  21. package/dist/src/types.d.ts +47 -0
  22. package/dist/src/types.js +1 -0
  23. package/dist/src/utils.d.ts +4 -2
  24. package/dist/src/utils.js +9 -4
  25. package/dist/src/watcher.d.ts +7 -1
  26. package/dist/src/watcher.js +170 -11
  27. package/dist/test/agent-queue.test.js +204 -0
  28. package/dist/test/auth.test.js +136 -0
  29. package/dist/test/auto-indexer.test.js +2 -0
  30. package/dist/test/dedup.test.js +62 -0
  31. package/dist/test/discover-schema.test.js +201 -0
  32. package/dist/test/modal-config.test.js +17 -2
  33. package/dist/test/queue-events.test.js +146 -0
  34. package/dist/test/rest-queue.test.js +234 -0
  35. package/dist/test/rest-setup.test.js +213 -0
  36. package/dist/test/rest-wiki.test.js +161 -0
  37. package/dist/test/server-ws.test.js +219 -0
  38. package/dist/test/tombstone.test.js +233 -0
  39. package/package.json +6 -2
  40. package/scripts/e2e-ws.mjs +364 -0
  41. package/scripts/fetch-modal-token.sh +66 -0
  42. package/scripts/modal-e2e-smoke.mjs +171 -0
  43. package/scripts/setup-test-config.sh +66 -0
  44. package/dist/test/index.test.d.ts +0 -1
@@ -0,0 +1,146 @@
1
+ import { strict as assert } from "node:assert";
2
+ import * as fs from "node:fs";
3
+ import * as path from "node:path";
4
+ import { afterEach, beforeEach, describe, it } from "node:test";
5
+ import { cancelJob, createJob, listJobs, markJobDone, markJobFailed, markJobRunning, subscribeQueue, } from "../src/agent-queue.js";
6
+ const tmpDir = () => path.join(process.cwd(), ".tmp-queue-events-test");
7
+ const makeVault = () => {
8
+ const vaultPath = tmpDir();
9
+ if (fs.existsSync(vaultPath))
10
+ fs.rmSync(vaultPath, { recursive: true });
11
+ fs.mkdirSync(vaultPath, { recursive: true });
12
+ return vaultPath;
13
+ };
14
+ const seed = (vaultPath, id) => createJob(vaultPath, {
15
+ id,
16
+ vaultPath,
17
+ filePath: path.join(vaultPath, "test.md"),
18
+ role: "miner",
19
+ agentName: "vault-mind-miner",
20
+ instruction: "test",
21
+ markerCount: 1,
22
+ });
23
+ describe("agent-queue event emitter", () => {
24
+ let vaultPath;
25
+ beforeEach(() => {
26
+ vaultPath = makeVault();
27
+ });
28
+ afterEach(() => {
29
+ if (fs.existsSync(vaultPath))
30
+ fs.rmSync(vaultPath, { recursive: true });
31
+ });
32
+ it("emits job-created when a job is created", () => {
33
+ const events = [];
34
+ const off = subscribeQueue((e) => events.push(e));
35
+ try {
36
+ const job = seed(vaultPath, "m1");
37
+ assert.equal(events.length, 1);
38
+ assert.equal(events[0].type, "job-created");
39
+ assert.equal(events[0].job.id, "m1");
40
+ assert.equal(events[0].job.status, "pending");
41
+ assert.equal(job.status, "pending");
42
+ }
43
+ finally {
44
+ off();
45
+ }
46
+ });
47
+ it("emits job-updated when markJobRunning is called", () => {
48
+ seed(vaultPath, "m1");
49
+ const events = [];
50
+ const off = subscribeQueue((e) => events.push(e));
51
+ try {
52
+ markJobRunning(vaultPath, "m1");
53
+ assert.equal(events.length, 1);
54
+ assert.equal(events[0].type, "job-updated");
55
+ assert.equal(events[0].job.status, "running");
56
+ }
57
+ finally {
58
+ off();
59
+ }
60
+ });
61
+ it("emits job-completed when markJobDone is called", () => {
62
+ seed(vaultPath, "m1");
63
+ markJobRunning(vaultPath, "m1");
64
+ const events = [];
65
+ const off = subscribeQueue((e) => events.push(e));
66
+ try {
67
+ markJobDone(vaultPath, "m1");
68
+ assert.equal(events.length, 1);
69
+ assert.equal(events[0].type, "job-completed");
70
+ assert.equal(events[0].job.status, "done");
71
+ }
72
+ finally {
73
+ off();
74
+ }
75
+ });
76
+ it("emits job-completed for failed and cancelled transitions", () => {
77
+ seed(vaultPath, "m1");
78
+ markJobRunning(vaultPath, "m1");
79
+ const events = [];
80
+ const off = subscribeQueue((e) => events.push(e));
81
+ try {
82
+ markJobFailed(vaultPath, "m1", "timeout");
83
+ cancelJob(vaultPath, "m2"); // does not exist; no event
84
+ const types = events.map((e) => e.type);
85
+ assert.deepEqual(types, ["job-completed"]);
86
+ }
87
+ finally {
88
+ off();
89
+ }
90
+ });
91
+ it("does not emit for cancel on non-pending jobs", () => {
92
+ seed(vaultPath, "m1");
93
+ markJobRunning(vaultPath, "m1");
94
+ const events = [];
95
+ const off = subscribeQueue((e) => events.push(e));
96
+ try {
97
+ const result = cancelJob(vaultPath, "m1");
98
+ assert.equal(result, undefined);
99
+ assert.equal(events.length, 0);
100
+ }
101
+ finally {
102
+ off();
103
+ }
104
+ });
105
+ it("supports multiple subscribers and unsubscribes correctly", () => {
106
+ const a = [];
107
+ const b = [];
108
+ const offA = subscribeQueue((e) => a.push(e));
109
+ const offB = subscribeQueue((e) => b.push(e));
110
+ seed(vaultPath, "m1");
111
+ assert.equal(a.length, 1);
112
+ assert.equal(b.length, 1);
113
+ offA();
114
+ seed(vaultPath, "m2");
115
+ assert.equal(a.length, 1);
116
+ assert.equal(b.length, 2);
117
+ offB();
118
+ });
119
+ it("a bad subscriber does not break the writer or other subscribers", () => {
120
+ const a = [];
121
+ subscribeQueue(() => {
122
+ throw new Error("bad listener");
123
+ });
124
+ const offA = subscribeQueue((e) => a.push(e));
125
+ try {
126
+ seed(vaultPath, "m1");
127
+ assert.equal(a.length, 1, "second subscriber still got the event");
128
+ }
129
+ finally {
130
+ offA();
131
+ }
132
+ });
133
+ it("events are not emitted for missing jobs", () => {
134
+ const events = [];
135
+ const off = subscribeQueue((e) => events.push(e));
136
+ try {
137
+ markJobRunning(vaultPath, "does-not-exist");
138
+ cancelJob(vaultPath, "does-not-exist");
139
+ assert.equal(events.length, 0);
140
+ assert.equal(listJobs(vaultPath).length, 0);
141
+ }
142
+ finally {
143
+ off();
144
+ }
145
+ });
146
+ });
@@ -0,0 +1,234 @@
1
+ import { strict as assert } from "node:assert";
2
+ import * as fs from "node:fs";
3
+ import * as path from "node:path";
4
+ import { afterEach, beforeEach, describe, it } from "node:test";
5
+ import { createJob, markJobFailed, markJobRunning } from "../src/agent-queue.js";
6
+ import { createServerState, startServer, stopServer } from "../src/server.js";
7
+ import { createWatcherState } from "../src/watcher.js";
8
+ const envVar = "PVM_API_TOKEN";
9
+ const tmpDir = () => path.join(process.cwd(), `.tmp-rest-queue-test-${Date.now()}-${Math.random().toString(36).slice(2)}`);
10
+ const makeVault = () => {
11
+ const p = tmpDir();
12
+ if (fs.existsSync(p))
13
+ fs.rmSync(p, { recursive: true });
14
+ fs.mkdirSync(p, { recursive: true });
15
+ return p;
16
+ };
17
+ const fetchJson = async (url, opts = {}) => {
18
+ const headers = { "Content-Type": "application/json" };
19
+ if (opts.token)
20
+ headers.authorization = `Bearer ${opts.token}`;
21
+ const resp = await fetch(url, {
22
+ method: opts.method ?? "GET",
23
+ headers,
24
+ body: opts.body !== undefined ? JSON.stringify(opts.body) : undefined,
25
+ });
26
+ const text = await resp.text();
27
+ let body;
28
+ try {
29
+ body = JSON.parse(text);
30
+ }
31
+ catch {
32
+ body = text;
33
+ }
34
+ return { status: resp.status, body };
35
+ };
36
+ describe("REST queue routes", () => {
37
+ let vaultPath;
38
+ let serverState;
39
+ let watcherState;
40
+ let port;
41
+ let savedEnv;
42
+ let savedHome;
43
+ const fakePi = {
44
+ sendUserMessage: () => { },
45
+ events: { emit: () => { } },
46
+ };
47
+ beforeEach(async () => {
48
+ savedEnv = process.env[envVar];
49
+ savedHome = process.env.HOME;
50
+ vaultPath = makeVault();
51
+ process.env.HOME = vaultPath;
52
+ process.env[envVar] = "test-token";
53
+ // Write a minimal global config so defaultVaultPath() resolves
54
+ const agentDir = path.join(vaultPath, ".pi", "agent");
55
+ fs.mkdirSync(agentDir, { recursive: true });
56
+ fs.writeFileSync(path.join(agentDir, "vault-mind.config.json"), JSON.stringify({ wiki: { vaults: { default: { path: vaultPath } } } }), "utf-8");
57
+ watcherState = createWatcherState();
58
+ serverState = createServerState(0);
59
+ startServer(fakePi, serverState, watcherState);
60
+ // Wait for the server to be ready
61
+ await new Promise((resolve) => {
62
+ const check = () => {
63
+ const addr = serverState.server?.address();
64
+ if (addr?.port) {
65
+ port = addr.port;
66
+ resolve();
67
+ }
68
+ else
69
+ setTimeout(check, 10);
70
+ };
71
+ check();
72
+ });
73
+ });
74
+ afterEach(() => {
75
+ stopServer(serverState);
76
+ if (savedEnv === undefined)
77
+ delete process.env[envVar];
78
+ else
79
+ process.env[envVar] = savedEnv;
80
+ if (savedHome === undefined)
81
+ process.env.HOME = undefined;
82
+ else
83
+ process.env.HOME = savedHome;
84
+ if (fs.existsSync(vaultPath))
85
+ fs.rmSync(vaultPath, { recursive: true });
86
+ });
87
+ it("GET /agent/queue returns 401 without token", async () => {
88
+ const { status } = await fetchJson(`http://127.0.0.1:${port}/agent/queue`);
89
+ assert.equal(status, 401);
90
+ });
91
+ it("GET /agent/queue returns empty array when no jobs exist", async () => {
92
+ const { status, body } = await fetchJson(`http://127.0.0.1:${port}/agent/queue`, {
93
+ token: "test-token",
94
+ });
95
+ assert.equal(status, 200);
96
+ assert.deepEqual(body, []);
97
+ });
98
+ it("GET /agent/queue returns jobs filtered by status", async () => {
99
+ createJob(vaultPath, {
100
+ id: "m1",
101
+ vaultPath,
102
+ filePath: path.join(vaultPath, "a.md"),
103
+ role: "miner",
104
+ agentName: "vault-mind-miner",
105
+ instruction: "a",
106
+ markerCount: 1,
107
+ });
108
+ createJob(vaultPath, {
109
+ id: "m2",
110
+ vaultPath,
111
+ filePath: path.join(vaultPath, "b.md"),
112
+ role: "miner",
113
+ agentName: "vault-mind-miner",
114
+ instruction: "b",
115
+ markerCount: 1,
116
+ });
117
+ markJobRunning(vaultPath, "m2");
118
+ const { status, body } = await fetchJson(`http://127.0.0.1:${port}/agent/queue?status=pending`, { token: "test-token" });
119
+ assert.equal(status, 200);
120
+ const jobs = body;
121
+ assert.equal(jobs.length, 1);
122
+ assert.equal(jobs[0].id, "m1");
123
+ });
124
+ it("GET /agent/jobs/:id returns 404 for missing job", async () => {
125
+ const { status } = await fetchJson(`http://127.0.0.1:${port}/agent/jobs/nonexistent`, {
126
+ token: "test-token",
127
+ });
128
+ assert.equal(status, 404);
129
+ });
130
+ it("GET /agent/jobs/:id returns the job", async () => {
131
+ createJob(vaultPath, {
132
+ id: "m1",
133
+ vaultPath,
134
+ filePath: path.join(vaultPath, "a.md"),
135
+ role: "miner",
136
+ agentName: "vault-mind-miner",
137
+ instruction: "a",
138
+ markerCount: 1,
139
+ });
140
+ const { status, body } = await fetchJson(`http://127.0.0.1:${port}/agent/jobs/m1`, {
141
+ token: "test-token",
142
+ });
143
+ assert.equal(status, 200);
144
+ assert.equal(body.id, "m1");
145
+ });
146
+ it("POST /agent/jobs/:id/retry succeeds on a failed job", async () => {
147
+ createJob(vaultPath, {
148
+ id: "m1",
149
+ vaultPath,
150
+ filePath: path.join(vaultPath, "a.md"),
151
+ role: "miner",
152
+ agentName: "vault-mind-miner",
153
+ instruction: "a",
154
+ markerCount: 1,
155
+ });
156
+ markJobRunning(vaultPath, "m1");
157
+ markJobFailed(vaultPath, "m1", "timeout");
158
+ const { status, body } = await fetchJson(`http://127.0.0.1:${port}/agent/jobs/m1/retry`, {
159
+ method: "POST",
160
+ token: "test-token",
161
+ });
162
+ assert.equal(status, 200);
163
+ const job = body;
164
+ assert.equal(job.status, "running");
165
+ assert.equal(job.attempts, 2);
166
+ });
167
+ it("POST /agent/jobs/:id/retry returns 409 on a pending job", async () => {
168
+ createJob(vaultPath, {
169
+ id: "m1",
170
+ vaultPath,
171
+ filePath: path.join(vaultPath, "a.md"),
172
+ role: "miner",
173
+ agentName: "vault-mind-miner",
174
+ instruction: "a",
175
+ markerCount: 1,
176
+ });
177
+ const { status } = await fetchJson(`http://127.0.0.1:${port}/agent/jobs/m1/retry`, {
178
+ method: "POST",
179
+ token: "test-token",
180
+ });
181
+ assert.equal(status, 409);
182
+ });
183
+ it("POST /agent/jobs/:id/cancel succeeds on a pending job", async () => {
184
+ createJob(vaultPath, {
185
+ id: "m1",
186
+ vaultPath,
187
+ filePath: path.join(vaultPath, "a.md"),
188
+ role: "miner",
189
+ agentName: "vault-mind-miner",
190
+ instruction: "a",
191
+ markerCount: 1,
192
+ });
193
+ const { status, body } = await fetchJson(`http://127.0.0.1:${port}/agent/jobs/m1/cancel`, {
194
+ method: "POST",
195
+ token: "test-token",
196
+ });
197
+ assert.equal(status, 200);
198
+ assert.equal(body.status, "cancelled");
199
+ });
200
+ it("POST /agent/jobs/:id/cancel returns 409 on a running job", async () => {
201
+ createJob(vaultPath, {
202
+ id: "m1",
203
+ vaultPath,
204
+ filePath: path.join(vaultPath, "a.md"),
205
+ role: "miner",
206
+ agentName: "vault-mind-miner",
207
+ instruction: "a",
208
+ markerCount: 1,
209
+ });
210
+ markJobRunning(vaultPath, "m1");
211
+ const { status } = await fetchJson(`http://127.0.0.1:${port}/agent/jobs/m1/cancel`, {
212
+ method: "POST",
213
+ token: "test-token",
214
+ });
215
+ assert.equal(status, 409);
216
+ });
217
+ it("all queue routes require auth", async () => {
218
+ const noToken = { token: undefined };
219
+ const r1 = await fetchJson(`http://127.0.0.1:${port}/agent/queue`, noToken);
220
+ const r2 = await fetchJson(`http://127.0.0.1:${port}/agent/jobs/x`, noToken);
221
+ const r3 = await fetchJson(`http://127.0.0.1:${port}/agent/jobs/x/retry`, {
222
+ ...noToken,
223
+ method: "POST",
224
+ });
225
+ const r4 = await fetchJson(`http://127.0.0.1:${port}/agent/jobs/x/cancel`, {
226
+ ...noToken,
227
+ method: "POST",
228
+ });
229
+ assert.equal(r1.status, 401);
230
+ assert.equal(r2.status, 401);
231
+ assert.equal(r3.status, 401);
232
+ assert.equal(r4.status, 401);
233
+ });
234
+ });
@@ -0,0 +1,213 @@
1
+ import { strict as assert } from "node:assert";
2
+ import * as fs from "node:fs";
3
+ import * as http from "node:http";
4
+ import * as path from "node:path";
5
+ import { afterEach, beforeEach, describe, it } from "node:test";
6
+ import { _resetAuthCache } from "../src/auth.js";
7
+ import { createServerState, startServer, stopServer } from "../src/server.js";
8
+ import { createWatcherState } from "../src/watcher.js";
9
+ const envVar = "PVM_API_TOKEN";
10
+ const tmpVault = () => {
11
+ const p = path.join(process.cwd(), `.tmp-rest-setup-${Date.now()}-${Math.random().toString(36).slice(2)}`);
12
+ if (fs.existsSync(p))
13
+ fs.rmSync(p, { recursive: true });
14
+ fs.mkdirSync(p, { recursive: true });
15
+ return p;
16
+ };
17
+ const fetchJson = (host, port, method, path, body, token) => {
18
+ return new Promise((resolve, reject) => {
19
+ const req = http.request({
20
+ hostname: host,
21
+ port,
22
+ path,
23
+ method,
24
+ headers: {
25
+ "Content-Type": "application/json",
26
+ ...(token ? { Authorization: `Bearer ${token}` } : {}),
27
+ },
28
+ }, (res) => {
29
+ let data = "";
30
+ res.on("data", (chunk) => {
31
+ data += chunk.toString();
32
+ });
33
+ res.on("end", () => {
34
+ try {
35
+ resolve({ status: res.statusCode ?? 0, body: JSON.parse(data || "{}") });
36
+ }
37
+ catch {
38
+ resolve({ status: res.statusCode ?? 0, body: data });
39
+ }
40
+ });
41
+ });
42
+ req.on("error", reject);
43
+ if (body !== undefined)
44
+ req.write(JSON.stringify(body));
45
+ req.end();
46
+ });
47
+ };
48
+ describe("REST setup routes", () => {
49
+ let vaultPath;
50
+ let serverState;
51
+ let watcherState;
52
+ let port;
53
+ let savedEnv;
54
+ let savedHome;
55
+ beforeEach(async () => {
56
+ savedEnv = process.env[envVar];
57
+ savedHome = process.env.HOME;
58
+ vaultPath = tmpVault();
59
+ process.env.HOME = vaultPath;
60
+ process.env[envVar] = "test-token";
61
+ _resetAuthCache();
62
+ watcherState = createWatcherState();
63
+ serverState = createServerState(0);
64
+ startServer({
65
+ sendUserMessage: () => { },
66
+ events: { emit: () => { } },
67
+ getAllTools: () => [],
68
+ registerCommand: () => { },
69
+ registerTool: () => { },
70
+ }, serverState, watcherState);
71
+ // Wait for the server to be listening, then read the actual port
72
+ await new Promise((resolve, reject) => {
73
+ const srv = serverState.server;
74
+ if (!srv)
75
+ return reject(new Error("server not created"));
76
+ if (srv.listening) {
77
+ const addr = srv.address();
78
+ port = typeof addr === "object" && addr ? addr.port : 0;
79
+ return resolve();
80
+ }
81
+ srv.once("listening", () => {
82
+ const addr = srv.address();
83
+ port = typeof addr === "object" && addr ? addr.port : 0;
84
+ resolve();
85
+ });
86
+ srv.once("error", reject);
87
+ });
88
+ });
89
+ afterEach(() => {
90
+ stopServer(serverState);
91
+ if (savedEnv === undefined)
92
+ delete process.env[envVar];
93
+ else
94
+ process.env[envVar] = savedEnv;
95
+ if (savedHome === undefined)
96
+ process.env.HOME = undefined;
97
+ else
98
+ process.env.HOME = savedHome;
99
+ if (fs.existsSync(vaultPath))
100
+ fs.rmSync(vaultPath, { recursive: true });
101
+ _resetAuthCache();
102
+ });
103
+ it("GET /wiki/status returns ok and configured flag", async () => {
104
+ const { status, body } = await fetchJson("127.0.0.1", port, "GET", "/wiki/status", undefined, "test-token");
105
+ assert.equal(status, 200);
106
+ const b = body;
107
+ assert.equal(b.ok, true);
108
+ assert.equal(typeof b.configured, "boolean");
109
+ assert.equal(typeof b.watcher, "boolean");
110
+ assert.equal(typeof b.provider, "string");
111
+ assert.equal(typeof b.version, "string");
112
+ });
113
+ it("GET /wiki/status returns 401 without token", async () => {
114
+ const { status } = await fetchJson("127.0.0.1", port, "GET", "/wiki/status");
115
+ assert.equal(status, 401);
116
+ });
117
+ it("POST /wiki/init scaffolds config and collections", async () => {
118
+ const { status, body } = await fetchJson("127.0.0.1", port, "POST", "/wiki/init", { vaultPath }, "test-token");
119
+ assert.equal(status, 200);
120
+ const b = body;
121
+ assert.equal(b.ok, true);
122
+ assert.equal(b.path, vaultPath);
123
+ assert.ok(fs.existsSync(path.join(vaultPath, "pi-vault-mind.config.json")));
124
+ assert.ok(fs.existsSync(path.join(vaultPath, "collections", "main.jsonl")));
125
+ assert.ok(fs.existsSync(path.join(vaultPath, ".gitignore")));
126
+ });
127
+ it("POST /wiki/init is idempotent", async () => {
128
+ await fetchJson("127.0.0.1", port, "POST", "/wiki/init", { vaultPath }, "test-token");
129
+ const { status, body } = await fetchJson("127.0.0.1", port, "POST", "/wiki/init", { vaultPath }, "test-token");
130
+ assert.equal(status, 200);
131
+ const b = body;
132
+ assert.equal(b.ok, true);
133
+ });
134
+ it("POST /wiki/init returns 401 without token", async () => {
135
+ const { status } = await fetchJson("127.0.0.1", port, "POST", "/wiki/init", { vaultPath });
136
+ assert.equal(status, 401);
137
+ });
138
+ it("POST /wiki/setup writes global config", async () => {
139
+ const { status, body } = await fetchJson("127.0.0.1", port, "POST", "/wiki/setup", { vault: vaultPath, provider: "ollama", model: "embeddinggemma" }, "test-token");
140
+ assert.equal(status, 200);
141
+ const b = body;
142
+ assert.equal(b.ok, true);
143
+ const globalPath = path.join(vaultPath, ".pi", "agent", "vault-mind.config.json");
144
+ assert.ok(fs.existsSync(globalPath));
145
+ const cfg = JSON.parse(fs.readFileSync(globalPath, "utf-8"));
146
+ assert.equal(cfg.wiki.embedding.provider, "ollama");
147
+ assert.equal(cfg.wiki.embedding.ollamaModel, "embeddinggemma");
148
+ });
149
+ it("POST /wiki/setup returns 401 without token", async () => {
150
+ const { status } = await fetchJson("127.0.0.1", port, "POST", "/wiki/setup", {
151
+ vault: vaultPath,
152
+ provider: "ollama",
153
+ });
154
+ assert.equal(status, 401);
155
+ });
156
+ it("POST /server/start returns ok when already running", async () => {
157
+ const { status, body } = await fetchJson("127.0.0.1", port, "POST", "/server/start", undefined, "test-token");
158
+ assert.equal(status, 200);
159
+ const b = body;
160
+ assert.equal(b.ok, true);
161
+ assert.equal(b.alreadyRunning, true);
162
+ });
163
+ it("GET /vault-mind/config returns config snapshot", async () => {
164
+ // First run init + setup so there is config to read
165
+ await fetchJson("127.0.0.1", port, "POST", "/wiki/init", { vaultPath }, "test-token");
166
+ await fetchJson("127.0.0.1", port, "POST", "/wiki/setup", { vault: vaultPath, provider: "ollama" }, "test-token");
167
+ const { status, body } = await fetchJson("127.0.0.1", port, "GET", "/vault-mind/config", undefined, "test-token");
168
+ assert.equal(status, 200);
169
+ const b = body;
170
+ assert.equal(typeof b.hasToken, "boolean");
171
+ assert.equal(typeof b.version, "string");
172
+ assert.ok(b.global !== null || b.project !== null);
173
+ });
174
+ it("GET /vault-mind/config returns 401 without token", async () => {
175
+ const { status } = await fetchJson("127.0.0.1", port, "GET", "/vault-mind/config");
176
+ assert.equal(status, 401);
177
+ });
178
+ it("POST /vault-mind/context stores editor context", async () => {
179
+ const { status, body } = await fetchJson("127.0.0.1", port, "POST", "/vault-mind/context", { filePath: "notes/foo.md", selection: "hello", cursor: 42 }, "test-token");
180
+ assert.equal(status, 200);
181
+ const b = body;
182
+ assert.equal(b.ok, true);
183
+ const ctx = b.context;
184
+ assert.equal(ctx.filePath, "notes/foo.md");
185
+ assert.equal(ctx.selection, "hello");
186
+ assert.equal(ctx.cursor, 42);
187
+ });
188
+ it("GET /vault-mind/context returns the latest editor context", async () => {
189
+ await fetchJson("127.0.0.1", port, "POST", "/vault-mind/context", { filePath: "notes/bar.md", selection: "world", cursor: 7 }, "test-token");
190
+ const { status, body } = await fetchJson("127.0.0.1", port, "GET", "/vault-mind/context", undefined, "test-token");
191
+ assert.equal(status, 200);
192
+ const b = body;
193
+ assert.equal(b.filePath, "notes/bar.md");
194
+ assert.equal(b.selection, "world");
195
+ assert.equal(b.cursor, 7);
196
+ });
197
+ it("GET /vault-mind/context returns nulls when no context has been pushed", async () => {
198
+ const { status, body } = await fetchJson("127.0.0.1", port, "GET", "/vault-mind/context", undefined, "test-token");
199
+ assert.equal(status, 200);
200
+ const b = body;
201
+ assert.equal(b.filePath, null);
202
+ assert.equal(b.selection, null);
203
+ assert.equal(b.cursor, null);
204
+ });
205
+ it("/vault-mind/context routes require auth", async () => {
206
+ const post = await fetchJson("127.0.0.1", port, "POST", "/vault-mind/context", {
207
+ filePath: "x.md",
208
+ });
209
+ assert.equal(post.status, 401);
210
+ const get = await fetchJson("127.0.0.1", port, "GET", "/vault-mind/context");
211
+ assert.equal(get.status, 401);
212
+ });
213
+ });