@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cocorograph/hub-agent",
3
- "version": "0.6.57",
3
+ "version": "0.6.59",
4
4
  "description": "Hub Hosted Cockpit のローカル常駐 agent。Hub と outbound WSS で接続し、ローカルの tmux/pty を中継する。",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",
package/src/main.mjs CHANGED
@@ -10,7 +10,8 @@
10
10
  *
11
11
  * 仕様書: ナレッジ/インフラ/cockpit-hub-hosted-integration-spec (id=6080)
12
12
  */
13
- import { readFileSync, watch as fsWatch } from "node:fs"
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
  }