Skip to content
Closed
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
6 changes: 3 additions & 3 deletions src/adapters/cli/claude-code.ts
Original file line number Diff line number Diff line change
Expand Up @@ -389,9 +389,9 @@ const claudeFirstWriteSeen = new WeakSet<PtyHandle>();
/** A member of the Claude-family CLIs: Claude Code itself and forks that share
* its on-disk session layout (per-project JSONL transcripts, `sessions/<pid>.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;
Expand Down
19 changes: 9 additions & 10 deletions src/adapters/cli/relay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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/<pid>.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/<pid>.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.
*
Expand Down
124 changes: 100 additions & 24 deletions src/adapters/cli/seed.ts
Original file line number Diff line number Diff line change
@@ -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/<pid>.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.
*
* `which seed` returns an ephemeral fnm/nvm shim (e.g.
* `/run/user/.../fnm_multishells/<pid>_.../bin/seed`); realpath follows the
* Works for both Seed and Relay — same package layout: `dist/cli.js` → two
* levels up is the package root → `.claude-runtime` inside it.
*
* `which <bin>` returns an ephemeral fnm/nvm shim (e.g.
* `/run/user/.../fnm_multishells/<pid>_.../bin/<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
* `<bin>` 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 {
Expand All @@ -43,23 +54,88 @@ 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 {
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 bin: string;
let binName: 'relay' | 'seed';

if (pathOverride) {
bin = resolveCommand(pathOverride);
binName = detectBinName(bin);
} else {
const relayBin = resolveCommand('relay');
if (isAbsolute(relayBin)) {
bin = relayBin;
binName = 'relay';
} else {
bin = resolveCommand('seed');
binName = 'seed';
}
}

const dataDir = deriveSeedDataDir(bin);

if (!pathOverride) {
logger.info(`[seed] using ${binName} binary at ${bin}`);
} else {
logger.info(`[seed] using override binary at ${bin} (detected as ${binName})`);
}

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);
}
Expand Down
12 changes: 12 additions & 0 deletions src/core/session-discovery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,12 @@ const CLI_COMM_MAP: Record<string, CliId> = {
// 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',
Expand Down Expand Up @@ -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;
}

Expand Down
2 changes: 1 addition & 1 deletion src/i18n/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -571,7 +571,7 @@ export const messages: Record<string, string> = {
'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): ',
Expand Down
2 changes: 1 addition & 1 deletion src/i18n/zh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -574,7 +574,7 @@ export const messages: Record<string, string> = {
'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,逗号分隔,留空=不限制): ',
Expand Down
2 changes: 1 addition & 1 deletion src/im/lark/card-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ export function buildConfigTextCard(data: ConfigCardData, locale?: Locale): stri

const cliDisplayNames: Record<CliId, string> = {
'claude-code': 'Claude',
'seed': 'Seed',
'seed': 'Seed/Relay',
'relay': 'Relay',
'aiden': 'Aiden',
'coco': 'CoCo',
Expand Down
2 changes: 1 addition & 1 deletion src/services/voice/sami.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down
2 changes: 1 addition & 1 deletion src/setup/bot-config-editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ const CLI_DISPLAY_LABELS: Record<CliId, string> = {
'hermes': 'Hermes',
'codex-app': 'Codex App',
'mira': 'Mira',
'seed': 'Seed',
'seed': 'Seed/Relay',
'traex': 'TRAE',
'pi': 'Pi',
'copilot': 'Copilot',
Expand Down
2 changes: 1 addition & 1 deletion src/setup/cli-selection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ const CJADK_X_CODEX: CliSelectOption = { key: 'cjadk-x-codex', label: 'CJADK ×
const CJADK_VARIANTS: ReadonlyArray<CliSelectOption> = [CJADK_X_CLAUDE, CJADK_X_CODEX];

// ─── ttadk 选项 ──────────────────────────────────────────────────────────────
// ttadk(@byted/ttadk)跟 cjadk 一样是网关装配启动器:`ttadk <子命令>` 是
// ttadk 跟 cjadk 一样是网关装配启动器:`ttadk <子命令>` 是
// `ttadk code -t <tool>` 的快捷写法,启动真实 CLI 前注入网关鉴权 env。与 cjadk
// 的关键差异:ttadk 默认会弹「交互式选模型菜单」卡住 PTY,靠 **`-m <model>`** 跳过
// (而非 cjadk 的 CJADK_INTERACTIVE env 开关),故模型对 managed 模型类 CLI 必填。
Expand Down
2 changes: 1 addition & 1 deletion src/worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ function ensureZellijAttachConfig(): string {

let sessionId = '';
let lastInitConfig: Extract<DaemonToWorker, { type: 'init' }> | null = null;
const CLI_DISPLAY_NAMES: Record<string, string> = { '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<string, string> = { '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. */
Expand Down
2 changes: 1 addition & 1 deletion test/relay-adapter.test.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down
Loading