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/watcher.ts DELETED
@@ -1,87 +0,0 @@
1
- import { watch } from "chokidar";
2
- import { extname, join, basename } from "node:path";
3
- import { parsePDF } from "./pdf.js";
4
- import { buildIndex } from "./indexer.js";
5
- import { AuthStorage } from "@mariozechner/pi-coding-agent";
6
- import chalk from "chalk";
7
-
8
- interface WatcherOptions {
9
- folder: string;
10
- sourcesDir: string;
11
- debounceMs?: number;
12
- authStorage?: AuthStorage;
13
- indexModel?: string;
14
- }
15
-
16
- export function startWatcher({ folder, sourcesDir, debounceMs = 2000, authStorage, indexModel }: WatcherOptions) {
17
- let pendingFiles: string[] = [];
18
- let debounceTimer: ReturnType<typeof setTimeout> | null = null;
19
-
20
- async function processBatch() {
21
- const files = [...pendingFiles];
22
- pendingFiles = [];
23
-
24
- if (files.length === 0) return;
25
-
26
- console.log();
27
- for (const filePath of files) {
28
- const name = basename(filePath);
29
- process.stdout.write(` Parsing ${name}...`);
30
- try {
31
- const result = await parsePDF(filePath, sourcesDir);
32
- if (result.skipped) {
33
- console.log(chalk.dim(` skipped (up to date)`));
34
- } else {
35
- console.log(chalk.green(` ✓ ${result.totalPages} pages`));
36
- }
37
- } catch (err: any) {
38
- console.log(chalk.red(` ✗ ${err.message}`));
39
- }
40
- }
41
-
42
- // Re-index
43
- process.stdout.write(` Re-indexing...`);
44
- try {
45
- await buildIndex(folder, sourcesDir, undefined, authStorage, indexModel);
46
- console.log(chalk.green(` ✓ index.md updated`));
47
- } catch (err: any) {
48
- console.log(chalk.red(` ✗ ${err.message}`));
49
- }
50
- }
51
-
52
- function queueFile(filePath: string) {
53
- if (!pendingFiles.includes(filePath)) {
54
- pendingFiles.push(filePath);
55
- }
56
- if (debounceTimer) clearTimeout(debounceTimer);
57
- debounceTimer = setTimeout(processBatch, debounceMs);
58
- }
59
-
60
- const watcher = watch(folder, {
61
- ignoreInitial: true,
62
- ignored: [
63
- "**/node_modules/**",
64
- "**/.llm-kb/**",
65
- "**/.git/**",
66
- ],
67
- depth: 10,
68
- });
69
-
70
- watcher.on("add", (filePath) => {
71
- const ext = extname(filePath).toLowerCase();
72
- if (ext === ".pdf") {
73
- console.log(chalk.dim(`\n New file: ${basename(filePath)}`));
74
- queueFile(filePath);
75
- }
76
- });
77
-
78
- watcher.on("change", (filePath) => {
79
- const ext = extname(filePath).toLowerCase();
80
- if (ext === ".pdf") {
81
- console.log(chalk.dim(`\n Changed: ${basename(filePath)}`));
82
- queueFile(filePath);
83
- }
84
- });
85
-
86
- return watcher;
87
- }
@@ -1,136 +0,0 @@
1
- import { getModels, completeSimple } from "@mariozechner/pi-ai";
2
- import { AuthStorage } from "@mariozechner/pi-coding-agent";
3
- import { existsSync } from "node:fs";
4
- import { readFile, writeFile, mkdir } from "node:fs/promises";
5
- import { join } from "node:path";
6
- import { homedir } from "node:os";
7
- import type { KBTrace } from "./trace-builder.js";
8
-
9
- async function resolveApiKey(authStorage?: AuthStorage): Promise<string | undefined> {
10
- if (authStorage) {
11
- return authStorage.getApiKey("anthropic");
12
- }
13
- const piAuthPath = join(homedir(), ".pi", "agent", "auth.json");
14
- if (existsSync(piAuthPath)) {
15
- const storage = AuthStorage.create(piAuthPath);
16
- return storage.getApiKey("anthropic");
17
- }
18
- return process.env.ANTHROPIC_API_KEY;
19
- }
20
-
21
- function buildPrompt(
22
- question: string,
23
- answer: string,
24
- sources: string,
25
- date: string,
26
- currentWiki: string
27
- ): string {
28
- const rules = `Rules for wiki structure:
29
- - Use ## for CONCEPTS and TOPICS — NOT source file names
30
- Good: "## Electronic Evidence", "## Mob Lynching", "## Burden of Proof"
31
- Bad: "## Indian Evidence Act.md", "## indian penal code - new.md"
32
- - Use ### for subtopics within a concept
33
- - A concept can draw from MULTIPLE source files — synthesize, don't separate by file
34
- - If knowledge from this Q&A fits an existing concept, ADD to it — never duplicate
35
- - If it's a genuinely new concept, create a new ## section
36
- - Be concise: bullet points for lists, short prose for explanations
37
- - Include source citations inline: (Source: filename, p.X)
38
- - Add cross-references where concepts relate: See also: [[Other Concept]]
39
- - End each ## section with: *Sources: file1, file2 · date*
40
- - Separate ## sections with: ---`;
41
-
42
- if (currentWiki.trim()) {
43
- return `You are maintaining a concept-organized knowledge wiki.
44
-
45
- ## Current wiki
46
- ${currentWiki}
47
-
48
- ## New Q&A to integrate
49
- **Question:** ${question}
50
- **Sources used:** ${sources}
51
- **Date:** ${date}
52
-
53
- **Answer:**
54
- ${answer}
55
-
56
- ---
57
-
58
- Update the wiki to integrate this new knowledge.
59
- ${rules}
60
-
61
- Return ONLY the complete updated wiki markdown. No explanation.`;
62
- }
63
-
64
- return `You are creating a concept-organized knowledge wiki.
65
-
66
- ## First Q&A to add
67
- **Question:** ${question}
68
- **Sources used:** ${sources}
69
- **Date:** ${date}
70
-
71
- **Answer:**
72
- ${answer}
73
-
74
- ---
75
-
76
- Create a clean wiki from this Q&A.
77
- - Start with: # Knowledge Wiki\\n\\n> Concept-organized knowledge base. Updated after each query.\\n\\n---
78
- ${rules}
79
-
80
- Return ONLY the wiki markdown. No explanation.`;
81
- }
82
-
83
- /**
84
- * Update .llm-kb/wiki/wiki.md using a direct Haiku call.
85
- * Organizes knowledge by CONCEPT (cross-cutting topics),
86
- * not by source file.
87
- */
88
- export async function updateWiki(
89
- kbRoot: string,
90
- trace: KBTrace,
91
- authStorage?: AuthStorage,
92
- indexModelId = "claude-haiku-4-5"
93
- ): Promise<void> {
94
- if (trace.mode !== "query" || !trace.question || !trace.answer) return;
95
-
96
- const wikiDir = join(kbRoot, ".llm-kb", "wiki");
97
- await mkdir(wikiDir, { recursive: true });
98
- const wikiPath = join(wikiDir, "wiki.md");
99
-
100
- const currentWiki = existsSync(wikiPath)
101
- ? await readFile(wikiPath, "utf-8").catch(() => "")
102
- : "";
103
-
104
- const sources = trace.filesRead
105
- .map((f) => f.split(/[\\/]/).pop() ?? f)
106
- .filter((f) => f.endsWith(".md") && f !== "index.md" && f !== "wiki.md")
107
- .join(", ") || "unknown";
108
-
109
- const date = new Date(trace.timestamp).toISOString().slice(0, 10);
110
- const prompt = buildPrompt(trace.question, trace.answer, sources, date, currentWiki);
111
-
112
- const apiKey = await resolveApiKey(authStorage);
113
- if (!apiKey) return;
114
-
115
- const model = getModels("anthropic").find((m) => m.id === indexModelId);
116
- if (!model) return;
117
-
118
- const result = await completeSimple(
119
- model,
120
- {
121
- 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.",
122
- messages: [{ role: "user", content: prompt, timestamp: Date.now() }],
123
- },
124
- { apiKey }
125
- );
126
-
127
- const text = result.content
128
- .filter((b) => b.type === "text")
129
- .map((b) => (b as any).text)
130
- .join("")
131
- .trim();
132
-
133
- if (text) {
134
- await writeFile(wikiPath, text + "\n", "utf-8");
135
- }
136
- }
package/test/auth.test.ts DELETED
@@ -1,65 +0,0 @@
1
- import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
2
-
3
- // Mock existsSync before importing auth module
4
- vi.mock("node:fs", async () => {
5
- const actual = await vi.importActual<typeof import("node:fs")>("node:fs");
6
- return { ...actual, existsSync: vi.fn() };
7
- });
8
-
9
- import { checkAuth } from "../src/auth.js";
10
- import { existsSync } from "node:fs";
11
-
12
- const mockExistsSync = vi.mocked(existsSync);
13
-
14
- describe("checkAuth", () => {
15
- const origEnv = process.env.ANTHROPIC_API_KEY;
16
-
17
- afterEach(() => {
18
- if (origEnv !== undefined) process.env.ANTHROPIC_API_KEY = origEnv;
19
- else delete process.env.ANTHROPIC_API_KEY;
20
- vi.restoreAllMocks();
21
- });
22
-
23
- it("returns pi-sdk when auth.json exists", () => {
24
- mockExistsSync.mockReturnValue(true);
25
- delete process.env.ANTHROPIC_API_KEY;
26
-
27
- const result = checkAuth();
28
- expect(result.ok).toBe(true);
29
- if (result.ok) {
30
- expect(result.method).toBe("pi-sdk");
31
- expect(result.authStorage).toBeUndefined(); // uses default
32
- }
33
- });
34
-
35
- it("returns api-key when ANTHROPIC_API_KEY is set", () => {
36
- mockExistsSync.mockReturnValue(false);
37
- process.env.ANTHROPIC_API_KEY = "sk-ant-test";
38
-
39
- const result = checkAuth();
40
- expect(result.ok).toBe(true);
41
- if (result.ok) {
42
- expect(result.method).toBe("api-key");
43
- expect(result.authStorage).toBeDefined();
44
- }
45
- });
46
-
47
- it("returns failure when neither is available", () => {
48
- mockExistsSync.mockReturnValue(false);
49
- delete process.env.ANTHROPIC_API_KEY;
50
-
51
- const result = checkAuth();
52
- expect(result.ok).toBe(false);
53
- });
54
-
55
- it("prefers pi-sdk over api-key when both exist", () => {
56
- mockExistsSync.mockReturnValue(true);
57
- process.env.ANTHROPIC_API_KEY = "sk-ant-test";
58
-
59
- const result = checkAuth();
60
- expect(result.ok).toBe(true);
61
- if (result.ok) {
62
- expect(result.method).toBe("pi-sdk");
63
- }
64
- });
65
- });
@@ -1,96 +0,0 @@
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
- });
@@ -1,98 +0,0 @@
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
- });
@@ -1,33 +0,0 @@
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
- });
package/test/scan.test.ts DELETED
@@ -1,65 +0,0 @@
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
- });