w-dispatch-ai 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 (73) hide show
  1. package/.editorconfig +9 -0
  2. package/.eslintignore +3 -0
  3. package/.eslintrc.js +55 -0
  4. package/.jsdoc +25 -0
  5. package/LICENSE +21 -0
  6. package/README.md +207 -0
  7. package/SECURITY.md +5 -0
  8. package/babel.config.js +16 -0
  9. package/dist/w-dispatch-ai.umd.js +7 -0
  10. package/dist/w-dispatch-ai.umd.js.map +1 -0
  11. package/docs/WDispatchAi.mjs.html +110 -0
  12. package/docs/adapters.mjs.html +100 -0
  13. package/docs/dispatchAi.mjs.html +133 -0
  14. package/docs/dispatchClaude.mjs.html +190 -0
  15. package/docs/dispatchCodex.mjs.html +191 -0
  16. package/docs/dispatchOpencode.mjs.html +260 -0
  17. package/docs/fonts/Montserrat/Montserrat-Bold.eot +0 -0
  18. package/docs/fonts/Montserrat/Montserrat-Bold.ttf +0 -0
  19. package/docs/fonts/Montserrat/Montserrat-Bold.woff +0 -0
  20. package/docs/fonts/Montserrat/Montserrat-Bold.woff2 +0 -0
  21. package/docs/fonts/Montserrat/Montserrat-Regular.eot +0 -0
  22. package/docs/fonts/Montserrat/Montserrat-Regular.ttf +0 -0
  23. package/docs/fonts/Montserrat/Montserrat-Regular.woff +0 -0
  24. package/docs/fonts/Montserrat/Montserrat-Regular.woff2 +0 -0
  25. package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.eot +0 -0
  26. package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.svg +978 -0
  27. package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.ttf +0 -0
  28. package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.woff +0 -0
  29. package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.woff2 +0 -0
  30. package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.eot +0 -0
  31. package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.svg +1049 -0
  32. package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.ttf +0 -0
  33. package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.woff +0 -0
  34. package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.woff2 +0 -0
  35. package/docs/getCliArgs.mjs.html +126 -0
  36. package/docs/getErrorResult.mjs.html +115 -0
  37. package/docs/global.html +2924 -0
  38. package/docs/index.html +84 -0
  39. package/docs/scripts/collapse.js +39 -0
  40. package/docs/scripts/commonNav.js +28 -0
  41. package/docs/scripts/linenumber.js +25 -0
  42. package/docs/scripts/nav.js +12 -0
  43. package/docs/scripts/polyfill.js +4 -0
  44. package/docs/scripts/prettify/Apache-License-2.0.txt +202 -0
  45. package/docs/scripts/prettify/lang-css.js +2 -0
  46. package/docs/scripts/prettify/prettify.js +28 -0
  47. package/docs/scripts/search.js +99 -0
  48. package/docs/styles/jsdoc.css +776 -0
  49. package/docs/styles/prettify.css +80 -0
  50. package/g.mjs +94 -0
  51. package/package.json +38 -0
  52. package/script.txt +17 -0
  53. package/src/WDispatchAi.mjs +38 -0
  54. package/src/adapters.mjs +28 -0
  55. package/src/dispatchAi.mjs +61 -0
  56. package/src/dispatchClaude.mjs +118 -0
  57. package/src/dispatchCodex.mjs +119 -0
  58. package/src/dispatchOpencode.mjs +188 -0
  59. package/src/getCliArgs.mjs +54 -0
  60. package/src/getErrorResult.mjs +43 -0
  61. package/test/tools/fakeCliForTest.mjs +139 -0
  62. package/test/unit-WDispatchAi.test.mjs +79 -0
  63. package/test/unit-adapters.test.mjs +34 -0
  64. package/test/unit-dispatchAi.test.mjs +104 -0
  65. package/test/unit-dispatchClaude.test.mjs +144 -0
  66. package/test/unit-dispatchCodex.test.mjs +113 -0
  67. package/test/unit-dispatchOpencode.test.mjs +176 -0
  68. package/test/unit-getCliArgs.test.mjs +55 -0
  69. package/test/unit-getErrorResult.test.mjs +45 -0
  70. package/toolg/addVersion.mjs +4 -0
  71. package/toolg/cleanFolder.mjs +5 -0
  72. package/toolg/gDistRollup.mjs +24 -0
  73. package/toolg/modifyReadme.mjs +4 -0
