@cocorograph/hub-agent 0.6.58 → 0.6.60
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/main.mjs +93 -1
- package/src/tmux.mjs +56 -0
package/package.json
CHANGED
package/src/main.mjs
CHANGED
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
*
|
|
11
11
|
* 仕様書: ナレッジ/インフラ/cockpit-hub-hosted-integration-spec (id=6080)
|
|
12
12
|
*/
|
|
13
|
-
import {
|
|
13
|
+
import { spawn as _childSpawn } from "node:child_process"
|
|
14
|
+
import { existsSync, readFileSync, watch as fsWatch } from "node:fs"
|
|
14
15
|
import { mkdir, readFile, readdir, rename, unlink, writeFile } from "node:fs/promises"
|
|
15
16
|
import { randomUUID } from "node:crypto"
|
|
16
17
|
import os from "node:os"
|
|
@@ -68,6 +69,7 @@ import {
|
|
|
68
69
|
removeWorktree as removeWorktreeDir,
|
|
69
70
|
resumeWithMessage,
|
|
70
71
|
setTmuxGlobalEnv,
|
|
72
|
+
setTuiModel,
|
|
71
73
|
} from "./tmux.mjs"
|
|
72
74
|
import { TuiPermissionBridge } from "./tui-permission-bridge.mjs"
|
|
73
75
|
import { TuiViewerRegistry } from "./tui-viewer-registry.mjs"
|
|
@@ -293,6 +295,57 @@ export async function syncTmuxProfileEnv(profile, log) {
|
|
|
293
295
|
)
|
|
294
296
|
}
|
|
295
297
|
|
|
298
|
+
/**
|
|
299
|
+
* `setup_hub_ai.py --silent` を best-effort でキックし、全プロファイルの settings.json へ
|
|
300
|
+
* bundle hooks (cockpit_permission_bridge 等) を反映させる。
|
|
301
|
+
*
|
|
302
|
+
* 新規プロファイル追加 (profile.add) 直後に呼ぶ。provisionProfile は
|
|
303
|
+
* `~/.claude/settings.json` を新プロファイルへコピーするが、コピー元が古い / setup 未走の
|
|
304
|
+
* タイミングだと新プロファイルにフックが欠けうる。setup_hub_ai は全プロファイル対応 (案1)
|
|
305
|
+
* なので 1 回叩けば新プロファイルの初回セッションでもフックが揃う。
|
|
306
|
+
*
|
|
307
|
+
* 失敗・未 setup 環境 (スクリプト不在 / python3 無し) でも **例外を投げない**。detached +
|
|
308
|
+
* stdio ignore で daemon をブロックしない。session_start.py の auto-update と同じ
|
|
309
|
+
* `--silent` で headless 実行する。
|
|
310
|
+
*
|
|
311
|
+
* @param {{logger?: object, spawnImpl?: Function, existsImpl?: Function, homedir?: string}} [opts]
|
|
312
|
+
* spawnImpl / existsImpl / homedir はテスト用の注入口 (既定は node 標準)。
|
|
313
|
+
* @returns {boolean} spawn を試みたら true、スクリプト不在等でスキップしたら false。
|
|
314
|
+
*/
|
|
315
|
+
export function kickSetupHubAi({
|
|
316
|
+
logger,
|
|
317
|
+
spawnImpl,
|
|
318
|
+
existsImpl,
|
|
319
|
+
homedir,
|
|
320
|
+
} = {}) {
|
|
321
|
+
try {
|
|
322
|
+
const home = homedir || os.homedir()
|
|
323
|
+
const setupScript = path.join(home, ".claude", "scripts", "setup_hub_ai.py")
|
|
324
|
+
const exists = existsImpl || existsSync
|
|
325
|
+
if (!exists(setupScript)) {
|
|
326
|
+
logger?.info?.(
|
|
327
|
+
{ setupScript },
|
|
328
|
+
"kickSetupHubAi: setup_hub_ai.py 不在 - skip",
|
|
329
|
+
)
|
|
330
|
+
return false
|
|
331
|
+
}
|
|
332
|
+
const doSpawn = spawnImpl || _childSpawn
|
|
333
|
+
const child = doSpawn("python3", [setupScript, "--silent"], {
|
|
334
|
+
detached: true,
|
|
335
|
+
stdio: "ignore",
|
|
336
|
+
})
|
|
337
|
+
child?.on?.("error", (e) =>
|
|
338
|
+
logger?.warn?.({ err: e?.message }, "kickSetupHubAi: spawn error"),
|
|
339
|
+
)
|
|
340
|
+
child?.unref?.()
|
|
341
|
+
logger?.info?.({}, "kickSetupHubAi: setup_hub_ai --silent を起動 (hooks 同期)")
|
|
342
|
+
return true
|
|
343
|
+
} catch (e) {
|
|
344
|
+
logger?.warn?.({ err: e?.message }, "kickSetupHubAi: 失敗 (無視)")
|
|
345
|
+
return false
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
296
349
|
export async function startDaemon({ version, ptyModule, claudeSdk } = {}) {
|
|
297
350
|
const config = await readConfig()
|
|
298
351
|
if (!config) {
|
|
@@ -1331,6 +1384,41 @@ async function dispatch(msg, ctx) {
|
|
|
1331
1384
|
})()
|
|
1332
1385
|
return
|
|
1333
1386
|
}
|
|
1387
|
+
case "claude.tui.setModel": {
|
|
1388
|
+
// モデルバッジ選択 → 対話 claude TUI へ `/model <id>` を送ってモデルを切り替える。
|
|
1389
|
+
// 権限循環 (cyclePermission) と同設計: agent が実キーを送出 → 確認プロンプトを確定 →
|
|
1390
|
+
// 全ブラウザへ claude.tui.model を broadcast し、実際に動いているターミナルのモデルを
|
|
1391
|
+
// 正本として全端末に同期する。jsonl の message.model は次の assistant ターンまで更新
|
|
1392
|
+
// されないため、この即時 broadcast で切替直後のバッジズレを解消する。
|
|
1393
|
+
const cwd = typeof msg.cwd === "string" ? msg.cwd : ""
|
|
1394
|
+
const sessionName =
|
|
1395
|
+
typeof msg.session_name === "string" ? msg.session_name : ""
|
|
1396
|
+
if (!sessionName) return
|
|
1397
|
+
// model="" は「デフォルト」= `/model default`。frontend へはそのまま空で返し、
|
|
1398
|
+
// 解決後の実 id は次ターンの jsonl 由来 (message.model) に委ねる。
|
|
1399
|
+
const model = typeof msg.model === "string" ? msg.model : ""
|
|
1400
|
+
;(async () => {
|
|
1401
|
+
try {
|
|
1402
|
+
await setTuiModel(sessionName, model || "default", { logger })
|
|
1403
|
+
ctx.client.send({
|
|
1404
|
+
type: "claude.tui.model",
|
|
1405
|
+
cwd: cwd || undefined,
|
|
1406
|
+
session_name: sessionName,
|
|
1407
|
+
model,
|
|
1408
|
+
})
|
|
1409
|
+
logger.info(
|
|
1410
|
+
{ session: sessionName, cwd, model: model || "(default)" },
|
|
1411
|
+
"tui model switched → notified browser",
|
|
1412
|
+
)
|
|
1413
|
+
} catch (err) {
|
|
1414
|
+
logger.warn(
|
|
1415
|
+
{ err: err?.message, session: sessionName },
|
|
1416
|
+
"claude.tui.setModel failed",
|
|
1417
|
+
)
|
|
1418
|
+
}
|
|
1419
|
+
})()
|
|
1420
|
+
return
|
|
1421
|
+
}
|
|
1334
1422
|
case "claude.tui.probePermission": {
|
|
1335
1423
|
// 読み取り専用の権限モード問い合わせ (cold-load seed)。キーは送らず、ペインを
|
|
1336
1424
|
// 読んで現在の実モードを claude.tui.permission として broadcast するだけ。
|
|
@@ -1795,6 +1883,10 @@ async function dispatch(msg, ctx) {
|
|
|
1795
1883
|
needs_login: created.needsLogin,
|
|
1796
1884
|
login_command: `CLAUDE_CONFIG_DIR=${created.configDir} claude`,
|
|
1797
1885
|
})
|
|
1886
|
+
// 新規プロファイルへ bundle hooks (cockpit_permission_bridge 等) を即時反映する
|
|
1887
|
+
// best-effort キック (詳細は kickSetupHubAi の docstring 参照)。失敗・未 setup
|
|
1888
|
+
// 環境でも profile.add は成功扱い (kickSetupHubAi は例外を投げない)。
|
|
1889
|
+
kickSetupHubAi({ logger: ctx.logger })
|
|
1798
1890
|
} catch (err) {
|
|
1799
1891
|
ctx.client.send({
|
|
1800
1892
|
type: "profile.add.result",
|
package/src/tmux.mjs
CHANGED
|
@@ -865,6 +865,62 @@ export async function cyclePermissionMode(name, opts = {}) {
|
|
|
865
865
|
}
|
|
866
866
|
}
|
|
867
867
|
|
|
868
|
+
/**
|
|
869
|
+
* 対話 claude TUI に `/model <id>` を送ってモデルを切り替える。
|
|
870
|
+
*
|
|
871
|
+
* cockpit のモデルバッジ選択 (claude.tui.setModel) の書込側。権限循環 (cyclePermissionMode)
|
|
872
|
+
* と同じく、frontend は raw pty.data ではなく claude.tui.setModel を送り、agent 側で本関数を
|
|
873
|
+
* 実行 → 全ブラウザへ claude.tui.model を broadcast して全端末を実モデルに揃える。
|
|
874
|
+
*
|
|
875
|
+
* `/model` はフルモデルID をそのまま受理する (起動時 `claude --model <id>` と同仕様)。引数
|
|
876
|
+
* `default` は起動時の既定へ戻す。会話に出力済みの履歴があるとモデル切替時に確認プロンプトが
|
|
877
|
+
* 出るため、Enter を送って本文確定 → 少し待って追い Enter で確認を畳む (素の入力欄では空 Enter
|
|
878
|
+
* となり無害)。copy-mode に入っているとキーが奪われるので先に抜ける。ベストエフォート。
|
|
879
|
+
*
|
|
880
|
+
* @param {string} name tmux セッション名
|
|
881
|
+
* @param {string} modelArg `/model` 引数 (フルモデルID または "default")
|
|
882
|
+
* @param {{logger?:object,tmuxBin?:string}} [opts]
|
|
883
|
+
* @returns {Promise<{ok:boolean, error?:string}>}
|
|
884
|
+
*/
|
|
885
|
+
export async function setTuiModel(name, modelArg, opts = {}) {
|
|
886
|
+
const bin = tmuxBin(opts)
|
|
887
|
+
const arg = String(modelArg || "default").replace(/[\r\n]+/g, " ").trim()
|
|
888
|
+
if (!arg) return { ok: false, error: "empty model arg" }
|
|
889
|
+
try {
|
|
890
|
+
// copy-mode 等に入っているとキーが奪われるので、入っている時だけ抜ける。
|
|
891
|
+
try {
|
|
892
|
+
const { stdout } = await execFileP(bin, [
|
|
893
|
+
"display-message",
|
|
894
|
+
"-p",
|
|
895
|
+
"-t",
|
|
896
|
+
`${name}:`,
|
|
897
|
+
"-F",
|
|
898
|
+
"#{pane_in_mode}",
|
|
899
|
+
])
|
|
900
|
+
if (stdout.trim() === "1") {
|
|
901
|
+
await execFileP(bin, ["send-keys", "-t", name, "-X", "cancel"])
|
|
902
|
+
}
|
|
903
|
+
} catch {
|
|
904
|
+
// pane_in_mode 取得失敗はベストエフォートで無視。
|
|
905
|
+
}
|
|
906
|
+
// `/model <id>` をリテラルで送る (-l でキー名解釈・スラッシュ補完の暴発を避ける)。
|
|
907
|
+
await execFileP(bin, ["send-keys", "-t", name, "-l", `/model ${arg}`])
|
|
908
|
+
await _delay(120)
|
|
909
|
+
// Enter で本文確定 (send-keys の離散イベントなので paste 吸収は起きにくい)。
|
|
910
|
+
await execFileP(bin, ["send-keys", "-t", name, "Enter"])
|
|
911
|
+
// prior output ありの場合に出るモデル切替の確認プロンプトを Enter で畳む。
|
|
912
|
+
await _delay(450)
|
|
913
|
+
await execFileP(bin, ["send-keys", "-t", name, "Enter"])
|
|
914
|
+
return { ok: true }
|
|
915
|
+
} catch (err) {
|
|
916
|
+
opts.logger?.warn(
|
|
917
|
+
{ session: name, model: arg, err: err?.message },
|
|
918
|
+
"setTuiModel failed",
|
|
919
|
+
)
|
|
920
|
+
return { ok: false, error: err?.message || String(err) }
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
|
|
868
924
|
/**
|
|
869
925
|
* 中断キャンセル後の入力欄復旧 (claude.tui.recoverInput / agent >= 0.6.57)。
|
|
870
926
|
*
|