@ganziliang/kb 0.1.7 → 0.3.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/model.d.ts CHANGED
@@ -1,9 +1,15 @@
1
1
  import type { ModelConfig } from "@ganziliang/kb-model-setup";
2
2
  import type { SearchResult, KnowledgeStore } from "./storage.js";
3
- export type ToolCall = {
4
- name: "search" | "read" | "write";
3
+ export type NativeToolCall = {
4
+ id: string;
5
+ name: string;
5
6
  arguments: Record<string, unknown>;
6
7
  };
8
+ export type ToolDefinition = {
9
+ name: string;
10
+ description: string;
11
+ inputSchema: Record<string, unknown>;
12
+ };
7
13
  export type ContentBlock = {
8
14
  type: "text";
9
15
  text: string;
@@ -19,13 +25,13 @@ export type ModelMessage = {
19
25
  };
20
26
  export type ModelReply = {
21
27
  text?: string;
22
- toolCalls?: ToolCall[];
28
+ toolCalls?: NativeToolCall[];
23
29
  };
24
30
  export declare function toAnthropicContent(content: string | ContentBlock[]): unknown;
25
31
  export declare function toOpenAIContent(content: string | ContentBlock[]): unknown;
26
32
  export declare function contentToText(content: string | ContentBlock[]): string;
27
33
  export type ModelTransport = {
28
- complete(config: ModelConfig, messages: ModelMessage[]): Promise<ModelReply>;
34
+ complete(config: ModelConfig, messages: ModelMessage[], tools?: ToolDefinition[]): Promise<ModelReply>;
29
35
  };
30
36
  export declare const fetchTransport: ModelTransport;
31
37
  export declare class Agent {
@@ -40,6 +46,11 @@ export declare class Agent {
40
46
  private execute;
41
47
  }
42
48
  export declare function formatResults(results: SearchResult[]): string;
49
+ /**
50
+ * 让模型在一轮对话里自己决定「回答」还是「录入」,
51
+ * 避免为了判断意图而多跑一次模型调用。
52
+ */
53
+ export declare const SAVE_KNOWLEDGE_TOOL: ToolDefinition;
43
54
  type VisionTransport = {
44
55
  complete: (config: ModelConfig, messages: ModelMessage[]) => Promise<ModelReply>;
45
56
  };
package/dist/model.js CHANGED
@@ -23,7 +23,7 @@ function endpoint(config) {
23
23
  return `${config.baseURL.replace(/\/$/, "")}${config.api === "anthropic-messages" ? "/v1/messages" : "/v1/responses"}`;
24
24
  }
25
25
  export const fetchTransport = {
26
- async complete(config, messages) {
26
+ async complete(config, messages, tools) {
27
27
  const headers = { "content-type": "application/json" };
28
28
  if (config.api === "anthropic-messages") {
29
29
  headers["x-api-key"] = config.apiKey;
@@ -31,19 +31,46 @@ export const fetchTransport = {
31
31
  }
32
32
  else
33
33
  headers.authorization = `Bearer ${config.apiKey}`;
34
+ const anthropicTools = tools?.length
35
+ ? { tools: tools.map((tool) => ({ name: tool.name, description: tool.description, input_schema: tool.inputSchema })) }
36
+ : {};
37
+ const openaiTools = tools?.length
38
+ ? { tools: tools.map((tool) => ({ type: "function", name: tool.name, description: tool.description, parameters: tool.inputSchema })) }
39
+ : {};
34
40
  const response = await fetch(endpoint(config), {
35
41
  method: "POST",
36
42
  headers,
37
43
  body: JSON.stringify(config.api === "anthropic-messages"
38
- ? { model: config.model, max_tokens: 4096, messages: messages.filter((m) => m.role !== "system").map((m) => ({ role: m.role, content: toAnthropicContent(m.content) })), system: messages.find((m) => m.role === "system")?.content }
39
- : { model: config.model, input: messages.map((m) => ({ role: m.role, content: toOpenAIContent(m.content) })) }),
44
+ ? { model: config.model, max_tokens: 4096, messages: messages.filter((m) => m.role !== "system").map((m) => ({ role: m.role, content: toAnthropicContent(m.content) })), system: messages.find((m) => m.role === "system")?.content, ...anthropicTools }
45
+ : { model: config.model, input: messages.map((m) => ({ role: m.role, content: toOpenAIContent(m.content) })), ...openaiTools }),
40
46
  });
41
47
  if (!response.ok)
42
48
  throw new Error(`Model request failed (${response.status}): ${await response.text()}`);
43
49
  const data = await response.json();
44
- if (config.api === "anthropic-messages")
45
- return { text: data.content?.map((item) => item.text ?? "").join("") };
46
- return { text: data.output_text ?? data.output?.map((item) => item.content?.map((part) => part.text ?? "").join("")).join("") ?? "" };
50
+ if (config.api === "anthropic-messages") {
51
+ let text = "";
52
+ const toolCalls = [];
53
+ for (const item of (data.content ?? [])) {
54
+ if (item.type === "text")
55
+ text += item.text ?? "";
56
+ else if (item.type === "tool_use")
57
+ toolCalls.push({ id: String(item.id ?? ""), name: String(item.name), arguments: (item.input ?? {}) });
58
+ }
59
+ return toolCalls.length ? { text, toolCalls } : { text };
60
+ }
61
+ const toolCalls = [];
62
+ for (const item of (data.output ?? [])) {
63
+ if (item.type !== "function_call")
64
+ continue;
65
+ let parsed = {};
66
+ try {
67
+ parsed = JSON.parse(item.arguments ?? "{}");
68
+ }
69
+ catch { /* 保留空参数 */ }
70
+ toolCalls.push({ id: String(item.call_id ?? item.id ?? ""), name: String(item.name), arguments: parsed });
71
+ }
72
+ const text = data.output_text ?? (data.output ?? []).map((item) => (item.content ?? []).map((part) => part.text ?? "").join("")).join("") ?? "";
73
+ return toolCalls.length ? { text, toolCalls } : { text };
47
74
  },
48
75
  };
49
76
  export class Agent {
@@ -72,12 +99,34 @@ export class Agent {
72
99
  return this.store.search(String(call.arguments.query ?? ""));
73
100
  if (call.name === "read")
74
101
  return this.store.read(String(call.arguments.id ?? "")) ?? { error: "Knowledge record not found" };
75
- return { error: "write requires CLI ingestion flow" };
102
+ return { error: `Unknown tool: ${call.name}` };
76
103
  }
77
104
  }
78
105
  export function formatResults(results) {
79
106
  return results.map((result) => `[${result.id}] ${result.title} (source: ${result.source}, v${result.version})\n${result.content}`).join("\n\n");
80
107
  }
108
+ /**
109
+ * 让模型在一轮对话里自己决定「回答」还是「录入」,
110
+ * 避免为了判断意图而多跑一次模型调用。
111
+ */
112
+ export const SAVE_KNOWLEDGE_TOOL = {
113
+ name: "save_knowledge",
114
+ description: [
115
+ "把用户提供的内容保存到本地知识库。",
116
+ "当用户意图是「记录 / 保存 / 录入 / 收藏 / 备忘 / 以后要用」某段内容时调用,",
117
+ "例如:记住…、记一下…、记录一下…、把这段存起来、帮我存个配置、以后就按这个来。",
118
+ "只有用户确实在提供一段希望长期保存的知识时才调用。",
119
+ "如果用户在询问、查询、确认、闲聊,不要调用,直接回答。",
120
+ ].join(""),
121
+ inputSchema: {
122
+ type: "object",
123
+ properties: {
124
+ title: { type: "string", description: "对内容的一句话概括,作为知识条目标题,20 字以内" },
125
+ content: { type: "string", description: "要保存的完整内容,规范 Markdown。必须完整保留用户给出的全部信息,不要自行增删或改写事实" },
126
+ },
127
+ required: ["title", "content"],
128
+ },
129
+ };
81
130
  const IMAGE_QUESTION = "\u8bf7\u7528\u4e2d\u6587\u8be6\u7ec6\u63cf\u8ff0\u8fd9\u5f20\u56fe\u7247\u7684\u5185\u5bb9\uff0c\u5199\u6e05\u6240\u6709\u53ef\u89c1\u7684\u6587\u5b57\u3001\u6570\u5b57\u3001\u8868\u683c\u3001\u754c\u9762\u5143\u7d20\u548c\u5173\u952e\u7ec6\u8282\uff0c\u4ee5\u53ca\u56fe\u7247\u6574\u4f53\u5728\u8bb2\u4ec0\u4e48\u3002\u53ea\u8f93\u51fa\u63cf\u8ff0\u6b63\u6587\uff0c\u4e0d\u8981\u5ba2\u5957\u8bdd\u3002";
82
131
  export async function describeImage(config, transport, image) {
83
132
  const reply = await transport.complete(config, [{
package/dist/storage.d.ts CHANGED
@@ -54,6 +54,8 @@ type KnowledgeStoreResult = {
54
54
  };
55
55
  export declare function readLocalFile(filePath: string): LocalFile;
56
56
  export declare function copyOriginal(filePath: string, root: string): string;
57
+ /** \u628a\u624b\u52a8\u5f55\u5165\u7684\u6587\u672c\u843d\u76d8\u4e3a originals/ \u4e0b\u7684 Markdown\uff0c\u8fd4\u56de\u8be5\u6587\u4ef6\u8def\u5f84\u3002 */
58
+ export declare function writeNote(root: string, title: string, content: string): string;
57
59
  export declare function defaultDataRoot(): string;
58
60
  export declare function knowledgeBasePath(root: string, id: string): string;
59
61
  export declare function listKnowledgeBases(root: string): KnowledgeBase[];
package/dist/storage.js CHANGED
@@ -1,4 +1,4 @@
1
- import { mkdirSync, copyFileSync, existsSync, readFileSync, statSync, cpSync, rmSync, readdirSync } from "node:fs";
1
+ import { mkdirSync, copyFileSync, existsSync, readFileSync, statSync, cpSync, rmSync, readdirSync, writeFileSync } from "node:fs";
2
2
  import { createHash } from "node:crypto";
3
3
  import { join, resolve, extname, basename } from "node:path";
4
4
  import { DatabaseSync } from "node:sqlite";
@@ -115,6 +115,15 @@ export function copyOriginal(filePath, root) {
115
115
  copyFileSync(filePath, destination);
116
116
  return destination;
117
117
  }
118
+ /** \u628a\u624b\u52a8\u5f55\u5165\u7684\u6587\u672c\u843d\u76d8\u4e3a originals/ \u4e0b\u7684 Markdown\uff0c\u8fd4\u56de\u8be5\u6587\u4ef6\u8def\u5f84\u3002 */
119
+ export function writeNote(root, title, content) {
120
+ const dir = join(root, "originals");
121
+ mkdirSync(dir, { recursive: true });
122
+ const slug = title.replace(/[^\p{L}\p{N}]+/gu, "-").replace(/^-+|-+$/g, "").slice(0, 40) || "note";
123
+ const target = join(dir, `${Date.now()}-${slug}.md`);
124
+ writeFileSync(target, content, "utf8");
125
+ return target;
126
+ }
118
127
  export function defaultDataRoot() {
119
128
  const home = process.env.HOME ?? process.env.USERPROFILE ?? process.cwd();
120
129
  return process.env.KB_DATA_DIR ?? join(home, ".kb");
@@ -0,0 +1,27 @@
1
+ import type { EditorTheme, ImageTheme, MarkdownTheme, SelectListTheme } from "@earendil-works/pi-tui";
2
+ export declare const fg: (r: number, g: number, b: number) => (s: string) => string;
3
+ export declare const bg: (r: number, g: number, b: number) => (s: string) => string;
4
+ export declare const bold: (s: string) => string;
5
+ export declare const dim: (s: string) => string;
6
+ export declare const italic: (s: string) => string;
7
+ export declare const underline: (s: string) => string;
8
+ export declare const strike: (s: string) => string;
9
+ /** 全局配色:slate/sky/teal 冷色调,深浅终端下都有足够对比度。 */
10
+ export declare const color: {
11
+ accent: (s: string) => string;
12
+ accent2: (s: string) => string;
13
+ user: (s: string) => string;
14
+ text: (s: string) => string;
15
+ muted: (s: string) => string;
16
+ dim: (s: string) => string;
17
+ border: (s: string) => string;
18
+ ok: (s: string) => string;
19
+ warn: (s: string) => string;
20
+ err: (s: string) => string;
21
+ chip: (s: string) => string;
22
+ userBg: (s: string) => string;
23
+ };
24
+ export declare const markdownTheme: MarkdownTheme;
25
+ export declare const selectListTheme: SelectListTheme;
26
+ export declare const editorTheme: EditorTheme;
27
+ export declare const imageTheme: ImageTheme;
package/dist/theme.js ADDED
@@ -0,0 +1,52 @@
1
+ export const fg = (r, g, b) => (s) => `\x1b[38;2;${r};${g};${b}m${s}\x1b[39m`;
2
+ export const bg = (r, g, b) => (s) => `\x1b[48;2;${r};${g};${b}m${s}\x1b[49m`;
3
+ export const bold = (s) => `\x1b[1m${s}\x1b[22m`;
4
+ export const dim = (s) => `\x1b[2m${s}\x1b[22m`;
5
+ export const italic = (s) => `\x1b[3m${s}\x1b[23m`;
6
+ export const underline = (s) => `\x1b[4m${s}\x1b[24m`;
7
+ export const strike = (s) => `\x1b[9m${s}\x1b[29m`;
8
+ /** 全局配色:slate/sky/teal 冷色调,深浅终端下都有足够对比度。 */
9
+ export const color = {
10
+ accent: fg(56, 189, 248),
11
+ accent2: fg(45, 212, 191),
12
+ user: fg(226, 232, 240),
13
+ text: fg(203, 213, 225),
14
+ muted: fg(100, 116, 139),
15
+ dim: fg(71, 85, 105),
16
+ border: fg(51, 65, 85),
17
+ ok: fg(74, 222, 128),
18
+ warn: fg(251, 191, 36),
19
+ err: fg(248, 113, 113),
20
+ chip: bg(30, 41, 59),
21
+ userBg: bg(15, 23, 42),
22
+ };
23
+ export const markdownTheme = {
24
+ heading: (t) => bold(color.accent2(t)),
25
+ link: (t) => color.accent(t),
26
+ linkUrl: (t) => dim(color.muted(t)),
27
+ code: (t) => color.warn(t),
28
+ codeBlock: (t) => color.text(t),
29
+ codeBlockBorder: (t) => color.border(t),
30
+ quote: (t) => dim(color.muted(t)),
31
+ quoteBorder: (t) => color.accent2(t),
32
+ hr: (t) => color.border(t),
33
+ listBullet: (t) => color.accent(t),
34
+ bold: (t) => bold(color.user(t)),
35
+ italic: (t) => italic(color.text(t)),
36
+ strikethrough: (t) => strike(color.text(t)),
37
+ underline: (t) => underline(color.text(t)),
38
+ };
39
+ export const selectListTheme = {
40
+ selectedPrefix: (t) => color.accent(bold(t)),
41
+ selectedText: (t) => bold(color.user(t)),
42
+ description: (t) => color.muted(t),
43
+ scrollInfo: (t) => dim(color.muted(t)),
44
+ noMatch: (t) => dim(color.muted(t)),
45
+ };
46
+ export const editorTheme = {
47
+ borderColor: (t) => color.border(t),
48
+ selectList: selectListTheme,
49
+ };
50
+ export const imageTheme = {
51
+ fallbackColor: (t) => color.muted(t),
52
+ };
package/dist/ui.d.ts ADDED
@@ -0,0 +1,88 @@
1
+ import { type Component } from "@earendil-works/pi-tui";
2
+ import { dim, selectListTheme } from "./theme.js";
3
+ /** 把文本按可见宽度换行;保证每行都不超过 width。 */
4
+ export declare function wrap(text: string, width: number): string[];
5
+ /** 通用多行文本组件,按渲染宽度截断,保证永不超宽。 */
6
+ export declare class Lines implements Component {
7
+ private lines;
8
+ constructor(lines?: string[]);
9
+ setLines(lines: string[]): void;
10
+ invalidate(): void;
11
+ render(width: number): string[];
12
+ }
13
+ /** 启动屏:logo + 环境信息。 */
14
+ export declare class Splash implements Component {
15
+ private info;
16
+ constructor(info: {
17
+ base: string;
18
+ model: string;
19
+ dataPath: string;
20
+ sources: number;
21
+ });
22
+ invalidate(): void;
23
+ render(width: number): string[];
24
+ }
25
+ /** 顶部状态条:带圆角边框的一行摘要。 */ export declare class StatusBar implements Component {
26
+ private info;
27
+ constructor(info: {
28
+ base: string;
29
+ model: string;
30
+ sources: number;
31
+ turns: number;
32
+ });
33
+ setInfo(info: {
34
+ base: string;
35
+ model: string;
36
+ sources: number;
37
+ turns: number;
38
+ }): void;
39
+ invalidate(): void;
40
+ render(width: number): string[];
41
+ }
42
+ /** 用户提问。 */
43
+ export declare class UserMessage implements Component {
44
+ private text;
45
+ constructor(text: string);
46
+ invalidate(): void;
47
+ render(width: number): string[];
48
+ }
49
+ /** 助手回答:图标 + Markdown 正文 + 元信息脚注。 */
50
+ export declare class AssistantMessage implements Component {
51
+ private meta;
52
+ private md;
53
+ constructor(text: string, meta?: string);
54
+ setMeta(meta: string): void;
55
+ invalidate(): void;
56
+ render(width: number): string[];
57
+ }
58
+ /** 单行提示(成功 / 信息 / 警告 / 错误)。 */
59
+ export declare class Notice implements Component {
60
+ private icon;
61
+ private style;
62
+ private text;
63
+ constructor(icon: string, style: (s: string) => string, text: string);
64
+ invalidate(): void;
65
+ render(width: number): string[];
66
+ }
67
+ /** 检索命中的附图标记。 */
68
+ export declare class ImageNotice implements Component {
69
+ private name;
70
+ private version;
71
+ constructor(name: string, version: number);
72
+ invalidate(): void;
73
+ render(width: number): string[];
74
+ }
75
+ /** 水平分隔线。 */
76
+ export declare class Rule implements Component {
77
+ invalidate(): void;
78
+ render(width: number): string[];
79
+ }
80
+ /** 帮助面板(作为 overlay 内容)。 */
81
+ export declare class HelpPanel implements Component {
82
+ private items;
83
+ constructor(items: Array<[string, string]>);
84
+ invalidate(): void;
85
+ render(width: number): string[];
86
+ }
87
+ /** 供 SelectList 复用的主题。 */
88
+ export { selectListTheme, dim };
package/dist/ui.js ADDED
@@ -0,0 +1,171 @@
1
+ import { Markdown, truncateToWidth, visibleWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";
2
+ import { bold, color, dim, markdownTheme, selectListTheme } from "./theme.js";
3
+ /** 把文本按可见宽度换行;保证每行都不超过 width。 */
4
+ export function wrap(text, width) {
5
+ if (width <= 0)
6
+ return [""];
7
+ const out = [];
8
+ for (const raw of text.split("\n")) {
9
+ if (raw === "") {
10
+ out.push("");
11
+ continue;
12
+ }
13
+ for (const piece of wrapTextWithAnsi(raw, width))
14
+ out.push(piece);
15
+ }
16
+ return out;
17
+ }
18
+ const pad = (s, w) => s + " ".repeat(Math.max(0, w - visibleWidth(s)));
19
+ /** 统一裁剪:保证组件输出永不超过给定宽度(pi-tui 会因超宽报错)。 */
20
+ const clip = (lines, width) => lines.map((l) => truncateToWidth(l, Math.max(0, width)));
21
+ /** 通用多行文本组件,按渲染宽度截断,保证永不超宽。 */
22
+ export class Lines {
23
+ lines = [];
24
+ constructor(lines = []) { this.lines = lines; }
25
+ setLines(lines) { this.lines = lines; }
26
+ invalidate() { }
27
+ render(width) {
28
+ return this.lines.map((l) => truncateToWidth(l, Math.max(0, width)));
29
+ }
30
+ }
31
+ /** 启动屏:logo + 环境信息。 */
32
+ export class Splash {
33
+ info;
34
+ constructor(info) {
35
+ this.info = info;
36
+ }
37
+ invalidate() { }
38
+ render(width) {
39
+ const logo = ["██╗ ██╗ ██████╗", "██║ ██╔╝ ██╔══██╗", "█████╔╝ ██████╔╝", "██╔═██╗ ██╔══██╗", "██║ ██╗ ██████╔╝", "╚═╝ ╚═╝ ╚═════╝ "];
40
+ const field = (k, v, val) => ` ${color.muted(pad(k, 8))}${val(v)}`;
41
+ const lines = [
42
+ "",
43
+ ...logo.map((l) => " " + color.accent(bold(l))),
44
+ "",
45
+ " " + color.text(bold("知识库")) + color.border(" · ") + color.muted("Knowledge Base"),
46
+ "",
47
+ field("知识库", this.info.base, color.user),
48
+ field("模型", this.info.model, color.user),
49
+ field("来源", String(this.info.sources), color.user),
50
+ field("数据", this.info.dataPath, (s) => color.dim(s)),
51
+ "",
52
+ " " + color.dim("输入问题开始,或键入 ") + color.accent("/help") + color.dim(" 查看命令"),
53
+ "",
54
+ ];
55
+ return lines.map((l) => truncateToWidth(l, Math.max(0, width)));
56
+ }
57
+ }
58
+ /** 顶部状态条:带圆角边框的一行摘要。 */ export class StatusBar {
59
+ info;
60
+ constructor(info) {
61
+ this.info = info;
62
+ }
63
+ setInfo(info) { this.info = info; }
64
+ invalidate() { }
65
+ render(width) {
66
+ const w = Math.max(0, width);
67
+ const inner = Math.max(0, w - 4);
68
+ const label = " KB ";
69
+ const top = color.border("╭─") + color.accent(bold(label)) + color.border("─".repeat(Math.max(0, w - 3 - visibleWidth(label))) + "╮");
70
+ const bot = color.border("╰" + "─".repeat(Math.max(0, w - 2)) + "╯");
71
+ const stat = (k, v) => `${color.muted(k)} ${color.user(v)}`;
72
+ const content = " " + [
73
+ stat("知识库", this.info.base),
74
+ stat("模型", this.info.model),
75
+ stat("来源", String(this.info.sources)),
76
+ stat("会话", String(this.info.turns)),
77
+ ].join(color.border(" · "));
78
+ const row = color.border("│") + " " + pad(truncateToWidth(content, inner), inner) + " " + color.border("│");
79
+ return clip([top, row, bot], w);
80
+ }
81
+ }
82
+ /** 用户提问。 */
83
+ export class UserMessage {
84
+ text;
85
+ constructor(text) {
86
+ this.text = text;
87
+ }
88
+ invalidate() { }
89
+ render(width) {
90
+ const prefix = ` ${color.accent2(bold("❯"))} `;
91
+ const body = wrap(this.text, Math.max(1, width - 4));
92
+ return body.map((l, i) => (i === 0 ? prefix + color.user(l) : " " + color.user(l)));
93
+ }
94
+ }
95
+ /** 助手回答:图标 + Markdown 正文 + 元信息脚注。 */
96
+ export class AssistantMessage {
97
+ meta;
98
+ md;
99
+ constructor(text, meta = "") {
100
+ this.meta = meta;
101
+ this.md = new Markdown(text, 0, 0, markdownTheme);
102
+ }
103
+ setMeta(meta) { this.meta = meta; }
104
+ invalidate() { this.md.invalidate(); }
105
+ render(width) {
106
+ const indent = " ";
107
+ const bodyWidth = Math.max(10, width - indent.length);
108
+ const lines = [` ${color.ok(bold("⏺"))}`, ""];
109
+ for (const l of this.md.render(bodyWidth))
110
+ lines.push(indent + l.replace(/\s+$/, ""));
111
+ if (this.meta)
112
+ lines.push("", `${indent}${color.dim("·")} ${color.muted(this.meta)}`);
113
+ return lines.map((l) => truncateToWidth(l, Math.max(0, width)));
114
+ }
115
+ }
116
+ /** 单行提示(成功 / 信息 / 警告 / 错误)。 */
117
+ export class Notice {
118
+ icon;
119
+ style;
120
+ text;
121
+ constructor(icon, style, text) {
122
+ this.icon = icon;
123
+ this.style = style;
124
+ this.text = text;
125
+ }
126
+ invalidate() { }
127
+ render(width) {
128
+ const prefix = ` ${this.style(this.icon)} `;
129
+ return wrap(this.text, Math.max(1, width - 4)).map((l, i) => (i === 0 ? prefix + color.text(l) : " " + color.text(l)));
130
+ }
131
+ }
132
+ /** 检索命中的附图标记。 */
133
+ export class ImageNotice {
134
+ name;
135
+ version;
136
+ constructor(name, version) {
137
+ this.name = name;
138
+ this.version = version;
139
+ }
140
+ invalidate() { }
141
+ render(width) {
142
+ const line = ` ${color.accent2("★")} ${color.muted("附图")} ${color.chip(color.text(` ${this.name} `))} ${color.dim("v" + this.version)}`;
143
+ return [truncateToWidth(line, Math.max(0, width))];
144
+ }
145
+ }
146
+ /** 水平分隔线。 */
147
+ export class Rule {
148
+ invalidate() { }
149
+ render(width) {
150
+ return [color.dim("─".repeat(Math.max(0, width)))];
151
+ }
152
+ }
153
+ /** 帮助面板(作为 overlay 内容)。 */
154
+ export class HelpPanel {
155
+ items;
156
+ constructor(items) { this.items = items; }
157
+ invalidate() { }
158
+ render(width) {
159
+ const w = Math.min(Math.max(0, width), 72);
160
+ const inner = Math.max(0, w - 4);
161
+ const title = " 命令 ";
162
+ const keyWidth = Math.max(10, ...this.items.map(([k]) => visibleWidth(k))) + 2;
163
+ const top = color.border("╭─") + color.accent(bold(title)) + color.border("─".repeat(Math.max(0, w - 3 - visibleWidth(title))) + "╮");
164
+ const bot = color.border("╰" + "─".repeat(Math.max(0, w - 2)) + "╯");
165
+ const row = (c) => color.border("│") + " " + pad(truncateToWidth(c, inner), inner) + " " + color.border("│");
166
+ const body = this.items.map(([k, d]) => row(k ? " " + color.accent(k) + " ".repeat(Math.max(1, keyWidth - visibleWidth(k))) + color.muted(d) : ""));
167
+ return clip([top, row(""), ...body, row(""), bot], w);
168
+ }
169
+ }
170
+ /** 供 SelectList 复用的主题。 */
171
+ export { selectListTheme, dim };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@ganziliang/kb",
3
- "version": "0.1.7",
4
- "description": "Local knowledge base agent CLI",
3
+ "version": "0.3.0",
4
+ "description": "Local knowledge base agent CLI with a pi-tui interface",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "kb": "dist/entry.js"
@@ -17,17 +17,15 @@
17
17
  "test": "npm run build && node --test"
18
18
  },
19
19
  "engines": {
20
- "node": ">=22.5.0"
20
+ "node": ">=22.19.0"
21
21
  },
22
22
  "dependencies": {
23
+ "@earendil-works/pi-tui": "^0.85.1",
23
24
  "@ganziliang/kb-model-setup": "^0.1.2",
24
- "ink": "^5.1.0",
25
- "react": "^18.3.1",
26
25
  "xlsx": "^0.18.5"
27
26
  },
28
27
  "devDependencies": {
29
28
  "@types/node": "^22.10.0",
30
- "@types/react": "^18.3.12",
31
29
  "typescript": "^5.7.2"
32
30
  }
33
31
  }