@cocorograph/hub-agent 0.6.35 → 0.6.37

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.35",
3
+ "version": "0.6.37",
4
4
  "description": "Hub Hosted Cockpit のローカル常駐 agent。Hub と outbound WSS で接続し、ローカルの tmux/pty を中継する。",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",
@@ -38,6 +38,11 @@ export function normalizeHistoryEvent(obj) {
38
38
  if (obj.message !== undefined) event.message = obj.message
39
39
  if (obj.subtype !== undefined) event.subtype = obj.subtype
40
40
  if (obj.uuid !== undefined) event.uuid = obj.uuid
41
+ // jsonl 各行の ISO8601 タイムスタンプ。Browser 側でバブル上の時刻表示・日付区切りに
42
+ // 使う。履歴 hydrate と live watch の両経路がこの関数を通るため、ここ 1 箇所で拾えば
43
+ // 両方に時刻が乗る (純粋なライブ SDK stream には timestamp が無く、Browser が受信時刻で
44
+ // 補完する)。
45
+ if (obj.timestamp !== undefined) event.timestamp = obj.timestamp
41
46
  if (obj.session_id !== undefined) event.session_id = obj.session_id
42
47
  else if (obj.sessionId !== undefined) event.session_id = obj.sessionId
43
48
  if (obj.model !== undefined) event.model = obj.model
@@ -55,6 +55,16 @@ const CHAT_RESIDENT_ENABLED = process.env.HUB_AGENT_CHAT_RESIDENT !== "0"
55
55
  const CHAT_RESIDENT_RESUME_ENABLED =
56
56
  process.env.HUB_AGENT_CHAT_RESIDENT_RESUME === "1"
57
57
 
58
+ /** MCP control 専用 query のアイドル破棄時間 (2026-06-03)。MCP サーバーは Claude Code
59
+ * 設定 (アカウント単位) で全セッション共有のため、チャットセッションとは独立した
60
+ * 「control 専用 query」を 1 本だけ遅延起動して状態取得・再認証・再接続を行う。
61
+ * ユーザーターンは一切送らない (resume なし・空 input のまま放置) ため、resume 経路の
62
+ * 自動継続バグ (上記 CHAT_RESIDENT_RESUME_ENABLED 参照) には触れない。最後のアクセスから
63
+ * この時間が経過したら query を畳み、次回アクセスで再起動する。 */
64
+ const MCP_CONTROL_IDLE_MS = Number(
65
+ process.env.HUB_AGENT_MCP_CONTROL_IDLE_MS || 5 * 60 * 1000,
66
+ )
67
+
58
68
  /** 多端末共有セッション (2026-05-30)。同じ Claude セッション (session_id) を複数端末で
59
69
  * 同時に開いて、どの端末でもライブ表示・入力・許可応答できるようにする。env
60
70
  * HUB_AGENT_CHAT_SHARED="1" で有効化 (デフォルト無効 = 従来の「最後の端末だけが live」)。
@@ -1052,6 +1062,15 @@ export class ClaudeStreamBridge extends EventEmitter {
1052
1062
  /** session_id → 生存セッション。再接続 (再アタッチ) で同一セッションを引く索引。
1053
1063
  * browser が切れてもここに残し、走行中ターンの継続 + 再接続でのライブ追従を可能にする。 */
1054
1064
  this._liveBySession = new Map()
1065
+
1066
+ /** MCP control 専用 query (agent-global)。遅延起動・アイドル破棄。session 非依存。 */
1067
+ this._mcpControlQuery = null
1068
+ /** control query を生かし続ける never-ending input (close で query を畳む)。 */
1069
+ this._mcpControlInput = null
1070
+ /** アイドル破棄タイマー (unref 済み = プロセスを生かし続けない)。 */
1071
+ this._mcpControlIdleTimer = null
1072
+ /** 起動中の Promise (同時アクセスの二重起動防止)。 */
1073
+ this._mcpControlStarting = null
1055
1074
  }
1056
1075
 
