botmux 3.18.3 → 3.18.5

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.
Files changed (101) hide show
  1. package/dist/.runtime-build-id +1 -1
  2. package/dist/adapters/hook-installer.d.ts.map +1 -1
  3. package/dist/adapters/hook-installer.js +75 -2
  4. package/dist/adapters/hook-installer.js.map +1 -1
  5. package/dist/cli/current-actor.d.ts.map +1 -1
  6. package/dist/cli/current-actor.js +2 -1
  7. package/dist/cli/current-actor.js.map +1 -1
  8. package/dist/cli/dashboard-endpoint.d.ts.map +1 -1
  9. package/dist/cli/dashboard-endpoint.js +5 -2
  10. package/dist/cli/dashboard-endpoint.js.map +1 -1
  11. package/dist/cli/exact-chat-grant-client.d.ts.map +1 -1
  12. package/dist/cli/exact-chat-grant-client.js +2 -1
  13. package/dist/cli/exact-chat-grant-client.js.map +1 -1
  14. package/dist/cli/vc-agent.d.ts.map +1 -1
  15. package/dist/cli/vc-agent.js +2 -1
  16. package/dist/cli/vc-agent.js.map +1 -1
  17. package/dist/cli.d.ts.map +1 -1
  18. package/dist/cli.js +105 -11
  19. package/dist/cli.js.map +1 -1
  20. package/dist/core/binary-install-shape.d.ts +77 -0
  21. package/dist/core/binary-install-shape.d.ts.map +1 -0
  22. package/dist/core/binary-install-shape.js +117 -0
  23. package/dist/core/binary-install-shape.js.map +1 -0
  24. package/dist/core/binary-self-update.d.ts +82 -0
  25. package/dist/core/binary-self-update.d.ts.map +1 -0
  26. package/dist/core/binary-self-update.js +239 -0
  27. package/dist/core/binary-self-update.js.map +1 -0
  28. package/dist/core/daemon-ipc-auth.d.ts +9 -1
  29. package/dist/core/daemon-ipc-auth.d.ts.map +1 -1
  30. package/dist/core/daemon-ipc-auth.js +14 -3
  31. package/dist/core/daemon-ipc-auth.js.map +1 -1
  32. package/dist/core/loopback-fetch.d.ts +59 -0
  33. package/dist/core/loopback-fetch.d.ts.map +1 -0
  34. package/dist/core/loopback-fetch.js +347 -0
  35. package/dist/core/loopback-fetch.js.map +1 -0
  36. package/dist/core/maintenance.d.ts +88 -1
  37. package/dist/core/maintenance.d.ts.map +1 -1
  38. package/dist/core/maintenance.js +149 -8
  39. package/dist/core/maintenance.js.map +1 -1
  40. package/dist/core/managed-origin-attestation.d.ts.map +1 -1
  41. package/dist/core/managed-origin-attestation.js +2 -1
  42. package/dist/core/managed-origin-attestation.js.map +1 -1
  43. package/dist/core/release-download.d.ts +13 -0
  44. package/dist/core/release-download.d.ts.map +1 -0
  45. package/dist/core/release-download.js +130 -0
  46. package/dist/core/release-download.js.map +1 -0
  47. package/dist/daemon.d.ts.map +1 -1
  48. package/dist/daemon.js +2 -1
  49. package/dist/daemon.js.map +1 -1
  50. package/dist/dashboard/aggregator.d.ts.map +1 -1
  51. package/dist/dashboard/aggregator.js +2 -1
  52. package/dist/dashboard/aggregator.js.map +1 -1
  53. package/dist/dashboard/daemon-internal-client.d.ts.map +1 -1
  54. package/dist/dashboard/daemon-internal-client.js +6 -1
  55. package/dist/dashboard/daemon-internal-client.js.map +1 -1
  56. package/dist/dashboard/groups-action-helpers.d.ts +0 -10
  57. package/dist/dashboard/groups-action-helpers.d.ts.map +1 -1
  58. package/dist/dashboard/groups-action-helpers.js +3 -2
  59. package/dist/dashboard/groups-action-helpers.js.map +1 -1
  60. package/dist/dashboard/hd2d-assets.d.ts +2 -3
  61. package/dist/dashboard/hd2d-assets.d.ts.map +1 -1
  62. package/dist/dashboard/hd2d-assets.js +7 -109
  63. package/dist/dashboard/hd2d-assets.js.map +1 -1
  64. package/dist/dashboard/web/app.d.ts.map +1 -1
  65. package/dist/dashboard/web/app.js +7 -1
  66. package/dist/dashboard/web/app.js.map +1 -1
  67. package/dist/dashboard/web/settings-page.d.ts.map +1 -1
  68. package/dist/dashboard/web/settings-page.js.map +1 -1
  69. package/dist/dashboard-web/app.js +1 -1
  70. package/dist/dashboard.js +122 -14
  71. package/dist/dashboard.js.map +1 -1
  72. package/dist/services/hook-runner.d.ts.map +1 -1
  73. package/dist/services/hook-runner.js +2 -1
  74. package/dist/services/hook-runner.js.map +1 -1
  75. package/dist/services/voice/openai.d.ts.map +1 -1
  76. package/dist/services/voice/openai.js +10 -1
  77. package/dist/services/voice/openai.js.map +1 -1
  78. package/dist/services/worktree-slug-ai.d.ts.map +1 -1
  79. package/dist/services/worktree-slug-ai.js +8 -1
  80. package/dist/services/worktree-slug-ai.js.map +1 -1
  81. package/dist/utils/file-lock.d.ts +25 -0
  82. package/dist/utils/file-lock.d.ts.map +1 -1
  83. package/dist/utils/file-lock.js +34 -4
  84. package/dist/utils/file-lock.js.map +1 -1
  85. package/dist/utils/global-install.d.ts +19 -0
  86. package/dist/utils/global-install.d.ts.map +1 -1
  87. package/dist/utils/global-install.js +28 -1
  88. package/dist/utils/global-install.js.map +1 -1
  89. package/dist/utils/install-info.d.ts +37 -1
  90. package/dist/utils/install-info.d.ts.map +1 -1
  91. package/dist/utils/install-info.js +45 -3
  92. package/dist/utils/install-info.js.map +1 -1
  93. package/dist/workflows/v3/daemon-ipc-client.d.ts.map +1 -1
  94. package/dist/workflows/v3/daemon-ipc-client.js +2 -1
  95. package/dist/workflows/v3/daemon-ipc-client.js.map +1 -1
  96. package/dist/workflows/v3/session-relay-client.d.ts.map +1 -1
  97. package/dist/workflows/v3/session-relay-client.js +2 -1
  98. package/dist/workflows/v3/session-relay-client.js.map +1 -1
  99. package/package.json +8 -7
  100. package/scripts/install-path-entry.mjs +263 -0
  101. package/scripts/postinstall-bin.mjs +41 -7
