@cocorograph/hub-agent 0.8.57 → 0.8.58

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.8.57",
3
+ "version": "0.8.58",
4
4
  "description": "Hub Hosted Cockpit のローカル常駐 agent。Hub と outbound WSS で接続し、ローカルの Claude / Codex チャットと tmux セッションを中継する。",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",
package/src/main.mjs CHANGED
@@ -520,9 +520,11 @@ export async function syncTmuxCodexEnv(profile, log) {
520
520
  * bundle hooks (cockpit_permission_bridge 等) を反映させる。
521
521
  *
522
522
  * 新規プロファイル追加 (profile.add) 直後に呼ぶ。provisionProfile は
523
- * `~/.claude/settings.json` を新プロファイルへコピーするが、コピー元が古い / setup 未走の
524
- * タイミングだと新プロファイルにフックが欠けうる。setup_hub_ai は全プロファイル対応 (案1)
525
- * なので 1 回叩けば新プロファイルの初回セッションでもフックが揃う。
523
+ * `~/.claude/settings.json` への symlink を新プロファイルへ張る (コピーではない) ので、
524
+ * 正本が最新ならフックは自動的に揃う。ただし正本自体が古い / setup 未走のタイミングだと
525
+ * 新プロファイルの初回セッションでもフックが欠けうるため、1 回叩いて正本を最新化する。
526
+ * 旧版がコピーで置いた実体 settings.json が残るプロファイルにも、setup_hub_ai は
527
+ * 全プロファイル対応 (案1) なので同じ 1 回で反映される。
526
528
  *
527
529
  * 失敗・未 setup 環境 (スクリプト不在 / python3 無し) でも **例外を投げない**。detached +
528
530
  * stdio ignore で daemon をブロックしない。session_start.py の auto-update と同じ
package/src/profiles.mjs CHANGED
@@ -4,8 +4,12 @@
4
4
  * 1 台のマシンで複数の Claude アカウント (例: 個人 Max / チーム) を `CLAUDE_CONFIG_DIR`
5
5
  * で切り替えて使うための仕組み。各プロファイルは独立した config ディレクトリを持ち、
6
6
  * 認証 (`.credentials.json` / Keychain) と `.claude.json` をアカウント別に保持する。一方で
7
- * skills / agents / commands / scripts / hooks / CLAUDE.md は symlink で共有し、
8
- * settings.json はコピーで共有する (provisionProfile が面倒を見る)。
7
+ * skills / agents / commands / scripts / hooks / CLAUDE.md / settings.json は symlink で
8
+ * 共有する (provisionProfile が面倒を見る)。settings.json はプロファイル間で共有扱いとし、
9
+ * アカウント別の上書きはサポートしない — つまり forceLoginOrgUUID / apiKeyHelper /
10
+ * env.ANTHROPIC_* のようなアカウント固有の値を settings.json に置くと全プロファイルへ効く
11
+ * (経緯と却下案は Hub ナレッジ
12
+ * `ナレッジ/Cockpit/自動コンパクションとPreCompactフック` の「プロファイル間の設定ドリフト」節)。
9
13
  *
10
14
  * 会話履歴 (`projects/`) は **既定 `~/.claude/projects` に集約して全プロファイルで共有する**
11
15
  * (ensureSharedProjects が `<configDir>/projects` を既定への symlink にする)。同じ作業
@@ -312,7 +316,7 @@ function nextNumericId(profiles) {
312
316
 
313
317
  /**
314
318
  * プロファイルを新規追加する (連番 id・中立パス `~/.claude-<n>`)。
315
- * ディレクトリ整備 (symlink / settings コピー) は provisionProfile が行う。
319
+ * ディレクトリ整備 (共有資産の symlink) は provisionProfile が行う。
316
320
  * 追加後もログインは未了 (ユーザーが端末で `/login` する必要がある)。
317
321
  *
318
322
  * @param {{label?: string}} [opts]
@@ -383,7 +387,8 @@ export async function hasFileCredentials(configDir) {
383
387
  }
384
388
  }
385
389
 
386
- /** 既定プロファイルから共有 symlink する資産 (アプリが書き込まない読み取り中心)。 */
390
+ /** 既定プロファイルから共有 symlink する資産。settings.json の書き手 (Claude Code 本体 /
391
+ * setup_hub_ai.py の hooks auto-merge) は symlink 越しに正本 `~/.claude/settings.json` を更新する。 */
387
392
  export const SHARED_SYMLINK_ITEMS = [
388
393
  "agents",
389
394
  "skills",
@@ -391,11 +396,9 @@ export const SHARED_SYMLINK_ITEMS = [
391
396
  "scripts",
392
397
  "hooks",
393
398
  "CLAUDE.md",
399
+ "settings.json",
394
400
  ]
395
401
 
396
- /** コピーで共有する資産 (アプリが書き換えるため symlink 不可)。 */
397
- export const SHARED_COPY_ITEMS = ["settings.json"]
398
-
399
402
  /**
400
403
  * 会話履歴 (`projects/`) を既定 `~/.claude/projects` に集約・共有する。
401
404
  *
@@ -459,18 +462,17 @@ export async function ensureSharedProjects(configDir) {
459
462
 
460
463
  /**
461
464
  * プロファイルディレクトリを整備する。
462
- * - 共有 symlink 資産: 既定 `~/.claude/<item>` への絶対 symlink を張る (既存・実体があれば触らない)。
463
- * - 共有コピー資産: 既定からコピー (プロファイル側に未存在のときだけ)。
465
+ * - 共有 symlink 資産: 既定 `~/.claude/<item>` への絶対 symlink を張る。既存 (symlink でも
466
+ * 実体でも) があれば触らない — 旧版がコピーで置いた settings.json も同じ規則で残る。
464
467
  * 認証・履歴・`.claude.json` には一切触れない (アカウント固有のため)。
465
468
  *
466
469
  * @param {string} configDir - 対象プロファイルの config ディレクトリ絶対パス
467
- * @returns {Promise<{linked: string[], copied: string[], skipped: string[]}>}
470
+ * @returns {Promise<{linked: string[], skipped: string[]}>}
468
471
  */
469
472
  export async function provisionProfile(configDir) {
470
473
  const base = defaultConfigDir()
471
474
  await fs.mkdir(configDir, { recursive: true, mode: 0o700 })
472
475
  const linked = []
473
- const copied = []
474
476
  const skipped = []
475
477
 
476
478
  for (const item of SHARED_SYMLINK_ITEMS) {
@@ -479,8 +481,19 @@ export async function provisionProfile(configDir) {
479
481
  try {
480
482
  await fs.access(src)
481
483
  } catch {
482
- skipped.push(`${item}(元無し)`)
483
- continue
484
+ // settings.json だけは正本を空 JSON で作ってから張る。ここで skip すると、直後の
485
+ // setup_hub_ai (hooks auto-merge) がプロファイル側に実体を作り、以後 provisionProfile が
486
+ // 「既存」として触らないため、コピー時代と同じドリフトが復活する。
487
+ // 既定側が壊れた symlink のときは書き込み先が分からないので触らない。
488
+ if (item !== "settings.json" || (await isBrokenSymlink(src))) {
489
+ skipped.push(`${item}(元無し)`)
490
+ continue
491
+ }
492
+ // 既定ディレクトリ自体が無い端末 (Claude Code 未起動) でも正本を作れるようにする。
493
+ await fs.mkdir(base, { recursive: true, mode: 0o700 })
494
+ await fs.writeFile(src, "{}\n", { flag: "wx" }).catch((err) => {
495
+ if (err?.code !== "EEXIST") throw err
496
+ })
484
497
  }
485
498
  // dst が既に存在 (symlink/実体) なら触らない。
486
499
  let exists = false
@@ -498,28 +511,6 @@ export async function provisionProfile(configDir) {
498
511
  linked.push(item)
499
512
  }
500
513
 
501
- for (const item of SHARED_COPY_ITEMS) {
502
- const src = path.join(base, item)
503
- const dst = path.join(configDir, item)
504
- let dstExists = false
505
- try {
506
- await fs.lstat(dst)
507
- dstExists = true
508
- } catch {
509
- /* not exists */
510
- }
511
- if (dstExists) {
512
- skipped.push(`${item}(既存)`)
513
- continue
514
- }
515
- try {
516
- await fs.copyFile(src, dst)
517
- copied.push(item)
518
- } catch {
519
- skipped.push(`${item}(元無し)`)
520
- }
521
- }
522
-
523
514
  // 会話履歴は既定 ~/.claude/projects へ集約して共有する (アカウント間で会話
524
515
  // コンテキストを引き継げるようにするため)。新規は素直に symlink、既存実体は
525
516
  // 無損失マージしてから symlink 化する。
@@ -531,7 +522,16 @@ export async function provisionProfile(configDir) {
531
522
  skipped.push(`projects(失敗:${err?.code || err?.message || "error"})`)
532
523
  }
533
524
 
534
- return { linked, copied, skipped }
525
+ return { linked, skipped }
526
+ }
527
+
528
+ /** `p` が lstat できるのに access できない (= リンク先を失った symlink) なら true。 */
529
+ async function isBrokenSymlink(p) {
530
+ try {
531
+ return (await fs.lstat(p)).isSymbolicLink()
532
+ } catch {
533
+ return false
534
+ }
535
535
  }
536
536
 
537
537
  /** テスト用: モジュールキャッシュを破棄する。 */