botmux 2.86.0 → 2.87.0

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 (141) hide show
  1. package/README.en.md +26 -0
  2. package/README.md +26 -0
  3. package/dist/adapters/backend/herdr-backend.d.ts.map +1 -1
  4. package/dist/adapters/backend/herdr-backend.js +12 -1
  5. package/dist/adapters/backend/herdr-backend.js.map +1 -1
  6. package/dist/adapters/backend/pty-backend.d.ts.map +1 -1
  7. package/dist/adapters/backend/pty-backend.js +4 -1
  8. package/dist/adapters/backend/pty-backend.js.map +1 -1
  9. package/dist/adapters/backend/tmux-backend.d.ts +1 -1
  10. package/dist/adapters/backend/tmux-backend.d.ts.map +1 -1
  11. package/dist/adapters/backend/tmux-backend.js +22 -9
  12. package/dist/adapters/backend/tmux-backend.js.map +1 -1
  13. package/dist/adapters/backend/tmux-pipe-backend.js +1 -1
  14. package/dist/adapters/backend/tmux-pipe-backend.js.map +1 -1
  15. package/dist/adapters/backend/types.d.ts +9 -0
  16. package/dist/adapters/backend/types.d.ts.map +1 -1
  17. package/dist/adapters/backend/types.js.map +1 -1
  18. package/dist/adapters/backend/zellij-backend.js +1 -1
  19. package/dist/adapters/backend/zellij-backend.js.map +1 -1
  20. package/dist/adapters/cli/codex.d.ts.map +1 -1
  21. package/dist/adapters/cli/codex.js +8 -5
  22. package/dist/adapters/cli/codex.js.map +1 -1
  23. package/dist/adapters/cli/shared-hints.d.ts +2 -1
  24. package/dist/adapters/cli/shared-hints.d.ts.map +1 -1
  25. package/dist/adapters/cli/shared-hints.js +25 -3
  26. package/dist/adapters/cli/shared-hints.js.map +1 -1
  27. package/dist/bot-registry.d.ts +10 -0
  28. package/dist/bot-registry.d.ts.map +1 -1
  29. package/dist/bot-registry.js +6 -0
  30. package/dist/bot-registry.js.map +1 -1
  31. package/dist/cli.d.ts.map +1 -1
  32. package/dist/cli.js +362 -16
  33. package/dist/cli.js.map +1 -1
  34. package/dist/core/command-handler.d.ts.map +1 -1
  35. package/dist/core/command-handler.js +6 -4
  36. package/dist/core/command-handler.js.map +1 -1
  37. package/dist/core/dashboard-ipc-server.d.ts.map +1 -1
  38. package/dist/core/dashboard-ipc-server.js +45 -0
  39. package/dist/core/dashboard-ipc-server.js.map +1 -1
  40. package/dist/core/maintenance.d.ts +18 -0
  41. package/dist/core/maintenance.d.ts.map +1 -1
  42. package/dist/core/maintenance.js +58 -30
  43. package/dist/core/maintenance.js.map +1 -1
  44. package/dist/core/per-bot-env.d.ts +38 -0
  45. package/dist/core/per-bot-env.d.ts.map +1 -0
  46. package/dist/core/per-bot-env.js +79 -0
  47. package/dist/core/per-bot-env.js.map +1 -0
  48. package/dist/core/session-manager.d.ts +3 -0
  49. package/dist/core/session-manager.d.ts.map +1 -1
  50. package/dist/core/session-manager.js +70 -6
  51. package/dist/core/session-manager.js.map +1 -1
  52. package/dist/core/skills/discovery.d.ts +7 -0
  53. package/dist/core/skills/discovery.d.ts.map +1 -1
  54. package/dist/core/skills/discovery.js +51 -0
  55. package/dist/core/skills/discovery.js.map +1 -1
  56. package/dist/core/trigger-session.d.ts.map +1 -1
  57. package/dist/core/trigger-session.js +5 -2
  58. package/dist/core/trigger-session.js.map +1 -1
  59. package/dist/core/update-check.d.ts +67 -0
  60. package/dist/core/update-check.d.ts.map +1 -0
  61. package/dist/core/update-check.js +159 -0
  62. package/dist/core/update-check.js.map +1 -0
  63. package/dist/core/worker-pool.d.ts.map +1 -1
  64. package/dist/core/worker-pool.js +4 -0
  65. package/dist/core/worker-pool.js.map +1 -1
  66. package/dist/daemon.d.ts.map +1 -1
  67. package/dist/daemon.js +29 -6
  68. package/dist/daemon.js.map +1 -1
  69. package/dist/dashboard/bot-payload.d.ts +1 -0
  70. package/dist/dashboard/bot-payload.d.ts.map +1 -1
  71. package/dist/dashboard/bot-payload.js +1 -0
  72. package/dist/dashboard/bot-payload.js.map +1 -1
  73. package/dist/dashboard/skill-install-request.d.ts +10 -0
  74. package/dist/dashboard/skill-install-request.d.ts.map +1 -1
  75. package/dist/dashboard/skill-install-request.js +17 -0
  76. package/dist/dashboard/skill-install-request.js.map +1 -1
  77. package/dist/dashboard/web/app.d.ts.map +1 -1
  78. package/dist/dashboard/web/app.js +29 -1
  79. package/dist/dashboard/web/app.js.map +1 -1
  80. package/dist/dashboard/web/bot-defaults.d.ts.map +1 -1
  81. package/dist/dashboard/web/bot-defaults.js +62 -1
  82. package/dist/dashboard/web/bot-defaults.js.map +1 -1
  83. package/dist/dashboard/web/i18n.d.ts.map +1 -1
  84. package/dist/dashboard/web/i18n.js +128 -0
  85. package/dist/dashboard/web/i18n.js.map +1 -1
  86. package/dist/dashboard/web/settings.d.ts.map +1 -1
  87. package/dist/dashboard/web/settings.js +289 -0
  88. package/dist/dashboard/web/settings.js.map +1 -1
  89. package/dist/dashboard/web/skills.d.ts.map +1 -1
  90. package/dist/dashboard/web/skills.js +183 -10
  91. package/dist/dashboard/web/skills.js.map +1 -1
  92. package/dist/dashboard/web/whiteboards.d.ts +2 -0
  93. package/dist/dashboard/web/whiteboards.d.ts.map +1 -0
  94. package/dist/dashboard/web/whiteboards.js +267 -0
  95. package/dist/dashboard/web/whiteboards.js.map +1 -0
  96. package/dist/dashboard-web/app.js +774 -613
  97. package/dist/dashboard-web/index.html +1 -0
  98. package/dist/dashboard-web/style.css +271 -1
  99. package/dist/dashboard.js +259 -3
  100. package/dist/dashboard.js.map +1 -1
  101. package/dist/global-config.d.ts +6 -0
  102. package/dist/global-config.d.ts.map +1 -1
  103. package/dist/global-config.js +12 -0
  104. package/dist/global-config.js.map +1 -1
  105. package/dist/im/lark/card-handler.d.ts.map +1 -1
  106. package/dist/im/lark/card-handler.js +7 -3
  107. package/dist/im/lark/card-handler.js.map +1 -1
  108. package/dist/services/bot-config-store.d.ts.map +1 -1
  109. package/dist/services/bot-config-store.js +19 -0
  110. package/dist/services/bot-config-store.js.map +1 -1
  111. package/dist/services/resumable-session-discovery.d.ts.map +1 -1
  112. package/dist/services/resumable-session-discovery.js +21 -4
  113. package/dist/services/resumable-session-discovery.js.map +1 -1
  114. package/dist/services/skill-registry-store.d.ts +1 -0
  115. package/dist/services/skill-registry-store.d.ts.map +1 -1
  116. package/dist/services/skill-registry-store.js +32 -0
  117. package/dist/services/skill-registry-store.js.map +1 -1
  118. package/dist/services/whiteboard-store.d.ts +67 -0
  119. package/dist/services/whiteboard-store.d.ts.map +1 -0
  120. package/dist/services/whiteboard-store.js +450 -0
  121. package/dist/services/whiteboard-store.js.map +1 -0
  122. package/dist/setup/bot-config-editor.d.ts +8 -0
  123. package/dist/setup/bot-config-editor.d.ts.map +1 -1
  124. package/dist/setup/bot-config-editor.js +9 -0
  125. package/dist/setup/bot-config-editor.js.map +1 -1
  126. package/dist/skills/definitions.d.ts.map +1 -1
  127. package/dist/skills/definitions.js +118 -0
  128. package/dist/skills/definitions.js.map +1 -1
  129. package/dist/types.d.ts +3 -0
  130. package/dist/types.d.ts.map +1 -1
  131. package/dist/utils/install-diagnostics.d.ts +52 -0
  132. package/dist/utils/install-diagnostics.d.ts.map +1 -0
  133. package/dist/utils/install-diagnostics.js +155 -0
  134. package/dist/utils/install-diagnostics.js.map +1 -0
  135. package/dist/utils/install-info.d.ts +4 -0
  136. package/dist/utils/install-info.d.ts.map +1 -1
  137. package/dist/utils/install-info.js +6 -0
  138. package/dist/utils/install-info.js.map +1 -1
  139. package/dist/worker.js +12 -0
  140. package/dist/worker.js.map +1 -1
  141. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -16,6 +16,7 @@
