botmux 2.12.2 → 2.13.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 (99) hide show
  1. package/README.en.md +29 -0
  2. package/README.md +29 -0
  3. package/dist/cli.js +104 -8
  4. package/dist/cli.js.map +1 -1
  5. package/dist/config.d.ts +6 -0
  6. package/dist/config.d.ts.map +1 -1
  7. package/dist/config.js +8 -0
  8. package/dist/config.js.map +1 -1
  9. package/dist/core/dashboard-events.d.ts +57 -0
  10. package/dist/core/dashboard-events.d.ts.map +1 -0
  11. package/dist/core/dashboard-events.js +23 -0
  12. package/dist/core/dashboard-events.js.map +1 -0
  13. package/dist/core/dashboard-ipc-server.d.ts +43 -0
  14. package/dist/core/dashboard-ipc-server.d.ts.map +1 -0
  15. package/dist/core/dashboard-ipc-server.js +232 -0
  16. package/dist/core/dashboard-ipc-server.js.map +1 -0
  17. package/dist/core/dashboard-locate.d.ts +20 -0
  18. package/dist/core/dashboard-locate.d.ts.map +1 -0
  19. package/dist/core/dashboard-locate.js +26 -0
  20. package/dist/core/dashboard-locate.js.map +1 -0
  21. package/dist/core/dashboard-rows.d.ts +30 -0
  22. package/dist/core/dashboard-rows.d.ts.map +1 -0
  23. package/dist/core/dashboard-rows.js +48 -0
  24. package/dist/core/dashboard-rows.js.map +1 -0
  25. package/dist/core/scheduler.d.ts +20 -0
  26. package/dist/core/scheduler.d.ts.map +1 -1
  27. package/dist/core/scheduler.js +89 -2
  28. package/dist/core/scheduler.js.map +1 -1
  29. package/dist/core/types.d.ts +5 -0
  30. package/dist/core/types.d.ts.map +1 -1
  31. package/dist/core/types.js.map +1 -1
  32. package/dist/core/worker-pool.d.ts +19 -1
  33. package/dist/core/worker-pool.d.ts.map +1 -1
  34. package/dist/core/worker-pool.js +146 -0
  35. package/dist/core/worker-pool.js.map +1 -1
  36. package/dist/daemon.d.ts.map +1 -1
  37. package/dist/daemon.js +87 -4
  38. package/dist/daemon.js.map +1 -1
  39. package/dist/dashboard/aggregator.d.ts +41 -0
  40. package/dist/dashboard/aggregator.d.ts.map +1 -0
  41. package/dist/dashboard/aggregator.js +125 -0
  42. package/dist/dashboard/aggregator.js.map +1 -0
  43. package/dist/dashboard/auth.d.ts +23 -0
  44. package/dist/dashboard/auth.d.ts.map +1 -0
  45. package/dist/dashboard/auth.js +66 -0
  46. package/dist/dashboard/auth.js.map +1 -0
  47. package/dist/dashboard/registry.d.ts +28 -0
  48. package/dist/dashboard/registry.d.ts.map +1 -0
  49. package/dist/dashboard/registry.js +74 -0
  50. package/dist/dashboard/registry.js.map +1 -0
  51. package/dist/dashboard/web/app.d.ts +2 -0
  52. package/dist/dashboard/web/app.d.ts.map +1 -0
  53. package/dist/dashboard/web/app.js +42 -0
  54. package/dist/dashboard/web/app.js.map +1 -0
  55. package/dist/dashboard/web/groups.d.ts +2 -0
  56. package/dist/dashboard/web/groups.d.ts.map +1 -0
  57. package/dist/dashboard/web/groups.js +152 -0
  58. package/dist/dashboard/web/groups.js.map +1 -0
  59. package/dist/dashboard/web/schedules.d.ts +2 -0
  60. package/dist/dashboard/web/schedules.d.ts.map +1 -0
  61. package/dist/dashboard/web/schedules.js +105 -0
  62. package/dist/dashboard/web/schedules.js.map +1 -0
  63. package/dist/dashboard/web/sessions.d.ts +2 -0
  64. package/dist/dashboard/web/sessions.d.ts.map +1 -0
  65. package/dist/dashboard/web/sessions.js +184 -0
  66. package/dist/dashboard/web/sessions.js.map +1 -0
  67. package/dist/dashboard/web/store.d.ts +23 -0
  68. package/dist/dashboard/web/store.d.ts.map +1 -0
  69. package/dist/dashboard/web/store.js +82 -0
  70. package/dist/dashboard/web/store.js.map +1 -0
  71. package/dist/dashboard-web/app.js +129 -0
  72. package/dist/dashboard-web/index.html +22 -0
  73. package/dist/dashboard-web/style.css +50 -0
  74. package/dist/dashboard.d.ts +2 -0
  75. package/dist/dashboard.d.ts.map +1 -0
  76. package/dist/dashboard.js +308 -0
  77. package/dist/dashboard.js.map +1 -0
  78. package/dist/services/bridge-turn-queue.d.ts.map +1 -1
  79. package/dist/services/bridge-turn-queue.js +12 -7
  80. package/dist/services/bridge-turn-queue.js.map +1 -1
  81. package/dist/services/groups-store.d.ts +35 -0
  82. package/dist/services/groups-store.d.ts.map +1 -0
  83. package/dist/services/groups-store.js +104 -0
  84. package/dist/services/groups-store.js.map +1 -0
  85. package/dist/services/schedule-store.d.ts +1 -0
  86. package/dist/services/schedule-store.d.ts.map +1 -1
  87. package/dist/services/schedule-store.js +70 -1
  88. package/dist/services/schedule-store.js.map +1 -1
  89. package/dist/services/session-store.d.ts.map +1 -1
  90. package/dist/services/session-store.js +1 -0
  91. package/dist/services/session-store.js.map +1 -1
  92. package/dist/skills/definitions.d.ts.map +1 -1
  93. package/dist/skills/definitions.js +34 -0
  94. package/dist/skills/definitions.js.map +1 -1
  95. package/dist/types.d.ts +2 -0
  96. package/dist/types.d.ts.map +1 -1
  97. package/dist/worker.js +47 -2
  98. package/dist/worker.js.map +1 -1
  99. package/package.json +4 -2
