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,175 @@
1
+ // bin/commands/prompt.mjs — prompt 指令群組(list / show / search)
2
+ // 無參數 → 列出全部提示詞
3
+ // 不認識的子指令 → 當關鍵字做全文模糊搜尋
4
+ import { timer, jsonOut, emitError, logProgress } from '../../lib/core/cli-utils.mjs';
5
+
6
+ /**
7
+ * prompt 指令主路由
8
+ *
9
+ * bh prompt → list(列出全部)
10
+ * bh prompt list → list
11
+ * bh prompt show <slug> → show
12
+ * bh prompt search <kw> → search
13
+ * bh prompt 零幻覺 → 自動搜尋「零幻覺」
14
+ *
15
+ * @param {string} subcommand - list / show / search / 或任意關鍵字
16
+ * @param {string[]} positionals - 位置參數
17
+ * @param {object} flags - CLI 旗標
18
+ */
19
+ export async function cmdPrompt(subcommand, positionals, flags) {
20
+ // --help(可能被 parseArgs 當成 subcommand)
21
+ if (!subcommand || subcommand === '--help' || subcommand === '-h') {
22
+ if (subcommand === '--help' || subcommand === '-h' || flags.help) {
23
+ const { showCommandHelp } = await import('./help.mjs');
24
+ showCommandHelp('prompt'); return;
25
+ }
26
+ // 無參數 → 列出全部
27
+ await cmdPromptList(flags);
28
+ return;
29
+ }
30
+
31
+ switch (subcommand) {
32
+ case 'list': await cmdPromptList(flags); break;
33
+ case 'show': await cmdPromptShow(positionals, flags); break;
34
+ case 'search': await cmdPromptSearch(positionals, flags); break;
35
+ default:
36
+ // 不認識的子指令 → 把 subcommand + positionals 合併為搜尋關鍵字
37
+ await cmdPromptSearch([subcommand, ...positionals], flags);
38
+ }
39
+ }
40
+
41
+ /** list — 列出所有提示詞(依系列分組) */
42
+ async function cmdPromptList(flags) {
43
+ const elapsed = timer();
44
+ const format = flags.format || 'text';
45
+ const { listPrompts } = await import('../../lib/prompt/prompt-store.mjs');
46
+
47
+ const all = listPrompts();
48
+ const filtered = flags.series
49
+ ? all.filter(p => p.meta.series === flags.series)
50
+ : all;
51
+
52
+ if (format === 'json') {
53
+ const data = filtered.map(p => ({
54
+ slug: p.slug,
55
+ title: p.meta.title,
56
+ series: p.meta.series,
57
+ tags: p.meta.tags || [],
58
+ aliases: p.meta.aliases || [],
59
+ body: p.body,
60
+ }));
61
+ process.stdout.write(jsonOut('prompt list', { count: data.length, prompts: data }, elapsed()));
62
+ return;
63
+ }
64
+
65
+ for (let i = 0; i < filtered.length; i++) {
66
+ const p = filtered[i];
67
+ if (i > 0) process.stdout.write('\n---\n\n');
68
+ process.stdout.write(`[${p.meta.series || ''}/${p.slug}] ${p.meta.title || p.slug}\n\n`);
69
+ process.stdout.write(p.body + '\n');
70
+ }
71
+ }
72
+
73
+ /** show — 顯示單一提示詞全文 */
74
+ async function cmdPromptShow(positionals, flags) {
75
+ const elapsed = timer();
76
+ const format = flags.format || 'text';
77
+ const slug = positionals[0];
78
+
79
+ if (!slug) {
80
+ emitError('prompt show', '請提供提示詞 slug。用法: bh prompt show <slug>', format);
81
+ process.exit(1);
82
+ }
83
+
84
+ const { getPrompt, listPrompts } = await import('../../lib/prompt/prompt-store.mjs');
85
+ let prompt = getPrompt(slug);
86
+
87
+ // 智慧 fallback:精確 slug 找不到 → 自動轉搜尋
88
+ if (!prompt) {
89
+ const { searchPrompts } = await import('../../lib/prompt/prompt-search.mjs');
90
+ const all = listPrompts();
91
+ const results = searchPrompts(all, slug);
92
+
93
+ if (results.length === 0) {
94
+ emitError('prompt show', `找不到提示詞「${slug}」,搜尋也無結果`, format);
95
+ process.exit(1);
96
+ }
97
+
98
+ if (results.length === 1) {
99
+ prompt = results[0].prompt;
100
+ logProgress(`提示: slug「${slug}」不存在,自動搜尋命中「${prompt.slug}」`, flags);
101
+ } else {
102
+ // 多筆命中 → 列出候選
103
+ if (format === 'json') {
104
+ const data = results.map(r => ({ slug: r.prompt.slug, title: r.prompt.meta.title, score: r.score }));
105
+ process.stdout.write(jsonOut('prompt show', { match: 'multiple', candidates: data }, elapsed()));
106
+ } else {
107
+ process.stderr.write(`找不到精確 slug「${slug}」,搜尋到 ${results.length} 筆候選:\n`);
108
+ for (const r of results) {
109
+ process.stderr.write(` ${r.prompt.slug.padEnd(16)} ${r.prompt.meta.title} (分數: ${r.score})\n`);
110
+ }
111
+ process.stderr.write(`\n請使用精確 slug,例如: bh prompt show ${results[0].prompt.slug}\n`);
112
+ }
113
+ return;
114
+ }
115
+ }
116
+
117
+ if (format === 'json') {
118
+ const data = {
119
+ slug: prompt.slug,
120
+ title: prompt.meta.title,
121
+ series: prompt.meta.series,
122
+ tags: prompt.meta.tags || [],
123
+ aliases: prompt.meta.aliases || [],
124
+ body: prompt.body,
125
+ };
126
+ process.stdout.write(jsonOut('prompt show', data, elapsed()));
127
+ } else {
128
+ process.stdout.write(prompt.body + '\n');
129
+ }
130
+ }
131
+
132
+ /** search — 模糊搜尋提示詞(加權全文比對) */
133
+ async function cmdPromptSearch(positionals, flags) {
134
+ const elapsed = timer();
135
+ const format = flags.format || 'text';
136
+ const query = positionals.join(' ');
137
+
138
+ if (!query) {
139
+ emitError('prompt search', '請提供搜尋關鍵字。用法: bh prompt search <關鍵字>', format);
140
+ process.exit(1);
141
+ }
142
+
143
+ const { listPrompts } = await import('../../lib/prompt/prompt-store.mjs');
144
+ const { searchPrompts } = await import('../../lib/prompt/prompt-search.mjs');
145
+
146
+ const all = listPrompts();
147
+ const results = searchPrompts(all, query, { tag: flags.tag });
148
+
149
+ if (format === 'json') {
150
+ const data = results.map(r => ({
151
+ slug: r.prompt.slug,
152
+ title: r.prompt.meta.title,
153
+ series: r.prompt.meta.series,
154
+ score: r.score,
155
+ tags: r.prompt.meta.tags || [],
156
+ body: r.prompt.body,
157
+ }));
158
+ process.stdout.write(jsonOut('prompt search', { query, count: data.length, results: data }, elapsed()));
159
+ return;
160
+ }
161
+
162
+ if (results.length === 0) {
163
+ process.stdout.write(`搜尋「${query}」:無結果\n`);
164
+ return;
165
+ }
166
+
167
+ logProgress(`搜尋「${query}」:${results.length} 筆結果`, flags);
168
+
169
+ for (let i = 0; i < results.length; i++) {
170
+ const r = results[i];
171
+ if (i > 0) process.stdout.write('\n---\n\n');
172
+ process.stdout.write(`[${r.prompt.meta.series || ''}/${r.prompt.slug}] ${r.prompt.meta.title}\n\n`);
173
+ process.stdout.write(r.prompt.body + '\n');
174
+ }
175
+ }
@@ -0,0 +1,258 @@
1
+ // bin/commands/schema.mjs — 所有指令的結構化 schema(AI/LLM 友善)
2
+ // 集中管理:方便 AI agent 讀取完整指令清單
3
+
4
+ export const COMMANDS = {
5
+ doc: {
6
+ name: 'doc',
7
+ description: '文件處理(拆頁、轉文字、入庫)— 支援 PDF 與 Office 全系列',
8
+ requires_sqlite: false,
9
+ subcommands: {
10
+ split: '通用 PDF 拆頁(每頁輸出為獨立 PDF)',
11
+ text: 'PDF 轉純文字(保留頁碼與排版,不需先拆頁)',
12
+ ingest: 'PDF/Office 入庫流水線(轉文字 → SQLite FTS5)[需 --experimental-sqlite]',
13
+ },
14
+ options: {
15
+ '--project=<名稱>': { type: 'string', default: 'nstc', description: '專案名稱' },
16
+ '--dir=<路徑>': { type: 'string', description: '掃描單一目錄內所有文件' },
17
+ '--dirs=<路徑1,路徑2>': { type: 'string', description: '掃描多個目錄(逗號分隔)' },
18
+ '--outdir=<路徑>': { type: 'string', description: '拆頁輸出目錄(split 指令用)' },
19
+ '--split-pdf': { type: 'boolean', default: false, description: '[ingest] 同時拆頁成獨立 PDF' },
20
+ '--recursive': { type: 'boolean', default: false, description: '遞迴掃描子目錄' },
21
+ '--force': { type: 'boolean', default: false, description: '強制重新入庫(忽略斷點續作紀錄)' },
22
+ '--concurrency=N': { type: 'integer', default: 5, description: '平行處理數' },
23
+ '--quiet': { type: 'boolean', default: false, description: '靜默模式(不輸出進度到 stderr,GNU parallel 友善)' },
24
+ '--print0': { type: 'boolean', default: false, description: 'NUL 分隔路徑輸出(與 xargs -0 相容)' },
25
+ '--format=json|text': { type: 'string', default: 'text', description: '輸出格式' },
26
+ },
27
+ examples: [
28
+ 'bh doc split file1.pdf file2.pdf --project=nstc',
29
+ 'bh doc text --dir=projects/nstc/output/pdfs',
30
+ 'node --experimental-sqlite bin/bh.mjs doc ingest --dir=projects/nstc/pdfs --project=nstc',
31
+ 'node --experimental-sqlite bin/bh.mjs doc ingest --dirs=pdfs/,docs/ --project=nstc --recursive',
32
+ 'node --experimental-sqlite bin/bh.mjs doc ingest file.docx report.xlsx --project=nstc',
33
+ 'node --experimental-sqlite bin/bh.mjs doc ingest --dir=pdfs/ --force --split-pdf --project=nstc',
34
+ 'ls pdfs/*.pdf | node --experimental-sqlite bin/bh.mjs doc ingest --project=nstc',
35
+ 'find . -name "*.pdf" -print0 | xargs -0 -P 4 node bin/bh.mjs doc text --quiet',
36
+ ],
37
+ },
38
+ writing: {
39
+ name: 'writing',
40
+ description: '公文撰寫(生成 PDF)',
41
+ requires_sqlite: false,
42
+ subcommands: {
43
+ generate: '平行生成 PDF 文件(DAG 依賴感知批次處理)',
44
+ },
45
+ options: {
46
+ '--project=<名稱>': { type: 'string', default: 'nstc', description: '專案名稱' },
47
+ '--concurrency=N': { type: 'integer', default: 10, description: '平行並發數' },
48
+ '--ids=1,2,3': { type: 'string', description: '指定文件 ID(逗號分隔)' },
49
+ '--format=json|text': { type: 'string', default: 'text', description: '輸出格式' },
50
+ },
51
+ examples: [
52
+ 'bh writing generate --project=nstc',
53
+ 'bh writing generate --project=nstc --ids=1,2,3',
54
+ ],
55
+ },
56
+ dag: {
57
+ name: 'dag',
58
+ description: 'DAG 依賴追蹤管理',
59
+ requires_sqlite: false,
60
+ subcommands: {
61
+ init: '初始化 DAG 狀態',
62
+ status: '顯示整體 DAG 進度摘要',
63
+ ready: '列出目前可執行的任務',
64
+ list: '列出所有任務及其狀態',
65
+ },
66
+ options: {
67
+ '--project=<名稱>': { type: 'string', default: 'nstc', description: '專案名稱' },
68
+ '--format=json|text': { type: 'string', default: 'text', description: '輸出格式' },
69
+ },
70
+ examples: [
71
+ 'bh dag init --project=nstc',
72
+ 'bh dag status --project=nstc',
73
+ 'bh dag ready --format=json --project=nstc',
74
+ ],
75
+ },
76
+ search: {
77
+ name: 'search',
78
+ description: 'FTS5 trigram 全文檢索',
79
+ requires_sqlite: true,
80
+ subcommands: { query: '搜尋關鍵字' },
81
+ options: {
82
+ '--project=<名稱>': { type: 'string', default: 'nstc', description: '專案名稱' },
83
+ '--db=<DB路徑>': { type: 'string', description: 'SQLite 資料庫路徑' },
84
+ '--limit=N': { type: 'integer', description: '限制結果數量' },
85
+ '--format=json|text': { type: 'string', default: 'text', description: '輸出格式' },
86
+ },
87
+ examples: ['bh search query "研究計畫" --project=nstc'],
88
+ },
89
+ db: {
90
+ name: 'db',
91
+ description: 'SQLite 資料庫管理',
92
+ requires_sqlite: true,
93
+ subcommands: {
94
+ stats: '顯示 DB 統計',
95
+ docs: '列出所有文件 metadata',
96
+ 'pages <doc_id>': '列出指定文件的所有頁面',
97
+ },
98
+ options: {
99
+ '--project=<名稱>': { type: 'string', default: 'nstc', description: '專案名稱' },
100
+ '--db=<DB路徑>': { type: 'string', description: 'SQLite 路徑' },
101
+ '--format=json|text': { type: 'string', default: 'text', description: '輸出格式' },
102
+ },
103
+ examples: ['bh db stats --project=nstc', 'bh db docs --format=json --project=nstc'],
104
+ },
105
+ gemini: {
106
+ name: 'gemini',
107
+ description: 'Gemini 3 Flash Preview AI 提問(thinking + grounding + url context)',
108
+ requires_sqlite: false,
109
+ subcommands: { ask: '向 Gemini 提問(預設啟用深度思考、搜尋接地、URL 擷取)' },
110
+ options: {
111
+ '--project=<名稱>': { type: 'string', default: 'nstc', description: '專案名稱' },
112
+ '--provider=<名稱>': { type: 'string', default: 'auto', description: 'LLM provider (auto|gemini|gemini-api)' },
113
+ '--thinking=<等級>': { type: 'string', default: 'MEDIUM', description: '思考等級 (NONE|MINIMAL|LOW|MEDIUM|HIGH)' },
114
+ '--system-prompt=<文字>': { type: 'string', default: '', description: '系統提示詞' },
115
+ '--no-grounding': { type: 'boolean', default: false, description: '停用 Google Search 接地' },
116
+ '--no-url-context': { type: 'boolean', default: false, description: '停用 URL 內容擷取' },
117
+ '--no-stream': { type: 'boolean', default: false, description: '停用串流(等待完整回應)' },
118
+ '--format=json|text': { type: 'string', default: 'text', description: '輸出格式' },
119
+ },
120
+ examples: [
121
+ 'bh gemini ask "量子糾纏的原理是什麼?"',
122
+ 'bh gemini ask "2026 台灣經濟預測" --format=json',
123
+ 'echo "解釋 AI" | bh gemini ask',
124
+ ],
125
+ },
126
+ nchc: {
127
+ name: 'nchc',
128
+ description: '國網 GenAI AI 提問(Mistral 系列,OpenAI 相容介面)',
129
+ requires_sqlite: false,
130
+ subcommands: {
131
+ ask: '向國網 GenAI 提問(支援串流輸出)',
132
+ agent: '啟動 Agentic AI 代理(工具呼叫、多輪對話)',
133
+ code: '啟動 Agentic Coding 代理(@opencode-ai/sdk + 675B,需 opencode CLI)',
134
+ models: '列出可用模型',
135
+ },
136
+ options: {
137
+ '--model=<名稱>': { type: 'string', default: 'Mistral-Large-3-675B-Instruct-2512', description: '模型名稱' },
138
+ '--system-prompt=<文字>': { type: 'string', default: '', description: '系統提示詞' },
139
+ '--max-tokens=N': { type: 'integer', default: 8192, description: '最大輸出 tokens' },
140
+ '--temperature=N': { type: 'number', default: 0.7, description: '溫度(0.0~1.0)' },
141
+ '--no-stream': { type: 'boolean', default: false, description: '停用串流' },
142
+ '--max-turns=N': { type: 'integer', default: 20, description: '[agent] 最大迴圈次數' },
143
+ '--format=json|text': { type: 'string', default: 'text', description: '輸出格式' },
144
+ },
145
+ examples: [
146
+ 'bh nchc ask "台灣半導體產業分析"',
147
+ 'bh nchc agent "列出 lib/ 目錄下所有 .mjs 檔案並說明架構"',
148
+ 'bh nchc models',
149
+ ],
150
+ },
151
+ openrouter: {
152
+ name: 'openrouter',
153
+ description: 'OpenRouter AI 提問(400+ 模型,支援網路搜尋)',
154
+ requires_sqlite: false,
155
+ subcommands: {
156
+ ask: '向 OpenRouter 任意模型提問(支援串流 + 網路搜尋)',
157
+ models: '列出常用模型(含網路搜尋支援說明)',
158
+ },
159
+ options: {
160
+ '--model=<名稱>': { type: 'string', default: 'openai/gpt-4o-mini', description: '模型名稱(任意 OpenRouter 模型 ID,可加 :online 後綴)' },
161
+ '--web-search': { type: 'boolean', default: false, description: '啟用網路搜尋(plugins: web,適用所有模型)' },
162
+ '--max-results=N': { type: 'integer', default: 5, description: '搜尋最大結果數(--web-search 時有效)' },
163
+ '--system-prompt=<文字>': { type: 'string', default: '', description: '系統提示詞' },
164
+ '--max-tokens=N': { type: 'integer', default: 8192, description: '最大輸出 tokens' },
165
+ '--temperature=N': { type: 'number', default: 0.7, description: '溫度(0.0~2.0)' },
166
+ '--no-stream': { type: 'boolean', default: false, description: '停用串流(等待完整回應)' },
167
+ '--quiet': { type: 'boolean', default: false, description: '靜默模式(不輸出進度到 stderr,GNU parallel 友善)' },
168
+ '--format=json|text': { type: 'string', default: 'text', description: '輸出格式(json 含 sources、usage、perf,適合 Agentic AI)' },
169
+ },
170
+ examples: [
171
+ 'bh openrouter ask "台灣今日重要新聞" --web-search',
172
+ 'bh openrouter ask "分析 React vs Vue" --model=anthropic/claude-sonnet-4-6',
173
+ 'bh openrouter ask "最新 AI 研究" --model=perplexity/sonar-pro --format=json',
174
+ 'echo "解釋量子糾纏" | bh openrouter ask --model=openai/gpt-4o',
175
+ 'bh openrouter ask "整理今天新聞" --web-search --quiet --format=json | jq .text',
176
+ 'bh openrouter models',
177
+ ],
178
+ },
179
+ legal: {
180
+ name: 'legal',
181
+ description: '法律查詢(AI Router + 兩步驟提示工程 + SQLite 快取)[需 --experimental-sqlite]',
182
+ requires_sqlite: true,
183
+ subcommands: {
184
+ ask: '法律問題查詢(Mistral 14B 路由 → 快取命中或 Gemini Pro 搜尋法規 → 法律分析)',
185
+ cache: '快取管理(list: 列出快取, stats: 統計)',
186
+ },
187
+ options: {
188
+ '--project=<名稱>': { type: 'string', default: 'legal-cache', description: '專案名稱(決定 SQLite 路徑)' },
189
+ '--skip-cache': { type: 'boolean', default: false, description: '強制跳過快取,直接呼叫大模型' },
190
+ '--skip-save': { type: 'boolean', default: false, description: '不將結果存入快取' },
191
+ '--limit=N': { type: 'integer', default: 20, description: '[cache list] 顯示筆數上限' },
192
+ '--quiet': { type: 'boolean', default: false, description: '靜默模式(不輸出進度到 stderr)' },
193
+ '--format=json|text': { type: 'string', default: 'text', description: '輸出格式' },
194
+ },
195
+ examples: [
196
+ 'node --experimental-sqlite bin/bh.mjs legal ask "房東可以隨意趕走我嗎?"',
197
+ 'node --experimental-sqlite bin/bh.mjs legal ask "勞工被無故解雇有哪些保護?" --format=json',
198
+ 'echo "我的房東說要漲租金,我有辦法拒絕嗎?" | node --experimental-sqlite bin/bh.mjs legal ask',
199
+ 'node --experimental-sqlite bin/bh.mjs legal cache list --project=legal-cache',
200
+ 'node --experimental-sqlite bin/bh.mjs legal cache stats',
201
+ 'node --experimental-sqlite bin/bh.mjs legal ask "問題" --skip-cache --project=legal-cache',
202
+ ],
203
+ },
204
+ prompt: {
205
+ name: 'prompt',
206
+ description: '提示詞管理(無參數=全列出,帶關鍵字=模糊搜尋)— 零框架提示系列',
207
+ requires_sqlite: false,
208
+ subcommands: {
209
+ '(無)': '列出所有提示詞(依系列分組)',
210
+ '<關鍵字>': '模糊搜尋提示詞(加權全文比對)',
211
+ list: '列出所有提示詞(同無參數)',
212
+ show: '顯示指定提示詞全文(支援智慧 fallback 搜尋)',
213
+ search: '模糊搜尋提示詞(同 <關鍵字>)',
214
+ },
215
+ options: {
216
+ '--series=<名稱>': { type: 'string', description: '[list] 過濾指定系列' },
217
+ '--tag=<標籤>': { type: 'string', description: '[search] 按標籤過濾' },
218
+ '--format=json|text': { type: 'string', default: 'text', description: '輸出格式' },
219
+ '--quiet': { type: 'boolean', default: false, description: '靜默模式' },
220
+ },
221
+ examples: [
222
+ 'bh prompt # 列出全部提示詞',
223
+ 'bh prompt 零幻覺 # 模糊搜尋「零幻覺」',
224
+ 'bh prompt BDD # 搜尋含 BDD 的提示',
225
+ 'bh prompt list --series=zero-framework',
226
+ 'bh prompt show coding',
227
+ 'bh prompt show coding | pbcopy',
228
+ 'bh prompt show coding --format=json',
229
+ 'bh prompt search 寫程式',
230
+ 'bh prompt show 切 # 智慧 fallback:slug 不存在 → 自動搜尋',
231
+ ],
232
+ },
233
+ portal: {
234
+ name: 'portal',
235
+ description: '智慧入口(預留)',
236
+ requires_sqlite: false,
237
+ subcommands: { status: '顯示入口狀態' },
238
+ options: {},
239
+ examples: ['bh portal status'],
240
+ },
241
+ ocr: {
242
+ name: 'ocr',
243
+ description: 'OCR 辨識(預留)',
244
+ requires_sqlite: false,
245
+ subcommands: { scan: '掃描文件' },
246
+ options: {},
247
+ examples: ['bh ocr scan file.pdf'],
248
+ },
249
+ commands: {
250
+ name: 'commands',
251
+ description: '列出所有指令的結構化 schema(AI/LLM 友善)',
252
+ requires_sqlite: false,
253
+ options: {
254
+ '--format=json|text': { type: 'string', default: 'json', description: '輸出格式' },
255
+ },
256
+ examples: ['bh commands', "bh commands --format=json | jq 'keys'"],
257
+ },
258
+ };
@@ -0,0 +1,46 @@
1
+ // bin/commands/search.mjs — search 指令群組(query)
2
+ import * as paths from '../../lib/core/paths.mjs';
3
+ import { timer, jsonOut, emitError, readStdin } from '../../lib/core/cli-utils.mjs';
4
+
5
+ export async function cmdSearch(subcommand, positionals, flags) {
6
+ const elapsed = timer();
7
+ const format = flags.format || 'text';
8
+ const project = flags.project || 'nstc';
9
+ const { DocStore } = await import('../../lib/core/db.mjs');
10
+ const dbPathVal = flags.db || paths.dbPath(project);
11
+ const limit = flags.limit ? parseInt(flags.limit) : null;
12
+
13
+ let query = positionals[0];
14
+ if (query === '-' || (!query && !process.stdin.isTTY)) {
15
+ query = await readStdin();
16
+ }
17
+ if (!query) {
18
+ emitError('search', '請提供搜尋關鍵字(至少 3 個字元)', format);
19
+ process.exit(1);
20
+ }
21
+
22
+ const store = new DocStore(dbPathVal);
23
+ store.initSchema();
24
+ let results = store.search(query);
25
+ if (limit && limit > 0) results = results.slice(0, limit);
26
+
27
+ if (format === 'json') {
28
+ process.stdout.write(jsonOut('search', { query, count: results.length, results }, elapsed()));
29
+ } else {
30
+ if (results.length === 0) {
31
+ process.stdout.write(`查無「${query}」相關結果\n`);
32
+ } else {
33
+ process.stdout.write(`搜尋「${query}」找到 ${results.length} 筆結果:\n\n`);
34
+ for (const r of results) {
35
+ process.stdout.write(`[${r.doc_type}] ${r.title} — 第 ${r.source_page} 頁\n`);
36
+ process.stdout.write(` 引證: ${r.source_file}:${r.source_page}\n`);
37
+ process.stdout.write(` 摘要: ${r.snippet}\n\n`);
38
+ }
39
+ }
40
+ }
41
+
42
+ store.close();
43
+ if (format !== 'json' && !flags.quiet) {
44
+ process.stderr.write(`檢索完成 (${elapsed()}s)\n`);
45
+ }
46
+ }
@@ -0,0 +1,33 @@
1
+ // bin/commands/writing.mjs — writing 指令群組(generate)
2
+ import { timer, jsonOut, readStdin } from '../../lib/core/cli-utils.mjs';
3
+
4
+ export async function cmdWritingGenerate(flags) {
5
+ const elapsed = timer();
6
+ const format = flags.format || 'text';
7
+ const project = flags.project || 'nstc';
8
+ const concurrency = parseInt(flags.concurrency) || 10;
9
+
10
+ const { GENERATORS, TOP5_OVERRIDES } = await import('../../lib/writing/generators/nstc-generators.mjs');
11
+
12
+ let ids = null;
13
+ if (flags.ids === '-' || (flags.ids === true && !process.stdin.isTTY)) {
14
+ const stdin = await readStdin();
15
+ if (stdin) ids = stdin.split(/[,\s\n]+/).map(Number).filter(Boolean);
16
+ } else if (flags.ids && flags.ids !== true) {
17
+ ids = flags.ids.split(',').map(Number).filter(Boolean);
18
+ }
19
+
20
+ const { generateAll } = await import('../../lib/writing/generate.mjs');
21
+ const result = await generateAll({
22
+ concurrency, ids, project,
23
+ generators: GENERATORS,
24
+ overrides: TOP5_OVERRIDES,
25
+ });
26
+
27
+ if (format === 'json') {
28
+ process.stdout.write(jsonOut('writing generate', { ok: result.ok, fail: result.fail }, elapsed()));
29
+ } else {
30
+ process.stderr.write(`\n${result.status}\n`);
31
+ process.stderr.write(`完成: ${result.ok} 成功, ${result.fail} 失敗 (${elapsed()}s)\n`);
32
+ }
33
+ }
@@ -0,0 +1,52 @@
1
+ // lib/core/adapters/base.mjs — LLM Adapter 抽象基類
2
+ //
3
+ // SOLID 原則:
4
+ // LSP — 所有 Adapter 必須繼承此類並實作抽象方法,保證可替換性
5
+ // OCP — 透過繼承擴充新 provider,不修改既有 adapter
6
+ // ISP — 介面精簡:僅定義所有 adapter 共同需要的方法
7
+ //
8
+ // 使用方式:
9
+ // export class MyAdapter extends BaseAdapter { ... }
10
+ //
11
+ // 強制實作:generateContent() 與 generateContentStream()
12
+ // 預設提供:_formatError() 統一錯誤格式(DRY)
13
+
14
+ export class BaseAdapter {
15
+ /**
16
+ * 向 LLM 提問(非串流)
17
+ * @abstract
18
+ * @param {object} params
19
+ * @param {string} params.prompt - 使用者提問
20
+ * @param {string} [params.systemInstruction] - 系統指示
21
+ * @returns {Promise<LlmResponse>}
22
+ */
23
+ async generateContent(_params) {
24
+ throw new Error(`${this.constructor.name}.generateContent() 尚未實作`);
25
+ }
26
+
27
+ /**
28
+ * 向 LLM 提問(AsyncGenerator 串流)
29
+ * @abstract
30
+ * @param {object} params
31
+ * @param {string} params.prompt - 使用者提問
32
+ * @param {string} [params.systemInstruction] - 系統指示
33
+ * @yields {{ type: 'text'|'thinking'|'metadata', text?: string, ... }}
34
+ */
35
+ async *generateContentStream(_params) {
36
+ throw new Error(`${this.constructor.name}.generateContentStream() 尚未實作`);
37
+ }
38
+
39
+ /**
40
+ * 統一錯誤格式化(DRY — 所有子類共用)
41
+ * @param {Error} err
42
+ * @param {number} [code=1]
43
+ * @returns {{ ok: false, error: string, code: number }}
44
+ */
45
+ _formatError(err, code = 1) {
46
+ return {
47
+ ok: false,
48
+ error: err?.message || String(err),
49
+ code,
50
+ };
51
+ }
52
+ }
@@ -0,0 +1,13 @@
1
+ // lib/core/adapters/claude.mjs — Claude LLM adapter
2
+ // TODO: 實作 Anthropic Claude API 呼叫
3
+
4
+ export class ClaudeAdapter {
5
+ constructor(config = {}) {
6
+ this.model = config.model || 'claude-sonnet-4-6';
7
+ // TODO: 初始化 API client
8
+ }
9
+
10
+ async chat(messages) {
11
+ throw new Error('ClaudeAdapter.chat() 尚未實作');
12
+ }
13
+ }