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
|
@@ -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
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
1
2
|
import * as fs from "node:fs";
|
|
2
3
|
import * as path from "node:path";
|
|
3
4
|
import { Container, SelectList, Text } from "@earendil-works/pi-tui";
|
|
@@ -6,11 +7,16 @@ import { getActiveCollection, setActiveCollection } from "./state.js";
|
|
|
6
7
|
import { DEFAULT_CONFIG } from "./types.js";
|
|
7
8
|
import { CONFIG_FILES, EXT_ROOT, collectionNames, ensureDir, findConfig, getPiContextConfig, hasPiContextTools, loadConfig, } from "./utils.js";
|
|
8
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";
|
|
9
13
|
import { connect, pullOllamaModel, testOllamaConnection } from "./lance.js";
|
|
10
14
|
import { MODAL_TOKEN_ENV, createModalClient, isModalConfigured, resolveBaseUrl, resolveModalToken, resolveWorkspace, } from "./modal-config.js";
|
|
11
15
|
import { createServerState } from "./server.js";
|
|
12
16
|
import { createCollectionWizard, createInjectorWizard, openSettingsDashboard, setupWizard, } from "./settings-ui.js";
|
|
13
17
|
import { reindexRemote, syncAll, syncCollection } from "./sync.js";
|
|
18
|
+
import { tombstoneEntryById } from "./tombstone.js";
|
|
19
|
+
import { expandHome } from "./utils.js";
|
|
14
20
|
import { createWatcherState, getWatcherStatus, startWatcher, stopWatcher } from "./watcher.js";
|
|
15
21
|
// ── Shared helpers ───────────────────────────────────────────────────────────
|
|
16
22
|
const WIKI_USAGE = [
|
|
@@ -19,11 +25,13 @@ const WIKI_USAGE = [
|
|
|
19
25
|
" /wiki init Scaffold config + collections",
|
|
20
26
|
" /wiki validate Check LanceDB, config, and collection health",
|
|
21
27
|
" /wiki approve [collection] Batch-review pending entries",
|
|
28
|
+
" /wiki tombstone <collection> <entry-id> Redact an entry in place",
|
|
22
29
|
" /wiki settings Open interactive settings dashboard",
|
|
23
30
|
" /wiki audit Audit config for missing defaults",
|
|
24
31
|
" /wiki reindex Rebuild FTS + vector indexes (replaces old /qmd-index)",
|
|
25
32
|
" /wiki collection select Select active collection",
|
|
26
33
|
" /wiki collection create Create a new collection (wizard)",
|
|
34
|
+
" /wiki discover-schema Infer schema from a JSONL file",
|
|
27
35
|
" /wiki injector create Create a new injector (wizard)",
|
|
28
36
|
" /wiki context enable|disable Enable/disable pi-context integration",
|
|
29
37
|
" /wiki context status Show pi-context integration status",
|
|
@@ -40,6 +48,9 @@ const WIKI_USAGE = [
|
|
|
40
48
|
" /wiki watcher start Start passive file watcher",
|
|
41
49
|
" /wiki watcher stop Stop passive file watcher",
|
|
42
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",
|
|
43
54
|
" /wiki server status Show HTTP server status + port",
|
|
44
55
|
" /wiki setup Interactive global config wizard",
|
|
45
56
|
" /wiki setup --vault <path> CLI: set vault path",
|
|
@@ -383,6 +394,24 @@ const handleApprove = async (args, ctx) => {
|
|
|
383
394
|
fs.writeFileSync(pending.path, rejected.join("\n") + (rejected.length > 0 ? "\n" : ""));
|
|
384
395
|
ctx.ui.notify(`Approved ${approved} → "${targetName}". Rejected ${rejected.length}.`, "info");
|
|
385
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
|
+
};
|
|
386
415
|
// ── /wiki reindex ────────────────────────────────────────────────────────────
|
|
387
416
|
const handleReindex = async (args, ctx, pi) => {
|
|
388
417
|
const cfg = loadConfig(ctx.cwd);
|
|
@@ -825,7 +854,7 @@ const handleModalConfig = async (args, ctx) => {
|
|
|
825
854
|
return;
|
|
826
855
|
}
|
|
827
856
|
case "token": {
|
|
828
|
-
|
|
857
|
+
await handleModalToken(ctx, parts[1]);
|
|
829
858
|
return;
|
|
830
859
|
}
|
|
831
860
|
default:
|
|
@@ -1178,6 +1207,11 @@ const handleServer = (ctx) => {
|
|
|
1178
1207
|
const handleWatcher = async (args, ctx, pi) => {
|
|
1179
1208
|
const parts = args.trim().split(/\s+/g);
|
|
1180
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
|
+
};
|
|
1181
1215
|
switch (subcommand) {
|
|
1182
1216
|
case "start": {
|
|
1183
1217
|
const cfg = loadConfig(ctx.cwd);
|
|
@@ -1202,6 +1236,92 @@ const handleWatcher = async (args, ctx, pi) => {
|
|
|
1202
1236
|
ctx.ui.notify("Watcher stopped.", "info");
|
|
1203
1237
|
return;
|
|
1204
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
|
+
}
|
|
1205
1325
|
default: {
|
|
1206
1326
|
ctx.ui.notify(getWatcherStatus(watcherState), "info");
|
|
1207
1327
|
return;
|
|
@@ -1230,6 +1350,88 @@ const handleSetup = async (args, ctx) => {
|
|
|
1230
1350
|
const hasCliArgs = cliArgs.vault || cliArgs.provider || cliArgs.model || cliArgs.workspace;
|
|
1231
1351
|
await setupWizard(ctx, hasCliArgs ? cliArgs : undefined);
|
|
1232
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
|
+
};
|
|
1384
|
+
// ── /wiki modal token ────────────────────────────────────────────────────────
|
|
1385
|
+
/** Dotenv path where the Modal token is persisted. */
|
|
1386
|
+
const MODAL_TOKEN_ENV_PATH = path.join(process.env.HOME || process.env.USERPROFILE || "", ".pi", "agent", "vault-mind.env");
|
|
1387
|
+
const writeTokenEnv = (token) => {
|
|
1388
|
+
const dir = path.dirname(MODAL_TOKEN_ENV_PATH);
|
|
1389
|
+
if (!fs.existsSync(dir))
|
|
1390
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
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);
|
|
1394
|
+
};
|
|
1395
|
+
const fetchTokenFrom1Password = () => {
|
|
1396
|
+
try {
|
|
1397
|
+
return execFileSync("op", ["item", "get", "pi-vault-mind-auth", "--reveal", "--field", "password"], {
|
|
1398
|
+
encoding: "utf-8",
|
|
1399
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
1400
|
+
}).trim();
|
|
1401
|
+
}
|
|
1402
|
+
catch {
|
|
1403
|
+
return null;
|
|
1404
|
+
}
|
|
1405
|
+
};
|
|
1406
|
+
const handleModalToken = async (ctx, cliToken) => {
|
|
1407
|
+
if (cliToken) {
|
|
1408
|
+
writeTokenEnv(cliToken);
|
|
1409
|
+
ctx.ui.notify(`✅ Wrote token to ${MODAL_TOKEN_ENV_PATH}`, "info");
|
|
1410
|
+
return;
|
|
1411
|
+
}
|
|
1412
|
+
const envToken = process.env[MODAL_TOKEN_ENV];
|
|
1413
|
+
if (envToken) {
|
|
1414
|
+
ctx.ui.notify("PVM_API_TOKEN is already exported in env. Skipping write so we don't clobber it.", "warning");
|
|
1415
|
+
return;
|
|
1416
|
+
}
|
|
1417
|
+
const tokenFrom1Pass = fetchTokenFrom1Password();
|
|
1418
|
+
if (tokenFrom1Pass) {
|
|
1419
|
+
writeTokenEnv(tokenFrom1Pass);
|
|
1420
|
+
ctx.ui.notify(`✅ Fetched token from 1Password item "pi-vault-mind-auth" and wrote it to ${MODAL_TOKEN_ENV_PATH}`, "info");
|
|
1421
|
+
return;
|
|
1422
|
+
}
|
|
1423
|
+
if (!ctx.hasUI) {
|
|
1424
|
+
ctx.ui.notify("No 1Password token found and no TUI available. Use:\n /wiki modal token <token>", "error");
|
|
1425
|
+
return;
|
|
1426
|
+
}
|
|
1427
|
+
const token = await ctx.ui.input("Modal API token:", "paste bearer token here (will be written to ~/.pi/agent/vault-mind.env)");
|
|
1428
|
+
if (!token) {
|
|
1429
|
+
ctx.ui.notify("Token setup cancelled.", "warning");
|
|
1430
|
+
return;
|
|
1431
|
+
}
|
|
1432
|
+
writeTokenEnv(token);
|
|
1433
|
+
ctx.ui.notify(`✅ Wrote token to ${MODAL_TOKEN_ENV_PATH}`, "info");
|
|
1434
|
+
};
|
|
1233
1435
|
// ── Main /wiki command ───────────────────────────────────────────────────────
|
|
1234
1436
|
export const registerCommands = (pi) => {
|
|
1235
1437
|
pi.registerCommand("wiki", {
|
|
@@ -1240,11 +1442,13 @@ export const registerCommands = (pi) => {
|
|
|
1240
1442
|
"init",
|
|
1241
1443
|
"validate",
|
|
1242
1444
|
"approve",
|
|
1445
|
+
"tombstone",
|
|
1243
1446
|
"settings",
|
|
1244
1447
|
"audit",
|
|
1245
1448
|
"help",
|
|
1246
1449
|
"reindex",
|
|
1247
1450
|
"collection",
|
|
1451
|
+
"discover-schema",
|
|
1248
1452
|
"injector",
|
|
1249
1453
|
"context",
|
|
1250
1454
|
"embedding",
|
|
@@ -1271,6 +1475,11 @@ export const registerCommands = (pi) => {
|
|
|
1271
1475
|
.filter((c) => c.startsWith(prefix))
|
|
1272
1476
|
.map((c) => ({ label: c, value: c, description: `context ${c}` }));
|
|
1273
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
|
+
}
|
|
1274
1483
|
if (subcommand === "collection") {
|
|
1275
1484
|
return ["select", "create"]
|
|
1276
1485
|
.filter((c) => c.startsWith(prefix))
|
|
@@ -1287,17 +1496,17 @@ export const registerCommands = (pi) => {
|
|
|
1287
1496
|
.map((c) => ({ label: c, value: c, description: `reindex ${c}` }));
|
|
1288
1497
|
}
|
|
1289
1498
|
if (subcommand === "modal") {
|
|
1290
|
-
return ["status", "config", "sync", "jobs", "migrate"]
|
|
1499
|
+
return ["status", "config", "auto", "token", "sync", "jobs", "migrate"]
|
|
1291
1500
|
.filter((c) => c.startsWith(prefix))
|
|
1292
1501
|
.map((c) => ({ label: c, value: c, description: `modal ${c}` }));
|
|
1293
1502
|
}
|
|
1294
1503
|
if (subcommand === "watcher") {
|
|
1295
|
-
return ["start", "stop", "status"]
|
|
1504
|
+
return ["start", "stop", "status", "queue", "retry", "cancel"]
|
|
1296
1505
|
.filter((c) => c.startsWith(prefix))
|
|
1297
1506
|
.map((c) => ({ label: c, value: c, description: `watcher ${c}` }));
|
|
1298
1507
|
}
|
|
1299
1508
|
if (subcommand === "setup") {
|
|
1300
|
-
return ["--vault", "--provider", "--model"]
|
|
1509
|
+
return ["--vault", "--provider", "--model", "--workspace"]
|
|
1301
1510
|
.filter((c) => c.startsWith(prefix))
|
|
1302
1511
|
.map((c) => ({ label: c, value: c, description: `setup ${c}` }));
|
|
1303
1512
|
}
|
|
@@ -1315,12 +1524,16 @@ export const registerCommands = (pi) => {
|
|
|
1315
1524
|
return handleValidate(rest, ctx, pi);
|
|
1316
1525
|
case "approve":
|
|
1317
1526
|
return handleApprove(rest, ctx);
|
|
1527
|
+
case "tombstone":
|
|
1528
|
+
return handleTombstone(rest, ctx);
|
|
1318
1529
|
case "settings":
|
|
1319
1530
|
return openSettingsDashboard(ctx);
|
|
1320
1531
|
case "audit":
|
|
1321
1532
|
return auditConfig(ctx);
|
|
1322
1533
|
case "reindex":
|
|
1323
1534
|
return handleReindex(rest, ctx, pi);
|
|
1535
|
+
case "discover-schema":
|
|
1536
|
+
return handleDiscoverSchema(rest, ctx);
|
|
1324
1537
|
case "collection":
|
|
1325
1538
|
return handleCollection(rest, ctx);
|
|
1326
1539
|
case "injector":
|
|
@@ -1343,4 +1556,52 @@ export const registerCommands = (pi) => {
|
|
|
1343
1556
|
},
|
|
1344
1557
|
});
|
|
1345
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
|
+
});
|
|
1346
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>;
|