@ganziliang/kb 0.1.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/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,173 @@
1
+ #!/usr/bin/env node
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useEffect, useRef, useState } from "react";
4
+ import { Box, Text, render, useApp, useInput } from "ink";
5
+ import { mkdirSync } from "node:fs";
6
+ import { ensureModelConfig, saveModels } from "./config.js";
7
+ import { Agent, fetchTransport } from "./model.js";
8
+ import { copyOriginal, defaultDataRoot, ensureKnowledgeBase, KnowledgeStore, readLocalFile, listKnowledgeBases, deleteKnowledgeBase, backupKnowledgeBase, restoreKnowledgeBase } from "./storage.js";
9
+ function App({ config, root, knowledgeBase }) {
10
+ const { exit } = useApp();
11
+ const [input, setInput] = useState("");
12
+ const [lines, setLines] = useState(["Local Knowledge Agent", `knowledge base: ${knowledgeBase.name}`, "Type a question, or /help."]);
13
+ const [busy, setBusy] = useState(false);
14
+ const [messages, setMessages] = useState([]);
15
+ const [modelIndex, setModelIndex] = useState(0);
16
+ const [currentBase, setCurrentBase] = useState(knowledgeBase);
17
+ const storeRef = useRef(new KnowledgeStore(knowledgeBase.path));
18
+ const store = storeRef.current;
19
+ useEffect(() => () => storeRef.current.close(), []);
20
+ useInput((char, key) => {
21
+ if (key.ctrl && char === "c") {
22
+ store.close();
23
+ exit();
24
+ return;
25
+ }
26
+ if (key.return && input.trim() && !busy) {
27
+ void submit(input.trim());
28
+ setInput("");
29
+ return;
30
+ }
31
+ if (key.backspace || key.delete) {
32
+ setInput((value) => value.slice(0, -1));
33
+ return;
34
+ }
35
+ if (!key.ctrl && !key.meta && char)
36
+ setInput((value) => value + char);
37
+ });
38
+ async function submit(value) {
39
+ if (value === "/quit" || value === "/exit") {
40
+ store.close();
41
+ exit();
42
+ return;
43
+ }
44
+ if (value === "/clear") {
45
+ setLines(["Conversation cleared."]);
46
+ setMessages([]);
47
+ return;
48
+ }
49
+ if (value === "/help") {
50
+ setLines((old) => [...old, "/help /clear /sources /backup /restore /cleanup /model /models /kb /quit"]);
51
+ return;
52
+ }
53
+ if (value === "/sources") {
54
+ setLines((old) => [...old, ...store.sources().map((s) => `${s.name} | ${s.path} | v${s.version} | ${s.updatedAt}`)]);
55
+ return;
56
+ }
57
+ if (value === "/model" || value === "/models") {
58
+ setLines((old) => [...old, ...config.map((item, i) => `${i === modelIndex ? "*" : " "} ${i}: ${item.provider}/${item.model} (${item.api})`)]);
59
+ return;
60
+ }
61
+ if (value.startsWith("/model ")) {
62
+ const index = Number(value.slice(7));
63
+ if (config[index]) {
64
+ setModelIndex(index);
65
+ saveModels([config[index], ...config.filter((_, i) => i !== index)]);
66
+ setLines((old) => [...old, `Using ${config[index].model}`]);
67
+ }
68
+ else
69
+ setLines((old) => [...old, "Unknown model index."]);
70
+ return;
71
+ }
72
+ if (value.startsWith("/kb ")) {
73
+ const [command, ...args] = value.slice(4).trim().split(/\s+/);
74
+ if (command === "list") {
75
+ setLines((old) => [...old, ...listKnowledgeBases(root).map((item) => `${item.id}${item.id === currentBase.id ? " *" : ""}`)]);
76
+ return;
77
+ }
78
+ if (command === "current") {
79
+ setLines((old) => [...old, `${currentBase.name} (${currentBase.id})`]);
80
+ return;
81
+ }
82
+ if (command === "create" && args.length) {
83
+ const created = ensureKnowledgeBase(root, args.join(" "));
84
+ setLines((old) => [...old, `Created ${created.name} (${created.id})`]);
85
+ return;
86
+ }
87
+ if (command === "use" && args[0]) {
88
+ const selected = listKnowledgeBases(root).find((item) => item.id === args[0] || item.name === args.join(" "));
89
+ if (!selected) {
90
+ setLines((old) => [...old, "Knowledge base not found."]);
91
+ return;
92
+ }
93
+ store.close();
94
+ storeRef.current = new KnowledgeStore(selected.path);
95
+ setCurrentBase(selected);
96
+ setMessages([]);
97
+ setLines((old) => [...old, `Using ${selected.name}`]);
98
+ return;
99
+ }
100
+ if (command === "delete" && args[0] && args[1] === "confirm") {
101
+ deleteKnowledgeBase(root, args[0]);
102
+ setLines((old) => [...old, `Deleted ${args[0]}; backups were retained.`]);
103
+ return;
104
+ }
105
+ setLines((old) => [...old, "Usage: /kb list | /kb create <name> | /kb use <id> | /kb current | /kb delete <id> confirm"]);
106
+ return;
107
+ }
108
+ if (value === "/backup" || value.startsWith("/backup ")) {
109
+ setLines((old) => [...old, `Backup created at ${backupKnowledgeBase(currentBase, value.slice(7).trim() || undefined)}`]);
110
+ return;
111
+ }
112
+ if (value.startsWith("/restore ")) {
113
+ restoreKnowledgeBase(currentBase, value.slice(9).trim());
114
+ setLines((old) => [...old, "Knowledge base restored. Restart the session to reopen the database."]);
115
+ return;
116
+ }
117
+ if (value === "/cleanup") {
118
+ const oldVersions = store.oldVersions();
119
+ setLines((old) => [...old, oldVersions.length ? `Old versions (delete with /cleanup <id> confirm): ${oldVersions.map((item) => `${item.id} ${item.title} v${item.version}`).join("; ")}` : "No old versions."]);
120
+ return;
121
+ }
122
+ if (value.startsWith("/cleanup ")) {
123
+ const [id, confirmation] = value.slice(9).trim().split(/\s+/);
124
+ if (confirmation === "confirm") {
125
+ store.removeVersions([id]);
126
+ setLines((old) => [...old, `Deleted old version ${id}.`]);
127
+ }
128
+ else
129
+ setLines((old) => [...old, "Usage: /cleanup <version-id> confirm"]);
130
+ return;
131
+ }
132
+ setBusy(true);
133
+ setLines((old) => [...old, `> ${value}`]);
134
+ try {
135
+ const result = await ingestOrAsk(value, store, config[modelIndex], messages);
136
+ setMessages(result.messages);
137
+ store.saveMessage("user", value);
138
+ store.saveMessage("assistant", result.text);
139
+ setLines((old) => [...old, result.text]);
140
+ }
141
+ catch (error) {
142
+ setLines((old) => [...old, `Error: ${error instanceof Error ? error.message : String(error)}`]);
143
+ }
144
+ finally {
145
+ setBusy(false);
146
+ }
147
+ }
148
+ return _jsxs(Box, { flexDirection: "column", padding: 1, children: [lines.slice(-18).map((line, i) => _jsx(Text, { children: line }, `${i}-${line}`)), _jsxs(Text, { color: busy ? "yellow" : "cyan", children: [busy ? "Searching..." : "> ", input] })] });
149
+ }
150
+ async function ingestOrAsk(value, store, modelConfig, previous) {
151
+ const pathMatch = value.match(/(?:录入|导入|整理|ingest|import)\s+(.+)$/i);
152
+ if (pathMatch) {
153
+ const source = readLocalFile(pathMatch[1].trim().replace(/^['"]|['"]$/g, ""));
154
+ if (!source.hash)
155
+ return { text: `${source.title}: unable to parse (${source.content})`, messages: previous };
156
+ const copied = copyOriginal(source.originalPath, store.root);
157
+ const result = store.addOrUpdateSource(copied, source.title, source.content);
158
+ return { text: `${result.isNew ? "Imported" : "New version created for"} ${result.title} (v${result.version})`, messages: previous };
159
+ }
160
+ const results = store.search(value);
161
+ const context = results.length ? results.map((item) => `[${item.id}] ${item.title} (source: ${item.source}, v${item.version})\n${item.content}`).join("\n\n") : "No matching knowledge was found.";
162
+ const agent = new Agent(store, modelConfig, fetchTransport);
163
+ return agent.answer(`Answer only from the local knowledge below. If it is insufficient, say no knowledge was found. Include source filename and version.\n\n${context}\n\nQuestion: ${value}`, previous);
164
+ }
165
+ async function main() {
166
+ const root = defaultDataRoot();
167
+ mkdirSync(root, { recursive: true });
168
+ const knowledgeRoot = root;
169
+ const base = ensureKnowledgeBase(root, "default");
170
+ const configs = await ensureModelConfig();
171
+ render(_jsx(App, { config: configs, root: knowledgeRoot, knowledgeBase: base }));
172
+ }
173
+ void main();
@@ -0,0 +1,5 @@
1
+ import { type ModelConfig } from "@ganziliang/kb-model-setup";
2
+ export declare function configPath(): string;
3
+ export declare function loadModels(): ModelConfig[];
4
+ export declare function saveModels(models: ModelConfig[]): void;
5
+ export declare function ensureModelConfig(): Promise<ModelConfig[]>;
package/dist/config.js ADDED
@@ -0,0 +1,46 @@
1
+ import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import * as readline from "node:readline/promises";
4
+ import { configFromEnvironment, createModelSetupExtension } from "@ganziliang/kb-model-setup";
5
+ export function configPath() {
6
+ const home = process.env.HOME ?? process.env.USERPROFILE ?? process.cwd();
7
+ return process.env.KB_CONFIG_FILE ?? join(home, ".config", "kb", "config.json");
8
+ }
9
+ export function loadModels() {
10
+ try {
11
+ return JSON.parse(readFileSync(configPath(), "utf8")).models ?? [];
12
+ }
13
+ catch {
14
+ return [];
15
+ }
16
+ }
17
+ export function saveModels(models) {
18
+ mkdirSync(dirname(configPath()), { recursive: true });
19
+ writeFileSync(configPath(), JSON.stringify({ models, activeModel: models[0]?.model }, null, 2));
20
+ }
21
+ function readlinePrompter() {
22
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
23
+ return {
24
+ close: () => rl.close(),
25
+ ask: (question, initial) => rl.question(`${question}${initial ? ` [${initial}]` : ""}: `).then((value) => value || initial || ""),
26
+ select: async (question, choices, initial = 0) => { console.log(choices.map((choice, i) => `${i === initial ? "*" : " "} ${i}: ${choice}`).join("\n")); const value = await rl.question(`${question} [${initial}]: `); return Number.isFinite(Number(value)) ? Number(value) : initial; },
27
+ confirm: (question) => rl.question(`${question} [y/N]: `).then((value) => /^y(es)?$/i.test(value)),
28
+ notice: async (message) => { console.log(message); },
29
+ };
30
+ }
31
+ export async function ensureModelConfig() {
32
+ const envConfig = configFromEnvironment();
33
+ if (envConfig) {
34
+ saveModels([envConfig]);
35
+ return [envConfig];
36
+ }
37
+ const models = loadModels();
38
+ if (models.length)
39
+ return models;
40
+ const extension = createModelSetupExtension();
41
+ const prompter = readlinePrompter();
42
+ const config = await extension.configure(prompter);
43
+ prompter.close?.();
44
+ saveModels([config]);
45
+ return [config];
46
+ }
@@ -0,0 +1,2 @@
1
+ export * from "./storage.js";
2
+ export * from "./model.js";
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export * from "./storage.js";
2
+ export * from "./model.js";
@@ -0,0 +1,31 @@
1
+ import type { ModelConfig } from "@ganziliang/kb-model-setup";
2
+ import type { SearchResult, KnowledgeStore } from "./storage.js";
3
+ export type ToolCall = {
4
+ name: "search" | "read" | "write";
5
+ arguments: Record<string, unknown>;
6
+ };
7
+ export type ModelMessage = {
8
+ role: "system" | "user" | "assistant" | "tool";
9
+ content: string;
10
+ toolCallId?: string;
11
+ };
12
+ export type ModelReply = {
13
+ text?: string;
14
+ toolCalls?: ToolCall[];
15
+ };
16
+ export type ModelTransport = {
17
+ complete(config: ModelConfig, messages: ModelMessage[]): Promise<ModelReply>;
18
+ };
19
+ export declare const fetchTransport: ModelTransport;
20
+ export declare class Agent {
21
+ private readonly store;
22
+ private readonly config;
23
+ private readonly transport;
24
+ constructor(store: KnowledgeStore, config: ModelConfig, transport?: ModelTransport);
25
+ answer(question: string, previous?: ModelMessage[]): Promise<{
26
+ text: string;
27
+ messages: ModelMessage[];
28
+ }>;
29
+ private execute;
30
+ }
31
+ export declare function formatResults(results: SearchResult[]): string;
package/dist/model.js ADDED
@@ -0,0 +1,59 @@
1
+ function endpoint(config) {
2
+ return `${config.baseURL.replace(/\/$/, "")}${config.api === "anthropic-messages" ? "/v1/messages" : "/v1/responses"}`;
3
+ }
4
+ export const fetchTransport = {
5
+ async complete(config, messages) {
6
+ const headers = { "content-type": "application/json" };
7
+ if (config.api === "anthropic-messages") {
8
+ headers["x-api-key"] = config.apiKey;
9
+ headers["anthropic-version"] = "2023-06-01";
10
+ }
11
+ else
12
+ headers.authorization = `Bearer ${config.apiKey}`;
13
+ const response = await fetch(endpoint(config), {
14
+ method: "POST",
15
+ headers,
16
+ body: JSON.stringify(config.api === "anthropic-messages"
17
+ ? { model: config.model, max_tokens: 4096, messages: messages.filter((m) => m.role !== "system"), system: messages.find((m) => m.role === "system")?.content }
18
+ : { model: config.model, input: messages.map((m) => ({ role: m.role, content: m.content })) }),
19
+ });
20
+ if (!response.ok)
21
+ throw new Error(`Model request failed (${response.status}): ${await response.text()}`);
22
+ const data = await response.json();
23
+ if (config.api === "anthropic-messages")
24
+ return { text: data.content?.map((item) => item.text ?? "").join("") };
25
+ return { text: data.output_text ?? data.output?.map((item) => item.content?.map((part) => part.text ?? "").join("")).join("") ?? "" };
26
+ },
27
+ };
28
+ export class Agent {
29
+ store;
30
+ config;
31
+ transport;
32
+ constructor(store, config, transport = fetchTransport) {
33
+ this.store = store;
34
+ this.config = config;
35
+ this.transport = transport;
36
+ }
37
+ async answer(question, previous = []) {
38
+ const messages = [...previous, { role: "user", content: question }];
39
+ const first = await this.transport.complete(this.config, messages);
40
+ if (!first.toolCalls?.length)
41
+ return { text: first.text ?? "", messages: [...messages, { role: "assistant", content: first.text ?? "" }] };
42
+ for (const call of first.toolCalls) {
43
+ const result = this.execute(call);
44
+ messages.push({ role: "tool", content: JSON.stringify(result), toolCallId: call.name });
45
+ }
46
+ const final = await this.transport.complete(this.config, messages);
47
+ return { text: final.text ?? "", messages: [...messages, { role: "assistant", content: final.text ?? "" }] };
48
+ }
49
+ execute(call) {
50
+ if (call.name === "search")
51
+ return this.store.search(String(call.arguments.query ?? ""));
52
+ if (call.name === "read")
53
+ return this.store.read(String(call.arguments.id ?? "")) ?? { error: "Knowledge record not found" };
54
+ return { error: "write requires CLI ingestion flow" };
55
+ }
56
+ }
57
+ export function formatResults(results) {
58
+ return results.map((result) => `[${result.id}] ${result.title} (source: ${result.source}, v${result.version})\n${result.content}`).join("\n\n");
59
+ }
@@ -0,0 +1,54 @@
1
+ import { DatabaseSync } from "node:sqlite";
2
+ export type KnowledgeBase = {
3
+ id: string;
4
+ name: string;
5
+ path: string;
6
+ };
7
+ export type SearchResult = {
8
+ id: string;
9
+ title: string;
10
+ content: string;
11
+ source: string;
12
+ version: number;
13
+ updatedAt: string;
14
+ };
15
+ export declare function tokenize(text: string): string;
16
+ export declare class KnowledgeStore {
17
+ readonly root: string;
18
+ readonly db: DatabaseSync;
19
+ constructor(root: string);
20
+ close(): void;
21
+ addOrUpdateSource(filePath: string, title: string, content: string): KnowledgeStoreResult;
22
+ search(query: string, limit?: number): SearchResult[];
23
+ read(id: string): SearchResult | undefined;
24
+ sources(): Array<Record<string, unknown>>;
25
+ oldVersions(): Array<Record<string, unknown>>;
26
+ removeVersions(ids: string[]): void;
27
+ saveMessage(role: string, content: string): void;
28
+ listMessages(): Array<{
29
+ role: string;
30
+ content: string;
31
+ }>;
32
+ }
33
+ type KnowledgeStoreResult = {
34
+ id: string;
35
+ sourceId: string;
36
+ version: number;
37
+ title: string;
38
+ isNew: boolean;
39
+ };
40
+ export declare function readLocalFile(filePath: string): {
41
+ title: string;
42
+ content: string;
43
+ originalPath: string;
44
+ hash: string;
45
+ };
46
+ export declare function copyOriginal(filePath: string, root: string): string;
47
+ export declare function defaultDataRoot(): string;
48
+ export declare function knowledgeBasePath(root: string, id: string): string;
49
+ export declare function listKnowledgeBases(root: string): KnowledgeBase[];
50
+ export declare function deleteKnowledgeBase(root: string, id: string): void;
51
+ export declare function backupKnowledgeBase(base: KnowledgeBase, destination?: string): string;
52
+ export declare function restoreKnowledgeBase(base: KnowledgeBase, backupPath: string): void;
53
+ export declare function ensureKnowledgeBase(root: string, name?: string): KnowledgeBase;
54
+ export {};
@@ -0,0 +1,130 @@
1
+ import { mkdirSync, copyFileSync, existsSync, readFileSync, statSync, cpSync, rmSync, readdirSync } from "node:fs";
2
+ import { createHash } from "node:crypto";
3
+ import { join, resolve, extname, basename } from "node:path";
4
+ import { DatabaseSync } from "node:sqlite";
5
+ import * as XLSX from "xlsx";
6
+ export function tokenize(text) {
7
+ const segmenter = new Intl.Segmenter("zh", { granularity: "word" });
8
+ const words = [...segmenter.segment(text)]
9
+ .map(({ segment }) => segment.trim())
10
+ .filter((segment) => /[\p{L}\p{N}_]/u.test(segment));
11
+ return words.length ? words.join(" ") : text.split("").join(" ");
12
+ }
13
+ export class KnowledgeStore {
14
+ root;
15
+ db;
16
+ constructor(root) {
17
+ this.root = root;
18
+ mkdirSync(root, { recursive: true });
19
+ mkdirSync(join(root, "originals"), { recursive: true });
20
+ mkdirSync(join(root, "backups"), { recursive: true });
21
+ this.db = new DatabaseSync(join(root, "knowledge.db"));
22
+ this.db.exec("PRAGMA journal_mode = WAL;");
23
+ this.db.exec(`
24
+ CREATE TABLE IF NOT EXISTS sources (id TEXT PRIMARY KEY, name TEXT NOT NULL, original_path TEXT NOT NULL, hash TEXT NOT NULL, created_at TEXT NOT NULL);
25
+ CREATE TABLE IF NOT EXISTS knowledge (id TEXT PRIMARY KEY, source_id TEXT NOT NULL, version INTEGER NOT NULL, title TEXT NOT NULL, content TEXT NOT NULL, tokenized_content TEXT NOT NULL, updated_at TEXT NOT NULL, current INTEGER NOT NULL DEFAULT 1, FOREIGN KEY(source_id) REFERENCES sources(id));
26
+ CREATE VIRTUAL TABLE IF NOT EXISTS knowledge_fts USING fts5(knowledge_id UNINDEXED, title, tokenized_content);
27
+ CREATE TABLE IF NOT EXISTS conversations (id TEXT PRIMARY KEY, role TEXT NOT NULL, content TEXT NOT NULL, created_at TEXT NOT NULL);
28
+ `);
29
+ }
30
+ close() { this.db.close(); }
31
+ addOrUpdateSource(filePath, title, content) {
32
+ const absolute = resolve(filePath);
33
+ const hash = createHash("sha256").update(content).digest("hex");
34
+ const now = new Date().toISOString();
35
+ const existing = this.db.prepare("SELECT id FROM sources WHERE original_path = ? OR hash = ? LIMIT 1").get(absolute, hash);
36
+ const sourceId = existing?.id ?? crypto.randomUUID();
37
+ if (!existing)
38
+ this.db.prepare("INSERT INTO sources (id,name,original_path,hash,created_at) VALUES (?,?,?,?,?)").run(sourceId, title, absolute, hash, now);
39
+ const latest = this.db.prepare("SELECT COALESCE(MAX(version), 0) AS version FROM knowledge WHERE source_id = ?").get(sourceId);
40
+ const version = Number(latest.version) + 1;
41
+ this.db.prepare("UPDATE knowledge SET current = 0 WHERE source_id = ?").run(sourceId);
42
+ const id = crypto.randomUUID();
43
+ this.db.prepare("INSERT INTO knowledge VALUES (?,?,?,?,?,?,?,1)").run(id, sourceId, version, title, content, tokenize(content), now);
44
+ this.db.prepare("INSERT INTO knowledge_fts VALUES (?,?,?)").run(id, title, tokenize(content));
45
+ return { id, sourceId, version, title, isNew: !existing };
46
+ }
47
+ search(query, limit = 8) {
48
+ const match = tokenize(query).split(/\s+/).filter(Boolean).map((part) => `"${part.replaceAll('"', '""')}"`).join(" OR ");
49
+ if (!match)
50
+ return [];
51
+ return this.db.prepare(`SELECT k.id, k.title, k.content, s.name AS source, k.version, k.updated_at AS updatedAt FROM knowledge_fts f JOIN knowledge k ON k.id = f.knowledge_id JOIN sources s ON s.id = k.source_id WHERE f.knowledge_fts MATCH ? AND k.current = 1 ORDER BY rank LIMIT ?`).all(match, limit);
52
+ }
53
+ read(id) {
54
+ return this.db.prepare("SELECT k.id,k.title,k.content,s.name AS source,k.version,k.updated_at AS updatedAt FROM knowledge k JOIN sources s ON s.id=k.source_id WHERE k.id=?").get(id);
55
+ }
56
+ sources() { return this.db.prepare("SELECT s.name,s.original_path AS path,MAX(k.updated_at) AS updatedAt,MAX(k.version) AS version FROM sources s JOIN knowledge k ON k.source_id=s.id AND k.current=1 GROUP BY s.id ORDER BY updatedAt DESC").all(); }
57
+ oldVersions() { return this.db.prepare("SELECT k.id,k.title,k.version,s.name AS source FROM knowledge k JOIN sources s ON s.id=k.source_id WHERE k.current=0 ORDER BY s.name,k.version").all(); }
58
+ removeVersions(ids) { const remove = this.db.prepare("DELETE FROM knowledge WHERE id=?"); const removeIndex = this.db.prepare("DELETE FROM knowledge_fts WHERE knowledge_id=?"); for (const id of ids) {
59
+ removeIndex.run(id);
60
+ remove.run(id);
61
+ } }
62
+ saveMessage(role, content) { this.db.prepare("INSERT INTO conversations VALUES (?,?,?,?)").run(crypto.randomUUID(), role, content, new Date().toISOString()); }
63
+ listMessages() { return this.db.prepare("SELECT role,content FROM conversations ORDER BY created_at").all(); }
64
+ }
65
+ export function readLocalFile(filePath) {
66
+ const originalPath = resolve(filePath);
67
+ const ext = extname(originalPath).toLowerCase();
68
+ let content;
69
+ if ([".md", ".txt", ".json", ".csv"].includes(ext))
70
+ content = readFileSync(originalPath, "utf8");
71
+ else if (ext === ".xlsx" || ext === ".xls") {
72
+ const workbook = XLSX.readFile(originalPath);
73
+ content = workbook.SheetNames.map((name) => `## ${name}\n${XLSX.utils.sheet_to_csv(workbook.Sheets[name])}`).join("\n\n");
74
+ }
75
+ else if (ext === ".pdf") {
76
+ throw new Error("PDF parsing is not available in this initial runtime; convert the PDF to Markdown or text first.");
77
+ }
78
+ else {
79
+ const stat = statSync(originalPath);
80
+ return { title: basename(originalPath), content: `[Unparseable file: ${ext || "unknown"}; ${stat.size} bytes]`, originalPath, hash: "" };
81
+ }
82
+ return { title: basename(originalPath), content, originalPath, hash: createHash("sha256").update(content).digest("hex") };
83
+ }
84
+ export function copyOriginal(filePath, root) {
85
+ const destination = join(root, "originals", `${Date.now()}-${basename(filePath)}`);
86
+ copyFileSync(filePath, destination);
87
+ return destination;
88
+ }
89
+ export function defaultDataRoot() {
90
+ const home = process.env.HOME ?? process.env.USERPROFILE ?? process.cwd();
91
+ return process.env.KB_DATA_DIR ?? join(home, ".kb");
92
+ }
93
+ export function knowledgeBasePath(root, id) { return join(root, "knowledge-bases", id); }
94
+ export function listKnowledgeBases(root) {
95
+ const base = join(root, "knowledge-bases");
96
+ mkdirSync(base, { recursive: true });
97
+ return readdirSync(base, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => ({ id: entry.name, name: entry.name, path: join(base, entry.name) }));
98
+ }
99
+ export function deleteKnowledgeBase(root, id) {
100
+ if (id === "default")
101
+ throw new Error("The default knowledge base cannot be deleted.");
102
+ rmSync(knowledgeBasePath(root, id), { recursive: true, force: true });
103
+ }
104
+ export function backupKnowledgeBase(base, destination) {
105
+ const target = destination ? resolve(destination) : join(base.path, "backups", `${base.id}-${new Date().toISOString().replaceAll(":", "-")}`);
106
+ mkdirSync(target, { recursive: true });
107
+ cpSync(join(base.path, "knowledge.db"), join(target, "knowledge.db"));
108
+ if (existsSync(join(base.path, "originals")))
109
+ cpSync(join(base.path, "originals"), join(target, "originals"), { recursive: true });
110
+ return target;
111
+ }
112
+ export function restoreKnowledgeBase(base, backupPath) {
113
+ const source = resolve(backupPath);
114
+ if (!existsSync(join(source, "knowledge.db")))
115
+ throw new Error("Backup does not contain knowledge.db");
116
+ const safety = join(base.path, "backups", `before-restore-${Date.now()}`);
117
+ backupKnowledgeBase(base, safety);
118
+ rmSync(join(base.path, "originals"), { recursive: true, force: true });
119
+ cpSync(join(source, "knowledge.db"), join(base.path, "knowledge.db"));
120
+ if (existsSync(join(source, "originals")))
121
+ cpSync(join(source, "originals"), join(base.path, "originals"), { recursive: true });
122
+ }
123
+ export function ensureKnowledgeBase(root, name = "default") {
124
+ const id = name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || "default";
125
+ const path = knowledgeBasePath(root, id);
126
+ mkdirSync(path, { recursive: true });
127
+ const store = new KnowledgeStore(path);
128
+ store.close();
129
+ return { id, name, path };
130
+ }
package/package.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "@ganziliang/kb",
3
+ "version": "0.1.0",
4
+ "description": "Local knowledge base agent CLI",
5
+ "type": "module",
6
+ "bin": { "kb": "dist/cli.js" },
7
+ "main": "dist/index.js",
8
+ "types": "dist/index.d.ts",
9
+ "files": ["dist"],
10
+ "scripts": {
11
+ "build": "tsc -p tsconfig.json",
12
+ "typecheck": "tsc -p tsconfig.json --noEmit",
13
+ "test": "npm run build && node --test"
14
+ },
15
+ "engines": { "node": ">=22.5.0" },
16
+ "dependencies": {
17
+ "@ganziliang/kb-model-setup": "^0.1.0",
18
+ "ink": "^5.1.0",
19
+ "react": "^18.3.1",
20
+ "xlsx": "^0.18.5"
21
+ },
22
+ "devDependencies": {
23
+ "@types/node": "^22.10.0",
24
+ "@types/react": "^18.3.12",
25
+ "typescript": "^5.7.2"
26
+ }
27
+ }