16
16
  * botmux delete <id> — close a session by ID prefix
17
17
  * botmux delete all — close all active sessions
18
18
  * botmux autostart enable|disable|status — manage boot-time autostart (launchd / user systemd / Windows Task Scheduler)
19
+ * botmux whiteboard status|enable|disable|current|list|read|update|write — local project whiteboard
19
20
  */
20
21
  import { execSync, execFileSync, spawnSync, spawn } from 'node:child_process';
21
22
  import { existsSync, mkdirSync, copyFileSync, readFileSync, writeFileSync, renameSync, readdirSync, readlinkSync, appendFileSync, statSync, unlinkSync } from 'node:fs';
@@ -47,7 +48,8 @@ import { formatBotInfoEntriesForCli, formatChatBotsForCli, } from './cli/bots-li
47
48
  import { buildFooterAddressing, hasKnownBotMention, knownBotOpenIdsFromCrossRef, orderedFooterRecipients, } from './utils/bot-routing.js';
48
49
  import { isLocale, localeForBot, setDefaultLocale, SUPPORTED_LOCALES, t } from './i18n/index.js';
49
50
  import { chatAppLink, larkHosts, normalizeBrand, sdkDomain } from './im/lark/lark-hosts.js';
50
- import { readGlobalConfig, setGlobalLocale, globalConfigPath } from './global-config.js';
51
+ import { mergeGlobalConfig, readGlobalConfig, setGlobalLocale, globalConfigPath } from './global-config.js';
52
+ import { createWhiteboard, ensureDefaultWhiteboard, getWhiteboard, listWhiteboards, readWhiteboard, whiteboardEnabled, whiteboardPath, } from './services/whiteboard-store.js';
51
53
  import { buildBridgeSendMarkerContent } from './services/bridge-fallback-gate.js';
