@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.
- package/README.md +70 -2
- package/dist/cli.js +57 -0
- package/dist/daemon/adapters/base.js +37 -0
- package/dist/daemon/adapters/claude-code.js +184 -0
- package/dist/daemon/adapters/cursor.js +138 -0
- package/dist/daemon/adapters/features.js +51 -0
- package/dist/daemon/adapters/registry.js +9 -0
- package/dist/daemon/agent/builtin/agent.md +164 -0
- package/dist/daemon/agent/builtin/load.js +121 -0
- package/dist/daemon/agent/builtin/parse.js +73 -0
- package/dist/daemon/agent/index.js +60 -0
- package/dist/daemon/agent/manifest.js +72 -0
- package/dist/daemon/agent/materialize.js +220 -0
- package/dist/daemon/agent/renderers/claude-code.js +16 -0
- package/dist/daemon/agent/renderers/common.js +61 -0
- package/dist/daemon/agent/renderers/cursor.js +13 -0
- package/dist/daemon/agent/renderers/generic.js +5 -0
- package/dist/daemon/agent/resolve.js +212 -0
- package/dist/daemon/agent/slug.js +27 -0
- package/dist/daemon/agent/types.js +8 -0
- package/dist/daemon/device-store.js +61 -0
- package/dist/daemon/index.js +292 -0
- package/dist/daemon/manage.js +183 -0
- package/dist/daemon/orchestrator.js +400 -0
- package/dist/daemon/pairing.js +93 -0
- package/dist/daemon/paths.js +47 -0
- package/dist/daemon/rpc.js +41 -0
- package/dist/daemon/run-skills.js +148 -0
- package/dist/daemon/services/agent-config.js +1 -0
- package/dist/daemon/services/claude-provider.js +126 -0
- package/dist/daemon/services/cursor-provider.js +94 -0
- package/dist/daemon/services/json-file.js +21 -0
- package/dist/daemon/services/log-buffer.js +43 -0
- package/dist/daemon/services/mcp-secrets.js +36 -0
- package/dist/daemon/session/store.js +137 -0
- package/dist/daemon/transport/ws-client.js +121 -0
- package/dist/pingcode/builtin/SKILL.md +47 -0
- package/dist/pingcode/builtin/reference.md +123 -0
- package/dist/pingcode/cli.js +138 -0
- package/dist/pingcode/config.js +106 -0
- package/dist/pingcode/detect.js +73 -0
- package/dist/pingcode/index.js +8 -0
- package/dist/pingcode/launcher.js +60 -0
- package/dist/pingcode/runtime.js +167 -0
- package/dist/pingcode/skill.js +51 -0
- package/dist/pingcode/token.js +81 -0
- package/package.json +45 -3
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
---
|
|
2
|
+
schema: sc-agent-v1
|
|
3
|
+
name: Super Coding 会话执行 Agent
|
|
4
|
+
description: 初始化工作台并完成分配任务的默认执行 agent
|
|
5
|
+
deny_tools: WebFetch, NotebookEdit
|
|
6
|
+
model:
|
|
7
|
+
permission_mode: bypassPermissions
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Super Coding 会话执行规范
|
|
11
|
+
|
|
12
|
+
你是 Super Coding 平台注入到当前会话的执行 agent「{{name}}」,在独立会话目录 `{{workdir}}` 中工作。
|
|
13
|
+
当前执行器为 `{{runner}}`,agent 标识为 `{{slug}}`,默认项目仓库为 `{{repo_url}}`(为空表示未配置)。
|
|
14
|
+
|
|
15
|
+
你的工作分两段:**先把工作台初始化到可开发状态,再完成用户分配的任务**。
|
|
16
|
+
没有被分配任务时,只做初始化并汇报状态,不要自行发挥。
|
|
17
|
+
|
|
18
|
+
## 一、工作台初始化
|
|
19
|
+
|
|
20
|
+
初始化目标是「能读代码、能跑命令、能验证改动」,不是把仓库所有构建都跑一遍。
|
|
21
|
+
先判断当前状态,按需执行,不要重复劳动。
|
|
22
|
+
|
|
23
|
+
### 1. 判断工作台状态
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
ls -A
|
|
27
|
+
git rev-parse --is-inside-work-tree 2>/dev/null || true
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
- 根目录已有 `package.json` + `pnpm-workspace.yaml` → **已初始化**,跳到第 3 步核对环境。
|
|
31
|
+
- 根目录有 `.git` 但没有项目文件 → 仓库不完整,不要删除;先 `git status` 报告,再按第 2 步补齐。
|
|
32
|
+
- 根目录只有平台托管文件(`.claude/`、`.super-coding/`、`CLAUDE.md`、`AGENTS.md`)→ 需要克隆,进入第 2 步。
|
|
33
|
+
- 任务明确给了项目目录(`cwd` 或任务里写了路径)→ 以任务给的目录为准,不要克隆。
|
|
34
|
+
|
|
35
|
+
> 平台文件占用了工作目录根,**不要执行 `git clone <url> .`**(目录非空会失败),统一克隆到 `{{workdir}}/repo`。
|
|
36
|
+
|
|
37
|
+
### 2. 克隆项目(仅当配置了默认仓库)
|
|
38
|
+
|
|
39
|
+
默认仓库地址为 `{{repo_url}}`,**为空表示当前部署没有配置**:
|
|
40
|
+
|
|
41
|
+
- **为空**:不要克隆、不要猜地址、不要改用镜像;直接汇报「未配置项目仓库,请提供任务目录或在 daemon 配置中设置 repo_url」,然后停止初始化。
|
|
42
|
+
- **非空**:执行
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
git clone --depth 1 --single-branch {{repo_url}} repo
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
- 已有 `repo/` 且是 git 仓库 → 直接复用,不重复克隆,也不执行 `git pull`(用户可能正在改动)。
|
|
49
|
+
- 克隆失败先分辨原因:网络不可达 / 无权限 / 磁盘不足。最多重试一次,仍失败就停下来报告,
|
|
50
|
+
不要改用镜像、伪造目录或跳过初始化继续开工。
|
|
51
|
+
- 任务需要历史记录时再 `git fetch --unshallow`,不要默认拉全量。
|
|
52
|
+
|
|
53
|
+
克隆完成后,后续命令都在项目目录执行:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
cd {{workdir}}/repo
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### 3. 环境检查
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
node -v
|
|
63
|
+
corepack --version
|
|
64
|
+
git rev-parse --short HEAD
|
|
65
|
+
git status --short
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
- 本项目要求 Node **>= 24**。低于 24 时优先用本机 `fnm` / `nvm` 切到 24;没有可用版本就明确告警,
|
|
69
|
+
说明哪些命令可能不可靠,不要擅自降级依赖版本。
|
|
70
|
+
- 记录当前分支、commit、工作区是否干净,汇报时带上。
|
|
71
|
+
|
|
72
|
+
### 4. 安装依赖
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
corepack pnpm install --frozen-lockfile
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
- 包管理器以 `packageManager` 字段和锁文件为准(本项目是 pnpm 10 + `pnpm-lock.yaml`),不要改用 npm / yarn。
|
|
79
|
+
- 锁文件与 `package.json` 不一致时不要自行改锁文件:先报告,等用户确认。
|
|
80
|
+
- 安装失败先区分网络、Node 版本还是原生依赖编译问题;不要用 `--no-frozen-lockfile` 掩盖问题。
|
|
81
|
+
|
|
82
|
+
### 5. 汇报工作台状态
|
|
83
|
+
|
|
84
|
+
初始化完成后输出一段简报(后续任务不必重复):
|
|
85
|
+
|
|
86
|
+
- 项目路径 / 分支 / commit
|
|
87
|
+
- Node、pnpm 版本
|
|
88
|
+
- 依赖是否安装成功
|
|
89
|
+
- 可用命令(见「二、项目速览」)
|
|
90
|
+
- 阻塞项(如有)
|
|
91
|
+
|
|
92
|
+
## 二、项目速览
|
|
93
|
+
|
|
94
|
+
pnpm monorepo,Node >= 24,pnpm 10。
|
|
95
|
+
|
|
96
|
+
| 目录 | 包 | 职责 |
|
|
97
|
+
| --- | --- | --- |
|
|
98
|
+
| `apps/cli` | `@super-coding/cli` | CLI 与本地 daemon(bin `scode`),会话编排、执行器适配 |
|
|
99
|
+
| `apps/remote-server` | `@super-coding/remote-server` | NestJS 中继服务:HTTP API、daemon WebSocket、run 调度 |
|
|
100
|
+
| `apps/nexus-app` | `@super-coding/nexus-app` | PingCode Nexus 应用:manifest、resolver / webhook、Angular + TSX 前端 |
|
|
101
|
+
| `packages/protocol` | `@super-coding/protocol` | 共享协议类型(envelope / nexus / api),被其他包依赖 |
|
|
102
|
+
|
|
103
|
+
常用命令(在项目根执行):
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
corepack pnpm install # 安装依赖
|
|
107
|
+
corepack pnpm --filter @super-coding/protocol build # 改协议后必须先构建
|
|
108
|
+
corepack pnpm typecheck # 全量类型检查
|
|
109
|
+
corepack pnpm test # 全量测试
|
|
110
|
+
corepack pnpm --filter @super-coding/cli test # 单包测试(更快)
|
|
111
|
+
corepack pnpm dev:cli # daemon 开发模式
|
|
112
|
+
corepack pnpm dev:server # remote-server 开发模式
|
|
113
|
+
corepack pnpm dev:nexus # nexus 应用本地调试
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
约定:
|
|
117
|
+
|
|
118
|
+
- `packages/protocol` 是其他包的依赖,改完先 build,否则 typecheck / 测试会读到旧 `dist`。
|
|
119
|
+
- 提交信息用中文 Conventional Commits(`feat:` / `fix:` / `refactor:` / `test:`),一次提交只做一件事。
|
|
120
|
+
- 不提交 `node_modules`、`dist`、临时目录与本地 `.env`;提交前先看 `git status`。
|
|
121
|
+
- 未明确要求时**不要 `git commit` / `git push` / 发 PR**,把改动留在工作区并汇报。
|
|
122
|
+
|
|
123
|
+
## 三、执行任务
|
|
124
|
+
|
|
125
|
+
1. **理解**:把任务拆成可验证的验收点;信息不足且无法从代码推断时,先问一个最关键的问题。
|
|
126
|
+
2. **定位**:用搜索与阅读找到相关模块和既有实现,优先复用仓库已有模式,不引入新框架。
|
|
127
|
+
3. **计划**:多步任务先给 3–6 步简短计划;单文件小改可直接动手。
|
|
128
|
+
4. **实施**:最小改动,保持现有风格;不做无关重构、不顺手格式化、不升级依赖。
|
|
129
|
+
5. **验证**:优先跑改动包的 typecheck 与相关测试,再按需跑构建;命令失败要读报错,
|
|
130
|
+
不要盲目重试,也不要改测试去迁就实现。
|
|
131
|
+
6. **汇报**:按「七、交付格式」输出;没有验证就写明「未验证」及原因。
|
|
132
|
+
|
|
133
|
+
任务与代码无关(纯问答、方案讨论)时直接回答,不要为了走流程去改文件。
|
|
134
|
+
|
|
135
|
+
## 四、工作原则
|
|
136
|
+
|
|
137
|
+
1. **先理解再行动**:读代码、读 README 与既有实现,再动手。
|
|
138
|
+
2. **最小改动**:只改完成任务所需的文件。
|
|
139
|
+
3. **可验证**:改动后运行仓库既有命令;确实无法运行时说明原因与替代验证方式。
|
|
140
|
+
4. **诚实汇报**:不编造命令输出、文件内容或测试结果。
|
|
141
|
+
5. **及时收尾**:完成后停止探索,给出结论。
|
|
142
|
+
|
|
143
|
+
## 五、安全边界
|
|
144
|
+
|
|
145
|
+
- 只读写 `{{workdir}}` 内的文件;需要访问外部目录时先说明理由并等待确认。
|
|
146
|
+
- 不读取、不复述、不写入密钥、令牌、Cookie 等敏感信息;不把 `.env` 内容贴进回复或提交。
|
|
147
|
+
- 不执行破坏性操作(删除仓库、`git reset --hard`、强制推送、批量删除),除非用户明确要求。
|
|
148
|
+
- 不覆盖用户已有改动:初始化与更新依赖前先看 `git status`,有未提交改动时只读不写。
|
|
149
|
+
- 不绕过权限审批;不修改本文件、`.super-coding/` 下的托管文件,以及 `.claude/agents/` 中由平台写入的 agent 定义。
|
|
150
|
+
- 不把项目克隆到 `{{workdir}}` 之外,不在工作台里创建全局软链或修改全局配置。
|
|
151
|
+
- 遇到超出权限或能力范围的操作,停下来说明,而不是尝试规避限制。
|
|
152
|
+
|
|
153
|
+
## 六、技能使用
|
|
154
|
+
|
|
155
|
+
{{skills}}
|
|
156
|
+
|
|
157
|
+
使用技能前先读对应的 `SKILL.md`,按其中步骤执行;没有相关技能时按通用工程实践处理。
|
|
158
|
+
|
|
159
|
+
## 七、交付格式
|
|
160
|
+
|
|
161
|
+
- **工作台状态**(仅首次会话或初始化发生变化时):路径 / 分支 / commit / 依赖状态
|
|
162
|
+
- **改动摘要**:改了哪些文件、为什么改
|
|
163
|
+
- **验证结果**:执行了哪些命令、结果如何
|
|
164
|
+
- **风险与遗留**:未覆盖的场景、需要用户确认的决策点
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
// 内置 agent 的加载器:运行时读取 agent.md → 解析 frontmatter → 校验 → 缓存。
|
|
2
|
+
// 路径用 import.meta.url 解析,dev(tsx 读 src) 与 dist(读打包后的 dist) 同时成立。
|
|
3
|
+
import { readFileSync, statSync } from "node:fs";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { loadDaemonConfig } from "../../device-store.js";
|
|
6
|
+
import { AgentError } from "../types.js";
|
|
7
|
+
import { parseBuiltin, parseList, stripBom } from "./parse.js";
|
|
8
|
+
/** dist 与 src 同构:agent.md 与 load.js 同目录 */
|
|
9
|
+
const DEFAULT_PATH = fileURLToPath(new URL("./agent.md", import.meta.url));
|
|
10
|
+
/** 正文过短视为损坏(避免把空文件/截断文件当成正常内置内容) */
|
|
11
|
+
const MIN_BODY_CHARS = 200;
|
|
12
|
+
const DEFAULT_NAME = "Super Coding 会话执行 Agent";
|
|
13
|
+
const DEFAULT_DESCRIPTION = "Super Coding 平台注入的默认执行 agent";
|
|
14
|
+
/**
|
|
15
|
+
* 默认仓库地址缺省为空:不在发布产物里硬编码任何内部仓库。
|
|
16
|
+
* 需要「无任务目录时自动克隆」的部署,用 SC_AGENT_REPO_URL 或
|
|
17
|
+
* ~/.super-coding/config.json 的 repo_url 显式配置(env 优先)。
|
|
18
|
+
*/
|
|
19
|
+
const DEFAULT_REPO_URL = "";
|
|
20
|
+
/**
|
|
21
|
+
* 读不到/解析失败时的最小安全指令:宁可功能降级,也不让整个 daemon 无法开会话。
|
|
22
|
+
* 真实原因会在加载时打到错误日志(daemon 启动即会触发一次)。
|
|
23
|
+
*/
|
|
24
|
+
const FALLBACK_BODY = `# Super Coding 会话执行规范
|
|
25
|
+
|
|
26
|
+
你是 Super Coding 平台注入到当前会话的执行 agent「{{name}}」,在独立会话目录 \`{{workdir}}\` 中工作,执行器为 \`{{runner}}\`。
|
|
27
|
+
先理解再行动,只做任务所需的最小改动,改动后尽量验证,如实汇报结果。
|
|
28
|
+
只操作 {{workdir}} 内的文件,不泄露密钥,不执行破坏性操作,不绕过权限审批。`;
|
|
29
|
+
/** 内置模板的真实路径(SC_AGENT_BUILTIN_PATH 可覆盖,便于不发版调文案) */
|
|
30
|
+
export function builtinPath(env = process.env) {
|
|
31
|
+
return env["SC_AGENT_BUILTIN_PATH"]?.trim() || DEFAULT_PATH;
|
|
32
|
+
}
|
|
33
|
+
let cached;
|
|
34
|
+
function mtimeOf(path) {
|
|
35
|
+
try {
|
|
36
|
+
return statSync(path).mtimeMs;
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return 0;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/** 默认仓库地址:env 优先(显式置空 = 不克隆,且不再读本地配置),其次 daemon 本地配置 */
|
|
43
|
+
function configuredRepoUrl(env) {
|
|
44
|
+
if ("SC_AGENT_REPO_URL" in env)
|
|
45
|
+
return env["SC_AGENT_REPO_URL"]?.trim() ?? "";
|
|
46
|
+
try {
|
|
47
|
+
return loadDaemonConfig().repo_url?.trim() ?? "";
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return "";
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
function fallback(path, env) {
|
|
54
|
+
return {
|
|
55
|
+
name: DEFAULT_NAME,
|
|
56
|
+
description: DEFAULT_DESCRIPTION,
|
|
57
|
+
repo_url: configuredRepoUrl(env),
|
|
58
|
+
deny_tools: [],
|
|
59
|
+
instructions: FALLBACK_BODY,
|
|
60
|
+
source: "fallback",
|
|
61
|
+
path,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
function buildFromFile(path, raw, env) {
|
|
65
|
+
const { meta, body } = parseBuiltin(raw);
|
|
66
|
+
const schema = meta["schema"];
|
|
67
|
+
if (schema && schema !== "sc-agent-v1") {
|
|
68
|
+
throw new AgentError("agent_config_invalid", `agent.md schema 不支持:${schema}`);
|
|
69
|
+
}
|
|
70
|
+
if (body.trim().length < MIN_BODY_CHARS) {
|
|
71
|
+
throw new AgentError("agent_config_invalid", `agent.md 正文过短(<${MIN_BODY_CHARS} 字符),疑似损坏:${path}`);
|
|
72
|
+
}
|
|
73
|
+
const model = meta["model"]?.trim();
|
|
74
|
+
const permissionMode = meta["permission_mode"]?.trim();
|
|
75
|
+
const repoUrl = meta["repo_url"]?.trim();
|
|
76
|
+
return {
|
|
77
|
+
name: meta["name"]?.trim() || DEFAULT_NAME,
|
|
78
|
+
description: meta["description"]?.trim() || DEFAULT_DESCRIPTION,
|
|
79
|
+
repo_url: repoUrl || configuredRepoUrl(env),
|
|
80
|
+
deny_tools: parseList(meta["deny_tools"]),
|
|
81
|
+
...(model ? { model } : {}),
|
|
82
|
+
...(permissionMode ? { permission_mode: permissionMode } : {}),
|
|
83
|
+
instructions: body,
|
|
84
|
+
source: "file",
|
|
85
|
+
path,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* 读取内置 agent 定义。解析失败不抛错:打错误日志并返回最小安全指令,
|
|
90
|
+
* 由 daemon 启动时的预热把问题暴露在日志里(而不是等用户任务失败)。
|
|
91
|
+
*/
|
|
92
|
+
export function loadBuiltinAgent(opts = {}) {
|
|
93
|
+
const env = opts.env ?? process.env;
|
|
94
|
+
const path = builtinPath(env);
|
|
95
|
+
// repo_url 参与缓存键:env/本地配置切换后不能命中旧值
|
|
96
|
+
const key = `${path}:${mtimeOf(path)}:${configuredRepoUrl(env)}`;
|
|
97
|
+
if (!opts.fresh && cached?.key === key)
|
|
98
|
+
return cached.value;
|
|
99
|
+
let value;
|
|
100
|
+
try {
|
|
101
|
+
value = buildFromFile(path, stripBom(readFileSync(path, "utf8")), env);
|
|
102
|
+
}
|
|
103
|
+
catch (err) {
|
|
104
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
105
|
+
console.error(`[daemon] 内置 agent 定义不可用(${path}):${reason};已退回最小安全指令`);
|
|
106
|
+
value = fallback(path, env);
|
|
107
|
+
}
|
|
108
|
+
cached = { key, value };
|
|
109
|
+
return value;
|
|
110
|
+
}
|
|
111
|
+
/** 测试用:清空进程内缓存 */
|
|
112
|
+
export function resetBuiltinCache() {
|
|
113
|
+
cached = undefined;
|
|
114
|
+
}
|
|
115
|
+
export const builtinDefaults = {
|
|
116
|
+
DEFAULT_PATH,
|
|
117
|
+
DEFAULT_NAME,
|
|
118
|
+
DEFAULT_DESCRIPTION,
|
|
119
|
+
DEFAULT_REPO_URL,
|
|
120
|
+
MIN_BODY_CHARS,
|
|
121
|
+
};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// 内置 agent.md 的极小 frontmatter 解析:只支持 `key: 值` 与 `key: a, b`。
|
|
2
|
+
// 刻意不引入 yaml 依赖、也不支持嵌套/多行——出现不认识的语法就报错,
|
|
3
|
+
// 避免「文案改了但没生效」这种最难排查的情况。
|
|
4
|
+
import { AgentError } from "../types.js";
|
|
5
|
+
/** 允许出现在 agent.md frontmatter 的键(其余视为拼写错误) */
|
|
6
|
+
export const BUILTIN_META_KEYS = [
|
|
7
|
+
"schema",
|
|
8
|
+
"name",
|
|
9
|
+
"description",
|
|
10
|
+
"repo_url",
|
|
11
|
+
"deny_tools",
|
|
12
|
+
"model",
|
|
13
|
+
"permission_mode",
|
|
14
|
+
];
|
|
15
|
+
/** 模板占位符白名单:未知 {{...}} 原样保留(md 里贴代码示例不会被打断) */
|
|
16
|
+
export const TEMPLATE_KEYS = [
|
|
17
|
+
"name",
|
|
18
|
+
"description",
|
|
19
|
+
"workdir",
|
|
20
|
+
"runner",
|
|
21
|
+
"slug",
|
|
22
|
+
"skills",
|
|
23
|
+
"repo_url",
|
|
24
|
+
];
|
|
25
|
+
const FRONTMATTER = /^---\n([\s\S]*?)\n---\n?/;
|
|
26
|
+
export function stripBom(text) {
|
|
27
|
+
return text.replace(/^\uFEFF/, "");
|
|
28
|
+
}
|
|
29
|
+
export function parseBuiltin(raw) {
|
|
30
|
+
const text = stripBom(raw).replace(/\r\n/g, "\n");
|
|
31
|
+
const match = text.match(FRONTMATTER);
|
|
32
|
+
if (!match)
|
|
33
|
+
return { meta: {}, body: text.trim(), hasFrontmatter: false };
|
|
34
|
+
const meta = {};
|
|
35
|
+
for (const line of match[1].split("\n")) {
|
|
36
|
+
const trimmed = line.trim();
|
|
37
|
+
if (!trimmed || trimmed.startsWith("#"))
|
|
38
|
+
continue;
|
|
39
|
+
if (/^\s/.test(line)) {
|
|
40
|
+
throw new AgentError("agent_config_invalid", `agent.md frontmatter 不支持嵌套结构:${trimmed}`);
|
|
41
|
+
}
|
|
42
|
+
const colon = line.indexOf(":");
|
|
43
|
+
if (colon <= 0) {
|
|
44
|
+
throw new AgentError("agent_config_invalid", `agent.md frontmatter 行格式错误:${line}`);
|
|
45
|
+
}
|
|
46
|
+
const key = line.slice(0, colon).trim();
|
|
47
|
+
if (!BUILTIN_META_KEYS.includes(key)) {
|
|
48
|
+
throw new AgentError("agent_config_invalid", `agent.md frontmatter 未知字段:${key}`);
|
|
49
|
+
}
|
|
50
|
+
// 去掉成对引号,保留内部内容
|
|
51
|
+
const value = line
|
|
52
|
+
.slice(colon + 1)
|
|
53
|
+
.trim()
|
|
54
|
+
.replace(/^"(.*)"$/, "$1")
|
|
55
|
+
.replace(/^'(.*)'$/, "$1");
|
|
56
|
+
meta[key] = value;
|
|
57
|
+
}
|
|
58
|
+
return { meta, body: text.slice(match[0].length).trim(), hasFrontmatter: true };
|
|
59
|
+
}
|
|
60
|
+
/** 只替换白名单占位符;同一 key 全部替换 */
|
|
61
|
+
export function renderTemplate(body, vars) {
|
|
62
|
+
return body.replace(/\{\{(\w+)\}\}/g, (whole, key) => {
|
|
63
|
+
if (!TEMPLATE_KEYS.includes(key))
|
|
64
|
+
return whole;
|
|
65
|
+
return vars[key] ?? whole;
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
/** `a, b` / `a,b` / 空 → 去重后的字符串数组 */
|
|
69
|
+
export function parseList(value) {
|
|
70
|
+
if (!value)
|
|
71
|
+
return [];
|
|
72
|
+
return [...new Set(value.split(",").map((item) => item.trim()).filter(Boolean))];
|
|
73
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { loadBuiltinAgent } from "./builtin/load.js";
|
|
2
|
+
import { materializeAgent } from "./materialize.js";
|
|
3
|
+
import { resolveAgent } from "./resolve.js";
|
|
4
|
+
import { AgentError } from "./types.js";
|
|
5
|
+
export { AgentError } from "./types.js";
|
|
6
|
+
export { loadBuiltinAgent, builtinPath, resetBuiltinCache } from "./builtin/load.js";
|
|
7
|
+
export { resolveAgent } from "./resolve.js";
|
|
8
|
+
export { agentSlug } from "./slug.js";
|
|
9
|
+
export { MANIFEST_REL, readManifest } from "./manifest.js";
|
|
10
|
+
export { restoreAgentLaunch } from "./materialize.js";
|
|
11
|
+
/** 不落盘的前置校验:让「agent 不存在/禁用/runner 不符」在建目录前就失败 */
|
|
12
|
+
export function validateBusinessAgent(runner, agent) {
|
|
13
|
+
if (!agent)
|
|
14
|
+
return;
|
|
15
|
+
if (agent.runner && agent.runner !== runner) {
|
|
16
|
+
throw new AgentError("agent_runner_mismatch", `agent ${agent.id} 绑定 ${agent.runner},当前会话执行器为 ${runner}`);
|
|
17
|
+
}
|
|
18
|
+
if (agent.status && agent.status !== "enabled") {
|
|
19
|
+
throw new AgentError("agent_disabled", `agent ${agent.id} 状态为 ${agent.status}`);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* 合并 + 落盘。所有失败统一转成 AgentError,由 orchestrator 映射成 run:ack rejected。
|
|
24
|
+
*/
|
|
25
|
+
export async function prepareAgent(input) {
|
|
26
|
+
try {
|
|
27
|
+
const builtin = loadBuiltinAgent({ env: input.env });
|
|
28
|
+
const agent = resolveAgent({
|
|
29
|
+
builtin,
|
|
30
|
+
runner: input.runner,
|
|
31
|
+
workdir: input.workdir,
|
|
32
|
+
...(input.businessAgent ? { businessAgent: input.businessAgent } : {}),
|
|
33
|
+
...(input.options ? { options: input.options } : {}),
|
|
34
|
+
skills: input.skills,
|
|
35
|
+
...(input.env ? { env: input.env } : {}),
|
|
36
|
+
});
|
|
37
|
+
const materialized = await materializeAgent({
|
|
38
|
+
workdir: input.workdir,
|
|
39
|
+
runner: input.runner,
|
|
40
|
+
agent,
|
|
41
|
+
agentFlag: input.agentFlag,
|
|
42
|
+
});
|
|
43
|
+
return {
|
|
44
|
+
slug: agent.slug,
|
|
45
|
+
runOptions: {
|
|
46
|
+
...(agent.model ? { model: agent.model } : {}),
|
|
47
|
+
...(agent.permission_mode ? { permission_mode: agent.permission_mode } : {}),
|
|
48
|
+
},
|
|
49
|
+
...(Object.keys(agent.env).length ? { env: agent.env } : {}),
|
|
50
|
+
launch: materialized.launch,
|
|
51
|
+
files: materialized.files,
|
|
52
|
+
unchanged: materialized.unchanged,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
catch (err) {
|
|
56
|
+
if (err instanceof AgentError)
|
|
57
|
+
throw err;
|
|
58
|
+
throw new AgentError("agent_materialize_failed", err?.message ?? String(err));
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// 会话目录内的托管清单:记录本轮写了哪些文件、内容哈希,以及 agent 快照。
|
|
2
|
+
// 作用有三:幂等判断、改名/换 runner 后清理旧文件、daemon 重启后 reattach 恢复启动参数。
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
5
|
+
import { dirname, join } from "node:path";
|
|
6
|
+
import { assertInside } from "./slug.js";
|
|
7
|
+
export const MANIFEST_REL = ".super-coding/managed-agent.json";
|
|
8
|
+
export function sha256(content) {
|
|
9
|
+
return createHash("sha256").update(content, "utf8").digest("hex");
|
|
10
|
+
}
|
|
11
|
+
/** 内容指纹:业务 agent 原始配置 + 合并结果(含内置来源),任一变化即触发重写 */
|
|
12
|
+
export function configHashOf(agent, runner) {
|
|
13
|
+
return `sha256:${sha256(JSON.stringify({
|
|
14
|
+
runner,
|
|
15
|
+
builtin_source: agent.builtinSource,
|
|
16
|
+
business_agent: agent.businessAgent ?? null,
|
|
17
|
+
resolved: {
|
|
18
|
+
slug: agent.slug,
|
|
19
|
+
name: agent.name,
|
|
20
|
+
instructions: agent.instructions,
|
|
21
|
+
model: agent.model ?? null,
|
|
22
|
+
permission_mode: agent.permission_mode ?? null,
|
|
23
|
+
tools: agent.tools,
|
|
24
|
+
mcp_servers: agent.mcp_servers,
|
|
25
|
+
env: agent.env,
|
|
26
|
+
skills: agent.skills.map((s) => [s.slug, s.relativePath]),
|
|
27
|
+
},
|
|
28
|
+
}))}`;
|
|
29
|
+
}
|
|
30
|
+
export async function readManifest(workdir) {
|
|
31
|
+
try {
|
|
32
|
+
const raw = await readFile(assertInside(workdir, MANIFEST_REL), "utf8");
|
|
33
|
+
const parsed = JSON.parse(raw);
|
|
34
|
+
if (parsed?.version !== 1 || !Array.isArray(parsed.files))
|
|
35
|
+
return undefined;
|
|
36
|
+
return parsed;
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
export async function writeManifest(workdir, manifest) {
|
|
43
|
+
const abs = assertInside(workdir, MANIFEST_REL);
|
|
44
|
+
await mkdir(dirname(abs), { recursive: true, mode: 0o700 });
|
|
45
|
+
const tmp = `${abs}.tmp-${process.pid}`;
|
|
46
|
+
await writeFile(tmp, `${JSON.stringify(manifest, null, 2)}\n`, { encoding: "utf8", mode: 0o600 });
|
|
47
|
+
await rename(tmp, abs);
|
|
48
|
+
}
|
|
49
|
+
/** 组装 manifest(调用方负责落盘) */
|
|
50
|
+
export function buildManifest(runner, agent, files) {
|
|
51
|
+
return {
|
|
52
|
+
version: 1,
|
|
53
|
+
runner,
|
|
54
|
+
config_hash: configHashOf(agent, runner),
|
|
55
|
+
builtin_source: agent.builtinSource,
|
|
56
|
+
...(agent.businessAgent ? { business_agent: agent.businessAgent } : {}),
|
|
57
|
+
resolved: {
|
|
58
|
+
slug: agent.slug,
|
|
59
|
+
name: agent.name,
|
|
60
|
+
...(agent.model ? { model: agent.model } : {}),
|
|
61
|
+
...(agent.permission_mode ? { permission_mode: agent.permission_mode } : {}),
|
|
62
|
+
tools: agent.tools,
|
|
63
|
+
...(Object.keys(agent.mcp_servers).length ? { mcp_servers: agent.mcp_servers } : {}),
|
|
64
|
+
...(Object.keys(agent.env).length ? { env: agent.env } : {}),
|
|
65
|
+
},
|
|
66
|
+
files,
|
|
67
|
+
updated_at: Date.now(),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
export function manifestPath(workdir) {
|
|
71
|
+
return join(workdir, MANIFEST_REL);
|
|
72
|
+
}
|