@cocorograph/hub-agent 0.6.40 → 0.6.42

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cocorograph/hub-agent",
3
- "version": "0.6.40",
3
+ "version": "0.6.42",
4
4
  "description": "Hub Hosted Cockpit のローカル常駐 agent。Hub と outbound WSS で接続し、ローカルの tmux/pty を中継する。",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",
@@ -20,6 +20,9 @@
20
20
  */
21
21
  import { EventEmitter } from "node:events"
22
22
  import { randomUUID } from "node:crypto"
23
+ import { readFileSync } from "node:fs"
24
+ import os from "node:os"
25
+ import path from "node:path"
23
26
 
24
27
  import { jsonlPath } from "./claude-history.mjs"
25
28
  import { watchSessionFile } from "./claude-history-watch.mjs"
@@ -65,6 +68,36 @@ const MCP_CONTROL_IDLE_MS = Number(
65
68
  process.env.HUB_AGENT_MCP_CONTROL_IDLE_MS || 5 * 60 * 1000,
66
69
  )
67
70
 
71
+ /** claude が使う設定ディレクトリ (CLAUDE_CONFIG_DIR 優先、無ければ ~/.claude)。 */
72
+ function claudeConfigDir() {
73
+ return process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), ".claude")
74
+ }
75
+
76
+ /**
77
+ * claude の「要認証 (needs-auth)」キャッシュからサーバー名一覧を読む。
78
+ *
79
+ * `<configDir>/mcp-needs-auth-cache.json` は claude が「認証が必要」と判定した MCP
80
+ * サーバー (claude.ai リモートコネクター含む) を記録するファイルで、`{ "<server名>":
81
+ * { timestamp, id? }, ... }` 形式。**一度も接続したことがない** コネクターも含まれる。
82
+ *
83
+ * 長寿命プロセスの control query は claude のコネクター列挙キャッシュ挙動により、
84
+ * never-connected な claude.ai コネクターを `mcpServerStatus()` に列挙しそびれること
85
+ * があるため、このファイルを補完ソースに使う (mcpStatus でマージ)。
86
+ *
87
+ * @returns {string[]} 認証待ちサーバー名の配列 (読めない / 不正なら空配列)
88
+ */
89
+ function readNeedsAuthCacheNames() {
90
+ try {
91
+ const p = path.join(claudeConfigDir(), "mcp-needs-auth-cache.json")
92
+ const obj = JSON.parse(readFileSync(p, "utf8"))
93
+ if (!obj || typeof obj !== "object") return []
94
+ return Object.keys(obj).filter((k) => typeof k === "string" && k.length > 0)
95
+ } catch {
96
+ // ファイル不在 / JSON 破損は無視 (補完なしで従来どおり動く)。
97
+ return []
98
+ }
99
+ }
100
+
68
101
  /** 多端末共有セッション (2026-05-30)。同じ Claude セッション (session_id) を複数端末で
69
102
  * 同時に開いて、どの端末でもライブ表示・入力・許可応答できるようにする。env
70
103
  * HUB_AGENT_CHAT_SHARED="1" で有効化 (デフォルト無効 = 従来の「最後の端末だけが live」)。
@@ -1061,13 +1094,15 @@ export class ClaudeStreamBridge extends EventEmitter {
1061
1094
  * (テストでは `{ query: stubQuery }` を渡す)
1062
1095
  * @param {import('pino').Logger} [opts.logger]
1063
1096
  */
