@cocorograph/hub-agent 0.7.32 → 0.7.34
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/hub-agent.mjs +96 -22
- package/package.json +1 -1
- package/src/codex-appserver-client.mjs +14 -1
- package/src/codex-rollout-watch.mjs +148 -0
- package/src/codex-stream-bridge.mjs +41 -0
- package/src/enroll.mjs +48 -27
- package/src/hub-bundle.mjs +115 -38
- package/src/main.mjs +164 -2
- package/src/session-mirror.mjs +190 -0
- package/src/skills.mjs +33 -6
- package/src/tmux.mjs +195 -5
- package/src/usage.mjs +194 -0
package/bin/hub-agent.mjs
CHANGED
|
@@ -66,13 +66,18 @@ program
|
|
|
66
66
|
skipBundle: opts.bundle === false,
|
|
67
67
|
})
|
|
68
68
|
console.log(`enroll succeeded: agent_id=${r.agent_id} primary=${r.is_primary} hub=${r.hub_url}`)
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
`
|
|
75
|
-
)
|
|
69
|
+
// Cockpit Codex 両対応 (Stream A2): claude/codex 両方が検出された環境では
|
|
70
|
+
// r.bundles に両方の結果が入る。無ければ従来どおり r.bundle 単体を見る。
|
|
71
|
+
const bundleEntries = r.bundles ? Object.entries(r.bundles) : r.bundle ? [["claude", r.bundle]] : []
|
|
72
|
+
for (const [cliKind, b] of bundleEntries) {
|
|
73
|
+
if (b?.error) {
|
|
74
|
+
console.error(`hub bundle sync warning (${cliKind}): ${b.error}`)
|
|
75
|
+
} else if (b) {
|
|
76
|
+
console.log(
|
|
77
|
+
`hub bundle synced (${cliKind}): version=${b.version} written=${b.written?.length ?? 0} ` +
|
|
78
|
+
`skipped_same=${b.skipped_same?.length ?? 0}`,
|
|
79
|
+
)
|
|
80
|
+
}
|
|
76
81
|
}
|
|
77
82
|
} catch (err) {
|
|
78
83
|
console.error(`enroll failed: ${err.message}`)
|
|
@@ -82,27 +87,50 @@ program
|
|
|
82
87
|
|
|
83
88
|
program
|
|
84
89
|
.command("sync-bundle")
|
|
85
|
-
.description(
|
|
86
|
-
|
|
90
|
+
.description(
|
|
91
|
+
"Hub AI bundle を手動で再同期 (CLAUDE.md / scripts / hooks / projects.json を ~/.claude/ に展開)。" +
|
|
92
|
+
"ローカルで検出された CLI 全て (claude / codex) を既定で同期する",
|
|
93
|
+
)
|
|
94
|
+
.option("--cli-kind <kind>", "'claude' または 'codex' のみ同期する (省略時は検出された全 CLI)")
|
|
95
|
+
.action(async (opts) => {
|
|
87
96
|
try {
|
|
88
97
|
const cfg = await readConfig()
|
|
89
98
|
if (!cfg) {
|
|
90
99
|
console.error("not enrolled (run `hub-agent enroll <token>` first)")
|
|
91
100
|
process.exit(2)
|
|
92
101
|
}
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
102
|
+
// Cockpit Codex 両対応 (Stream A2, 2026-07-02): --cli-kind 未指定なら
|
|
103
|
+
// detectAvailableClis() で検出された全 CLI を同期する(1 台に claude/codex
|
|
104
|
+
// 両方入っていれば両方ブートストラップされる)。
|
|
105
|
+
let targets
|
|
106
|
+
if (opts.cliKind) {
|
|
107
|
+
if (opts.cliKind !== "claude" && opts.cliKind !== "codex") {
|
|
108
|
+
console.error(`--cli-kind は 'claude' か 'codex' のみ対応: ${opts.cliKind}`)
|
|
109
|
+
process.exit(2)
|
|
110
|
+
}
|
|
111
|
+
targets = [opts.cliKind]
|
|
112
|
+
} else {
|
|
113
|
+
const { detectAvailableClis } = await import("../src/config.mjs")
|
|
114
|
+
const detected = (await detectAvailableClis()).filter((c) => c === "claude" || c === "codex")
|
|
115
|
+
targets = detected.length > 0 ? detected : ["claude"]
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
for (const cliKind of targets) {
|
|
119
|
+
const r = await syncBundle({
|
|
120
|
+
hubUrl: cfg.hub_url,
|
|
121
|
+
agentId: cfg.agent_id,
|
|
122
|
+
agentToken: cfg.agent_token,
|
|
123
|
+
cliKind,
|
|
124
|
+
})
|
|
125
|
+
const mcpSummary = r.mcp
|
|
126
|
+
? ` mcp_added=${r.mcp.added.length}${r.mcp.skipped.length > 0 ? ` mcp_skipped=${r.mcp.skipped.length}` : ""}`
|
|
127
|
+
: ""
|
|
128
|
+
console.log(
|
|
129
|
+
`hub bundle synced (${cliKind}): version=${r.version} written=${r.written.length} skipped_same=${r.skipped_same.length}${mcpSummary}`,
|
|
130
|
+
)
|
|
131
|
+
if (r.mcp && r.mcp.added.length > 0) {
|
|
132
|
+
console.log(` mcp servers added: ${r.mcp.added.join(", ")}`)
|
|
133
|
+
}
|
|
106
134
|
}
|
|
107
135
|
} catch (err) {
|
|
108
136
|
console.error(`sync-bundle failed: ${err.message}`)
|
|
@@ -110,6 +138,52 @@ program
|
|
|
110
138
|
}
|
|
111
139
|
})
|
|
112
140
|
|
|
141
|
+
program
|
|
142
|
+
.command("mirror")
|
|
143
|
+
.description(
|
|
144
|
+
"チャット(SDK/app-server)の会話を、このターミナルに読み取り専用で実況表示する " +
|
|
145
|
+
"(Cockpit Codex 両対応 Stream C)。チャットの動作には一切影響しない (書き込まない)。" +
|
|
146
|
+
"Ctrl-C / SIGTERM で停止する",
|
|
147
|
+
)
|
|
148
|
+
.requiredOption("--cwd <path>", "対象セッションの cwd")
|
|
149
|
+
.requiredOption("--session <id>", "Claude session_id または Codex threadId")
|
|
150
|
+
.option("--cli-kind <kind>", "'claude' または 'codex'", "claude")
|
|
151
|
+
.action(async (opts) => {
|
|
152
|
+
if (opts.cliKind !== "claude" && opts.cliKind !== "codex") {
|
|
153
|
+
console.error(`--cli-kind は 'claude' か 'codex' のみ対応: ${opts.cliKind}`)
|
|
154
|
+
process.exit(2)
|
|
155
|
+
}
|
|
156
|
+
const { runClaudeMirror, runCodexMirror } = await import("../src/session-mirror.mjs")
|
|
157
|
+
// watchSessionFile / watchCodexRollout のポーリング timer は常駐 daemon (main.mjs) の
|
|
158
|
+
// graceful shutdown を妨げないよう意図的に unref() されている。この単体 CLI プロセスは
|
|
159
|
+
// 他に event loop を保持する要因が無いため、unref timer だけでは初回同期処理の直後に
|
|
160
|
+
// 即終了してしまう (実機検証で確認)。ref 付きの keep-alive timer で明示的に生かす。
|
|
161
|
+
const keepAlive = setInterval(() => {}, 60_000)
|
|
162
|
+
let watcher
|
|
163
|
+
try {
|
|
164
|
+
watcher =
|
|
165
|
+
opts.cliKind === "codex"
|
|
166
|
+
? await runCodexMirror({ threadId: opts.session })
|
|
167
|
+
: runClaudeMirror({ cwd: opts.cwd, sessionId: opts.session })
|
|
168
|
+
} catch (err) {
|
|
169
|
+
clearInterval(keepAlive)
|
|
170
|
+
console.error(`mirror failed: ${err.message}`)
|
|
171
|
+
process.exit(1)
|
|
172
|
+
}
|
|
173
|
+
const stop = () => {
|
|
174
|
+
clearInterval(keepAlive)
|
|
175
|
+
try {
|
|
176
|
+
watcher?.stop?.()
|
|
177
|
+
} catch {
|
|
178
|
+
/* ignore */
|
|
179
|
+
}
|
|
180
|
+
console.log("[cockpit mirror] 停止しました")
|
|
181
|
+
process.exit(0)
|
|
182
|
+
}
|
|
183
|
+
process.on("SIGINT", stop)
|
|
184
|
+
process.on("SIGTERM", stop)
|
|
185
|
+
})
|
|
186
|
+
|
|
113
187
|
program
|
|
114
188
|
.command("start")
|
|
115
189
|
.description("daemon を起動して Hub と WSS 接続する")
|
package/package.json
CHANGED
|
@@ -40,7 +40,20 @@ export class CodexAppServerClient extends EventEmitter {
|
|
|
40
40
|
/** codex app-server プロセスを起動する。二重呼び出しは無害 (no-op)。 */
|
|
41
41
|
start() {
|
|
42
42
|
if (this.child || this.closed) return
|
|
43
|
-
|
|
43
|
+
// Cockpit Codex 両対応 Stream A5(2026-07-02 実機検証で確定、中村さん明示承認済み):
|
|
44
|
+
// app-server は stdio 駆動で TTY が無いため、Codex 標準の対話的 hook 信頼確認
|
|
45
|
+
// プロンプトを出す余地が無い。--dangerously-bypass-hook-trust を付けないと
|
|
46
|
+
// Hub 配信の hooks.json が永久に発火しない(tmux.mjs の composeCodexFlags と同じ
|
|
47
|
+
// 理由・同じ既定、HUB_CODEX_BYPASS_HOOK_TRUST=0 で無効化可)。ルートオプションは
|
|
48
|
+
// サブコマンド (`app-server`) より前に置く必要がある(`codex app-server --help`
|
|
49
|
+
// にはこのフラグが出ない = サブコマンド固有オプションではないため)。
|
|
50
|
+
const bypassHookTrust = process.env.HUB_CODEX_BYPASS_HOOK_TRUST !== "0"
|
|
51
|
+
const args = [
|
|
52
|
+
...(bypassHookTrust ? ["--dangerously-bypass-hook-trust"] : []),
|
|
53
|
+
"app-server",
|
|
54
|
+
"--stdio",
|
|
55
|
+
]
|
|
56
|
+
this.child = this._spawnFn(this.codexCmd, args, {
|
|
44
57
|
cwd: this.cwd,
|
|
45
58
|
env: this.env || process.env,
|
|
46
59
|
stdio: ["pipe", "pipe", "pipe"],
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Codex rollout jsonl のライブ追従 watcher (Cockpit Codex 両対応 Stream C: ターミナル
|
|
3
|
+
* ミラー、2026-07-03)。
|
|
4
|
+
*
|
|
5
|
+
* `claude-history-watch.mjs`(Claude 版 jsonl tail)と対の役割。Codex の
|
|
6
|
+
* `~/.codex/sessions/Y/M/D/rollout-<ts>-<threadId>.jsonl` を byte offset で tail し、
|
|
7
|
+
* 新規行を 1 行ずつ onEvent に渡す。フィルタは行わない(呼び出し側の formatter が
|
|
8
|
+
* 表示対象の `event_msg.payload.type` を選別する)。
|
|
9
|
+
*
|
|
10
|
+
* 設計はミラーリング用途に絞って `claude-history-watch.mjs` と同じ tail 機構
|
|
11
|
+
* (byte offset + fs.watch + アダプティブポーリング + truncate 検知)を複製する。
|
|
12
|
+
* Claude 側の汎用化リファクタは既存の chat 表示経路への回帰リスクがあるため見送り、
|
|
13
|
+
* 新規ファイルとして独立させた(意図的な重複、コメントで明示)。
|
|
14
|
+
*/
|
|
15
|
+
import { watch as fsWatch } from "node:fs"
|
|
16
|
+
import { open, stat } from "node:fs/promises"
|
|
17
|
+
|
|
18
|
+
const POLL_FAST_MS = Number(process.env.HUB_AGENT_JSONL_POLL_FAST_MS ?? 400)
|
|
19
|
+
const POLL_IDLE_MS = Number(process.env.HUB_AGENT_JSONL_POLL_IDLE_MS ?? 1500)
|
|
20
|
+
const FAST_WINDOW_MS = Number(process.env.HUB_AGENT_JSONL_FAST_WINDOW_MS ?? 5000)
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* 1 つの rollout jsonl ファイルを tail する watcher を生成する。
|
|
24
|
+
*
|
|
25
|
+
* @param {object} args
|
|
26
|
+
* @param {string} args.filePath - 監視対象の rollout jsonl 絶対パス
|
|
27
|
+
* @param {(rawLineObj: object) => void} args.onEvent - 新規行ごとに呼ばれる (パース済み JSON)
|
|
28
|
+
* @param {boolean} [args.fromEnd=true] - 既存内容は飛ばし、監視開始後の追記のみ拾う
|
|
29
|
+
* @param {import('pino').Logger} [args.logger]
|
|
30
|
+
* @returns {{ stop: () => void }}
|
|
31
|
+
*/
|
|
32
|
+
export function watchCodexRollout({ filePath, onEvent, fromEnd = true, logger }) {
|
|
33
|
+
let offset = 0
|
|
34
|
+
let reading = false
|
|
35
|
+
let stopped = false
|
|
36
|
+
let leftover = ""
|
|
37
|
+
let fsWatcher = null
|
|
38
|
+
let pollTimer = null
|
|
39
|
+
let initialized = false
|
|
40
|
+
let lastChangeAt = 0
|
|
41
|
+
|
|
42
|
+
async function initOffset() {
|
|
43
|
+
try {
|
|
44
|
+
const st = await stat(filePath)
|
|
45
|
+
offset = fromEnd ? st.size : 0
|
|
46
|
+
initialized = true
|
|
47
|
+
} catch {
|
|
48
|
+
offset = 0
|
|
49
|
+
initialized = false
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function readIncrement() {
|
|
54
|
+
if (stopped || reading) return
|
|
55
|
+
reading = true
|
|
56
|
+
try {
|
|
57
|
+
let st
|
|
58
|
+
try {
|
|
59
|
+
st = await stat(filePath)
|
|
60
|
+
} catch {
|
|
61
|
+
return // まだ無い (rollout はターン開始直後に作られる)
|
|
62
|
+
}
|
|
63
|
+
if (!initialized) initialized = true
|
|
64
|
+
if (st.size < offset) {
|
|
65
|
+
// truncate / rotate された → 先頭から読み直す (通常 rollout は追記のみだが念のため)
|
|
66
|
+
offset = 0
|
|
67
|
+
leftover = ""
|
|
68
|
+
}
|
|
69
|
+
if (st.size <= offset) return
|
|
70
|
+
let fh
|
|
71
|
+
try {
|
|
72
|
+
fh = await open(filePath, "r")
|
|
73
|
+
const len = st.size - offset
|
|
74
|
+
const buf = Buffer.allocUnsafe(len)
|
|
75
|
+
await fh.read(buf, 0, len, offset)
|
|
76
|
+
offset = st.size
|
|
77
|
+
lastChangeAt = Date.now()
|
|
78
|
+
const text = leftover + buf.toString("utf-8")
|
|
79
|
+
const lines = text.split("\n")
|
|
80
|
+
leftover = lines.pop() ?? ""
|
|
81
|
+
for (const line of lines) {
|
|
82
|
+
if (!line) continue
|
|
83
|
+
let d
|
|
84
|
+
try {
|
|
85
|
+
d = JSON.parse(line)
|
|
86
|
+
} catch {
|
|
87
|
+
continue // 途中で読んだ不完全な行 (稀) はスキップ
|
|
88
|
+
}
|
|
89
|
+
try {
|
|
90
|
+
onEvent(d)
|
|
91
|
+
} catch (err) {
|
|
92
|
+
logger?.warn?.({ err: err?.message }, "codex rollout watch onEvent failed")
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
} finally {
|
|
96
|
+
try {
|
|
97
|
+
await fh?.close()
|
|
98
|
+
} catch {
|
|
99
|
+
/* ignore */
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
} catch (err) {
|
|
103
|
+
logger?.debug?.({ err: err?.message, filePath }, "codex rollout watch read failed")
|
|
104
|
+
} finally {
|
|
105
|
+
reading = false
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function schedulePoll() {
|
|
110
|
+
if (stopped) return
|
|
111
|
+
const idle = Date.now() - lastChangeAt > FAST_WINDOW_MS
|
|
112
|
+
const delay = idle ? POLL_IDLE_MS : POLL_FAST_MS
|
|
113
|
+
pollTimer = setTimeout(async () => {
|
|
114
|
+
await readIncrement()
|
|
115
|
+
schedulePoll()
|
|
116
|
+
}, delay)
|
|
117
|
+
pollTimer?.unref?.()
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async function start() {
|
|
121
|
+
await initOffset()
|
|
122
|
+
await readIncrement()
|
|
123
|
+
try {
|
|
124
|
+
fsWatcher = fsWatch(filePath, { persistent: false }, () => {
|
|
125
|
+
readIncrement()
|
|
126
|
+
})
|
|
127
|
+
} catch {
|
|
128
|
+
// ファイル未存在等で fs.watch が張れない場合はポーリングのみに頼る
|
|
129
|
+
}
|
|
130
|
+
schedulePoll()
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
start()
|
|
134
|
+
|
|
135
|
+
return {
|
|
136
|
+
stop() {
|
|
137
|
+
stopped = true
|
|
138
|
+
if (pollTimer) clearTimeout(pollTimer)
|
|
139
|
+
try {
|
|
140
|
+
fsWatcher?.close()
|
|
141
|
+
} catch {
|
|
142
|
+
/* ignore */
|
|
143
|
+
}
|
|
144
|
+
},
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export default watchCodexRollout
|
|
@@ -125,6 +125,47 @@ export class CodexStreamBridge extends EventEmitter {
|
|
|
125
125
|
}
|
|
126
126
|
}
|
|
127
127
|
|
|
128
|
+
/**
|
|
129
|
+
* cwd 配下の Codex thread 一覧を返す (Cockpit Codex 両対応 セッションピッカー/最新
|
|
130
|
+
* セッション自動選択用。claude 側 listSessions と対称)。
|
|
131
|
+
*
|
|
132
|
+
* `thread/list` は特定のスレッドに紐付かない app-server のグローバル RPC (公式スキーマ
|
|
133
|
+
* `ThreadListParams`/`ThreadListResponse` で確認済み: cwd フィルタ・sortKey='recency_at'・
|
|
134
|
+
* sortDirection='desc' が使える) なので、attach() のような常駐セッションは作らず、
|
|
135
|
+
* 呼び出しごとに使い捨てクライアントで initialize → thread/list → stop する
|
|
136
|
+
* (list のためだけに長寿命プロセスを維持しない = リーク防止)。
|
|
137
|
+
*
|
|
138
|
+
* @returns {Promise<Array<{thread_id: string, cwd: string, preview: string, updated_at_ms: number}>>}
|
|
139
|
+
*/
|
|
140
|
+
async listThreads({ cwd, limit } = {}) {
|
|
141
|
+
const client = this._clientFactory({ cwd, codexCmd: this.codexCmd, logger: this.logger })
|
|
142
|
+
client.start()
|
|
143
|
+
try {
|
|
144
|
+
await client.request("initialize", { clientInfo: CLIENT_INFO })
|
|
145
|
+
const result = await client.request("thread/list", {
|
|
146
|
+
cwd: cwd || undefined,
|
|
147
|
+
limit: limit || undefined,
|
|
148
|
+
// recency_at は「読み込みも含めた実際の使用感」の並び順 (公式スキーマの説明:
|
|
149
|
+
// "Unix timestamp used for thread recency ordering")。updated_at だと最終書き込み
|
|
150
|
+
// のみで並ぶため、Claude 側の mtime (listSessions) と体感を揃えるならこちらが近い。
|
|
151
|
+
sortKey: "recency_at",
|
|
152
|
+
sortDirection: "desc",
|
|
153
|
+
})
|
|
154
|
+
const threads = Array.isArray(result?.data) ? result.data : []
|
|
155
|
+
return threads.map((t) => ({
|
|
156
|
+
thread_id: t.id,
|
|
157
|
+
cwd: t.cwd || "",
|
|
158
|
+
preview: typeof t.preview === "string" ? t.preview.slice(0, 80) : "",
|
|
159
|
+
// recencyAt 優先 (無ければ updatedAt) を ms へ変換。claude 側 ClaudeSessionSummary.mtime
|
|
160
|
+
// (ms epoch) と同じ単位に揃え、frontend が同じ並び替えロジックを使い回せるようにする。
|
|
161
|
+
updated_at_ms:
|
|
162
|
+
(typeof t.recencyAt === "number" ? t.recencyAt : t.updatedAt || 0) * 1000,
|
|
163
|
+
}))
|
|
164
|
+
} finally {
|
|
165
|
+
client.stop()
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
128
169
|
_wireClient(session) {
|
|
129
170
|
const { client, stream_id } = session
|
|
130
171
|
client.on("notification", (msg) => this._onNotification(session, msg))
|
package/src/enroll.mjs
CHANGED
|
@@ -71,34 +71,51 @@ export async function enroll(enrollmentToken, opts = {}) {
|
|
|
71
71
|
// Hub AI bundle を続けて同期する (CLAUDE.md / scripts / hooks / projects.json
|
|
72
72
|
// を ~/.claude/ に展開)。失敗しても enroll 自体は成功扱いにする (--no-bundle
|
|
73
73
|
// で skip 可能)。
|
|
74
|
-
|
|
74
|
+
//
|
|
75
|
+
// Cockpit Codex 両対応 (Stream A2, 2026-07-02): 1 台のマシンに claude/codex
|
|
76
|
+
// 両方入っている場合 (availableClis に両方含まれる)、両方の bundle を同期する
|
|
77
|
+
// (~/.claude/ と ~/.codex/ の両方を自己完結的にブートストラップ)。claude が
|
|
78
|
+
// 未検出の環境は無い想定だが、fail-soft のため availableClis に claude が
|
|
79
|
+
// 無ければ codex のみ同期する。
|
|
80
|
+
const bundleResults = {}
|
|
75
81
|
if (!opts.skipBundle) {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
82
|
+
const { syncBundle } = await import("./hub-bundle.mjs")
|
|
83
|
+
const { kickSetupHubAi } = await import("./main.mjs")
|
|
84
|
+
// Hub backend が cli_kind='claude'/'codex' のみ対応するため他候補は無視する。
|
|
85
|
+
// detectAvailableClis() が空を返す (PATH 検出失敗等) 場合も fail-soft で
|
|
86
|
+
// 従来どおり claude 向け同期だけは必ず試みる。
|
|
87
|
+
const detected = availableClis.filter((c) => c === "claude" || c === "codex")
|
|
88
|
+
const targets = detected.length > 0 ? detected : ["claude"]
|
|
89
|
+
for (const cliKind of targets) {
|
|
90
|
+
try {
|
|
91
|
+
bundleResults[cliKind] = await syncBundle({
|
|
92
|
+
hubUrl,
|
|
93
|
+
agentId: data.agent_id,
|
|
94
|
+
agentToken: data.agent_token,
|
|
95
|
+
logger: opts.logger,
|
|
96
|
+
fetchImpl,
|
|
97
|
+
cliKind,
|
|
98
|
+
})
|
|
99
|
+
} catch (err) {
|
|
100
|
+
opts.logger?.warn?.({ cliKind, err: err.message }, "hub bundle sync failed (enroll still ok)")
|
|
101
|
+
bundleResults[cliKind] = { error: err.message }
|
|
102
|
+
}
|
|
89
103
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
104
|
+
// bundle 同期で ~/.claude/scripts/setup_hub_ai.py (or ~/.codex/...) が
|
|
105
|
+
// 配置された直後に kick する。設計穴: 初回 enroll では誰も
|
|
106
|
+
// setup_hub_ai.py を実行しないため、`step_claude_md` が走らず
|
|
107
|
+
// CLAUDE.md/AGENTS.md がいつまでも作成されない事故があった
|
|
108
|
+
// (2026-06-17 toma 環境)。bundle 配信は scripts と
|
|
109
|
+
// claude_md_fragment.md は含むが、CLAUDE.md/AGENTS.md と
|
|
110
|
+
// settings.json/hooks.json は含まないため、誰かが setup_hub_ai を
|
|
111
|
+
// 一度叩かないと self-bootstrap しない。enroll 直後に best-effort で
|
|
112
|
+
// 1 回叩く。既存環境では step_claude_md がマーカー範囲を冪等更新
|
|
113
|
+
// するだけで害なし。
|
|
114
|
+
try {
|
|
115
|
+
kickSetupHubAi({ logger: opts.logger, cliKind })
|
|
116
|
+
} catch (err) {
|
|
117
|
+
opts.logger?.warn?.({ cliKind, err: err.message }, "kickSetupHubAi from enroll failed (ignored)")
|
|
118
|
+
}
|
|
102
119
|
}
|
|
103
120
|
}
|
|
104
121
|
|
|
@@ -106,7 +123,11 @@ export async function enroll(enrollmentToken, opts = {}) {
|
|
|
106
123
|
agent_id: data.agent_id,
|
|
107
124
|
is_primary: !!data.is_primary,
|
|
108
125
|
hub_url: hubUrl,
|
|
109
|
-
bundle
|
|
126
|
+
// 後方互換: 呼び出し元 (bin/hub-agent.mjs 等) は従来 `bundle`(claude 単体の
|
|
127
|
+
// 結果オブジェクト)を見ていたため、claude の結果をそのまま残しつつ、
|
|
128
|
+
// 複数 CLI 分の内訳を `bundles` に別途持たせる。
|
|
129
|
+
bundle: bundleResults.claude ?? Object.values(bundleResults)[0] ?? null,
|
|
130
|
+
bundles: bundleResults,
|
|
110
131
|
}
|
|
111
132
|
}
|
|
112
133
|
|