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
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ billion-context/
│ ├── session-id.ts # Session ID generation
│ ├── persist.ts # On-disk session persistence (kernel StateStore)
│ ├── update.ts # Auto-update: checks npm, auto-installs latest
│ ├── launcher.ts # `bili <client>` launchers (pi/codex/claude/omp/opencode/hermes)
│ ├── launcher.ts # `bili <client>` launchers (pi/codex/claude/omp/opencode/hermes/dsh/trae)
│ ├── client-config.ts # READ-only discovery of each client's upstream config
│ ├── mitm.ts / ca.ts # Cert-MITM proxying + lazily generated root CA
│ ├── mcp.ts # Plugin-in-launcher MCP shell (spawn-time injection)
Expand Down
1 change: 1 addition & 0 deletions CONFIGURATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -435,6 +435,7 @@ Full command surface (`bili --help` prints an abridged version). Precedence ever
| `bili opencode [opts --] [args]` | Proxy + **opencode** |
| `bili hermes [opts --] [args]` | Proxy + **hermes-agent** (`/bili/` rewrite) |
| `bili dsh [opts --] [args]` | Proxy + **deepseek-harness** (non-loopback upstreams via proxy envs, loopback via `/bili/` rewrite — #535; args like `--profile web "task"` pass through) |
| `bili trae [opts --] [args]` | Proxy + **Trae CLI** (ByteDance, closed Go binary) — cert-MITM via `HTTPS_PROXY` + `SSL_CERT_FILE`; model host from `TRAE_CLI_API_HOST` or the default enterprise gateway (#655) |
| `bili test pi` | Non-polluting end-to-end smoke test of the pi path |
| `bili export [session] [--full] [--output FILE]` | List persisted sessions / export one as a Markdown handoff — see [Sessions & Migration](#sessions--migration) |
| `bili update` | Check for & install a newer version now (bypasses the 3-minute throttle) |
Expand Down
1 change: 1 addition & 0 deletions CONFIGURATION.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -432,6 +432,7 @@
| `bili opencode [opts --] [args]` | 代理 + **opencode** |
| `bili hermes [opts --] [args]` | 代理 + **hermes-agent**(`/bili/` 重写) |
| `bili dsh [opts --] [args]` | 代理 + **deepseek-harness**(`/bili/` 重写;`--profile web "task"` 等参数原样透传) |
| `bili trae [opts --] [args]` | 代理 + **Trae CLI**(字节跳动,闭源 Go 二进制)—— 证书 MITM(`HTTPS_PROXY` + `SSL_CERT_FILE`);模型主机取 `TRAE_CLI_API_HOST` 或默认企业网关(#655) |
| `bili test pi` | 无污染的 pi 链路端到端冒烟测试 |
| `bili export [session] [--full] [--output FILE]` | 列出持久化会话 / 把一个会话导出为 Markdown 交接文档 —— 见[会话与迁移](#会话与迁移) |
| `bili update` | 立即检查并安装新版本(绕过 3 分钟节流) |
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ rewrite until dsh gains a settings-path env or an upstream loopback opt-out.
Overlay dirs created by older versions are left in place and never merged back
into the real home.

### Option 1 — Launcher (`bili pi` / `bili codex` / `bili claude` / `bili omp` / `bili opencode` / `bili hermes` / `bili dsh`)
### Option 1 — Launcher (`bili pi` / `bili codex` / `bili claude` / `bili omp` / `bili opencode` / `bili hermes` / `bili dsh` / `bili trae`)

The launcher wraps a client in one command: it starts a proxy on an
independent port (a fresh instance is always spawned — a port is never
Expand All @@ -177,6 +177,7 @@ bili omp # pi-style, file-free (#535): env + extens
bili opencode # MITM for HTTPS + temp opencode.json (/bili/ for HTTP) + thin /acp plugin
bili hermes # file-free (#535): hermes proxy env (HTTPS_PROXY + HERMES_CA_BUNDLE) — https via CONNECT MITM, http via absolute-form forward proxy; real ~/.hermes untouched
bili dsh # deepseek-harness: non-loopback upstreams ride proxy envs (https MITM, http absolute-form), loopback keeps the overlay DSH_HOME (~/.dsh-bili) rewrite (#535), built-in deepseek route via DEEPSEEK_BASE_URL, native /acp command injected via --patch
bili trae # Trae CLI (ByteDance, closed Go binary, no base-URL override) — cert-MITM via HTTPS_PROXY + SSL_CERT_FILE, model host from TRAE_CLI_API_HOST or the default enterprise gateway (#655)
bili pi --mitm-domain api.foo.com # add a domain to the MITM whitelist
```

Expand Down
3 changes: 2 additions & 1 deletion README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ npm install -g billion-context

bili 永不拥有用户数据:每个被启动的客户端都跑在**真实 home** 上,运行期写入落在用户预期的位置。把客户端指向代理时,启动器按优先级选择——**优先 env 变量**(hermes/dsh/codex 的代理/CA env;pi/omp 的 `BILI_PROVIDER_REWRITES` URL 清单,由扩展加载时经 `registerProvider` 消费),其次 **CLI 参数或扩展 API**(codex `-c key=value`、opencode 插件),最后才是**生成文件**——目前仅剩 opencode 的临时 `opencode.json`(退出即删)和 dsh 的回环例外:dsh 的 fetch 栈对回环目标无条件绕过代理 env,所以本地上游保留持久 `~/.dsh-bili` overlay 改写,直到 dsh 提供 settings-path env 或上游支持回环 opt-out。旧版本创建的 overlay 目录原地保留,绝不合并回真实 home。

### 方式 1 —— 启动器(`bili pi` / `bili codex` / `bili claude` / `bili omp` / `bili opencode` / `bili hermes` / `bili dsh`)
### 方式 1 —— 启动器(`bili pi` / `bili codex` / `bili claude` / `bili omp` / `bili opencode` / `bili hermes` / `bili dsh` / `bili trae`)

启动器把客户端包进一条命令:在独立端口拉起一个代理(总是全新实例,绝不复用端口),再按客户端支持的机制把它指向代理 —— 能吃代理/CA 环境变量的走**证书 MITM**,不吃的走隔离的**`/bili/` 配置重写**。真实配置文件从不被修改;客户端自己的配置只被**读取**,用来发现它实际连接的 HTTPS 上游主机,把这些主机加入 MITM 白名单 —— 代理只 TLS 终结它们,其余流量盲透传。

Expand All @@ -96,6 +96,7 @@ bili omp # pi 同款,file-free(#535):环境变量 +
bili opencode # HTTPS 走 MITM + 临时 opencode.json(HTTP 走 /bili/)+ 轻量 /acp 插件
bili hermes # file-free(#535):hermes 代理环境变量(HTTPS_PROXY + HERMES_CA_BUNDLE)—— https 走 CONNECT MITM,http 走绝对形式转发;真实 ~/.hermes 不动
bili dsh # deepseek-harness:非回环上游走代理 env(https MITM、http absolute-form),回环上游保留 overlay DSH_HOME(~/.dsh-bili)改写(#535),内置 deepseek 路由走 DEEPSEEK_BASE_URL,经 --patch 注入原生 /acp 命令
bili trae # Trae CLI(字节跳动,闭源 Go 二进制,无 base-URL 覆盖)—— 证书 MITM(HTTPS_PROXY + SSL_CERT_FILE),模型主机取 TRAE_CLI_API_HOST 或默认企业网关(#655)
bili pi --mitm-domain api.foo.com # 向 MITM 白名单追加域名
```

Expand Down
12 changes: 7 additions & 5 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ Usage:
bili opencode [opts --] [args] start a proxy + launch opencode against it (cert-MITM)
bili hermes [opts --] [args] start a proxy + launch hermes-agent against it (/bili/ rewrite)
bili dsh [opts --] [args] start a proxy + launch deepseek-harness against it (/bili/ rewrite)
bili trae [opts --] [args] start a proxy + launch Trae CLI against it (cert-MITM)
bili test pi non-polluting pi smoke test through the proxy
bili export [session] [--full] list sessions / export one as a Markdown handoff
(--full includes original messages; --output FILE)
Expand All @@ -81,12 +82,12 @@ Usage:
bili --version print version
bili --help show this help

Launcher (bili pi / bili codex / bili claude / bili omp / bili opencode / bili hermes / bili dsh):
Launcher (bili pi / bili codex / bili claude / bili omp / bili opencode / bili hermes / bili dsh / bili trae):
Brings up a proxy on an independent port (a fresh instance every launch), then runs the client pointed at it via HTTPS_PROXY + the proxy's
MITM CA — no config-file edits. Discovered HTTPS upstream domains are
auto-whitelisted for MITM so the proxy TLS-terminates exactly the hosts the
client uses; HTTP / localhost providers go direct. pi/claude trust the CA
via NODE_EXTRA_CA_CERTS, codex via SSL_CERT_FILE. Proxy killed on client exit.
client uses; HTTP / localhost providers go direct. pi/claude trust the CA
via NODE_EXTRA_CA_CERTS, codex/trae via SSL_CERT_FILE. Proxy killed on client exit.
bili flags (-F, --mitm-domain, --port, ...) must precede the client name;
everything after the client name is passed through to the client.
bili pi # launch pi through the proxy
Expand All @@ -96,8 +97,9 @@ Launcher (bili pi / bili codex / bili claude / bili omp / bili opencode / bili h
bili claude # launch claude through the proxy
bili omp # launch omp through the proxy (pi-based; /bili/ rewrite)
bili hermes # launch hermes-agent through the proxy (/bili/ rewrite of ~/.hermes/config.yaml)
bili dsh --profile web "task" # launch deepseek-harness through the proxy (/bili/ rewrite of ~/.dsh/settings.yaml)
bili test pi # quick end-to-end check of the pi path
bili dsh --profile web "task" # launch deepseek-harness through the proxy (/bili/ rewrite of ~/.dsh/settings.yaml)
bili trae # launch Trae CLI through the proxy (cert-MITM; model host via TRAE_CLI_API_HOST or --mitm-domain)
bili test pi # quick end-to-end check of the pi path
bili --mitm-domain api.foo.com pi # add a domain to the MITM whitelist (flags precede the client)
bili -F http://127.0.0.1:7897 codex # route bili's upstream through a proxy (gost-style -F)

Expand Down
35 changes: 34 additions & 1 deletion src/client-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,10 @@ export interface DshConfig {
baseUrls: string[];
}

export interface TraeConfig {
modelApiHost?: string;
}

export interface ClientConfig {
claude?: ClaudeSettings;
codex?: CodexConfig;
Expand All @@ -102,6 +106,7 @@ export interface ClientConfig {
opencode?: OpencodeConfig;
hermes?: HermesConfig;
dsh?: DshConfig;
trae?: TraeConfig;
}

export function nonEmpty(s: unknown): s is string {
Expand Down Expand Up @@ -179,6 +184,33 @@ export function readDshConfig(dshHome: string): DshConfig {
return { baseUrls: parseDshSettingsYaml(text) };
}

/** Default model API gateways for Trae CLI (ByteDance). The CLI is a Go
* binary that honors HTTPS_PROXY (Go net/http) and resolves its API host
* from TRAE_CLI_API_HOST (chatmodel.resolveBaseURL); without it the
* enterprise gateway is console.enterprise.trae.cn. These hosts are
* cert-MITM'd so `bili trae` can compress the model traffic. */
export const TRAE_DEFAULT_MODEL_HOSTS = [
"console.enterprise.trae.cn",
"www.trae.cn",
];

/** Trae CLI keeps its config under TRAE_CONFIG_DIR (default ~/.trae):
* traecli.yaml, skills, session state. */
export function resolveTraeHome(env: NodeJS.ProcessEnv): string {
const h = os.homedir();
return nonEmpty(env.TRAE_CONFIG_DIR) ? env.TRAE_CONFIG_DIR!
: path.join(h, ".trae");
}

export function readTraeConfig(env: NodeJS.ProcessEnv): TraeConfig {
const result: TraeConfig = {};
const host = nonEmpty(env.TRAE_CLI_API_HOST)
? env.TRAE_CLI_API_HOST!.replace(/^https?:\/\//i, "").replace(/\/+$/, "")
: undefined;
if (host) result.modelApiHost = host;
return result;
}

export function readClaudeSettings(homeDir: string, cwd: string, env: NodeJS.ProcessEnv = process.env): ClaudeSettings {
const files = [
path.join(homeDir, ".claude", "settings.json"),
Expand Down Expand Up @@ -570,14 +602,15 @@ export function loadClientConfig(env: NodeJS.ProcessEnv, cwd: string): ClientCon
config.opencode = readOpencodeConfig(resolveOpencodeConfigFile(env));
config.hermes = readHermesConfig(resolveHermesHome(env));
config.dsh = readDshConfig(resolveDshHome(env));
config.trae = readTraeConfig(env);
return config;
}

/** The client a launcher run targets. Scopes model-window collection so a
* launched client's own declarations are authoritative (#436: launching
* `bili omp` with omp's models.yml declaring 131072 must not be overridden by
* another client's larger declaration for the same model id). */
export type ModelWindowScope = "claude" | "codex" | "pi" | "omp" | "opencode" | "hermes" | "dsh";
export type ModelWindowScope = "claude" | "codex" | "pi" | "omp" | "opencode" | "hermes" | "dsh" | "trae";

/** Collect per-model context windows from client configs the launcher can
* read (pi models.json, omp models.yml, opencode opencode.json, codex
Expand Down
7 changes: 6 additions & 1 deletion src/discover.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { loadClientConfig, resolvePiHome, nonEmpty, type ClientConfig } from "./client-config.js";
import { loadClientConfig, resolvePiHome, resolveTraeHome, TRAE_DEFAULT_MODEL_HOSTS, nonEmpty, type ClientConfig } from "./client-config.js";

const TTL_MS = 2000;

Expand Down Expand Up @@ -43,6 +43,10 @@ export function extractHttpsHosts(config: ClientConfig): string[] {
if (config.zcode) {
for (const prov of Object.values(config.zcode.providers)) push(prov.baseURL);
}
if (config.trae) {
const hosts = nonEmpty(config.trae.modelApiHost) ? [config.trae.modelApiHost] : TRAE_DEFAULT_MODEL_HOSTS;
for (const h of hosts) push(`https://${h}`);
}
return out;
}

Expand All @@ -56,6 +60,7 @@ function configFilePaths(env: NodeJS.ProcessEnv): string[] {
path.join(codexHome, "config.toml"),
path.join(resolvePiHome(env), "models.json"),
path.join(zcodeHome, "v2", "config.json"),
path.join(resolveTraeHome(env), "traecli.yaml"),
];
}

Expand Down
49 changes: 45 additions & 4 deletions src/launcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ import { selfPackageRoot, isBiliPiEntry, ompPluginLoadedFrom } from "./plugin-in
function selfDistFile(name: string): string {
return path.join(selfPackageRoot(), "dist", name);
}
import { nonEmpty, resolvePiHome, resolveOmpHome, resolveDshHome, loadClientConfig, collectModelWindows, type ClientConfig, type CodexConfig, resolveOpencodeConfigFile, type OpencodeConfig, type OpencodeProvider, type HermesConfig, type HermesProvider } from "./client-config.js";
import { nonEmpty, resolvePiHome, resolveOmpHome, resolveDshHome, resolveTraeHome, readTraeConfig, TRAE_DEFAULT_MODEL_HOSTS, type TraeConfig, loadClientConfig, collectModelWindows, type ClientConfig, type CodexConfig, resolveOpencodeConfigFile, type OpencodeConfig, type OpencodeProvider, type HermesConfig, type HermesProvider } from "./client-config.js";
import { loadRoutes, resolveConfiguredContextLimit, lookupContextLimit, type ProviderRoutes } from "./config.js";
import { contextFromRegistry } from "./registry.js";

Expand Down Expand Up @@ -75,16 +75,20 @@ export {
readDshConfig,
parseDshSettingsYaml,
resolveDshHome,
resolveTraeHome,
readTraeConfig,
TRAE_DEFAULT_MODEL_HOSTS,
type TraeConfig,
resolveOpencodeConfigFile,
readOpencodeConfig,
type OpencodeConfig,
type OpencodeProvider,
} from "./client-config.js";

export const LAUNCHER_DEFAULT_HOST = "127.0.0.1";
export const LAUNCH_CLIENTS = ["pi", "codex", "claude", "omp", "opencode", "hermes", "dsh", "pi-test"] as const;
export const LAUNCH_CLIENTS = ["pi", "codex", "claude", "omp", "opencode", "hermes", "dsh", "trae", "pi-test"] as const;
export type ClientName = (typeof LAUNCH_CLIENTS)[number];
export type BaseClientName = "claude" | "codex" | "pi" | "omp" | "opencode" | "hermes" | "dsh";
export type BaseClientName = "claude" | "codex" | "pi" | "omp" | "opencode" | "hermes" | "dsh" | "trae";

const HEALTH_PATH = "/__bili/health";
const HEALTH_POLL_INTERVAL_MS = 200;
Expand Down Expand Up @@ -358,6 +362,24 @@ export function discoverRoutes(client: ClientName, config: ClientConfig): Discov
// Unparseable endpoint: skip.
}
}
} else if (client === "trae") {
// #655: Trae CLI is a closed Go binary (no base-URL override) that
// honors HTTPS_PROXY; the model API host is TRAE_CLI_API_HOST or the
// default enterprise gateway. Whitelist the host(s) for cert-MITM so
// the proxy can compress the model traffic. No /bili/ rewrite (the
// scheme is hardcoded https).
const hosts = nonEmpty(config.trae?.modelApiHost)
? [config.trae!.modelApiHost!]
: TRAE_DEFAULT_MODEL_HOSTS;
for (const h of hosts) {
// MITM whitelist matches the port-less SNI hostname (isMitmHost), so
// reduce host:port to its host or the entry never matches.
const host = h.split(":", 2)[0]!.toLowerCase();
if (host && !httpsSeen.has(host)) {
httpsSeen.add(host);
httpsDomains.push(host);
}
}
} else {
for (const [name, prov] of Object.entries(config.codex?.providers ?? {})) {
classify(prov.baseUrl, `model_providers.${name}.base_url`);
Expand Down Expand Up @@ -408,6 +430,12 @@ export function buildCodexEnv(origin: string, caPath: string, baseEnv: NodeJS.Pr
return { ...baseEnv, HTTPS_PROXY: origin, SSL_CERT_FILE: caPath, BILLION_CONTEXT_PROXY: origin };
}

export function buildTraeEnv(origin: string, caPath: string, baseEnv: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
// #655: trae is a Go binary like codex — the CA rides SSL_CERT_FILE (the
// combined bundle, since it replaces Go's system trust store).
return { ...baseEnv, HTTPS_PROXY: origin, SSL_CERT_FILE: caPath, BILLION_CONTEXT_PROXY: origin };
}

export function buildCodexArgs(
origin: string,
httpRewrites: HttpRewrite[],
Expand Down Expand Up @@ -615,7 +643,7 @@ function isPrivateIPv4(host: string): boolean {
* understands) is the sane default. `BILI_LAUNCHER_PLUGIN=1` forces plugin
* mode regardless of the upstream. */
export function launcherInjectMcp(env: NodeJS.ProcessEnv, base: string, codexUpstream?: string): boolean {
if (base === "pi" || base === "omp" || base === "opencode" || base === "hermes" || base === "dsh") return false;
if (base === "pi" || base === "omp" || base === "opencode" || base === "hermes" || base === "dsh" || base === "trae") return false;
if (env.BILI_LAUNCHER_PLUGIN === "0") return false;
if (base === "codex" && env.BILI_LAUNCHER_PLUGIN === undefined && codexUpstream !== undefined && isPrivateUpstreamHost(codexUpstream)) {
return false;
Expand Down Expand Up @@ -1656,6 +1684,12 @@ export function resolveClientCommand(
);
return { command: process.execPath, prefixArgs: [cli] };
}
if (client === "trae") {
const traeBin = resolveOnPath("traecli", env)
?? resolveOnPath("trae-cli", env)
?? resolveOnPath("trae", env);
return { command: traeBin ?? "traecli", prefixArgs: [] };
}
const resolved = resolveOnPath(client, env);
return { command: resolved ?? client, prefixArgs: [] };
}
Expand Down Expand Up @@ -1886,6 +1920,13 @@ export async function runLaunch(params: RunLaunchParams, deps: LauncherDeps = {}
// settings rewrite above), so it exists on every profile dsh boots.
const dshAcpPatch = writeDshAcpPatch(dshHomeDir);
if (dshAcpPatch) clientArgs = dshArgsWithPatch(clientArgs, dshAcpPatch);
} else if (base === "trae") {
// #655: cert-MITM like codex/qoder (Go binary honors HTTPS_PROXY; CA
// via SSL_CERT_FILE combined bundle). No budget env (the CLI manages
// its own context window) and no transport forcing — the wire is the
// proprietary /api/ide/v2/llm_raw_chat, recognized as OpenAI by the
// proxy.
env = buildTraeEnv(origin, resolveCombinedCaPath(process.env), stripInheritedProxy(process.env));
} else if (base === "codex") {
// Per-spawn conversation id for the MCP shell's headless
// self-registration (codex provides no session id of its own).
Expand Down
Loading
Loading