1064
- constructor({ sdk, logger } = {}) {
1097
+ constructor({ sdk, logger, readNeedsAuthCacheNames: readNeedsAuth } = {}) {
1065
1098
  super()
1066
1099
  if (!sdk || typeof sdk.query !== "function") {
1067
1100
  throw new TypeError("ClaudeStreamBridge requires { sdk: { query } }")
1068
1101
  }
1069
1102
  this.sdk = sdk
1070
1103
  this.logger = logger
1104
+ /** needs-auth キャッシュ名読み取り (テスト差し替え用。既定は実ファイル読み込み)。 */
1105
+ this._readNeedsAuthCacheNames = readNeedsAuth || readNeedsAuthCacheNames
1071
1106
  /** @type {Map<string, ClaudeStreamSession>} */
1072
1107
  this.sessions = new Map()
1073
1108
  /** session_id → 生存セッション。再接続 (再アタッチ) で同一セッションを引く索引。
@@ -1364,11 +1399,62 @@ export class ClaudeStreamBridge extends EventEmitter {
1364
1399
  }
1365
1400
  }
1366
1401
 
1367
- /** 接続中 MCP サーバーの状態一覧を返す (McpServerStatus[])。 */
1368
- async mcpStatus() {
1402
+ /** 接続中 MCP サーバーの状態一覧を返す (McpServerStatus[])。
1403
+ *
1404
+ * @param {{ reload?: boolean }} [opts]
1405
+ * reload=true のときは control 専用 query を一度畳んでから作り直す。control プロセスは
1406
+ * 起動時に一度だけ MCP 設定 (ローカル `claude mcp add` / `.claude.json`) を読み込み、
1407
+ * かつ claude.ai のリモートコネクターを取得するため、稼働中プロセスに status を聞き直す
1408
+ * だけでは「後から追加したサーバー」が出てこない。teardown→再生成で新規プロセスに
1409
+ * 設定を読み直させ、ローカル config と claude.ai コネクターの両方を拾えるようにする
1410
+ * (フロントの「再取得」ボタン用)。reload=false は従来どおりの軽量な状態取得。 */
1411
+ async mcpStatus(opts = {}) {
1412
+ if (opts.reload) this._teardownMcpControlQuery()
1369
1413
  const q = await this._ensureMcpControlQuery()
1370
1414
  this._armMcpControlIdle()
1371
- return await q.mcpServerStatus()
1415
+ const servers = await q.mcpServerStatus()
1416
+ return this._mergeNeedsAuthCache(servers)
1417
+ }
1418
+
1419
+ /**
1420
+ * `mcpServerStatus()` の結果に、needs-auth キャッシュにあるが結果へ含まれない
1421
+ * サーバーを `needs-auth` として補完する。
1422
+ *
1423
+ * 背景: 長寿命プロセスの control query は claude のコネクター列挙キャッシュ挙動に
1424
+ * より、「一度も接続したことがない」claude.ai コネクターを `mcpServerStatus()` に
1425
+ * 列挙しそびれることがある (新規プロセスでは列挙される)。その結果 Cockpit の一覧に
1426
+ * 出ず、初回接続を別経路 (ターミナル / claude.ai) で済ませるまで再認証導線が出ない
1427
+ * 問題があった。needs-auth キャッシュは never-connected も記録しているため、これを
1428
+ * マージして「要再認証」で常に一覧へ出し、その場で再認証 → 接続できるようにする。
1429
+ *
1430
+ * @param {Array<{name?: string, status?: string}>} servers - SDK の状態一覧
1431
+ * @returns {Array<{name: string, status: string}>} 補完後の一覧
1432
+ */
1433
+ _mergeNeedsAuthCache(servers) {
1434
+ const list = Array.isArray(servers) ? servers.slice() : []
1435
+ try {
1436
+ const known = new Set(list.map((s) => s?.name).filter(Boolean))
1437
+ let added = 0
1438
+ for (const name of this._readNeedsAuthCacheNames()) {
1439
+ if (!name || known.has(name)) continue
1440
+ // SDK が列挙しなかった未接続コネクター。要再認証として補完する。
1441
+ list.push({ name, status: "needs-auth" })
1442
+ known.add(name)
1443
+ added += 1
1444
+ }
1445
+ if (added > 0) {
1446
+ this.logger?.info(
1447
+ { added },
1448
+ "mcp status: surfaced never-connected servers from needs-auth cache",
1449
+ )
1450
+ }
1451
+ } catch (err) {
1452
+ this.logger?.warn(
1453
+ { err: err?.message },
1454
+ "mcp needs-auth cache merge failed (ignored)",
1455
+ )
1456
+ }
1457
+ return list
1372
1458
  }
1373
1459
 
1374
1460
  /** 指定 MCP サーバーを control 専用 query 内で再接続し、最新状態一覧を返す。 */
@@ -1376,7 +1462,8 @@ export class ClaudeStreamBridge extends EventEmitter {
1376
1462
  const q = await this._ensureMcpControlQuery()
1377
1463
  this._armMcpControlIdle()
1378
1464
  await q.reconnectMcpServer(serverName)
1379
- return await q.mcpServerStatus()
1465
+ const servers = await q.mcpServerStatus()
1466
+ return this._mergeNeedsAuthCache(servers)
1380
1467
  }
1381
1468
 
1382
1469
  /** 指定 MCP サーバーを「動作中の全 resident セッション」+ control query で再接続する。
package/src/main.mjs CHANGED
@@ -997,7 +997,14 @@ async function dispatch(msg, ctx) {
997
997
  let result
998
998
  switch (msg.type) {
999
999
  case "claude.mcp.status":
1000
- result = { servers: await ctx.claudeBridge.mcpStatus() }
1000
+ // reload=true: control 専用 query を作り直して MCP 設定を読み直す
1001
+ // (後から追加したローカル / claude.ai コネクターを拾うため。フロントの
1002
+ // 「再取得」ボタンがこれを送る)。未指定 / false は従来の軽量取得。
1003
+ result = {
1004
+ servers: await ctx.claudeBridge.mcpStatus({
1005
+ reload: msg.reload === true,
1006
+ }),
1007
+ }
1001
1008
  break
1002
1009
  case "claude.mcp.reconnect": {
1003
1010
  // 動作中の全 resident セッション + control query を再接続 (会話中の MCP に波及)。