@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
@@ -0,0 +1,220 @@
1
+ // 把合并后的 agent 物化到会话目录:原生 agent 定义 + 主会话指令块 + MCP 配置。
2
+ // 幂等:内容一致就不写盘;清理只删「上一轮写过且未被用户改动」的文件。
3
+ import { constants } from "node:fs";
4
+ import { access, mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
5
+ import { dirname, join } from "node:path";
6
+ import { buildManifest, readManifest, sha256, writeManifest, } from "./manifest.js";
7
+ import { renderClaudeAgentDoc } from "./renderers/claude-code.js";
8
+ import { renderCursorAgentDoc } from "./renderers/cursor.js";
9
+ import { removeBlock, sessionBlock, upsertBlock } from "./renderers/common.js";
10
+ import { renderGenericAgentDoc } from "./renderers/generic.js";
11
+ import { assertInside } from "./slug.js";
12
+ import { AgentError } from "./types.js";
13
+ const AGENT_FILE_MODE = 0o644;
14
+ const SECRET_FILE_MODE = 0o600;
15
+ const exists = async (path) => {
16
+ try {
17
+ await access(path, constants.F_OK);
18
+ return true;
19
+ }
20
+ catch {
21
+ return false;
22
+ }
23
+ };
24
+ async function readText(path) {
25
+ try {
26
+ return await readFile(path, "utf8");
27
+ }
28
+ catch {
29
+ return undefined;
30
+ }
31
+ }
32
+ /** 原子写:tmp + rename(rename 会带上 tmp 的权限位) */
33
+ async function writeAtomic(abs, content, mode) {
34
+ await mkdir(dirname(abs), { recursive: true });
35
+ const tmp = `${abs}.tmp-${process.pid}`;
36
+ await writeFile(tmp, content, { encoding: "utf8", mode });
37
+ await rename(tmp, abs);
38
+ }
39
+ async function syncManaged(workdir, rel, content, fileMode) {
40
+ const abs = assertInside(workdir, rel);
41
+ const current = await readText(abs);
42
+ if (current === content)
43
+ return false;
44
+ await writeAtomic(abs, content, fileMode);
45
+ return true;
46
+ }
47
+ async function syncBlock(workdir, rel, block) {
48
+ const abs = assertInside(workdir, rel);
49
+ const current = (await readText(abs)) ?? "";
50
+ const next = upsertBlock(current, block);
51
+ if (next === current)
52
+ return { content: next, changed: false };
53
+ await writeAtomic(abs, next, AGENT_FILE_MODE);
54
+ return { content: next, changed: true };
55
+ }
56
+ const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
57
+ /**
58
+ * 合并项目级 MCP 配置(cursor 只能写 .cursor/mcp.json,无法与用户全局完全隔离)。
59
+ * 只覆盖同名 key;本轮取消的 key 仅在「值仍是我们上次写入的值」时删除,避免误删用户改动。
60
+ */
61
+ async function mergeMcpJson(workdir, rel, servers, previous) {
62
+ const abs = assertInside(workdir, rel);
63
+ const raw = await readText(abs);
64
+ let doc = {};
65
+ if (raw?.trim()) {
66
+ try {
67
+ const parsed = JSON.parse(raw);
68
+ if (!isRecord(parsed))
69
+ throw new Error("not an object");
70
+ doc = parsed;
71
+ }
72
+ catch {
73
+ throw new AgentError("agent_materialize_failed", `${rel} 不是合法 JSON,拒绝覆盖`);
74
+ }
75
+ }
76
+ const currentServers = isRecord(doc["mcpServers"]) ? { ...doc["mcpServers"] } : {};
77
+ for (const [name, value] of Object.entries(previous ?? {})) {
78
+ if (name in servers)
79
+ continue;
80
+ if (JSON.stringify(currentServers[name]) === JSON.stringify(value))
81
+ delete currentServers[name];
82
+ }
83
+ Object.assign(currentServers, servers);
84
+ doc["mcpServers"] = currentServers;
85
+ const content = `${JSON.stringify(doc, null, 2)}\n`;
86
+ if (raw === content)
87
+ return { content, changed: false };
88
+ await writeAtomic(abs, content, SECRET_FILE_MODE);
89
+ return { content, changed: true };
90
+ }
91
+ /** 清理上一轮 manifest 里、本轮不再产出的条目 */
92
+ async function cleanupStale(workdir, previous, nextPaths) {
93
+ if (!previous)
94
+ return;
95
+ for (const file of previous.files) {
96
+ if (nextPaths.has(file.path) || file.mode === "merge")
97
+ continue;
98
+ const abs = assertInside(workdir, file.path);
99
+ const current = await readText(abs);
100
+ if (current === undefined)
101
+ continue;
102
+ if (file.mode === "managed") {
103
+ if (sha256(current) === file.sha256) {
104
+ await rm(abs, { force: true });
105
+ }
106
+ else {
107
+ console.warn(`[daemon] 保留被修改过的托管文件(不清理):${file.path}`);
108
+ }
109
+ continue;
110
+ }
111
+ // block:只摘掉托管块,保留用户内容
112
+ const stripped = removeBlock(current);
113
+ if (stripped !== current)
114
+ await writeAtomic(abs, stripped, AGENT_FILE_MODE);
115
+ }
116
+ }
117
+ function planFor(input) {
118
+ const { runner, agent, agentFlag } = input;
119
+ const slug = agent.slug;
120
+ if (runner === "claude-code") {
121
+ const agentDocPath = `.claude/agents/${slug}.md`;
122
+ const planned = [
123
+ { rel: agentDocPath, mode: "managed", content: renderClaudeAgentDoc(agent), fileMode: AGENT_FILE_MODE },
124
+ ];
125
+ if (Object.keys(agent.mcp_servers).length) {
126
+ planned.push({
127
+ rel: `.super-coding/agent/${slug}.mcp.json`,
128
+ mode: "managed",
129
+ content: `${JSON.stringify({ mcpServers: agent.mcp_servers }, null, 2)}\n`,
130
+ fileMode: SECRET_FILE_MODE,
131
+ });
132
+ }
133
+ return {
134
+ planned,
135
+ agentDocPath,
136
+ launch: {
137
+ ...(agentFlag ? { agentName: slug } : {}),
138
+ ...(Object.keys(agent.mcp_servers).length
139
+ ? { mcpConfigPath: assertInside(input.workdir, `.super-coding/agent/${slug}.mcp.json`) }
140
+ : {}),
141
+ ...(agent.tools.deny?.length ? { disallowedTools: agent.tools.deny } : {}),
142
+ },
143
+ };
144
+ }
145
+ if (runner === "cursor") {
146
+ const agentDocPath = `.cursor/agents/${slug}.md`;
147
+ return {
148
+ planned: [
149
+ { rel: agentDocPath, mode: "managed", content: renderCursorAgentDoc(agent), fileMode: AGENT_FILE_MODE },
150
+ ],
151
+ agentDocPath,
152
+ launch: {},
153
+ };
154
+ }
155
+ const agentDocPath = `.super-coding/agent/${slug}.md`;
156
+ return {
157
+ planned: [
158
+ { rel: agentDocPath, mode: "managed", content: renderGenericAgentDoc(agent), fileMode: AGENT_FILE_MODE },
159
+ ],
160
+ agentDocPath,
161
+ // 未知 runner 不保证读 AGENTS.md:把指令前缀到 prompt,语义不丢
162
+ launch: { promptPrefix: agent.instructions },
163
+ };
164
+ }
165
+ export async function materializeAgent(input) {
166
+ const { workdir, runner, agent } = input;
167
+ const previous = await readManifest(workdir);
168
+ const { planned, agentDocPath, launch } = planFor(input);
169
+ // 主会话指令文件(claude/cursor 优先各自生态,其余用 AGENTS.md)
170
+ const instructionFile = runner === "claude-code" ? "CLAUDE.md" : "AGENTS.md";
171
+ const block = sessionBlock(agent, { pointer: runner === "claude-code" && !!input.agentFlag, agentDocPath });
172
+ const files = [];
173
+ let changed = false;
174
+ for (const file of planned) {
175
+ const wrote = await syncManaged(workdir, file.rel, file.content, file.fileMode);
176
+ changed = changed || wrote;
177
+ files.push({ path: file.rel, mode: file.mode, sha256: sha256(file.content) });
178
+ }
179
+ const blockResult = await syncBlock(workdir, instructionFile, block);
180
+ changed = changed || blockResult.changed;
181
+ files.push({ path: instructionFile, mode: "block", sha256: sha256(blockResult.content) });
182
+ // MCP:claude 已在上面的 planned 里落文件;cursor 需要合并项目级 .cursor/mcp.json
183
+ if (runner === "cursor" && Object.keys(agent.mcp_servers).length) {
184
+ const merged = await mergeMcpJson(workdir, ".cursor/mcp.json", agent.mcp_servers, previous?.resolved.mcp_servers);
185
+ changed = changed || merged.changed;
186
+ files.push({ path: ".cursor/mcp.json", mode: "merge", sha256: sha256(merged.content) });
187
+ }
188
+ const nextPaths = new Set(files.map((f) => f.path));
189
+ await cleanupStale(workdir, previous, nextPaths);
190
+ const nextManifest = buildManifest(runner, agent, files);
191
+ const manifestChanged = !previous ||
192
+ previous.config_hash !== nextManifest.config_hash ||
193
+ JSON.stringify(previous.files) !== JSON.stringify(nextManifest.files);
194
+ if (manifestChanged)
195
+ await writeManifest(workdir, nextManifest);
196
+ return {
197
+ files: files.map((f) => f.path).sort(),
198
+ unchanged: !changed && !manifestChanged,
199
+ launch,
200
+ manifest: nextManifest,
201
+ };
202
+ }
203
+ /** reattach 用:从会话目录恢复上一轮的启动参数(daemon 重启后没有 run:start payload) */
204
+ export async function restoreAgentLaunch(workdir) {
205
+ const manifest = await readManifest(workdir);
206
+ if (!manifest)
207
+ return undefined;
208
+ const slug = manifest.resolved.slug;
209
+ if (!/^[a-z0-9][a-z0-9_-]{0,63}$/.test(slug))
210
+ return undefined;
211
+ const mcpRel = manifest.runner === "claude-code" ? `.super-coding/agent/${slug}.mcp.json` : undefined;
212
+ const mcpConfigPath = mcpRel && (await exists(join(workdir, mcpRel))) ? assertInside(workdir, mcpRel) : undefined;
213
+ return {
214
+ manifest,
215
+ launch: {
216
+ ...(mcpConfigPath ? { mcpConfigPath } : {}),
217
+ ...(manifest.resolved.tools.deny?.length ? { disallowedTools: manifest.resolved.tools.deny } : {}),
218
+ },
219
+ };
220
+ }
@@ -0,0 +1,16 @@
1
+ import { yamlScalar } from "./common.js";
2
+ /** claude-code 原生 agent 定义(.claude/agents/<slug>.md) */
3
+ export function renderClaudeAgentDoc(agent) {
4
+ const head = [
5
+ "---",
6
+ `name: ${agent.slug}`,
7
+ `description: ${yamlScalar(agent.description)}`,
8
+ ...(agent.tools.allow?.length ? [`tools: ${agent.tools.allow.join(", ")}`] : []),
9
+ ...(agent.tools.deny?.length ? [`disallowedTools: ${agent.tools.deny.join(", ")}`] : []),
10
+ ...(agent.model ? [`model: ${yamlScalar(agent.model)}`] : []),
11
+ ...(agent.permission_mode ? [`permissionMode: ${yamlScalar(agent.permission_mode)}`] : []),
12
+ "---",
13
+ "",
14
+ ];
15
+ return `${head.join("\n")}${agent.instructions.trim()}\n`;
16
+ }
@@ -0,0 +1,61 @@
1
+ export const BLOCK_BEGIN = "<!-- super-coding:agent:begin -->";
2
+ export const BLOCK_END = "<!-- super-coding:agent:end -->";
3
+ /** 需要引号时用 JSON 字符串(YAML 双引号语法兼容),否则原样输出 */
4
+ export function yamlScalar(value) {
5
+ const oneLine = value.replace(/\r?\n/g, " ").trim();
6
+ if (/[:#\n"'&*!?|>%@`]|^\s|\s$/.test(oneLine))
7
+ return JSON.stringify(oneLine);
8
+ return oneLine;
9
+ }
10
+ /** 工具约束的文字说明(runner 不支持结构化工具字段时作为软约束) */
11
+ export function toolsNote(tools) {
12
+ const lines = [];
13
+ if (tools.allow?.length)
14
+ lines.push(`- 只使用这些工具:${tools.allow.join(", ")}`);
15
+ if (tools.deny?.length)
16
+ lines.push(`- 禁止使用这些工具:${tools.deny.join(", ")}`);
17
+ return lines.length ? `## 工具约束\n\n${lines.join("\n")}` : "";
18
+ }
19
+ export function skillsNote(agent) {
20
+ if (agent.skills.length === 0)
21
+ return "";
22
+ const lines = agent.skills.map((skill) => {
23
+ const description = skill.description ? `:${skill.description}` : "";
24
+ return `- \`${skill.slug}\`${description}(${skill.relativePath})`;
25
+ });
26
+ return `### 可用技能\n\n${lines.join("\n")}`;
27
+ }
28
+ /** 主会话指令块:只维护 marker 之间内容,保留用户已有的 CLAUDE.md/AGENTS.md */
29
+ export function sessionBlock(agent, opts) {
30
+ const head = `## Super Coding Agent:${agent.name}`;
31
+ if (opts.pointer) {
32
+ return [
33
+ BLOCK_BEGIN,
34
+ head,
35
+ "",
36
+ `本会话由平台注入的原生 agent \`${agent.slug}\` 执行,定义与系统指令见 \`${opts.agentDocPath}\`。`,
37
+ "请始终以该 agent 的身份与约束工作。",
38
+ ...(skillsNote(agent) ? ["", skillsNote(agent)] : []),
39
+ BLOCK_END,
40
+ ].join("\n");
41
+ }
42
+ return [BLOCK_BEGIN, head, "", agent.instructions, "", BLOCK_END].join("\n");
43
+ }
44
+ /** 把托管块写入既有文件内容(保留块外内容,重复调用结果稳定) */
45
+ export function upsertBlock(existing, block) {
46
+ const start = existing.indexOf(BLOCK_BEGIN);
47
+ const end = existing.indexOf(BLOCK_END);
48
+ if (start >= 0 && end > start) {
49
+ return `${existing.slice(0, start)}${block}${existing.slice(end + BLOCK_END.length)}`;
50
+ }
51
+ const trimmed = existing.replace(/\s*$/, "");
52
+ return trimmed ? `${trimmed}\n\n${block}\n` : `${block}\n`;
53
+ }
54
+ /** 去掉托管块(换 runner / 清理时用),保留用户内容 */
55
+ export function removeBlock(existing) {
56
+ const start = existing.indexOf(BLOCK_BEGIN);
57
+ const end = existing.indexOf(BLOCK_END);
58
+ if (start < 0 || end <= start)
59
+ return existing;
60
+ return `${existing.slice(0, start)}${existing.slice(end + BLOCK_END.length)}`.replace(/\n{3,}/g, "\n\n").replace(/^\s+/, "");
61
+ }
@@ -0,0 +1,13 @@
1
+ import { toolsNote, yamlScalar } from "./common.js";
2
+ export function renderCursorAgentDoc(agent) {
3
+ const head = [
4
+ "---",
5
+ `name: ${agent.slug}`,
6
+ `description: ${yamlScalar(agent.description)}`,
7
+ ...(agent.model ? [`model: ${yamlScalar(agent.model)}`] : []),
8
+ "---",
9
+ "",
10
+ ];
11
+ const note = toolsNote(agent.tools);
12
+ return `${head.join("\n")}${note ? `${note}\n\n` : ""}${agent.instructions.trim()}\n`;
13
+ }
@@ -0,0 +1,5 @@
1
+ import { toolsNote } from "./common.js";
2
+ export function renderGenericAgentDoc(agent) {
3
+ const note = toolsNote(agent.tools);
4
+ return `# ${agent.name}\n\n${note ? `${note}\n\n` : ""}${agent.instructions.trim()}\n`;
5
+ }
@@ -0,0 +1,212 @@
1
+ import { renderTemplate } from "./builtin/parse.js";
2
+ import { builtinDefaults } from "./builtin/load.js";
3
+ import { agentSlug } from "./slug.js";
4
+ import { AgentError } from "./types.js";
5
+ /** claude 侧接受的权限模式;bypassPermissions 默认拒绝(可用环境变量显式放行) */
6
+ const PERMISSION_MODES = new Set(["default", "acceptEdits", "auto", "dontAsk", "plan", "bypassPermissions"]);
7
+ const ENV_KEY = /^[A-Za-z_][A-Za-z0-9_]*$/;
8
+ const ENV_DENY_KEYS = new Set(["PATH", "HOME", "SHELL", "PWD", "NODE_OPTIONS", "NODE_PATH"]);
9
+ const ENV_DENY_PREFIXES = ["SC_", "DAEMON_", "NEXUS_", "LD_", "DYLD_"];
10
+ const MAX_ENV_ENTRIES = 64;
11
+ const MAX_ENV_VALUE = 8 * 1024;
12
+ const MAX_MCP_SERVERS = 32;
13
+ const MAX_INSTRUCTIONS = 100_000;
14
+ const MAX_SKILLS = 64;
15
+ const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
16
+ function stringList(value, field) {
17
+ if (value === undefined)
18
+ return [];
19
+ if (!Array.isArray(value) || value.some((item) => typeof item !== "string")) {
20
+ throw new AgentError("agent_config_invalid", `${field} 必须是字符串数组`);
21
+ }
22
+ return [...new Set(value.map((item) => item.trim()).filter(Boolean))];
23
+ }
24
+ function normalizeTools(value) {
25
+ if (value === undefined)
26
+ return {};
27
+ if (Array.isArray(value))
28
+ return { allow: stringList(value, "config.tools") };
29
+ if (!isRecord(value))
30
+ throw new AgentError("agent_config_invalid", "config.tools 必须是数组或 {allow,deny}");
31
+ const tools = {};
32
+ const allow = stringList(value["allow"], "config.tools.allow");
33
+ const deny = stringList(value["deny"], "config.tools.deny");
34
+ if (allow.length)
35
+ tools.allow = allow;
36
+ if (deny.length)
37
+ tools.deny = deny;
38
+ return tools;
39
+ }
40
+ function normalizeEnv(value) {
41
+ if (value === undefined)
42
+ return {};
43
+ if (!isRecord(value))
44
+ throw new AgentError("agent_config_invalid", "config.env 必须是对象");
45
+ const entries = Object.entries(value);
46
+ if (entries.length > MAX_ENV_ENTRIES) {
47
+ throw new AgentError("agent_config_invalid", `config.env 超过 ${MAX_ENV_ENTRIES} 项`);
48
+ }
49
+ const env = {};
50
+ for (const [key, raw] of entries) {
51
+ if (!ENV_KEY.test(key) || key === "__proto__" || key === "constructor" || key === "prototype") {
52
+ throw new AgentError("agent_config_invalid", `config.env 非法变量名:${key}`);
53
+ }
54
+ if (ENV_DENY_KEYS.has(key.toUpperCase())) {
55
+ throw new AgentError("agent_config_invalid", `config.env 不允许覆盖 ${key}`);
56
+ }
57
+ const upper = key.toUpperCase();
58
+ if (ENV_DENY_PREFIXES.some((prefix) => upper.startsWith(prefix))) {
59
+ throw new AgentError("agent_config_invalid", `config.env 不允许使用 ${key} 前缀`);
60
+ }
61
+ if (typeof raw !== "string")
62
+ throw new AgentError("agent_config_invalid", `config.env.${key} 必须是字符串`);
63
+ if (raw.length > MAX_ENV_VALUE) {
64
+ throw new AgentError("agent_config_invalid", `config.env.${key} 超过 ${MAX_ENV_VALUE} 字符`);
65
+ }
66
+ env[key] = raw;
67
+ }
68
+ return env;
69
+ }
70
+ function normalizeMcp(value) {
71
+ if (value === undefined)
72
+ return {};
73
+ if (!isRecord(value))
74
+ throw new AgentError("agent_config_invalid", "config.mcp_servers 必须是对象");
75
+ const entries = Object.entries(value);
76
+ if (entries.length > MAX_MCP_SERVERS) {
77
+ throw new AgentError("agent_config_invalid", `config.mcp_servers 超过 ${MAX_MCP_SERVERS} 个`);
78
+ }
79
+ const servers = {};
80
+ for (const [name, raw] of entries) {
81
+ if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/.test(name)) {
82
+ throw new AgentError("agent_config_invalid", `config.mcp_servers 非法名称:${name}`);
83
+ }
84
+ if (!isRecord(raw))
85
+ throw new AgentError("agent_config_invalid", `config.mcp_servers.${name} 必须是对象`);
86
+ const type = raw["type"];
87
+ if (type !== undefined && type !== "stdio" && type !== "sse" && type !== "http") {
88
+ throw new AgentError("agent_config_invalid", `config.mcp_servers.${name}.type 不支持:${String(type)}`);
89
+ }
90
+ const command = typeof raw["command"] === "string" ? raw["command"].trim() : "";
91
+ const url = typeof raw["url"] === "string" ? raw["url"].trim() : "";
92
+ if (!command && !url) {
93
+ throw new AgentError("agent_config_invalid", `config.mcp_servers.${name} 需要 command 或 url`);
94
+ }
95
+ const args = raw["args"];
96
+ if (args !== undefined && (!Array.isArray(args) || args.some((a) => typeof a !== "string"))) {
97
+ throw new AgentError("agent_config_invalid", `config.mcp_servers.${name}.args 必须是字符串数组`);
98
+ }
99
+ const head = raw["headers"];
100
+ const processEnv = raw["env"];
101
+ for (const [field, value2] of [["env", processEnv], ["headers", head]]) {
102
+ if (value2 === undefined)
103
+ continue;
104
+ if (!isRecord(value2))
105
+ throw new AgentError("agent_config_invalid", `config.mcp_servers.${name}.${field} 必须是对象`);
106
+ for (const [k, v] of Object.entries(value2)) {
107
+ if (typeof v !== "string" || v.length > MAX_ENV_VALUE) {
108
+ throw new AgentError("agent_config_invalid", `config.mcp_servers.${name}.${field}.${k} 非法`);
109
+ }
110
+ }
111
+ }
112
+ servers[name] = {
113
+ ...(type ? { type } : { type: command ? "stdio" : "http" }),
114
+ ...(command ? { command } : {}),
115
+ ...(Array.isArray(args) ? { args: args } : {}),
116
+ ...(processEnv ? { env: processEnv } : {}),
117
+ ...(url ? { url } : {}),
118
+ ...(head ? { headers: head } : {}),
119
+ };
120
+ }
121
+ return servers;
122
+ }
123
+ function permissionMode(value, env) {
124
+ const mode = value?.trim();
125
+ if (!mode)
126
+ return undefined;
127
+ if (!PERMISSION_MODES.has(mode)) {
128
+ throw new AgentError("agent_config_invalid", `permission_mode 不支持:${mode}`);
129
+ }
130
+ // daemon 是无人值守执行器,权限审批无人应答;bypass 默认放开(deny_tools 仍生效),
131
+ // 需要收紧的平台可用 SC_ALLOW_BYPASS_PERMISSIONS=0 关闭。
132
+ if (mode === "bypassPermissions" && env["SC_ALLOW_BYPASS_PERMISSIONS"] === "0") {
133
+ throw new AgentError("agent_config_invalid", "permission_mode=bypassPermissions 已被 SC_ALLOW_BYPASS_PERMISSIONS=0 禁用");
134
+ }
135
+ return mode;
136
+ }
137
+ /** {{skills}} 的正文:只列名称与路径,具体步骤由执行器读 SKILL.md */
138
+ export function renderSkills(skills) {
139
+ if (skills.length === 0)
140
+ return "当前会话没有关联任何技能,按通用工程实践执行即可。";
141
+ const lines = skills.slice(0, MAX_SKILLS).map((skill) => {
142
+ const description = skill.description ? `:${skill.description}` : "";
143
+ return `- \`${skill.slug}\`${description}(${skill.relativePath})`;
144
+ });
145
+ return ["当前会话已挂载以下技能:", ...lines].join("\n");
146
+ }
147
+ function mergedInstructions(builtin, vars, user) {
148
+ const base = renderTemplate(builtin.instructions, vars).trim();
149
+ const extra = user?.trim();
150
+ if (!extra)
151
+ return base;
152
+ if (extra.length > MAX_INSTRUCTIONS) {
153
+ throw new AgentError("agent_config_invalid", `instructions 超过 ${MAX_INSTRUCTIONS} 字符`);
154
+ }
155
+ // 内置在前:安全基线不可被用户指令覆盖,只做追加
156
+ return `${base}\n\n---\n\n## 业务 Agent 补充指令\n\n${extra}`;
157
+ }
158
+ export function resolveAgent(input) {
159
+ const { builtin, runner, workdir, businessAgent, skills } = input;
160
+ const env = input.env ?? process.env;
161
+ if (businessAgent) {
162
+ if (businessAgent.runner && businessAgent.runner !== runner) {
163
+ throw new AgentError("agent_runner_mismatch", `agent ${businessAgent.id} 绑定 ${businessAgent.runner},当前会话执行器为 ${runner}`);
164
+ }
165
+ if (businessAgent.status && businessAgent.status !== "enabled") {
166
+ throw new AgentError("agent_disabled", `agent ${businessAgent.id} 状态为 ${businessAgent.status}`);
167
+ }
168
+ }
169
+ const config = businessAgent?.config;
170
+ if (config !== undefined && !isRecord(config)) {
171
+ throw new AgentError("agent_config_invalid", "agent.config 必须是对象");
172
+ }
173
+ const cfg = (config ?? {});
174
+ const name = businessAgent?.name?.trim() || builtin.name;
175
+ const description = businessAgent?.description?.trim() || builtin.description;
176
+ const slug = businessAgent ? agentSlug(businessAgent.id, name) : "sc-builtin-agent";
177
+ const userTools = normalizeTools(cfg.tools);
178
+ const tools = {
179
+ ...(userTools.allow?.length ? { allow: userTools.allow } : {}),
180
+ deny: [...new Set([...builtin.deny_tools, ...(userTools.deny ?? [])])],
181
+ };
182
+ if (!tools.deny?.length)
183
+ delete tools.deny;
184
+ const mcpServers = {
185
+ ...normalizeMcp(cfg.mcp_servers ?? cfg.mcpServers),
186
+ };
187
+ const instructions = mergedInstructions(builtin, {
188
+ name,
189
+ description,
190
+ workdir,
191
+ runner,
192
+ slug,
193
+ skills: renderSkills(skills),
194
+ repo_url: builtin.repo_url?.trim() || builtinDefaults.DEFAULT_REPO_URL,
195
+ }, typeof cfg.instructions === "string" ? cfg.instructions : undefined);
196
+ const model = input.options?.model?.trim() || cfg.model?.trim() || builtin.model;
197
+ const mode = permissionMode(input.options?.permission_mode ?? cfg.permission_mode ?? builtin.permission_mode, env);
198
+ return {
199
+ slug,
200
+ name,
201
+ description,
202
+ instructions,
203
+ ...(model ? { model } : {}),
204
+ ...(mode ? { permission_mode: mode } : {}),
205
+ tools,
206
+ mcp_servers: mcpServers,
207
+ env: normalizeEnv(cfg.env),
208
+ skills,
209
+ ...(businessAgent ? { businessAgent } : {}),
210
+ builtinSource: builtin.source,
211
+ };
212
+ }
@@ -0,0 +1,27 @@
1
+ // agent 在会话目录里的文件名:必须是 ASCII 安全 slug(执行器按目录名/文件名识别)。
2
+ // 用户配置里的 name 可能是中文或带空格,这里统一归一化后追加 id 短哈希避免重名。
3
+ import { createHash } from "node:crypto";
4
+ import { resolve, sep } from "node:path";
5
+ import { AgentError } from "./types.js";
6
+ const MAX_BASE = 32;
7
+ export function agentSlug(id, name) {
8
+ const base = name
9
+ .normalize("NFKD")
10
+ .toLowerCase()
11
+ .replace(/[^a-z0-9]+/g, "-")
12
+ .replace(/^-+|-+$/g, "")
13
+ .slice(0, MAX_BASE)
14
+ .replace(/-+$/g, "") || "agent";
15
+ // 名字也参与哈希:中文名归一化后都退化成 agent,仅用 id 会让改名拿不到新 slug(旧文件清不掉)
16
+ const hash = createHash("sha256").update(`${id}\u0000${name}`).digest("hex").slice(0, 8);
17
+ return `sc-${base}-${hash}`;
18
+ }
19
+ /** 解析相对路径并确保仍在会话目录内(防路径逃逸) */
20
+ export function assertInside(workdir, relative) {
21
+ const root = resolve(workdir);
22
+ const abs = resolve(root, relative);
23
+ if (abs !== root && !abs.startsWith(root + sep)) {
24
+ throw new AgentError("agent_config_invalid", `路径越界:${relative}`);
25
+ }
26
+ return abs;
27
+ }
@@ -0,0 +1,8 @@
1
+ export class AgentError extends Error {
2
+ code;
3
+ constructor(code, message) {
4
+ super(message);
5
+ this.code = code;
6
+ this.name = "AgentError";
7
+ }
8
+ }
@@ -0,0 +1,61 @@
1
+ import { chmodSync, copyFileSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { join } from "node:path";
4
+ const defaultDir = () => process.env.SC_HOME ?? join(homedir(), ".super-coding");
5
+ const file = (dir) => join(dir, "daemon.json");
6
+ const backup = (dir) => join(dir, "daemon.json.bak");
7
+ export function loadDevice(dir = defaultDir()) {
8
+ if (!existsSync(file(dir)))
9
+ return null;
10
+ return JSON.parse(readFileSync(file(dir), "utf8"));
11
+ }
12
+ export function saveDevice(cfg, dir = defaultDir()) {
13
+ mkdirSync(dir, { recursive: true });
14
+ writeFileSync(file(dir), JSON.stringify(cfg, null, 2), { mode: 0o600 });
15
+ chmodSync(file(dir), 0o600);
16
+ }
17
+ /**
18
+ * 删除本地设备凭证。
19
+ * keepBackup=true(默认,4401 自愈路径):删前备份 daemon.json.bak,启动时可自动恢复;
20
+ * keepBackup=false(真解绑 device:unpair):连备份一并清除,不复活已解绑凭证。
21
+ */
22
+ export function removeDevice(dir = defaultDir(), opts = {}) {
23
+ const keepBackup = opts.keepBackup ?? true;
24
+ if (existsSync(file(dir))) {
25
+ if (keepBackup) {
26
+ try {
27
+ copyFileSync(file(dir), backup(dir));
28
+ }
29
+ catch {
30
+ /* 备份失败不阻塞删除 */
31
+ }
32
+ }
33
+ rmSync(file(dir), { force: true });
34
+ }
35
+ if (!keepBackup)
36
+ rmSync(backup(dir), { force: true });
37
+ }
38
+ /** 从 daemon.json.bak 恢复凭证(4401 误删自愈);无备份返 null */
39
+ export function restoreDeviceBackup(dir = defaultDir()) {
40
+ if (existsSync(file(dir)) || !existsSync(backup(dir)))
41
+ return null;
42
+ try {
43
+ copyFileSync(backup(dir), file(dir));
44
+ chmodSync(file(dir), 0o600);
45
+ return loadDevice(dir);
46
+ }
47
+ catch {
48
+ return null;
49
+ }
50
+ }
51
+ export function loadDaemonConfig(dir = defaultDir()) {
52
+ const path = join(dir, "config.json");
53
+ if (!existsSync(path))
54
+ return {};
55
+ try {
56
+ return JSON.parse(readFileSync(path, "utf8"));
57
+ }
58
+ catch {
59
+ return {};
60
+ }
61
+ }