pi-vault-mind 0.7.5 → 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 (42) 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 +213 -2
  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 +5 -2
  40. package/scripts/e2e-ws.mjs +364 -0
  41. package/scripts/fetch-modal-token.sh +4 -1
  42. package/scripts/setup-test-config.sh +66 -0
@@ -0,0 +1,122 @@
1
+ /**
2
+ * Background auto-indexing for collection directories.
3
+ *
4
+ * Watches the directories that contain configured collection JSONL files.
5
+ * When a collection file is created, modified, or deleted, a debounced
6
+ * reindex is triggered for that collection.
7
+ *
8
+ * Controlled by `wiki.autoIndex` (default false). Started alongside the HTTP
9
+ * server and stopped on session shutdown.
10
+ */
11
+ import * as fs from "node:fs";
12
+ import * as path from "node:path";
13
+ import { upsertEntry } from "./lance.js";
14
+ export const DEFAULT_DEBOUNCE_MS = 2000;
15
+ export function createAutoIndexState() {
16
+ return { running: false, watchers: new Map(), debounceTimers: new Map() };
17
+ }
18
+ /**
19
+ * Start watching configured collection directories.
20
+ * Returns a stop function that closes watchers and clears timers.
21
+ */
22
+ export function startAutoIndex(cfg, options = {}) {
23
+ const state = options.state ?? createAutoIndexState();
24
+ if (state.running) {
25
+ console.warn("[pi-vault-mind] Auto-index already running.");
26
+ return { stop: () => stopAutoIndex(state), state };
27
+ }
28
+ state.running = true;
29
+ const debounceMs = options.debounceMs ?? DEFAULT_DEBOUNCE_MS;
30
+ const reindexFn = options.reindex ?? reindexCollection;
31
+ // Group collections by parent directory so a single watcher covers multiple
32
+ // collection files in the same directory.
33
+ const dirMap = new Map();
34
+ for (const [collection, def] of Object.entries(cfg.collections)) {
35
+ const dir = path.dirname(def.path);
36
+ const fileName = path.basename(def.path);
37
+ const entries = dirMap.get(dir) ?? [];
38
+ entries.push({ collection, fileName, def });
39
+ dirMap.set(dir, entries);
40
+ }
41
+ for (const [dir, entries] of dirMap) {
42
+ if (!fs.existsSync(dir)) {
43
+ console.warn(`[pi-vault-mind] Auto-index skipping missing directory: ${dir}`);
44
+ continue;
45
+ }
46
+ const watcher = fs.watch(dir, { recursive: false }, (_eventType, filename) => {
47
+ if (!filename || !state.running)
48
+ return;
49
+ const matched = entries.filter((e) => e.fileName === filename);
50
+ if (matched.length === 0)
51
+ return;
52
+ for (const m of matched) {
53
+ scheduleReindex(state, m.collection, m.def, cfg.wiki, reindexFn, debounceMs);
54
+ }
55
+ });
56
+ watcher.on("error", (err) => {
57
+ console.error(`[pi-vault-mind] Auto-index watcher error for ${dir}:`, err);
58
+ });
59
+ state.watchers.set(dir, watcher);
60
+ }
61
+ if (state.watchers.size > 0) {
62
+ console.log(`[pi-vault-mind] Auto-index started: watching ${state.watchers.size} collection dir(s).`);
63
+ }
64
+ return { stop: () => stopAutoIndex(state), state };
65
+ }
66
+ /**
67
+ * Stop all watchers and clear pending debounces.
68
+ */
69
+ export function stopAutoIndex(state) {
70
+ state.running = false;
71
+ for (const [, timer] of state.debounceTimers) {
72
+ clearTimeout(timer);
73
+ }
74
+ state.debounceTimers.clear();
75
+ for (const [dir, watcher] of state.watchers) {
76
+ watcher.close();
77
+ console.log(`[pi-vault-mind] Auto-index stopped watching ${dir}.`);
78
+ }
79
+ state.watchers.clear();
80
+ }
81
+ function scheduleReindex(state, collection, def, wikiCfg, reindexFn, debounceMs) {
82
+ const existing = state.debounceTimers.get(collection);
83
+ if (existing)
84
+ clearTimeout(existing);
85
+ const timer = setTimeout(() => {
86
+ state.debounceTimers.delete(collection);
87
+ reindexFn(wikiCfg.dataDir, collection, def, wikiCfg).catch((err) => {
88
+ const message = err instanceof Error ? err.message : String(err);
89
+ console.warn(`[pi-vault-mind] Auto-index reindex failed for "${collection}": ${message}`);
90
+ });
91
+ }, debounceMs);
92
+ state.debounceTimers.set(collection, timer);
93
+ }
94
+ /**
95
+ * Re-index a single collection by reading its JSONL source file and upserting
96
+ * each valid entry into LanceDB. Malformed lines are skipped.
97
+ */
98
+ export const reindexCollection = async (dataDir, collection, def, cfg) => {
99
+ if (!fs.existsSync(def.path)) {
100
+ console.log(`[pi-vault-mind] Auto-index skipped "${collection}" (file missing): ${def.path}`);
101
+ return;
102
+ }
103
+ const content = fs.readFileSync(def.path, "utf-8");
104
+ const lines = content.split("\n").filter(Boolean);
105
+ let indexed = 0;
106
+ let skipped = 0;
107
+ for (const line of lines) {
108
+ try {
109
+ const entry = JSON.parse(line);
110
+ if (typeof entry !== "object" || entry === null) {
111
+ skipped++;
112
+ continue;
113
+ }
114
+ await upsertEntry(dataDir, collection, entry, cfg);
115
+ indexed++;
116
+ }
117
+ catch {
118
+ skipped++;
119
+ }
120
+ }
121
+ console.log(`[pi-vault-mind] Auto-index reindexed "${collection}": ${indexed}/${lines.length} entries${skipped > 0 ? `, ${skipped} skipped` : ""}`);
122
+ };
@@ -7,11 +7,16 @@ import { getActiveCollection, setActiveCollection } from "./state.js";
7
7
  import { DEFAULT_CONFIG } from "./types.js";
