@super-coding/cli 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/README.md +70 -2
  2. package/dist/cli.js +57 -0
  3. package/dist/daemon/adapters/base.js +37 -0
  4. package/dist/daemon/adapters/claude-code.js +184 -0
  5. package/dist/daemon/adapters/cursor.js +138 -0
  6. package/dist/daemon/adapters/features.js +51 -0
  7. package/dist/daemon/adapters/registry.js +9 -0
  8. package/dist/daemon/agent/builtin/agent.md +164 -0
  9. package/dist/daemon/agent/builtin/load.js +121 -0
  10. package/dist/daemon/agent/builtin/parse.js +73 -0
  11. package/dist/daemon/agent/index.js +60 -0
  12. package/dist/daemon/agent/manifest.js +72 -0
  13. package/dist/daemon/agent/materialize.js +220 -0
  14. package/dist/daemon/agent/renderers/claude-code.js +16 -0
  15. package/dist/daemon/agent/renderers/common.js +61 -0
  16. package/dist/daemon/agent/renderers/cursor.js +13 -0
  17. package/dist/daemon/agent/renderers/generic.js +5 -0
  18. package/dist/daemon/agent/resolve.js +212 -0
  19. package/dist/daemon/agent/slug.js +27 -0
  20. package/dist/daemon/agent/types.js +8 -0
  21. package/dist/daemon/device-store.js +61 -0
  22. package/dist/daemon/index.js +292 -0
  23. package/dist/daemon/manage.js +183 -0
  24. package/dist/daemon/orchestrator.js +400 -0
  25. package/dist/daemon/pairing.js +93 -0
  26. package/dist/daemon/paths.js +47 -0
  27. package/dist/daemon/rpc.js +41 -0
  28. package/dist/daemon/run-skills.js +148 -0
  29. package/dist/daemon/services/agent-config.js +1 -0
  30. package/dist/daemon/services/claude-provider.js +126 -0
  31. package/dist/daemon/services/cursor-provider.js +94 -0
  32. package/dist/daemon/services/json-file.js +21 -0
  33. package/dist/daemon/services/log-buffer.js +43 -0
  34. package/dist/daemon/services/mcp-secrets.js +36 -0
  35. package/dist/daemon/session/store.js +137 -0
  36. package/dist/daemon/transport/ws-client.js +121 -0
  37. package/dist/pingcode/builtin/SKILL.md +47 -0
  38. package/dist/pingcode/builtin/reference.md +123 -0
  39. package/dist/pingcode/cli.js +138 -0
  40. package/dist/pingcode/config.js +106 -0
  41. package/dist/pingcode/detect.js +73 -0
  42. package/dist/pingcode/index.js +8 -0
  43. package/dist/pingcode/launcher.js +60 -0
  44. package/dist/pingcode/runtime.js +167 -0
  45. package/dist/pingcode/skill.js +51 -0
  46. package/dist/pingcode/token.js +81 -0
  47. package/package.json +45 -3
