diff --git a/src/cli.ts b/src/cli.ts index f460ee48..41f0d21e 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -1040,6 +1040,7 @@ async function promptEditBotConfig( printInputHelp('会话后端 backendType', [ '可选。pty 更轻量;tmux 支持 adopt 和 Web Terminal 附着;herdr 支持托管持久会话;zellij 为实验后端(需 zellij >= 0.44)。', + '选择 traex + herdr 时,botmux start/restart 会提示并尝试安装 Herdr TraeX 插件:https://github.com/Phoobobo/herdr-traex-integration。', '留空保留当前值;输入 - 回到自动检测;接受 pty / tmux / herdr / zellij。', ]); input.backendType = await ask(rl, `会话后端 backendType [${formatOptionalValue(bot.backendType)}]: `); diff --git a/src/i18n/en.ts b/src/i18n/en.ts index 39731f9f..ff8845a1 100644 --- a/src/i18n/en.ts +++ b/src/i18n/en.ts @@ -150,7 +150,7 @@ export const messages: Record = { 'card.adopt.title': '📡 Choose a CLI session to adopt', 'card.adopt.placeholder_select': 'Pick a CLI session', 'card.adopt.uptime_unknown': 'unknown', - 'card.adopt.section_live': '**Take over a running session** (observe a live tmux / zellij process)', + 'card.adopt.section_live': '**Take over a running session** (observe a live tmux / herdr / zellij process)', 'card.adopt.section_resume': '**Resume a past session** (import from disk via --resume; no live process needed)', 'card.adopt.placeholder_resume': 'Pick a past session to resume', 'card.codex_app_thread.title': '📱 Choose a Codex App conversation', diff --git a/src/i18n/zh.ts b/src/i18n/zh.ts index 17c18877..85d3ae45 100644 --- a/src/i18n/zh.ts +++ b/src/i18n/zh.ts @@ -153,7 +153,7 @@ export const messages: Record = { 'card.adopt.title': '📡 选择要接入的 CLI 会话', 'card.adopt.placeholder_select': '选择 CLI 会话', 'card.adopt.uptime_unknown': '未知', - 'card.adopt.section_live': '**接管运行中的会话**(观察 tmux / zellij 里的活进程)', + 'card.adopt.section_live': '**接管运行中的会话**(观察 tmux / herdr / zellij 里的活进程)', 'card.adopt.section_resume': '**恢复历史会话**(从磁盘 resume 导入,无需进程在跑)', 'card.adopt.placeholder_resume': '选择要 resume 的历史会话', 'card.codex_app_thread.title': '📱 选择要继续的 Codex App 对话', diff --git a/src/setup/ensure-herdr-integrations.ts b/src/setup/ensure-herdr-integrations.ts index d5849a94..f3a2bf8d 100644 --- a/src/setup/ensure-herdr-integrations.ts +++ b/src/setup/ensure-herdr-integrations.ts @@ -7,8 +7,10 @@ * Per user decision in setup, we only install integrations for CLIs that * the current `bots.json` actually uses. Mappings come from * https://herdr.dev/docs/integrations/ (claude/codex/opencode/hermes are - * the ones with botmux adapter equivalents). The `pi`, `omp`, `qodercli` - * upstream integrations have no botmux adapter and are not auto-installed. + * the ones with botmux adapter equivalents). TraeX is not built into herdr + * upstream yet, so we install the community herdr plugin that wires TraeX + * hooks into herdr's state API. The `pi`, `omp`, `qodercli` upstream + * integrations have no botmux adapter and are not auto-installed. * * Like ensureTmux/ensureHerdr, this never throws — failures only generate * warnings. The caller decides whether to surface them. @@ -30,6 +32,10 @@ const CLI_TO_HERDR_INTEGRATION: Partial> = { 'hermes': 'hermes', }; +const TRAEX_PLUGIN_ID = 'com.traex.herdr-integration'; +const TRAEX_PLUGIN_SPEC = 'Phoobobo/herdr-traex-integration'; +const TRAEX_PLUGIN_INSTALL_COMMAND = `herdr plugin install ${TRAEX_PLUGIN_SPEC} --yes && herdr plugin action invoke ${TRAEX_PLUGIN_ID}.install`; + export interface HerdrIntegrationResult { /** Integrations we attempted (after dedup + filtering by available CLIs). */ attempted: string[]; @@ -38,7 +44,15 @@ export interface HerdrIntegrationResult { /** Already-present integrations we skipped. */ alreadyInstalled: string[]; /** Integrations whose `herdr integration install` returned non-zero. */ - failed: { name: string; reason: string }[]; + failed: { name: string; reason: string; manualCommand?: string }[]; + /** TraeX community herdr plugin status, when a herdr+traex bot exists. */ + traexPlugin?: { + attempted: boolean; + installed: boolean; + alreadyInstalled: boolean; + actionInvoked: boolean; + failed?: { step: 'install' | 'action'; reason: string; manualCommand: string }; + }; /** CliIds in bots.json that have no upstream herdr integration mapping. */ unsupportedCliIds: CliId[]; } @@ -78,18 +92,71 @@ function listInstalledIntegrations(): Set | undefined { } } -function installSingleIntegration(name: string): { ok: true } | { ok: false; reason: string } { - const result = spawnSync('herdr', ['integration', 'install', name], { +function spawnHerdr(args: string[], timeout = 60_000): { ok: true; stdout: string } | { ok: false; reason: string; stdout: string } { + const result = spawnSync('herdr', args, { stdio: ['ignore', 'pipe', 'pipe'], - timeout: 60_000, + timeout, encoding: 'utf-8', }); - if (result.status === 0) return { ok: true }; const stderr = (result.stderr ?? '').toString().trim(); const stdout = (result.stdout ?? '').toString().trim(); + if (result.status === 0) return { ok: true, stdout }; return { ok: false, - reason: stderr || stdout || `exit ${result.status}`, + reason: stderr || stdout || (result.error ? String(result.error.message ?? result.error) : `exit ${result.status}`), + stdout, + }; +} + +function installSingleIntegration(name: string): { ok: true } | { ok: false; reason: string } { + const result = spawnHerdr(['integration', 'install', name]); + return result.ok ? { ok: true } : { ok: false, reason: result.reason }; +} + +function isTraexPluginInstalled(): boolean { + const result = spawnHerdr(['plugin', 'list', '--json'], 5000); + if (!result.ok) return false; + try { + const parsed = JSON.parse(result.stdout); + const plugins = parsed?.result?.plugins; + return Array.isArray(plugins) && plugins.some((p: any) => p?.plugin_id === TRAEX_PLUGIN_ID); + } catch { + return result.stdout.includes(TRAEX_PLUGIN_ID); + } +} + +function ensureTraexPlugin(): NonNullable { + const alreadyInstalled = isTraexPluginInstalled(); + if (!alreadyInstalled) { + console.log(` 安装 herdr TraeX plugin: ${TRAEX_PLUGIN_SPEC}`); + const install = spawnHerdr(['plugin', 'install', TRAEX_PLUGIN_SPEC, '--yes'], 120_000); + if (!install.ok) { + return { + attempted: true, + installed: false, + alreadyInstalled: false, + actionInvoked: false, + failed: { step: 'install', reason: install.reason, manualCommand: TRAEX_PLUGIN_INSTALL_COMMAND }, + }; + } + } + + const action = spawnHerdr(['plugin', 'action', 'invoke', `${TRAEX_PLUGIN_ID}.install`], 60_000); + if (!action.ok) { + return { + attempted: true, + installed: !alreadyInstalled, + alreadyInstalled, + actionInvoked: false, + failed: { step: 'action', reason: action.reason, manualCommand: TRAEX_PLUGIN_INSTALL_COMMAND }, + }; + } + + return { + attempted: true, + installed: !alreadyInstalled, + alreadyInstalled, + actionInvoked: true, }; } @@ -105,7 +172,9 @@ export async function ensureHerdrIntegrations(cliIds: Iterable): Promise< const seenCli = new Set(cliIds); const unsupportedCliIds: CliId[] = []; const targetIntegrations = new Set(); + const wantsTraex = seenCli.has('traex'); for (const cli of seenCli) { + if (cli === 'traex') continue; // handled by the community plugin path below. const integration = CLI_TO_HERDR_INTEGRATION[cli]; if (!integration) { unsupportedCliIds.push(cli); @@ -122,6 +191,7 @@ export async function ensureHerdrIntegrations(cliIds: Iterable): Promise< unsupportedCliIds, }; + if (wantsTraex) result.traexPlugin = ensureTraexPlugin(); if (targetIntegrations.size === 0) return result; const alreadyInstalled = listInstalledIntegrations(); @@ -136,7 +206,7 @@ export async function ensureHerdrIntegrations(cliIds: Iterable): Promise< if (r.ok) { result.installed.push(name); } else { - result.failed.push({ name, reason: r.reason }); + result.failed.push({ name, reason: r.reason, manualCommand: `herdr integration install ${name}` }); } } diff --git a/src/setup/index.ts b/src/setup/index.ts index 9134ad72..142578a8 100644 --- a/src/setup/index.ts +++ b/src/setup/index.ts @@ -156,12 +156,23 @@ export async function ensureDependencies(): Promise { } function reportHerdrIntegrations(r: HerdrIntegrationResult): void { - if (r.attempted.length === 0 && r.unsupportedCliIds.length === 0) return; + if (r.attempted.length === 0 && r.unsupportedCliIds.length === 0 && !r.traexPlugin?.attempted) return; if (r.installed.length > 0) console.log(`✓ herdr integrations 已安装: ${r.installed.join(' / ')}`); if (r.alreadyInstalled.length > 0) console.log(`✓ herdr integrations (existing): ${r.alreadyInstalled.join(' / ')}`); + if (r.traexPlugin) { + if (r.traexPlugin.failed) { + console.warn(`⚠️ herdr TraeX plugin ${r.traexPlugin.failed.step === 'install' ? '安装' : '配置'}失败:${r.traexPlugin.failed.reason}`); + console.warn(` 手动尝试:${r.traexPlugin.failed.manualCommand}`); + console.warn(' 说明:herdr + traex 不装该插件也能启动,但状态只能退回屏幕启发式检测。'); + } else if (r.traexPlugin.installed) { + console.log('✓ herdr TraeX plugin 已安装并写入 ~/.trae hooks'); + } else if (r.traexPlugin.alreadyInstalled) { + console.log('✓ herdr TraeX plugin (existing),已确认 ~/.trae hooks'); + } + } for (const f of r.failed) { console.warn(`⚠️ herdr integration 安装失败: ${f.name} — ${f.reason}`); - console.warn(` 手动尝试:herdr integration install ${f.name}`); + console.warn(` 手动尝试:${f.manualCommand ?? `herdr integration install ${f.name}`}`); } if (r.unsupportedCliIds.length > 0) { console.warn( diff --git a/src/setup/setup-args.ts b/src/setup/setup-args.ts index 4a12d926..18a271ce 100644 --- a/src/setup/setup-args.ts +++ b/src/setup/setup-args.ts @@ -101,6 +101,8 @@ export const SETUP_CLI_USAGE = `botmux setup — 脚本化(非 TUI)用法 --wrapper-cli 通用启动前缀(如 "aiden x claude"),覆盖 --cli 推导值 --model CLI 模型名 --backend 会话后端 pty | tmux | herdr | zellij + traex + herdr 会在 start/restart 时提示并尝试安装 + https://github.com/Phoobobo/herdr-traex-integration --working-dir 仓库选择卡片的扫描根目录(逗号分隔多个) --default-working-dir 固定默认目录:新话题直接在此目录启动、不弹仓库 选择卡片;传 - 清空、回到弹卡模式