52
54
  import { writeManualIntentIfAbsentTo } from './services/restart-intent-store.js';
53
55
  import { stripLegacyPendingCardFields } from './services/session-store.js';
@@ -257,6 +259,19 @@ function ecosystemConfig() {
257
259
  autorestart: true,
258
260
  max_restarts: 10,
259
261
  restart_delay: 3000,
262
+ // A graceful daemon shutdown exits 0 (SIGTERM/SIGINT → drain → process.exit(0)).
263
+ // Tell pm2 that exit 0 is intentional so it does NOT autorestart the daemon
264
+ // while `botmux restart` is tearing the fleet down — otherwise pm2 revives
265
+ // each daemon (after restart_delay) the instant our parallel SIGTERM drains
266
+ // it, and re-deleting those revivals one-by-one re-serializes the teardown
267
+ // (~13s of churn for 31 bots). Crashes (non-zero exit / killed by signal)
268
+ // are NOT in this list, so genuine crash-autorestart is preserved.
269
+ stop_exit_codes: [0],
270
+ // pm2's default kill_timeout (1.6s) is SHORTER than the daemon's own
271
+ // SHUTDOWN_GRACE_MS (3s), so any daemon pm2 has to signal directly gets
272
+ // SIGKILL'd mid-drain → orphaned (ppid=1) workers. Give pm2 headroom past
273
+ // the daemon's graceful-drain budget so it never force-kills mid-shutdown.
274
+ kill_timeout: 3500,
260
275
  log_date_format: 'YYYY-MM-DD HH:mm:ss',
261
276
  merge_logs: true,
262
277
  node_args: [
@@ -288,6 +303,10 @@ function ecosystemConfig() {
288
303
  autorestart: true,
289
304
  max_restarts: 10,
290
305
  restart_delay: 3000,
306
+ // Same rationale as the bot daemons: don't let pm2 revive on graceful exit-0
307
+ // during a fleet teardown, and don't SIGKILL mid-shutdown. (See baseApp.)
308
+ stop_exit_codes: [0],
309
+ kill_timeout: 3500,
291
310
  error_file: join(LOG_DIR, 'dashboard-error.log'),
292
311
  out_file: join(LOG_DIR, 'dashboard-out.log'),
293
312
  merge_logs: true,
@@ -1146,27 +1165,87 @@ function cleanupStaleDaemonDescriptors() {
1146
1165
  catch { /* ignore */ }
1147
1166
  }
1148
1167
  }
1168
+ /** Block the current thread for `ms`. Safe here: the restart CLI is a one-shot
1169
+ * process, so stalling its event loop during the shutdown poll is harmless. */
1170
+ function sleepSyncMs(ms) {
1171
+ if (ms <= 0)
1172
+ return;
1173
+ try {
1174
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
1175
+ }
1176
+ catch { /* SAB unavailable → no-op */ }
1177
+ }
1149
1178
  /** Delete all pm2 processes matching botmux / botmux-* under the given PM2_HOME. */
1150
1179
  function deleteAllBotmuxProcesses(home = PM2_HOME) {
1180
+ let entries;
1151
1181
  try {
1152
- const output = pm2Capture(['jlist'], home);
1153
- const apps = JSON.parse(output);
1154
- for (const app of apps) {
1155
- if (app.name === PM2_NAME || app.name.startsWith(`${PM2_NAME}-`)) {
1156
- try {
1157
- runPm2(['delete', app.name], false, home, 10_000);
1158
- }
1159
- catch (e) {
1160
- // Don't swallow silently — a failed delete here used to leave the
1161
- // restart half-done with no trace. Surface it (the auto-restart
1162
- // driver captures stderr to ~/.botmux/logs/maintenance-restart.log).
1163
- console.error(`[restart] pm2 delete ${app.name} failed: ${e instanceof Error ? e.message : e}`);
1164
- }
1165
- }
1166
- }
1182
+ const apps = JSON.parse(pm2Capture(['jlist'], home));
1183
+ entries = (Array.isArray(apps) ? apps : [])
1184
+ .filter(a => a && (a.name === PM2_NAME || String(a.name).startsWith(`${PM2_NAME}-`)))
1185
+ .map(a => ({ name: String(a.name), pid: Number(a.pid) || 0, online: a?.pm2_env?.status === 'online' }));
1167
1186
  }
1168
1187
  catch (e) {
1169
1188
  console.error(`[restart] pm2 jlist failed (pm2 not running or no apps?): ${e instanceof Error ? e.message : e}`);
1189
+ return;
1190
+ }
1191
+ if (entries.length === 0)
1192
+ return;
1193
+ const names = entries.map(e => e.name);
1194
+ // Parallel graceful shutdown. pm2's own delete stops apps one-at-a-time
1195
+ // (async eachLimit, concurrency 1) and each botmux daemon's drain eats pm2's
1196
+ // full kill_timeout (~1.6s) → ~N×1.6s serial (~38s for 31 bots). Instead we
1197
+ // SIGTERM every online daemon AT ONCE so their graceful drains overlap (the
1198
+ // daemon's SIGTERM handler detaches workers within SHUTDOWN_GRACE_MS), wait
1199
+ // once for them all to exit, then let pm2 delete reap the now-dead entries
1200
+ // instantly. Orphan-safe: each daemon runs its FULL graceful drain and we wait
1201
+ // for real exit before pm2 touches it — avoiding the mid-drain SIGKILL the old
1202
+ // path forced (pm2 kill_timeout 1.6s < daemon SHUTDOWN_GRACE_MS 3s).
1203
+ const pids = entries.filter(e => e.online && e.pid > 0).map(e => e.pid);
1204
+ for (const pid of pids) {
1205
+ try {
1206
+ process.kill(pid, 'SIGTERM');
1207
+ }
1208
+ catch { /* already gone */ }
1209
+ }
1210
+ // Poll until every signalled daemon has exited (bounded). SHUTDOWN_GRACE_MS is
1211
+ // 3s; give headroom. Exits early the moment the last one dies.
1212
+ const deadline = Date.now() + 5_000;
1213
+ let alive = pids.slice();
1214
+ while (alive.length > 0 && Date.now() < deadline) {
1215
+ sleepSyncMs(50);
1216
+ alive = alive.filter(pid => { try {
1217
+ process.kill(pid, 0);
1218
+ return true;
1219
+ }
1220
+ catch {
1221
+ return false;
1222
+ } });
1223
+ }
1224
+ // Reap pm2 entries. Processes are already dead → each delete is instant, and
1225
+ // ONE batched `pm2 delete name1 name2 …` collapses N pm2 CLI cold-boots
1226
+ // (~315ms each) into one. A revived (autorestart, gated by restart_delay)
1227
+ // instance is still removed by name.
1228
+ const batchTimeout = Math.max(15_000, names.length * 2_500);
1229
+ try {
1230
+ runPm2(['delete', ...names], false, home, batchTimeout);
1231
+ return;
1232
+ }
1233
+ catch (e) {
1234
+ // pm2's batched delete (async eachLimit) aborts on the first failed name,
1235
+ // so a mid-batch failure can leave stragglers. Fall back to the resilient
1236
+ // per-name loop that try/catches each name independently.
1237
+ console.error(`[restart] batched pm2 delete failed, falling back to per-name: ${e instanceof Error ? e.message : e}`);
1238
+ }
1239
+ for (const name of names) {
1240
+ try {
1241
+ runPm2(['delete', name], false, home, 10_000);
1242
+ }
1243
+ catch (e) {
1244
+ // Don't swallow silently — a failed delete here used to leave the
1245
+ // restart half-done with no trace. Surface it (the auto-restart
1246
+ // driver captures stderr to ~/.botmux/logs/maintenance-restart.log).
1247
+ console.error(`[restart] pm2 delete ${name} failed: ${e instanceof Error ? e.message : e}`);
1248
+ }
1170
1249
  }
1171
1250
  }
1172
1251
  function killPm2GodDaemon(home = PM2_HOME) {
@@ -2482,6 +2561,9 @@ botmux v${getVersion()} — IM ↔ AI 编程 CLI 桥接
2482
2561
  voice 配置语音总结(高级功能,独立于 setup)— 交互式填 TTS 引擎+凭证
2483
2562
  voice status 查看当前语音配置(凭证打码)
2484
2563
  voice disable 关闭语音功能(移除配置)
2564
+ whiteboard status|enable|disable
2565
+ 本地项目白板(默认关闭;enable 只打开能力,不创建白板)
2566
+ current --create / list / read / update / write --yes
2485
2567
 
2486
2568
  定时任务(可在 CLI 会话内自动推断 chat):
2487
2569
  schedule list 列出所有任务
@@ -2622,6 +2704,266 @@ function positionals(args, booleanFlags = []) {
2622
2704
  }
2623
2705
  return out;
2624
2706
  }
2707
+ function readStdinUtf8() {
2708
+ // On a TTY, readFileSync(0) blocks waiting for terminal EOF (Ctrl+D) with no
2709
+ // prompt — `whiteboard update` with no text and no pipe looked frozen. Treat
2710
+ // a TTY as "no stdin input" so the caller's empty-content guard surfaces a
2711
+ // real error instead of an indefinite hang.
2712
+ if (process.stdin.isTTY)
2713
+ return '';
2714
+ try {
2715
+ return readFileSync(0, 'utf-8');
2716
+ }
2717
+ catch {
2718
+ return '';
2719
+ }
2720
+ }
2721
+ function currentWhiteboardContext(args) {
2722
+ const sessionIdArg = argValue(args, '--session-id');
2723
+ const sessions = loadSessions();
2724
+ const sid = sessionIdArg || findAncestorSessionId() || undefined;
2725
+ const session = sid ? sessions.get(sid) : undefined;
2726
+ return {
2727
+ session,
2728
+ sessionId: session?.sessionId ?? sid,
2729
+ larkAppId: argValue(args, '--lark-app-id', '--app-id') ?? session?.larkAppId ?? process.env.LARK_APP_ID,
2730
+ chatId: argValue(args, '--chat-id') ?? session?.chatId,
2731
+ workingDir: argValue(args, '--working-dir', '--repo') ?? session?.workingDir ?? process.cwd(),
2732
+ };
2733
+ }
2734
+ function requireWhiteboardEnabled() {
2735
+ if (whiteboardEnabled())
2736
+ return;
2737
+ console.error('Whiteboard is disabled. Enable it with `botmux whiteboard enable` or the dashboard Settings page.');
2738
+ process.exit(2);
2739
+ }
2740
+ // Boolean flags valid on `read`/`update`/`write` that must NOT be parsed as
2741
+ // value-taking. Without this hint, `positionals()` treats e.g. a bare `--yes`
2742
+ // as a value flag and swallows the *following* positional arg as its "value" —
2743
+ // the content ends up empty and the board is silently blanked (a shared
2744
+ // current-state snapshot lost with no history). `--create` belongs to
2745
+ // `current`, `--yes` to `write`, `--json` to `read`; all harmless to declare
2746
+ // together so content parsing never mis-eats a flag's neighbor.
2747
+ const WHITEBOARD_BOOLEAN_FLAGS = ['--create', '--yes', '--json'];
2748
+ function whiteboardContentFromArgs(args, booleanFlags = []) {
2749
+ const file = argValue(args, '--content-file', '--file');
2750
+ if (file)
2751
+ return readFileSync(file, 'utf-8');
2752
+ const pos = positionals(args, booleanFlags);
2753
+ return pos.length > 0 ? pos.join(' ') : readStdinUtf8();
2754
+ }
2755
+ /** Translate store-level whiteboard write errors into friendly CLI exits. The
2756
+ * store throws stable machine codes (whiteboard_cas_mismatch /
2757
+ * whiteboard_empty_content / whiteboard_not_found); map each to a clear,
2758
+ * actionable message so an agent or human reading stderr knows what to do
2759
+ * next instead of seeing a bare code. Always exits. */
2760
+ function handleWhiteboardWriteError(e, id) {
2761
+ const msg = e?.message ?? String(e);
2762
+ if (msg === 'whiteboard_cas_mismatch') {
2763
+ console.error(`Whiteboard was modified since you last read it (CAS mismatch). Re-run ` +
2764
+ `\`botmux whiteboard read --id ${id} --json\` to get the latest content ` +
2765
+ `+ updatedAt, re-merge your changes against it, then update again with ` +
2766
+ `--expected-updated-at <new updatedAt>.`);
2767
+ process.exit(2);
2768
+ }
2769
+ if (msg === 'whiteboard_empty_content') {
2770
+ console.error('Refusing to write empty whiteboard content. Pass text as args, pipe stdin, or use --content-file <path>. (The board is a shared current-state snapshot and cannot be blanked.)');
2771
+ process.exit(2);
2772
+ }
2773
+ if (msg === 'whiteboard_not_found') {
2774
+ console.error(`Whiteboard not found: ${id}`);
2775
+ process.exit(1);
2776
+ }
2777
+ console.error(`Whiteboard write failed: ${msg}`);
2778
+ process.exit(1);
2779
+ }
2780
+ async function cmdWhiteboard(sub, rest) {
2781
+ process.env.SESSION_DATA_DIR ??= resolveDataDir();
2782
+ const action = sub || 'status';
2783
+ if (action === 'help' || action === '--help' || action === '-h') {
2784
+ console.log(`botmux whiteboard <command>
2785
+
2786
+ Commands:
2787
+ status Show whether whiteboard is enabled
2788
+ enable | disable Toggle optional whiteboard feature (does not create boards)
2789
+ list List local whiteboards (read-only, even when disabled)
2790
+ current [--create] Show current default board; --create ensures it when enabled
2791
+ create [--id ID] [--title T] Create a board for current/bound context
2792
+ read [--id ID] [--json] Read board.md (requires enabled). --json emits
2793
+ { id, updatedAt, content } so a caller can CAS on update
2794
+ path [--id ID] Print board/meta/log paths
2795
+ update [--id ID] [text...] Replace board.md current state (or stdin / --content-file).
2796
+ --expected-updated-at <ts> refuses the write if the board
2797
+ changed since that version (CAS); exit 2 with a re-read hint
2798
+ write --yes [--id ID] ... Force-overwrite board.md; --yes required. Also honors
2799
+ --expected-updated-at when supplied
2800
+
2801
+ Context flags: --session-id, --lark-app-id, --chat-id, --working-dir/--repo`);
2802
+ return;
2803
+ }
2804
+ if (action === 'status') {
2805
+ console.log(JSON.stringify({ enabled: whiteboardEnabled(), count: listWhiteboards().length }, null, 2));
2806
+ return;
2807
+ }
2808
+ if (action === 'enable' || action === 'on') {
2809
+ mergeGlobalConfig({ whiteboard: { enabled: true } });
2810
+ console.log('Whiteboard enabled. No board was created; a board is ensured only when first needed.');
2811
+ return;
2812
+ }
2813
+ if (action === 'disable' || action === 'off') {
2814
+ mergeGlobalConfig({ whiteboard: { enabled: false } });
2815
+ console.log('Whiteboard disabled. Existing boards remain on disk and dashboard can show history read-only.');
2816
+ return;
2817
+ }
2818
+ if (action === 'list' || action === 'ls') {
2819
+ const boards = listWhiteboards().map(b => ({ id: b.id, title: b.title, scope: b.scope, larkAppId: b.larkAppId, chatId: b.chatId, workingDir: b.workingDir, updatedAt: b.updatedAt, path: b.path }));
2820
+ console.log(JSON.stringify({ enabled: whiteboardEnabled(), boards }, null, 2));
2821
+ return;
2822
+ }
2823
+ if (action === 'current') {
2824
+ requireWhiteboardEnabled();
2825
+ const id = argValue(rest, '--id');
2826
+ if (id) {
2827
+ const meta = getWhiteboard(id);
2828
+ if (!meta) {
2829
+ console.error(`Whiteboard not found: ${id}`);
2830
+ process.exit(1);
2831
+ }
2832
+ console.log(JSON.stringify({ enabled: true, current: meta, path: whiteboardPath(id) }, null, 2));
2833
+ return;
2834
+ }
2835
+ const ctx = currentWhiteboardContext(rest);
2836
+ let meta = ctx.session?.whiteboardId ? getWhiteboard(ctx.session.whiteboardId) : undefined;
2837
+ if (!meta && argFlag(rest, '--create')) {
2838
+ meta = ensureDefaultWhiteboard({ larkAppId: ctx.larkAppId, chatId: ctx.chatId, workingDir: ctx.workingDir, sessionId: ctx.sessionId });
2839
+ if (ctx.session) {
2840
+ ctx.session.whiteboardId = meta.id;
2841
+ saveSession(ctx.session);
2842
+ }
2843
+ }
2844
+ if (!meta) {
2845
+ console.log(JSON.stringify({ enabled: true, current: null, hint: 'Run `botmux whiteboard current --create` to ensure the default board.' }, null, 2));
2846
+ return;
2847
+ }
2848
+ console.log(JSON.stringify({ enabled: true, current: meta, path: whiteboardPath(meta.id) }, null, 2));
2849
+ return;
2850
+ }
2851
+ if (action === 'create') {
2852
+ requireWhiteboardEnabled();
2853
+ const ctx = currentWhiteboardContext(rest);
2854
+ const meta = createWhiteboard({ id: argValue(rest, '--id'), title: argValue(rest, '--title'), larkAppId: ctx.larkAppId, chatId: ctx.chatId, workingDir: ctx.workingDir, sessionId: ctx.sessionId });
2855
+ if (ctx.session && !ctx.session.whiteboardId) {
2856
+ ctx.session.whiteboardId = meta.id;
2857
+ saveSession(ctx.session);
2858
+ }
2859
+ console.log(JSON.stringify({ board: meta, path: whiteboardPath(meta.id) }, null, 2));
2860
+ return;
2861
+ }
2862
+ // Anything reaching here must be one of the file-operating subcommands; the
2863
+ // earlier branches (help/status/enable/disable/list/current/create) already
2864
+ // returned. Reject unknown actions BEFORE computing an id — otherwise a typo
2865
+ // like `post` fell through to the misleading "No whiteboard id" error.
2866
+ if (!['read', 'path', 'update', 'write'].includes(action)) {
2867
+ console.error(`Unknown whiteboard command: ${action}`);
2868
+ process.exit(1);
2869
+ }
2870
+ if (['read', 'update', 'write'].includes(action))
2871
+ requireWhiteboardEnabled();
2872
+ const explicitId = argValue(rest, '--id');
2873
+ const ctx = currentWhiteboardContext(rest);
2874
+ let id = explicitId ?? ctx.session?.whiteboardId;
2875
+ if (!id && whiteboardEnabled() && action === 'update') {
2876
+ const meta = ensureDefaultWhiteboard({ larkAppId: ctx.larkAppId, chatId: ctx.chatId, workingDir: ctx.workingDir, sessionId: ctx.sessionId });
2877
+ id = meta.id;
2878
+ if (ctx.session) {
2879
+ ctx.session.whiteboardId = id;
2880
+ saveSession(ctx.session);
2881
+ }
2882
+ }
2883
+ if (!id) {
2884
+ console.error('No whiteboard id. Pass --id or run `botmux whiteboard current --create`.');
2885
+ process.exit(1);
2886
+ }
2887
+ if (action === 'read') {
2888
+ requireWhiteboardEnabled();
2889
+ // Default: stream raw board.md to stdout (back-compat for agents/skills
2890
+ // that treat stdout as the board content). `--json` returns
2891
+ // { id, updatedAt, content } so an agent can capture the version it read
2892
+ // and pass it back as --expected-updated-at on update — the compare-and-set
2893
+ // that turns the read→merge→update flow from blind last-writer-wins into a
2894
+ // conflict-detecting update.
2895
+ if (argFlag(rest, '--json')) {
2896
+ const meta = getWhiteboard(id);
2897
+ if (!meta) {
2898
+ console.error(`Whiteboard not found: ${id}`);
2899
+ process.exit(1);
2900
+ }
2901
+ console.log(JSON.stringify({ id: meta.id, updatedAt: meta.updatedAt, content: readWhiteboard(id) }));
2902
+ }
2903
+ else {
2904
+ process.stdout.write(readWhiteboard(id));
2905
+ }
2906
+ return;
2907
+ }
2908
+ if (action === 'path') {
2909
+ const meta = getWhiteboard(id);
2910
+ if (!meta) {
2911
+ console.error(`Whiteboard not found: ${id}`);
2912
+ process.exit(1);
2913
+ }
2914
+ console.log(JSON.stringify({ board: meta, path: whiteboardPath(id) }, null, 2));
2915
+ return;
2916
+ }
2917
+ if (action === 'update') {
2918
+ requireWhiteboardEnabled();
2919
+ const content = whiteboardContentFromArgs(rest, WHITEBOARD_BOOLEAN_FLAGS);
2920
+ if (!content.trim()) {
2921
+ console.error('Refusing to write empty whiteboard content. Pass text as args, pipe stdin, or use --content-file <path>. (The board is a shared current-state snapshot and cannot be blanked.)');
2922
+ process.exit(2);
2923
+ }
2924
+ // Optional CAS: the agent passes the updatedAt it observed at read time.
2925
+ // If the board changed in between, the store refuses with
2926
+ // whiteboard_cas_mismatch → friendly exit 2 so the agent re-reads/merges
2927
+ // instead of silently clobbering the other writer's update.
2928
+ const expectedUpdatedAt = argValue(rest, '--expected-updated-at');
2929
+ const { writeWhiteboard } = await import('./services/whiteboard-store.js');
2930
+ try {
2931
+ const meta = writeWhiteboard(id, content, { actor: ctx.sessionId, kind: 'update', expectedUpdatedAt });
2932
+ console.log(JSON.stringify({ ok: true, board: meta }, null, 2));
2933
+ }
2934
+ catch (e) {
2935
+ handleWhiteboardWriteError(e, id);
2936
+ }
2937
+ return;
2938
+ }
2939
+ if (action === 'write') {
2940
+ requireWhiteboardEnabled();
2941
+ if (!argFlag(rest, '--yes')) {
2942
+ console.error('Refusing to overwrite whiteboard without --yes. Prefer `botmux whiteboard update` for current-state updates.');
2943
+ process.exit(2);
2944
+ }
2945
+ const content = whiteboardContentFromArgs(rest, WHITEBOARD_BOOLEAN_FLAGS);
2946
+ if (!content.trim()) {
2947
+ console.error('Refusing to write empty whiteboard content. Pass text as args, pipe stdin, or use --content-file <path>. (The board is a shared current-state snapshot and cannot be blanked.)');
2948
+ process.exit(2);
2949
+ }
2950
+ // `write --yes` is the human force-overwrite escape hatch, but if a CAS
2951
+ // version is supplied we still honor it — a conscious writer that knows
2952
+ // the base version should still get a conflict signal rather than clobber.
2953
+ const expectedUpdatedAt = argValue(rest, '--expected-updated-at');
2954
+ const { writeWhiteboard } = await import('./services/whiteboard-store.js');
2955
+ try {
2956
+ const meta = writeWhiteboard(id, content, { actor: ctx.sessionId, expectedUpdatedAt });
2957
+ console.log(JSON.stringify({ ok: true, board: meta }, null, 2));
2958
+ }
2959
+ catch (e) {
2960
+ handleWhiteboardWriteError(e, id);
2961
+ }
2962
+ return;
2963
+ }
2964
+ console.error(`Unknown whiteboard command: ${action}`);
2965
+ process.exit(1);
2966
+ }
2625
2967
  async function cmdSchedule(sub, rest) {
2626
2968
  // Ensure SESSION_DATA_DIR points at the daemon's data dir so schedule-store
2627
2969
  // writes to the right file even when invoked outside the daemon env.
@@ -5219,6 +5561,10 @@ switch (command) {
5219
5561
  case 'voice':
5220
5562
  await cmdVoiceSetup(process.argv.slice(3));
5221
5563
  break;
5564
+ case 'whiteboard':
5565
+ case 'wb':
5566
+ await cmdWhiteboard(process.argv[3] ?? 'status', process.argv.slice(4));
5567
+ break;
5222
5568
  case 'thread': {
5223
5569
  // Removed in favor of `botmux history` (普通群也兼容). Friendly stderr so
5224
5570
  // pre-rename scripts/skills surface the rename instead of "unknown command".