package/dist/cli.js CHANGED
@@ -56,7 +56,7 @@ import { validateCodexAppManagedSendOrigin, } from './utils/codex-app-dispatch-l
56
56
  import { hasProtectedSessionMutationOwnership } from './core/session-mutation-guard.js';
57
57
  import { logger } from './utils/logger.js';
58
58
  import { reapLegacyPm2, liveGodAt } from './core/legacy-pm2-reaper.js';
59
- import { withFileLock, withFileLockSync } from './utils/file-lock.js';
59
+ import { withFileLock, withFileLockSync, FileLockTimeoutError } from './utils/file-lock.js';
60
60
  import { scheduleTimeZone } from './utils/timezone.js';
61
61
  import { expandHomePath, invalidWorkingDirs } from './utils/working-dir.js';
62
62
  import { firstPositional, hasFlagOrEq } from './cli/arg-utils.js';
@@ -78,7 +78,10 @@ import { isRemoteAccessEnabled } from './global-config.js';
78
78
  import { DASHBOARD_COMMAND_USAGE, executeDashboardCommand, formatDashboardFallbackFailure, formatDashboardSuccessLines, } from './cli/dashboard-command.js';
79
79
  import { globalInstallUpdateLockTarget, globalInstallUpdateLockTargetIn, installLatestBotmuxSync } from './core/maintenance.js';
80
80
  import { formatGlobalInstallCommand, resolveGlobalInstallPlan, UnsupportedGlobalInstallError, } from './utils/global-install.js';
81
- import { isLocalDevInstall, botmuxCliEntryAt, bakedBinaryVersion } from './utils/install-info.js';
81
+ import { isLocalDevInstall, botmuxCliEntryAt, bakedBinaryVersion, botmuxInstallRoot } from './utils/install-info.js';
82
+ import { currentUpdateStrategy, replaceStandaloneBinary } from './core/binary-self-update.js';
83
+ import { fetchLatestVersion, isNewerVersion } from './core/update-check.js';
84
+ import { resolveCurrentVersion } from './utils/install-diagnostics.js';
82
85
  import { resolveLocalDevCheckoutDir, isGitWorktree, gitPorcelainStatus, localDevUpdateSteps, describeSpawnFailure, } from './utils/local-dev-update.js';
