botmux 2.71.5 → 2.73.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/dist/adapters/backend/tmux-pipe-backend.d.ts +6 -0
- package/dist/adapters/backend/tmux-pipe-backend.d.ts.map +1 -1
- package/dist/adapters/backend/tmux-pipe-backend.js +28 -0
- package/dist/adapters/backend/tmux-pipe-backend.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +175 -121
- package/dist/cli.js.map +1 -1
- package/dist/core/dashboard-ipc-server.d.ts.map +1 -1
- package/dist/core/dashboard-ipc-server.js +134 -2
- package/dist/core/dashboard-ipc-server.js.map +1 -1
- package/dist/core/dashboard-rows.d.ts +4 -0
- package/dist/core/dashboard-rows.d.ts.map +1 -1
- package/dist/core/dashboard-rows.js +4 -0
- package/dist/core/dashboard-rows.js.map +1 -1
- package/dist/core/session-board.d.ts +9 -0
- package/dist/core/session-board.d.ts.map +1 -0
- package/dist/core/session-board.js +24 -0
- package/dist/core/session-board.js.map +1 -0
- package/dist/daemon.d.ts.map +1 -1
- package/dist/daemon.js +6 -4
- package/dist/daemon.js.map +1 -1
- package/dist/dashboard/federated-group-core.d.ts.map +1 -1
- package/dist/dashboard/federated-group-core.js +5 -0
- package/dist/dashboard/federated-group-core.js.map +1 -1
- package/dist/dashboard/federation-api.d.ts.map +1 -1
- package/dist/dashboard/federation-api.js +70 -1
- package/dist/dashboard/federation-api.js.map +1 -1
- package/dist/dashboard/federation-spoke-api.d.ts +16 -2
- package/dist/dashboard/federation-spoke-api.d.ts.map +1 -1
- package/dist/dashboard/federation-spoke-api.js +96 -3
- package/dist/dashboard/federation-spoke-api.js.map +1 -1
- package/dist/dashboard/registry.d.ts +6 -1
- package/dist/dashboard/registry.d.ts.map +1 -1
- package/dist/dashboard/registry.js +13 -1
- package/dist/dashboard/registry.js.map +1 -1
- package/dist/dashboard/web/app.d.ts.map +1 -1
- package/dist/dashboard/web/app.js +52 -0
- package/dist/dashboard/web/app.js.map +1 -1
- package/dist/dashboard/web/i18n.d.ts.map +1 -1
- package/dist/dashboard/web/i18n.js +64 -0
- package/dist/dashboard/web/i18n.js.map +1 -1
- package/dist/dashboard/web/kanban-model.d.ts +19 -0
- package/dist/dashboard/web/kanban-model.d.ts.map +1 -0
- package/dist/dashboard/web/kanban-model.js +40 -0
- package/dist/dashboard/web/kanban-model.js.map +1 -0
- package/dist/dashboard/web/preferences.d.ts +12 -1
- package/dist/dashboard/web/preferences.d.ts.map +1 -1
- package/dist/dashboard/web/preferences.js +32 -1
- package/dist/dashboard/web/preferences.js.map +1 -1
- package/dist/dashboard/web/sessions.d.ts.map +1 -1
- package/dist/dashboard/web/sessions.js +1059 -10
- package/dist/dashboard/web/sessions.js.map +1 -1
- package/dist/dashboard-web/app.js +519 -433
- package/dist/dashboard-web/index.html +3 -0
- package/dist/dashboard-web/style.css +692 -1
- package/dist/dashboard.js +72 -3
- package/dist/dashboard.js.map +1 -1
- package/dist/im/lark/client.d.ts +8 -0
- package/dist/im/lark/client.d.ts.map +1 -1
- package/dist/im/lark/client.js +32 -0
- package/dist/im/lark/client.js.map +1 -1
- package/dist/index-daemon.js +9 -0
- package/dist/index-daemon.js.map +1 -1
- package/dist/services/hook-runner.d.ts +10 -0
- package/dist/services/hook-runner.d.ts.map +1 -1
- package/dist/services/hook-runner.js +53 -19
- package/dist/services/hook-runner.js.map +1 -1
- package/dist/services/team-board-store.d.ts +33 -0
- package/dist/services/team-board-store.d.ts.map +1 -0
- package/dist/services/team-board-store.js +88 -0
- package/dist/services/team-board-store.js.map +1 -0
- package/dist/services/team-groups-store.d.ts +8 -0
- package/dist/services/team-groups-store.d.ts.map +1 -0
- package/dist/services/team-groups-store.js +31 -0
- package/dist/services/team-groups-store.js.map +1 -0
- package/dist/setup/bot-config-editor.d.ts +3 -2
- package/dist/setup/bot-config-editor.d.ts.map +1 -1
- package/dist/setup/bot-config-editor.js +1 -2
- package/dist/setup/bot-config-editor.js.map +1 -1
- package/dist/types.d.ts +5 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/worker.js +1 -1
- package/dist/worker.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* botmux setup --no-open-platform-auto — skip Feishu Open Platform automation
|
|
8
8
|
* botmux start — start daemon (pm2)
|
|
9
9
|
* botmux stop — stop daemon
|
|
10
|
-
* botmux restart
|
|
10
|
+
* botmux restart [--include-pm2] — restart daemon (optionally restart PM2 God too)
|
|
11
11
|
* botmux logs [--lines] — view daemon logs
|
|
12
12
|
* botmux status — show daemon status
|
|
13
13
|
* botmux upgrade — upgrade to latest version
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
* botmux autostart enable|disable|status — manage boot-time autostart (launchd / user systemd)
|
|
19
19
|
* botmux worker-budget status|set|unset — inspect/override idle worker suspension budget
|
|
20
20
|
*/
|
|
21
|
-
import { execSync, spawnSync, spawn } from 'node:child_process';
|
|
21
|
+
import { execSync, execFileSync, spawnSync, spawn } from 'node:child_process';
|
|
22
22
|
import { existsSync, mkdirSync, copyFileSync, readFileSync, writeFileSync, renameSync, readdirSync, readlinkSync, appendFileSync, statSync, unlinkSync } from 'node:fs';
|
|
23
23
|
import { atomicWriteFileSync } from './utils/atomic-write.js';
|
|
24
24
|
import { join, dirname, basename } from 'node:path';
|
|
@@ -35,7 +35,6 @@ import { tmuxEnv } from './setup/ensure-tmux.js';
|
|
|
35
35
|
import { writeBotsJsonAtomic as writeBotsAtomic } from './setup/bots-store.js';
|
|
36
36
|
import { applyBotConfigEdits, assertUniqueBotProcessNames, botProcessName, normalizeBotConfig, parseBotConfigsJson, parseBotSelection, removeBotConfig, resolveCliId, assertOwnerWhenChatGroups, findInvalidAllowedUserEntries, hasOwnerEntry, } from './setup/bot-config-editor.js';
|
|
37
37
|
import { buildPreset, serializePreset, presetFilename } from './setup/agent-preset.js';
|
|
38
|
-
import { createCliAdapterSync } from './adapters/cli/registry.js';
|
|
39
38
|
import { logger } from './utils/logger.js';
|
|
40
39
|
import { invalidWorkingDirs } from './utils/working-dir.js';
|
|
41
40
|
import { firstPositional } from './cli/arg-utils.js';
|
|
@@ -297,63 +296,6 @@ function printInputHelp(title, lines) {
|
|
|
297
296
|
console.log(` ${line}`);
|
|
298
297
|
}
|
|
299
298
|
}
|
|
300
|
-
/**
|
|
301
|
-
* 读取指定 CLI 适配器声明的候选 model 列表 —— 不支持 model 配置的 CLI(aiden/
|
|
302
|
-
* antigravity/mtr 等没声明 modelChoices)返回 null,promptModel 据此整段
|
|
303
|
-
* 跳过提问。适配器解析失败也按"不支持"处理,避免在 setup 中冒出陌生堆栈。
|
|
304
|
-
*/
|
|
305
|
-
function cliModelChoices(cliId) {
|
|
306
|
-
try {
|
|
307
|
-
const adapter = createCliAdapterSync(cliId);
|
|
308
|
-
return adapter.modelChoices && adapter.modelChoices.length > 0
|
|
309
|
-
? adapter.modelChoices
|
|
310
|
-
: null;
|
|
311
|
-
}
|
|
312
|
-
catch {
|
|
313
|
-
return null;
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
/**
|
|
317
|
-
* 询问"指定 CLI 用哪个 model"。
|
|
318
|
-
* - cliId 对应的 adapter 没声明 modelChoices → return undefined(跳过提问)
|
|
319
|
-
* - 用户输入序号 → 取候选列表对应项
|
|
320
|
-
* - 输入空 + 提供 current → 保留当前值(current 透传出去)
|
|
321
|
-
* - 输入空 + 无 current → return undefined(用 CLI 默认)
|
|
322
|
-
* - 输入 `-` → return null(语义:清空,调用方据此 delete model 字段)
|
|
323
|
-
* - 输入自由文本 → 原样返回
|
|
324
|
-
*/
|
|
325
|
-
async function promptModel(rl, cliId, current) {
|
|
326
|
-
const choices = cliModelChoices(cliId);
|
|
327
|
-
if (!choices)
|
|
328
|
-
return undefined;
|
|
329
|
-
const numbered = choices.map((m, i) => `${i + 1}) ${m}`).join(' ');
|
|
330
|
-
printInputHelp(`CLI Model(${cliId})`, [
|
|
331
|
-
'可选。在 spawn CLI 时注入 model 参数;同一 CLI 配多个 bot 可以跑不同 model。',
|
|
332
|
-
`候选: ${numbered} ${choices.length + 1}) Other(自定义输入)`,
|
|
333
|
-
current
|
|
334
|
-
? '留空保留当前值;输入 - 清空(回到 CLI 默认)。'
|
|
335
|
-
: '留空 = 不设置(用 CLI 默认)。',
|
|
336
|
-
]);
|
|
337
|
-
const label = current ? formatOptionalValue(current) : '未设置';
|
|
338
|
-
const raw = (await ask(rl, `CLI Model [${label}]: `)).trim();
|
|
339
|
-
if (!raw)
|
|
340
|
-
return current ?? undefined;
|
|
341
|
-
if (raw === '-')
|
|
342
|
-
return null;
|
|
343
|
-
const numIdx = Number(raw);
|
|
344
|
-
if (Number.isInteger(numIdx) && numIdx >= 1 && numIdx <= choices.length) {
|
|
345
|
-
return choices[numIdx - 1];
|
|
346
|
-
}
|
|
347
|
-
if (Number.isInteger(numIdx) && numIdx === choices.length + 1) {
|
|
348
|
-
// 选了 Other,进一步要 free-form
|
|
349
|
-
const customRaw = (await ask(rl, '请输入 model 名: ')).trim();
|
|
350
|
-
if (!customRaw)
|
|
351
|
-
return current ?? undefined;
|
|
352
|
-
return customRaw;
|
|
353
|
-
}
|
|
354
|
-
// 直接当成自由输入
|
|
355
|
-
return raw;
|
|
356
|
-
}
|
|
357
299
|
// Thin wrapper around setup/bots-store.writeBotsJsonAtomic so call-sites keep
|
|
358
300
|
// the same name without passing BOTS_JSON_FILE explicitly each time.
|
|
359
301
|
function writeBotsJsonAtomic(bots) {
|
|
@@ -630,7 +572,6 @@ async function promptBotConfig(rl) {
|
|
|
630
572
|
return null;
|
|
631
573
|
}
|
|
632
574
|
const workingDir = await ask(rl, '默认工作目录 [~]: ');
|
|
633
|
-
const modelChoice = await promptModel(rl, cliId);
|
|
634
575
|
const bot = {
|
|
635
576
|
larkAppId: creds.appId,
|
|
636
577
|
larkAppSecret: creds.appSecret,
|
|
@@ -645,10 +586,8 @@ async function promptBotConfig(rl) {
|
|
|
645
586
|
if (creds.brand === 'lark') {
|
|
646
587
|
bot.brand = 'lark';
|
|
647
588
|
}
|
|
648
|
-
//
|
|
649
|
-
|
|
650
|
-
bot.model = modelChoice;
|
|
651
|
-
}
|
|
589
|
+
// setup 不再询问 model(用户常选到无权限的 model,setup 完一发消息就 spawn
|
|
590
|
+
// 报错,排查成本高)。需要指定 model 走 /config 卡片或手动编辑 bots.json。
|
|
652
591
|
// 扫码场景默认填扫码人自己 (registerApp 返回里有 open_id), 天然就是 owner.
|
|
653
592
|
// 优先解析成 union_id (on_,跨应用稳定);失败则 fallback 到 open_id (ou_)。
|
|
654
593
|
// 手动 fallback 场景没 open_id —— 必须显式指定 owner, 否则配置无 owner:
|
|
@@ -723,29 +662,14 @@ async function promptEditBotConfig(rl, bot) {
|
|
|
723
662
|
'留空保留当前值;输入 - 清空覆盖,回到 PATH 查 cliId 对应的默认二进制。',
|
|
724
663
|
]);
|
|
725
664
|
input.cliPathOverride = await ask(rl, `CLI 可执行文件路径覆盖 [${formatOptionalValue(bot.cliPathOverride)}]: `);
|
|
726
|
-
//
|
|
727
|
-
//
|
|
728
|
-
//
|
|
729
|
-
// string = 设值
|
|
730
|
-
// 用本轮编辑后的 cliId 而非 bot.cliId —— 用户可能刚换了 CLI。
|
|
731
|
-
const effectiveCliIdForModel = (resolveCliId(input.cliChoice) ?? bot.cliId ?? 'claude-code');
|
|
665
|
+
// setup 不再询问 model(同 promptBotConfig 的理由)。但切换 CLI 时旧 model
|
|
666
|
+
// 是上一个 CLI 的值,套到新 CLI 上没意义甚至直接 spawn 报错,必须强制清空;
|
|
667
|
+
// 未换 CLI 时 input.model 留 undefined,applyBotConfigEdits 保持原值不动。
|
|
732
668
|
const cliChanged = !!resolveCliId(input.cliChoice) && resolveCliId(input.cliChoice) !== bot.cliId;
|
|
733
|
-
if (cliChanged &&
|
|
734
|
-
|
|
735
|
-
// 即使原本配了 model 也要主动清空,避免 bots.json 里残留陈旧字段——
|
|
736
|
-
// 否则用户后面再换回支持 model 的 adapter 一路回车时,旧 model
|
|
737
|
-
// 会被当作"当前值"保留下来误套到新 CLI 上。
|
|
738
|
-
console.log('\n⚠️ 新 CLI 不支持 --model 参数,已清空原 model 字段。');
|
|
669
|
+
if (cliChanged && bot.model) {
|
|
670
|
+
console.log('\n⚠️ 已切换 CLI,原 model 字段已清空(如需指定 model 请用 /config 卡片或编辑 bots.json)。');
|
|
739
671
|
input.model = null;
|
|
740
672
|
}
|
|
741
|
-
else {
|
|
742
|
-
const promptCurrent = cliChanged ? undefined : (typeof bot.model === 'string' ? bot.model : undefined);
|
|
743
|
-
const result = await promptModel(rl, effectiveCliIdForModel, promptCurrent);
|
|
744
|
-
// 切换 CLI 时哪怕用户留空也要清掉旧 model —— 旧值是上一个 CLI 的 model,
|
|
745
|
-
// 套到新 CLI 上没意义。result === undefined 在"未变 CLI"分支等价于"保留旧值",
|
|
746
|
-
// 但在 cliChanged 分支等价于"用户没指定,回到新 CLI 默认",必须 force null。
|
|
747
|
-
input.model = cliChanged && result === undefined ? null : result;
|
|
748
|
-
}
|
|
749
673
|
printInputHelp('会话后端 backendType', [
|
|
750
674
|
'可选。pty 更轻量;tmux 支持 adopt 和 Web Terminal 附着;herdr 支持托管持久会话;zellij 为实验后端(需 zellij >= 0.44)。',
|
|
751
675
|
'留空保留当前值;输入 - 回到自动检测;接受 pty / tmux / herdr / zellij。',
|
|
@@ -1181,6 +1105,32 @@ function deleteAllBotmuxProcesses(home = PM2_HOME) {
|
|
|
1181
1105
|
}
|
|
1182
1106
|
catch { /* pm2 not running or no apps */ }
|
|
1183
1107
|
}
|
|
1108
|
+
function killPm2GodDaemon(home = PM2_HOME) {
|
|
1109
|
+
try {
|
|
1110
|
+
execSync(`${pm2Bin()} kill`, {
|
|
1111
|
+
stdio: 'inherit',
|
|
1112
|
+
env: pm2Env(home),
|
|
1113
|
+
timeout: 15_000,
|
|
1114
|
+
});
|
|
1115
|
+
return;
|
|
1116
|
+
}
|
|
1117
|
+
catch {
|
|
1118
|
+
// Fall back to direct pid cleanup below.
|
|
1119
|
+
}
|
|
1120
|
+
for (const pid of listPm2GodDaemonPids(home)) {
|
|
1121
|
+
try {
|
|
1122
|
+
process.kill(pid, 'SIGTERM');
|
|
1123
|
+
}
|
|
1124
|
+
catch { /* already gone */ }
|
|
1125
|
+
}
|
|
1126
|
+
for (const pid of listPm2GodDaemonPids(home)) {
|
|
1127
|
+
try {
|
|
1128
|
+
process.kill(pid, 0);
|
|
1129
|
+
process.kill(pid, 'SIGKILL');
|
|
1130
|
+
}
|
|
1131
|
+
catch { /* already gone */ }
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1184
1134
|
/**
|
|
1185
1135
|
* One-time migration for users upgrading from versions that used the default
|
|
1186
1136
|
* ~/.pm2 directory. Removes any lingering botmux-* processes registered under
|
|
@@ -1249,6 +1199,7 @@ async function cmdRestart() {
|
|
|
1249
1199
|
process.exit(1);
|
|
1250
1200
|
}
|
|
1251
1201
|
ensureConfigDir();
|
|
1202
|
+
const includePm2 = process.argv.includes('--include-pm2');
|
|
1252
1203
|
// Drop a restart-intent breadcrumb so the fresh daemon knows this was an
|
|
1253
1204
|
// intentional restart and DMs the owner a summary. `IfAbsent` preserves a
|
|
1254
1205
|
// richer breadcrumb (update / auto-restart) already written by the
|
|
@@ -1266,6 +1217,9 @@ async function cmdRestart() {
|
|
|
1266
1217
|
cleanupLegacyPm2();
|
|
1267
1218
|
// Delete all botmux processes (handles both old single-process and new multi-process)
|
|
1268
1219
|
deleteAllBotmuxProcesses();
|
|
1220
|
+
if (includePm2) {
|
|
1221
|
+
killPm2GodDaemon();
|
|
1222
|
+
}
|
|
1269
1223
|
// Wipe abandoned dashboard-daemon descriptors left behind by killed daemons.
|
|
1270
1224
|
cleanupStaleDaemonDescriptors();
|
|
1271
1225
|
runPm2(['start', cfg]);
|
|
@@ -1727,11 +1681,13 @@ function formatSessionRow(s, multiBot, botLabels, cols) {
|
|
|
1727
1681
|
}
|
|
1728
1682
|
const title = padEndDisplay(truncate((s.title || '(untitled)').replace(/[\r\n]+/g, ' '), cols.title), cols.title);
|
|
1729
1683
|
const dir = padEndDisplay(truncate(s.workingDir || '-', cols.dir), cols.dir);
|
|
1730
|
-
const
|
|
1684
|
+
const displayPid = sessionDisplayPid(s);
|
|
1685
|
+
const pid = displayPid ? String(displayPid).padEnd(cols.pid) : '-'.padEnd(cols.pid);
|
|
1731
1686
|
const uptime = formatDuration(Date.now() - new Date(s.createdAt).getTime()).padEnd(cols.uptime);
|
|
1732
|
-
const alive =
|
|
1733
|
-
const status = (
|
|
1734
|
-
|
|
1687
|
+
const alive = isSessionAliveForList(s);
|
|
1688
|
+
const status = padEndDisplay(sessionStatusLabel(s), cols.status);
|
|
1689
|
+
const target = padEndDisplay(truncate(sessionTargetLabel(s), cols.target), cols.target);
|
|
1690
|
+
parts.push(title, dir, pid, uptime, status, target);
|
|
1735
1691
|
return { text: parts.join(' │ '), alive };
|
|
1736
1692
|
}
|
|
1737
1693
|
/** Print plain session table (non-interactive). */
|
|
@@ -1743,11 +1699,11 @@ function printSessionTable(active) {
|
|
|
1743
1699
|
const b = botConfigs[i];
|
|
1744
1700
|
botLabels.set(b.larkAppId, `bot${i + 1} (${b.cliId ?? 'claude-code'})`);
|
|
1745
1701
|
}
|
|
1746
|
-
const cols = { id: 10, ...(multiBot ? { bot: 22 } : {}), title: 28, dir: 28, pid: 8, uptime: 8, status: 8 };
|
|
1702
|
+
const cols = { id: 10, ...(multiBot ? { bot: 22 } : {}), title: 28, dir: 28, pid: 8, uptime: 8, status: 8, target: 26 };
|
|
1747
1703
|
const headerParts = ['id'.padEnd(cols.id)];
|
|
1748
1704
|
if (multiBot)
|
|
1749
1705
|
headerParts.push('bot'.padEnd(cols.bot));
|
|
1750
|
-
headerParts.push('title'.padEnd(cols.title), 'working dir'.padEnd(cols.dir), 'pid'.padEnd(cols.pid), 'uptime'.padEnd(cols.uptime), 'status'.padEnd(cols.status));
|
|
1706
|
+
headerParts.push('title'.padEnd(cols.title), 'working dir'.padEnd(cols.dir), 'pid'.padEnd(cols.pid), 'uptime'.padEnd(cols.uptime), 'status'.padEnd(cols.status), 'target'.padEnd(cols.target));
|
|
1751
1707
|
const header = headerParts.join(' │ ');
|
|
1752
1708
|
const separator = '─'.repeat(displayWidth(header));
|
|
1753
1709
|
console.log(separator);
|
|
@@ -1770,6 +1726,66 @@ function tmuxSessionExists(name) {
|
|
|
1770
1726
|
return false;
|
|
1771
1727
|
}
|
|
1772
1728
|
}
|
|
1729
|
+
function applyTmuxWindowSizeLargest(sessionName) {
|
|
1730
|
+
try {
|
|
1731
|
+
execFileSync('tmux', ['set-option', '-t', sessionName, 'window-size', 'largest'], {
|
|
1732
|
+
stdio: 'ignore',
|
|
1733
|
+
timeout: 3000,
|
|
1734
|
+
env: tmuxEnv(),
|
|
1735
|
+
});
|
|
1736
|
+
}
|
|
1737
|
+
catch { /* best-effort: attach can still proceed */ }
|
|
1738
|
+
}
|
|
1739
|
+
function isAdoptedSession(s) {
|
|
1740
|
+
return !!s.adoptedFrom && typeof s.adoptedFrom === 'object';
|
|
1741
|
+
}
|
|
1742
|
+
function adoptedCliPid(s) {
|
|
1743
|
+
const pid = isAdoptedSession(s) ? s.adoptedFrom.originalCliPid : undefined;
|
|
1744
|
+
return typeof pid === 'number' && pid > 0 ? pid : undefined;
|
|
1745
|
+
}
|
|
1746
|
+
function adoptTargetLabel(s) {
|
|
1747
|
+
if (!isAdoptedSession(s))
|
|
1748
|
+
return '';
|
|
1749
|
+
const a = s.adoptedFrom;
|
|
1750
|
+
if (a.source === 'zellij' || a.zellijPaneId) {
|
|
1751
|
+
const target = a.zellijSession && a.zellijPaneId
|
|
1752
|
+
? `${a.zellijSession}/${a.zellijPaneId}`
|
|
1753
|
+
: a.zellijPaneId || a.zellijSession || '?';
|
|
1754
|
+
return `adopt: zellij ${target}`;
|
|
1755
|
+
}
|
|
1756
|
+
if (a.source === 'herdr' || a.herdrSessionName || a.herdrPaneId || a.herdrTarget) {
|
|
1757
|
+
const pane = a.herdrTarget ?? a.herdrPaneId ?? '?';
|
|
1758
|
+
const target = a.herdrSessionName ? `${a.herdrSessionName}:${pane}` : pane;
|
|
1759
|
+
return `adopt: herdr ${target}`;
|
|
1760
|
+
}
|
|
1761
|
+
return `adopt: tmux ${a.tmuxTarget ?? '?'}`;
|
|
1762
|
+
}
|
|
1763
|
+
function sessionDisplayPid(s) {
|
|
1764
|
+
return adoptedCliPid(s) ?? s.pid;
|
|
1765
|
+
}
|
|
1766
|
+
function isSessionAliveForList(s) {
|
|
1767
|
+
const pid = sessionDisplayPid(s);
|
|
1768
|
+
return !!(pid && isProcessAlive(pid));
|
|
1769
|
+
}
|
|
1770
|
+
function sessionStatusLabel(s) {
|
|
1771
|
+
if (isAdoptedSession(s)) {
|
|
1772
|
+
const pid = adoptedCliPid(s);
|
|
1773
|
+
if (pid)
|
|
1774
|
+
return isProcessAlive(pid) ? 'adopt' : 'stopped';
|
|
1775
|
+
return s.pid && isProcessAlive(s.pid) ? 'adopt' : 'idle';
|
|
1776
|
+
}
|
|
1777
|
+
return s.pid && isProcessAlive(s.pid) ? 'online' : s.pid ? 'stopped' : 'idle';
|
|
1778
|
+
}
|
|
1779
|
+
function sessionTargetLabel(s, tmuxName, hasTmux) {
|
|
1780
|
+
if (isAdoptedSession(s))
|
|
1781
|
+
return adoptTargetLabel(s);
|
|
1782
|
+
if (hasTmux === undefined) {
|
|
1783
|
+
const name = tmuxName ?? `bmx-${s.sessionId.substring(0, 8)}`;
|
|
1784
|
+
hasTmux = tmuxSessionExists(name);
|
|
1785
|
+
tmuxName = name;
|
|
1786
|
+
}
|
|
1787
|
+
return hasTmux ? `tmux: ${tmuxName}` : '-';
|
|
1788
|
+
}
|
|
1773
1789
|
/** Shorten path for display: replace $HOME with ~. */
|
|
1774
1790
|
function shortenPath(p) {
|
|
1775
1791
|
const home = homedir();
|
|
@@ -1788,10 +1804,10 @@ function interactiveSessionPicker(active) {
|
|
|
1788
1804
|
const termWidth = process.stdout.columns || 100;
|
|
1789
1805
|
const PREFIX = 4; // " ❯ " or " "
|
|
1790
1806
|
const SEP_W = 3; // " │ "
|
|
1791
|
-
const fixedCols = { id: 10, pid: 8, uptime: 7, status: 7 };
|
|
1807
|
+
const fixedCols = { id: 10, pid: 8, uptime: 7, status: 7, target: 26 };
|
|
1792
1808
|
const botW = multiBot ? 18 : 0;
|
|
1793
|
-
const numSeps = (multiBot ?
|
|
1794
|
-
const fixedTotal = PREFIX + fixedCols.id + botW + fixedCols.pid + fixedCols.uptime + fixedCols.status + numSeps * SEP_W;
|
|
1809
|
+
const numSeps = (multiBot ? 8 : 7) - 1; // separators between columns
|
|
1810
|
+
const fixedTotal = PREFIX + fixedCols.id + botW + fixedCols.pid + fixedCols.uptime + fixedCols.status + fixedCols.target + numSeps * SEP_W;
|
|
1795
1811
|
const flexTotal = Math.max(20, termWidth - fixedTotal);
|
|
1796
1812
|
const titleW = Math.floor(flexTotal * 0.4);
|
|
1797
1813
|
const dirW = flexTotal - titleW;
|
|
@@ -1803,10 +1819,15 @@ function interactiveSessionPicker(active) {
|
|
|
1803
1819
|
pid: fixedCols.pid,
|
|
1804
1820
|
uptime: fixedCols.uptime,
|
|
1805
1821
|
status: fixedCols.status,
|
|
1822
|
+
target: fixedCols.target,
|
|
1806
1823
|
};
|
|
1807
1824
|
// Build row data — use shortened paths for TUI
|
|
1808
1825
|
function buildRows() {
|
|
1809
1826
|
return active.map(s => {
|
|
1827
|
+
const tmuxName = `bmx-${s.sessionId.substring(0, 8)}`;
|
|
1828
|
+
const isAdopt = isAdoptedSession(s);
|
|
1829
|
+
const hasTmux = !isAdopt && tmuxSessionExists(tmuxName);
|
|
1830
|
+
const targetLabel = sessionTargetLabel(s, tmuxName, hasTmux);
|
|
1810
1831
|
// Build row text with shortened dir
|
|
1811
1832
|
const id = padEndDisplay(s.sessionId.substring(0, 8), cols.id);
|
|
1812
1833
|
const parts = [id];
|
|
@@ -1816,14 +1837,14 @@ function interactiveSessionPicker(active) {
|
|
|
1816
1837
|
}
|
|
1817
1838
|
const title = padEndDisplay(truncate((s.title || '(untitled)').replace(/[\r\n]+/g, ' '), cols.title), cols.title);
|
|
1818
1839
|
const dir = padEndDisplay(truncate(shortenPath(s.workingDir || '-'), cols.dir), cols.dir);
|
|
1819
|
-
const
|
|
1840
|
+
const displayPid = sessionDisplayPid(s);
|
|
1841
|
+
const pid = displayPid ? String(displayPid).padEnd(cols.pid) : '-'.padEnd(cols.pid);
|
|
1820
1842
|
const uptime = formatDuration(Date.now() - new Date(s.createdAt).getTime()).padEnd(cols.uptime);
|
|
1821
|
-
const alive =
|
|
1822
|
-
const status = (
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
return { session: s, text: parts.join(' │ '), alive, tmuxName, hasTmux };
|
|
1843
|
+
const alive = isSessionAliveForList(s);
|
|
1844
|
+
const status = padEndDisplay(sessionStatusLabel(s), cols.status);
|
|
1845
|
+
const target = padEndDisplay(truncate(targetLabel, cols.target), cols.target);
|
|
1846
|
+
parts.push(title, dir, pid, uptime, status, target);
|
|
1847
|
+
return { session: s, text: parts.join(' │ '), alive, tmuxName, hasTmux, isAdopt, targetLabel, canAttach: hasTmux && !isAdopt };
|
|
1827
1848
|
});
|
|
1828
1849
|
}
|
|
1829
1850
|
let rows = buildRows();
|
|
@@ -1832,7 +1853,7 @@ function interactiveSessionPicker(active) {
|
|
|
1832
1853
|
const hParts = ['id'.padEnd(cols.id)];
|
|
1833
1854
|
if (multiBot)
|
|
1834
1855
|
hParts.push('bot'.padEnd(cols.bot));
|
|
1835
|
-
hParts.push('title'.padEnd(cols.title), 'working dir'.padEnd(cols.dir), 'pid'.padEnd(cols.pid), 'uptime'.padEnd(cols.uptime), 'status'.padEnd(cols.status));
|
|
1856
|
+
hParts.push('title'.padEnd(cols.title), 'working dir'.padEnd(cols.dir), 'pid'.padEnd(cols.pid), 'uptime'.padEnd(cols.uptime), 'status'.padEnd(cols.status), 'target'.padEnd(cols.target));
|
|
1836
1857
|
return hParts.join(' │ ');
|
|
1837
1858
|
}
|
|
1838
1859
|
const header = buildHeader();
|
|
@@ -1866,10 +1887,12 @@ function interactiveSessionPicker(active) {
|
|
|
1866
1887
|
process.stdout.write(` ${separator}\n`);
|
|
1867
1888
|
// Footer info
|
|
1868
1889
|
const selected = rows[cursor];
|
|
1869
|
-
const
|
|
1870
|
-
? `\x1b[
|
|
1871
|
-
:
|
|
1872
|
-
|
|
1890
|
+
const targetHint = selected.isAdopt
|
|
1891
|
+
? `\x1b[33m${selected.targetLabel}\x1b[0m \x1b[2mEnter 已禁用;请直接使用原 tmux/zellij/herdr 客户端。\x1b[0m`
|
|
1892
|
+
: selected.hasTmux
|
|
1893
|
+
? `\x1b[32mtmux: ${selected.tmuxName}\x1b[0m`
|
|
1894
|
+
: `\x1b[2mtmux: 无会话\x1b[0m`;
|
|
1895
|
+
process.stdout.write(`\n ${targetHint}\n`);
|
|
1873
1896
|
// Flash message or confirmation prompt
|
|
1874
1897
|
if (confirmDelete) {
|
|
1875
1898
|
const s = selected.session;
|
|
@@ -1882,7 +1905,7 @@ function interactiveSessionPicker(active) {
|
|
|
1882
1905
|
process.stdout.write('\n');
|
|
1883
1906
|
}
|
|
1884
1907
|
// Keybinding hints
|
|
1885
|
-
process.stdout.write(`\n \x1b[2m↑/↓ 选择 ⏎ 连接 d 删除 q 退出\x1b[0m\n`);
|
|
1908
|
+
process.stdout.write(`\n \x1b[2m↑/↓ 选择 ⏎ ${selected?.canAttach ? '连接' : '不可连接'} d 删除 q 退出\x1b[0m\n`);
|
|
1886
1909
|
}
|
|
1887
1910
|
return new Promise((resolve) => {
|
|
1888
1911
|
process.stdin.setRawMode(true);
|
|
@@ -1900,12 +1923,14 @@ function interactiveSessionPicker(active) {
|
|
|
1900
1923
|
function deleteSession(idx) {
|
|
1901
1924
|
const r = rows[idx];
|
|
1902
1925
|
const s = r.session;
|
|
1903
|
-
// Kill
|
|
1904
|
-
|
|
1926
|
+
// Kill botmux's worker process. For adopted sessions, never kill the
|
|
1927
|
+
// user's original CLI pid if an old record stored it in `pid`.
|
|
1928
|
+
const originalPid = adoptedCliPid(s);
|
|
1929
|
+
if (s.pid && s.pid !== originalPid && isProcessAlive(s.pid)) {
|
|
1905
1930
|
killProcess(s.pid);
|
|
1906
1931
|
}
|
|
1907
|
-
// Kill tmux
|
|
1908
|
-
if (r.hasTmux) {
|
|
1932
|
+
// Kill only botmux-owned tmux sessions. Adopted panes belong to the user.
|
|
1933
|
+
if (!r.isAdopt && r.hasTmux) {
|
|
1909
1934
|
try {
|
|
1910
1935
|
execSync(`tmux kill-session -t '${r.tmuxName}' 2>/dev/null`, { stdio: 'ignore', env: tmuxEnv() });
|
|
1911
1936
|
}
|
|
@@ -1970,11 +1995,17 @@ function interactiveSessionPicker(active) {
|
|
|
1970
1995
|
// Enter — attach to tmux
|
|
1971
1996
|
if (key === '\r' || key === '\n') {
|
|
1972
1997
|
const selected = rows[cursor];
|
|
1973
|
-
if (
|
|
1998
|
+
if (selected.isAdopt) {
|
|
1999
|
+
flashMsg = `\x1b[33m这是 adopt 会话;botmux 不 attach 用户 pane。目标: ${selected.targetLabel}\x1b[0m`;
|
|
2000
|
+
render();
|
|
2001
|
+
return;
|
|
2002
|
+
}
|
|
2003
|
+
if (!selected.canAttach) {
|
|
1974
2004
|
flashMsg = '\x1b[33m该会话没有 tmux,无法连接\x1b[0m';
|
|
1975
2005
|
render();
|
|
1976
2006
|
return;
|
|
1977
2007
|
}
|
|
2008
|
+
applyTmuxWindowSizeLargest(selected.tmuxName);
|
|
1978
2009
|
cleanup();
|
|
1979
2010
|
spawnSync('tmux', ['attach-session', '-t', selected.tmuxName], {
|
|
1980
2011
|
stdio: 'inherit',
|
|
@@ -2001,6 +2032,20 @@ async function cmdList() {
|
|
|
2001
2032
|
const prunedScratch = [];
|
|
2002
2033
|
const live = [];
|
|
2003
2034
|
for (const s of active) {
|
|
2035
|
+
if (isAdoptedSession(s)) {
|
|
2036
|
+
const pid = adoptedCliPid(s);
|
|
2037
|
+
if (pid && isProcessAlive(pid)) {
|
|
2038
|
+
live.push(s);
|
|
2039
|
+
}
|
|
2040
|
+
else if (pid) {
|
|
2041
|
+
pruned.push(s);
|
|
2042
|
+
}
|
|
2043
|
+
else {
|
|
2044
|
+
const hasPid = !!(s.pid && isProcessAlive(s.pid));
|
|
2045
|
+
hasPid ? live.push(s) : pruned.push(s);
|
|
2046
|
+
}
|
|
2047
|
+
continue;
|
|
2048
|
+
}
|
|
2004
2049
|
const hasPid = !!(s.pid && isProcessAlive(s.pid));
|
|
2005
2050
|
const hasTmux = tmuxSessionExists(`bmx-${s.sessionId.substring(0, 8)}`);
|
|
2006
2051
|
if (!hasPid && !hasTmux) {
|
|
@@ -2022,7 +2067,7 @@ async function cmdList() {
|
|
|
2022
2067
|
closeNow(prunedScratch);
|
|
2023
2068
|
if (pruned.length > 0) {
|
|
2024
2069
|
closeNow(pruned);
|
|
2025
|
-
console.log(`已自动清理 ${pruned.length}
|
|
2070
|
+
console.log(`已自动清理 ${pruned.length} 个不可恢复的会话(进程已退出或无可恢复后端)`);
|
|
2026
2071
|
}
|
|
2027
2072
|
// Sort by creation time, newest first
|
|
2028
2073
|
live.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
|
|
@@ -2056,6 +2101,10 @@ function cmdDelete() {
|
|
|
2056
2101
|
}
|
|
2057
2102
|
else if (target === 'stopped') {
|
|
2058
2103
|
toDelete = active.filter(s => {
|
|
2104
|
+
if (isAdoptedSession(s)) {
|
|
2105
|
+
const pid = adoptedCliPid(s);
|
|
2106
|
+
return pid ? !isProcessAlive(pid) : !(s.pid && isProcessAlive(s.pid));
|
|
2107
|
+
}
|
|
2059
2108
|
const hasPid = !!(s.pid && isProcessAlive(s.pid));
|
|
2060
2109
|
const hasTmux = tmuxSessionExists(`bmx-${s.sessionId.substring(0, 8)}`);
|
|
2061
2110
|
return !hasPid && !hasTmux;
|
|
@@ -2082,18 +2131,23 @@ function cmdDelete() {
|
|
|
2082
2131
|
}
|
|
2083
2132
|
}
|
|
2084
2133
|
for (const s of toDelete) {
|
|
2085
|
-
|
|
2086
|
-
|
|
2134
|
+
const originalPid = adoptedCliPid(s);
|
|
2135
|
+
// Kill botmux's worker process if running. For adopted sessions, never
|
|
2136
|
+
// kill the user's original CLI pid.
|
|
2137
|
+
if (s.pid && s.pid !== originalPid && isProcessAlive(s.pid)) {
|
|
2087
2138
|
killProcess(s.pid);
|
|
2088
2139
|
console.log(` killed pid ${s.pid}`);
|
|
2089
2140
|
}
|
|
2090
|
-
// Kill associated tmux session if it exists
|
|
2141
|
+
// Kill associated botmux-owned tmux session if it exists. Adopted panes
|
|
2142
|
+
// belong to the user and must be left untouched.
|
|
2091
2143
|
const tmuxName = `bmx-${s.sessionId.substring(0, 8)}`;
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2144
|
+
if (!isAdoptedSession(s)) {
|
|
2145
|
+
try {
|
|
2146
|
+
execSync(`tmux kill-session -t '${tmuxName}' 2>/dev/null`, { stdio: 'ignore', env: tmuxEnv() });
|
|
2147
|
+
console.log(` killed tmux ${tmuxName}`);
|
|
2148
|
+
}
|
|
2149
|
+
catch { /* no tmux session */ }
|
|
2095
2150
|
}
|
|
2096
|
-
catch { /* no tmux session */ }
|
|
2097
2151
|
// Mark session as closed
|
|
2098
2152
|
s.status = 'closed';
|
|
2099
2153
|
s.closedAt = new Date().toISOString();
|
|
@@ -2360,7 +2414,7 @@ botmux v${getVersion()} — IM ↔ AI 编程 CLI 桥接
|
|
|
2360
2414
|
默认使用 botmux 内置 Feishu Web QR 登录尝试自动导入权限/redirect/发布版本;可加 --no-open-platform-auto 跳过
|
|
2361
2415
|
start 启动 daemon
|
|
2362
2416
|
stop 停止 daemon
|
|
2363
|
-
restart 重启 daemon
|
|
2417
|
+
restart 重启 daemon(自动恢复活跃会话;--include-pm2 同时重启 PM2 God)
|
|
2364
2418
|
logs 查看 daemon 日志(--lines N, --bot <0-based-index|pm2-name|appId>)
|
|
2365
2419
|
status 查看 daemon 状态
|
|
2366
2420
|
upgrade 升级到最新版本
|