botrun-horse 2.28.4 → 2.29.2

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/bin/bh.mjs CHANGED
@@ -166,9 +166,9 @@ switch (command) {
166
166
  break;
167
167
  }
168
168
 
169
- case 'prompt': {
170
- const { cmdPrompt } = await import('./commands/prompt.mjs');
171
- await cmdPrompt(subcommand, positionals, flags);
169
+ case 'skill': {
170
+ const { cmdSkill } = await import('./commands/skill.mjs');
171
+ await cmdSkill(subcommand, positionals, flags);
172
172
  break;
173
173
  }
174
174
 
@@ -79,7 +79,7 @@ botrun-horse — 多專案文件處理系統 CLI v${VERSION}
79
79
  dag <子指令> DAG 依賴追蹤 (init|status|ready|list)
80
80
  search <子指令> 全文檢索 (query) [需 --experimental-sqlite]
81
81
  db <子指令> 資料庫管理 (stats|docs|pages) [需 --experimental-sqlite]
82
- prompt [關鍵字] 提示詞管理(無參數=全列出,帶關鍵字=模糊搜尋)
82
+ skill [關鍵字] 技能管理(無參數=全列出,帶關鍵字=模糊搜尋)
83
83
  portal <子指令> 智慧入口 (預留)
84
84
  ocr <子指令> OCR 辨識 (預留)
85
85
  commands 所有指令的結構化 JSON schema
@@ -201,33 +201,29 @@ export const COMMANDS = {
201
201
  'node --experimental-sqlite bin/bh.mjs legal ask "問題" --skip-cache --project=legal-cache',
202
202
  ],
203
203
  },
204
- prompt: {
205
- name: 'prompt',
206
- description: '提示詞管理(無參數=全列出,帶關鍵字=模糊搜尋)— 零框架提示系列',
204
+ skill: {
205
+ name: 'skill',
206
+ description: '技能管理(無參數=全列出,帶關鍵字=模糊搜尋)— 零框架技能系列',
207
207
  requires_sqlite: false,
208
208
  subcommands: {
209
- '(無)': '列出所有提示詞(依系列分組)',
210
- '<關鍵字>': '模糊搜尋提示詞(加權全文比對)',
211
- list: '列出所有提示詞(同無參數)',
212
- show: '顯示指定提示詞全文(支援智慧 fallback 搜尋)',
213
- search: '模糊搜尋提示詞(同 <關鍵字>)',
209
+ '(無)': '列出所有技能(依系列分組)',
210
+ '<關鍵字>': '模糊搜尋技能(加權全文比對)',
211
+ list: '列出所有技能(同無參數)',
212
+ show: '顯示指定技能全文(支援智慧 fallback 搜尋)',
213
+ search: '模糊搜尋技能(同 <關鍵字>)',
214
214
  },
215
215
  options: {
216
- '--series=<名稱>': { type: 'string', description: '[list] 過濾指定系列' },
217
- '--tag=<標籤>': { type: 'string', description: '[search] 按標籤過濾' },
218
216
  '--format=json|text': { type: 'string', default: 'text', description: '輸出格式' },
219
217
  '--quiet': { type: 'boolean', default: false, description: '靜默模式' },
220
218
  },
221
219
  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 不存在 → 自動搜尋',
220
+ 'bh skill # 列出全部技能',
221
+ 'bh skill 零幻覺 # 模糊搜尋「零幻覺」',
222
+ 'bh skill show botrun-horse-coding',
223
+ 'bh skill show botrun-horse-coding | pbcopy',
224
+ 'bh skill show botrun-horse-coding --format=json',
225
+ 'bh skill search 換位思考',
226
+ 'bh skill show 切片 # 智慧 fallback:slug 不存在 → 自動搜尋',
231
227
  ],
232
228
  },