package/README.en.md CHANGED
@@ -424,6 +424,7 @@ botmux setup
424
424
  | `botmux autostart enable` | Register boot-time autostart (macOS launchd / Linux user systemd, no sudo) |
425
425
  | `botmux autostart disable` | Unregister boot-time autostart |
426
426
  | `botmux autostart status` | Show autostart status |
427
+ | `botmux dashboard` | Print a fresh Web Dashboard URL (rotates the token; previous URL becomes invalid) |
427
428
 
428
429
  ### Boot-time Autostart
429
430
 
@@ -453,6 +454,34 @@ These require the `~/.botmux/bin/botmux` wrapper, which the daemon writes at sta
453
454
 
454
455
  ---
455
456
 
457
+ ## Web Dashboard
458
+
459
+ botmux ships a LAN-accessible Web Dashboard for managing all sessions and scheduled tasks across every configured bot.
460
+
461
+ ```bash
462
+ botmux dashboard
463
+ # prints: http://<lan-ip>:7891/?t=<token>
464
+ ```
465
+
466
+ Each invocation rotates the token — previous URLs are invalidated immediately. This is by design, so a leaked link stops working as soon as you fetch a new one.
467
+
468
+ v1 features:
469
+ - **Sessions board** — every active and closed session across every bot, filterable by CLI / status / adopt / free-text. The detail drawer exposes a "📍 定位到飞书话题" button that posts a marker into the original thread (workaround for Feishu having no public topic deep-link), then opens AppLink to the chat. Also: copy IDs, close session, open xterm.
470
+ - **Schedules board** — every scheduled task across every bot, with Run-now / Pause / Resume actions.
471
+
472
+ Environment variables (set in `~/.botmux/.env`):
473
+
474
+ | Variable | Default | Purpose |
475
+ |----------|---------|---------|
476
+ | `BOTMUX_DASHBOARD_HOST` | `0.0.0.0` | Dashboard HTTP bind address |
477
+ | `BOTMUX_DASHBOARD_PORT` | `7891` | Dashboard HTTP port |
478
+ | `BOTMUX_DASHBOARD_EXTERNAL_HOST` | `WEB_EXTERNAL_HOST` or LAN-IP autodetect | Host used in the printed URL |
479
+ | `BOTMUX_DAEMON_IPC_BASE_PORT` | `7892` | Per-daemon IPC port = base + botIndex |
480
+
481
+ The dashboard runs as its own pm2 process (`botmux-dashboard`) — `pnpm daemon:restart` brings it up alongside every bot daemon. Each daemon exposes a localhost-only IPC at `127.0.0.1:7892+botIndex`; the dashboard process is a thin reverse proxy + token gate. The HMAC secret at `~/.botmux/.dashboard-secret` (mode `0600`) is generated on first start and is used only to sign `botmux dashboard` rotation requests — it never reaches the browser.
482
+
483
+ ---
484
+
456
485
  ## Contributing
