@cocorograph/hub-agent 0.6.57 → 0.6.59
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 +57 -1
- package/src/tui-viewer-registry.mjs +17 -1
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"
|
|
@@ -293,6 +294,57 @@ export async function syncTmuxProfileEnv(profile, log) {
|
|
|
293
294
|
)
|
|
294
295
|
}
|
|
295
296
|
|
|
297
|
+
/**
|
|
298
|
+
* `setup_hub_ai.py --silent` を best-effort でキックし、全プロファイルの settings.json へ
|
|
299
|
+
* bundle hooks (cockpit_permission_bridge 等) を反映させる。
|
|
300
|
+
*
|
|
301
|
+
* 新規プロファイル追加 (profile.add) 直後に呼ぶ。provisionProfile は
|
|
302
|
+
* `~/.claude/settings.json` を新プロファイルへコピーするが、コピー元が古い / setup 未走の
|
|
303
|
+
* タイミングだと新プロファイルにフックが欠けうる。setup_hub_ai は全プロファイル対応 (案1)
|
|
304
|
+
* なので 1 回叩けば新プロファイルの初回セッションでもフックが揃う。
|
|
305
|
+
*
|
|
306
|
+
* 失敗・未 setup 環境 (スクリプト不在 / python3 無し) でも **例外を投げない**。detached +
|
|
307
|
+
* stdio ignore で daemon をブロックしない。session_start.py の auto-update と同じ
|
|
308
|
+
* `--silent` で headless 実行する。
|
|
309
|
+
*
|
|
310
|
+
* @param {{logger?: object, spawnImpl?: Function, existsImpl?: Function, homedir?: string}} [opts]
|
|
311
|
+
* spawnImpl / existsImpl / homedir はテスト用の注入口 (既定は node 標準)。
|
|
312
|
+
* @returns {boolean} spawn を試みたら true、スクリプト不在等でスキップしたら false。
|
|
313
|
+
*/
|
|
314
|
+
export function kickSetupHubAi({
|
|
315
|
+
logger,
|
|
316
|
+
spawnImpl,
|
|
317
|
+
existsImpl,
|
|
318
|
+
homedir,
|
|
319
|
+
} = {}) {
|
|
320
|
+
try {
|
|
321
|
+
const home = homedir || os.homedir()
|
|
322
|
+
const setupScript = path.join(home, ".claude", "scripts", "setup_hub_ai.py")
|
|
323
|
+
const exists = existsImpl || existsSync
|
|
324
|
+
if (!exists(setupScript)) {
|
|
325
|
+
logger?.info?.(
|
|
326
|
+
{ setupScript },
|
|
327
|
+
"kickSetupHubAi: setup_hub_ai.py 不在 - skip",
|
|
328
|
+
)
|
|
329
|
+
return false
|
|
330
|
+
}
|
|
331
|
+
const doSpawn = spawnImpl || _childSpawn
|
|
332
|
+
const child = doSpawn("python3", [setupScript, "--silent"], {
|
|
333
|
+
detached: true,
|
|
334
|
+
stdio: "ignore",
|
|
335
|
+
})
|
|
336
|
+
child?.on?.("error", (e) =>
|
|
337
|
+
logger?.warn?.({ err: e?.message }, "kickSetupHubAi: spawn error"),
|
|
338
|
+
)
|
|
339
|
+
child?.unref?.()
|
|
340
|
+
logger?.info?.({}, "kickSetupHubAi: setup_hub_ai --silent を起動 (hooks 同期)")
|
|
341
|
+
return true
|
|
342
|
+
} catch (e) {
|
|
343
|
+
logger?.warn?.({ err: e?.message }, "kickSetupHubAi: 失敗 (無視)")
|
|
344
|
+
return false
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
296
348
|
export async function startDaemon({ version, ptyModule, claudeSdk } = {}) {
|
|
297
349
|
const config = await readConfig()
|
|
298
350
|
if (!config) {
|
|
@@ -1795,6 +1847,10 @@ async function dispatch(msg, ctx) {
|
|
|
1795
1847
|
needs_login: created.needsLogin,
|
|
1796
1848
|
login_command: `CLAUDE_CONFIG_DIR=${created.configDir} claude`,
|
|
1797
1849
|
})
|
|
1850
|
+
// 新規プロファイルへ bundle hooks (cockpit_permission_bridge 等) を即時反映する
|
|
1851
|
+
// best-effort キック (詳細は kickSetupHubAi の docstring 参照)。失敗・未 setup
|
|
1852
|
+
// 環境でも profile.add は成功扱い (kickSetupHubAi は例外を投げない)。
|
|
1853
|
+
kickSetupHubAi({ logger: ctx.logger })
|
|
1798
1854
|
} catch (err) {
|
|
1799
1855
|
ctx.client.send({
|
|
1800
1856
|
type: "profile.add.result",
|
|
@@ -66,6 +66,8 @@ export class TuiViewerRegistry {
|
|
|
66
66
|
this.ttlSec = ttlSec
|
|
67
67
|
this.logger = logger
|
|
68
68
|
this._sweepTimer = null
|
|
69
|
+
/** atomic write の tmp 名を一意化するための単調増加カウンタ (race 回避)。 */
|
|
70
|
+
this._tmpSeq = 0
|
|
69
71
|
}
|
|
70
72
|
|
|
71
73
|
/** ディレクトリを作成し、起動時の残骸を掃除して周期 sweep を張る。 */
|
|
@@ -131,15 +133,29 @@ export class TuiViewerRegistry {
|
|
|
131
133
|
|
|
132
134
|
/**
|
|
133
135
|
* atomic write (tmp+rename)。書けなくても agent は落とさない。
|
|
136
|
+
*
|
|
137
|
+
* tmp 名は ``<fp>.<pid>.<seq>.tmp`` で **書込ごとに一意化**する。固定 ``<fp>.tmp`` を
|
|
138
|
+
* 使うと、同一マーカー (同 session_id / 同 cwd) へ複数タブ・高頻度ハートビートが並行
|
|
139
|
+
* note() したとき、先勝ちの rename が tmp を移動した後に後発の rename が ENOENT で
|
|
140
|
+
* 失敗する (rename '<fp>.tmp' → '<fp>': no such file)。マーカー自体は勝者が書くので
|
|
141
|
+
* 致命的ではないが、ログを汚し鮮度更新を取りこぼす余地があった。一意 tmp なら各書込が
|
|
142
|
+
* 独立した tmp を rename するため衝突しない。
|
|
143
|
+
*
|
|
134
144
|
* @param {string} fp
|
|
135
145
|
* @param {string} body
|
|
136
146
|
*/
|
|
137
147
|
async _atomicWrite(fp, body) {
|
|
138
|
-
const tmp = `${fp}.tmp`
|
|
148
|
+
const tmp = `${fp}.${process.pid}.${++this._tmpSeq}.tmp`
|
|
139
149
|
try {
|
|
140
150
|
await writeFile(tmp, body)
|
|
141
151
|
await rename(tmp, fp)
|
|
142
152
|
} catch (err) {
|
|
153
|
+
// rename 前に書けた tmp が残りうるので掃除する (sweep は *.json のみ対象)。
|
|
154
|
+
try {
|
|
155
|
+
await unlink(tmp)
|
|
156
|
+
} catch {
|
|
157
|
+
/* 無ければ no-op */
|
|
158
|
+
}
|
|
143
159
|
this.logger?.warn({ err: err?.message, fp }, "viewer marker write failed")
|
|
144
160
|
}
|
|
145
161
|
}
|