llm-kb 0.4.0 → 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/README.md +183 -42
  2. package/bin/anthropic-5TIU2EED.js +5515 -0
  3. package/bin/azure-openai-responses-ZVUVMK3G.js +190 -0
  4. package/bin/chunk-2WV6TQRI.js +4792 -0
  5. package/bin/chunk-3YMNGUZZ.js +262 -0
  6. package/bin/chunk-5PYKQQLA.js +14295 -0
  7. package/bin/chunk-65KFH7OI.js +31 -0
  8. package/bin/chunk-DHOXVEIR.js +7261 -0
  9. package/bin/chunk-EAQYK3U2.js +41 -0
  10. package/bin/chunk-IFS3OKBN.js +428 -0
  11. package/bin/chunk-LDHOKBJA.js +86 -0
  12. package/bin/chunk-SLYBG6ZQ.js +32681 -0
  13. package/bin/chunk-UEODFF7H.js +17 -0
  14. package/bin/chunk-XCXTZJGO.js +174 -0
  15. package/bin/chunk-XFV534WU.js +7056 -0
  16. package/bin/cli.js +30 -4
  17. package/bin/dist-3YH7P2QF.js +1244 -0
  18. package/bin/google-JFC43EFJ.js +371 -0
  19. package/bin/google-gemini-cli-K4XNMYDI.js +712 -0
  20. package/bin/google-vertex-Y42F254G.js +414 -0
  21. package/bin/indexer-KSYRIVVN.js +10 -0
  22. package/bin/mistral-ZU2JS5XZ.js +38406 -0
  23. package/bin/multipart-parser-CO464TZY.js +371 -0
  24. package/bin/openai-codex-responses-NW2LELBH.js +712 -0
  25. package/bin/openai-completions-TW3VKTHO.js +662 -0
  26. package/bin/openai-responses-VGL522MK.js +198 -0
  27. package/bin/src-Y22OHE3S.js +1408 -0
  28. package/package.json +6 -1
  29. package/PHASE2_SPEC.md +0 -274
  30. package/PHASE3_SPEC.md +0 -245
  31. package/PHASE4_SPEC.md +0 -358
  32. package/SPEC.md +0 -275
  33. package/plan.md +0 -300
  34. package/src/auth.ts +0 -55
  35. package/src/cli.ts +0 -257
  36. package/src/config.ts +0 -61
  37. package/src/eval.ts +0 -548
  38. package/src/indexer.ts +0 -152
  39. package/src/md-stream.ts +0 -133
  40. package/src/pdf.ts +0 -119
  41. package/src/query.ts +0 -408
  42. package/src/resolve-kb.ts +0 -19
  43. package/src/scan.ts +0 -59
  44. package/src/session-store.ts +0 -22
  45. package/src/session-watcher.ts +0 -89
  46. package/src/trace-builder.ts +0 -168
  47. package/src/tui-display.ts +0 -281
  48. package/src/utils.ts +0 -17
  49. package/src/watcher.ts +0 -87
  50. package/src/wiki-updater.ts +0 -136
  51. package/test/auth.test.ts +0 -65
  52. package/test/config.test.ts +0 -96
  53. package/test/md-stream.test.ts +0 -98
  54. package/test/resolve-kb.test.ts +0 -33
  55. package/test/scan.test.ts +0 -65
  56. package/test/trace-builder.test.ts +0 -215
  57. package/tsconfig.json +0 -14
  58. package/vitest.config.ts +0 -8