1057
1076
  /** 多端末共有: あるセッションに紐づく全端末 stream_id を sessions Map から外し、
@@ -1246,6 +1265,130 @@ export class ClaudeStreamBridge extends EventEmitter {
1246
1265
  return true
1247
1266
  }
1248
1267
 
1268
+ // ============================================================
1269
+ // MCP control (agent-global, session 非依存) — 2026-06-03
1270
+ //
1271
+ // MCP サーバーは Claude Code 設定 (アカウント単位) で全セッション共有。チャットの
1272
+ // resident / per-message モードに依存せず MCP 状態取得・再認証・再接続を行うため、
1273
+ // チャットセッションとは独立した「control 専用 query」を 1 本だけ遅延起動する。
1274
+ // ユーザーターンは一切送らない (resume なし・空 input) ため、init を抜けた後は
1275
+ // control channel (mcpServerStatus 等) のみを叩く。実機検証済み: 空 input の
1276
+ // control-only query でも mcpServerStatus() が 17 件返ることを確認 (probe 2026-06-03)。
1277
+ // ============================================================
1278
+
1279
+ /** control 専用 query を遅延起動し、起動済みハンドルを返す。二重起動は Promise で防ぐ。 */
1280
+ async _ensureMcpControlQuery() {
1281
+ if (this._mcpControlQuery) {
1282
+ this._armMcpControlIdle()
1283
+ return this._mcpControlQuery
1284
+ }
1285
+ if (this._mcpControlStarting) return this._mcpControlStarting
1286
+ this._mcpControlStarting = (async () => {
1287
+ // never-ending input: close() するまで終わらない (= query を生かし続ける)。
1288
+ // resume を付けないので「空 input + resume」の自動継続バグ窓には入らない。
1289
+ const input = new InputQueue()
1290
+ const q = this.sdk.query({ prompt: input, options: {} })
1291
+ // メッセージストリームを背景で drain する (control 専用なので捨てる)。
1292
+ // control メソッド (mcpServerStatus 等) は別チャネル経由なので drain と独立に動く。
1293
+ ;(async () => {
1294
+ try {
1295
+ for await (const _msg of q) {
1296
+ void _msg
1297
+ }
1298
+ } catch (err) {
1299
+ this.logger?.warn(
1300
+ { err: err?.message },
1301
+ "mcp control query stream ended",
1302
+ )
1303
+ } finally {
1304
+ if (this._mcpControlQuery === q) {
1305
+ this._mcpControlQuery = null
1306
+ this._mcpControlInput = null
1307
+ }
1308
+ }
1309
+ })()
1310
+ this._mcpControlQuery = q
1311
+ this._mcpControlInput = input
1312
+ return q
1313
+ })()
1314
+ try {
1315
+ return await this._mcpControlStarting
1316
+ } finally {
1317
+ this._mcpControlStarting = null
1318
+ this._armMcpControlIdle()
1319
+ }
1320
+ }
1321
+
1322
+ /** アイドル破棄タイマーを張り直す。アクセスのたびに呼ぶ。 */
1323
+ _armMcpControlIdle() {
1324
+ if (this._mcpControlIdleTimer) clearTimeout(this._mcpControlIdleTimer)
1325
+ this._mcpControlIdleTimer = setTimeout(() => {
1326
+ this._teardownMcpControlQuery()
1327
+ }, MCP_CONTROL_IDLE_MS)
1328
+ // プロセスを生かし続けないよう unref (hub-agent の silent-exit 設計と整合)。
1329
+ if (typeof this._mcpControlIdleTimer.unref === "function") {
1330
+ this._mcpControlIdleTimer.unref()
1331
+ }
1332
+ }
1333
+
1334
+ /** control 専用 query を畳む (アイドル / shutdown)。 */
1335
+ _teardownMcpControlQuery() {
1336
+ if (this._mcpControlIdleTimer) {
1337
+ clearTimeout(this._mcpControlIdleTimer)
1338
+ this._mcpControlIdleTimer = null
1339
+ }
1340
+ const input = this._mcpControlInput
1341
+ const q = this._mcpControlQuery
1342
+ this._mcpControlInput = null
1343
+ this._mcpControlQuery = null
1344
+ try {
1345
+ input?.close()
1346
+ } catch {
1347
+ /* ignore */
1348
+ }
1349
+ try {
1350
+ q?.interrupt?.()
1351
+ } catch {
1352
+ /* ignore */
1353
+ }
1354
+ }
1355
+
1356
+ /** 接続中 MCP サーバーの状態一覧を返す (McpServerStatus[])。 */
1357
+ async mcpStatus() {
1358
+ const q = await this._ensureMcpControlQuery()
1359
+ this._armMcpControlIdle()
1360
+ return await q.mcpServerStatus()
1361
+ }
1362
+
1363
+ /** 指定 MCP サーバーを再接続し、最新状態一覧を返す。 */
1364
+ async reconnectMcp(serverName) {
1365
+ const q = await this._ensureMcpControlQuery()
1366
+ this._armMcpControlIdle()
1367
+ await q.reconnectMcpServer(serverName)
1368
+ return await q.mcpServerStatus()
1369
+ }
1370
+
1371
+ /** 指定 MCP サーバーの OAuth 認証を開始する。認可 URL を含む応答を返す。 */
1372
+ async mcpAuthenticate(serverName, redirectUri) {
1373
+ const q = await this._ensureMcpControlQuery()
1374
+ this._armMcpControlIdle()
1375
+ return await q.mcpAuthenticate(serverName, redirectUri)
1376
+ }
1377
+
1378
+ /** ブラウザ認証後のコールバック URL を投入して OAuth を完了する。 */
1379
+ async mcpSubmitOAuthCallback(serverName, callbackUrl) {
1380
+ const q = await this._ensureMcpControlQuery()
1381
+ this._armMcpControlIdle()
1382
+ return await q.mcpSubmitOAuthCallbackUrl(serverName, callbackUrl)
1383
+ }
1384
+
1385
+ /** 指定 MCP サーバーの保存済み認証情報を破棄する (期限切れトークンの掃除)。 */
1386
+ async mcpClearAuth(serverName) {
1387
+ const q = await this._ensureMcpControlQuery()
1388
+ this._armMcpControlIdle()
1389
+ return await q.mcpClearAuth(serverName)
1390
+ }
1391
+
1249
1392
  /** キャンセル機能 (0.6.26): browser → 送信待ち (pending) メッセージを id 指定で取り消す。
1250
1393
  * 実行中ターンには影響しない (中断は interrupt を使う)。 */