8
8
  import { CONFIG_FILES, EXT_ROOT, collectionNames, ensureDir, findConfig, getPiContextConfig, hasPiContextTools, loadConfig, } from "./utils.js";
9
9
  import { updateActiveCollectionWidget } from "./widget.js";
10
+ import { cancelJobHelper, listJobs, retryJob, } from "./agent-queue.js";
11
+ import { handleDiscoverSchema } from "./discover-schema.js";
12
+ import { runAskIntake } from "./intake.js";
10
13
  import { connect, pullOllamaModel, testOllamaConnection } from "./lance.js";
11
14
  import { MODAL_TOKEN_ENV, createModalClient, isModalConfigured, resolveBaseUrl, resolveModalToken, resolveWorkspace, } from "./modal-config.js";
12
15
  import { createServerState } from "./server.js";
13
16
  import { createCollectionWizard, createInjectorWizard, openSettingsDashboard, setupWizard, } from "./settings-ui.js";
14
17
  import { reindexRemote, syncAll, syncCollection } from "./sync.js";
18
+ import { tombstoneEntryById } from "./tombstone.js";
19
+ import { expandHome } from "./utils.js";
15
20
  import { createWatcherState, getWatcherStatus, startWatcher, stopWatcher } from "./watcher.js";
16
21
  // ── Shared helpers ───────────────────────────────────────────────────────────
