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.
- 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 +213 -2
- 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 +5 -2
- package/scripts/e2e-ws.mjs +364 -0
- package/scripts/fetch-modal-token.sh +4 -1
- 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
|
+
};
|
package/dist/src/commands.js
CHANGED
|
@@ -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
|
|
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>;
|