@cocorograph/hub-agent 0.7.24 → 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/claude-history.mjs +81 -0
- package/src/config.mjs +32 -0
- package/src/enroll.mjs +4 -1
- package/src/main.mjs +50 -1
- package/src/tmux.mjs +122 -11
- package/src/ws-client.mjs +3 -0
package/package.json
CHANGED
package/src/claude-history.mjs
CHANGED
|
@@ -341,6 +341,87 @@ export async function listSessions({ cwd, projectsRoot, limit = 30, logger }) {
|
|
|
341
341
|
* @param {{viewingSessionId?: string|null, newestSessionId?: string|null, lastNotifiedNewId?: string|null, lastNotifiedAt?: number|null, now?: number|null, reNotifyMs?: number}} args
|
|
342
342
|
* @returns {{rotated: boolean, newSessionId?: string}}
|
|
343
343
|
*/
|
|
344
|
+
/**
|
|
345
|
+
* 回転候補 jsonl が「bound TUI セッションの本物の /clear 先」か「別アクティビティの jsonl」かを
|
|
346
|
+
* head 4KB だけ読んで判定する純粋ヘルパ (2a 根治)。
|
|
347
|
+
*
|
|
348
|
+
* 背景: 同一 cwd-encoded dir には bound TUI 以外の jsonl が混入し得る:
|
|
349
|
+
* - Task tool subagent (`parent_uuid` / `parentUuid` 付き)
|
|
350
|
+
* - SDK headless `claude -p` (末尾近傍に `type=result`)
|
|
351
|
+
* - 並走 cockpit / 別ターミナル起動の別 TUI (cwd は一致するため弱い signal)
|
|
352
|
+
* - 既存 jsonl が touch されて mtime 最新化 (cwd が viewCwd と不一致なケースは cross-cwd 汚染)
|
|
353
|
+
* これらに対して decideSessionRotation は newest≠viewing で無条件発火するため、ローテに乗ると
|
|
354
|
+
* viewer が別 jsonl へ rotate-bind されて /clear 後の三点リーダー固着・誤生成中表示を起こす。
|
|
355
|
+
*
|
|
356
|
+
* 検出基準 (どれか 1 つでも該当したら reject = 抑止):
|
|
357
|
+
* - `parent_uuid` / `parentUuid` / `parent_session_id` / `parentSessionId` (subagent marker)
|
|
358
|
+
* - `type === 'result'` (SDK headless の完了 marker。先頭 4KB 内に現れたら頭 0KB の極短セッション)
|
|
359
|
+
* - `cwd` フィールドが `viewCwd` と完全不一致 (cross-cwd 汚染)
|
|
360
|
+
*
|
|
361
|
+
* 失敗安全: head 読み失敗 / parse 失敗時は accept (旧挙動 = 即時 rotate へ degrade)。
|
|
362
|
+
* 「/clear が壊れる」リスクは取らないため、不確実時は安全側 (accept) に倒す。
|
|
363
|
+
*
|
|
364
|
+
* @param {{candidatePath?: string|null, viewCwd?: string|null, readImpl?: (p:string,n:number)=>Promise<string|null>, logger?: import('pino').Logger}} args
|
|
365
|
+
* @returns {Promise<{accept: boolean, reason: string}>}
|
|
366
|
+
*/
|
|
367
|
+
export async function validateRotationCandidate({
|
|
368
|
+
candidatePath,
|
|
369
|
+
viewCwd,
|
|
370
|
+
readImpl,
|
|
371
|
+
logger,
|
|
372
|
+
} = {}) {
|
|
373
|
+
if (!candidatePath || !viewCwd) {
|
|
374
|
+
return { accept: true, reason: "missing_args" }
|
|
375
|
+
}
|
|
376
|
+
let text
|
|
377
|
+
try {
|
|
378
|
+
if (typeof readImpl === "function") {
|
|
379
|
+
text = await readImpl(candidatePath, 4096)
|
|
380
|
+
} else {
|
|
381
|
+
const handle = await open(candidatePath, "r")
|
|
382
|
+
try {
|
|
383
|
+
const buf = Buffer.allocUnsafe(4096)
|
|
384
|
+
const { bytesRead } = await handle.read(buf, 0, buf.length, 0)
|
|
385
|
+
text = buf.toString("utf-8", 0, bytesRead)
|
|
386
|
+
} finally {
|
|
387
|
+
await handle.close().catch(() => {})
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
} catch (err) {
|
|
391
|
+
logger?.warn(
|
|
392
|
+
{ err: err?.message, candidatePath },
|
|
393
|
+
"rotation candidate head read failed → accept (degrade to legacy behavior)",
|
|
394
|
+
)
|
|
395
|
+
return { accept: true, reason: `read_failed:${err?.code || "unknown"}` }
|
|
396
|
+
}
|
|
397
|
+
if (!text) return { accept: true, reason: "empty_text" }
|
|
398
|
+
for (const line of text.split("\n")) {
|
|
399
|
+
if (!line) continue
|
|
400
|
+
let obj
|
|
401
|
+
try {
|
|
402
|
+
obj = JSON.parse(line)
|
|
403
|
+
} catch {
|
|
404
|
+
continue
|
|
405
|
+
}
|
|
406
|
+
if (!obj || typeof obj !== "object") continue
|
|
407
|
+
if (
|
|
408
|
+
obj.parent_uuid ||
|
|
409
|
+
obj.parentUuid ||
|
|
410
|
+
obj.parent_session_id ||
|
|
411
|
+
obj.parentSessionId
|
|
412
|
+
) {
|
|
413
|
+
return { accept: false, reason: "subagent_marker" }
|
|
414
|
+
}
|
|
415
|
+
if (obj.type === "result") {
|
|
416
|
+
return { accept: false, reason: "headless_result_marker" }
|
|
417
|
+
}
|
|
418
|
+
if (typeof obj.cwd === "string" && obj.cwd && obj.cwd !== viewCwd) {
|
|
419
|
+
return { accept: false, reason: "cwd_mismatch" }
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
return { accept: true, reason: "passed" }
|
|
423
|
+
}
|
|
424
|
+
|
|
344
425
|
export function decideSessionRotation({
|
|
345
426
|
viewingSessionId,
|
|
346
427
|
newestSessionId,
|
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"
|
|
@@ -30,7 +30,9 @@ import { requestSelfUninstall } from "./service-install.mjs"
|
|
|
30
30
|
import {
|
|
31
31
|
decideSessionRotation,
|
|
32
32
|
fetchSessionHistory,
|
|
33
|
+
jsonlPath,
|
|
33
34
|
listSessions,
|
|
35
|
+
validateRotationCandidate,
|
|
34
36
|
} from "./claude-history.mjs"
|
|
35
37
|
import { listAgents } from "./agents.mjs"
|
|
36
38
|
import { listSkills } from "./skills.mjs"
|
|
@@ -440,10 +442,15 @@ export async function startDaemon({ version, ptyModule, claudeSdk } = {}) {
|
|
|
440
442
|
logger.info({ bundleVersion }, "hub bundle version detected")
|
|
441
443
|
}
|
|
442
444
|
|
|
445
|
+
// ホストで起動可能な CLI (claude / codex) を 1 回検出して hello で広告する。
|
|
446
|
+
const availableClis = await detectAvailableClis()
|
|
447
|
+
logger.info({ availableClis }, "available CLIs detected")
|
|
448
|
+
|
|
443
449
|
const client = new WsClient(config, {
|
|
444
450
|
logger,
|
|
445
451
|
version,
|
|
446
452
|
platform: detectPlatform(),
|
|
453
|
+
availableClis,
|
|
447
454
|
bundleVersion,
|
|
448
455
|
bundleVersionProvider: readBundleVersionSync,
|
|
449
456
|
bundleManifestPath: BUNDLE_MANIFEST_PATH,
|
|
@@ -2240,6 +2247,39 @@ async function dispatch(msg, ctx) {
|
|
|
2240
2247
|
return
|
|
2241
2248
|
}
|
|
2242
2249
|
if (!rotated) return
|
|
2250
|
+
|
|
2251
|
+
// 2a 根治: rotate-bind の前に「候補が本物の /clear 先か」を head 4KB で検証する。
|
|
2252
|
+
// subagent (parent_uuid 等) / SDK headless (type=result) / cwd 不一致のものは
|
|
2253
|
+
// 抑止し、別アクティビティ jsonl への汚染 bind を断つ。検証 NG 時は capped と同様に
|
|
2254
|
+
// tuiRotationNotified を進めて連発を防ぐ。検証失敗 (read error) は accept = 旧挙動。
|
|
2255
|
+
const candidatePath = jsonlPath({
|
|
2256
|
+
cwd: viewCwd,
|
|
2257
|
+
session_id: newSessionId,
|
|
2258
|
+
projectsRoot,
|
|
2259
|
+
})
|
|
2260
|
+
const { accept, reason } = await validateRotationCandidate({
|
|
2261
|
+
candidatePath,
|
|
2262
|
+
viewCwd,
|
|
2263
|
+
logger,
|
|
2264
|
+
})
|
|
2265
|
+
if (!accept) {
|
|
2266
|
+
ctx.tuiRotationNotified.set(key, {
|
|
2267
|
+
newId: newSessionId,
|
|
2268
|
+
ts: now,
|
|
2269
|
+
count: newSessionId === prevNewId ? prevCount + 1 : 1,
|
|
2270
|
+
})
|
|
2271
|
+
logger.warn(
|
|
2272
|
+
{
|
|
2273
|
+
session: viewName,
|
|
2274
|
+
cwd: viewCwd,
|
|
2275
|
+
view: viewSid,
|
|
2276
|
+
newest: newestId,
|
|
2277
|
+
reason,
|
|
2278
|
+
},
|
|
2279
|
+
"tui session rotation: suppressed (sibling activity / not a real /clear)",
|
|
2280
|
+
)
|
|
2281
|
+
return
|
|
2282
|
+
}
|
|
2243
2283
|
// {newId, ts, count} で保存。同一 new への再通知をスロットリング+回数キャップする。
|
|
2244
2284
|
// newId が変われば count を 1 にリセット、同一なら加算する。
|
|
2245
2285
|
ctx.tuiRotationNotified.set(key, {
|
|
@@ -3189,10 +3229,19 @@ async function dispatch(msg, ctx) {
|
|
|
3189
3229
|
}
|
|
3190
3230
|
try {
|
|
3191
3231
|
await createTmuxSession(name, cwd, {
|
|
3232
|
+
// cli_kind: 'codex' なら codex を、それ以外は claude を pane 起動する (既定 claude)。
|
|
3233
|
+
cliKind: msg.cli_kind === "codex" ? "codex" : "claude",
|
|
3192
3234
|
claudeCmd:
|
|
3193
3235
|
typeof msg.claude_cmd === "string"
|
|
3194
3236
|
? msg.claude_cmd
|
|
3195
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,
|
|
3196
3245
|
initialPrompt:
|
|
3197
3246
|
typeof msg.initial_prompt === "string" ? msg.initial_prompt : undefined,
|
|
3198
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 が拾い、
|