From ad08291b893d14951d8f02cf65087dac1a166377 Mon Sep 17 00:00:00 2001 From: sensuossss Date: Wed, 17 Jun 2026 12:39:29 +0800 Subject: [PATCH 1/2] =?UTF-8?q?feat(seed):=20seed=20=E9=80=82=E9=85=8D?= =?UTF-8?q?=E5=99=A8=E4=BC=98=E5=85=88=E7=94=A8=20relay=20=E4=BA=8C?= =?UTF-8?q?=E8=BF=9B=E5=88=B6=EF=BC=8Cfallback=20=E5=9B=9E=20seed?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Seed 已升级为 Relay,seed 适配器现在优先探测 `relay` 二进制; 找不到时 fallback 回 `seed`。适配器 id 保持 'seed' 不变,现有 bot 配置无需改动。 - createSeedAdapter: relay 优先 + seed fallback,resumeBin 跟随实际二进制 - 显示名统一为 "Seed/Relay"(setup / worker / card-builder 三处 + i18n) - session-discovery: seed 过滤器也匹配 relay 进程(同 mtr/opencode 模式) 同时清理了注释中的内部 npm 包名引用(@bytedance-*、@byted/*、@byted-sami/*)。 Co-Authored-By: Claude Opus 4.6 --- src/adapters/cli/claude-code.ts | 6 +-- src/adapters/cli/relay.ts | 19 ++++---- src/adapters/cli/seed.ts | 84 +++++++++++++++++++++++---------- src/core/session-discovery.ts | 12 +++++ src/i18n/en.ts | 2 +- src/i18n/zh.ts | 2 +- src/im/lark/card-builder.ts | 2 +- src/services/voice/sami.ts | 2 +- src/setup/bot-config-editor.ts | 2 +- src/setup/cli-selection.ts | 2 +- src/worker.ts | 2 +- test/relay-adapter.test.ts | 2 +- test/seed-adapter.test.ts | 2 +- 13 files changed, 93 insertions(+), 46 deletions(-) diff --git a/src/adapters/cli/claude-code.ts b/src/adapters/cli/claude-code.ts index 8ea60beb..b8e1cea4 100644 --- a/src/adapters/cli/claude-code.ts +++ b/src/adapters/cli/claude-code.ts @@ -389,9 +389,9 @@ const claudeFirstWriteSeen = new WeakSet(); /** A member of the Claude-family CLIs: Claude Code itself and forks that share * its on-disk session layout (per-project JSONL transcripts, `sessions/.json` * pid-state, `tasks/` fd locks, keybindings.json, settings.json hooks) but - * relocate the data root and/or rename the binary. Seed CLI - * (`@bytedance-seed/claude-code`, binary `seed`) is one such fork — it reuses - * this entire adapter, only swapping `dataDir`/`stateJsonPath`/binary. */ + * relocate the data root and/or rename the binary. Seed CLI (binary `seed`) is + * one such fork — it reuses this entire adapter, only swapping + * `dataDir`/`stateJsonPath`/binary. */ export interface ClaudeFamilyVariant { /** CliId for this variant (`claude-code`, `seed`, …). */ readonly id: CliId; diff --git a/src/adapters/cli/relay.ts b/src/adapters/cli/relay.ts index 234851d4..ca5371d5 100644 --- a/src/adapters/cli/relay.ts +++ b/src/adapters/cli/relay.ts @@ -6,19 +6,18 @@ import { createClaudeFamilyAdapter } from './claude-code.js'; import { logger } from '../../utils/logger.js'; import type { CliAdapter } from './types.js'; -/** Relay CLI (`@bytedance-relay/claude-code`, binary `relay`) is the current - * release name of what used to ship as Seed — a ByteDance fork of Claude Code. - * It is identical to Claude Code in flags, slash commands and on-disk session - * layout (per-project JSONL transcripts, `sessions/.json`, `tasks/` fd - * locks, keybindings.json, settings.json hooks); it differs only in the binary - * name, its auth (ByteCloud / bytedcli / SuperRelay), and its data root — which - * it isolates to a `.claude-runtime` directory *inside its own install package* - * (rather than `~/.claude`), respecting `CLAUDE_CONFIG_DIR` when set. +/** Relay CLI (binary `relay`) is the current release name of what used to ship + * as Seed — a fork of Claude Code. It is identical to Claude Code in flags, + * slash commands and on-disk session layout (per-project JSONL transcripts, + * `sessions/.json`, `tasks/` fd locks, keybindings.json, settings.json + * hooks); it differs only in the binary name, its auth, and its data root — + * which it isolates to a `.claude-runtime` directory *inside its own install + * package* (rather than `~/.claude`), respecting `CLAUDE_CONFIG_DIR` when set. * * Relay and Seed share the same package internals, so this adapter is a near * clone of `seed.ts`; it lives as its own file (and CliId) because they are - * distinct binaries with distinct npm packages — a user may have either one, - * or both, on PATH, and botmux must spawn/resume each by its real name. */ + * distinct binaries — a user may have either one, or both, on PATH, and + * botmux must spawn/resume each by its real name. */ /** Derive Relay's `.claude-runtime` data root from the resolved binary. * diff --git a/src/adapters/cli/seed.ts b/src/adapters/cli/seed.ts index 24d8c167..fe826a3d 100644 --- a/src/adapters/cli/seed.ts +++ b/src/adapters/cli/seed.ts @@ -1,35 +1,46 @@ import { realpathSync } from 'node:fs'; import { homedir } from 'node:os'; -import { dirname, join } from 'node:path'; +import { dirname, isAbsolute, join } from 'node:path'; import { resolveCommand } from './registry.js'; import { createClaudeFamilyAdapter } from './claude-code.js'; import { logger } from '../../utils/logger.js'; import type { CliAdapter } from './types.js'; -/** Seed CLI (`@bytedance-seed/claude-code`, binary `seed`) is a ByteDance fork - * of Claude Code: identical flags, slash commands, and on-disk session layout +/** Seed CLI (binary `seed`) is a fork of Claude Code. It has since been + * rebranded as **Relay CLI** (binary `relay`). + * + * Both share identical flags, slash commands, and on-disk session layout * (per-project JSONL transcripts, `sessions/.json`, `tasks/` fd locks, - * keybindings.json, settings.json hooks). It differs only in the binary name, - * its auth, and its data root — which it isolates to a - * `.claude-runtime` directory *inside its own install package* (rather than - * `~/.claude`), respecting `CLAUDE_CONFIG_DIR` when set. + * keybindings.json, settings.json hooks). They differ only in the binary name + * and the data root — each isolates its `.claude-runtime` directory *inside + * its own install package* (rather than `~/.claude`), respecting + * `CLAUDE_CONFIG_DIR` when set. + * + * **Fallback behavior**: this adapter prefers the newer `relay` binary when + * available on PATH (since Seed has been superseded by Relay). If `relay` is + * not found it falls back to the legacy `seed` binary. The adapter's `id` + * remains `'seed'` for backward compatibility with existing bot configs — + * users don't need to reconfigure anything to get the new binary. * * So Seed reuses the entire Claude-family adapter; the only work here is - * locating that `.claude-runtime` so botmux watches exactly where Seed writes. */ + * locating the right `.claude-runtime` so botmux watches exactly where the + * CLI writes. */ -/** Derive Seed's `.claude-runtime` data root from the resolved binary. +/** Derive the `.claude-runtime` data root from the resolved binary. + * + * Works for both Seed and Relay — same package layout: `dist/cli.js` → two + * levels up is the package root → `.claude-runtime` inside it. * - * `which seed` returns an ephemeral fnm/nvm shim (e.g. - * `/run/user/.../fnm_multishells/_.../bin/seed`); realpath follows the + * `which ` returns an ephemeral fnm/nvm shim (e.g. + * `/run/user/.../fnm_multishells/_.../bin/`); realpath follows the * symlink chain to the package's `dist/cli.js`, whose package root is two - * levels up. `.claude-runtime` sits at that package root. Deriving from the - * binary on every spawn means a node/fnm switch (which moves the binary) + * levels up. Deriving from the binary on every spawn means a node/fnm switch * auto-tracks to the matching runtime dir — and it equals the path a bare - * `seed` uses by default, so botmux-spawned and hand-started Seed sessions + * `` uses by default, so botmux-spawned and hand-started sessions * share one config (settings, history, cross-resume). * * Falls back to `~/.claude-runtime` only if realpath fails (unusual install - * layout) — Seed still runs, but the JSONL bridge may target the wrong dir; + * layout) — the CLI still runs, but the JSONL bridge may target the wrong dir; * we log so it's diagnosable rather than silently degraded. */ export function deriveSeedDataDir(bin: string): string { try { @@ -44,22 +55,47 @@ export function deriveSeedDataDir(bin: string): string { } export function createSeedAdapter(pathOverride?: string): CliAdapter { - const bin = resolveCommand(pathOverride ?? 'seed'); + // When no explicit path is given, prefer the newer `relay` binary (Seed has + // been rebranded to Relay). Fall back to legacy `seed` if relay is not + // installed. An explicit pathOverride always wins as-is. + let binName: 'relay' | 'seed' = 'seed'; + let bin: string; + + if (pathOverride) { + bin = resolveCommand(pathOverride); + } else { + const relayBin = resolveCommand('relay'); + if (isAbsolute(relayBin)) { + bin = relayBin; + binName = 'relay'; + } else { + bin = resolveCommand('seed'); + } + } + const dataDir = deriveSeedDataDir(bin); + + if (!pathOverride) { + logger.info(`[seed] using ${binName} binary at ${bin}`); + } + return createClaudeFamilyAdapter({ id: 'seed', + // Seed / Relay both use bytedcli login state — keep the bytedcli dir + // real + writable inside the file sandbox so token refresh/login persist. authPaths: ['~/.local/share/bytedcli'], - resumeBin: 'seed', + resumeBin: binName, dataDir, - // Seed keeps `.claude.json` inside its data root (CLAUDE_CONFIG_DIR layout), - // unlike Claude Code which puts it at `~/.claude.json`. + // Seed / Relay keeps `.claude.json` inside its data root (CLAUDE_CONFIG_DIR + // layout), unlike Claude Code which puts it at `~/.claude.json`. stateJsonPath: join(dataDir, '.claude.json'), - // Pin CLAUDE_CONFIG_DIR to Seed's own default so the dir botmux watches and - // the dir Seed writes to are provably identical — and still equal to what a - // hand-started `seed` resolves, preserving config alignment. + // Pin CLAUDE_CONFIG_DIR to the CLI's own default so the dir botmux watches + // and the dir the CLI writes to are provably identical — and still equal + // to what a hand-started `seed` / `relay` resolves, preserving config + // alignment. spawnEnv: { CLAUDE_CONFIG_DIR: dataDir }, - // Seed's model set is gateway-defined, not the - // Anthropic aliases — skip the setup model prompt; users pick via /model. + // Seed/Relay's model set is gateway-defined, not the Anthropic aliases — + // skip the setup model prompt; users pick via /model. modelChoices: undefined, }, bin); } diff --git a/src/core/session-discovery.ts b/src/core/session-discovery.ts index 79cba203..b06789c5 100644 --- a/src/core/session-discovery.ts +++ b/src/core/session-discovery.ts @@ -49,6 +49,12 @@ const CLI_COMM_MAP: Record = { // from adopting the other's (or claude's) sessions. seed: 'seed', relay: 'relay', + // Seed adapter prefers the `relay` binary when available (Seed has been + // rebranded to Relay). When a seed-configured bot scans for adoptable + // sessions or resolves wrapperCli child pids, a running `relay` process + // should also count as a match — same way mtr accepts opendir processes. + // The reverse (relay bot accepting seed processes) is not needed since + // relay is the newer name and explicitly-relay configs target relay. codex: 'codex', aiden: 'aiden', coco: 'coco', @@ -88,6 +94,12 @@ export function cliIdForComm(comm: string, filterCliId?: CliId): CliId | undefin // native process as "opencode". When an MTR bot asks to adopt, treat that // process as MTR so the bot's filter does not hide its own sessions. if (filterCliId === 'mtr' && direct === 'opencode') return 'mtr'; + // Seed adapter prefers the `relay` binary when available (Seed has been + // rebranded to Relay). When a seed-configured bot scans for sessions or + // resolves wrapperCli child pids, a running `relay` process should also + // count as a match. The reverse is not needed — relay is the newer name + // and explicitly-relay configs target relay. + if (filterCliId === 'seed' && direct === 'relay') return 'seed'; return direct; } diff --git a/src/i18n/en.ts b/src/i18n/en.ts index cba59720..77dc31c7 100644 --- a/src/i18n/en.ts +++ b/src/i18n/en.ts @@ -571,7 +571,7 @@ export const messages: Record = { 'setup.lark_perm_chat': ' - im:chat (group info)', 'setup.lark_perm_user_base': ' - contact:user.base:readonly (user info)', 'setup.lark_enable_events': 'Enable Event Subscription (WebSocket mode):', - 'setup.supported_clis': 'Supported CLIs: 1) claude-code 2) aiden 3) coco 4) codex 5) cursor 6) gemini 7) opencode 8) antigravity 9) mtr 10) hermes 11) codex-app 12) mira 13) seed 14) traex 15) pi 16) copilot 17) oh-my-pi 18) relay', + 'setup.supported_clis': 'Supported CLIs: 1) claude-code 2) aiden 3) coco 4) codex 5) cursor 6) gemini 7) opencode 8) antigravity 9) mtr 10) hermes 11) codex-app 12) mira 13) seed/relay 14) traex 15) pi 16) copilot 17) oh-my-pi 18) relay', 'setup.prompt_cli_choice': 'CLI adapter [1]: ', 'setup.prompt_working_dir': 'Default working directory [~]: ', 'setup.prompt_allowed_users': 'Allowed users (emails or open_ids, comma-separated; empty = no restriction): ', diff --git a/src/i18n/zh.ts b/src/i18n/zh.ts index a667f02f..eb2296d0 100644 --- a/src/i18n/zh.ts +++ b/src/i18n/zh.ts @@ -574,7 +574,7 @@ export const messages: Record = { 'setup.lark_perm_chat': ' - im:chat (群信息)', 'setup.lark_perm_user_base': ' - contact:user.base:readonly (用户信息)', 'setup.lark_enable_events': '启用事件订阅 (WebSocket 模式):', - 'setup.supported_clis': '支持的 CLI: 1) claude-code 2) aiden 3) coco 4) codex 5) cursor 6) gemini 7) opencode 8) antigravity 9) mtr 10) hermes 11) codex-app 12) mira 13) seed 14) traex 15) pi 16) copilot 17) oh-my-pi 18) relay', + 'setup.supported_clis': '支持的 CLI: 1) claude-code 2) aiden 3) coco 4) codex 5) cursor 6) gemini 7) opencode 8) antigravity 9) mtr 10) hermes 11) codex-app 12) mira 13) seed/relay 14) traex 15) pi 16) copilot 17) oh-my-pi 18) relay', 'setup.prompt_cli_choice': 'CLI 适配器 [1]: ', 'setup.prompt_working_dir': '默认工作目录 [~]: ', 'setup.prompt_allowed_users': '允许的用户 (邮箱或 open_id,逗号分隔,留空=不限制): ', diff --git a/src/im/lark/card-builder.ts b/src/im/lark/card-builder.ts index fdcd0067..e8205cf7 100644 --- a/src/im/lark/card-builder.ts +++ b/src/im/lark/card-builder.ts @@ -174,7 +174,7 @@ export function buildConfigTextCard(data: ConfigCardData, locale?: Locale): stri const cliDisplayNames: Record = { 'claude-code': 'Claude', - 'seed': 'Seed', + 'seed': 'Seed/Relay', 'relay': 'Relay', 'aiden': 'Aiden', 'coco': 'CoCo', diff --git a/src/services/voice/sami.ts b/src/services/voice/sami.ts index d1160284..45c3b1f6 100644 --- a/src/services/voice/sami.ts +++ b/src/services/voice/sami.ts @@ -1,7 +1,7 @@ /** * SAMI (ByteDance speech) TTS adapter — pure JSON-over-WebSocket, no SDK. * - * The official `@byted-sami/speech-sdk` is a browser bundle (needs window/ + * The official speech SDK is a browser bundle (needs window/ * document shims + a subprocess), but the `tts.sync` endpoint it drives is * dead simple: send ONE JSON text frame, receive binary audio frames + JSON * status frames. We reimplement just that here in ~40 lines so the SAMI engine diff --git a/src/setup/bot-config-editor.ts b/src/setup/bot-config-editor.ts index 591ad519..c964037c 100644 --- a/src/setup/bot-config-editor.ts +++ b/src/setup/bot-config-editor.ts @@ -41,7 +41,7 @@ const CLI_DISPLAY_LABELS: Record = { 'hermes': 'Hermes', 'codex-app': 'Codex App', 'mira': 'Mira', - 'seed': 'Seed', + 'seed': 'Seed/Relay', 'traex': 'TRAE', 'pi': 'Pi', 'copilot': 'Copilot', diff --git a/src/setup/cli-selection.ts b/src/setup/cli-selection.ts index dc8c5305..15eafa55 100644 --- a/src/setup/cli-selection.ts +++ b/src/setup/cli-selection.ts @@ -68,7 +68,7 @@ const CJADK_X_CODEX: CliSelectOption = { key: 'cjadk-x-codex', label: 'CJADK × const CJADK_VARIANTS: ReadonlyArray = [CJADK_X_CLAUDE, CJADK_X_CODEX]; // ─── ttadk 选项 ────────────────────────────────────────────────────────────── -// ttadk(@byted/ttadk)跟 cjadk 一样是网关装配启动器:`ttadk <子命令>` 是 +// ttadk 跟 cjadk 一样是网关装配启动器:`ttadk <子命令>` 是 // `ttadk code -t ` 的快捷写法,启动真实 CLI 前注入网关鉴权 env。与 cjadk // 的关键差异:ttadk 默认会弹「交互式选模型菜单」卡住 PTY,靠 **`-m `** 跳过 // (而非 cjadk 的 CJADK_INTERACTIVE env 开关),故模型对 managed 模型类 CLI 必填。 diff --git a/src/worker.ts b/src/worker.ts index 35629e7b..3871e194 100644 --- a/src/worker.ts +++ b/src/worker.ts @@ -152,7 +152,7 @@ function ensureZellijAttachConfig(): string { let sessionId = ''; let lastInitConfig: Extract | null = null; -const CLI_DISPLAY_NAMES: Record = { 'claude-code': 'Claude', seed: 'Seed', relay: 'Relay', aiden: 'Aiden', coco: 'CoCo', codex: 'Codex', 'codex-app': 'Codex App', cursor: 'Cursor', gemini: 'Gemini', opencode: 'OpenCode', antigravity: 'Antigravity', mtr: 'MTR', hermes: 'Hermes', mira: 'Mira', traex: 'TRAE', pi: 'Pi', copilot: 'Copilot', 'oh-my-pi': 'Oh My Pi' }; +const CLI_DISPLAY_NAMES: Record = { 'claude-code': 'Claude', seed: 'Seed/Relay', relay: 'Relay', aiden: 'Aiden', coco: 'CoCo', codex: 'Codex', 'codex-app': 'Codex App', cursor: 'Cursor', gemini: 'Gemini', opencode: 'OpenCode', antigravity: 'Antigravity', mtr: 'MTR', hermes: 'Hermes', mira: 'Mira', traex: 'TRAE', pi: 'Pi', copilot: 'Copilot', 'oh-my-pi': 'Oh My Pi' }; function cliName(): string { return CLI_DISPLAY_NAMES[lastInitConfig?.cliId ?? ''] ?? 'CLI'; } let isPromptReady = false; /** Mutex for async flushPending — prevents concurrent flush loops. */ diff --git a/test/relay-adapter.test.ts b/test/relay-adapter.test.ts index fe9951cf..ef13ddee 100644 --- a/test/relay-adapter.test.ts +++ b/test/relay-adapter.test.ts @@ -1,7 +1,7 @@ /** * relay-adapter.test.ts * - * Relay CLI (`@bytedance-relay/claude-code`) is the current release name of the + * Relay CLI (binary `relay`) is the current release name of the * Seed fork — a Claude Code fork that reuses the entire Claude-family adapter, * only relocating its data root to the package's `.claude-runtime` and renaming * the binary. These tests lock in the variant wiring: data-root derivation, diff --git a/test/seed-adapter.test.ts b/test/seed-adapter.test.ts index 2706b037..cd11861f 100644 --- a/test/seed-adapter.test.ts +++ b/test/seed-adapter.test.ts @@ -1,7 +1,7 @@ /** * seed-adapter.test.ts * - * Seed CLI (`@bytedance-seed/claude-code`) is a Claude Code fork that reuses the + * Seed CLI (binary `seed`) is a Claude Code fork that reuses the * entire Claude-family adapter, only relocating its data root to the package's * `.claude-runtime` and renaming the binary. These tests lock in the variant * wiring: data-root derivation, CLAUDE_CONFIG_DIR injection, the `.claude.json` From fa00a5ce322c37671de8dfafc0f4cd1575a3980a Mon Sep 17 00:00:00 2001 From: sensuossss Date: Wed, 17 Jun 2026 13:03:36 +0800 Subject: [PATCH 2/2] =?UTF-8?q?fix(seed):=20pathOverride=20=E6=8C=87?= =?UTF-8?q?=E5=90=91=20relay=20=E4=BA=8C=E8=BF=9B=E5=88=B6=E6=97=B6=20resu?= =?UTF-8?q?meBin=20=E8=B7=9F=E9=9A=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit createSeedAdapter 用 pathOverride 时,之前 binName 硬编码 seed,导致 override 到 relay 二进制时,closed-session card 展示 seed --resume,用户复制去跑会 从 seed 的默认 runtime 找 transcript,而真实会话写在 relay runtime 里。 修复:新增 detectBinName(),根据二进制 basename + realpath 后的包路径 (scoped package @*-relay 或裸 relay 目录)识别是 seed 还是 relay 变种, resumeBin / dataDir 都跟实际二进制对齐。同时加了 4 个回归测试。 Co-Authored-By: Claude Opus 4.6 --- src/adapters/cli/seed.ts | 42 +++++++++++++++++++++++++++- test/seed-adapter.test.ts | 59 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 100 insertions(+), 1 deletion(-) diff --git a/src/adapters/cli/seed.ts b/src/adapters/cli/seed.ts index fe826a3d..7b0c66a3 100644 --- a/src/adapters/cli/seed.ts +++ b/src/adapters/cli/seed.ts @@ -54,15 +54,52 @@ export function deriveSeedDataDir(bin: string): string { } } +/** Given a resolved binary path (may be a shim symlink or the real cli.js), + * determine whether it's the Seed or Relay variant. + * + * Heuristics (in order), checked against the realpath-resolved binary: + * 1. If the binary's own basename is `relay` → relay + * 2. If the binary's own basename is `seed` → seed + * 3. Walk up from the binary and look for a path component exactly named + * `relay` (unscoped package) + * 4. Walk up and look for a `@*` scope directory whose name *ends with* + * `-relay` (scoped package, e.g. `@scope-relay/claude-code`) + * 5. Otherwise → seed (safe default) + * + * We walk the realpath components so symlinks / shims don't hide the + * underlying package. We only match whole path segments (not substrings) + * to avoid false positives from parent directories that happen to contain + * the string "relay" (e.g. temp dirs, project names). + */ +function detectBinName(bin: string): 'relay' | 'seed' { + const base = bin.split('/').pop() ?? ''; + if (base === 'relay') return 'relay'; + if (base === 'seed') return 'seed'; + try { + const real = realpathSync(bin); + const parts = real.split('/'); + // Check each path segment + for (let i = 0; i < parts.length; i++) { + const part = parts[i]!; + if (part === 'relay') return 'relay'; + if (part.startsWith('@') && part.endsWith('-relay')) return 'relay'; + } + } catch { + // realpath failed — fall through to default + } + return 'seed'; +} + export function createSeedAdapter(pathOverride?: string): CliAdapter { // When no explicit path is given, prefer the newer `relay` binary (Seed has // been rebranded to Relay). Fall back to legacy `seed` if relay is not // installed. An explicit pathOverride always wins as-is. - let binName: 'relay' | 'seed' = 'seed'; let bin: string; + let binName: 'relay' | 'seed'; if (pathOverride) { bin = resolveCommand(pathOverride); + binName = detectBinName(bin); } else { const relayBin = resolveCommand('relay'); if (isAbsolute(relayBin)) { @@ -70,6 +107,7 @@ export function createSeedAdapter(pathOverride?: string): CliAdapter { binName = 'relay'; } else { bin = resolveCommand('seed'); + binName = 'seed'; } } @@ -77,6 +115,8 @@ export function createSeedAdapter(pathOverride?: string): CliAdapter { if (!pathOverride) { logger.info(`[seed] using ${binName} binary at ${bin}`); + } else { + logger.info(`[seed] using override binary at ${bin} (detected as ${binName})`); } return createClaudeFamilyAdapter({ diff --git a/test/seed-adapter.test.ts b/test/seed-adapter.test.ts index cd11861f..a25909fe 100644 --- a/test/seed-adapter.test.ts +++ b/test/seed-adapter.test.ts @@ -110,3 +110,62 @@ describe('createSeedAdapter', () => { expect(dirname(adapter.claudeStateJsonPath!)).toBe(expectedDataDir); }); }); + +describe('createSeedAdapter with relay-path pathOverride', () => { + // When pathOverride points to a Relay binary, the adapter should detect it + // and use 'relay' as resumeBin so the closed-session card shows the right + // resume command — not 'seed --resume' which would fail because the + // transcript lives in relay's runtime dir. + const root = realpathSync(mkdtempSync(join(tmpdir(), 'seed-ovr-test-'))); + + // Scoped package layout (mirrors real npm install): + // node_modules/@scope-relay/claude-code/dist/cli.js + // node_modules/@scope-seed/claude-code/dist/cli.js + const relayPkg = join(root, 'node_modules', '@scope-relay', 'claude-code'); + const seedPkg = join(root, 'node_modules', '@scope-seed', 'claude-code'); + mkdirSync(join(relayPkg, 'dist'), { recursive: true }); + mkdirSync(join(seedPkg, 'dist'), { recursive: true }); + writeFileSync(join(relayPkg, 'dist', 'cli.js'), '// relay'); + writeFileSync(join(seedPkg, 'dist', 'cli.js'), '// seed'); + + // Case 1: direct path to cli.js inside a relay scoped package + const relayCliJs = join(relayPkg, 'dist', 'cli.js'); + + // Case 2: shim symlink named "relay" pointing to cli.js + const shimDir = join(root, 'shim', 'bin'); + mkdirSync(shimDir, { recursive: true }); + const relayShim = join(shimDir, 'relay'); + symlinkSync(relayCliJs, relayShim); + + // Case 3: shim symlink named "seed" pointing to seed cli.js (baseline) + const seedShim = join(shimDir, 'seed'); + symlinkSync(join(seedPkg, 'dist', 'cli.js'), seedShim); + + it('detects relay from scoped package path (@scope-relay/claude-code)', () => { + const a = createSeedAdapter(relayCliJs); + expect(a.id).toBe('seed'); + expect(a.claudeDataDir).toBe(join(relayPkg, '.claude-runtime')); + expect(a.buildResumeCommand?.({ sessionId: 'sid', cliSessionId: 'cli-sid' })) + .toBe('relay --resume cli-sid'); + }); + + it('detects relay from a shim symlink whose basename is "relay"', () => { + const a = createSeedAdapter(relayShim); + expect(a.id).toBe('seed'); + expect(a.claudeDataDir).toBe(join(relayPkg, '.claude-runtime')); + expect(a.buildResumeCommand?.({ sessionId: 'sid', cliSessionId: 'cli-sid' })) + .toBe('relay --resume cli-sid'); + }); + + it('keeps seed resumeBin for a seed scoped package path (baseline)', () => { + const a = createSeedAdapter(join(seedPkg, 'dist', 'cli.js')); + expect(a.buildResumeCommand?.({ sessionId: 'sid', cliSessionId: 'cli-sid' })) + .toBe('seed --resume cli-sid'); + }); + + it('keeps seed resumeBin for a shim named "seed" (baseline)', () => { + const a = createSeedAdapter(seedShim); + expect(a.buildResumeCommand?.({ sessionId: 'sid', cliSessionId: 'cli-sid' })) + .toBe('seed --resume cli-sid'); + }); +});