457
486
 
458
487
  See [CONTRIBUTING.md](CONTRIBUTING.md).
package/README.md CHANGED
@@ -357,6 +357,7 @@ botmux autostart enable
357
357
  | `botmux autostart enable` | 注册开机自启(macOS launchd / Linux user systemd,无需 sudo) |
358
358
  | `botmux autostart disable` | 注销开机自启 |
359
359
  | `botmux autostart status` | 查看自启状态 |
360
+ | `botmux dashboard` | 输出一次 Web Dashboard URL(每次刷 token,旧链接立即失效) |
360
361
 
361
362
  ### 开机自启
362
363
 
@@ -386,6 +387,34 @@ botmux autostart enable
386
387
 
387
388
  ---
388
389
 
390
+ ## Web Dashboard
391
+
392
+ botmux 启动后会自带一个 Web Dashboard 用来管理所有会话和定时任务。
393
+
394
+ ```bash
395
+ botmux dashboard
396
+ # 输出: http://<lan-ip>:7891/?t=<token>
397
+ ```
398
+
399
+ 每次跑 `botmux dashboard` 都会换一个 token,老 URL 立即失效——这是有意为之,符合一次一密的取链方式。
400
+
401
+ 页面功能(v1):
402
+ - **Sessions**:跨所有 bot 列出活跃和已关闭会话,支持按 CLI / 状态 / adopt / 文本搜索过滤。点进 detail drawer 后可以「定位到飞书话题」(机器人在原话题发一条 📍 标记 + 浏览器自动开 chat AppLink,规避飞书没有公开 topic deep-link 的限制)、复制各种 ID、关闭会话。
403
+ - **Schedules**:列出所有定时任务,可以 Run now / Pause / Resume。
404
+
405
+ 环境变量(写在 `~/.botmux/.env`):
406
+
407
+ | 变量 | 默认 | 说明 |
408
+ |------|------|------|
409
+ | `BOTMUX_DASHBOARD_HOST` | `0.0.0.0` | dashboard HTTP 绑定地址 |
410
+ | `BOTMUX_DASHBOARD_PORT` | `7891` | dashboard HTTP 端口 |
411
+ | `BOTMUX_DASHBOARD_EXTERNAL_HOST` | `WEB_EXTERNAL_HOST` 或 LAN IP 自动探测 | CLI 输出 URL 用的 host |
412
+ | `BOTMUX_DAEMON_IPC_BASE_PORT` | `7892` | 每个 daemon 的 IPC 端口 = base + botIndex |
413
+
414
+ dashboard 走单独 pm2 进程 `botmux-dashboard`,跟着 `pnpm daemon:restart` 一起起停。每个 daemon 在 127.0.0.1 暴露内部 IPC(仅本机),dashboard 进程做反向代理 + 鉴权。`.dashboard-secret` 在首次启动时生成(`~/.botmux/.dashboard-secret`,mode 0600),仅用于 `botmux dashboard` 命令的 HMAC 鉴权,不下发给浏览器。
415
+
416
+ ---
417
+
389
418
  ## 配置
