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.
@@ -0,0 +1,96 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from "vitest";
2
+ import { loadConfig, ensureConfig, DEFAULT_INDEX_MODEL, DEFAULT_QUERY_MODEL } from "../src/config.js";
3
+ import { mkdtemp, rm, readFile } from "node:fs/promises";
4
+ import { join } from "node:path";
5
+ import { tmpdir } from "node:os";
6
+ import { existsSync } from "node:fs";
7
+
8
+ describe("config", () => {
9
+ let tempDir: string;
10
+
11
+ beforeEach(async () => {
12
+ tempDir = await mkdtemp(join(tmpdir(), "llm-kb-test-"));
13
+ });
14
+
15
+ afterEach(async () => {
16
+ await rm(tempDir, { recursive: true, force: true });
17
+ });
18
+
19
+ describe("loadConfig", () => {
20
+ it("returns defaults when no config file exists", async () => {
21
+ const config = await loadConfig(tempDir);
22
+ expect(config.indexModel).toBe(DEFAULT_INDEX_MODEL);
23
+ expect(config.queryModel).toBe(DEFAULT_QUERY_MODEL);
24
+ });
25
+
26
+ it("reads config from .llm-kb/config.json", async () => {
27
+ const { mkdir, writeFile } = await import("node:fs/promises");
28
+ await mkdir(join(tempDir, ".llm-kb"), { recursive: true });
29
+ await writeFile(
30
+ join(tempDir, ".llm-kb", "config.json"),
31
+ JSON.stringify({ indexModel: "custom-haiku", queryModel: "custom-sonnet" })
32
+ );
33
+
34
+ const config = await loadConfig(tempDir);
35
+ expect(config.indexModel).toBe("custom-haiku");
36
+ expect(config.queryModel).toBe("custom-sonnet");
37
+ });
38
+
39
+ it("env vars override config file", async () => {
40
+ const { mkdir, writeFile } = await import("node:fs/promises");
41
+ await mkdir(join(tempDir, ".llm-kb"), { recursive: true });
42
+ await writeFile(
43
+ join(tempDir, ".llm-kb", "config.json"),
44
+ JSON.stringify({ indexModel: "from-file", queryModel: "from-file" })
45
+ );
46
+
47
+ process.env.LLM_KB_INDEX_MODEL = "from-env";
48
+ process.env.LLM_KB_QUERY_MODEL = "from-env";
49
+
50
+ try {
51
+ const config = await loadConfig(tempDir);
52
+ expect(config.indexModel).toBe("from-env");
53
+ expect(config.queryModel).toBe("from-env");
54
+ } finally {
55
+ delete process.env.LLM_KB_INDEX_MODEL;
56
+ delete process.env.LLM_KB_QUERY_MODEL;
57
+ }
58
+ });
59
+
60
+ it("handles malformed config gracefully", async () => {
61
+ const { mkdir, writeFile } = await import("node:fs/promises");
62
+ await mkdir(join(tempDir, ".llm-kb"), { recursive: true });
63
+ await writeFile(join(tempDir, ".llm-kb", "config.json"), "not json{{{");
64
+
65
+ const config = await loadConfig(tempDir);
66
+ expect(config.indexModel).toBe(DEFAULT_INDEX_MODEL);
67
+ expect(config.queryModel).toBe(DEFAULT_QUERY_MODEL);
68
+ });
69
+ });
70
+
71
+ describe("ensureConfig", () => {
72
+ it("creates config.json with defaults when none exists", async () => {
73
+ const config = await ensureConfig(tempDir);
74
+ expect(config.indexModel).toBe(DEFAULT_INDEX_MODEL);
75
+ expect(config.queryModel).toBe(DEFAULT_QUERY_MODEL);
76
+
77
+ const path = join(tempDir, ".llm-kb", "config.json");
78
+ expect(existsSync(path)).toBe(true);
79
+
80
+ const raw = JSON.parse(await readFile(path, "utf-8"));
81
+ expect(raw.indexModel).toBe(DEFAULT_INDEX_MODEL);
82
+ });
83
+
84
+ it("does not overwrite existing config", async () => {
85
+ const { mkdir, writeFile } = await import("node:fs/promises");
86
+ await mkdir(join(tempDir, ".llm-kb"), { recursive: true });
87
+ await writeFile(
88
+ join(tempDir, ".llm-kb", "config.json"),
89
+ JSON.stringify({ indexModel: "my-model", queryModel: "my-model" })
90
+ );
91
+
92
+ const config = await ensureConfig(tempDir);
93
+ expect(config.indexModel).toBe("my-model");
94
+ });
95
+ });
96
+ });
@@ -0,0 +1,98 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { MarkdownStream } from "../src/md-stream.js";
3
+
4
+ describe("MarkdownStream", () => {
5
+ describe("non-TTY (passthrough)", () => {
6
+ it("returns chunks unchanged", () => {
7
+ const md = new MarkdownStream(false);
8
+ expect(md.push("**bold**")).toBe("**bold**");
9
+ expect(md.push("# Header\n")).toBe("# Header\n");
10
+ });
11
+ });
12
+
13
+ describe("TTY mode", () => {
14
+ it("renders bold text", () => {
15
+ const md = new MarkdownStream(true);
16
+ const out = md.push("**hello**\n");
17
+ expect(out).not.toContain("**");
18
+ expect(out).toContain("hello");
19
+ });
20
+
21
+ it("renders italic text", () => {
22
+ const md = new MarkdownStream(true);
23
+ const out = md.push("*world*\n");
24
+ expect(out).not.toContain("*world*");
25
+ expect(out).toContain("world");
26
+ });
27
+
28
+ it("renders inline code", () => {
29
+ const md = new MarkdownStream(true);
30
+ const out = md.push("`code`\n");
31
+ expect(out).not.toContain("`");
32
+ expect(out).toContain("code");
33
+ });
34
+
35
+ it("renders headers without # symbols", () => {
36
+ const md = new MarkdownStream(true);
37
+ const out = md.push("## Section Title\n");
38
+ expect(out).not.toContain("##");
39
+ expect(out).toContain("Section Title");
40
+ });
41
+
42
+ it("renders bullet points with •", () => {
43
+ const md = new MarkdownStream(true);
44
+ const out = md.push("- item one\n");
45
+ expect(out).toContain("•");
46
+ expect(out).toContain("item one");
47
+ expect(out).not.toMatch(/^- /m);
48
+ });
49
+
50
+ it("renders horizontal rules as ─", () => {
51
+ const md = new MarkdownStream(true);
52
+ const out = md.push("---\n");
53
+ expect(out).toContain("─");
54
+ expect(out).not.toContain("---");
55
+ });
56
+
57
+ it("renders block quotes with │", () => {
58
+ const md = new MarkdownStream(true);
59
+ const out = md.push("> quoted text\n");
60
+ expect(out).toContain("│");
61
+ expect(out).toContain("quoted text");
62
+ });
63
+
64
+ it("handles incomplete patterns by buffering", () => {
65
+ const md = new MarkdownStream(true);
66
+ // Push "**bo" — bold is incomplete, should buffer
67
+ const out1 = md.push("**bo");
68
+ // Push "ld**\n" — completes the bold
69
+ const out2 = md.push("ld**\n");
70
+ const combined = out1 + out2;
71
+ expect(combined).toContain("bold");
72
+ expect(combined).not.toContain("**");
73
+ });
74
+
75
+ it("flushes remaining buffer on end()", () => {
76
+ const md = new MarkdownStream(true);
77
+ // Push incomplete bold — gets buffered
78
+ const out1 = md.push("**unfin");
79
+ expect(out1).toBe(""); // buffered, not yet flushed
80
+ const out2 = md.end();
81
+ expect(out2).toContain("unfin"); // force-flushed
82
+ });
83
+
84
+ it("renders table separator rows as dim", () => {
85
+ const md = new MarkdownStream(true);
86
+ const out = md.push("|---|---|\n");
87
+ expect(out).toContain("---|---");
88
+ });
89
+
90
+ it("renders links by showing label and dim URL", () => {
91
+ const md = new MarkdownStream(true);
92
+ const out = md.push("[Click here](https://example.com)\n");
93
+ expect(out).toContain("Click here");
94
+ expect(out).toContain("example.com");
95
+ expect(out).not.toContain("[Click here]");
96
+ });
97
+ });
98
+ });
@@ -0,0 +1,33 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from "vitest";
2
+ import { resolveKnowledgeBase } from "../src/resolve-kb.js";
3
+ import { mkdtemp, rm, mkdir } from "node:fs/promises";
4
+ import { join } from "node:path";
5
+ import { tmpdir } from "node:os";
6
+
7
+ describe("resolveKnowledgeBase", () => {
8
+ let tempDir: string;
9
+
10
+ beforeEach(async () => {
11
+ tempDir = await mkdtemp(join(tmpdir(), "llm-kb-test-"));
12
+ });
13
+
14
+ afterEach(async () => {
15
+ await rm(tempDir, { recursive: true, force: true });
16
+ });
17
+
18
+ it("returns null when no .llm-kb directory exists", () => {
19
+ expect(resolveKnowledgeBase(tempDir)).toBeNull();
20
+ });
21
+
22
+ it("finds .llm-kb in the current directory", async () => {
23
+ await mkdir(join(tempDir, ".llm-kb"), { recursive: true });
24
+ expect(resolveKnowledgeBase(tempDir)).toBe(tempDir);
25
+ });
26
+
27
+ it("walks up to find .llm-kb in parent directory", async () => {
28
+ const child = join(tempDir, "subdir", "deep");
29
+ await mkdir(child, { recursive: true });
30
+ await mkdir(join(tempDir, ".llm-kb"), { recursive: true });
31
+ expect(resolveKnowledgeBase(child)).toBe(tempDir);
32
+ });
33
+ });
@@ -0,0 +1,65 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from "vitest";
2
+ import { scan, summarize } from "../src/scan.js";
3
+ import { mkdtemp, rm, writeFile, mkdir } from "node:fs/promises";
4
+ import { join } from "node:path";
5
+ import { tmpdir } from "node:os";
6
+
7
+ describe("scan", () => {
8
+ let tempDir: string;
9
+
10
+ beforeEach(async () => {
11
+ tempDir = await mkdtemp(join(tmpdir(), "llm-kb-test-"));
12
+ });
13
+
14
+ afterEach(async () => {
15
+ await rm(tempDir, { recursive: true, force: true });
16
+ });
17
+
18
+ it("finds supported files", async () => {
19
+ await writeFile(join(tempDir, "doc.pdf"), "");
20
+ await writeFile(join(tempDir, "data.xlsx"), "");
21
+ await writeFile(join(tempDir, "notes.md"), "");
22
+ await writeFile(join(tempDir, "ignore.exe"), "");
23
+
24
+ const files = await scan(tempDir);
25
+ expect(files.length).toBe(3);
26
+ expect(files.map((f) => f.ext).sort()).toEqual([".md", ".pdf", ".xlsx"]);
27
+ });
28
+
29
+ it("skips .llm-kb internal directory", async () => {
30
+ await mkdir(join(tempDir, ".llm-kb"), { recursive: true });
31
+ await writeFile(join(tempDir, ".llm-kb", "index.md"), "");
32
+ await writeFile(join(tempDir, "real.pdf"), "");
33
+
34
+ const files = await scan(tempDir);
35
+ expect(files.length).toBe(1);
36
+ expect(files[0].name).toBe("real.pdf");
37
+ });
38
+
39
+ it("returns empty for empty directory", async () => {
40
+ const files = await scan(tempDir);
41
+ expect(files.length).toBe(0);
42
+ });
43
+
44
+ it("scans subdirectories", async () => {
45
+ await mkdir(join(tempDir, "sub"), { recursive: true });
46
+ await writeFile(join(tempDir, "sub", "nested.pdf"), "");
47
+
48
+ const files = await scan(tempDir);
49
+ expect(files.length).toBe(1);
50
+ expect(files[0].path).toContain("sub");
51
+ });
52
+ });
53
+
54
+ describe("summarize", () => {
55
+ it("summarizes file counts by extension", () => {
56
+ const files = [
57
+ { name: "a.pdf", path: "a.pdf", ext: ".pdf" },
58
+ { name: "b.pdf", path: "b.pdf", ext: ".pdf" },
59
+ { name: "c.xlsx", path: "c.xlsx", ext: ".xlsx" },
60
+ ];
61
+ const result = summarize(files);
62
+ expect(result).toContain("2 PDF");
63
+ expect(result).toContain("1 XLSX");
64
+ });
65
+ });
@@ -0,0 +1,215 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from "vitest";
2
+ import { buildTrace, saveTrace, appendToQueryLog, KBTrace } from "../src/trace-builder.js";
3
+ import { mkdtemp, rm, writeFile, readFile, mkdir } from "node:fs/promises";
4
+ import { join } from "node:path";
5
+ import { tmpdir } from "node:os";
6
+ import { existsSync } from "node:fs";
7
+
8
+ describe("trace-builder", () => {
9
+ let tempDir: string;
10
+ let sourcesDir: string;
11
+
12
+ beforeEach(async () => {
13
+ tempDir = await mkdtemp(join(tmpdir(), "llm-kb-test-"));
14
+ sourcesDir = join(tempDir, ".llm-kb", "wiki", "sources");
15
+ await mkdir(sourcesDir, { recursive: true });
16
+ // Create some source files
17
+ await writeFile(join(sourcesDir, "doc1.md"), "# Doc 1");
18
+ await writeFile(join(sourcesDir, "doc2.md"), "# Doc 2");
19
+ });
20
+
21
+ afterEach(async () => {
22
+ await rm(tempDir, { recursive: true, force: true });
23
+ });
24
+
25
+ function makeSessionJsonl(entries: {
26
+ header?: any;
27
+ modelChange?: any;
28
+ sessionInfo?: any;
29
+ messages?: any[];
30
+ }): string {
31
+ const lines: string[] = [];
32
+ const header = entries.header ?? {
33
+ type: "session", version: 3, id: "test-session-id",
34
+ timestamp: "2026-04-06T10:00:00Z", cwd: tempDir,
35
+ };
36
+ lines.push(JSON.stringify(header));
37
+
38
+ if (entries.modelChange) lines.push(JSON.stringify(entries.modelChange));
39
+ if (entries.sessionInfo) lines.push(JSON.stringify(entries.sessionInfo));
40
+ for (const msg of entries.messages ?? []) lines.push(JSON.stringify(msg));
41
+
42
+ return lines.join("\n");
43
+ }
44
+
45
+ describe("buildTrace", () => {
46
+ it("returns null for incomplete sessions (no stop message)", async () => {
47
+ const jsonl = makeSessionJsonl({
48
+ messages: [
49
+ {
50
+ type: "message", id: "m1", parentId: null, timestamp: "2026-04-06T10:00:01Z",
51
+ message: { role: "user", content: [{ type: "text", text: "Hello" }], timestamp: 1 },
52
+ },
53
+ {
54
+ type: "message", id: "m2", parentId: "m1", timestamp: "2026-04-06T10:00:02Z",
55
+ message: { role: "assistant", content: [{ type: "text", text: "Hi" }], stopReason: "toolUse", timestamp: 2 },
56
+ },
57
+ ],
58
+ });
59
+ const file = join(tempDir, "session.jsonl");
60
+ await writeFile(file, jsonl);
61
+ expect(await buildTrace(file, sourcesDir)).toBeNull();
62
+ });
63
+
64
+ it("parses a complete query session", async () => {
65
+ const jsonl = makeSessionJsonl({
66
+ modelChange: { type: "model_change", id: "mc1", parentId: null, timestamp: "2026-04-06T10:00:00Z", provider: "anthropic", modelId: "claude-sonnet-4-6" },
67
+ sessionInfo: { type: "session_info", id: "si1", parentId: "mc1", timestamp: "2026-04-06T10:00:00Z", name: "query: What is doc1?" },
68
+ messages: [
69
+ {
70
+ type: "message", id: "m1", parentId: "si1", timestamp: "2026-04-06T10:00:01Z",
71
+ message: { role: "user", content: [{ type: "text", text: "What is doc1?" }], timestamp: 1000 },
72
+ },
73
+ {
74
+ type: "message", id: "m2", parentId: "m1", timestamp: "2026-04-06T10:00:05Z",
75
+ message: {
76
+ role: "assistant",
77
+ content: [
78
+ { type: "toolCall", id: "tc1", name: "read", arguments: { path: ".llm-kb/wiki/sources/doc1.md" } },
79
+ { type: "text", text: "Doc 1 is about X." },
80
+ ],
81
+ model: "claude-sonnet-4-6", stopReason: "stop", timestamp: 5000,
82
+ },
83
+ },
84
+ ],
85
+ });
86
+
87
+ const file = join(tempDir, "session.jsonl");
88
+ await writeFile(file, jsonl);
89
+ const trace = await buildTrace(file, sourcesDir);
90
+
91
+ expect(trace).not.toBeNull();
92
+ expect(trace!.mode).toBe("query");
93
+ expect(trace!.question).toBe("What is doc1?");
94
+ expect(trace!.answer).toBe("Doc 1 is about X.");
95
+ expect(trace!.model).toBe("claude-sonnet-4-6");
96
+ expect(trace!.filesRead).toContain(".llm-kb/wiki/sources/doc1.md");
97
+ expect(trace!.filesAvailable).toContain("doc1.md");
98
+ expect(trace!.filesSkipped).toContain("doc2.md");
99
+ expect(trace!.durationMs).toBe(4000);
100
+ });
101
+
102
+ it("identifies index sessions by session name", async () => {
103
+ const jsonl = makeSessionJsonl({
104
+ sessionInfo: { type: "session_info", id: "si1", parentId: null, timestamp: "2026-04-06T10:00:00Z", name: "index: 2026-04-06" },
105
+ messages: [
106
+ {
107
+ type: "message", id: "m1", parentId: "si1", timestamp: "2026-04-06T10:00:01Z",
108
+ message: { role: "user", content: "Build the index", timestamp: 1 },
109
+ },
110
+ {
111
+ type: "message", id: "m2", parentId: "m1", timestamp: "2026-04-06T10:00:05Z",
112
+ message: { role: "assistant", content: [{ type: "text", text: "Done" }], stopReason: "stop", timestamp: 2 },
113
+ },
114
+ ],
115
+ });
116
+
117
+ const file = join(tempDir, "session.jsonl");
118
+ await writeFile(file, jsonl);
119
+ const trace = await buildTrace(file, sourcesDir);
120
+ expect(trace!.mode).toBe("index");
121
+ });
122
+
123
+ it("returns null for files with fewer than 2 lines", async () => {
124
+ const file = join(tempDir, "empty.jsonl");
125
+ await writeFile(file, '{"type":"session"}\n');
126
+ expect(await buildTrace(file, sourcesDir)).toBeNull();
127
+ });
128
+ });
129
+
130
+ describe("saveTrace", () => {
131
+ it("writes trace JSON to .llm-kb/traces/", async () => {
132
+ const trace: KBTrace = {
133
+ sessionId: "abc123",
134
+ sessionFile: "session.jsonl",
135
+ timestamp: "2026-04-06T10:00:00Z",
136
+ mode: "query",
137
+ question: "test?",
138
+ answer: "yes",
139
+ filesRead: [],
140
+ filesAvailable: [],
141
+ filesSkipped: [],
142
+ model: "test-model",
143
+ };
144
+
145
+ await saveTrace(tempDir, trace);
146
+ const path = join(tempDir, ".llm-kb", "traces", "abc123.json");
147
+ expect(existsSync(path)).toBe(true);
148
+
149
+ const saved = JSON.parse(await readFile(path, "utf-8"));
150
+ expect(saved.sessionId).toBe("abc123");
151
+ expect(saved.question).toBe("test?");
152
+ });
153
+ });
154
+
155
+ describe("appendToQueryLog", () => {
156
+ it("creates queries.md with header on first call", async () => {
157
+ const trace: KBTrace = {
158
+ sessionId: "abc",
159
+ sessionFile: "s.jsonl",
160
+ timestamp: "2026-04-06T10:00:00Z",
161
+ mode: "query",
162
+ question: "What is X?",
163
+ answer: "X is Y.",
164
+ filesRead: ["doc1.md"],
165
+ filesAvailable: ["doc1.md"],
166
+ filesSkipped: [],
167
+ model: "test",
168
+ };
169
+
170
+ await appendToQueryLog(tempDir, trace);
171
+ const logPath = join(tempDir, ".llm-kb", "wiki", "queries.md");
172
+ expect(existsSync(logPath)).toBe(true);
173
+
174
+ const content = await readFile(logPath, "utf-8");
175
+ expect(content).toContain("# Query Log");
176
+ expect(content).toContain("## What is X?");
177
+ expect(content).toContain("X is Y.");
178
+ });
179
+
180
+ it("skips non-query traces", async () => {
181
+ const trace: KBTrace = {
182
+ sessionId: "abc",
183
+ sessionFile: "s.jsonl",
184
+ timestamp: "2026-04-06T10:00:00Z",
185
+ mode: "index",
186
+ filesRead: [],
187
+ filesAvailable: [],
188
+ filesSkipped: [],
189
+ };
190
+
191
+ await appendToQueryLog(tempDir, trace);
192
+ const logPath = join(tempDir, ".llm-kb", "wiki", "queries.md");
193
+ expect(existsSync(logPath)).toBe(false);
194
+ });
195
+
196
+ it("prepends new entries to existing log", async () => {
197
+ const trace1: KBTrace = {
198
+ sessionId: "a", sessionFile: "s.jsonl", timestamp: "2026-04-06T10:00:00Z",
199
+ mode: "query", question: "First?", answer: "One", filesRead: [], filesAvailable: [], filesSkipped: [],
200
+ };
201
+ const trace2: KBTrace = {
202
+ sessionId: "b", sessionFile: "s.jsonl", timestamp: "2026-04-06T11:00:00Z",
203
+ mode: "query", question: "Second?", answer: "Two", filesRead: [], filesAvailable: [], filesSkipped: [],
204
+ };
205
+
206
+ await appendToQueryLog(tempDir, trace1);
207
+ await appendToQueryLog(tempDir, trace2);
208
+
209
+ const content = await readFile(join(tempDir, ".llm-kb", "wiki", "queries.md"), "utf-8");
210
+ const firstIdx = content.indexOf("Second?");
211
+ const secondIdx = content.indexOf("First?");
212
+ expect(firstIdx).toBeLessThan(secondIdx); // newest first
213
+ });
214
+ });
215
+ });
@@ -0,0 +1,8 @@
1
+ import { defineConfig } from "vitest/config";
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ include: ["test/**/*.test.ts"],
6
+ testTimeout: 10000,
7
+ },
8
+ });
@@ -1,118 +0,0 @@
1
- // src/indexer.ts
2
- import {
3
- createAgentSession,
4
- createBashTool,
5
- createReadTool,
6
- createWriteTool,
7
- DefaultResourceLoader,
8
- SessionManager,
9
- SettingsManager
10
- } from "@mariozechner/pi-coding-agent";
11
- import { readdir, readFile } from "fs/promises";
12
- import { join, dirname } from "path";
13
- import { fileURLToPath } from "url";
14
- var __filename = fileURLToPath(import.meta.url);
15
- var __dirname = dirname(__filename);
16
- function getNodeModulesPath() {
17
- let dir = __dirname;
18
- for (let i = 0; i < 5; i++) {
19
- const candidate = join(dir, "node_modules");
20
- try {
21
- return candidate;
22
- } catch {
23
- dir = dirname(dir);
24
- }
25
- }
26
- return join(process.cwd(), "node_modules");
27
- }
28
- function buildAgentsContent(sourcesDir, files) {
29
- const sourceList = files.filter((f) => f.endsWith(".md")).map((f) => ` - ${f}`).join("\n");
30
- return `# llm-kb Knowledge Base
31
-
32
- ## How to access documents
33
-
34
- ### PDFs (pre-parsed)
35
- PDFs have been parsed to markdown with bounding boxes.
36
- Read the markdown versions in \`.llm-kb/wiki/sources/\` instead of the raw PDFs.
37
-
38
- Available parsed sources:
39
- ${sourceList}
40
-
41
- ### Other file types (Excel, Word, PowerPoint, CSV, images)
42
- You have bash and read tools. These libraries are pre-installed and available:
43
- - **exceljs** \u2014 for .xlsx/.xls files
44
- - **mammoth** \u2014 for .docx files
45
- - **officeparser** \u2014 for .pptx files
46
- - **csv-parse** \u2014 built into Node.js, use fs + split for .csv
47
-
48
- Write a quick Node.js script to extract content when needed.
49
-
50
- ## Index file
51
- Write the index to \`.llm-kb/wiki/index.md\`.
52
-
53
- The index should be a markdown file with:
54
- 1. A title and last-updated timestamp
55
- 2. A summary table with columns: Source, Type, Pages/Size, Summary, Key Topics
56
- 3. Each source gets a one-line summary (read the first ~500 chars of each file to generate it)
57
- 4. Total word count across all sources
58
- `;
59
- }
60
- async function buildIndex(folder, sourcesDir, onOutput) {
61
- const files = await readdir(sourcesDir);
62
- const mdFiles = files.filter((f) => f.endsWith(".md"));
63
- if (mdFiles.length === 0) {
64
- throw new Error("No source files found to index");
65
- }
66
- const agentsContent = buildAgentsContent(sourcesDir, files);
67
- const nodeModulesPath = getNodeModulesPath();
68
- process.env.NODE_PATH = nodeModulesPath;
69
- const loader = new DefaultResourceLoader({
70
- cwd: folder,
71
- agentsFilesOverride: (current) => ({
72
- agentsFiles: [
73
- ...current.agentsFiles,
74
- { path: ".llm-kb/AGENTS.md", content: agentsContent }
75
- ]
76
- })
77
- });
78
- await loader.reload();
79
- const { session } = await createAgentSession({
80
- cwd: folder,
81
- resourceLoader: loader,
82
- tools: [
83
- createReadTool(folder),
84
- createBashTool(folder),
85
- createWriteTool(folder)
86
- ],
87
- sessionManager: SessionManager.inMemory(),
88
- settingsManager: SettingsManager.inMemory({
89
- compaction: { enabled: false }
90
- })
91
- });
92
- if (onOutput) {
93
- session.subscribe((event) => {
94
- if (event.type === "message_update" && event.assistantMessageEvent.type === "text_delta") {
95
- onOutput(event.assistantMessageEvent.delta);
96
- }
97
- });
98
- }
99
- const prompt = `Read each file in .llm-kb/wiki/sources/ (one at a time, just the first 500 characters of each).
100
- Then write .llm-kb/wiki/index.md with a summary table of all sources.
101
-
102
- Include: Source filename, Type (PDF/Excel/Word/etc), Pages (from the JSON if available), a one-line summary, and key topics.
103
- Add a total word count estimate at the bottom.`;
104
- await session.prompt(prompt);
105
- const indexPath = join(sourcesDir, "..", "index.md");
106
- try {
107
- const content = await readFile(indexPath, "utf-8");
108
- session.dispose();
109
- return content;
110
- } catch {
111
- session.dispose();
112
- throw new Error("Agent did not create index.md");
113
- }
114
- }
115
-
116
- export {
117
- buildIndex
118
- };
@@ -1,6 +0,0 @@
1
- import {
2
- buildIndex
3
- } from "./chunk-MYQ36JJB.js";
4
- export {
5
- buildIndex
6
- };