description: "面向用户与维护者的 Cursor CLI 一次性 subagent 提供方:把 cursor-agent 作为 DSH 子智能体挂进 Profile,配置权限档、排障与安全边界。" kind: "package-bundle"
dsh-subagent-cursor
简体中文 · English
把 Cursor CLI(cursor-agent)作为 DSH 子智能体提供方:每次委派在父会话的工作区里 spawn 一个全新的 cursor-agent 进程,跑完一个任务,只返回它的单条权威答案。
目录
为什么需要它
DSH 官方提供两个子智能体后端:@deepseek-ai/dsh-subagent-codex(Codex)和 @deepseek-ai/dsh-subagent-claude-code(Claude Code)。没有 Cursor。
而 Cursor CLI 是一个值得接进来的后端:它用的是 Cursor 订阅额度,跑的是真实的 agent 循环(读写文件、跑命令、调 MCP),启动和鉴权都走 cursor-agent 自己的配置。装上这个包,模型就能用 subagent_cursor 把一段重活丢给 Cursor 去跑,父会话只收一条结论,token 不随上下文线性膨胀。
如果你已经有 Cursor 订阅,这个包让你不必额外买 API key 就能多一个能真正干活的委派后端。
使用本包
1. 装 Cursor CLI 并登录
见前提条件。这一步不做完,插件装上也不会跑起来。
2. 在 Profile 里加依赖
编辑 Profile 的 package.json(Web/Desktop 安装默认在 ~/.dsh/profiles/desktop/package.json)。本地开发用 link:(与 DSH 其他本地插件一致);发布后改成版本号 "dsh-subagent-cursor": "0.1.1":
{
"dependencies": {
"dsh-subagent-cursor": "link:D:/workspace/self/dsh-plugins/dsh-subagent-cursor"
}
}
3. 在 dsh.profile.bundles 里追加条目
追加到数组末尾(加载层顺序:base / web 在前,本包在后):
{
"dsh": {
"profile": {
"bundles": [
"@deepseek-ai/dsh-base",
"@deepseek-ai/dsh-web-app",
"dsh-subagent-cursor"
]
}
}
}
4. 安装并重启
cd ~/.dsh/profiles/desktop
pnpm install
然后重启 DSH。安装器提示需要重启时必须重启。
本包的 cordis.patch.yml 只插入一行——这个插件同时注册 provider 和它自己的工具:
- insert:
- id: subagent-cursor
name: dsh-subagent-cursor
为什么不用官方的
@deepseek-ai/dsh-tool-subagent:它在install时通过ctx.subagents.getProvider('cursor')找 provider,找不到就等subagent/provider-added事件。当 provider 和它是同层兄弟行时,两条路都可能错过,结果是工具静默缺席(只记一条 info 日志,不报错)。自己注册工具就没有这个时序依赖。细节见 spec §14。
5. 验证
让模型调一次最小任务。期望:拿到一段文本(例如 OK),stopReason 为 completed。
用一个 subagent_cursor 任务,只回答 OK,不要调用任何工具。
不放心就先跑一条命令行自检,绕开 DSH 只测 CLI:
node "$LOCALAPPDATA/cursor-agent/versions/<版本>/node.exe" \
"$LOCALAPPDATA/cursor-agent/versions/<版本>/index.js" \
-p --output-format stream-json --trust --approve-mcps "Reply with exactly: PONG"
<版本> 取 %LOCALAPPDATA%\cursor-agent\versions\ 下最新目录名(本机实测写作时的最新是 2026.10.22-27dcf5f)。这条命令等价于插件默认档 trust + approveMcps: true 生成的 argv,能跑通就说明 CLI 侧的鉴权和模型选择都没问题。
暴露的工具
subagent_cursor 由本插件自己用 ctx.tools.register() 注册(不依赖 dsh-tool-subagent)。它是 Host 级全局工具,所有 Agent 可见。工具名可用配置项 toolName 改;同机挂两套 Cursor 实例时 providerName 与 toolName 都要改——前者区分 provider,后者区分工具,两个实例共用一个工具名会互相抢占。
工具接受任务后返回 child id;任务完成后把答案作为通知送回父 agent。这是一次性委派:后端不接受追问(工具描述原文:This backend is single-shot: the agent cannot be steered while running and its conversation cannot be continued afterwards.)。要接着追问,就在新的委派里把必要上下文重新写进 prompt。
前提条件
已登录 Cursor CLI。 插件不复用 DSH 的凭据,也不读
CURSOR_API_KEY;它只依赖cursor-agent自己的登录态。先登录:cursor-agent login自检通过。 下面这条应当打印已登录账号(本机实测输出
✓ Logged in as <你的邮箱>):cursor-agent status在 Windows 上如果
cursor-agent不在PATH,用 §5 的node.exe index.js形式:... index.js status。CLI 装在默认位置,或配好
cliPath。 默认位置是%LOCALAPPDATA%\cursor-agent\,插件会自己按版本目录名挑最新的一版。装在别处(或想锁定某一版)就配cliPath。Node.js ≥ 22(
package.json的engines)。实际执行用的是 Cursor CLI 自带打包的node.exe,宿主 Node 只用于加载插件。模型可用。 不配
model时用 CLI 默认/账号设置。要固定模型就先确认它在你的账号下存在:cursor-agent models配了不存在的模型,CLI 会在 stderr 打出
Cannot use this model: ... Available models: ...然后失败。
版本参考:本包实测基线是 Cursor CLI
2026.10.22-27dcf5f。CLI 的stream-json输出没有版本化的契约承诺,解析器按"宽容 + 可诊断"实现;换版本后建议重跑一次验证步骤。
配置
字段定义以 src/index.js 的 Config 为唯一真源。
| 字段 | 默认值 | 含义 |
|---|---|---|
providerName |
cursor |
ctx.subagents 上的注册名。同一台机器挂两套 Cursor(比如一套 trust、一套 plan)时改它区分 |
model |
省略 | 固定的 CLI 模型 id(非空字符串)。省略则不传 --model,由 CLI 默认/账号设置决定 |
env |
{} |
显式子进程环境条目,叠在共享 subprocess seam 已清洗过的父环境之上 |
permissionMode |
trust |
非交互权限档,五选一,见权限档对照 |
approveMcps |
true |
为真时加 --approve-mcps,自动批准已配置的 MCP server。独立开关,与 permissionMode 正交 |
extraArgs |
[] |
逃生舱:原样追加到 argv 里(prompt 之前)。不做任何校验,见下方警告 |
disposeGraceMs |
3000 |
分级终止之间的宽限毫秒数。必须是正的有限数,且不超过 timer 上界 |
toolName |
subagent_cursor |
本插件自己注册的工具名(Host 级全局)。同机挂两套实例时必须各不相同(见暴露的工具) |
cliPath |
省略 | 显式 CLI 入口:一个 index.js 路径,或含 index.js + node.exe 的目录。省略则自动发现最新版本 |
permissionMode 与 approveMcps 不能混为一谈:--force(yolo 档)覆盖的是 shell 与文件编辑,不覆盖第三方 MCP server。所以 MCP 自动批准是一个独立布尔量,不在任何权限档里。
extraArgs警告:这里传进去的东西不经过任何校验,直接进 argv。不要传会改变输出格式的 flag(例如换--output-format),否则解析器拿不到result事件,运行必然失败。也不要把 prompt 塞进extraArgs——prompt 是最后一个位置参数,插件自己会追加。
disposeGraceMs 在加载期就被校验:非正数、非有限数或超过上界都会让插件加载失败,而不是留到销毁时才出问题。
能力与深度治理
本提供方的能力标志全部为 false:agentOptions、agentPreset、outputSchema、depthLimit 都不支持。原因很直接——子进程在另一个进程里,无法兑现父级强制的能力;共享服务会在 start 之前就拒绝需要这些能力的请求(不是"接了再忽略")。
因此深度治理由本插件自己拥有:startActivation 的 request 里不传 maxDepth,provider 也不接受父级下发的深度上限。如果你需要限制 Cursor 子任务的嵌套,只能靠 prompt 约束或在 CLI 侧限制。
同理,Cursor 是单文本入口,没有"传 schema""传工具白名单""传 persona"的原生通道。这些约束只能写在 prompt 里,属于 prompt 工程,不算 provider 能力。
权限档对照
五个取值,互斥枚举(不允许自由组合,因为组合语义未验证)。--trust 是每一档都带的 base flag,所以 trust 档不额外追加任何 flag。
permissionMode |
追加的 CLI 参数 | 平台 / 行为 |
|---|---|---|
trust(默认) |
无(只有 base 的 --trust) |
全平台可用。工作区写操作自动放行 |
safe |
--sandbox enabled |
仅 macOS / Linux。Windows 上 CLI exit 1 且 stdout 全空,插件在 start() 期就报错 |
yolo |
--force |
全放行,含 shell / 命令 |
plan |
--mode plan |
只读 / 规划 |
ask |
--mode ask |
只读问答 |
为什么默认是 trust 而不是 safe
这是实测结论,不是口味选择。Windows 上三档真跑一次「创建文件并写入 OK」:
| 档位 | 参数 | 结果 |
|---|---|---|
trust |
--trust |
22.8s,exit 0,文件真的被写了,is_error: false,正常 result 事件 |
yolo |
--trust --force |
23.2s,exit 0,文件被写,正常 |
safe |
--trust --sandbox enabled |
2.6s,exit 1,stdout 完全空、零 result 事件 |
safe 的 stderr 是:Error: Sandbox mode is enabled but not available on this system. Sandbox requires macOS or Linux.
结论:safe 在 Windows 上 100% 起不来。如果默认给 safe,这台机器上每一次委派都会失败。能跑的默认值胜过名义上更严但永远跑不起来的默认值。
safe 在 Windows 上的守卫
只改默认值还不够——用户仍可能显式配 safe 跑在 Windows 上。不拦的话,子进程会跑 2.6 秒、吐一个空 stdout,然后被记成一句泛泛的 child run failed (error),根本看不出是"沙箱不可用"这种配置问题。
所以插件在 start() 期、spawn 之前就做平台守卫并抛错,错误消息直接说明 safe 仅 macOS/Linux 并给出替代档位(trust / yolo / plan / ask)。零进程开销、即时失败、错误可读。
未验证:
safe在 macOS / Linux 上按 CLI 报错文案应当可用,但我们没有在这两个平台上实测过。要用请自建验证:跑一次带文件写入的任务,确认stopReason为completed且文件确实落了盘。
你会得到什么
完成通知里是 Cursor 给出的最终答案文本,或者停止原因加一段安全的诊断。
进父会话的:最终答案(或失败诊断)。
不进父会话的:Cursor 的中间 assistant 消息、thinking 推理、工具调用事件、原始 stderr、token 用量、工作区 diff。
这一点是有意的:assistant 事件只是中间消息,一次运行可能出现多条,拼起来会把模型的思考过程当答案交回去。详见踩坑必读第 5 条。
踩坑必读
以下都是拿真机实测换来的,写 prompt 或排障前先读。
1. prompt 必须以祈使指令开头
实测三组 prompt 结构的工具调用次数:
| prompt 结构 | 长度 | 工具调用 |
|---|---|---|
| 只有核心指令 | 138 | 78 次 |
| 核心指令在前 + 约束在后 | 394 | 92 次 |
| 「工作目录:xxx」放在最前 | 443 | 0 次 |
把路径 / 环境说明放在最前面,agent 会把它当成环境交代,回一句「已确认工作目录」就结束了,一个工具都不调。
对本插件的含义:模型写 subagent_cursor 的 task 时,第一句必须是祈使句("读取 X 并总结""在 Y 里创建 Z")。工作目录、日期、时区、仓库根这类上下文一律放在指令之后。插件不会替你重排 prompt —— 它按原样传给 CLI,顺序变了就是调用方写错了。
另外,子进程的 cwd 已经由 request.cwd 决定,不要在 prompt 里重复交代工作目录,那正是触发上面那个 0 次退化的写法。
2. 工具事件叫 tool_call,不是 tool_use
stream-json 里统计工具调用要看 "type": "tool_call",subtype 是 started / completed。早期误用 tool_use 统计,把一次成功运行误判成 0 次工具调用。
对本插件的含义:你自己写脚本解析 CLI 输出、或排查"它到底动没动手"时,按 tool_call 数。插件的解析器(src/wire.js)也是按 tool_call 计数,并且只计入这两个已知 subtype,不猜。
3. 终端中文乱码的根因在消费端,不是 CLI
实测:CLI 输出是合法的 UTF-8。 乱码来自消费端按 GBK 解码——Windows 控制台的默认代码页是 936,把 UTF-8 字节按 GBK 读就成了乱码。
对本插件的含义:插件用 StringDecoder('utf8') 按块解码 stdout(src/wire.js),不是 chunk.toString()。这一点很关键:一次 UTF-8 序列可能被 chunk 边界切断,逐块 toString() 会产生替换字符,StringDecoder 会把不完整的尾字节留到下一块再拼。
对你自测的含义:用 PowerShell 手跑 CLI 看到中文乱码时,别急着怪 CLI;那只是控制台代码页。要干净地看输出,重定向到文件再用 UTF-8 读,或在 PowerShell 里设 $OutputEncoding / [Console]::OutputEncoding 为 UTF-8。
4. Windows 上 cursor-agent 是三层包装,插件绕开它
%LOCALAPPDATA%\cursor-agent\cursor-agent.cmd 只是转调 powershell.exe,PowerShell 再调 versions\<最新>\node.exe 跑 versions\<最新>\index.js。即 .cmd → PowerShell → node.exe 三层。
本插件的做法:直接 spawn node.exe index.js。
- 绝不 spawn
.cmd:Node 不加shell: true根本执行不了.cmd。 - 绝不用
shell: true:那会把 prompt 丢回cmd.exe,%VAR%会被展开,&、|会被当命令分隔符——中文和空格 prompt 尤其危险。
顺带的好处是省掉 PowerShell 启动(每次约 300–800 ms)、没有引号地狱、argv 原样传递。版本目录解析复刻了 cursor-agent.ps1 的规则:目录名匹配 ^\d{4}\.\d{1,2}\.\d{1,2}(-\d{2}-\d{2}-\d{2})?-[a-f0-9]+$,按日期段降序取最新。
注意:C:\Program Files\cursor\...\cursor 是 IDE 启动器,不是 CLI agent,别指到那里去。
5. 答案只认 result.result
一次运行里 assistant 事件有多条(实测一次"ls → pwd → 总结"的运行有 3 条,另一类运行出现过 8 条),result 只有 1 条。实测 result.result 恰好等于全部 assistant 文本按空串拼接。
所以:
- 只取最后一条
assistant,会丢掉前面的内容(上面那例丢了 22%)。 - 把
assistant事件拼起来当答案,会把思考过程("先执行 ls,完成后执行 pwd")掺进结论里。
本插件的做法:output 只由 result.result 构成。 assistant、thinking、user 事件一律不参与答案构造。result 缺失(进程被杀等)时不会退回到 thinking 文本——宁可报 error,也不吐半成品推理。
对你排障的含义:自己写脚本时,只看 result 那一行。
6. 其它
- prompt 走命令行位置参数,受长度上限约束。 Windows 上大约 32 KB。超长 prompt 会直接失败,而不是被静默截断——这比截断好,但把大段材料写进文件再让 Cursor 去读,别塞进 prompt。
plan/ask是只读的。 别指望plan档干活。- 冷启动慢。 CLI 冷启动实测 3~6 s,但
start()不等这段——spawn 成功即发布 run,冷启动体现在拿到result的延迟上。超时要按这个量级算。 - 每次委派有约 10K input token 的地板价(实测 9880 / 9883 等固定开销)。很短的任务用 Cursor 不划算。
与官方 codex 提供方的差异
| 维度 | dsh-subagent-codex |
本包(cursor) |
|---|---|---|
| 进程/协议模型 | Codex app-server,JSON-RPC 双向(要回审批请求) | Cursor CLI stream-json NDJSON,单向(stdout 只往下流) |
| 握手 | initialize → initialized → thread/start |
无握手;system/init 只是元数据事件,不设 ready 门(spawn 成功即发布 run) |
| 答案选取 | final → partial → 末条无 phase 消息 | 只认 result.result,无回退链 |
| 能力标志 | 全 false | 全 false(同) |
| 人在环审批 | 逐次请求,可 decline / cancel | 没有请求通道;启动时一次性定死(--trust / --force / --approve-mcps) |
| 中途 steering | 有 interrupt | 没有,只有 kill 进程 |
| 续接 / resume | 不支持 | 不支持(CLI 有 --resume,但本插件不实现 resume) |
| 冷启动 | 几百 ms | 3~6 s(实测 4.4s / 5.8s / 3.2s) |
| 单次 token 地板 | 小 | 约 10K input tokens |
| 子进程入口 | 随包的平台载荷 | 外部安装的 cursor-agent(不随包分发) |
一句话:codex 走的是双向协议、能做逐次审批;cursor 走的是单向事件流、审批全部前置到启动参数。后者更粗,但无人值守场景下反而更简单——不存在"没人回答审批请求导致挂死"这种失败模式。
理解实现
实现细节——点击展开设计理念
- 一次委派 = 一个进程 = 一个 chat。 每次运行 spawn 全新
cursor-agent,跑完一个 prompt 就退出。没有续接、恢复、池化。 - 原生配置是权威。 登录态、模型默认值、MCP 配置都来自 Cursor CLI 自己的设置。插件只覆盖可选
model、显式env、权限档与 MCP 自动批准。 - 刻意无人值守。 CLI 单向输出、stdin 关闭,没有回话通道。所有授权在启动前一次性定死。
- 失败要 loud。 能在启动期判定的配置错误(CLI 缺失、平台不支持某权限档、cwd 不可用)一律在
start()抛错,绝不拖到运行期用一句通用错误掩盖。
源码地图
| 文件 | 职责 |
|---|---|
src/index.js |
插件入口:name / inject / Config schema / CursorProvider 类 / apply(注册 provider 与工具)/ Windows 平台守卫 |
src/tool.js |
subagent_cursor 工具定义:defineTool 参数与输出契约 + startActivation 调用 |
src/argv.js |
CLI 定位(版本目录解析)+ buildArgv + permissionArgs(权限档 → CLI flag) |
src/run.js |
运行生命周期:spawn → 读流 → 等终态 → settleRunResult → 销毁 |
src/wire.js |
CursorStreamReader:NDJSON 行解析、UTF-8 分块解码、答案选取、诊断摘要 |
cordis.patch.yml |
插入单个 bundle 行(provider 与工具都由本插件自己注册) |
locale/zh.json · locale/en.json |
插件页显示名与描述 |
tests/ |
单测:argv / wire / run / index 四组(见开发与测试) |
运行流程
校验与定位。
start()先assertUsableCwd校验request.cwd,再做平台守卫(safe+win32直接抛错),然后resolveCursorCli定位node.exe+index.js(优先级:cliPath→%LOCALAPPDATA%\cursor-agent\{index.js,node.exe}→versions\<最新>\{index.js,node.exe})。拼 argv。
buildArgv组装固定顺序:node.exe index.js -p --output-format stream-json --trust [--model m] [<权限档 flag>] [--approve-mcps] [<extraArgs>] <prompt>prompt 永远是最后一个位置参数,其后不能有任何 flag(否则会被当成 prompt 的一部分而不是 flag)。实测默认档的 argv 是:
["-p","--output-format","stream-json","--trust","--approve-mcps","Reply with exactly: PONG"]spawn。 经共享
ctx.subprocess.spawn起子进程,stdio.stdin = 'ignore'(prompt 是位置参数,stdin 保持关闭,同时避免 CLI 阻塞在一个永远不会有人写的管道上),cwd取request.cwd,env是 seam 清洗过的父环境叠加显式env。spawn 之后先校验 seam 返回的 handle 形状(stdout/stderr/done/terminate/waitForExit),不符合就 loud 报错并尽力清理。子进程 stderr 原样转发到宿主 stderr(process.stderr.write,遇到背压就 pause/resume),同时只滚存末 8 KiB 供失败诊断。读 NDJSON。 stdout 按块喂给
CursorStreamReader,StringDecoder负责跨块的 UTF-8 拼接,按\n切行、逐行JSON.parse。不是 JSON 对象的行记成噪音跳过(CLI 会往 stdout 混横幅和进度文本),最多留 5 条供失败诊断,不致命;单行无换行且超过 8 MiB 则判流 framing 失败。stdout'end'时还会调reader.end()收尾,让没有尾换行的最后一行(尤其最后那条result)也能解析。只取
result.result。 收到result事件即算终态。is_error: true判失败;答案为空(或全空白)也判失败。第二个result事件视为流畸形,保留第一条,让结果稳定。销毁。
requestCancel()先触发,然后terminate()终止托管进程组、等退出、等done,最后reader.end()并摘掉流监听。失败诊断里会带退出码、信号、result事件数与事件类型集合、stderr 摘录(头 1 KiB + 尾 1 KiB,剥 ANSI)和最多 5 条 stdout 噪音行,但绝不带答案正文。
启动期失败(CLI 找不到、cwd 不可用、spawn 抛错、平台守卫)直接 throw,不发布 run;发布之后的失败全部折叠成 stopReason 与安全诊断。
开发与测试
单测用 Node.js 内置 test runner,不需要构建,也不需要真正的 Cursor CLI:DSH 运行时包由 tests/_support/hooks.mjs 的模块解析钩子顶替,子进程用假 child(PassThrough 流 + thenable done)代替,时间只推进微任务,不 sleep、不联网。
npm test # 运行 package.json 的 test script(Node 内置 test runner)
node --test "tests/**/*.test.mjs" # 等价:直接调用 runner
四组文件:argv.test.mjs(CLI 定位与 argv 顺序)、wire.test.mjs(NDJSON 解析与分块 UTF-8)、run.test.mjs(运行生命周期与失败折叠)、index.test.mjs(Config 默认值、win32 守卫、provider/工具注册)。当前基线:114 个用例全部通过。
本仓库没有 CI workflow:peer 依赖是私有的
@deepseek-ai包,托管 CI 上npm ci装不起来;测试只在本地跑(npm test)。
已知限制
- 一次委派 = 一个进程 = 一个 chat。 没有续接、恢复、池化,也不复用 chat id。工具描述已经如实写明单发模型:不能中途 steering,也不能在同一 chat 上继续。
- 没有中途 steering。 CLI 的
--print模式没有输入通道(stdout 单向、stdin 关闭),所以不实现sendMessage。取消是终止式的:只能 kill 进程。 - 子级 token 不进父会话。 父会话只收到最终答案或失败诊断;中间消息、thinking、工具活动、stderr、用量、工作区 diff 都不复制过来。子级自己的 token 成本由它自己承担。
- 取消/失败后已改的文件不回滚。 没有副作用回滚,也没有按实际经过时间触发的超时。
trust/yolo档下 Cursor 可能真的写过文件、跑过命令——那些都不恢复原状。 - 不支持
outputSchema/agentPreset/agentOptions/depthLimit。 能力标志全 false,请求这些能力会在start前被共享服务拒绝。 - CLI 必须外部安装。 本包不随附 Cursor CLI,也没有"回退到宿主 CLI"这回事。CLI 缺失是启动期错误,会指出试过的路径。
- prompt 受命令行长度上限约束。 约 32 KB(Windows)。超长会直接失败而不是截断。
- stdout 单行有 8 MiB 上限。 解析器拒绝无限缓冲一条无换行的行(
MAX_LINE_CHARS);超限判stream framing failed→stopReason: error。正常事件行远小于该值,纯属防御。 - 每次委派约 10K input token 起步,冷启动 3~6 s。 短任务不划算。
stream-json无版本化契约。 解析器按宽容实现,但 CLI 大改版后仍可能需要跟着改。实测基线2026.10.22-27dcf5f。safe档未在 macOS / Linux 实测。 按 CLI 报错文案应当可用,未验证。
No comments yet. Be the first to write one.