@cocorograph/hub-agent 0.7.61 → 0.7.63

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 CHANGED
@@ -27,7 +27,7 @@ if (!process.env.LC_ALL) process.env.LC_ALL = "C.UTF-8"
27
27
 
28
28
  import { hasConfig, readConfig } from "../src/config.mjs"
29
29
  import { enroll } from "../src/enroll.mjs"
30
- import { syncBundle } from "../src/hub-bundle.mjs"
30
+ import { resolveSyncTargets, syncBundle } from "../src/hub-bundle.mjs"
31
31
  import {
32
32
  installPlugin,
33
33
  listInstallableExamples,
@@ -110,28 +110,40 @@ program
110
110
  }
111
111
  targets = [opts.cliKind]
112
112
  } else {
113
+ // 対象決定は enroll / self-heal と共通の resolveSyncTargets に寄せる。
113
114
  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"]
115
+ targets = resolveSyncTargets(await detectAvailableClis())
116
116
  }
117
117
 
118
+ // 各 CLI を **setup 完了まで同期的に**処理し、失敗は CLI 名付きで報告して continue
119
+ // (一方の失敗で他方を止めない)。1 つでも失敗したら非 0 で終える。
120
+ const failures = []
118
121
  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(", ")}`)
122
+ try {
123
+ const r = await syncBundle({
124
+ hubUrl: cfg.hub_url,
125
+ agentId: cfg.agent_id,
126
+ agentToken: cfg.agent_token,
127
+ cliKind,
128
+ })
129
+ const mcpSummary = r.mcp
130
+ ? ` mcp_added=${r.mcp.added.length}${r.mcp.skipped.length > 0 ? ` mcp_skipped=${r.mcp.skipped.length}` : ""}`
131
+ : ""
132
+ console.log(
133
+ `hub bundle synced (${cliKind}): version=${r.version} written=${r.written.length} skipped_same=${r.skipped_same.length}${mcpSummary}`,
134
+ )
135
+ if (r.mcp && r.mcp.added.length > 0) {
136
+ console.log(` mcp servers added: ${r.mcp.added.join(", ")}`)
137
+ }
138
+ } catch (err) {
139
+ console.error(`sync-bundle failed (${cliKind}): ${err.message}`)
140
+ failures.push(cliKind)
133
141
  }
134
142
  }
143
+ if (failures.length > 0) {
144
+ console.error(`sync-bundle 失敗: ${failures.join(", ")}`)
145
+ process.exit(1)
146
+ }
135
147
  } catch (err) {
136
148
  console.error(`sync-bundle failed: ${err.message}`)
137
149
  process.exit(1)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cocorograph/hub-agent",
3
- "version": "0.7.61",
3
+ "version": "0.7.63",
4
4
  "description": "Hub Hosted Cockpit のローカル常駐 agent。Hub と outbound WSS で接続し、ローカルの tmux/pty を中継する。",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",
@@ -646,30 +646,57 @@ do_enroll() {
646
646
  # マーカーは通常 syncBundle の最終ステップで書かれるが、scripts/manifest.json
647
647
  # が無いと SessionStart hook / hub-helper.py が動かないため両方必須。どちらかが
648
648
  # 欠けていれば半完了状態とみなして sync-bundle を retry。
649
- if _hub_bundle_incomplete; then
650
- color_warn "Hub AI bundle が ~/.claude に完全展開されていない可能性。sync-bundle を再試行"
651
- if ! retry 2 hub-agent sync-bundle; then
652
- color_warn "hub-agent sync-bundle が継続失敗。あとで手動実行してください:"
653
- color_warn " hub-agent sync-bundle"
654
- fi
655
- fi
649
+ # 検出された Claude/Codex 双方を個別検証し、不完全な CLI だけ再同期する
650
+ # (2026-07-15。旧実装は ~/.claude しか見ず Codex の欠損を検出・修復できなかった)。
651
+ _hub_reconcile_bundles
656
652
  }
657
653
 
658
- # Hub AI bundle の展開状況を判定する。完全に展開されていれば 0、欠けていれば 1。
659
- # do_enroll の事後 verify と verify_setup の最終チェックで共通利用する。
660
- _hub_bundle_incomplete() {
661
- if [[ ! -f "$HOME/.claude/CLAUDE.md" ]]; then
662
- return 0
663
- fi
664
- if ! grep -q "BEGIN HUB-AI-RULES" "$HOME/.claude/CLAUDE.md" 2>/dev/null; then
665
- return 0
666
- fi
667
- if [[ ! -f "$HOME/.claude/scripts/manifest.json" ]]; then
668
- return 0
654
+ # 検証対象の CLI を列挙する。claude は常に対象、codex は PATH にある時だけ対象。
655
+ # 各行 "cli_kind:dir:config_md:hooks_file"(Codex 未インストール環境では codex を要求しない)。
656
+ _hub_target_clis() {
657
+ echo "claude:.claude:CLAUDE.md:settings.json"
658
+ if present codex; then
659
+ echo "codex:.codex:AGENTS.md:hooks.json"
669
660
  fi
661
+ }
662
+
663
+ # 単一 CLI の bundle 完全性を判定する。完全なら 0、欠けていれば 1。
664
+ # $1=dir(.claude/.codex) $2=config_md(CLAUDE.md/AGENTS.md) $3=hooks_file(settings.json/hooks.json)
665
+ # 検証項目: scripts/manifest.json / CONFIG_MD の HUB-AI-RULES マーカー /
666
+ # hooks 設定に Hub SessionStart hook(session_start.py) / 正規 .hub_token.json
667
+ _hub_bundle_incomplete_cli() {
668
+ local dir="$1" config_md="$2" hooks_file="$3"
669
+ local base="$HOME/$dir"
670
+ [[ -f "$base/scripts/manifest.json" ]] || return 0
671
+ [[ -f "$base/$config_md" ]] || return 0
672
+ grep -q "BEGIN HUB-AI-RULES" "$base/$config_md" 2>/dev/null || return 0
673
+ grep -q "session_start.py" "$base/$hooks_file" 2>/dev/null || return 0
674
+ [[ -f "$base/.hub_token.json" ]] || return 0
670
675
  return 1
671
676
  }
672
677
 
678
+ # 後方互換: 従来 claude 前提で呼んでいた箇所向けの薄いラッパ。
679
+ _hub_bundle_incomplete() {
680
+ _hub_bundle_incomplete_cli ".claude" "CLAUDE.md" "settings.json"
681
+ }
682
+
683
+ # 検出された各 CLI の bundle 完全性を検証し、**不完全な CLI だけ** を
684
+ # `hub-agent sync-bundle --cli-kind <kind>` で再同期する(Codex 未インストールなら対象外)。
685
+ _hub_reconcile_bundles() {
686
+ local line cli_kind dir config_md hooks_file
687
+ while IFS= read -r line; do
688
+ [[ -z "$line" ]] && continue
689
+ IFS=":" read -r cli_kind dir config_md hooks_file <<< "$line"
690
+ if _hub_bundle_incomplete_cli "$dir" "$config_md" "$hooks_file"; then
691
+ color_warn "Hub AI bundle が ~/$dir に完全展開されていない可能性 ($cli_kind)。sync-bundle を再試行"
692
+ if ! retry 2 hub-agent sync-bundle --cli-kind "$cli_kind"; then
693
+ color_warn "hub-agent sync-bundle --cli-kind $cli_kind が継続失敗。あとで手動実行してください:"
694
+ color_warn " hub-agent sync-bundle --cli-kind $cli_kind"
695
+ fi
696
+ fi
697
+ done < <(_hub_target_clis)
698
+ }
699
+
673
700
  do_install_service() {
674
701
  color_step "OS サービスとして自動起動を登録"
675
702
  # install-service は launchctl bootout/bootstrap の transient で初回失敗する
@@ -687,17 +714,26 @@ do_install_service() {
687
714
  # verify を成功させる。setup_hub_ai 不在 (bundle 同期失敗) のときは色付き
688
715
  # warn を出して継続。再実行時は step_* が冪等処理を返すだけで害なし。
689
716
  do_bootstrap_hub_ai() {
690
- local setup_script="$HOME/.claude/scripts/setup_hub_ai.py"
691
- if [[ ! -f "$setup_script" ]]; then
692
- color_warn "setup_hub_ai.py 不在 (bundle 同期が失敗した可能性)。スキップ"
693
- return 0
694
- fi
695
- color_step "setup_hub_ai.py --silent を実行 (~/.claude/CLAUDE.md / settings.json 初期化)"
696
- if python3 "$setup_script" --silent; then
697
- color_ok "Hub AI bootstrap 完了"
698
- else
699
- color_warn "setup_hub_ai.py --silent が exit≠0 で終了 (継続)。手動で再実行: python3 $setup_script"
700
- fi
717
+ # 検出された各 CLI(claude 常に / codex は present 時)の setup_hub_ai.py を
718
+ # 同期実行する。enroll/sync 内でも setup は走るが、install.sh では verify を確実に
719
+ # 成功させるため各 CLI で同期実行する(一方の失敗で他方を止めない)。2026-07-15 に
720
+ # Claude 固定から双方へ拡張。
721
+ local line cli_kind dir config_md hooks_file setup_script
722
+ while IFS= read -r line; do
723
+ [[ -z "$line" ]] && continue
724
+ IFS=":" read -r cli_kind dir config_md hooks_file <<< "$line"
725
+ setup_script="$HOME/$dir/scripts/setup_hub_ai.py"
726
+ if [[ ! -f "$setup_script" ]]; then
727
+ color_warn "setup_hub_ai.py 不在 ($cli_kind, bundle 同期が失敗した可能性)。スキップ"
728
+ continue
729
+ fi
730
+ color_step "setup_hub_ai.py --silent を実行 ($cli_kind: ~/$dir/$config_md / $hooks_file 初期化)"
731
+ if python3 "$setup_script" --silent; then
732
+ color_ok "Hub AI bootstrap 完了 ($cli_kind)"
733
+ else
734
+ color_warn "setup_hub_ai.py --silent が exit≠0 ($cli_kind, 継続)。手動再実行: python3 $setup_script"
735
+ fi
736
+ done < <(_hub_target_clis)
701
737
  }
702
738
 
703
739
  # セットアップ最終検証。
@@ -721,22 +757,22 @@ verify_setup() {
721
757
  color_warn "Claude Code (claude) コマンドが見つかりません (cockpit から claude を呼ぶ場合は必要)"
722
758
  fi
723
759
 
724
- # 3. Hub AI bundle 展開 (do_enroll と同じ 2 段チェック: CLAUDE.md マーカー +
725
- # scripts/manifest.json)。CLAUDE.md は書き込まれたが manifest.json が落ちて
726
- # いない半完了状態も検知する。
727
- if ! _hub_bundle_incomplete; then
728
- local mver
729
- mver="$(grep -o '"version"[[:space:]]*:[[:space:]]*"[^"]*"' "$HOME/.claude/scripts/manifest.json" 2>/dev/null | head -1 | sed 's/.*"\([^"]*\)"$/\1/')"
730
- color_ok "Hub AI bundle 展開済み (~/.claude/CLAUDE.md + scripts/manifest.json ${mver:+v$mver})"
731
- else
732
- if [[ ! -f "$HOME/.claude/CLAUDE.md" ]] || ! grep -q "BEGIN HUB-AI-RULES" "$HOME/.claude/CLAUDE.md" 2>/dev/null; then
733
- color_warn "Hub AI bundle が ~/.claude に展開されていません (CLAUDE.md マーカー欠落)"
760
+ # 3. Hub AI bundle 展開を **CLI 別** に検証(manifest / CONFIG_MD マーカー /
761
+ # hooks の Hub SessionStart hook / 正規 .hub_token.json)。Codex 未インストールなら
762
+ # codex は検証しない(2026-07-15)。
763
+ local line cli_kind dir config_md hooks_file mver
764
+ while IFS= read -r line; do
765
+ [[ -z "$line" ]] && continue
766
+ IFS=":" read -r cli_kind dir config_md hooks_file <<< "$line"
767
+ if ! _hub_bundle_incomplete_cli "$dir" "$config_md" "$hooks_file"; then
768
+ mver="$(grep -o '"version"[[:space:]]*:[[:space:]]*"[^"]*"' "$HOME/$dir/scripts/manifest.json" 2>/dev/null | head -1 | sed 's/.*"\([^"]*\)"$/\1/')"
769
+ color_ok "Hub AI bundle 展開済み ($cli_kind: ~/$dir/$config_md + scripts/manifest.json ${mver:+v$mver})"
734
770
  else
735
- color_warn "Hub AI bundle が半完了状態 (~/.claude/scripts/manifest.json 欠落)"
771
+ color_warn "Hub AI bundle が半完了状態 ($cli_kind: ~/$dir の manifest / $config_md マーカー / $hooks_file の SessionStart hook / .hub_token.json のいずれか欠落)"
772
+ color_warn " 手動再同期: hub-agent sync-bundle --cli-kind $cli_kind"
773
+ errors=$((errors + 1))
736
774
  fi
737
- color_warn " 手動再同期: hub-agent sync-bundle"
738
- errors=$((errors + 1))
739
- fi
775
+ done < <(_hub_target_clis)
740
776
 
741
777
  # 4. OS サービス起動 (Darwin 限定の確認。Linux は systemctl --user で別途)
742
778
  if [[ "$(uname)" == "Darwin" ]]; then
package/src/enroll.mjs CHANGED
@@ -82,15 +82,18 @@ export async function enroll(enrollmentToken, opts = {}) {
82
82
  // 無ければ codex のみ同期する。
83
83
  const bundleResults = {}
84
84
  if (!opts.skipBundle) {
85
- const { syncBundle } = await import("./hub-bundle.mjs")
86
- const { kickSetupHubAi } = await import("./main.mjs")
87
- // Hub backend が cli_kind='claude'/'codex' のみ対応するため他候補は無視する。
88
- // detectAvailableClis() が空を返す (PATH 検出失敗等) 場合も fail-soft で
89
- // 従来どおり claude 向け同期だけは必ず試みる。
90
- const detected = availableClis.filter((c) => c === "claude" || c === "codex")
91
- const targets = detected.length > 0 ? detected : ["claude"]
85
+ const { resolveSyncTargets, syncBundle } = await import("./hub-bundle.mjs")
86
+ // 検出された claude/codex を対象にする(Hub backend は cli_kind='claude'/'codex'
87
+ // のみ対応)。未検出は fail-soft で claude フォールバック。対象決定は
88
+ // resolveSyncTargets() に共通化し、enroll / sync-bundle CLI / self-heal で分岐させない。
89
+ const targets = resolveSyncTargets(availableClis)
92
90
  for (const cliKind of targets) {
93
91
  try {
92
+ // syncBundle が applyBundle → post_install(setup_hub_ai.py --silent) を **同期実行**
93
+ // するため、ここで別途 setup を kick する必要は無い(2026-07-15 に一本化。旧実装は
94
+ // enroll だけ別経路で detached kickSetupHubAi を叩き、sync-bundle / self-heal と
95
+ // 挙動が分岐していた)。CONFIG_MD / hooks / version / 旧 JWT の正規 token 移行まで
96
+ // syncBundle 完了時に揃う。
94
97
  bundleResults[cliKind] = await syncBundle({
95
98
  hubUrl,
96
99
  agentId: data.agent_id,
@@ -103,22 +106,6 @@ export async function enroll(enrollmentToken, opts = {}) {
103
106
  opts.logger?.warn?.({ cliKind, err: err.message }, "hub bundle sync failed (enroll still ok)")
104
107
  bundleResults[cliKind] = { error: err.message }
105
108
  }
106
-
107
- // bundle 同期で ~/.claude/scripts/setup_hub_ai.py (or ~/.codex/...) が
108
- // 配置された直後に kick する。設計穴: 初回 enroll では誰も
109
- // setup_hub_ai.py を実行しないため、`step_claude_md` が走らず
110
- // CLAUDE.md/AGENTS.md がいつまでも作成されない事故があった
111
- // (2026-06-17 toma 環境)。bundle 配信は scripts と
112
- // claude_md_fragment.md は含むが、CLAUDE.md/AGENTS.md と
113
- // settings.json/hooks.json は含まないため、誰かが setup_hub_ai を
114
- // 一度叩かないと self-bootstrap しない。enroll 直後に best-effort で
115
- // 1 回叩く。既存環境では step_claude_md がマーカー範囲を冪等更新
116
- // するだけで害なし。
117
- try {
118
- kickSetupHubAi({ logger: opts.logger, cliKind })
119
- } catch (err) {
120
- opts.logger?.warn?.({ cliKind, err: err.message }, "kickSetupHubAi from enroll failed (ignored)")
121
- }
122
109
  }
123
110
  }
124
111
 
@@ -180,8 +180,24 @@ export async function applyBundle({ bundle, logger, cliKind } = {}) {
180
180
  logger?.info({ dest, mode: mode.toString(8) }, "hub-bundle wrote file")
181
181
  }
182
182
 
183
- // JWT を保存 (~/.claude/scripts/.hub_jwt.json、または ~/.codex/scripts/.hub_jwt.json)
183
+ // token を保存する。役割分担(2026-07-15 整理、G):
184
+ // - 正規 `<cliDir>/.hub_token.json`(`{access, refresh}` のみ)= setup_hub_ai.py が
185
+ // step_token で読む SoT。これを書くことで sync/enroll 経由でも setup が token を
186
+ // 見つけて認証をスキップでき、Codex でも setup が完走する(旧実装は下記 legacy しか
187
+ // 書かず、Codex では setup が token を見つけられず --silent で中断していた)。
188
+ // - legacy `<cliDir>/scripts/.hub_jwt.json`(version 付き)= 後方互換で従来どおり残す。
189
+ // 既存端末の reader やロールバックのため削除しない。
190
+ // cliKind 別ディレクトリに書くため Claude/Codex の token が混在しない。
184
191
  if (bundle.token) {
192
+ const canonical = {}
193
+ if (typeof bundle.token.access === "string") canonical.access = bundle.token.access
194
+ if (typeof bundle.token.refresh === "string") canonical.refresh = bundle.token.refresh
195
+ if (canonical.access && canonical.refresh) {
196
+ const canonPath = path.join(resolveClaudeDir(kind), ".hub_token.json")
197
+ await fs.mkdir(path.dirname(canonPath), { recursive: true })
198
+ await fs.writeFile(canonPath, JSON.stringify(canonical), { mode: 0o600 })
199
+ written.push(canonPath)
200
+ }
185
201
  const tp = tokenPath(kind)
186
202
  await fs.mkdir(path.dirname(tp), { recursive: true })
187
203
  await fs.writeFile(
@@ -256,17 +272,44 @@ function extractMarkerBlock(text) {
256
272
  * post_install_commands を順次実行する。manifest に list で書く想定。
257
273
  * Hub backend が冪等なコマンドを記述する責任を持つ (例: chmod +x ...)。
258
274
  */
259
- export async function runPostInstall(bundle, { logger } = {}) {
275
+ export async function runPostInstall(bundle, { logger, isWindows } = {}) {
276
+ const win = isWindows ?? process.platform === "win32"
260
277
  const cmds = Array.isArray(bundle.post_install_commands)
261
278
  ? bundle.post_install_commands
262
279
  : []
263
280
  for (const cmd of cmds) {
264
- const argv = typeof cmd === "string" ? ["/bin/sh", "-c", cmd] : cmd
281
+ const argv = _postInstallArgv(cmd, win)
265
282
  if (!Array.isArray(argv) || argv.length === 0) continue
266
283
  await runOnce(argv, { logger })
267
284
  }
268
285
  }
269
286
 
287
+ /**
288
+ * post_install エントリを実行 argv に解決する。次の 3 形式を受ける:
289
+ * - 文字列: シェルコマンドとして `/bin/sh -c <str>`(Windows は `cmd /c <str>`)
290
+ * - 配列: そのまま argv(`spawn(bin, args)`)
291
+ * - オブジェクト `{cmd_unix, cmd_windows, description}`: OS に応じて cmd_unix /
292
+ * cmd_windows を選びシェルコマンドとして実行する
293
+ *
294
+ * ⚠️ 旧実装はオブジェクト形式を `typeof==="string"` でも `Array.isArray` でもないため
295
+ * 黙って `continue` していた(= backend manifest の `{cmd_unix,...}` 形式 post_install が
296
+ * 一切実行されず、sync-bundle 後の setup_hub_ai.py が走らなかった。2026-07-15 修正)。
297
+ * cli_kind 別パス(`~/.claude` / `~/.codex`)は backend が `{{CLI_DIR_NAME}}` を解決済みで
298
+ * 返すため、ここでは OS 選択だけを行い、不用意な追加 shell 展開はしない。
299
+ */
300
+ function _postInstallArgv(cmd, win) {
301
+ if (typeof cmd === "string") {
302
+ return win ? ["cmd", "/c", cmd] : ["/bin/sh", "-c", cmd]
303
+ }
304
+ if (Array.isArray(cmd)) return cmd
305
+ if (cmd && typeof cmd === "object") {
306
+ const line = win ? cmd.cmd_windows : cmd.cmd_unix
307
+ if (typeof line !== "string" || !line.trim()) return null
308
+ return win ? ["cmd", "/c", line] : ["/bin/sh", "-c", line]
309
+ }
310
+ return null
311
+ }
312
+
270
313
  function runOnce(argv, { logger } = {}) {
271
314
  return new Promise((resolve, reject) => {
272
315
  const [bin, ...args] = argv
@@ -453,9 +496,41 @@ async function resolveCliBin(binName) {
453
496
  }
454
497
 
455
498
  /**
456
- * fetch + apply + post_install + MCP server 自動登録をまとめて実行。
499
+ * 同期対象の CLI を決定する(共通ロジック、2026-07-15)。
500
+ *
501
+ * enroll / `sync-bundle` CLI / daemon self-heal で同じ規則を使い、経路ごとの分岐を無くす:
502
+ * - `detectAvailableClis()` の結果から backend 対応の `claude` / `codex` だけを採る
503
+ * - 明示 cliKind 指定時はそれ 1 件だけ(呼び出し側で `[cliKind]` を渡す)
504
+ * - 検出ゼロ(PATH 検出失敗等)は既存互換として `['claude']` にフォールバック
505
+ *
506
+ * @param {string[]} availableClis detectAvailableClis() の戻り
507
+ * @returns {Array<'claude'|'codex'>}
508
+ */
509
+ export function resolveSyncTargets(availableClis) {
510
+ const detected = (Array.isArray(availableClis) ? availableClis : []).filter(
511
+ (c) => c === "claude" || c === "codex",
512
+ )
513
+ return detected.length > 0 ? detected : ["claude"]
514
+ }
515
+
516
+ /**
517
+ * fetch + apply + post_install(setup) + MCP server 自動登録を **同期的に** まとめて実行。
457
518
  * cliKind='codex' で Codex 向け(`~/.codex/`、`codex mcp add`)に切り替わる
458
519
  * (Stream A2、2026-07-02。既定 'claude' は完全に従来どおり)。
520
+ *
521
+ * **責務と SoT(2026-07-15、二重取得の整理)**:
522
+ * - `applyBundle` = 最小ブートストラップ。files[] 配置・CONFIG_MD マーカー・token
523
+ * (正規 `.hub_token.json` + legacy `.hub_jwt.json`)を置き、**setup_hub_ai.py が存在し
524
+ * 実行できる状態**を作る。
525
+ * - `runPostInstall` は backend が cli_kind 別にレンダリングした post_install_commands
526
+ * (= `python3 <cliDir>/scripts/setup_hub_ai.py --silent`)を **await で同期実行** する。
527
+ * `setup_hub_ai.py` が **完全セットアップの SoT**:hooks の冪等マージ(settings.json /
528
+ * hooks.json)・installed_version 記録・旧 JWT の正規 token 移行・CONFIG_MD 生成を担う。
529
+ * その中で `step_install_files` が bundle を再取得して files を冪等に再検証する(= 2 回目の
530
+ * fetch。SoT の自己完結検証として許容。applyBundle の配置と重複しても冪等)。
531
+ * - よって **本関数が成功を返す時点で setup は完了しており、detached の待ち残りは無い**
532
+ * (手動 `hub-agent sync-bundle` は setup 完了まで同期的に待つ)。CLI 別パスは backend が
533
+ * `{{CLI_DIR_NAME}}` を解決して返すため、Codex 同期で `~/.claude` の setup を誤実行しない。
459
534
  */
460
535
  export async function syncBundle({ hubUrl, agentId, agentToken, logger, fetchImpl, cliKind = "claude" } = {}) {
461
536
  const bundle = await fetchBundle({ hubUrl, agentId, agentToken, fetchImpl, cliKind })
@@ -463,7 +538,7 @@ export async function syncBundle({ hubUrl, agentId, agentToken, logger, fetchImp
463
538
  try {
464
539
  await runPostInstall(bundle, { logger })
465
540
  } catch (err) {
466
- logger?.warn({ err: err.message }, "post_install partially failed (continuing)")
541
+ logger?.warn({ cliKind, err: err.message }, "post_install/setup partially failed (continuing)")
467
542
  }
468
543
  let mcp = { added: [], skipped: [] }
469
544
  try {
package/src/main.mjs CHANGED
@@ -460,6 +460,38 @@ export function kickSetupHubAi({
460
460
  }
461
461
  }
462
462
 
463
+ /**
464
+ * daemon 起動時 self-heal: 検出された Claude/Codex **双方**の setup_hub_ai.py を
465
+ * best-effort で kick する(2026-07-15)。旧実装は `kickSetupHubAi({logger})` = Claude 固定で、
466
+ * Codex が入っていても hooks.json/AGENTS.md/version が修復されなかった。
467
+ *
468
+ * - 対象決定は enroll / sync-bundle と共通の resolveSyncTargets に寄せる
469
+ * - 各 CLI を detached で best-effort 実行(既に整った環境では step_* が冪等で変更ゼロ)
470
+ * - kickSetupHubAi は例外を投げないため、一方の失敗で他方を止めない
471
+ * - ログには必ず cliKind を含める
472
+ */
473
+ export async function kickSetupHubAiForDetectedClis({
474
+ logger,
475
+ detectImpl,
476
+ homedir,
477
+ spawnImpl,
478
+ existsImpl,
479
+ } = {}) {
480
+ let targets = ["claude"]
481
+ try {
482
+ const detect = detectImpl || detectAvailableClis
483
+ const { resolveSyncTargets } = await import("./hub-bundle.mjs")
484
+ targets = resolveSyncTargets(await detect())
485
+ } catch (e) {
486
+ logger?.warn?.({ err: e?.message }, "kickSetupHubAiForDetectedClis: CLI 検出失敗 - claude のみ")
487
+ targets = ["claude"]
488
+ }
489
+ for (const cliKind of targets) {
490
+ kickSetupHubAi({ logger, cliKind, homedir, spawnImpl, existsImpl })
491
+ }
492
+ return targets
493
+ }
494
+
463
495
  export async function startDaemon({ version, ptyModule, claudeSdk } = {}) {
464
496
  const config = await readConfig()
465
497
  if (!config) {
@@ -511,7 +543,8 @@ export async function startDaemon({ version, ptyModule, claudeSdk } = {}) {
511
543
  // `hub-agent restart` だけで CLAUDE.md と hooks を自動修復できるようにする。
512
544
  // 既に整っている環境では step_* がマーカー範囲を冪等更新するだけで害なし
513
545
  // (detached + stdio ignore で daemon は妨げない)。WHY: 2026-06-17 toma 環境調査。
514
- kickSetupHubAi({ logger })
546
+ // 2026-07-15: Claude 固定から **検出された Claude/Codex 双方** の kick へ拡張。
547
+ await kickSetupHubAiForDetectedClis({ logger })
515
548
 
516
549
  // node-pty (Microsoft 公式) は macOS arm64 で PTY master fd を spawn ごとに leak し
517
550
  // (kill/destroy でも解放されず)、kern.tty.ptmx_max=511 到達で「新規ターミナルが
package/src/usage.mjs CHANGED
@@ -1445,15 +1445,22 @@ export async function codexUsageForThread(threadId) {
1445
1445
  const rateLimits = found.payload.rate_limits
1446
1446
 
1447
1447
  let contextPercent = null
1448
- const totalTok = info?.total_token_usage
1449
1448
  const windowSize = info?.model_context_window
1450
- if (totalTok && typeof windowSize === "number" && windowSize > 0) {
1449
+ // context 窓の「現在の占有率」は last_token_usage(直近ターンで実際に窓へ載った
1450
+ // トークン)で測る。total_token_usage は会話累積(キャッシュ再利用分も加算し
1451
+ // 単調増加)なので、ターンを重ねると窓サイズを超過し Math.min(100,…) で 100% に
1452
+ // クランプされてドーナツが固着する(2026-07-15 バグ修正: 実測で累積 1,341,719 /
1453
+ // 窓 258,400 = 519% → 100% 張り付き。実占有は last=66,973 → 25.9%)。Codex は
1454
+ // 自動コンパクションで実文脈を使い回すため、累積で割ると必ず張り付く。
1455
+ // last_token_usage が無い旧 Codex 版のみ従来の total_token_usage にフォールバック。
1456
+ const usageForContext = info?.last_token_usage ?? info?.total_token_usage
1457
+ if (usageForContext && typeof windowSize === "number" && windowSize > 0) {
1451
1458
  const tokens =
1452
- typeof totalTok.total_tokens === "number"
1453
- ? totalTok.total_tokens
1454
- : (totalTok.input_tokens || 0) +
1455
- (totalTok.cached_input_tokens || 0) +
1456
- (totalTok.output_tokens || 0)
1459
+ typeof usageForContext.total_tokens === "number"
1460
+ ? usageForContext.total_tokens
1461
+ : (usageForContext.input_tokens || 0) +
1462
+ (usageForContext.cached_input_tokens || 0) +
1463
+ (usageForContext.output_tokens || 0)
1457
1464
  contextPercent = Math.min(100, Math.round((tokens / windowSize) * 1000) / 10)
1458
1465
  }
1459
1466