1251
1394
  cancelQueued({ stream_id, id }) {
@@ -1348,6 +1491,8 @@ export class ClaudeStreamBridge extends EventEmitter {
1348
1491
  }
1349
1492
  // 常駐化: 再アタッチ索引も全消去 (プロセス終了時の後始末)。
1350
1493
  this._liveBySession.clear()
1494
+ // MCP control 専用 query も畳む。
1495
+ this._teardownMcpControlQuery()
1351
1496
  }
1352
1497
 
1353
1498
  /** 現在 attach 中の stream_id 一覧 (debug 用)。 */
package/src/main.mjs CHANGED
@@ -965,6 +965,80 @@ async function dispatch(msg, ctx) {
965
965
  if (!ctx.claudeBridge) return
966
966
  ctx.claudeBridge.interrupt({ stream_id: msg.stream_id })
967
967
  return
968
+ case "claude.mcp.status":
969
+ case "claude.mcp.reconnect":
970
+ case "claude.mcp.authenticate":
971
+ case "claude.mcp.callback":
972
+ case "claude.mcp.clearauth": {
973
+ // MCP control (agent-global, session 非依存) — 2026-06-03。
974
+ // MCP サーバーは Claude Code 設定 (アカウント単位) で全セッション共有のため、
975
+ // チャットの resident/per-message モードに依存しない control 専用 query で扱う。
976
+ // request_id で相関 (profile.list と同方式)。失敗しても agent は落とさない。
977
+ const request_id = msg.request_id
978
+ if (!ctx.claudeBridge) {
979
+ ctx.client.send({
980
+ type: "claude.mcp.response",
981
+ request_id,
982
+ action: msg.type,
983
+ error: "stream_mode_unavailable",
984
+ })
985
+ return
986
+ }
987
+ try {
988
+ let result
989
+ switch (msg.type) {
990
+ case "claude.mcp.status":
991
+ result = { servers: await ctx.claudeBridge.mcpStatus() }
992
+ break
993
+ case "claude.mcp.reconnect":
994
+ result = {
995
+ servers: await ctx.claudeBridge.reconnectMcp(msg.server_name),
996
+ }
997
+ break
998
+ case "claude.mcp.authenticate":
999
+ result = {
1000
+ auth: await ctx.claudeBridge.mcpAuthenticate(
1001
+ msg.server_name,
1002
+ msg.redirect_uri,
1003
+ ),
1004
+ }
1005
+ break
1006
+ case "claude.mcp.callback":
1007
+ result = {
1008
+ auth: await ctx.claudeBridge.mcpSubmitOAuthCallback(
1009
+ msg.server_name,
1010
+ msg.callback_url,
1011
+ ),
1012
+ }
1013
+ break
1014
+ case "claude.mcp.clearauth":
1015
+ result = {
1016
+ cleared: await ctx.claudeBridge.mcpClearAuth(msg.server_name),
1017
+ }
1018
+ break
1019
+ }
1020
+ ctx.client.send({
1021
+ type: "claude.mcp.response",
1022
+ request_id,
1023
+ action: msg.type,
1024
+ server_name: msg.server_name,
1025
+ ...result,
1026
+ })
1027
+ } catch (err) {
1028
+ ctx.logger?.warn(
1029
+ { err: err?.message, action: msg.type, server: msg.server_name },
1030
+ "claude.mcp control request failed",
1031
+ )
1032
+ ctx.client.send({
1033
+ type: "claude.mcp.response",
1034
+ request_id,
1035
+ action: msg.type,
1036
+ server_name: msg.server_name,
1037
+ error: err?.message || "mcp_control_failed",
1038
+ })
1039
+ }
1040
+ return
1041
+ }
968
1042
  case "claude.queue.cancel":
969
1043
  // 送信待ち (pending) メッセージを id 指定で取り消す (0.6.26)。
970
1044
  if (!ctx.claudeBridge) return