@cocorograph/hub-agent 0.8.58 → 0.8.60
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/README.md +3 -1
- package/package.json +1 -1
- package/scripts/install.sh +276 -48
- package/src/claude-history.mjs +202 -2
- package/src/claude-stream-bridge.mjs +82 -3
package/README.md
CHANGED
|
@@ -17,8 +17,10 @@ curl -fsSL https://api.hub.cocorograph.com/api/cockpit/agents/install-script | b
|
|
|
17
17
|
|
|
18
18
|
このスクリプトは以下を **全自動** で実行します:
|
|
19
19
|
- Homebrew (macOS で無ければ install)
|
|
20
|
-
- tmux + node
|
|
20
|
+
- tmux + node (Active LTS) + python3 (無ければ install)
|
|
21
|
+
- GNU coreutils の `timeout` (補助コマンド。macOS では brew で導入して `gtimeout` への symlink を `~/.local/bin` に作成、Linux は存在確認のみで不在なら手動導入を案内。無くてもセットアップは続行)
|
|
21
22
|
- `npm i -g @cocorograph/hub-agent`
|
|
23
|
+
- `npm i -g @anthropic-ai/claude-code` / `npm i -g @openai/codex` (Claude Code / Codex CLI。既存なら更新)
|
|
22
24
|
- `hub-agent enroll <token>` (token は Hub session 経由で埋め込み済、5 分有効)
|
|
23
25
|
- `hub-agent install-service` で OS サービス登録
|
|
24
26
|
|
package/package.json
CHANGED
package/scripts/install.sh
CHANGED
|
@@ -12,11 +12,14 @@
|
|
|
12
12
|
# 1. macOS なら Homebrew (なければ install)
|
|
13
13
|
# - 管理者権限が無い環境では自動で $HOME/homebrew にユーザーローカル
|
|
14
14
|
# インストールに切り替わる (HUB_AGENT_USER_BREW=1 で強制も可)
|
|
15
|
-
# 2. tmux + node (なければ install via brew / apt)
|
|
16
|
-
# 3.
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
#
|
|
15
|
+
# 2. tmux + node + python3 (なければ install via brew / apt 等)
|
|
16
|
+
# 3. GNU coreutils の timeout (補助コマンド。macOS では brew で入れて gtimeout への
|
|
17
|
+
# symlink を ~/.local/bin に作成。Linux は存在確認のみで、不在なら手動導入を案内)
|
|
18
|
+
# 4. npm i -g @cocorograph/hub-agent
|
|
19
|
+
# 5. npm i -g @anthropic-ai/claude-code (既存があれば upgrade のみ)
|
|
20
|
+
# 6. npm i -g @openai/codex (常に install / upgrade)
|
|
21
|
+
# 7. HUB_AGENT_TOKEN が設定されていれば hub-agent enroll を自動実行
|
|
22
|
+
# 8. hub-agent install-service で OS サービス化
|
|
20
23
|
# =============================================================================
|
|
21
24
|
|
|
22
25
|
set -euo pipefail
|
|
@@ -41,6 +44,7 @@ NODE_DEFAULT_BREW_FORMULA="node@24"
|
|
|
41
44
|
|
|
42
45
|
PACKAGE_NAME="@cocorograph/hub-agent"
|
|
43
46
|
CLAUDE_CODE_PACKAGE="@anthropic-ai/claude-code"
|
|
47
|
+
CODEX_PACKAGE="@openai/codex"
|
|
44
48
|
|
|
45
49
|
color_step() { printf "\033[1;34m==> %s\033[0m\n" "$1"; }
|
|
46
50
|
color_ok() { printf "\033[1;32m✓ %s\033[0m\n" "$1"; }
|
|
@@ -223,6 +227,49 @@ persist_user_brew_shellenv() {
|
|
|
223
227
|
done
|
|
224
228
|
}
|
|
225
229
|
|
|
230
|
+
# snippet をマーカーで囲んで各 profile に冪等追記する。
|
|
231
|
+
# 使い方: persist_profile_snippet '<snippet>' '<label>' '<marker>' '<end_marker>' <profile>...
|
|
232
|
+
# 既に「snippet と完全一致する行」があれば skip する(部分一致だとコメントアウトされた
|
|
233
|
+
# `#export PATH=...` を既存扱いにして追記を飛ばす。マーカーの有無は見ない——マーカーだけ
|
|
234
|
+
# 残って snippet が欠けた部分書込みを再実行で補完できるようにするため)。
|
|
235
|
+
# 追記できない profile(root 所有・読み取り専用)は警告して次へ進み、最後に 1 を返す。
|
|
236
|
+
# 呼び出し側が失敗を致命にするか警告に留めるかを決める(set -e 下で裸で呼べば致命)。
|
|
237
|
+
persist_profile_snippet() {
|
|
238
|
+
local snippet="$1" label="$2" marker="$3" end_marker="$4"
|
|
239
|
+
shift 4
|
|
240
|
+
local target rc=0
|
|
241
|
+
for target in "$@"; do
|
|
242
|
+
if [[ -f "$target" ]] && grep -Fxq "$snippet" "$target"; then
|
|
243
|
+
continue
|
|
244
|
+
fi
|
|
245
|
+
if {
|
|
246
|
+
printf '\n%s\n' "$marker"
|
|
247
|
+
printf '%s\n' "$snippet"
|
|
248
|
+
printf '%s\n' "$end_marker"
|
|
249
|
+
} >> "$target" 2>/dev/null; then
|
|
250
|
+
color_ok "$target に $label を追記 (新規シェルから有効)"
|
|
251
|
+
else
|
|
252
|
+
color_warn "$target に $label を追記できません (書き込み権限を確認してください)"
|
|
253
|
+
rc=1
|
|
254
|
+
fi
|
|
255
|
+
done
|
|
256
|
+
return "$rc"
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
# bash の login profile を 1 つ選ぶ。bash は ~/.bash_profile → ~/.bash_login → ~/.profile の
|
|
260
|
+
# うち最初に存在するものだけを読むため、~/.profile しか無い環境で ~/.bash_profile を
|
|
261
|
+
# 新規作成すると ~/.profile が読まれなくなる。既存のものを優先し、無ければ ~/.bash_profile。
|
|
262
|
+
bash_login_profile() {
|
|
263
|
+
local f
|
|
264
|
+
for f in "$HOME/.bash_profile" "$HOME/.bash_login" "$HOME/.profile"; do
|
|
265
|
+
if [[ -f "$f" ]]; then
|
|
266
|
+
printf '%s\n' "$f"
|
|
267
|
+
return 0
|
|
268
|
+
fi
|
|
269
|
+
done
|
|
270
|
+
printf '%s\n' "$HOME/.bash_profile"
|
|
271
|
+
}
|
|
272
|
+
|
|
226
273
|
# user-mode の場合、npm の global prefix を $HOME/.npm-global に切り替える。
|
|
227
274
|
# brew が $HOME/homebrew にいるとき、その prefix への書き込み権限はあるものの、
|
|
228
275
|
# 後で system 版 node / npm に切り替わる可能性も考えて user 領域に逃がしておく。
|
|
@@ -256,8 +303,6 @@ ensure_npm_user_prefix() {
|
|
|
256
303
|
export PATH="$HOME/.npm-global/bin:$PATH"
|
|
257
304
|
|
|
258
305
|
local snippet='export PATH="$HOME/.npm-global/bin:$PATH"'
|
|
259
|
-
local marker="# >>> hub-agent: npm-global PATH (user-mode) >>>"
|
|
260
|
-
local end_marker="# <<< hub-agent: npm-global PATH <<<"
|
|
261
306
|
# 永続先は OS で出し分け。Linux の hub-agent は systemd --user 起動時や
|
|
262
307
|
# `$SHELL -lic` 経由の env 注入で PATH を解決するため、login(.profile) と
|
|
263
308
|
# interactive(.bashrc) の双方に通す。macOS は従来どおり zsh/bash の profile。
|
|
@@ -267,18 +312,9 @@ ensure_npm_user_prefix() {
|
|
|
267
312
|
else
|
|
268
313
|
targets=("$HOME/.bashrc" "$HOME/.profile")
|
|
269
314
|
fi
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
continue
|
|
274
|
-
fi
|
|
275
|
-
{
|
|
276
|
-
printf '\n%s\n' "$marker"
|
|
277
|
-
printf '%s\n' "$snippet"
|
|
278
|
-
printf '%s\n' "$end_marker"
|
|
279
|
-
} >> "$target"
|
|
280
|
-
color_ok "$target に npm-global PATH を追記"
|
|
281
|
-
done
|
|
315
|
+
persist_profile_snippet "$snippet" "npm-global PATH" \
|
|
316
|
+
"# >>> hub-agent: npm-global PATH (user-mode) >>>" "# <<< hub-agent: npm-global PATH <<<" \
|
|
317
|
+
"${targets[@]}"
|
|
282
318
|
}
|
|
283
319
|
|
|
284
320
|
# =============================================================================
|
|
@@ -458,28 +494,40 @@ ensure_node_tls_works() {
|
|
|
458
494
|
exit 1
|
|
459
495
|
}
|
|
460
496
|
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
497
|
+
# パッケージマネージャ分岐の共通処理。失敗は戻り値で返し、exit するかは呼び出し側が決める。
|
|
498
|
+
# 1 … 導入コマンドが失敗した / 2 … 対応するパッケージマネージャが無い
|
|
499
|
+
# ⚠️ display_cmd は復旧案内に出す「入れたい実行コマンド」。パッケージ名と一致しない
|
|
500
|
+
# ことがある (pacman の python パッケージ / 検証対象は python3 コマンド)。
|
|
501
|
+
_pm_install() {
|
|
502
|
+
local brew_pkg="$1"
|
|
503
|
+
local apt_pkg="$2"
|
|
504
|
+
local pacman_pkg="${3:-$apt_pkg}"
|
|
505
|
+
local display_cmd="${4:-$brew_pkg}"
|
|
467
506
|
if [[ "$(uname)" == "Darwin" ]]; then
|
|
468
507
|
# brew install は transient (network / hash mismatch / pour 失敗) を retry で吸収
|
|
469
|
-
retry 3 brew install "$brew_pkg"
|
|
508
|
+
retry 3 brew install "$brew_pkg" || return 1
|
|
470
509
|
elif present apt-get; then
|
|
471
|
-
retry 3 sudo apt-get update -y
|
|
472
|
-
retry 3 sudo apt-get install -y "$apt_pkg"
|
|
510
|
+
retry 3 sudo apt-get update -y || return 1
|
|
511
|
+
retry 3 sudo apt-get install -y "$apt_pkg" || return 1
|
|
473
512
|
elif present dnf; then
|
|
474
|
-
retry 3 sudo dnf install -y "$apt_pkg"
|
|
513
|
+
retry 3 sudo dnf install -y "$apt_pkg" || return 1
|
|
475
514
|
elif present pacman; then
|
|
476
|
-
retry 3 sudo pacman -S --noconfirm "$
|
|
515
|
+
retry 3 sudo pacman -S --noconfirm "$pacman_pkg" || return 1
|
|
477
516
|
else
|
|
478
|
-
color_err "対応するパッケージマネージャ (brew/apt/dnf/pacman) が見つかりません。$
|
|
479
|
-
|
|
517
|
+
color_err "対応するパッケージマネージャ (brew/apt/dnf/pacman) が見つかりません。$display_cmd を手動で install してください"
|
|
518
|
+
return 2
|
|
480
519
|
fi
|
|
481
520
|
}
|
|
482
521
|
|
|
522
|
+
ensure_pkg() {
|
|
523
|
+
local cmd="$1"
|
|
524
|
+
local brew_pkg="$2"
|
|
525
|
+
local apt_pkg="${3:-$brew_pkg}"
|
|
526
|
+
if present "$cmd"; then color_ok "$cmd already installed"; return 0; fi
|
|
527
|
+
color_step "$cmd をインストール"
|
|
528
|
+
_pm_install "$brew_pkg" "$apt_pkg" "" "$cmd" || exit 1
|
|
529
|
+
}
|
|
530
|
+
|
|
483
531
|
# macOS で brew 経由で Active LTS の node (NODE_DEFAULT_BREW_FORMULA) を導入/切替する。
|
|
484
532
|
# 既存の無印 node が link されていれば unlink してから新 formula を link --overwrite --force。
|
|
485
533
|
_install_node_lts_brew() {
|
|
@@ -544,6 +592,111 @@ ensure_build_tools() {
|
|
|
544
592
|
fi
|
|
545
593
|
}
|
|
546
594
|
|
|
595
|
+
# Hub AI bundle のフック・helper と hub-agent bootstrap が使う python3 を保証する。
|
|
596
|
+
# macOS の /usr/bin/python3 は Xcode CLT 未導入時にスタブの場合があるため、
|
|
597
|
+
# command の存在ではなく実際の version コマンドが成功するかで判定する。
|
|
598
|
+
ensure_python3() {
|
|
599
|
+
if python3 --version >/dev/null 2>&1; then
|
|
600
|
+
color_ok "$(python3 --version 2>&1)"
|
|
601
|
+
return 0
|
|
602
|
+
fi
|
|
603
|
+
|
|
604
|
+
color_step "python3 をインストール"
|
|
605
|
+
# macOS で python3 が動かないのは、ほぼ Xcode CLT 未導入(/usr/bin/python3 がスタブ)。
|
|
606
|
+
# brew 経路が失敗したら、set -e で黙って落ちる前に実際の復旧手順を出す。
|
|
607
|
+
# pacman のパッケージ名は python3 でなく python。案内に出すコマンド名は python3。
|
|
608
|
+
if ! _pm_install python python3 python python3; then
|
|
609
|
+
if [[ "$(uname)" == "Darwin" ]]; then
|
|
610
|
+
color_err "python3 を導入できません。\`xcode-select --install\` を実行してから再実行してください"
|
|
611
|
+
fi
|
|
612
|
+
exit 1
|
|
613
|
+
fi
|
|
614
|
+
hash -r 2>/dev/null || true
|
|
615
|
+
|
|
616
|
+
if python3 --version >/dev/null 2>&1; then
|
|
617
|
+
color_ok "$(python3 --version 2>&1)"
|
|
618
|
+
else
|
|
619
|
+
color_err "python3 の導入後も実行できません"
|
|
620
|
+
exit 1
|
|
621
|
+
fi
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
# timeout を保証する(補助コマンド。AI セッションの Bash やスクリプトが使うが、
|
|
625
|
+
# hub-agent の常駐動作には必須ではないため、導入失敗は警告して継続する。
|
|
626
|
+
# 必須ランタイムの python3 とは契約を分けている)。
|
|
627
|
+
# macOS の coreutils は gtimeout 名で入るため、timeout だけを ~/.local/bin に公開する。
|
|
628
|
+
# coreutils の gnubin 全体を PATH に足すと sed / date 等まで GNU 版へ置き換わり、
|
|
629
|
+
# 既存スクリプトの挙動が変わるため採用しない。
|
|
630
|
+
_report_timeout() {
|
|
631
|
+
if present timeout; then
|
|
632
|
+
color_ok "timeout $(timeout --version 2>/dev/null </dev/null | head -1 || echo 'installed')"
|
|
633
|
+
else
|
|
634
|
+
color_warn "timeout コマンドが PATH に見つかりません (補助コマンドのため継続。新規シェルで再確認してください)"
|
|
635
|
+
fi
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
ensure_timeout() {
|
|
639
|
+
if [[ "$(uname)" != "Darwin" ]]; then
|
|
640
|
+
# Linux の timeout は coreutils (Debian の essential / busybox にも同梱) なので
|
|
641
|
+
# 通常は存在する。無い環境はインストーラが推測で入れず、警告に留める。
|
|
642
|
+
if ! present timeout; then
|
|
643
|
+
color_warn "timeout が見つかりません (coreutils を手動で導入してください。補助コマンドのため継続)"
|
|
644
|
+
fi
|
|
645
|
+
_report_timeout
|
|
646
|
+
return 0
|
|
647
|
+
fi
|
|
648
|
+
|
|
649
|
+
local local_bin="$HOME/.local/bin"
|
|
650
|
+
local link="$local_bin/timeout"
|
|
651
|
+
local found
|
|
652
|
+
found="$(command -v timeout 2>/dev/null || true)"
|
|
653
|
+
# このインストーラ管理の symlink 以外の timeout が既に見えていれば、それを尊重する
|
|
654
|
+
if [[ -n "$found" && "$found" != "$link" ]]; then
|
|
655
|
+
color_ok "timeout already installed ($found)"
|
|
656
|
+
return 0
|
|
657
|
+
fi
|
|
658
|
+
|
|
659
|
+
if [[ -e "$link" && ! -L "$link" ]]; then
|
|
660
|
+
# ユーザーが自前で置いた実体は上書きしない(PATH 永続化だけ揃える)
|
|
661
|
+
color_ok "$link は手動管理のため変更しません"
|
|
662
|
+
else
|
|
663
|
+
local gtimeout
|
|
664
|
+
gtimeout="$(command -v gtimeout 2>/dev/null || true)"
|
|
665
|
+
if [[ -z "$gtimeout" ]]; then
|
|
666
|
+
color_step "GNU coreutils (timeout) をインストール"
|
|
667
|
+
if ! retry 3 brew install coreutils; then
|
|
668
|
+
color_warn "coreutils の導入に失敗しました (timeout は補助コマンドのため継続)"
|
|
669
|
+
return 0
|
|
670
|
+
fi
|
|
671
|
+
hash -r 2>/dev/null || true
|
|
672
|
+
gtimeout="$(command -v gtimeout 2>/dev/null || true)"
|
|
673
|
+
fi
|
|
674
|
+
if [[ -z "$gtimeout" ]]; then
|
|
675
|
+
color_warn "gtimeout が PATH に見つかりません (timeout は補助コマンドのため継続)"
|
|
676
|
+
return 0
|
|
677
|
+
fi
|
|
678
|
+
if ! mkdir -p "$local_bin" 2>/dev/null || ! ln -sfn "$gtimeout" "$link" 2>/dev/null; then
|
|
679
|
+
color_warn "$link を作成できません (書き込み権限を確認してください。補助コマンドのため継続)"
|
|
680
|
+
return 0
|
|
681
|
+
fi
|
|
682
|
+
fi
|
|
683
|
+
|
|
684
|
+
# symlink の有無と PATH 永続化は別々に収束させる。再実行時に片方だけ欠けていても
|
|
685
|
+
# ここで補完される(早期 return で profile の修復を飛ばさない)。
|
|
686
|
+
# ~/.local/bin は PATH の「末尾」に足す。先頭に置くと pipx / pyenv 等が置いた
|
|
687
|
+
# ~/.local/bin/python3 や node のシムが brew / system のものを隠してしまう。
|
|
688
|
+
# timeout を見つけるだけなら末尾で足りる(別の timeout があれば上で return 済み)。
|
|
689
|
+
# PATH 代入で bash の command hash は全消去されるので、この後の hash -r は不要。
|
|
690
|
+
export PATH="$PATH:$local_bin"
|
|
691
|
+
# npm prefix と同じく zsh / bash 双方の login profile へ永続化する。
|
|
692
|
+
# 追記失敗(root 所有の profile 等)はこの補助コマンドの契約に合わせて警告に留める。
|
|
693
|
+
persist_profile_snippet 'export PATH="$PATH:$HOME/.local/bin"' "~/.local/bin PATH" \
|
|
694
|
+
"# >>> hub-agent: local-bin PATH (timeout) >>>" "# <<< hub-agent: local-bin PATH <<<" \
|
|
695
|
+
"$HOME/.zprofile" "$(bash_login_profile)" \
|
|
696
|
+
|| color_warn "PATH の永続化に失敗した profile があります (timeout は補助コマンドのため継続)"
|
|
697
|
+
_report_timeout
|
|
698
|
+
}
|
|
699
|
+
|
|
547
700
|
# Node.js のサポート範囲ポリシー判定。
|
|
548
701
|
# - 未インストール → LTS を install
|
|
549
702
|
# - [MIN, MAX] 範囲内 → 現状維持(ユーザーの環境を尊重)
|
|
@@ -599,29 +752,69 @@ ensure_global_install() {
|
|
|
599
752
|
color_ok "hub-agent $(hub-agent --version)"
|
|
600
753
|
}
|
|
601
754
|
|
|
602
|
-
#
|
|
603
|
-
#
|
|
604
|
-
#
|
|
605
|
-
|
|
606
|
-
|
|
755
|
+
# npm 配信の CLI を導入/upgrade する共通処理。
|
|
756
|
+
# required=1 … 新規導入の失敗を非 0 で呼び出し側へ返す。main から素の
|
|
757
|
+
# コマンドとして呼べば set -e でセットアップ全体が止まる
|
|
758
|
+
# (この関数自身は exit しないので、条件式から呼べば拾える)
|
|
759
|
+
# required=0 … 新規導入が失敗しても警告のみでセットアップを継続する
|
|
760
|
+
# 上記以外 … 契約違反として 2 を返す (タイプミスを optional へ倒さない)
|
|
761
|
+
# ⚠️ fail_hint は「導入できなかったとき何が使えなくなるか」を呼び出し側が渡す。
|
|
762
|
+
# この関数に CLI 固有の文言を書かないこと (次の CLI を足したとき嘘になる)。
|
|
763
|
+
_ensure_npm_cli() {
|
|
764
|
+
local cmd="$1"
|
|
765
|
+
local package="$2"
|
|
766
|
+
local label="$3"
|
|
767
|
+
local required="$4"
|
|
768
|
+
local fail_hint="${5:-}"
|
|
769
|
+
# ⚠️ required は 0/1 のみ受け付ける。"true" 等のタイプミスを optional 扱いへ
|
|
770
|
+
# 黙って倒すと、必須 CLI が入らないままセットアップが完了する。
|
|
771
|
+
case "$required" in
|
|
772
|
+
0|1) ;;
|
|
773
|
+
*)
|
|
774
|
+
color_err "_ensure_npm_cli: required は 0/1 のみです (受け取った値: '${required}')"
|
|
775
|
+
return 2
|
|
776
|
+
;;
|
|
777
|
+
esac
|
|
778
|
+
if present "$cmd"; then
|
|
607
779
|
local cur
|
|
608
|
-
cur=$(
|
|
609
|
-
color_step "
|
|
780
|
+
cur=$("$cmd" --version 2>/dev/null </dev/null || echo "unknown")
|
|
781
|
+
color_step "$label (現在 $cur) を最新版にアップデート"
|
|
610
782
|
# claude は Anthropic 配信。失敗しても既存版で継続 (warning のみ)。
|
|
611
|
-
retry 2 npm install -g "${
|
|
612
|
-
|| color_warn "
|
|
783
|
+
retry 2 npm install -g "${package}@latest" --force \
|
|
784
|
+
|| color_warn "$label upgrade に失敗 (既存版で継続)"
|
|
613
785
|
else
|
|
614
|
-
color_step "$
|
|
615
|
-
|
|
786
|
+
color_step "$package を install"
|
|
787
|
+
if [[ "$required" == "1" ]]; then
|
|
788
|
+
# ⚠️ 明示的に return する。条件式 (`if f; then`) の中では関数内の errexit が
|
|
789
|
+
# 抑止されるため、暗黙の set -e に頼ると必須契約が呼び出し方で消える。
|
|
790
|
+
retry 3 npm install -g "${package}@latest" --force || return 1
|
|
791
|
+
elif ! retry 3 npm install -g "${package}@latest" --force; then
|
|
792
|
+
color_warn "$label の導入に失敗しました。${fail_hint:-セットアップは継続します}"
|
|
793
|
+
color_warn " 手動で実行: npm install -g ${package}@latest"
|
|
794
|
+
fi
|
|
616
795
|
fi
|
|
617
796
|
hash -r 2>/dev/null || true
|
|
618
|
-
if present
|
|
619
|
-
color_ok "
|
|
797
|
+
if present "$cmd"; then
|
|
798
|
+
color_ok "$cmd $("$cmd" --version 2>/dev/null </dev/null || echo 'installed')"
|
|
620
799
|
else
|
|
621
|
-
color_warn "
|
|
800
|
+
color_warn "$cmd コマンドが PATH に見つかりません。新規シェルで再確認してください"
|
|
622
801
|
fi
|
|
623
802
|
}
|
|
624
803
|
|
|
804
|
+
# Claude Code CLI を同梱でセットアップ。Cockpit 側から claude を呼び出すので、
|
|
805
|
+
# 同じ npm-global prefix に入れておくとパス解決が一貫する。
|
|
806
|
+
# 既存があれば upgrade のみ(破壊しない)。hub-agent のチャットに必須なので required=1。
|
|
807
|
+
ensure_claude_code() {
|
|
808
|
+
_ensure_npm_cli claude "$CLAUDE_CODE_PACKAGE" "Claude Code" 1
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
# Codex CLI を同梱でセットアップ。hub-agent 自体の動作には必須ではないため、
|
|
812
|
+
# 新規導入・upgrade とも失敗時は警告してセットアップを継続する。
|
|
813
|
+
ensure_codex() {
|
|
814
|
+
_ensure_npm_cli codex "$CODEX_PACKAGE" "Codex CLI" 0 \
|
|
815
|
+
"Codex チャット / 委譲 / 画像生成は使えません"
|
|
816
|
+
}
|
|
817
|
+
|
|
625
818
|
do_enroll() {
|
|
626
819
|
if [[ -z "${HUB_AGENT_TOKEN:-}" ]]; then
|
|
627
820
|
color_warn "HUB_AGENT_TOKEN が未設定。enroll は skip します"
|
|
@@ -767,6 +960,24 @@ verify_setup() {
|
|
|
767
960
|
color_warn "Claude Code (claude) コマンドが見つかりません (cockpit から claude を呼ぶ場合は必要)"
|
|
768
961
|
fi
|
|
769
962
|
|
|
963
|
+
# Codex CLI (任意なので warn のみ)
|
|
964
|
+
if present codex; then
|
|
965
|
+
color_ok "Codex CLI: $(codex --version 2>/dev/null </dev/null || echo 'installed')"
|
|
966
|
+
else
|
|
967
|
+
color_warn "Codex CLI (codex) コマンドが見つかりません (Codex チャット / 委譲 / 画像生成には必要)"
|
|
968
|
+
fi
|
|
969
|
+
|
|
970
|
+
# python3 (Hub AI bundle と hub-agent bootstrap に必須)
|
|
971
|
+
if python3 --version >/dev/null 2>&1; then
|
|
972
|
+
color_ok "Python 3: $(python3 --version 2>&1)"
|
|
973
|
+
else
|
|
974
|
+
color_err "python3 コマンドが実行できません"
|
|
975
|
+
errors=$((errors + 1))
|
|
976
|
+
fi
|
|
977
|
+
|
|
978
|
+
# timeout は補助コマンド (ensure_timeout と同じ契約) なので warn のみ
|
|
979
|
+
_report_timeout
|
|
980
|
+
|
|
770
981
|
# 3. Hub AI bundle 展開を **CLI 別** に検証(manifest / CONFIG_MD マーカー /
|
|
771
982
|
# hooks の Hub SessionStart hook / 正規 .hub_token.json)。Codex 未インストールなら
|
|
772
983
|
# codex は検証しない(2026-07-15)。
|
|
@@ -831,7 +1042,7 @@ EOF
|
|
|
831
1042
|
|
|
832
1043
|
main() {
|
|
833
1044
|
color_step "hub-agent ワンライナーセットアップを開始"
|
|
834
|
-
STEP_TOTAL=
|
|
1045
|
+
STEP_TOTAL=15
|
|
835
1046
|
STEP_NUM=0
|
|
836
1047
|
|
|
837
1048
|
step_header "Homebrew"
|
|
@@ -842,6 +1053,10 @@ main() {
|
|
|
842
1053
|
ensure_node_version
|
|
843
1054
|
step_header "Build tools (native addons)"
|
|
844
1055
|
ensure_build_tools
|
|
1056
|
+
step_header "Python 3"
|
|
1057
|
+
ensure_python3
|
|
1058
|
+
step_header "GNU coreutils (timeout)"
|
|
1059
|
+
ensure_timeout
|
|
845
1060
|
step_header "npm global prefix"
|
|
846
1061
|
ensure_npm_user_prefix
|
|
847
1062
|
# ensure_node_version 直後だと brew link でシェルの command hash がズレている
|
|
@@ -854,6 +1069,8 @@ main() {
|
|
|
854
1069
|
ensure_global_install
|
|
855
1070
|
step_header "Claude Code install"
|
|
856
1071
|
ensure_claude_code
|
|
1072
|
+
step_header "Codex CLI install"
|
|
1073
|
+
ensure_codex
|
|
857
1074
|
step_header "enroll + bundle 同期"
|
|
858
1075
|
do_enroll
|
|
859
1076
|
step_header "Hub AI bootstrap (CLAUDE.md / hooks 初期化)"
|
|
@@ -869,4 +1086,15 @@ main() {
|
|
|
869
1086
|
macos_perm_guidance
|
|
870
1087
|
}
|
|
871
1088
|
|
|
872
|
-
|
|
1089
|
+
# このファイルが source されたか(実行されたか)を判定する。
|
|
1090
|
+
# - `bash -c "$(curl ...)"` / `curl ... | bash` : BASH_SOURCE が空 → 実行
|
|
1091
|
+
# - `bash install.sh` / `./install.sh` : BASH_SOURCE[0] == $0 → 実行
|
|
1092
|
+
# - `source install.sh`(test/install-sh.test.mjs): BASH_SOURCE[0] != $0 → source
|
|
1093
|
+
# 環境変数で切り替えると、変数が残ったシェルや CI でワンライナーが無言の no-op になるため使わない。
|
|
1094
|
+
_install_sh_is_sourced() {
|
|
1095
|
+
[[ -n "${BASH_SOURCE[0]:-}" && "${BASH_SOURCE[0]}" != "$0" ]]
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
if ! _install_sh_is_sourced; then
|
|
1099
|
+
main "$@"
|
|
1100
|
+
fi
|
package/src/claude-history.mjs
CHANGED
|
@@ -169,10 +169,209 @@ async function getLastTurnAtMsCached(fileMap, filePath, mtimeMs, size) {
|
|
|
169
169
|
// round2 minor 対応)。前回値も無ければ最後の手段として mtime。
|
|
170
170
|
const extracted = await extractLastTurnAtMs(filePath, null)
|
|
171
171
|
const lastTurnAtMs = extracted ?? entry?.lastTurnAtMs ?? mtimeMs
|
|
172
|
-
fileMap.set(filePath, { mtimeMs, size, lastTurnAtMs })
|
|
172
|
+
fileMap.set(filePath, { ...entry, mtimeMs, size, lastTurnAtMs })
|
|
173
173
|
return lastTurnAtMs
|
|
174
174
|
}
|
|
175
175
|
|
|
176
|
+
const TITLE_SCAN_CHUNK_SIZE = 64 * 1024
|
|
177
|
+
const TITLE_FINGERPRINT_SIZE = 256
|
|
178
|
+
let titleScanBytesForTest = 0
|
|
179
|
+
let titleMaxCarryBytesForTest = 0
|
|
180
|
+
export function getTitleScanBytesForTest() {
|
|
181
|
+
return titleScanBytesForTest
|
|
182
|
+
}
|
|
183
|
+
export function resetTitleScanBytesForTest() {
|
|
184
|
+
titleScanBytesForTest = 0
|
|
185
|
+
}
|
|
186
|
+
export function getTitleMaxCarryBytesForTest() {
|
|
187
|
+
return titleMaxCarryBytesForTest
|
|
188
|
+
}
|
|
189
|
+
export function resetTitleMaxCarryBytesForTest() {
|
|
190
|
+
titleMaxCarryBytesForTest = 0
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function takeTitleFromLine(line, entry) {
|
|
194
|
+
if (!line.includes('"custom-title"') && !line.includes('"ai-title"')) return
|
|
195
|
+
let obj
|
|
196
|
+
try {
|
|
197
|
+
obj = JSON.parse(line)
|
|
198
|
+
} catch {
|
|
199
|
+
return
|
|
200
|
+
}
|
|
201
|
+
if (obj?.type === "custom-title" && typeof obj.customTitle === "string") {
|
|
202
|
+
entry.customTitle = obj.customTitle
|
|
203
|
+
} else if (obj?.type === "ai-title" && typeof obj.aiTitle === "string") {
|
|
204
|
+
entry.aiTitle = obj.aiTitle
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function titleFromEntry(entry) {
|
|
209
|
+
const customTitle = entry.customTitle?.trim().replace(/\s+/g, " ")
|
|
210
|
+
const aiTitle = entry.aiTitle?.trim().replace(/\s+/g, " ")
|
|
211
|
+
const title = customTitle || aiTitle
|
|
212
|
+
return title ? (MASK_ENABLED ? redactSecrets(title) : title) : undefined
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** jsonl の前回処理済み改行以降だけを読み、表示名を返す。
|
|
216
|
+
*
|
|
217
|
+
* 呼び出し側の stat (size / mtimeMs) は「走査が要るか」の判定にだけ使い、走査範囲と commit 値は
|
|
218
|
+
* open 後の fstat で取り直す (cross-review round2 F2: 古い stat を持った呼び出しが新しい走査の
|
|
219
|
+
* 後に到着すると、古い size まで再走査してキャッシュを巻き戻していた)。進行中の走査を待った
|
|
220
|
+
* 呼び出しは、その走査が fstat 時点の最新を commit しているので再走査せず commit 値を返す。
|
|
221
|
+
*
|
|
222
|
+
* 書き換え検知は「size 減少」と「scannedBytes 直前 256 バイトの fingerprint 不一致」で行う。
|
|
223
|
+
* ⚠️ 既知の限界 (round2 F1 で指摘・意図的に採らない): 末尾 256 バイトを保ったまま前方だけを
|
|
224
|
+
* 書き換える置換は検出できない。jsonl を書くのは Claude CLI だけで、title の変更は新しい
|
|
225
|
+
* custom-title / ai-title 行の追記 (last-wins) として現れ、既存行の置換は行わない。完全検知には
|
|
226
|
+
* 変更のたびに処理済み領域全体の再読込が要り、ハートビート経路の差分キャッシュが成立しなくなる。 */
|
|
227
|
+
async function extractTitleCached(fileMap, filePath, size, mtimeMs) {
|
|
228
|
+
let entry = fileMap.get(filePath)
|
|
229
|
+
if (!entry) {
|
|
230
|
+
entry = { scannedBytes: 0 }
|
|
231
|
+
fileMap.set(filePath, entry)
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (entry.titleScanPromise) {
|
|
235
|
+
// 進行中の走査は fstat 時点の最新状態を commit する。待った側の stat が新しくても古くても、
|
|
236
|
+
// ここで再走査すると (古い stat なら) 巻き戻し、(新しい stat なら) 二重走査になるだけなので
|
|
237
|
+
// commit 値をそのまま返す。走査後に追記された分は次回の listSessions で拾う。
|
|
238
|
+
await entry.titleScanPromise
|
|
239
|
+
return titleFromEntry(fileMap.get(filePath) || entry)
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (size === entry.scannedBytes && mtimeMs === entry.titleMtimeMs) {
|
|
243
|
+
return titleFromEntry(entry)
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const scanPromise = (async () => {
|
|
247
|
+
const previousScannedBytes = typeof entry.scannedBytes === "number" ? entry.scannedBytes : 0
|
|
248
|
+
let scanFrom = previousScannedBytes
|
|
249
|
+
let customTitle = entry.customTitle
|
|
250
|
+
let aiTitle = entry.aiTitle
|
|
251
|
+
let fingerprint = Buffer.isBuffer(entry.titleFingerprint) ? entry.titleFingerprint : Buffer.alloc(0)
|
|
252
|
+
let skippingHugeLine = entry.titleSkippingHugeLine === true
|
|
253
|
+
let handle
|
|
254
|
+
try {
|
|
255
|
+
handle = await open(filePath, "r")
|
|
256
|
+
// 走査範囲と commit 値は呼び出し側の snapshot ではなく open した実体の fstat を使う。
|
|
257
|
+
const actualStat = await handle.stat()
|
|
258
|
+
size = actualStat.size
|
|
259
|
+
mtimeMs = actualStat.mtimeMs
|
|
260
|
+
let reset = size < previousScannedBytes
|
|
261
|
+
if (!reset && previousScannedBytes > 0) {
|
|
262
|
+
const fingerprintStart = Math.max(0, previousScannedBytes - TITLE_FINGERPRINT_SIZE)
|
|
263
|
+
const expectedLength = previousScannedBytes - fingerprintStart
|
|
264
|
+
const actual = Buffer.allocUnsafe(expectedLength)
|
|
265
|
+
const { bytesRead } = await handle.read(actual, 0, expectedLength, fingerprintStart)
|
|
266
|
+
titleScanBytesForTest += bytesRead
|
|
267
|
+
reset = bytesRead !== fingerprint.length
|
|
268
|
+
|| !actual.subarray(0, bytesRead).equals(fingerprint)
|
|
269
|
+
}
|
|
270
|
+
if (reset) {
|
|
271
|
+
scanFrom = 0
|
|
272
|
+
customTitle = undefined
|
|
273
|
+
aiTitle = undefined
|
|
274
|
+
fingerprint = Buffer.alloc(0)
|
|
275
|
+
skippingHugeLine = false
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
let position = scanFrom
|
|
279
|
+
let scannedBytes = scanFrom
|
|
280
|
+
let fingerprintTail = fingerprint
|
|
281
|
+
let carry = Buffer.alloc(0)
|
|
282
|
+
while (position < size) {
|
|
283
|
+
const length = Math.min(TITLE_SCAN_CHUNK_SIZE, size - position)
|
|
284
|
+
const chunk = Buffer.allocUnsafe(length)
|
|
285
|
+
const { bytesRead } = await handle.read(chunk, 0, length, position)
|
|
286
|
+
if (bytesRead === 0) break
|
|
287
|
+
titleScanBytesForTest += bytesRead
|
|
288
|
+
position += bytesRead
|
|
289
|
+
let incoming = chunk.subarray(0, bytesRead)
|
|
290
|
+
if (skippingHugeLine) {
|
|
291
|
+
const newline = incoming.indexOf(NEWLINE_BYTE)
|
|
292
|
+
if (newline < 0) {
|
|
293
|
+
scannedBytes = position
|
|
294
|
+
fingerprintTail = Buffer.concat([fingerprintTail, incoming]).subarray(
|
|
295
|
+
Math.max(0, fingerprintTail.length + incoming.length - TITLE_FINGERPRINT_SIZE),
|
|
296
|
+
)
|
|
297
|
+
continue
|
|
298
|
+
}
|
|
299
|
+
const skippedThroughNewline = incoming.subarray(0, newline + 1)
|
|
300
|
+
fingerprintTail = Buffer.concat([fingerprintTail, skippedThroughNewline]).subarray(
|
|
301
|
+
Math.max(
|
|
302
|
+
0,
|
|
303
|
+
fingerprintTail.length + skippedThroughNewline.length - TITLE_FINGERPRINT_SIZE,
|
|
304
|
+
),
|
|
305
|
+
)
|
|
306
|
+
incoming = incoming.subarray(newline + 1)
|
|
307
|
+
skippingHugeLine = false
|
|
308
|
+
scannedBytes = position - incoming.length
|
|
309
|
+
}
|
|
310
|
+
const region = carry.length
|
|
311
|
+
? Buffer.concat([carry, incoming])
|
|
312
|
+
: incoming
|
|
313
|
+
const lastNewline = region.lastIndexOf(0x0a)
|
|
314
|
+
if (lastNewline < 0) {
|
|
315
|
+
if (region.length > LAST_TURN_MAX_LINE) {
|
|
316
|
+
carry = Buffer.alloc(0)
|
|
317
|
+
skippingHugeLine = true
|
|
318
|
+
scannedBytes = position
|
|
319
|
+
fingerprintTail = region.subarray(Math.max(0, region.length - TITLE_FINGERPRINT_SIZE))
|
|
320
|
+
} else {
|
|
321
|
+
carry = region
|
|
322
|
+
titleMaxCarryBytesForTest = Math.max(titleMaxCarryBytesForTest, carry.length)
|
|
323
|
+
}
|
|
324
|
+
continue
|
|
325
|
+
}
|
|
326
|
+
const complete = region.subarray(0, lastNewline)
|
|
327
|
+
for (const lineBuf of splitLinesBuf(complete)) {
|
|
328
|
+
if (lineBuf.length > LAST_TURN_MAX_LINE) continue
|
|
329
|
+
const titleState = { customTitle, aiTitle }
|
|
330
|
+
takeTitleFromLine(lineBuf.toString("utf-8"), titleState)
|
|
331
|
+
customTitle = titleState.customTitle
|
|
332
|
+
aiTitle = titleState.aiTitle
|
|
333
|
+
}
|
|
334
|
+
carry = region.subarray(lastNewline + 1)
|
|
335
|
+
titleMaxCarryBytesForTest = Math.max(titleMaxCarryBytesForTest, carry.length)
|
|
336
|
+
scannedBytes = position - carry.length
|
|
337
|
+
const consumed = region.subarray(0, lastNewline + 1)
|
|
338
|
+
fingerprintTail = Buffer.concat([fingerprintTail, consumed]).subarray(
|
|
339
|
+
Math.max(0, fingerprintTail.length + consumed.length - TITLE_FINGERPRINT_SIZE),
|
|
340
|
+
)
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const current = fileMap.get(filePath) || {}
|
|
344
|
+
fileMap.set(filePath, {
|
|
345
|
+
...current,
|
|
346
|
+
customTitle,
|
|
347
|
+
aiTitle,
|
|
348
|
+
scannedBytes,
|
|
349
|
+
titleFingerprint: Buffer.from(fingerprintTail),
|
|
350
|
+
titleMtimeMs: mtimeMs,
|
|
351
|
+
titleSkippingHugeLine: skippingHugeLine,
|
|
352
|
+
titleScanPromise: scanPromise,
|
|
353
|
+
})
|
|
354
|
+
} catch {
|
|
355
|
+
// 個別ファイルの read エラーでは一覧自体を失敗させず、前回値を維持する。
|
|
356
|
+
} finally {
|
|
357
|
+
await handle?.close()
|
|
358
|
+
}
|
|
359
|
+
})()
|
|
360
|
+
entry.titleScanPromise = scanPromise
|
|
361
|
+
try {
|
|
362
|
+
await scanPromise
|
|
363
|
+
} finally {
|
|
364
|
+
const current = fileMap.get(filePath)
|
|
365
|
+
if (current?.titleScanPromise === scanPromise) delete current.titleScanPromise
|
|
366
|
+
}
|
|
367
|
+
return titleFromEntry(fileMap.get(filePath) || entry)
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/** テスト用: 呼び出し側 snapshot を偽装して extractTitleCached を直接叩く (round2 F2 の逆順再現)。 */
|
|
371
|
+
export function extractTitleCachedForTest(filePath, size, mtimeMs) {
|
|
372
|
+
return extractTitleCached(getLastTurnDirMap(path.dirname(filePath)), filePath, size, mtimeMs)
|
|
373
|
+
}
|
|
374
|
+
|
|
176
375
|
/** テスト用: last_turn_at_ms キャッシュを全消去する。 */
|
|
177
376
|
export function clearLastTurnCacheForTest() {
|
|
178
377
|
lastTurnDirCache.clear()
|
|
@@ -968,7 +1167,8 @@ export async function listSessions({ cwd, projectsRoot, limit = 30, logger }) {
|
|
|
968
1167
|
const sessions = []
|
|
969
1168
|
for (const s of top) {
|
|
970
1169
|
const preview = await extractPreview(s.file_path)
|
|
971
|
-
|
|
1170
|
+
const title = await extractTitleCached(fileMap, s.file_path, s.size_bytes, s.mtime)
|
|
1171
|
+
sessions.push({ ...s, preview, ...(title === undefined ? {} : { title }) })
|
|
972
1172
|
}
|
|
973
1173
|
return { sessions }
|
|
974
1174
|
}
|
|
@@ -422,6 +422,9 @@ function isSessionRunningEvent(msg) {
|
|
|
422
422
|
* 正しく終端として扱う (ガードが固着して _busy が永久 true になる = T06052 の再発を防ぐ安全弁)。 */
|
|
423
423
|
const STALE_IDLE_GUARD_MS = 5000
|
|
424
424
|
|
|
425
|
+
/** per-message の title control request を待って InputQueue を閉じる上限。 */
|
|
426
|
+
const SESSION_TITLE_CLOSE_WAIT_MS = 15_000
|
|
427
|
+
|
|
425
428
|
/**
|
|
426
429
|
* 1 stream に対応する Claude セッション (per-message query モデル)。
|
|
427
430
|
*/
|
|
@@ -498,6 +501,12 @@ export class ClaudeStreamSession {
|
|
|
498
501
|
|
|
499
502
|
/** 次の query() で resume に使う session_id。各ターンの system/init で更新。 */
|
|
500
503
|
this.sessionId = resumeSessionId || null
|
|
504
|
+
/** fresh resident query の初回プロンプト。query 起動時に一度だけ消費する。 */
|
|
505
|
+
this._pendingTitleSeed = null
|
|
506
|
+
/** 同じ session_id への title control request の二重送信を防ぐ。 */
|
|
507
|
+
this._titleRequestedFor = null
|
|
508
|
+
/** テストで timeout 経路を短縮可能にするが、実運用値は常に 15 秒。 */
|
|
509
|
+
this._sessionTitleCloseWaitMs = SESSION_TITLE_CLOSE_WAIT_MS
|
|
501
510
|
|
|
502
511
|
/** browser 切断中フラグ。true でも query は中断せず継続する (絶対に落とさない)。 */
|
|
503
512
|
this._detached = false
|
|
@@ -1356,6 +1365,7 @@ export class ClaudeStreamSession {
|
|
|
1356
1365
|
// Phase 2 (major#1): resident fresh start は started を出さないため、注入なら source 付きで
|
|
1357
1366
|
// 昇格バブルを作るよう started を先出しする (通常メッセージは no-op)。
|
|
1358
1367
|
this._maybeEmitInjectStarted(prompt)
|
|
1368
|
+
this._pendingTitleSeed = prompt
|
|
1359
1369
|
this._startResidentTurnWithInput(toSDKUserMessage(prompt), ultracode)
|
|
1360
1370
|
return { accepted: true }
|
|
1361
1371
|
}
|
|
@@ -1445,6 +1455,20 @@ export class ClaudeStreamSession {
|
|
|
1445
1455
|
}
|
|
1446
1456
|
// 直前ターンまでの session_id があれば resume チェーン
|
|
1447
1457
|
if (this.sessionId) options.resume = this.sessionId
|
|
1458
|
+
const isFreshStart = !options.resume
|
|
1459
|
+
// title 待ちの後始末 (timer / abort listener / InputQueue close) は try の外に持ち、
|
|
1460
|
+
// 正常完走・init 未到達の異常終端・abort のどの経路でも finally で 1 回だけ冪等に行う
|
|
1461
|
+
// (cross-review F4: init 前に generator が return/reject すると timer と listener が残っていた)。
|
|
1462
|
+
const abortSignal = this._abortController.signal
|
|
1463
|
+
let titleWait = null
|
|
1464
|
+
const finishTitleWait = () => {
|
|
1465
|
+
if (!titleWait) return
|
|
1466
|
+
const { inputQueue, timer } = titleWait
|
|
1467
|
+
titleWait = null
|
|
1468
|
+
if (timer) clearTimeout(timer)
|
|
1469
|
+
abortSignal.removeEventListener("abort", finishTitleWait)
|
|
1470
|
+
inputQueue.close()
|
|
1471
|
+
}
|
|
1448
1472
|
|
|
1449
1473
|
try {
|
|
1450
1474
|
// 改修4 (A): canUseTool は streaming input (AsyncIterable prompt) でのみ確実に機能する。
|
|
@@ -1452,9 +1476,20 @@ export class ClaudeStreamSession {
|
|
|
1452
1476
|
// resume チェーン挙動は維持しつつ、文字列 prompt + canUseTool の SDK 制約リスクを回避する。
|
|
1453
1477
|
const inputQueue = new InputQueue()
|
|
1454
1478
|
inputQueue.push(toSDKUserMessage(prompt))
|
|
1455
|
-
|
|
1456
|
-
const
|
|
1457
|
-
|
|
1479
|
+
const q = this.sdk.query({ prompt: inputQueue, options })
|
|
1480
|
+
const shouldWaitForTitle =
|
|
1481
|
+
isFreshStart && this._canGenerateSessionTitle(q, prompt)
|
|
1482
|
+
if (shouldWaitForTitle) {
|
|
1483
|
+
// 入力を即 close すると title control request が "Query closed before response
|
|
1484
|
+
// received" で失敗する (実測) ため、title settle か上限時間まで close を遅らせる。
|
|
1485
|
+
const timer = setTimeout(finishTitleWait, this._sessionTitleCloseWaitMs)
|
|
1486
|
+
timer.unref?.()
|
|
1487
|
+
titleWait = { inputQueue, timer }
|
|
1488
|
+
abortSignal.addEventListener("abort", finishTitleWait, { once: true })
|
|
1489
|
+
} else {
|
|
1490
|
+
inputQueue.close()
|
|
1491
|
+
}
|
|
1492
|
+
for await (const msg of q) {
|
|
1458
1493
|
if (
|
|
1459
1494
|
msg?.type === "system" &&
|
|
1460
1495
|
msg?.subtype === "init" &&
|
|
@@ -1463,6 +1498,9 @@ export class ClaudeStreamSession {
|
|
|
1463
1498
|
this.sessionId = msg.session_id
|
|
1464
1499
|
// session_id が確定/変化したら watch をその jsonl に張り替える
|
|
1465
1500
|
this._ensureWatch()
|
|
1501
|
+
if (shouldWaitForTitle) {
|
|
1502
|
+
this._requestSessionTitle(q, prompt, msg.session_id).finally(finishTitleWait)
|
|
1503
|
+
}
|
|
1466
1504
|
}
|
|
1467
1505
|
// result イベントでも session_id が来ることがある (念のため拾う)
|
|
1468
1506
|
if (msg?.type === "result") {
|
|
@@ -1490,6 +1528,8 @@ export class ClaudeStreamSession {
|
|
|
1490
1528
|
}
|
|
1491
1529
|
}
|
|
1492
1530
|
} finally {
|
|
1531
|
+
// init 未到達で generator が終端した場合も title 待ちの timer / listener を残さない。
|
|
1532
|
+
finishTitleWait()
|
|
1493
1533
|
this._abortController = null
|
|
1494
1534
|
// 未解決 permission は閉じ、browser の孤児カードも撤去する
|
|
1495
1535
|
this._denyAllPendingPermissions("turn ended")
|
|
@@ -1575,6 +1615,40 @@ export class ClaudeStreamSession {
|
|
|
1575
1615
|
}
|
|
1576
1616
|
}
|
|
1577
1617
|
|
|
1618
|
+
/** title 生成の静的条件。session_id 単位のガードは init 受信後に別途判定する。 */
|
|
1619
|
+
_canGenerateSessionTitle(q, seedText) {
|
|
1620
|
+
const trimmed = seedText.trim()
|
|
1621
|
+
return (
|
|
1622
|
+
process.env.HUB_AGENT_SESSION_TITLE !== "0" &&
|
|
1623
|
+
trimmed.length >= 10 &&
|
|
1624
|
+
!trimmed.startsWith("/") &&
|
|
1625
|
+
typeof q?.generateSessionTitle === "function"
|
|
1626
|
+
)
|
|
1627
|
+
}
|
|
1628
|
+
|
|
1629
|
+
/** title control request。失敗を通常ターンへ伝播させない。 */
|
|
1630
|
+
async _requestSessionTitle(q, seedText, sessionId) {
|
|
1631
|
+
if (
|
|
1632
|
+
this._titleRequestedFor === sessionId ||
|
|
1633
|
+
!this._canGenerateSessionTitle(q, seedText)
|
|
1634
|
+
) {
|
|
1635
|
+
return
|
|
1636
|
+
}
|
|
1637
|
+
this._titleRequestedFor = sessionId
|
|
1638
|
+
try {
|
|
1639
|
+
const result = await q.generateSessionTitle(seedText, { persist: true })
|
|
1640
|
+
this.logger?.info(
|
|
1641
|
+
{ title: result?.title, stream_id: this.stream_id },
|
|
1642
|
+
"session title generated",
|
|
1643
|
+
)
|
|
1644
|
+
} catch (err) {
|
|
1645
|
+
this.logger?.warn(
|
|
1646
|
+
{ err: err.message, stream_id: this.stream_id },
|
|
1647
|
+
"session title generation failed",
|
|
1648
|
+
)
|
|
1649
|
+
}
|
|
1650
|
+
}
|
|
1651
|
+
|
|
1578
1652
|
/** 改修3: pending キューの先頭を取り出して次の per-message ターンを発火する。
|
|
1579
1653
|
* 各ターンの finally から呼ばれ、キューが空になるまで resume チェーンが続く。 */
|
|
1580
1654
|
_drainPending() {
|
|
@@ -2042,6 +2116,8 @@ export class ClaudeStreamSession {
|
|
|
2042
2116
|
// 改修4: 起動時に sessionId (= resumeSessionId) があれば resume チェーンで文脈を引き継ぐ。
|
|
2043
2117
|
// query 起動時点の値のみ有効 (起動後に確定/変化する session_id は同一 query 内で継続される)。
|
|
2044
2118
|
if (this.sessionId) options.resume = this.sessionId
|
|
2119
|
+
const titleSeed = options.resume ? null : this._pendingTitleSeed
|
|
2120
|
+
this._pendingTitleSeed = null
|
|
2045
2121
|
// 空 InputQueue 窓封鎖 (T06118): fresh start の初回ターンが ultracode を要求していれば、
|
|
2046
2122
|
// per-message と同じく起動時 options.settings に焼き込む。こうすることで「query を空 queue
|
|
2047
2123
|
// で先に起動 → applyFlagSettings を await → push」という空窓経路を踏まずに済む。初回だけ
|
|
@@ -2068,6 +2144,9 @@ export class ClaudeStreamSession {
|
|
|
2068
2144
|
typeof msg.session_id === "string"
|
|
2069
2145
|
) {
|
|
2070
2146
|
this.sessionId = msg.session_id
|
|
2147
|
+
if (titleSeed != null) {
|
|
2148
|
+
void this._requestSessionTitle(q, titleSeed, msg.session_id)
|
|
2149
|
+
}
|
|
2071
2150
|
}
|
|
2072
2151
|
if (msg?.type === "result") {
|
|
2073
2152
|
if (typeof msg.session_id === "string") this.sessionId = msg.session_id
|