botrun-horse 1.0.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.
Files changed (64) hide show
  1. package/README.md +1 -0
  2. package/bin/bh.mjs +193 -0
  3. package/bin/commands/dag-cmd.mjs +74 -0
  4. package/bin/commands/db-cmd.mjs +73 -0
  5. package/bin/commands/doc.mjs +185 -0
  6. package/bin/commands/gemini.mjs +120 -0
  7. package/bin/commands/help.mjs +109 -0
  8. package/bin/commands/legal.mjs +174 -0
  9. package/bin/commands/nchc.mjs +212 -0
  10. package/bin/commands/openrouter.mjs +154 -0
  11. package/bin/commands/prompt.mjs +175 -0
  12. package/bin/commands/schema.mjs +258 -0
  13. package/bin/commands/search.mjs +46 -0
  14. package/bin/commands/writing.mjs +33 -0
  15. package/lib/core/adapters/base.mjs +52 -0
  16. package/lib/core/adapters/claude.mjs +13 -0
  17. package/lib/core/adapters/gemini-api.mjs +174 -0
  18. package/lib/core/adapters/gemini-shared.mjs +164 -0
  19. package/lib/core/adapters/gemini-vertex.mjs +232 -0
  20. package/lib/core/adapters/local.mjs +13 -0
  21. package/lib/core/adapters/nchc.mjs +236 -0
  22. package/lib/core/adapters/openai-shared.mjs +34 -0
  23. package/lib/core/adapters/openrouter.mjs +304 -0
  24. package/lib/core/ai-cache.mjs +277 -0
  25. package/lib/core/ai-router.mjs +217 -0
  26. package/lib/core/cli-utils.mjs +170 -0
  27. package/lib/core/dag.mjs +114 -0
  28. package/lib/core/db.mjs +412 -0
  29. package/lib/core/env.mjs +64 -0
  30. package/lib/core/llm.mjs +58 -0
  31. package/lib/core/paths.mjs +115 -0
  32. package/lib/core/proxy.mjs +46 -0
  33. package/lib/core/watermelon.mjs +9 -0
  34. package/lib/doc/index.mjs +419 -0
  35. package/lib/doc/office2text.mjs +234 -0
  36. package/lib/doc/pdf2text.mjs +133 -0
  37. package/lib/doc/split.mjs +132 -0
  38. package/lib/flows/draft-writing.mjs +29 -0
  39. package/lib/flows/gemini-ask.mjs +185 -0
  40. package/lib/flows/hatch-portal.mjs +13 -0
  41. package/lib/flows/legal-ask.mjs +325 -0
  42. package/lib/flows/openai-agent.mjs +167 -0
  43. package/lib/flows/opencode-agent.mjs +240 -0
  44. package/lib/flows/openrouter-ask.mjs +111 -0
  45. package/lib/flows/review-doc.mjs +18 -0
  46. package/lib/ocr/index.mjs +6 -0
  47. package/lib/portal/hatch.mjs +6 -0
  48. package/lib/portal/index.mjs +6 -0
  49. package/lib/prompt/prompt-search.mjs +55 -0
  50. package/lib/prompt/prompt-store.mjs +94 -0
  51. package/lib/prompt/prompts/zero-framework/coding.md +15 -0
  52. package/lib/prompt/prompts/zero-framework/search.md +12 -0
  53. package/lib/prompt/prompts/zero-framework/slice.md +11 -0
  54. package/lib/search/crawler.mjs +6 -0
  55. package/lib/search/index.mjs +7 -0
  56. package/lib/tools/fs-tools.mjs +268 -0
  57. package/lib/tools/index.mjs +27 -0
  58. package/lib/writing/generate.mjs +86 -0
  59. package/lib/writing/generators/nstc-generators.mjs +279 -0
  60. package/lib/writing/generators/nstc-top5.mjs +554 -0
  61. package/lib/writing/index.mjs +5 -0
  62. package/lib/writing/layouts/nstc-layout.mjs +249 -0
  63. package/lib/writing/renderer.mjs +61 -0
  64. package/package.json +35 -0