390
419
 
391
420
  通过 `~/.botmux/bots.json` 配置机器人。运行 `botmux setup` 交互式创建,或手动编辑。
package/dist/cli.js CHANGED
@@ -17,12 +17,13 @@
17
17
  * botmux autostart enable|disable|status — manage boot-time autostart (launchd / user systemd)
18
18
  */
19
19
  import { execSync, spawnSync, spawn } from 'node:child_process';
20
- import { existsSync, mkdirSync, readFileSync, writeFileSync, renameSync, readdirSync, readlinkSync, appendFileSync } from 'node:fs';
20
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, renameSync, readdirSync, readlinkSync, appendFileSync, statSync, unlinkSync } from 'node:fs';
21
21
  import { join, dirname } from 'node:path';
22
22
  import { homedir } from 'node:os';
23
23
  import { fileURLToPath } from 'node:url';
24
24
  import { createInterface } from 'node:readline';
25
25
  import { createRequire } from 'node:module';
26
+ import { createHmac, randomBytes } from 'node:crypto';
26
27
  import { enableAutostart, disableAutostart, autostartStatus, refreshAutostart } from './autostart.js';
27
28
  const __filename = fileURLToPath(import.meta.url);
28
29
  const __dirname = dirname(__filename);
@@ -108,6 +109,21 @@ function ecosystemConfig() {
108
109
  out_file: join(LOG_DIR, `daemon-${i}-out.log`),
109
110
  env: { SESSION_DATA_DIR: DATA_DIR, BOTMUX_BOT_INDEX: String(i) },
110
111
  }));
112
+ apps.push({
113
+ name: 'botmux-dashboard',
114
+ script: join(PKG_ROOT, 'dist', 'dashboard.js'),
115
+ cwd: PKG_ROOT,
116
+ autorestart: true,
117
+ max_restarts: 10,
118
+ restart_delay: 3000,
119
+ error_file: join(LOG_DIR, 'dashboard-error.log'),
120
+ out_file: join(LOG_DIR, 'dashboard-out.log'),
121
+ merge_logs: true,
122
+ env: {
123
+ BOTMUX_DASHBOARD_HOST: process.env.BOTMUX_DASHBOARD_HOST ?? '0.0.0.0',
124
+ BOTMUX_DASHBOARD_PORT: process.env.BOTMUX_DASHBOARD_PORT ?? '7891',
125
+ },
126
+ });
111
127
  const cfg = { apps };
112
128
  const tmpFile = join(CONFIG_DIR, 'ecosystem.config.json');
113
129
  writeFileSync(tmpFile, JSON.stringify(cfg, null, 2));
@@ -368,6 +384,28 @@ function cmdStart() {
368
384
  console.log(` autostart unit 已同步到当前 Node/cli.js 路径`);
369
385
  }
370
386
  }
387
+ /**
388
+ * Wipe stale dashboard-daemon descriptors (mtime older than 5 minutes).
389
+ * Live daemons refresh their descriptor every 30s via heartbeat; anything
390
+ * older is from a daemon that exited without cleaning up. Called as part of
391
+ * the pm2 zombie-cleanup flow so the dashboard registry stays consistent.
392
+ */
393
+ function cleanupStaleDaemonDescriptors() {
394
+ const regDir = join(DATA_DIR, 'dashboard-daemons');
395
+ if (!existsSync(regDir))
396
+ return;
397
+ for (const f of readdirSync(regDir)) {
398
+ if (!f.endsWith('.json'))
399
+ continue;
400
+ const fp = join(regDir, f);
401
+ try {
402
+ const stat = statSync(fp);
403
+ if (Date.now() - stat.mtimeMs > 5 * 60_000)
404
+ unlinkSync(fp);
405
+ }
406
+ catch { /* ignore */ }
407
+ }
408
+ }
371
409
  /** Delete all pm2 processes matching botmux / botmux-* under the given PM2_HOME. */
