@cryer/star-cli 0.3.8 → 0.3.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -0
- package/README.zh-CN.md +4 -0
- package/dist/chunk-4IRY44TG.js +34 -0
- package/dist/chunk-4IRY44TG.js.map +1 -0
- package/dist/chunk-56ZIA57D.js +493 -0
- package/dist/chunk-56ZIA57D.js.map +1 -0
- package/dist/chunk-A5PXPWHB.js +2014 -0
- package/dist/chunk-A5PXPWHB.js.map +1 -0
- package/dist/chunk-MVAMPVTA.js +232 -0
- package/dist/chunk-MVAMPVTA.js.map +1 -0
- package/dist/chunk-OIJYNQFZ.js +1163 -0
- package/dist/chunk-OIJYNQFZ.js.map +1 -0
- package/dist/chunk-RE3NOULF.js +3415 -0
- package/dist/chunk-RE3NOULF.js.map +1 -0
- package/dist/{chunk-IYWYSFSF.js → chunk-VJFWTXWN.js} +336 -218
- package/dist/chunk-VJFWTXWN.js.map +1 -0
- package/dist/loop-PVOSF6LR.js +17 -0
- package/dist/main.js +257 -79
- package/dist/main.js.map +1 -1
- package/dist/provider-EWMUPBLH.js +11 -0
- package/dist/provider-EWMUPBLH.js.map +1 -0
- package/dist/{repl-YA23CK5R.js → repl-OO7C6V7M.js} +435 -262
- package/dist/repl-OO7C6V7M.js.map +1 -0
- package/dist/tools-JDTACJYX.js +18 -0
- package/dist/tools-JDTACJYX.js.map +1 -0
- package/package.json +17 -26
- package/dist/chunk-IYWYSFSF.js.map +0 -1
- package/dist/chunk-OHLA2DP7.js +0 -5849
- package/dist/chunk-OHLA2DP7.js.map +0 -1
- package/dist/loop-2MOFLXTG.js +0 -12
- package/dist/repl-YA23CK5R.js.map +0 -1
- /package/dist/{loop-2MOFLXTG.js.map → loop-PVOSF6LR.js.map} +0 -0
|
@@ -0,0 +1,2014 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/agent/agent-tasks.ts
|
|
4
|
+
import { EventEmitter } from "events";
|
|
5
|
+
var MAX_FINISHED_RECORDS = 50;
|
|
6
|
+
var AgentTaskManager = class extends EventEmitter {
|
|
7
|
+
records = /* @__PURE__ */ new Map();
|
|
8
|
+
counter = 0;
|
|
9
|
+
start(run, meta) {
|
|
10
|
+
const id = `agent-${++this.counter}`;
|
|
11
|
+
const rec = {
|
|
12
|
+
id,
|
|
13
|
+
prompt: meta.prompt,
|
|
14
|
+
description: meta.description,
|
|
15
|
+
status: "running",
|
|
16
|
+
startedAt: Date.now(),
|
|
17
|
+
result: "",
|
|
18
|
+
controller: new AbortController(),
|
|
19
|
+
notified: false
|
|
20
|
+
};
|
|
21
|
+
this.records.set(id, rec);
|
|
22
|
+
void run(rec.controller.signal).then((result) => this.finish(rec, "completed", result)).catch(
|
|
23
|
+
(error) => this.finish(rec, "failed", error instanceof Error ? error.message : String(error))
|
|
24
|
+
);
|
|
25
|
+
this.emitUpdate(rec);
|
|
26
|
+
return this.snapshot(rec);
|
|
27
|
+
}
|
|
28
|
+
get(id) {
|
|
29
|
+
const rec = this.records.get(id);
|
|
30
|
+
return rec ? this.snapshot(rec) : void 0;
|
|
31
|
+
}
|
|
32
|
+
list() {
|
|
33
|
+
return [...this.records.values()].map((rec) => this.snapshot(rec));
|
|
34
|
+
}
|
|
35
|
+
runningCount() {
|
|
36
|
+
let count = 0;
|
|
37
|
+
for (const rec of this.records.values()) {
|
|
38
|
+
if (rec.status === "running") count += 1;
|
|
39
|
+
}
|
|
40
|
+
return count;
|
|
41
|
+
}
|
|
42
|
+
kill(id) {
|
|
43
|
+
const rec = this.records.get(id);
|
|
44
|
+
if (!rec || rec.status !== "running") return void 0;
|
|
45
|
+
rec.controller.abort();
|
|
46
|
+
this.finish(rec, "stopped", rec.result || "Stopped by user.");
|
|
47
|
+
return this.snapshot(rec);
|
|
48
|
+
}
|
|
49
|
+
cleanup() {
|
|
50
|
+
const killed = [];
|
|
51
|
+
for (const rec of this.records.values()) {
|
|
52
|
+
if (rec.status === "running") {
|
|
53
|
+
rec.controller.abort();
|
|
54
|
+
this.finish(rec, "stopped", rec.result || "Stopped on shutdown.");
|
|
55
|
+
killed.push(this.snapshot(rec));
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return killed;
|
|
59
|
+
}
|
|
60
|
+
// Finished tasks whose result has not yet been handed to the parent loop.
|
|
61
|
+
drainNotifications() {
|
|
62
|
+
const drained = [];
|
|
63
|
+
for (const rec of this.records.values()) {
|
|
64
|
+
if (rec.status !== "running" && !rec.notified) {
|
|
65
|
+
rec.notified = true;
|
|
66
|
+
drained.push(this.snapshot(rec));
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
this.pruneFinished();
|
|
70
|
+
return drained;
|
|
71
|
+
}
|
|
72
|
+
// Map iteration is insertion-ordered, so the first finished records met are
|
|
73
|
+
// the oldest. Never drop an un-notified record: the parent has not seen its
|
|
74
|
+
// result yet. Running records are untouched.
|
|
75
|
+
pruneFinished() {
|
|
76
|
+
let finished = 0;
|
|
77
|
+
for (const rec of this.records.values()) {
|
|
78
|
+
if (rec.status !== "running") finished += 1;
|
|
79
|
+
}
|
|
80
|
+
let excess = finished - MAX_FINISHED_RECORDS;
|
|
81
|
+
if (excess <= 0) return;
|
|
82
|
+
for (const rec of this.records.values()) {
|
|
83
|
+
if (excess <= 0) return;
|
|
84
|
+
if (rec.status !== "running" && rec.notified) {
|
|
85
|
+
this.records.delete(rec.id);
|
|
86
|
+
excess -= 1;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
finish(rec, status, result) {
|
|
91
|
+
if (rec.status !== "running") return;
|
|
92
|
+
rec.status = status;
|
|
93
|
+
rec.result = result;
|
|
94
|
+
rec.endedAt = Date.now();
|
|
95
|
+
this.emitUpdate(rec);
|
|
96
|
+
}
|
|
97
|
+
emitUpdate(rec) {
|
|
98
|
+
this.emit("update", this.snapshot(rec));
|
|
99
|
+
}
|
|
100
|
+
snapshot(rec) {
|
|
101
|
+
return {
|
|
102
|
+
id: rec.id,
|
|
103
|
+
prompt: rec.prompt,
|
|
104
|
+
description: rec.description,
|
|
105
|
+
status: rec.status,
|
|
106
|
+
startedAt: rec.startedAt,
|
|
107
|
+
endedAt: rec.endedAt,
|
|
108
|
+
result: rec.result
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
var defaultAgentTasks = new AgentTaskManager();
|
|
113
|
+
function formatAgentTaskUpdate(task) {
|
|
114
|
+
const label = task.description ? `: ${task.description}` : "";
|
|
115
|
+
if (task.status === "running") {
|
|
116
|
+
return `Background subagent ${task.id} started${label}`;
|
|
117
|
+
}
|
|
118
|
+
let text = `Background subagent ${task.id} ${task.status}${label}`;
|
|
119
|
+
if (task.status === "failed") {
|
|
120
|
+
const reason = (task.result.split("\n")[0] ?? "").replace(/^Subagent failed:\s*/, "");
|
|
121
|
+
if (reason) {
|
|
122
|
+
text += ` \u2014 ${reason.length > 140 ? `${reason.slice(0, 140)}\u2026` : reason}`;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return text;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// src/core/http-error.ts
|
|
129
|
+
function responseBodyOf(error) {
|
|
130
|
+
const body = error.responseBody;
|
|
131
|
+
return typeof body === "string" && body.length > 0 ? body : void 0;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// src/core/image.ts
|
|
135
|
+
var MAX_IMAGE_DIMENSION = 2e3;
|
|
136
|
+
var OVERSIZED_IMAGE_FALLBACK_BYTES = 5 * 1024 * 1024;
|
|
137
|
+
var IMAGE_REMOVED_PLACEHOLDER = "[image removed: too large for the model]";
|
|
138
|
+
var IMAGE_UNSUPPORTED_PLACEHOLDER = "[image removed: the model cannot read images]";
|
|
139
|
+
var PNG_SIGNATURE = [137, 80, 78, 71, 13, 10, 26, 10];
|
|
140
|
+
function probePng(buf) {
|
|
141
|
+
if (buf.length < 24) return null;
|
|
142
|
+
for (let i = 0; i < PNG_SIGNATURE.length; i++) {
|
|
143
|
+
if (buf[i] !== PNG_SIGNATURE[i]) return null;
|
|
144
|
+
}
|
|
145
|
+
if (buf.toString("ascii", 12, 16) !== "IHDR") return null;
|
|
146
|
+
return { width: buf.readUInt32BE(16), height: buf.readUInt32BE(20) };
|
|
147
|
+
}
|
|
148
|
+
var JPEG_SOF_MARKERS = /* @__PURE__ */ new Set([
|
|
149
|
+
192,
|
|
150
|
+
193,
|
|
151
|
+
194,
|
|
152
|
+
195,
|
|
153
|
+
197,
|
|
154
|
+
198,
|
|
155
|
+
199,
|
|
156
|
+
201,
|
|
157
|
+
202,
|
|
158
|
+
203,
|
|
159
|
+
205,
|
|
160
|
+
206,
|
|
161
|
+
207
|
|
162
|
+
]);
|
|
163
|
+
function probeJpeg(buf) {
|
|
164
|
+
if (buf.length < 4 || buf[0] !== 255 || buf[1] !== 216) return null;
|
|
165
|
+
let offset = 2;
|
|
166
|
+
while (offset + 3 < buf.length) {
|
|
167
|
+
if (buf[offset] !== 255) return null;
|
|
168
|
+
const marker = buf[offset + 1];
|
|
169
|
+
if (marker === void 0) return null;
|
|
170
|
+
if (marker === 216 || marker === 1 || marker >= 208 && marker <= 215) {
|
|
171
|
+
offset += 2;
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
const length = buf.readUInt16BE(offset + 2);
|
|
175
|
+
if (length < 2) return null;
|
|
176
|
+
if (JPEG_SOF_MARKERS.has(marker)) {
|
|
177
|
+
if (offset + 9 > buf.length) return null;
|
|
178
|
+
return { height: buf.readUInt16BE(offset + 5), width: buf.readUInt16BE(offset + 7) };
|
|
179
|
+
}
|
|
180
|
+
offset += 2 + length;
|
|
181
|
+
}
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
function probeGif(buf) {
|
|
185
|
+
if (buf.length < 10) return null;
|
|
186
|
+
const signature = buf.toString("ascii", 0, 6);
|
|
187
|
+
if (signature !== "GIF87a" && signature !== "GIF89a") return null;
|
|
188
|
+
return { width: buf.readUInt16LE(6), height: buf.readUInt16LE(8) };
|
|
189
|
+
}
|
|
190
|
+
function probeWebp(buf) {
|
|
191
|
+
if (buf.length < 30) return null;
|
|
192
|
+
if (buf.toString("ascii", 0, 4) !== "RIFF" || buf.toString("ascii", 8, 12) !== "WEBP") {
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
const chunk = buf.toString("ascii", 12, 16);
|
|
196
|
+
if (chunk === "VP8X") {
|
|
197
|
+
return { width: 1 + buf.readUIntLE(24, 3), height: 1 + buf.readUIntLE(27, 3) };
|
|
198
|
+
}
|
|
199
|
+
if (chunk === "VP8 ") {
|
|
200
|
+
return {
|
|
201
|
+
width: buf.readUInt16LE(26) & 16383,
|
|
202
|
+
height: buf.readUInt16LE(28) & 16383
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
if (chunk === "VP8L") {
|
|
206
|
+
if (buf[20] !== 47) return null;
|
|
207
|
+
const b1 = buf[21];
|
|
208
|
+
const b2 = buf[22];
|
|
209
|
+
const b3 = buf[23];
|
|
210
|
+
const b4 = buf[24];
|
|
211
|
+
return {
|
|
212
|
+
width: 1 + ((b2 & 63) << 8 | b1),
|
|
213
|
+
height: 1 + ((b4 & 15) << 10 | b3 << 2 | (b2 & 192) >> 6)
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
return null;
|
|
217
|
+
}
|
|
218
|
+
function probeImageDimensions(buf) {
|
|
219
|
+
return probePng(buf) ?? probeJpeg(buf) ?? probeGif(buf) ?? probeWebp(buf);
|
|
220
|
+
}
|
|
221
|
+
var IMAGE_SIZE_KEYWORDS = /too[\s-]*(large|big)|exceeds?|dimensions?|resolution|size\s*limit|max(?:imum)?[\s-]*size/i;
|
|
222
|
+
function isOversizedImageError(error) {
|
|
223
|
+
const status = error.statusCode;
|
|
224
|
+
if (status === void 0 || status < 400 || status >= 500) return false;
|
|
225
|
+
const text = [error.message, responseBodyOf(error)].filter((part) => typeof part === "string").join("\n");
|
|
226
|
+
return /image/i.test(text) && IMAGE_SIZE_KEYWORDS.test(text);
|
|
227
|
+
}
|
|
228
|
+
var VISION_UNSUPPORTED_KEYWORDS = /vision\s*encoder|can(?:not|'t) (?:read|process|handle|accept) images?|does(?:\s*not|n't) support (?:image|vision|multimodal)|images?(?:\s*input)? (?:are|is) not supported|not a vision|no vision (?:capability|support|encoder)|multimodal(?:\s*input)? (?:is )?not supported/i;
|
|
229
|
+
function isVisionUnsupportedError(error) {
|
|
230
|
+
const status = error.statusCode;
|
|
231
|
+
if (status === void 0 || status < 400 || status >= 500) return false;
|
|
232
|
+
const text = [error.message, responseBodyOf(error)].filter((part) => typeof part === "string").join("\n");
|
|
233
|
+
return /image|vision|multimodal/i.test(text) && VISION_UNSUPPORTED_KEYWORDS.test(text);
|
|
234
|
+
}
|
|
235
|
+
function imagePartBytes(image) {
|
|
236
|
+
if (typeof image === "string") {
|
|
237
|
+
const base64 = image.startsWith("data:") ? image.slice(image.indexOf(",") + 1) : image;
|
|
238
|
+
try {
|
|
239
|
+
return Buffer.from(base64, "base64");
|
|
240
|
+
} catch {
|
|
241
|
+
return null;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
if (image instanceof Uint8Array) return Buffer.from(image);
|
|
245
|
+
return null;
|
|
246
|
+
}
|
|
247
|
+
function isOversizedImageData(buf) {
|
|
248
|
+
const dims = probeImageDimensions(buf);
|
|
249
|
+
if (dims) return Math.max(dims.width, dims.height) > MAX_IMAGE_DIMENSION;
|
|
250
|
+
return buf.length > OVERSIZED_IMAGE_FALLBACK_BYTES;
|
|
251
|
+
}
|
|
252
|
+
function stripOversizedImages(messages) {
|
|
253
|
+
let removed = 0;
|
|
254
|
+
const result = messages.map((message) => {
|
|
255
|
+
if (!Array.isArray(message.content)) return message;
|
|
256
|
+
let changed = false;
|
|
257
|
+
const content = message.content.map((part) => {
|
|
258
|
+
if (part.type !== "image") return part;
|
|
259
|
+
const bytes = imagePartBytes(part.image);
|
|
260
|
+
if (!bytes || !isOversizedImageData(bytes)) return part;
|
|
261
|
+
changed = true;
|
|
262
|
+
removed++;
|
|
263
|
+
return { type: "text", text: IMAGE_REMOVED_PLACEHOLDER };
|
|
264
|
+
});
|
|
265
|
+
return changed ? { ...message, content } : message;
|
|
266
|
+
});
|
|
267
|
+
return { messages: removed > 0 ? result : messages, removed };
|
|
268
|
+
}
|
|
269
|
+
function stripAllImages(messages) {
|
|
270
|
+
let removed = 0;
|
|
271
|
+
const result = messages.map((message) => {
|
|
272
|
+
if (!Array.isArray(message.content)) return message;
|
|
273
|
+
let changed = false;
|
|
274
|
+
const content = message.content.map((part) => {
|
|
275
|
+
if (part.type !== "image") return part;
|
|
276
|
+
changed = true;
|
|
277
|
+
removed++;
|
|
278
|
+
return { type: "text", text: IMAGE_UNSUPPORTED_PLACEHOLDER };
|
|
279
|
+
});
|
|
280
|
+
return changed ? { ...message, content } : message;
|
|
281
|
+
});
|
|
282
|
+
return { messages: removed > 0 ? result : messages, removed };
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// src/config/paths.ts
|
|
286
|
+
import os from "os";
|
|
287
|
+
import path from "path";
|
|
288
|
+
function starHome() {
|
|
289
|
+
return process.env.STAR_HOME ?? path.join(os.homedir(), ".star-cli");
|
|
290
|
+
}
|
|
291
|
+
function globalConfigPath() {
|
|
292
|
+
return path.join(starHome(), "config.toml");
|
|
293
|
+
}
|
|
294
|
+
function sessionsDir() {
|
|
295
|
+
return path.join(starHome(), "sessions");
|
|
296
|
+
}
|
|
297
|
+
function userMemoryPath() {
|
|
298
|
+
return path.join(starHome(), "MEMORY.md");
|
|
299
|
+
}
|
|
300
|
+
function gitTreesDir() {
|
|
301
|
+
return path.join(starHome(), "git-trees");
|
|
302
|
+
}
|
|
303
|
+
function envFilePath() {
|
|
304
|
+
return path.join(starHome(), ".env");
|
|
305
|
+
}
|
|
306
|
+
function projectConfigPath(cwd) {
|
|
307
|
+
return path.join(cwd, ".star", "config.toml");
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// src/tasks/manager.ts
|
|
311
|
+
import { spawn as spawn2 } from "child_process";
|
|
312
|
+
import { EventEmitter as EventEmitter2 } from "events";
|
|
313
|
+
|
|
314
|
+
// src/tools/bash.ts
|
|
315
|
+
import { spawn } from "child_process";
|
|
316
|
+
import { existsSync } from "fs";
|
|
317
|
+
import path2 from "path";
|
|
318
|
+
import { z } from "zod";
|
|
319
|
+
|
|
320
|
+
// src/config/env.ts
|
|
321
|
+
import fs from "fs";
|
|
322
|
+
var ENV_KEY_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
323
|
+
function parseEnvContent(content) {
|
|
324
|
+
const vars = {};
|
|
325
|
+
for (const rawLine of content.split(/\r?\n/)) {
|
|
326
|
+
const line = rawLine.trim();
|
|
327
|
+
if (!line || line.startsWith("#")) continue;
|
|
328
|
+
const eq = line.indexOf("=");
|
|
329
|
+
if (eq <= 0) continue;
|
|
330
|
+
const key = line.slice(0, eq).trim();
|
|
331
|
+
if (!ENV_KEY_PATTERN.test(key)) continue;
|
|
332
|
+
let value = line.slice(eq + 1).trim();
|
|
333
|
+
if (value.length >= 2 && (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'"))) {
|
|
334
|
+
value = value.slice(1, -1);
|
|
335
|
+
}
|
|
336
|
+
vars[key] = value;
|
|
337
|
+
}
|
|
338
|
+
return vars;
|
|
339
|
+
}
|
|
340
|
+
var envFileKeySet = /* @__PURE__ */ new Set();
|
|
341
|
+
function getEnvFileKeys() {
|
|
342
|
+
return envFileKeySet;
|
|
343
|
+
}
|
|
344
|
+
function loadEnvFile(filePath = envFilePath()) {
|
|
345
|
+
let content;
|
|
346
|
+
try {
|
|
347
|
+
content = fs.readFileSync(filePath, "utf8");
|
|
348
|
+
} catch {
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
for (const [key, value] of Object.entries(parseEnvContent(content))) {
|
|
352
|
+
envFileKeySet.add(key);
|
|
353
|
+
if (process.env[key] === void 0) {
|
|
354
|
+
process.env[key] = value;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
function upsertEnvContent(content, key, value) {
|
|
359
|
+
const entry = `${key}=${value.replace(/[\r\n]/g, "")}`;
|
|
360
|
+
const pattern = new RegExp(`^\\s*${key}\\s*=`);
|
|
361
|
+
const lines = content.split("\n");
|
|
362
|
+
while (lines.length > 0 && (lines[lines.length - 1] ?? "").trim() === "") lines.pop();
|
|
363
|
+
for (let i = 0; i < lines.length; i++) {
|
|
364
|
+
if (pattern.test(lines[i] ?? "")) {
|
|
365
|
+
lines[i] = entry;
|
|
366
|
+
return `${lines.join("\n")}
|
|
367
|
+
`;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
lines.push(entry);
|
|
371
|
+
return `${lines.join("\n")}
|
|
372
|
+
`;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// src/tools/bash.ts
|
|
376
|
+
var MAX_OUTPUT = 3e4;
|
|
377
|
+
var HALF_OUTPUT = MAX_OUTPUT / 2;
|
|
378
|
+
var DEFAULT_TIMEOUT = 120;
|
|
379
|
+
var MAX_TIMEOUT = 600;
|
|
380
|
+
function findOnPath(exe, exclude) {
|
|
381
|
+
for (const dir of (process.env.PATH ?? "").split(path2.delimiter)) {
|
|
382
|
+
if (!dir || exclude?.(dir)) {
|
|
383
|
+
continue;
|
|
384
|
+
}
|
|
385
|
+
const full = path2.join(dir, exe);
|
|
386
|
+
if (existsSync(full)) {
|
|
387
|
+
return full;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
return null;
|
|
391
|
+
}
|
|
392
|
+
function resolveShellUncached() {
|
|
393
|
+
if (process.platform !== "win32") {
|
|
394
|
+
return { shell: "sh", wrap: (c) => ["-c", c], label: "sh" };
|
|
395
|
+
}
|
|
396
|
+
const isWslStub = (dir) => /\\(system32|windowsapps)\\?$/i.test(dir.trim());
|
|
397
|
+
const fromPath = findOnPath("bash.exe", isWslStub);
|
|
398
|
+
if (fromPath) {
|
|
399
|
+
return { shell: fromPath, wrap: (c) => ["-c", c], label: "bash" };
|
|
400
|
+
}
|
|
401
|
+
const gitExe = findOnPath("git.exe");
|
|
402
|
+
if (gitExe) {
|
|
403
|
+
const sibling = path2.join(path2.dirname(path2.dirname(gitExe)), "bin", "bash.exe");
|
|
404
|
+
if (existsSync(sibling)) {
|
|
405
|
+
return { shell: sibling, wrap: (c) => ["-c", c], label: "bash" };
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
const seen = /* @__PURE__ */ new Set();
|
|
409
|
+
for (const root of [
|
|
410
|
+
process.env.ProgramFiles,
|
|
411
|
+
process.env["ProgramFiles(x86)"],
|
|
412
|
+
process.env.ProgramW6432
|
|
413
|
+
]) {
|
|
414
|
+
if (!root) continue;
|
|
415
|
+
const candidate = path2.join(root, "Git", "bin", "bash.exe");
|
|
416
|
+
const key = candidate.toLowerCase();
|
|
417
|
+
if (seen.has(key)) continue;
|
|
418
|
+
seen.add(key);
|
|
419
|
+
if (existsSync(candidate)) {
|
|
420
|
+
return { shell: candidate, wrap: (c) => ["-c", c], label: "bash" };
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
const comspec = process.env.ComSpec ?? "cmd.exe";
|
|
424
|
+
return { shell: comspec, wrap: (c) => ["/d", "/s", "/c", c], label: "cmd" };
|
|
425
|
+
}
|
|
426
|
+
var cachedSpec = null;
|
|
427
|
+
function resolveShell() {
|
|
428
|
+
if (!cachedSpec) {
|
|
429
|
+
cachedSpec = resolveShellUncached();
|
|
430
|
+
}
|
|
431
|
+
return cachedSpec;
|
|
432
|
+
}
|
|
433
|
+
var schema = z.object({
|
|
434
|
+
command: z.string().describe("Shell command to execute"),
|
|
435
|
+
timeout: z.number().positive().max(MAX_TIMEOUT).optional().describe("Timeout in seconds (default 120, max 600)"),
|
|
436
|
+
description: z.string().optional().describe("Short description of what the command does"),
|
|
437
|
+
run_in_background: z.boolean().optional().describe(
|
|
438
|
+
"Run the command in the background and return a task id immediately (default false). Use task_output/task_kill to inspect or stop it."
|
|
439
|
+
)
|
|
440
|
+
});
|
|
441
|
+
function killTree(child) {
|
|
442
|
+
if (process.platform === "win32") {
|
|
443
|
+
if (child.pid) {
|
|
444
|
+
spawn("taskkill", ["/pid", String(child.pid), "/t", "/f"], {
|
|
445
|
+
windowsHide: true,
|
|
446
|
+
stdio: "ignore"
|
|
447
|
+
}).unref();
|
|
448
|
+
}
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
if (child.pid) {
|
|
452
|
+
try {
|
|
453
|
+
process.kill(-child.pid, "SIGKILL");
|
|
454
|
+
return;
|
|
455
|
+
} catch {
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
child.kill("SIGKILL");
|
|
459
|
+
}
|
|
460
|
+
var BoundedOutput = class {
|
|
461
|
+
head = Buffer.alloc(0);
|
|
462
|
+
tail = Buffer.alloc(0);
|
|
463
|
+
dropped = 0;
|
|
464
|
+
overflow = false;
|
|
465
|
+
append(chunk) {
|
|
466
|
+
if (!this.overflow) {
|
|
467
|
+
this.head = this.head.length === 0 ? chunk : Buffer.concat([this.head, chunk]);
|
|
468
|
+
if (this.head.length <= MAX_OUTPUT) {
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
this.overflow = true;
|
|
472
|
+
this.tail = this.head.subarray(HALF_OUTPUT);
|
|
473
|
+
this.head = this.head.subarray(0, HALF_OUTPUT);
|
|
474
|
+
if (this.tail.length > HALF_OUTPUT) {
|
|
475
|
+
this.dropped = this.tail.length - HALF_OUTPUT;
|
|
476
|
+
this.tail = this.tail.subarray(this.tail.length - HALF_OUTPUT);
|
|
477
|
+
}
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
this.tail = this.tail.length === 0 ? chunk : Buffer.concat([this.tail, chunk]);
|
|
481
|
+
if (this.tail.length > HALF_OUTPUT) {
|
|
482
|
+
this.dropped += this.tail.length - HALF_OUTPUT;
|
|
483
|
+
this.tail = this.tail.subarray(this.tail.length - HALF_OUTPUT);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
toString() {
|
|
487
|
+
if (!this.overflow) {
|
|
488
|
+
return decodeOutput(this.head);
|
|
489
|
+
}
|
|
490
|
+
const head = decodeOutput(trimUtf8End(this.head));
|
|
491
|
+
const tail = decodeOutput(trimUtf8Start(this.tail));
|
|
492
|
+
return `${head}
|
|
493
|
+
... [${this.dropped} bytes truncated] ...
|
|
494
|
+
${tail}`;
|
|
495
|
+
}
|
|
496
|
+
};
|
|
497
|
+
function trimUtf8End(buf) {
|
|
498
|
+
let cont = 0;
|
|
499
|
+
let i = buf.length;
|
|
500
|
+
while (i > 0 && cont < 3 && ((buf[i - 1] ?? 0) & 192) === 128) {
|
|
501
|
+
i--;
|
|
502
|
+
cont++;
|
|
503
|
+
}
|
|
504
|
+
if (i === 0) return buf;
|
|
505
|
+
const lead = buf[i - 1] ?? 0;
|
|
506
|
+
if (lead < 128 || (lead & 192) === 128) return buf;
|
|
507
|
+
const needed = lead < 224 ? 1 : lead < 240 ? 2 : 3;
|
|
508
|
+
return cont < needed ? buf.subarray(0, i - 1) : buf;
|
|
509
|
+
}
|
|
510
|
+
function trimUtf8Start(buf) {
|
|
511
|
+
let i = 0;
|
|
512
|
+
while (i < buf.length && i < 3 && ((buf[i] ?? 0) & 192) === 128) i++;
|
|
513
|
+
return i === 0 ? buf : buf.subarray(i);
|
|
514
|
+
}
|
|
515
|
+
function decodeOutput(buf, platform = process.platform) {
|
|
516
|
+
const text = buf.toString("utf8");
|
|
517
|
+
if (platform === "win32" && text.includes("\uFFFD")) {
|
|
518
|
+
try {
|
|
519
|
+
return new TextDecoder("gbk").decode(buf);
|
|
520
|
+
} catch {
|
|
521
|
+
return text;
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
return text;
|
|
525
|
+
}
|
|
526
|
+
function childEnv() {
|
|
527
|
+
const env = { ...process.env };
|
|
528
|
+
for (const key of getEnvFileKeys()) {
|
|
529
|
+
delete env[key];
|
|
530
|
+
}
|
|
531
|
+
return env;
|
|
532
|
+
}
|
|
533
|
+
var bashTool = {
|
|
534
|
+
name: "bash",
|
|
535
|
+
description: "Execute a shell command (Git Bash on Windows when available, otherwise cmd; sh elsewhere). Stdout and stderr are merged. Output is truncated to 30000 bytes. Set run_in_background for long-running commands; it returns a task id for task_list/task_output/task_kill.",
|
|
536
|
+
permission: "exec",
|
|
537
|
+
parameters: schema,
|
|
538
|
+
execute(args, ctx) {
|
|
539
|
+
if (args.run_in_background) {
|
|
540
|
+
const task = defaultTaskManager.start({
|
|
541
|
+
command: args.command,
|
|
542
|
+
description: args.description,
|
|
543
|
+
cwd: ctx.cwd,
|
|
544
|
+
timeoutSeconds: args.timeout,
|
|
545
|
+
ownerId: ctx.agentId ?? "root",
|
|
546
|
+
env: childEnv()
|
|
547
|
+
});
|
|
548
|
+
return Promise.resolve({
|
|
549
|
+
content: `Background task started: ${task.id}
|
|
550
|
+
command: ${args.command}
|
|
551
|
+
description: ${args.description ?? "(none)"}`
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
const timeoutSeconds = Math.min(args.timeout ?? DEFAULT_TIMEOUT, MAX_TIMEOUT);
|
|
555
|
+
const spec = resolveShell();
|
|
556
|
+
return new Promise((resolve) => {
|
|
557
|
+
const child = spawn(spec.shell, spec.wrap(args.command), {
|
|
558
|
+
cwd: ctx.cwd,
|
|
559
|
+
env: childEnv(),
|
|
560
|
+
windowsHide: true,
|
|
561
|
+
detached: process.platform !== "win32",
|
|
562
|
+
// No stdin: a command that reads it (cat, read, input()) must get
|
|
563
|
+
// EOF immediately instead of hanging until the timeout.
|
|
564
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
565
|
+
});
|
|
566
|
+
const output = new BoundedOutput();
|
|
567
|
+
let settled = false;
|
|
568
|
+
const finish = (result) => {
|
|
569
|
+
if (settled) {
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
572
|
+
settled = true;
|
|
573
|
+
clearTimeout(timer);
|
|
574
|
+
ctx.abortSignal?.removeEventListener("abort", onAbort);
|
|
575
|
+
resolve(result);
|
|
576
|
+
};
|
|
577
|
+
const onAbort = () => {
|
|
578
|
+
killTree(child);
|
|
579
|
+
finish({ content: `${output}
|
|
580
|
+
Command aborted`, isError: true });
|
|
581
|
+
};
|
|
582
|
+
const timer = setTimeout(() => {
|
|
583
|
+
killTree(child);
|
|
584
|
+
finish({
|
|
585
|
+
content: `${output}
|
|
586
|
+
Command timed out after ${timeoutSeconds}s`,
|
|
587
|
+
isError: true
|
|
588
|
+
});
|
|
589
|
+
}, timeoutSeconds * 1e3);
|
|
590
|
+
timer.unref();
|
|
591
|
+
child.stdout.on("data", (d) => output.append(d));
|
|
592
|
+
child.stderr.on("data", (d) => output.append(d));
|
|
593
|
+
child.on("error", (err) => {
|
|
594
|
+
finish({ content: `Failed to start shell '${spec.label}': ${err.message}`, isError: true });
|
|
595
|
+
});
|
|
596
|
+
ctx.abortSignal?.addEventListener("abort", onAbort);
|
|
597
|
+
child.on("close", (code) => {
|
|
598
|
+
const trimmed = output.toString().replace(/\s+$/, "");
|
|
599
|
+
if (code === 0) {
|
|
600
|
+
finish({ content: trimmed || "(no output)" });
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
603
|
+
finish({
|
|
604
|
+
content: `${trimmed}${trimmed ? "\n" : ""}Exit code: ${code ?? "unknown"}`,
|
|
605
|
+
isError: true
|
|
606
|
+
});
|
|
607
|
+
});
|
|
608
|
+
});
|
|
609
|
+
}
|
|
610
|
+
};
|
|
611
|
+
|
|
612
|
+
// src/tasks/manager.ts
|
|
613
|
+
var MAX_TASK_OUTPUT = 1e5;
|
|
614
|
+
var DEFAULT_TASK_TIMEOUT = 3600;
|
|
615
|
+
var MAX_FINISHED_RECORDS2 = 50;
|
|
616
|
+
var TRUNCATED_PREFIX = "[... earlier output truncated ...]\n";
|
|
617
|
+
var TaskManager = class extends EventEmitter2 {
|
|
618
|
+
records = /* @__PURE__ */ new Map();
|
|
619
|
+
seq = 0;
|
|
620
|
+
start(opts) {
|
|
621
|
+
const id = `task-${++this.seq}`;
|
|
622
|
+
const spec = resolveShell();
|
|
623
|
+
const child = spawn2(spec.shell, spec.wrap(opts.command), {
|
|
624
|
+
cwd: opts.cwd,
|
|
625
|
+
env: opts.env ?? childEnv(),
|
|
626
|
+
windowsHide: true,
|
|
627
|
+
// No stdin: background commands that read it would hang until the
|
|
628
|
+
// (much longer) task timeout; give them EOF immediately.
|
|
629
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
630
|
+
// POSIX: the child leads its own process group so killTree can SIGKILL
|
|
631
|
+
// the whole tree with a negative pid (Windows uses taskkill /t).
|
|
632
|
+
detached: process.platform !== "win32"
|
|
633
|
+
});
|
|
634
|
+
const rec = {
|
|
635
|
+
id,
|
|
636
|
+
ownerId: opts.ownerId ?? "root",
|
|
637
|
+
command: opts.command,
|
|
638
|
+
description: opts.description,
|
|
639
|
+
status: "running",
|
|
640
|
+
startedAt: Date.now(),
|
|
641
|
+
outputChunks: [],
|
|
642
|
+
outputBytes: 0,
|
|
643
|
+
truncated: false,
|
|
644
|
+
child
|
|
645
|
+
};
|
|
646
|
+
this.records.set(id, rec);
|
|
647
|
+
const append = (d) => {
|
|
648
|
+
rec.outputChunks.push(d);
|
|
649
|
+
rec.outputBytes += d.length;
|
|
650
|
+
if (rec.outputBytes > MAX_TASK_OUTPUT) {
|
|
651
|
+
const merged = Buffer.concat(rec.outputChunks, rec.outputBytes);
|
|
652
|
+
const kept = trimUtf8Start(merged.subarray(merged.length - MAX_TASK_OUTPUT));
|
|
653
|
+
rec.outputChunks = [kept];
|
|
654
|
+
rec.outputBytes = kept.length;
|
|
655
|
+
rec.truncated = true;
|
|
656
|
+
}
|
|
657
|
+
};
|
|
658
|
+
child.stdout.on("data", append);
|
|
659
|
+
child.stderr.on("data", append);
|
|
660
|
+
child.on("error", (err) => {
|
|
661
|
+
append(Buffer.from(`Failed to start shell '${spec.label}': ${err.message}
|
|
662
|
+
`));
|
|
663
|
+
if (rec.status === "running") {
|
|
664
|
+
this.finish(rec, "failed", null);
|
|
665
|
+
}
|
|
666
|
+
});
|
|
667
|
+
child.on("close", (code) => {
|
|
668
|
+
if (rec.status !== "running") return;
|
|
669
|
+
this.finish(rec, code === 0 ? "completed" : "failed", code);
|
|
670
|
+
});
|
|
671
|
+
const timeoutSeconds = opts.timeoutSeconds ?? DEFAULT_TASK_TIMEOUT;
|
|
672
|
+
rec.timer = setTimeout(() => this.terminate(rec, "timed_out"), timeoutSeconds * 1e3);
|
|
673
|
+
rec.timer.unref();
|
|
674
|
+
this.emitUpdate(rec);
|
|
675
|
+
return this.snapshot(rec);
|
|
676
|
+
}
|
|
677
|
+
get(id) {
|
|
678
|
+
const rec = this.records.get(id);
|
|
679
|
+
return rec ? this.snapshot(rec) : void 0;
|
|
680
|
+
}
|
|
681
|
+
list() {
|
|
682
|
+
return [...this.records.values()].map((rec) => this.snapshot(rec));
|
|
683
|
+
}
|
|
684
|
+
runningCount() {
|
|
685
|
+
let count = 0;
|
|
686
|
+
for (const rec of this.records.values()) {
|
|
687
|
+
if (rec.status === "running") count += 1;
|
|
688
|
+
}
|
|
689
|
+
return count;
|
|
690
|
+
}
|
|
691
|
+
kill(id) {
|
|
692
|
+
const rec = this.records.get(id);
|
|
693
|
+
if (!rec || rec.status !== "running") return void 0;
|
|
694
|
+
this.terminate(rec, "stopped");
|
|
695
|
+
return this.snapshot(rec);
|
|
696
|
+
}
|
|
697
|
+
cleanup() {
|
|
698
|
+
const killed = [];
|
|
699
|
+
for (const rec of this.records.values()) {
|
|
700
|
+
if (rec.status === "running") {
|
|
701
|
+
this.terminate(rec, "stopped");
|
|
702
|
+
killed.push(this.snapshot(rec));
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
return killed;
|
|
706
|
+
}
|
|
707
|
+
terminate(rec, status) {
|
|
708
|
+
killTree(rec.child);
|
|
709
|
+
this.finish(rec, status, null);
|
|
710
|
+
}
|
|
711
|
+
finish(rec, status, exitCode) {
|
|
712
|
+
if (rec.timer) clearTimeout(rec.timer);
|
|
713
|
+
rec.status = status;
|
|
714
|
+
rec.exitCode = exitCode;
|
|
715
|
+
rec.endedAt = Date.now();
|
|
716
|
+
this.pruneFinished();
|
|
717
|
+
this.emitUpdate(rec);
|
|
718
|
+
}
|
|
719
|
+
pruneFinished() {
|
|
720
|
+
const finished = [...this.records.values()].filter((rec) => rec.status !== "running");
|
|
721
|
+
if (finished.length <= MAX_FINISHED_RECORDS2) return;
|
|
722
|
+
finished.sort((a, b) => (a.endedAt ?? 0) - (b.endedAt ?? 0));
|
|
723
|
+
for (const rec of finished.slice(0, finished.length - MAX_FINISHED_RECORDS2)) {
|
|
724
|
+
this.records.delete(rec.id);
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
emitUpdate(rec) {
|
|
728
|
+
this.emit("update", this.snapshot(rec));
|
|
729
|
+
}
|
|
730
|
+
snapshot(rec) {
|
|
731
|
+
const output = decodeOutput(Buffer.concat(rec.outputChunks, rec.outputBytes));
|
|
732
|
+
return {
|
|
733
|
+
id: rec.id,
|
|
734
|
+
ownerId: rec.ownerId,
|
|
735
|
+
command: rec.command,
|
|
736
|
+
description: rec.description,
|
|
737
|
+
status: rec.status,
|
|
738
|
+
startedAt: rec.startedAt,
|
|
739
|
+
endedAt: rec.endedAt,
|
|
740
|
+
exitCode: rec.exitCode,
|
|
741
|
+
output: rec.truncated ? TRUNCATED_PREFIX + output : output
|
|
742
|
+
};
|
|
743
|
+
}
|
|
744
|
+
};
|
|
745
|
+
var defaultTaskManager = new TaskManager();
|
|
746
|
+
|
|
747
|
+
// src/tools/fs/snapshots.ts
|
|
748
|
+
import { randomUUID } from "crypto";
|
|
749
|
+
import { mkdir, readFile, rm, stat, writeFile } from "fs/promises";
|
|
750
|
+
import os2 from "os";
|
|
751
|
+
import path3 from "path";
|
|
752
|
+
var MAX_SNAPSHOTS = 50;
|
|
753
|
+
var MAX_SNAPSHOT_CONTENT_BYTES = 5 * 1024 * 1024;
|
|
754
|
+
var stack = [];
|
|
755
|
+
var activeTurn = 0;
|
|
756
|
+
var activeTurnMessageIndex = -1;
|
|
757
|
+
var checkpointSeq = 0;
|
|
758
|
+
var hooks = null;
|
|
759
|
+
var spillDir = path3.join(
|
|
760
|
+
os2.tmpdir(),
|
|
761
|
+
`star-cli-snapshots-${process.pid}-${randomUUID().slice(0, 8)}`
|
|
762
|
+
);
|
|
763
|
+
function isSpillPath(p) {
|
|
764
|
+
return p.startsWith(spillDir + path3.sep);
|
|
765
|
+
}
|
|
766
|
+
async function spillContent(snapshot) {
|
|
767
|
+
if (snapshot.content === null || snapshot.contentFile) return;
|
|
768
|
+
try {
|
|
769
|
+
const file = path3.join(spillDir, `${snapshot.id}.snap`);
|
|
770
|
+
await mkdir(spillDir, { recursive: true, mode: 448 });
|
|
771
|
+
await writeFile(file, snapshot.content, { mode: 384 });
|
|
772
|
+
snapshot.contentFile = file;
|
|
773
|
+
snapshot.content = null;
|
|
774
|
+
} catch {
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
async function discardSpill(snapshot) {
|
|
778
|
+
if (snapshot.contentFile && isSpillPath(snapshot.contentFile)) {
|
|
779
|
+
await rm(snapshot.contentFile, { force: true }).catch(() => {
|
|
780
|
+
});
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
function setSnapshotHooks(next) {
|
|
784
|
+
hooks = next;
|
|
785
|
+
}
|
|
786
|
+
function nextSnapshotId() {
|
|
787
|
+
return ++checkpointSeq;
|
|
788
|
+
}
|
|
789
|
+
function beginTurn(messageIndex = -1) {
|
|
790
|
+
activeTurnMessageIndex = messageIndex;
|
|
791
|
+
return ++activeTurn;
|
|
792
|
+
}
|
|
793
|
+
function currentTurnSeq() {
|
|
794
|
+
return activeTurn;
|
|
795
|
+
}
|
|
796
|
+
function currentTurnMessageIndex() {
|
|
797
|
+
return activeTurnMessageIndex;
|
|
798
|
+
}
|
|
799
|
+
function ownerOf(snapshot) {
|
|
800
|
+
return snapshot.owner ?? "root";
|
|
801
|
+
}
|
|
802
|
+
async function captureSnapshot(filePath, toolName, snapshotContext) {
|
|
803
|
+
let content = null;
|
|
804
|
+
let existed = false;
|
|
805
|
+
let contentTooLarge = false;
|
|
806
|
+
const st = await stat(filePath).catch(() => null);
|
|
807
|
+
if (st?.isFile()) {
|
|
808
|
+
existed = true;
|
|
809
|
+
if (st.size > MAX_SNAPSHOT_CONTENT_BYTES) {
|
|
810
|
+
contentTooLarge = true;
|
|
811
|
+
} else {
|
|
812
|
+
try {
|
|
813
|
+
content = await readFile(filePath);
|
|
814
|
+
} catch {
|
|
815
|
+
existed = false;
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
return {
|
|
820
|
+
id: nextSnapshotId(),
|
|
821
|
+
path: filePath,
|
|
822
|
+
existed,
|
|
823
|
+
content,
|
|
824
|
+
contentTooLarge: contentTooLarge || void 0,
|
|
825
|
+
toolName,
|
|
826
|
+
timestamp: Date.now(),
|
|
827
|
+
turn: snapshotContext?.turn ?? activeTurn,
|
|
828
|
+
messageIndex: snapshotContext?.messageIndex ?? activeTurnMessageIndex,
|
|
829
|
+
owner: snapshotContext?.owner ?? "root"
|
|
830
|
+
};
|
|
831
|
+
}
|
|
832
|
+
async function pushSnapshot(snapshot) {
|
|
833
|
+
stack.push(snapshot);
|
|
834
|
+
try {
|
|
835
|
+
await hooks?.onPush?.(snapshot);
|
|
836
|
+
} catch (err) {
|
|
837
|
+
if (process.env.STAR_DEBUG) {
|
|
838
|
+
console.error("[star] failed to persist snapshot checkpoint:", err);
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
await spillContent(snapshot);
|
|
842
|
+
if (stack.length > MAX_SNAPSHOTS) {
|
|
843
|
+
const evicted = stack.splice(0, stack.length - MAX_SNAPSHOTS);
|
|
844
|
+
for (const old of evicted) await discardSpill(old);
|
|
845
|
+
await removeFromStore(evicted.map((s) => s.id));
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
async function removeFromStore(ids) {
|
|
849
|
+
if (ids.length > 0) await hooks?.onRemove?.(ids);
|
|
850
|
+
}
|
|
851
|
+
function listSnapshots() {
|
|
852
|
+
return [...stack];
|
|
853
|
+
}
|
|
854
|
+
function clearSnapshots() {
|
|
855
|
+
for (const snapshot of stack) void discardSpill(snapshot);
|
|
856
|
+
stack.length = 0;
|
|
857
|
+
}
|
|
858
|
+
function hydrateSnapshots(snapshots) {
|
|
859
|
+
for (const snapshot of stack) void discardSpill(snapshot);
|
|
860
|
+
stack.length = 0;
|
|
861
|
+
stack.push(...snapshots.slice(-MAX_SNAPSHOTS));
|
|
862
|
+
for (const snapshot of stack) {
|
|
863
|
+
if (snapshot.id > checkpointSeq) checkpointSeq = snapshot.id;
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
async function resolveContent(snapshot) {
|
|
867
|
+
if (snapshot.content !== null) return snapshot.content;
|
|
868
|
+
if (!snapshot.contentFile) return null;
|
|
869
|
+
try {
|
|
870
|
+
return await readFile(snapshot.contentFile);
|
|
871
|
+
} catch {
|
|
872
|
+
return null;
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
async function revert(snapshot) {
|
|
876
|
+
if (snapshot.existed) {
|
|
877
|
+
if (snapshot.contentTooLarge) {
|
|
878
|
+
return `Skipped ${snapshot.path}: content too large, not restored.`;
|
|
879
|
+
}
|
|
880
|
+
const content = await resolveContent(snapshot);
|
|
881
|
+
if (content === null) {
|
|
882
|
+
return `Skipped ${snapshot.path}: the snapshot content is no longer available.`;
|
|
883
|
+
}
|
|
884
|
+
await mkdir(path3.dirname(snapshot.path), { recursive: true });
|
|
885
|
+
await writeFile(snapshot.path, content);
|
|
886
|
+
return `Restored ${snapshot.path} to its state before ${snapshot.toolName}.`;
|
|
887
|
+
}
|
|
888
|
+
await rm(snapshot.path, { force: true });
|
|
889
|
+
return `Deleted ${snapshot.path} (it did not exist before ${snapshot.toolName}).`;
|
|
890
|
+
}
|
|
891
|
+
function listTurnSnapshots(turn, includeSubagent = false) {
|
|
892
|
+
return stack.filter(
|
|
893
|
+
(snapshot) => snapshot.turn === turn && (includeSubagent || ownerOf(snapshot) === "root")
|
|
894
|
+
).reverse();
|
|
895
|
+
}
|
|
896
|
+
async function resolveSnapshotContent(snapshot) {
|
|
897
|
+
const content = await resolveContent(snapshot);
|
|
898
|
+
if (content === null) return null;
|
|
899
|
+
return typeof content === "string" ? content : content.toString("utf8");
|
|
900
|
+
}
|
|
901
|
+
async function undoTurnSnapshots(turn, includeSubagent = false) {
|
|
902
|
+
const matches = [];
|
|
903
|
+
for (let i = stack.length - 1; i >= 0; i--) {
|
|
904
|
+
const snapshot = stack[i];
|
|
905
|
+
if (snapshot && snapshot.turn === turn && (includeSubagent || ownerOf(snapshot) === "root")) {
|
|
906
|
+
matches.push(snapshot);
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
const messages = [];
|
|
910
|
+
const revertedIds = [];
|
|
911
|
+
for (const snapshot of matches) {
|
|
912
|
+
try {
|
|
913
|
+
messages.push(await revert(snapshot));
|
|
914
|
+
const idx = stack.indexOf(snapshot);
|
|
915
|
+
if (idx >= 0) stack.splice(idx, 1);
|
|
916
|
+
revertedIds.push(snapshot.id);
|
|
917
|
+
await discardSpill(snapshot);
|
|
918
|
+
} catch (err) {
|
|
919
|
+
messages.push(
|
|
920
|
+
`Failed to revert ${snapshot.path}: ${err.message} (checkpoint kept)`
|
|
921
|
+
);
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
await removeFromStore(revertedIds);
|
|
925
|
+
return messages;
|
|
926
|
+
}
|
|
927
|
+
async function dropTurnSnapshots(turn, includeSubagent = false) {
|
|
928
|
+
const dropped = [];
|
|
929
|
+
const droppedSnapshots = [];
|
|
930
|
+
for (let i = stack.length - 1; i >= 0; i--) {
|
|
931
|
+
const snapshot = stack[i];
|
|
932
|
+
if (snapshot && snapshot.turn === turn && (includeSubagent || ownerOf(snapshot) === "root")) {
|
|
933
|
+
dropped.push(snapshot.id);
|
|
934
|
+
droppedSnapshots.push(snapshot);
|
|
935
|
+
stack.splice(i, 1);
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
for (const snapshot of droppedSnapshots) await discardSpill(snapshot);
|
|
939
|
+
await removeFromStore(dropped);
|
|
940
|
+
return dropped.length;
|
|
941
|
+
}
|
|
942
|
+
async function rewindToSnapshot(id, includeSubagent = false) {
|
|
943
|
+
if (!stack.some((snapshot) => snapshot.id === id)) return null;
|
|
944
|
+
const matches = [];
|
|
945
|
+
for (let i = stack.length - 1; i >= 0; i--) {
|
|
946
|
+
const snapshot = stack[i];
|
|
947
|
+
if (snapshot && snapshot.id >= id && (includeSubagent || ownerOf(snapshot) === "root")) {
|
|
948
|
+
matches.push(snapshot);
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
const reverted = [];
|
|
952
|
+
const revertedIds = [];
|
|
953
|
+
let messageIndex = null;
|
|
954
|
+
for (const snapshot of matches) {
|
|
955
|
+
try {
|
|
956
|
+
reverted.push(await revert(snapshot));
|
|
957
|
+
const idx = stack.indexOf(snapshot);
|
|
958
|
+
if (idx >= 0) stack.splice(idx, 1);
|
|
959
|
+
revertedIds.push(snapshot.id);
|
|
960
|
+
await discardSpill(snapshot);
|
|
961
|
+
if (snapshot.messageIndex >= 0) {
|
|
962
|
+
messageIndex = messageIndex === null ? snapshot.messageIndex : Math.min(messageIndex, snapshot.messageIndex);
|
|
963
|
+
}
|
|
964
|
+
} catch (err) {
|
|
965
|
+
reverted.push(
|
|
966
|
+
`Failed to revert ${snapshot.path}: ${err.message} (checkpoint kept)`
|
|
967
|
+
);
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
await removeFromStore(revertedIds);
|
|
971
|
+
return { reverted, messageIndex };
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
// src/tools/todo.ts
|
|
975
|
+
import { mkdir as mkdir2, readFile as readFile2, rename, rm as rm2, writeFile as writeFile2 } from "fs/promises";
|
|
976
|
+
import path4 from "path";
|
|
977
|
+
import { z as z2 } from "zod";
|
|
978
|
+
var todoItemSchema = z2.object({
|
|
979
|
+
id: z2.number().int(),
|
|
980
|
+
title: z2.string(),
|
|
981
|
+
status: z2.enum(["pending", "in_progress", "done"])
|
|
982
|
+
});
|
|
983
|
+
var fileSchema = z2.object({
|
|
984
|
+
sessionId: z2.string().nullable(),
|
|
985
|
+
items: z2.array(todoItemSchema)
|
|
986
|
+
});
|
|
987
|
+
var writeSchema = z2.object({
|
|
988
|
+
todos: z2.array(todoItemSchema).describe("The full todo list, replacing the current one")
|
|
989
|
+
});
|
|
990
|
+
var readSchema = z2.object({});
|
|
991
|
+
function fileFor(cwd) {
|
|
992
|
+
return path4.join(cwd, ".star", "todos.json");
|
|
993
|
+
}
|
|
994
|
+
var persistGuard = () => true;
|
|
995
|
+
function setTodoPersistGuard(guard) {
|
|
996
|
+
persistGuard = guard;
|
|
997
|
+
}
|
|
998
|
+
var boundSessionId = null;
|
|
999
|
+
function setTodoSession(id) {
|
|
1000
|
+
boundSessionId = id;
|
|
1001
|
+
}
|
|
1002
|
+
var TodoStore = class {
|
|
1003
|
+
items = /* @__PURE__ */ new Map();
|
|
1004
|
+
list() {
|
|
1005
|
+
return [...this.items.values()];
|
|
1006
|
+
}
|
|
1007
|
+
replace(items) {
|
|
1008
|
+
this.items.clear();
|
|
1009
|
+
for (const item of items) {
|
|
1010
|
+
this.items.set(item.id, item);
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
async load(cwd, sessionId) {
|
|
1014
|
+
let raw;
|
|
1015
|
+
try {
|
|
1016
|
+
raw = await readFile2(fileFor(cwd), "utf8");
|
|
1017
|
+
} catch {
|
|
1018
|
+
return;
|
|
1019
|
+
}
|
|
1020
|
+
try {
|
|
1021
|
+
const parsed = JSON.parse(raw);
|
|
1022
|
+
if (Array.isArray(parsed)) {
|
|
1023
|
+
this.replace(parsed.filter((it) => todoItemSchema.safeParse(it).success));
|
|
1024
|
+
return;
|
|
1025
|
+
}
|
|
1026
|
+
const file = fileSchema.safeParse(parsed);
|
|
1027
|
+
if (!file.success) {
|
|
1028
|
+
return;
|
|
1029
|
+
}
|
|
1030
|
+
if (file.data.sessionId !== (sessionId ?? null)) {
|
|
1031
|
+
this.replace([]);
|
|
1032
|
+
return;
|
|
1033
|
+
}
|
|
1034
|
+
this.replace(file.data.items);
|
|
1035
|
+
} catch {
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
// Atomic write (tmp + rename): a crash or failed save can never leave a
|
|
1039
|
+
// truncated todos.json behind. An empty or fully completed list has no
|
|
1040
|
+
// resume value — rehydrating it would only restore finished work — so the
|
|
1041
|
+
// file is deleted instead of persisted.
|
|
1042
|
+
async save(cwd) {
|
|
1043
|
+
if (!persistGuard()) {
|
|
1044
|
+
return;
|
|
1045
|
+
}
|
|
1046
|
+
const items = this.list();
|
|
1047
|
+
const file = fileFor(cwd);
|
|
1048
|
+
if (items.length === 0 || items.every((it) => it.status === "done")) {
|
|
1049
|
+
await rm2(file, { force: true });
|
|
1050
|
+
return;
|
|
1051
|
+
}
|
|
1052
|
+
await mkdir2(path4.join(cwd, ".star"), { recursive: true });
|
|
1053
|
+
const tmp = `${file}.tmp`;
|
|
1054
|
+
await writeFile2(tmp, `${JSON.stringify({ sessionId: boundSessionId, items }, null, 2)}
|
|
1055
|
+
`);
|
|
1056
|
+
await rename(tmp, file);
|
|
1057
|
+
}
|
|
1058
|
+
};
|
|
1059
|
+
var SYMBOLS = {
|
|
1060
|
+
pending: "[ ]",
|
|
1061
|
+
in_progress: "[~]",
|
|
1062
|
+
done: "[x]"
|
|
1063
|
+
};
|
|
1064
|
+
function formatTodos(items) {
|
|
1065
|
+
if (items.length === 0) {
|
|
1066
|
+
return "No todos.";
|
|
1067
|
+
}
|
|
1068
|
+
const lines = items.map((it) => `${SYMBOLS[it.status]} ${it.id}. ${it.title}`);
|
|
1069
|
+
const width = Math.max(...lines.map((l) => l.length));
|
|
1070
|
+
return lines.map((line, i) => {
|
|
1071
|
+
const status = items[i]?.status;
|
|
1072
|
+
return status === "pending" ? line : `${line.padEnd(width + 4)}(${status})`;
|
|
1073
|
+
}).join("\n");
|
|
1074
|
+
}
|
|
1075
|
+
var defaultStore = new TodoStore();
|
|
1076
|
+
function parseTodoArgs(args) {
|
|
1077
|
+
const parsed = writeSchema.safeParse(args);
|
|
1078
|
+
return parsed.success ? parsed.data.todos : null;
|
|
1079
|
+
}
|
|
1080
|
+
function pendingTodoTitles(args) {
|
|
1081
|
+
return (parseTodoArgs(args) ?? []).filter((t) => t.status !== "done").map((t) => t.title);
|
|
1082
|
+
}
|
|
1083
|
+
async function loadTodos(cwd, sessionId) {
|
|
1084
|
+
await defaultStore.load(cwd, sessionId);
|
|
1085
|
+
return defaultStore.list();
|
|
1086
|
+
}
|
|
1087
|
+
function resetTodos() {
|
|
1088
|
+
defaultStore.replace([]);
|
|
1089
|
+
}
|
|
1090
|
+
function createTodoTools(store = defaultStore) {
|
|
1091
|
+
const todoWrite = {
|
|
1092
|
+
name: "todo_write",
|
|
1093
|
+
description: "Replace the current todo list with the given items. Each item has an id, title, and status (pending, in_progress, done).",
|
|
1094
|
+
permission: "read",
|
|
1095
|
+
parameters: writeSchema,
|
|
1096
|
+
async execute(args, ctx) {
|
|
1097
|
+
const parsed = writeSchema.safeParse(args);
|
|
1098
|
+
if (!parsed.success) {
|
|
1099
|
+
return { content: `Invalid todos: ${parsed.error.message}`, isError: true };
|
|
1100
|
+
}
|
|
1101
|
+
const previous = store.list();
|
|
1102
|
+
store.replace(parsed.data.todos);
|
|
1103
|
+
try {
|
|
1104
|
+
await store.save(ctx.cwd);
|
|
1105
|
+
} catch (err) {
|
|
1106
|
+
store.replace(previous);
|
|
1107
|
+
return { content: `Failed to save todos: ${err.message}`, isError: true };
|
|
1108
|
+
}
|
|
1109
|
+
const items = store.list();
|
|
1110
|
+
const done = items.filter((it) => it.status === "done").length;
|
|
1111
|
+
return { content: `Todo list updated (${done}/${items.length} done).` };
|
|
1112
|
+
}
|
|
1113
|
+
};
|
|
1114
|
+
const todoRead = {
|
|
1115
|
+
name: "todo_read",
|
|
1116
|
+
description: "Read the current todo list.",
|
|
1117
|
+
permission: "read",
|
|
1118
|
+
parameters: readSchema,
|
|
1119
|
+
async execute() {
|
|
1120
|
+
return { content: formatTodos(store.list()) };
|
|
1121
|
+
}
|
|
1122
|
+
};
|
|
1123
|
+
return [todoWrite, todoRead];
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
// src/core/image-resize.ts
|
|
1127
|
+
import { execFile } from "child_process";
|
|
1128
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
1129
|
+
import { readFile as readFile3, unlink, writeFile as writeFile3 } from "fs/promises";
|
|
1130
|
+
import os3 from "os";
|
|
1131
|
+
import path5 from "path";
|
|
1132
|
+
import { promisify } from "util";
|
|
1133
|
+
var RESIZE_TIMEOUT_MS = 1e4;
|
|
1134
|
+
var execFileAsync = promisify(execFile);
|
|
1135
|
+
function windowsResizeScript(inputFile, outputFile, maxDimension) {
|
|
1136
|
+
const input = inputFile.replace(/'/g, "''");
|
|
1137
|
+
const output = outputFile.replace(/'/g, "''");
|
|
1138
|
+
return [
|
|
1139
|
+
"Add-Type -AssemblyName System.Drawing",
|
|
1140
|
+
`$img = [System.Drawing.Image]::FromFile('${input}')`,
|
|
1141
|
+
`$scale = [Math]::Min(1.0, ${maxDimension} / [Math]::Max($img.Width, $img.Height))`,
|
|
1142
|
+
"$w = [int][Math]::Max(1, [Math]::Round($img.Width * $scale))",
|
|
1143
|
+
"$h = [int][Math]::Max(1, [Math]::Round($img.Height * $scale))",
|
|
1144
|
+
"$bmp = New-Object System.Drawing.Bitmap($w, $h)",
|
|
1145
|
+
"$g = [System.Drawing.Graphics]::FromImage($bmp)",
|
|
1146
|
+
"$g.InterpolationMode = [System.Drawing.Drawing2D.InterpolationMode]::HighQualityBicubic",
|
|
1147
|
+
"$g.DrawImage($img, 0, 0, $w, $h)",
|
|
1148
|
+
`$bmp.Save('${output}', [System.Drawing.Imaging.ImageFormat]::Png)`,
|
|
1149
|
+
"$g.Dispose(); $bmp.Dispose(); $img.Dispose()"
|
|
1150
|
+
].join("; ");
|
|
1151
|
+
}
|
|
1152
|
+
function resizeImagePlan(platform, inputFile, outputFile, maxDimension, tools = {}) {
|
|
1153
|
+
if (platform === "win32") {
|
|
1154
|
+
return {
|
|
1155
|
+
command: "powershell.exe",
|
|
1156
|
+
args: [
|
|
1157
|
+
"-NoProfile",
|
|
1158
|
+
"-NonInteractive",
|
|
1159
|
+
"-Command",
|
|
1160
|
+
windowsResizeScript(inputFile, outputFile, maxDimension)
|
|
1161
|
+
]
|
|
1162
|
+
};
|
|
1163
|
+
}
|
|
1164
|
+
if (platform === "darwin") {
|
|
1165
|
+
return {
|
|
1166
|
+
command: "sips",
|
|
1167
|
+
args: ["-Z", String(maxDimension), inputFile, "--out", outputFile]
|
|
1168
|
+
};
|
|
1169
|
+
}
|
|
1170
|
+
if (platform === "linux") {
|
|
1171
|
+
if (tools.hasConvert) {
|
|
1172
|
+
return {
|
|
1173
|
+
command: "convert",
|
|
1174
|
+
// -auto-orient keeps EXIF-rotated photos upright after the resize
|
|
1175
|
+
// strips their orientation metadata.
|
|
1176
|
+
args: [
|
|
1177
|
+
inputFile,
|
|
1178
|
+
"-auto-orient",
|
|
1179
|
+
"-resize",
|
|
1180
|
+
`${maxDimension}x${maxDimension}>`,
|
|
1181
|
+
outputFile
|
|
1182
|
+
]
|
|
1183
|
+
};
|
|
1184
|
+
}
|
|
1185
|
+
if (tools.hasFfmpeg) {
|
|
1186
|
+
return {
|
|
1187
|
+
command: "ffmpeg",
|
|
1188
|
+
args: [
|
|
1189
|
+
"-y",
|
|
1190
|
+
"-i",
|
|
1191
|
+
inputFile,
|
|
1192
|
+
"-vf",
|
|
1193
|
+
`scale='if(gt(iw,ih),min(iw,${maxDimension}),-1)':'if(gt(iw,ih),-1,min(ih,${maxDimension}))'`,
|
|
1194
|
+
outputFile
|
|
1195
|
+
]
|
|
1196
|
+
};
|
|
1197
|
+
}
|
|
1198
|
+
return null;
|
|
1199
|
+
}
|
|
1200
|
+
return null;
|
|
1201
|
+
}
|
|
1202
|
+
async function commandExists(command) {
|
|
1203
|
+
const checker = process.platform === "win32" ? "where" : "which";
|
|
1204
|
+
try {
|
|
1205
|
+
await execFileAsync(checker, [command], { timeout: RESIZE_TIMEOUT_MS });
|
|
1206
|
+
return true;
|
|
1207
|
+
} catch {
|
|
1208
|
+
return false;
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
async function downsampleImageIfNeeded(image) {
|
|
1212
|
+
const buf = Buffer.from(image.data, "base64");
|
|
1213
|
+
const dims = probeImageDimensions(buf);
|
|
1214
|
+
if (!dims || Math.max(dims.width, dims.height) <= MAX_IMAGE_DIMENSION) {
|
|
1215
|
+
return { image, oversized: false, resized: false };
|
|
1216
|
+
}
|
|
1217
|
+
const inputFile = path5.join(os3.tmpdir(), `star-image-${randomUUID2()}.src`);
|
|
1218
|
+
const outputFile = path5.join(os3.tmpdir(), `star-image-${randomUUID2()}.png`);
|
|
1219
|
+
try {
|
|
1220
|
+
const [hasConvert, hasFfmpeg] = process.platform === "linux" ? await Promise.all([commandExists("convert"), commandExists("ffmpeg")]) : [false, false];
|
|
1221
|
+
const plan = resizeImagePlan(process.platform, inputFile, outputFile, MAX_IMAGE_DIMENSION, {
|
|
1222
|
+
hasConvert,
|
|
1223
|
+
hasFfmpeg
|
|
1224
|
+
});
|
|
1225
|
+
if (!plan) return { image, oversized: true, resized: false };
|
|
1226
|
+
await writeFile3(inputFile, buf);
|
|
1227
|
+
await execFileAsync(plan.command, plan.args, { timeout: RESIZE_TIMEOUT_MS });
|
|
1228
|
+
const out = await readFile3(outputFile).catch(() => null);
|
|
1229
|
+
if (!out || out.length === 0) return { image, oversized: true, resized: false };
|
|
1230
|
+
return {
|
|
1231
|
+
image: { path: image.path, mimeType: "image/png", data: out.toString("base64") },
|
|
1232
|
+
oversized: true,
|
|
1233
|
+
resized: true
|
|
1234
|
+
};
|
|
1235
|
+
} catch {
|
|
1236
|
+
return { image, oversized: true, resized: false };
|
|
1237
|
+
} finally {
|
|
1238
|
+
await unlink(inputFile).catch(() => {
|
|
1239
|
+
});
|
|
1240
|
+
await unlink(outputFile).catch(() => {
|
|
1241
|
+
});
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
// src/core/sensitive.ts
|
|
1246
|
+
var SENSITIVE_BASENAMES = /* @__PURE__ */ new Set([
|
|
1247
|
+
".env",
|
|
1248
|
+
"id_rsa",
|
|
1249
|
+
"id_dsa",
|
|
1250
|
+
"id_ecdsa",
|
|
1251
|
+
"id_ed25519",
|
|
1252
|
+
".npmrc",
|
|
1253
|
+
".netrc",
|
|
1254
|
+
".pgpass"
|
|
1255
|
+
]);
|
|
1256
|
+
var SENSITIVE_SUFFIXES = [".pem", ".key", ".p12", ".pfx"];
|
|
1257
|
+
var SENSITIVE_ENV_EXEMPTIONS = /* @__PURE__ */ new Set([".env.example", ".env.sample", ".env.template"]);
|
|
1258
|
+
var extraPatterns = [];
|
|
1259
|
+
function registerSensitivePatterns(patterns) {
|
|
1260
|
+
extraPatterns.length = 0;
|
|
1261
|
+
extraPatterns.push(...patterns.map((p) => p.trim()).filter((p) => p.length > 0));
|
|
1262
|
+
}
|
|
1263
|
+
function globToRegExp(pattern) {
|
|
1264
|
+
const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
|
|
1265
|
+
return new RegExp(`^${escaped}$`, "i");
|
|
1266
|
+
}
|
|
1267
|
+
function pathSegments(p) {
|
|
1268
|
+
return p.replace(/\\/g, "/").split("/").filter(Boolean).map((s) => s.toLowerCase());
|
|
1269
|
+
}
|
|
1270
|
+
function isSensitivePath(p) {
|
|
1271
|
+
const segments = pathSegments(p);
|
|
1272
|
+
const base = segments[segments.length - 1] ?? "";
|
|
1273
|
+
const normalized = p.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
1274
|
+
for (const pattern of extraPatterns) {
|
|
1275
|
+
const re = globToRegExp(pattern);
|
|
1276
|
+
if (re.test(normalized) || re.test(base)) {
|
|
1277
|
+
return true;
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
if (SENSITIVE_ENV_EXEMPTIONS.has(base)) {
|
|
1281
|
+
return false;
|
|
1282
|
+
}
|
|
1283
|
+
if (base === ".env" || base.startsWith(".env.")) {
|
|
1284
|
+
return true;
|
|
1285
|
+
}
|
|
1286
|
+
if (SENSITIVE_BASENAMES.has(base)) {
|
|
1287
|
+
return true;
|
|
1288
|
+
}
|
|
1289
|
+
if (SENSITIVE_SUFFIXES.some((suffix) => base.endsWith(suffix))) {
|
|
1290
|
+
return true;
|
|
1291
|
+
}
|
|
1292
|
+
const dir = segments[segments.length - 2] ?? "";
|
|
1293
|
+
if (dir === ".aws" && base === "credentials") {
|
|
1294
|
+
return true;
|
|
1295
|
+
}
|
|
1296
|
+
if (dir === ".kube" && base === "config") {
|
|
1297
|
+
return true;
|
|
1298
|
+
}
|
|
1299
|
+
return false;
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
// src/tools/fs/util.ts
|
|
1303
|
+
import { readFileSync, statSync } from "fs";
|
|
1304
|
+
import { readdir, realpath, stat as stat2 } from "fs/promises";
|
|
1305
|
+
import path6 from "path";
|
|
1306
|
+
var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist"]);
|
|
1307
|
+
var WALK_CONCURRENCY = 16;
|
|
1308
|
+
function createLimiter(concurrency) {
|
|
1309
|
+
let active = 0;
|
|
1310
|
+
const waiting = [];
|
|
1311
|
+
return async function limit(fn) {
|
|
1312
|
+
while (active >= concurrency) {
|
|
1313
|
+
await new Promise((resolve) => waiting.push(resolve));
|
|
1314
|
+
}
|
|
1315
|
+
active += 1;
|
|
1316
|
+
try {
|
|
1317
|
+
return await fn();
|
|
1318
|
+
} finally {
|
|
1319
|
+
active -= 1;
|
|
1320
|
+
waiting.shift()?.();
|
|
1321
|
+
}
|
|
1322
|
+
};
|
|
1323
|
+
}
|
|
1324
|
+
function layerIgnores(layers, rel, isDir) {
|
|
1325
|
+
for (const layer of layers) {
|
|
1326
|
+
const local = layer.relBase === "" ? rel : rel.startsWith(`${layer.relBase}/`) ? rel.slice(layer.relBase.length + 1) : null;
|
|
1327
|
+
if (local === null) {
|
|
1328
|
+
continue;
|
|
1329
|
+
}
|
|
1330
|
+
for (const { pattern, dirOnly } of layer.patterns) {
|
|
1331
|
+
if (dirOnly && !isDir) {
|
|
1332
|
+
continue;
|
|
1333
|
+
}
|
|
1334
|
+
if (matchesGlob(pattern, local)) {
|
|
1335
|
+
return true;
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
return false;
|
|
1340
|
+
}
|
|
1341
|
+
async function walkFiles(root, skipDirs = SKIP_DIRS, options = {}) {
|
|
1342
|
+
const out = [];
|
|
1343
|
+
const limit = createLimiter(WALK_CONCURRENCY);
|
|
1344
|
+
const signal = options.signal;
|
|
1345
|
+
const caseKey = (p) => process.platform === "win32" ? p.toLowerCase() : p;
|
|
1346
|
+
const visited = /* @__PURE__ */ new Set();
|
|
1347
|
+
const rootReal = await realpath(root).catch(() => null);
|
|
1348
|
+
if (rootReal !== null) {
|
|
1349
|
+
visited.add(caseKey(rootReal));
|
|
1350
|
+
}
|
|
1351
|
+
let boundaryReal;
|
|
1352
|
+
async function resolveLink(abs) {
|
|
1353
|
+
const real = await limit(() => realpath(abs)).catch(() => null);
|
|
1354
|
+
if (real === null) {
|
|
1355
|
+
return null;
|
|
1356
|
+
}
|
|
1357
|
+
const boundary = options.symlinkBoundary;
|
|
1358
|
+
if (boundary === void 0) {
|
|
1359
|
+
return real;
|
|
1360
|
+
}
|
|
1361
|
+
if (boundaryReal === void 0) {
|
|
1362
|
+
boundaryReal = await limit(() => realpath(boundary)).catch(() => null);
|
|
1363
|
+
}
|
|
1364
|
+
if (boundaryReal === null) {
|
|
1365
|
+
return null;
|
|
1366
|
+
}
|
|
1367
|
+
const realKey = caseKey(real);
|
|
1368
|
+
const boundaryKey = caseKey(boundaryReal);
|
|
1369
|
+
return realKey === boundaryKey || realKey.startsWith(`${boundaryKey}${path6.sep}`) ? real : null;
|
|
1370
|
+
}
|
|
1371
|
+
async function walkDir(dir, relBase, parentLayers) {
|
|
1372
|
+
if (signal?.aborted) {
|
|
1373
|
+
return;
|
|
1374
|
+
}
|
|
1375
|
+
const entries = await limit(() => readdir(dir, { withFileTypes: true })).catch(() => null);
|
|
1376
|
+
if (!entries) {
|
|
1377
|
+
return;
|
|
1378
|
+
}
|
|
1379
|
+
let layers = parentLayers;
|
|
1380
|
+
if (options.nestedIgnore && entries.some((e) => e.name === IGNORE_FILE)) {
|
|
1381
|
+
const patterns = loadIgnorePatterns(dir);
|
|
1382
|
+
if (patterns.length > 0) {
|
|
1383
|
+
layers = [...parentLayers, { relBase, patterns }];
|
|
1384
|
+
}
|
|
1385
|
+
}
|
|
1386
|
+
const subs = [];
|
|
1387
|
+
for (const entry of entries) {
|
|
1388
|
+
if (signal?.aborted) {
|
|
1389
|
+
break;
|
|
1390
|
+
}
|
|
1391
|
+
const abs = path6.join(dir, entry.name);
|
|
1392
|
+
const rel = relBase ? `${relBase}/${entry.name}` : entry.name;
|
|
1393
|
+
let targetStat = null;
|
|
1394
|
+
let resolved;
|
|
1395
|
+
if (entry.isSymbolicLink()) {
|
|
1396
|
+
targetStat = await limit(() => stat2(abs)).catch(() => null);
|
|
1397
|
+
if (!targetStat || !targetStat.isDirectory() && !targetStat.isFile()) {
|
|
1398
|
+
continue;
|
|
1399
|
+
}
|
|
1400
|
+
const real = await resolveLink(abs);
|
|
1401
|
+
if (real === null) {
|
|
1402
|
+
continue;
|
|
1403
|
+
}
|
|
1404
|
+
resolved = real;
|
|
1405
|
+
if (targetStat.isDirectory()) {
|
|
1406
|
+
const key = caseKey(real);
|
|
1407
|
+
if (visited.has(key)) {
|
|
1408
|
+
continue;
|
|
1409
|
+
}
|
|
1410
|
+
visited.add(key);
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1413
|
+
const isDir = entry.isDirectory() || (targetStat?.isDirectory() ?? false);
|
|
1414
|
+
const isFile = entry.isFile() || (targetStat?.isFile() ?? false);
|
|
1415
|
+
if (isDir) {
|
|
1416
|
+
if (skipDirs.has(entry.name) || options.ignore?.(abs, true)) {
|
|
1417
|
+
continue;
|
|
1418
|
+
}
|
|
1419
|
+
if (layers.length > 0 && layerIgnores(layers, rel, true)) {
|
|
1420
|
+
continue;
|
|
1421
|
+
}
|
|
1422
|
+
subs.push(walkDir(abs, rel, layers));
|
|
1423
|
+
} else if (isFile) {
|
|
1424
|
+
if (options.ignore?.(abs, false)) {
|
|
1425
|
+
continue;
|
|
1426
|
+
}
|
|
1427
|
+
if (layers.length > 0 && layerIgnores(layers, rel, false)) {
|
|
1428
|
+
continue;
|
|
1429
|
+
}
|
|
1430
|
+
if (options.withMtime === false) {
|
|
1431
|
+
out.push(
|
|
1432
|
+
resolved === void 0 ? { abs, rel, mtimeMs: 0 } : { abs, rel, mtimeMs: 0, resolved }
|
|
1433
|
+
);
|
|
1434
|
+
continue;
|
|
1435
|
+
}
|
|
1436
|
+
const st = targetStat ?? await limit(() => stat2(abs)).catch(() => null);
|
|
1437
|
+
if (st) {
|
|
1438
|
+
out.push(
|
|
1439
|
+
resolved === void 0 ? { abs, rel, mtimeMs: st.mtimeMs } : { abs, rel, mtimeMs: st.mtimeMs, resolved }
|
|
1440
|
+
);
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1443
|
+
}
|
|
1444
|
+
await Promise.all(subs);
|
|
1445
|
+
}
|
|
1446
|
+
await walkDir(root, "", []);
|
|
1447
|
+
out.sort((a, b) => a.rel < b.rel ? -1 : a.rel > b.rel ? 1 : 0);
|
|
1448
|
+
return { files: out, aborted: signal?.aborted ?? false };
|
|
1449
|
+
}
|
|
1450
|
+
var REGEX_SPECIALS = /[.+^${}()|[\]\\]/g;
|
|
1451
|
+
function escapeRe(c) {
|
|
1452
|
+
return c.replace(REGEX_SPECIALS, "\\$&");
|
|
1453
|
+
}
|
|
1454
|
+
function globToRegExp2(pattern) {
|
|
1455
|
+
let i = 0;
|
|
1456
|
+
function parseClass() {
|
|
1457
|
+
let j = i + 1;
|
|
1458
|
+
let negate = false;
|
|
1459
|
+
if (pattern.charAt(j) === "!") {
|
|
1460
|
+
negate = true;
|
|
1461
|
+
j += 1;
|
|
1462
|
+
}
|
|
1463
|
+
let body = "";
|
|
1464
|
+
if (pattern.charAt(j) === "]") {
|
|
1465
|
+
body += "\\]";
|
|
1466
|
+
j += 1;
|
|
1467
|
+
}
|
|
1468
|
+
while (j < pattern.length && pattern.charAt(j) !== "]") {
|
|
1469
|
+
const ch = pattern.charAt(j);
|
|
1470
|
+
body += ch === "\\" ? "\\\\" : ch;
|
|
1471
|
+
j += 1;
|
|
1472
|
+
}
|
|
1473
|
+
if (j < pattern.length) {
|
|
1474
|
+
i = j + 1;
|
|
1475
|
+
if (body.startsWith("^")) body = `\\${body}`;
|
|
1476
|
+
return `[${negate ? "^" : ""}${body}]`;
|
|
1477
|
+
}
|
|
1478
|
+
i += 1;
|
|
1479
|
+
return "\\[";
|
|
1480
|
+
}
|
|
1481
|
+
function parseSegment(depth) {
|
|
1482
|
+
let re2 = "";
|
|
1483
|
+
while (i < pattern.length) {
|
|
1484
|
+
const c = pattern.charAt(i);
|
|
1485
|
+
if (depth > 0 && (c === "," || c === "}")) {
|
|
1486
|
+
return { re: re2, end: c };
|
|
1487
|
+
}
|
|
1488
|
+
if (c === "*") {
|
|
1489
|
+
if (pattern.charAt(i + 1) === "*") {
|
|
1490
|
+
i += 2;
|
|
1491
|
+
if (pattern.charAt(i) === "/") {
|
|
1492
|
+
i += 1;
|
|
1493
|
+
re2 += "(?:[^/]+/)*";
|
|
1494
|
+
while (pattern.startsWith("**/", i)) {
|
|
1495
|
+
i += 3;
|
|
1496
|
+
}
|
|
1497
|
+
} else {
|
|
1498
|
+
re2 += ".*";
|
|
1499
|
+
}
|
|
1500
|
+
} else {
|
|
1501
|
+
re2 += "[^/]*";
|
|
1502
|
+
i += 1;
|
|
1503
|
+
}
|
|
1504
|
+
continue;
|
|
1505
|
+
}
|
|
1506
|
+
if (c === "?") {
|
|
1507
|
+
re2 += "[^/]";
|
|
1508
|
+
i += 1;
|
|
1509
|
+
continue;
|
|
1510
|
+
}
|
|
1511
|
+
if (c === "[") {
|
|
1512
|
+
re2 += parseClass();
|
|
1513
|
+
continue;
|
|
1514
|
+
}
|
|
1515
|
+
if (c === "{" && depth < 2) {
|
|
1516
|
+
const close = braceEnd(i);
|
|
1517
|
+
if (close === -1) {
|
|
1518
|
+
re2 += "\\{";
|
|
1519
|
+
i += 1;
|
|
1520
|
+
continue;
|
|
1521
|
+
}
|
|
1522
|
+
i += 1;
|
|
1523
|
+
const alts = [];
|
|
1524
|
+
for (; ; ) {
|
|
1525
|
+
const part = parseSegment(depth + 1);
|
|
1526
|
+
alts.push(part.re);
|
|
1527
|
+
if (part.end !== ",") break;
|
|
1528
|
+
i += 1;
|
|
1529
|
+
}
|
|
1530
|
+
i += 1;
|
|
1531
|
+
re2 += `(?:${alts.join("|")})`;
|
|
1532
|
+
continue;
|
|
1533
|
+
}
|
|
1534
|
+
re2 += escapeRe(c);
|
|
1535
|
+
i += 1;
|
|
1536
|
+
}
|
|
1537
|
+
return { re: re2, end: "" };
|
|
1538
|
+
}
|
|
1539
|
+
function braceEnd(from) {
|
|
1540
|
+
let depth = 0;
|
|
1541
|
+
for (let j = from; j < pattern.length; j++) {
|
|
1542
|
+
const ch = pattern.charAt(j);
|
|
1543
|
+
if (ch === "{") depth += 1;
|
|
1544
|
+
else if (ch === "}") {
|
|
1545
|
+
depth -= 1;
|
|
1546
|
+
if (depth === 0) return j;
|
|
1547
|
+
}
|
|
1548
|
+
}
|
|
1549
|
+
return -1;
|
|
1550
|
+
}
|
|
1551
|
+
const { re } = parseSegment(0);
|
|
1552
|
+
return new RegExp(`^${re}$`);
|
|
1553
|
+
}
|
|
1554
|
+
function matchesGlob(pattern, relPath) {
|
|
1555
|
+
const normalized = pattern.replace(/\\/g, "/");
|
|
1556
|
+
if (!normalized.includes("/")) {
|
|
1557
|
+
const base = relPath.split("/").pop() ?? relPath;
|
|
1558
|
+
return globToRegExp2(normalized).test(base);
|
|
1559
|
+
}
|
|
1560
|
+
return globToRegExp2(normalized).test(relPath);
|
|
1561
|
+
}
|
|
1562
|
+
var IGNORE_FILE = ".starignore";
|
|
1563
|
+
var ignoreCache = /* @__PURE__ */ new Map();
|
|
1564
|
+
function parseIgnorePatterns(body) {
|
|
1565
|
+
const out = [];
|
|
1566
|
+
for (const raw of body.split("\n")) {
|
|
1567
|
+
const line = raw.trim();
|
|
1568
|
+
if (!line || line.startsWith("#")) {
|
|
1569
|
+
continue;
|
|
1570
|
+
}
|
|
1571
|
+
if (line.endsWith("/")) {
|
|
1572
|
+
const pattern = line.slice(0, -1);
|
|
1573
|
+
if (pattern) {
|
|
1574
|
+
out.push({ pattern, dirOnly: true });
|
|
1575
|
+
}
|
|
1576
|
+
} else {
|
|
1577
|
+
out.push({ pattern: line, dirOnly: false });
|
|
1578
|
+
}
|
|
1579
|
+
}
|
|
1580
|
+
return out;
|
|
1581
|
+
}
|
|
1582
|
+
function loadIgnorePatterns(dir) {
|
|
1583
|
+
const file = path6.join(dir, IGNORE_FILE);
|
|
1584
|
+
let mtimeMs;
|
|
1585
|
+
try {
|
|
1586
|
+
mtimeMs = statSync(file).mtimeMs;
|
|
1587
|
+
} catch {
|
|
1588
|
+
return [];
|
|
1589
|
+
}
|
|
1590
|
+
const hit = ignoreCache.get(file);
|
|
1591
|
+
if (hit && hit.mtimeMs === mtimeMs) {
|
|
1592
|
+
return hit.patterns;
|
|
1593
|
+
}
|
|
1594
|
+
let patterns = [];
|
|
1595
|
+
try {
|
|
1596
|
+
patterns = parseIgnorePatterns(readFileSync(file, "utf8"));
|
|
1597
|
+
} catch {
|
|
1598
|
+
patterns = [];
|
|
1599
|
+
}
|
|
1600
|
+
ignoreCache.set(file, { mtimeMs, patterns });
|
|
1601
|
+
return patterns;
|
|
1602
|
+
}
|
|
1603
|
+
function createIgnorePredicate(cwd) {
|
|
1604
|
+
const patterns = loadIgnorePatterns(cwd);
|
|
1605
|
+
if (patterns.length === 0) {
|
|
1606
|
+
return void 0;
|
|
1607
|
+
}
|
|
1608
|
+
const root = path6.resolve(cwd);
|
|
1609
|
+
return (absPath, isDir) => {
|
|
1610
|
+
const rel = path6.relative(root, absPath).split(path6.sep).join("/");
|
|
1611
|
+
if (!rel || rel.startsWith("..")) {
|
|
1612
|
+
return false;
|
|
1613
|
+
}
|
|
1614
|
+
for (const { pattern, dirOnly } of patterns) {
|
|
1615
|
+
if (dirOnly && !isDir) {
|
|
1616
|
+
continue;
|
|
1617
|
+
}
|
|
1618
|
+
if (matchesGlob(pattern, rel)) {
|
|
1619
|
+
return true;
|
|
1620
|
+
}
|
|
1621
|
+
}
|
|
1622
|
+
return false;
|
|
1623
|
+
};
|
|
1624
|
+
}
|
|
1625
|
+
|
|
1626
|
+
// src/tools/web/fetch.ts
|
|
1627
|
+
import dns from "dns";
|
|
1628
|
+
import { z as z3 } from "zod";
|
|
1629
|
+
var DEFAULT_MAX_CHARS = 2e4;
|
|
1630
|
+
var MAX_BYTES = 2 * 1024 * 1024;
|
|
1631
|
+
var TIMEOUT_MS = 3e4;
|
|
1632
|
+
var MAX_REDIRECTS = 5;
|
|
1633
|
+
var MAX_ANNOTATED_LINKS = 50;
|
|
1634
|
+
var PDF_MAGIC = [37, 80, 68, 70];
|
|
1635
|
+
var schema2 = z3.object({
|
|
1636
|
+
url: z3.string().describe("The http(s) URL to fetch"),
|
|
1637
|
+
maxChars: z3.number().int().positive().optional().describe("Maximum characters of text to return (default 20000)")
|
|
1638
|
+
});
|
|
1639
|
+
var defaultResolver = async (hostname) => {
|
|
1640
|
+
const results = await dns.promises.lookup(hostname, { all: true });
|
|
1641
|
+
return results.map((result) => result.address);
|
|
1642
|
+
};
|
|
1643
|
+
var configuredAllowPrivateHosts = false;
|
|
1644
|
+
function setWebFetchAllowPrivateHosts(allow) {
|
|
1645
|
+
configuredAllowPrivateHosts = allow;
|
|
1646
|
+
}
|
|
1647
|
+
function isPrivateIpv4(ip) {
|
|
1648
|
+
const parts = ip.split(".").map(Number);
|
|
1649
|
+
if (parts.length !== 4 || parts.some((n) => !Number.isInteger(n) || n < 0 || n > 255)) {
|
|
1650
|
+
return false;
|
|
1651
|
+
}
|
|
1652
|
+
const [a = 0, b = 0] = parts;
|
|
1653
|
+
return a === 0 || a === 10 || a === 127 || a === 172 && b >= 16 && b <= 31 || a === 192 && b === 168 || a === 169 && b === 254;
|
|
1654
|
+
}
|
|
1655
|
+
function isPrivateAddress(address) {
|
|
1656
|
+
let ip = address.toLowerCase();
|
|
1657
|
+
if (ip.startsWith("::ffff:")) ip = ip.slice("::ffff:".length);
|
|
1658
|
+
if (ip.includes(".")) return isPrivateIpv4(ip);
|
|
1659
|
+
if (ip === "::1" || ip === "::") return true;
|
|
1660
|
+
if (ip.startsWith("fc") || ip.startsWith("fd")) return true;
|
|
1661
|
+
if (/^fe[89ab]/.test(ip)) return true;
|
|
1662
|
+
return false;
|
|
1663
|
+
}
|
|
1664
|
+
function isLocalHostname(hostname) {
|
|
1665
|
+
const h = hostname.toLowerCase().replace(/\.$/, "");
|
|
1666
|
+
return h === "localhost" || h.endsWith(".localhost") || h.endsWith(".local") || h.endsWith(".internal");
|
|
1667
|
+
}
|
|
1668
|
+
async function privateHostRefusal(url, resolve) {
|
|
1669
|
+
const hostname = url.hostname.replace(/^\[|\]$/g, "");
|
|
1670
|
+
if (isLocalHostname(hostname)) {
|
|
1671
|
+
return `"${hostname}" is a local/internal hostname`;
|
|
1672
|
+
}
|
|
1673
|
+
let addresses;
|
|
1674
|
+
try {
|
|
1675
|
+
addresses = await resolve(hostname);
|
|
1676
|
+
} catch (err) {
|
|
1677
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1678
|
+
return `could not resolve "${hostname}" (${message})`;
|
|
1679
|
+
}
|
|
1680
|
+
const blocked = addresses.find(isPrivateAddress);
|
|
1681
|
+
if (blocked !== void 0) {
|
|
1682
|
+
return `"${hostname}" resolves to a private/reserved address (${blocked})`;
|
|
1683
|
+
}
|
|
1684
|
+
if (addresses.length === 0) {
|
|
1685
|
+
return `could not resolve "${hostname}"`;
|
|
1686
|
+
}
|
|
1687
|
+
return null;
|
|
1688
|
+
}
|
|
1689
|
+
function safeCodePoint(n, raw) {
|
|
1690
|
+
if (!Number.isInteger(n) || n < 0 || n > 1114111) {
|
|
1691
|
+
return raw;
|
|
1692
|
+
}
|
|
1693
|
+
return String.fromCodePoint(n);
|
|
1694
|
+
}
|
|
1695
|
+
function decodeEntities(s) {
|
|
1696
|
+
return s.replace(/&#(\d+);/g, (m, n) => safeCodePoint(Number(n), m)).replace(/&#x([0-9a-fA-F]+);/gi, (m, n) => safeCodePoint(Number.parseInt(n, 16), m)).replace(/&(quot|lt|gt|nbsp);/g, (m) => {
|
|
1697
|
+
switch (m) {
|
|
1698
|
+
case """:
|
|
1699
|
+
return '"';
|
|
1700
|
+
case "<":
|
|
1701
|
+
return "<";
|
|
1702
|
+
case ">":
|
|
1703
|
+
return ">";
|
|
1704
|
+
case " ":
|
|
1705
|
+
return " ";
|
|
1706
|
+
default:
|
|
1707
|
+
return m;
|
|
1708
|
+
}
|
|
1709
|
+
}).replace(/&/g, "&");
|
|
1710
|
+
}
|
|
1711
|
+
function annotateLinks(html, base) {
|
|
1712
|
+
let annotated = 0;
|
|
1713
|
+
return html.replace(
|
|
1714
|
+
/<a\b[^>]*?\shref\s*=\s*(?:"([^"]*)"|'([^']*)')[^>]*>([\s\S]*?)<\/a\s*>/gi,
|
|
1715
|
+
(_match, doubleQuoted, singleQuoted, inner) => {
|
|
1716
|
+
const rawText = String(inner).replace(/<[^>]+>/g, " ");
|
|
1717
|
+
const href = decodeEntities(String(doubleQuoted ?? singleQuoted ?? "")).trim();
|
|
1718
|
+
if (annotated >= MAX_ANNOTATED_LINKS || href === "" || href.startsWith("#")) {
|
|
1719
|
+
return rawText;
|
|
1720
|
+
}
|
|
1721
|
+
let resolved;
|
|
1722
|
+
try {
|
|
1723
|
+
resolved = new URL(href, base);
|
|
1724
|
+
} catch {
|
|
1725
|
+
return rawText;
|
|
1726
|
+
}
|
|
1727
|
+
if (resolved.protocol !== "http:" && resolved.protocol !== "https:") {
|
|
1728
|
+
return rawText;
|
|
1729
|
+
}
|
|
1730
|
+
const text = decodeEntities(rawText).replace(/\s+/g, " ").trim();
|
|
1731
|
+
if (text === "") {
|
|
1732
|
+
annotated += 1;
|
|
1733
|
+
return resolved.href;
|
|
1734
|
+
}
|
|
1735
|
+
if (text === resolved.href || text === href) {
|
|
1736
|
+
return rawText;
|
|
1737
|
+
}
|
|
1738
|
+
annotated += 1;
|
|
1739
|
+
return `${rawText} (${resolved.href})`;
|
|
1740
|
+
}
|
|
1741
|
+
);
|
|
1742
|
+
}
|
|
1743
|
+
function htmlToText(html, base) {
|
|
1744
|
+
const noBlocks = html.replace(/<(script|style|noscript|template)\b[^>]*>[\s\S]*?<\/\1>/gi, " ");
|
|
1745
|
+
const withLinks = annotateLinks(noBlocks, base);
|
|
1746
|
+
const withBreaks = withLinks.replace(
|
|
1747
|
+
/<br\s*\/?>|<\/(p|div|li|tr|h[1-6]|section|article|header|footer|blockquote|pre)>/gi,
|
|
1748
|
+
"\n"
|
|
1749
|
+
);
|
|
1750
|
+
const stripped = decodeEntities(withBreaks.replace(/<[^>]+>/g, " "));
|
|
1751
|
+
return stripped.split("\n").map((line) => line.replace(/\s+/g, " ").trim()).join("\n").replace(/\n{3,}/g, "\n\n").trim();
|
|
1752
|
+
}
|
|
1753
|
+
function charsetDecoder(contentTypeHeader) {
|
|
1754
|
+
const match = contentTypeHeader.match(/charset\s*=\s*"?([^\s";]+)"?/i);
|
|
1755
|
+
const label = match?.[1]?.toLowerCase() ?? "utf-8";
|
|
1756
|
+
if (label === "utf-8" || label === "utf8") {
|
|
1757
|
+
return new TextDecoder("utf-8");
|
|
1758
|
+
}
|
|
1759
|
+
try {
|
|
1760
|
+
return new TextDecoder(label);
|
|
1761
|
+
} catch {
|
|
1762
|
+
return new TextDecoder("utf-8");
|
|
1763
|
+
}
|
|
1764
|
+
}
|
|
1765
|
+
async function readBody(res, maxChars, decoder) {
|
|
1766
|
+
if (!res.body) {
|
|
1767
|
+
return "";
|
|
1768
|
+
}
|
|
1769
|
+
const reader = res.body.getReader();
|
|
1770
|
+
let bytes = 0;
|
|
1771
|
+
let text = "";
|
|
1772
|
+
try {
|
|
1773
|
+
for (; ; ) {
|
|
1774
|
+
const { done, value } = await reader.read();
|
|
1775
|
+
if (done) {
|
|
1776
|
+
break;
|
|
1777
|
+
}
|
|
1778
|
+
bytes += value.byteLength;
|
|
1779
|
+
text += decoder.decode(value, { stream: true });
|
|
1780
|
+
if (bytes >= MAX_BYTES || text.length >= maxChars * 2) {
|
|
1781
|
+
break;
|
|
1782
|
+
}
|
|
1783
|
+
}
|
|
1784
|
+
text += decoder.decode();
|
|
1785
|
+
} finally {
|
|
1786
|
+
reader.cancel().catch(() => {
|
|
1787
|
+
});
|
|
1788
|
+
}
|
|
1789
|
+
return text;
|
|
1790
|
+
}
|
|
1791
|
+
async function readPrefix(res, n) {
|
|
1792
|
+
if (!res.body) {
|
|
1793
|
+
return new Uint8Array(0);
|
|
1794
|
+
}
|
|
1795
|
+
const reader = res.body.getReader();
|
|
1796
|
+
const out = new Uint8Array(n);
|
|
1797
|
+
let total = 0;
|
|
1798
|
+
try {
|
|
1799
|
+
while (total < n) {
|
|
1800
|
+
const { done, value } = await reader.read();
|
|
1801
|
+
if (done) {
|
|
1802
|
+
break;
|
|
1803
|
+
}
|
|
1804
|
+
const take = Math.min(value.byteLength, n - total);
|
|
1805
|
+
out.set(value.subarray(0, take), total);
|
|
1806
|
+
total += take;
|
|
1807
|
+
}
|
|
1808
|
+
} finally {
|
|
1809
|
+
reader.cancel().catch(() => {
|
|
1810
|
+
});
|
|
1811
|
+
}
|
|
1812
|
+
return out.subarray(0, total);
|
|
1813
|
+
}
|
|
1814
|
+
function isPdfPrefix(bytes) {
|
|
1815
|
+
return PDF_MAGIC.every((b, i) => bytes[i] === b);
|
|
1816
|
+
}
|
|
1817
|
+
function pdfError(url) {
|
|
1818
|
+
return {
|
|
1819
|
+
content: `ERROR: PDF is not supported by web_fetch: ${url.href} is a PDF document. Do not retry this URL \u2014 use an HTML alternative (landing page, abstract, or HTML version) or a different source instead.`,
|
|
1820
|
+
isError: true
|
|
1821
|
+
};
|
|
1822
|
+
}
|
|
1823
|
+
function truncate(s, maxChars) {
|
|
1824
|
+
if (s.length <= maxChars) {
|
|
1825
|
+
return s;
|
|
1826
|
+
}
|
|
1827
|
+
return `${s.slice(0, maxChars)}
|
|
1828
|
+
[truncated, showing first ${maxChars} of ${s.length} chars]`;
|
|
1829
|
+
}
|
|
1830
|
+
function createWebFetchTool(options = {}) {
|
|
1831
|
+
const resolve = options.resolver ?? defaultResolver;
|
|
1832
|
+
const fetchImpl = options.fetchImpl ?? ((...args) => fetch(...args));
|
|
1833
|
+
const allowPrivate = () => options.allowPrivateHosts ?? configuredAllowPrivateHosts;
|
|
1834
|
+
return {
|
|
1835
|
+
name: "web_fetch",
|
|
1836
|
+
description: "Fetch a URL over http(s) and return its contents as readable text. HTML pages are converted to plain text (tags, scripts and styles removed; links kept inline as `anchor text (absolute-url)`). The charset declared in the response content-type is honored (GBK, Shift_JIS, Big5, etc.). PDF documents are not supported and return a clear error \u2014 do not retry them with this tool. Output is truncated to maxChars (default 20000). Private/reserved addresses (loopback, RFC1918, link-local incl. 169.254.169.254, localhost/.local/.internal names) are refused by default, on redirect hops too \u2014 the global config key webFetchAllowPrivateHosts lifts that for trusted intranet use.",
|
|
1837
|
+
permission: "read",
|
|
1838
|
+
parameters: schema2,
|
|
1839
|
+
async execute(args, ctx) {
|
|
1840
|
+
let url;
|
|
1841
|
+
try {
|
|
1842
|
+
url = new URL(args.url);
|
|
1843
|
+
} catch {
|
|
1844
|
+
return { content: `ERROR: invalid URL: ${args.url}`, isError: true };
|
|
1845
|
+
}
|
|
1846
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
1847
|
+
return {
|
|
1848
|
+
content: `ERROR: only http(s) URLs are supported, got: ${url.protocol}`,
|
|
1849
|
+
isError: true
|
|
1850
|
+
};
|
|
1851
|
+
}
|
|
1852
|
+
if (!allowPrivate()) {
|
|
1853
|
+
const refusal = await privateHostRefusal(url, resolve);
|
|
1854
|
+
if (refusal !== null) {
|
|
1855
|
+
return { content: `ERROR: refused to fetch ${args.url}: ${refusal}`, isError: true };
|
|
1856
|
+
}
|
|
1857
|
+
}
|
|
1858
|
+
const maxChars = args.maxChars ?? DEFAULT_MAX_CHARS;
|
|
1859
|
+
const timeoutSignal = AbortSignal.timeout(TIMEOUT_MS);
|
|
1860
|
+
const signal = ctx.abortSignal ? AbortSignal.any([timeoutSignal, ctx.abortSignal]) : timeoutSignal;
|
|
1861
|
+
let res;
|
|
1862
|
+
let current = url;
|
|
1863
|
+
for (let hops = 0; ; hops += 1) {
|
|
1864
|
+
try {
|
|
1865
|
+
res = await fetchImpl(current, { signal, redirect: "manual" });
|
|
1866
|
+
} catch (err) {
|
|
1867
|
+
if (ctx.abortSignal?.aborted) {
|
|
1868
|
+
return { content: "ERROR: request aborted", isError: true };
|
|
1869
|
+
}
|
|
1870
|
+
if (timeoutSignal.aborted) {
|
|
1871
|
+
return {
|
|
1872
|
+
content: `ERROR: request timed out after ${TIMEOUT_MS / 1e3}s`,
|
|
1873
|
+
isError: true
|
|
1874
|
+
};
|
|
1875
|
+
}
|
|
1876
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1877
|
+
return { content: `ERROR: failed to fetch ${current.href}: ${message}`, isError: true };
|
|
1878
|
+
}
|
|
1879
|
+
const location = res.headers.get("location");
|
|
1880
|
+
if (res.status >= 300 && res.status < 400 && location && hops < MAX_REDIRECTS) {
|
|
1881
|
+
await res.body?.cancel().catch(() => {
|
|
1882
|
+
});
|
|
1883
|
+
let next;
|
|
1884
|
+
try {
|
|
1885
|
+
next = new URL(location, current);
|
|
1886
|
+
} catch {
|
|
1887
|
+
return { content: `ERROR: invalid redirect Location: ${location}`, isError: true };
|
|
1888
|
+
}
|
|
1889
|
+
if (next.protocol !== "http:" && next.protocol !== "https:") {
|
|
1890
|
+
return {
|
|
1891
|
+
content: `ERROR: redirect to non-http(s) URL refused: ${next.protocol}`,
|
|
1892
|
+
isError: true
|
|
1893
|
+
};
|
|
1894
|
+
}
|
|
1895
|
+
if (!allowPrivate()) {
|
|
1896
|
+
const refusal = await privateHostRefusal(next, resolve);
|
|
1897
|
+
if (refusal !== null) {
|
|
1898
|
+
return {
|
|
1899
|
+
content: `ERROR: refused redirect to ${next.href}: ${refusal}`,
|
|
1900
|
+
isError: true
|
|
1901
|
+
};
|
|
1902
|
+
}
|
|
1903
|
+
}
|
|
1904
|
+
current = next;
|
|
1905
|
+
continue;
|
|
1906
|
+
}
|
|
1907
|
+
break;
|
|
1908
|
+
}
|
|
1909
|
+
if (!res.ok) {
|
|
1910
|
+
await res.body?.cancel().catch(() => {
|
|
1911
|
+
});
|
|
1912
|
+
return {
|
|
1913
|
+
content: `ERROR: HTTP ${res.status} ${res.statusText} for ${current.href}`,
|
|
1914
|
+
isError: true
|
|
1915
|
+
};
|
|
1916
|
+
}
|
|
1917
|
+
const contentTypeHeader = res.headers.get("content-type") ?? "text/plain";
|
|
1918
|
+
const [rawType = ""] = contentTypeHeader.split(";");
|
|
1919
|
+
const contentType = rawType.trim().toLowerCase();
|
|
1920
|
+
const isHtml = contentType === "text/html" || contentType === "application/xhtml+xml";
|
|
1921
|
+
const isText = contentType.startsWith("text/") || contentType === "application/json" || contentType === "application/xml" || contentType.endsWith("+json") || contentType.endsWith("+xml");
|
|
1922
|
+
if (contentType === "application/pdf") {
|
|
1923
|
+
await res.body?.cancel().catch(() => {
|
|
1924
|
+
});
|
|
1925
|
+
return pdfError(current);
|
|
1926
|
+
}
|
|
1927
|
+
if (!isHtml && !isText) {
|
|
1928
|
+
const prefix = await readPrefix(res, PDF_MAGIC.length);
|
|
1929
|
+
if (isPdfPrefix(prefix)) {
|
|
1930
|
+
return pdfError(current);
|
|
1931
|
+
}
|
|
1932
|
+
return {
|
|
1933
|
+
content: `ERROR: unsupported content type: ${contentType} (${current.href})`,
|
|
1934
|
+
isError: true
|
|
1935
|
+
};
|
|
1936
|
+
}
|
|
1937
|
+
let body;
|
|
1938
|
+
try {
|
|
1939
|
+
body = await readBody(res, maxChars, charsetDecoder(contentTypeHeader));
|
|
1940
|
+
} catch (err) {
|
|
1941
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1942
|
+
return { content: `ERROR: failed to read response body: ${message}`, isError: true };
|
|
1943
|
+
}
|
|
1944
|
+
if (body.startsWith("%PDF")) {
|
|
1945
|
+
return pdfError(current);
|
|
1946
|
+
}
|
|
1947
|
+
const text = isHtml ? htmlToText(body, current.href) : body;
|
|
1948
|
+
return { content: truncate(text, maxChars) };
|
|
1949
|
+
}
|
|
1950
|
+
};
|
|
1951
|
+
}
|
|
1952
|
+
var webFetchTool = createWebFetchTool();
|
|
1953
|
+
|
|
1954
|
+
export {
|
|
1955
|
+
defaultAgentTasks,
|
|
1956
|
+
formatAgentTaskUpdate,
|
|
1957
|
+
responseBodyOf,
|
|
1958
|
+
MAX_IMAGE_DIMENSION,
|
|
1959
|
+
probeImageDimensions,
|
|
1960
|
+
isOversizedImageError,
|
|
1961
|
+
isVisionUnsupportedError,
|
|
1962
|
+
stripOversizedImages,
|
|
1963
|
+
stripAllImages,
|
|
1964
|
+
downsampleImageIfNeeded,
|
|
1965
|
+
registerSensitivePatterns,
|
|
1966
|
+
isSensitivePath,
|
|
1967
|
+
SKIP_DIRS,
|
|
1968
|
+
WALK_CONCURRENCY,
|
|
1969
|
+
walkFiles,
|
|
1970
|
+
matchesGlob,
|
|
1971
|
+
createIgnorePredicate,
|
|
1972
|
+
starHome,
|
|
1973
|
+
globalConfigPath,
|
|
1974
|
+
sessionsDir,
|
|
1975
|
+
userMemoryPath,
|
|
1976
|
+
gitTreesDir,
|
|
1977
|
+
envFilePath,
|
|
1978
|
+
projectConfigPath,
|
|
1979
|
+
setWebFetchAllowPrivateHosts,
|
|
1980
|
+
decodeEntities,
|
|
1981
|
+
webFetchTool,
|
|
1982
|
+
loadEnvFile,
|
|
1983
|
+
upsertEnvContent,
|
|
1984
|
+
MAX_OUTPUT,
|
|
1985
|
+
resolveShell,
|
|
1986
|
+
bashTool,
|
|
1987
|
+
defaultTaskManager,
|
|
1988
|
+
MAX_SNAPSHOT_CONTENT_BYTES,
|
|
1989
|
+
setSnapshotHooks,
|
|
1990
|
+
nextSnapshotId,
|
|
1991
|
+
beginTurn,
|
|
1992
|
+
currentTurnSeq,
|
|
1993
|
+
currentTurnMessageIndex,
|
|
1994
|
+
captureSnapshot,
|
|
1995
|
+
pushSnapshot,
|
|
1996
|
+
listSnapshots,
|
|
1997
|
+
clearSnapshots,
|
|
1998
|
+
hydrateSnapshots,
|
|
1999
|
+
listTurnSnapshots,
|
|
2000
|
+
resolveSnapshotContent,
|
|
2001
|
+
undoTurnSnapshots,
|
|
2002
|
+
dropTurnSnapshots,
|
|
2003
|
+
rewindToSnapshot,
|
|
2004
|
+
setTodoPersistGuard,
|
|
2005
|
+
setTodoSession,
|
|
2006
|
+
TodoStore,
|
|
2007
|
+
formatTodos,
|
|
2008
|
+
parseTodoArgs,
|
|
2009
|
+
pendingTodoTitles,
|
|
2010
|
+
loadTodos,
|
|
2011
|
+
resetTodos,
|
|
2012
|
+
createTodoTools
|
|
2013
|
+
};
|
|
2014
|
+
//# sourceMappingURL=chunk-A5PXPWHB.js.map
|