@cocorograph/hub-agent 0.7.25 → 0.7.26
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/package.json +1 -1
- package/src/config.mjs +32 -0
- package/src/enroll.mjs +4 -1
- package/src/main.mjs +15 -1
- package/src/tmux.mjs +122 -11
- package/src/ws-client.mjs +3 -0
package/package.json
CHANGED
package/src/config.mjs
CHANGED
|
@@ -10,6 +10,10 @@
|
|
|
10
10
|
import { promises as fs, constants as fsConsts, readFileSync } from "node:fs"
|
|
11
11
|
import path from "node:path"
|
|
12
12
|
import os from "node:os"
|
|
13
|
+
import { execFile } from "node:child_process"
|
|
14
|
+
import { promisify } from "node:util"
|
|
15
|
+
|
|
16
|
+
const _execFileP = promisify(execFile)
|
|
13
17
|
|
|
14
18
|
/**
|
|
15
19
|
* 実行プラットフォームを返す。Hub の UI が OS 別コマンドを出し分けるために使う。
|
|
@@ -35,6 +39,34 @@ export function detectPlatform() {
|
|
|
35
39
|
return process.platform || "unknown"
|
|
36
40
|
}
|
|
37
41
|
|
|
42
|
+
/** このホストで起動可否を検出する CLI 種別の候補。 */
|
|
43
|
+
export const CLI_CANDIDATES = ["claude", "codex"]
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* PATH 上に存在する CLI 種別 (capability) を検出する。Cockpit が「この hub-agent で
|
|
47
|
+
* どの CLI を起動できるか」を知るため、起動時に 1 回呼んで enroll/hello payload で
|
|
48
|
+
* Hub に広告する (`CockpitAgent.available_clis`)。which/where が成功したものだけ返す。
|
|
49
|
+
*
|
|
50
|
+
* @param {{candidates?:string[], exec?:(cmd:string,args:string[])=>Promise<any>}} [opts]
|
|
51
|
+
* exec はテスト差し替え用 (既定は which/where を叩く promisified execFile)。
|
|
52
|
+
* @returns {Promise<string[]>} 例: ["claude", "codex"]
|
|
53
|
+
*/
|
|
54
|
+
export async function detectAvailableClis(opts = {}) {
|
|
55
|
+
const candidates = opts.candidates || CLI_CANDIDATES
|
|
56
|
+
const exec = opts.exec || _execFileP
|
|
57
|
+
const lookup = process.platform === "win32" ? "where" : "which"
|
|
58
|
+
const found = []
|
|
59
|
+
for (const cli of candidates) {
|
|
60
|
+
try {
|
|
61
|
+
await exec(lookup, [cli])
|
|
62
|
+
found.push(cli)
|
|
63
|
+
} catch {
|
|
64
|
+
/* PATH に無い = 未インストール */
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return found
|
|
68
|
+
}
|
|
69
|
+
|
|
38
70
|
function resolveConfigDir() {
|
|
39
71
|
if (process.env.HUB_AGENT_CONFIG_DIR) return process.env.HUB_AGENT_CONFIG_DIR
|
|
40
72
|
return path.join(os.homedir(), ".hub")
|
package/src/enroll.mjs
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import os from "node:os"
|
|
10
10
|
|
|
11
|
-
import { detectPlatform, hasConfig, writeConfig } from "./config.mjs"
|
|
11
|
+
import { detectAvailableClis, detectPlatform, hasConfig, writeConfig } from "./config.mjs"
|
|
12
12
|
|
|
13
13
|
const DEFAULT_HUB_URL = process.env.HUB_URL || "https://hub.cocorograph.com"
|
|
14
14
|
|
|
@@ -34,6 +34,8 @@ export async function enroll(enrollmentToken, opts = {}) {
|
|
|
34
34
|
throw new Error("global fetch がありません。Node 20+ で実行してください。")
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
const availableClis = await detectAvailableClis()
|
|
38
|
+
|
|
37
39
|
const res = await fetchImpl(`${hubUrl}/api/cockpit/agents/enroll-complete/`, {
|
|
38
40
|
method: "POST",
|
|
39
41
|
headers: { "content-type": "application/json" },
|
|
@@ -42,6 +44,7 @@ export async function enroll(enrollmentToken, opts = {}) {
|
|
|
42
44
|
hostname,
|
|
43
45
|
version,
|
|
44
46
|
platform: detectPlatform(),
|
|
47
|
+
available_clis: availableClis,
|
|
45
48
|
}),
|
|
46
49
|
})
|
|
47
50
|
|
package/src/main.mjs
CHANGED
|
@@ -19,7 +19,7 @@ import path from "node:path"
|
|
|
19
19
|
|
|
20
20
|
import pino from "pino"
|
|
21
21
|
|
|
22
|
-
import { detectPlatform, readConfig, writeConfig } from "./config.mjs"
|
|
22
|
+
import { detectAvailableClis, detectPlatform, readConfig, writeConfig } from "./config.mjs"
|
|
23
23
|
import { extractLastAssistantText, pickContextText } from "./extract-paragraph.mjs"
|
|
24
24
|
import { loadPlugins, runHookBroadcast, runHookChain } from "./plugin-loader.mjs"
|
|
25
25
|
import { WsClient } from "./ws-client.mjs"
|
|
@@ -442,10 +442,15 @@ export async function startDaemon({ version, ptyModule, claudeSdk } = {}) {
|
|
|
442
442
|
logger.info({ bundleVersion }, "hub bundle version detected")
|
|
443
443
|
}
|
|
444
444
|
|
|
445
|
+
// ホストで起動可能な CLI (claude / codex) を 1 回検出して hello で広告する。
|
|
446
|
+
const availableClis = await detectAvailableClis()
|
|
447
|
+
logger.info({ availableClis }, "available CLIs detected")
|
|
448
|
+
|
|
445
449
|
const client = new WsClient(config, {
|
|
446
450
|
logger,
|
|
447
451
|
version,
|
|
448
452
|
platform: detectPlatform(),
|
|
453
|
+
availableClis,
|
|
449
454
|
bundleVersion,
|
|
450
455
|
bundleVersionProvider: readBundleVersionSync,
|
|
451
456
|
bundleManifestPath: BUNDLE_MANIFEST_PATH,
|
|
@@ -3224,10 +3229,19 @@ async function dispatch(msg, ctx) {
|
|
|
3224
3229
|
}
|
|
3225
3230
|
try {
|
|
3226
3231
|
await createTmuxSession(name, cwd, {
|
|
3232
|
+
// cli_kind: 'codex' なら codex を、それ以外は claude を pane 起動する (既定 claude)。
|
|
3233
|
+
cliKind: msg.cli_kind === "codex" ? "codex" : "claude",
|
|
3227
3234
|
claudeCmd:
|
|
3228
3235
|
typeof msg.claude_cmd === "string"
|
|
3229
3236
|
? msg.claude_cmd
|
|
3230
3237
|
: claudeCmdFromAgentConfig(ctx.config),
|
|
3238
|
+
codexCmd: typeof msg.codex_cmd === "string" ? msg.codex_cmd : undefined,
|
|
3239
|
+
// codex 用設定 (claude 経路では claudeCmd を直接使うため無視される)。
|
|
3240
|
+
model: typeof msg.model === "string" ? msg.model : undefined,
|
|
3241
|
+
sandbox: typeof msg.sandbox === "string" ? msg.sandbox : undefined,
|
|
3242
|
+
approvalPolicy:
|
|
3243
|
+
typeof msg.approval_policy === "string" ? msg.approval_policy : undefined,
|
|
3244
|
+
effort: typeof msg.effort === "string" ? msg.effort : undefined,
|
|
3231
3245
|
initialPrompt:
|
|
3232
3246
|
typeof msg.initial_prompt === "string" ? msg.initial_prompt : undefined,
|
|
3233
3247
|
logger: ctx.logger,
|
package/src/tmux.mjs
CHANGED
|
@@ -840,16 +840,29 @@ export async function createSession(name, cwd, opts = {}) {
|
|
|
840
840
|
// display-message 失敗は致命的でないので飲み込む
|
|
841
841
|
}
|
|
842
842
|
}
|
|
843
|
-
|
|
844
|
-
//
|
|
845
|
-
//
|
|
846
|
-
const
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
843
|
+
// cliKind に応じて claude / codex いずれの CLI を pane で起動するか分岐する。
|
|
844
|
+
// 既定は claude(後方互換)。initialPrompt はどちらも位置引数として末尾注入する
|
|
845
|
+
// (claude "<p>" / codex "<p>" / codex resume <id> "<p>" いずれも [PROMPT] を受ける)。
|
|
846
|
+
const launchCmd =
|
|
847
|
+
opts.cliKind === "codex"
|
|
848
|
+
? injectInitialPrompt(
|
|
849
|
+
injectCodexHomeEnv(
|
|
850
|
+
opts.codexCmd ?? buildCodexCmd(opts),
|
|
851
|
+
opts.codexHome ?? process.env.CODEX_HOME ?? null,
|
|
852
|
+
),
|
|
853
|
+
opts.initialPrompt,
|
|
854
|
+
)
|
|
855
|
+
: injectInitialPrompt(
|
|
856
|
+
// アカウント・プロファイル切替 (CLAUDE_CONFIG_DIR) を TUI セッションにも効かせる。
|
|
857
|
+
// applyActiveProfileEnv が process.env を最新に保つため、既定では env から拾う。
|
|
858
|
+
injectConfigDirEnv(
|
|
859
|
+
opts.claudeCmd ?? DEFAULT_CLAUDE_CMD,
|
|
860
|
+
opts.claudeConfigDir ?? process.env.CLAUDE_CONFIG_DIR ?? null,
|
|
861
|
+
),
|
|
862
|
+
opts.initialPrompt,
|
|
863
|
+
)
|
|
864
|
+
if (launchCmd) {
|
|
865
|
+
await execFileP(tmuxBin(opts), ["send-keys", "-t", name, launchCmd, "Enter"])
|
|
853
866
|
}
|
|
854
867
|
}
|
|
855
868
|
|
|
@@ -932,6 +945,99 @@ export function buildFreshCmd(opts = {}) {
|
|
|
932
945
|
return `claude${flags ? " " + flags : ""}`.trim()
|
|
933
946
|
}
|
|
934
947
|
|
|
948
|
+
// ---------------------------------------------------------------------------
|
|
949
|
+
// Codex CLI 起動コマンドビルダ (claude 版と同形の純粋関数)
|
|
950
|
+
//
|
|
951
|
+
// 設計メモ:
|
|
952
|
+
// - codex の対話起動フラグ (v0.142.x 実機確認): `-m/--model` / `-s/--sandbox`
|
|
953
|
+
// {read-only|workspace-write|danger-full-access} / `-a/--ask-for-approval`
|
|
954
|
+
// {untrusted|on-request|never} / `-C/--cd` / `-p/--profile`。
|
|
955
|
+
// - 値は安全なトークン (モデル名・enum) を前提に bare で並べる (claude 版と同方針)。
|
|
956
|
+
// - codex は claude の `--continue` のような **cwd スコープ継続を持たない**
|
|
957
|
+
// (rollout は ~/.codex/sessions/Y/M/D に日付シャードで保存され cwd 非依存)。
|
|
958
|
+
// そのため既定は fresh 起動とし、継続は明示 session_id で resume する。
|
|
959
|
+
// - アカウント/プロファイル切替は CLAUDE_CONFIG_DIR に対応する `CODEX_HOME` の
|
|
960
|
+
// ディレクトリ切替で行う (injectCodexHomeEnv)。
|
|
961
|
+
// ---------------------------------------------------------------------------
|
|
962
|
+
|
|
963
|
+
/**
|
|
964
|
+
* agent 設定から codex 起動 flags を組み立てる (純粋関数)。
|
|
965
|
+
* model / sandbox / approvalPolicy / effort のうち指定されたものだけを並べる。
|
|
966
|
+
* @param {{model?:string,sandbox?:string,approvalPolicy?:string,effort?:string}} [opts]
|
|
967
|
+
* @returns {string}
|
|
968
|
+
*/
|
|
969
|
+
export function composeCodexFlags(opts = {}) {
|
|
970
|
+
const model = (opts.model || "").trim()
|
|
971
|
+
const sandbox = (opts.sandbox || "").trim()
|
|
972
|
+
const approval = (opts.approvalPolicy || "").trim()
|
|
973
|
+
const effort = (opts.effort || "").trim()
|
|
974
|
+
const flags = []
|
|
975
|
+
if (model) flags.push(`--model ${model}`)
|
|
976
|
+
if (sandbox) flags.push(`--sandbox ${sandbox}`)
|
|
977
|
+
if (approval) flags.push(`--ask-for-approval ${approval}`)
|
|
978
|
+
// effort はフラグが無いため -c 設定上書きで渡す (TOML 解析失敗時は raw 文字列)。
|
|
979
|
+
if (effort) flags.push(`-c model_reasoning_effort=${effort}`)
|
|
980
|
+
return flags.join(" ")
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
/**
|
|
984
|
+
* codex 既定起動コマンド (claude の buildClaudeCmd 相当)。
|
|
985
|
+
* hub オプション (model/sandbox/approval/effort) があれば反映、無ければ
|
|
986
|
+
* HUB_CODEX_CMD 環境変数、どちらも無ければ最小デフォルト `codex`。
|
|
987
|
+
* @param {{model?:string,sandbox?:string,approvalPolicy?:string,effort?:string}} [opts]
|
|
988
|
+
* @returns {string}
|
|
989
|
+
*/
|
|
990
|
+
export function buildCodexCmd(opts = {}) {
|
|
991
|
+
const flags = composeCodexFlags(opts)
|
|
992
|
+
if (flags) return `codex ${flags}`.trim()
|
|
993
|
+
if (process.env.HUB_CODEX_CMD) return process.env.HUB_CODEX_CMD
|
|
994
|
+
return "codex"
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
/**
|
|
998
|
+
* まっさらな新規 codex セッション起動用コマンド (純粋関数)。
|
|
999
|
+
* resume を付けないので codex は新しい rollout で起動する。
|
|
1000
|
+
* @param {{model?:string,sandbox?:string,approvalPolicy?:string,effort?:string}} [opts]
|
|
1001
|
+
* @returns {string}
|
|
1002
|
+
*/
|
|
1003
|
+
export function buildCodexFreshCmd(opts = {}) {
|
|
1004
|
+
const flags = composeCodexFlags(opts)
|
|
1005
|
+
return `codex${flags ? " " + flags : ""}`.trim()
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
/**
|
|
1009
|
+
* TUI 載せ替え用の `codex resume <id> [flags]` コマンド (純粋関数)。
|
|
1010
|
+
* session_id を明示 resume することで表示と書込の rollout を一致させる。
|
|
1011
|
+
* @param {string} sessionId codex セッション id (UUID 等)
|
|
1012
|
+
* @param {{model?:string,sandbox?:string,approvalPolicy?:string,effort?:string}} [opts]
|
|
1013
|
+
* @returns {string}
|
|
1014
|
+
* @throws session_id が安全形でなければ throw (コマンド注入防止)
|
|
1015
|
+
*/
|
|
1016
|
+
export function buildCodexResumeCmd(sessionId, opts = {}) {
|
|
1017
|
+
if (!isSafeSessionId(sessionId)) {
|
|
1018
|
+
throw new Error(`unsafe session_id for codex resume: ${String(sessionId)}`)
|
|
1019
|
+
}
|
|
1020
|
+
const flags = composeCodexFlags(opts)
|
|
1021
|
+
return `codex resume ${sessionId}${flags ? " " + flags : ""}`.trim()
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
/**
|
|
1025
|
+
* codexCmd の各セグメント先頭に `CODEX_HOME=<dir>` 環境変数代入を注入する
|
|
1026
|
+
* (claude の injectConfigDirEnv 相当)。codexHome が空なら何もしない。
|
|
1027
|
+
* @param {string} codexCmd
|
|
1028
|
+
* @param {string} codexHome
|
|
1029
|
+
* @returns {string}
|
|
1030
|
+
*/
|
|
1031
|
+
export function injectCodexHomeEnv(codexCmd, codexHome) {
|
|
1032
|
+
if (!codexHome || typeof codexHome !== "string") return codexCmd
|
|
1033
|
+
if (typeof codexCmd !== "string" || codexCmd.length === 0) return codexCmd
|
|
1034
|
+
const assignment = `CODEX_HOME=${JSON.stringify(codexHome)}`
|
|
1035
|
+
return codexCmd
|
|
1036
|
+
.split("||")
|
|
1037
|
+
.map((part) => `${assignment} ${part.trim()}`)
|
|
1038
|
+
.join(" || ")
|
|
1039
|
+
}
|
|
1040
|
+
|
|
935
1041
|
const _delay = (ms) => new Promise((r) => setTimeout(r, ms))
|
|
936
1042
|
|
|
937
1043
|
/**
|
|
@@ -1772,7 +1878,12 @@ export async function rebindClaudeSession(name, sessionId, opts = {}) {
|
|
|
1772
1878
|
let cmd
|
|
1773
1879
|
try {
|
|
1774
1880
|
// fresh=新規起動 (--resume 無し)。それ以外は session_id 検証込みの resume。
|
|
1775
|
-
|
|
1881
|
+
// cliKind に応じて claude / codex のビルダを使い分ける (既定は claude)。
|
|
1882
|
+
if (opts.cliKind === "codex") {
|
|
1883
|
+
cmd = opts.fresh ? buildCodexFreshCmd(opts) : buildCodexResumeCmd(sessionId, opts)
|
|
1884
|
+
} else {
|
|
1885
|
+
cmd = opts.fresh ? buildFreshCmd(opts) : buildResumeCmd(sessionId, opts)
|
|
1886
|
+
}
|
|
1776
1887
|
} catch (err) {
|
|
1777
1888
|
return { ok: false, error: err?.message || String(err) }
|
|
1778
1889
|
}
|
package/src/ws-client.mjs
CHANGED
|
@@ -113,6 +113,8 @@ export class WsClient extends EventEmitter {
|
|
|
113
113
|
this.bundleWatchDebounceTimer = null
|
|
114
114
|
this.hostname = opts.hostname || os.hostname()
|
|
115
115
|
this.platform = opts.platform || "unknown"
|
|
116
|
+
// このホストで起動可能な CLI 種別 (capability)。起動時に 1 回検出して hello で広告する。
|
|
117
|
+
this.availableClis = Array.isArray(opts.availableClis) ? opts.availableClis : []
|
|
116
118
|
this.ws = null
|
|
117
119
|
this.heartbeatTimer = null
|
|
118
120
|
this.reconnectTimer = null
|
|
@@ -226,6 +228,7 @@ export class WsClient extends EventEmitter {
|
|
|
226
228
|
version: this.version,
|
|
227
229
|
bundle_version: this.bundleVersion,
|
|
228
230
|
platform: this.platform,
|
|
231
|
+
available_clis: this.availableClis,
|
|
229
232
|
})
|
|
230
233
|
// Plan ε: 毎回 reconnect 直後に backend ↔ agent の stream_id を能動同期する
|
|
231
234
|
// (orphan stream の即時 kill 用)。response は main.mjs の dispatch が拾い、
|