package/README.md CHANGED
@@ -1,3 +1,71 @@
1
- # Temporary Holding Version
1
+ # @super-coding/cli
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ 把本机的 **Claude Code** / **Cursor** 作为执行器接入 Super Coding 控制台:远端下发任务,本地 daemon 在独立会话目录中执行并回传过程与结果。
4
+
5
+ 包名是 `@super-coding/cli`,安装后提供的命令是 **`scode`**。
6
+
7
+ ## 环境要求
8
+
9
+ - Node.js **>= 24**
10
+ - 已安装并登录 `claude`(Claude Code CLI)或 `cursor-agent`,运行时按需探测,未安装的执行器不会上报
11
+
12
+ ## 安装与启动
13
+
14
+ ```bash
15
+ npm install -g @super-coding/cli
16
+ scode daemon # 后台启动(等价于 scode daemon start)
17
+ ```
18
+
19
+ 首次启动且本地没有设备凭证时,会在终端输出 6 位配对码,按提示在控制台完成配对;凭证写入 `~/.super-coding/daemon.json`(权限 `0600`)。
20
+
21
+ ## 命令
22
+
23
+ ```bash
24
+ scode daemon # 后台启动(已在运行则提示)
25
+ scode daemon start # 同上
26
+ scode daemon stop # 停止(SIGTERM,超时 SIGKILL)
27
+ scode daemon restart # 重启
28
+ scode daemon status # 状态(pid / 服务地址 / 心跳)
29
+ scode daemon logs [-f] [-n 50] # 查看日志,-f 持续输出
30
+ scode daemon run # 前台运行(调试或交给外部 supervisor)
31
+ ```
32
+
33
+ ## 配置
34
+
35
+ 配置文件:`~/.super-coding/config.json`(目录可用 `SC_HOME` 覆盖)。
36
+
37
+ | 字段 | 说明 |
38
+ | --- | --- |
39
+ | `allowedCwds` | `run:start` 允许的 cwd 前缀白名单;缺省不限制 |
40
+ | `repo_url` | 默认项目仓库地址:任务未给目录时,内置 agent 会克隆它到会话目录的 `repo/`;缺省为空(不克隆) |
41
+
42
+ 环境变量:
43
+
44
+ | 变量 | 说明 |
45
+ | --- | --- |
46
+ | `DAEMON_SERVER_URL` | 中转服务 WebSocket 地址(默认连接 Super Coding 官方中转;自建/本地调试时覆盖) |
47
+ | `DAEMON_SERVER_HTTP_URL` | 配对等 HTTP 基址;缺省由 WS 地址推导 |
48
+ | `SC_HOME` | 配置与运行目录,默认 `~/.super-coding` |
49
+ | `SC_AGENT_REPO_URL` | 默认项目仓库地址,优先级高于配置文件的 `repo_url`;显式置空表示不克隆 |
50
+ | `SC_AGENT_INJECTION=off` | 关闭 agent 注入(不写 agent 文件、不声明能力),退回旧行为 |
51
+ | `SC_ALLOW_BYPASS_PERMISSIONS=0` | 禁止 `bypassPermissions`,任务执行前需要审批 |
52
+ | `SC_AGENT_BUILTIN_PATH` | 覆盖内置 agent 定义文件路径(调文案用) |
53
+
54
+ ## 安全与数据说明
55
+
56
+ daemon 本质上是**把本机作为远程执行器**,请只在可信环境中使用:
57
+
58
+ - **任务执行**:远端下发的任务在本机会话目录(`~/.super-coding/workspaces/session-*`)中执行。内置 agent 默认 `permission_mode: bypassPermissions`(无人值守,不会弹审批),默认禁用 `WebFetch`、`NotebookEdit`。需要收紧时设置 `SC_ALLOW_BYPASS_PERMISSIONS=0`。
59
+ - **凭证**:设备 token 仅在配对时明文出现一次,落盘为 `~/.super-coding/daemon.json`(`0600`),不会写入日志或上报。
60
+ - **会上传的内容**:执行事件与日志、会话目录路径、执行器与模型配置摘要、任务提示词及执行输出。
61
+ - **MCP 配置**:`mcp:list` 只返回 `env` / `headers` 的**键名**,值统一脱敏为 `***`;原样回写时保留磁盘上的真实值,显式改值才会覆盖。
62
+ - **本地日志**:`~/.super-coding/daemon.log` 含执行路径与任务信息,默认按当前 umask 创建,建议自行收紧权限。
63
+
64
+ ## 开发
65
+
66
+ ```bash
67
+ pnpm --filter @super-coding/cli build # 构建(先构建 protocol,再编译并拷贝 agent.md)
68
+ pnpm --filter @super-coding/cli test # 单测
69
+ pnpm --filter @super-coding/cli typecheck
70
+ pnpm dev:cli # daemon 开发模式
71
+ ```
package/dist/cli.js ADDED
@@ -0,0 +1,57 @@
1
+ #!/usr/bin/env node
2
+ import { createRequire } from "node:module";
3
+ import { Command } from "commander";
4
+ import { logsDaemon, restartDaemon, runDaemonForeground, startDaemon, statusDaemon, stopDaemon, } from "./daemon/manage.js";
5
+ import { connectPingcode, disconnectPingcode, printPingcodeStatus } from "./pingcode/index.js";
6
+ const pkg = createRequire(import.meta.url)("../package.json");
7
+ const program = new Command();
8
+ program.name("scode").version(pkg.version).description("super-coding CLI");
9
+ /** 统一错误出口:失败打印一行并以退出码 1 结束 */
10
+ async function run(fn) {
11
+ try {
12
+ await fn();
13
+ }
14
+ catch (err) {
15
+ console.error(err?.message ?? String(err));
16
+ process.exitCode = 1;
17
+ }
18
+ }
19
+ const daemon = program
20
+ .command("daemon")
21
+ .description("本地守护进程:连接 remote-server,编排本机 AI agent(默认后台启动)")
22
+ .action(() => run(() => startDaemon())); // 裸命令 = start
23
+ daemon.command("start").description("后台启动守护进程(已在运行则提示)").action(() => run(() => startDaemon()));
24
+ daemon.command("stop").description("停止守护进程(SIGTERM→SIGKILL 兜底)").action(() => run(() => stopDaemon()));
25
+ daemon.command("restart").description("重启守护进程").action(() => run(() => restartDaemon()));
26
+ daemon.command("status").description("查看守护进程状态").action(() => run(() => statusDaemon()));
27
+ daemon
28
+ .command("logs")
29
+ .description("查看守护进程日志")
30
+ .option("-f, --follow", "持续输出新日志(tail -f 语义)")
31
+ .option("-n, --lines <n>", "显示行数", "50")
32
+ .action((opts) => run(() => logsDaemon({ follow: !!opts.follow, lines: Number(opts.lines) || 50 })));
33
+ daemon.command("run").description("前台运行(调试/外部 supervisor 托管)").action(() => run(() => runDaemonForeground()));
34
+ // 嵌套命令默认无 help 子命令;补上使 `daemon help` 可用(npm 会吞掉 -h,`help` 是 npm 安全路径)
35
+ daemon.addHelpCommand("help [command]");
36
+ const pingcode = program
37
+ .command("pingcode")
38
+ .description("PingCode(pc)集成:保存本机凭据、查看可用性");
39
+ pingcode
40
+ .command("status")
41
+ .description("查看 pc 二进制、凭据来源与凭据注入方式")
42
+ .action(() => run(() => printPingcodeStatus()));
43
+ pingcode
44
+ .command("connect")
45
+ .description("保存 PingCode 凭据(写入 ~/.super-coding/pingcode.json,权限 0600)")
46
+ .option("--token <token>", "用户令牌(以个人身份调用)")
47
+ .option("--client-id <id>", "应用 Client ID(与 client-secret 成对)")
48
+ .option("--client-secret <secret>", "应用 Client Secret")
49
+ .option("--base-url <url>", "Open API 地址(默认 https://open.pingcode.com;私有部署填 https://<host>/open)")
50
+ .option("--no-verify", "跳过 client 凭据的令牌换取校验")
51
+ .action((opts) => run(() => connectPingcode(opts)));
52
+ pingcode
53
+ .command("disconnect")
54
+ .description("删除本地 PingCode 凭据")
55
+ .action(() => run(() => disconnectPingcode()));
56
+ pingcode.addHelpCommand("help [command]");
57
+ await program.parseAsync();
@@ -0,0 +1,37 @@
1
+ /** push/close 异步队列:AgentSession 的事件缓冲 */
2
+ export class EventQueue {
3
+ buffer = [];
4
+ waiters = [];
5
+ ended = false;
6
+ push(event) {
7
+ if (this.ended)
8
+ return;
9
+ const waiter = this.waiters.shift();
10
+ if (waiter) {
11
+ waiter({ value: event, done: false });
12
+ }
13
+ else {
14
+ this.buffer.push(event);
15
+ }
16
+ }
17
+ end() {
18
+ if (this.ended)
19
+ return;
20
+ this.ended = true;
21
+ for (const waiter of this.waiters.splice(0)) {
22
+ waiter({ value: undefined, done: true });
23
+ }
24
+ }
25
+ [Symbol.asyncIterator]() {
26
+ return {
27
+ next: () => {
28
+ const buffered = this.buffer.shift();
29
+ if (buffered !== undefined)
30
+ return Promise.resolve({ value: buffered, done: false });
31
+ if (this.ended)
32
+ return Promise.resolve({ value: undefined, done: true });
33
+ return new Promise((resolve) => this.waiters.push(resolve));
34
+ },
35
+ };
36
+ }
37
+ }
@@ -0,0 +1,184 @@
1
+ import { spawn } from "node:child_process";
2
+ import { randomUUID } from "node:crypto";
3
+ import { createInterface } from "node:readline";
4
+ import { EventQueue } from "./base.js";
5
+ /** binary 以 .mjs 结尾时用当前 node 执行(测试注入 fake CLI) */
6
+ const spawnCli = (binary, args, cwd, extraEnv) => {
7
+ const env = extraEnv ? { ...process.env, ...extraEnv } : process.env;
8
+ if (binary.endsWith(".mjs")) {
9
+ return spawn(process.execPath, [binary, ...args], { cwd, stdio: ["pipe", "pipe", "inherit"], env });
10
+ }
11
+ return spawn(binary, args, { cwd, stdio: ["pipe", "pipe", "inherit"], env });
12
+ };
13
+ class ClaudeCodeSession {
14
+ opts;
15
+ queue = new EventQueue();
16
+ proc;
17
+ sawDone = false;
18
+ constructor(opts) {
19
+ this.opts = opts;
20
+ const binary = opts.binary ?? "claude";
21
+ const { model, permission_mode, resume_session_id } = opts.options ?? {};
22
+ this.proc = spawnCli(binary, [
23
+ "-p", "--output-format", "stream-json", "--input-format", "stream-json", "--verbose",
24
+ ...(model ? ["--model", model] : []),
25
+ ...(permission_mode ? ["--permission-mode", permission_mode] : []),
26
+ ...(resume_session_id ? ["--resume", resume_session_id] : []),
27
+ // 以平台物化的原生 agent 作为主会话身份(版本不支持时由 orchestrator 降级为 CLAUDE.md)
28
+ ...(opts.agentName ? ["--agent", opts.agentName] : []),
29
+ // 只认本轮下发的 MCP 配置,与用户 ~/.claude.json 隔离
30
+ ...(opts.mcpConfigPath ? ["--mcp-config", opts.mcpConfigPath, "--strict-mcp-config"] : []),
31
+ // variadic 必须放最后(不然后续 flag 会被当成工具名)
32
+ ...(opts.disallowedTools?.length ? ["--disallowedTools", ...opts.disallowedTools] : []),
33
+ ], opts.cwd, opts.env);
34
+ // spawn 失败(如 agent 未安装 ENOENT):语义化 error + done,不静默悬挂
35
+ this.proc.on("error", (err) => {
36
+ this.sawDone = true;
37
+ this.queue.push({ kind: "error", message: `spawn ${binary} failed: ${err.message}`, retryable: false });
38
+ this.queue.push({ kind: "done", status: "failed" });
39
+ this.queue.end();
40
+ });
41
+ // 首条 user 消息(prompt);空 prompt(如 resume 重挂)则不写,由 sendMessage 驱动
42
+ if (opts.prompt) {
43
+ this.write({
44
+ type: "user",
45
+ message: { role: "user", content: [{ type: "text", text: opts.prompt }] },
46
+ parent_tool_use_id: null,
47
+ });
48
+ }
49
+ const rl = createInterface({ input: this.proc.stdout });
50
+ rl.on("line", (line) => {
51
+ let msg;
52
+ try {
53
+ msg = JSON.parse(line);
54
+ }
55
+ catch {
56
+ return;
57
+ }
58
+ for (const event of this.normalize(msg)) {
59
+ this.queue.push(event);
60
+ }
61
+ if (msg.type === "result")
62
+ this.sawDone = true;
63
+ });
64
+ // 进程退出且 stdout 耗尽后结束迭代器
65
+ rl.on("close", () => {
66
+ if (!this.sawDone) {
67
+ this.queue.push({ kind: "done", status: "failed" });
68
+ }
69
+ this.queue.end();
70
+ });
71
+ }
72
+ *normalize(msg) {
73
+ if (msg.type === "system" && msg.subtype === "init") {
74
+ yield {
75
+ kind: "init",
76
+ agent: "claude-code",
77
+ session_id: msg.session_id ?? "",
78
+ model: msg.model,
79
+ capabilities: msg.capabilities ?? [],
80
+ };
81
+ return;
82
+ }
83
+ if (msg.type === "assistant") {
84
+ for (const block of msg.message?.content ?? []) {
85
+ if (block.type === "text" && block.text) {
86
+ yield { kind: "text_delta", text: block.text };
87
+ }
88
+ else if ((block.type === "thinking" || block.type === "reasoning") && (block.thinking || block.text)) {
89
+ yield { kind: "thinking_delta", text: block.thinking ?? block.text ?? "" };
90
+ }
91
+ else if (block.type === "tool_use") {
92
+ yield {
93
+ kind: "tool_call_start",
94
+ tool_call_id: block.id ?? "",
95
+ tool: block.name ?? "",
96
+ input: block.input,
97
+ };
98
+ }
99
+ }
100
+ return;
101
+ }
102
+ if (msg.type === "user") {
103
+ for (const block of msg.message?.content ?? []) {
104
+ if (block.type === "tool_result") {
105
+ yield {
106
+ kind: "tool_call_end",
107
+ tool_call_id: block.tool_use_id ?? "",
108
+ output: typeof block.content === "string" ? block.content : JSON.stringify(block.content),
109
+ is_error: block.is_error ?? false,
110
+ };
111
+ }
112
+ }
113
+ return;
114
+ }
115
+ if (msg.type === "control_request" && msg.request?.subtype === "can_use_tool") {
116
+ yield {
117
+ kind: "permission_request",
118
+ request_id: msg.request_id ?? "",
119
+ tool: msg.request.tool_name ?? "",
120
+ input: msg.request.input,
121
+ options: ["allow_once", "allow_always", "deny"],
122
+ };
123
+ return;
124
+ }
125
+ if (msg.type === "result") {
126
+ const status = msg.subtype === "success"
127
+ ? "succeeded"
128
+ : msg.subtype === "error_interrupted"
129
+ ? "cancelled"
130
+ : "failed";
131
+ const u = msg.usage;
132
+ const total_usage = u && (u.input_tokens != null || u.output_tokens != null)
133
+ ? {
134
+ input_tokens: u.input_tokens ?? 0,
135
+ output_tokens: u.output_tokens ?? 0,
136
+ ...(u.cache_read_input_tokens != null ? { cache_read_tokens: u.cache_read_input_tokens } : {}),
137
+ ...(u.cache_creation_input_tokens != null ? { cache_write_tokens: u.cache_creation_input_tokens } : {}),
138
+ }
139
+ : undefined;
140
+ yield { kind: "done", status, final_text: msg.result, ...(total_usage ? { total_usage } : {}) };
141
+ return;
142
+ }
143
+ }
144
+ write(obj) {
145
+ if (this.proc.stdin?.writable) {
146
+ this.proc.stdin.write(JSON.stringify(obj) + "\n");
147
+ }
148
+ }
149
+ async sendMessage(text) {
150
+ this.write({
151
+ type: "user",
152
+ message: { role: "user", content: [{ type: "text", text }] },
153
+ parent_tool_use_id: null,
154
+ });
155
+ }
156
+ async respondPermission(request_id, decision) {
157
+ this.write({
158
+ type: "control_response",
159
+ response: {
160
+ request_id: request_id,
161
+ response: { behavior: decision === "deny" ? "deny" : "allow" },
162
+ },
163
+ });
164
+ }
165
+ async interrupt() {
166
+ this.write({
167
+ type: "control_request",
168
+ request_id: randomUUID(),
169
+ request: { subtype: "interrupt" },
170
+ });
171
+ }
172
+ async close() {
173
+ this.proc.kill("SIGTERM");
174
+ this.queue.end();
175
+ }
176
+ [Symbol.asyncIterator]() {
177
+ return this.queue[Symbol.asyncIterator]();
178
+ }
179
+ }
180
+ export const claudeCodeAdapter = {
181
+ id: "claude-code",
182
+ capabilities: { can_interactive_approve: true, can_resume: true, can_steer: true },
183
+ start: async (opts) => new ClaudeCodeSession(opts),
184
+ };
@@ -0,0 +1,138 @@
1
+ import { spawn } from "node:child_process";
2
+ import { createInterface } from "node:readline";
3
+ import { EventQueue } from "./base.js";
4
+ /** binary 以 .mjs 结尾时用当前 node 执行(测试注入 fake CLI) */
5
+ const spawnCli = (binary, args, cwd, extraEnv) => {
6
+ const env = extraEnv ? { ...process.env, ...extraEnv } : process.env;
7
+ if (binary.endsWith(".mjs")) {
8
+ return spawn(process.execPath, [binary, ...args], { cwd, stdio: ["pipe", "pipe", "inherit"], env });
9
+ }
10
+ return spawn(binary, args, { cwd, stdio: ["pipe", "pipe", "inherit"], env });
11
+ };
12
+ /**
13
+ * cursor-agent 适配器:单向流(无 stdin 协议)。
14
+ * 多轮通过 respawn 实现:sendMessage 等当前进程退出后以 `--resume <chatId>` 重新拉起。
15
+ * 不支持运行中审批(启动即 --force),respondPermission 抛错。
16
+ */
17
+ class CursorSession {
18
+ opts;
19
+ queue = new EventQueue();
20
+ proc;
21
+ session_id;
22
+ closed = false;
23
+ constructor(opts) {
24
+ this.opts = opts;
25
+ this.session_id = opts.options?.resume_session_id;
26
+ // 空 prompt(resume 重挂):延迟到 sendMessage 才拉起进程
27
+ if (opts.prompt)
28
+ this.spawnTurn(opts.prompt, this.session_id);
29
+ }
30
+ spawnTurn(prompt, resumeId) {
31
+ const binary = this.opts.binary ?? "cursor-agent";
32
+ const args = [
33
+ "-p", "--output-format", "stream-json", "--force",
34
+ ...(this.opts.options?.model ? ["--model", this.opts.options.model] : []),
35
+ ...(resumeId ? ["--resume", resumeId] : []),
36
+ prompt,
37
+ ];
38
+ this.proc = spawnCli(binary, args, this.opts.cwd, this.opts.env);
39
+ // spawn 失败(如 agent 未安装 ENOENT):语义化 error + done
40
+ this.proc.on("error", (err) => {
41
+ this.queue.push({ kind: "error", message: `spawn ${binary} failed: ${err.message}`, retryable: false });
42
+ this.queue.push({ kind: "done", status: "failed" });
43
+ this.proc = undefined;
44
+ });
45
+ const rl = createInterface({ input: this.proc.stdout });
46
+ rl.on("line", (line) => {
47
+ let msg;
48
+ try {
49
+ msg = JSON.parse(line);
50
+ }
51
+ catch {
52
+ return;
53
+ }
54
+ for (const event of this.normalize(msg)) {
55
+ this.queue.push(event);
56
+ }
57
+ });
58
+ rl.on("close", () => {
59
+ // 单轮结束:迭代器保持打开,等待 sendMessage 开启下一轮或 close() 终结
60
+ this.proc = undefined;
61
+ });
62
+ }
63
+ *normalize(msg) {
64
+ if (msg.type === "system" && msg.subtype === "init") {
65
+ this.session_id = msg.session_id;
66
+ yield { kind: "init", agent: "cursor", session_id: msg.session_id ?? "", capabilities: [] };
67
+ return;
68
+ }
69
+ if (msg.type === "assistant") {
70
+ for (const block of msg.message?.content ?? []) {
71
+ if (block.type === "text" && block.text) {
72
+ yield { kind: "text_delta", text: block.text };
73
+ }
74
+ else if ((block.type === "thinking" || block.type === "reasoning") && (block.thinking || block.text)) {
75
+ yield { kind: "thinking_delta", text: block.thinking ?? block.text ?? "" };
76
+ }
77
+ else if (block.type === "tool_use") {
78
+ yield {
79
+ kind: "tool_call_start",
80
+ tool_call_id: block.id ?? "",
81
+ tool: block.name ?? "",
82
+ input: block.input,
83
+ };
84
+ }
85
+ }
86
+ return;
87
+ }
88
+ if (msg.type === "user") {
89
+ for (const block of msg.message?.content ?? []) {
90
+ if (block.type === "tool_result") {
91
+ yield {
92
+ kind: "tool_call_end",
93
+ tool_call_id: block.tool_use_id ?? "",
94
+ output: typeof block.content === "string" ? block.content : JSON.stringify(block.content),
95
+ is_error: block.is_error ?? false,
96
+ };
97
+ }
98
+ }
99
+ return;
100
+ }
101
+ if (msg.type === "result") {
102
+ yield {
103
+ kind: "done",
104
+ status: msg.subtype === "success" ? "succeeded" : "failed",
105
+ final_text: msg.result,
106
+ };
107
+ return;
108
+ }
109
+ }
110
+ async sendMessage(text) {
111
+ if (this.closed)
112
+ return;
113
+ // 等当前轮进程退出后 respawn(fake/真实 CLI 都会在 result 后退出)
114
+ if (this.proc) {
115
+ await new Promise((resolve) => this.proc.once("exit", () => resolve()));
116
+ }
117
+ this.spawnTurn(text, this.session_id);
118
+ }
119
+ async respondPermission() {
120
+ throw new Error("cursor 不支持运行中审批");
121
+ }
122
+ async interrupt() {
123
+ this.proc?.kill("SIGTERM");
124
+ }
125
+ async close() {
126
+ this.closed = true;
127
+ this.proc?.kill("SIGTERM");
128
+ this.queue.end();
129
+ }
130
+ [Symbol.asyncIterator]() {
131
+ return this.queue[Symbol.asyncIterator]();
132
+ }
133
+ }
134
+ export const cursorAdapter = {
135
+ id: "cursor",
136
+ capabilities: { can_interactive_approve: false, can_resume: true, can_steer: false },
137
+ start: async (opts) => new CursorSession(opts),
138
+ };
@@ -0,0 +1,51 @@
1
+ // runner 能力探测:决定能否用 --agent 让原生 agent 成为主会话身份。
2
+ // 探测失败不阻塞启动(按不支持处理),结果进程内缓存一次。
3
+ import { execFile } from "node:child_process";
4
+ const UNSUPPORTED = { agentFlag: false, strictMcp: false };
5
+ const cache = new Map();
6
+ /** SC_CLAUDE_AGENT_FLAG=1/0 可强制覆盖(运维应急 / 灰度) */
7
+ function override() {
8
+ const value = process.env["SC_CLAUDE_AGENT_FLAG"];
9
+ if (value === "1")
10
+ return true;
11
+ if (value === "0")
12
+ return false;
13
+ return undefined;
14
+ }
15
+ function probeHelp(binary) {
16
+ return new Promise((resolve) => {
17
+ execFile(binary, ["--help"], { timeout: 5_000, maxBuffer: 1024 * 1024 }, (err, stdout) => {
18
+ // 多数 CLI --help 退出码为 0;非 0 但有输出也照常解析
19
+ resolve(err && !stdout ? "" : String(stdout ?? ""));
20
+ });
21
+ });
22
+ }
23
+ /**
24
+ * 探测 runner 能力。cursor 没有对应 flag,直接返回不支持;
25
+ * 未知 runner 一律按「只支持主指令文件」处理。
26
+ */
27
+ export async function detectRunnerFeatures(runner, binary) {
28
+ if (runner !== "claude-code")
29
+ return UNSUPPORTED;
30
+ const key = `${runner}:${binary}`;
31
+ const cached = cache.get(key);
32
+ if (cached)
33
+ return cached;
34
+ const forced = override();
35
+ if (forced !== undefined) {
36
+ const features = { agentFlag: forced, strictMcp: true };
37
+ cache.set(key, features);
38
+ return features;
39
+ }
40
+ const help = await probeHelp(binary);
41
+ const features = {
42
+ agentFlag: /--agent\b/.test(help),
43
+ strictMcp: /--mcp-config\b/.test(help),
44
+ };
45
+ cache.set(key, features);
46
+ return features;
47
+ }
48
+ /** 测试用:清空探测缓存 */
49
+ export function resetFeatureCache() {
50
+ cache.clear();
51
+ }
@@ -0,0 +1,9 @@
1
+ import { claudeCodeAdapter } from './claude-code.js';
2
+ import { cursorAdapter } from './cursor.js';
3
+ /** agent_id → adapter */
4
+ export const adapters = new Map([claudeCodeAdapter, cursorAdapter].map(a => [a.id, a]));
5
+ /** agent_id → 本机 CLI 可执行文件名(detectAgents 探测用) */
6
+ export const agentBinaries = {
7
+ 'claude-code': 'claude',
8
+ cursor: 'cursor-agent',
9
+ };