pi-vault-mind 0.7.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 (46) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +428 -0
  3. package/dist/index.d.ts +1 -0
  4. package/dist/index.js +1 -0
  5. package/dist/src/commands.d.ts +9 -0
  6. package/dist/src/commands.js +813 -0
  7. package/dist/src/events.d.ts +13 -0
  8. package/dist/src/events.js +236 -0
  9. package/dist/src/graph.d.ts +3 -0
  10. package/dist/src/graph.js +234 -0
  11. package/dist/src/index.d.ts +2 -0
  12. package/dist/src/index.js +61 -0
  13. package/dist/src/lance.d.ts +40 -0
  14. package/dist/src/lance.js +409 -0
  15. package/dist/src/server.d.ts +25 -0
  16. package/dist/src/server.js +180 -0
  17. package/dist/src/settings-ui.d.ts +9 -0
  18. package/dist/src/settings-ui.js +313 -0
  19. package/dist/src/state.d.ts +2 -0
  20. package/dist/src/state.js +16 -0
  21. package/dist/src/tools.d.ts +2 -0
  22. package/dist/src/tools.js +772 -0
  23. package/dist/src/types.d.ts +103 -0
  24. package/dist/src/types.js +51 -0
  25. package/dist/src/utils.d.ts +17 -0
  26. package/dist/src/utils.js +102 -0
  27. package/dist/src/vault-writer.d.ts +17 -0
  28. package/dist/src/vault-writer.js +141 -0
  29. package/dist/src/watcher.d.ts +91 -0
  30. package/dist/src/watcher.js +411 -0
  31. package/dist/src/widget.d.ts +3 -0
  32. package/dist/src/widget.js +12 -0
  33. package/dist/test/index.test.d.ts +1 -0
  34. package/dist/test/index.test.js +368 -0
  35. package/package.json +83 -0
  36. package/skills/vault-mind/SKILL.md +260 -0
  37. package/skills/vault-mind/references/tool-reference.md +53 -0
  38. package/skills/vault-mind-broadcaster/SKILL.md +112 -0
  39. package/skills/vault-mind-heavy-lifter/SKILL.md +34 -0
  40. package/skills/vault-mind-manager/SKILL.md +35 -0
  41. package/skills/vault-mind-miner/SKILL.md +40 -0
  42. package/skills/vault-mind-setup/SKILL.md +385 -0
  43. package/skills/vault-mind-setup/references/obsidian-cli-and-plugins.md +269 -0
  44. package/skills/vault-mind-setup/references/obsidian-vault-structure.md +106 -0
  45. package/skills/vault-mind-setup/references/pi-extension-wiring.md +236 -0
  46. package/skills/vault-mind-setup/references/troubleshooting-tree.md +147 -0
