Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)}]: `);
Expand Down
2 changes: 1 addition & 1 deletion src/i18n/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ export const messages: Record<string, string> = {
'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',
Expand Down
2 changes: 1 addition & 1 deletion src/i18n/zh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ export const messages: Record<string, string> = {
'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 对话',
Expand Down
88 changes: 79 additions & 9 deletions src/setup/ensure-herdr-integrations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -30,6 +32,10 @@ const CLI_TO_HERDR_INTEGRATION: Partial<Record<CliId, string>> = {
'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[];
Expand All @@ -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[];
}
Expand Down Expand Up @@ -78,18 +92,71 @@ function listInstalledIntegrations(): Set<string> | 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<HerdrIntegrationResult['traexPlugin']> {
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,
};
}

Expand All @@ -105,7 +172,9 @@ export async function ensureHerdrIntegrations(cliIds: Iterable<CliId>): Promise<
const seenCli = new Set<CliId>(cliIds);
const unsupportedCliIds: CliId[] = [];
const targetIntegrations = new Set<string>();
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);
Expand All @@ -122,6 +191,7 @@ export async function ensureHerdrIntegrations(cliIds: Iterable<CliId>): Promise<
unsupportedCliIds,
};

if (wantsTraex) result.traexPlugin = ensureTraexPlugin();
if (targetIntegrations.size === 0) return result;

const alreadyInstalled = listInstalledIntegrations();
Expand All @@ -136,7 +206,7 @@ export async function ensureHerdrIntegrations(cliIds: Iterable<CliId>): 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}` });
}
}

Expand Down
15 changes: 13 additions & 2 deletions src/setup/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,12 +156,23 @@ export async function ensureDependencies(): Promise<DependenciesReport> {
}

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(
Expand Down
2 changes: 2 additions & 0 deletions src/setup/setup-args.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,8 @@ export const SETUP_CLI_USAGE = `botmux setup — 脚本化(非 TUI)用法
--wrapper-cli <prefix> 通用启动前缀(如 "aiden x claude"),覆盖 --cli 推导值
--model <m> CLI 模型名
--backend <b> 会话后端 pty | tmux | herdr | zellij
traex + herdr 会在 start/restart 时提示并尝试安装
https://github.com/Phoobobo/herdr-traex-integration
--working-dir <dirs> 仓库选择卡片的扫描根目录(逗号分隔多个)
--default-working-dir <d> 固定默认目录:新话题直接在此目录启动、不弹仓库
选择卡片;传 - 清空、回到弹卡模式
Expand Down