@@ -0,0 +1,240 @@
1
+ // lib/flows/opencode-agent.mjs — @opencode-ai/sdk Agentic Coding 代理
2
+ //
3
+ // 架構:
4
+ // createOpencodeServer() → 嵌入式 OpenCode server(注入 NCHC provider 設定)
5
+ // createOpencodeClient() → REST client
6
+ // session.create() → 建立會話(自動批准所有權限)
7
+ // session.prompt() → 發送任務(指定 NCHC 675B model)
8
+ // event.subscribe() SSE → 串流回應事件直到 session.idle
9
+ //
10
+ // DDD 領域概念:
11
+ // CodeSession — OpenCode 會話(由 session.create 建立)
12
+ // AgentEvent — 代理執行事件(delta/text/done/session_created/timeout)
13
+ // ProviderConfig — NCHC provider 設定(Value Object)
14
+ //
15
+ // SOLID:
16
+ // SRP — OpencodeAgent 只負責 session 生命週期;事件處理獨立
17
+ // OCP — _factory 注入讓 server/client 可替換(測試 + 切換 provider)
18
+ // LSP — 與 NchcAgent 相同 run() AsyncGenerator 介面
19
+ // DIP — createServer / createClient 依賴抽象工廠,非直接 import
20
+ //
21
+ // 注意:需要 `opencode` CLI 安裝(npm install -g opencode-ai)
22
+ // 且 @ai-sdk/openai-compatible 套件可被 OpenCode 存取
23
+
24
+ import { createOpencodeServer, createOpencodeClient } from '@opencode-ai/sdk';
25
+
26
+ // ── 常數 ───────────────────────────────────────────────────────────────────
27
+
28
+ const NCHC_BASE_URL = 'https://portal.genai.nchc.org.tw/api/v1';
29
+ export const OPENCODE_NCHC_PROVIDER_ID = 'nchc';
30
+ export const OPENCODE_NCHC_MODEL_ID = 'Mistral-Large-3-675B-Instruct-2512';
31
+ const DEFAULT_SERVER_PORT = 14096;
32
+ const DEFAULT_MAX_WAIT_MS = 120_000; // 2 分鐘(agentic coding 可能需較長時間)
33
+ const DEFAULT_SERVER_START_TIMEOUT = 15_000;
34
+
35
+ // ── NCHC Provider 設定(DDD Value Object) ────────────────────────────────
36
+
37
+ /**
38
+ * 建立 OpenCode NCHC provider 設定物件
39
+ * 傳入 createOpencodeServer({ config: buildNchcConfig(apiKey) }) 使用
40
+ *
41
+ * @param {string} apiKey - NCHC GenAI API Key
42
+ * @returns {object} OpenCode Config 物件(符合 types.gen.d.ts Config 型別)
43
+ */
44
+ export function buildNchcConfig(apiKey) {
45
+ return {
46
+ providers: {
47
+ [OPENCODE_NCHC_PROVIDER_ID]: {
48
+ npm: '@ai-sdk/openai-compatible',
49
+ name: 'NCHC GenAI',
50
+ options: {
51
+ baseURL: NCHC_BASE_URL,
52
+ apiKey,
53
+ },
54
+ models: {
55
+ [OPENCODE_NCHC_MODEL_ID]: {
56
+ name: 'Mistral Large 3 675B',
57
+ tool_call: true,
58
+ },
59
+ },
60
+ },
61
+ },
62
+ };
63
+ }
64
+
65
+ // ── OpencodeAgent ─────────────────────────────────────────────────────────
66
+
67
+ /**
68
+ * OpenCode SDK Agentic Coding 代理
69
+ *
70
+ * 透過 @opencode-ai/sdk 的 REST + SSE API 與 OpenCode server 溝通,
71
+ * 讓 OpenCode 以 NCHC 675B 為 LLM 執行 agentic coding 任務。
72
+ *
73
+ * @example
74
+ * // 自動啟動嵌入式 OpenCode server(需 opencode CLI)
75
+ * const agent = new OpencodeAgent();
76
+ * for await (const event of agent.run('寫一個 hello.mjs 並執行')) {
77
+ * if (event.type === 'delta') process.stdout.write(event.text);
78
+ * if (event.type === 'done') console.log('\n完成,共', event.turns, '輪');
79
+ * }
80
+ *
81
+ * @example
82
+ * // 連接到現有的 OpenCode server
83
+ * const agent = new OpencodeAgent({ serverUrl: 'http://localhost:4096' });
84
+ */
85
+ export class OpencodeAgent {
86
+ /**
87
+ * @param {object} [opts]
88
+ * @param {string} [opts.nchcApiKey] - NCHC GenAI API Key(預設 NCHC_GENAI_API_KEY)
89
+ * @param {string} [opts.providerID] - OpenCode provider ID(預設 'nchc')
90
+ * @param {string} [opts.modelID] - 模型 ID(預設 675B)
91
+ * @param {string} [opts.cwd] - 工作目錄(OpenCode session 目錄)
92
+ * @param {number} [opts.port] - 嵌入式 server 埠號(預設 14096)
93
+ * @param {number} [opts.maxWaitMs] - 等待回應最大毫秒數(預設 120000)
94
+ * @param {string} [opts.serverUrl] - 使用現有 server URL(跳過自動啟動)
95
+ * @param {object} [opts._factory] - 注入假工廠(測試用,DIP)
96
+ * _factory.createServer(opts) → Promise<{url, close}>
97
+ * _factory.createClient(cfg) → OpencodeClient
98
+ */
99
+ constructor(opts = {}) {
100
+ const apiKey = opts.nchcApiKey || process.env.NCHC_GENAI_API_KEY;
101
+ if (!apiKey && !opts._factory) {
102
+ throw new Error(
103
+ 'OpencodeAgent: 需要 NCHC GenAI API Key。\n' +
104
+ '請設定環境變數 NCHC_GENAI_API_KEY 或傳入 nchcApiKey 參數。'
105
+ );
106
+ }
107
+
108
+ this.apiKey = apiKey;
109
+ this.providerID = opts.providerID || OPENCODE_NCHC_PROVIDER_ID;
110
+ this.modelID = opts.modelID || OPENCODE_NCHC_MODEL_ID;
111
+ this.cwd = opts.cwd || process.cwd();
112
+ this.port = opts.port || DEFAULT_SERVER_PORT;
113
+ this.maxWaitMs = opts.maxWaitMs || DEFAULT_MAX_WAIT_MS;
114
+ this.serverUrl = opts.serverUrl || null; // 若有值則跳過自動啟動
115
+
116
+ // DIP:測試時注入假工廠以隔離 HTTP 呼叫
117
+ this._factory = opts._factory || {
118
+ createServer: createOpencodeServer,
119
+ createClient: (cfg) => createOpencodeClient(cfg),
120
+ };
121
+ }
122
+
123
+ /**
124
+ * 執行 Agentic Coding 任務(AsyncGenerator)
125
+ *
126
+ * @param {string} task - 任務描述
127
+ * @yields {AgentEvent}
128
+ * { type: 'session_created', sessionID } — 會話建立成功
129
+ * { type: 'delta', text } — 串流文字片段
130
+ * { type: 'text', text } — 完整回應(累積)
131
+ * { type: 'done', text, timedOut } — 任務完成
132
+ */
133
+ async *run(task) {
134
+ const ac = new AbortController();
135
+ let ownedServer = false;
136
+
137
+ try {
138
+ // 1. 啟動或連接 OpenCode server ──────────────────────────────────────
139
+ let serverUrl = this.serverUrl;
140
+
141
+ if (!serverUrl) {
142
+ const config = buildNchcConfig(this.apiKey);
143
+ const srv = await this._factory.createServer({
144
+ port: this.port,
145
+ signal: ac.signal,
146
+ config,
147
+ timeout: DEFAULT_SERVER_START_TIMEOUT,
148
+ });
149
+ serverUrl = srv.url;
150
+ ownedServer = true;
151
+ }
152
+
153
+ const client = this._factory.createClient({ baseUrl: serverUrl });
154
+
155
+ // 2. 訂閱全域 SSE 事件(在 session 建立前訂閱,避免錯過早期事件)
156
+ const { stream } = await client.event.subscribe({ directory: this.cwd });
157
+
158
+ // 3. 建立 session(自動批准所有工具呼叫權限)
159
+ const sessionRes = await client.session.create({
160
+ directory: this.cwd,
161
+ permission: [
162
+ { permission: '*', pattern: '*', action: 'allow' },
163
+ ],
164
+ });
165
+
166
+ const sessionID = sessionRes.data?.id;
167
+ if (!sessionID) {
168
+ throw new Error(
169
+ 'OpencodeAgent: 無法建立 OpenCode session。\n' +
170
+ `API 回應:${JSON.stringify(sessionRes)}`
171
+ );
172
+ }
173
+
174
+ yield { type: 'session_created', sessionID };
175
+
176
+ // 4. 發送任務(指定 NCHC 675B model)──────────────────────────────────
177
+ await client.session.prompt({
178
+ sessionID,
179
+ directory: this.cwd,
180
+ parts: [{ type: 'text', text: task }],
181
+ model: {
182
+ providerID: this.providerID,
183
+ modelID: this.modelID,
184
+ },
185
+ });
186
+
187
+ // 5. 監聽 SSE 事件流直到 session.idle ────────────────────────────────
188
+ let fullText = '';
189
+ const deadline = Date.now() + this.maxWaitMs;
190
+
191
+ for await (const event of stream) {
192
+ // 逾時保護
193
+ if (Date.now() > deadline) {
194
+ yield { type: 'text', text: fullText };
195
+ yield { type: 'done', text: fullText, timedOut: true };
196
+ return;
197
+ }
198
+
199
+ const { type, properties } = event ?? {};
200
+ if (!type) continue;
201
+
202
+ // 文字串流 delta(session 範圍過濾)
203
+ if (
204
+ type === 'message.part.delta' &&
205
+ properties?.sessionID === sessionID
206
+ ) {
207
+ const delta = properties.delta ?? '';
208
+ if (delta) {
209
+ fullText += delta;
210
+ yield { type: 'delta', text: delta };
211
+ }
212
+ }
213
+
214
+ // 任務完成
215
+ if (
216
+ type === 'session.idle' &&
217
+ properties?.sessionID === sessionID
218
+ ) {
219
+ yield { type: 'text', text: fullText };
220
+ yield { type: 'done', text: fullText, timedOut: false };
221
+ return;
222
+ }
223
+
224
+ // 錯誤處理
225
+ if (
226
+ type === 'session.error' &&
227
+ (properties?.sessionID === sessionID || !properties?.sessionID)
228
+ ) {
229
+ throw new Error(
230
+ `OpencodeAgent session error: ${JSON.stringify(properties)}`
231
+ );
232
+ }
233
+ }
234
+
235
+ } finally {
236
+ // 只有自己啟動的 server 才需要關閉
237
+ if (ownedServer) ac.abort();
238
+ }
239
+ }
240
+ }
@@ -0,0 +1,111 @@
1
+ // lib/flows/openrouter-ask.mjs — OpenRouter 提問流程(Application Layer)
2
+ // 職責: 組裝 adapter + 格式化輸出
3
+ // 原則: KISS — 流程越簡單越好,複雜邏輯在 adapter
4
+
5
+ import { createLLM } from '../core/llm.mjs';
6
+
7
+ /**
8
+ * 執行 OpenRouter 提問流程(非串流)
9
+ * @param {object} params
10
+ * @param {string} params.prompt - 使用者提問
11
+ * @param {string} [params.systemInstruction] - 系統提示詞
12
+ * @param {string} [params.model] - 模型名稱(預設 openai/gpt-4o-mini)
13
+ * @param {boolean} [params.webSearch] - 啟用網路搜尋
14
+ * @param {number} [params.maxResults] - 搜尋最大結果數
15
+ * @param {number} [params.maxTokens] - 最大輸出 tokens
16
+ * @param {number} [params.temperature] - 溫度
17
+ * @returns {Promise<LlmResponse>}
18
+ */
19
+ export async function openrouterAsk({
20
+ prompt,
21
+ systemInstruction,
22
+ model,
23
+ webSearch = false,
24
+ maxResults = 5,
25
+ maxTokens,
26
+ temperature,
27
+ }) {
28
+ const adapter = await createLLM({
29
+ provider: 'openrouter',
30
+ model,
31
+ webSearch,
32
+ maxResults,
33
+ maxTokens,
34
+ temperature,
35
+ });
36
+ return adapter.generateContent({ prompt, systemInstruction });
37
+ }
38
+
39
+ /**
40
+ * 串流執行 OpenRouter 提問流程(AsyncGenerator)
41
+ * yield* 委託 adapter.generateContentStream()
42
+ * @param {object} params - 同 openrouterAsk 參數
43
+ * @yields {{ type: 'text', text: string } | { type: 'metadata', ... }}
44
+ */
45
+ export async function* openrouterAskStream({
46
+ prompt,
47
+ systemInstruction,
48
+ model,
49
+ webSearch = false,
50
+ maxResults = 5,
51
+ maxTokens,
52
+ temperature,
53
+ }) {
54
+ const adapter = await createLLM({
55
+ provider: 'openrouter',
56
+ model,
57
+ webSearch,
58
+ maxResults,
59
+ maxTokens,
60
+ temperature,
61
+ });
62
+ yield* adapter.generateContentStream({ prompt, systemInstruction });
63
+ }
64
+
65
+ /**
66
+ * 格式化回應為純文字(含引證 + 效能)
67
+ * @param {object} result - openrouterAsk 回應
68
+ * @returns {string} 格式化文字
69
+ */
70
+ export function formatTextOutput(result) {
71
+ const lines = [result.text];
72
+
73
+ if (result.sources?.length > 0) {
74
+ lines.push('');
75
+ lines.push('--- 引證來源 ---');
76
+ for (const src of result.sources) {
77
+ lines.push(` [${src.title || src.uri}] ${src.uri}`);
78
+ }
79
+ }
80
+
81
+ if (result.perf) {
82
+ lines.push('');
83
+ lines.push('--- 效能測量 ---');
84
+ lines.push(` 延遲: ${result.perf.latencySec}s`);
85
+ if (result.perf.ttftSec !== undefined) {
86
+ lines.push(` TTFT: ${result.perf.ttftSec}s`);
87
+ }
88
+ lines.push(` 輸出速度: ${result.perf.outputTokensPerSec} tokens/s`);
89
+ lines.push(` token: prompt=${result.usage.promptTokens} output=${result.usage.outputTokens} total=${result.usage.totalTokens}`);
90
+ }
91
+
92
+ return lines.join('\n');
93
+ }
94
+
95
+ /**
96
+ * 格式化回應為 JSON(含效能指標)
97
+ * @param {object} result - openrouterAsk 回應
98
+ * @param {string} model - 模型名稱
99
+ * @param {string} elapsed - 總耗時
100
+ * @returns {string} JSON 字串
101
+ */
102
+ export function formatJsonOutput(result, model, elapsed) {
103
+ return JSON.stringify({
104
+ _meta: { command: 'openrouter ask', model: result.model || model, elapsed },
105
+ text: result.text,
106
+ sources: result.sources,
107
+ usage: result.usage,
108
+ perf: result.perf || null,
109
+ finishReason: result.finishReason || null,
110
+ }, null, 2);
111
+ }
@@ -0,0 +1,18 @@
1
+ // lib/flows/review-doc.mjs — 文件審閱業務流程
2
+ // 串接:doc/ingest → search → 格式化
3
+
4
+ /**
5
+ * 執行文件審閱流程
6
+ * 1. 匯入 PDF 到 SQLite
7
+ * 2. 全文檢索相關段落
8
+ * 3. 格式化輸出
9
+ *
10
+ * @param {object} opts
11
+ * @param {string} opts.project - 專案名稱
12
+ * @param {string[]} opts.files - PDF 檔案路徑
13
+ * @param {string} [opts.query] - 搜尋關鍵字
14
+ */
15
+ export async function reviewDoc(opts = {}) {
16
+ // TODO: 實作完整文件審閱流程
17
+ throw new Error('reviewDoc flow 尚未實作');
18
+ }
@@ -0,0 +1,6 @@
1
+ // lib/ocr/index.mjs — OCR 辨識模組
2
+ // TODO: 實作 PDF/圖片 OCR 辨識
3
+
4
+ export async function recognize(filePath, opts = {}) {
5
+ throw new Error('ocr.recognize() 尚未實作');
6
+ }
@@ -0,0 +1,6 @@
1
+ // lib/portal/hatch.mjs — 全自動孵化
2
+ // TODO: 實作端到端文件處理自動化流程
3
+
4
+ export async function hatch(config) {
5
+ throw new Error('portal.hatch() 尚未實作');
6
+ }
@@ -0,0 +1,6 @@
1
+ // lib/portal/index.mjs — 智慧入口
2
+ // TODO: 實作文件智慧分類與路由
3
+
4
+ export async function classify(input) {
5
+ throw new Error('portal.classify() 尚未實作');
6
+ }
@@ -0,0 +1,55 @@
1
+ // lib/prompt/prompt-search.mjs — 加權評分模糊搜尋
2
+ // 依 slug → alias → title → tag → series → content 順序加權比對
3
+
4
+ const WEIGHTS = { slug: 10, alias: 8, title: 6, tag: 5, series: 4, content: 1 };
5
+
6
+ /**
7
+ * 加權模糊搜尋
8
+ * @param {Array<{ slug, meta, body }>} prompts - 所有提示詞
9
+ * @param {string} query - 搜尋關鍵字
10
+ * @param {{ tag?: string }} [opts] - 過濾選項
11
+ * @returns {Array<{ prompt, score }>} 按分數降序排列
12
+ */
13
+ export function searchPrompts(prompts, query, opts = {}) {
14
+ const q = query.toLowerCase();
15
+ const results = [];
16
+
17
+ for (const p of prompts) {
18
+ // --tag 前置過濾
19
+ if (opts.tag) {
20
+ const tags = (p.meta.tags || []).map(t => t.toLowerCase());
21
+ if (!tags.some(t => t.includes(opts.tag.toLowerCase()))) continue;
22
+ }
23
+
24
+ let score = 0;
25
+ const slug = p.slug.toLowerCase();
26
+ const aliases = (p.meta.aliases || []).map(a => a.toLowerCase());
27
+ const title = (p.meta.title || '').toLowerCase();
28
+ const tags = (p.meta.tags || []).map(t => t.toLowerCase());
29
+ const series = (p.meta.series || '').toLowerCase();
30
+ const body = p.body.toLowerCase();
31
+
32
+ // slug 精確匹配 → 滿分;部分匹配 → 6 分
33
+ if (slug === q) score += WEIGHTS.slug;
34
+ else if (slug.includes(q)) score += 6;
35
+
36
+ // alias 包含查詢(讓「切」匹配 alias「切片」)
37
+ if (aliases.some(a => a.includes(q) || q.includes(a))) score += WEIGHTS.alias;
38
+
39
+ // title 包含
40
+ if (title.includes(q)) score += WEIGHTS.title;
41
+
42
+ // tag 匹配
43
+ if (tags.some(t => t.includes(q) || q.includes(t))) score += WEIGHTS.tag;
44
+
45
+ // series 匹配
46
+ if (series.includes(q)) score += WEIGHTS.series;
47
+
48
+ // 全文內容包含
49
+ if (body.includes(q)) score += WEIGHTS.content;
50
+
51
+ if (score > 0) results.push({ prompt: p, score });
52
+ }
53
+
54
+ return results.sort((a, b) => b.score - a.score);
55
+ }
@@ -0,0 +1,94 @@
1
+ // lib/prompt/prompt-store.mjs — 提示詞 Repository 層
2
+ // 掃描 lib/prompt/prompts/ 目錄,解析 YAML frontmatter,提供 listPrompts / getPrompt
3
+
4
+ import fs from 'fs';
5
+ import path from 'path';
6
+ import { root } from '../core/paths.mjs';
7
+
8
+ /** 提示詞目錄(相對於專案根目錄) */
9
+ const PROMPTS_DIR = () => path.join(root(), 'lib', 'prompt', 'prompts');
10
+
11
+ /**
12
+ * 零依賴 YAML frontmatter 解析
13
+ * 支援 key: value 與 key: [a, b, c] 陣列語法
14
+ * @param {string} content - .md 檔案原始內容
15
+ * @returns {{ meta: object, body: string }}
16
+ */
17
+ export function parseFrontmatter(content) {
18
+ const match = content.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
19
+ if (!match) return { meta: {}, body: content };
20
+
21
+ const meta = {};
22
+ for (const line of match[1].split('\n')) {
23
+ const kv = line.match(/^(\w[\w-]*):\s*(.+)$/);
24
+ if (!kv) continue;
25
+ const [, key, rawVal] = kv;
26
+ // YAML 陣列: [a, b, c]
27
+ if (rawVal.startsWith('[') && rawVal.endsWith(']')) {
28
+ meta[key] = rawVal.slice(1, -1).split(',').map(s => s.trim()).filter(Boolean);
29
+ } else {
30
+ meta[key] = rawVal.trim();
31
+ }
32
+ }
33
+ return { meta, body: match[2] };
34
+ }
35
+
36
+ /**
37
+ * 遞迴掃描目錄下所有 .md 檔案
38
+ * @param {string} dir - 起始目錄
39
+ * @param {string} baseDir - 根目錄(用於計算相對路徑)
40
+ * @returns {string[]} 相對路徑陣列
41
+ */
42
+ function scanMarkdownFiles(dir, baseDir) {
43
+ const results = [];
44
+ if (!fs.existsSync(dir)) return results;
45
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
46
+ const fullPath = path.join(dir, entry.name);
47
+ if (entry.isDirectory()) {
48
+ results.push(...scanMarkdownFiles(fullPath, baseDir));
49
+ } else if (entry.name.endsWith('.md') && entry.name !== 'README.md') {
50
+ results.push(path.relative(baseDir, fullPath));
51
+ }
52
+ }
53
+ return results.sort();
54
+ }
55
+
56
+ /**
57
+ * 載入並解析單一提示詞檔案
58
+ * @param {string} relPath - 相對於 prompts/ 的路徑(如 zero-framework/coding.md)
59
+ * @param {string} baseDir - prompts/ 目錄絕對路徑
60
+ * @returns {{ slug, meta, body, filePath, series }}
61
+ */
62
+ function loadPromptFile(relPath, baseDir) {
63
+ const fullPath = path.join(baseDir, relPath);
64
+ const content = fs.readFileSync(fullPath, 'utf-8');
65
+ const { meta, body } = parseFrontmatter(content);
66
+ const slug = path.basename(relPath, '.md');
67
+ const series = path.dirname(relPath) === '.' ? null : path.dirname(relPath);
68
+ return {
69
+ slug,
70
+ meta: { ...meta, series: meta.series || series },
71
+ body: body.trim(),
72
+ filePath: fullPath,
73
+ };
74
+ }
75
+
76
+ /**
77
+ * 列出所有提示詞
78
+ * @returns {Array<{ slug, meta, body, filePath }>}
79
+ */
80
+ export function listPrompts() {
81
+ const baseDir = PROMPTS_DIR();
82
+ const files = scanMarkdownFiles(baseDir, baseDir);
83
+ return files.map(f => loadPromptFile(f, baseDir));
84
+ }
85
+
86
+ /**
87
+ * 取得單一提示詞(精確 slug 匹配檔名)
88
+ * @param {string} slug - 提示詞 slug(如 'coding')
89
+ * @returns {{ slug, meta, body, filePath } | null}
90
+ */
91
+ export function getPrompt(slug) {
92
+ const all = listPrompts();
93
+ return all.find(p => p.slug === slug) || null;
94
+ }
@@ -0,0 +1,15 @@
1
+ ---
2
+ title: 零框架扣頂 coding
3
+ series: zero-framework
4
+ tags: [換位思考, 三方案, 六軟工, BDD, TDD, SOLID, DRY, KISS, DDD, HTTPS代理, 待辦]
5
+ aliases: [coding, 扣頂, 寫程式, 開發]
6
+ ---
7
+
8
+ # 零框架扣頂 coding
9
+
10
+ - **換位思考**:先掃描資料與提問,推理需求背後動機,具體列出 5 個潛在動機
11
+ - **新文件**:根據提問與需求上網查詢最新 SDK API 文件,因為隨時都在變化
12
+ - **三方案**:提出 3 替代方案多維度打分,以使用者為中心選最適合方案
13
+ - **六軟工**:採用 BDD TDD SOLID DRY KISS DDD 實作
14
+ - **待辦**:在 `./todo/` 設計 DAG todo list,盡力平行子代理實作,追蹤測試完整
15
+ - **HTTPS 代理**:系統有機率在沙盒 HTTPS proxy 環境變數之下執行,必須相容
@@ -0,0 +1,12 @@
1
+ ---
2
+ title: 零框架搜尋
3
+ series: zero-framework
4
+ tags: [搜尋, grounding, 引證, URL, 網路搜尋]
5
+ aliases: [search, 搜尋, 查詢, 找資料]
6
+ ---
7
+
8
+ # 零框架搜尋
9
+
10
+ - **搜尋接地**:啟用 Google Search grounding,確保回應以真實搜尋結果為依據
11
+ - **引證 URL**:回應必須包含引證來源的 URL,讓使用者可驗證
12
+ - **零幻覺**:不捏造不存在的資料,所有事實陳述須有搜尋結果佐證
@@ -0,0 +1,11 @@
1
+ ---
2
+ title: 零框架切片
3
+ series: zero-framework
4
+ tags: [切片, 零幻覺引證, PDF, Office, 頁碼, 純文字]
5
+ aliases: [slice, 切片, 切頁, 文件處理]
6
+ ---
7
+
8
+ # 零框架切片
9
+
10
+ - **切片**:如果資料檔案是 PDF 或 Office,記得先切頁(保留頁碼可引證)轉為純文字才去處理
11
+ - **零幻覺引證**:回應須引證來源,包含:網址、檔案名稱、頁碼、時間點、不竄改的原始文字
@@ -0,0 +1,6 @@
1
+ // lib/search/crawler.mjs — 聯網爬取
2
+ // TODO: 實作網頁爬取與結構化擷取
3
+
4
+ export async function crawl(url, opts = {}) {
5
+ throw new Error('crawler.crawl() 尚未實作');
6
+ }
@@ -0,0 +1,7 @@
1
+ // lib/search/index.mjs — 搜尋模組入口
2
+ // 整合 FTS5 全文檢索與聯網搜尋
3
+
4
+ export { DocStore } from '../core/db.mjs';
5
+ export { crawl } from './crawler.mjs';
6
+
7
+ // TODO: 實作統一搜尋介面