@lwmxiaobei/xbcode 1.0.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/LICENSE +21 -0
- package/README.md +631 -0
- package/README.zh-CN.md +542 -0
- package/dist/agent.js +1450 -0
- package/dist/busy-status.js +29 -0
- package/dist/clipboard-image.js +97 -0
- package/dist/commands.js +109 -0
- package/dist/compact.js +262 -0
- package/dist/config.js +516 -0
- package/dist/error-log.js +80 -0
- package/dist/http.js +89 -0
- package/dist/idle-watchdog.js +88 -0
- package/dist/index.js +2031 -0
- package/dist/input-submit.js +41 -0
- package/dist/mcp/client.js +466 -0
- package/dist/mcp/manager.js +275 -0
- package/dist/mcp/runtime.js +420 -0
- package/dist/mcp/types.js +12 -0
- package/dist/message-bus.js +180 -0
- package/dist/oauth/openai.js +326 -0
- package/dist/prompt.js +156 -0
- package/dist/session-store.js +186 -0
- package/dist/skills/frontmatter.js +85 -0
- package/dist/skills/index.js +2 -0
- package/dist/skills/loader.js +88 -0
- package/dist/skills/render.js +35 -0
- package/dist/skills/types.js +1 -0
- package/dist/subagents.js +64 -0
- package/dist/supervisor.js +58 -0
- package/dist/task-manager.js +280 -0
- package/dist/team-types.js +1 -0
- package/dist/teammate-manager.js +266 -0
- package/dist/tools.js +1068 -0
- package/dist/trust-store.js +42 -0
- package/dist/types.js +1 -0
- package/dist/usage.js +226 -0
- package/dist/utils.js +21 -0
- package/package.json +67 -0
- package/scripts/postinstall.mjs +30 -0
- package/skills/code-review/SKILL.md +22 -0
- package/skills/pdf/SKILL.md +18 -0
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import crypto from "node:crypto";
|
|
5
|
+
const XB_CONFIG_DIR = path.join(os.homedir(), ".xbcode");
|
|
6
|
+
/**
|
|
7
|
+
* 返回 session transcript 的根目录。
|
|
8
|
+
*
|
|
9
|
+
* 为什么支持环境变量覆写:
|
|
10
|
+
* - 生产环境默认仍然应该落在 `~/.xbcode/sessions`,方便和 settings 放在一起。
|
|
11
|
+
* - 测试和受限沙箱里,home 目录不一定可写,硬编码会让本地持久化能力无法验证。
|
|
12
|
+
* - 用一个集中 helper 做覆写点,可以避免把“测试专用路径”散落到业务逻辑里。
|
|
13
|
+
*/
|
|
14
|
+
function getSessionRootDir() {
|
|
15
|
+
const override = process.env.XBCODE_SESSION_DIR?.trim();
|
|
16
|
+
if (override) {
|
|
17
|
+
return override;
|
|
18
|
+
}
|
|
19
|
+
return path.join(XB_CONFIG_DIR, "sessions");
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* 为 CLI 会话生成一个可读且稳定的本地 ID。
|
|
23
|
+
*
|
|
24
|
+
* 为什么不用纯随机 UUID:
|
|
25
|
+
* - 会话 ID 会直接展示给用户做 `/resume <id>`,太长会明显影响可输入性。
|
|
26
|
+
* - 我们仍然需要足够低的冲突概率,因此保留时间戳前缀,再拼接短随机串。
|
|
27
|
+
* - 这个 ID 只用于本地文件命名和恢复,不承担安全边界,所以短 ID 足够。
|
|
28
|
+
*/
|
|
29
|
+
export function createSessionId(now = new Date()) {
|
|
30
|
+
const timestamp = now.toISOString().replaceAll(/[-:.TZ]/g, "").slice(0, 14);
|
|
31
|
+
const suffix = crypto.randomBytes(3).toString("hex");
|
|
32
|
+
return `${timestamp}-${suffix}`;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* 计算当前 workspace 对应的 session 目录。
|
|
36
|
+
*
|
|
37
|
+
* 为什么要按 workspace 分目录:
|
|
38
|
+
* - Claude Code 的 transcript 恢复是“当前项目视角”的;用户恢复时只应该看到
|
|
39
|
+
* 当前工程下的历史会话,而不是所有项目的混合列表。
|
|
40
|
+
* - 目录名如果直接使用绝对路径会包含斜杠和空格,不适合作为文件系统路径。
|
|
41
|
+
* - 这里保留 basename 方便人工识别,再拼接 hash 保证不同同名目录不冲突。
|
|
42
|
+
*/
|
|
43
|
+
function getWorkspaceSessionDir(workspace) {
|
|
44
|
+
const base = path.basename(workspace) || "workspace";
|
|
45
|
+
const safeBase = base.replaceAll(/[^A-Za-z0-9._-]/g, "_");
|
|
46
|
+
const hash = crypto.createHash("sha1").update(workspace).digest("hex").slice(0, 12);
|
|
47
|
+
return path.join(getSessionRootDir(), `${safeBase}-${hash}`);
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* 返回某个 session 对应的 JSONL transcript 路径。
|
|
51
|
+
*
|
|
52
|
+
* 为什么统一通过 helper:
|
|
53
|
+
* - 创建、列出、恢复都会用到同一套路径规则,集中在这里可以避免散落的拼接逻辑。
|
|
54
|
+
* - 未来如果要把单文件拆成目录结构,只需要改这一处,不必全局替换。
|
|
55
|
+
*/
|
|
56
|
+
function getSessionPath(workspace, sessionId) {
|
|
57
|
+
return path.join(getWorkspaceSessionDir(workspace), `${sessionId}.jsonl`);
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* 以追加写方式持久化当前会话快照。
|
|
61
|
+
*
|
|
62
|
+
* 为什么选择 append-only JSONL 而不是反复覆盖一个 JSON:
|
|
63
|
+
* - 这和 Claude Code 的 transcript 思路一致,写入路径更简单,也更抗意外中断。
|
|
64
|
+
* - 追加一行比重写整个文件更稳妥,尤其是在消息越来越多之后。
|
|
65
|
+
* - 当前实现只在 checkpoint 级别恢复,因此读取时只需要最后一个 checkpoint,
|
|
66
|
+
* 但保留历史增量可以给后续调试和回溯留下空间。
|
|
67
|
+
*/
|
|
68
|
+
export function appendSessionCheckpoint(workspace, snapshot) {
|
|
69
|
+
const filePath = getSessionPath(workspace, snapshot.state.sessionId);
|
|
70
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
71
|
+
if (!fs.existsSync(filePath)) {
|
|
72
|
+
const meta = {
|
|
73
|
+
kind: "meta",
|
|
74
|
+
sessionId: snapshot.state.sessionId,
|
|
75
|
+
workspace,
|
|
76
|
+
createdAt: new Date(snapshot.state.launchedAt).toISOString(),
|
|
77
|
+
};
|
|
78
|
+
fs.appendFileSync(filePath, `${JSON.stringify(meta)}\n`, "utf8");
|
|
79
|
+
}
|
|
80
|
+
const entry = {
|
|
81
|
+
kind: "checkpoint",
|
|
82
|
+
sessionId: snapshot.state.sessionId,
|
|
83
|
+
savedAt: snapshot.savedAt,
|
|
84
|
+
snapshot,
|
|
85
|
+
};
|
|
86
|
+
fs.appendFileSync(filePath, `${JSON.stringify(entry)}\n`, "utf8");
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* 读取当前 workspace 下最近的历史会话摘要。
|
|
90
|
+
*
|
|
91
|
+
* 为什么这里接受“读全文件再找最后 checkpoint”的简化实现:
|
|
92
|
+
* - `code-agent` 目前还没有 Claude Code 那种超大 transcript 和分页恢复需求。
|
|
93
|
+
* - 先把恢复闭环做通,比过早引入 head/tail 扫描、锁文件、lite metadata 更重要。
|
|
94
|
+
* - 当 session 文件明显变大时,再把这里替换成轻量扫描即可,不影响上层接口。
|
|
95
|
+
*/
|
|
96
|
+
export function listRecentSessions(workspace, limit = 10) {
|
|
97
|
+
const dir = getWorkspaceSessionDir(workspace);
|
|
98
|
+
if (!fs.existsSync(dir)) {
|
|
99
|
+
return [];
|
|
100
|
+
}
|
|
101
|
+
const items = [];
|
|
102
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
103
|
+
if (!entry.isFile() || !entry.name.endsWith(".jsonl")) {
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
const loaded = loadSessionFromFile(path.join(dir, entry.name));
|
|
107
|
+
if (!loaded) {
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
const title = extractSessionTitle(loaded.snapshot.messages);
|
|
111
|
+
items.push({
|
|
112
|
+
sessionId: loaded.snapshot.state.sessionId,
|
|
113
|
+
createdAt: loaded.createdAt,
|
|
114
|
+
savedAt: loaded.snapshot.savedAt,
|
|
115
|
+
title,
|
|
116
|
+
turnCount: loaded.snapshot.state.turnCount,
|
|
117
|
+
model: loaded.snapshot.model,
|
|
118
|
+
providerName: loaded.snapshot.providerName,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
return items
|
|
122
|
+
.sort((left, right) => right.savedAt.localeCompare(left.savedAt))
|
|
123
|
+
.slice(0, Math.max(0, limit));
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* 加载一个指定的历史会话,并返回最后一次 checkpoint。
|
|
127
|
+
*
|
|
128
|
+
* 为什么只恢复最后 checkpoint:
|
|
129
|
+
* - 当前恢复目标是“把模型上下文和界面消息恢复到可继续工作的最近状态”。
|
|
130
|
+
* - 既然 checkpoint 已经包含完整 `AgentState` 和 UI 消息,就没有必要重放整个日志。
|
|
131
|
+
* - 这也让恢复过程保持确定性,避免中途存在半写入流式消息时的复杂合并。
|
|
132
|
+
*/
|
|
133
|
+
export function loadSession(workspace, sessionId) {
|
|
134
|
+
const filePath = getSessionPath(workspace, sessionId);
|
|
135
|
+
const loaded = loadSessionFromFile(filePath);
|
|
136
|
+
return loaded?.snapshot ?? null;
|
|
137
|
+
}
|
|
138
|
+
function loadSessionFromFile(filePath) {
|
|
139
|
+
try {
|
|
140
|
+
const content = fs.readFileSync(filePath, "utf8");
|
|
141
|
+
const lines = content.split(/\r?\n/).filter((line) => line.trim().length > 0);
|
|
142
|
+
let createdAt = "";
|
|
143
|
+
let lastSnapshot = null;
|
|
144
|
+
for (const line of lines) {
|
|
145
|
+
const parsed = JSON.parse(line);
|
|
146
|
+
if (parsed.kind === "meta") {
|
|
147
|
+
createdAt = parsed.createdAt;
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
if (parsed.kind === "checkpoint") {
|
|
151
|
+
lastSnapshot = parsed.snapshot;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
if (!lastSnapshot) {
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
return {
|
|
158
|
+
createdAt: createdAt || new Date(lastSnapshot.state.launchedAt).toISOString(),
|
|
159
|
+
snapshot: lastSnapshot,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
catch {
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* 从已持久化消息里提取会话标题。
|
|
168
|
+
*
|
|
169
|
+
* 为什么优先看第一条用户消息:
|
|
170
|
+
* - Claude Code 在 session 列表里也依赖“第一条有意义的用户输入”来帮助识别会话。
|
|
171
|
+
* - 这比模型自动命名更稳定,也不需要额外一次生成或保存专门标题字段。
|
|
172
|
+
* - 工具、系统提示、thinking 内容都不是用户真正想恢复的主题,所以应跳过。
|
|
173
|
+
*/
|
|
174
|
+
function extractSessionTitle(messages) {
|
|
175
|
+
for (const message of messages) {
|
|
176
|
+
if (message.kind !== "user") {
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
const compact = message.text.replaceAll(/\s+/g, " ").trim();
|
|
180
|
+
if (!compact) {
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
return compact.length > 80 ? `${compact.slice(0, 80).trim()}...` : compact;
|
|
184
|
+
}
|
|
185
|
+
return "(untitled session)";
|
|
186
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
function stripWrappingQuotes(value) {
|
|
2
|
+
const trimmed = value.trim();
|
|
3
|
+
if ((trimmed.startsWith('"') && trimmed.endsWith('"')) ||
|
|
4
|
+
(trimmed.startsWith("'") && trimmed.endsWith("'"))) {
|
|
5
|
+
return trimmed.slice(1, -1);
|
|
6
|
+
}
|
|
7
|
+
return trimmed;
|
|
8
|
+
}
|
|
9
|
+
function parseListValue(value) {
|
|
10
|
+
return stripWrappingQuotes(value)
|
|
11
|
+
.split(",")
|
|
12
|
+
.map((item) => item.trim())
|
|
13
|
+
.filter(Boolean);
|
|
14
|
+
}
|
|
15
|
+
export function parseSkillFile(text) {
|
|
16
|
+
const match = text.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);
|
|
17
|
+
if (!match) {
|
|
18
|
+
return { meta: {}, body: text.trim() };
|
|
19
|
+
}
|
|
20
|
+
const meta = {};
|
|
21
|
+
const lines = (match[1] ?? "").split(/\r?\n/);
|
|
22
|
+
let currentListKey = null;
|
|
23
|
+
for (const rawLine of lines) {
|
|
24
|
+
const line = rawLine.trimEnd();
|
|
25
|
+
if (!line.trim() || line.trimStart().startsWith("#")) {
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
const listMatch = line.match(/^\s*-\s+(.+)$/);
|
|
29
|
+
if (listMatch && currentListKey) {
|
|
30
|
+
const existing = meta[currentListKey];
|
|
31
|
+
const nextValue = stripWrappingQuotes(listMatch[1] ?? "");
|
|
32
|
+
if (Array.isArray(existing)) {
|
|
33
|
+
existing.push(nextValue);
|
|
34
|
+
}
|
|
35
|
+
else if (typeof existing === "string" && existing.length > 0) {
|
|
36
|
+
meta[currentListKey] = [existing, nextValue];
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
meta[currentListKey] = [nextValue];
|
|
40
|
+
}
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
const colonIndex = line.indexOf(":");
|
|
44
|
+
if (colonIndex === -1) {
|
|
45
|
+
currentListKey = null;
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
const key = line.slice(0, colonIndex).trim();
|
|
49
|
+
const rawValue = line.slice(colonIndex + 1).trim();
|
|
50
|
+
if (!rawValue) {
|
|
51
|
+
meta[key] = [];
|
|
52
|
+
currentListKey = key;
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
if (key === "allowed-tools") {
|
|
56
|
+
meta[key] = parseListValue(rawValue);
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
meta[key] = stripWrappingQuotes(rawValue);
|
|
60
|
+
}
|
|
61
|
+
currentListKey = null;
|
|
62
|
+
}
|
|
63
|
+
return {
|
|
64
|
+
meta: meta,
|
|
65
|
+
body: (match[2] ?? "").trim(),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
export function buildSkillDescriptor(meta, body, fallbackName, filePath, baseDir) {
|
|
69
|
+
const allowedTools = Array.isArray(meta["allowed-tools"])
|
|
70
|
+
? meta["allowed-tools"]
|
|
71
|
+
: typeof meta["allowed-tools"] === "string"
|
|
72
|
+
? parseListValue(meta["allowed-tools"])
|
|
73
|
+
: [];
|
|
74
|
+
return {
|
|
75
|
+
name: meta.name || fallbackName,
|
|
76
|
+
description: meta.description || "No description",
|
|
77
|
+
tags: meta.tags,
|
|
78
|
+
whenToUse: meta.when_to_use,
|
|
79
|
+
allowedTools,
|
|
80
|
+
argumentHint: meta["argument-hint"],
|
|
81
|
+
body,
|
|
82
|
+
filePath,
|
|
83
|
+
baseDir,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { debugLog } from "../utils.js";
|
|
4
|
+
import { buildSkillDescriptor, parseSkillFile } from "./frontmatter.js";
|
|
5
|
+
import { renderPromptCommand } from "./render.js";
|
|
6
|
+
export class SkillLoader {
|
|
7
|
+
commands = new Map();
|
|
8
|
+
constructor(skillsDirs) {
|
|
9
|
+
for (const dir of skillsDirs) {
|
|
10
|
+
this.loadAll(dir);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
loadAll(skillsDir) {
|
|
14
|
+
if (!fs.existsSync(skillsDir))
|
|
15
|
+
return;
|
|
16
|
+
const entries = fs.readdirSync(skillsDir, { withFileTypes: true });
|
|
17
|
+
for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
18
|
+
if (!entry.isDirectory())
|
|
19
|
+
continue;
|
|
20
|
+
const baseDir = path.join(skillsDir, entry.name);
|
|
21
|
+
const filePath = path.join(baseDir, "SKILL.md");
|
|
22
|
+
if (!fs.existsSync(filePath))
|
|
23
|
+
continue;
|
|
24
|
+
const text = fs.readFileSync(filePath, "utf8");
|
|
25
|
+
const { meta, body } = parseSkillFile(text);
|
|
26
|
+
const descriptor = buildSkillDescriptor(meta, body, entry.name, filePath, baseDir);
|
|
27
|
+
this.commands.set(descriptor.name, this.toPromptCommand(descriptor));
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
toPromptCommand(skill) {
|
|
31
|
+
return {
|
|
32
|
+
name: skill.name,
|
|
33
|
+
description: skill.description,
|
|
34
|
+
tags: skill.tags,
|
|
35
|
+
whenToUse: skill.whenToUse,
|
|
36
|
+
allowedTools: [...skill.allowedTools],
|
|
37
|
+
argumentHint: skill.argumentHint,
|
|
38
|
+
content: skill.body,
|
|
39
|
+
filePath: skill.filePath,
|
|
40
|
+
baseDir: skill.baseDir,
|
|
41
|
+
source: "skill",
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
getPromptCommands() {
|
|
45
|
+
return [...this.commands.values()];
|
|
46
|
+
}
|
|
47
|
+
getDescriptions() {
|
|
48
|
+
const commands = this.getPromptCommands();
|
|
49
|
+
debugLog("promptCommands=%s", commands);
|
|
50
|
+
if (commands.length === 0)
|
|
51
|
+
return "(no skills available)";
|
|
52
|
+
return commands
|
|
53
|
+
.map((command) => {
|
|
54
|
+
let line = ` - ${command.name}: ${command.description}`;
|
|
55
|
+
if (command.tags)
|
|
56
|
+
line += ` [${command.tags}]`;
|
|
57
|
+
return line;
|
|
58
|
+
})
|
|
59
|
+
.join("\n");
|
|
60
|
+
}
|
|
61
|
+
getSkill(name) {
|
|
62
|
+
const command = this.commands.get(name);
|
|
63
|
+
if (!command) {
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
66
|
+
return {
|
|
67
|
+
name: command.name,
|
|
68
|
+
description: command.description,
|
|
69
|
+
tags: command.tags,
|
|
70
|
+
whenToUse: command.whenToUse,
|
|
71
|
+
allowedTools: [...command.allowedTools],
|
|
72
|
+
argumentHint: command.argumentHint,
|
|
73
|
+
body: command.content,
|
|
74
|
+
filePath: command.filePath,
|
|
75
|
+
baseDir: command.baseDir,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
getCommand(name) {
|
|
79
|
+
return this.commands.get(name);
|
|
80
|
+
}
|
|
81
|
+
renderSkill(name, args) {
|
|
82
|
+
const command = this.commands.get(name);
|
|
83
|
+
if (!command) {
|
|
84
|
+
return `Error: Unknown skill '${name}'. Available: ${[...this.commands.keys()].join(", ")}`;
|
|
85
|
+
}
|
|
86
|
+
return renderPromptCommand(command, args);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export function renderPromptCommand(command, args) {
|
|
2
|
+
const parts = [`<skill name="${command.name}">`];
|
|
3
|
+
if (command.baseDir) {
|
|
4
|
+
parts.push(`Base directory for this skill: ${command.baseDir}`);
|
|
5
|
+
}
|
|
6
|
+
if (command.whenToUse) {
|
|
7
|
+
parts.push(`When to use: ${command.whenToUse}`);
|
|
8
|
+
}
|
|
9
|
+
if (command.allowedTools.length > 0) {
|
|
10
|
+
parts.push(`Allowed tools: ${command.allowedTools.join(", ")}`);
|
|
11
|
+
}
|
|
12
|
+
if (command.argumentHint) {
|
|
13
|
+
parts.push(`Argument hint: ${command.argumentHint}`);
|
|
14
|
+
}
|
|
15
|
+
if (args && args.trim()) {
|
|
16
|
+
parts.push(`User args:\n${args.trim()}`);
|
|
17
|
+
}
|
|
18
|
+
parts.push(command.content);
|
|
19
|
+
parts.push("</skill>");
|
|
20
|
+
return parts.join("\n\n");
|
|
21
|
+
}
|
|
22
|
+
export function renderSkillContent(skill, args) {
|
|
23
|
+
return renderPromptCommand({
|
|
24
|
+
name: skill.name,
|
|
25
|
+
description: skill.description,
|
|
26
|
+
tags: skill.tags,
|
|
27
|
+
whenToUse: skill.whenToUse,
|
|
28
|
+
allowedTools: skill.allowedTools,
|
|
29
|
+
argumentHint: skill.argumentHint,
|
|
30
|
+
content: skill.body,
|
|
31
|
+
filePath: skill.filePath,
|
|
32
|
+
baseDir: skill.baseDir,
|
|
33
|
+
source: "skill",
|
|
34
|
+
}, args);
|
|
35
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// 两个内置子代理:
|
|
2
|
+
export const SUBAGENT_DEFINITIONS = [
|
|
3
|
+
{
|
|
4
|
+
name: "general-purpose",
|
|
5
|
+
whenToUse: "Default worker for implementation, editing, and focused subtasks inside the current workspace.",
|
|
6
|
+
systemPrompt: "You are a general-purpose sub-agent handling a specific task. Complete it thoroughly, use tools directly, and end with a concise summary of what you changed or found.",
|
|
7
|
+
allowedTools: [
|
|
8
|
+
"bash",
|
|
9
|
+
"read_file",
|
|
10
|
+
"write_file",
|
|
11
|
+
"edit_file",
|
|
12
|
+
"glob",
|
|
13
|
+
"grep",
|
|
14
|
+
"task_create",
|
|
15
|
+
"task_update",
|
|
16
|
+
"task_list",
|
|
17
|
+
"task_get",
|
|
18
|
+
"list_mcp_resources",
|
|
19
|
+
"read_mcp_resource",
|
|
20
|
+
"mcp_call",
|
|
21
|
+
"load_skill",
|
|
22
|
+
],
|
|
23
|
+
maxRounds: 30,
|
|
24
|
+
readOnlyShell: false,
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
name: "explore",
|
|
28
|
+
whenToUse: "Read-only codebase exploration, searching, tracing behavior, and answering implementation questions without changing files.",
|
|
29
|
+
systemPrompt: `You are a read-only exploration sub-agent.
|
|
30
|
+
Your only job is to inspect the workspace, search code, read files, and report findings clearly.
|
|
31
|
+
You must not modify files.
|
|
32
|
+
You must not run shell commands that change files, install dependencies, create directories, or alter git state.
|
|
33
|
+
Prefer glob for file discovery, grep for content search, and read_file for targeted inspection. Use bash for read-only commands such as ls, find, cat, git status, and git diff.
|
|
34
|
+
Return findings as a concise factual summary.`,
|
|
35
|
+
allowedTools: [
|
|
36
|
+
"bash",
|
|
37
|
+
"read_file",
|
|
38
|
+
"glob",
|
|
39
|
+
"grep",
|
|
40
|
+
"task_list",
|
|
41
|
+
"task_get",
|
|
42
|
+
"list_mcp_resources",
|
|
43
|
+
"read_mcp_resource",
|
|
44
|
+
"mcp_call",
|
|
45
|
+
"load_skill",
|
|
46
|
+
],
|
|
47
|
+
maxRounds: 20,
|
|
48
|
+
readOnlyShell: true,
|
|
49
|
+
},
|
|
50
|
+
];
|
|
51
|
+
// 这里提供统一入口,而不是让调用方自己遍历数组,
|
|
52
|
+
// 这样后续如果 agent 定义改成从文件或配置加载,调用方不需要改。
|
|
53
|
+
export function getSubagentDefinition(name) {
|
|
54
|
+
if (!name) {
|
|
55
|
+
return SUBAGENT_DEFINITIONS[0];
|
|
56
|
+
}
|
|
57
|
+
return SUBAGENT_DEFINITIONS.find((agent) => agent.name === name) ?? SUBAGENT_DEFINITIONS[0];
|
|
58
|
+
}
|
|
59
|
+
// Task 工具描述里直接暴露可选 agent,有助于主 agent 在不额外读文档的情况下学会委派。
|
|
60
|
+
export function describeSubagentsForHumans() {
|
|
61
|
+
return SUBAGENT_DEFINITIONS
|
|
62
|
+
.map((agent) => `- ${agent.name}: ${agent.whenToUse}`)
|
|
63
|
+
.join("\n");
|
|
64
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { taskManager, teammateManager } from "./tools.js";
|
|
2
|
+
function updateTeammateFromEvent(event) {
|
|
3
|
+
if (!event.from || event.from === "lead") {
|
|
4
|
+
return;
|
|
5
|
+
}
|
|
6
|
+
const task = typeof event.taskId === "number" ? taskManager.getTask(event.taskId) : null;
|
|
7
|
+
const threadId = event.threadId ?? task?.threadId;
|
|
8
|
+
if (event.eventType === "task_started") {
|
|
9
|
+
teammateManager.setCurrentWork(event.from, event.taskId, threadId, event.content);
|
|
10
|
+
teammateManager.markWorking(event.from);
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
if (event.eventType === "task_blocked") {
|
|
14
|
+
teammateManager.setCurrentWork(event.from, event.taskId, threadId, event.content);
|
|
15
|
+
teammateManager.markBlocked(event.from, event.content);
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
if (event.eventType === "task_completed" || event.eventType === "task_failed") {
|
|
19
|
+
teammateManager.setCurrentWork(event.from, event.taskId, threadId, event.content);
|
|
20
|
+
teammateManager.markIdle(event.from);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function applyTaskEvent(event) {
|
|
24
|
+
if (typeof event.taskId !== "number") {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
const task = taskManager.getTask(event.taskId);
|
|
28
|
+
if (!task) {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
updateTeammateFromEvent(event);
|
|
32
|
+
switch (event.eventType) {
|
|
33
|
+
case "task_started":
|
|
34
|
+
taskManager.update(task.id, "in_progress");
|
|
35
|
+
return `task_started #${task.id}`;
|
|
36
|
+
case "task_blocked":
|
|
37
|
+
taskManager.update(task.id, "blocked", undefined, undefined, undefined, undefined, event.content);
|
|
38
|
+
return `task_blocked #${task.id}`;
|
|
39
|
+
case "task_completed":
|
|
40
|
+
taskManager.update(task.id, "completed", undefined, undefined, undefined, event.content);
|
|
41
|
+
return `task_completed #${task.id}`;
|
|
42
|
+
case "task_failed":
|
|
43
|
+
taskManager.update(task.id, "failed", undefined, undefined, undefined, event.content);
|
|
44
|
+
return `task_failed #${task.id}`;
|
|
45
|
+
default:
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
export function processLeadInboxEvents(events) {
|
|
50
|
+
const applied = [];
|
|
51
|
+
for (const event of events) {
|
|
52
|
+
const result = applyTaskEvent(event);
|
|
53
|
+
if (result) {
|
|
54
|
+
applied.push(result);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return { applied };
|
|
58
|
+
}
|