llm-kb 0.4.2 → 0.6.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 +11 -6
- package/bin/{chunk-DHOXVEIR.js → chunk-3WBSKCCH.js} +96 -119
- package/bin/chunk-EZ7LPPEP.js +218 -0
- package/bin/chunk-Y2764FFH.js +1356 -0
- package/bin/cli.js +385 -874
- package/bin/{indexer-KSYRIVVN.js → indexer-K37QM2HP.js} +2 -1
- package/bin/public/index.html +949 -0
- package/bin/server-QC5SN6T4.js +1069 -0
- package/package.json +4 -3
package/bin/cli.js
CHANGED
|
@@ -1,16 +1,21 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
-
buildIndex
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
3
|
+
buildIndex
|
|
4
|
+
} from "./chunk-EZ7LPPEP.js";
|
|
5
|
+
import {
|
|
6
|
+
appendToQueryLog,
|
|
7
|
+
buildTrace,
|
|
8
|
+
completeWithFallback,
|
|
9
|
+
createChat,
|
|
10
|
+
parseCitations,
|
|
11
|
+
query,
|
|
12
|
+
saveTrace,
|
|
13
|
+
updateWiki
|
|
14
|
+
} from "./chunk-Y2764FFH.js";
|
|
15
|
+
import "./chunk-3WBSKCCH.js";
|
|
9
16
|
import "./chunk-3YMNGUZZ.js";
|
|
10
17
|
import "./chunk-LDHOKBJA.js";
|
|
11
|
-
import
|
|
12
|
-
getModels
|
|
13
|
-
} from "./chunk-5PYKQQLA.js";
|
|
18
|
+
import "./chunk-5PYKQQLA.js";
|
|
14
19
|
import "./chunk-EAQYK3U2.js";
|
|
15
20
|
|
|
16
21
|
// src/cli.ts
|
|
@@ -126,6 +131,19 @@ ${p.text}`).join("\n\n---\n\n");
|
|
|
126
131
|
};
|
|
127
132
|
await writeFile(mdPath, markdown);
|
|
128
133
|
await writeFile(jsonPath, JSON.stringify(bboxData, null, 2));
|
|
134
|
+
const pagesDir = join(outputDir, `${name}.pages`);
|
|
135
|
+
await mkdir(pagesDir, { recursive: true });
|
|
136
|
+
for (const pageData of bboxData.pages) {
|
|
137
|
+
const pageFile = join(pagesDir, `${pageData.page}.json`);
|
|
138
|
+
await writeFile(pageFile, JSON.stringify({
|
|
139
|
+
source: bboxData.source,
|
|
140
|
+
totalPages: bboxData.totalPages,
|
|
141
|
+
page: pageData.page,
|
|
142
|
+
width: pageData.width,
|
|
143
|
+
height: pageData.height,
|
|
144
|
+
textItems: pageData.textItems
|
|
145
|
+
}));
|
|
146
|
+
}
|
|
129
147
|
return {
|
|
130
148
|
name,
|
|
131
149
|
mdPath,
|
|
@@ -140,7 +158,7 @@ ${p.text}`).join("\n\n---\n\n");
|
|
|
140
158
|
import { watch } from "chokidar";
|
|
141
159
|
import { extname as extname2, basename as basename2 } from "path";
|
|
142
160
|
import chalk from "chalk";
|
|
143
|
-
function startWatcher({ folder, sourcesDir, debounceMs = 2e3, authStorage, indexModel }) {
|
|
161
|
+
function startWatcher({ folder, sourcesDir, debounceMs = 2e3, authStorage, indexModel, onSourcesChanged }) {
|
|
144
162
|
let pendingFiles = [];
|
|
145
163
|
let debounceTimer = null;
|
|
146
164
|
async function processBatch() {
|
|
@@ -166,6 +184,12 @@ function startWatcher({ folder, sourcesDir, debounceMs = 2e3, authStorage, index
|
|
|
166
184
|
try {
|
|
167
185
|
await buildIndex(folder, sourcesDir, void 0, authStorage, indexModel);
|
|
168
186
|
console.log(chalk.green(` \u2713 index.md updated`));
|
|
187
|
+
if (onSourcesChanged) {
|
|
188
|
+
try {
|
|
189
|
+
await onSourcesChanged();
|
|
190
|
+
} catch {
|
|
191
|
+
}
|
|
192
|
+
}
|
|
169
193
|
} catch (err) {
|
|
170
194
|
console.log(chalk.red(` \u2717 ${err.message}`));
|
|
171
195
|
}
|
|
@@ -207,250 +231,28 @@ function startWatcher({ folder, sourcesDir, debounceMs = 2e3, authStorage, index
|
|
|
207
231
|
|
|
208
232
|
// src/session-watcher.ts
|
|
209
233
|
import { watch as watch2 } from "chokidar";
|
|
210
|
-
import { join as
|
|
211
|
-
import { readdir as
|
|
212
|
-
import { existsSync as existsSync3 } from "fs";
|
|
213
|
-
|
|
214
|
-
// src/trace-builder.ts
|
|
215
|
-
import { readFile, writeFile as writeFile2, mkdir as mkdir2, readdir as readdir2 } from "fs/promises";
|
|
234
|
+
import { join as join3, basename as basename3 } from "path";
|
|
235
|
+
import { readdir as readdir2, readFile, writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
|
|
216
236
|
import { existsSync } from "fs";
|
|
217
|
-
import { join as join3, basename as pathBasename } from "path";
|
|
218
|
-
async function buildTrace(sessionFile, sourcesDir) {
|
|
219
|
-
const raw = await readFile(sessionFile, "utf-8");
|
|
220
|
-
const lines = raw.trim().split("\n").filter(Boolean);
|
|
221
|
-
if (lines.length < 2) return null;
|
|
222
|
-
const entries = [];
|
|
223
|
-
let header = null;
|
|
224
|
-
for (const line of lines) {
|
|
225
|
-
try {
|
|
226
|
-
const obj = JSON.parse(line);
|
|
227
|
-
if (obj.type === "session") header = obj;
|
|
228
|
-
else entries.push(obj);
|
|
229
|
-
} catch {
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
if (!header) return null;
|
|
233
|
-
const messages = entries.filter((e) => e.type === "message");
|
|
234
|
-
const lastAssistant = [...messages].reverse().find(
|
|
235
|
-
(e) => e.message?.role === "assistant" && e.message?.stopReason === "stop"
|
|
236
|
-
);
|
|
237
|
-
if (!lastAssistant) return null;
|
|
238
|
-
const modelChange = entries.find((e) => e.type === "model_change");
|
|
239
|
-
const model = modelChange?.modelId ?? lastAssistant.message?.model ?? void 0;
|
|
240
|
-
const firstUser = messages.find((e) => e.message?.role === "user");
|
|
241
|
-
const question = extractText(firstUser?.message?.content);
|
|
242
|
-
const sessionInfo = entries.find((e) => e.type === "session_info");
|
|
243
|
-
const sessionName = sessionInfo?.name ?? "";
|
|
244
|
-
const mode = sessionName.startsWith("index:") ? "index" : sessionName.startsWith("query:") || question ? "query" : "unknown";
|
|
245
|
-
const answer = extractText(lastAssistant.message?.content);
|
|
246
|
-
const filesRead = [];
|
|
247
|
-
for (const entry of messages) {
|
|
248
|
-
if (entry.message?.role !== "assistant") continue;
|
|
249
|
-
for (const block of entry.message?.content ?? []) {
|
|
250
|
-
if (block.type === "toolCall" && block.name === "read") {
|
|
251
|
-
const p = block.arguments?.path ?? "";
|
|
252
|
-
if (p && !filesRead.includes(p)) filesRead.push(p);
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
let filesAvailable = [];
|
|
257
|
-
try {
|
|
258
|
-
const all = await readdir2(sourcesDir);
|
|
259
|
-
filesAvailable = all.filter((f) => f.endsWith(".md"));
|
|
260
|
-
} catch {
|
|
261
|
-
}
|
|
262
|
-
const filesSkipped = filesAvailable.filter(
|
|
263
|
-
(f) => !filesRead.some((r) => r.endsWith(f))
|
|
264
|
-
);
|
|
265
|
-
const firstMsg = messages[0];
|
|
266
|
-
const lastMsg = messages[messages.length - 1];
|
|
267
|
-
let durationMs;
|
|
268
|
-
if (firstMsg?.timestamp && lastMsg?.timestamp) {
|
|
269
|
-
durationMs = new Date(lastMsg.timestamp).getTime() - new Date(firstMsg.timestamp).getTime();
|
|
270
|
-
}
|
|
271
|
-
return {
|
|
272
|
-
sessionId: header.id,
|
|
273
|
-
sessionFile: pathBasename(sessionFile),
|
|
274
|
-
timestamp: header.timestamp,
|
|
275
|
-
mode,
|
|
276
|
-
question: question || void 0,
|
|
277
|
-
answer: answer || void 0,
|
|
278
|
-
filesRead,
|
|
279
|
-
filesAvailable,
|
|
280
|
-
filesSkipped,
|
|
281
|
-
model,
|
|
282
|
-
durationMs
|
|
283
|
-
};
|
|
284
|
-
}
|
|
285
|
-
async function saveTrace(kbRoot, trace) {
|
|
286
|
-
const tracesDir = join3(kbRoot, ".llm-kb", "traces");
|
|
287
|
-
await mkdir2(tracesDir, { recursive: true });
|
|
288
|
-
const outPath = join3(tracesDir, `${trace.sessionId}.json`);
|
|
289
|
-
await writeFile2(outPath, JSON.stringify(trace, null, 2) + "\n", "utf-8");
|
|
290
|
-
}
|
|
291
|
-
async function appendToQueryLog(kbRoot, trace) {
|
|
292
|
-
if (trace.mode !== "query" || !trace.question) return;
|
|
293
|
-
const wikiDir = join3(kbRoot, ".llm-kb", "wiki");
|
|
294
|
-
await mkdir2(wikiDir, { recursive: true });
|
|
295
|
-
const logPath = join3(wikiDir, "queries.md");
|
|
296
|
-
const date = new Date(trace.timestamp).toISOString().replace("T", " ").slice(0, 19);
|
|
297
|
-
const durationSec = trace.durationMs ? `${(trace.durationMs / 1e3).toFixed(1)}s` : "?";
|
|
298
|
-
const filesLine = trace.filesRead.length > 0 ? trace.filesRead.map((f) => pathBasename(f)).join(", ") : "_none_";
|
|
299
|
-
let header = "";
|
|
300
|
-
if (!existsSync(logPath)) {
|
|
301
|
-
header = `# Query Log
|
|
302
|
-
|
|
303
|
-
All queries run against this knowledge base.
|
|
304
|
-
|
|
305
|
-
---
|
|
306
|
-
|
|
307
|
-
`;
|
|
308
|
-
}
|
|
309
|
-
const entry = [
|
|
310
|
-
`## ${trace.question}`,
|
|
311
|
-
``,
|
|
312
|
-
`- **Date:** ${date}`,
|
|
313
|
-
`- **Model:** ${trace.model ?? "unknown"}`,
|
|
314
|
-
`- **Duration:** ${durationSec}`,
|
|
315
|
-
`- **Files read:** ${filesLine}`,
|
|
316
|
-
trace.filesSkipped.length > 0 ? `- **Files skipped:** ${trace.filesSkipped.join(", ")}` : null,
|
|
317
|
-
``,
|
|
318
|
-
trace.answer ? `### Answer
|
|
319
|
-
|
|
320
|
-
${trace.answer}` : null,
|
|
321
|
-
``,
|
|
322
|
-
`---`,
|
|
323
|
-
``
|
|
324
|
-
].filter((l) => l !== null).join("\n");
|
|
325
|
-
const existing = existsSync(logPath) ? await readFile(logPath, "utf-8") : "";
|
|
326
|
-
await writeFile2(logPath, header + entry + existing, "utf-8");
|
|
327
|
-
}
|
|
328
|
-
function extractText(content) {
|
|
329
|
-
if (!content) return "";
|
|
330
|
-
if (typeof content === "string") return content;
|
|
331
|
-
if (Array.isArray(content)) {
|
|
332
|
-
return content.filter((b) => b.type === "text").map((b) => b.text ?? "").join("").trim();
|
|
333
|
-
}
|
|
334
|
-
return "";
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
// src/wiki-updater.ts
|
|
338
|
-
import { AuthStorage } from "@mariozechner/pi-coding-agent";
|
|
339
|
-
import { existsSync as existsSync2 } from "fs";
|
|
340
|
-
import { readFile as readFile2, writeFile as writeFile3, mkdir as mkdir3 } from "fs/promises";
|
|
341
|
-
import { join as join4 } from "path";
|
|
342
|
-
import { homedir } from "os";
|
|
343
|
-
async function resolveApiKey(authStorage) {
|
|
344
|
-
if (authStorage) {
|
|
345
|
-
return authStorage.getApiKey("anthropic");
|
|
346
|
-
}
|
|
347
|
-
const piAuthPath = join4(homedir(), ".pi", "agent", "auth.json");
|
|
348
|
-
if (existsSync2(piAuthPath)) {
|
|
349
|
-
const storage = AuthStorage.create(piAuthPath);
|
|
350
|
-
return storage.getApiKey("anthropic");
|
|
351
|
-
}
|
|
352
|
-
return process.env.ANTHROPIC_API_KEY;
|
|
353
|
-
}
|
|
354
|
-
function buildPrompt(question, answer, sources, date, currentWiki) {
|
|
355
|
-
const rules = `Rules for wiki structure:
|
|
356
|
-
- Use ## for CONCEPTS and TOPICS \u2014 NOT source file names
|
|
357
|
-
Good: "## Electronic Evidence", "## Mob Lynching", "## Burden of Proof"
|
|
358
|
-
Bad: "## Indian Evidence Act.md", "## indian penal code - new.md"
|
|
359
|
-
- Use ### for subtopics within a concept
|
|
360
|
-
- A concept can draw from MULTIPLE source files \u2014 synthesize, don't separate by file
|
|
361
|
-
- If knowledge from this Q&A fits an existing concept, ADD to it \u2014 never duplicate
|
|
362
|
-
- If it's a genuinely new concept, create a new ## section
|
|
363
|
-
- Be concise: bullet points for lists, short prose for explanations
|
|
364
|
-
- Include source citations inline: (Source: filename, p.X)
|
|
365
|
-
- Add cross-references where concepts relate: See also: [[Other Concept]]
|
|
366
|
-
- End each ## section with: *Sources: file1, file2 \xB7 date*
|
|
367
|
-
- Separate ## sections with: ---`;
|
|
368
|
-
if (currentWiki.trim()) {
|
|
369
|
-
return `You are maintaining a concept-organized knowledge wiki.
|
|
370
|
-
|
|
371
|
-
## Current wiki
|
|
372
|
-
${currentWiki}
|
|
373
|
-
|
|
374
|
-
## New Q&A to integrate
|
|
375
|
-
**Question:** ${question}
|
|
376
|
-
**Sources used:** ${sources}
|
|
377
|
-
**Date:** ${date}
|
|
378
|
-
|
|
379
|
-
**Answer:**
|
|
380
|
-
${answer}
|
|
381
|
-
|
|
382
|
-
---
|
|
383
|
-
|
|
384
|
-
Update the wiki to integrate this new knowledge.
|
|
385
|
-
${rules}
|
|
386
|
-
|
|
387
|
-
Return ONLY the complete updated wiki markdown. No explanation.`;
|
|
388
|
-
}
|
|
389
|
-
return `You are creating a concept-organized knowledge wiki.
|
|
390
|
-
|
|
391
|
-
## First Q&A to add
|
|
392
|
-
**Question:** ${question}
|
|
393
|
-
**Sources used:** ${sources}
|
|
394
|
-
**Date:** ${date}
|
|
395
|
-
|
|
396
|
-
**Answer:**
|
|
397
|
-
${answer}
|
|
398
|
-
|
|
399
|
-
---
|
|
400
|
-
|
|
401
|
-
Create a clean wiki from this Q&A.
|
|
402
|
-
- Start with: # Knowledge Wiki\\n\\n> Concept-organized knowledge base. Updated after each query.\\n\\n---
|
|
403
|
-
${rules}
|
|
404
|
-
|
|
405
|
-
Return ONLY the wiki markdown. No explanation.`;
|
|
406
|
-
}
|
|
407
|
-
async function updateWiki(kbRoot, trace, authStorage, indexModelId = "claude-haiku-4-5") {
|
|
408
|
-
if (trace.mode !== "query" || !trace.question || !trace.answer) return;
|
|
409
|
-
const wikiDir = join4(kbRoot, ".llm-kb", "wiki");
|
|
410
|
-
await mkdir3(wikiDir, { recursive: true });
|
|
411
|
-
const wikiPath = join4(wikiDir, "wiki.md");
|
|
412
|
-
const currentWiki = existsSync2(wikiPath) ? await readFile2(wikiPath, "utf-8").catch(() => "") : "";
|
|
413
|
-
const sources = trace.filesRead.map((f) => f.split(/[\\/]/).pop() ?? f).filter((f) => f.endsWith(".md") && f !== "index.md" && f !== "wiki.md").join(", ") || "unknown";
|
|
414
|
-
const date = new Date(trace.timestamp).toISOString().slice(0, 10);
|
|
415
|
-
const prompt = buildPrompt(trace.question, trace.answer, sources, date, currentWiki);
|
|
416
|
-
const apiKey = await resolveApiKey(authStorage);
|
|
417
|
-
if (!apiKey) return;
|
|
418
|
-
const model = getModels("anthropic").find((m) => m.id === indexModelId);
|
|
419
|
-
if (!model) return;
|
|
420
|
-
const result = await completeSimple(
|
|
421
|
-
model,
|
|
422
|
-
{
|
|
423
|
-
systemPrompt: "You are a precise knowledge librarian. Organize information by CONCEPT, not by source file. Synthesize knowledge from multiple sources into unified topic articles. Return only clean markdown.",
|
|
424
|
-
messages: [{ role: "user", content: prompt, timestamp: Date.now() }]
|
|
425
|
-
},
|
|
426
|
-
{ apiKey }
|
|
427
|
-
);
|
|
428
|
-
const text = result.content.filter((b) => b.type === "text").map((b) => b.text).join("").trim();
|
|
429
|
-
if (text) {
|
|
430
|
-
await writeFile3(wikiPath, text + "\n", "utf-8");
|
|
431
|
-
}
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
// src/session-watcher.ts
|
|
435
237
|
var PROCESSED_LOG = ".llm-kb/traces/.processed";
|
|
436
238
|
async function loadProcessed(kbRoot) {
|
|
437
|
-
const path3 =
|
|
438
|
-
if (!
|
|
239
|
+
const path3 = join3(kbRoot, PROCESSED_LOG);
|
|
240
|
+
if (!existsSync(path3)) return /* @__PURE__ */ new Set();
|
|
439
241
|
try {
|
|
440
|
-
const lines = (await
|
|
242
|
+
const lines = (await readFile(path3, "utf-8")).split("\n").filter(Boolean);
|
|
441
243
|
return new Set(lines);
|
|
442
244
|
} catch {
|
|
443
245
|
return /* @__PURE__ */ new Set();
|
|
444
246
|
}
|
|
445
247
|
}
|
|
446
248
|
async function markProcessed(kbRoot, sessionId) {
|
|
447
|
-
const path3 =
|
|
448
|
-
await
|
|
449
|
-
await
|
|
249
|
+
const path3 = join3(kbRoot, PROCESSED_LOG);
|
|
250
|
+
await mkdir2(join3(kbRoot, ".llm-kb", "traces"), { recursive: true });
|
|
251
|
+
await writeFile2(path3, sessionId + "\n", { flag: "a" });
|
|
450
252
|
}
|
|
451
253
|
async function startSessionWatcher(kbRoot) {
|
|
452
|
-
const sessionsDir =
|
|
453
|
-
const sourcesDir =
|
|
254
|
+
const sessionsDir = join3(kbRoot, ".llm-kb", "sessions");
|
|
255
|
+
const sourcesDir = join3(kbRoot, ".llm-kb", "wiki", "sources");
|
|
454
256
|
const processed = await loadProcessed(kbRoot);
|
|
455
257
|
const timers = /* @__PURE__ */ new Map();
|
|
456
258
|
async function processSession(filePath) {
|
|
@@ -478,11 +280,11 @@ async function startSessionWatcher(kbRoot) {
|
|
|
478
280
|
}, 1500);
|
|
479
281
|
timers.set(filePath, timer);
|
|
480
282
|
}
|
|
481
|
-
if (
|
|
283
|
+
if (existsSync(sessionsDir)) {
|
|
482
284
|
try {
|
|
483
|
-
const files = (await
|
|
285
|
+
const files = (await readdir2(sessionsDir)).filter((f) => f.endsWith(".jsonl"));
|
|
484
286
|
for (const f of files) {
|
|
485
|
-
await processSession(
|
|
287
|
+
await processSession(join3(sessionsDir, f));
|
|
486
288
|
}
|
|
487
289
|
} catch {
|
|
488
290
|
}
|
|
@@ -500,482 +302,23 @@ async function startSessionWatcher(kbRoot) {
|
|
|
500
302
|
});
|
|
501
303
|
}
|
|
502
304
|
|
|
503
|
-
// src/query.ts
|
|
504
|
-
import {
|
|
505
|
-
createAgentSession,
|
|
506
|
-
createBashTool,
|
|
507
|
-
createReadTool,
|
|
508
|
-
createWriteTool,
|
|
509
|
-
DefaultResourceLoader,
|
|
510
|
-
SettingsManager
|
|
511
|
-
} from "@mariozechner/pi-coding-agent";
|
|
512
|
-
import { readdir as readdir4, mkdir as mkdir5, readFile as readFile4 } from "fs/promises";
|
|
513
|
-
import { existsSync as existsSync4 } from "fs";
|
|
514
|
-
import { join as join6, basename as basename4 } from "path";
|
|
515
|
-
import chalk3 from "chalk";
|
|
516
|
-
|
|
517
|
-
// src/md-stream.ts
|
|
518
|
-
import chalk2 from "chalk";
|
|
519
|
-
var MarkdownStream = class {
|
|
520
|
-
buffer = "";
|
|
521
|
-
isTTY;
|
|
522
|
-
constructor(isTTY = false) {
|
|
523
|
-
this.isTTY = isTTY;
|
|
524
|
-
}
|
|
525
|
-
/** Feed a text_delta chunk. Returns styled string ready for stdout. */
|
|
526
|
-
push(chunk) {
|
|
527
|
-
if (!this.isTTY) return chunk;
|
|
528
|
-
this.buffer += chunk;
|
|
529
|
-
return this.drain(false);
|
|
530
|
-
}
|
|
531
|
-
/** Flush remaining buffer (call on text_end). */
|
|
532
|
-
end() {
|
|
533
|
-
if (!this.isTTY) return "";
|
|
534
|
-
const out = this.drain(true);
|
|
535
|
-
this.buffer = "";
|
|
536
|
-
return out;
|
|
537
|
-
}
|
|
538
|
-
drain(final) {
|
|
539
|
-
let out = "";
|
|
540
|
-
while (true) {
|
|
541
|
-
const nlIdx = this.buffer.indexOf("\n");
|
|
542
|
-
if (nlIdx === -1) {
|
|
543
|
-
if (final && this.buffer.length > 0) {
|
|
544
|
-
out += this.renderLine(this.buffer);
|
|
545
|
-
this.buffer = "";
|
|
546
|
-
}
|
|
547
|
-
break;
|
|
548
|
-
}
|
|
549
|
-
const line = this.buffer.slice(0, nlIdx);
|
|
550
|
-
this.buffer = this.buffer.slice(nlIdx + 1);
|
|
551
|
-
out += this.renderLine(line) + "\n";
|
|
552
|
-
}
|
|
553
|
-
return out;
|
|
554
|
-
}
|
|
555
|
-
/** Render a single complete line with block + inline styling. */
|
|
556
|
-
renderLine(line) {
|
|
557
|
-
const trimmed = line.trimStart();
|
|
558
|
-
if (/^-{3,}\s*$/.test(trimmed) || /^\*{3,}\s*$/.test(trimmed)) {
|
|
559
|
-
const cols = process.stdout.columns || 80;
|
|
560
|
-
return chalk2.dim("\u2500".repeat(Math.min(cols, 60)));
|
|
561
|
-
}
|
|
562
|
-
const headerMatch = trimmed.match(/^(#{1,6})\s+(.*)$/);
|
|
563
|
-
if (headerMatch) {
|
|
564
|
-
const text = this.inline(headerMatch[2]);
|
|
565
|
-
return "\n" + chalk2.bold(text);
|
|
566
|
-
}
|
|
567
|
-
const bulletMatch = trimmed.match(/^[-*+]\s+(.*)$/);
|
|
568
|
-
if (bulletMatch) {
|
|
569
|
-
const indent = line.length - trimmed.length;
|
|
570
|
-
return " ".repeat(indent) + chalk2.dim("\u2022") + " " + this.inline(bulletMatch[1]);
|
|
571
|
-
}
|
|
572
|
-
const numMatch = trimmed.match(/^(\d+)[.)]\s+(.*)$/);
|
|
573
|
-
if (numMatch) {
|
|
574
|
-
const indent = line.length - trimmed.length;
|
|
575
|
-
return " ".repeat(indent) + chalk2.dim(numMatch[1] + ".") + " " + this.inline(numMatch[2]);
|
|
576
|
-
}
|
|
577
|
-
if (/^\|[\s\-:|]+\|$/.test(trimmed)) {
|
|
578
|
-
return chalk2.dim(trimmed);
|
|
579
|
-
}
|
|
580
|
-
if (trimmed.startsWith("|") && trimmed.endsWith("|")) {
|
|
581
|
-
return this.inline(line);
|
|
582
|
-
}
|
|
583
|
-
if (trimmed.startsWith(">")) {
|
|
584
|
-
const content = trimmed.replace(/^>+\s*/, "");
|
|
585
|
-
return chalk2.dim("\u2502 ") + chalk2.italic(this.inline(content));
|
|
586
|
-
}
|
|
587
|
-
return this.inline(line);
|
|
588
|
-
}
|
|
589
|
-
/** Apply inline markdown styling to text. */
|
|
590
|
-
inline(text) {
|
|
591
|
-
text = text.replace(/`([^`]+)`/g, (_, c) => chalk2.cyan(c));
|
|
592
|
-
text = text.replace(/\*\*\*(.+?)\*\*\*/g, (_, t) => chalk2.bold.italic(t));
|
|
593
|
-
text = text.replace(/\*\*(.+?)\*\*/g, (_, t) => chalk2.bold(t));
|
|
594
|
-
text = text.replace(/(?<!\*)\*(.+?)\*(?!\*)/g, (_, t) => chalk2.italic(t));
|
|
595
|
-
text = text.replace(/~~(.+?)~~/g, (_, t) => chalk2.strikethrough(t));
|
|
596
|
-
text = text.replace(
|
|
597
|
-
/\[([^\]]+)\]\(([^)]+)\)/g,
|
|
598
|
-
(_, label, url) => `${label} ${chalk2.dim(`(${url})`)}`
|
|
599
|
-
);
|
|
600
|
-
return text;
|
|
601
|
-
}
|
|
602
|
-
};
|
|
603
|
-
|
|
604
|
-
// src/query.ts
|
|
605
|
-
function extractAnswerText(content) {
|
|
606
|
-
return (content ?? []).filter((b) => b.type === "text").map((b) => b.text ?? "").join("").trim();
|
|
607
|
-
}
|
|
608
|
-
function extractFilesRead(messages) {
|
|
609
|
-
const paths = [];
|
|
610
|
-
for (const msg of messages) {
|
|
611
|
-
if (msg.role !== "assistant") continue;
|
|
612
|
-
for (const block of msg.content ?? []) {
|
|
613
|
-
if (block.type === "toolCall" && block.name === "read") {
|
|
614
|
-
const p = block.arguments?.path ?? "";
|
|
615
|
-
if (p && !paths.includes(p)) paths.push(p);
|
|
616
|
-
}
|
|
617
|
-
}
|
|
618
|
-
}
|
|
619
|
-
return paths;
|
|
620
|
-
}
|
|
621
|
-
function getToolLabel(toolName, args) {
|
|
622
|
-
if (toolName === "read" || toolName === "write" || toolName === "edit") {
|
|
623
|
-
const file = basename4(args?.path ?? "");
|
|
624
|
-
if (!file || !/\.[a-z0-9]{1,6}$/i.test(file)) return null;
|
|
625
|
-
const verb = toolName === "read" ? "Reading" : toolName === "write" ? "Writing" : "Editing";
|
|
626
|
-
return `${verb} ${file}`;
|
|
627
|
-
}
|
|
628
|
-
if (toolName === "bash" && args?.command) {
|
|
629
|
-
return `Running bash`;
|
|
630
|
-
}
|
|
631
|
-
return null;
|
|
632
|
-
}
|
|
633
|
-
function buildQueryAgents(sourceFiles, save, wikiContent) {
|
|
634
|
-
const sourceList = sourceFiles.map((f) => ` - ${f}`).join("\n");
|
|
635
|
-
const wikiSection = wikiContent ? `## Knowledge Wiki (use this first)
|
|
636
|
-
|
|
637
|
-
The wiki below contains knowledge already extracted from this knowledge base.
|
|
638
|
-
If the user's question is covered here, answer directly from it \u2014 no need to re-read source files.
|
|
639
|
-
Always cite the original source files mentioned in the wiki.
|
|
640
|
-
|
|
641
|
-
${wikiContent}
|
|
642
|
-
|
|
643
|
-
---
|
|
644
|
-
|
|
645
|
-
` : "";
|
|
646
|
-
const sourceStep = wikiContent ? "If not covered in the wiki above: read the sources" : "How to answer";
|
|
647
|
-
const lines = [
|
|
648
|
-
`# llm-kb Knowledge Base \u2014 Query Mode`,
|
|
649
|
-
``,
|
|
650
|
-
wikiSection,
|
|
651
|
-
`## ${sourceStep}`,
|
|
652
|
-
``,
|
|
653
|
-
`1. Read .llm-kb/wiki/index.md to understand all available sources`,
|
|
654
|
-
`2. Select the most relevant source files (usually 2-5) and read them in full`,
|
|
655
|
-
`3. Answer with inline citations: (filename, page number)`,
|
|
656
|
-
`4. If you can't find the answer, say so \u2014 don't hallucinate`,
|
|
657
|
-
``,
|
|
658
|
-
`## Available parsed sources`,
|
|
659
|
-
sourceList,
|
|
660
|
-
``,
|
|
661
|
-
`## Non-PDF files (docx, xlsx, pptx)`,
|
|
662
|
-
`Use bash to run Node.js scripts. Libraries are pre-installed via require().`,
|
|
663
|
-
``,
|
|
664
|
-
`### Word (.docx) \u2014 structured XML`,
|
|
665
|
-
`.docx files are ZIP archives containing word/document.xml.`,
|
|
666
|
-
`Read them SELECTIVELY \u2014 extract only what is relevant to the question:`,
|
|
667
|
-
``,
|
|
668
|
-
"```javascript",
|
|
669
|
-
`const AdmZip = require('adm-zip');`,
|
|
670
|
-
`const zip = new AdmZip('file.docx');`,
|
|
671
|
-
`const xml = zip.readAsText('word/document.xml');`,
|
|
672
|
-
`// Parse XML to find specific paragraphs, headings, tables`,
|
|
673
|
-
"```",
|
|
674
|
-
``,
|
|
675
|
-
`Strategy for large .docx files:`,
|
|
676
|
-
`1. First: extract headings/structure to understand the document layout`,
|
|
677
|
-
`2. Then: extract only the sections relevant to the user's question`,
|
|
678
|
-
`NEVER dump the entire document.`,
|
|
679
|
-
``,
|
|
680
|
-
`### Excel (.xlsx) \u2014 use exceljs`,
|
|
681
|
-
`Read specific sheets and ranges, not the whole workbook:`,
|
|
682
|
-
``,
|
|
683
|
-
"```javascript",
|
|
684
|
-
`const ExcelJS = require('exceljs');`,
|
|
685
|
-
`const wb = new ExcelJS.Workbook();`,
|
|
686
|
-
`await wb.xlsx.readFile('file.xlsx');`,
|
|
687
|
-
`const sheet = wb.getWorksheet(1);`,
|
|
688
|
-
`// Read specific rows/columns relevant to the question`,
|
|
689
|
-
"```",
|
|
690
|
-
``,
|
|
691
|
-
`### PowerPoint (.pptx) \u2014 use officeparser`,
|
|
692
|
-
``,
|
|
693
|
-
"```javascript",
|
|
694
|
-
`const officeparser = require('officeparser');`,
|
|
695
|
-
`const text = await officeparser.parseOfficeAsync('file.pptx');`,
|
|
696
|
-
"```",
|
|
697
|
-
``,
|
|
698
|
-
`## Rules`,
|
|
699
|
-
`- Always cite sources with filename and page number`,
|
|
700
|
-
`- Read the FULL source file, not just the beginning (for .md sources)`,
|
|
701
|
-
`- For non-PDF files, extract ONLY relevant sections \u2014 never dump entire files`,
|
|
702
|
-
`- Prefer primary sources over previous analyses`,
|
|
703
|
-
``,
|
|
704
|
-
`## Guidelines`,
|
|
705
|
-
`A guidelines file may exist at .llm-kb/guidelines.md with learned rules from`,
|
|
706
|
-
`past evaluations and user preferences. Read it when:`,
|
|
707
|
-
`- You're unsure about citation accuracy or format`,
|
|
708
|
-
`- You're about to read source files (guidelines may suggest using wiki instead)`,
|
|
709
|
-
`- The question touches a topic that may have had issues in past evaluations`
|
|
710
|
-
];
|
|
711
|
-
if (save) {
|
|
712
|
-
lines.push(``, `## Research Mode`, `Save your analysis to .llm-kb/wiki/outputs/ with a descriptive filename.`, `Include the question at the top and all citations.`);
|
|
713
|
-
}
|
|
714
|
-
return lines.join("\n");
|
|
715
|
-
}
|
|
716
|
-
var WikiUpdateScheduler = class {
|
|
717
|
-
constructor(everyN, everyMin) {
|
|
718
|
-
this.everyN = everyN;
|
|
719
|
-
this.everyMin = everyMin;
|
|
720
|
-
}
|
|
721
|
-
everyN;
|
|
722
|
-
everyMin;
|
|
723
|
-
stopMsgCount = 0;
|
|
724
|
-
lastUpdateAt = 0;
|
|
725
|
-
chain = Promise.resolve();
|
|
726
|
-
shouldUpdate() {
|
|
727
|
-
return this.stopMsgCount > 0 && this.stopMsgCount % this.everyN === 0 || this.lastUpdateAt > 0 && Date.now() - this.lastUpdateAt > this.everyMin * 6e4;
|
|
728
|
-
}
|
|
729
|
-
enqueue(work) {
|
|
730
|
-
this.chain = this.chain.then(() => work().catch(() => {
|
|
731
|
-
}));
|
|
732
|
-
}
|
|
733
|
-
onMessageEnd(msg, snap, doUpdate) {
|
|
734
|
-
if (msg.role !== "assistant" || msg.stopReason !== "stop") return;
|
|
735
|
-
this.stopMsgCount++;
|
|
736
|
-
if (this.shouldUpdate()) {
|
|
737
|
-
this.lastUpdateAt = Date.now();
|
|
738
|
-
this.enqueue(() => doUpdate(snap().messages));
|
|
739
|
-
}
|
|
740
|
-
}
|
|
741
|
-
onAgentEnd(msgs, doUpdate) {
|
|
742
|
-
this.lastUpdateAt = Date.now();
|
|
743
|
-
this.enqueue(() => doUpdate(msgs));
|
|
744
|
-
}
|
|
745
|
-
flush() {
|
|
746
|
-
return this.chain;
|
|
747
|
-
}
|
|
748
|
-
};
|
|
749
|
-
function subscribeDisplay(session, opts) {
|
|
750
|
-
const ui = opts.tuiDisplay;
|
|
751
|
-
const dim = (s) => process.stdout.isTTY ? chalk3.dim(s) : s;
|
|
752
|
-
const thinLine = () => dim("\u2500".repeat(process.stdout.columns || 80));
|
|
753
|
-
let phase = "idle";
|
|
754
|
-
let filesReadCount = 0;
|
|
755
|
-
let shownToolCalls = /* @__PURE__ */ new Set();
|
|
756
|
-
let startTime = Date.now();
|
|
757
|
-
let md = new MarkdownStream(process.stdout.isTTY ?? false);
|
|
758
|
-
let lastQuestion = "";
|
|
759
|
-
const scheduler = new WikiUpdateScheduler(5, 3);
|
|
760
|
-
const buildTrace2 = (messages) => {
|
|
761
|
-
const last = [...messages].reverse().find((m) => m.role === "assistant" && m.stopReason === "stop");
|
|
762
|
-
if (!last) return null;
|
|
763
|
-
const filesRead = extractFilesRead(messages);
|
|
764
|
-
return {
|
|
765
|
-
sessionId: session.sessionId,
|
|
766
|
-
sessionFile: session.sessionFile ?? "",
|
|
767
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
768
|
-
mode: "query",
|
|
769
|
-
question: lastQuestion,
|
|
770
|
-
answer: extractAnswerText(last.content),
|
|
771
|
-
filesRead,
|
|
772
|
-
filesAvailable: opts.mdFiles,
|
|
773
|
-
filesSkipped: opts.mdFiles.filter((f) => !filesRead.some((r) => r.endsWith(f))),
|
|
774
|
-
model: last.model
|
|
775
|
-
};
|
|
776
|
-
};
|
|
777
|
-
const doUpdate = async (messages) => {
|
|
778
|
-
const trace = buildTrace2(messages);
|
|
779
|
-
if (!trace) return;
|
|
780
|
-
await saveTrace(opts.folder, trace);
|
|
781
|
-
await appendToQueryLog(opts.folder, trace);
|
|
782
|
-
await updateWiki(opts.folder, trace, opts.authStorage);
|
|
783
|
-
};
|
|
784
|
-
session.subscribe((event) => {
|
|
785
|
-
if (event.type === "agent_start") {
|
|
786
|
-
phase = "idle";
|
|
787
|
-
filesReadCount = 0;
|
|
788
|
-
shownToolCalls = /* @__PURE__ */ new Set();
|
|
789
|
-
startTime = Date.now();
|
|
790
|
-
md = new MarkdownStream(process.stdout.isTTY ?? false);
|
|
791
|
-
const modelName = opts.modelId ?? "claude-sonnet-4-6";
|
|
792
|
-
if (ui) {
|
|
793
|
-
ui.disableInput();
|
|
794
|
-
ui.beginResponse(modelName);
|
|
795
|
-
} else process.stdout.write(dim(`\u27E1 ${modelName}`) + "\n");
|
|
796
|
-
}
|
|
797
|
-
if (event.type === "message_update") {
|
|
798
|
-
const ae = event.assistantMessageEvent;
|
|
799
|
-
if (ae.type === "thinking_start") {
|
|
800
|
-
if (!ui) process.stdout.write(dim("\n\u25B8 Thinking\n"));
|
|
801
|
-
phase = "thinking";
|
|
802
|
-
}
|
|
803
|
-
if (ae.type === "thinking_delta") {
|
|
804
|
-
if (ui) ui.appendThinking(ae.delta);
|
|
805
|
-
else process.stdout.write(dim(` ${ae.delta}`));
|
|
806
|
-
}
|
|
807
|
-
if (ae.type === "thinking_end") {
|
|
808
|
-
if (ui) ui.endThinking();
|
|
809
|
-
else process.stdout.write("\n");
|
|
810
|
-
}
|
|
811
|
-
}
|
|
812
|
-
if (event.type === "message_update") {
|
|
813
|
-
const ae = event.assistantMessageEvent;
|
|
814
|
-
if (ae.type === "toolcall_end" && ae.toolCall) {
|
|
815
|
-
const label = getToolLabel(ae.toolCall.name, ae.toolCall.arguments);
|
|
816
|
-
if (label) {
|
|
817
|
-
if (!ui && phase !== "tools") process.stdout.write("\n");
|
|
818
|
-
phase = "tools";
|
|
819
|
-
if (ui) {
|
|
820
|
-
ui.addToolCall(ae.toolCall.id, label, ae.toolCall.name);
|
|
821
|
-
if (ae.toolCall.name === "bash" && ae.toolCall.arguments?.command) {
|
|
822
|
-
ui.addCodeBlock(ae.toolCall.arguments.command);
|
|
823
|
-
}
|
|
824
|
-
} else {
|
|
825
|
-
process.stdout.write(dim(` \u25B8 ${label}`) + "\n");
|
|
826
|
-
if (ae.toolCall.name === "bash" && ae.toolCall.arguments?.command) {
|
|
827
|
-
const code = ae.toolCall.arguments.command;
|
|
828
|
-
process.stdout.write(dim(code.split("\n").map((l) => ` ${l}`).join("\n")) + "\n");
|
|
829
|
-
}
|
|
830
|
-
shownToolCalls.add(ae.toolCall.id);
|
|
831
|
-
if (ae.toolCall.name === "read") filesReadCount++;
|
|
832
|
-
}
|
|
833
|
-
}
|
|
834
|
-
}
|
|
835
|
-
}
|
|
836
|
-
if (event.type === "tool_execution_start") {
|
|
837
|
-
const { toolCallId, toolName, args } = event;
|
|
838
|
-
if (ui) {
|
|
839
|
-
const label = getToolLabel(toolName, args);
|
|
840
|
-
if (label) ui.addToolCall(toolCallId, label, toolName);
|
|
841
|
-
} else if (!shownToolCalls.has(toolCallId)) {
|
|
842
|
-
const label = getToolLabel(toolName, args);
|
|
843
|
-
if (label) {
|
|
844
|
-
if (phase !== "tools") process.stdout.write("\n");
|
|
845
|
-
phase = "tools";
|
|
846
|
-
process.stdout.write(dim(` \u25B8 ${label}`) + "\n");
|
|
847
|
-
shownToolCalls.add(toolCallId);
|
|
848
|
-
if (toolName === "read") filesReadCount++;
|
|
849
|
-
}
|
|
850
|
-
}
|
|
851
|
-
}
|
|
852
|
-
if (event.type === "tool_execution_end") {
|
|
853
|
-
const { toolCallId, isError } = event;
|
|
854
|
-
if (ui) ui.addToolResult(toolCallId, isError);
|
|
855
|
-
}
|
|
856
|
-
if (event.type === "message_update") {
|
|
857
|
-
const ae = event.assistantMessageEvent;
|
|
858
|
-
if (ae.type === "text_start" && phase !== "answer") {
|
|
859
|
-
if (ui) ui.beginAnswer();
|
|
860
|
-
else if (phase === "thinking" || phase === "tools") {
|
|
861
|
-
process.stdout.write(`
|
|
862
|
-
${thinLine()}
|
|
863
|
-
|
|
864
|
-
`);
|
|
865
|
-
}
|
|
866
|
-
phase = "answer";
|
|
867
|
-
}
|
|
868
|
-
if (ae.type === "text_delta") {
|
|
869
|
-
if (ui) ui.appendAnswer(ae.delta);
|
|
870
|
-
else process.stdout.write(md.push(ae.delta));
|
|
871
|
-
}
|
|
872
|
-
if (ae.type === "text_end" && !ui) process.stdout.write(md.end());
|
|
873
|
-
}
|
|
874
|
-
if (event.type === "agent_end") {
|
|
875
|
-
if (ui) {
|
|
876
|
-
ui.showCompletion();
|
|
877
|
-
ui.enableInput();
|
|
878
|
-
} else {
|
|
879
|
-
const elapsed = ((Date.now() - startTime) / 1e3).toFixed(1);
|
|
880
|
-
const source = filesReadCount > 0 ? `${filesReadCount} file${filesReadCount !== 1 ? "s" : ""} read` : "wiki";
|
|
881
|
-
const stats = `${elapsed}s \xB7 ${source}`;
|
|
882
|
-
const cols = process.stdout.columns || 80;
|
|
883
|
-
const pad = Math.max(0, cols - stats.length - 4);
|
|
884
|
-
process.stdout.write(`
|
|
885
|
-
|
|
886
|
-
${dim("\u2500\u2500 " + stats + " " + "\u2500".repeat(pad))}
|
|
887
|
-
`);
|
|
888
|
-
}
|
|
889
|
-
scheduler.onAgentEnd(event.messages, doUpdate);
|
|
890
|
-
}
|
|
891
|
-
if (event.type === "message_end") {
|
|
892
|
-
scheduler.onMessageEnd(event.message, () => ({ messages: session.state.messages }), doUpdate);
|
|
893
|
-
}
|
|
894
|
-
});
|
|
895
|
-
return {
|
|
896
|
-
setQuestion(q) {
|
|
897
|
-
lastQuestion = q;
|
|
898
|
-
},
|
|
899
|
-
flush() {
|
|
900
|
-
return scheduler.flush();
|
|
901
|
-
}
|
|
902
|
-
};
|
|
903
|
-
}
|
|
904
|
-
async function createChat(folder, options) {
|
|
905
|
-
const sourcesDir = join6(folder, ".llm-kb", "wiki", "sources");
|
|
906
|
-
const files = await readdir4(sourcesDir);
|
|
907
|
-
const mdFiles = files.filter((f) => f.endsWith(".md"));
|
|
908
|
-
if (mdFiles.length === 0) throw new Error("No sources found. Run 'llm-kb run' first.");
|
|
909
|
-
if (options.save) await mkdir5(join6(folder, ".llm-kb", "wiki", "outputs"), { recursive: true });
|
|
910
|
-
process.env.NODE_PATH = getNodeModulesPath();
|
|
911
|
-
const wikiPath = join6(folder, ".llm-kb", "wiki", "wiki.md");
|
|
912
|
-
const wikiContent = existsSync4(wikiPath) ? await readFile4(wikiPath, "utf-8").catch(() => "") : "";
|
|
913
|
-
const agentsContent = buildQueryAgents(mdFiles, !!options.save, wikiContent);
|
|
914
|
-
const loader = new DefaultResourceLoader({
|
|
915
|
-
cwd: folder,
|
|
916
|
-
agentsFilesOverride: (current) => ({
|
|
917
|
-
agentsFiles: [...current.agentsFiles, { path: ".llm-kb/AGENTS.md", content: agentsContent }]
|
|
918
|
-
})
|
|
919
|
-
});
|
|
920
|
-
await loader.reload();
|
|
921
|
-
const tools = [
|
|
922
|
-
createReadTool(folder),
|
|
923
|
-
createBashTool(folder),
|
|
924
|
-
createWriteTool(folder)
|
|
925
|
-
];
|
|
926
|
-
const model = options.modelId ? getModels("anthropic").find((m) => m.id === options.modelId) : void 0;
|
|
927
|
-
const { session } = await createAgentSession({
|
|
928
|
-
cwd: folder,
|
|
929
|
-
resourceLoader: loader,
|
|
930
|
-
tools,
|
|
931
|
-
sessionManager: options.save ? await createKBSession(folder) : await continueKBSession(folder),
|
|
932
|
-
settingsManager: SettingsManager.inMemory({ compaction: { enabled: false } }),
|
|
933
|
-
thinkingLevel: "low",
|
|
934
|
-
...options.authStorage ? { authStorage: options.authStorage } : {},
|
|
935
|
-
...model ? { model } : {}
|
|
936
|
-
});
|
|
937
|
-
const display = subscribeDisplay(session, {
|
|
938
|
-
modelId: options.modelId,
|
|
939
|
-
authStorage: options.authStorage,
|
|
940
|
-
folder,
|
|
941
|
-
mdFiles,
|
|
942
|
-
tuiDisplay: options.tuiDisplay
|
|
943
|
-
});
|
|
944
|
-
return { session, display };
|
|
945
|
-
}
|
|
946
|
-
async function query(folder, question, options) {
|
|
947
|
-
const { session, display } = await createChat(folder, options);
|
|
948
|
-
session.setSessionName(`query: ${question}`);
|
|
949
|
-
display.setQuestion(question);
|
|
950
|
-
await session.prompt(question);
|
|
951
|
-
await display.flush();
|
|
952
|
-
session.dispose();
|
|
953
|
-
if (options.save) {
|
|
954
|
-
const sourcesDir = join6(folder, ".llm-kb", "wiki", "sources");
|
|
955
|
-
const { buildIndex: buildIndex2 } = await import("./indexer-KSYRIVVN.js");
|
|
956
|
-
await buildIndex2(folder, sourcesDir, void 0, options.authStorage);
|
|
957
|
-
}
|
|
958
|
-
}
|
|
959
|
-
|
|
960
305
|
// src/eval.ts
|
|
961
|
-
import {
|
|
962
|
-
import {
|
|
963
|
-
import {
|
|
964
|
-
import { join as join7, basename as basename5 } from "path";
|
|
965
|
-
import { homedir as homedir2 } from "os";
|
|
306
|
+
import { readFile as readFile2, readdir as readdir3, writeFile as writeFile3, mkdir as mkdir3 } from "fs/promises";
|
|
307
|
+
import { existsSync as existsSync2 } from "fs";
|
|
308
|
+
import { join as join4, basename as basename4 } from "path";
|
|
966
309
|
async function parseSessionsForEval(sessionsDir, sourcesDir, limit) {
|
|
967
|
-
if (!
|
|
968
|
-
const sessionFiles = (await
|
|
310
|
+
if (!existsSync2(sessionsDir)) return [];
|
|
311
|
+
const sessionFiles = (await readdir3(sessionsDir)).filter((f) => f.endsWith(".jsonl")).sort().reverse();
|
|
969
312
|
const files = limit ? sessionFiles.slice(0, limit) : sessionFiles;
|
|
970
313
|
const qas = [];
|
|
971
314
|
let filesAvailable = [];
|
|
972
315
|
try {
|
|
973
|
-
filesAvailable = (await
|
|
316
|
+
filesAvailable = (await readdir3(sourcesDir)).filter((f) => f.endsWith(".md"));
|
|
974
317
|
} catch {
|
|
975
318
|
}
|
|
976
319
|
for (const file of files) {
|
|
977
320
|
try {
|
|
978
|
-
const raw = await
|
|
321
|
+
const raw = await readFile2(join4(sessionsDir, file), "utf-8");
|
|
979
322
|
const lines = raw.trim().split("\n").filter(Boolean);
|
|
980
323
|
const entries = [];
|
|
981
324
|
for (const line of lines) {
|
|
@@ -1000,6 +343,7 @@ async function parseSessionsForEval(sessionsDir, sourcesDir, limit) {
|
|
|
1000
343
|
if (!msg) continue;
|
|
1001
344
|
if (msg.role === "user") {
|
|
1002
345
|
if (currentQuestion && currentAnswer) {
|
|
346
|
+
const parsed = parseCitations(currentAnswer);
|
|
1003
347
|
qas.push({
|
|
1004
348
|
sessionFile: file,
|
|
1005
349
|
question: currentQuestion,
|
|
@@ -1011,10 +355,11 @@ async function parseSessionsForEval(sessionsDir, sourcesDir, limit) {
|
|
|
1011
355
|
),
|
|
1012
356
|
answer: currentAnswer,
|
|
1013
357
|
model: currentModel,
|
|
1014
|
-
durationMs: endTs - startTs
|
|
358
|
+
durationMs: endTs - startTs,
|
|
359
|
+
citations: parsed.citations
|
|
1015
360
|
});
|
|
1016
361
|
}
|
|
1017
|
-
currentQuestion =
|
|
362
|
+
currentQuestion = extractText(msg.content);
|
|
1018
363
|
currentThinking = "";
|
|
1019
364
|
currentFilesRead = [];
|
|
1020
365
|
currentAnswer = "";
|
|
@@ -1035,7 +380,7 @@ async function parseSessionsForEval(sessionsDir, sourcesDir, limit) {
|
|
|
1035
380
|
for (const block of prev.message?.content ?? []) {
|
|
1036
381
|
if (block.type === "toolCall" && block.id === toolCallId && block.name === "read") {
|
|
1037
382
|
const path3 = block.arguments?.path ?? "";
|
|
1038
|
-
const content =
|
|
383
|
+
const content = extractText(msg.content);
|
|
1039
384
|
if (path3 && content) {
|
|
1040
385
|
currentFilesRead.push({ path: path3, content: content.slice(0, 2e3) });
|
|
1041
386
|
}
|
|
@@ -1045,6 +390,7 @@ async function parseSessionsForEval(sessionsDir, sourcesDir, limit) {
|
|
|
1045
390
|
}
|
|
1046
391
|
}
|
|
1047
392
|
if (currentQuestion && currentAnswer) {
|
|
393
|
+
const parsed = parseCitations(currentAnswer);
|
|
1048
394
|
qas.push({
|
|
1049
395
|
sessionFile: file,
|
|
1050
396
|
question: currentQuestion,
|
|
@@ -1056,7 +402,8 @@ async function parseSessionsForEval(sessionsDir, sourcesDir, limit) {
|
|
|
1056
402
|
),
|
|
1057
403
|
answer: currentAnswer,
|
|
1058
404
|
model: currentModel,
|
|
1059
|
-
durationMs: endTs - startTs
|
|
405
|
+
durationMs: endTs - startTs,
|
|
406
|
+
citations: parsed.citations
|
|
1060
407
|
});
|
|
1061
408
|
}
|
|
1062
409
|
} catch {
|
|
@@ -1084,13 +431,37 @@ function calculateMetrics(qas) {
|
|
|
1084
431
|
}
|
|
1085
432
|
for (const f of sourceFilesRead) {
|
|
1086
433
|
totalFilesRead++;
|
|
1087
|
-
const name =
|
|
434
|
+
const name = basename4(f.path);
|
|
1088
435
|
uniqueFiles.set(name, (uniqueFiles.get(name) ?? 0) + 1);
|
|
1089
436
|
if (!qa.answer.includes(name) && !qa.answer.includes(name.replace(".md", ""))) {
|
|
1090
437
|
wastedReads++;
|
|
1091
438
|
}
|
|
1092
439
|
}
|
|
1093
440
|
}
|
|
441
|
+
let totalCitations = 0;
|
|
442
|
+
let withBbox = 0;
|
|
443
|
+
let withoutBbox = 0;
|
|
444
|
+
let multiPage = 0;
|
|
445
|
+
let answersWithCitations = 0;
|
|
446
|
+
let answersWithoutCitations = 0;
|
|
447
|
+
for (const qa of qas) {
|
|
448
|
+
if (qa.citations.length > 0) {
|
|
449
|
+
answersWithCitations++;
|
|
450
|
+
for (const c of qa.citations) {
|
|
451
|
+
totalCitations++;
|
|
452
|
+
if (c.bbox || c.pages && c.pages.length > 0) {
|
|
453
|
+
withBbox++;
|
|
454
|
+
} else {
|
|
455
|
+
withoutBbox++;
|
|
456
|
+
}
|
|
457
|
+
if (c.pages && c.pages.length > 0) {
|
|
458
|
+
multiPage++;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
} else {
|
|
462
|
+
answersWithoutCitations++;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
1094
465
|
return {
|
|
1095
466
|
totalSessions: uniqueSessions.size,
|
|
1096
467
|
totalQAs: qas.length,
|
|
@@ -1099,23 +470,21 @@ function calculateMetrics(qas) {
|
|
|
1099
470
|
sourceReads,
|
|
1100
471
|
totalFilesRead,
|
|
1101
472
|
uniqueFilesRead: uniqueFiles,
|
|
1102
|
-
wastedReads
|
|
473
|
+
wastedReads,
|
|
474
|
+
citations: {
|
|
475
|
+
totalCitations,
|
|
476
|
+
withBbox,
|
|
477
|
+
withoutBbox,
|
|
478
|
+
multiPage,
|
|
479
|
+
avgPerAnswer: qas.length > 0 ? totalCitations / qas.length : 0,
|
|
480
|
+
answersWithCitations,
|
|
481
|
+
answersWithoutCitations
|
|
482
|
+
}
|
|
1103
483
|
};
|
|
1104
484
|
}
|
|
1105
|
-
async function
|
|
1106
|
-
if (authStorage) return authStorage.getApiKey("anthropic");
|
|
1107
|
-
const piAuthPath = join7(homedir2(), ".pi", "agent", "auth.json");
|
|
1108
|
-
if (existsSync5(piAuthPath)) {
|
|
1109
|
-
const storage = AuthStorage3.create(piAuthPath);
|
|
1110
|
-
return storage.getApiKey("anthropic");
|
|
1111
|
-
}
|
|
1112
|
-
return process.env.ANTHROPIC_API_KEY;
|
|
1113
|
-
}
|
|
1114
|
-
async function judgeQA(qa, apiKey, modelId) {
|
|
485
|
+
async function judgeQA(qa, modelId, authStorage) {
|
|
1115
486
|
const issues = [];
|
|
1116
|
-
const
|
|
1117
|
-
if (!model) return issues;
|
|
1118
|
-
const filesSummary = qa.filesRead.map((f) => `File: ${basename5(f.path)}
|
|
487
|
+
const filesSummary = qa.filesRead.map((f) => `File: ${basename4(f.path)}
|
|
1119
488
|
Content (first 2000 chars):
|
|
1120
489
|
${f.content}`).join("\n\n---\n\n");
|
|
1121
490
|
const skippedList = qa.filesSkipped.join(", ") || "none";
|
|
@@ -1148,13 +517,14 @@ Checks:
|
|
|
1148
517
|
Return ONLY a JSON array. If no issues found, return [].
|
|
1149
518
|
Example: [{"type":"wiki-gap","severity":"warning","detail":"Electronic evidence topic not in wiki","recommendation":"Add electronic evidence section to wiki"}]`;
|
|
1150
519
|
try {
|
|
1151
|
-
const result = await
|
|
1152
|
-
|
|
520
|
+
const result = await completeWithFallback(
|
|
521
|
+
modelId,
|
|
522
|
+
authStorage,
|
|
523
|
+
"eval",
|
|
1153
524
|
{
|
|
1154
525
|
systemPrompt: "You are a precise QA evaluator. Return only valid JSON arrays. No explanation.",
|
|
1155
526
|
messages: [{ role: "user", content: prompt, timestamp: Date.now() }]
|
|
1156
|
-
}
|
|
1157
|
-
{ apiKey }
|
|
527
|
+
}
|
|
1158
528
|
);
|
|
1159
529
|
const text = result.content.filter((b) => b.type === "text").map((b) => b.text).join("").trim();
|
|
1160
530
|
const jsonMatch = text.match(/\[[\s\S]*\]/);
|
|
@@ -1193,6 +563,18 @@ function buildReport(result) {
|
|
|
1193
563
|
lines.push(`| Total file reads | ${metrics.totalFilesRead} |`);
|
|
1194
564
|
lines.push(`| Wasted reads | ${metrics.wastedReads} |`);
|
|
1195
565
|
lines.push(``);
|
|
566
|
+
const cm = metrics.citations;
|
|
567
|
+
lines.push(`## Citations`);
|
|
568
|
+
lines.push(``);
|
|
569
|
+
lines.push(`| Metric | Value |`);
|
|
570
|
+
lines.push(`|---|---|`);
|
|
571
|
+
lines.push(`| Total citations | ${cm.totalCitations} |`);
|
|
572
|
+
lines.push(`| Avg per answer | ${cm.avgPerAnswer.toFixed(1)} |`);
|
|
573
|
+
lines.push(`| With bbox | ${cm.withBbox} (${cm.totalCitations > 0 ? Math.round(cm.withBbox / cm.totalCitations * 100) : 0}%) |`);
|
|
574
|
+
lines.push(`| Without bbox | ${cm.withoutBbox} |`);
|
|
575
|
+
lines.push(`| Multi-page | ${cm.multiPage} |`);
|
|
576
|
+
lines.push(`| Answers with citations | ${cm.answersWithCitations}/${metrics.totalQAs} (${metrics.totalQAs > 0 ? Math.round(cm.answersWithCitations / metrics.totalQAs * 100) : 0}%) |`);
|
|
577
|
+
lines.push(``);
|
|
1196
578
|
if (metrics.uniqueFilesRead.size > 0) {
|
|
1197
579
|
lines.push(`### Most Read Files`);
|
|
1198
580
|
lines.push(``);
|
|
@@ -1278,6 +660,20 @@ function buildAgentsInsights(result) {
|
|
|
1278
660
|
lines.push(``);
|
|
1279
661
|
}
|
|
1280
662
|
}
|
|
663
|
+
const cm = metrics.citations;
|
|
664
|
+
if (cm.totalCitations > 0) {
|
|
665
|
+
const bboxRate = Math.round(cm.withBbox / cm.totalCitations * 100);
|
|
666
|
+
lines.push(`### Citation Quality`);
|
|
667
|
+
lines.push(`- Bbox coverage: ${bboxRate}% (target: 100%)`);
|
|
668
|
+
lines.push(`- Avg citations per answer: ${cm.avgPerAnswer.toFixed(1)}`);
|
|
669
|
+
if (cm.withoutBbox > 0) {
|
|
670
|
+
lines.push(`- ${cm.withoutBbox} citations missing bbox \u2014 agent should always read .json files`);
|
|
671
|
+
}
|
|
672
|
+
if (cm.answersWithoutCitations > 0) {
|
|
673
|
+
lines.push(`- ${cm.answersWithoutCitations} answers had no citations \u2014 every answer must cite sources`);
|
|
674
|
+
}
|
|
675
|
+
lines.push(``);
|
|
676
|
+
}
|
|
1281
677
|
const hitRate = metrics.totalQAs > 0 ? Math.round(metrics.wikiHits / metrics.totalQAs * 100) : 0;
|
|
1282
678
|
lines.push(`### Performance`);
|
|
1283
679
|
lines.push(`- Wiki hit rate: ${hitRate}% (target: 80%+)`);
|
|
@@ -1286,8 +682,8 @@ function buildAgentsInsights(result) {
|
|
|
1286
682
|
return lines.join("\n");
|
|
1287
683
|
}
|
|
1288
684
|
async function runEval(kbRoot, options) {
|
|
1289
|
-
const sessionsDir =
|
|
1290
|
-
const sourcesDir =
|
|
685
|
+
const sessionsDir = join4(kbRoot, ".llm-kb", "sessions");
|
|
686
|
+
const sourcesDir = join4(kbRoot, ".llm-kb", "wiki", "sources");
|
|
1291
687
|
const log = options.onProgress ?? (() => {
|
|
1292
688
|
});
|
|
1293
689
|
log("Reading sessions...");
|
|
@@ -1295,7 +691,7 @@ async function runEval(kbRoot, options) {
|
|
|
1295
691
|
log(`Found ${qas.length} Q&A exchanges across sessions`);
|
|
1296
692
|
if (qas.length === 0) {
|
|
1297
693
|
return {
|
|
1298
|
-
metrics: { totalSessions: 0, totalQAs: 0, avgDurationMs: 0, wikiHits: 0, sourceReads: 0, totalFilesRead: 0, uniqueFilesRead: /* @__PURE__ */ new Map(), wastedReads: 0 },
|
|
694
|
+
metrics: { totalSessions: 0, totalQAs: 0, avgDurationMs: 0, wikiHits: 0, sourceReads: 0, totalFilesRead: 0, uniqueFilesRead: /* @__PURE__ */ new Map(), wastedReads: 0, citations: { totalCitations: 0, withBbox: 0, withoutBbox: 0, multiPage: 0, avgPerAnswer: 0, answersWithCitations: 0, answersWithoutCitations: 0 } },
|
|
1299
695
|
issues: [],
|
|
1300
696
|
wikiGaps: [],
|
|
1301
697
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
@@ -1303,17 +699,16 @@ async function runEval(kbRoot, options) {
|
|
|
1303
699
|
}
|
|
1304
700
|
log("Calculating metrics...");
|
|
1305
701
|
const metrics = calculateMetrics(qas);
|
|
1306
|
-
const apiKey = await resolveApiKey2(options.authStorage);
|
|
1307
702
|
const allIssues = [];
|
|
1308
|
-
|
|
1309
|
-
|
|
703
|
+
const modelId = "claude-haiku-4-5";
|
|
704
|
+
try {
|
|
1310
705
|
for (let i = 0; i < qas.length; i++) {
|
|
1311
706
|
log(`Judging ${i + 1}/${qas.length}: "${qas[i].question.slice(0, 50)}..."`);
|
|
1312
|
-
const issues = await judgeQA(qas[i],
|
|
707
|
+
const issues = await judgeQA(qas[i], modelId, options.authStorage);
|
|
1313
708
|
allIssues.push(...issues);
|
|
1314
709
|
}
|
|
1315
|
-
}
|
|
1316
|
-
log(
|
|
710
|
+
} catch (err) {
|
|
711
|
+
log(`LLM judge unavailable \u2014 skipping (${err instanceof Error ? err.message : String(err)})`);
|
|
1317
712
|
}
|
|
1318
713
|
const wikiGaps = allIssues.filter((i) => i.type === "wiki-gap").map((i) => i.detail);
|
|
1319
714
|
const result = {
|
|
@@ -1325,11 +720,11 @@ async function runEval(kbRoot, options) {
|
|
|
1325
720
|
};
|
|
1326
721
|
result.agentsInsights = buildAgentsInsights(result);
|
|
1327
722
|
log("Writing eval report + insights...");
|
|
1328
|
-
const outputsDir =
|
|
1329
|
-
await
|
|
723
|
+
const outputsDir = join4(kbRoot, ".llm-kb", "wiki", "outputs");
|
|
724
|
+
await mkdir3(outputsDir, { recursive: true });
|
|
1330
725
|
const report = buildReport(result);
|
|
1331
|
-
await
|
|
1332
|
-
const guidelinesPath =
|
|
726
|
+
await writeFile3(join4(outputsDir, "eval-report.md"), report, "utf-8");
|
|
727
|
+
const guidelinesPath = join4(kbRoot, ".llm-kb", "guidelines.md");
|
|
1333
728
|
await writeGuidelines(guidelinesPath, result.agentsInsights);
|
|
1334
729
|
log("Insights saved to .llm-kb/guidelines.md (agent reads on-demand)");
|
|
1335
730
|
return result;
|
|
@@ -1338,20 +733,20 @@ var EVAL_SECTION_RE = /## Eval Insights[\s\S]*?(?=\n## |$)/;
|
|
|
1338
733
|
async function writeGuidelines(path3, evalSection) {
|
|
1339
734
|
let existing = "";
|
|
1340
735
|
try {
|
|
1341
|
-
existing = await
|
|
736
|
+
existing = await readFile2(path3, "utf-8");
|
|
1342
737
|
} catch {
|
|
1343
738
|
}
|
|
1344
739
|
if (!existing) {
|
|
1345
|
-
await
|
|
740
|
+
await writeFile3(path3, evalSection, "utf-8");
|
|
1346
741
|
return;
|
|
1347
742
|
}
|
|
1348
743
|
if (EVAL_SECTION_RE.test(existing)) {
|
|
1349
|
-
await
|
|
744
|
+
await writeFile3(path3, existing.replace(EVAL_SECTION_RE, evalSection.trim()), "utf-8");
|
|
1350
745
|
} else {
|
|
1351
|
-
await
|
|
746
|
+
await writeFile3(path3, evalSection + "\n\n" + existing, "utf-8");
|
|
1352
747
|
}
|
|
1353
748
|
}
|
|
1354
|
-
function
|
|
749
|
+
function extractText(content) {
|
|
1355
750
|
if (!content) return "";
|
|
1356
751
|
if (typeof content === "string") return content;
|
|
1357
752
|
if (Array.isArray(content)) {
|
|
@@ -5247,33 +4642,33 @@ var ProcessTerminal = class {
|
|
|
5247
4642
|
};
|
|
5248
4643
|
|
|
5249
4644
|
// src/tui-display.ts
|
|
5250
|
-
import
|
|
4645
|
+
import chalk2 from "chalk";
|
|
5251
4646
|
function createMarkdownTheme() {
|
|
5252
4647
|
return {
|
|
5253
|
-
heading: (t) =>
|
|
5254
|
-
link: (t) =>
|
|
5255
|
-
linkUrl: (t) =>
|
|
5256
|
-
code: (t) =>
|
|
5257
|
-
codeBlock: (t) =>
|
|
5258
|
-
codeBlockBorder: (t) =>
|
|
5259
|
-
quote: (t) =>
|
|
5260
|
-
quoteBorder: (t) =>
|
|
5261
|
-
hr: (t) =>
|
|
5262
|
-
listBullet: (t) =>
|
|
5263
|
-
bold: (t) =>
|
|
5264
|
-
italic: (t) =>
|
|
5265
|
-
underline: (t) =>
|
|
5266
|
-
strikethrough: (t) =>
|
|
4648
|
+
heading: (t) => chalk2.bold(t),
|
|
4649
|
+
link: (t) => chalk2.cyan(t),
|
|
4650
|
+
linkUrl: (t) => chalk2.dim(t),
|
|
4651
|
+
code: (t) => chalk2.cyan(t),
|
|
4652
|
+
codeBlock: (t) => chalk2.dim(t),
|
|
4653
|
+
codeBlockBorder: (t) => chalk2.dim(t),
|
|
4654
|
+
quote: (t) => chalk2.italic(t),
|
|
4655
|
+
quoteBorder: (t) => chalk2.dim(t),
|
|
4656
|
+
hr: (t) => chalk2.dim(t),
|
|
4657
|
+
listBullet: (t) => chalk2.dim(t),
|
|
4658
|
+
bold: (t) => chalk2.bold(t),
|
|
4659
|
+
italic: (t) => chalk2.italic(t),
|
|
4660
|
+
underline: (t) => chalk2.underline(t),
|
|
4661
|
+
strikethrough: (t) => chalk2.strikethrough(t)
|
|
5267
4662
|
};
|
|
5268
4663
|
}
|
|
5269
4664
|
var mdTheme = createMarkdownTheme();
|
|
5270
4665
|
function dimText(text, px = 1, py = 0) {
|
|
5271
|
-
return new Text(
|
|
4666
|
+
return new Text(chalk2.dim(text), px, py);
|
|
5272
4667
|
}
|
|
5273
4668
|
var HRule = class {
|
|
5274
4669
|
colorFn;
|
|
5275
4670
|
constructor(colorFn) {
|
|
5276
|
-
this.colorFn = colorFn ??
|
|
4671
|
+
this.colorFn = colorFn ?? chalk2.dim;
|
|
5277
4672
|
}
|
|
5278
4673
|
invalidate() {
|
|
5279
4674
|
}
|
|
@@ -5295,6 +4690,8 @@ var ChatDisplay = class {
|
|
|
5295
4690
|
// active thinking block
|
|
5296
4691
|
hadSeparator = false;
|
|
5297
4692
|
// has a ─── line been drawn?
|
|
4693
|
+
accumulatedAnswer = "";
|
|
4694
|
+
// full answer text for citation stripping
|
|
5298
4695
|
filesReadCount = 0;
|
|
5299
4696
|
shownToolCalls = /* @__PURE__ */ new Set();
|
|
5300
4697
|
startTime = Date.now();
|
|
@@ -5306,7 +4703,7 @@ var ChatDisplay = class {
|
|
|
5306
4703
|
this.messageArea = new Container();
|
|
5307
4704
|
this.tui.addChild(this.messageArea);
|
|
5308
4705
|
this.inputArea = new Container();
|
|
5309
|
-
this.inputArea.addChild(new HRule((s) =>
|
|
4706
|
+
this.inputArea.addChild(new HRule((s) => chalk2.hex("#c678dd")(s)));
|
|
5310
4707
|
this.input = new Input();
|
|
5311
4708
|
this.input.onSubmit = (text) => {
|
|
5312
4709
|
if (text.trim() && this.onSubmit) {
|
|
@@ -5316,7 +4713,7 @@ var ChatDisplay = class {
|
|
|
5316
4713
|
this.input.setValue("");
|
|
5317
4714
|
};
|
|
5318
4715
|
this.inputArea.addChild(this.input);
|
|
5319
|
-
this.inputArea.addChild(new HRule((s) =>
|
|
4716
|
+
this.inputArea.addChild(new HRule((s) => chalk2.hex("#c678dd")(s)));
|
|
5320
4717
|
this.tui.addChild(this.inputArea);
|
|
5321
4718
|
this.tui.setFocus(this.input);
|
|
5322
4719
|
}
|
|
@@ -5338,7 +4735,7 @@ var ChatDisplay = class {
|
|
|
5338
4735
|
}
|
|
5339
4736
|
addUserMessage(text) {
|
|
5340
4737
|
this.messageArea.addChild(new Spacer(1));
|
|
5341
|
-
this.messageArea.addChild(new Text(
|
|
4738
|
+
this.messageArea.addChild(new Text(chalk2.bold(text), 1, 0));
|
|
5342
4739
|
this.tui.requestRender();
|
|
5343
4740
|
}
|
|
5344
4741
|
// ── Per-prompt lifecycle (events arrive in any order) ───────────────────
|
|
@@ -5349,6 +4746,7 @@ var ChatDisplay = class {
|
|
|
5349
4746
|
this.currentMd = null;
|
|
5350
4747
|
this.currentThinking = null;
|
|
5351
4748
|
this.hadSeparator = false;
|
|
4749
|
+
this.accumulatedAnswer = "";
|
|
5352
4750
|
this.currentResponse = new Container();
|
|
5353
4751
|
this.currentResponse.addChild(new Spacer(1));
|
|
5354
4752
|
this.currentResponse.addChild(dimText(`\u27E1 ${modelName}`));
|
|
@@ -5362,12 +4760,12 @@ var ChatDisplay = class {
|
|
|
5362
4760
|
if (!this.currentThinking) {
|
|
5363
4761
|
this.currentResponse.addChild(new Spacer(1));
|
|
5364
4762
|
this.currentResponse.addChild(dimText("\u25B8 Thinking"));
|
|
5365
|
-
this.currentThinking = new Text(
|
|
4763
|
+
this.currentThinking = new Text(chalk2.dim(chalk2.italic(text)), 2, 0);
|
|
5366
4764
|
this.currentResponse.addChild(this.currentThinking);
|
|
5367
4765
|
} else {
|
|
5368
4766
|
const prev = this.currentThinking.text ?? "";
|
|
5369
4767
|
this.currentThinking.setText(
|
|
5370
|
-
|
|
4768
|
+
chalk2.dim(chalk2.italic(prev.replace(/\x1b\[[0-9;]*m/g, "") + text))
|
|
5371
4769
|
);
|
|
5372
4770
|
}
|
|
5373
4771
|
this.tui.requestRender();
|
|
@@ -5396,7 +4794,7 @@ var ChatDisplay = class {
|
|
|
5396
4794
|
addToolResult(toolCallId, isError) {
|
|
5397
4795
|
if (!this.currentResponse) return;
|
|
5398
4796
|
if (isError) {
|
|
5399
|
-
this.currentResponse.addChild(new Text(
|
|
4797
|
+
this.currentResponse.addChild(new Text(chalk2.red(" \u2717 failed"), 0, 0));
|
|
5400
4798
|
this.tui.requestRender();
|
|
5401
4799
|
}
|
|
5402
4800
|
}
|
|
@@ -5427,19 +4825,61 @@ var ChatDisplay = class {
|
|
|
5427
4825
|
}
|
|
5428
4826
|
const prev = this.currentMd.text ?? "";
|
|
5429
4827
|
this.currentMd.setText(prev + text);
|
|
4828
|
+
this.accumulatedAnswer += text;
|
|
5430
4829
|
this.tui.requestRender();
|
|
5431
4830
|
}
|
|
5432
|
-
|
|
4831
|
+
/** Strip CITATIONS block from answer and show formatted citation footer */
|
|
4832
|
+
showCitations(citations) {
|
|
4833
|
+
if (!this.currentResponse || citations.length === 0) return;
|
|
4834
|
+
if (this.currentMd) {
|
|
4835
|
+
const citIdx = this.accumulatedAnswer.search(/^CITATIONS:\s*$/im);
|
|
4836
|
+
if (citIdx >= 0) {
|
|
4837
|
+
const clean = this.accumulatedAnswer.slice(0, citIdx).trimEnd();
|
|
4838
|
+
this.currentMd.setText(clean);
|
|
4839
|
+
}
|
|
4840
|
+
}
|
|
4841
|
+
this.currentResponse.addChild(new Spacer(1));
|
|
4842
|
+
this.currentResponse.addChild(new HRule());
|
|
4843
|
+
for (let i = 0; i < citations.length; i++) {
|
|
4844
|
+
const c = citations[i];
|
|
4845
|
+
const num = chalk2.bold(` [${i + 1}]`);
|
|
4846
|
+
const file = chalk2.cyan(c.file);
|
|
4847
|
+
const pageStr = c.pages && c.pages.length > 0 ? `p.${c.pages.map((p) => p.page).join("-")}` : `p.${c.page}`;
|
|
4848
|
+
let status;
|
|
4849
|
+
if (c.bbox || c.pages && c.pages.length > 0) {
|
|
4850
|
+
const bboxInfo = c.pages && c.pages.length > 0 ? `(${c.pages.length} pages)` : c.bbox ? `(${c.bbox.x},${c.bbox.y} \u2192 ${Math.round(c.bbox.x + c.bbox.width)},${Math.round(c.bbox.y + c.bbox.height)})` : "";
|
|
4851
|
+
status = chalk2.green(`\u2705 bbox ${bboxInfo}`);
|
|
4852
|
+
} else {
|
|
4853
|
+
status = chalk2.yellow(`\u26A0\uFE0F no bbox`);
|
|
4854
|
+
}
|
|
4855
|
+
const quote = c.quote.length > 60 ? c.quote.slice(0, 57) + "..." : c.quote;
|
|
4856
|
+
this.currentResponse.addChild(
|
|
4857
|
+
new Text(`${num} \u{1F4C4} ${file}, ${pageStr}`, 0, 0)
|
|
4858
|
+
);
|
|
4859
|
+
this.currentResponse.addChild(
|
|
4860
|
+
new Text(chalk2.dim(` "${quote}"`), 0, 0)
|
|
4861
|
+
);
|
|
4862
|
+
this.currentResponse.addChild(
|
|
4863
|
+
new Text(` ${status}`, 0, 0)
|
|
4864
|
+
);
|
|
4865
|
+
}
|
|
4866
|
+
this.tui.requestRender();
|
|
4867
|
+
}
|
|
4868
|
+
showCompletion(citations) {
|
|
5433
4869
|
if (!this.currentResponse) return;
|
|
4870
|
+
if (citations && citations.length > 0) {
|
|
4871
|
+
this.showCitations(citations);
|
|
4872
|
+
}
|
|
5434
4873
|
const elapsed = ((Date.now() - this.startTime) / 1e3).toFixed(1);
|
|
5435
4874
|
const source = this.filesReadCount > 0 ? `${this.filesReadCount} file${this.filesReadCount !== 1 ? "s" : ""} read` : "wiki";
|
|
5436
|
-
const
|
|
4875
|
+
const citCount = citations && citations.length > 0 ? ` \xB7 ${citations.length} citation${citations.length !== 1 ? "s" : ""}` : "";
|
|
4876
|
+
const stats = `\u2500\u2500 ${elapsed}s \xB7 ${source}${citCount} `;
|
|
5437
4877
|
const completion = {
|
|
5438
4878
|
invalidate() {
|
|
5439
4879
|
},
|
|
5440
4880
|
render(width) {
|
|
5441
4881
|
const pad = Math.max(0, width - stats.length);
|
|
5442
|
-
return [
|
|
4882
|
+
return [chalk2.dim(stats + "\u2500".repeat(pad))];
|
|
5443
4883
|
}
|
|
5444
4884
|
};
|
|
5445
4885
|
this.currentResponse.addChild(new Spacer(1));
|
|
@@ -5459,12 +4899,12 @@ var ChatDisplay = class {
|
|
|
5459
4899
|
};
|
|
5460
4900
|
|
|
5461
4901
|
// src/resolve-kb.ts
|
|
5462
|
-
import { existsSync as
|
|
5463
|
-
import { resolve as resolve2, join as
|
|
4902
|
+
import { existsSync as existsSync3 } from "fs";
|
|
4903
|
+
import { resolve as resolve2, join as join7, dirname as dirname2 } from "path";
|
|
5464
4904
|
function resolveKnowledgeBase(startDir) {
|
|
5465
4905
|
let dir = resolve2(startDir);
|
|
5466
4906
|
while (true) {
|
|
5467
|
-
if (
|
|
4907
|
+
if (existsSync3(join7(dir, ".llm-kb"))) {
|
|
5468
4908
|
return dir;
|
|
5469
4909
|
}
|
|
5470
4910
|
const parent = dirname2(dir);
|
|
@@ -5474,39 +4914,53 @@ function resolveKnowledgeBase(startDir) {
|
|
|
5474
4914
|
}
|
|
5475
4915
|
|
|
5476
4916
|
// src/auth.ts
|
|
5477
|
-
import { existsSync as
|
|
5478
|
-
import { join as
|
|
5479
|
-
import { homedir as
|
|
5480
|
-
import { AuthStorage
|
|
5481
|
-
import
|
|
4917
|
+
import { existsSync as existsSync4 } from "fs";
|
|
4918
|
+
import { join as join8 } from "path";
|
|
4919
|
+
import { homedir as homedir2 } from "os";
|
|
4920
|
+
import { AuthStorage } from "@mariozechner/pi-coding-agent";
|
|
4921
|
+
import chalk3 from "chalk";
|
|
5482
4922
|
function checkAuth() {
|
|
5483
|
-
const piAuthPath =
|
|
5484
|
-
if (
|
|
5485
|
-
const authStorage =
|
|
5486
|
-
return { ok: true, method: "pi-sdk", authStorage };
|
|
4923
|
+
const piAuthPath = join8(homedir2(), ".pi", "agent", "auth.json");
|
|
4924
|
+
if (existsSync4(piAuthPath)) {
|
|
4925
|
+
const authStorage = AuthStorage.create();
|
|
4926
|
+
return { ok: true, method: "pi-sdk", authStorage, providers: ["pi-sdk"] };
|
|
5487
4927
|
}
|
|
4928
|
+
const providers = {};
|
|
4929
|
+
const names = [];
|
|
5488
4930
|
if (process.env.ANTHROPIC_API_KEY) {
|
|
5489
|
-
|
|
5490
|
-
|
|
5491
|
-
|
|
5492
|
-
|
|
4931
|
+
providers.anthropic = { type: "api_key", key: process.env.ANTHROPIC_API_KEY };
|
|
4932
|
+
names.push("anthropic");
|
|
4933
|
+
}
|
|
4934
|
+
if (process.env.OPENROUTER_API_KEY) {
|
|
4935
|
+
providers.openrouter = { type: "api_key", key: process.env.OPENROUTER_API_KEY };
|
|
4936
|
+
names.push("openrouter");
|
|
4937
|
+
}
|
|
4938
|
+
if (process.env.OPENAI_API_KEY) {
|
|
4939
|
+
providers.openai = { type: "api_key", key: process.env.OPENAI_API_KEY };
|
|
4940
|
+
names.push("openai");
|
|
4941
|
+
}
|
|
4942
|
+
if (names.length > 0) {
|
|
4943
|
+
const authStorage = AuthStorage.inMemory(providers);
|
|
4944
|
+
return { ok: true, method: "env", authStorage, providers: names };
|
|
5493
4945
|
}
|
|
5494
4946
|
return { ok: false };
|
|
5495
4947
|
}
|
|
5496
4948
|
function exitWithAuthError() {
|
|
5497
|
-
console.error(
|
|
5498
|
-
console.error(` ${
|
|
5499
|
-
console.error(
|
|
5500
|
-
console.error(
|
|
5501
|
-
console.error(` ${
|
|
5502
|
-
console.error(
|
|
4949
|
+
console.error(chalk3.red("\n No LLM authentication found.\n"));
|
|
4950
|
+
console.error(` ${chalk3.bold("Option 1:")} Install Pi SDK ${chalk3.dim("(recommended)")}`);
|
|
4951
|
+
console.error(chalk3.dim(" npm install -g @mariozechner/pi-coding-agent"));
|
|
4952
|
+
console.error(chalk3.dim(" pi\n"));
|
|
4953
|
+
console.error(` ${chalk3.bold("Option 2:")} Set one or more provider API keys`);
|
|
4954
|
+
console.error(chalk3.dim(" export ANTHROPIC_API_KEY=sk-ant-..."));
|
|
4955
|
+
console.error(chalk3.dim(" export OPENROUTER_API_KEY=sk-or-..."));
|
|
4956
|
+
console.error(chalk3.dim(" export OPENAI_API_KEY=sk-...\n"));
|
|
5503
4957
|
process.exit(1);
|
|
5504
4958
|
}
|
|
5505
4959
|
|
|
5506
4960
|
// src/config.ts
|
|
5507
|
-
import { existsSync as
|
|
5508
|
-
import { readFile as
|
|
5509
|
-
import { join as
|
|
4961
|
+
import { existsSync as existsSync5 } from "fs";
|
|
4962
|
+
import { readFile as readFile3, writeFile as writeFile4, mkdir as mkdir4 } from "fs/promises";
|
|
4963
|
+
import { join as join9 } from "path";
|
|
5510
4964
|
var DEFAULT_INDEX_MODEL = "claude-haiku-4-5";
|
|
5511
4965
|
var DEFAULT_QUERY_MODEL = "claude-sonnet-4-6";
|
|
5512
4966
|
var DEFAULTS = {
|
|
@@ -5514,14 +4968,14 @@ var DEFAULTS = {
|
|
|
5514
4968
|
queryModel: DEFAULT_QUERY_MODEL
|
|
5515
4969
|
};
|
|
5516
4970
|
function configPath(kbRoot) {
|
|
5517
|
-
return
|
|
4971
|
+
return join9(kbRoot, ".llm-kb", "config.json");
|
|
5518
4972
|
}
|
|
5519
4973
|
async function loadConfig(kbRoot) {
|
|
5520
4974
|
let base = { ...DEFAULTS };
|
|
5521
4975
|
const path3 = configPath(kbRoot);
|
|
5522
|
-
if (
|
|
4976
|
+
if (existsSync5(path3)) {
|
|
5523
4977
|
try {
|
|
5524
|
-
const raw = await
|
|
4978
|
+
const raw = await readFile3(path3, "utf-8");
|
|
5525
4979
|
const parsed = JSON.parse(raw);
|
|
5526
4980
|
if (parsed.indexModel) base.indexModel = parsed.indexModel;
|
|
5527
4981
|
if (parsed.queryModel) base.queryModel = parsed.queryModel;
|
|
@@ -5534,29 +4988,35 @@ async function loadConfig(kbRoot) {
|
|
|
5534
4988
|
}
|
|
5535
4989
|
async function ensureConfig(kbRoot) {
|
|
5536
4990
|
const path3 = configPath(kbRoot);
|
|
5537
|
-
if (!
|
|
5538
|
-
await
|
|
5539
|
-
await
|
|
4991
|
+
if (!existsSync5(path3)) {
|
|
4992
|
+
await mkdir4(join9(kbRoot, ".llm-kb"), { recursive: true });
|
|
4993
|
+
await writeFile4(path3, JSON.stringify(DEFAULTS, null, 2) + "\n", "utf-8");
|
|
5540
4994
|
return { ...DEFAULTS };
|
|
5541
4995
|
}
|
|
5542
4996
|
return loadConfig(kbRoot);
|
|
5543
4997
|
}
|
|
5544
4998
|
|
|
5545
4999
|
// src/cli.ts
|
|
5546
|
-
import { existsSync as
|
|
5547
|
-
import {
|
|
5548
|
-
import {
|
|
5549
|
-
import
|
|
5000
|
+
import { existsSync as existsSync6 } from "fs";
|
|
5001
|
+
import { readFileSync } from "fs";
|
|
5002
|
+
import { mkdir as mkdir5, readdir as readdir4, stat as stat2 } from "fs/promises";
|
|
5003
|
+
import { resolve as resolve3, join as join10, dirname as dirname3 } from "path";
|
|
5004
|
+
import { fileURLToPath } from "url";
|
|
5005
|
+
import chalk4 from "chalk";
|
|
5006
|
+
var __filename = fileURLToPath(import.meta.url);
|
|
5007
|
+
var __dirname = dirname3(__filename);
|
|
5008
|
+
var pkg = JSON.parse(readFileSync(join10(__dirname, "..", "package.json"), "utf-8"));
|
|
5009
|
+
var VERSION = pkg.version;
|
|
5550
5010
|
var program = new Command();
|
|
5551
|
-
program.name("llm-kb").description("Drop files into a folder. Get a knowledge base you can query.").version(
|
|
5011
|
+
program.name("llm-kb").description("Drop files into a folder. Get a knowledge base you can query.").version(VERSION);
|
|
5552
5012
|
program.command("run").description("Scan, parse, index, and watch a folder").argument("<folder>", "Path to your documents folder").action(async (folder) => {
|
|
5553
5013
|
console.log(`
|
|
5554
|
-
${
|
|
5014
|
+
${chalk4.bold("llm-kb")} v${VERSION}
|
|
5555
5015
|
`);
|
|
5556
5016
|
const auth = checkAuth();
|
|
5557
5017
|
if (!auth.ok) exitWithAuthError();
|
|
5558
|
-
if (!
|
|
5559
|
-
console.error(
|
|
5018
|
+
if (!existsSync6(folder)) {
|
|
5019
|
+
console.error(chalk4.red(`Error: Folder not found: ${folder}`));
|
|
5560
5020
|
process.exit(1);
|
|
5561
5021
|
}
|
|
5562
5022
|
const root = resolve3(folder);
|
|
@@ -5564,72 +5024,95 @@ ${chalk6.bold("llm-kb")} v0.4.0
|
|
|
5564
5024
|
console.log(`Scanning ${folder}...`);
|
|
5565
5025
|
const files = await scan(folder);
|
|
5566
5026
|
if (files.length === 0) {
|
|
5567
|
-
console.log(
|
|
5027
|
+
console.log(chalk4.yellow(" No supported files found."));
|
|
5568
5028
|
return;
|
|
5569
5029
|
}
|
|
5570
5030
|
const pdfs = files.filter((f) => f.ext === ".pdf");
|
|
5571
|
-
console.log(` Found ${
|
|
5031
|
+
console.log(` Found ${chalk4.bold(files.length.toString())} files (${summarize(files)})`);
|
|
5572
5032
|
if (pdfs.length === 0) return;
|
|
5573
|
-
const sourcesDir =
|
|
5574
|
-
await
|
|
5033
|
+
const sourcesDir = join10(root, ".llm-kb", "wiki", "sources");
|
|
5034
|
+
await mkdir5(sourcesDir, { recursive: true });
|
|
5575
5035
|
let parsed = 0;
|
|
5576
5036
|
let skipped = 0;
|
|
5577
5037
|
let failed = 0;
|
|
5578
5038
|
const errors = [];
|
|
5579
|
-
|
|
5580
|
-
|
|
5581
|
-
|
|
5582
|
-
|
|
5583
|
-
|
|
5584
|
-
|
|
5585
|
-
|
|
5586
|
-
|
|
5587
|
-
|
|
5588
|
-
|
|
5589
|
-
|
|
5039
|
+
const CONCURRENCY = Math.min(8, pdfs.length);
|
|
5040
|
+
let nextIdx = 0;
|
|
5041
|
+
let completed = 0;
|
|
5042
|
+
const parseStart = Date.now();
|
|
5043
|
+
let activeCount = 0;
|
|
5044
|
+
function renderProgress(currentFile) {
|
|
5045
|
+
const cols = process.stdout.columns || 80;
|
|
5046
|
+
const pct = Math.round(completed / pdfs.length * 100);
|
|
5047
|
+
const barWidth = Math.min(30, cols - 40);
|
|
5048
|
+
const filled = Math.round(completed / pdfs.length * barWidth);
|
|
5049
|
+
const active = Math.min(activeCount, barWidth - filled);
|
|
5050
|
+
const bar = chalk4.green("\u2588".repeat(filled)) + chalk4.yellow("\u2593".repeat(active)) + chalk4.dim("\u2591".repeat(barWidth - filled - active));
|
|
5051
|
+
const elapsed = ((Date.now() - parseStart) / 1e3).toFixed(0);
|
|
5052
|
+
const line = ` ${bar} ${chalk4.bold(`${completed}/${pdfs.length}`)} ${chalk4.cyan(`(${pct}%)`)} ${chalk4.dim(`${elapsed}s`)} ${chalk4.yellow(`${activeCount} active`)}`;
|
|
5053
|
+
process.stdout.write(`\r${line.padEnd(cols)}`);
|
|
5054
|
+
}
|
|
5055
|
+
async function worker() {
|
|
5056
|
+
while (nextIdx < pdfs.length) {
|
|
5057
|
+
const i = nextIdx++;
|
|
5058
|
+
const pdf = pdfs[i];
|
|
5059
|
+
activeCount++;
|
|
5060
|
+
renderProgress(pdf.name);
|
|
5061
|
+
try {
|
|
5062
|
+
const result = await parsePDF(join10(root, pdf.path), sourcesDir);
|
|
5063
|
+
if (result.skipped) skipped++;
|
|
5064
|
+
else parsed++;
|
|
5065
|
+
} catch (err) {
|
|
5066
|
+
failed++;
|
|
5067
|
+
errors.push({ name: pdf.name, message: err.message });
|
|
5068
|
+
}
|
|
5069
|
+
activeCount--;
|
|
5070
|
+
completed++;
|
|
5071
|
+
renderProgress(pdf.name);
|
|
5590
5072
|
}
|
|
5591
5073
|
}
|
|
5074
|
+
await Promise.all(Array.from({ length: CONCURRENCY }, () => worker()));
|
|
5592
5075
|
process.stdout.write(`\r${"".padEnd(process.stdout.columns || 80)}\r`);
|
|
5593
5076
|
const parts = [];
|
|
5594
|
-
if (parsed > 0) parts.push(
|
|
5595
|
-
if (skipped > 0) parts.push(
|
|
5596
|
-
if (failed > 0) parts.push(
|
|
5077
|
+
if (parsed > 0) parts.push(chalk4.green(`${parsed} parsed`));
|
|
5078
|
+
if (skipped > 0) parts.push(chalk4.dim(`${skipped} skipped (up to date)`));
|
|
5079
|
+
if (failed > 0) parts.push(chalk4.red(`${failed} failed`));
|
|
5597
5080
|
console.log(` ${parts.join(", ")}`);
|
|
5598
|
-
for (const err of errors) console.log(
|
|
5599
|
-
const indexFile =
|
|
5081
|
+
for (const err of errors) console.log(chalk4.red(` \u2717 ${err.name} \u2014 ${err.message}`));
|
|
5082
|
+
const indexFile = join10(root, ".llm-kb", "wiki", "index.md");
|
|
5600
5083
|
let indexUpToDate = false;
|
|
5601
|
-
if (parsed === 0 &&
|
|
5084
|
+
if (parsed === 0 && existsSync6(indexFile)) {
|
|
5602
5085
|
try {
|
|
5603
5086
|
const indexMtime = (await stat2(indexFile)).mtimeMs;
|
|
5604
|
-
const sourceFiles = await
|
|
5605
|
-
const mtimes = await Promise.all(sourceFiles.map((f) => stat2(
|
|
5087
|
+
const sourceFiles = await readdir4(sourcesDir);
|
|
5088
|
+
const mtimes = await Promise.all(sourceFiles.map((f) => stat2(join10(sourcesDir, f)).then((s) => s.mtimeMs)));
|
|
5606
5089
|
indexUpToDate = mtimes.every((mt) => indexMtime >= mt);
|
|
5607
5090
|
} catch {
|
|
5608
5091
|
}
|
|
5609
5092
|
}
|
|
5610
5093
|
if (indexUpToDate) {
|
|
5611
|
-
console.log(
|
|
5094
|
+
console.log(chalk4.dim(`
|
|
5612
5095
|
Index up to date.`));
|
|
5613
5096
|
} else {
|
|
5614
5097
|
console.log(`
|
|
5615
|
-
Building index... ${
|
|
5098
|
+
Building index... ${chalk4.dim(`(${config.indexModel})`)}`);
|
|
5616
5099
|
try {
|
|
5617
5100
|
await buildIndex(root, sourcesDir, void 0, auth.authStorage, config.indexModel);
|
|
5618
|
-
console.log(
|
|
5101
|
+
console.log(chalk4.green(` Index built: .llm-kb/wiki/index.md`));
|
|
5619
5102
|
} catch (err) {
|
|
5620
|
-
console.error(
|
|
5103
|
+
console.error(chalk4.red(` Index failed: ${err.message}`));
|
|
5621
5104
|
}
|
|
5622
5105
|
}
|
|
5623
5106
|
console.log(`
|
|
5624
|
-
${
|
|
5625
|
-
startWatcher({ folder: root, sourcesDir, authStorage: auth.authStorage, indexModel: config.indexModel });
|
|
5626
|
-
startSessionWatcher(root);
|
|
5107
|
+
${chalk4.dim("Output:")} ${sourcesDir}`);
|
|
5627
5108
|
const chatUI = new ChatDisplay();
|
|
5628
|
-
const { session, display } = await createChat(root, {
|
|
5109
|
+
const { session, display, reloadSources } = await createChat(root, {
|
|
5629
5110
|
authStorage: auth.authStorage,
|
|
5630
5111
|
modelId: config.queryModel,
|
|
5631
5112
|
tuiDisplay: chatUI
|
|
5632
5113
|
});
|
|
5114
|
+
startWatcher({ folder: root, sourcesDir, authStorage: auth.authStorage, indexModel: config.indexModel, onSourcesChanged: reloadSources });
|
|
5115
|
+
startSessionWatcher(root);
|
|
5633
5116
|
chatUI.onSubmit = (text) => {
|
|
5634
5117
|
display.setQuestion(text);
|
|
5635
5118
|
session.prompt(text).catch(() => {
|
|
@@ -5642,7 +5125,7 @@ ${chalk6.bold("llm-kb")} v0.4.0
|
|
|
5642
5125
|
});
|
|
5643
5126
|
};
|
|
5644
5127
|
console.log(`
|
|
5645
|
-
${
|
|
5128
|
+
${chalk4.bold("Ready.")} Ask a question or drop files in to re-index.
|
|
5646
5129
|
`);
|
|
5647
5130
|
chatUI.start();
|
|
5648
5131
|
});
|
|
@@ -5651,7 +5134,7 @@ program.command("query").description("Ask a single question (non-interactive, st
|
|
|
5651
5134
|
if (!auth.ok) exitWithAuthError();
|
|
5652
5135
|
const root = resolveKnowledgeBase(options.folder || process.cwd());
|
|
5653
5136
|
if (!root) {
|
|
5654
|
-
console.error(
|
|
5137
|
+
console.error(chalk4.red("No knowledge base found. Run 'llm-kb run <folder>' first."));
|
|
5655
5138
|
process.exit(1);
|
|
5656
5139
|
}
|
|
5657
5140
|
const config = await loadConfig(root);
|
|
@@ -5662,7 +5145,7 @@ program.command("query").description("Ask a single question (non-interactive, st
|
|
|
5662
5145
|
modelId: config.queryModel
|
|
5663
5146
|
});
|
|
5664
5147
|
} catch (err) {
|
|
5665
|
-
console.error(
|
|
5148
|
+
console.error(chalk4.red(err.message));
|
|
5666
5149
|
process.exit(1);
|
|
5667
5150
|
}
|
|
5668
5151
|
});
|
|
@@ -5671,46 +5154,49 @@ program.command("eval").description("Analyze sessions for quality issues, wiki g
|
|
|
5671
5154
|
if (!auth.ok) exitWithAuthError();
|
|
5672
5155
|
const root = resolveKnowledgeBase(options.folder || process.cwd());
|
|
5673
5156
|
if (!root) {
|
|
5674
|
-
console.error(
|
|
5157
|
+
console.error(chalk4.red("No knowledge base found. Run 'llm-kb run <folder>' first."));
|
|
5675
5158
|
process.exit(1);
|
|
5676
5159
|
}
|
|
5677
5160
|
console.log(`
|
|
5678
|
-
${
|
|
5161
|
+
${chalk4.bold("llm-kb eval")}
|
|
5679
5162
|
`);
|
|
5680
5163
|
const result = await runEval(root, {
|
|
5681
5164
|
authStorage: auth.authStorage,
|
|
5682
5165
|
last: options.last,
|
|
5683
|
-
onProgress: (msg) => console.log(
|
|
5166
|
+
onProgress: (msg) => console.log(chalk4.dim(` ${msg}`))
|
|
5684
5167
|
});
|
|
5685
5168
|
const { metrics, issues, wikiGaps } = result;
|
|
5686
5169
|
const errors = issues.filter((i) => i.severity === "error").length;
|
|
5687
5170
|
const warnings = issues.filter((i) => i.severity === "warning").length;
|
|
5688
5171
|
console.log();
|
|
5689
|
-
console.log(` ${
|
|
5172
|
+
console.log(` ${chalk4.bold("Results:")}`);
|
|
5690
5173
|
console.log(` Queries analyzed: ${metrics.totalQAs}`);
|
|
5691
5174
|
console.log(` Wiki hit rate: ${metrics.totalQAs > 0 ? Math.round(metrics.wikiHits / metrics.totalQAs * 100) : 0}%`);
|
|
5692
5175
|
console.log(` Wasted reads: ${metrics.wastedReads}`);
|
|
5693
|
-
|
|
5694
|
-
|
|
5176
|
+
const cm = metrics.citations;
|
|
5177
|
+
const bboxPct = cm.totalCitations > 0 ? Math.round(cm.withBbox / cm.totalCitations * 100) : 0;
|
|
5178
|
+
console.log(` Citations: ${cm.totalCitations} total, ${chalk4.green(`${cm.withBbox} with bbox`)}${cm.withoutBbox > 0 ? chalk4.yellow(` ${cm.withoutBbox} without`) : ""} (${bboxPct}%)`);
|
|
5179
|
+
console.log(` Issues: ${errors > 0 ? chalk4.red(`${errors} errors`) : chalk4.green("0 errors")} ${warnings > 0 ? chalk4.yellow(`${warnings} warnings`) : chalk4.dim("0 warnings")}`);
|
|
5180
|
+
console.log(` Wiki gaps: ${wikiGaps.length > 0 ? chalk4.yellow(String(wikiGaps.length)) : chalk4.green("0")}`);
|
|
5695
5181
|
console.log();
|
|
5696
|
-
console.log(
|
|
5182
|
+
console.log(chalk4.green(` Report: .llm-kb/wiki/outputs/eval-report.md`));
|
|
5697
5183
|
console.log();
|
|
5698
5184
|
});
|
|
5699
5185
|
program.command("status").description("Show knowledge base stats and current config").option("--folder <path>", "Path to document folder (auto-detects if omitted)").action(async (options) => {
|
|
5700
5186
|
const root = resolveKnowledgeBase(options.folder || process.cwd());
|
|
5701
5187
|
if (!root) {
|
|
5702
|
-
console.error(
|
|
5188
|
+
console.error(chalk4.red("No knowledge base found. Run 'llm-kb run <folder>' first."));
|
|
5703
5189
|
process.exit(1);
|
|
5704
5190
|
}
|
|
5705
5191
|
const auth = checkAuth();
|
|
5706
5192
|
const config = await loadConfig(root);
|
|
5707
|
-
const sourcesDir =
|
|
5708
|
-
const indexFile =
|
|
5709
|
-
const articlesDir =
|
|
5710
|
-
const outputsDir =
|
|
5193
|
+
const sourcesDir = join10(root, ".llm-kb", "wiki", "sources");
|
|
5194
|
+
const indexFile = join10(root, ".llm-kb", "wiki", "index.md");
|
|
5195
|
+
const articlesDir = join10(root, ".llm-kb", "wiki", "articles");
|
|
5196
|
+
const outputsDir = join10(root, ".llm-kb", "wiki", "outputs");
|
|
5711
5197
|
let sourceCount = 0;
|
|
5712
5198
|
try {
|
|
5713
|
-
sourceCount = (await
|
|
5199
|
+
sourceCount = (await readdir4(sourcesDir)).filter((f) => f.endsWith(".md")).length;
|
|
5714
5200
|
} catch {
|
|
5715
5201
|
}
|
|
5716
5202
|
let indexAge = "not built yet";
|
|
@@ -5721,23 +5207,48 @@ program.command("status").description("Show knowledge base stats and current con
|
|
|
5721
5207
|
}
|
|
5722
5208
|
let outputCount = 0;
|
|
5723
5209
|
try {
|
|
5724
|
-
outputCount = (await
|
|
5210
|
+
outputCount = (await readdir4(outputsDir)).filter((f) => f.endsWith(".md")).length;
|
|
5725
5211
|
} catch {
|
|
5726
5212
|
}
|
|
5727
5213
|
console.log(`
|
|
5728
|
-
${
|
|
5729
|
-
console.log(` ${
|
|
5730
|
-
console.log(` ${
|
|
5731
|
-
console.log(` ${
|
|
5214
|
+
${chalk4.bold("Knowledge Base Status")}`);
|
|
5215
|
+
console.log(` ${chalk4.dim("Folder:")} ${root}`);
|
|
5216
|
+
console.log(` ${chalk4.dim("Sources:")} ${sourceCount > 0 ? `${sourceCount} parsed source${sourceCount !== 1 ? "s" : ""}` : chalk4.yellow("none yet")}`);
|
|
5217
|
+
console.log(` ${chalk4.dim("Index:")} ${indexAge}`);
|
|
5732
5218
|
let articleCount = 0;
|
|
5733
5219
|
try {
|
|
5734
|
-
articleCount = (await
|
|
5220
|
+
articleCount = (await readdir4(articlesDir)).filter((f) => f.endsWith(".md") && f !== "index.md").length;
|
|
5735
5221
|
} catch {
|
|
5736
5222
|
}
|
|
5737
|
-
if (articleCount > 0) console.log(` ${
|
|
5738
|
-
if (outputCount > 0) console.log(` ${
|
|
5739
|
-
console.log(` ${
|
|
5740
|
-
console.log(` ${
|
|
5223
|
+
if (articleCount > 0) console.log(` ${chalk4.dim("Articles:")} ${articleCount} compiled`);
|
|
5224
|
+
if (outputCount > 0) console.log(` ${chalk4.dim("Outputs:")} ${outputCount} saved answer${outputCount !== 1 ? "s" : ""}`);
|
|
5225
|
+
console.log(` ${chalk4.dim("Models:")} ${chalk4.cyan(config.queryModel)} ${chalk4.dim("(query)")} ${chalk4.cyan(config.indexModel)} ${chalk4.dim("(index)")}`);
|
|
5226
|
+
console.log(` ${chalk4.dim("Auth:")} ${auth.ok ? auth.method === "pi-sdk" ? "Pi SDK" : `env (${auth.providers.join(", ")})` : chalk4.red("not configured")}`);
|
|
5741
5227
|
console.log();
|
|
5742
5228
|
});
|
|
5229
|
+
program.command("ui").description("Open the web UI with chat, citations, and source viewer").argument("<folder>", "Path to your documents folder").option("--port <n>", "Port number", parseInt, 3947).option("--no-open", "Don't auto-open the browser").action(async (folder, options) => {
|
|
5230
|
+
console.log(`
|
|
5231
|
+
${chalk4.bold("llm-kb")} web UI
|
|
5232
|
+
`);
|
|
5233
|
+
const auth = checkAuth();
|
|
5234
|
+
if (!auth.ok) exitWithAuthError();
|
|
5235
|
+
if (!existsSync6(folder)) {
|
|
5236
|
+
console.error(chalk4.red(`Error: Folder not found: ${folder}`));
|
|
5237
|
+
process.exit(1);
|
|
5238
|
+
}
|
|
5239
|
+
const root = resolve3(folder);
|
|
5240
|
+
const config = await loadConfig(root);
|
|
5241
|
+
if (!existsSync6(join10(root, ".llm-kb", "wiki", "sources"))) {
|
|
5242
|
+
console.error(chalk4.red("No knowledge base found. Run 'llm-kb run <folder>' first."));
|
|
5243
|
+
process.exit(1);
|
|
5244
|
+
}
|
|
5245
|
+
const { startWebUI } = await import("./server-QC5SN6T4.js");
|
|
5246
|
+
await startWebUI({
|
|
5247
|
+
folder: root,
|
|
5248
|
+
port: options.port,
|
|
5249
|
+
open: options.open,
|
|
5250
|
+
authStorage: auth.authStorage,
|
|
5251
|
+
modelId: config.queryModel
|
|
5252
|
+
});
|
|
5253
|
+
});
|
|
5743
5254
|
program.parse();
|