233
229
  portal: {
@@ -0,0 +1,163 @@
1
+ // bin/commands/skill.mjs — skill 指令群組(list / show / search)
2
+ // 無參數 → 列出全部技能
3
+ // 不認識的子指令 → 當關鍵字做全文模糊搜尋
4
+ import { timer, jsonOut, emitError, logProgress } from '../../lib/core/cli-utils.mjs';
5
+
6
+ /**
7
+ * skill 指令主路由
8
+ *
9
+ * bh skill → list(列出全部)
10
+ * bh skill list → list
11
+ * bh skill show <slug> → show
12
+ * bh skill search <kw> → search
13
+ * bh skill 零幻覺 → 自動搜尋「零幻覺」
14
+ *
15
+ * @param {string} subcommand - list / show / search / 或任意關鍵字
16
+ * @param {string[]} positionals - 位置參數
17
+ * @param {object} flags - CLI 旗標
18
+ */
19
+ export async function cmdSkill(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('skill'); return;
25
+ }
26
+ // 無參數 → 列出全部
27
+ await cmdSkillList(flags);
28
+ return;
29
+ }
30
+
31
+ switch (subcommand) {
32
+ case 'list': await cmdSkillList(flags); break;
33
+ case 'show': await cmdSkillShow(positionals, flags); break;
34
+ case 'search': await cmdSkillSearch(positionals, flags); break;
35
+ default:
36
+ // 不認識的子指令 → 把 subcommand + positionals 合併為搜尋關鍵字
37
+ await cmdSkillSearch([subcommand, ...positionals], flags);
38
+ }
39
+ }
40
+
41
+ /** list — 列出所有技能(依系列分組) */
42
+ async function cmdSkillList(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
+
49
+ if (format === 'json') {
50
+ const data = all.map(p => ({
51
+ slug: p.slug,
52
+ name: p.meta.name || p.slug,
53
+ description: p.meta.description || null,
54
+ body: p.body,
55
+ }));
56
+ process.stdout.write(jsonOut('skill list', { count: data.length, skills: data }, elapsed()));
57
+ return;
58
+ }
59
+
60
+ for (let i = 0; i < all.length; i++) {
61
+ if (i > 0) process.stdout.write('\n');
62
+ process.stdout.write(all[i].rawContent + '\n');
63
+ }
64
+ }
65
+
66
+ /** show — 顯示單一技能全文 */
67
+ async function cmdSkillShow(positionals, flags) {
68
+ const elapsed = timer();
69
+ const format = flags.format || 'text';
70
+ const slug = positionals[0];
71
+
72
+ if (!slug) {
73
+ emitError('skill show', '請提供技能 slug。用法: bh skill show <slug>', format);
74
+ process.exit(1);
75
+ }
76
+
77
+ const { getPrompt, listPrompts } = await import('../../lib/prompt/prompt-store.mjs');
78
+ let skill = getPrompt(slug);
79
+
80
+ // 智慧 fallback:精確 slug 找不到 → 自動轉搜尋
81
+ if (!skill) {
82
+ const { searchPrompts } = await import('../../lib/prompt/prompt-search.mjs');
83
+ const all = listPrompts();
84
+ const results = searchPrompts(all, slug);
85
+
86
+ if (results.length === 0) {
87
+ emitError('skill show', `找不到技能「${slug}」,搜尋也無結果`, format);
88
+ process.exit(1);
89
+ }
90
+
91
+ if (results.length === 1) {
92
+ skill = results[0].prompt;
93
+ logProgress(`提示: slug「${slug}」不存在,自動搜尋命中「${skill.slug}」`, flags);
94
+ } else {
95
+ // 多筆命中 → 列出候選
96
+ if (format === 'json') {
97
+ const data = results.map(r => ({ slug: r.prompt.slug, title: r.prompt.meta.title, score: r.score }));
98
+ process.stdout.write(jsonOut('skill show', { match: 'multiple', candidates: data }, elapsed()));
99
+ } else {
100
+ process.stderr.write(`找不到精確 slug「${slug}」,搜尋到 ${results.length} 筆候選:\n`);
101
+ for (const r of results) {
102
+ process.stderr.write(` ${r.prompt.slug.padEnd(16)} ${r.prompt.meta.title} (分數: ${r.score})\n`);
103
+ }
104
+ process.stderr.write(`\n請使用精確 slug,例如: bh skill show ${results[0].prompt.slug}\n`);
105
+ }
106
+ return;
107
+ }
108
+ }
109
+
110
+ if (format === 'json') {
111
+ const data = {
112
+ slug: skill.slug,
113
+ name: skill.meta.name || skill.slug,
114
+ description: skill.meta.description || null,
115
+ body: skill.body,
116
+ };
117
+ process.stdout.write(jsonOut('skill show', data, elapsed()));
118
+ } else {
119
+ process.stdout.write(skill.rawContent + '\n');
120
+ }
121
+ }
122
+
123
+ /** search — 模糊搜尋技能(加權全文比對) */
124
+ async function cmdSkillSearch(positionals, flags) {
125
+ const elapsed = timer();
126
+ const format = flags.format || 'text';
127
+ const query = positionals.join(' ');
128
+
129
+ if (!query) {
130
+ emitError('skill search', '請提供搜尋關鍵字。用法: bh skill search <關鍵字>', format);
131
+ process.exit(1);
132
+ }
133
+
134
+ const { listPrompts } = await import('../../lib/prompt/prompt-store.mjs');
135
+ const { searchPrompts } = await import('../../lib/prompt/prompt-search.mjs');
136
+
137
+ const all = listPrompts();
138
+ const results = searchPrompts(all, query);
139
+
140
+ if (format === 'json') {
141
+ const data = results.map(r => ({
142
+ slug: r.prompt.slug,
143
+ name: r.prompt.meta.name || r.prompt.slug,
144
+ description: r.prompt.meta.description || null,
145
+ score: r.score,
146
+ body: r.prompt.body,
147
+ }));
148
+ process.stdout.write(jsonOut('skill search', { query, count: data.length, results: data }, elapsed()));
149
+ return;
150
+ }
151
+
152
+ if (results.length === 0) {
153
+ process.stdout.write(`搜尋「${query}」:無結果\n`);
154
+ return;
155
+ }
156
+
157
+ logProgress(`搜尋「${query}」:${results.length} 筆結果`, flags);
158
+
159
+ for (let i = 0; i < results.length; i++) {
160
+ if (i > 0) process.stdout.write('\n');
161
+ process.stdout.write(results[i].prompt.rawContent + '\n');
162
+ }
163
+ }
@@ -25,7 +25,7 @@ export function parseArgs(argv) {
25
25
  const positionals = [];
26
26
 
27
27
  // 判斷需跳過子指令的指令群組
28
- const hasSubcommand = ['dag', 'db', 'doc', 'writing', 'search', 'portal', 'ocr', 'gemini', 'prompt'].includes(command);
28
+ const hasSubcommand = ['dag', 'db', 'doc', 'writing', 'search', 'portal', 'ocr', 'gemini', 'skill'].includes(command);
29
29
  const flagStart = hasSubcommand ? 2 : 1;
30
30
  for (const arg of args.slice(flagStart)) {
31
31
  if (arg.startsWith('--')) {
@@ -1,49 +1,31 @@
1
1
  // lib/prompt/prompt-search.mjs — 加權評分模糊搜尋
2
- // 依 slug → aliastitle → tag → series → content 順序加權比對
2
+ // 依 slug/namedescription → content 順序加權比對
3
+ // SKILL.md 標準 frontmatter(name, description)
3
4
 
4
- const WEIGHTS = { slug: 10, alias: 8, title: 6, tag: 5, series: 4, content: 1 };
5
+ const WEIGHTS = { slug: 10, description: 5, content: 1 };
5
6
 
6
7
  /**
7
8
  * 加權模糊搜尋
8
- * @param {Array<{ slug, meta, body }>} prompts - 所有提示詞
9
+ * @param {Array<{ slug, meta, body }>} prompts - 所有技能
9
10
  * @param {string} query - 搜尋關鍵字
10
- * @param {{ tag?: string }} [opts] - 過濾選項
11
11
  * @returns {Array<{ prompt, score }>} 按分數降序排列
12
12
  */
13
- export function searchPrompts(prompts, query, opts = {}) {
13
+ export function searchPrompts(prompts, query) {
14
14
  const q = query.toLowerCase();
15
15
  const results = [];
16
16
 
17
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
18
  let score = 0;
25
19
  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();
20
+ const description = (p.meta.description || '').toLowerCase();
30
21
  const body = p.body.toLowerCase();
31
22
 
32
- // slug 精確匹配 → 滿分;部分匹配 → 6 分
23
+ // slug/name 精確匹配 → 滿分;部分匹配 → 6 分
33
24
  if (slug === q) score += WEIGHTS.slug;
34
25
  else if (slug.includes(q)) score += 6;
35
26
 
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;
27
+ // description 包含(SKILL.md 標準欄位)
28
+ if (description.includes(q)) score += WEIGHTS.description;
47
29
 
48
30
  // 全文內容包含
49
31
  if (body.includes(q)) score += WEIGHTS.content;
@@ -1,16 +1,16 @@
1
- // lib/prompt/prompt-store.mjs — 提示詞 Repository 層
1
+ // lib/prompt/prompt-store.mjs — 技能 Repository 層
2
2
  // 掃描 lib/prompt/prompts/ 目錄,解析 YAML frontmatter,提供 listPrompts / getPrompt
3
3
 
4
4
  import fs from 'fs';
5
5
  import path from 'path';
6
6
  import { root } from '../core/paths.mjs';
7
7
 
8
- /** 提示詞目錄(相對於專案根目錄) */
8
+ /** 技能目錄(相對於專案根目錄) */
9
9
  const PROMPTS_DIR = () => path.join(root(), 'lib', 'prompt', 'prompts');
10
10
 
11
11
  /**
12
12
  * 零依賴 YAML frontmatter 解析
13
- * 支援 key: value 與 key: [a, b, c] 陣列語法
13
+ * SKILL.md 標準欄位:name, description
14
14
  * @param {string} content - .md 檔案原始內容
15
15
  * @returns {{ meta: object, body: string }}
16
16
  */
@@ -23,12 +23,7 @@ export function parseFrontmatter(content) {
23
23
  const kv = line.match(/^(\w[\w-]*):\s*(.+)$/);
24
24
  if (!kv) continue;
25
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
- }
26
+ meta[key] = rawVal.trim();
32
27
  }
33
28
  return { meta, body: match[2] };
34
29
  }
@@ -54,27 +49,28 @@ function scanMarkdownFiles(dir, baseDir) {
54
49
  }
55
50
 
56
51
  /**
57
- * 載入並解析單一提示詞檔案
52
+ * 載入並解析單一技能檔案
53
+ * SKILL.md 標準 frontmatter:name, description
58
54
  * @param {string} relPath - 相對於 prompts/ 的路徑(如 zero-framework/coding.md)
59
55
  * @param {string} baseDir - prompts/ 目錄絕對路徑
60
- * @returns {{ slug, meta, body, filePath, series }}
56
+ * @returns {{ slug, meta, body, rawContent, filePath }}
61
57
  */
62
58
  function loadPromptFile(relPath, baseDir) {
63
59
  const fullPath = path.join(baseDir, relPath);
64
60
  const content = fs.readFileSync(fullPath, 'utf-8');
65
61
  const { meta, body } = parseFrontmatter(content);
66
- const slug = path.basename(relPath, '.md');
67
- const series = path.dirname(relPath) === '.' ? null : path.dirname(relPath);
62
+ const slug = meta.name || path.basename(relPath, '.md');
68
63
  return {
69
64
  slug,
70
- meta: { ...meta, series: meta.series || series },
65
+ meta,
71
66
  body: body.trim(),
67
+ rawContent: content.trimEnd(),
72
68
  filePath: fullPath,
73
69
  };
74
70
  }
75
71
 
76
72
  /**
77
- * 列出所有提示詞
73
+ * 列出所有技能
78
74
  * @returns {Array<{ slug, meta, body, filePath }>}
79
75
  */
80
76
  export function listPrompts() {
@@ -84,8 +80,8 @@ export function listPrompts() {
84
80
  }
85
81
 
86
82
  /**
87
- * 取得單一提示詞(精確 slug 匹配檔名)
88
- * @param {string} slug - 提示詞 slug(如 'coding')
83
+ * 取得單一技能(精確 slug 匹配檔名)
84
+ * @param {string} slug - 技能 slug(如 'coding')
89
85
  * @returns {{ slug, meta, body, filePath } | null}
90
86
  */
91
87
  export function getPrompt(slug) {
@@ -1,12 +1,8 @@
1
1
  ---
2
- title: 零框架扣頂 coding
3
- series: zero-framework
4
- tags: [換位思考, 三方案, 六軟工, BDD, TDD, SOLID, DRY, KISS, DDD, HTTPS代理, 待辦]
5
- aliases: [coding, 扣頂, 寫程式, 開發]
2
+ name: botrun-horse-coding
3
+ description: 零框架扣頂 coding — 換位思考、查新文件、三方案打分、六軟工實作、DAG 待辦追蹤、HTTPS 代理相容
6
4
  ---
7
5
 
8
- # 零框架扣頂 coding
9
-
10
6
  🔄 換位思考:先掃描資料與提問,推理需求背後動機,具體列出 5 個潛在動機
11
7
  📡 新文件:根據提問與需求上網查詢最新 SDK API 文件,因為隨時都在變化
12
8
  🥊 三方案:提出 3 替代方案多維度打分,以使用者為中心選最適合方案
@@ -1,12 +1,50 @@
1
1
  ---
2
- title: 零框架搜尋
3
- series: zero-framework
4
- tags: [搜尋, grounding, 引證, URL, 網路搜尋]
5
- aliases: [search, 搜尋, 查詢, 找資料]
2
+ name: botrun-horse-search
3
+ description: 零框架搜尋 — Google Search grounding 搜尋接地、引證 URL、零幻覺多步驟交叉驗證問答
6
4
  ---
7
5
 
8
- # 零框架搜尋
9
-
10
6
  🔍 搜尋接地:啟用 Google Search grounding,確保回應以真實搜尋結果為依據
11
7
  🔗 引證 URL:回應必須包含引證來源的 URL,讓使用者可驗證
12
8
  🚫 零幻覺:不捏造不存在的資料,所有事實陳述須有搜尋結果佐證
9
+
10
+ ## 系統提示詞模板
11
+
12
+ 變數說明:
13
+ - `$0` / `{{機關單位}}` — 回答時的專業領域角色,例如「經濟部產業發展署」「衛生福利部」「內政部營建署」
14
+ - `$1` / `{{使用者提問}}` — 實際要搜尋回答的問題
15
+
16
+ ```
17
+ <系統提示工程>
18
+ . 你是臺灣「{{機關單位}}」的資深專業人員,回答時不用說自己是誰,但要溫暖,換位思考使用者真正能聽懂的,必須用大白話淺顯的解說
19
+ . 請你分成多步驟進行
20
+ 001 上網搜尋或抓取,並列出所有相關文字引證證據,絕對不可延伸文字,只能原文照抄引證
21
+ 002 基於 001 嚴格的引證原文文字不能竄改,然後進行第一次初稿回答
22
+ 003 假定 002 是很多幻覺很多錯的,驗證並且推翻 002 錯誤的部分,要請再次上網搜尋證據,只要有超出證據之外的,予以推翻,此時必須注意有些規定是正面表列規定,並沒有負向表列沒做會怎樣,不要弄錯規定
23
+ 004 彙整最終回答,此時,每一句回答都必須引證001證據
24
+ 回應時,每個步驟都請用以下XML標籤分隔明確
25
+ <001>
26
+ ...
27
+ </001>
28
+ <002>
29
+ ...
30
+ </002>
31
+ ...
32
+ <004>
33
+ ...
34
+ </004>
35
+ </系統提示工程>
36
+
37
+ <使用者提問>
38
+ {{使用者提問}}
39
+ </使用者提問>
40
+ ```
41
+
42
+ ## 用法範例
43
+
44
+ ```bash
45
+ # 直接帶入變數使用
46
+ bh gemini ask --system-prompt "你是臺灣「經濟部產業發展署」的資深專業人員..." "我如何辦理工廠危險品停用解列?"
47
+
48
+ # 搭配 pipe
49
+ echo "我的工廠預計4/1不再使用工廠危險品,如何辦理停用解列?" | bh gemini ask --project=ida
50
+ ```
@@ -1,11 +1,7 @@
1
1
  ---
2
- title: 零框架切片
3
- series: zero-framework
4
- tags: [切片, 零幻覺引證, PDF, Office, 頁碼, 純文字]
5
- aliases: [slice, 切片, 切頁, 文件處理]
2
+ name: botrun-horse-slice
3
+ description: 零框架切片 — PDF/Office 切頁保留頁碼轉純文字,零幻覺引證來源含網址、檔名、頁碼、原始文字
6
4
  ---
7
5
 
8
- # 零框架切片
9
-
10
6
  🔪 切片:如果資料檔案是 PDF 或 Office,記得先切頁(保留頁碼可引證)轉為純文字才去處理
11
7
  📎 零幻覺引證:回應須引證來源,包含:網址、檔案名稱、頁碼、時間點、不竄改的原始文字
package/package.json CHANGED
@@ -1,10 +1,11 @@
1
1
  {
2
2
  "name": "botrun-horse",
3
- "version": "2.28.4",
4
- "description": "多專案文件處理系統 CLI — AI 提示詞管理、文件擷取、公文撰寫",
3
+ "version": "2.29.2",
4
+ "description": "多專案文件處理系統 CLI — AI 技能管理、文件擷取、公文撰寫",
5
5
  "type": "module",
6
6
  "bin": {
7
- "bh": "./bin/bh.mjs"
7
+ "bh": "./bin/bh.mjs",
8
+ "botrun-horse": "./bin/bh.mjs"
8
9
  },
9
10
  "scripts": {
10
11
  "test": "node --test test/**/*.test.mjs",
@@ -24,6 +25,10 @@
24
25
  "dag:status": "node src/infrastructure/dag-tracker.mjs status",
25
26
  "dag:ready": "node src/infrastructure/dag-tracker.mjs ready"
26
27
  },
28
+ "overrides": {
29
+ "rimraf": "^6.1.3",
30
+ "node-fetch": "npm:node-fetch-native@^1.6.7"
31
+ },
27
32
  "dependencies": {
28
33
  "@ai-sdk/openai-compatible": "^2.0.30",
29
34
  "@google/genai": "^1.43.0",
@@ -1,180 +0,0 @@
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
- /** 去掉 body 開頭的 # 標題行(CLI 標頭已顯示,避免重複) */
7
- function stripTitle(body) {
8
- return body.replace(/^#\s+.+\n+/, '');
9
- }
10
-
11
- /**
12
- * prompt 指令主路由
13
- *
14
- * bh prompt → list(列出全部)
15
- * bh prompt list → list
16
- * bh prompt show <slug> → show
17
- * bh prompt search <kw> → search
18
- * bh prompt 零幻覺 → 自動搜尋「零幻覺」
19
- *
20
- * @param {string} subcommand - list / show / search / 或任意關鍵字
21
- * @param {string[]} positionals - 位置參數
22
- * @param {object} flags - CLI 旗標
23
- */
24
- export async function cmdPrompt(subcommand, positionals, flags) {
25
- // --help(可能被 parseArgs 當成 subcommand)
26
- if (!subcommand || subcommand === '--help' || subcommand === '-h') {
27
- if (subcommand === '--help' || subcommand === '-h' || flags.help) {
28
- const { showCommandHelp } = await import('./help.mjs');
29
- showCommandHelp('prompt'); return;
30
- }
31
- // 無參數 → 列出全部
32
- await cmdPromptList(flags);
33
- return;
34
- }
35
-
36
- switch (subcommand) {
37
- case 'list': await cmdPromptList(flags); break;
38
- case 'show': await cmdPromptShow(positionals, flags); break;
39
- case 'search': await cmdPromptSearch(positionals, flags); break;
40
- default:
41
- // 不認識的子指令 → 把 subcommand + positionals 合併為搜尋關鍵字
42
- await cmdPromptSearch([subcommand, ...positionals], flags);
43
- }
44
- }
45
-
46
- /** list — 列出所有提示詞(依系列分組) */
47
- async function cmdPromptList(flags) {
48
- const elapsed = timer();
49
- const format = flags.format || 'text';
50
- const { listPrompts } = await import('../../lib/prompt/prompt-store.mjs');
51
-
52
- const all = listPrompts();
53
- const filtered = flags.series
54
- ? all.filter(p => p.meta.series === flags.series)
55
- : all;
56
-
57
- if (format === 'json') {
58
- const data = filtered.map(p => ({
59
- slug: p.slug,
60
- title: p.meta.title,
61
- series: p.meta.series,
62
- tags: p.meta.tags || [],
63
- aliases: p.meta.aliases || [],
64
- body: p.body,
65
- }));
66
- process.stdout.write(jsonOut('prompt list', { count: data.length, prompts: data }, elapsed()));
67
- return;
68
- }
69
-
70
- for (let i = 0; i < filtered.length; i++) {
71
- const p = filtered[i];
72
- if (i > 0) process.stdout.write('\n---\n\n');
73
- process.stdout.write(`[${p.meta.series || ''}/${p.slug}] ${p.meta.title || p.slug}\n\n`);
74
- process.stdout.write(stripTitle(p.body) + '\n');
75
- }
76
- }
77
-
78
- /** show — 顯示單一提示詞全文 */
79
- async function cmdPromptShow(positionals, flags) {
80
- const elapsed = timer();
81
- const format = flags.format || 'text';
82
- const slug = positionals[0];
83
-
84
- if (!slug) {
85
- emitError('prompt show', '請提供提示詞 slug。用法: bh prompt show <slug>', format);
86
- process.exit(1);
87
- }
88
-
89
- const { getPrompt, listPrompts } = await import('../../lib/prompt/prompt-store.mjs');
90
- let prompt = getPrompt(slug);
91
-
92
- // 智慧 fallback:精確 slug 找不到 → 自動轉搜尋
93
- if (!prompt) {
94
- const { searchPrompts } = await import('../../lib/prompt/prompt-search.mjs');
95
- const all = listPrompts();
96
- const results = searchPrompts(all, slug);
97
-
98
- if (results.length === 0) {
99
- emitError('prompt show', `找不到提示詞「${slug}」,搜尋也無結果`, format);
100
- process.exit(1);
101
- }
102
-
103
- if (results.length === 1) {
104
- prompt = results[0].prompt;
105
- logProgress(`提示: slug「${slug}」不存在,自動搜尋命中「${prompt.slug}」`, flags);
106
- } else {
107
- // 多筆命中 → 列出候選
108
- if (format === 'json') {
109
- const data = results.map(r => ({ slug: r.prompt.slug, title: r.prompt.meta.title, score: r.score }));
110
- process.stdout.write(jsonOut('prompt show', { match: 'multiple', candidates: data }, elapsed()));
111
- } else {
112
- process.stderr.write(`找不到精確 slug「${slug}」,搜尋到 ${results.length} 筆候選:\n`);
113
- for (const r of results) {
114
- process.stderr.write(` ${r.prompt.slug.padEnd(16)} ${r.prompt.meta.title} (分數: ${r.score})\n`);
115
- }
116
- process.stderr.write(`\n請使用精確 slug,例如: bh prompt show ${results[0].prompt.slug}\n`);
117
- }
118
- return;
119
- }
120
- }
121
-
122
- if (format === 'json') {
123
- const data = {
124
- slug: prompt.slug,
125
- title: prompt.meta.title,
126
- series: prompt.meta.series,
127
- tags: prompt.meta.tags || [],
128
- aliases: prompt.meta.aliases || [],
129
- body: prompt.body,
130
- };
131
- process.stdout.write(jsonOut('prompt show', data, elapsed()));
132
- } else {
133
- process.stdout.write(stripTitle(prompt.body) + '\n');
134
- }
135
- }
136
-
137
- /** search — 模糊搜尋提示詞(加權全文比對) */
138
- async function cmdPromptSearch(positionals, flags) {
139
- const elapsed = timer();
140
- const format = flags.format || 'text';
141
- const query = positionals.join(' ');
142
-
143
- if (!query) {
144
- emitError('prompt search', '請提供搜尋關鍵字。用法: bh prompt search <關鍵字>', format);
145
- process.exit(1);
146
- }
147
-
148
- const { listPrompts } = await import('../../lib/prompt/prompt-store.mjs');
149
- const { searchPrompts } = await import('../../lib/prompt/prompt-search.mjs');
150
-
151
- const all = listPrompts();
152
- const results = searchPrompts(all, query, { tag: flags.tag });
153
-
154
- if (format === 'json') {
155
- const data = results.map(r => ({
156
- slug: r.prompt.slug,
157
- title: r.prompt.meta.title,
158
- series: r.prompt.meta.series,
159
- score: r.score,
160
- tags: r.prompt.meta.tags || [],
161
- body: r.prompt.body,
162
- }));
163
- process.stdout.write(jsonOut('prompt search', { query, count: data.length, results: data }, elapsed()));
164
- return;
165
- }
166
-
167
- if (results.length === 0) {
168
- process.stdout.write(`搜尋「${query}」:無結果\n`);
169
- return;
170
- }
171
-
172
- logProgress(`搜尋「${query}」:${results.length} 筆結果`, flags);
173
-
174
- for (let i = 0; i < results.length; i++) {
175
- const r = results[i];
176
- if (i > 0) process.stdout.write('\n---\n\n');
177
- process.stdout.write(`[${r.prompt.meta.series || ''}/${r.prompt.slug}] ${r.prompt.meta.title}\n\n`);
178
- process.stdout.write(stripTitle(r.prompt.body) + '\n');
179
- }
180
- }