17
22
  const WIKI_USAGE = [
@@ -20,11 +25,13 @@ const WIKI_USAGE = [
20
25
  " /wiki init Scaffold config + collections",
21
26
  " /wiki validate Check LanceDB, config, and collection health",
22
27
  " /wiki approve [collection] Batch-review pending entries",
28
+ " /wiki tombstone <collection> <entry-id> Redact an entry in place",
23
29
  " /wiki settings Open interactive settings dashboard",
24
30
  " /wiki audit Audit config for missing defaults",
25
31
  " /wiki reindex Rebuild FTS + vector indexes (replaces old /qmd-index)",
26
32
  " /wiki collection select Select active collection",
27
33
  " /wiki collection create Create a new collection (wizard)",
34
+ " /wiki discover-schema Infer schema from a JSONL file",
28
35
  " /wiki injector create Create a new injector (wizard)",
29
36
  " /wiki context enable|disable Enable/disable pi-context integration",
30
37
  " /wiki context status Show pi-context integration status",
@@ -41,6 +48,9 @@ const WIKI_USAGE = [
41
48
  " /wiki watcher start Start passive file watcher",
42
49
  " /wiki watcher stop Stop passive file watcher",
43
50
  " /wiki watcher status Show watcher status",
51
+ " /wiki watcher queue [status] Show agent dispatch queue (pending|running|done|failed|cancelled|all)",
52
+ " /wiki watcher retry <id> Retry a failed or cancelled agent job",
53
+ " /wiki watcher cancel <id> Cancel a pending agent job",
44
54
  " /wiki server status Show HTTP server status + port",
45
55
  " /wiki setup Interactive global config wizard",
46
56
  " /wiki setup --vault <path> CLI: set vault path",
@@ -384,6 +394,24 @@ const handleApprove = async (args, ctx) => {
384
394
  fs.writeFileSync(pending.path, rejected.join("\n") + (rejected.length > 0 ? "\n" : ""));
385
395
  ctx.ui.notify(`Approved ${approved} → "${targetName}". Rejected ${rejected.length}.`, "info");
386
396
  };
397
+ // ── /wiki tombstone ─────────────────────────────────────────────────────────
398
+ const handleTombstone = async (args, ctx) => {
399
+ const parts = args.trim().split(/\s+/).filter(Boolean);
400
+ if (parts.length < 2) {
401
+ ctx.ui.notify("Usage: /wiki tombstone <collection> <entry-id>", "error");
402
+ return;
403
+ }
404
+ const collection = parts[0];
405
+ const entryId = parts.slice(1).join(" ");
406
+ const cfg = loadConfig(ctx.cwd);
407
+ const def = cfg.collections[collection];
408
+ if (!def) {
409
+ ctx.ui.notify(`Unknown collection "${collection}".`, "error");
410
+ return;
411
+ }
412
+ const result = tombstoneEntryById(def.path, entryId);
413
+ ctx.ui.notify(result.message, result.success ? "info" : "error");
414
+ };
387
415
  // ── /wiki reindex ────────────────────────────────────────────────────────────
388
416
  const handleReindex = async (args, ctx, pi) => {
389
417
  const cfg = loadConfig(ctx.cwd);
@@ -1179,6 +1207,11 @@ const handleServer = (ctx) => {
1179
1207
  const handleWatcher = async (args, ctx, pi) => {
1180
1208
  const parts = args.trim().split(/\s+/g);
1181
1209
  const subcommand = parts[0]?.toLowerCase() || "status";
1210
+ const defaultVaultPath = () => {
1211
+ const cfg = loadConfig(ctx.cwd);
1212
+ const p = cfg.wiki.vaults?.default?.path;
1213
+ return p ? expandHome(p) : undefined;
1214
+ };
1182
1215
  switch (subcommand) {
1183
1216
  case "start": {
1184
1217
  const cfg = loadConfig(ctx.cwd);
@@ -1203,6 +1236,92 @@ const handleWatcher = async (args, ctx, pi) => {
1203
1236
  ctx.ui.notify("Watcher stopped.", "info");
1204
1237
  return;
1205
1238
  }
1239
+ case "queue": {
1240
+ const vaultPath = defaultVaultPath();
1241
+ if (!vaultPath) {
1242
+ ctx.ui.notify("No default vault configured. Set one with /wiki setup --vault <path>.", "error");
1243
+ return;
1244
+ }
1245
+ const statusArg = parts[1]?.toLowerCase();
1246
+ const statuses = ["pending", "running", "done", "failed", "cancelled"];
1247
+ const status = statusArg === "all"
1248
+ ? undefined
1249
+ : statuses.includes(statusArg || "")
1250
+ ? statusArg
1251
+ : undefined;
1252
+ const jobs = listJobs(vaultPath, status);
1253
+ if (jobs.length === 0) {
1254
+ ctx.ui.notify(`No ${status || "agent"} jobs in queue for this vault.`, "info");
1255
+ return;
1256
+ }
1257
+ const lines = [
1258
+ `**Agent Queue (${status || "all"}) — ${jobs.length} job(s):**`,
1259
+ "",
1260
+ "| id | role | status | attempts | file |",
1261
+ "|---|---|---|---|---|",
1262
+ ...jobs.map((j) => {
1263
+ const rel = j.filePath.split("/").slice(-3).join("/");
1264
+ return `| ${j.id} | ${j.role} | ${j.status} | ${j.attempts} | ${rel} |`;
1265
+ }),
1266
+ ];
1267
+ ctx.ui.notify(lines.join("\n"), "info");
1268
+ return;
1269
+ }
1270
+ case "retry": {
1271
+ const id = parts[1];
1272
+ if (!id) {
1273
+ ctx.ui.notify("Usage: /wiki watcher retry <job-id>", "error");
1274
+ return;
1275
+ }
1276
+ const vaultPath = defaultVaultPath();
1277
+ if (!vaultPath) {
1278
+ ctx.ui.notify("No default vault configured.", "error");
1279
+ return;
1280
+ }
1281
+ const result = retryJob(vaultPath, id);
1282
+ if ("error" in result) {
1283
+ ctx.ui.notify(result.error, "warning");
1284
+ return;
1285
+ }
1286
+ const job = result.job;
1287
+ // Re-create the dispatch by re-sending the task as a user message
1288
+ const taskDescription = [
1289
+ `Retry of dispatch ${id} for @agent-${job.role} in ${job.filePath}.`,
1290
+ "",
1291
+ "Please dispatch a subagent to handle it:",
1292
+ "",
1293
+ "```",
1294
+ "subagent({",
1295
+ ` agent: "${job.agentName}",`,
1296
+ ` task: ${JSON.stringify(job.instruction)},`,
1297
+ ` context: "fork",`,
1298
+ " async: true,",
1299
+ "})",
1300
+ "```",
1301
+ ].join("\n");
1302
+ pi.sendUserMessage(taskDescription, { deliverAs: "followUp" });
1303
+ ctx.ui.notify(`✅ Retried job ${id} → ${job.agentName}`, "info");
1304
+ return;
1305
+ }
1306
+ case "cancel": {
1307
+ const id = parts[1];
1308
+ if (!id) {
1309
+ ctx.ui.notify("Usage: /wiki watcher cancel <job-id>", "error");
1310
+ return;
1311
+ }
1312
+ const vaultPath = defaultVaultPath();
1313
+ if (!vaultPath) {
1314
+ ctx.ui.notify("No default vault configured.", "error");
1315
+ return;
1316
+ }
1317
+ const result = cancelJobHelper(vaultPath, id);
1318
+ if ("error" in result) {
1319
+ ctx.ui.notify(result.error, "warning");
1320
+ return;
1321
+ }
1322
+ ctx.ui.notify(`✅ Cancelled job ${id}`, "info");
1323
+ return;
1324
+ }
1206
1325
  default: {
1207
1326
  ctx.ui.notify(getWatcherStatus(watcherState), "info");
1208
1327
  return;
@@ -1231,6 +1350,37 @@ const handleSetup = async (args, ctx) => {
1231
1350
  const hasCliArgs = cliArgs.vault || cliArgs.provider || cliArgs.model || cliArgs.workspace;
1232
1351
  await setupWizard(ctx, hasCliArgs ? cliArgs : undefined);
1233
1352
  };
1353
+ // ── /wiki intake ─────────────────────────────────────────────────────────────
1354
+ const handleIntake = async (args, ctx) => {
1355
+ const filePath = args.trim();
1356
+ if (!filePath) {
1357
+ ctx.ui.notify("Usage: /wiki intake <path-to-json-spec>", "error");
1358
+ return;
1359
+ }
1360
+ if (!fs.existsSync(filePath)) {
1361
+ ctx.ui.notify(`Intake spec not found: ${filePath}`, "error");
1362
+ return;
1363
+ }
1364
+ let parsed;
1365
+ try {
1366
+ parsed = JSON.parse(fs.readFileSync(filePath, "utf-8"));
1367
+ }
1368
+ catch (err) {
1369
+ ctx.ui.notify(`Failed to read intake spec: ${err.message}`, "error");
1370
+ return;
1371
+ }
1372
+ if (!parsed || typeof parsed !== "object" || !Array.isArray(parsed.questions)) {
1373
+ ctx.ui.notify("Invalid intake spec: expected { questions: [...], persist?, collection? }", "error");
1374
+ return;
1375
+ }
1376
+ const result = await runAskIntake(ctx, parsed);
1377
+ if (result.cancelled) {
1378
+ ctx.ui.notify("Intake cancelled.", "warning");
1379
+ }
1380
+ else {
1381
+ ctx.ui.notify("Intake complete. Answers stored.", "info");
1382
+ }
1383
+ };
1234
1384
  // ── /wiki modal token ────────────────────────────────────────────────────────
1235
1385
  /** Dotenv path where the Modal token is persisted. */
1236
1386
  const MODAL_TOKEN_ENV_PATH = path.join(process.env.HOME || process.env.USERPROFILE || "", ".pi", "agent", "vault-mind.env");
@@ -1238,7 +1388,9 @@ const writeTokenEnv = (token) => {
1238
1388
  const dir = path.dirname(MODAL_TOKEN_ENV_PATH);
1239
1389
  if (!fs.existsSync(dir))
1240
1390
  fs.mkdirSync(dir, { recursive: true });
1241
- fs.writeFileSync(MODAL_TOKEN_ENV_PATH, `PVM_API_TOKEN="${token}"\n`, { mode: 0o600 });
1391
+ // fs mode bits are unsupported on Windows; skip them to avoid EINVAL.
1392
+ const options = process.platform === "win32" ? {} : { mode: 0o600 };
1393
+ fs.writeFileSync(MODAL_TOKEN_ENV_PATH, `PVM_API_TOKEN="${token}"\n`, options);
1242
1394
  };
1243
1395
  const fetchTokenFrom1Password = () => {
1244
1396
  try {
@@ -1290,11 +1442,13 @@ export const registerCommands = (pi) => {
1290
1442
  "init",
1291
1443
  "validate",
1292
1444
  "approve",
1445
+ "tombstone",
1293
1446
  "settings",
1294
1447
  "audit",
1295
1448
  "help",
1296
1449
  "reindex",
1297
1450
  "collection",
1451
+ "discover-schema",
1298
1452
  "injector",
1299
1453
  "context",
1300
1454
  "embedding",
@@ -1321,6 +1475,11 @@ export const registerCommands = (pi) => {
1321
1475
  .filter((c) => c.startsWith(prefix))
1322
1476
  .map((c) => ({ label: c, value: c, description: `context ${c}` }));
1323
1477
  }
1478
+ if (subcommand === "discover-schema") {
1479
+ return ["--name", "--sample"]
1480
+ .filter((c) => c.startsWith(prefix))
1481
+ .map((c) => ({ label: c, value: c, description: `discover-schema ${c}` }));
1482
+ }
1324
1483
  if (subcommand === "collection") {
1325
1484
  return ["select", "create"]
1326
1485
  .filter((c) => c.startsWith(prefix))
@@ -1342,7 +1501,7 @@ export const registerCommands = (pi) => {
1342
1501
  .map((c) => ({ label: c, value: c, description: `modal ${c}` }));
1343
1502
  }
1344
1503
  if (subcommand === "watcher") {
1345
- return ["start", "stop", "status"]
1504
+ return ["start", "stop", "status", "queue", "retry", "cancel"]
1346
1505
  .filter((c) => c.startsWith(prefix))
1347
1506
  .map((c) => ({ label: c, value: c, description: `watcher ${c}` }));
1348
1507
  }
@@ -1365,12 +1524,16 @@ export const registerCommands = (pi) => {
1365
1524
  return handleValidate(rest, ctx, pi);
1366
1525
  case "approve":
1367
1526
  return handleApprove(rest, ctx);
1527
+ case "tombstone":
1528
+ return handleTombstone(rest, ctx);
1368
1529
  case "settings":
1369
1530
  return openSettingsDashboard(ctx);
1370
1531
  case "audit":
1371
1532
  return auditConfig(ctx);
1372
1533
  case "reindex":
1373
1534
  return handleReindex(rest, ctx, pi);
1535
+ case "discover-schema":
1536
+ return handleDiscoverSchema(rest, ctx);
1374
1537
  case "collection":
1375
1538
  return handleCollection(rest, ctx);
1376
1539
  case "injector":
@@ -1393,4 +1556,52 @@ export const registerCommands = (pi) => {
1393
1556
  },
1394
1557
  });
1395
1558
  // Legacy aliases — /wiki-* commands delegate to /wiki subcommand handlers
1559
+ /* ── /intake ───────────────────────────────────────────────────────────── */
1560
+ pi.registerCommand("intake", {
1561
+ description: "Run an intake questionnaire from a JSON spec file. Optionally write the result to --output.",
1562
+ handler: async (args, ctx) => {
1563
+ const parts = args.trim().split(/\s+/);
1564
+ const outputIdx = parts.indexOf("--output");
1565
+ const outputPath = outputIdx >= 0 ? parts[outputIdx + 1] : undefined;
1566
+ const specPath = outputIdx === 0 ? parts[2] : parts[0];
1567
+ if (!specPath || specPath === "--output") {
1568
+ ctx.ui.notify("Usage: /intake <path-to-json-spec> [--output <result-json-path>]", "error");
1569
+ return;
1570
+ }
1571
+ const resolved = path.isAbsolute(specPath) ? specPath : path.join(ctx.cwd, specPath);
1572
+ if (!fs.existsSync(resolved)) {
1573
+ ctx.ui.notify(`Intake spec not found: ${resolved}`, "error");
1574
+ return;
1575
+ }
1576
+ let parsed;
1577
+ try {
1578
+ parsed = JSON.parse(fs.readFileSync(resolved, "utf-8"));
1579
+ }
1580
+ catch (err) {
1581
+ ctx.ui.notify(`Failed to read intake spec: ${err.message}`, "error");
1582
+ return;
1583
+ }
1584
+ if (!parsed || typeof parsed !== "object" || !Array.isArray(parsed.questions)) {
1585
+ ctx.ui.notify("Invalid intake spec: expected { questions: [...], persist?, collection? }", "error");
1586
+ return;
1587
+ }
1588
+ const result = await runAskIntake(ctx, parsed);
1589
+ if (outputPath) {
1590
+ try {
1591
+ const out = path.isAbsolute(outputPath) ? outputPath : path.join(ctx.cwd, outputPath);
1592
+ ensureDir(out);
1593
+ fs.writeFileSync(out, `${JSON.stringify(result, null, 2)}\n`, "utf-8");
1594
+ }
1595
+ catch (err) {
1596
+ ctx.ui.notify(`Failed to write intake result: ${err.message}`, "error");
1597
+ }
1598
+ }
1599
+ if (result.cancelled) {
1600
+ ctx.ui.notify("Intake cancelled.", "warning");
1601
+ }
1602
+ else {
1603
+ ctx.ui.notify(`Intake complete. ${Object.keys(result.answers).length} answer(s).${result.ledgerEntry ? ` Ledger: ${result.ledgerEntry.path}.` : ""}${result.vaultEntry ? ` Vault: ${result.vaultEntry.path}.` : ""}`, "info");
1604
+ }
1605
+ },
1606
+ });
1396
1607
  };
@@ -0,0 +1,27 @@
1
+ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+ import type { CollectionDef } from "./types.js";
3
+ /** Scalar type family that can be inferred from JSON values. */
4
+ export type InferredType = "string" | "number" | "boolean" | "array" | "object";
5
+ /** Result returned by the discover_schema tool and helper functions. */
6
+ export interface DiscoverSchemaResult {
7
+ collectionName: string;
8
+ path: string;
9
+ schema: string[];
10
+ types: Record<string, InferredType | "mixed">;
11
+ rowCount: number;
12
+ sampled: number;
13
+ }
14
+ /** Infer a single JSON value's type. */
15
+ export declare const inferType: (value: unknown) => InferredType | null;
16
+ /** Read the first `sample` JSON lines from a .jsonl file and infer schema. */
17
+ export declare const discoverSchemaFromFile: (filePath: string, sample?: number) => Omit<DiscoverSchemaResult, "collectionName" | "path"> & {
18
+ allKeys: string[];
19
+ };
20
+ /** Register (or update) a collection in the project config. */
21
+ export declare const writeCollectionToConfig: (cwd: string, name: string, def: CollectionDef) => string;
22
+ /** Run the full discover_schema flow and update config. */
23
+ export declare const runDiscoverSchema: (cwd: string, file: string, name?: string, sample?: number) => DiscoverSchemaResult;
24
+ /** Register the `discover_schema` Pi tool. */
25
+ export declare const registerDiscoverSchemaTool: (pi: ExtensionAPI) => void;
26
+ /** Handler for the /wiki discover-schema slash command. */
27
+ export declare const handleDiscoverSchema: (args: string, ctx: ExtensionContext) => Promise<void>;