@cocorograph/hub-agent 0.6.36 → 0.6.38
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/src/claude-stream-bridge.mjs +182 -0
- package/src/main.mjs +83 -0
package/package.json
CHANGED
|
@@ -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」)。
|
|
@@ -988,6 +998,17 @@ class ClaudeStreamSession {
|
|
|
988
998
|
}
|
|
989
999
|
}
|
|
990
1000
|
|
|
1001
|
+
/** このセッションの常駐 query 内で MCP サーバーを再接続する (動作中チャットへ波及)。
|
|
1002
|
+
* 常駐 query が無い (per-message セッション) 場合は false。per-message は毎ターン
|
|
1003
|
+
* 新プロセスで MCP を張り直すため、次メッセージで自動的に再接続される。
|
|
1004
|
+
* @returns {Promise<boolean>} 再接続を発行できたら true */
|
|
1005
|
+
async reconnectMcp(serverName) {
|
|
1006
|
+
const q = this._residentQuery
|
|
1007
|
+
if (!q || typeof q.reconnectMcpServer !== "function") return false
|
|
1008
|
+
await q.reconnectMcpServer(serverName)
|
|
1009
|
+
return true
|
|
1010
|
+
}
|
|
1011
|
+
|
|
991
1012
|
/**
|
|
992
1013
|
* graceful detach: 実行中ターンがあれば中断せず完走を待つ (完走後に finally で
|
|
993
1014
|
* 自動クローズ + onReap)。アイドルなら即クローズする。
|
|
@@ -1052,6 +1073,15 @@ export class ClaudeStreamBridge extends EventEmitter {
|
|
|
1052
1073
|
/** session_id → 生存セッション。再接続 (再アタッチ) で同一セッションを引く索引。
|
|
1053
1074
|
* browser が切れてもここに残し、走行中ターンの継続 + 再接続でのライブ追従を可能にする。 */
|
|
1054
1075
|
this._liveBySession = new Map()
|
|
1076
|
+
|
|
1077
|
+
/** MCP control 専用 query (agent-global)。遅延起動・アイドル破棄。session 非依存。 */
|
|
1078
|
+
this._mcpControlQuery = null
|
|
1079
|
+
/** control query を生かし続ける never-ending input (close で query を畳む)。 */
|
|
1080
|
+
this._mcpControlInput = null
|
|
1081
|
+
/** アイドル破棄タイマー (unref 済み = プロセスを生かし続けない)。 */
|
|
1082
|
+
this._mcpControlIdleTimer = null
|
|
1083
|
+
/** 起動中の Promise (同時アクセスの二重起動防止)。 */
|
|
1084
|
+
this._mcpControlStarting = null
|
|
1055
1085
|
}
|
|
1056
1086
|
|
|
1057
1087
|
/** 多端末共有: あるセッションに紐づく全端末 stream_id を sessions Map から外し、
|
|
@@ -1246,6 +1276,156 @@ export class ClaudeStreamBridge extends EventEmitter {
|
|
|
1246
1276
|
return true
|
|
1247
1277
|
}
|
|
1248
1278
|
|
|
1279
|
+
// ============================================================
|
|
1280
|
+
// MCP control (agent-global, session 非依存) — 2026-06-03
|
|
1281
|
+
//
|
|
1282
|
+
// MCP サーバーは Claude Code 設定 (アカウント単位) で全セッション共有。チャットの
|
|
1283
|
+
// resident / per-message モードに依存せず MCP 状態取得・再認証・再接続を行うため、
|
|
1284
|
+
// チャットセッションとは独立した「control 専用 query」を 1 本だけ遅延起動する。
|
|
1285
|
+
// ユーザーターンは一切送らない (resume なし・空 input) ため、init を抜けた後は
|
|
1286
|
+
// control channel (mcpServerStatus 等) のみを叩く。実機検証済み: 空 input の
|
|
1287
|
+
// control-only query でも mcpServerStatus() が 17 件返ることを確認 (probe 2026-06-03)。
|
|
1288
|
+
// ============================================================
|
|
1289
|
+
|
|
1290
|
+
/** control 専用 query を遅延起動し、起動済みハンドルを返す。二重起動は Promise で防ぐ。 */
|
|
1291
|
+
async _ensureMcpControlQuery() {
|
|
1292
|
+
if (this._mcpControlQuery) {
|
|
1293
|
+
this._armMcpControlIdle()
|
|
1294
|
+
return this._mcpControlQuery
|
|
1295
|
+
}
|
|
1296
|
+
if (this._mcpControlStarting) return this._mcpControlStarting
|
|
1297
|
+
this._mcpControlStarting = (async () => {
|
|
1298
|
+
// never-ending input: close() するまで終わらない (= query を生かし続ける)。
|
|
1299
|
+
// resume を付けないので「空 input + resume」の自動継続バグ窓には入らない。
|
|
1300
|
+
const input = new InputQueue()
|
|
1301
|
+
const q = this.sdk.query({ prompt: input, options: {} })
|
|
1302
|
+
// メッセージストリームを背景で drain する (control 専用なので捨てる)。
|
|
1303
|
+
// control メソッド (mcpServerStatus 等) は別チャネル経由なので drain と独立に動く。
|
|
1304
|
+
;(async () => {
|
|
1305
|
+
try {
|
|
1306
|
+
for await (const _msg of q) {
|
|
1307
|
+
void _msg
|
|
1308
|
+
}
|
|
1309
|
+
} catch (err) {
|
|
1310
|
+
this.logger?.warn(
|
|
1311
|
+
{ err: err?.message },
|
|
1312
|
+
"mcp control query stream ended",
|
|
1313
|
+
)
|
|
1314
|
+
} finally {
|
|
1315
|
+
if (this._mcpControlQuery === q) {
|
|
1316
|
+
this._mcpControlQuery = null
|
|
1317
|
+
this._mcpControlInput = null
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
})()
|
|
1321
|
+
this._mcpControlQuery = q
|
|
1322
|
+
this._mcpControlInput = input
|
|
1323
|
+
return q
|
|
1324
|
+
})()
|
|
1325
|
+
try {
|
|
1326
|
+
return await this._mcpControlStarting
|
|
1327
|
+
} finally {
|
|
1328
|
+
this._mcpControlStarting = null
|
|
1329
|
+
this._armMcpControlIdle()
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
/** アイドル破棄タイマーを張り直す。アクセスのたびに呼ぶ。 */
|
|
1334
|
+
_armMcpControlIdle() {
|
|
1335
|
+
if (this._mcpControlIdleTimer) clearTimeout(this._mcpControlIdleTimer)
|
|
1336
|
+
this._mcpControlIdleTimer = setTimeout(() => {
|
|
1337
|
+
this._teardownMcpControlQuery()
|
|
1338
|
+
}, MCP_CONTROL_IDLE_MS)
|
|
1339
|
+
// プロセスを生かし続けないよう unref (hub-agent の silent-exit 設計と整合)。
|
|
1340
|
+
if (typeof this._mcpControlIdleTimer.unref === "function") {
|
|
1341
|
+
this._mcpControlIdleTimer.unref()
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
/** control 専用 query を畳む (アイドル / shutdown)。 */
|
|
1346
|
+
_teardownMcpControlQuery() {
|
|
1347
|
+
if (this._mcpControlIdleTimer) {
|
|
1348
|
+
clearTimeout(this._mcpControlIdleTimer)
|
|
1349
|
+
this._mcpControlIdleTimer = null
|
|
1350
|
+
}
|
|
1351
|
+
const input = this._mcpControlInput
|
|
1352
|
+
const q = this._mcpControlQuery
|
|
1353
|
+
this._mcpControlInput = null
|
|
1354
|
+
this._mcpControlQuery = null
|
|
1355
|
+
try {
|
|
1356
|
+
input?.close()
|
|
1357
|
+
} catch {
|
|
1358
|
+
/* ignore */
|
|
1359
|
+
}
|
|
1360
|
+
try {
|
|
1361
|
+
q?.interrupt?.()
|
|
1362
|
+
} catch {
|
|
1363
|
+
/* ignore */
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
/** 接続中 MCP サーバーの状態一覧を返す (McpServerStatus[])。 */
|
|
1368
|
+
async mcpStatus() {
|
|
1369
|
+
const q = await this._ensureMcpControlQuery()
|
|
1370
|
+
this._armMcpControlIdle()
|
|
1371
|
+
return await q.mcpServerStatus()
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
/** 指定 MCP サーバーを control 専用 query 内で再接続し、最新状態一覧を返す。 */
|
|
1375
|
+
async reconnectMcp(serverName) {
|
|
1376
|
+
const q = await this._ensureMcpControlQuery()
|
|
1377
|
+
this._armMcpControlIdle()
|
|
1378
|
+
await q.reconnectMcpServer(serverName)
|
|
1379
|
+
return await q.mcpServerStatus()
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
/** 指定 MCP サーバーを「動作中の全 resident セッション」+ control query で再接続する。
|
|
1383
|
+
* MCP 接続はプロセス単位のため、control query だけ張り直しても動作中チャットには
|
|
1384
|
+
* 波及しない。そこで全 resident セッションの常駐 query にも reconnect を発行し、
|
|
1385
|
+
* 会話中の MCP 接続を実際に張り直す。reconnect は冪等なので多重発行は無害。
|
|
1386
|
+
* @returns {Promise<{servers: object[], targeted: number}>}
|
|
1387
|
+
* servers: control query の最新状態 (popover 表示用) / targeted: 波及したセッション数 */
|
|
1388
|
+
async reconnectMcpAllSessions(serverName) {
|
|
1389
|
+
const seen = new Set()
|
|
1390
|
+
let targeted = 0
|
|
1391
|
+
for (const session of this.sessions.values()) {
|
|
1392
|
+
if (seen.has(session)) continue
|
|
1393
|
+
seen.add(session)
|
|
1394
|
+
try {
|
|
1395
|
+
if (await session.reconnectMcp(serverName)) targeted += 1
|
|
1396
|
+
} catch (err) {
|
|
1397
|
+
this.logger?.warn(
|
|
1398
|
+
{ err: err?.message, server: serverName },
|
|
1399
|
+
"session mcp reconnect failed",
|
|
1400
|
+
)
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
// control query も張り直して popover 表示用の最新状態を返す。
|
|
1404
|
+
const servers = await this.reconnectMcp(serverName)
|
|
1405
|
+
return { servers, targeted }
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
/** 指定 MCP サーバーの OAuth 認証を開始する。認可 URL を含む応答を返す。 */
|
|
1409
|
+
async mcpAuthenticate(serverName, redirectUri) {
|
|
1410
|
+
const q = await this._ensureMcpControlQuery()
|
|
1411
|
+
this._armMcpControlIdle()
|
|
1412
|
+
return await q.mcpAuthenticate(serverName, redirectUri)
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
/** ブラウザ認証後のコールバック URL を投入して OAuth を完了する。 */
|
|
1416
|
+
async mcpSubmitOAuthCallback(serverName, callbackUrl) {
|
|
1417
|
+
const q = await this._ensureMcpControlQuery()
|
|
1418
|
+
this._armMcpControlIdle()
|
|
1419
|
+
return await q.mcpSubmitOAuthCallbackUrl(serverName, callbackUrl)
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
/** 指定 MCP サーバーの保存済み認証情報を破棄する (期限切れトークンの掃除)。 */
|
|
1423
|
+
async mcpClearAuth(serverName) {
|
|
1424
|
+
const q = await this._ensureMcpControlQuery()
|
|
1425
|
+
this._armMcpControlIdle()
|
|
1426
|
+
return await q.mcpClearAuth(serverName)
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1249
1429
|
/** キャンセル機能 (0.6.26): browser → 送信待ち (pending) メッセージを id 指定で取り消す。
|
|
1250
1430
|
* 実行中ターンには影響しない (中断は interrupt を使う)。 */
|
|
1251
1431
|
cancelQueued({ stream_id, id }) {
|
|
@@ -1348,6 +1528,8 @@ export class ClaudeStreamBridge extends EventEmitter {
|
|
|
1348
1528
|
}
|
|
1349
1529
|
// 常駐化: 再アタッチ索引も全消去 (プロセス終了時の後始末)。
|
|
1350
1530
|
this._liveBySession.clear()
|
|
1531
|
+
// MCP control 専用 query も畳む。
|
|
1532
|
+
this._teardownMcpControlQuery()
|
|
1351
1533
|
}
|
|
1352
1534
|
|
|
1353
1535
|
/** 現在 attach 中の stream_id 一覧 (debug 用)。 */
|
package/src/main.mjs
CHANGED
|
@@ -965,6 +965,89 @@ 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
|
+
// 動作中の全 resident セッション + control query を再接続 (会話中の MCP に波及)。
|
|
995
|
+
const r = await ctx.claudeBridge.reconnectMcpAllSessions(
|
|
996
|
+
msg.server_name,
|
|
997
|
+
)
|
|
998
|
+
result = { servers: r.servers, targeted: r.targeted }
|
|
999
|
+
break
|
|
1000
|
+
}
|
|
1001
|
+
case "claude.mcp.authenticate":
|
|
1002
|
+
result = {
|
|
1003
|
+
auth: await ctx.claudeBridge.mcpAuthenticate(
|
|
1004
|
+
msg.server_name,
|
|
1005
|
+
msg.redirect_uri,
|
|
1006
|
+
),
|
|
1007
|
+
}
|
|
1008
|
+
break
|
|
1009
|
+
case "claude.mcp.callback": {
|
|
1010
|
+
const auth = await ctx.claudeBridge.mcpSubmitOAuthCallback(
|
|
1011
|
+
msg.server_name,
|
|
1012
|
+
msg.callback_url,
|
|
1013
|
+
)
|
|
1014
|
+
// 再認証で得たトークンはディスクに永続するが、動作中セッションは別プロセス
|
|
1015
|
+
// のため自動では拾わない。全 resident セッション + control query を再接続し、
|
|
1016
|
+
// 会話中もすぐ新トークンで使えるようにする。
|
|
1017
|
+
const r = await ctx.claudeBridge.reconnectMcpAllSessions(
|
|
1018
|
+
msg.server_name,
|
|
1019
|
+
)
|
|
1020
|
+
result = { auth, servers: r.servers, targeted: r.targeted }
|
|
1021
|
+
break
|
|
1022
|
+
}
|
|
1023
|
+
case "claude.mcp.clearauth":
|
|
1024
|
+
result = {
|
|
1025
|
+
cleared: await ctx.claudeBridge.mcpClearAuth(msg.server_name),
|
|
1026
|
+
}
|
|
1027
|
+
break
|
|
1028
|
+
}
|
|
1029
|
+
ctx.client.send({
|
|
1030
|
+
type: "claude.mcp.response",
|
|
1031
|
+
request_id,
|
|
1032
|
+
action: msg.type,
|
|
1033
|
+
server_name: msg.server_name,
|
|
1034
|
+
...result,
|
|
1035
|
+
})
|
|
1036
|
+
} catch (err) {
|
|
1037
|
+
ctx.logger?.warn(
|
|
1038
|
+
{ err: err?.message, action: msg.type, server: msg.server_name },
|
|
1039
|
+
"claude.mcp control request failed",
|
|
1040
|
+
)
|
|
1041
|
+
ctx.client.send({
|
|
1042
|
+
type: "claude.mcp.response",
|
|
1043
|
+
request_id,
|
|
1044
|
+
action: msg.type,
|
|
1045
|
+
server_name: msg.server_name,
|
|
1046
|
+
error: err?.message || "mcp_control_failed",
|
|
1047
|
+
})
|
|
1048
|
+
}
|
|
1049
|
+
return
|
|
1050
|
+
}
|
|
968
1051
|
case "claude.queue.cancel":
|
|
969
1052
|
// 送信待ち (pending) メッセージを id 指定で取り消す (0.6.26)。
|
|
970
1053
|
if (!ctx.claudeBridge) return
|