372
410
  function deleteAllBotmuxProcesses(home = PM2_HOME) {
373
411
  try {
@@ -448,6 +486,8 @@ function cmdStop() {
448
486
  }
449
487
  }
450
488
  catch { /* */ }
489
+ // Wipe abandoned dashboard-daemon descriptors left behind by stopped daemons.
490
+ cleanupStaleDaemonDescriptors();
451
491
  if (!stopped)
452
492
  console.log('daemon 未在运行。');
453
493
  }
@@ -462,6 +502,8 @@ function cmdRestart() {
462
502
  cleanupLegacyPm2();
463
503
  // Delete all botmux processes (handles both old single-process and new multi-process)
464
504
  deleteAllBotmuxProcesses();
505
+ // Wipe abandoned dashboard-daemon descriptors left behind by killed daemons.
506
+ cleanupStaleDaemonDescriptors();
465
507
  const cfg = ecosystemConfig();
466
508
  runPm2(['start', cfg]);
467
509
  if (refreshAutostart({ pkgRoot: PKG_ROOT, configDir: CONFIG_DIR, logDir: LOG_DIR })) {
@@ -550,6 +592,44 @@ function cmdUpgrade() {
550
592
  process.exit(1);
551
593
  }
552
594
  }
595
+ /**
596
+ * Print a fresh dashboard URL by HMAC-authing to the dashboard process's
597
+ * loopback rotation endpoint. Each call invalidates the previously-issued
598
+ * token, so sharing a URL is the same as sharing a one-shot session.
599
+ */
600
+ async function cmdDashboard() {
601
+ const SECRET_PATH = join(CONFIG_DIR, '.dashboard-secret');
602
+ if (!existsSync(SECRET_PATH)) {
603
+ console.error('Dashboard not initialised. Run `pnpm daemon:restart` first.');
604
+ process.exit(1);
605
+ }
606
+ const secret = readFileSync(SECRET_PATH, 'utf8').trim();
607
+ const ts = Math.floor(Date.now() / 1000).toString();
608
+ const nonce = randomBytes(8).toString('hex');
609
+ const sig = createHmac('sha256', secret).update(`${ts}:${nonce}`).digest('base64url');
610
+ const port = process.env.BOTMUX_DASHBOARD_PORT ?? '7891';
611
+ let res;
612
+ try {
613
+ res = await fetch(`http://127.0.0.1:${port}/__cli/rotate`, {
614
+ method: 'POST',
615
+ headers: {
616
+ 'X-Botmux-Cli-Ts': ts,
617
+ 'X-Botmux-Cli-Nonce': nonce,
618
+ 'X-Botmux-Cli-Auth': sig,
619
+ },
620
+ });
621
+ }
622
+ catch {
623
+ console.error(`dashboard process not reachable on 127.0.0.1:${port} — \`pnpm daemon:restart\` will start it`);
624
+ process.exit(1);
625
+ }
626
+ if (!res.ok) {
627
+ console.error('Rotation failed:', res.status, await res.text());
628
+ process.exit(1);
629
+ }
630
+ const body = await res.json();
631
+ console.log(body.url);
632
+ }
553
633
  /**
554
634
  * Resolve the session data directory.
555
635
  * Priority: SESSION_DATA_DIR env > daemon breadcrumb (~/.botmux/.data-dir) > default (~/.botmux/data)
@@ -1160,6 +1240,7 @@ botmux v${getVersion()} — IM ↔ AI 编程 CLI 桥接
1160
1240
  logs 查看 daemon 日志(--lines N, --bot <index>)
1161
1241
  status 查看 daemon 状态
1162
1242
  upgrade 升级到最新版本
1243
+ dashboard 打印新的 Web Dashboard 一次性登录 URL(旧 token 同时失效)
1163
1244
  list 列出活跃会话(交互式选择并连接 tmux)
1164
1245
  --plain 纯文本表格输出(管道/脚本场景)
1165
1246
  delete <id> 关闭指定会话(支持 ID 前缀匹配)
@@ -1263,13 +1344,18 @@ function argFlag(args, flag) {
1263
1344
  return args.includes(flag);
1264
1345
  }
1265
1346
  /** Extract positional args, skipping --flag and the value that follows it
1266
- * (for --flag <value> style). --flag=value style is self-contained. */
1267
- function positionals(args) {
1347
+ * (for --flag <value> style). --flag=value style is self-contained.
1348
+ * `booleanFlags` lists flags that take no value — without this hint the
1349
+ * parser swallows the *next* arg as the flag's value, which silently eats
1350
+ * positional content (or, worse, a following --flag's value). */
1351
+ function positionals(args, booleanFlags = []) {
1268
1352
  const out = [];
1269
1353
  for (let i = 0; i < args.length; i++) {
1270
1354
  const a = args[i];
1271
1355
  if (a.startsWith('--')) {
1272
- if (!a.includes('=') && i + 1 < args.length)
1356
+ const flagName = a.includes('=') ? a.slice(0, a.indexOf('=')) : a;
1357
+ const isBoolean = booleanFlags.includes(flagName);
1358
+ if (!a.includes('=') && !isBoolean && i + 1 < args.length)
1273
1359
  i++; // skip value
1274
1360
  continue;
1275
1361
  }
@@ -1669,7 +1755,7 @@ async function cmdSend(rest) {
1669
1755
  content = readFileSync(contentFile, 'utf-8');
1670
1756
  }
1671
1757
  else {
1672
- const pos = positionals(rest);
1758
+ const pos = positionals(rest, ['--card', '--text', '--top-level']);
1673
1759
  if (pos.length > 0) {
1674
1760
  content = pos.join(' ');
1675
1761
  }
@@ -1844,7 +1930,11 @@ async function cmdSend(rest) {
1844
1930
  // be the session owner), plus a `cc` line listing oncall owners so they
1845
1931
  // stay informed. Non-oncall: keep owner-only behaviour.
1846
1932
  const footerParts = ['[botmux](https://github.com/deepcoldy/botmux)'];
1847
- const addressing = buildFooterAddressing(s, oncallEntry);
1933
+ // Top-level publish has no specific recipient — drop "发送给/cc" addressing
1934
+ // so the message doesn't @ the session owner who isn't even in the target chat.
1935
+ const addressing = sendTopLevel
1936
+ ? { sendTo: undefined, cc: [] }
1937
+ : buildFooterAddressing(s, oncallEntry);
1848
1938
  if (addressing.sendTo)
1849
1939
  footerParts.push(`发送给:<at id=${addressing.sendTo}></at>`);
1850
1940
  if (addressing.cc.length > 0) {
@@ -1901,8 +1991,11 @@ async function cmdSend(rest) {
1901
1991
  }
1902
1992
  // Footer: mirror the card layout — a blank paragraph separates the body
1903
1993
  // from the addressing line(s). `发送给: @<caller>` always; oncall groups
1904
- // additionally get `cc: @<owners>` on the next line.
1905
- const addressing = buildFooterAddressing(s, oncallEntry);
1994
+ // additionally get `cc: @<owners>` on the next line. Top-level publish
1995
+ // has no specific recipient — skip addressing entirely.
1996
+ const addressing = sendTopLevel
1997
+ ? { sendTo: undefined, cc: [] }
1998
+ : buildFooterAddressing(s, oncallEntry);
1906
1999
  if (addressing.sendTo || addressing.cc.length > 0) {
1907
2000
  if (postContent.length > 0)
1908
2001
  postContent.push([{ tag: 'text', text: '' }]);
@@ -2106,6 +2199,9 @@ switch (command) {
2106
2199
  case 'upgrade':
2107
2200
  cmdUpgrade();
2108
2201
  break;
2202
+ case 'dashboard':
2203
+ await cmdDashboard();
2204
+ break;
2109
2205
  case 'list':
2110
2206
  case 'ls':
2111
2207
  await cmdList();