83
86
  import { loadDashboardSecret } from './dashboard/auth.js';
84
87
  import { postWorkflowDaemonMutation, } from './workflows/v3/daemon-ipc-client.js';
@@ -92,6 +95,7 @@ import { attestManagedOrigin, } from './core/managed-origin-attestation.js';
92
95
  import { rejectLikelyWindowsStdinMojibake, decodeStdinBytes } from './cli/stdin-encoding.js';
93
96
  import { collaborationHelp, formatBotInfoEntriesForCli, formatChatBotsForCli, } from './cli/bots-list-output.js';
94
97
  import { ensureBotChatGrantMatrix, requestExactChatGrant } from './cli/exact-chat-grant-client.js';
98
+ import { loopbackFetch } from './core/loopback-fetch.js';
95
99
  import { buildFooterAddressing, hasKnownBotMention, knownBotOpenIdsFromCrossRef, orderedFooterRecipients, stripCodeSpans, } from './utils/bot-routing.js';
96
100
  import { isLocale, localeForBot, setDefaultLocale, SUPPORTED_LOCALES, t } from './i18n/index.js';
97
101
  import { registerPromptOverrideResolver } from './skills/effective-builtins.js';
@@ -2667,7 +2671,7 @@ async function cmdStatus() {
2667
2671
  // registry to reconcile against. Legacy pm2 still gets surfaced by
2668
2672
  // warnIfLegacyBotmuxAlive above, which is what a pre-migration host needs.
2669
2673
  }
