@cocorograph/hub-agent 0.7.87 → 0.7.88
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 +99 -20
- package/src/main.mjs +10 -1
- package/src/skills.mjs +94 -3
package/package.json
CHANGED
|
@@ -764,13 +764,17 @@ class ClaudeStreamSession {
|
|
|
764
764
|
* 常駐 query が異常終了して resume 再起動する際 (_runResidentQuery) に最新値で
|
|
765
765
|
* 再 spawn させるため、および per-message セッションが次ターンの options に反映する
|
|
766
766
|
* ため。
|
|
767
|
-
* -
|
|
768
|
-
*
|
|
767
|
+
* - model / permissionMode は起動済み常駐 query の SDK 制御メソッド (setModel /
|
|
768
|
+
* setPermissionMode) を呼び、プロセス再起動なしで次ターンから即反映する
|
|
769
769
|
* (公式 streaming input mode のランタイム制御。stdin に control_request を流す)。
|
|
770
|
+
* - maxThinkingTokens(思考 budget) / effort はランタイム setter が無い (SDK の
|
|
771
|
+
* setMaxThinkingTokens は @deprecated、setThinking/setEffort は未提供) ため、
|
|
772
|
+
* 保持値の更新のみ行い restart-on-change で反映する (T06119)。per-message は次
|
|
773
|
+
* ターンの options 構築で即反映、常駐 query は次 spawn (resume 再起動) で反映。
|
|
770
774
|
*
|
|
771
775
|
* 値が undefined のキーは「変更なし」として無視する (バッジ未送出時に既存値を消さない)。
|
|
772
776
|
* model に空文字/null が来たら setModel(undefined) でデフォルトへ戻す。
|
|
773
|
-
* maxThinkingTokens に 0/null
|
|
777
|
+
* maxThinkingTokens に 0/null が来たら次 spawn 時に budget オフ (thinking 未指定) となる。 */
|
|
774
778
|
applyRuntimeOptions({ model, permissionMode, maxThinkingTokens, effort } = {}) {
|
|
775
779
|
const q = this._residentQuery
|
|
776
780
|
// モデル
|
|
@@ -805,23 +809,18 @@ class ClaudeStreamSession {
|
|
|
805
809
|
}
|
|
806
810
|
}
|
|
807
811
|
}
|
|
808
|
-
// 拡張思考予算
|
|
812
|
+
// 拡張思考予算 (非 effort モデルの固定 budget)。SDK の
|
|
813
|
+
// setMaxThinkingTokens は @deprecated で、公式後継 `thinking` オプションは
|
|
814
|
+
// 構築時専用 (ランタイム setThinking 相当が無い)。そこで effort と同じ
|
|
815
|
+
// restart-on-change に統一し、保持値の更新のみ行う。per-message は次ターンの
|
|
816
|
+
// options 構築時に _applyThinkingOptions が this.maxThinkingTokens を読むため
|
|
817
|
+
// 即反映。常駐 query は次 spawn (resume 再起動) 時に反映される。
|
|
809
818
|
if (maxThinkingTokens !== undefined) {
|
|
810
819
|
const next =
|
|
811
820
|
typeof maxThinkingTokens === "number" && maxThinkingTokens > 0
|
|
812
821
|
? maxThinkingTokens
|
|
813
822
|
: null
|
|
814
|
-
|
|
815
|
-
this.maxThinkingTokens = next
|
|
816
|
-
if (q && typeof q.setMaxThinkingTokens === "function") {
|
|
817
|
-
q.setMaxThinkingTokens(next).catch((err) =>
|
|
818
|
-
this.logger?.warn(
|
|
819
|
-
{ err: err?.message, stream_id: this.stream_id },
|
|
820
|
-
"setMaxThinkingTokens failed",
|
|
821
|
-
),
|
|
822
|
-
)
|
|
823
|
-
}
|
|
824
|
-
}
|
|
823
|
+
this.maxThinkingTokens = next
|
|
825
824
|
}
|
|
826
825
|
// effort (Opus 4.6+)。SDK には setEffort 相当のランタイム制御メソッドが無いため、
|
|
827
826
|
// 保持値の更新のみ行う。per-message セッションは次ターンの options 構築時に
|
|
@@ -858,9 +857,12 @@ class ClaudeStreamSession {
|
|
|
858
857
|
// budget 方式 (maxThinkingTokens) は使わない (Opus 4.7+ で非対応)。
|
|
859
858
|
options.thinking = { type: "adaptive" }
|
|
860
859
|
if (this.effort) options.effort = this.effort
|
|
861
|
-
} else if (this.maxThinkingTokens != null) {
|
|
862
|
-
// 非 effort モデル (Sonnet / Haiku 等)
|
|
863
|
-
|
|
860
|
+
} else if (this.maxThinkingTokens != null && this.maxThinkingTokens > 0) {
|
|
861
|
+
// 非 effort モデル (Sonnet / Haiku 等) は固定 budget 方式を維持するが、
|
|
862
|
+
// SDK 公式後継の `thinking:{type:'enabled',budgetTokens}` で渡す。旧
|
|
863
|
+
// `options.maxThinkingTokens`(@deprecated) は使わない。0/null は何も
|
|
864
|
+
// 設定せず SDK 既定に委ねる (従来と同じ挙動)。
|
|
865
|
+
options.thinking = { type: "enabled", budgetTokens: this.maxThinkingTokens }
|
|
864
866
|
}
|
|
865
867
|
}
|
|
866
868
|
|
|
@@ -1845,6 +1847,8 @@ export class ClaudeStreamBridge extends EventEmitter {
|
|
|
1845
1847
|
this._mcpControlIdleTimer = null
|
|
1846
1848
|
/** 起動中の Promise (同時アクセスの二重起動防止)。 */
|
|
1847
1849
|
this._mcpControlStarting = null
|
|
1850
|
+
/** listSupportedCommands() の短命キャッシュ {at:ms, commands:SlashCommand[]} (T06117)。 */
|
|
1851
|
+
this._supportedCommandsCache = null
|
|
1848
1852
|
}
|
|
1849
1853
|
|
|
1850
1854
|
/** _liveBySession の複合キー (T06271): cwd を含めることで、別 workspace の同一 session_id を
|
|
@@ -2213,8 +2217,24 @@ export class ClaudeStreamBridge extends EventEmitter {
|
|
|
2213
2217
|
// control メソッド (mcpServerStatus 等) は別チャネル経由なので drain と独立に動く。
|
|
2214
2218
|
;(async () => {
|
|
2215
2219
|
try {
|
|
2216
|
-
for await (const
|
|
2217
|
-
|
|
2220
|
+
for await (const msg of q) {
|
|
2221
|
+
// T06117: supportedCommands() は init スナップショットで、以降の動的
|
|
2222
|
+
// 変更 (サブディレクトリでのスキル発見 / plugin reload 等) を反映しない。
|
|
2223
|
+
// 権威的な最新一覧は commands_changed push でしか来ないため、この control
|
|
2224
|
+
// query の stream で拾ってキャッシュを更新する (TTL 再取得では stale 固定)。
|
|
2225
|
+
// ⚠️ 同一性ガード (codex-review v3): teardown → 新 query 起動後に、畳んだ
|
|
2226
|
+
// 旧 query のバッファ済み / 遅延 commands_changed が到着すると、新 query 由来の
|
|
2227
|
+
// 最新キャッシュを旧一覧で上書きしてしまう。この drain が現アクティブ query の
|
|
2228
|
+
// ものか (this._mcpControlQuery === q) を確認してから更新する (finally と同型)。
|
|
2229
|
+
if (
|
|
2230
|
+
this._mcpControlQuery === q &&
|
|
2231
|
+
msg?.type === "system" &&
|
|
2232
|
+
msg?.subtype === "commands_changed"
|
|
2233
|
+
) {
|
|
2234
|
+
this._supportedCommandsCache = {
|
|
2235
|
+
commands: Array.isArray(msg.commands) ? msg.commands : [],
|
|
2236
|
+
}
|
|
2237
|
+
}
|
|
2218
2238
|
}
|
|
2219
2239
|
} catch (err) {
|
|
2220
2240
|
this.logger?.warn(
|
|
@@ -2225,6 +2245,9 @@ export class ClaudeStreamBridge extends EventEmitter {
|
|
|
2225
2245
|
if (this._mcpControlQuery === q) {
|
|
2226
2246
|
this._mcpControlQuery = null
|
|
2227
2247
|
this._mcpControlInput = null
|
|
2248
|
+
// control query が死んだらキャッシュも破棄する。次回アクセスで新しい
|
|
2249
|
+
// query から取り直す (別 query の init スナップショットに更新される)。
|
|
2250
|
+
this._supportedCommandsCache = null
|
|
2228
2251
|
}
|
|
2229
2252
|
}
|
|
2230
2253
|
})()
|
|
@@ -2262,6 +2285,12 @@ export class ClaudeStreamBridge extends EventEmitter {
|
|
|
2262
2285
|
const q = this._mcpControlQuery
|
|
2263
2286
|
this._mcpControlInput = null
|
|
2264
2287
|
this._mcpControlQuery = null
|
|
2288
|
+
// T06117 (codex-review): teardown は _mcpControlQuery を先に null 化するため、
|
|
2289
|
+
// 後追いで走る drain finally の同一性ガード (this._mcpControlQuery === q) が外れ、
|
|
2290
|
+
// finally 側のキャッシュ破棄が発火しない。ここで直接破棄しないと、idle reap /
|
|
2291
|
+
// reload / shutdown 後も古いキャッシュが残り続け、listSupportedCommands が新しい
|
|
2292
|
+
// query を起動せず stale を返し続ける (ライフタイムキャッシュの永続化バグ)。
|
|
2293
|
+
this._supportedCommandsCache = null
|
|
2265
2294
|
try {
|
|
2266
2295
|
input?.close()
|
|
2267
2296
|
} catch {
|
|
@@ -2324,6 +2353,56 @@ export class ClaudeStreamBridge extends EventEmitter {
|
|
|
2324
2353
|
}
|
|
2325
2354
|
}
|
|
2326
2355
|
|
|
2356
|
+
/**
|
|
2357
|
+
* SDK `supportedCommands()` を control-only query 経由で引き、そのセッションで実際に
|
|
2358
|
+
* ロードされている slash command / skill の権威一覧 (SlashCommand[]) を返す (T06117)。
|
|
2359
|
+
*
|
|
2360
|
+
* hub-agent 既定 cwd の control query を使うため、返るのは **組込コマンド + グローバル
|
|
2361
|
+
* skill (~/.claude/skills) + plugin / dynamic workflow** で、FS 走査では取りこぼす
|
|
2362
|
+
* プラグイン提供・無効化・動的発見スキルも SDK が正しく反映する。project-local skill
|
|
2363
|
+
* (~/hub/projects/<name>/.claude/skills) は control query の cwd 外なので含まれず、
|
|
2364
|
+
* 呼び出し側 (skills.mjs) が FS 走査で補完する。
|
|
2365
|
+
*
|
|
2366
|
+
* キャッシュは control query の **ライフタイム単位**。初回に init スナップショットを
|
|
2367
|
+
* 積み、以降は control query stream の commands_changed push で更新、query 死亡
|
|
2368
|
+
* (idle reap 等) で破棄する (_ensureMcpControlQuery の drain / finally)。
|
|
2369
|
+
* ⚠️ supportedCommands() の再取得は init スナップショットを返すだけで動的更新
|
|
2370
|
+
* (commands_changed) を取り込めない (SDK 仕様) ため、TTL 再取得はしない
|
|
2371
|
+
* (取り込んだ最新一覧を stale スナップショットで上書きしてしまう)。失敗時は null を
|
|
2372
|
+
* 返し、呼び出し側は FS 走査へフォールバックする (SDK 未実装 / 一時エラーでも
|
|
2373
|
+
* SkillBar が空にならない)。
|
|
2374
|
+
*
|
|
2375
|
+
* @returns {Promise<Array<{name:string,description:string,argumentHint?:string,aliases?:string[]}>|null>}
|
|
2376
|
+
*/
|
|
2377
|
+
async listSupportedCommands() {
|
|
2378
|
+
// ライフタイムキャッシュ: 存在すれば (init スナップショット + commands_changed で
|
|
2379
|
+
// 反映済みの最新) をそのまま返す。再取得しない (上記 ⚠️)。
|
|
2380
|
+
if (this._supportedCommandsCache) return this._supportedCommandsCache.commands
|
|
2381
|
+
try {
|
|
2382
|
+
const q = await this._ensureMcpControlQuery()
|
|
2383
|
+
this._armMcpControlIdle()
|
|
2384
|
+
if (typeof q.supportedCommands !== "function") {
|
|
2385
|
+
this.logger?.warn({}, "SDK に supportedCommands() が無い (FS 走査へフォールバック)")
|
|
2386
|
+
return null
|
|
2387
|
+
}
|
|
2388
|
+
const commands = await q.supportedCommands()
|
|
2389
|
+
const arr = Array.isArray(commands) ? commands : []
|
|
2390
|
+
// commands_changed が先に届いてキャッシュを埋めていたら、その最新を尊重する
|
|
2391
|
+
// (init スナップショットで上書きしない)。
|
|
2392
|
+
if (!this._supportedCommandsCache) {
|
|
2393
|
+
this._supportedCommandsCache = { commands: arr }
|
|
2394
|
+
}
|
|
2395
|
+
this.logger?.info({ count: this._supportedCommandsCache.commands.length }, "supported commands loaded from SDK")
|
|
2396
|
+
return this._supportedCommandsCache.commands
|
|
2397
|
+
} catch (err) {
|
|
2398
|
+
this.logger?.warn(
|
|
2399
|
+
{ err: err?.message },
|
|
2400
|
+
"listSupportedCommands failed (skills は FS 走査へフォールバック)",
|
|
2401
|
+
)
|
|
2402
|
+
return null
|
|
2403
|
+
}
|
|
2404
|
+
}
|
|
2405
|
+
|
|
2327
2406
|
/**
|
|
2328
2407
|
* アカウント全体の 5h/7d rate-limit 使用率 + プラン種別を返す (usage.mjs
|
|
2329
2408
|
* recordChatUsageSnapshot に渡す想定)。mcpStatus と同じ control-only query を共有する
|
package/src/main.mjs
CHANGED
|
@@ -5471,9 +5471,18 @@ async function dispatch(msg, ctx) {
|
|
|
5471
5471
|
// Cockpit Codex 両対応 (Stream A3, 2026-07-02): browser が cli_kind を
|
|
5472
5472
|
// 添えれば Codex 向け skills (~/.codex/skills/) を返す。未指定は
|
|
5473
5473
|
// 従来どおり claude 向け。
|
|
5474
|
+
const skillsCliKind = msg.cli_kind === "codex" ? "codex" : "claude"
|
|
5475
|
+
// T06117: claude は SDK supportedCommands() を権威に一覧を作る (組込 +
|
|
5476
|
+
// グローバル skill + plugin/dynamic を FS 走査の取りこぼしなく反映)。
|
|
5477
|
+
// 取れなければ null で listSkills が FS 走査へフォールバックする。
|
|
5478
|
+
let sdkCommands = null
|
|
5479
|
+
if (skillsCliKind === "claude" && ctx.claudeBridge?.listSupportedCommands) {
|
|
5480
|
+
sdkCommands = await ctx.claudeBridge.listSupportedCommands()
|
|
5481
|
+
}
|
|
5474
5482
|
const skills = await listSkills({
|
|
5475
5483
|
sessionName: msg.session_name,
|
|
5476
|
-
cliKind:
|
|
5484
|
+
cliKind: skillsCliKind,
|
|
5485
|
+
sdkCommands,
|
|
5477
5486
|
})
|
|
5478
5487
|
ctx.client.send({
|
|
5479
5488
|
type: "skills.response",
|
package/src/skills.mjs
CHANGED
|
@@ -28,7 +28,12 @@ function skillsDirFor(cliKind) {
|
|
|
28
28
|
return cliKind === "codex" ? CODEX_GLOBAL_SKILLS_DIR : GLOBAL_SKILLS_DIR
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
// Claude Code
|
|
31
|
+
// Claude Code 組み込みスラッシュコマンド。
|
|
32
|
+
// ⚠️ T06117: 通常経路では **SDK supportedCommands() を権威** に組込コマンドを得るため
|
|
33
|
+
// このハードコードは使わない。ここに残すのは「SDK が引けない (未実装 SDK / 一時エラー /
|
|
34
|
+
// 起動直後で control query 未確立) ときの cold-start フォールバック専用」。SDK 実返却の
|
|
35
|
+
// 組込名 (usage / context / reload-skills 等) とは一部ズレるが、フォールバックとして
|
|
36
|
+
// SkillBar を空にしないための安全網。正確な一覧は SDK 側が権威。
|
|
32
37
|
const BUILT_IN_COMMANDS = [
|
|
33
38
|
{ name: "clear", description: "会話履歴をクリア (短期メモリのリセット)", source: "builtin" },
|
|
34
39
|
{ name: "compact", description: "会話を要約して圧縮 (context 節約)", source: "builtin" },
|
|
@@ -145,6 +150,20 @@ async function loadCommandsDir(commandsDir, source) {
|
|
|
145
150
|
*/
|
|
146
151
|
export async function listSkills(opts = {}) {
|
|
147
152
|
const cliKind = opts.cliKind === "codex" ? "codex" : "claude"
|
|
153
|
+
const sessionName = opts.sessionName
|
|
154
|
+
const projectsRoot = opts.projectsRoot || path.join(os.homedir(), "hub", "projects")
|
|
155
|
+
|
|
156
|
+
// SDK 権威経路 (T06117): claude かつ呼び出し側が supportedCommands() の結果
|
|
157
|
+
// (opts.sdkCommands) を渡してきたら、組込 + グローバル skill + plugin / dynamic
|
|
158
|
+
// workflow を **SDK 一覧** から構築する。FS 走査では取りこぼすプラグイン提供・
|
|
159
|
+
// 無効化・動的発見スキルも SDK が正しく反映する。label_ja は SDK が持たないため
|
|
160
|
+
// FS の frontmatter から name でオーバーレイし、control query の cwd 外にある
|
|
161
|
+
// project-local skill は FS 走査で補完する。
|
|
162
|
+
if (cliKind === "claude" && Array.isArray(opts.sdkCommands)) {
|
|
163
|
+
return await listSkillsFromSdk(opts.sdkCommands, sessionName, projectsRoot)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// フォールバック経路: SDK 未取得 / Codex は従来どおり FS 走査 + 組込ハードコード。
|
|
148
167
|
const out = []
|
|
149
168
|
out.push(...(cliKind === "codex" ? CODEX_BUILT_IN_COMMANDS : BUILT_IN_COMMANDS))
|
|
150
169
|
out.push(...(await loadSkillsDir(skillsDirFor(cliKind), "skill")))
|
|
@@ -152,8 +171,6 @@ export async function listSkills(opts = {}) {
|
|
|
152
171
|
out.push(...(await loadCommandsDir(GLOBAL_COMMANDS_DIR, "command")))
|
|
153
172
|
}
|
|
154
173
|
|
|
155
|
-
const sessionName = opts.sessionName
|
|
156
|
-
const projectsRoot = opts.projectsRoot || path.join(os.homedir(), "hub", "projects")
|
|
157
174
|
if (sessionName && /^D\d{5}_/.test(sessionName)) {
|
|
158
175
|
const projectDirName = cliKind === "codex" ? ".codex" : ".claude"
|
|
159
176
|
const projectDir = path.join(projectsRoot, sessionName, projectDirName)
|
|
@@ -173,3 +190,77 @@ export async function listSkills(opts = {}) {
|
|
|
173
190
|
deduped.sort((a, b) => a.name.localeCompare(b.name))
|
|
174
191
|
return deduped
|
|
175
192
|
}
|
|
193
|
+
|
|
194
|
+
/** SDK supportedCommands() の description 末尾に付くソース注記
|
|
195
|
+
* (` (user)` / ` (dynamic workflow)` / ` (resumable with /resume)` 等) を除去する。
|
|
196
|
+
* SDK は末尾の括弧にソースを埋めるため、tooltip 表示用に落とす。 */
|
|
197
|
+
export function stripCommandSourceSuffix(desc) {
|
|
198
|
+
if (!desc) return ""
|
|
199
|
+
return desc.replace(/\s*\([^()]*\)\s*$/, "").trim()
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** description 末尾のソース注記から CockpitSkill.source を導出する。
|
|
203
|
+
* `(user)` → skill / `(project…)` → project-skill / それ以外 (組込・plugin・
|
|
204
|
+
* dynamic workflow・注記なし) → builtin。claude は常に `/` 起動なので source は主に
|
|
205
|
+
* frontend の「builtin 日本語ラベル辞書引き」に使われる (skill は label_ja を持つ)。 */
|
|
206
|
+
export function sourceFromCommandSuffix(desc) {
|
|
207
|
+
const m = (desc || "").match(/\(([^()]*)\)\s*$/)
|
|
208
|
+
const tag = m ? m[1].toLowerCase() : ""
|
|
209
|
+
if (tag === "user") return "skill"
|
|
210
|
+
if (tag.startsWith("project")) return "project-skill"
|
|
211
|
+
return "builtin"
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* SDK supportedCommands() の結果 (SlashCommand[]) を CockpitSkill[] へ変換する。
|
|
216
|
+
* - ソース注記を strip / source を導出
|
|
217
|
+
* - FS の frontmatter (label_ja) を name でオーバーレイ
|
|
218
|
+
* - control query の cwd 外にある project-local skill/command を FS 走査で補完
|
|
219
|
+
* - 同名 dedup / 名前順 sort
|
|
220
|
+
*/
|
|
221
|
+
async function listSkillsFromSdk(sdkCommands, sessionName, projectsRoot) {
|
|
222
|
+
const entries = []
|
|
223
|
+
const seen = new Set()
|
|
224
|
+
for (const c of sdkCommands) {
|
|
225
|
+
if (!c || !c.name || seen.has(c.name)) continue
|
|
226
|
+
seen.add(c.name)
|
|
227
|
+
entries.push({
|
|
228
|
+
name: c.name,
|
|
229
|
+
description: stripCommandSourceSuffix(c.description),
|
|
230
|
+
source: sourceFromCommandSuffix(c.description),
|
|
231
|
+
})
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// label_ja オーバーレイ用の FS 走査 (global) + project-local の補完材料。
|
|
235
|
+
const globalLabelSources = [
|
|
236
|
+
...(await loadSkillsDir(GLOBAL_SKILLS_DIR, "skill")),
|
|
237
|
+
...(await loadCommandsDir(GLOBAL_COMMANDS_DIR, "command")),
|
|
238
|
+
]
|
|
239
|
+
let projectEntries = []
|
|
240
|
+
if (sessionName && /^D\d{5}_/.test(sessionName)) {
|
|
241
|
+
const projectDir = path.join(projectsRoot, sessionName, ".claude")
|
|
242
|
+
projectEntries = [
|
|
243
|
+
...(await loadSkillsDir(path.join(projectDir, "skills"), "project-skill")),
|
|
244
|
+
...(await loadCommandsDir(path.join(projectDir, "commands"), "project-command")),
|
|
245
|
+
]
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const labelMap = new Map()
|
|
249
|
+
for (const s of [...globalLabelSources, ...projectEntries]) {
|
|
250
|
+
if (s.label) labelMap.set(s.name, s.label)
|
|
251
|
+
}
|
|
252
|
+
for (const e of entries) {
|
|
253
|
+
const label = labelMap.get(e.name)
|
|
254
|
+
if (label) e.label = label
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// project-local は control query の cwd 外なので SDK 一覧に無い名前だけ追加する。
|
|
258
|
+
for (const p of projectEntries) {
|
|
259
|
+
if (seen.has(p.name)) continue
|
|
260
|
+
seen.add(p.name)
|
|
261
|
+
entries.push(p)
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
entries.sort((a, b) => a.name.localeCompare(b.name))
|
|
265
|
+
return entries
|
|
266
|
+
}
|