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/scan.ts DELETED
@@ -1,59 +0,0 @@
1
- import { readdir } from "node:fs/promises";
2
- import { resolve, extname, relative } from "node:path";
3
-
4
- export interface ScannedFile {
5
- name: string;
6
- path: string;
7
- ext: string;
8
- }
9
-
10
- const SUPPORTED_EXTENSIONS = new Set([
11
- ".pdf",
12
- ".xlsx",
13
- ".xls",
14
- ".docx",
15
- ".pptx",
16
- ".jpg",
17
- ".jpeg",
18
- ".png",
19
- ".txt",
20
- ".md",
21
- ".csv",
22
- ]);
23
-
24
- export async function scan(folder: string): Promise<ScannedFile[]> {
25
- const root = resolve(folder);
26
- const entries = await readdir(root, { recursive: true, withFileTypes: true });
27
-
28
- const files: ScannedFile[] = [];
29
-
30
- for (const entry of entries) {
31
- if (!entry.isFile()) continue;
32
-
33
- const fullPath = resolve(entry.parentPath, entry.name);
34
- const rel = relative(root, fullPath);
35
-
36
- // Skip .llm-kb internal folder
37
- if (rel.startsWith(".llm-kb")) continue;
38
-
39
- const ext = extname(entry.name).toLowerCase();
40
- if (!SUPPORTED_EXTENSIONS.has(ext)) continue;
41
-
42
- files.push({ name: entry.name, path: rel, ext });
43
- }
44
-
45
- return files;
46
- }
47
-
48
- export function summarize(files: ScannedFile[]): string {
49
- const counts = new Map<string, number>();
50
- for (const f of files) {
51
- counts.set(f.ext, (counts.get(f.ext) || 0) + 1);
52
- }
53
-
54
- const parts = Array.from(counts.entries())
55
- .sort((a, b) => b[1] - a[1])
56
- .map(([ext, count]) => `${count} ${ext.toUpperCase().slice(1)}`);
57
-
58
- return parts.join(", ");
59
- }
@@ -1,22 +0,0 @@
1
- import { SessionManager } from "@mariozechner/pi-coding-agent";
2
- import { mkdir } from "node:fs/promises";
3
- import { join } from "node:path";
4
-
5
- /**
6
- * Continue the most recent session, or create a new one if none exists.
7
- * Sessions persist in .llm-kb/sessions/ — conversation history survives restarts.
8
- */
9
- export async function continueKBSession(kbRoot: string): Promise<SessionManager> {
10
- const sessionDir = join(kbRoot, ".llm-kb", "sessions");
11
- await mkdir(sessionDir, { recursive: true });
12
- return SessionManager.continueRecent(kbRoot, sessionDir);
13
- }
14
-
15
- /**
16
- * Always create a fresh session (for one-shot `llm-kb query` or indexing).
17
- */
18
- export async function createKBSession(kbRoot: string): Promise<SessionManager> {
19
- const sessionDir = join(kbRoot, ".llm-kb", "sessions");
20
- await mkdir(sessionDir, { recursive: true });
21
- return SessionManager.create(kbRoot, sessionDir);
22
- }
@@ -1,89 +0,0 @@
1
- import { watch } from "chokidar";
2
- import { join, basename } from "node:path";
3
- import { readdir, readFile, writeFile, mkdir } from "node:fs/promises";
4
- import { existsSync } from "node:fs";
5
- import { buildTrace, saveTrace, appendToQueryLog } from "./trace-builder.js";
6
- import { updateWiki } from "./wiki-updater.js";
7
-
8
- const PROCESSED_LOG = ".llm-kb/traces/.processed";
9
-
10
- async function loadProcessed(kbRoot: string): Promise<Set<string>> {
11
- const path = join(kbRoot, PROCESSED_LOG);
12
- if (!existsSync(path)) return new Set();
13
- try {
14
- const lines = (await readFile(path, "utf-8")).split("\n").filter(Boolean);
15
- return new Set(lines);
16
- } catch {
17
- return new Set();
18
- }
19
- }
20
-
21
- async function markProcessed(kbRoot: string, sessionId: string): Promise<void> {
22
- const path = join(kbRoot, PROCESSED_LOG);
23
- await mkdir(join(kbRoot, ".llm-kb", "traces"), { recursive: true });
24
- await writeFile(path, sessionId + "\n", { flag: "a" });
25
- }
26
-
27
- /**
28
- * Watch .llm-kb/sessions/ for completed session files.
29
- * Processes them silently — saves traces, updates wiki, logs queries.
30
- * Persists processed IDs to .llm-kb/traces/.processed to survive restarts.
31
- */
32
- export async function startSessionWatcher(kbRoot: string): Promise<void> {
33
- const sessionsDir = join(kbRoot, ".llm-kb", "sessions");
34
- const sourcesDir = join(kbRoot, ".llm-kb", "wiki", "sources");
35
-
36
- const processed = await loadProcessed(kbRoot);
37
- const timers = new Map<string, ReturnType<typeof setTimeout>>();
38
-
39
- async function processSession(filePath: string): Promise<void> {
40
- const sessionId = basename(filePath, ".jsonl").split("_")[1] ?? basename(filePath, ".jsonl");
41
- if (processed.has(sessionId)) return;
42
-
43
- try {
44
- const trace = await buildTrace(filePath, sourcesDir);
45
- if (!trace) return;
46
-
47
- processed.add(trace.sessionId);
48
- await markProcessed(kbRoot, trace.sessionId);
49
-
50
- await saveTrace(kbRoot, trace);
51
-
52
- if (trace.mode === "query") {
53
- await appendToQueryLog(kbRoot, trace);
54
- await updateWiki(kbRoot, trace);
55
- }
56
- } catch {
57
- // Non-fatal — session may still be in progress
58
- }
59
- }
60
-
61
- function scheduleProcess(filePath: string): void {
62
- const existing = timers.get(filePath);
63
- if (existing) clearTimeout(existing);
64
- const timer = setTimeout(() => {
65
- timers.delete(filePath);
66
- processSession(filePath);
67
- }, 1500);
68
- timers.set(filePath, timer);
69
- }
70
-
71
- // Catch-up: process existing unprocessed sessions silently
72
- if (existsSync(sessionsDir)) {
73
- try {
74
- const files = (await readdir(sessionsDir)).filter((f) => f.endsWith(".jsonl"));
75
- for (const f of files) {
76
- await processSession(join(sessionsDir, f));
77
- }
78
- } catch { /* ignore */ }
79
- }
80
-
81
- const watcher = watch(sessionsDir, {
82
- ignoreInitial: true,
83
- awaitWriteFinish: { stabilityThreshold: 500, pollInterval: 100 },
84
- depth: 0,
85
- });
86
-
87
- watcher.on("add", (p) => { if (p.endsWith(".jsonl")) scheduleProcess(p); });
88
- watcher.on("change", (p) => { if (p.endsWith(".jsonl")) scheduleProcess(p); });
89
- }
@@ -1,168 +0,0 @@
1
- import { readFile, writeFile, mkdir, readdir } from "node:fs/promises";
2
- import { existsSync } from "node:fs";
3
- import { join, basename as pathBasename } from "node:path";
4
-
5
- export interface KBTrace {
6
- sessionId: string;
7
- sessionFile: string;
8
- timestamp: string;
9
- mode: "query" | "index" | "unknown";
10
- question?: string;
11
- answer?: string;
12
- filesRead: string[];
13
- filesAvailable: string[];
14
- filesSkipped: string[];
15
- model?: string;
16
- durationMs?: number;
17
- }
18
-
19
- /** Parse a session JSONL file and build a KBTrace. Returns null if session isn't complete yet. */
20
- export async function buildTrace(
21
- sessionFile: string,
22
- sourcesDir: string
23
- ): Promise<KBTrace | null> {
24
- const raw = await readFile(sessionFile, "utf-8");
25
- const lines = raw.trim().split("\n").filter(Boolean);
26
- if (lines.length < 2) return null;
27
-
28
- const entries: any[] = [];
29
- let header: any = null;
30
- for (const line of lines) {
31
- try {
32
- const obj = JSON.parse(line);
33
- if (obj.type === "session") header = obj;
34
- else entries.push(obj);
35
- } catch { /* skip malformed lines */ }
36
- }
37
-
38
- if (!header) return null;
39
-
40
- const messages = entries.filter((e) => e.type === "message");
41
- const lastAssistant = [...messages].reverse().find(
42
- (e) => e.message?.role === "assistant" && e.message?.stopReason === "stop"
43
- );
44
- if (!lastAssistant) return null;
45
-
46
- const modelChange = entries.find((e) => e.type === "model_change");
47
- const model = modelChange?.modelId ?? lastAssistant.message?.model ?? undefined;
48
-
49
- const firstUser = messages.find((e) => e.message?.role === "user");
50
- const question = extractText(firstUser?.message?.content);
51
-
52
- const sessionInfo = entries.find((e) => e.type === "session_info");
53
- const sessionName: string = sessionInfo?.name ?? "";
54
- const mode: KBTrace["mode"] = sessionName.startsWith("index:")
55
- ? "index"
56
- : sessionName.startsWith("query:") || question
57
- ? "query"
58
- : "unknown";
59
-
60
- const answer = extractText(lastAssistant.message?.content);
61
-
62
- const filesRead: string[] = [];
63
- for (const entry of messages) {
64
- if (entry.message?.role !== "assistant") continue;
65
- for (const block of entry.message?.content ?? []) {
66
- if (block.type === "toolCall" && block.name === "read") {
67
- const p: string = block.arguments?.path ?? "";
68
- if (p && !filesRead.includes(p)) filesRead.push(p);
69
- }
70
- }
71
- }
72
-
73
- let filesAvailable: string[] = [];
74
- try {
75
- const all = await readdir(sourcesDir);
76
- filesAvailable = all.filter((f) => f.endsWith(".md"));
77
- } catch { /* sources dir may not exist */ }
78
-
79
- const filesSkipped = filesAvailable.filter(
80
- (f) => !filesRead.some((r) => r.endsWith(f))
81
- );
82
-
83
- const firstMsg = messages[0];
84
- const lastMsg = messages[messages.length - 1];
85
- let durationMs: number | undefined;
86
- if (firstMsg?.timestamp && lastMsg?.timestamp) {
87
- durationMs = new Date(lastMsg.timestamp).getTime() - new Date(firstMsg.timestamp).getTime();
88
- }
89
-
90
- return {
91
- sessionId: header.id,
92
- sessionFile: pathBasename(sessionFile),
93
- timestamp: header.timestamp,
94
- mode,
95
- question: question || undefined,
96
- answer: answer || undefined,
97
- filesRead,
98
- filesAvailable,
99
- filesSkipped,
100
- model,
101
- durationMs,
102
- };
103
- }
104
-
105
- /** Write a KBTrace to .llm-kb/traces/<sessionId>.json */
106
- export async function saveTrace(kbRoot: string, trace: KBTrace): Promise<void> {
107
- const tracesDir = join(kbRoot, ".llm-kb", "traces");
108
- await mkdir(tracesDir, { recursive: true });
109
- const outPath = join(tracesDir, `${trace.sessionId}.json`);
110
- await writeFile(outPath, JSON.stringify(trace, null, 2) + "\n", "utf-8");
111
- }
112
-
113
- /** Append a query entry to .llm-kb/wiki/queries.md (newest first) */
114
- export async function appendToQueryLog(kbRoot: string, trace: KBTrace): Promise<void> {
115
- if (trace.mode !== "query" || !trace.question) return;
116
-
117
- const wikiDir = join(kbRoot, ".llm-kb", "wiki");
118
- await mkdir(wikiDir, { recursive: true });
119
- const logPath = join(wikiDir, "queries.md");
120
-
121
- const date = new Date(trace.timestamp).toISOString().replace("T", " ").slice(0, 19);
122
- const durationSec = trace.durationMs ? `${(trace.durationMs / 1000).toFixed(1)}s` : "?";
123
- const filesLine = trace.filesRead.length > 0
124
- ? trace.filesRead.map((f) => pathBasename(f)).join(", ")
125
- : "_none_";
126
-
127
- let header = "";
128
- if (!existsSync(logPath)) {
129
- header = `# Query Log\n\nAll queries run against this knowledge base.\n\n---\n\n`;
130
- }
131
-
132
- const entry = [
133
- `## ${trace.question}`,
134
- ``,
135
- `- **Date:** ${date}`,
136
- `- **Model:** ${trace.model ?? "unknown"}`,
137
- `- **Duration:** ${durationSec}`,
138
- `- **Files read:** ${filesLine}`,
139
- trace.filesSkipped.length > 0
140
- ? `- **Files skipped:** ${trace.filesSkipped.join(", ")}`
141
- : null,
142
- ``,
143
- trace.answer ? `### Answer\n\n${trace.answer}` : null,
144
- ``,
145
- `---`,
146
- ``,
147
- ]
148
- .filter((l) => l !== null)
149
- .join("\n");
150
-
151
- const existing = existsSync(logPath) ? await readFile(logPath, "utf-8") : "";
152
- await writeFile(logPath, header + entry + existing, "utf-8");
153
- }
154
-
155
- // --- helpers ---
156
-
157
- function extractText(content: any): string {
158
- if (!content) return "";
159
- if (typeof content === "string") return content;
160
- if (Array.isArray(content)) {
161
- return content
162
- .filter((b) => b.type === "text")
163
- .map((b) => b.text ?? "")
164
- .join("")
165
- .trim();
166
- }
167
- return "";
168
- }
@@ -1,281 +0,0 @@
1
- import {
2
- TUI, Container, Spacer, Text, Markdown, ProcessTerminal,
3
- type MarkdownTheme, type Component, Input,
4
- } from "@mariozechner/pi-tui";
5
- import chalk from "chalk";
6
-
7
- // ── Markdown theme ──────────────────────────────────────────────────────────
8
-
9
- function createMarkdownTheme(): MarkdownTheme {
10
- return {
11
- heading: (t) => chalk.bold(t),
12
- link: (t) => chalk.cyan(t),
13
- linkUrl: (t) => chalk.dim(t),
14
- code: (t) => chalk.cyan(t),
15
- codeBlock: (t) => chalk.dim(t),
16
- codeBlockBorder: (t) => chalk.dim(t),
17
- quote: (t) => chalk.italic(t),
18
- quoteBorder: (t) => chalk.dim(t),
19
- hr: (t) => chalk.dim(t),
20
- listBullet: (t) => chalk.dim(t),
21
- bold: (t) => chalk.bold(t),
22
- italic: (t) => chalk.italic(t),
23
- underline: (t) => chalk.underline(t),
24
- strikethrough: (t) => chalk.strikethrough(t),
25
- };
26
- }
27
-
28
- const mdTheme = createMarkdownTheme();
29
-
30
- // ── Helper components ───────────────────────────────────────────────────────
31
-
32
- function dimText(text: string, px = 1, py = 0): Text {
33
- return new Text(chalk.dim(text), px, py);
34
- }
35
-
36
- class HRule implements Component {
37
- private colorFn: (s: string) => string;
38
- constructor(colorFn?: (s: string) => string) {
39
- this.colorFn = colorFn ?? chalk.dim;
40
- }
41
- invalidate() {}
42
- render(width: number): string[] {
43
- return [this.colorFn("\u2500".repeat(width))];
44
- }
45
- }
46
-
47
- // ── Chat display ────────────────────────────────────────────────────────────
48
- //
49
- // All components are appended sequentially to currentResponse in the order
50
- // events arrive. This naturally handles interleaved thinking/tools/text:
51
- //
52
- // ⟡ model
53
- // ▸ Thinking: reasoning...
54
- // Let me read the file... ← 1st Markdown block
55
- // ▸ Reading file.md ← tool call (between text blocks)
56
- // ──────────────────────
57
- // Based on the document... ← 2nd Markdown block
58
- // ▸ Thinking: let me check another...
59
- // ▸ Reading file2.md
60
- // ──────────────────────
61
- // The final answer is... ← 3rd Markdown block
62
- // ── 12.3s · 2 files read ──────
63
-
64
- export class ChatDisplay {
65
- private tui: TUI;
66
- private terminal: ProcessTerminal;
67
- private messageArea: Container;
68
- private inputArea: Container;
69
- private input: Input;
70
-
71
- // Current response (reset per prompt)
72
- private currentResponse: Container | null = null;
73
- private currentMd: Markdown | null = null; // active text block
74
- private currentThinking: Text | null = null; // active thinking block
75
- private hadSeparator = false; // has a ─── line been drawn?
76
-
77
- private filesReadCount = 0;
78
- private shownToolCalls = new Set<string>();
79
- private startTime = Date.now();
80
-
81
- onSubmit?: (text: string) => void;
82
- onExit?: () => void;
83
-
84
- constructor() {
85
- this.terminal = new ProcessTerminal();
86
- this.tui = new TUI(this.terminal);
87
-
88
- this.messageArea = new Container();
89
- this.tui.addChild(this.messageArea);
90
-
91
- this.inputArea = new Container();
92
- this.inputArea.addChild(new HRule((s) => chalk.hex("#c678dd")(s)));
93
-
94
- this.input = new Input();
95
- this.input.onSubmit = (text) => {
96
- if (text.trim() && this.onSubmit) {
97
- this.addUserMessage(text.trim());
98
- this.onSubmit(text.trim());
99
- }
100
- this.input.setValue("");
101
- };
102
- this.inputArea.addChild(this.input);
103
- this.inputArea.addChild(new HRule((s) => chalk.hex("#c678dd")(s)));
104
-
105
- this.tui.addChild(this.inputArea);
106
- this.tui.setFocus(this.input);
107
- }
108
-
109
- start(): void {
110
- this.tui.start();
111
-
112
- this.tui.addInputListener((data) => {
113
- if (data === "\x03" || data === "\x04") {
114
- this.stop();
115
- if (this.onExit) this.onExit();
116
- else process.exit(0);
117
- return { consume: true };
118
- }
119
- return undefined;
120
- });
121
-
122
- this.tui.requestRender();
123
- }
124
-
125
- stop(): void {
126
- this.tui.stop();
127
- }
128
-
129
- addUserMessage(text: string): void {
130
- this.messageArea.addChild(new Spacer(1));
131
- this.messageArea.addChild(new Text(chalk.bold(text), 1, 0));
132
- this.tui.requestRender();
133
- }
134
-
135
- // ── Per-prompt lifecycle (events arrive in any order) ───────────────────
136
-
137
- beginResponse(modelName: string): void {
138
- this.filesReadCount = 0;
139
- this.shownToolCalls = new Set();
140
- this.startTime = Date.now();
141
- this.currentMd = null;
142
- this.currentThinking = null;
143
- this.hadSeparator = false;
144
-
145
- this.currentResponse = new Container();
146
- this.currentResponse.addChild(new Spacer(1));
147
- this.currentResponse.addChild(dimText(`\u27e1 ${modelName}`));
148
- this.messageArea.addChild(this.currentResponse);
149
- this.tui.requestRender();
150
- }
151
-
152
- /** Start or continue a thinking block. Closes any active text block. */
153
- appendThinking(text: string): void {
154
- if (!this.currentResponse) return;
155
-
156
- // Close active text block — thinking interrupts it
157
- this.currentMd = null;
158
-
159
- if (!this.currentThinking) {
160
- this.currentResponse.addChild(new Spacer(1));
161
- this.currentResponse.addChild(dimText("\u25b8 Thinking"));
162
- this.currentThinking = new Text(chalk.dim(chalk.italic(text)), 2, 0);
163
- this.currentResponse.addChild(this.currentThinking);
164
- } else {
165
- const prev = (this.currentThinking as any).text ?? "";
166
- this.currentThinking.setText(
167
- chalk.dim(chalk.italic(prev.replace(/\x1b\[[0-9;]*m/g, "") + text))
168
- );
169
- }
170
- this.tui.requestRender();
171
- }
172
-
173
- /** End the current thinking block */
174
- endThinking(): void {
175
- this.currentThinking = null;
176
- }
177
-
178
- /** Add a tool call line. Closes active text block. */
179
- addToolCall(toolCallId: string, label: string, toolName: string): void {
180
- if (!this.currentResponse || this.shownToolCalls.has(toolCallId)) return;
181
- this.shownToolCalls.add(toolCallId);
182
- if (toolName === "read") this.filesReadCount++;
183
-
184
- // Close active text block — tool call interrupts it
185
- this.currentMd = null;
186
-
187
- this.currentResponse.addChild(dimText(` \u25b8 ${label}`));
188
- this.tui.requestRender();
189
- }
190
-
191
- /** Show code block (e.g. bash script the agent wrote) */
192
- addCodeBlock(code: string): void {
193
- if (!this.currentResponse) return;
194
- this.currentMd = null;
195
- // Use Markdown component to render the code block with proper formatting
196
- this.currentResponse.addChild(new Markdown("```javascript\n" + code + "\n```", 2, 0, mdTheme));
197
- this.tui.requestRender();
198
- }
199
-
200
- /** Show tool execution result (success/error) */
201
- addToolResult(toolCallId: string, isError: boolean): void {
202
- if (!this.currentResponse) return;
203
- if (isError) {
204
- this.currentResponse.addChild(new Text(chalk.red(" \u2717 failed"), 0, 0));
205
- this.tui.requestRender();
206
- }
207
- }
208
-
209
- /** Start a NEW text block with a separator (if not the first). */
210
- beginAnswer(): void {
211
- if (!this.currentResponse) return;
212
-
213
- // Close previous thinking
214
- this.currentThinking = null;
215
-
216
- // Add separator line before answer text
217
- this.currentResponse.addChild(new Spacer(1));
218
- this.currentResponse.addChild(new HRule());
219
- this.currentResponse.addChild(new Spacer(1));
220
- this.hadSeparator = true;
221
-
222
- // Create a new Markdown block for this text segment
223
- this.currentMd = new Markdown("", 1, 0, mdTheme);
224
- this.currentResponse.addChild(this.currentMd);
225
- this.tui.requestRender();
226
- }
227
-
228
- /** Append text to the active Markdown block (creates one if needed) */
229
- appendAnswer(text: string): void {
230
- if (!this.currentResponse) return;
231
-
232
- if (!this.currentMd) {
233
- // No active text block — create one (with separator if this is the first)
234
- if (!this.hadSeparator) {
235
- this.currentResponse.addChild(new Spacer(1));
236
- this.currentResponse.addChild(new HRule());
237
- this.currentResponse.addChild(new Spacer(1));
238
- this.hadSeparator = true;
239
- }
240
- this.currentMd = new Markdown("", 1, 0, mdTheme);
241
- this.currentResponse.addChild(this.currentMd);
242
- }
243
-
244
- const prev = (this.currentMd as any).text ?? "";
245
- this.currentMd.setText(prev + text);
246
- this.tui.requestRender();
247
- }
248
-
249
- showCompletion(): void {
250
- if (!this.currentResponse) return;
251
- const elapsed = ((Date.now() - this.startTime) / 1000).toFixed(1);
252
- const source = this.filesReadCount > 0
253
- ? `${this.filesReadCount} file${this.filesReadCount !== 1 ? "s" : ""} read`
254
- : "wiki";
255
- const stats = `\u2500\u2500 ${elapsed}s \u00b7 ${source} `;
256
-
257
- const completion: Component = {
258
- invalidate() {},
259
- render(width: number) {
260
- const pad = Math.max(0, width - stats.length);
261
- return [chalk.dim(stats + "\u2500".repeat(pad))];
262
- },
263
- };
264
-
265
- this.currentResponse.addChild(new Spacer(1));
266
- this.currentResponse.addChild(completion);
267
- this.currentResponse = null;
268
- this.currentMd = null;
269
- this.currentThinking = null;
270
- this.tui.requestRender();
271
- }
272
-
273
- enableInput(): void {
274
- this.tui.setFocus(this.input);
275
- this.tui.requestRender();
276
- }
277
-
278
- disableInput(): void {
279
- this.tui.setFocus(null);
280
- }
281
- }
package/src/utils.ts DELETED
@@ -1,17 +0,0 @@
1
- import { join, dirname } from "node:path";
2
- import { fileURLToPath } from "node:url";
3
-
4
- const __dirname = dirname(fileURLToPath(import.meta.url));
5
-
6
- /**
7
- * Find the node_modules directory for llm-kb's bundled libraries.
8
- * Walks up from the current file to locate the nearest node_modules.
9
- */
10
- export function getNodeModulesPath(): string {
11
- let dir = __dirname;
12
- for (let i = 0; i < 5; i++) {
13
- const candidate = join(dir, "node_modules");
14
- try { return candidate; } catch { dir = dirname(dir); }
15
- }
16
- return join(process.cwd(), "node_modules");
17
- }