@@ -0,0 +1,80 @@
1
+ .pln {
2
+ color: #ddd;
3
+ }
4
+
5
+ /* string content */
6
+ .str {
7
+ color: #61ce3c;
8
+ }
9
+
10
+ /* a keyword */
11
+ .kwd {
12
+ color: #fbde2d;
13
+ }
14
+
15
+ /* a comment */
16
+ .com {
17
+ color: #aeaeae;
18
+ }
19
+
20
+ /* a type name */
21
+ .typ {
22
+ color: #8da6ce;
23
+ }
24
+
25
+ /* a literal value */
26
+ .lit {
27
+ color: #fbde2d;
28
+ }
29
+
30
+ /* punctuation */
31
+ .pun {
32
+ color: #ddd;
33
+ }
34
+
35
+ /* lisp open bracket */
36
+ .opn {
37
+ color: #000000;
38
+ }
39
+
40
+ /* lisp close bracket */
41
+ .clo {
42
+ color: #000000;
43
+ }
44
+
45
+ /* a markup tag name */
46
+ .tag {
47
+ color: #8da6ce;
48
+ }
49
+
50
+ /* a markup attribute name */
51
+ .atn {
52
+ color: #fbde2d;
53
+ }
54
+
55
+ /* a markup attribute value */
56
+ .atv {
57
+ color: #ddd;
58
+ }
59
+
60
+ /* a declaration */
61
+ .dec {
62
+ color: #EF5050;
63
+ }
64
+
65
+ /* a variable name */
66
+ .var {
67
+ color: #c82829;
68
+ }
69
+
70
+ /* a function name */
71
+ .fun {
72
+ color: #4271ae;
73
+ }
74
+
75
+ /* Specify class=linenums on a pre to get line numbering */
76
+ ol.linenums {
77
+ margin-top: 0;
78
+ margin-bottom: 0;
79
+ padding-bottom: 2px;
80
+ }
package/g.mjs ADDED
@@ -0,0 +1,94 @@
1
+ import wdi from './src/WDispatchAi.mjs'
2
+
3
+
4
+ //由.env載入金鑰, OPENCODE_KEYS與AGNES_KEYS各以逗號分隔多把, 未提供時沿用各CLI既有登入狀態
5
+ try {
6
+ process.loadEnvFile('./.env')
7
+ }
8
+ catch {}
9
+ let opencodeKeys = (process.env.OPENCODE_KEYS || '').split(',').filter(Boolean)
10
+ let agnesKeys = (process.env.AGNES_KEYS || '').split(',').filter(Boolean)
11
+
12
+
13
+ //agnes-ai為opencode未內建之第三方provider, 須另給其provider定義
14
+ let configAgnes = {
15
+ provider: {
16
+ 'agnes-ai': {
17
+ npm: '@ai-sdk/openai-compatible',
18
+ name: 'Agnes',
19
+ options: { baseURL: 'https://apihub.agnes-ai.com/v1' },
20
+ models: { 'agnes-2.0-flash': { name: 'Agnes 2.0 Flash' } },
21
+ },
22
+ },
23
+ }
24
+
25
+
26
+ let test = async () => {
27
+
28
+ //可用之AI供應商種類
29
+ console.log('KINDS:', wdi.KINDS)
30
+ // => KINDS: [ 'opencode', 'claude', 'codex' ]
31
+
32
+ let prompt = '請只回覆兩個字:完成,不要有任何其他文字'
33
+
34
+ //以Claude Code CLI呼叫, 沿用CLI既有登入狀態
35
+ let r1 = await wdi.dispatchClaude(prompt, { model: 'sonnet' })
36
+ console.log('claude:', r1.ok, r1.stdout.trim())
37
+ // => claude: true 完成
38
+
39
+ //以Codex CLI呼叫, 可指定沙箱模式
40
+ let r2 = await wdi.dispatchCodex(prompt, { model: 'gpt-5.6-luna', sandbox: 'read-only' })
41
+ console.log('codex:', r2.ok, r2.stdout.trim())
42
+ // => codex: true 完成
43
+
44
+ //以opencode CLI呼叫, 未給key與provider即沿用CLI既有登入狀態
45
+ let r3 = await wdi.dispatchOpencode(prompt, { model: 'opencode/deepseek-v4-flash-free', timeoutMs: 180000 })
46
+ console.log('opencode:', r3.ok, r3.stdout.trim())
47
+ // => opencode: true 完成
48
+
49
+ //以供應商條目輪替, 一個條目即一組(kind, model, 可選的key與provider與config), 輪到誰就用誰的CLI與模型
50
+ //opencode支援逐次注入金鑰, 故同一provider之多把金鑰可各成一個條目
51
+ let items = [
52
+ { kind: 'claude', model: 'sonnet' },
53
+ { kind: 'codex', model: 'gpt-5.6-luna', sandbox: 'read-only' },
54
+ { kind: 'opencode', model: 'opencode/deepseek-v4-flash-free', provider: 'opencode', key: opencodeKeys[0], timeoutMs: 180000 },
55
+ { kind: 'opencode', model: 'opencode/deepseek-v4-flash-free', provider: 'opencode', key: opencodeKeys[1], timeoutMs: 180000 },
56
+ { kind: 'opencode', model: 'agnes-ai/agnes-2.0-flash', provider: 'agnes-ai', key: agnesKeys[0], config: configAgnes, timeoutMs: 180000 },
57
+ ]
58
+ for (let item of items) {
59
+ let r = await wdi.dispatchAi(item.kind, prompt, item)
60
+ console.log('dispatchAi ' + item.model + ':', r.ok, r.stdout.trim())
61
+ // => dispatchAi sonnet: true 完成
62
+ // => dispatchAi gpt-5.6-luna: true 完成
63
+ // => dispatchAi opencode/deepseek-v4-flash-free: true 完成
64
+ // => dispatchAi opencode/deepseek-v4-flash-free: true 完成
65
+ // => dispatchAi agnes-ai/agnes-2.0-flash: true 完成
66
+ }
67
+
68
+ //未知供應商回傳error結果物件, 不會reject
69
+ let r4 = await wdi.dispatchAi('gemini', prompt)
70
+ console.log('invalid kind:', r4.ok, r4.error)
71
+ // => invalid kind: false unknown ai kind: "gemini" (available: opencode, claude, codex)
72
+
73
+ //prompt非有效字串亦回傳error結果物件
74
+ let r5 = await wdi.dispatchClaude('')
75
+ console.log('invalid prompt:', r5.ok, r5.error)
76
+ // => invalid prompt: false prompt must be a non-empty string
77
+
78
+ //CLI執行失敗時, 由ok、code、error與stderr判斷原因
79
+ let r6 = await wdi.dispatchOpencode(prompt, {
80
+ model: 'opencode/deepseek-v4-flash-free',
81
+ provider: 'opencode',
82
+ key: 'sk-invalid-key',
83
+ })
84
+ console.log('invalid key:', r6.ok, r6.code, r6.error, r6.stderr.includes('Invalid API key'))
85
+ // => invalid key: false 1 Exit code 1 true
86
+
87
+ }
88
+ await test()
89
+ .catch((err) => {
90
+ console.log(err)
91
+ })
92
+
93
+
94
+ //node g.mjs
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "w-dispatch-ai",
3
+ "version": "1.0.0",
4
+ "main": "dist/w-dispatch-ai.umd.js",
5
+ "dependencies": {
6
+ "wsemi": "^1.8.70"
7
+ },
8
+ "devDependencies": {
9
+ "w-package-tools": "^1.1.12"
10
+ },
11
+ "scripts": {
12
+ "test": "mocha --parallel --timeout 60000",
13
+ "deploy": "gh-pages -d docs"
14
+ },
15
+ "mocha": {
16
+ "spec": [
17
+ "test/unit-*.test.mjs"
18
+ ]
19
+ },
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "git+https://github.com/yuda-lyu/w-dispatch-ai.git"
23
+ },
24
+ "keywords": [
25
+ "package",
26
+ "tool",
27
+ "dispatch",
28
+ "ai",
29
+ "llm",
30
+ "cli",
31
+ "claude",
32
+ "codex",
33
+ "opencode",
34
+ "nodejs"
35
+ ],
36
+ "author": "yuda-lyu(semisphere)",
37
+ "license": "MIT"
38
+ }
package/script.txt ADDED
@@ -0,0 +1,17 @@
1
+ #node toolg/addVersion.mjs
2
+ node toolg/modifyReadme.mjs
3
+
4
+ node toolg/cleanFolder.mjs
5
+ ./node_modules/.bin/jsdoc -c .jsdoc
6
+
7
+ node toolg/gDistRollup.mjs
8
+
9
+ git add . -A
10
+ git commit -m 'modify: '
11
+ git push origin master:master
12
+
13
+ npm run deploy
14
+
15
+ #npm test
16
+
17
+ #npm publish
@@ -0,0 +1,38 @@
1
+ import keys from 'lodash-es/keys.js'
2
+ import adapters from './adapters.mjs'
3
+ import dispatchAi from './dispatchAi.mjs'
4
+ import dispatchOpencode from './dispatchOpencode.mjs'
5
+ import dispatchClaude from './dispatchClaude.mjs'
6
+ import dispatchCodex from './dispatchCodex.mjs'
7
+
8
+
9
+ // WDispatchAi.mjs — AI供應商分派層
10
+ //
11
+ // 本套件封裝「以CLI調用各家AI」的差異,對外提供單一介面。
12
+ // 依專案方針,不引用全域技能(dispatch-claude/dispatch-codex/dispatch-opencode),
13
+ // 而是把其調用方式移植於各轉接器內,方便自行偵錯、修改與擴充。
14
+
15
+
16
+ //KINDS, 由adapters鍵名產生, 令供應商清單只有一處來源
17
+ let KINDS = keys(adapters)
18
+
19
+
20
+ /**
21
+ * AI供應商分派
22
+ *
23
+ * @returns {Object} 回傳物件,其內含KINDS(可用供應商種類字串陣列),以及dispatchAi、dispatchOpencode、dispatchClaude、dispatchCodex之async函數
24
+ * @example
25
+ *
26
+ * 詳見dispatchAi、dispatchOpencode、dispatchClaude、dispatchCodex範例
27
+ *
28
+ */
29
+ let WDispatchAi = {
30
+ KINDS,
31
+ dispatchAi,
32
+ dispatchOpencode,
33
+ dispatchClaude,
34
+ dispatchCodex,
35
+ }
36
+
37
+
38
+ export default WDispatchAi
@@ -0,0 +1,28 @@
1
+ import dispatchOpencode from './dispatchOpencode.mjs'
2
+ import dispatchClaude from './dispatchClaude.mjs'
3
+ import dispatchCodex from './dispatchCodex.mjs'
4
+
5
+
6
+ /**
7
+ * 各AI供應商種類(kind)對CLI轉接器函數之對照表
8
+ *
9
+ * 本對照表為kind之唯一來源,dispatchAi以其鍵值分派,WDispatchAi以其鍵名產生KINDS,
10
+ * 新增供應商時僅須於此加入一個鍵值對即可
11
+ *
12
+ * @returns {Object} 回傳對照表物件,鍵名為供應商種類字串,鍵值為對應之dispatch函數
13
+ * @example
14
+ *
15
+ * import adapters from './src/adapters.mjs'
16
+ *
17
+ * console.log(Object.keys(adapters))
18
+ * // => ['opencode', 'claude', 'codex']
19
+ *
20
+ */
21
+ let adapters = {
22
+ opencode: dispatchOpencode,
23
+ claude: dispatchClaude,
24
+ codex: dispatchCodex,
25
+ }
26
+
27
+
28
+ export default adapters
@@ -0,0 +1,61 @@
1
+ import keys from 'lodash-es/keys.js'
2
+ import includes from 'lodash-es/includes.js'
3
+ import isestr from 'wsemi/src/isestr.mjs'
4
+ import adapters from './adapters.mjs'
5
+ import getErrorResult from './getErrorResult.mjs'
6
+
7
+
8
+ /**
9
+ * 依供應商種類(kind)分派至對應之CLI轉接器
10
+ *
11
+ * 三種供應商的差異(2026-08-08於本機實測確認):
12
+ * opencode支援逐次注入金鑰(OPENCODE_AUTH_CONTENT),故可多把金鑰輪替;
13
+ * claude與codex則沿用CLI既有登入狀態,無逐次金鑰概念。
14
+ * 故「輪替」的單位是「供應商條目」而非單純的金鑰:一個條目即一組(kind, model, 可選的key/provider),
15
+ * 輪到誰就用誰的CLI與模型
16
+ *
17
+ * @param {String} kind 輸入供應商種類字串,可選'opencode'、'claude'、'codex'
18
+ * @param {String} prompt 輸入提示詞字串,一律以stdin傳入子進程
19
+ * @param {Object} [opt={}] 輸入設定物件,原樣轉傳對應轉接器,各轉接器可用設定詳見dispatchOpencode、dispatchClaude、dispatchCodex,預設{}
20
+ * @returns {Promise} 回傳Promise,resolve回傳結果物件,內含ok(是否成功布林值)、stdout(標準輸出字串)、stderr(標準錯誤字串)、code(離開碼)、error(錯誤訊息字串,成功時為空字串)、durationMs(耗時毫秒)、attempts(實際嘗試次數),本函數不會reject
21
+ * @example
22
+ * //need claude, codex or opencode cli in system PATH
23
+ *
24
+ * import dispatchAi from './src/dispatchAi.mjs'
25
+ *
26
+ * let test = async () => {
27
+ *
28
+ * let r = await dispatchAi('claude', '請只回覆兩個字:完成', { model: 'sonnet' })
29
+ * console.log(r.ok, r.stdout.trim())
30
+ * // => true '完成'
31
+ *
32
+ * let re = await dispatchAi('gemini', 'abc')
33
+ * console.log(re.ok, re.error)
34
+ * // => false 'unknown ai kind: "gemini" (available: opencode, claude, codex)'
35
+ *
36
+ * }
37
+ * await test()
38
+ * .catch((err) => {
39
+ * console.log(err)
40
+ * })
41
+ *
42
+ */
43
+ async function dispatchAi(kind, prompt, opt = {}) {
44
+
45
+ //ks
46
+ let ks = keys(adapters)
47
+
48
+ //check kind, 須為對照表既有鍵名, 不可用物件取值判斷, 否則'constructor'等原型鍵會誤判為有效
49
+ if (!isestr(kind) || !includes(ks, kind)) {
50
+ return getErrorResult(`unknown ai kind: "${kind}" (available: ${ks.join(', ')})`)
51
+ }
52
+
53
+ //fn
54
+ let fn = adapters[kind]
55
+
56
+ //dispatch, prompt與opt之檢核由各轉接器自行負責
57
+ return fn(prompt, opt)
58
+ }
59
+
60
+
61
+ export default dispatchAi
@@ -0,0 +1,118 @@
1
+ import get from 'lodash-es/get.js'
2
+ import omit from 'lodash-es/omit.js'
3
+ import isbol from 'wsemi/src/isbol.mjs'
4
+ import isestr from 'wsemi/src/isestr.mjs'
5
+ import execCli from 'wsemi/src/execCli.mjs'
6
+ import getCliArgs from './getCliArgs.mjs'
7
+ import getErrorResult from './getErrorResult.mjs'
8
+
9
+
10
+ // dispatchClaude.mjs — 以Claude Code CLI呼叫Claude模型
11
+ //
12
+ // 【調用方式】參考全域技能dispatch-claude之慣例,依專案方針「不直接引用全域技能」,
13
+ // 將其調用方式移植於此,方便本專案自行偵錯、修改與擴充。
14
+ // 2026-08-08於本機實測:`claude -p --dangerously-skip-permissions --model sonnet` + stdin
15
+ // 可正確產出合規JSON摘要(15.5s)。
16
+ //
17
+ // 【認證】沿用Claude Code既有登入狀態(帳號層級),不需也不支援逐次注入API key,
18
+ // 故本轉接器無key參數——輪替時它是「一個獨立的供應商」而非「另一把金鑰」。
19
+
20
+
21
+ //預設值
22
+ let DEFAULT_EXE = 'claude'
23
+
24
+
25
+ //本轉接器自用之設定鍵, 其餘鍵一律原樣轉傳execCli
26
+ let OWN_KEYS = ['exe', 'model', 'skipPermissions', 'extraArgs', 'input']
27
+
28
+
29
+ /**
30
+ * 以Claude Code CLI呼叫Claude模型
31
+ *
32
+ * 特點:
33
+ * prompt一律走stdin而非位置參數,因摘要內文可達數萬字,當命令列參數會spawn ENAMETOOLONG;
34
+ * 沿用Claude Code既有登入狀態,無逐次注入API key之概念,故無key參數;
35
+ * 未給model時不帶`--model`旗標,由CLI自行決定使用模型;
36
+ * 預設帶`--dangerously-skip-permissions`令非互動之`-p`模式不因權限確認而卡住,
37
+ * 惟prompt含不可信內容(例如待摘要之網頁)時該內容之指示亦將無權限閘門執行,
38
+ * 可給予skipPermissions為false保留CLI權限閘門;
39
+ * 本函數不會reject,一律以結果物件之ok與error欄位回報成敗
40
+ *
41
+ * @param {String} prompt 輸入提示詞字串,一律以stdin傳入子進程
42
+ * @param {Object} [opt={}] 輸入設定物件,預設{}
43
+ * @param {String} [opt.exe='claude'] 輸入claude執行檔名稱或絕對路徑字串,給予名稱時由execCli自系統PATH解析,預設'claude'
44
+ * @param {String} [opt.model=''] 輸入模型別名或模型ID字串,例如'sonnet'、'opus',預設''代表不帶`--model`旗標
45
+ * @param {Boolean} [opt.skipPermissions=true] 輸入是否帶`--dangerously-skip-permissions`旗標布林值,false代表保留CLI權限閘門,預設true
46
+ * @param {Array} [opt.extraArgs=[]] 輸入額外命令列旗標字串陣列,將接於固定旗標之後,預設[]
47
+ * @param {Number} [opt.timeoutMs=120000] 輸入逾時毫秒正整數,逾時將強制關閉子進程及其子孫程序,預設120000
48
+ * @param {String} [opt.cwd=process.cwd()] 輸入子進程工作目錄字串,預設process.cwd()
49
+ * @param {String|Function} [opt.validate=undefined] 輸入stdout驗證規則字串或自訂驗證函數,規則字串支援'nonempty'、'json'、'min:100',多規則可用逗號串接,預設undefined代表不驗證
50
+ * @param {Number} [opt.maxRetries=0] 輸入失敗後最大重試次數非負整數,預設0
51
+ * @returns {Promise} 回傳Promise,resolve回傳結果物件,內含ok(是否成功布林值)、stdout(標準輸出字串)、stderr(標準錯誤字串)、code(離開碼)、error(錯誤訊息字串,成功時為空字串)、durationMs(耗時毫秒)、attempts(實際嘗試次數),本函數不會reject
52
+ * @example
53
+ * //need claude cli in system PATH
54
+ *
55
+ * import dispatchClaude from './src/dispatchClaude.mjs'
56
+ *
57
+ * let test = async () => {
58
+ *
59
+ * let r = await dispatchClaude('請只回覆兩個字:完成', { model: 'sonnet', timeoutMs: 120000 })
60
+ * console.log(r.ok, r.stdout.trim())
61
+ * // => true '完成'
62
+ *
63
+ * let re = await dispatchClaude('')
64
+ * console.log(re.ok, re.error)
65
+ * // => false 'prompt must be a non-empty string'
66
+ *
67
+ * }
68
+ * await test()
69
+ * .catch((err) => {
70
+ * console.log(err)
71
+ * })
72
+ *
73
+ */
74
+ async function dispatchClaude(prompt, opt = {}) {
75
+
76
+ //check prompt, 不reject故以錯誤結果物件回報
77
+ if (!isestr(prompt)) {
78
+ return getErrorResult('prompt must be a non-empty string')
79
+ }
80
+
81
+ //exe, 無效回退預設'claude', 由execCli自系統PATH解析實體路徑
82
+ let exe = get(opt, 'exe', null)
83
+ if (!isestr(exe)) {
84
+ exe = DEFAULT_EXE
85
+ }
86
+
87
+ //model, 無效時整段`--model`旗標不出現, 由CLI自行決定使用模型
88
+ let model = get(opt, 'model', null)
89
+
90
+ //skipPermissions, 非布林值回退預設true(非互動-p模式不卡權限確認)
91
+ let skipPermissions = get(opt, 'skipPermissions', null)
92
+ if (!isbol(skipPermissions)) {
93
+ skipPermissions = true
94
+ }
95
+
96
+ //extraArgs
97
+ let extraArgs = get(opt, 'extraArgs', null)
98
+
99
+ //args
100
+ let args = getCliArgs(
101
+ '-p',
102
+ skipPermissions ? '--dangerously-skip-permissions' : [],
103
+ isestr(model) ? ['--model', model] : [],
104
+ extraArgs,
105
+ )
106
+
107
+ //optCli, 剔除本轉接器自用鍵後原樣轉傳, 令呼叫端可用execCli全部設定(例如onStdout、maxBuffer)
108
+ let optCli = omit(opt, OWN_KEYS)
109
+
110
+ //execCli, prompt一律走stdin
111
+ return execCli(exe, args, {
112
+ ...optCli,
113
+ input: prompt,
114
+ })
115
+ }
116
+
117
+
118
+ export default dispatchClaude
@@ -0,0 +1,119 @@
1
+ import get from 'lodash-es/get.js'
2
+ import omit from 'lodash-es/omit.js'
3
+ import isestr from 'wsemi/src/isestr.mjs'
4
+ import execCli from 'wsemi/src/execCli.mjs'
5
+ import getCliArgs from './getCliArgs.mjs'
6
+ import getErrorResult from './getErrorResult.mjs'
7
+
8
+
9
+ // dispatchCodex.mjs — 以OpenAI Codex CLI呼叫GPT模型
10
+ //
11
+ // 【調用方式】參考全域技能dispatch-codex之慣例,依專案方針「不直接引用全域技能」,
12
+ // 將其調用方式移植於此,方便本專案自行偵錯、修改與擴充。
13
+ //
14
+ // 【走stdin而非位置參數】技能文件的範例是把prompt當位置參數,但摘要內文可達數萬字,
15
+ // 當命令列參數會spawn ENAMETOOLONG(本專案在opencode上已實際踩過)。
16
+ // 2026-08-08實測:codex exec未帶位置prompt時可從stdin讀取,
17
+ // 以gpt-5.6-luna產出合規JSON摘要(10.5s),與位置參數版結果等價。
18
+ //
19
+ // 【認證】沿用Codex CLI既有登入狀態,不逐次注入API key。
20
+
21
+
22
+ //預設值
23
+ let DEFAULT_EXE = 'codex'
24
+ let DEFAULT_SANDBOX = 'workspace-write'
25
+
26
+
27
+ //本轉接器自用之設定鍵, 其餘鍵一律原樣轉傳execCli
28
+ let OWN_KEYS = ['exe', 'model', 'sandbox', 'extraArgs', 'input']
29
+
30
+
31
+ /**
32
+ * 以OpenAI Codex CLI呼叫GPT模型
33
+ *
34
+ * 特點:
35
+ * prompt一律走stdin而非位置參數,因摘要內文可達數萬字,當命令列參數會spawn ENAMETOOLONG;
36
+ * 固定帶`--skip-git-repo-check`,令非git倉庫之工作目錄亦可執行;
37
+ * 沿用Codex CLI既有登入狀態,無逐次注入API key之概念,故無key參數;
38
+ * 未給model時不帶`-m`旗標,由CLI自行決定使用模型;
39
+ * 本函數不會reject,一律以結果物件之ok與error欄位回報成敗
40
+ *
41
+ * @param {String} prompt 輸入提示詞字串,一律以stdin傳入子進程
42
+ * @param {Object} [opt={}] 輸入設定物件,預設{}
43
+ * @param {String} [opt.exe='codex'] 輸入codex執行檔名稱或絕對路徑字串,給予名稱時由execCli自系統PATH解析,預設'codex'
44
+ * @param {String} [opt.model=''] 輸入模型ID字串,例如'gpt-5.6-luna',預設''代表不帶`-m`旗標
45
+ * @param {String} [opt.sandbox='workspace-write'] 輸入沙箱模式字串,例如'read-only'、'workspace-write'、'danger-full-access',預設'workspace-write'
46
+ * @param {Array} [opt.extraArgs=[]] 輸入額外命令列旗標字串陣列,例如['--config', 'model_reasoning_effort="max"'],將接於固定旗標之後,預設[]
47
+ * @param {Number} [opt.timeoutMs=120000] 輸入逾時毫秒正整數,逾時將強制關閉子進程及其子孫程序,預設120000
48
+ * @param {String} [opt.cwd=process.cwd()] 輸入子進程工作目錄字串,預設process.cwd()
49
+ * @param {String|Function} [opt.validate=undefined] 輸入stdout驗證規則字串或自訂驗證函數,規則字串支援'nonempty'、'json'、'min:100',多規則可用逗號串接,預設undefined代表不驗證
50
+ * @param {Number} [opt.maxRetries=0] 輸入失敗後最大重試次數非負整數,預設0
51
+ * @returns {Promise} 回傳Promise,resolve回傳結果物件,內含ok(是否成功布林值)、stdout(標準輸出字串)、stderr(標準錯誤字串)、code(離開碼)、error(錯誤訊息字串,成功時為空字串)、durationMs(耗時毫秒)、attempts(實際嘗試次數),本函數不會reject
52
+ * @example
53
+ * //need codex cli in system PATH
54
+ *
55
+ * import dispatchCodex from './src/dispatchCodex.mjs'
56
+ *
57
+ * let test = async () => {
58
+ *
59
+ * let r = await dispatchCodex('請只回覆兩個字:完成', { model: 'gpt-5.6-luna', sandbox: 'read-only' })
60
+ * console.log(r.ok, r.stdout.includes('完成'))
61
+ * // => true true
62
+ *
63
+ * let re = await dispatchCodex('abc', { exe: 'codex-not-exist' })
64
+ * console.log(re.ok, re.error.includes('ENOENT'))
65
+ * // => false true
66
+ *
67
+ * }
68
+ * await test()
69
+ * .catch((err) => {
70
+ * console.log(err)
71
+ * })
72
+ *
73
+ */
74
+ async function dispatchCodex(prompt, opt = {}) {
75
+
76
+ //check prompt, 不reject故以錯誤結果物件回報
77
+ if (!isestr(prompt)) {
78
+ return getErrorResult('prompt must be a non-empty string')
79
+ }
80
+
81
+ //exe, 無效回退預設'codex', 由execCli自系統PATH解析實體路徑
82
+ let exe = get(opt, 'exe', null)
83
+ if (!isestr(exe)) {
84
+ exe = DEFAULT_EXE
85
+ }
86
+
87
+ //model, 無效時整段`-m`旗標不出現, 由CLI自行決定使用模型
88
+ let model = get(opt, 'model', null)
89
+
90
+ //sandbox, 無效回退預設'workspace-write'
91
+ let sandbox = get(opt, 'sandbox', null)
92
+ if (!isestr(sandbox)) {
93
+ sandbox = DEFAULT_SANDBOX
94
+ }
95
+
96
+ //extraArgs
97
+ let extraArgs = get(opt, 'extraArgs', null)
98
+
99
+ //args, --skip-git-repo-check令非git倉庫之工作目錄亦可執行, 缺此旗標codex會拒絕執行
100
+ let args = getCliArgs(
101
+ 'exec',
102
+ ['--sandbox', sandbox],
103
+ '--skip-git-repo-check',
104
+ isestr(model) ? ['-m', model] : [],
105
+ extraArgs,
106
+ )
107
+
108
+ //optCli, 剔除本轉接器自用鍵後原樣轉傳, 令呼叫端可用execCli全部設定(例如onStdout、maxBuffer)
109
+ let optCli = omit(opt, OWN_KEYS)
110
+
111
+ //execCli, prompt一律走stdin
112
+ return execCli(exe, args, {
113
+ ...optCli,
114
+ input: prompt,
115
+ })
116
+ }
117
+
118
+
119
+ export default dispatchCodex