import get from 'lodash-es/get.js'
import omit from 'lodash-es/omit.js'
import isbol from 'wsemi/src/isbol.mjs'
import isestr from 'wsemi/src/isestr.mjs'
import execCli from 'wsemi/src/execCli.mjs'
import getCliArgs from './getCliArgs.mjs'
import getErrorResult from './getErrorResult.mjs'
import { attachErrorType } from './getErrorType.mjs'
import castPintOr from './castPintOr.mjs'
import dfTimeoutMs from './dfTimeoutMs.mjs'
// dispatchClaude.mjs — 以Claude Code CLI呼叫Claude模型
//
// 【調用方式】參考全域技能dispatch-claude之慣例,依專案方針「不直接引用全域技能」,
// 將其調用方式移植於此,方便本專案自行偵錯、修改與擴充。
// 2026-08-08於本機實測:`claude -p --dangerously-skip-permissions --model sonnet` + stdin
// 可正確產出合規JSON摘要(15.5s)。
//
// 【認證】沿用Claude Code既有登入狀態(帳號層級),不需也不支援逐次注入API key,
// 故本轉接器無key參數——輪替時它是「一個獨立的供應商」而非「另一把金鑰」。
//
// 【勿帶--bare, 並留意其未來成為-p預設(2026-09-23查官方headless文件)】文件原文:
// 「--bare is the recommended mode for scripted and SDK calls, and will become the default for -p in a
// future release」且「In bare mode, Claude Code never reads OAuth credentials or the system keychain」。
// 本轉接器靠訂閱登入(OAuth), 故extraArgs勿帶--bare(會直接認證失敗); 若日後某版把-p預設改為bare,
// 所有訂閱條目會一併失效——徵狀為非零離開碼, 失敗訊息印在stdout而非stderr(官方: 執行期失敗如未登入
// 以result印在stdout)。2.1.280尚無退出bare之旗標, 無法預先處理; 每次升級Claude Code後跑一次claude
// 條目即可偵測, 屆時再查該版之退出方式調整此處。
// 另兩點同日查證: stdin傳入之prompt上限10MB(超過即非零離開, 見@param prompt);
// 2.1.280起Pro/Team Standard方案未給model時預設由Sonnet改為Opus(只影響未帶--model之呼叫)。
//預設值
let DEFAULT_EXE = 'claude'
//本轉接器自用之設定鍵, 其餘鍵一律原樣轉傳execCli
let OWN_KEYS = ['exe', 'model', 'skipPermissions', 'extraArgs', 'input']
/**
* 以Claude Code CLI呼叫Claude模型
*
* 特點:
* prompt一律走stdin而非位置參數,因摘要內文可達數萬字,當命令列參數會spawn ENAMETOOLONG;
* 沿用Claude Code既有登入狀態,無逐次注入API key之概念,故無key參數;
* 未給model時不帶`--model`旗標,由CLI自行決定使用模型;
* 預設帶`--dangerously-skip-permissions`令非互動之`-p`模式不因權限確認而卡住,
* 惟prompt含不可信內容(例如待摘要之網頁)時該內容之指示亦將無權限閘門執行,
* 可給予skipPermissions為false保留CLI權限閘門;
* 本函數不會reject,一律以結果物件之ok與error欄位回報成敗
*
* @param {String} prompt 輸入提示詞字串,一律以stdin傳入子進程(Claude Code之stdin上限10MB,超過即非零離開)
* @param {Object} [opt={}] 輸入設定物件,預設{}
* @param {String} [opt.exe='claude'] 輸入claude執行檔名稱或絕對路徑字串,給予名稱時由execCli自系統PATH解析,預設'claude'
* @param {String} [opt.model=''] 輸入模型別名或模型ID字串,例如'claude-opus-5-5'(全名, 固定版本)、'opus'或'sonnet'(別名, 隨CLI指向最新版),預設''代表不帶`--model`旗標,由CLI依方案決定(2.1.280起Pro/Team Standard亦預設Opus)
* @param {Boolean} [opt.skipPermissions=true] 輸入是否帶`--dangerously-skip-permissions`旗標布林值,false代表保留CLI權限閘門,預設true
* @param {Array} [opt.extraArgs=[]] 輸入額外命令列旗標字串陣列,將接於固定旗標之後,預設[]
* @param {Number} [opt.timeoutMs=300000] 輸入逾時毫秒正整數,逾時將強制關閉子進程及其子孫程序,全套件統一預設300000
* @param {String} [opt.cwd=process.cwd()] 輸入子進程工作目錄字串,預設process.cwd()
* @param {String|Function} [opt.validate=undefined] 輸入stdout驗證規則字串或自訂驗證函數,規則字串支援'nonempty'、'json'、'min:100',多規則可用逗號串接,預設undefined代表不驗證
* @param {Number} [opt.maxRetries=0] 輸入失敗後最大重試次數非負整數,預設0
* @returns {Promise} 回傳Promise,resolve回傳結果物件,內含ok(是否成功布林值)、stdout(標準輸出字串)、stderr(標準錯誤字串)、code(離開碼)、error(錯誤訊息字串,成功時為空字串)、durationMs(耗時毫秒)、attempts(實際嘗試次數),本函數不會reject
* @example
* //need claude cli in system PATH
*
* import dispatchClaude from './src/dispatchClaude.mjs'
*
* let test = async () => {
*
* let r = await dispatchClaude('請只回覆兩個字:完成', { model: 'sonnet' })
* console.log(r.ok, r.stdout.trim())
* // => true '完成'
*
* let re = await dispatchClaude('')
* console.log(re.ok, re.error)
* // => false 'prompt must be a non-empty string'
*
* }
* await test()
* .catch((err) => {
* console.log(err)
* })
*
*/
async function dispatchClaude(prompt, opt = {}) {
//check prompt, 不reject故以錯誤結果物件回報
if (!isestr(prompt)) {
return getErrorResult('prompt must be a non-empty string')
}
//exe, 無效回退預設'claude', 由execCli自系統PATH解析實體路徑
let exe = get(opt, 'exe', null)
if (!isestr(exe)) {
exe = DEFAULT_EXE
}
//model, 無效時整段`--model`旗標不出現, 由CLI自行決定使用模型
let model = get(opt, 'model', null)
//skipPermissions, 非布林值回退預設true(非互動-p模式不卡權限確認)
let skipPermissions = get(opt, 'skipPermissions', null)
if (!isbol(skipPermissions)) {
skipPermissions = true
}
//extraArgs
let extraArgs = get(opt, 'extraArgs', null)
//args
let args = getCliArgs(
'-p',
skipPermissions ? '--dangerously-skip-permissions' : [],
isestr(model) ? ['--model', model] : [],
extraArgs,
)
//timeoutMs, 無效回退全套件統一預設(dfTimeoutMs=300000), 各轉接器一致令呼叫方無須記多套數字
let timeoutMs = castPintOr(get(opt, 'timeoutMs', null), dfTimeoutMs)
//optCli, 剔除本轉接器自用鍵後原樣轉傳, 令呼叫端可用execCli全部設定(例如onStdout、maxBuffer)
let optCli = omit(opt, OWN_KEYS)
//execCli, prompt一律走stdin; 失敗結果補上機器可讀之errorType(僅機械可判者, 見getErrorType.mjs)
let r = await execCli(exe, args, {
...optCli,
input: prompt,
timeoutMs,
})
return attachErrorType(r)
}
export default dispatchClaude