@zhongqian97-code/ecode 0.5.33 → 0.5.35
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/dist/chunk-GR5MASXF.js +60 -0
- package/dist/chunk-JG2IGHYY.js +46 -0
- package/dist/chunk-O4YFKL3N.js +265 -0
- package/dist/chunk-VM35XIBY.js +1951 -0
- package/dist/index.js +234 -7133
- package/dist/ui-VPHPVIS5.js +2994 -0
- package/dist/web-Y5CK2WBF.js +1870 -0
- package/package.json +2 -2
|
@@ -0,0 +1,1951 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
const _ew=process.emitWarning.bind(process);process.emitWarning=function(w,...a){if((w?.message??w)?.includes?.('punycode'))return;_ew(w,...a);};
|
|
3
|
+
import {
|
|
4
|
+
createSessionMetadata,
|
|
5
|
+
updateSessionMetadata,
|
|
6
|
+
writeSessionMetadata
|
|
7
|
+
} from "./chunk-O4YFKL3N.js";
|
|
8
|
+
|
|
9
|
+
// src/providers/openai.ts
|
|
10
|
+
import OpenAI from "openai";
|
|
11
|
+
function createOpenAIProvider(profile) {
|
|
12
|
+
const THINK_END = "</think>";
|
|
13
|
+
const openai = new OpenAI({
|
|
14
|
+
baseURL: profile.baseUrl,
|
|
15
|
+
apiKey: profile.apiKey
|
|
16
|
+
});
|
|
17
|
+
const capabilities = Object.freeze(
|
|
18
|
+
Object.defineProperties({}, {
|
|
19
|
+
supportsTools: { value: true, writable: false, enumerable: true, configurable: false },
|
|
20
|
+
supportsReasoningStream: { value: true, writable: false, enumerable: true, configurable: false },
|
|
21
|
+
supportsImages: { value: false, writable: false, enumerable: true, configurable: false },
|
|
22
|
+
supportsJsonSchema: { value: true, writable: false, enumerable: true, configurable: false }
|
|
23
|
+
})
|
|
24
|
+
);
|
|
25
|
+
return {
|
|
26
|
+
capabilities,
|
|
27
|
+
/**
|
|
28
|
+
* stream 方法向 LLM 发起一次流式对话请求,返回异步可迭代的 chunk 序列。
|
|
29
|
+
*
|
|
30
|
+
* 实现细节:
|
|
31
|
+
* - 使用 `Symbol.asyncIterator` + async generator 实现懒执行:
|
|
32
|
+
* 只有调用方执行 `for await` 时才真正发起 HTTP 请求,避免浪费连接
|
|
33
|
+
* - 内部维护两个累加器:
|
|
34
|
+
* 1. `tcAccumulator`:按 index 聚合工具调用的分片参数(JSON 字符串)
|
|
35
|
+
* 2. `reasoningAccumulator`:拼接思考链的所有分片文本
|
|
36
|
+
* - 只在最终 chunk(`finish_reason !== null`)中 yield done=true 的完整信息
|
|
37
|
+
*
|
|
38
|
+
* @param messages 完整的对话历史,包含 user/assistant/tool 所有轮次
|
|
39
|
+
* @param tools 可选的工具列表,不传或传空数组时不附加 tools 字段
|
|
40
|
+
* @param signal 可选的 AbortSignal,用于取消请求
|
|
41
|
+
*/
|
|
42
|
+
stream(messages, tools, signal) {
|
|
43
|
+
return {
|
|
44
|
+
[Symbol.asyncIterator]: async function* () {
|
|
45
|
+
var _a, _b, _c;
|
|
46
|
+
let thinkPhase = "pre";
|
|
47
|
+
let scanningForClose = false;
|
|
48
|
+
let closeSearchBuffer = "";
|
|
49
|
+
let visibleTextTail = "";
|
|
50
|
+
function rememberVisibleText(text) {
|
|
51
|
+
if (!text) return;
|
|
52
|
+
visibleTextTail = (visibleTextTail + text).slice(-256);
|
|
53
|
+
}
|
|
54
|
+
function hasNearbyVisibleOpenThink(maxDistance = 4) {
|
|
55
|
+
const openIdx = visibleTextTail.lastIndexOf("<think>");
|
|
56
|
+
const closeIdx = visibleTextTail.lastIndexOf(THINK_END);
|
|
57
|
+
if (openIdx === -1 || openIdx < closeIdx) return false;
|
|
58
|
+
return visibleTextTail.length - (openIdx + "<think>".length) <= maxDistance;
|
|
59
|
+
}
|
|
60
|
+
function processContent(raw) {
|
|
61
|
+
if (!raw) return { text: "", thinking: "" };
|
|
62
|
+
if (thinkPhase === "post") return { text: raw, thinking: "" };
|
|
63
|
+
if (thinkPhase === "pre") {
|
|
64
|
+
if (raw.startsWith("<think>")) {
|
|
65
|
+
thinkPhase = "in";
|
|
66
|
+
raw = raw.slice(7);
|
|
67
|
+
} else {
|
|
68
|
+
thinkPhase = "post";
|
|
69
|
+
return { text: raw, thinking: "" };
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
const endIdx = raw.indexOf(THINK_END);
|
|
73
|
+
if (endIdx === -1) return { text: "", thinking: raw };
|
|
74
|
+
const thinking = raw.slice(0, endIdx);
|
|
75
|
+
thinkPhase = "post";
|
|
76
|
+
return { text: raw.slice(endIdx + THINK_END.length), thinking };
|
|
77
|
+
}
|
|
78
|
+
const requestParams = {
|
|
79
|
+
model: profile.model,
|
|
80
|
+
messages,
|
|
81
|
+
stream: true,
|
|
82
|
+
stream_options: { include_usage: true }
|
|
83
|
+
};
|
|
84
|
+
if (tools && tools.length > 0) {
|
|
85
|
+
requestParams.tools = tools;
|
|
86
|
+
}
|
|
87
|
+
const response = await openai.chat.completions.create(
|
|
88
|
+
requestParams,
|
|
89
|
+
signal ? { signal } : void 0
|
|
90
|
+
);
|
|
91
|
+
const tcAccumulator = /* @__PURE__ */ new Map();
|
|
92
|
+
let reasoningAccumulator = "";
|
|
93
|
+
const reasoningDetailsAcc = /* @__PURE__ */ new Map();
|
|
94
|
+
for await (const chunk of response) {
|
|
95
|
+
const choice = chunk.choices[0];
|
|
96
|
+
if (!choice) continue;
|
|
97
|
+
const delta = choice.delta;
|
|
98
|
+
if (delta.reasoning_content) {
|
|
99
|
+
reasoningAccumulator += delta.reasoning_content;
|
|
100
|
+
}
|
|
101
|
+
if (delta.reasoning_details && delta.reasoning_details.length > 0) {
|
|
102
|
+
scanningForClose = true;
|
|
103
|
+
for (const rd of delta.reasoning_details) {
|
|
104
|
+
const id = rd.id ?? "";
|
|
105
|
+
const text = rd.text ?? "";
|
|
106
|
+
if (!reasoningDetailsAcc.has(id)) {
|
|
107
|
+
reasoningDetailsAcc.set(id, {
|
|
108
|
+
type: rd.type ?? "reasoning.text",
|
|
109
|
+
id,
|
|
110
|
+
format: rd.format ?? "",
|
|
111
|
+
index: rd.index ?? 0,
|
|
112
|
+
text: ""
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
const existing = reasoningDetailsAcc.get(id);
|
|
116
|
+
existing.text += text;
|
|
117
|
+
if (!delta.reasoning_content && text) {
|
|
118
|
+
reasoningAccumulator += text;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
if (delta.tool_calls) {
|
|
123
|
+
for (const tc of delta.tool_calls) {
|
|
124
|
+
if (!tcAccumulator.has(tc.index)) {
|
|
125
|
+
tcAccumulator.set(tc.index, {
|
|
126
|
+
id: tc.id ?? "",
|
|
127
|
+
name: ((_a = tc.function) == null ? void 0 : _a.name) ?? "",
|
|
128
|
+
arguments: ""
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
const existing = tcAccumulator.get(tc.index);
|
|
132
|
+
if (tc.id) existing.id = tc.id;
|
|
133
|
+
if ((_b = tc.function) == null ? void 0 : _b.name) existing.name = tc.function.name;
|
|
134
|
+
existing.arguments += ((_c = tc.function) == null ? void 0 : _c.arguments) ?? "";
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
const isLast = choice.finish_reason != null;
|
|
138
|
+
let rawToProcess;
|
|
139
|
+
if (delta.reasoning_details && delta.reasoning_details.length > 0) {
|
|
140
|
+
rawToProcess = "";
|
|
141
|
+
} else if (scanningForClose) {
|
|
142
|
+
closeSearchBuffer += delta.content ?? "";
|
|
143
|
+
const closeIdx = closeSearchBuffer.indexOf(THINK_END);
|
|
144
|
+
if (closeIdx !== -1) {
|
|
145
|
+
const openIdx = closeSearchBuffer.lastIndexOf("<think>", closeIdx);
|
|
146
|
+
const hasNearbyLiteralPair = openIdx !== -1 && closeIdx - (openIdx + "<think>".length) <= 8;
|
|
147
|
+
const afterClose = closeSearchBuffer.slice(closeIdx + THINK_END.length);
|
|
148
|
+
rawToProcess = hasNearbyLiteralPair ? closeSearchBuffer : afterClose.length > 0 ? afterClose : closeSearchBuffer.slice(0, closeIdx);
|
|
149
|
+
scanningForClose = false;
|
|
150
|
+
closeSearchBuffer = "";
|
|
151
|
+
thinkPhase = "post";
|
|
152
|
+
} else if (isLast) {
|
|
153
|
+
rawToProcess = closeSearchBuffer;
|
|
154
|
+
scanningForClose = false;
|
|
155
|
+
closeSearchBuffer = "";
|
|
156
|
+
thinkPhase = "post";
|
|
157
|
+
} else {
|
|
158
|
+
rawToProcess = "";
|
|
159
|
+
}
|
|
160
|
+
} else {
|
|
161
|
+
rawToProcess = delta.content ?? "";
|
|
162
|
+
}
|
|
163
|
+
if (thinkPhase === "post" && rawToProcess.endsWith(THINK_END) && !rawToProcess.includes("<think>") && !hasNearbyVisibleOpenThink()) {
|
|
164
|
+
rawToProcess = rawToProcess.slice(0, -THINK_END.length);
|
|
165
|
+
}
|
|
166
|
+
const { text: filteredText, thinking: thinkContent } = processContent(rawToProcess);
|
|
167
|
+
rememberVisibleText(filteredText);
|
|
168
|
+
if (thinkContent) {
|
|
169
|
+
reasoningAccumulator += thinkContent;
|
|
170
|
+
}
|
|
171
|
+
if (isLast) {
|
|
172
|
+
const rawUsage = chunk.usage;
|
|
173
|
+
yield {
|
|
174
|
+
text: filteredText,
|
|
175
|
+
done: true,
|
|
176
|
+
finishReason: choice.finish_reason,
|
|
177
|
+
// tcAccumulator 为空说明本轮没有工具调用,传 undefined 而非空数组,
|
|
178
|
+
// 让调用方用 if (chunk.toolCalls) 做简洁判断
|
|
179
|
+
toolCalls: tcAccumulator.size > 0 ? Array.from(tcAccumulator.values()) : void 0,
|
|
180
|
+
reasoning: reasoningAccumulator || void 0,
|
|
181
|
+
// reasoningDetails 仅在流中出现过结构化推理时返回(MiniMax 兼容)
|
|
182
|
+
reasoningDetails: reasoningDetailsAcc.size > 0 ? Array.from(reasoningDetailsAcc.values()) : void 0,
|
|
183
|
+
// 将 snake_case 的原始字段映射为 camelCase,对外接口保持一致
|
|
184
|
+
usage: rawUsage ? {
|
|
185
|
+
promptTokens: rawUsage.prompt_tokens,
|
|
186
|
+
completionTokens: rawUsage.completion_tokens,
|
|
187
|
+
totalTokens: rawUsage.total_tokens
|
|
188
|
+
} : void 0
|
|
189
|
+
};
|
|
190
|
+
} else {
|
|
191
|
+
const incrementalReasoning = delta.reasoning_content || thinkContent || (delta.reasoning_details && delta.reasoning_details.length > 0 ? delta.reasoning_details.map((rd) => rd.text ?? "").join("") : void 0) || void 0;
|
|
192
|
+
yield {
|
|
193
|
+
text: filteredText,
|
|
194
|
+
reasoning: incrementalReasoning || void 0,
|
|
195
|
+
done: false
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// src/providers/index.ts
|
|
206
|
+
function createProvider(profile) {
|
|
207
|
+
return createOpenAIProvider(profile);
|
|
208
|
+
}
|
|
209
|
+
function resolveActiveProfile(config, providerName) {
|
|
210
|
+
var _a, _b;
|
|
211
|
+
if (providerName !== void 0) {
|
|
212
|
+
const profile = (_a = config.providers) == null ? void 0 : _a[providerName];
|
|
213
|
+
if (!profile) {
|
|
214
|
+
throw new Error(`Provider '${providerName}' not found`);
|
|
215
|
+
}
|
|
216
|
+
return profile;
|
|
217
|
+
}
|
|
218
|
+
if (config.defaultProvider) {
|
|
219
|
+
const profile = (_b = config.providers) == null ? void 0 : _b[config.defaultProvider];
|
|
220
|
+
if (profile) return profile;
|
|
221
|
+
}
|
|
222
|
+
if (config.providers) {
|
|
223
|
+
if (config.providers["default"]) {
|
|
224
|
+
return config.providers["default"];
|
|
225
|
+
}
|
|
226
|
+
const first = Object.values(config.providers)[0];
|
|
227
|
+
if (first) return first;
|
|
228
|
+
}
|
|
229
|
+
return {
|
|
230
|
+
baseUrl: config.baseUrl,
|
|
231
|
+
apiKey: config.apiKey,
|
|
232
|
+
model: config.model
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// src/skills/loader.ts
|
|
237
|
+
import { readFile, readdir, stat } from "fs/promises";
|
|
238
|
+
import { join, dirname, basename, resolve, sep } from "path";
|
|
239
|
+
function parseFrontmatter(content) {
|
|
240
|
+
const FRONTMATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/;
|
|
241
|
+
const match = FRONTMATTER_RE.exec(content);
|
|
242
|
+
if (!match) {
|
|
243
|
+
return { data: {}, body: content };
|
|
244
|
+
}
|
|
245
|
+
const rawFrontmatter = match[1];
|
|
246
|
+
const body = match[2] ?? "";
|
|
247
|
+
const data = {};
|
|
248
|
+
for (const line of rawFrontmatter.split(/\r?\n/)) {
|
|
249
|
+
const colonIdx = line.indexOf(":");
|
|
250
|
+
if (colonIdx === -1) continue;
|
|
251
|
+
const key = line.slice(0, colonIdx).trim();
|
|
252
|
+
const value = line.slice(colonIdx + 1).trim();
|
|
253
|
+
if (key) {
|
|
254
|
+
data[key] = value;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
return { data, body };
|
|
258
|
+
}
|
|
259
|
+
function isTrustedSkillPath(skillDir, trustedDirs) {
|
|
260
|
+
const normalized = resolve(skillDir);
|
|
261
|
+
for (const trusted of trustedDirs) {
|
|
262
|
+
const normalizedTrusted = resolve(trusted);
|
|
263
|
+
if (normalized === normalizedTrusted || normalized.startsWith(normalizedTrusted + sep)) {
|
|
264
|
+
return true;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
return false;
|
|
268
|
+
}
|
|
269
|
+
async function fileExists(filePath) {
|
|
270
|
+
try {
|
|
271
|
+
await stat(filePath);
|
|
272
|
+
return true;
|
|
273
|
+
} catch {
|
|
274
|
+
return false;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
async function loadTools(skillDir) {
|
|
278
|
+
const toolsJsonPath = join(skillDir, "tools.json");
|
|
279
|
+
if (!await fileExists(toolsJsonPath)) return [];
|
|
280
|
+
let raw;
|
|
281
|
+
try {
|
|
282
|
+
raw = await readFile(toolsJsonPath, "utf-8");
|
|
283
|
+
} catch {
|
|
284
|
+
return [];
|
|
285
|
+
}
|
|
286
|
+
let parsed;
|
|
287
|
+
try {
|
|
288
|
+
parsed = JSON.parse(raw);
|
|
289
|
+
} catch {
|
|
290
|
+
return [];
|
|
291
|
+
}
|
|
292
|
+
if (!Array.isArray(parsed)) return [];
|
|
293
|
+
const tools = [];
|
|
294
|
+
for (const item of parsed) {
|
|
295
|
+
if (typeof item === "object" && item !== null && typeof item["name"] === "string" && typeof item["description"] === "string") {
|
|
296
|
+
const entry = item;
|
|
297
|
+
tools.push({
|
|
298
|
+
name: entry["name"],
|
|
299
|
+
description: entry["description"],
|
|
300
|
+
parameters: entry["parameters"] ?? {},
|
|
301
|
+
scriptPath: join(skillDir, `${entry["name"]}.sh`)
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
return tools;
|
|
306
|
+
}
|
|
307
|
+
async function loadSkillFile(skillMdPath) {
|
|
308
|
+
const content = await readFile(skillMdPath, "utf-8");
|
|
309
|
+
const { data, body } = parseFrontmatter(content);
|
|
310
|
+
const skillDir = dirname(skillMdPath);
|
|
311
|
+
const dirName = basename(skillDir);
|
|
312
|
+
const [tools, hasPreScript, hasPostScript] = await Promise.all([
|
|
313
|
+
loadTools(skillDir),
|
|
314
|
+
fileExists(join(skillDir, "pre.sh")),
|
|
315
|
+
fileExists(join(skillDir, "post.sh"))
|
|
316
|
+
]);
|
|
317
|
+
return {
|
|
318
|
+
name: data["name"] ?? dirName,
|
|
319
|
+
description: data["description"] ?? "",
|
|
320
|
+
body,
|
|
321
|
+
source: skillMdPath,
|
|
322
|
+
tools,
|
|
323
|
+
preScript: hasPreScript ? join(skillDir, "pre.sh") : null,
|
|
324
|
+
postScript: hasPostScript ? join(skillDir, "post.sh") : null
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
async function loadSkillsFromDir(dir) {
|
|
328
|
+
let entries;
|
|
329
|
+
try {
|
|
330
|
+
entries = await readdir(dir);
|
|
331
|
+
} catch {
|
|
332
|
+
return [];
|
|
333
|
+
}
|
|
334
|
+
const skills = [];
|
|
335
|
+
for (const entry of entries) {
|
|
336
|
+
const entryPath = join(dir, entry);
|
|
337
|
+
let entryStat;
|
|
338
|
+
try {
|
|
339
|
+
entryStat = await stat(entryPath);
|
|
340
|
+
} catch {
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
343
|
+
if (!entryStat.isDirectory()) continue;
|
|
344
|
+
const skillMdPath = join(entryPath, "SKILL.md");
|
|
345
|
+
try {
|
|
346
|
+
await stat(skillMdPath);
|
|
347
|
+
} catch {
|
|
348
|
+
continue;
|
|
349
|
+
}
|
|
350
|
+
const skill = await loadSkillFile(skillMdPath);
|
|
351
|
+
skills.push(skill);
|
|
352
|
+
}
|
|
353
|
+
return skills.sort((a, b) => a.name.localeCompare(b.name));
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// src/tools/read.ts
|
|
357
|
+
import * as fs from "fs/promises";
|
|
358
|
+
var READ_TOOL = {
|
|
359
|
+
type: "function",
|
|
360
|
+
function: {
|
|
361
|
+
name: "read",
|
|
362
|
+
description: "\u8BFB\u53D6\u6587\u4EF6\u5185\u5BB9\u3002\u4F7F\u7528 offset\uFF080-based \u884C\u7D22\u5F15\uFF09\u548C limit \u53EF\u8BFB\u53D6\u6307\u5B9A\u884C\u8303\u56F4\uFF0C\u9002\u5408\u5927\u6587\u4EF6\u5C40\u90E8\u9605\u8BFB\u3002",
|
|
363
|
+
parameters: {
|
|
364
|
+
type: "object",
|
|
365
|
+
properties: {
|
|
366
|
+
path: { type: "string", description: "\u6587\u4EF6\u7684\u7EDD\u5BF9\u8DEF\u5F84\u6216\u76F8\u5BF9\u8DEF\u5F84\u3002" },
|
|
367
|
+
offset: { type: "number", description: "\u8D77\u59CB\u884C\u7D22\u5F15\uFF080-based\uFF0C\u9ED8\u8BA4 0\uFF09\u3002" },
|
|
368
|
+
limit: { type: "number", description: "\u6700\u591A\u8FD4\u56DE\u7684\u884C\u6570\u3002" }
|
|
369
|
+
},
|
|
370
|
+
required: ["path"]
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
};
|
|
374
|
+
async function readFile3(params) {
|
|
375
|
+
const { path: path7, offset = 0, limit } = params;
|
|
376
|
+
let raw;
|
|
377
|
+
try {
|
|
378
|
+
raw = await fs.readFile(path7, "utf8");
|
|
379
|
+
} catch (err) {
|
|
380
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
381
|
+
return `Error reading ${path7}: ${msg}`;
|
|
382
|
+
}
|
|
383
|
+
const lines = raw.split("\n");
|
|
384
|
+
const sliced = limit !== void 0 ? lines.slice(offset, offset + limit) : lines.slice(offset);
|
|
385
|
+
return sliced.map((line, i) => `${String(offset + i + 1).padStart(4)} ${line}`).join("\n");
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// src/tools/glob.ts
|
|
389
|
+
import * as fs2 from "fs/promises";
|
|
390
|
+
import * as path from "path";
|
|
391
|
+
var GLOB_TOOL = {
|
|
392
|
+
type: "function",
|
|
393
|
+
function: {
|
|
394
|
+
name: "glob",
|
|
395
|
+
description: "Find files matching a glob pattern. Returns relative paths (one per line). Default excludes node_modules, .git and hidden dirs; pass includeHidden=true to include them.",
|
|
396
|
+
parameters: {
|
|
397
|
+
type: "object",
|
|
398
|
+
properties: {
|
|
399
|
+
pattern: { type: "string", description: "Glob pattern, e.g. '**/*.ts', 'src/**', '*.md'" },
|
|
400
|
+
cwd: { type: "string", description: "Base directory to search in (default: current working directory)" },
|
|
401
|
+
includeHidden: { type: "boolean", description: "If true, include hidden files/dirs (starting with '.')" },
|
|
402
|
+
limit: { type: "number", description: "Max number of results to return (default 100)" }
|
|
403
|
+
},
|
|
404
|
+
required: ["pattern"]
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
};
|
|
408
|
+
function globToRegex(pattern) {
|
|
409
|
+
const hasSlash = pattern.includes("/");
|
|
410
|
+
const DSS = "\0DSS\0";
|
|
411
|
+
const DS = "\0DS\0";
|
|
412
|
+
const SS = "\0SS\0";
|
|
413
|
+
const QQ = "\0QQ\0";
|
|
414
|
+
let p = pattern.replace(/\*\*\//g, DSS).replace(/\*\*/g, DS).replace(/\*/g, SS).replace(/\?/g, QQ);
|
|
415
|
+
p = p.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
416
|
+
p = p.replace(new RegExp(DSS, "g"), "(.*/)?").replace(new RegExp(DS, "g"), ".*").replace(new RegExp(SS, "g"), "[^/]*").replace(new RegExp(QQ, "g"), "[^/]");
|
|
417
|
+
if (!hasSlash) {
|
|
418
|
+
}
|
|
419
|
+
return new RegExp(`^${p}$`);
|
|
420
|
+
}
|
|
421
|
+
async function walk(dir, cwd, regex, includeHidden, results, total, limit) {
|
|
422
|
+
let entries;
|
|
423
|
+
try {
|
|
424
|
+
entries = await fs2.readdir(dir, { withFileTypes: true });
|
|
425
|
+
} catch {
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
429
|
+
for (const entry of entries) {
|
|
430
|
+
const name = entry.name;
|
|
431
|
+
if (name === "node_modules") continue;
|
|
432
|
+
if (!includeHidden && name.startsWith(".")) continue;
|
|
433
|
+
const abs = path.join(dir, name);
|
|
434
|
+
const rel = path.relative(cwd, abs);
|
|
435
|
+
if (entry.isDirectory()) {
|
|
436
|
+
await walk(abs, cwd, regex, includeHidden, results, total, limit);
|
|
437
|
+
} else if (entry.isFile()) {
|
|
438
|
+
const relPosix = rel.split(path.sep).join("/");
|
|
439
|
+
if (regex.test(relPosix)) {
|
|
440
|
+
total.count++;
|
|
441
|
+
if (results.length < limit) {
|
|
442
|
+
results.push(relPosix);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
async function globFiles(params) {
|
|
449
|
+
const {
|
|
450
|
+
pattern,
|
|
451
|
+
cwd = process.cwd(),
|
|
452
|
+
includeHidden = false,
|
|
453
|
+
limit = 100
|
|
454
|
+
} = params;
|
|
455
|
+
try {
|
|
456
|
+
const stat4 = await fs2.stat(cwd);
|
|
457
|
+
if (!stat4.isDirectory()) {
|
|
458
|
+
return `Error: ${cwd} is not a directory`;
|
|
459
|
+
}
|
|
460
|
+
} catch (err) {
|
|
461
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
462
|
+
return `Error: ${msg}`;
|
|
463
|
+
}
|
|
464
|
+
const regex = globToRegex(pattern);
|
|
465
|
+
const results = [];
|
|
466
|
+
const total = { count: 0 };
|
|
467
|
+
await walk(cwd, cwd, regex, includeHidden, results, total, limit);
|
|
468
|
+
if (results.length === 0) {
|
|
469
|
+
return `No files found matching pattern: ${pattern}`;
|
|
470
|
+
}
|
|
471
|
+
results.sort();
|
|
472
|
+
let output = results.join("\n");
|
|
473
|
+
if (total.count > limit) {
|
|
474
|
+
output += `
|
|
475
|
+
... (truncated, ${total.count} total matches)`;
|
|
476
|
+
}
|
|
477
|
+
return output;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// src/tools/grep.ts
|
|
481
|
+
import * as fs3 from "fs/promises";
|
|
482
|
+
import * as path2 from "path";
|
|
483
|
+
var GREP_TOOL = {
|
|
484
|
+
type: "function",
|
|
485
|
+
function: {
|
|
486
|
+
name: "grep",
|
|
487
|
+
description: "\u5728\u6587\u4EF6\u4E2D\u641C\u7D22\u6B63\u5219\u8868\u8FBE\u5F0F\u6A21\u5F0F\u3002\u8FD4\u56DE file:linenum:content \u683C\u5F0F\uFF0Ccontext \u884C\u7528 -- \u5206\u9694\u3002",
|
|
488
|
+
parameters: {
|
|
489
|
+
type: "object",
|
|
490
|
+
properties: {
|
|
491
|
+
pattern: { type: "string", description: "\u6B63\u5219\u8868\u8FBE\u5F0F\u6A21\u5F0F\u3002" },
|
|
492
|
+
path: { type: "string", description: "\u641C\u7D22\u7684\u6587\u4EF6\u6216\u76EE\u5F55\u8DEF\u5F84\uFF08\u9ED8\u8BA4\u5F53\u524D\u76EE\u5F55\uFF09\u3002" },
|
|
493
|
+
cwd: { type: "string", description: "\u5DE5\u4F5C\u76EE\u5F55\uFF08\u9ED8\u8BA4 process.cwd()\uFF09\u3002" },
|
|
494
|
+
context: { type: "number", description: "\u5339\u914D\u524D\u540E\u7684\u4E0A\u4E0B\u6587\u884C\u6570\uFF08\u9ED8\u8BA4 0\uFF09\u3002" },
|
|
495
|
+
includeHidden: { type: "boolean", description: "\u662F\u5426\u5305\u542B\u9690\u85CF\u76EE\u5F55\uFF08\u9ED8\u8BA4 false\uFF09\u3002" },
|
|
496
|
+
limit: { type: "number", description: "\u6700\u5927\u8FD4\u56DE\u5339\u914D\u6570\uFF08\u9ED8\u8BA4 50\uFF09\u3002" }
|
|
497
|
+
},
|
|
498
|
+
required: ["pattern"]
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
};
|
|
502
|
+
async function searchFile(absFile, displayFile, regex, matches, totalCount, limit) {
|
|
503
|
+
let text;
|
|
504
|
+
try {
|
|
505
|
+
text = await fs3.readFile(absFile, "utf8");
|
|
506
|
+
} catch {
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
const lines = text.split("\n");
|
|
510
|
+
const lastIdx = lines.length - 1;
|
|
511
|
+
for (let i = 0; i < lines.length; i++) {
|
|
512
|
+
if (i === lastIdx && lines[i] === "") continue;
|
|
513
|
+
if (regex.test(lines[i])) {
|
|
514
|
+
totalCount.count++;
|
|
515
|
+
if (matches.length < limit) {
|
|
516
|
+
matches.push({ file: displayFile, line: i + 1, content: lines[i] });
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
async function walkDir(dir, cwd, regex, includeHidden, matches, totalCount, limit) {
|
|
522
|
+
let entries;
|
|
523
|
+
try {
|
|
524
|
+
entries = await fs3.readdir(dir, { withFileTypes: true });
|
|
525
|
+
} catch {
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
529
|
+
for (const entry of entries) {
|
|
530
|
+
const name = entry.name;
|
|
531
|
+
if (name === "node_modules") continue;
|
|
532
|
+
if (!includeHidden && name.startsWith(".")) continue;
|
|
533
|
+
const abs = path2.join(dir, name);
|
|
534
|
+
if (entry.isDirectory()) {
|
|
535
|
+
await walkDir(abs, cwd, regex, includeHidden, matches, totalCount, limit);
|
|
536
|
+
} else if (entry.isFile()) {
|
|
537
|
+
const rel = path2.relative(cwd, abs).split(path2.sep).join("/");
|
|
538
|
+
await searchFile(abs, rel, regex, matches, totalCount, limit);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
async function renderWithContext(entries, contextLines) {
|
|
543
|
+
const outputLines = [];
|
|
544
|
+
for (const { absFile, displayFile, matches: fileMatches } of entries) {
|
|
545
|
+
let allLines = [];
|
|
546
|
+
try {
|
|
547
|
+
const text = await fs3.readFile(absFile, "utf8");
|
|
548
|
+
allLines = text.split("\n");
|
|
549
|
+
} catch {
|
|
550
|
+
}
|
|
551
|
+
if (allLines.length === 0) {
|
|
552
|
+
for (const m of fileMatches) {
|
|
553
|
+
outputLines.push(`${displayFile}:${m.line}:${m.content}`);
|
|
554
|
+
}
|
|
555
|
+
continue;
|
|
556
|
+
}
|
|
557
|
+
const ranges = [];
|
|
558
|
+
for (const m of fileMatches) {
|
|
559
|
+
const start = Math.max(0, m.line - 1 - contextLines);
|
|
560
|
+
const end = Math.min(allLines.length - 1, m.line - 1 + contextLines);
|
|
561
|
+
const last = ranges[ranges.length - 1];
|
|
562
|
+
if (last && start <= last.end + 1) {
|
|
563
|
+
last.end = Math.max(last.end, end);
|
|
564
|
+
} else {
|
|
565
|
+
ranges.push({ start, end });
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
for (let ri = 0; ri < ranges.length; ri++) {
|
|
569
|
+
if (ri > 0) outputLines.push("--");
|
|
570
|
+
const { start, end } = ranges[ri];
|
|
571
|
+
for (let i = start; i <= end; i++) {
|
|
572
|
+
if (i === allLines.length - 1 && allLines[i] === "") continue;
|
|
573
|
+
outputLines.push(`${displayFile}:${i + 1}:${allLines[i]}`);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
return outputLines;
|
|
578
|
+
}
|
|
579
|
+
async function grepFiles(params) {
|
|
580
|
+
const {
|
|
581
|
+
pattern,
|
|
582
|
+
path: searchPath = ".",
|
|
583
|
+
cwd = process.cwd(),
|
|
584
|
+
context = 0,
|
|
585
|
+
includeHidden = false,
|
|
586
|
+
limit = 50
|
|
587
|
+
} = params;
|
|
588
|
+
let regex;
|
|
589
|
+
try {
|
|
590
|
+
regex = new RegExp(pattern);
|
|
591
|
+
} catch (err) {
|
|
592
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
593
|
+
return `Error: Invalid regex: ${msg}`;
|
|
594
|
+
}
|
|
595
|
+
const target = path2.isAbsolute(searchPath) ? searchPath : path2.resolve(cwd, searchPath);
|
|
596
|
+
let stat4;
|
|
597
|
+
try {
|
|
598
|
+
stat4 = await fs3.stat(target);
|
|
599
|
+
} catch (err) {
|
|
600
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
601
|
+
return `Error: ${msg}`;
|
|
602
|
+
}
|
|
603
|
+
const matches = [];
|
|
604
|
+
const totalCount = { count: 0 };
|
|
605
|
+
if (stat4.isFile()) {
|
|
606
|
+
await searchFile(target, searchPath, regex, matches, totalCount, limit);
|
|
607
|
+
} else if (stat4.isDirectory()) {
|
|
608
|
+
await walkDir(target, target, regex, includeHidden, matches, totalCount, limit);
|
|
609
|
+
} else {
|
|
610
|
+
return `Error: ${target} is neither a file nor a directory`;
|
|
611
|
+
}
|
|
612
|
+
if (matches.length === 0 && totalCount.count === 0) {
|
|
613
|
+
return "No matches found";
|
|
614
|
+
}
|
|
615
|
+
const matchesByFile = /* @__PURE__ */ new Map();
|
|
616
|
+
for (const m of matches) {
|
|
617
|
+
let group = matchesByFile.get(m.file);
|
|
618
|
+
if (!group) {
|
|
619
|
+
group = [];
|
|
620
|
+
matchesByFile.set(m.file, group);
|
|
621
|
+
}
|
|
622
|
+
group.push(m);
|
|
623
|
+
}
|
|
624
|
+
let outputLines;
|
|
625
|
+
if (context > 0) {
|
|
626
|
+
const baseDir = stat4.isDirectory() ? target : path2.dirname(target);
|
|
627
|
+
const entries = Array.from(matchesByFile.entries()).map(([displayFile, fileMatches]) => {
|
|
628
|
+
const absFile = path2.isAbsolute(displayFile) ? displayFile : path2.resolve(baseDir, displayFile);
|
|
629
|
+
return { absFile, displayFile, matches: fileMatches };
|
|
630
|
+
});
|
|
631
|
+
outputLines = await renderWithContext(entries, context);
|
|
632
|
+
} else {
|
|
633
|
+
outputLines = [];
|
|
634
|
+
for (const m of matches) {
|
|
635
|
+
outputLines.push(`${m.file}:${m.line}:${m.content}`);
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
let output = outputLines.join("\n");
|
|
639
|
+
if (totalCount.count > limit) {
|
|
640
|
+
const extra = totalCount.count - limit;
|
|
641
|
+
output += `
|
|
642
|
+
... (truncated, ${extra} more matches)`;
|
|
643
|
+
}
|
|
644
|
+
return output;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
// src/tools/web_fetch.ts
|
|
648
|
+
import * as http from "http";
|
|
649
|
+
import * as https from "https";
|
|
650
|
+
var WEB_FETCH_TOOL = {
|
|
651
|
+
type: "function",
|
|
652
|
+
function: {
|
|
653
|
+
name: "web_fetch",
|
|
654
|
+
description: "Fetch a URL and extract its text content. Supports HTML, plain text and markdown pages. Returns a structured response with headers and extracted content.",
|
|
655
|
+
parameters: {
|
|
656
|
+
type: "object",
|
|
657
|
+
properties: {
|
|
658
|
+
url: { type: "string", description: "The URL to fetch (http or https only)." },
|
|
659
|
+
extract_mode: {
|
|
660
|
+
type: "string",
|
|
661
|
+
enum: ["text", "markdown"],
|
|
662
|
+
description: "How to extract content from HTML: 'markdown' (default) preserves headings/lists as markdown, 'text' strips all tags."
|
|
663
|
+
},
|
|
664
|
+
max_chars: {
|
|
665
|
+
type: "number",
|
|
666
|
+
description: "Maximum characters to return (default 20000, max 100000)."
|
|
667
|
+
},
|
|
668
|
+
timeout_ms: {
|
|
669
|
+
type: "number",
|
|
670
|
+
description: "Request timeout in milliseconds (default 20000, min 1000, max 60000)."
|
|
671
|
+
}
|
|
672
|
+
},
|
|
673
|
+
required: ["url"]
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
};
|
|
677
|
+
var CACHE_TTL = 6e5;
|
|
678
|
+
var CACHE_MAX = 64;
|
|
679
|
+
var cache = /* @__PURE__ */ new Map();
|
|
680
|
+
function cacheGet(key) {
|
|
681
|
+
const entry = cache.get(key);
|
|
682
|
+
if (!entry) return void 0;
|
|
683
|
+
if (Date.now() - entry.at > CACHE_TTL) {
|
|
684
|
+
cache.delete(key);
|
|
685
|
+
return void 0;
|
|
686
|
+
}
|
|
687
|
+
return entry.result;
|
|
688
|
+
}
|
|
689
|
+
function cacheSet(key, result) {
|
|
690
|
+
if (cache.size >= CACHE_MAX) {
|
|
691
|
+
const oldest = cache.keys().next().value;
|
|
692
|
+
if (oldest !== void 0) cache.delete(oldest);
|
|
693
|
+
}
|
|
694
|
+
cache.set(key, { at: Date.now(), result });
|
|
695
|
+
}
|
|
696
|
+
function isPrivateHost(hostname) {
|
|
697
|
+
if (hostname === "localhost" || hostname === "0.0.0.0" || hostname === "::1") return true;
|
|
698
|
+
if (hostname.endsWith(".local")) return true;
|
|
699
|
+
const v4 = hostname.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/);
|
|
700
|
+
if (v4) {
|
|
701
|
+
const [, a, b] = v4.map(Number);
|
|
702
|
+
if (a === 127) return true;
|
|
703
|
+
if (a === 10) return true;
|
|
704
|
+
if (a === 192 && b === 168) return true;
|
|
705
|
+
if (a === 172 && b >= 16 && b <= 31) return true;
|
|
706
|
+
if (a === 0) return true;
|
|
707
|
+
}
|
|
708
|
+
return false;
|
|
709
|
+
}
|
|
710
|
+
function validateWebFetchUrl(rawUrl) {
|
|
711
|
+
let parsed;
|
|
712
|
+
try {
|
|
713
|
+
parsed = new URL(rawUrl);
|
|
714
|
+
} catch {
|
|
715
|
+
return { ok: false, error: `invalid URL: ${rawUrl}` };
|
|
716
|
+
}
|
|
717
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
718
|
+
return { ok: false, error: `unsupported protocol: ${parsed.protocol}` };
|
|
719
|
+
}
|
|
720
|
+
if (parsed.username || parsed.password) {
|
|
721
|
+
return { ok: false, error: "URL must not contain credentials" };
|
|
722
|
+
}
|
|
723
|
+
if (isPrivateHost(parsed.hostname)) {
|
|
724
|
+
return { ok: false, error: `host is private/local: ${parsed.hostname}` };
|
|
725
|
+
}
|
|
726
|
+
return { ok: true, parsed };
|
|
727
|
+
}
|
|
728
|
+
function rootHost(hostname) {
|
|
729
|
+
const parts = hostname.split(".");
|
|
730
|
+
return parts.length > 2 ? parts.slice(-2).join(".") : hostname;
|
|
731
|
+
}
|
|
732
|
+
function isSameOrWwwHost(a, b) {
|
|
733
|
+
if (a === b) return true;
|
|
734
|
+
const ra = rootHost(a);
|
|
735
|
+
const rb = rootHost(b);
|
|
736
|
+
return ra === rb && (a === `www.${ra}` || b === `www.${ra}` || a === ra || b === ra);
|
|
737
|
+
}
|
|
738
|
+
function extractHtmlText(html, mode) {
|
|
739
|
+
let s = html;
|
|
740
|
+
s = s.replace(/<script[\s\S]*?<\/script>/gi, "");
|
|
741
|
+
s = s.replace(/<style[\s\S]*?<\/style>/gi, "");
|
|
742
|
+
s = s.replace(/<!--[\s\S]*?-->/g, "");
|
|
743
|
+
if (mode === "markdown") {
|
|
744
|
+
s = s.replace(/<pre[^>]*>\s*<code[^>]*>([\s\S]*?)<\/code>\s*<\/pre>/gi, (_, code) => {
|
|
745
|
+
const decoded = decodeEntities(code);
|
|
746
|
+
return `
|
|
747
|
+
|
|
748
|
+
\`\`\`
|
|
749
|
+
${decoded}
|
|
750
|
+
\`\`\`
|
|
751
|
+
|
|
752
|
+
`;
|
|
753
|
+
});
|
|
754
|
+
s = s.replace(/<code[^>]*>([\s\S]*?)<\/code>/gi, (_, code) => `\`${code}\``);
|
|
755
|
+
for (let n = 1; n <= 6; n++) {
|
|
756
|
+
const hashes = "#".repeat(n);
|
|
757
|
+
s = s.replace(new RegExp(`<h${n}[^>]*>`, "gi"), `
|
|
758
|
+
|
|
759
|
+
${hashes} `);
|
|
760
|
+
s = s.replace(new RegExp(`</h${n}>`, "gi"), "\n\n");
|
|
761
|
+
}
|
|
762
|
+
s = s.replace(/<li[^>]*>/gi, "\n- ");
|
|
763
|
+
} else {
|
|
764
|
+
for (let n = 1; n <= 6; n++) {
|
|
765
|
+
s = s.replace(new RegExp(`<h${n}[^>]*>`, "gi"), "\n\n");
|
|
766
|
+
s = s.replace(new RegExp(`</h${n}>`, "gi"), "\n\n");
|
|
767
|
+
}
|
|
768
|
+
s = s.replace(/<li[^>]*>/gi, "\n");
|
|
769
|
+
}
|
|
770
|
+
s = s.replace(/<\/p>/gi, "\n\n");
|
|
771
|
+
s = s.replace(/<p[^>]*>/gi, "\n\n");
|
|
772
|
+
s = s.replace(/<\/div>/gi, "\n");
|
|
773
|
+
s = s.replace(/<br\s*\/?>/gi, "\n");
|
|
774
|
+
s = s.replace(/<\/li>/gi, "\n");
|
|
775
|
+
s = s.replace(/<[^>]+>/g, "");
|
|
776
|
+
s = decodeEntities(s);
|
|
777
|
+
s = s.replace(/[ \t]+/g, " ");
|
|
778
|
+
s = s.replace(/\n{3,}/g, "\n\n");
|
|
779
|
+
return s.trim();
|
|
780
|
+
}
|
|
781
|
+
function decodeEntities(s) {
|
|
782
|
+
return s.replace(/ /g, " ").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/g, "'");
|
|
783
|
+
}
|
|
784
|
+
function statusText(status) {
|
|
785
|
+
const map = {
|
|
786
|
+
200: "OK",
|
|
787
|
+
201: "Created",
|
|
788
|
+
204: "No Content",
|
|
789
|
+
301: "Moved Permanently",
|
|
790
|
+
302: "Found",
|
|
791
|
+
303: "See Other",
|
|
792
|
+
307: "Temporary Redirect",
|
|
793
|
+
308: "Permanent Redirect",
|
|
794
|
+
400: "Bad Request",
|
|
795
|
+
401: "Unauthorized",
|
|
796
|
+
403: "Forbidden",
|
|
797
|
+
404: "Not Found",
|
|
798
|
+
405: "Method Not Allowed",
|
|
799
|
+
429: "Too Many Requests",
|
|
800
|
+
500: "Internal Server Error",
|
|
801
|
+
502: "Bad Gateway",
|
|
802
|
+
503: "Service Unavailable"
|
|
803
|
+
};
|
|
804
|
+
return map[status] ?? String(status);
|
|
805
|
+
}
|
|
806
|
+
async function nodeHttpFetch(url, signal) {
|
|
807
|
+
return new Promise((resolve4, reject) => {
|
|
808
|
+
const parsed = new URL(url);
|
|
809
|
+
const isHttps = parsed.protocol === "https:";
|
|
810
|
+
const lib = isHttps ? https : http;
|
|
811
|
+
const onAbort = () => {
|
|
812
|
+
req.destroy();
|
|
813
|
+
const err = new Error("The user aborted a request.");
|
|
814
|
+
err.name = "AbortError";
|
|
815
|
+
reject(err);
|
|
816
|
+
};
|
|
817
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
818
|
+
const req = lib.request(
|
|
819
|
+
{
|
|
820
|
+
hostname: parsed.hostname,
|
|
821
|
+
port: parsed.port ? parseInt(parsed.port, 10) : isHttps ? 443 : 80,
|
|
822
|
+
path: (parsed.pathname || "/") + parsed.search,
|
|
823
|
+
method: "GET",
|
|
824
|
+
headers: {
|
|
825
|
+
Accept: "text/html, text/plain, text/markdown, application/xhtml+xml, */*",
|
|
826
|
+
"User-Agent": "ecode-web-fetch/0.1"
|
|
827
|
+
}
|
|
828
|
+
},
|
|
829
|
+
(res) => {
|
|
830
|
+
const status = res.statusCode ?? 0;
|
|
831
|
+
const rawHeaders = res.headers;
|
|
832
|
+
const headerObj = {
|
|
833
|
+
get(name) {
|
|
834
|
+
const val = rawHeaders[name.toLowerCase()];
|
|
835
|
+
if (val == null) return null;
|
|
836
|
+
return Array.isArray(val) ? val.join(", ") : String(val);
|
|
837
|
+
}
|
|
838
|
+
};
|
|
839
|
+
const chunks = [];
|
|
840
|
+
res.on("data", (chunk) => chunks.push(chunk));
|
|
841
|
+
res.on("end", () => {
|
|
842
|
+
signal.removeEventListener("abort", onAbort);
|
|
843
|
+
const body = Buffer.concat(chunks).toString("utf8");
|
|
844
|
+
resolve4({
|
|
845
|
+
status,
|
|
846
|
+
url,
|
|
847
|
+
headers: headerObj,
|
|
848
|
+
text: () => Promise.resolve(body)
|
|
849
|
+
});
|
|
850
|
+
});
|
|
851
|
+
res.on("error", (err) => {
|
|
852
|
+
signal.removeEventListener("abort", onAbort);
|
|
853
|
+
reject(err);
|
|
854
|
+
});
|
|
855
|
+
}
|
|
856
|
+
);
|
|
857
|
+
req.on("error", (err) => {
|
|
858
|
+
signal.removeEventListener("abort", onAbort);
|
|
859
|
+
reject(err);
|
|
860
|
+
});
|
|
861
|
+
req.end();
|
|
862
|
+
});
|
|
863
|
+
}
|
|
864
|
+
async function fetchWithRedirects(url, timeoutMs, signal) {
|
|
865
|
+
let current = url;
|
|
866
|
+
let hops = 0;
|
|
867
|
+
const MAX_REDIRECTS = 5;
|
|
868
|
+
const originalHost = new URL(url).hostname;
|
|
869
|
+
const useFetch = typeof fetch !== "undefined";
|
|
870
|
+
while (hops <= MAX_REDIRECTS) {
|
|
871
|
+
const response = useFetch ? await fetch(current, {
|
|
872
|
+
redirect: "manual",
|
|
873
|
+
signal,
|
|
874
|
+
headers: {
|
|
875
|
+
Accept: "text/html, text/plain, text/markdown, application/xhtml+xml, */*",
|
|
876
|
+
"User-Agent": "ecode-web-fetch/0.1"
|
|
877
|
+
}
|
|
878
|
+
}) : await nodeHttpFetch(current, signal);
|
|
879
|
+
const status = response.status;
|
|
880
|
+
if (status >= 300 && status < 400) {
|
|
881
|
+
const location = response.headers.get("location");
|
|
882
|
+
if (!location) {
|
|
883
|
+
return { response, finalUrl: current };
|
|
884
|
+
}
|
|
885
|
+
const nextUrl = new URL(location, current).toString();
|
|
886
|
+
const nextHost = new URL(nextUrl).hostname;
|
|
887
|
+
if (!isSameOrWwwHost(originalHost, nextHost)) {
|
|
888
|
+
throw new Error(`redirect to different host is not allowed automatically (-> ${nextUrl})`);
|
|
889
|
+
}
|
|
890
|
+
current = nextUrl;
|
|
891
|
+
hops++;
|
|
892
|
+
continue;
|
|
893
|
+
}
|
|
894
|
+
return { response, finalUrl: current };
|
|
895
|
+
}
|
|
896
|
+
throw new Error(`too many redirects (> ${MAX_REDIRECTS})`);
|
|
897
|
+
}
|
|
898
|
+
async function webFetch(params) {
|
|
899
|
+
const {
|
|
900
|
+
url,
|
|
901
|
+
extract_mode = "markdown",
|
|
902
|
+
max_chars: rawMaxChars = 2e4,
|
|
903
|
+
timeout_ms: rawTimeout = 2e4
|
|
904
|
+
} = params;
|
|
905
|
+
const maxChars = Math.min(Math.max(rawMaxChars, 1), 1e5);
|
|
906
|
+
const timeoutMs = Math.min(Math.max(rawTimeout, 1e3), 6e4);
|
|
907
|
+
const validation = validateWebFetchUrl(url);
|
|
908
|
+
if (!validation.ok) {
|
|
909
|
+
return `Error fetching ${url}: ${validation.error}`;
|
|
910
|
+
}
|
|
911
|
+
const cacheKey = `${url}:${extract_mode}:${maxChars}`;
|
|
912
|
+
const cached = cacheGet(cacheKey);
|
|
913
|
+
if (cached !== void 0) return cached;
|
|
914
|
+
const controller = new AbortController();
|
|
915
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
916
|
+
let response;
|
|
917
|
+
let finalUrl;
|
|
918
|
+
try {
|
|
919
|
+
({ response, finalUrl } = await fetchWithRedirects(url, timeoutMs, controller.signal));
|
|
920
|
+
} catch (err) {
|
|
921
|
+
clearTimeout(timer);
|
|
922
|
+
if (err instanceof Error && err.name === "AbortError") {
|
|
923
|
+
return `Error fetching ${url}: request timed out after ${timeoutMs}ms`;
|
|
924
|
+
}
|
|
925
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
926
|
+
return `Error fetching ${url}: ${msg}`;
|
|
927
|
+
} finally {
|
|
928
|
+
clearTimeout(timer);
|
|
929
|
+
}
|
|
930
|
+
if (response.url) {
|
|
931
|
+
let resolvedHost;
|
|
932
|
+
try {
|
|
933
|
+
resolvedHost = new URL(response.url).hostname;
|
|
934
|
+
} catch {
|
|
935
|
+
resolvedHost = validation.parsed.hostname;
|
|
936
|
+
}
|
|
937
|
+
if (!isSameOrWwwHost(validation.parsed.hostname, resolvedHost)) {
|
|
938
|
+
return `Error fetching ${url}: redirect to different host is not allowed automatically (-> ${response.url})`;
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
const status = response.status;
|
|
942
|
+
const contentTypeHeader = response.headers.get("content-type") ?? "";
|
|
943
|
+
const mimeType = contentTypeHeader.split(";")[0].trim().toLowerCase();
|
|
944
|
+
if (status >= 400) {
|
|
945
|
+
return `Error fetching ${url}: HTTP ${status} ${statusText(status)}`;
|
|
946
|
+
}
|
|
947
|
+
const supportedTypes = ["text/html", "text/plain", "text/markdown", "application/xhtml+xml"];
|
|
948
|
+
if (mimeType && !supportedTypes.includes(mimeType)) {
|
|
949
|
+
return `Error fetching ${url}: unsupported content-type ${mimeType}`;
|
|
950
|
+
}
|
|
951
|
+
let body;
|
|
952
|
+
try {
|
|
953
|
+
body = await response.text();
|
|
954
|
+
} catch (err) {
|
|
955
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
956
|
+
return `Error fetching ${url}: failed to read response body: ${msg}`;
|
|
957
|
+
}
|
|
958
|
+
let extracted;
|
|
959
|
+
if (mimeType === "text/plain" || mimeType === "text/markdown") {
|
|
960
|
+
extracted = body;
|
|
961
|
+
} else {
|
|
962
|
+
extracted = extractHtmlText(body, extract_mode);
|
|
963
|
+
}
|
|
964
|
+
const truncated = extracted.length > maxChars;
|
|
965
|
+
const content = truncated ? extracted.slice(0, maxChars) : extracted;
|
|
966
|
+
const result = [
|
|
967
|
+
`URL: ${url}`,
|
|
968
|
+
`Final-URL: ${finalUrl}`,
|
|
969
|
+
`Status: ${status} ${statusText(status)}`,
|
|
970
|
+
`Content-Type: ${contentTypeHeader || "(none)"}`,
|
|
971
|
+
`Bytes: ${body.length}`,
|
|
972
|
+
`Extract-Mode: ${extract_mode}`,
|
|
973
|
+
`Truncated: ${truncated ? "yes" : "no"}`,
|
|
974
|
+
"",
|
|
975
|
+
"<content>",
|
|
976
|
+
content,
|
|
977
|
+
"</content>"
|
|
978
|
+
].join("\n");
|
|
979
|
+
cacheSet(cacheKey, result);
|
|
980
|
+
return result;
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
// src/logger.ts
|
|
984
|
+
import * as fs4 from "fs";
|
|
985
|
+
import * as path3 from "path";
|
|
986
|
+
function createLogger(logDir, sessionStart) {
|
|
987
|
+
fs4.mkdirSync(logDir, { recursive: true });
|
|
988
|
+
const filename = sessionStart.toISOString().replace(/:/g, "-").replace(/\..+/, "") + ".jsonl";
|
|
989
|
+
const filePath = path3.join(logDir, filename);
|
|
990
|
+
return {
|
|
991
|
+
filePath,
|
|
992
|
+
/**
|
|
993
|
+
* 将单条日志条目序列化为 JSON 并同步追加到文件(末尾加换行符)。
|
|
994
|
+
*
|
|
995
|
+
* 使用 appendFileSync 而非 appendFile(异步版本)的原因:
|
|
996
|
+
* 进程崩溃或 Ctrl-C 退出时,异步写操作可能尚未完成,导致最后几条记录丢失。
|
|
997
|
+
* 同步写入虽然阻塞事件循环,但日志条目通常很小(< 1KB),延迟可忽略。
|
|
998
|
+
*
|
|
999
|
+
* 写入失败时输出到 stderr 而非抛出异常,防止日志错误中断正常业务流程。
|
|
1000
|
+
*/
|
|
1001
|
+
append(entry) {
|
|
1002
|
+
try {
|
|
1003
|
+
fs4.appendFileSync(filePath, JSON.stringify(entry) + "\n");
|
|
1004
|
+
} catch (err) {
|
|
1005
|
+
process.stderr.write(`[logger] Failed to write log entry: ${err}
|
|
1006
|
+
`);
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
};
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
// src/tools/bash.ts
|
|
1013
|
+
import { exec } from "child_process";
|
|
1014
|
+
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
1015
|
+
function executeBash(cmd, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
1016
|
+
return new Promise((resolve4) => {
|
|
1017
|
+
exec(cmd, { timeout: timeoutMs }, (err, stdout, stderr) => {
|
|
1018
|
+
if (err) {
|
|
1019
|
+
const exitCode = err.code ?? 1;
|
|
1020
|
+
resolve4({ stdout: stdout ?? "", stderr: stderr ?? "", exitCode });
|
|
1021
|
+
} else {
|
|
1022
|
+
resolve4({ stdout: stdout ?? "", stderr: stderr ?? "", exitCode: 0 });
|
|
1023
|
+
}
|
|
1024
|
+
});
|
|
1025
|
+
});
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
// src/tools/write.ts
|
|
1029
|
+
import * as fs5 from "fs/promises";
|
|
1030
|
+
import * as path4 from "path";
|
|
1031
|
+
var WRITE_TOOL = {
|
|
1032
|
+
type: "function",
|
|
1033
|
+
function: {
|
|
1034
|
+
name: "write",
|
|
1035
|
+
description: "\u5C06\u5185\u5BB9\u5199\u5165\u6587\u4EF6\uFF08\u8986\u76D6\u6A21\u5F0F\uFF09\uFF0C\u81EA\u52A8\u521B\u5EFA\u7236\u76EE\u5F55\u3002\u5199\u5165\u524D\u8BF7\u5148\u7528 read \u786E\u8BA4\u73B0\u6709\u5185\u5BB9\u3002",
|
|
1036
|
+
parameters: {
|
|
1037
|
+
type: "object",
|
|
1038
|
+
properties: {
|
|
1039
|
+
path: { type: "string", description: "\u6587\u4EF6\u7684\u7EDD\u5BF9\u8DEF\u5F84\u6216\u76F8\u5BF9\u8DEF\u5F84\u3002" },
|
|
1040
|
+
content: { type: "string", description: "\u8981\u5199\u5165\u7684\u5B8C\u6574\u5185\u5BB9\u3002" }
|
|
1041
|
+
},
|
|
1042
|
+
required: ["path", "content"]
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
};
|
|
1046
|
+
async function writeFile2(params) {
|
|
1047
|
+
const { path: filePath, content } = params;
|
|
1048
|
+
try {
|
|
1049
|
+
await fs5.mkdir(path4.dirname(filePath), { recursive: true });
|
|
1050
|
+
await fs5.writeFile(filePath, content, "utf8");
|
|
1051
|
+
return `Written ${filePath}`;
|
|
1052
|
+
} catch (err) {
|
|
1053
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1054
|
+
return `Error writing ${filePath}: ${msg}`;
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
// src/tools/edit.ts
|
|
1059
|
+
import * as fs6 from "fs/promises";
|
|
1060
|
+
var EDIT_TOOL = {
|
|
1061
|
+
type: "function",
|
|
1062
|
+
function: {
|
|
1063
|
+
name: "edit",
|
|
1064
|
+
description: "\u5728\u6587\u4EF6\u4E2D\u7CBE\u786E\u66FF\u6362\u4E00\u6BB5\u5B57\u7B26\u4E32\u3002old_string \u5728\u6587\u4EF6\u4E2D\u5FC5\u987B\u552F\u4E00\u51FA\u73B0\u2014\u2014\u82E5\u6709\u6B67\u4E49\u8BF7\u63D0\u4F9B\u66F4\u591A\u4E0A\u4E0B\u6587\u3002",
|
|
1065
|
+
parameters: {
|
|
1066
|
+
type: "object",
|
|
1067
|
+
properties: {
|
|
1068
|
+
path: { type: "string", description: "\u6587\u4EF6\u7684\u7EDD\u5BF9\u8DEF\u5F84\u6216\u76F8\u5BF9\u8DEF\u5F84\u3002" },
|
|
1069
|
+
old_string: { type: "string", description: "\u8981\u67E5\u627E\u7684\u7CBE\u786E\u5B57\u7B26\u4E32\uFF08\u5728\u6587\u4EF6\u4E2D\u5FC5\u987B\u552F\u4E00\uFF09\u3002" },
|
|
1070
|
+
new_string: { type: "string", description: "\u66FF\u6362\u540E\u7684\u5B57\u7B26\u4E32\u3002" }
|
|
1071
|
+
},
|
|
1072
|
+
required: ["path", "old_string", "new_string"]
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
};
|
|
1076
|
+
async function editFile(params) {
|
|
1077
|
+
const { path: path7, old_string, new_string } = params;
|
|
1078
|
+
let content;
|
|
1079
|
+
try {
|
|
1080
|
+
content = await fs6.readFile(path7, "utf8");
|
|
1081
|
+
} catch (err) {
|
|
1082
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1083
|
+
return `Error reading ${path7}: ${msg}`;
|
|
1084
|
+
}
|
|
1085
|
+
const count = countOccurrences(content, old_string);
|
|
1086
|
+
if (count === 0) {
|
|
1087
|
+
return `Error: old_string not found in ${path7}`;
|
|
1088
|
+
}
|
|
1089
|
+
if (count > 1) {
|
|
1090
|
+
return `Error: old_string appears ${count} times in ${path7} (ambiguous \u2014 add more context)`;
|
|
1091
|
+
}
|
|
1092
|
+
const updated = content.replace(old_string, new_string);
|
|
1093
|
+
try {
|
|
1094
|
+
await fs6.writeFile(path7, updated, "utf8");
|
|
1095
|
+
return `Edited ${path7}`;
|
|
1096
|
+
} catch (err) {
|
|
1097
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1098
|
+
return `Error writing ${path7}: ${msg}`;
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
function countOccurrences(haystack, needle) {
|
|
1102
|
+
if (needle === "") return 0;
|
|
1103
|
+
let count = 0;
|
|
1104
|
+
let pos = 0;
|
|
1105
|
+
while ((pos = haystack.indexOf(needle, pos)) !== -1) {
|
|
1106
|
+
count++;
|
|
1107
|
+
pos += needle.length;
|
|
1108
|
+
}
|
|
1109
|
+
return count;
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
// src/tools/apply_patch.ts
|
|
1113
|
+
import * as fs7 from "fs/promises";
|
|
1114
|
+
import * as path5 from "path";
|
|
1115
|
+
var APPLY_PATCH_TOOL = {
|
|
1116
|
+
type: "function",
|
|
1117
|
+
function: {
|
|
1118
|
+
name: "apply_patch",
|
|
1119
|
+
description: "\u5C06 unified diff \u683C\u5F0F\u7684 patch \u5E94\u7528\u5230\u6587\u4EF6\u3002\u652F\u6301 context \u4E0A\u4E0B\u6587\u884C\u6A21\u7CCA\u5B9A\u4F4D\uFF08\u5141\u8BB8\u5C11\u91CF\u884C\u53F7\u504F\u79FB\uFF09\u3002",
|
|
1120
|
+
parameters: {
|
|
1121
|
+
type: "object",
|
|
1122
|
+
properties: {
|
|
1123
|
+
patch: {
|
|
1124
|
+
type: "string",
|
|
1125
|
+
description: "Unified diff \u683C\u5F0F\u7684 patch \u5185\u5BB9\u3002"
|
|
1126
|
+
},
|
|
1127
|
+
file: {
|
|
1128
|
+
type: "string",
|
|
1129
|
+
description: "\u76EE\u6807\u6587\u4EF6\u8DEF\u5F84\uFF08\u53EF\u9009\uFF0C\u82E5 patch \u4E2D\u5305\u542B --- / +++ \u5934\u5219\u4ECE\u4E2D\u89E3\u6790\uFF09\u3002"
|
|
1130
|
+
},
|
|
1131
|
+
cwd: {
|
|
1132
|
+
type: "string",
|
|
1133
|
+
description: "\u5DE5\u4F5C\u76EE\u5F55\uFF08\u9ED8\u8BA4 process.cwd()\uFF09\u3002"
|
|
1134
|
+
},
|
|
1135
|
+
dryRun: {
|
|
1136
|
+
type: "boolean",
|
|
1137
|
+
description: "\u82E5\u4E3A true\uFF0C\u4EC5\u6821\u9A8C patch \u662F\u5426\u80FD\u5E94\u7528\uFF0C\u4E0D\u4FEE\u6539\u6587\u4EF6\uFF08\u9ED8\u8BA4 false\uFF09\u3002"
|
|
1138
|
+
}
|
|
1139
|
+
},
|
|
1140
|
+
required: ["patch"]
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
};
|
|
1144
|
+
function parseFilePath(patch) {
|
|
1145
|
+
for (const line of patch.split("\n")) {
|
|
1146
|
+
if (line.startsWith("+++ ")) {
|
|
1147
|
+
let p = line.slice(4).trim();
|
|
1148
|
+
if (p.startsWith("b/")) p = p.slice(2);
|
|
1149
|
+
if (p === "/dev/null" || p === "") return null;
|
|
1150
|
+
return p;
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
return null;
|
|
1154
|
+
}
|
|
1155
|
+
function parseHunkHeader(header) {
|
|
1156
|
+
const m = header.match(/^@@\s+-(\d+)(?:,(\d+))?\s+\+\d+(?:,\d+)?\s+@@/);
|
|
1157
|
+
if (!m) return null;
|
|
1158
|
+
const oldStart = parseInt(m[1], 10) - 1;
|
|
1159
|
+
const oldCount = m[2] !== void 0 ? parseInt(m[2], 10) : 1;
|
|
1160
|
+
return { oldStart, oldCount };
|
|
1161
|
+
}
|
|
1162
|
+
function parseHunks(patch) {
|
|
1163
|
+
const rawLines = patch.split("\n");
|
|
1164
|
+
const hunks = [];
|
|
1165
|
+
let current = null;
|
|
1166
|
+
for (const raw of rawLines) {
|
|
1167
|
+
if (raw.startsWith("diff --git ")) continue;
|
|
1168
|
+
if (raw.startsWith("--- ") || raw.startsWith("+++ ")) continue;
|
|
1169
|
+
if (raw.startsWith("\\ ")) continue;
|
|
1170
|
+
if (raw.startsWith("@@ ")) {
|
|
1171
|
+
const parsed = parseHunkHeader(raw);
|
|
1172
|
+
if (!parsed) continue;
|
|
1173
|
+
current = {
|
|
1174
|
+
header: raw,
|
|
1175
|
+
oldStart: parsed.oldStart,
|
|
1176
|
+
oldCount: parsed.oldCount,
|
|
1177
|
+
lines: []
|
|
1178
|
+
};
|
|
1179
|
+
hunks.push(current);
|
|
1180
|
+
continue;
|
|
1181
|
+
}
|
|
1182
|
+
if (current === null) continue;
|
|
1183
|
+
if (raw.startsWith("+")) {
|
|
1184
|
+
current.lines.push({ kind: "add", content: raw.slice(1) });
|
|
1185
|
+
} else if (raw.startsWith("-")) {
|
|
1186
|
+
current.lines.push({ kind: "remove", content: raw.slice(1) });
|
|
1187
|
+
} else if (raw.startsWith(" ")) {
|
|
1188
|
+
current.lines.push({ kind: "context", content: raw.slice(1) });
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
return hunks;
|
|
1192
|
+
}
|
|
1193
|
+
var FUZZY_RADIUS = 3;
|
|
1194
|
+
function fuzzyFind(fileLines, hunk, suggestedStart) {
|
|
1195
|
+
const expected = hunk.lines.filter((l) => l.kind === "context" || l.kind === "remove").map((l) => l.content);
|
|
1196
|
+
if (expected.length === 0) {
|
|
1197
|
+
return Math.max(0, Math.min(suggestedStart, fileLines.length));
|
|
1198
|
+
}
|
|
1199
|
+
const lo = Math.max(0, suggestedStart - FUZZY_RADIUS);
|
|
1200
|
+
const hi = Math.min(
|
|
1201
|
+
fileLines.length - expected.length,
|
|
1202
|
+
suggestedStart + FUZZY_RADIUS
|
|
1203
|
+
);
|
|
1204
|
+
for (let start = lo; start <= hi; start++) {
|
|
1205
|
+
let match = true;
|
|
1206
|
+
for (let i = 0; i < expected.length; i++) {
|
|
1207
|
+
if (fileLines[start + i] !== expected[i]) {
|
|
1208
|
+
match = false;
|
|
1209
|
+
break;
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
if (match) return start;
|
|
1213
|
+
}
|
|
1214
|
+
return null;
|
|
1215
|
+
}
|
|
1216
|
+
function applyHunk(fileLines, hunk, actualStart) {
|
|
1217
|
+
const oldLineCount = hunk.lines.filter(
|
|
1218
|
+
(l) => l.kind === "context" || l.kind === "remove"
|
|
1219
|
+
).length;
|
|
1220
|
+
const newBlock = [];
|
|
1221
|
+
for (const hl of hunk.lines) {
|
|
1222
|
+
if (hl.kind === "context" || hl.kind === "add") {
|
|
1223
|
+
newBlock.push(hl.content);
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
return [
|
|
1227
|
+
...fileLines.slice(0, actualStart),
|
|
1228
|
+
...newBlock,
|
|
1229
|
+
...fileLines.slice(actualStart + oldLineCount)
|
|
1230
|
+
];
|
|
1231
|
+
}
|
|
1232
|
+
async function applyPatch(params) {
|
|
1233
|
+
const { patch, file: fileParam, cwd = process.cwd(), dryRun = false } = params;
|
|
1234
|
+
let filePath;
|
|
1235
|
+
if (fileParam) {
|
|
1236
|
+
filePath = path5.isAbsolute(fileParam) ? fileParam : path5.resolve(cwd, fileParam);
|
|
1237
|
+
} else {
|
|
1238
|
+
const parsed = parseFilePath(patch);
|
|
1239
|
+
if (!parsed) {
|
|
1240
|
+
return "Error: no target file path found in patch headers (--- / +++ missing) and no 'file' param provided";
|
|
1241
|
+
}
|
|
1242
|
+
filePath = path5.isAbsolute(parsed) ? parsed : path5.resolve(cwd, parsed);
|
|
1243
|
+
}
|
|
1244
|
+
let originalContent;
|
|
1245
|
+
try {
|
|
1246
|
+
originalContent = await fs7.readFile(filePath, "utf8");
|
|
1247
|
+
} catch (err) {
|
|
1248
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1249
|
+
return `Error: ${msg}`;
|
|
1250
|
+
}
|
|
1251
|
+
const hasTrailingNewline = originalContent.endsWith("\n");
|
|
1252
|
+
const rawLines = originalContent.split("\n");
|
|
1253
|
+
let fileLines = hasTrailingNewline ? rawLines.slice(0, -1) : rawLines;
|
|
1254
|
+
const hunks = parseHunks(patch);
|
|
1255
|
+
if (hunks.length === 0) {
|
|
1256
|
+
return `Error: no valid hunks found in patch`;
|
|
1257
|
+
}
|
|
1258
|
+
let lineOffset = 0;
|
|
1259
|
+
for (const hunk of hunks) {
|
|
1260
|
+
const suggestedStart = hunk.oldStart + lineOffset;
|
|
1261
|
+
const actualStart = fuzzyFind(fileLines, hunk, suggestedStart);
|
|
1262
|
+
if (actualStart === null) {
|
|
1263
|
+
return `Error: hunk ${hunk.header} does not apply (context mismatch)`;
|
|
1264
|
+
}
|
|
1265
|
+
const addCount = hunk.lines.filter((l) => l.kind === "add").length;
|
|
1266
|
+
const removeCount = hunk.lines.filter((l) => l.kind === "remove").length;
|
|
1267
|
+
fileLines = applyHunk(fileLines, hunk, actualStart);
|
|
1268
|
+
lineOffset += addCount - removeCount;
|
|
1269
|
+
}
|
|
1270
|
+
const displayPath = path5.relative(cwd, filePath) || filePath;
|
|
1271
|
+
if (dryRun) {
|
|
1272
|
+
return `Patch applies cleanly to ${displayPath} (dry run)`;
|
|
1273
|
+
}
|
|
1274
|
+
const newContent = fileLines.join("\n") + (hasTrailingNewline ? "\n" : "");
|
|
1275
|
+
try {
|
|
1276
|
+
await fs7.writeFile(filePath, newContent, "utf8");
|
|
1277
|
+
} catch (err) {
|
|
1278
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1279
|
+
return `Error: failed to write file: ${msg}`;
|
|
1280
|
+
}
|
|
1281
|
+
return `Applied ${hunks.length} hunk(s) to ${displayPath}`;
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
// src/tools/todo.ts
|
|
1285
|
+
var TODO_TOOL = {
|
|
1286
|
+
type: "function",
|
|
1287
|
+
function: {
|
|
1288
|
+
name: "todo",
|
|
1289
|
+
description: "\u7BA1\u7406\u5F53\u524D\u4F1A\u8BDD\u7684\u4EFB\u52A1\u6E05\u5355\u3002op=read \u8BFB\u53D6\uFF0Cop=write \u5168\u91CF\u8986\u5199\uFF0Cop=update \u66F4\u65B0\u5355\u9879\u3002",
|
|
1290
|
+
parameters: {
|
|
1291
|
+
type: "object",
|
|
1292
|
+
properties: {
|
|
1293
|
+
op: {
|
|
1294
|
+
type: "string",
|
|
1295
|
+
enum: ["read", "write", "update"],
|
|
1296
|
+
description: "\u64CD\u4F5C\u7C7B\u578B\u3002"
|
|
1297
|
+
},
|
|
1298
|
+
todos: {
|
|
1299
|
+
type: "array",
|
|
1300
|
+
description: "write \u64CD\u4F5C\uFF1A\u5168\u91CF\u66FF\u6362\u7684\u4EFB\u52A1\u5217\u8868\u3002",
|
|
1301
|
+
items: {
|
|
1302
|
+
type: "object",
|
|
1303
|
+
properties: {
|
|
1304
|
+
id: { type: "string" },
|
|
1305
|
+
content: { type: "string" },
|
|
1306
|
+
status: {
|
|
1307
|
+
type: "string",
|
|
1308
|
+
enum: ["pending", "in_progress", "completed", "cancelled"]
|
|
1309
|
+
}
|
|
1310
|
+
},
|
|
1311
|
+
required: ["id", "content", "status"]
|
|
1312
|
+
}
|
|
1313
|
+
},
|
|
1314
|
+
id: {
|
|
1315
|
+
type: "string",
|
|
1316
|
+
description: "update \u64CD\u4F5C\uFF1A\u76EE\u6807\u4EFB\u52A1\u7684\u552F\u4E00 id\u3002"
|
|
1317
|
+
},
|
|
1318
|
+
status: {
|
|
1319
|
+
type: "string",
|
|
1320
|
+
enum: ["pending", "in_progress", "completed", "cancelled"],
|
|
1321
|
+
description: "update \u64CD\u4F5C\uFF1A\u65B0\u72B6\u6001\uFF08\u53EF\u9009\uFF09\u3002"
|
|
1322
|
+
},
|
|
1323
|
+
content: {
|
|
1324
|
+
type: "string",
|
|
1325
|
+
description: "update \u64CD\u4F5C\uFF1A\u65B0\u5185\u5BB9\uFF08\u53EF\u9009\uFF09\u3002"
|
|
1326
|
+
}
|
|
1327
|
+
},
|
|
1328
|
+
required: ["op"]
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
};
|
|
1332
|
+
var _todos = [];
|
|
1333
|
+
var STATUS_SYMBOL = {
|
|
1334
|
+
pending: "[ ]",
|
|
1335
|
+
in_progress: "[~]",
|
|
1336
|
+
completed: "[x]",
|
|
1337
|
+
cancelled: "[-]"
|
|
1338
|
+
};
|
|
1339
|
+
function opRead() {
|
|
1340
|
+
if (_todos.length === 0) {
|
|
1341
|
+
return "(no todos)";
|
|
1342
|
+
}
|
|
1343
|
+
return _todos.map((item) => `- ${STATUS_SYMBOL[item.status]} ${item.id}: ${item.content}`).join("\n");
|
|
1344
|
+
}
|
|
1345
|
+
function opWrite(params) {
|
|
1346
|
+
if (params.todos === void 0) {
|
|
1347
|
+
return "Error: write operation requires 'todos' parameter";
|
|
1348
|
+
}
|
|
1349
|
+
_todos = params.todos.map((t) => ({ ...t }));
|
|
1350
|
+
return `Wrote ${_todos.length} todos.`;
|
|
1351
|
+
}
|
|
1352
|
+
function opUpdate(params) {
|
|
1353
|
+
if (params.id === void 0) {
|
|
1354
|
+
return "Error: update operation requires 'id' parameter";
|
|
1355
|
+
}
|
|
1356
|
+
if (params.status === void 0 && params.content === void 0) {
|
|
1357
|
+
return "Error: update operation requires at least 'status' or 'content' parameter";
|
|
1358
|
+
}
|
|
1359
|
+
const idx = _todos.findIndex((t) => t.id === params.id);
|
|
1360
|
+
if (idx === -1) {
|
|
1361
|
+
return `Error: todo '${params.id}' not found`;
|
|
1362
|
+
}
|
|
1363
|
+
const updated = {
|
|
1364
|
+
..._todos[idx],
|
|
1365
|
+
...params.status !== void 0 ? { status: params.status } : {},
|
|
1366
|
+
...params.content !== void 0 ? { content: params.content } : {}
|
|
1367
|
+
};
|
|
1368
|
+
_todos = [
|
|
1369
|
+
..._todos.slice(0, idx),
|
|
1370
|
+
updated,
|
|
1371
|
+
..._todos.slice(idx + 1)
|
|
1372
|
+
];
|
|
1373
|
+
return `Updated todo ${params.id}.`;
|
|
1374
|
+
}
|
|
1375
|
+
function todo(params) {
|
|
1376
|
+
switch (params.op) {
|
|
1377
|
+
case "read":
|
|
1378
|
+
return opRead();
|
|
1379
|
+
case "write":
|
|
1380
|
+
return opWrite(params);
|
|
1381
|
+
case "update":
|
|
1382
|
+
return opUpdate(params);
|
|
1383
|
+
default: {
|
|
1384
|
+
const unknownOp = params.op;
|
|
1385
|
+
return `Error: unknown op '${unknownOp}'`;
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
// src/tools/task.ts
|
|
1391
|
+
import * as crypto from "crypto";
|
|
1392
|
+
import * as path6 from "path";
|
|
1393
|
+
|
|
1394
|
+
// src/subagent/runtime.ts
|
|
1395
|
+
async function runSubagentRuntime(options) {
|
|
1396
|
+
const { messages: initial, tools, provider, maxTurns, executeToolCall, onOutput } = options;
|
|
1397
|
+
const messages = [...initial];
|
|
1398
|
+
let turnCount = 0;
|
|
1399
|
+
while (turnCount < maxTurns) {
|
|
1400
|
+
turnCount++;
|
|
1401
|
+
let assistantText = "";
|
|
1402
|
+
const toolCalls = [];
|
|
1403
|
+
for await (const chunk of provider.stream(messages, tools)) {
|
|
1404
|
+
if (chunk.text) {
|
|
1405
|
+
assistantText += chunk.text;
|
|
1406
|
+
onOutput == null ? void 0 : onOutput(chunk.text);
|
|
1407
|
+
}
|
|
1408
|
+
if (chunk.done && chunk.toolCalls) {
|
|
1409
|
+
toolCalls.push(...chunk.toolCalls);
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1412
|
+
if (toolCalls.length === 0) {
|
|
1413
|
+
messages.push({ role: "assistant", content: assistantText });
|
|
1414
|
+
return { success: true, finalText: assistantText, turnCount };
|
|
1415
|
+
}
|
|
1416
|
+
messages.push({
|
|
1417
|
+
role: "assistant",
|
|
1418
|
+
content: assistantText || null,
|
|
1419
|
+
tool_calls: toolCalls.map((tc) => ({
|
|
1420
|
+
id: tc.id,
|
|
1421
|
+
type: "function",
|
|
1422
|
+
function: { name: tc.name, arguments: tc.arguments }
|
|
1423
|
+
}))
|
|
1424
|
+
});
|
|
1425
|
+
for (const tc of toolCalls) {
|
|
1426
|
+
const result = await executeToolCall(tc);
|
|
1427
|
+
messages.push({ role: "tool", tool_call_id: tc.id, content: result });
|
|
1428
|
+
}
|
|
1429
|
+
}
|
|
1430
|
+
return {
|
|
1431
|
+
success: false,
|
|
1432
|
+
finalText: "",
|
|
1433
|
+
turnCount,
|
|
1434
|
+
error: `Subagent exceeded max turns (${maxTurns})`
|
|
1435
|
+
};
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1438
|
+
// src/repl.ts
|
|
1439
|
+
import * as readline from "readline";
|
|
1440
|
+
|
|
1441
|
+
// src/safety.ts
|
|
1442
|
+
var ALLOWLIST = [
|
|
1443
|
+
"ls",
|
|
1444
|
+
"cat",
|
|
1445
|
+
"pwd",
|
|
1446
|
+
"echo",
|
|
1447
|
+
"head",
|
|
1448
|
+
"tail",
|
|
1449
|
+
"wc",
|
|
1450
|
+
"date",
|
|
1451
|
+
"whoami",
|
|
1452
|
+
"which",
|
|
1453
|
+
"printenv"
|
|
1454
|
+
// "env" 故意排除:env <CMD> 会执行任意子命令,不适合 allow 级别。
|
|
1455
|
+
// 仅显示环境变量的 printenv 保留在白名单。
|
|
1456
|
+
];
|
|
1457
|
+
var DEFAULT_DANGER_LIST = [
|
|
1458
|
+
"rm -rf",
|
|
1459
|
+
"sudo",
|
|
1460
|
+
"chmod",
|
|
1461
|
+
"chown",
|
|
1462
|
+
"mkfs",
|
|
1463
|
+
"dd",
|
|
1464
|
+
"fdisk",
|
|
1465
|
+
"kill",
|
|
1466
|
+
"pkill",
|
|
1467
|
+
"killall",
|
|
1468
|
+
"reboot",
|
|
1469
|
+
"shutdown",
|
|
1470
|
+
"halt",
|
|
1471
|
+
"curl -X DELETE",
|
|
1472
|
+
"wget --delete-after"
|
|
1473
|
+
];
|
|
1474
|
+
var INDIRECT_EXEC_LIST = [
|
|
1475
|
+
"xargs",
|
|
1476
|
+
"python -c",
|
|
1477
|
+
"python3 -c",
|
|
1478
|
+
"node -e",
|
|
1479
|
+
"perl -e",
|
|
1480
|
+
"ruby -e",
|
|
1481
|
+
// eval 执行任意字符串作为 shell 命令,无论 payload 内容如何都视为危险。
|
|
1482
|
+
// 与 xargs/python -c 的性质相同:执行能力本身就是风险。
|
|
1483
|
+
"eval"
|
|
1484
|
+
];
|
|
1485
|
+
function hasFindExec(cmd) {
|
|
1486
|
+
return /^find(\s|$)/.test(cmd) && /\s-exec(\s|$)/.test(cmd);
|
|
1487
|
+
}
|
|
1488
|
+
function matchesEntry(cmd, entry) {
|
|
1489
|
+
return cmd === entry || cmd.startsWith(entry + " ");
|
|
1490
|
+
}
|
|
1491
|
+
function splitByControlOps(cmd) {
|
|
1492
|
+
const segments = [];
|
|
1493
|
+
let current = "";
|
|
1494
|
+
let inSingle = false;
|
|
1495
|
+
let inDouble = false;
|
|
1496
|
+
let i = 0;
|
|
1497
|
+
while (i < cmd.length) {
|
|
1498
|
+
const ch = cmd[i];
|
|
1499
|
+
if (ch === "'" && !inDouble) {
|
|
1500
|
+
inSingle = !inSingle;
|
|
1501
|
+
current += ch;
|
|
1502
|
+
i++;
|
|
1503
|
+
continue;
|
|
1504
|
+
}
|
|
1505
|
+
if (ch === '"' && !inSingle) {
|
|
1506
|
+
inDouble = !inDouble;
|
|
1507
|
+
current += ch;
|
|
1508
|
+
i++;
|
|
1509
|
+
continue;
|
|
1510
|
+
}
|
|
1511
|
+
if (inSingle || inDouble) {
|
|
1512
|
+
if (inDouble && ch === "\\" && i + 1 < cmd.length) {
|
|
1513
|
+
current += ch + cmd[i + 1];
|
|
1514
|
+
i += 2;
|
|
1515
|
+
} else {
|
|
1516
|
+
current += ch;
|
|
1517
|
+
i++;
|
|
1518
|
+
}
|
|
1519
|
+
continue;
|
|
1520
|
+
}
|
|
1521
|
+
if (ch === "&" && cmd[i + 1] === "&") {
|
|
1522
|
+
segments.push(current.trim());
|
|
1523
|
+
current = "";
|
|
1524
|
+
i += 2;
|
|
1525
|
+
continue;
|
|
1526
|
+
}
|
|
1527
|
+
if (ch === "|" && cmd[i + 1] === "|") {
|
|
1528
|
+
segments.push(current.trim());
|
|
1529
|
+
current = "";
|
|
1530
|
+
i += 2;
|
|
1531
|
+
continue;
|
|
1532
|
+
}
|
|
1533
|
+
if (ch === ";" || ch === "|") {
|
|
1534
|
+
segments.push(current.trim());
|
|
1535
|
+
current = "";
|
|
1536
|
+
i++;
|
|
1537
|
+
continue;
|
|
1538
|
+
}
|
|
1539
|
+
current += ch;
|
|
1540
|
+
i++;
|
|
1541
|
+
}
|
|
1542
|
+
segments.push(current.trim());
|
|
1543
|
+
return segments.filter((s) => s.length > 0);
|
|
1544
|
+
}
|
|
1545
|
+
function stripShellWrapper(cmd) {
|
|
1546
|
+
const trimmed = cmd.trim();
|
|
1547
|
+
const shellMatch = trimmed.match(/^(bash|sh)\s+([\s\S]*)/);
|
|
1548
|
+
if (!shellMatch) return trimmed;
|
|
1549
|
+
let rest = shellMatch[2];
|
|
1550
|
+
let hasCFlag = false;
|
|
1551
|
+
while (rest.length > 0) {
|
|
1552
|
+
const flagMatch = rest.match(/^(-\w+)\s*([\s\S]*)/);
|
|
1553
|
+
if (!flagMatch) break;
|
|
1554
|
+
const flag = flagMatch[1];
|
|
1555
|
+
rest = flagMatch[2];
|
|
1556
|
+
if (flag.includes("c")) {
|
|
1557
|
+
hasCFlag = true;
|
|
1558
|
+
rest = rest.trimStart();
|
|
1559
|
+
break;
|
|
1560
|
+
}
|
|
1561
|
+
rest = rest.trimStart();
|
|
1562
|
+
}
|
|
1563
|
+
if (!hasCFlag || !rest.length) return trimmed;
|
|
1564
|
+
if (rest.startsWith('"')) {
|
|
1565
|
+
const m = rest.match(/^"((?:[^"\\]|\\.)*)"/);
|
|
1566
|
+
if (m) return m[1];
|
|
1567
|
+
} else if (rest.startsWith("'")) {
|
|
1568
|
+
const m = rest.match(/^'([^']*)'/);
|
|
1569
|
+
if (m) return m[1];
|
|
1570
|
+
} else {
|
|
1571
|
+
const m = rest.match(/^(\S+)/);
|
|
1572
|
+
if (m) return m[1];
|
|
1573
|
+
}
|
|
1574
|
+
return trimmed;
|
|
1575
|
+
}
|
|
1576
|
+
function stripEnvWrapper(cmd) {
|
|
1577
|
+
const trimmed = cmd.trim();
|
|
1578
|
+
if (!trimmed.startsWith("env ") && trimmed !== "env") return trimmed;
|
|
1579
|
+
let rest = trimmed.slice(3).trimStart();
|
|
1580
|
+
while (rest.length > 0) {
|
|
1581
|
+
const assignMatch = rest.match(/^(\w+=\S*)\s*([\s\S]*)/);
|
|
1582
|
+
if (!assignMatch) break;
|
|
1583
|
+
rest = assignMatch[2].trimStart();
|
|
1584
|
+
}
|
|
1585
|
+
if (!rest.length) return trimmed;
|
|
1586
|
+
return rest;
|
|
1587
|
+
}
|
|
1588
|
+
function stripCommandWrapper(cmd) {
|
|
1589
|
+
const trimmed = cmd.trim();
|
|
1590
|
+
const m = trimmed.match(/^command\s+([\s\S]+)/);
|
|
1591
|
+
if (m) return m[1].trimStart();
|
|
1592
|
+
return trimmed;
|
|
1593
|
+
}
|
|
1594
|
+
function classifyCommand(cmd, dangerPatterns, _depth = 0) {
|
|
1595
|
+
const trimmed = cmd.trim();
|
|
1596
|
+
const patterns = dangerPatterns ?? DEFAULT_DANGER_LIST;
|
|
1597
|
+
if (_depth < 5) {
|
|
1598
|
+
const shellInner = stripShellWrapper(trimmed);
|
|
1599
|
+
if (shellInner !== trimmed) {
|
|
1600
|
+
if (classifyCommand(shellInner.trim(), dangerPatterns, _depth + 1) === "danger") return "danger";
|
|
1601
|
+
return "normal";
|
|
1602
|
+
}
|
|
1603
|
+
const envInner = stripEnvWrapper(trimmed);
|
|
1604
|
+
if (envInner !== trimmed) {
|
|
1605
|
+
if (classifyCommand(envInner.trim(), dangerPatterns, _depth + 1) === "danger") return "danger";
|
|
1606
|
+
return "normal";
|
|
1607
|
+
}
|
|
1608
|
+
const commandInner = stripCommandWrapper(trimmed);
|
|
1609
|
+
if (commandInner !== trimmed) {
|
|
1610
|
+
if (classifyCommand(commandInner.trim(), dangerPatterns, _depth + 1) === "danger") return "danger";
|
|
1611
|
+
return "normal";
|
|
1612
|
+
}
|
|
1613
|
+
}
|
|
1614
|
+
const segments = splitByControlOps(trimmed);
|
|
1615
|
+
if (segments.length > 1) {
|
|
1616
|
+
for (const seg of segments) {
|
|
1617
|
+
if (classifyCommand(seg, dangerPatterns, _depth + 1) === "danger") {
|
|
1618
|
+
return "danger";
|
|
1619
|
+
}
|
|
1620
|
+
}
|
|
1621
|
+
return "normal";
|
|
1622
|
+
}
|
|
1623
|
+
for (const entry of patterns) {
|
|
1624
|
+
if (matchesEntry(trimmed, entry)) return "danger";
|
|
1625
|
+
}
|
|
1626
|
+
for (const entry of INDIRECT_EXEC_LIST) {
|
|
1627
|
+
if (matchesEntry(trimmed, entry)) return "danger";
|
|
1628
|
+
}
|
|
1629
|
+
if (hasFindExec(trimmed)) return "danger";
|
|
1630
|
+
for (const entry of ALLOWLIST) {
|
|
1631
|
+
if (matchesEntry(trimmed, entry)) return "allow";
|
|
1632
|
+
}
|
|
1633
|
+
return "normal";
|
|
1634
|
+
}
|
|
1635
|
+
|
|
1636
|
+
// src/prompts.ts
|
|
1637
|
+
var DEFAULT_SYSTEM_PROMPT = `You are ecode, a terminal-based coding assistant focused on software engineering work.
|
|
1638
|
+
|
|
1639
|
+
Your job is to help the user complete coding tasks accurately, efficiently, and safely. Prefer doing useful work over discussing work abstractly, but do not pretend to have done things you did not do.
|
|
1640
|
+
|
|
1641
|
+
# Core behavior
|
|
1642
|
+
|
|
1643
|
+
- Treat the user's request as a real software engineering task unless they clearly ask for pure explanation.
|
|
1644
|
+
- Read relevant code before proposing or making changes.
|
|
1645
|
+
- Understand the local context before editing. Do not guess how the codebase works when you can inspect it.
|
|
1646
|
+
- Prefer the simplest approach that fully solves the requested problem.
|
|
1647
|
+
- Do not add features, refactors, configurability, abstractions, or cleanups that were not requested unless they are necessary to complete the task correctly.
|
|
1648
|
+
- Do not make speculative improvements for hypothetical future needs.
|
|
1649
|
+
- If an approach fails, diagnose the reason before switching tactics. Do not blindly retry the same failing action.
|
|
1650
|
+
|
|
1651
|
+
# Editing discipline
|
|
1652
|
+
|
|
1653
|
+
- Prefer dedicated file tools over shell commands when available.
|
|
1654
|
+
- Use read-like tools to inspect files, edit/patch tools to modify files, and write tools only when replacing full file content is the right choice.
|
|
1655
|
+
- Do not use shell tricks as a substitute for structured file operations when a dedicated tool exists.
|
|
1656
|
+
- Make the smallest coherent change that solves the problem.
|
|
1657
|
+
- Do not create new files unless they are actually needed.
|
|
1658
|
+
- Do not rewrite large files when a targeted edit is enough.
|
|
1659
|
+
- Preserve existing code style unless the task explicitly requires style changes.
|
|
1660
|
+
- Do not add comments unless they help explain a non-obvious constraint or decision.
|
|
1661
|
+
|
|
1662
|
+
# Tool usage
|
|
1663
|
+
|
|
1664
|
+
- Prefer dedicated tools over bash for file reading, file editing, search, patching, and task tracking.
|
|
1665
|
+
- Use bash for commands that are genuinely shell-oriented: tests, builds, git inspection, package manager commands, environment inspection, and other terminal operations.
|
|
1666
|
+
- When there are multiple independent lookups, do them efficiently. When steps depend on each other, do them in order.
|
|
1667
|
+
- Treat tool output as evidence. Base conclusions on what you actually observed.
|
|
1668
|
+
- If tool output is noisy, extract and carry forward only the important facts.
|
|
1669
|
+
|
|
1670
|
+
# Safety and confirmation
|
|
1671
|
+
|
|
1672
|
+
- Be careful with destructive, irreversible, or externally visible actions.
|
|
1673
|
+
- Ask before actions like deleting important files, overwriting significant uncommitted work, force-pushing, changing remote state, sending messages, or performing other hard-to-reverse operations.
|
|
1674
|
+
- Do not treat a previous approval as blanket approval for future risky actions.
|
|
1675
|
+
- If the user denies a risky action, do not immediately retry the same action. Adjust your plan.
|
|
1676
|
+
- Safety checks are a convenience layer, not a guarantee. Act cautiously even when a command appears allowed.
|
|
1677
|
+
|
|
1678
|
+
# Verification and honesty
|
|
1679
|
+
|
|
1680
|
+
- When you change code, verify the result when practical: run tests, type checks, linters, builds, or the narrowest useful validation step.
|
|
1681
|
+
- Prefer the smallest meaningful verification rather than expensive blanket verification when the task is local and narrow.
|
|
1682
|
+
- If you could not verify something, say so plainly.
|
|
1683
|
+
- Never claim success, passing tests, or completed work unless the evidence supports it.
|
|
1684
|
+
- If a command failed, a test failed, or verification is incomplete, report that accurately.
|
|
1685
|
+
|
|
1686
|
+
# Communication style
|
|
1687
|
+
|
|
1688
|
+
- Be concise, direct, and useful.
|
|
1689
|
+
- Give short progress updates when starting multi-step work, when you discover something important, when changing direction, or when blocked.
|
|
1690
|
+
- Do not narrate every obvious tool call.
|
|
1691
|
+
- Do not pad responses with filler, hype, or unnecessary repetition.
|
|
1692
|
+
- When reporting completion, lead with what changed, then include relevant verification or blockers.
|
|
1693
|
+
- When you need user input, ask for the single most important missing decision.
|
|
1694
|
+
|
|
1695
|
+
# Scope control
|
|
1696
|
+
|
|
1697
|
+
- Solve the requested problem completely, but do not quietly widen scope.
|
|
1698
|
+
- If you notice an adjacent issue that matters, mention it briefly and separate it from the requested task.
|
|
1699
|
+
- Favor correctness and clarity over cleverness.
|
|
1700
|
+
|
|
1701
|
+
# Failure mode handling
|
|
1702
|
+
|
|
1703
|
+
- If paths, commands, or assumptions are wrong, correct course based on inspection rather than defending the original plan.
|
|
1704
|
+
- If you are blocked by missing information, say exactly what is missing.
|
|
1705
|
+
- If you are blocked by tool limits, choose the next best bounded action instead of stalling.`;
|
|
1706
|
+
|
|
1707
|
+
// src/repl.ts
|
|
1708
|
+
var SKIP_MESSAGE = "Command skipped by user.";
|
|
1709
|
+
var BASH_TOOL = {
|
|
1710
|
+
type: "function",
|
|
1711
|
+
function: {
|
|
1712
|
+
name: "bash",
|
|
1713
|
+
description: "Execute a shell command and return its output.",
|
|
1714
|
+
parameters: {
|
|
1715
|
+
type: "object",
|
|
1716
|
+
properties: {
|
|
1717
|
+
command: { type: "string", description: "The shell command to run." }
|
|
1718
|
+
},
|
|
1719
|
+
required: ["command"]
|
|
1720
|
+
}
|
|
1721
|
+
}
|
|
1722
|
+
};
|
|
1723
|
+
async function handleBashTool(command, deps) {
|
|
1724
|
+
const { confirm, print, dangerousPatterns, autoApproveNormal } = deps;
|
|
1725
|
+
const cls = classifyCommand(command, dangerousPatterns);
|
|
1726
|
+
if (cls === "normal") {
|
|
1727
|
+
if (!autoApproveNormal) {
|
|
1728
|
+
const ok = await confirm(`Execute command: ${command}
|
|
1729
|
+
Proceed? (y/n) `);
|
|
1730
|
+
if (!ok) return SKIP_MESSAGE;
|
|
1731
|
+
}
|
|
1732
|
+
} else if (cls === "danger") {
|
|
1733
|
+
print(`\u26A0\uFE0F DANGEROUS COMMAND: ${command}`);
|
|
1734
|
+
const first = await confirm("Are you sure? (y/n) ");
|
|
1735
|
+
if (!first) return SKIP_MESSAGE;
|
|
1736
|
+
const second = await confirm(
|
|
1737
|
+
"Confirm again \u2014 this is destructive. Continue? (y/n) "
|
|
1738
|
+
);
|
|
1739
|
+
if (!second) return SKIP_MESSAGE;
|
|
1740
|
+
}
|
|
1741
|
+
const result = await deps.executeBash(command);
|
|
1742
|
+
let output = "";
|
|
1743
|
+
if (result.stdout) output += result.stdout;
|
|
1744
|
+
if (result.stderr) output += result.stderr;
|
|
1745
|
+
if (result.exitCode !== 0) output += `
|
|
1746
|
+
[exit code: ${result.exitCode}]`;
|
|
1747
|
+
return output || "(no output)";
|
|
1748
|
+
}
|
|
1749
|
+
|
|
1750
|
+
// src/tools/task.ts
|
|
1751
|
+
var TASK_TOOL = {
|
|
1752
|
+
type: "function",
|
|
1753
|
+
function: {
|
|
1754
|
+
name: "task",
|
|
1755
|
+
description: "Delegate a well-defined subtask to an isolated child agent. The child agent runs its own agentic loop, executes tools, and returns a single result. Use this when a subtask is independent, well-scoped, and benefits from a clean context.",
|
|
1756
|
+
parameters: {
|
|
1757
|
+
type: "object",
|
|
1758
|
+
properties: {
|
|
1759
|
+
description: {
|
|
1760
|
+
type: "string",
|
|
1761
|
+
description: "Short title for the subtask (used in logs and status output)."
|
|
1762
|
+
},
|
|
1763
|
+
prompt: {
|
|
1764
|
+
type: "string",
|
|
1765
|
+
description: "Full task instructions for the child agent."
|
|
1766
|
+
},
|
|
1767
|
+
context: {
|
|
1768
|
+
type: "string",
|
|
1769
|
+
description: "Optional additional context to pass to the child agent."
|
|
1770
|
+
},
|
|
1771
|
+
cwd: {
|
|
1772
|
+
type: "string",
|
|
1773
|
+
description: "Working directory for the child agent (defaults to current directory)."
|
|
1774
|
+
},
|
|
1775
|
+
model: {
|
|
1776
|
+
type: "string",
|
|
1777
|
+
description: "Optional model override for the child agent."
|
|
1778
|
+
},
|
|
1779
|
+
max_turns: {
|
|
1780
|
+
type: "number",
|
|
1781
|
+
description: "Maximum agentic loop turns for the child agent (default: 8)."
|
|
1782
|
+
}
|
|
1783
|
+
},
|
|
1784
|
+
required: ["description", "prompt"]
|
|
1785
|
+
}
|
|
1786
|
+
}
|
|
1787
|
+
};
|
|
1788
|
+
var SUBAGENT_SYSTEM_PROMPT = `You are a focused subagent executing a delegated task on behalf of a parent agent.
|
|
1789
|
+
|
|
1790
|
+
Rules:
|
|
1791
|
+
- Complete the task independently using the tools available to you.
|
|
1792
|
+
- Do not ask the human clarifying questions for routine operations \u2014 make reasonable decisions.
|
|
1793
|
+
- When done, return a clear, concise result.
|
|
1794
|
+
- If you are blocked, describe the blocker and the minimum next step needed.
|
|
1795
|
+
- You cannot delegate tasks to other agents.`;
|
|
1796
|
+
var SUBAGENT_TOOLS = [
|
|
1797
|
+
BASH_TOOL,
|
|
1798
|
+
READ_TOOL,
|
|
1799
|
+
WRITE_TOOL,
|
|
1800
|
+
EDIT_TOOL,
|
|
1801
|
+
GLOB_TOOL,
|
|
1802
|
+
GREP_TOOL,
|
|
1803
|
+
APPLY_PATCH_TOOL,
|
|
1804
|
+
TODO_TOOL
|
|
1805
|
+
];
|
|
1806
|
+
async function handleTaskTool(args, deps) {
|
|
1807
|
+
const { description, prompt, context, max_turns = 8 } = args;
|
|
1808
|
+
const { provider, print = () => void 0, logDir } = deps;
|
|
1809
|
+
const taskId = crypto.randomUUID().slice(0, 8);
|
|
1810
|
+
let logger;
|
|
1811
|
+
if (logDir) {
|
|
1812
|
+
logger = createLogger(logDir, /* @__PURE__ */ new Date());
|
|
1813
|
+
const meta = createSessionMetadata(logger.filePath, "subagent");
|
|
1814
|
+
writeSessionMetadata(logger.filePath, {
|
|
1815
|
+
...meta,
|
|
1816
|
+
title: `[subagent] ${description}`
|
|
1817
|
+
});
|
|
1818
|
+
}
|
|
1819
|
+
const userContent = context ? `Context:
|
|
1820
|
+
${context}
|
|
1821
|
+
|
|
1822
|
+
Task:
|
|
1823
|
+
${prompt}` : prompt;
|
|
1824
|
+
const messages = [
|
|
1825
|
+
{ role: "system", content: SUBAGENT_SYSTEM_PROMPT },
|
|
1826
|
+
{ role: "user", content: userContent }
|
|
1827
|
+
];
|
|
1828
|
+
if (logger) {
|
|
1829
|
+
logger.append({ ts: (/* @__PURE__ */ new Date()).toISOString(), role: "user", content: userContent });
|
|
1830
|
+
}
|
|
1831
|
+
const bashDeps = {
|
|
1832
|
+
executeBash,
|
|
1833
|
+
confirm: async () => false,
|
|
1834
|
+
// danger commands will be denied
|
|
1835
|
+
print,
|
|
1836
|
+
autoApproveNormal: true
|
|
1837
|
+
};
|
|
1838
|
+
const executeToolCall = async (tc) => {
|
|
1839
|
+
let result;
|
|
1840
|
+
if (tc.name === "bash") {
|
|
1841
|
+
const tcArgs = JSON.parse(tc.arguments);
|
|
1842
|
+
result = await handleBashTool(tcArgs.command, bashDeps);
|
|
1843
|
+
} else if (tc.name === "read") {
|
|
1844
|
+
const tcArgs = JSON.parse(tc.arguments);
|
|
1845
|
+
result = await readFile3(tcArgs);
|
|
1846
|
+
} else if (tc.name === "write") {
|
|
1847
|
+
const tcArgs = JSON.parse(tc.arguments);
|
|
1848
|
+
result = await writeFile2(tcArgs);
|
|
1849
|
+
} else if (tc.name === "edit") {
|
|
1850
|
+
const tcArgs = JSON.parse(tc.arguments);
|
|
1851
|
+
result = await editFile(tcArgs);
|
|
1852
|
+
} else if (tc.name === "glob") {
|
|
1853
|
+
const tcArgs = JSON.parse(tc.arguments);
|
|
1854
|
+
result = await globFiles(tcArgs);
|
|
1855
|
+
} else if (tc.name === "grep") {
|
|
1856
|
+
const tcArgs = JSON.parse(tc.arguments);
|
|
1857
|
+
result = await grepFiles(tcArgs);
|
|
1858
|
+
} else if (tc.name === "apply_patch") {
|
|
1859
|
+
const tcArgs = JSON.parse(tc.arguments);
|
|
1860
|
+
result = await applyPatch(tcArgs);
|
|
1861
|
+
} else if (tc.name === "todo") {
|
|
1862
|
+
const tcArgs = JSON.parse(tc.arguments);
|
|
1863
|
+
result = todo(tcArgs);
|
|
1864
|
+
} else {
|
|
1865
|
+
result = `Unknown tool: ${tc.name}`;
|
|
1866
|
+
}
|
|
1867
|
+
if (logger) {
|
|
1868
|
+
logger.append({
|
|
1869
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1870
|
+
role: "tool",
|
|
1871
|
+
content: result,
|
|
1872
|
+
tool_call_id: tc.id
|
|
1873
|
+
});
|
|
1874
|
+
}
|
|
1875
|
+
return result;
|
|
1876
|
+
};
|
|
1877
|
+
const runtime = await runSubagentRuntime({
|
|
1878
|
+
messages,
|
|
1879
|
+
tools: SUBAGENT_TOOLS,
|
|
1880
|
+
provider,
|
|
1881
|
+
maxTurns: max_turns,
|
|
1882
|
+
executeToolCall,
|
|
1883
|
+
onOutput: (text) => print(text)
|
|
1884
|
+
});
|
|
1885
|
+
if (logger) {
|
|
1886
|
+
logger.append({
|
|
1887
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1888
|
+
role: "assistant",
|
|
1889
|
+
content: runtime.finalText || runtime.error || null
|
|
1890
|
+
});
|
|
1891
|
+
updateSessionMetadata(logger.filePath, {
|
|
1892
|
+
lastActivity: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1893
|
+
turnCount: runtime.turnCount,
|
|
1894
|
+
title: `[subagent] ${description}`
|
|
1895
|
+
});
|
|
1896
|
+
}
|
|
1897
|
+
const logLine = logger ? `log_file: ${path6.basename(logger.filePath)}
|
|
1898
|
+
` : "";
|
|
1899
|
+
if (!runtime.success) {
|
|
1900
|
+
return [
|
|
1901
|
+
`task_id: ${taskId}`,
|
|
1902
|
+
`description: ${description}`,
|
|
1903
|
+
logLine.trim(),
|
|
1904
|
+
"",
|
|
1905
|
+
`<task_result>`,
|
|
1906
|
+
`Error: ${runtime.error ?? "Subagent failed"}`,
|
|
1907
|
+
`</task_result>`
|
|
1908
|
+
].filter((l) => l !== "" || l === "").join("\n").replace(/\n{3,}/g, "\n\n");
|
|
1909
|
+
}
|
|
1910
|
+
return [
|
|
1911
|
+
`task_id: ${taskId}`,
|
|
1912
|
+
`description: ${description}`,
|
|
1913
|
+
logLine.trim(),
|
|
1914
|
+
"",
|
|
1915
|
+
`<task_result>`,
|
|
1916
|
+
runtime.finalText,
|
|
1917
|
+
`</task_result>`
|
|
1918
|
+
].join("\n").replace(/\n{3,}/g, "\n\n");
|
|
1919
|
+
}
|
|
1920
|
+
|
|
1921
|
+
export {
|
|
1922
|
+
createProvider,
|
|
1923
|
+
resolveActiveProfile,
|
|
1924
|
+
isTrustedSkillPath,
|
|
1925
|
+
loadSkillsFromDir,
|
|
1926
|
+
READ_TOOL,
|
|
1927
|
+
readFile3 as readFile,
|
|
1928
|
+
GLOB_TOOL,
|
|
1929
|
+
globFiles,
|
|
1930
|
+
GREP_TOOL,
|
|
1931
|
+
grepFiles,
|
|
1932
|
+
WEB_FETCH_TOOL,
|
|
1933
|
+
webFetch,
|
|
1934
|
+
createLogger,
|
|
1935
|
+
DEFAULT_SYSTEM_PROMPT,
|
|
1936
|
+
classifyCommand,
|
|
1937
|
+
executeBash,
|
|
1938
|
+
WRITE_TOOL,
|
|
1939
|
+
writeFile2 as writeFile,
|
|
1940
|
+
EDIT_TOOL,
|
|
1941
|
+
editFile,
|
|
1942
|
+
APPLY_PATCH_TOOL,
|
|
1943
|
+
applyPatch,
|
|
1944
|
+
TODO_TOOL,
|
|
1945
|
+
todo,
|
|
1946
|
+
TASK_TOOL,
|
|
1947
|
+
handleTaskTool,
|
|
1948
|
+
SKIP_MESSAGE,
|
|
1949
|
+
BASH_TOOL,
|
|
1950
|
+
handleBashTool
|
|
1951
|
+
};
|