@cocorograph/hub-agent 0.7.52 → 0.7.53

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.7.52",
3
+ "version": "0.7.53",
4
4
  "description": "Hub Hosted Cockpit のローカル常駐 agent。Hub と outbound WSS で接続し、ローカルの tmux/pty を中継する。",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",
@@ -184,6 +184,20 @@ export function getModelCapability(model) {
184
184
  return _modelCapabilities.get(model) ?? null
185
185
  }
186
186
 
187
+ /** レジストリを Hub 報告用 [{model_name, supports_effort, supports_xhigh_effort}] へ変換する。
188
+ * 第2段 (2026-07-11): backend の apply_capability_report が期待する形。 */
189
+ export function getReportModels() {
190
+ const out = []
191
+ for (const [name, cap] of _modelCapabilities) {
192
+ out.push({
193
+ model_name: name,
194
+ supports_effort: cap.supportsEffort === true,
195
+ supports_xhigh_effort: cap.supportsXhighEffort === true,
196
+ })
197
+ }
198
+ return out
199
+ }
200
+
187
201
  /** テスト用: レジストリをクリアする。 */
188
202
  export function _resetModelCapabilities() {
189
203
  _modelCapabilities.clear()
package/src/main.mjs CHANGED
@@ -24,7 +24,12 @@ import { extractLastAssistantText, pickContextText } from "./extract-paragraph.m
24
24
  import { loadPlugins, runHookBroadcast, runHookChain } from "./plugin-loader.mjs"
25
25
  import { WsClient } from "./ws-client.mjs"
26
26
  import { PtyBridge } from "./pty-bridge.mjs"
27
- import { ClaudeStreamBridge } from "./claude-stream-bridge.mjs"
27
+ import { ClaudeStreamBridge, getReportModels } from "./claude-stream-bridge.mjs"
28
+ import {
29
+ buildCapabilityReport,
30
+ readCodexReport,
31
+ readSdkVersion,
32
+ } from "./model-capability-report.mjs"
28
33
  import { CodexStreamBridge, classifyCodexInputError } from "./codex-stream-bridge.mjs"
29
34
  import { UploadManager } from "./claude-upload.mjs"
30
35
  import { requestSelfUninstall } from "./service-install.mjs"
@@ -1059,12 +1064,44 @@ export async function startDaemon({ version, ptyModule, claudeSdk } = {}) {
1059
1064
  if (claudeBridge) {
1060
1065
  ;(async () => {
1061
1066
  const delaysMs = [0, 3000, 15000]
1067
+ let claudeOk = false
1062
1068
  for (const d of delaysMs) {
1063
1069
  if (d) await new Promise((r) => setTimeout(r, d))
1064
1070
  const n = await claudeBridge.refreshModelCapabilities()
1065
- if (n) return // 1 件でも載れば成功
1071
+ if (n) {
1072
+ claudeOk = true
1073
+ break // 1 件でも載れば成功
1074
+ }
1075
+ }
1076
+ if (!claudeOk) {
1077
+ logger.warn({}, "model capability を起動フェーズで取得できず (browser effort へフォールバック)")
1078
+ }
1079
+
1080
+ // 第2段 (2026-07-11): 取得した能力を Hub へ報告する。backend が is_staff ゲート+
1081
+ // バージョン単調ラチェットでモデルマスタの能力列へ反映する(詳細は
1082
+ // apps/ai_models/capability_sync.py)。claude はレジストリ由来、codex は codex CLI が
1083
+ // model/list をキャッシュした ~/.codex/models_cache.json を読む(案A、ゼロ spawn)。
1084
+ // 送信は fire-and-forget。backend が古い版/非staffなら黙って無視するので害はない。
1085
+ try {
1086
+ const codex = await readCodexReport().catch(() => null)
1087
+ const report = buildCapabilityReport({
1088
+ claudeModels: claudeOk ? getReportModels() : [],
1089
+ sdkVersion: readSdkVersion(),
1090
+ codex,
1091
+ })
1092
+ if (report) {
1093
+ client.send(report)
1094
+ logger.info(
1095
+ {
1096
+ claude: report.claude?.models?.length ?? 0,
1097
+ codex: report.codex?.models?.length ?? 0,
1098
+ },
1099
+ "model capability report を Hub へ送信",
1100
+ )
1101
+ }
1102
+ } catch (err) {
1103
+ logger.warn({ err: err?.message }, "model capability report 送信に失敗")
1066
1104
  }
1067
- logger.warn({}, "model capability を起動フェーズで取得できず (browser effort へフォールバック)")
1068
1105
  })().catch((err) =>
1069
1106
  logger.warn({ err: err?.message }, "model capability 取得ループが例外終了"),
1070
1107
  )
@@ -0,0 +1,118 @@
1
+ /**
2
+ * モデル能力報告の組み立て(第2段, 2026-07-11)。
3
+ *
4
+ * hub-agent が上流(Claude Agent SDK / codex)から取得したモデル能力を、Hub backend の
5
+ * `model.capabilities.report` メッセージ形に整形する。backend は
6
+ * apply_capability_report() でバージョン単調ラチェットにより AIModelConfig の能力列へ反映する。
7
+ *
8
+ * - Claude: SDK `supportedModels()` 由来の能力レジストリ(getReportModels)+ SDK バージョン。
9
+ * - Codex: codex CLI 自身が `model/list` 応答をキャッシュした `~/.codex/models_cache.json` を
10
+ * 読む(案A)。起動ごとに codex app-server を spawn せずに済み、ラチェットに必要な
11
+ * client_version / fetched_at / etag / supported_reasoning_levels を全て持つ。
12
+ * ※ runtime の判定に cache を使うわけではない(それは第1段で直した context% の轍)。
13
+ * ここは「一度きりの報告」の一次データとして codex 自身の materialized view を読むだけ。
14
+ */
15
+ import { promises as fs, readFileSync } from "node:fs"
16
+ import os from "node:os"
17
+ import path from "node:path"
18
+ import { createRequire } from "node:module"
19
+
20
+ /** インストール済み @anthropic-ai/claude-agent-sdk の version を読む。失敗時 null。
21
+ *
22
+ * SDK は `exports` フィールドで `./package.json` サブパスをブロックするため、
23
+ * `require("@anthropic-ai/claude-agent-sdk/package.json")` は
24
+ * ERR_PACKAGE_PATH_NOT_EXPORTED で失敗する。代わりにメインエントリを解決して
25
+ * そのパッケージルート(= dirname)の package.json を fs で直読みする。 */
26
+ export function readSdkVersion() {
27
+ try {
28
+ const require = createRequire(import.meta.url)
29
+ // "." export → 例: .../@anthropic-ai/claude-agent-sdk/sdk.mjs(パッケージルート直下)
30
+ const entry = require.resolve("@anthropic-ai/claude-agent-sdk")
31
+ // sdk.mjs はパッケージルートにあるため dirname がパッケージルート。
32
+ const pkgPath = path.join(path.dirname(entry), "package.json")
33
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf8"))
34
+ return typeof pkg?.version === "string" && pkg.version ? pkg.version : null
35
+ } catch {
36
+ return null
37
+ }
38
+ }
39
+
40
+ function codexModelsCachePath() {
41
+ const dir = process.env.CODEX_DIR || path.join(os.homedir(), ".codex")
42
+ return process.env.HUB_CODEX_MODELS_CACHE || path.join(dir, "models_cache.json")
43
+ }
44
+
45
+ /**
46
+ * codex の models_cache.json を読み、報告ブロックへ整形する。失敗/未存在は null。
47
+ *
48
+ * `supported_reasoning_levels`([{effort, description}, ...])から effort 対応を導出:
49
+ * - supports_effort = low/medium/high 等が 1 つでもあれば true(reasoning あり)
50
+ * - supports_xhigh_effort = effort に "xhigh" を含むか
51
+ * `visibility === "hide"` のモデルはピッカー非表示だが、能力自体は正しいので報告に含める
52
+ * (backend は「マスタに存在する行」しか触らないため、hide モデルが紛れても実害なし)。
53
+ */
54
+ export async function readCodexReport(cachePath = codexModelsCachePath()) {
55
+ let text
56
+ try {
57
+ text = await fs.readFile(cachePath, "utf8")
58
+ } catch {
59
+ return null
60
+ }
61
+ let j
62
+ try {
63
+ j = JSON.parse(text)
64
+ } catch {
65
+ return null
66
+ }
67
+ const version = j?.client_version
68
+ if (typeof version !== "string" || !version) return null
69
+ const rawModels = Array.isArray(j?.models) ? j.models : []
70
+ const models = []
71
+ for (const m of rawModels) {
72
+ const name = m?.slug
73
+ if (typeof name !== "string" || !name) continue
74
+ const levels = Array.isArray(m.supported_reasoning_levels)
75
+ ? m.supported_reasoning_levels
76
+ : []
77
+ const efforts = levels
78
+ .map((x) => (x && typeof x.effort === "string" ? x.effort : null))
79
+ .filter(Boolean)
80
+ models.push({
81
+ model_name: name,
82
+ supports_effort: efforts.length > 0,
83
+ supports_xhigh_effort: efforts.includes("xhigh") || efforts.includes("max"),
84
+ })
85
+ }
86
+ return {
87
+ version,
88
+ fetched_at: typeof j?.fetched_at === "string" ? j.fetched_at : undefined,
89
+ etag: typeof j?.etag === "string" ? j.etag : undefined,
90
+ models,
91
+ }
92
+ }
93
+
94
+ /**
95
+ * 報告メッセージを組み立てる。claude / codex は取得できたものだけ含める。
96
+ * どちらも空なら null を返す(送る意味がない)。
97
+ *
98
+ * @param {object} args
99
+ * @param {Array} args.claudeModels getReportModels() の戻り値(能力レジストリ由来)
100
+ * @param {string|null} args.sdkVersion readSdkVersion()
101
+ * @param {object|null} args.codex readCodexReport() の戻り値
102
+ */
103
+ export function buildCapabilityReport({ claudeModels, sdkVersion, codex }) {
104
+ const msg = { type: "model.capabilities.report" }
105
+ if (sdkVersion && Array.isArray(claudeModels) && claudeModels.length > 0) {
106
+ msg.claude = { version: sdkVersion, models: claudeModels }
107
+ }
108
+ if (codex && codex.version && Array.isArray(codex.models) && codex.models.length > 0) {
109
+ msg.codex = {
110
+ version: codex.version,
111
+ models: codex.models,
112
+ }
113
+ if (codex.fetched_at) msg.codex.fetched_at = codex.fetched_at
114
+ if (codex.etag) msg.codex.etag = codex.etag
115
+ }
116
+ if (!msg.claude && !msg.codex) return null
117
+ return msg
118
+ }