llm-kb 0.2.0 → 0.4.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/PHASE3_SPEC.md +245 -0
- package/PHASE4_SPEC.md +358 -0
- package/README.md +182 -61
- package/bin/cli.js +5473 -165
- package/package.json +11 -6
- package/plan.md +248 -3
- package/src/auth.ts +55 -0
- package/src/cli.ts +163 -38
- package/src/config.ts +61 -0
- package/src/eval.ts +548 -0
- package/src/indexer.ts +36 -32
- package/src/md-stream.ts +133 -0
- package/src/query.ts +408 -132
- package/src/session-store.ts +22 -0
- package/src/session-watcher.ts +89 -0
- package/src/trace-builder.ts +168 -0
- package/src/tui-display.ts +281 -0
- package/src/utils.ts +17 -0
- package/src/watcher.ts +5 -2
- package/src/wiki-updater.ts +136 -0
- package/test/auth.test.ts +65 -0
- package/test/config.test.ts +96 -0
- package/test/md-stream.test.ts +98 -0
- package/test/resolve-kb.test.ts +33 -0
- package/test/scan.test.ts +65 -0
- package/test/trace-builder.test.ts +215 -0
- package/vitest.config.ts +8 -0
- package/bin/chunk-MYQ36JJB.js +0 -118
- package/bin/indexer-LSYSZXZX.js +0 -6
package/src/indexer.ts
CHANGED
|
@@ -6,31 +6,13 @@ import {
|
|
|
6
6
|
DefaultResourceLoader,
|
|
7
7
|
SessionManager,
|
|
8
8
|
SettingsManager,
|
|
9
|
+
AuthStorage,
|
|
9
10
|
} from "@mariozechner/pi-coding-agent";
|
|
11
|
+
import { getModels } from "@mariozechner/pi-ai";
|
|
10
12
|
import { readdir, readFile } from "node:fs/promises";
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
|
|
14
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
15
|
-
const __dirname = dirname(__filename);
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Find the node_modules directory for llm-kb's bundled libraries.
|
|
19
|
-
* When running from bin/cli.js, node_modules is at ../node_modules.
|
|
20
|
-
*/
|
|
21
|
-
function getNodeModulesPath(): string {
|
|
22
|
-
// Walk up from this file to find node_modules
|
|
23
|
-
let dir = __dirname;
|
|
24
|
-
for (let i = 0; i < 5; i++) {
|
|
25
|
-
const candidate = join(dir, "node_modules");
|
|
26
|
-
try {
|
|
27
|
-
return candidate;
|
|
28
|
-
} catch {
|
|
29
|
-
dir = dirname(dir);
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
return join(process.cwd(), "node_modules");
|
|
33
|
-
}
|
|
13
|
+
import { createKBSession } from "./session-store.js";
|
|
14
|
+
import { getNodeModulesPath } from "./utils.js";
|
|
15
|
+
import { join } from "node:path";
|
|
34
16
|
|
|
35
17
|
function buildAgentsContent(sourcesDir: string, files: string[]): string {
|
|
36
18
|
const sourceList = files
|
|
@@ -49,14 +31,25 @@ Read the markdown versions in \`.llm-kb/wiki/sources/\` instead of the raw PDFs.
|
|
|
49
31
|
Available parsed sources:
|
|
50
32
|
${sourceList}
|
|
51
33
|
|
|
52
|
-
### Other file types (Excel, Word, PowerPoint
|
|
53
|
-
You have bash and read tools.
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
34
|
+
### Other file types (Excel, Word, PowerPoint)
|
|
35
|
+
You have bash and read tools. Use bash to run Node.js scripts.
|
|
36
|
+
Libraries are pre-installed via require().
|
|
37
|
+
|
|
38
|
+
For .docx (structured XML — ZIP containing word/document.xml):
|
|
39
|
+
const AdmZip = require('adm-zip');
|
|
40
|
+
const zip = new AdmZip('file.docx');
|
|
41
|
+
const xml = zip.readAsText('word/document.xml');
|
|
42
|
+
// Parse XML to extract headings and first paragraphs for summary
|
|
58
43
|
|
|
59
|
-
|
|
44
|
+
For .xlsx use exceljs:
|
|
45
|
+
const ExcelJS = require('exceljs');
|
|
46
|
+
const wb = new ExcelJS.Workbook();
|
|
47
|
+
await wb.xlsx.readFile('file.xlsx');
|
|
48
|
+
const sheet = wb.getWorksheet(1);
|
|
49
|
+
|
|
50
|
+
For .pptx use officeparser:
|
|
51
|
+
const officeparser = require('officeparser');
|
|
52
|
+
const text = await officeparser.parseOfficeAsync('file.pptx');
|
|
60
53
|
|
|
61
54
|
## Index file
|
|
62
55
|
Write the index to \`.llm-kb/wiki/index.md\`.
|
|
@@ -72,7 +65,9 @@ The index should be a markdown file with:
|
|
|
72
65
|
export async function buildIndex(
|
|
73
66
|
folder: string,
|
|
74
67
|
sourcesDir: string,
|
|
75
|
-
onOutput?: (text: string) => void
|
|
68
|
+
onOutput?: (text: string) => void,
|
|
69
|
+
authStorage?: AuthStorage,
|
|
70
|
+
modelId?: string
|
|
76
71
|
): Promise<string> {
|
|
77
72
|
// List source files
|
|
78
73
|
const files = await readdir(sourcesDir);
|
|
@@ -100,6 +95,10 @@ export async function buildIndex(
|
|
|
100
95
|
});
|
|
101
96
|
await loader.reload();
|
|
102
97
|
|
|
98
|
+
const model = modelId
|
|
99
|
+
? getModels("anthropic").find((m) => m.id === modelId)
|
|
100
|
+
: undefined;
|
|
101
|
+
|
|
103
102
|
const { session } = await createAgentSession({
|
|
104
103
|
cwd: folder,
|
|
105
104
|
resourceLoader: loader,
|
|
@@ -108,10 +107,12 @@ export async function buildIndex(
|
|
|
108
107
|
createBashTool(folder),
|
|
109
108
|
createWriteTool(folder),
|
|
110
109
|
],
|
|
111
|
-
sessionManager:
|
|
110
|
+
sessionManager: await createKBSession(folder),
|
|
112
111
|
settingsManager: SettingsManager.inMemory({
|
|
113
112
|
compaction: { enabled: false },
|
|
114
113
|
}),
|
|
114
|
+
...(authStorage ? { authStorage } : {}),
|
|
115
|
+
...(model ? { model } : {}),
|
|
115
116
|
});
|
|
116
117
|
|
|
117
118
|
// Subscribe to streaming output
|
|
@@ -126,6 +127,9 @@ export async function buildIndex(
|
|
|
126
127
|
});
|
|
127
128
|
}
|
|
128
129
|
|
|
130
|
+
// Tag the session so the session-watcher can identify it as an index run
|
|
131
|
+
session.setSessionName(`index: ${new Date().toISOString()}`);
|
|
132
|
+
|
|
129
133
|
// Build the prompt
|
|
130
134
|
const prompt = `Read each file in .llm-kb/wiki/sources/ (one at a time, just the first 500 characters of each).
|
|
131
135
|
Then write .llm-kb/wiki/index.md with a summary table of all sources.
|
package/src/md-stream.ts
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
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
|
+
}
|