2670
- function cmdUpgrade() {
2674
+ async function cmdUpgrade() {
2671
2675
  // 本地 checkout(有 .git/src):走 git pull --ff-only → 重新 build → 从本
2672
2676
  // checkout 重启,而不是拿全局包管理器去升级(那对 dev 部署无效,见
2673
2677
  // install-info.ts 的 isLocalDevInstall 说明)。
@@ -2675,8 +2679,66 @@ function cmdUpgrade() {
2675
2679
  cmdUpgradeLocalDev();
2676
2680
  return;
2677
2681
  }
2682
+ // 编译版单文件二进制没有 package.json 落盘 ⟹ resolveGlobalInstallPlan 恒抛
2683
+ // Unsupported(实测真实 v3.18.4 二进制就是这条)。改为先按「二进制装在哪」
2684
+ // 判形态:npm 子包形态交回 npm/pnpm/bun,install.sh 形态自己换二进制。
2685
+ const strategy = currentUpdateStrategy(botmuxInstallRoot());
2686
+ if (strategy.kind === 'self-replace') {
2687
+ try {
2688
+ const latest = await fetchLatestVersion();
2689
+ if (!latest) {
2690
+ console.error('❌ 无法获取最新版本号(网络不可达或 registry 异常)。');
2691
+ process.exit(1);
2692
+ }
2693
+ const current = resolveCurrentVersion();
2694
+ if (!isNewerVersion(latest, current)) {
2695
+ console.log(`✅ 已是最新版本(${current})。`);
2696
+ return;
2697
+ }
2698
+ console.log(`🔄 升级中:下载 v${latest} 二进制并替换 ${strategy.target}`);
2699
+ // 握与 dashboard / maintenance 同一把跨进程锁:这条路径是**写同一个文件**,
2700
+ // 两个 update 并发跑会互相盖掉临时文件与 rename。锁文件父目录可能还不存在
2701
+ // (daemon 从未在本机起过就先跑 update),先建再握,否则 ENOENT 会盖掉真实错误。
2702
+ const lockTarget = globalInstallUpdateLockTarget();
2703
+ mkdirSync(dirname(lockTarget), { recursive: true });
2704
+ let acquired = false;
2705
+ try {
2706
+ await withFileLock(lockTarget, async () => {
2707
+ acquired = true;
2708
+ const r = await replaceStandaloneBinary(latest, strategy.target);
2709
+ console.log(`✅ 升级完成:${r.asset} → ${r.target}(${current} → ${latest})。运行 botmux restart 以应用更新。`);
2710
+ }, { maxWaitMs: 2_000 });
2711
+ }
2712
+ catch (error) {
2713
+ // ⚠️ 三态,不是二态。`withFileLock` 拿不到锁时是**抛异常**不是安静返回,
2714
+ // 但 `acquired === false` 只证明「回调没执行」,**不等于「别人持锁」**:
2715
+ // 回调之前还可能因 `open` 的 EACCES/ENOSPC、holder 写入失败、holder 元数据
2716
+ // 不可读、stale-claim `link` 失败而抛错。只看 `acquired` 会把这些**真实故障
2717
+ // 全部误报成「另一个更新正在进行」**——磁盘满被说成并发冲突,是最坏的那种
2718
+ // 误导。所以必须同时要求它是 timeout 类型:
2719
+ // ① 超时且回调未进入 → 友好并发提示(正常互斥结果,不是故障)
2720
+ // ② 回调已进入后失败 → 透出真实错误(下载/校验/替换)
2721
+ // ③ 回调前的基础设施失败 → 同样透出真实错误
2722
+ // 判类型而不是匹文案:file-lock 的文案被多处按字符串匹配,不能动,但新代码
2723
+ // 应该用 FileLockTimeoutError(async/sync 两处语义一致)。
2724
+ if (!acquired && error instanceof FileLockTimeoutError) {
2725
+ console.error('❌ 另一个更新正在进行中(dashboard 或定时任务),请稍后重试。');
2726
+ process.exit(1);
2727
+ }
2728
+ throw error; // ②③ 交给外层统一报错,不被友好文案吞掉
2729
+ }
2730
+ }
2731
+ catch (error) {
2732
+ console.error(`❌ 升级失败:${error instanceof Error ? error.message : error}`);
2733
+ process.exit(1);
2734
+ }
2735
+ return;
2736
+ }
2678
2737
  try {
2679
- const plan = resolveGlobalInstallPlan();
2738
+ if (strategy.kind === 'unsupported') {
2739
+ throw new UnsupportedGlobalInstallError('unknown', process.execPath);
2740
+ }
2741
+ const plan = resolveGlobalInstallPlan(strategy.packageRoot);
2680
2742
  console.log(`🔄 升级中:${formatGlobalInstallCommand(plan)}`);
2681
2743
  installLatestBotmuxSync(plan);
2682
2744
  console.log('\n✅ 升级完成。运行 botmux restart 以应用更新。');
@@ -4417,7 +4479,7 @@ async function postSessionCliIpc(ipcPort, sessionId, route, payload) {
4417
4479
  };
4418
4480
  return hostSecret
4419
4481
  ? fetchDaemonIpc(ipcPort, path, init, hostSecret)
4420
- : fetch(`http://127.0.0.1:${ipcPort}${path}`, init);
4482
+ : loopbackFetch(`http://127.0.0.1:${ipcPort}${path}`, init);
4421
4483
  }
4422
4484
  /** `botmux preview <port>` registers a reachable loopback Web service for the
4423
4485
  * exact current session. There is intentionally no `--session` escape hatch:
@@ -8802,7 +8864,7 @@ async function cmdSend(rest) {
8802
8864
  catch { /* Seatbelt/read-isolated CLI */ }
8803
8865
  const res = secret
8804
8866
  ? await fetchDaemonIpc(attentionPort, '/api/attention', request, secret)
8805
- : await fetch(`http://127.0.0.1:${attentionPort}/api/attention`, request);
8867
+ : await loopbackFetch(`http://127.0.0.1:${attentionPort}/api/attention`, request);
8806
8868
  if (!res.ok)
8807
8869
  throw new Error(`daemon HTTP ${res.status}`);
8808
8870
  attentionRaised = true;
@@ -8915,7 +8977,7 @@ async function postCurrentSessionDaemonRoute(input) {
8915
8977
  }, hostSecret);
8916
8978
  }
8917
8979
  const originClaim = readManagedOriginCapability(resolveDataDir(), input.sessionId, relayDir, process.env.BOTMUX_ORIGIN_CHANNEL_ID);
