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/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 { join, dirname } from "node:path";
12
- import { fileURLToPath } from "node:url";
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, CSV, images)
53
- You have bash and read tools. These libraries are pre-installed and available:
54
- - **exceljs** for .xlsx/.xls files
55
- - **mammoth** — for .docx files
56
- - **officeparser**for .pptx files
57
- - **csv-parse** built into Node.js, use fs + split for .csv
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
- Write a quick Node.js script to extract content when needed.
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: SessionManager.inMemory(),
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.
@@ -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
+ }