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.
- package/README.md +183 -42
- package/bin/anthropic-5TIU2EED.js +5515 -0
- package/bin/azure-openai-responses-ZVUVMK3G.js +190 -0
- package/bin/chunk-2WV6TQRI.js +4792 -0
- package/bin/chunk-3YMNGUZZ.js +262 -0
- package/bin/chunk-5PYKQQLA.js +14295 -0
- package/bin/chunk-65KFH7OI.js +31 -0
- package/bin/chunk-DHOXVEIR.js +7261 -0
- package/bin/chunk-EAQYK3U2.js +41 -0
- package/bin/chunk-IFS3OKBN.js +428 -0
- package/bin/chunk-LDHOKBJA.js +86 -0
- package/bin/chunk-SLYBG6ZQ.js +32681 -0
- package/bin/chunk-UEODFF7H.js +17 -0
- package/bin/chunk-XCXTZJGO.js +174 -0
- package/bin/chunk-XFV534WU.js +7056 -0
- package/bin/cli.js +30 -4
- package/bin/dist-3YH7P2QF.js +1244 -0
- package/bin/google-JFC43EFJ.js +371 -0
- package/bin/google-gemini-cli-K4XNMYDI.js +712 -0
- package/bin/google-vertex-Y42F254G.js +414 -0
- package/bin/indexer-KSYRIVVN.js +10 -0
- package/bin/mistral-ZU2JS5XZ.js +38406 -0
- package/bin/multipart-parser-CO464TZY.js +371 -0
- package/bin/openai-codex-responses-NW2LELBH.js +712 -0
- package/bin/openai-completions-TW3VKTHO.js +662 -0
- package/bin/openai-responses-VGL522MK.js +198 -0
- package/bin/src-Y22OHE3S.js +1408 -0
- package/package.json +6 -1
- package/PHASE2_SPEC.md +0 -274
- package/PHASE3_SPEC.md +0 -245
- package/PHASE4_SPEC.md +0 -358
- package/SPEC.md +0 -275
- package/plan.md +0 -300
- package/src/auth.ts +0 -55
- package/src/cli.ts +0 -257
- package/src/config.ts +0 -61
- package/src/eval.ts +0 -548
- package/src/indexer.ts +0 -152
- package/src/md-stream.ts +0 -133
- package/src/pdf.ts +0 -119
- package/src/query.ts +0 -408
- package/src/resolve-kb.ts +0 -19
- package/src/scan.ts +0 -59
- package/src/session-store.ts +0 -22
- package/src/session-watcher.ts +0 -89
- package/src/trace-builder.ts +0 -168
- package/src/tui-display.ts +0 -281
- package/src/utils.ts +0 -17
- package/src/watcher.ts +0 -87
- package/src/wiki-updater.ts +0 -136
- package/test/auth.test.ts +0 -65
- package/test/config.test.ts +0 -96
- package/test/md-stream.test.ts +0 -98
- package/test/resolve-kb.test.ts +0 -33
- package/test/scan.test.ts +0 -65
- package/test/trace-builder.test.ts +0 -215
- package/tsconfig.json +0 -14
- 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
|
-
}
|
package/src/wiki-updater.ts
DELETED
|
@@ -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
|
-
});
|
package/test/config.test.ts
DELETED
|
@@ -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
|
-
});
|
package/test/md-stream.test.ts
DELETED
|
@@ -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
|
-
});
|
package/test/resolve-kb.test.ts
DELETED
|
@@ -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
|
-
});
|