让 Copilot Chat 控制 Cursor Max Mode:跨进程配置传递实战
Cursor CLI 支持 Max Mode——扩展上下文窗口和工具调用限制,代价是更高的 token 消耗。在 Cursor IDE 里这是一个 toggle,但在 CLI 中只能通过 cli-config.json 的 maxMode 字段控制,且 CLI 读取后会自动重置为 false(一次性消费)。
我们的 VSCode 扩展 cursor-copilot-bridge 将 Cursor CLI 封装为 Copilot Chat 的 Language Model Provider。本文记录如何为它添加 Max Mode 开关。
架构:Preflight 脚本
VSCode 扩展宿主进程和 Cursor CLI 是完全隔离的——扩展无法直接修改 CLI 的运行时状态。可选方案:
| 方案 | 问题 |
|---|---|
CLI 命令行参数 --max-mode | 不存在,CLI 不支持 |
环境变量 CURSOR_MAX_MODE | CLI 不读这个变量 |
| 直接在扩展中写 config | 扩展是 JS,CLI 是独立 node 进程,config 路径解析取决于运行时环境变量 |
| Preflight 脚本 | 用 CLI 自带的 node.exe 执行,继承相同环境变量,路径解析天然一致 ✓ |
最终方案:在 spawn(CLI) 前,用 execFileSync 同步执行一个轻量脚本,写入 maxMode: true。execFileSync 阻塞直到完成,不存在时序问题。
execFileSync(node.exe, [preflight.js]) // 同步,阻塞
↓ 写入 cli-config.json: maxMode=true
spawn(node.exe, [index.js, ...args]) // preflight 完成后才执行
↓ CLI 读取 maxMode=true → 发送请求 → 重置为 false关键:镜像 CLI 的 Config 路径解析
Preflight 的 config 路径解析必须与 CLI 完全一致,否则两边读写的是不同文件。
从 CLI 的 minified 源码(index.js)中提取出 config 目录解析函数:
// CLI 源码(反混淆后)
function getConfigDir() {
const e = process.env.CURSOR_CONFIG_DIR;
if (e?.trim()) return e;
const t = process.env.XDG_CONFIG_HOME;
return t?.trim() ? path.join(t, "cursor") : path.join(os.homedir(), ".cursor");
}
function getCliConfigPath() {
return path.join(getConfigDir(), "cli-config.json");
}三级优先级:CURSOR_CONFIG_DIR → XDG_CONFIG_HOME/cursor → ~/.cursor。
Preflight 1:1 复刻:
// max-mode-preflight.ts
function getConfigDir(): string {
const envDir = process.env.CURSOR_CONFIG_DIR;
if (envDir?.trim()) return envDir;
const xdg = process.env.XDG_CONFIG_HOME;
if (xdg?.trim()) return path.join(xdg, "cursor");
return path.join(os.homedir(), ".cursor");
}
const configPath = path.join(getConfigDir(), "cli-config.json");读取并写入 maxMode: true。Windows 下部分编辑器会给 JSON 加 UTF-8 BOM,需要容错:
const raw = JSON.parse(rawStr.replace(/^\uFEFF/, ""));
raw.maxMode = true;
fs.writeFileSync(configPath, JSON.stringify(raw, null, 2), "utf-8");Preflight 和 CLI 都是扩展宿主的子进程,继承相同环境变量,用相同的三级逻辑,天然指向同一个文件。spawnAgent 无需做任何额外处理。
Protobuf 抓包验证
CLI 真的把 maxMode=true 发给了服务器吗?通过 NODE_OPTIONS=--require 注入 HTTP/2 hook,拦截 CLI 发往 agent.v1.AgentService/Run 的 Connect Protocol 请求体(protobuf 二进制)。对比 RequestedModel 消息:
maxMode=true → 4a21 0a1d "claude-4.6-opus-high-thinking" 1001
^^^^
protobuf field#2 = 1 (true)
maxMode=false → 4a1f 0a1d "claude-4.6-opus-high-thinking"
(field#2 缺失,默认 false)帧大小差 2 字节,与 10 01 完全对应。cursor.com/usage 页面对 maxMode=true 的请求标注 MAX 徽章。
排查技巧:利用 CLI 的自动重置行为
CLI 读取 maxMode: true 后会自动重置为 false。这个行为是天然的路径一致性探针:
| 请求后 config 状态 | 含义 |
|---|---|
maxMode: false | CLI 读取并重置 ✓ preflight 和 CLI 读写同一个文件 |
maxMode: true | CLI 根本没读这个文件 ✗ 路径不一致 |
如果发现 config 没被重置,说明 preflight 和 CLI 解析到了不同的文件。此时应该对照 CLI 源码检查路径解析逻辑是否完全一致。
完整数据流
用户在 VSCode 设置中勾选 cursorBridge.maxMode
↓
provider.ts 读取 config.maxMode = true
↓
process.ts → spawnAgent:
① resolveAgent("agent") → PATH 查找 → agent.cmd → node.exe + index.js
② execFileSync(node.exe, [preflight.js]) // 同步阻塞
→ getConfigDir() 解析路径(与 CLI 相同的三级逻辑)
→ 写入 cli-config.json: maxMode = true
③ spawn(node.exe, [index.js, ...args])
↓
CLI 内部调用同样的 getConfigDir() → 读取同一个 cli-config.json → maxMode = true
↓
构建 RequestedModel protobuf (field#2 = 1) → 发送到 Cursor 服务器
↓
CLI 自动重置 maxMode = false
↓
cursor.com/usage 显示 MAX 标记 ✓收获
照抄源码,不要猜路径——config 路径解析直接从 CLI 的 minified 源码中提取,1:1 复刻。两个进程用完全相同的逻辑,就不会出现路径分歧。
子进程天然共享环境——preflight 和 CLI 都是扩展宿主的子进程,继承相同的环境变量。只要路径解析逻辑一致,不需要手动注入环境变量或添加额外配置项。
Protobuf 抓包比推理有效——与其反复推理 minified 代码的行为,不如直接看请求的二进制差异。
NODE_OPTIONS=--require+ HTTP/2 stream hook 在不需要代理的情况下就能完成。善用消费方的副作用做探针——CLI 读取
maxMode后自动重置为false,这个副作用可以直接判断文件是否被正确读取,比添加 debug 日志更快。