8918
- return fetch(`http://127.0.0.1:${port}${input.path}`, {
8980
+ return loopbackFetch(`http://127.0.0.1:${port}${input.path}`, {
8919
8981
  method: 'POST',
8920
8982
  headers: { 'content-type': 'application/json' },
8921
8983
  body: JSON.stringify({
@@ -10115,7 +10177,7 @@ async function postAsk(body) {
10115
10177
  }
10116
10178
  res = hostSecret
10117
10179
  ? await fetchDaemonIpc(daemon.ipcPort, '/api/asks', init, hostSecret)
10118
- : await fetch(`http://127.0.0.1:${daemon.ipcPort}/api/asks`, init);
10180
+ : await loopbackFetch(`http://127.0.0.1:${daemon.ipcPort}/api/asks`, init);
10119
10181
  }
10120
10182
  catch (fetchErr) {
10121
10183
  // Socket refused / reset / timeout → daemon is down or restarting → retryable.
@@ -10575,7 +10637,7 @@ async function cmdSessionReady() {
10575
10637
  catch { /* Seatbelt/read-isolated CLI */ }
10576
10638
  }
10577
10639
  if (!hostSecret) {
10578
- await fetch(`http://127.0.0.1:${ipcPort}/api/session-ready`, init);
10640
+ await loopbackFetch(`http://127.0.0.1:${ipcPort}/api/session-ready`, init);
10579
10641
  }
10580
10642
  else {
10581
10643
  await fetchDaemonIpc(ipcPort, '/api/session-ready', init, hostSecret);
@@ -10684,7 +10746,7 @@ async function cmdUserPromptHook() {
10684
10746
  const path = `/api/sessions/${encodeURIComponent(sessionId)}/prompt-ctx/claim`;
10685
10747
  const res = hostSecret
10686
10748
  ? await fetchDaemonIpc(ipcPort, path, init, hostSecret)
10687
- : await fetch(`http://127.0.0.1:${ipcPort}${path}`, init);
10749
+ : await loopbackFetch(`http://127.0.0.1:${ipcPort}${path}`, init);
10688
10750
  if (res.status !== 200)
10689
10751
  process.exit(0);
10690
10752
  const data = await res.json();
@@ -11387,6 +11449,38 @@ if (__entrySubcommand) {
11387
11449
  // below never runs. This awaits forever; the imported module owns exit().
11388
11450
  await new Promise(() => { });
11389
11451
  }
11452
+ // Hidden: perform a compiled-binary self-replace and exit. Invoked by
11453
+ // `runSelfReplaceBlocking` (src/core/maintenance.ts) so the synchronous
11454
+ // maintenance tick / dashboard handler can wait on one child instead of
11455
+ // restructuring around an await. Not a user-facing command; it only ever makes
11456
+ // sense for the standalone binary, which owns its own file.
11457
+ if (command === '__self-update') {
11458
+ const requested = process.argv[3] || 'latest';
11459
+ try {
11460
+ const { replaceStandaloneBinary, currentBinaryInstallShape } = await import('./core/binary-self-update.js');
11461
+ if (currentBinaryInstallShape() !== 'curl-binary') {
11462
+ // Fail closed rather than write into a tree a package manager owns.
11463
+ console.error('__self-update: 当前不是独立二进制安装(install.sh 形态),拒绝自替换');
11464
+ process.exit(1);
11465
+ }
11466
+ const { fetchLatestVersion } = await import('./core/update-check.js');
11467
+ const version = requested === 'latest' ? await fetchLatestVersion() : requested;
11468
+ if (!version) {
11469
+ console.error('__self-update: 无法解析最新版本(网络不可达或 registry 异常)');
11470
+ process.exit(1);
11471
+ }
11472
+ const r = await replaceStandaloneBinary(version);
11473
+ // The caller parses this line: after a self-replace the running process still
11474
+ // reports its OWN baked version, so it cannot discover the new one by re-reading.
11475
+ console.log(`BOTMUX_SELF_UPDATE_VERSION=${version}`);
11476
+ console.log(`__self-update: ${r.asset} → ${r.target} (${r.bytes} bytes)`);
11477
+ process.exit(0);
11478
+ }
11479
+ catch (error) {
11480
+ console.error(`__self-update: ${error instanceof Error ? error.message : error}`);
11481
+ process.exit(1);
11482
+ }
11483
+ }
11390
11484
  const ROOT_FLEET_MUTATION_COMMANDS = new Set(['start', 'stop', 'restart', 'upgrade', 'update']);
11391
11485
  if (ROOT_FLEET_MUTATION_COMMANDS.has(command ?? '')
11392
11486
  && process.argv.slice(3).some(arg => arg === '--help' || arg === '-h')) {
@@ -12244,7 +12338,7 @@ switch (command) {
12244
12338
  break;
12245
12339
  case 'upgrade':
12246
12340
  case 'update':
12247
- cmdUpgrade();
12341
+ await cmdUpgrade();
12248
12342
  break;
12249
12343
  case 'dashboard':
12250
12344
  await cmdDashboard(process.argv.slice(3));