package/src/md-stream.ts DELETED
@@ -1,133 +0,0 @@
1
- import chalk from "chalk";
2
-
3
- /**
4
- * Streaming markdown renderer for terminal output.
5
- *
6
- * Processes text_delta chunks and applies ANSI styling as patterns complete.
7
- * Handles: **bold**, *italic*, `code`, ## headers, --- hr, • bullets,
8
- * > blockquotes, [links](url), ~~strikethrough~~, | tables.
9
- */
10
- export class MarkdownStream {
11
- private buffer = "";
12
- private isTTY: boolean;
13
-
14
- constructor(isTTY = false) {
15
- this.isTTY = isTTY;
16
- }
17
-
18
- /** Feed a text_delta chunk. Returns styled string ready for stdout. */
19
- push(chunk: string): string {
20
- if (!this.isTTY) return chunk;
21
-
22
- this.buffer += chunk;
23
- return this.drain(false);
24
- }
25
-
26
- /** Flush remaining buffer (call on text_end). */
27
- end(): string {
28
- if (!this.isTTY) return "";
29
- const out = this.drain(true);
30
- this.buffer = "";
31
- return out;
32
- }
33
-
34
- private drain(final: boolean): string {
35
- let out = "";
36
-
37
- while (true) {
38
- const nlIdx = this.buffer.indexOf("\n");
39
-
40
- if (nlIdx === -1) {
41
- if (final && this.buffer.length > 0) {
42
- // Final flush — render whatever's left
43
- out += this.renderLine(this.buffer);
44
- this.buffer = "";
45
- }
46
- // else: wait for more data (incomplete line)
47
- break;
48
- }
49
-
50
- // Complete line found — render it
51
- const line = this.buffer.slice(0, nlIdx);
52
- this.buffer = this.buffer.slice(nlIdx + 1);
53
- out += this.renderLine(line) + "\n";
54
- }
55
-
56
- return out;
57
- }
58
-
59
- /** Render a single complete line with block + inline styling. */
60
- private renderLine(line: string): string {
61
- const trimmed = line.trimStart();
62
-
63
- // Horizontal rule
64
- if (/^-{3,}\s*$/.test(trimmed) || /^\*{3,}\s*$/.test(trimmed)) {
65
- const cols = process.stdout.columns || 80;
66
- return chalk.dim("\u2500".repeat(Math.min(cols, 60)));
67
- }
68
-
69
- // Headers — strip # prefix, render bold
70
- const headerMatch = trimmed.match(/^(#{1,6})\s+(.*)$/);
71
- if (headerMatch) {
72
- const text = this.inline(headerMatch[2]);
73
- return "\n" + chalk.bold(text);
74
- }
75
-
76
- // Bullet points
77
- const bulletMatch = trimmed.match(/^[-*+]\s+(.*)$/);
78
- if (bulletMatch) {
79
- const indent = line.length - trimmed.length;
80
- return " ".repeat(indent) + chalk.dim("\u2022") + " " + this.inline(bulletMatch[1]);
81
- }
82
-
83
- // Numbered lists
84
- const numMatch = trimmed.match(/^(\d+)[.)]\s+(.*)$/);
85
- if (numMatch) {
86
- const indent = line.length - trimmed.length;
87
- return " ".repeat(indent) + chalk.dim(numMatch[1] + ".") + " " + this.inline(numMatch[2]);
88
- }
89
-
90
- // Table separator row
91
- if (/^\|[\s\-:|]+\|$/.test(trimmed)) {
92
- return chalk.dim(trimmed);
93
- }
94
-
95
- // Table data row
96
- if (trimmed.startsWith("|") && trimmed.endsWith("|")) {
97
- return this.inline(line);
98
- }
99
-
100
- // Block quotes — support nested > > and inline formatting
101
- if (trimmed.startsWith(">")) {
102
- const content = trimmed.replace(/^>+\s*/, "");
103
- return chalk.dim("\u2502 ") + chalk.italic(this.inline(content));
104
- }
105
-
106
- return this.inline(line);
107
- }
108
-
109
- /** Apply inline markdown styling to text. */
110
- private inline(text: string): string {
111
- // Code spans (before bold/italic to avoid conflicts inside backticks)
112
- text = text.replace(/`([^`]+)`/g, (_, c) => chalk.cyan(c));
113
-
114
- // Bold + italic
115
- text = text.replace(/\*\*\*(.+?)\*\*\*/g, (_, t) => chalk.bold.italic(t));
116
-
117
- // Bold
118
- text = text.replace(/\*\*(.+?)\*\*/g, (_, t) => chalk.bold(t));
119
-
120
- // Italic (single * not adjacent to another *)
121
- text = text.replace(/(?<!\*)\*(.+?)\*(?!\*)/g, (_, t) => chalk.italic(t));
122
-
123
- // Strikethrough
124
- text = text.replace(/~~(.+?)~~/g, (_, t) => chalk.strikethrough(t));
125
-
126
- // Links
127
- text = text.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_, label, url) =>
128
- `${label} ${chalk.dim(`(${url})`)}`
129
- );
130
-
131
- return text;
132
- }
133
- }
package/src/pdf.ts DELETED
@@ -1,119 +0,0 @@
1
- import { LiteParse } from "@llamaindex/liteparse";
2
- import { writeFile, mkdir, stat } from "node:fs/promises";
3
- import { join, basename } from "node:path";
4
- import { cpus } from "node:os";
5
-
6
- export interface ParsedPDF {
7
- name: string;
8
- mdPath: string;
9
- jsonPath: string;
10
- totalPages: number;
11
- textLength: number;
12
- skipped: boolean;
13
- }
14
-
15
- /**
16
- * Check if source PDF is newer than the parsed output.
17
- * Returns true if we can skip parsing.
18
- */
19
- async function isUpToDate(
20
- pdfPath: string,
21
- mdPath: string,
22
- jsonPath: string
23
- ): Promise<boolean> {
24
- try {
25
- const [pdfStat, mdStat, jsonStat] = await Promise.all([
26
- stat(pdfPath),
27
- stat(mdPath),
28
- stat(jsonPath),
29
- ]);
30
- return pdfStat.mtimeMs <= mdStat.mtimeMs && pdfStat.mtimeMs <= jsonStat.mtimeMs;
31
- } catch {
32
- return false;
33
- }
34
- }
35
-
36
- /**
37
- * Suppress stderr temporarily to hide noisy library warnings.
38
- */
39
- function suppressStderr(): () => void {
40
- const originalWrite = process.stderr.write.bind(process.stderr);
41
- process.stderr.write = (() => true) as any;
42
- return () => {
43
- process.stderr.write = originalWrite;
44
- };
45
- }
46
-
47
- export async function parsePDF(
48
- pdfPath: string,
49
- outputDir: string
50
- ): Promise<ParsedPDF> {
51
- const name = basename(pdfPath, ".pdf");
52
- await mkdir(outputDir, { recursive: true });
53
-
54
- const mdPath = join(outputDir, `${name}.md`);
55
- const jsonPath = join(outputDir, `${name}.json`);
56
-
57
- // Skip if already parsed and source hasn't changed
58
- if (await isUpToDate(pdfPath, mdPath, jsonPath)) {
59
- return { name, mdPath, jsonPath, totalPages: 0, textLength: 0, skipped: true };
60
- }
61
-
62
- const ocrServerUrl = process.env.OCR_SERVER_URL;
63
- const ocrEnabled = ocrServerUrl ? true : process.env.OCR_ENABLED === "true";
64
-
65
- const parser = new LiteParse({
66
- ocrEnabled,
67
- outputFormat: "json",
68
- numWorkers: cpus().length,
69
- ...(ocrServerUrl ? { ocrServerUrl } : {}),
70
- });
71
-
72
- // Suppress noisy Tesseract/PDF.js warnings during parse
73
- const restore = suppressStderr();
74
- let result;
75
- try {
76
- result = await parser.parse(pdfPath, true);
77
- } finally {
78
- restore();
79
- }
80
-
81
- // Build markdown — spatial text per page
82
- const markdown = result.pages
83
- .map((p: any) => `# Page ${p.pageNum}\n\n${p.text}`)
84
- .join("\n\n---\n\n");
85
-
86
- // Build bounding box JSON
87
- const bboxData = {
88
- source: basename(pdfPath),
89
- totalPages: result.pages.length,
90
- pages: result.pages.map((p: any) => ({
91
- page: p.pageNum,
92
- width: p.width,
93
- height: p.height,
94
- textItems: p.textItems.map((item: any) => ({
95
- text: (item.str ?? item.text ?? "").trim(),
96
- x: Math.round(item.x * 100) / 100,
97
- y: Math.round(item.y * 100) / 100,
98
- width: Math.round((item.width ?? item.w ?? 0) * 100) / 100,
99
- height: Math.round((item.height ?? item.h ?? 0) * 100) / 100,
100
- fontName: item.fontName,
101
- fontSize: item.fontSize
102
- ? Math.round(item.fontSize * 100) / 100
103
- : undefined,
104
- })),
105
- })),
106
- };
107
-
108
- await writeFile(mdPath, markdown);
109
- await writeFile(jsonPath, JSON.stringify(bboxData, null, 2));
110
-
111
- return {
112
- name,
113
- mdPath,
114
- jsonPath,
115
- totalPages: result.pages.length,
116
- textLength: markdown.length,
117
- skipped: false,
118
- };
119
- }
package/src/query.ts DELETED
@@ -1,408 +0,0 @@
1
- import {
2
- createAgentSession,
3
- createBashTool,
4
- createReadTool,
5
- createWriteTool,
6
- DefaultResourceLoader,
7
- SettingsManager,
8
- AuthStorage,
9
- } from "@mariozechner/pi-coding-agent";
10
- import type { AgentSession } from "@mariozechner/pi-coding-agent";
11
- import { getModels } from "@mariozechner/pi-ai";
12
- import { readdir, mkdir, readFile } from "node:fs/promises";
13
- import { existsSync } from "node:fs";
14
- import { createKBSession, continueKBSession } from "./session-store.js";
15
- import { saveTrace, appendToQueryLog, KBTrace } from "./trace-builder.js";
16
- import { updateWiki } from "./wiki-updater.js";
17
- import { join, basename } from "node:path";
18
- import chalk from "chalk";
19
- import { getNodeModulesPath } from "./utils.js";
20
- import { MarkdownStream } from "./md-stream.js";
21
- import type { ChatDisplay } from "./tui-display.js";
22
-
23
- // ── Helpers ─────────────────────────────────────────────────────────────────
24
-
25
- function extractAnswerText(content: any[]): string {
26
- return (content ?? [])
27
- .filter((b: any) => b.type === "text")
28
- .map((b: any) => b.text ?? "")
29
- .join("")
30
- .trim();
31
- }
32
-
33
- function extractFilesRead(messages: any[]): string[] {
34
- const paths: string[] = [];
35
- for (const msg of messages) {
36
- if (msg.role !== "assistant") continue;
37
- for (const block of msg.content ?? []) {
38
- if (block.type === "toolCall" && block.name === "read") {
39
- const p: string = block.arguments?.path ?? "";
40
- if (p && !paths.includes(p)) paths.push(p);
41
- }
42
- }
43
- }
44
- return paths;
45
- }
46
-
47
- function getToolLabel(toolName: string, args: any): string | null {
48
- if (toolName === "read" || toolName === "write" || toolName === "edit") {
49
- const file = basename((args?.path as string) ?? "");
50
- if (!file || !/\.[a-z0-9]{1,6}$/i.test(file)) return null;
51
- const verb = toolName === "read" ? "Reading" : toolName === "write" ? "Writing" : "Editing";
52
- return `${verb} ${file}`;
53
- }
54
- if (toolName === "bash" && args?.command) {
55
- return `Running bash`;
56
- }
57
- return null;
58
- }
59
-
60
- // ── AGENTS.md ───────────────────────────────────────────────────────────────
61
-
62
- function buildQueryAgents(sourceFiles: string[], save: boolean, wikiContent: string): string {
63
- const sourceList = sourceFiles.map((f) => ` - ${f}`).join("\n");
64
- const wikiSection = wikiContent
65
- ? `## Knowledge Wiki (use this first)\n\nThe wiki below contains knowledge already extracted from this knowledge base.\nIf the user's question is covered here, answer directly from it — no need to re-read source files.\nAlways cite the original source files mentioned in the wiki.\n\n${wikiContent}\n\n---\n\n`
66
- : "";
67
- const sourceStep = wikiContent ? "If not covered in the wiki above: read the sources" : "How to answer";
68
-
69
- const lines = [
70
- `# llm-kb Knowledge Base — Query Mode`,
71
- ``,
72
- wikiSection,
73
- `## ${sourceStep}`,
74
- ``,
75
- `1. Read .llm-kb/wiki/index.md to understand all available sources`,
76
- `2. Select the most relevant source files (usually 2-5) and read them in full`,
77
- `3. Answer with inline citations: (filename, page number)`,
78
- `4. If you can't find the answer, say so — don't hallucinate`,
79
- ``,
80
- `## Available parsed sources`,
81
- sourceList,
82
- ``,
83
- `## Non-PDF files (docx, xlsx, pptx)`,
84
- `Use bash to run Node.js scripts. Libraries are pre-installed via require().`,
85
- ``,
86
- `### Word (.docx) — structured XML`,
87
- `.docx files are ZIP archives containing word/document.xml.`,
88
- `Read them SELECTIVELY — extract only what is relevant to the question:`,
89
- ``,
90
- "```javascript",
91
- `const AdmZip = require('adm-zip');`,
92
- `const zip = new AdmZip('file.docx');`,
93
- `const xml = zip.readAsText('word/document.xml');`,
94
- `// Parse XML to find specific paragraphs, headings, tables`,
95
- "```",
96
- ``,
97
- `Strategy for large .docx files:`,
98
- `1. First: extract headings/structure to understand the document layout`,
99
- `2. Then: extract only the sections relevant to the user's question`,
100
- `NEVER dump the entire document.`,
101
- ``,
102
- `### Excel (.xlsx) — use exceljs`,
103
- `Read specific sheets and ranges, not the whole workbook:`,
104
- ``,
105
- "```javascript",
106
- `const ExcelJS = require('exceljs');`,
107
- `const wb = new ExcelJS.Workbook();`,
108
- `await wb.xlsx.readFile('file.xlsx');`,
109
- `const sheet = wb.getWorksheet(1);`,
110
- `// Read specific rows/columns relevant to the question`,
111
- "```",
112
- ``,
113
- `### PowerPoint (.pptx) — use officeparser`,
114
- ``,
115
- "```javascript",
116
- `const officeparser = require('officeparser');`,
117
- `const text = await officeparser.parseOfficeAsync('file.pptx');`,
118
- "```",
119
- ``,
120
- `## Rules`,
121
- `- Always cite sources with filename and page number`,
122
- `- Read the FULL source file, not just the beginning (for .md sources)`,
123
- `- For non-PDF files, extract ONLY relevant sections — never dump entire files`,
124
- `- Prefer primary sources over previous analyses`,
125
- ];
126
-
127
- if (save) {
128
- 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.`);
129
- }
130
-
131
- return lines.join("\n");
132
- }
133
-
134
- // ── Wiki update scheduler ───────────────────────────────────────────────────
135
-
136
- class WikiUpdateScheduler {
137
- private stopMsgCount = 0;
138
- private lastUpdateAt = 0;
139
- private chain: Promise<void> = Promise.resolve();
140
- constructor(private readonly everyN: number, private readonly everyMin: number) {}
141
- private shouldUpdate() {
142
- return (this.stopMsgCount > 0 && this.stopMsgCount % this.everyN === 0) ||
143
- (this.lastUpdateAt > 0 && Date.now() - this.lastUpdateAt > this.everyMin * 60_000);
144
- }
145
- private enqueue(work: () => Promise<void>) { this.chain = this.chain.then(() => work().catch(() => {})); }
146
- onMessageEnd(msg: any, snap: () => { messages: any[] }, doUpdate: (m: any[]) => Promise<void>) {
147
- if (msg.role !== "assistant" || msg.stopReason !== "stop") return;
148
- this.stopMsgCount++;
149
- if (this.shouldUpdate()) { this.lastUpdateAt = Date.now(); this.enqueue(() => doUpdate(snap().messages)); }
150
- }
151
- onAgentEnd(msgs: any[], doUpdate: (m: any[]) => Promise<void>) {
152
- this.lastUpdateAt = Date.now(); this.enqueue(() => doUpdate(msgs));
153
- }
154
- flush() { return this.chain; }
155
- }
156
-
157
- // ── Display subscriber ──────────────────────────────────────────────────────
158
- // Routes events to either TUI components (interactive) or stdout (one-shot)
159
-
160
- function subscribeDisplay(
161
- session: AgentSession,
162
- opts: {
163
- modelId?: string;
164
- authStorage?: AuthStorage;
165
- folder: string;
166
- mdFiles: string[];
167
- tuiDisplay?: ChatDisplay;
168
- }
169
- ) {
170
- const ui = opts.tuiDisplay;
171
- const dim = (s: string) => process.stdout.isTTY ? chalk.dim(s) : s;
172
- const thinLine = () => dim("\u2500".repeat(process.stdout.columns || 80));
173
-
174
- let phase: "idle" | "thinking" | "tools" | "answer" = "idle";
175
- let filesReadCount = 0;
176
- let shownToolCalls = new Set<string>();
177
- let startTime = Date.now();
178
- let md = new MarkdownStream(process.stdout.isTTY ?? false);
179
- let lastQuestion = "";
180
-
181
- const scheduler = new WikiUpdateScheduler(5, 3);
182
-
183
- const buildTrace = (messages: any[]): KBTrace | null => {
184
- const last = [...messages].reverse().find((m) => m.role === "assistant" && m.stopReason === "stop");
185
- if (!last) return null;
186
- const filesRead = extractFilesRead(messages);
187
- return {
188
- sessionId: session.sessionId, sessionFile: session.sessionFile ?? "",
189
- timestamp: new Date().toISOString(), mode: "query", question: lastQuestion,
190
- answer: extractAnswerText(last.content), filesRead,
191
- filesAvailable: opts.mdFiles,
192
- filesSkipped: opts.mdFiles.filter((f) => !filesRead.some((r) => r.endsWith(f))),
193
- model: last.model,
194
- };
195
- };
196
-
197
- const doUpdate = async (messages: any[]) => {
198
- const trace = buildTrace(messages);
199
- if (!trace) return;
200
- await saveTrace(opts.folder, trace);
201
- await appendToQueryLog(opts.folder, trace);
202
- await updateWiki(opts.folder, trace, opts.authStorage);
203
- };
204
-
205
- session.subscribe((event) => {
206
-
207
- // ── Reset ────────────────────────────────────────────────────────────
208
- if (event.type === "agent_start") {
209
- phase = "idle";
210
- filesReadCount = 0;
211
- shownToolCalls = new Set();
212
- startTime = Date.now();
213
- md = new MarkdownStream(process.stdout.isTTY ?? false);
214
- const modelName = opts.modelId ?? "claude-sonnet-4-6";
215
- if (ui) { ui.disableInput(); ui.beginResponse(modelName); }
216
- else process.stdout.write(dim(`\u27e1 ${modelName}`) + "\n");
217
- }
218
-
219
- // ── Thinking ─────────────────────────────────────────────────────────
220
- if (event.type === "message_update") {
221
- const ae = event.assistantMessageEvent;
222
- if (ae.type === "thinking_start") {
223
- if (!ui) process.stdout.write(dim("\n\u25b8 Thinking\n"));
224
- phase = "thinking";
225
- }
226
- if (ae.type === "thinking_delta") {
227
- if (ui) ui.appendThinking(ae.delta);
228
- else process.stdout.write(dim(` ${ae.delta}`));
229
- }
230
- if (ae.type === "thinking_end") {
231
- if (ui) ui.endThinking();
232
- else process.stdout.write("\n");
233
- }
234
- }
235
-
236
- // ── Tool calls ───────────────────────────────────────────────────────
237
- if (event.type === "message_update") {
238
- const ae = event.assistantMessageEvent as any;
239
- if (ae.type === "toolcall_end" && ae.toolCall) {
240
- const label = getToolLabel(ae.toolCall.name, ae.toolCall.arguments);
241
- if (label) {
242
- if (!ui && phase !== "tools") process.stdout.write("\n");
243
- phase = "tools";
244
- if (ui) {
245
- ui.addToolCall(ae.toolCall.id, label, ae.toolCall.name);
246
- // Show the actual bash code the agent wrote
247
- if (ae.toolCall.name === "bash" && ae.toolCall.arguments?.command) {
248
- ui.addCodeBlock(ae.toolCall.arguments.command);
249
- }
250
- } else {
251
- process.stdout.write(dim(` \u25b8 ${label}`) + "\n");
252
- // Show bash code in stdout mode too
253
- if (ae.toolCall.name === "bash" && ae.toolCall.arguments?.command) {
254
- const code = ae.toolCall.arguments.command as string;
255
- process.stdout.write(dim(code.split("\n").map(l => ` ${l}`).join("\n")) + "\n");
256
- }
257
- shownToolCalls.add(ae.toolCall.id);
258
- if (ae.toolCall.name === "read") filesReadCount++;
259
- }
260
- }
261
- }
262
- }
263
-
264
- if (event.type === "tool_execution_start") {
265
- const { toolCallId, toolName, args } = event as any;
266
- if (ui) {
267
- const label = getToolLabel(toolName, args);
268
- if (label) ui.addToolCall(toolCallId, label, toolName);
269
- } else if (!shownToolCalls.has(toolCallId)) {
270
- const label = getToolLabel(toolName, args);
271
- if (label) {
272
- if (phase !== "tools") process.stdout.write("\n");
273
- phase = "tools";
274
- process.stdout.write(dim(` \u25b8 ${label}`) + "\n");
275
- shownToolCalls.add(toolCallId);
276
- if (toolName === "read") filesReadCount++;
277
- }
278
- }
279
- }
280
-
281
- // tool result (show errors)
282
- if (event.type === "tool_execution_end") {
283
- const { toolCallId, isError } = event as any;
284
- if (ui) ui.addToolResult(toolCallId, isError);
285
- }
286
-
287
- // ── Answer ───────────────────────────────────────────────────────────
288
- if (event.type === "message_update") {
289
- const ae = event.assistantMessageEvent;
290
- if (ae.type === "text_start" && phase !== "answer") {
291
- if (ui) ui.beginAnswer();
292
- else if (phase === "thinking" || phase === "tools") {
293
- process.stdout.write(`\n${thinLine()}\n\n`);
294
- }
295
- phase = "answer";
296
- }
297
- if (ae.type === "text_delta") {
298
- if (ui) ui.appendAnswer(ae.delta);
299
- else process.stdout.write(md.push(ae.delta));
300
- }
301
- if (ae.type === "text_end" && !ui) process.stdout.write(md.end());
302
- }
303
-
304
- // ── Completion ───────────────────────────────────────────────────────
305
- if (event.type === "agent_end") {
306
- if (ui) { ui.showCompletion(); ui.enableInput(); }
307
- else {
308
- const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
309
- const source = filesReadCount > 0
310
- ? `${filesReadCount} file${filesReadCount !== 1 ? "s" : ""} read` : "wiki";
311
- const stats = `${elapsed}s \u00b7 ${source}`;
312
- const cols = process.stdout.columns || 80;
313
- const pad = Math.max(0, cols - stats.length - 4);
314
- process.stdout.write(`\n\n${dim("\u2500\u2500 " + stats + " " + "\u2500".repeat(pad))}\n`);
315
- }
316
- scheduler.onAgentEnd(event.messages as any[], doUpdate);
317
- }
318
-
319
- // ── Wiki throttle ────────────────────────────────────────────────────
320
- if (event.type === "message_end") {
321
- scheduler.onMessageEnd(event.message, () => ({ messages: session.state.messages as any[] }), doUpdate);
322
- }
323
- });
324
-
325
- return {
326
- setQuestion(q: string) { lastQuestion = q; },
327
- flush() { return scheduler.flush(); },
328
- };
329
- }
330
-
331
- // ── Session factory ─────────────────────────────────────────────────────────
332
-
333
- export interface ChatSession {
334
- session: AgentSession;
335
- display: ReturnType<typeof subscribeDisplay>;
336
- }
337
-
338
- export async function createChat(
339
- folder: string,
340
- options: { save?: boolean; authStorage?: AuthStorage; modelId?: string; tuiDisplay?: ChatDisplay }
341
- ): Promise<ChatSession> {
342
- const sourcesDir = join(folder, ".llm-kb", "wiki", "sources");
343
- const files = await readdir(sourcesDir);
344
- const mdFiles = files.filter((f) => f.endsWith(".md"));
345
- if (mdFiles.length === 0) throw new Error("No sources found. Run 'llm-kb run' first.");
346
- if (options.save) await mkdir(join(folder, ".llm-kb", "wiki", "outputs"), { recursive: true });
347
-
348
- process.env.NODE_PATH = getNodeModulesPath();
349
-
350
- const wikiPath = join(folder, ".llm-kb", "wiki", "wiki.md");
351
- const wikiContent = existsSync(wikiPath) ? await readFile(wikiPath, "utf-8").catch(() => "") : "";
352
- const agentsContent = buildQueryAgents(mdFiles, !!options.save, wikiContent);
353
-
354
- const loader = new DefaultResourceLoader({
355
- cwd: folder,
356
- agentsFilesOverride: (current) => ({
357
- agentsFiles: [...current.agentsFiles, { path: ".llm-kb/AGENTS.md", content: agentsContent }],
358
- }),
359
- });
360
- await loader.reload();
361
-
362
- // Always include all tools — agent needs bash for .docx/.xlsx reading
363
- const tools = [
364
- createReadTool(folder),
365
- createBashTool(folder),
366
- createWriteTool(folder),
367
- ];
368
-
369
- const model = options.modelId ? getModels("anthropic").find((m) => m.id === options.modelId) : undefined;
370
-
371
- const { session } = await createAgentSession({
372
- cwd: folder,
373
- resourceLoader: loader,
374
- tools,
375
- sessionManager: options.save ? await createKBSession(folder) : await continueKBSession(folder),
376
- settingsManager: SettingsManager.inMemory({ compaction: { enabled: false } }),
377
- thinkingLevel: "low",
378
- ...(options.authStorage ? { authStorage: options.authStorage } : {}),
379
- ...(model ? { model } : {}),
380
- });
381
-
382
- const display = subscribeDisplay(session, {
383
- modelId: options.modelId, authStorage: options.authStorage,
384
- folder, mdFiles, tuiDisplay: options.tuiDisplay,
385
- });
386
-
387
- return { session, display };
388
- }
389
-
390
- // ── One-shot query (stdout mode, for `llm-kb query` command) ────────────────
391
-
392
- export async function query(
393
- folder: string,
394
- question: string,
395
- options: { save?: boolean; authStorage?: AuthStorage; modelId?: string }
396
- ): Promise<void> {
397
- const { session, display } = await createChat(folder, options);
398
- session.setSessionName(`query: ${question}`);
399
- display.setQuestion(question);
400
- await session.prompt(question);
401
- await display.flush();
402
- session.dispose();
403
- if (options.save) {
404
- const sourcesDir = join(folder, ".llm-kb", "wiki", "sources");
405
- const { buildIndex } = await import("./indexer.js");
406
- await buildIndex(folder, sourcesDir, undefined, options.authStorage);
407
- }
408
- }
package/src/resolve-kb.ts DELETED
@@ -1,19 +0,0 @@
1
- import { existsSync } from "node:fs";
2
- import { resolve, join, dirname } from "node:path";
3
-
4
- /**
5
- * Walk up from startDir looking for a .llm-kb/ directory.
6
- * Returns the folder containing .llm-kb/, or null if not found.
7
- */
8
- export function resolveKnowledgeBase(startDir: string): string | null {
9
- let dir = resolve(startDir);
10
-
11
- while (true) {
12
- if (existsSync(join(dir, ".llm-kb"))) {
13
- return dir;
14
- }
15
- const parent = dirname(dir);
16
- if (parent === dir) return null;
17
- dir = parent;
18
- }
19
- }