@cocorograph/hub-agent 0.5.28 → 0.5.30
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/scripts/install.sh +35 -9
- package/src/hub-bundle.mjs +31 -2
- package/src/main.mjs +48 -0
- package/src/ws-client.mjs +39 -16
package/package.json
CHANGED
package/scripts/install.sh
CHANGED
|
@@ -570,10 +570,14 @@ do_enroll() {
|
|
|
570
570
|
|
|
571
571
|
# enroll 内部の syncBundle 失敗は warning だけで握りつぶされる (enroll 自体は
|
|
572
572
|
# success 扱い) ため、ここで bundle 展開状況を verify する。
|
|
573
|
-
#
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
573
|
+
# 2 段チェック:
|
|
574
|
+
# - CLAUDE.md の HUB-AI-RULES マーカー (運用ルール本体が入っているか)
|
|
575
|
+
# - scripts/manifest.json の存在 (バンドル本体のファイル群が落ちているか)
|
|
576
|
+
# マーカーは通常 syncBundle の最終ステップで書かれるが、scripts/manifest.json
|
|
577
|
+
# が無いと SessionStart hook / hub-helper.py が動かないため両方必須。どちらかが
|
|
578
|
+
# 欠けていれば半完了状態とみなして sync-bundle を retry。
|
|
579
|
+
if _hub_bundle_incomplete; then
|
|
580
|
+
color_warn "Hub AI bundle が ~/.claude に完全展開されていない可能性。sync-bundle を再試行"
|
|
577
581
|
if ! retry 2 hub-agent sync-bundle; then
|
|
578
582
|
color_warn "hub-agent sync-bundle が継続失敗。あとで手動実行してください:"
|
|
579
583
|
color_warn " hub-agent sync-bundle"
|
|
@@ -581,6 +585,21 @@ do_enroll() {
|
|
|
581
585
|
fi
|
|
582
586
|
}
|
|
583
587
|
|
|
588
|
+
# Hub AI bundle の展開状況を判定する。完全に展開されていれば 0、欠けていれば 1。
|
|
589
|
+
# do_enroll の事後 verify と verify_setup の最終チェックで共通利用する。
|
|
590
|
+
_hub_bundle_incomplete() {
|
|
591
|
+
if [[ ! -f "$HOME/.claude/CLAUDE.md" ]]; then
|
|
592
|
+
return 0
|
|
593
|
+
fi
|
|
594
|
+
if ! grep -q "BEGIN HUB-AI-RULES" "$HOME/.claude/CLAUDE.md" 2>/dev/null; then
|
|
595
|
+
return 0
|
|
596
|
+
fi
|
|
597
|
+
if [[ ! -f "$HOME/.claude/scripts/manifest.json" ]]; then
|
|
598
|
+
return 0
|
|
599
|
+
fi
|
|
600
|
+
return 1
|
|
601
|
+
}
|
|
602
|
+
|
|
584
603
|
do_install_service() {
|
|
585
604
|
color_step "OS サービスとして自動起動を登録"
|
|
586
605
|
# install-service は launchctl bootout/bootstrap の transient で初回失敗する
|
|
@@ -610,12 +629,19 @@ verify_setup() {
|
|
|
610
629
|
color_warn "Claude Code (claude) コマンドが見つかりません (cockpit から claude を呼ぶ場合は必要)"
|
|
611
630
|
fi
|
|
612
631
|
|
|
613
|
-
# 3. Hub AI bundle 展開
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
632
|
+
# 3. Hub AI bundle 展開 (do_enroll と同じ 2 段チェック: CLAUDE.md マーカー +
|
|
633
|
+
# scripts/manifest.json)。CLAUDE.md は書き込まれたが manifest.json が落ちて
|
|
634
|
+
# いない半完了状態も検知する。
|
|
635
|
+
if ! _hub_bundle_incomplete; then
|
|
636
|
+
local mver
|
|
637
|
+
mver="$(grep -o '"version"[[:space:]]*:[[:space:]]*"[^"]*"' "$HOME/.claude/scripts/manifest.json" 2>/dev/null | head -1 | sed 's/.*"\([^"]*\)"$/\1/')"
|
|
638
|
+
color_ok "Hub AI bundle 展開済み (~/.claude/CLAUDE.md + scripts/manifest.json ${mver:+v$mver})"
|
|
617
639
|
else
|
|
618
|
-
|
|
640
|
+
if [[ ! -f "$HOME/.claude/CLAUDE.md" ]] || ! grep -q "BEGIN HUB-AI-RULES" "$HOME/.claude/CLAUDE.md" 2>/dev/null; then
|
|
641
|
+
color_warn "Hub AI bundle が ~/.claude に展開されていません (CLAUDE.md マーカー欠落)"
|
|
642
|
+
else
|
|
643
|
+
color_warn "Hub AI bundle が半完了状態 (~/.claude/scripts/manifest.json 欠落)"
|
|
644
|
+
fi
|
|
619
645
|
color_warn " 手動再同期: hub-agent sync-bundle"
|
|
620
646
|
errors=$((errors + 1))
|
|
621
647
|
fi
|
package/src/hub-bundle.mjs
CHANGED
|
@@ -63,12 +63,41 @@ export async function fetchBundle({ hubUrl, agentId, agentToken, fetchImpl } = {
|
|
|
63
63
|
}
|
|
64
64
|
|
|
65
65
|
function isClaudeMdEntry(entry) {
|
|
66
|
-
return
|
|
66
|
+
return (
|
|
67
|
+
entry.path === "CLAUDE.md" ||
|
|
68
|
+
entry.path === "./CLAUDE.md" ||
|
|
69
|
+
entry.path === "~/.claude/CLAUDE.md"
|
|
70
|
+
)
|
|
67
71
|
}
|
|
68
72
|
|
|
73
|
+
/**
|
|
74
|
+
* backend が返した bundle entry の path 文字列を `~/.claude/` 配下の絶対パスに
|
|
75
|
+
* 展開する。
|
|
76
|
+
*
|
|
77
|
+
* 受け付ける入力形式:
|
|
78
|
+
* - `./scripts/hub_helper.py` — relative (legacy)
|
|
79
|
+
* - `scripts/hub_helper.py` — relative (legacy bare)
|
|
80
|
+
* - `~/.claude/scripts/hub_helper.py` — `~/.claude/` プレフィックス付き (現行)
|
|
81
|
+
*
|
|
82
|
+
* いずれの形式でも `<HOME>/.claude/scripts/hub_helper.py` に解決する。
|
|
83
|
+
*
|
|
84
|
+
* 2026-05-27 までの実装は `~/.claude/` を strip せず `path.join(base, rel)` を
|
|
85
|
+
* 呼んでいたため、`~` がリテラルディレクトリとして作成され
|
|
86
|
+
* `<HOME>/.claude/~/.claude/scripts/...` という二重展開された壊れたパスに
|
|
87
|
+
* ファイルが配置される事故が発生 (新規端末で hub-agent CLI 経由でセットアップ
|
|
88
|
+
* すると、`readBundleVersion()` が正規パスを読めず bundle_version が null に
|
|
89
|
+
* なる症状で顕在化)。
|
|
90
|
+
*/
|
|
69
91
|
function intoClaudeAbsolute(rel) {
|
|
70
92
|
const base = resolveClaudeDir()
|
|
71
|
-
|
|
93
|
+
// `./` / `~/.claude/` の二系統を strip して、純粋な `~/.claude/` 配下の
|
|
94
|
+
// サブパスに正規化してから join する。順序に依存しないよう両方を試す。
|
|
95
|
+
let cleaned = rel.replace(/^\.\//, "")
|
|
96
|
+
cleaned = cleaned.replace(/^~\/\.claude\//, "")
|
|
97
|
+
// 末端で `~/.claude` (末尾スラッシュ無し) を渡された場合のために最終チェック。
|
|
98
|
+
if (cleaned === "~/.claude" || cleaned === "~") {
|
|
99
|
+
cleaned = ""
|
|
100
|
+
}
|
|
72
101
|
return path.join(base, cleaned)
|
|
73
102
|
}
|
|
74
103
|
|
package/src/main.mjs
CHANGED
|
@@ -403,6 +403,45 @@ export async function maybeSyncClaudeSettings(msg, ctx) {
|
|
|
403
403
|
}
|
|
404
404
|
}
|
|
405
405
|
|
|
406
|
+
/**
|
|
407
|
+
* Plan ε: `agent.streams.sync.response` を受けて backend ↔ agent の現存
|
|
408
|
+
* stream_id を能動同期する。
|
|
409
|
+
*
|
|
410
|
+
* - local にあって backend にない → ptyBridge.detach で即時 kill (孤児)
|
|
411
|
+
* - backend にあって local にない → backend に pty.error を通知 (browser に
|
|
412
|
+
* "pty 死んでます" を表示するため)
|
|
413
|
+
*
|
|
414
|
+
* dispatch から呼ぶ用のヘルパーだが、テストから直接叩けるよう export している。
|
|
415
|
+
*/
|
|
416
|
+
export function handleStreamsSyncResponse(msg, ctx) {
|
|
417
|
+
const activeIds = new Set(
|
|
418
|
+
Array.isArray(msg?.active_stream_ids) ? msg.active_stream_ids : [],
|
|
419
|
+
)
|
|
420
|
+
const localIds = new Set(ctx.ptyBridge.list())
|
|
421
|
+
for (const sid of localIds) {
|
|
422
|
+
if (!activeIds.has(sid)) {
|
|
423
|
+
ctx.logger?.warn?.(
|
|
424
|
+
{ stream_id: sid, request_id: msg?.request_id },
|
|
425
|
+
"Plan ε: orphan stream detected, detaching",
|
|
426
|
+
)
|
|
427
|
+
ctx.ptyBridge.detach({ stream_id: sid })
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
for (const sid of activeIds) {
|
|
431
|
+
if (!localIds.has(sid)) {
|
|
432
|
+
ctx.logger?.warn?.(
|
|
433
|
+
{ stream_id: sid, request_id: msg?.request_id },
|
|
434
|
+
"Plan ε: stream missing on agent, notifying backend (pty.error)",
|
|
435
|
+
)
|
|
436
|
+
ctx.client.send({
|
|
437
|
+
type: "pty.error",
|
|
438
|
+
stream_id: sid,
|
|
439
|
+
error: "stream_not_found_on_agent",
|
|
440
|
+
})
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
406
445
|
async function dispatch(msg, ctx) {
|
|
407
446
|
const t = msg?.type || ""
|
|
408
447
|
try {
|
|
@@ -456,6 +495,15 @@ async function dispatch(msg, ctx) {
|
|
|
456
495
|
case "pty.detach":
|
|
457
496
|
ctx.ptyBridge.detach({ stream_id: msg.stream_id })
|
|
458
497
|
return
|
|
498
|
+
case "agent.streams.sync.response":
|
|
499
|
+
// Plan ε: backend ↔ agent の現存 stream_id 能動同期 (WS reconnect 直後に
|
|
500
|
+
// ws-client が agent.streams.sync.request を送って取得した結果)。
|
|
501
|
+
// ロジックは handleStreamsSyncResponse へ抽出 (テストから直接呼べる)。
|
|
502
|
+
// backend が旧 (= Plan ε 未対応) で response が一切来ないケースは何もしない。
|
|
503
|
+
// 5s タイムアウトのような明示的な timer は持たず、再接続時に再度 request
|
|
504
|
+
// が飛ぶので無害。
|
|
505
|
+
handleStreamsSyncResponse(msg, ctx)
|
|
506
|
+
return
|
|
459
507
|
case "tmux.exec": {
|
|
460
508
|
const args = Array.isArray(msg.args) ? msg.args : []
|
|
461
509
|
const hookResult = await runHookChain(ctx.plugins, "interceptTmuxExec", {
|
package/src/ws-client.mjs
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
*
|
|
12
12
|
* 仕様書: ナレッジ/インフラ/cockpit-hub-hosted-integration-spec (id=6080)
|
|
13
13
|
*/
|
|
14
|
+
import { randomUUID } from "node:crypto"
|
|
14
15
|
import { EventEmitter } from "node:events"
|
|
15
16
|
import fs from "node:fs"
|
|
16
17
|
import os from "node:os"
|
|
@@ -68,22 +69,7 @@ export class WsClient extends EventEmitter {
|
|
|
68
69
|
const ws = new WebSocket(wsUrl, { headers })
|
|
69
70
|
this.ws = ws
|
|
70
71
|
|
|
71
|
-
ws.on("open", () =>
|
|
72
|
-
this.backoff = MIN_BACKOFF_MS
|
|
73
|
-
this.logger?.info("ws open")
|
|
74
|
-
// hello 前に最新の bundle version を読み直す (再接続時の鮮度確保)
|
|
75
|
-
this._refreshBundleVersion()
|
|
76
|
-
this._sendJson({
|
|
77
|
-
type: "hello",
|
|
78
|
-
agent_id: this.config.agent_id,
|
|
79
|
-
hostname: this.hostname,
|
|
80
|
-
version: this.version,
|
|
81
|
-
bundle_version: this.bundleVersion,
|
|
82
|
-
})
|
|
83
|
-
this._startHeartbeat()
|
|
84
|
-
this._startBundleWatcher()
|
|
85
|
-
this.emit("open")
|
|
86
|
-
})
|
|
72
|
+
ws.on("open", () => this._onOpen())
|
|
87
73
|
|
|
88
74
|
ws.on("message", (data) => {
|
|
89
75
|
let msg
|
|
@@ -111,6 +97,43 @@ export class WsClient extends EventEmitter {
|
|
|
111
97
|
})
|
|
112
98
|
}
|
|
113
99
|
|
|
100
|
+
/**
|
|
101
|
+
* WS open 時の初期送信 + 後続タスク起動。
|
|
102
|
+
*
|
|
103
|
+
* - backoff をリセット
|
|
104
|
+
* - bundle version を最新化 → hello 送信
|
|
105
|
+
* - Plan ε: agent.streams.sync.request を送信 (毎 reconnect で能動同期)
|
|
106
|
+
* - heartbeat + bundle watcher を起動
|
|
107
|
+
*
|
|
108
|
+
* test からも直接呼べるように method として切り出している (`ws.on("open")`
|
|
109
|
+
* のクロージャだと spy しづらい)。
|
|
110
|
+
*/
|
|
111
|
+
_onOpen() {
|
|
112
|
+
this.backoff = MIN_BACKOFF_MS
|
|
113
|
+
this.logger?.info("ws open")
|
|
114
|
+
// hello 前に最新の bundle version を読み直す (再接続時の鮮度確保)
|
|
115
|
+
this._refreshBundleVersion()
|
|
116
|
+
this._sendJson({
|
|
117
|
+
type: "hello",
|
|
118
|
+
agent_id: this.config.agent_id,
|
|
119
|
+
hostname: this.hostname,
|
|
120
|
+
version: this.version,
|
|
121
|
+
bundle_version: this.bundleVersion,
|
|
122
|
+
})
|
|
123
|
+
// Plan ε: 毎回 reconnect 直後に backend ↔ agent の stream_id を能動同期する
|
|
124
|
+
// (orphan stream の即時 kill 用)。response は main.mjs の dispatch が拾い、
|
|
125
|
+
// ptyBridge.list() と差分を取って kill する。request_id は uuid で
|
|
126
|
+
// 重複排除。backend が古い (= Plan ε 未対応) なら "unknown_type" error が
|
|
127
|
+
// 返るだけで動作には影響しない。
|
|
128
|
+
this._sendJson({
|
|
129
|
+
type: "agent.streams.sync.request",
|
|
130
|
+
request_id: randomUUID(),
|
|
131
|
+
})
|
|
132
|
+
this._startHeartbeat()
|
|
133
|
+
this._startBundleWatcher()
|
|
134
|
+
this.emit("open")
|
|
135
|
+
}
|
|
136
|
+
|
|
114
137
|
/** メッセージを送る。未接続なら no-op (logger.warn)。 */
|
|
115
138
|
send(obj) {
|
|
116
139
|
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|