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.
- package/README.en.md +26 -0
- package/README.md +26 -0
- package/dist/adapters/backend/herdr-backend.d.ts.map +1 -1
- package/dist/adapters/backend/herdr-backend.js +12 -1
- package/dist/adapters/backend/herdr-backend.js.map +1 -1
- package/dist/adapters/backend/pty-backend.d.ts.map +1 -1
- package/dist/adapters/backend/pty-backend.js +4 -1
- package/dist/adapters/backend/pty-backend.js.map +1 -1
- package/dist/adapters/backend/tmux-backend.d.ts +1 -1
- package/dist/adapters/backend/tmux-backend.d.ts.map +1 -1
- package/dist/adapters/backend/tmux-backend.js +22 -9
- package/dist/adapters/backend/tmux-backend.js.map +1 -1
- package/dist/adapters/backend/tmux-pipe-backend.js +1 -1
- package/dist/adapters/backend/tmux-pipe-backend.js.map +1 -1
- package/dist/adapters/backend/types.d.ts +9 -0
- package/dist/adapters/backend/types.d.ts.map +1 -1
- package/dist/adapters/backend/types.js.map +1 -1
- package/dist/adapters/backend/zellij-backend.js +1 -1
- package/dist/adapters/backend/zellij-backend.js.map +1 -1
- package/dist/adapters/cli/codex.d.ts.map +1 -1
- package/dist/adapters/cli/codex.js +8 -5
- package/dist/adapters/cli/codex.js.map +1 -1
- package/dist/adapters/cli/shared-hints.d.ts +2 -1
- package/dist/adapters/cli/shared-hints.d.ts.map +1 -1
- package/dist/adapters/cli/shared-hints.js +25 -3
- package/dist/adapters/cli/shared-hints.js.map +1 -1
- package/dist/bot-registry.d.ts +10 -0
- package/dist/bot-registry.d.ts.map +1 -1
- package/dist/bot-registry.js +6 -0
- package/dist/bot-registry.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +362 -16
- package/dist/cli.js.map +1 -1
- package/dist/core/command-handler.d.ts.map +1 -1
- package/dist/core/command-handler.js +6 -4
- package/dist/core/command-handler.js.map +1 -1
- package/dist/core/dashboard-ipc-server.d.ts.map +1 -1
- package/dist/core/dashboard-ipc-server.js +45 -0
- package/dist/core/dashboard-ipc-server.js.map +1 -1
- package/dist/core/maintenance.d.ts +18 -0
- package/dist/core/maintenance.d.ts.map +1 -1
- package/dist/core/maintenance.js +58 -30
- package/dist/core/maintenance.js.map +1 -1
- package/dist/core/per-bot-env.d.ts +38 -0
- package/dist/core/per-bot-env.d.ts.map +1 -0
- package/dist/core/per-bot-env.js +79 -0
- package/dist/core/per-bot-env.js.map +1 -0
- package/dist/core/session-manager.d.ts +3 -0
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +70 -6
- package/dist/core/session-manager.js.map +1 -1
- package/dist/core/skills/discovery.d.ts +7 -0
- package/dist/core/skills/discovery.d.ts.map +1 -1
- package/dist/core/skills/discovery.js +51 -0
- package/dist/core/skills/discovery.js.map +1 -1
- package/dist/core/trigger-session.d.ts.map +1 -1
- package/dist/core/trigger-session.js +5 -2
- package/dist/core/trigger-session.js.map +1 -1
- package/dist/core/update-check.d.ts +67 -0
- package/dist/core/update-check.d.ts.map +1 -0
- package/dist/core/update-check.js +159 -0
- package/dist/core/update-check.js.map +1 -0
- package/dist/core/worker-pool.d.ts.map +1 -1
- package/dist/core/worker-pool.js +4 -0
- package/dist/core/worker-pool.js.map +1 -1
- package/dist/daemon.d.ts.map +1 -1
- package/dist/daemon.js +29 -6
- package/dist/daemon.js.map +1 -1
- package/dist/dashboard/bot-payload.d.ts +1 -0
- package/dist/dashboard/bot-payload.d.ts.map +1 -1
- package/dist/dashboard/bot-payload.js +1 -0
- package/dist/dashboard/bot-payload.js.map +1 -1
- package/dist/dashboard/skill-install-request.d.ts +10 -0
- package/dist/dashboard/skill-install-request.d.ts.map +1 -1
- package/dist/dashboard/skill-install-request.js +17 -0
- package/dist/dashboard/skill-install-request.js.map +1 -1
- package/dist/dashboard/web/app.d.ts.map +1 -1
- package/dist/dashboard/web/app.js +29 -1
- package/dist/dashboard/web/app.js.map +1 -1
- package/dist/dashboard/web/bot-defaults.d.ts.map +1 -1
- package/dist/dashboard/web/bot-defaults.js +62 -1
- package/dist/dashboard/web/bot-defaults.js.map +1 -1
- package/dist/dashboard/web/i18n.d.ts.map +1 -1
- package/dist/dashboard/web/i18n.js +128 -0
- package/dist/dashboard/web/i18n.js.map +1 -1
- package/dist/dashboard/web/settings.d.ts.map +1 -1
- package/dist/dashboard/web/settings.js +289 -0
- package/dist/dashboard/web/settings.js.map +1 -1
- package/dist/dashboard/web/skills.d.ts.map +1 -1
- package/dist/dashboard/web/skills.js +183 -10
- package/dist/dashboard/web/skills.js.map +1 -1
- package/dist/dashboard/web/whiteboards.d.ts +2 -0
- package/dist/dashboard/web/whiteboards.d.ts.map +1 -0
- package/dist/dashboard/web/whiteboards.js +267 -0
- package/dist/dashboard/web/whiteboards.js.map +1 -0
- package/dist/dashboard-web/app.js +774 -613
- package/dist/dashboard-web/index.html +1 -0
- package/dist/dashboard-web/style.css +271 -1
- package/dist/dashboard.js +259 -3
- package/dist/dashboard.js.map +1 -1
- package/dist/global-config.d.ts +6 -0
- package/dist/global-config.d.ts.map +1 -1
- package/dist/global-config.js +12 -0
- package/dist/global-config.js.map +1 -1
- package/dist/im/lark/card-handler.d.ts.map +1 -1
- package/dist/im/lark/card-handler.js +7 -3
- package/dist/im/lark/card-handler.js.map +1 -1
- package/dist/services/bot-config-store.d.ts.map +1 -1
- package/dist/services/bot-config-store.js +19 -0
- package/dist/services/bot-config-store.js.map +1 -1
- package/dist/services/resumable-session-discovery.d.ts.map +1 -1
- package/dist/services/resumable-session-discovery.js +21 -4
- package/dist/services/resumable-session-discovery.js.map +1 -1
- package/dist/services/skill-registry-store.d.ts +1 -0
- package/dist/services/skill-registry-store.d.ts.map +1 -1
- package/dist/services/skill-registry-store.js +32 -0
- package/dist/services/skill-registry-store.js.map +1 -1
- package/dist/services/whiteboard-store.d.ts +67 -0
- package/dist/services/whiteboard-store.d.ts.map +1 -0
- package/dist/services/whiteboard-store.js +450 -0
- package/dist/services/whiteboard-store.js.map +1 -0
- package/dist/setup/bot-config-editor.d.ts +8 -0
- package/dist/setup/bot-config-editor.d.ts.map +1 -1
- package/dist/setup/bot-config-editor.js +9 -0
- package/dist/setup/bot-config-editor.js.map +1 -1
- package/dist/skills/definitions.d.ts.map +1 -1
- package/dist/skills/definitions.js +118 -0
- package/dist/skills/definitions.js.map +1 -1
- package/dist/types.d.ts +3 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/install-diagnostics.d.ts +52 -0
- package/dist/utils/install-diagnostics.d.ts.map +1 -0
- package/dist/utils/install-diagnostics.js +155 -0
- package/dist/utils/install-diagnostics.js.map +1 -0
- package/dist/utils/install-info.d.ts +4 -0
- package/dist/utils/install-info.d.ts.map +1 -1
- package/dist/utils/install-info.js +6 -0
- package/dist/utils/install-info.js.map +1 -1
- package/dist/worker.js +12 -0
- package/dist/worker.js.map +1 -1
- 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
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
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".
|