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.
- package/README.md +21 -21
- package/dist/src/agent-queue.d.ts +108 -0
- package/dist/src/agent-queue.js +225 -0
- package/dist/src/auth.d.ts +30 -0
- package/dist/src/auth.js +93 -0
- package/dist/src/auto-index.d.ts +47 -0
- package/dist/src/auto-index.js +122 -0
- package/dist/src/commands.js +265 -4
- package/dist/src/discover-schema.d.ts +27 -0
- package/dist/src/discover-schema.js +255 -0
- package/dist/src/index.js +2 -0
- package/dist/src/intake.d.ts +4 -0
- package/dist/src/intake.js +248 -0
- package/dist/src/server.d.ts +31 -0
- package/dist/src/server.js +561 -4
- package/dist/src/settings-ui.js +11 -11
- package/dist/src/tombstone.d.ts +13 -0
- package/dist/src/tombstone.js +103 -0
- package/dist/src/tools.d.ts +8 -0
- package/dist/src/tools.js +58 -4
- package/dist/src/types.d.ts +47 -0
- package/dist/src/types.js +1 -0
- package/dist/src/utils.d.ts +4 -2
- package/dist/src/utils.js +9 -4
- package/dist/src/watcher.d.ts +7 -1
- package/dist/src/watcher.js +170 -11
- package/dist/test/agent-queue.test.js +204 -0
- package/dist/test/auth.test.js +136 -0
- package/dist/test/auto-indexer.test.js +2 -0
- package/dist/test/dedup.test.js +62 -0
- package/dist/test/discover-schema.test.js +201 -0
- package/dist/test/modal-config.test.js +17 -2
- package/dist/test/queue-events.test.js +146 -0
- package/dist/test/rest-queue.test.js +234 -0
- package/dist/test/rest-setup.test.js +213 -0
- package/dist/test/rest-wiki.test.js +161 -0
- package/dist/test/server-ws.test.js +219 -0
- package/dist/test/tombstone.test.js +233 -0
- package/package.json +6 -2
- package/scripts/e2e-ws.mjs +364 -0
- package/scripts/fetch-modal-token.sh +66 -0
- package/scripts/modal-e2e-smoke.mjs +171 -0
- package/scripts/setup-test-config.sh +66 -0
- package/dist/test/index.test.d.ts +0 -1
package/dist/src/server.js
CHANGED
|
@@ -5,18 +5,35 @@
|
|
|
5
5
|
* GET /vault-mind/status → { running, dispatches, vaults, uptime }
|
|
6
6
|
* POST /vault-mind/scan → scan a file for @agent markers { file }
|
|
7
7
|
* POST /vault-mind/dispatch → fire a manual dispatch
|
|
8
|
+
* POST /vault-mind/context → push active editor context from Obsidian { filePath, selection, cursor }
|
|
9
|
+
* GET /vault-mind/context → latest editor context (file, cursor, selection) for pi agents
|
|
8
10
|
*
|
|
9
11
|
* Binds 127.0.0.1 only — no network exposure. Port defaults to 11435,
|
|
10
12
|
* configurable via wiki.httpPort in pi-vault-mind.config.json.
|
|
11
13
|
*
|
|
12
14
|
* Used by the obsidian-shellcommands plugin to notify pi of file saves,
|
|
13
15
|
* replacing fs.watch reliance with explicit push notifications.
|
|
16
|
+
*
|
|
17
|
+
* New Stage-1 routes (auth-gated by `PVM_API_TOKEN`):
|
|
18
|
+
* GET /wiki/status, POST /wiki/init, POST /wiki/setup, POST /server/start,
|
|
19
|
+
* GET /vault-mind/config, GET /agent/queue, GET /agent/jobs/:id,
|
|
20
|
+
* POST /agent/jobs/:id/retry, POST /agent/jobs/:id/cancel,
|
|
21
|
+
* POST /wiki/search, POST /wiki/append, WS /agent/stream.
|
|
14
22
|
*/
|
|
15
23
|
import * as fs from "node:fs";
|
|
16
24
|
import * as http from "node:http";
|
|
17
|
-
import
|
|
25
|
+
import * as os from "node:os";
|
|
26
|
+
import * as path from "node:path";
|
|
27
|
+
import { WebSocketServer } from "ws";
|
|
28
|
+
import { cancelJobHelper, getJob, listJobs, retryJob, subscribeQueue, } from "./agent-queue.js";
|
|
29
|
+
import { authoriseRequest, resolveAuthToken } from "./auth.js";
|
|
30
|
+
import { GITIGNORE_ENTRIES } from "./commands.js";
|
|
31
|
+
import { searchHybrid, upsertEntry } from "./lance.js";
|
|
32
|
+
import { DEFAULT_CONFIG } from "./types.js";
|
|
33
|
+
import { EXT_ROOT, ensureDir, expandHome, findConfig, loadConfig, shrinkHome } from "./utils.js";
|
|
34
|
+
import { processQueue, scanFile, startWatcher, stopWatcher } from "./watcher.js";
|
|
18
35
|
export function createServerState(port = 11435) {
|
|
19
|
-
return { server: null, port, startTime: 0 };
|
|
36
|
+
return { server: null, wss: null, port, startTime: 0 };
|
|
20
37
|
}
|
|
21
38
|
export function startServer(pi, serverState, watcherState) {
|
|
22
39
|
if (serverState.server) {
|
|
@@ -24,6 +41,29 @@ export function startServer(pi, serverState, watcherState) {
|
|
|
24
41
|
return;
|
|
25
42
|
}
|
|
26
43
|
serverState.startTime = Date.now();
|
|
44
|
+
serverState.pi = pi;
|
|
45
|
+
serverState.watcherState = watcherState;
|
|
46
|
+
// Resolve the vault path from the global config's default vault, falling back to cwd.
|
|
47
|
+
const home = os.homedir();
|
|
48
|
+
const globalCfgPath = home ? path.join(home, ".pi", "agent", "vault-mind.config.json") : null;
|
|
49
|
+
let vaultPath = process.cwd();
|
|
50
|
+
if (globalCfgPath && fs.existsSync(globalCfgPath)) {
|
|
51
|
+
try {
|
|
52
|
+
const globalCfg = JSON.parse(fs.readFileSync(globalCfgPath, "utf-8"));
|
|
53
|
+
const defaultVault = globalCfg?.wiki?.vaults?.default?.path;
|
|
54
|
+
if (defaultVault) {
|
|
55
|
+
const resolved = defaultVault.startsWith("~")
|
|
56
|
+
? path.join(home, defaultVault.slice(1))
|
|
57
|
+
: defaultVault;
|
|
58
|
+
if (fs.existsSync(resolved))
|
|
59
|
+
vaultPath = resolved;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
// config parse error — keep cwd
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
serverState.vaultPath = vaultPath;
|
|
27
67
|
serverState.server = http.createServer((req, res) => {
|
|
28
68
|
// CORS for localhost-only access
|
|
29
69
|
res.setHeader("Access-Control-Allow-Origin", "http://localhost:11435");
|
|
@@ -57,9 +97,87 @@ export function startServer(pi, serverState, watcherState) {
|
|
|
57
97
|
}
|
|
58
98
|
handleDispatch(req, res, pi, watcherState);
|
|
59
99
|
break;
|
|
100
|
+
case "/vault-mind/context":
|
|
101
|
+
if (req.method === "POST") {
|
|
102
|
+
withAuth(req, res, () => handleVaultMindContextUpdate(req, res, serverState));
|
|
103
|
+
}
|
|
104
|
+
else if (req.method === "GET") {
|
|
105
|
+
withAuth(req, res, () => handleVaultMindContextGet(res, serverState));
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
res.writeHead(405);
|
|
109
|
+
res.end(JSON.stringify({ error: "Method not allowed" }));
|
|
110
|
+
}
|
|
111
|
+
break;
|
|
112
|
+
// ── Stage 1 auth-gated routes ──────────────────────────
|
|
113
|
+
case "/wiki/status":
|
|
114
|
+
withAuth(req, res, () => handleWikiStatus(res, serverState));
|
|
115
|
+
break;
|
|
116
|
+
case "/wiki/watcher/toggle":
|
|
117
|
+
if (req.method !== "POST") {
|
|
118
|
+
res.writeHead(405);
|
|
119
|
+
res.end(JSON.stringify({ error: "Method not allowed" }));
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
withAuth(req, res, () => handleWatcherToggle(res, serverState));
|
|
123
|
+
break;
|
|
124
|
+
case "/wiki/init":
|
|
125
|
+
if (req.method !== "POST") {
|
|
126
|
+
res.writeHead(405);
|
|
127
|
+
res.end(JSON.stringify({ error: "Method not allowed" }));
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
withAuth(req, res, () => handleWikiInit(req, res));
|
|
131
|
+
break;
|
|
132
|
+
case "/wiki/setup":
|
|
133
|
+
if (req.method !== "POST") {
|
|
134
|
+
res.writeHead(405);
|
|
135
|
+
res.end(JSON.stringify({ error: "Method not allowed" }));
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
withAuth(req, res, () => handleWikiSetup(req, res));
|
|
139
|
+
break;
|
|
140
|
+
case "/server/start":
|
|
141
|
+
if (req.method !== "POST") {
|
|
142
|
+
res.writeHead(405);
|
|
143
|
+
res.end(JSON.stringify({ error: "Method not allowed" }));
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
withAuth(req, res, () => handleServerStart(res, serverState));
|
|
147
|
+
break;
|
|
148
|
+
case "/vault-mind/config":
|
|
149
|
+
withAuth(req, res, () => handleVaultMindConfig(res));
|
|
150
|
+
break;
|
|
151
|
+
// ── Agent queue routes ────────────────────────────────
|
|
152
|
+
case "/agent/queue":
|
|
153
|
+
withAuth(req, res, () => handleAgentQueue(req, res));
|
|
154
|
+
break;
|
|
155
|
+
// ── Wiki tool routes ───────────────────────────────────
|
|
156
|
+
case "/wiki/search":
|
|
157
|
+
if (req.method !== "POST") {
|
|
158
|
+
res.writeHead(405);
|
|
159
|
+
res.end(JSON.stringify({ error: "Method not allowed" }));
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
withAuth(req, res, () => handleWikiSearch(req, res, serverState));
|
|
163
|
+
break;
|
|
164
|
+
case "/wiki/append":
|
|
165
|
+
if (req.method !== "POST") {
|
|
166
|
+
res.writeHead(405);
|
|
167
|
+
res.end(JSON.stringify({ error: "Method not allowed" }));
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
withAuth(req, res, () => handleWikiAppend(req, res, serverState));
|
|
171
|
+
break;
|
|
60
172
|
default:
|
|
61
|
-
|
|
62
|
-
|
|
173
|
+
if (url.pathname.startsWith("/agent/jobs/")) {
|
|
174
|
+
withAuth(req, res, () => handleAgentJob(req, res, url.pathname));
|
|
175
|
+
}
|
|
176
|
+
else {
|
|
177
|
+
res.writeHead(404);
|
|
178
|
+
res.end(JSON.stringify({ error: "Not found" }));
|
|
179
|
+
}
|
|
180
|
+
break;
|
|
63
181
|
}
|
|
64
182
|
}
|
|
65
183
|
catch (err) {
|
|
@@ -76,11 +194,52 @@ export function startServer(pi, serverState, watcherState) {
|
|
|
76
194
|
console.error("[pi-vault-mind] HTTP server error:", err);
|
|
77
195
|
}
|
|
78
196
|
});
|
|
197
|
+
// ── WebSocket mount for live queue streaming ────────────────────
|
|
198
|
+
const wss = new WebSocketServer({ noServer: true });
|
|
199
|
+
serverState.wss = wss;
|
|
200
|
+
wss.on("connection", (ws, req) => {
|
|
201
|
+
const auth = authoriseRequest(req);
|
|
202
|
+
if (!auth.ok) {
|
|
203
|
+
ws.close(4401, JSON.stringify({ error: "unauthorized", reason: auth.reason }));
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
const vaultPath = serverState.vaultPath;
|
|
207
|
+
if (vaultPath) {
|
|
208
|
+
const snapshot = listJobs(vaultPath);
|
|
209
|
+
ws.send(JSON.stringify({ type: "queue/snapshot", jobs: snapshot }));
|
|
210
|
+
}
|
|
211
|
+
const off = subscribeQueue((e) => {
|
|
212
|
+
if (ws.readyState === ws.OPEN) {
|
|
213
|
+
ws.send(JSON.stringify(e));
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
ws.on("close", off);
|
|
217
|
+
ws.on("error", () => {
|
|
218
|
+
// socket error — unsubscribe is handled by close
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
serverState.server.on("upgrade", (req, sock, head) => {
|
|
222
|
+
if (req.url?.startsWith("/agent/stream")) {
|
|
223
|
+
wss.handleUpgrade(req, sock, head, (ws) => {
|
|
224
|
+
wss.emit("connection", ws, req);
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
else {
|
|
228
|
+
sock.destroy();
|
|
229
|
+
}
|
|
230
|
+
});
|
|
79
231
|
serverState.server.listen(serverState.port, "127.0.0.1", () => {
|
|
232
|
+
const addr = serverState.server?.address();
|
|
233
|
+
if (addr && typeof addr === "object")
|
|
234
|
+
serverState.port = addr.port;
|
|
80
235
|
console.log(`[pi-vault-mind] HTTP server listening on http://127.0.0.1:${serverState.port}`);
|
|
81
236
|
});
|
|
82
237
|
}
|
|
83
238
|
export function stopServer(serverState) {
|
|
239
|
+
if (serverState.wss) {
|
|
240
|
+
serverState.wss.close();
|
|
241
|
+
serverState.wss = null;
|
|
242
|
+
}
|
|
84
243
|
if (serverState.server) {
|
|
85
244
|
serverState.server.close();
|
|
86
245
|
serverState.server = null;
|
|
@@ -178,3 +337,401 @@ function handleDispatch(req, res, _pi, _watcherState) {
|
|
|
178
337
|
message: "Manual dispatch not yet implemented. Use /wiki watcher status for state.",
|
|
179
338
|
}));
|
|
180
339
|
}
|
|
340
|
+
async function handleVaultMindContextUpdate(req, res, serverState) {
|
|
341
|
+
const body = (await readJsonBody(req));
|
|
342
|
+
serverState.latestEditorContext = {
|
|
343
|
+
filePath: typeof body.filePath === "string" ? body.filePath : null,
|
|
344
|
+
selection: typeof body.selection === "string" ? body.selection : null,
|
|
345
|
+
cursor: typeof body.cursor === "number" ? body.cursor : null,
|
|
346
|
+
};
|
|
347
|
+
res.writeHead(200);
|
|
348
|
+
res.end(JSON.stringify({ ok: true, context: serverState.latestEditorContext }));
|
|
349
|
+
}
|
|
350
|
+
function handleVaultMindContextGet(res, serverState) {
|
|
351
|
+
res.writeHead(200);
|
|
352
|
+
res.end(JSON.stringify(serverState.latestEditorContext ?? {
|
|
353
|
+
filePath: null,
|
|
354
|
+
selection: null,
|
|
355
|
+
cursor: null,
|
|
356
|
+
}));
|
|
357
|
+
}
|
|
358
|
+
// ── Stage 1 setup routes (auth-gated) ───────────────────────────────────────
|
|
359
|
+
function handleWikiStatus(res, serverState) {
|
|
360
|
+
const ws = serverState.watcherState;
|
|
361
|
+
const cfg = loadConfig(process.cwd());
|
|
362
|
+
const configured = !!findConfig(process.cwd()).project;
|
|
363
|
+
const provider = cfg.wiki.embedding.provider;
|
|
364
|
+
const model = provider === "ollama"
|
|
365
|
+
? cfg.wiki.embedding.ollamaModel || "embeddinggemma"
|
|
366
|
+
: provider === "modal"
|
|
367
|
+
? cfg.wiki.embedding.modal?.model || "embeddinggemma"
|
|
368
|
+
: "all-MiniLM-L6-v2";
|
|
369
|
+
const dim = provider === "modal"
|
|
370
|
+
? (cfg.wiki.embedding.modal?.dim ?? null)
|
|
371
|
+
: provider === "ollama"
|
|
372
|
+
? 768
|
|
373
|
+
: 384;
|
|
374
|
+
res.writeHead(200);
|
|
375
|
+
res.end(JSON.stringify({
|
|
376
|
+
ok: true,
|
|
377
|
+
configured,
|
|
378
|
+
watcher: ws?.running ?? false,
|
|
379
|
+
provider,
|
|
380
|
+
model,
|
|
381
|
+
dim,
|
|
382
|
+
queue: {
|
|
383
|
+
pending: ws?.pendingQueue?.length ?? 0,
|
|
384
|
+
running: ws?.activeDispatches ?? 0,
|
|
385
|
+
},
|
|
386
|
+
server: {
|
|
387
|
+
running: !!serverState.server,
|
|
388
|
+
port: serverState.port,
|
|
389
|
+
uptimeMs: Date.now() - serverState.startTime,
|
|
390
|
+
},
|
|
391
|
+
version: "0.8.0",
|
|
392
|
+
}));
|
|
393
|
+
}
|
|
394
|
+
function handleWatcherToggle(res, serverState) {
|
|
395
|
+
const ws = serverState.watcherState;
|
|
396
|
+
if (!ws) {
|
|
397
|
+
res.writeHead(500);
|
|
398
|
+
res.end(JSON.stringify({ error: "Watcher state not available." }));
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
if (ws.running) {
|
|
402
|
+
stopWatcher(ws);
|
|
403
|
+
res.writeHead(200);
|
|
404
|
+
res.end(JSON.stringify({ ok: true, watcher: false }));
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
const pi = serverState.pi;
|
|
408
|
+
if (!pi) {
|
|
409
|
+
res.writeHead(500);
|
|
410
|
+
res.end(JSON.stringify({ error: "Extension API not available." }));
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
const cfg = loadConfig(serverState.vaultPath || process.cwd());
|
|
414
|
+
const vaults = cfg.wiki.vaults;
|
|
415
|
+
if (!vaults || Object.keys(vaults).length === 0) {
|
|
416
|
+
res.writeHead(400);
|
|
417
|
+
res.end(JSON.stringify({ error: "No vaults configured. Add vaults to wiki.vaults first." }));
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
startWatcher(pi, vaults, ws);
|
|
421
|
+
res.writeHead(200);
|
|
422
|
+
res.end(JSON.stringify({ ok: true, watcher: true }));
|
|
423
|
+
}
|
|
424
|
+
function handleWikiInit(req, res) {
|
|
425
|
+
readBody(req)
|
|
426
|
+
.then((raw) => {
|
|
427
|
+
let parsed;
|
|
428
|
+
try {
|
|
429
|
+
parsed = JSON.parse(raw || "{}");
|
|
430
|
+
}
|
|
431
|
+
catch {
|
|
432
|
+
res.writeHead(400);
|
|
433
|
+
res.end(JSON.stringify({ error: "Invalid JSON body" }));
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
const vaultPath = parsed.vaultPath || process.cwd();
|
|
437
|
+
if (!fs.existsSync(vaultPath)) {
|
|
438
|
+
res.writeHead(400);
|
|
439
|
+
res.end(JSON.stringify({ error: `Vault path does not exist: ${vaultPath}` }));
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
// Scaffold project config
|
|
443
|
+
const cfgPath = path.join(vaultPath, "pi-vault-mind.config.json");
|
|
444
|
+
if (!fs.existsSync(cfgPath)) {
|
|
445
|
+
const tmpl = path.join(EXT_ROOT, "templates", "pi-vault-mind.config.json");
|
|
446
|
+
if (fs.existsSync(tmpl)) {
|
|
447
|
+
fs.copyFileSync(tmpl, cfgPath);
|
|
448
|
+
}
|
|
449
|
+
else {
|
|
450
|
+
fs.writeFileSync(cfgPath, `${JSON.stringify({ version: 2, collections: DEFAULT_CONFIG.collections, injectors: DEFAULT_CONFIG.injectors, wiki: DEFAULT_CONFIG.wiki }, null, 2)}\n`, "utf-8");
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
// Scaffold collections dir
|
|
454
|
+
const collDir = path.join(vaultPath, "collections");
|
|
455
|
+
if (!fs.existsSync(collDir))
|
|
456
|
+
fs.mkdirSync(collDir, { recursive: true });
|
|
457
|
+
for (const [name, def] of Object.entries(DEFAULT_CONFIG.collections)) {
|
|
458
|
+
const p = path.join(vaultPath, def.path);
|
|
459
|
+
if (!fs.existsSync(p)) {
|
|
460
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
461
|
+
fs.writeFileSync(p, "", "utf-8");
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
// Scaffold .gitignore
|
|
465
|
+
const giPath = path.join(vaultPath, ".gitignore");
|
|
466
|
+
const existing = fs.existsSync(giPath) ? fs.readFileSync(giPath, "utf-8").split(/\r?\n/) : [];
|
|
467
|
+
let changed = false;
|
|
468
|
+
for (const entry of GITIGNORE_ENTRIES) {
|
|
469
|
+
if (!existing.some((l) => l.trim() === entry)) {
|
|
470
|
+
existing.push(entry);
|
|
471
|
+
changed = true;
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
if (changed)
|
|
475
|
+
fs.writeFileSync(giPath, `${existing.join("\n")}\n`, "utf-8");
|
|
476
|
+
res.writeHead(200);
|
|
477
|
+
res.end(JSON.stringify({ ok: true, path: vaultPath }));
|
|
478
|
+
})
|
|
479
|
+
.catch((err) => {
|
|
480
|
+
if (!res.headersSent) {
|
|
481
|
+
res.writeHead(500);
|
|
482
|
+
res.end(JSON.stringify({ error: String(err) }));
|
|
483
|
+
}
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
function handleWikiSetup(req, res) {
|
|
487
|
+
readBody(req)
|
|
488
|
+
.then((raw) => {
|
|
489
|
+
let parsed;
|
|
490
|
+
try {
|
|
491
|
+
parsed = JSON.parse(raw || "{}");
|
|
492
|
+
}
|
|
493
|
+
catch {
|
|
494
|
+
res.writeHead(400);
|
|
495
|
+
res.end(JSON.stringify({ error: "Invalid JSON body" }));
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
const globalPath = path.join(os.homedir(), ".pi", "agent", "vault-mind.config.json");
|
|
499
|
+
const existingGlobal = fs.existsSync(globalPath);
|
|
500
|
+
const config = existingGlobal
|
|
501
|
+
? JSON.parse(fs.readFileSync(globalPath, "utf-8"))
|
|
502
|
+
: { wiki: { embedding: {}, vaults: {} } };
|
|
503
|
+
config.wiki = config.wiki || {};
|
|
504
|
+
config.wiki.embedding = config.wiki.embedding || {};
|
|
505
|
+
config.wiki.vaults = config.wiki.vaults || {};
|
|
506
|
+
if (parsed.vault) {
|
|
507
|
+
config.wiki.vaults.default = { path: shrinkHome(parsed.vault), autoSync: true };
|
|
508
|
+
}
|
|
509
|
+
if (parsed.provider && ["ollama", "transformers", "modal"].includes(parsed.provider)) {
|
|
510
|
+
config.wiki.embedding.provider = parsed.provider;
|
|
511
|
+
}
|
|
512
|
+
if (parsed.model) {
|
|
513
|
+
config.wiki.embedding.ollamaModel = parsed.model;
|
|
514
|
+
}
|
|
515
|
+
if (parsed.workspace) {
|
|
516
|
+
config.wiki.embedding.modal = config.wiki.embedding.modal || {};
|
|
517
|
+
config.wiki.embedding.modal.workspace = parsed.workspace;
|
|
518
|
+
}
|
|
519
|
+
const dir = path.dirname(globalPath);
|
|
520
|
+
if (!fs.existsSync(dir))
|
|
521
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
522
|
+
fs.writeFileSync(globalPath, `${JSON.stringify(config, null, 2)}\n`, "utf-8");
|
|
523
|
+
res.writeHead(200);
|
|
524
|
+
res.end(JSON.stringify({ ok: true }));
|
|
525
|
+
})
|
|
526
|
+
.catch((err) => {
|
|
527
|
+
if (!res.headersSent) {
|
|
528
|
+
res.writeHead(500);
|
|
529
|
+
res.end(JSON.stringify({ error: String(err) }));
|
|
530
|
+
}
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
function handleServerStart(res, serverState) {
|
|
534
|
+
if (serverState.server) {
|
|
535
|
+
res.writeHead(200);
|
|
536
|
+
res.end(JSON.stringify({ ok: true, port: serverState.port, alreadyRunning: true }));
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
const pi = serverState.pi;
|
|
540
|
+
const ws = serverState.watcherState;
|
|
541
|
+
if (!pi || !ws) {
|
|
542
|
+
res.writeHead(500);
|
|
543
|
+
res.end(JSON.stringify({
|
|
544
|
+
error: "Server cannot start: pi or watcherState not available. The extension must be loaded first.",
|
|
545
|
+
}));
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
startServer(pi, serverState, ws);
|
|
549
|
+
res.writeHead(200);
|
|
550
|
+
res.end(JSON.stringify({ ok: true, port: serverState.port }));
|
|
551
|
+
}
|
|
552
|
+
function handleVaultMindConfig(res) {
|
|
553
|
+
const globalPath2 = path.join(os.homedir(), ".pi", "agent", "vault-mind.config.json");
|
|
554
|
+
const globalCfg = fs.existsSync(globalPath2)
|
|
555
|
+
? JSON.parse(fs.readFileSync(globalPath2, "utf-8"))
|
|
556
|
+
: null;
|
|
557
|
+
const projectCfg = findConfig(process.cwd()).project;
|
|
558
|
+
const project = projectCfg ? JSON.parse(fs.readFileSync(projectCfg, "utf-8")) : null;
|
|
559
|
+
const hasToken = !!resolveAuthToken();
|
|
560
|
+
const modal = globalCfg?.wiki?.embedding?.modal ?? project?.wiki?.embedding?.modal ?? null;
|
|
561
|
+
res.writeHead(200);
|
|
562
|
+
res.end(JSON.stringify({
|
|
563
|
+
global: globalCfg,
|
|
564
|
+
project,
|
|
565
|
+
hasToken,
|
|
566
|
+
modal: modal
|
|
567
|
+
? {
|
|
568
|
+
workspace: modal.workspace ?? null,
|
|
569
|
+
baseUrl: modal.baseUrl ?? null,
|
|
570
|
+
model: modal.model ?? "embeddinggemma",
|
|
571
|
+
dim: modal.dim ?? null,
|
|
572
|
+
fallback: modal.fallback ?? null,
|
|
573
|
+
sync: modal.sync ?? null,
|
|
574
|
+
}
|
|
575
|
+
: null,
|
|
576
|
+
version: "0.8.0",
|
|
577
|
+
}));
|
|
578
|
+
}
|
|
579
|
+
/**
|
|
580
|
+
* Wrap a request handler with bearer-token auth. Use this for every
|
|
581
|
+
* new Stage-1 route so auth is consistent and testable in one place.
|
|
582
|
+
* The handler is only invoked if `authoriseRequest` passes.
|
|
583
|
+
*/
|
|
584
|
+
export const withAuth = (req, res, handler) => {
|
|
585
|
+
const result = authoriseRequest(req);
|
|
586
|
+
if (!result.ok) {
|
|
587
|
+
res.writeHead(401, { "WWW-Authenticate": "Bearer" });
|
|
588
|
+
res.end(JSON.stringify({
|
|
589
|
+
error: "unauthorized",
|
|
590
|
+
reason: result.reason,
|
|
591
|
+
hint: resolveAuthToken()
|
|
592
|
+
? "Send Authorization: Bearer <PVM_API_TOKEN>"
|
|
593
|
+
: "Set PVM_API_TOKEN in env or ~/.pi/agent/vault-mind.env, or in pi-vault-mind.config.json (wiki.embedding.modal.apiToken), then restart pi.",
|
|
594
|
+
}));
|
|
595
|
+
return;
|
|
596
|
+
}
|
|
597
|
+
handler();
|
|
598
|
+
};
|
|
599
|
+
/** Read the full request body as UTF-8. Already used by handleScan; exposed for new routes. */
|
|
600
|
+
export const readJsonBody = (req) => readBody(req).then((text) => (text ? JSON.parse(text) : {}));
|
|
601
|
+
// ── Agent queue handlers ────────────────────────────────────────────────────
|
|
602
|
+
const defaultVaultPath = () => {
|
|
603
|
+
const cfg = loadConfig(process.cwd());
|
|
604
|
+
const p = cfg.wiki.vaults?.default?.path;
|
|
605
|
+
return p ? expandHome(p) : undefined;
|
|
606
|
+
};
|
|
607
|
+
function handleAgentQueue(req, res) {
|
|
608
|
+
const url = new URL(req.url || "/", "http://127.0.0.1");
|
|
609
|
+
const statusParam = url.searchParams.get("status");
|
|
610
|
+
const validStatuses = ["pending", "running", "done", "failed", "cancelled"];
|
|
611
|
+
const status = statusParam && validStatuses.includes(statusParam)
|
|
612
|
+
? statusParam
|
|
613
|
+
: undefined;
|
|
614
|
+
const vaultPath = defaultVaultPath();
|
|
615
|
+
if (!vaultPath) {
|
|
616
|
+
res.writeHead(500);
|
|
617
|
+
res.end(JSON.stringify({ error: "No default vault configured." }));
|
|
618
|
+
return;
|
|
619
|
+
}
|
|
620
|
+
const jobs = listJobs(vaultPath, status);
|
|
621
|
+
res.writeHead(200);
|
|
622
|
+
res.end(JSON.stringify(jobs));
|
|
623
|
+
}
|
|
624
|
+
function handleAgentJob(req, res, pathname) {
|
|
625
|
+
// pathname is /agent/jobs/<id> or /agent/jobs/<id>/retry or /agent/jobs/<id>/cancel
|
|
626
|
+
const parts = pathname.split("/").filter(Boolean);
|
|
627
|
+
// parts = ["agent", "jobs", "<id>"] or ["agent", "jobs", "<id>", "retry"]
|
|
628
|
+
const id = parts[2];
|
|
629
|
+
const action = parts[3];
|
|
630
|
+
if (!id) {
|
|
631
|
+
res.writeHead(400);
|
|
632
|
+
res.end(JSON.stringify({ error: "Missing job id in path." }));
|
|
633
|
+
return;
|
|
634
|
+
}
|
|
635
|
+
const vaultPath = defaultVaultPath();
|
|
636
|
+
if (!vaultPath) {
|
|
637
|
+
res.writeHead(500);
|
|
638
|
+
res.end(JSON.stringify({ error: "No default vault configured." }));
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
if (!action) {
|
|
642
|
+
// GET /agent/jobs/:id
|
|
643
|
+
const job = getJob(vaultPath, id);
|
|
644
|
+
if (!job) {
|
|
645
|
+
res.writeHead(404);
|
|
646
|
+
res.end(JSON.stringify({ error: `Job ${id} not found.` }));
|
|
647
|
+
return;
|
|
648
|
+
}
|
|
649
|
+
res.writeHead(200);
|
|
650
|
+
res.end(JSON.stringify(job));
|
|
651
|
+
return;
|
|
652
|
+
}
|
|
653
|
+
if (req.method !== "POST") {
|
|
654
|
+
res.writeHead(405);
|
|
655
|
+
res.end(JSON.stringify({ error: "Method not allowed" }));
|
|
656
|
+
return;
|
|
657
|
+
}
|
|
658
|
+
if (action === "retry") {
|
|
659
|
+
const result = retryJob(vaultPath, id);
|
|
660
|
+
if ("error" in result) {
|
|
661
|
+
res.writeHead(409);
|
|
662
|
+
res.end(JSON.stringify({ error: result.error }));
|
|
663
|
+
return;
|
|
664
|
+
}
|
|
665
|
+
res.writeHead(200);
|
|
666
|
+
res.end(JSON.stringify(result.job));
|
|
667
|
+
return;
|
|
668
|
+
}
|
|
669
|
+
if (action === "cancel") {
|
|
670
|
+
const result = cancelJobHelper(vaultPath, id);
|
|
671
|
+
if ("error" in result) {
|
|
672
|
+
res.writeHead(409);
|
|
673
|
+
res.end(JSON.stringify({ error: result.error }));
|
|
674
|
+
return;
|
|
675
|
+
}
|
|
676
|
+
res.writeHead(200);
|
|
677
|
+
res.end(JSON.stringify(result.job));
|
|
678
|
+
return;
|
|
679
|
+
}
|
|
680
|
+
res.writeHead(404);
|
|
681
|
+
res.end(JSON.stringify({ error: `Unknown action: ${action}` }));
|
|
682
|
+
}
|
|
683
|
+
// ── Wiki tool routes ─────────────────────────────────────────────────────────
|
|
684
|
+
async function handleWikiSearch(req, res, serverState) {
|
|
685
|
+
const body = (await readJsonBody(req));
|
|
686
|
+
if (!body.query) {
|
|
687
|
+
res.writeHead(400);
|
|
688
|
+
res.end(JSON.stringify({ error: "Missing query." }));
|
|
689
|
+
return;
|
|
690
|
+
}
|
|
691
|
+
const vaultPath = serverState.vaultPath || process.cwd();
|
|
692
|
+
const cfg = loadConfig(vaultPath);
|
|
693
|
+
const collection = body.collection || "main";
|
|
694
|
+
const limit = body.limit ?? 5;
|
|
695
|
+
try {
|
|
696
|
+
const results = await searchHybrid(cfg.wiki.dataDir, collection, body.query, limit, cfg.wiki);
|
|
697
|
+
res.writeHead(200);
|
|
698
|
+
res.end(JSON.stringify({ hits: results, collection, query: body.query }));
|
|
699
|
+
}
|
|
700
|
+
catch (err) {
|
|
701
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
702
|
+
res.writeHead(500);
|
|
703
|
+
res.end(JSON.stringify({ error: message }));
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
async function handleWikiAppend(req, res, serverState) {
|
|
707
|
+
const body = (await readJsonBody(req));
|
|
708
|
+
if (!body.collection || !body.entry) {
|
|
709
|
+
res.writeHead(400);
|
|
710
|
+
res.end(JSON.stringify({ error: "Missing collection or entry." }));
|
|
711
|
+
return;
|
|
712
|
+
}
|
|
713
|
+
const vaultPath = serverState.vaultPath || process.cwd();
|
|
714
|
+
const cfg = loadConfig(vaultPath);
|
|
715
|
+
const def = cfg.collections[body.collection];
|
|
716
|
+
if (!def) {
|
|
717
|
+
res.writeHead(400);
|
|
718
|
+
res.end(JSON.stringify({
|
|
719
|
+
error: `Unknown collection "${body.collection}".`,
|
|
720
|
+
available: Object.keys(cfg.collections),
|
|
721
|
+
}));
|
|
722
|
+
return;
|
|
723
|
+
}
|
|
724
|
+
const line = `${JSON.stringify(body.entry)}\n`;
|
|
725
|
+
ensureDir(def.path);
|
|
726
|
+
fs.appendFileSync(def.path, line, "utf-8");
|
|
727
|
+
try {
|
|
728
|
+
await upsertEntry(cfg.wiki.dataDir, body.collection, body.entry, cfg.wiki);
|
|
729
|
+
res.writeHead(200);
|
|
730
|
+
res.end(JSON.stringify({ ok: true, collection: body.collection }));
|
|
731
|
+
}
|
|
732
|
+
catch (err) {
|
|
733
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
734
|
+
res.writeHead(500);
|
|
735
|
+
res.end(JSON.stringify({ error: message }));
|
|
736
|
+
}
|
|
737
|
+
}
|
package/dist/src/settings-ui.js
CHANGED
|
@@ -2,7 +2,7 @@ import * as fs from "node:fs";
|
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import { testOllamaConnection } from "./lance.js";
|
|
4
4
|
import { MODAL_TOKEN_ENV, createModalClient, modalUrl, resolveModalToken } from "./modal-config.js";
|
|
5
|
-
import {
|
|
5
|
+
import { collectionNames, findConfig, getGlobalConfigPath, loadConfig, shrinkHome, } from "./utils.js";
|
|
6
6
|
export const createCollectionWizard = async (ctx) => {
|
|
7
7
|
const { project: cfgPath } = findConfig(ctx.cwd);
|
|
8
8
|
if (!cfgPath) {
|
|
@@ -84,11 +84,11 @@ export const createInjectorWizard = async (ctx) => {
|
|
|
84
84
|
ctx.ui.notify(`✅ Injector "${name}" created and configured!`, "info");
|
|
85
85
|
};
|
|
86
86
|
export const setupWizard = async (ctx, cliArgs) => {
|
|
87
|
-
const existingGlobal = fs.existsSync(
|
|
87
|
+
const existingGlobal = fs.existsSync(getGlobalConfigPath());
|
|
88
88
|
// ── Non-interactive / CLI mode ──────────────────────────────────────────
|
|
89
89
|
if (cliArgs && (cliArgs.vault || cliArgs.provider || cliArgs.workspace)) {
|
|
90
90
|
const config = existingGlobal
|
|
91
|
-
? JSON.parse(fs.readFileSync(
|
|
91
|
+
? JSON.parse(fs.readFileSync(getGlobalConfigPath(), "utf-8"))
|
|
92
92
|
: { wiki: { embedding: {}, vaults: {} } };
|
|
93
93
|
config.wiki = config.wiki || {};
|
|
94
94
|
config.wiki.embedding = config.wiki.embedding || {};
|
|
@@ -106,11 +106,11 @@ export const setupWizard = async (ctx, cliArgs) => {
|
|
|
106
106
|
config.wiki.embedding.modal = config.wiki.embedding.modal || {};
|
|
107
107
|
config.wiki.embedding.modal.workspace = cliArgs.workspace;
|
|
108
108
|
}
|
|
109
|
-
const dir = path.dirname(
|
|
109
|
+
const dir = path.dirname(getGlobalConfigPath());
|
|
110
110
|
if (!fs.existsSync(dir))
|
|
111
111
|
fs.mkdirSync(dir, { recursive: true });
|
|
112
|
-
fs.writeFileSync(
|
|
113
|
-
const lines = ["✅ Global config written:", ` ${
|
|
112
|
+
fs.writeFileSync(getGlobalConfigPath(), `${JSON.stringify(config, null, 2)}\n`, "utf-8");
|
|
113
|
+
const lines = ["✅ Global config written:", ` ${getGlobalConfigPath()}`, ""];
|
|
114
114
|
if (cliArgs.vault)
|
|
115
115
|
lines.push(` Vault: ${cliArgs.vault}`);
|
|
116
116
|
if (cliArgs.provider)
|
|
@@ -137,7 +137,7 @@ export const setupWizard = async (ctx, cliArgs) => {
|
|
|
137
137
|
if (!choice || choice === "Exit")
|
|
138
138
|
return;
|
|
139
139
|
if (choice === "View current config") {
|
|
140
|
-
const cfg = JSON.parse(fs.readFileSync(
|
|
140
|
+
const cfg = JSON.parse(fs.readFileSync(getGlobalConfigPath(), "utf-8"));
|
|
141
141
|
ctx.ui.notify(JSON.stringify(cfg, null, 2), "info");
|
|
142
142
|
return;
|
|
143
143
|
}
|
|
@@ -189,7 +189,7 @@ export const setupWizard = async (ctx, cliArgs) => {
|
|
|
189
189
|
}
|
|
190
190
|
// ── Write config ────────────────────────────────────────────────────────
|
|
191
191
|
const config = existingGlobal
|
|
192
|
-
? JSON.parse(fs.readFileSync(
|
|
192
|
+
? JSON.parse(fs.readFileSync(getGlobalConfigPath(), "utf-8"))
|
|
193
193
|
: { wiki: { graph: { enabled: true, canvasSync: true }, ftsEnabled: true } };
|
|
194
194
|
config.wiki = config.wiki || {};
|
|
195
195
|
config.wiki.embedding = config.wiki.embedding || {};
|
|
@@ -202,13 +202,13 @@ export const setupWizard = async (ctx, cliArgs) => {
|
|
|
202
202
|
config.wiki.vaults.default = { path: shrinkHome(vaultPath), autoSync: true };
|
|
203
203
|
config.wiki.graph = config.wiki.graph || { enabled: true, canvasSync: true };
|
|
204
204
|
config.wiki.ftsEnabled = config.wiki.ftsEnabled !== false;
|
|
205
|
-
const dir = path.dirname(
|
|
205
|
+
const dir = path.dirname(getGlobalConfigPath());
|
|
206
206
|
if (!fs.existsSync(dir))
|
|
207
207
|
fs.mkdirSync(dir, { recursive: true });
|
|
208
|
-
fs.writeFileSync(
|
|
208
|
+
fs.writeFileSync(getGlobalConfigPath(), `${JSON.stringify(config, null, 2)}\n`, "utf-8");
|
|
209
209
|
ctx.ui.notify([
|
|
210
210
|
"✅ Global config written!",
|
|
211
|
-
` ${
|
|
211
|
+
` ${getGlobalConfigPath()}`,
|
|
212
212
|
"",
|
|
213
213
|
` Vault: ${vaultPath}`,
|
|
214
214
|
` Embedding: ${config.wiki.embedding.provider}`,
|