@@ -0,0 +1,409 @@
1
+ import * as fs from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import * as path from "node:path";
4
+ import * as lancedb from "@lancedb/lancedb";
5
+ import { LanceSchema, TextEmbeddingFunction } from "@lancedb/lancedb/embedding";
6
+ import * as arrow from "apache-arrow";
7
+ let db = null;
8
+ // ── Connection ──────────────────────────────────────────────────────────────
9
+ export const connect = async (dataDir) => {
10
+ if (db)
11
+ return db;
12
+ const absDir = path.isAbsolute(dataDir) ? dataDir : path.resolve(dataDir);
13
+ if (!fs.existsSync(absDir))
14
+ fs.mkdirSync(absDir, { recursive: true });
15
+ db = await lancedb.connect(absDir);
16
+ return db;
17
+ };
18
+ export const resetConnection = () => {
19
+ db = null;
20
+ tables = {};
21
+ };
22
+ // ── Embedding Functions ─────────────────────────────────────────────────────
23
+ /**
24
+ * Ollama embedding function that talks to a local Ollama instance.
25
+ * Uses the /api/embed endpoint with configurable model.
26
+ */
27
+ class OllamaEmbeddingFunction extends TextEmbeddingFunction {
28
+ model;
29
+ host;
30
+ ndimsValue;
31
+ constructor(options = {}) {
32
+ super();
33
+ this.model = options.model || "embeddinggemma";
34
+ this.host = options.host || "http://127.0.0.1:11434";
35
+ this.ndimsValue = options.dims;
36
+ }
37
+ ndims() {
38
+ return this.ndimsValue;
39
+ }
40
+ embeddingDataType() {
41
+ return new arrow.Float32();
42
+ }
43
+ async init() {
44
+ // Verify Ollama is reachable and the model is available
45
+ try {
46
+ const resp = await fetch(`${this.host}/api/tags`);
47
+ if (!resp.ok)
48
+ throw new Error(`Ollama returned ${resp.status}`);
49
+ const data = (await resp.json());
50
+ const modelNames = data.models?.map((m) => m.name) || [];
51
+ if (!modelNames.some((n) => n.startsWith(this.model))) {
52
+ console.warn(`[pi-vault-mind] Ollama model "${this.model}" not found. Available: ${modelNames.join(", ")}. Pull it with: ollama pull ${this.model}`);
53
+ }
54
+ }
55
+ catch (err) {
56
+ console.warn(`[pi-vault-mind] Cannot reach Ollama at ${this.host}: ${err}`);
57
+ }
58
+ }
59
+ async generateEmbeddings(texts) {
60
+ const embeddings = [];
61
+ for (const text of texts) {
62
+ const resp = await fetch(`${this.host}/api/embed`, {
63
+ method: "POST",
64
+ headers: { "Content-Type": "application/json" },
65
+ body: JSON.stringify({ model: this.model, input: text }),
66
+ });
67
+ if (!resp.ok) {
68
+ const body = await resp.text().catch(() => "");
69
+ throw new Error(`Ollama embed failed: ${resp.status} ${body}`);
70
+ }
71
+ const data = (await resp.json());
72
+ if (!data.embeddings?.[0]) {
73
+ throw new Error("Ollama returned no embedding for input");
74
+ }
75
+ embeddings.push(data.embeddings[0]);
76
+ }
77
+ return embeddings;
78
+ }
79
+ }
80
+ /**
81
+ * Transformers.js fallback — fully offline, works on any Node.js.
82
+ */
83
+ class TransformersEmbeddingFunction extends TextEmbeddingFunction {
84
+ extractor = null;
85
+ initPromise = null;
86
+ ndims() {
87
+ return 384;
88
+ }
89
+ embeddingDataType() {
90
+ return new arrow.Float32();
91
+ }
92
+ async init() {
93
+ if (!this.initPromise) {
94
+ this.initPromise = (async () => {
95
+ const { pipeline } = await import("@xenova/transformers");
96
+ this.extractor = await pipeline("feature-extraction", "Xenova/all-MiniLM-L6-v2");
97
+ })();
98
+ }
99
+ return this.initPromise;
100
+ }
101
+ async generateEmbeddings(texts) {
102
+ await this.init();
103
+ const embeddings = [];
104
+ for (const text of texts) {
105
+ const result = await this.extractor(text, { pooling: "mean", normalize: true });
106
+ embeddings.push(Array.from(result.data));
107
+ }
108
+ return embeddings;
109
+ }
110
+ }
111
+ const getModelsJsonPath = () => path.join(homedir(), ".pi", "agent", "models.json");
112
+ /** Read Pi's models.json for already-registered Ollama models. */
113
+ const loadPiModelsJson = () => {
114
+ const p = getModelsJsonPath();
115
+ if (!fs.existsSync(p))
116
+ return null;
117
+ try {
118
+ const data = JSON.parse(fs.readFileSync(p, "utf-8"));
119
+ return data?.providers?.ollama?.models || null;
120
+ }
121
+ catch {
122
+ return null;
123
+ }
124
+ };
125
+ /** Parse `ollama list` output (same format pi-model-router uses). */
126
+ const parseOllamaList = (output) => {
127
+ const lines = output.trim().split("\n");
128
+ const models = [];
129
+ for (let i = 1; i < lines.length; i++) {
130
+ const line = lines[i].trim();
131
+ if (!line)
132
+ continue;
133
+ const parts = line.split(/\s{2,}/);
134
+ if (parts.length >= 3) {
135
+ models.push({
136
+ name: parts[0],
137
+ size: parts[2],
138
+ modified: parts.slice(3).join(" "),
139
+ });
140
+ }
141
+ }
142
+ return models;
143
+ };
144
+ /**
145
+ * Discover Ollama models using multiple paths (matching pi-model-router pattern):
146
+ * 1. Try `pi.exec('ollama', ['list'])` — Pi's managed shell execution
147
+ * 2. Fall back to HTTP /api/tags
148
+ * 3. Fall back to Pi's cached models.json
149
+ */
150
+ export const discoverOllamaModels = async (piOrHost) => {
151
+ const host = typeof piOrHost === "string" ? piOrHost : "http://127.0.0.1:11434";
152
+ const pi = typeof piOrHost === "object" ? piOrHost : undefined;
153
+ // Path 1: pi.exec (preferred — uses Pi's managed process execution)
154
+ if (pi) {
155
+ try {
156
+ const result = await pi.exec("ollama", ["list"], { timeout: 10000 });
157
+ if (result.code === 0 && result.stdout) {
158
+ return parseOllamaList(result.stdout);
159
+ }
160
+ }
161
+ catch {
162
+ /* exec failed, try HTTP */
163
+ }
164
+ }
165
+ // Path 2: HTTP API
166
+ try {
167
+ const resp = await fetch(`${host}/api/tags`);
168
+ if (resp.ok) {
169
+ const data = (await resp.json());
170
+ return (data.models || []).map((m) => ({
171
+ name: m.name,
172
+ size: m.size ? `${(m.size / 1e9).toFixed(1)} GB` : "unknown",
173
+ modified: m.modified_at || "",
174
+ details: m.details,
175
+ }));
176
+ }
177
+ }
178
+ catch {
179
+ /* HTTP failed */
180
+ }
181
+ // Path 3: cached models.json (stale but no network needed)
182
+ const cached = loadPiModelsJson();
183
+ if (cached) {
184
+ return cached.map((m) => ({ name: m.id, size: "cached", modified: "" }));
185
+ }
186
+ return [];
187
+ };
188
+ /**
189
+ * Test Ollama connectivity. Uses pi.exec first, then HTTP.
190
+ */
191
+ export const testOllamaConnection = async (hostOrPi) => {
192
+ const models = await discoverOllamaModels(hostOrPi);
193
+ if (models.length > 0)
194
+ return { reachable: true, models };
195
+ // Last-ditch: raw connectivity test
196
+ const host = typeof hostOrPi === "string" ? hostOrPi : undefined;
197
+ if (host) {
198
+ try {
199
+ const resp = await fetch(`${host}/api/tags`, { signal: AbortSignal.timeout(3000) });
200
+ if (resp.ok) {
201
+ const data = (await resp.json());
202
+ return {
203
+ reachable: true,
204
+ models: (data.models || []).map((m) => ({ name: m.name, size: "", modified: "" })),
205
+ };
206
+ }
207
+ return { reachable: false, models: [], error: `HTTP ${resp.status}` };
208
+ }
209
+ catch (err) {
210
+ return { reachable: false, models: [], error: err.message };
211
+ }
212
+ }
213
+ return { reachable: false, models: [], error: "No connectivity paths available" };
214
+ };
215
+ /**
216
+ * Pull a model from Ollama. Uses pi.exec for managed timeout, falls back to HTTP.
217
+ */
218
+ export const pullOllamaModel = async (model, piOrHost) => {
219
+ const host = typeof piOrHost === "string" ? piOrHost : "http://127.0.0.1:11434";
220
+ const pi = typeof piOrHost === "object" ? piOrHost : undefined;
221
+ // Check if model already exists
222
+ const models = await discoverOllamaModels(piOrHost);
223
+ if (models.some((m) => m.name === model)) {
224
+ return { success: true, message: `Model "${model}" already exists` };
225
+ }
226
+ // Pull via pi.exec (preferred)
227
+ if (pi) {
228
+ try {
229
+ const result = await pi.exec("ollama", ["pull", model], { timeout: 120000 });
230
+ if (result.code === 0) {
231
+ return { success: true, message: `Successfully pulled "${model}"` };
232
+ }
233
+ return { success: false, message: `Pull exited with code ${result.code}: ${result.stderr}` };
234
+ }
235
+ catch (err) {
236
+ return { success: false, message: `Pull timed out or failed: ${err.message}` };
237
+ }
238
+ }
239
+ // Fall back to HTTP
240
+ try {
241
+ const resp = await fetch(`${host}/api/pull`, {
242
+ method: "POST",
243
+ headers: { "Content-Type": "application/json" },
244
+ body: JSON.stringify({ name: model, stream: false }),
245
+ });
246
+ if (!resp.ok) {
247
+ const body = await resp.text().catch(() => "");
248
+ return { success: false, message: `Pull failed: ${resp.status} ${body}` };
249
+ }
250
+ return { success: true, message: `Successfully pulled "${model}"` };
251
+ }
252
+ catch (err) {
253
+ return { success: false, message: `Error: ${err.message}` };
254
+ }
255
+ };
256
+ // ── Embedding Provider Factory ──────────────────────────────────────────────
257
+ const getEmbeddingFunction = async (cfg) => {
258
+ if (cfg.embedding.provider === "ollama") {
259
+ const fn = new OllamaEmbeddingFunction({
260
+ model: cfg.embedding.ollamaModel || "embeddinggemma",
261
+ host: cfg.embedding.ollamaHost || "http://127.0.0.1:11434",
262
+ });
263
+ await fn.init();
264
+ return fn;
265
+ }
266
+ const fn = new TransformersEmbeddingFunction();
267
+ await fn.init();
268
+ return fn;
269
+ };
270
+ // ── Schema Builders ─────────────────────────────────────────────────────────
271
+ function makeCollectionSchema(embeddingFn) {
272
+ return LanceSchema({
273
+ id: new arrow.Utf8(),
274
+ domain: new arrow.Utf8(),
275
+ source: new arrow.Utf8(),
276
+ fact: embeddingFn.sourceField(),
277
+ tag: new arrow.Utf8(),
278
+ artifact: new arrow.Utf8(),
279
+ created_at: new arrow.Utf8(),
280
+ vector: embeddingFn.vectorField(),
281
+ });
282
+ }
283
+ function makeEntitySchema(embeddingFn) {
284
+ return LanceSchema({
285
+ id: new arrow.Utf8(),
286
+ name: embeddingFn.sourceField(),
287
+ type: new arrow.Utf8(),
288
+ aliases: new arrow.Utf8(),
289
+ summary: new arrow.Utf8(),
290
+ collection_ids: new arrow.Utf8(),
291
+ created_at: new arrow.Utf8(),
292
+ updated_at: new arrow.Utf8(),
293
+ vector: embeddingFn.vectorField(),
294
+ });
295
+ }
296
+ function makeRelationSchema(embeddingFn) {
297
+ return LanceSchema({
298
+ id: new arrow.Utf8(),
299
+ from_entity_id: new arrow.Utf8(),
300
+ to_entity_id: new arrow.Utf8(),
301
+ relation_type: new arrow.Utf8(),
302
+ fact: embeddingFn.sourceField(),
303
+ fact_strength: new arrow.Float32(),
304
+ source_entry_ids: new arrow.Utf8(),
305
+ valid_at: new arrow.Utf8(),
306
+ expired_at: new arrow.Utf8(),
307
+ created_at: new arrow.Utf8(),
308
+ vector: embeddingFn.vectorField(),
309
+ });
310
+ }
311
+ let tables = {};
312
+ const getCollectionTable = async (dataDir, collectionName, cfg) => {
313
+ const conn = await connect(dataDir);
314
+ const tableName = `collection_${collectionName}`;
315
+ if (tables[tableName])
316
+ return tables[tableName];
317
+ const embeddingFn = await getEmbeddingFunction(cfg);
318
+ const schema = makeCollectionSchema(embeddingFn);
319
+ const existing = await conn.tableNames();
320
+ if (existing.includes(tableName)) {
321
+ tables[tableName] = await conn.openTable(tableName);
322
+ return tables[tableName];
323
+ }
324
+ tables[tableName] = await conn.createEmptyTable(tableName, schema);
325
+ if (cfg.ftsEnabled !== false) {
326
+ try {
327
+ await tables[tableName].createIndex("fact", { config: lancedb.Index.fts() });
328
+ }
329
+ catch {
330
+ /* already exists */
331
+ }
332
+ }
333
+ return tables[tableName];
334
+ };
335
+ const getEntityTable = async (dataDir, cfg) => {
336
+ const conn = await connect(dataDir);
337
+ const tableName = "entities";
338
+ if (tables[tableName])
339
+ return tables[tableName];
340
+ const embeddingFn = await getEmbeddingFunction(cfg);
341
+ const schema = makeEntitySchema(embeddingFn);
342
+ const existing = await conn.tableNames();
343
+ if (existing.includes(tableName)) {
344
+ tables[tableName] = await conn.openTable(tableName);
345
+ return tables[tableName];
346
+ }
347
+ tables[tableName] = await conn.createEmptyTable(tableName, schema);
348
+ return tables[tableName];
349
+ };
350
+ const getRelationTable = async (dataDir, cfg) => {
351
+ const conn = await connect(dataDir);
352
+ const tableName = "relations";
353
+ if (tables[tableName])
354
+ return tables[tableName];
355
+ const embeddingFn = await getEmbeddingFunction(cfg);
356
+ const schema = makeRelationSchema(embeddingFn);
357
+ const existing = await conn.tableNames();
358
+ if (existing.includes(tableName)) {
359
+ tables[tableName] = await conn.openTable(tableName);
360
+ return tables[tableName];
361
+ }
362
+ tables[tableName] = await conn.createEmptyTable(tableName, schema);
363
+ return tables[tableName];
364
+ };
365
+ // ── Public API ──────────────────────────────────────────────────────────────
366
+ export const upsertEntry = async (dataDir, collectionName, entry, cfg) => {
367
+ const table = await getCollectionTable(dataDir, collectionName, cfg);
368
+ const row = {
369
+ id: entry.id || crypto.randomUUID(),
370
+ domain: entry.domain || "",
371
+ source: entry.source || "",
372
+ fact: entry.fact || "",
373
+ tag: entry.tag || "",
374
+ artifact: entry.artifact || "",
375
+ created_at: entry.created_at || new Date().toISOString(),
376
+ };
377
+ await table.add([row]);
378
+ };
379
+ export const searchHybrid = async (dataDir, collectionName, query, limit, cfg) => {
380
+ const table = await getCollectionTable(dataDir, collectionName, cfg);
381
+ const embeddingFn = await getEmbeddingFunction(cfg);
382
+ const queryVector = await embeddingFn.generateEmbeddings([query]);
383
+ const results = await table.query().nearestTo(queryVector[0]).limit(limit).toArray();
384
+ return results;
385
+ };
386
+ export const searchFts = async (dataDir, collectionName, query, limit, cfg) => {
387
+ const table = await getCollectionTable(dataDir, collectionName, cfg);
388
+ const results = await table.search(query).limit(limit).toArray();
389
+ return results;
390
+ };
391
+ export const getStatus = async (dataDir) => {
392
+ const conn = await connect(dataDir);
393
+ const names = await conn.tableNames();
394
+ const status = {
395
+ dataDir,
396
+ tables: {},
397
+ };
398
+ for (const name of names) {
399
+ try {
400
+ const table = await conn.openTable(name);
401
+ const count = await table.countRows();
402
+ status.tables[name] = { rows: count };
403
+ }
404
+ catch {
405
+ status.tables[name] = { error: "failed to open" };
406
+ }
407
+ }
408
+ return status;
409
+ };
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Lightweight HTTP server for bidirectional Obsidian ↔ pi communication.
3
+ *
4
+ * Endpoints:
5
+ * GET /vault-mind/status → { running, dispatches, vaults, uptime }
6
+ * POST /vault-mind/scan → scan a file for @agent markers { file }
7
+ * POST /vault-mind/dispatch → fire a manual dispatch
8
+ *
9
+ * Binds 127.0.0.1 only — no network exposure. Port defaults to 11435,
10
+ * configurable via wiki.httpPort in pi-vault-mind.config.json.
11
+ *
12
+ * Used by the obsidian-shellcommands plugin to notify pi of file saves,
13
+ * replacing fs.watch reliance with explicit push notifications.
14
+ */
15
+ import * as http from "node:http";
16
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
17
+ import type { WatcherState } from "./watcher.js";
18
+ export interface ServerState {
19
+ server: http.Server | null;
20
+ port: number;
21
+ startTime: number;
22
+ }
23
+ export declare function createServerState(port?: number): ServerState;
24
+ export declare function startServer(pi: ExtensionAPI, serverState: ServerState, watcherState: WatcherState): void;
25
+ export declare function stopServer(serverState: ServerState): void;
@@ -0,0 +1,180 @@
1
+ /**
2
+ * Lightweight HTTP server for bidirectional Obsidian ↔ pi communication.
3
+ *
4
+ * Endpoints:
5
+ * GET /vault-mind/status → { running, dispatches, vaults, uptime }
6
+ * POST /vault-mind/scan → scan a file for @agent markers { file }
7
+ * POST /vault-mind/dispatch → fire a manual dispatch
8
+ *
9
+ * Binds 127.0.0.1 only — no network exposure. Port defaults to 11435,
10
+ * configurable via wiki.httpPort in pi-vault-mind.config.json.
11
+ *
12
+ * Used by the obsidian-shellcommands plugin to notify pi of file saves,
13
+ * replacing fs.watch reliance with explicit push notifications.
14
+ */
15
+ import * as fs from "node:fs";
16
+ import * as http from "node:http";
17
+ import { processQueue, scanFile } from "./watcher.js";
18
+ export function createServerState(port = 11435) {
19
+ return { server: null, port, startTime: 0 };
20
+ }
21
+ export function startServer(pi, serverState, watcherState) {
22
+ if (serverState.server) {
23
+ console.warn("[pi-vault-mind] HTTP server already running.");
24
+ return;
25
+ }
26
+ serverState.startTime = Date.now();
27
+ serverState.server = http.createServer((req, res) => {
28
+ // CORS for localhost-only access
29
+ res.setHeader("Access-Control-Allow-Origin", "http://localhost:11435");
30
+ res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
31
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type");
32
+ res.setHeader("Content-Type", "application/json");
33
+ if (req.method === "OPTIONS") {
34
+ res.writeHead(204);
35
+ res.end();
36
+ return;
37
+ }
38
+ const url = new URL(req.url || "/", `http://127.0.0.1:${serverState.port}`);
39
+ try {
40
+ switch (url.pathname) {
41
+ case "/vault-mind/status":
42
+ handleStatus(res, serverState, watcherState);
43
+ break;
44
+ case "/vault-mind/scan":
45
+ if (req.method !== "POST") {
46
+ res.writeHead(405);
47
+ res.end(JSON.stringify({ error: "Method not allowed" }));
48
+ return;
49
+ }
50
+ handleScan(req, res, pi, watcherState);
51
+ break;
52
+ case "/vault-mind/dispatch":
53
+ if (req.method !== "POST") {
54
+ res.writeHead(405);
55
+ res.end(JSON.stringify({ error: "Method not allowed" }));
56
+ return;
57
+ }
58
+ handleDispatch(req, res, pi, watcherState);
59
+ break;
60
+ default:
61
+ res.writeHead(404);
62
+ res.end(JSON.stringify({ error: "Not found" }));
63
+ }
64
+ }
65
+ catch (err) {
66
+ const message = err instanceof Error ? err.message : String(err);
67
+ res.writeHead(500);
68
+ res.end(JSON.stringify({ error: message }));
69
+ }
70
+ });
71
+ serverState.server.on("error", (err) => {
72
+ if (err.code === "EADDRINUSE") {
73
+ console.warn(`[pi-vault-mind] Port ${serverState.port} in use. HTTP server not started.`);
74
+ }
75
+ else {
76
+ console.error("[pi-vault-mind] HTTP server error:", err);
77
+ }
78
+ });
79
+ serverState.server.listen(serverState.port, "127.0.0.1", () => {
80
+ console.log(`[pi-vault-mind] HTTP server listening on http://127.0.0.1:${serverState.port}`);
81
+ });
82
+ }
83
+ export function stopServer(serverState) {
84
+ if (serverState.server) {
85
+ serverState.server.close();
86
+ serverState.server = null;
87
+ console.log("[pi-vault-mind] HTTP server stopped.");
88
+ }
89
+ }
90
+ function readBody(req) {
91
+ return new Promise((resolve, reject) => {
92
+ let body = "";
93
+ req.on("data", (chunk) => {
94
+ body += chunk.toString();
95
+ });
96
+ req.on("end", () => resolve(body));
97
+ req.on("error", reject);
98
+ });
99
+ }
100
+ function handleStatus(res, serverState, watcherState) {
101
+ const uptime = Date.now() - serverState.startTime;
102
+ const vaults = [...watcherState.watchers.keys()];
103
+ const recentDispatches = [...watcherState.activeDispatches_.values()].slice(-10).map((d) => ({
104
+ id: d.dispatchId,
105
+ file: d.filePath.split("/").slice(-3).join("/"),
106
+ role: d.role,
107
+ agent: d.agentName,
108
+ markers: d.markerCount,
109
+ when: d.dispatchedAt,
110
+ }));
111
+ res.writeHead(200);
112
+ res.end(JSON.stringify({
113
+ running: watcherState.running,
114
+ vaults,
115
+ activeDispatches: watcherState.activeDispatches,
116
+ pendingQueue: watcherState.pendingQueue.length,
117
+ maxConcurrent: watcherState.maxConcurrent,
118
+ dispatchRecords: watcherState.activeDispatches_.size,
119
+ recentDispatches,
120
+ uptimeMs: uptime,
121
+ uptime: `${Math.floor(uptime / 1000)}s`,
122
+ port: serverState.port,
123
+ }));
124
+ }
125
+ function handleScan(req, res, pi, watcherState) {
126
+ readBody(req)
127
+ .then((raw) => {
128
+ let parsed;
129
+ try {
130
+ parsed = JSON.parse(raw || "{}");
131
+ }
132
+ catch {
133
+ res.writeHead(400);
134
+ res.end(JSON.stringify({ error: "Invalid JSON body" }));
135
+ return;
136
+ }
137
+ if (!parsed.file) {
138
+ res.writeHead(400);
139
+ res.end(JSON.stringify({ error: "Missing 'file' field" }));
140
+ return;
141
+ }
142
+ const filePath = parsed.file;
143
+ if (!fs.existsSync(filePath)) {
144
+ res.writeHead(404);
145
+ res.end(JSON.stringify({ error: "File not found", file: filePath }));
146
+ return;
147
+ }
148
+ const groups = scanFile(filePath);
149
+ res.writeHead(200);
150
+ res.end(JSON.stringify({
151
+ file: filePath,
152
+ groups: groups.length,
153
+ details: groups.map((g) => ({
154
+ role: g.role,
155
+ markers: g.markers.length,
156
+ dispatchId: g.dispatchId,
157
+ })),
158
+ }));
159
+ // If markers found, queue them for dispatch
160
+ if (groups.length > 0) {
161
+ console.log(`[pi-vault-mind] HTTP scan: ${groups.length} group(s) in ${filePath}`);
162
+ for (const group of groups) {
163
+ watcherState.pendingQueue.push(group);
164
+ }
165
+ processQueue(pi, watcherState);
166
+ }
167
+ })
168
+ .catch((err) => {
169
+ if (!res.headersSent) {
170
+ res.writeHead(500);
171
+ res.end(JSON.stringify({ error: String(err) }));
172
+ }
173
+ });
174
+ }
175
+ function handleDispatch(req, res, _pi, _watcherState) {
176
+ res.writeHead(200);
177
+ res.end(JSON.stringify({
178
+ message: "Manual dispatch not yet implemented. Use /wiki watcher status for state.",
179
+ }));
180
+ }
@@ -0,0 +1,9 @@
1
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+ export declare const createCollectionWizard: (ctx: ExtensionContext) => Promise<void>;
3
+ export declare const createInjectorWizard: (ctx: ExtensionContext) => Promise<void>;
4
+ export declare const setupWizard: (ctx: ExtensionContext, cliArgs?: {
5
+ vault?: string;
6
+ provider?: string;
7
+ model?: string;
8
+ }) => Promise<void>;
9
+ export declare const openSettingsDashboard: (ctx: ExtensionContext) => Promise<void>;