@cocorograph/hub-agent 0.5.29 → 0.5.31

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
@@ -95,7 +95,15 @@ program
95
95
  agentId: cfg.agent_id,
96
96
  agentToken: cfg.agent_token,
97
97
  })
98
- console.log(`hub bundle synced: version=${r.version} written=${r.written.length} skipped_same=${r.skipped_same.length}`)
98
+ const mcpSummary = r.mcp
99
+ ? ` mcp_added=${r.mcp.added.length}${r.mcp.skipped.length > 0 ? ` mcp_skipped=${r.mcp.skipped.length}` : ""}`
100
+ : ""
101
+ console.log(
102
+ `hub bundle synced: version=${r.version} written=${r.written.length} skipped_same=${r.skipped_same.length}${mcpSummary}`,
103
+ )
104
+ if (r.mcp && r.mcp.added.length > 0) {
105
+ console.log(` mcp servers added: ${r.mcp.added.join(", ")}`)
106
+ }
99
107
  } catch (err) {
100
108
  console.error(`sync-bundle failed: ${err.message}`)
101
109
  process.exit(1)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cocorograph/hub-agent",
3
- "version": "0.5.29",
3
+ "version": "0.5.31",
4
4
  "description": "Hub Hosted Cockpit のローカル常駐 agent。Hub と outbound WSS で接続し、ローカルの tmux/pty を中継する。",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",
@@ -570,10 +570,14 @@ do_enroll() {
570
570
 
571
571
  # enroll 内部の syncBundle 失敗は warning だけで握りつぶされる (enroll 自体は
572
572
  # success 扱い) ため、ここで bundle 展開状況を verify する。
573
- # CLAUDE.md の HUB-AI-RULES マーカーが入っていれば bundle 反映済みと判定。
574
- if [[ ! -f "$HOME/.claude/CLAUDE.md" ]] \
575
- || ! grep -q "BEGIN HUB-AI-RULES" "$HOME/.claude/CLAUDE.md" 2>/dev/null; then
576
- color_warn "Hub AI bundle が ~/.claude に展開されていない可能性。sync-bundle を再試行"
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
- if [[ -f "$HOME/.claude/CLAUDE.md" ]] \
615
- && grep -q "BEGIN HUB-AI-RULES" "$HOME/.claude/CLAUDE.md" 2>/dev/null; then
616
- color_ok "Hub AI bundle 展開済み (~/.claude/CLAUDE.md)"
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
- color_warn "Hub AI bundle ~/.claude に展開されていません"
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
@@ -63,12 +63,41 @@ export async function fetchBundle({ hubUrl, agentId, agentToken, fetchImpl } = {
63
63
  }
64
64
 
65
65
  function isClaudeMdEntry(entry) {
66
- return entry.path === "CLAUDE.md" || entry.path === "./CLAUDE.md"
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
- const cleaned = rel.replace(/^\.\//, "")
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
 
@@ -228,7 +257,89 @@ function runOnce(argv, { logger } = {}) {
228
257
  }
229
258
 
230
259
  /**
231
- * fetch + apply + post_install をまとめて実行。
260
+ * Bundle response `mcp_servers` Claude Code CLI に登録する。
261
+ *
262
+ * Hub backend は user の権限 (is_staff 等) でフィルタした後のリストを返す
263
+ * ため、ここでは届いた全 server を `claude mcp add` する。staff 専用 URL は
264
+ * そもそも一般ユーザーのレスポンスに含まれないので、ここで二重判定する
265
+ * 必要は無い (= server URL を agent 側ロジックに埋め込まない設計)。
266
+ *
267
+ * `claude mcp add --transport <transport> --scope <scope> <name> <url>` を
268
+ * 冪等に呼ぶ。既に同名 server が登録されていれば CLI が「Updated server」
269
+ * メッセージで上書きする (= 何度実行しても安全)。
270
+ *
271
+ * `claude` CLI が PATH に居ない / 未インストール環境ではスキップ。
272
+ * (Cockpit 用途では install.sh が事前に claude をインストールしている
273
+ * 想定だが、独自運用環境への配慮で fail-soft にする)
274
+ */
275
+ export async function applyMcpServers(bundle, { logger } = {}) {
276
+ const servers = Array.isArray(bundle?.mcp_servers) ? bundle.mcp_servers : []
277
+ if (servers.length === 0) return { added: [], skipped: [] }
278
+
279
+ // `claude` CLI の存在確認 (which / where 相当)。なければ no-op。
280
+ const claudeBin = await resolveClaudeBin()
281
+ if (!claudeBin) {
282
+ logger?.info("claude CLI が PATH に無いため MCP server 自動登録をスキップ")
283
+ return { added: [], skipped: servers.map((s) => s.name) }
284
+ }
285
+
286
+ const added = []
287
+ const skipped = []
288
+ for (const s of servers) {
289
+ if (!s?.name || !s?.url) {
290
+ skipped.push(s?.name || "<unnamed>")
291
+ continue
292
+ }
293
+ const transport = s.transport || "http"
294
+ const scope = s.scope || "user"
295
+ const argv = [
296
+ claudeBin,
297
+ "mcp",
298
+ "add",
299
+ "--transport",
300
+ transport,
301
+ "--scope",
302
+ scope,
303
+ s.name,
304
+ s.url,
305
+ ]
306
+ try {
307
+ await runOnce(argv, { logger })
308
+ added.push(s.name)
309
+ } catch (err) {
310
+ // 既存のため fail することはほぼ無い (CLI は冪等)。ログだけ残して継続。
311
+ logger?.warn({ name: s.name, err: err.message }, "claude mcp add failed")
312
+ skipped.push(s.name)
313
+ }
314
+ }
315
+ return { added, skipped }
316
+ }
317
+
318
+ /**
319
+ * `claude` バイナリの絶対パスを返す。なければ null。
320
+ * PATH を辿る (`which` / `where`) ことで fnm / nvm / homebrew / volta 等の
321
+ * バージョンマネージャ配下にあるものも拾う。
322
+ */
323
+ async function resolveClaudeBin() {
324
+ const isWindows = process.platform === "win32"
325
+ const cmd = isWindows ? "where" : "which"
326
+ return new Promise((resolve) => {
327
+ const child = spawn(cmd, ["claude"], { stdio: ["ignore", "pipe", "ignore"] })
328
+ let out = ""
329
+ child.stdout?.on("data", (chunk) => {
330
+ out += chunk.toString("utf-8")
331
+ })
332
+ child.on("exit", (code) => {
333
+ if (code !== 0) return resolve(null)
334
+ const first = out.split(/\r?\n/).map((s) => s.trim()).filter(Boolean)[0]
335
+ resolve(first || null)
336
+ })
337
+ child.on("error", () => resolve(null))
338
+ })
339
+ }
340
+
341
+ /**
342
+ * fetch + apply + post_install + MCP server 自動登録をまとめて実行。
232
343
  */
233
344
  export async function syncBundle({ hubUrl, agentId, agentToken, logger, fetchImpl } = {}) {
234
345
  const bundle = await fetchBundle({ hubUrl, agentId, agentToken, fetchImpl })
@@ -238,5 +349,11 @@ export async function syncBundle({ hubUrl, agentId, agentToken, logger, fetchImp
238
349
  } catch (err) {
239
350
  logger?.warn({ err: err.message }, "post_install partially failed (continuing)")
240
351
  }
241
- return { version: bundle.version, ...result }
352
+ let mcp = { added: [], skipped: [] }
353
+ try {
354
+ mcp = await applyMcpServers(bundle, { logger })
355
+ } catch (err) {
356
+ logger?.warn({ err: err.message }, "applyMcpServers failed (continuing)")
357
+ }
358
+ return { version: bundle.version, ...result, mcp }
242
359
  }