@cryer/star-cli 0.3.0 → 0.3.2
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 +17 -9
- package/dist/{chunk-DZWTIMJ2.js → chunk-BV4PLM6X.js} +318 -86
- package/dist/chunk-BV4PLM6X.js.map +1 -0
- package/dist/chunk-LADQCYKL.js +113 -0
- package/dist/chunk-LADQCYKL.js.map +1 -0
- package/dist/{loop-EIODKYPT.js → loop-XKC5HQBT.js} +3 -2
- package/dist/main.js +1748 -406
- package/dist/main.js.map +1 -1
- package/dist/stream-OY2MKWYE.js +166 -0
- package/dist/stream-OY2MKWYE.js.map +1 -0
- package/package.json +1 -1
- package/dist/chunk-DZWTIMJ2.js.map +0 -1
- package/dist/stream-Y55NRF2S.js +0 -82
- package/dist/stream-Y55NRF2S.js.map +0 -1
- /package/dist/{loop-EIODKYPT.js.map → loop-XKC5HQBT.js.map} +0 -0
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
computeRetryDelayMs,
|
|
4
|
+
isRetryableStreamError,
|
|
5
|
+
starHome,
|
|
6
|
+
summarizeStreamError,
|
|
7
|
+
userMemoryPath
|
|
8
|
+
} from "./chunk-LADQCYKL.js";
|
|
2
9
|
|
|
3
10
|
// src/agent/loop.ts
|
|
4
11
|
import { tool as aiTool, generateText as generateText3 } from "ai";
|
|
@@ -235,6 +242,133 @@ ${unstaged}`);
|
|
|
235
242
|
return { status, diff: text, truncated };
|
|
236
243
|
}
|
|
237
244
|
|
|
245
|
+
// src/core/image.ts
|
|
246
|
+
var MAX_IMAGE_DIMENSION = 2e3;
|
|
247
|
+
var OVERSIZED_IMAGE_FALLBACK_BYTES = 5 * 1024 * 1024;
|
|
248
|
+
var IMAGE_REMOVED_PLACEHOLDER = "[image removed: too large for the model]";
|
|
249
|
+
var PNG_SIGNATURE = [137, 80, 78, 71, 13, 10, 26, 10];
|
|
250
|
+
function probePng(buf) {
|
|
251
|
+
if (buf.length < 24) return null;
|
|
252
|
+
for (let i = 0; i < PNG_SIGNATURE.length; i++) {
|
|
253
|
+
if (buf[i] !== PNG_SIGNATURE[i]) return null;
|
|
254
|
+
}
|
|
255
|
+
if (buf.toString("ascii", 12, 16) !== "IHDR") return null;
|
|
256
|
+
return { width: buf.readUInt32BE(16), height: buf.readUInt32BE(20) };
|
|
257
|
+
}
|
|
258
|
+
var JPEG_SOF_MARKERS = /* @__PURE__ */ new Set([
|
|
259
|
+
192,
|
|
260
|
+
193,
|
|
261
|
+
194,
|
|
262
|
+
195,
|
|
263
|
+
197,
|
|
264
|
+
198,
|
|
265
|
+
199,
|
|
266
|
+
201,
|
|
267
|
+
202,
|
|
268
|
+
203,
|
|
269
|
+
205,
|
|
270
|
+
206,
|
|
271
|
+
207
|
|
272
|
+
]);
|
|
273
|
+
function probeJpeg(buf) {
|
|
274
|
+
if (buf.length < 4 || buf[0] !== 255 || buf[1] !== 216) return null;
|
|
275
|
+
let offset = 2;
|
|
276
|
+
while (offset + 3 < buf.length) {
|
|
277
|
+
if (buf[offset] !== 255) return null;
|
|
278
|
+
const marker = buf[offset + 1];
|
|
279
|
+
if (marker === void 0) return null;
|
|
280
|
+
if (marker === 216 || marker === 1 || marker >= 208 && marker <= 215) {
|
|
281
|
+
offset += 2;
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
const length = buf.readUInt16BE(offset + 2);
|
|
285
|
+
if (length < 2) return null;
|
|
286
|
+
if (JPEG_SOF_MARKERS.has(marker)) {
|
|
287
|
+
if (offset + 9 > buf.length) return null;
|
|
288
|
+
return { height: buf.readUInt16BE(offset + 5), width: buf.readUInt16BE(offset + 7) };
|
|
289
|
+
}
|
|
290
|
+
offset += 2 + length;
|
|
291
|
+
}
|
|
292
|
+
return null;
|
|
293
|
+
}
|
|
294
|
+
function probeGif(buf) {
|
|
295
|
+
if (buf.length < 10) return null;
|
|
296
|
+
const signature = buf.toString("ascii", 0, 6);
|
|
297
|
+
if (signature !== "GIF87a" && signature !== "GIF89a") return null;
|
|
298
|
+
return { width: buf.readUInt16LE(6), height: buf.readUInt16LE(8) };
|
|
299
|
+
}
|
|
300
|
+
function probeWebp(buf) {
|
|
301
|
+
if (buf.length < 30) return null;
|
|
302
|
+
if (buf.toString("ascii", 0, 4) !== "RIFF" || buf.toString("ascii", 8, 12) !== "WEBP") {
|
|
303
|
+
return null;
|
|
304
|
+
}
|
|
305
|
+
const chunk = buf.toString("ascii", 12, 16);
|
|
306
|
+
if (chunk === "VP8X") {
|
|
307
|
+
return { width: 1 + buf.readUIntLE(24, 3), height: 1 + buf.readUIntLE(27, 3) };
|
|
308
|
+
}
|
|
309
|
+
if (chunk === "VP8 ") {
|
|
310
|
+
return {
|
|
311
|
+
width: buf.readUInt16LE(26) & 16383,
|
|
312
|
+
height: buf.readUInt16LE(28) & 16383
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
if (chunk === "VP8L") {
|
|
316
|
+
if (buf[20] !== 47) return null;
|
|
317
|
+
const b1 = buf[21];
|
|
318
|
+
const b2 = buf[22];
|
|
319
|
+
const b3 = buf[23];
|
|
320
|
+
const b4 = buf[24];
|
|
321
|
+
return {
|
|
322
|
+
width: 1 + ((b2 & 63) << 8 | b1),
|
|
323
|
+
height: 1 + ((b4 & 15) << 10 | b3 << 2 | (b2 & 192) >> 6)
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
return null;
|
|
327
|
+
}
|
|
328
|
+
function probeImageDimensions(buf) {
|
|
329
|
+
return probePng(buf) ?? probeJpeg(buf) ?? probeGif(buf) ?? probeWebp(buf);
|
|
330
|
+
}
|
|
331
|
+
var IMAGE_SIZE_KEYWORDS = /too[\s-]*(large|big)|exceeds?|dimensions?|resolution|size\s*limit|max(?:imum)?[\s-]*size/i;
|
|
332
|
+
function isOversizedImageError(error) {
|
|
333
|
+
const status = error.statusCode;
|
|
334
|
+
if (typeof status !== "number" || status < 400 || status >= 500) return false;
|
|
335
|
+
return /image/i.test(error.message) && IMAGE_SIZE_KEYWORDS.test(error.message);
|
|
336
|
+
}
|
|
337
|
+
function imagePartBytes(image) {
|
|
338
|
+
if (typeof image === "string") {
|
|
339
|
+
const base64 = image.startsWith("data:") ? image.slice(image.indexOf(",") + 1) : image;
|
|
340
|
+
try {
|
|
341
|
+
return Buffer.from(base64, "base64");
|
|
342
|
+
} catch {
|
|
343
|
+
return null;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
if (image instanceof Uint8Array) return Buffer.from(image);
|
|
347
|
+
return null;
|
|
348
|
+
}
|
|
349
|
+
function isOversizedImageData(buf) {
|
|
350
|
+
const dims = probeImageDimensions(buf);
|
|
351
|
+
if (dims) return Math.max(dims.width, dims.height) > MAX_IMAGE_DIMENSION;
|
|
352
|
+
return buf.length > OVERSIZED_IMAGE_FALLBACK_BYTES;
|
|
353
|
+
}
|
|
354
|
+
function stripOversizedImages(messages) {
|
|
355
|
+
let removed = 0;
|
|
356
|
+
const result = messages.map((message) => {
|
|
357
|
+
if (!Array.isArray(message.content)) return message;
|
|
358
|
+
let changed = false;
|
|
359
|
+
const content = message.content.map((part) => {
|
|
360
|
+
if (part.type !== "image") return part;
|
|
361
|
+
const bytes = imagePartBytes(part.image);
|
|
362
|
+
if (!bytes || !isOversizedImageData(bytes)) return part;
|
|
363
|
+
changed = true;
|
|
364
|
+
removed++;
|
|
365
|
+
return { type: "text", text: IMAGE_REMOVED_PLACEHOLDER };
|
|
366
|
+
});
|
|
367
|
+
return changed ? { ...message, content } : message;
|
|
368
|
+
});
|
|
369
|
+
return { messages: removed > 0 ? result : messages, removed };
|
|
370
|
+
}
|
|
371
|
+
|
|
238
372
|
// src/core/messages.ts
|
|
239
373
|
var MISSING_TOOL_RESULT_TEXT = "tool result missing (session was interrupted)";
|
|
240
374
|
function reconcileToolCalls(messages) {
|
|
@@ -664,6 +798,12 @@ async function revert(snapshot) {
|
|
|
664
798
|
await rm(snapshot.path, { force: true });
|
|
665
799
|
return `Deleted ${snapshot.path} (it did not exist before ${snapshot.toolName}).`;
|
|
666
800
|
}
|
|
801
|
+
function listTurnSnapshots(turn) {
|
|
802
|
+
return stack.filter((snapshot) => snapshot.turn === turn).reverse();
|
|
803
|
+
}
|
|
804
|
+
function resolveSnapshotContent(snapshot) {
|
|
805
|
+
return resolveContent(snapshot);
|
|
806
|
+
}
|
|
667
807
|
async function undoTurnSnapshots(turn) {
|
|
668
808
|
const matches = [];
|
|
669
809
|
for (let i = stack.length - 1; i >= 0; i--) {
|
|
@@ -778,6 +918,9 @@ async function loadTodos(cwd) {
|
|
|
778
918
|
await defaultStore.load(cwd);
|
|
779
919
|
return defaultStore.list();
|
|
780
920
|
}
|
|
921
|
+
function resetTodos() {
|
|
922
|
+
defaultStore.replace([]);
|
|
923
|
+
}
|
|
781
924
|
function createTodoTools(store = defaultStore) {
|
|
782
925
|
const todoWrite = {
|
|
783
926
|
name: "todo_write",
|
|
@@ -789,10 +932,11 @@ function createTodoTools(store = defaultStore) {
|
|
|
789
932
|
if (!parsed.success) {
|
|
790
933
|
return { content: `Invalid todos: ${parsed.error.message}`, isError: true };
|
|
791
934
|
}
|
|
792
|
-
await store.load(ctx.cwd);
|
|
793
935
|
store.replace(parsed.data.todos);
|
|
794
936
|
await store.save(ctx.cwd);
|
|
795
|
-
|
|
937
|
+
const items = store.list();
|
|
938
|
+
const done = items.filter((it) => it.status === "done").length;
|
|
939
|
+
return { content: `Todo list updated (${done}/${items.length} done).` };
|
|
796
940
|
}
|
|
797
941
|
};
|
|
798
942
|
const todoRead = {
|
|
@@ -800,8 +944,7 @@ function createTodoTools(store = defaultStore) {
|
|
|
800
944
|
description: "Read the current todo list.",
|
|
801
945
|
permission: "read",
|
|
802
946
|
parameters: readSchema,
|
|
803
|
-
async execute(
|
|
804
|
-
await store.load(ctx.cwd);
|
|
947
|
+
async execute() {
|
|
805
948
|
return { content: formatTodos(store.list()) };
|
|
806
949
|
}
|
|
807
950
|
};
|
|
@@ -936,29 +1079,8 @@ ${body}`;
|
|
|
936
1079
|
|
|
937
1080
|
// src/agent/skills.ts
|
|
938
1081
|
import { readFileSync as readFileSync2, readdirSync, statSync as statSync2 } from "fs";
|
|
939
|
-
import path6 from "path";
|
|
940
|
-
import { z as z2 } from "zod";
|
|
941
|
-
|
|
942
|
-
// src/config/paths.ts
|
|
943
|
-
import os from "os";
|
|
944
1082
|
import path5 from "path";
|
|
945
|
-
|
|
946
|
-
return process.env.STAR_HOME ?? path5.join(os.homedir(), ".star-cli");
|
|
947
|
-
}
|
|
948
|
-
function globalConfigPath() {
|
|
949
|
-
return path5.join(starHome(), "config.toml");
|
|
950
|
-
}
|
|
951
|
-
function sessionsDir() {
|
|
952
|
-
return path5.join(starHome(), "sessions");
|
|
953
|
-
}
|
|
954
|
-
function userMemoryPath() {
|
|
955
|
-
return path5.join(starHome(), "MEMORY.md");
|
|
956
|
-
}
|
|
957
|
-
function projectConfigPath(cwd) {
|
|
958
|
-
return path5.join(cwd, ".star", "config.toml");
|
|
959
|
-
}
|
|
960
|
-
|
|
961
|
-
// src/agent/skills.ts
|
|
1083
|
+
import { z as z2 } from "zod";
|
|
962
1084
|
var SKILL_FILE = "SKILL.md";
|
|
963
1085
|
var SKILL_MAX_CHARS = 32 * 1024;
|
|
964
1086
|
var NAME_PATTERN = /^[a-z0-9-]+$/;
|
|
@@ -988,7 +1110,7 @@ function parseSkillFile(filePath, dirName, scope) {
|
|
|
988
1110
|
const name = parsed?.meta.name || dirName;
|
|
989
1111
|
const description = parsed?.meta.description;
|
|
990
1112
|
if (parsed && NAME_PATTERN.test(name) && description) {
|
|
991
|
-
skill = { name, description, filePath, dir:
|
|
1113
|
+
skill = { name, description, filePath, dir: path5.dirname(filePath), scope };
|
|
992
1114
|
}
|
|
993
1115
|
} catch {
|
|
994
1116
|
skill = null;
|
|
@@ -1005,14 +1127,14 @@ function scanDir(base, scope, into) {
|
|
|
1005
1127
|
}
|
|
1006
1128
|
for (const entry of entries.sort()) {
|
|
1007
1129
|
if (!NAME_PATTERN.test(entry)) continue;
|
|
1008
|
-
const parsed = parseSkillFile(
|
|
1130
|
+
const parsed = parseSkillFile(path5.join(base, entry, SKILL_FILE), entry, scope);
|
|
1009
1131
|
if (parsed) into.set(parsed.name, parsed);
|
|
1010
1132
|
}
|
|
1011
1133
|
}
|
|
1012
1134
|
function discoverSkills(cwd, home = starHome()) {
|
|
1013
1135
|
const byName = /* @__PURE__ */ new Map();
|
|
1014
|
-
scanDir(
|
|
1015
|
-
scanDir(
|
|
1136
|
+
scanDir(path5.join(home, "skills"), "user", byName);
|
|
1137
|
+
scanDir(path5.join(cwd, ".star", "skills"), "project", byName);
|
|
1016
1138
|
return [...byName.values()];
|
|
1017
1139
|
}
|
|
1018
1140
|
function formatSkillsBlock(skills) {
|
|
@@ -1079,7 +1201,7 @@ import { z as z12 } from "zod";
|
|
|
1079
1201
|
// src/tools/bash.ts
|
|
1080
1202
|
import { spawn as spawn3 } from "child_process";
|
|
1081
1203
|
import { existsSync } from "fs";
|
|
1082
|
-
import
|
|
1204
|
+
import path6 from "path";
|
|
1083
1205
|
import { z as z3 } from "zod";
|
|
1084
1206
|
|
|
1085
1207
|
// src/tasks/manager.ts
|
|
@@ -1199,11 +1321,11 @@ var MAX_OUTPUT = 3e4;
|
|
|
1199
1321
|
var DEFAULT_TIMEOUT = 120;
|
|
1200
1322
|
var MAX_TIMEOUT = 600;
|
|
1201
1323
|
function findOnPath(exe, exclude) {
|
|
1202
|
-
for (const dir of (process.env.PATH ?? "").split(
|
|
1324
|
+
for (const dir of (process.env.PATH ?? "").split(path6.delimiter)) {
|
|
1203
1325
|
if (!dir || exclude?.(dir)) {
|
|
1204
1326
|
continue;
|
|
1205
1327
|
}
|
|
1206
|
-
const full =
|
|
1328
|
+
const full = path6.join(dir, exe);
|
|
1207
1329
|
if (existsSync(full)) {
|
|
1208
1330
|
return full;
|
|
1209
1331
|
}
|
|
@@ -1221,7 +1343,7 @@ function resolveShell() {
|
|
|
1221
1343
|
}
|
|
1222
1344
|
const gitExe = findOnPath("git.exe");
|
|
1223
1345
|
if (gitExe) {
|
|
1224
|
-
const sibling =
|
|
1346
|
+
const sibling = path6.join(path6.dirname(path6.dirname(gitExe)), "bin", "bash.exe");
|
|
1225
1347
|
if (existsSync(sibling)) {
|
|
1226
1348
|
return { shell: sibling, wrap: (c) => ["-c", c], label: "bash" };
|
|
1227
1349
|
}
|
|
@@ -1345,7 +1467,7 @@ Command timed out after ${timeoutSeconds}s`,
|
|
|
1345
1467
|
|
|
1346
1468
|
// src/tools/fs/edit.ts
|
|
1347
1469
|
import { readFile as readFile3, writeFile as writeFile3 } from "fs/promises";
|
|
1348
|
-
import
|
|
1470
|
+
import path7 from "path";
|
|
1349
1471
|
import { z as z4 } from "zod";
|
|
1350
1472
|
var schema2 = z4.object({
|
|
1351
1473
|
path: z4.string().describe("File path, absolute or relative to the working directory"),
|
|
@@ -1359,7 +1481,7 @@ var editFileTool = {
|
|
|
1359
1481
|
permission: "write",
|
|
1360
1482
|
parameters: schema2,
|
|
1361
1483
|
async execute(args, ctx) {
|
|
1362
|
-
const filePath =
|
|
1484
|
+
const filePath = path7.resolve(ctx.cwd, args.path);
|
|
1363
1485
|
let content;
|
|
1364
1486
|
try {
|
|
1365
1487
|
content = await readFile3(filePath, "utf8");
|
|
@@ -1402,13 +1524,13 @@ var editFileTool = {
|
|
|
1402
1524
|
};
|
|
1403
1525
|
|
|
1404
1526
|
// src/tools/fs/glob.ts
|
|
1405
|
-
import
|
|
1527
|
+
import path9 from "path";
|
|
1406
1528
|
import { z as z5 } from "zod";
|
|
1407
1529
|
|
|
1408
1530
|
// src/tools/fs/util.ts
|
|
1409
1531
|
import { readFileSync as readFileSync3, statSync as statSync3 } from "fs";
|
|
1410
1532
|
import { readdir, stat } from "fs/promises";
|
|
1411
|
-
import
|
|
1533
|
+
import path8 from "path";
|
|
1412
1534
|
var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist"]);
|
|
1413
1535
|
async function walkFiles(root, skipDirs = SKIP_DIRS, options = {}) {
|
|
1414
1536
|
const out = [];
|
|
@@ -1418,7 +1540,7 @@ async function walkFiles(root, skipDirs = SKIP_DIRS, options = {}) {
|
|
|
1418
1540
|
return;
|
|
1419
1541
|
}
|
|
1420
1542
|
for (const entry of entries) {
|
|
1421
|
-
const abs =
|
|
1543
|
+
const abs = path8.join(dir, entry.name);
|
|
1422
1544
|
const rel = relBase ? `${relBase}/${entry.name}` : entry.name;
|
|
1423
1545
|
if (entry.isDirectory()) {
|
|
1424
1546
|
if (skipDirs.has(entry.name) || options.ignore?.(abs, true)) {
|
|
@@ -1498,7 +1620,7 @@ function parseIgnorePatterns(body) {
|
|
|
1498
1620
|
return out;
|
|
1499
1621
|
}
|
|
1500
1622
|
function loadIgnorePatterns(dir) {
|
|
1501
|
-
const file =
|
|
1623
|
+
const file = path8.join(dir, IGNORE_FILE);
|
|
1502
1624
|
let mtimeMs;
|
|
1503
1625
|
try {
|
|
1504
1626
|
mtimeMs = statSync3(file).mtimeMs;
|
|
@@ -1523,9 +1645,9 @@ function createIgnorePredicate(cwd) {
|
|
|
1523
1645
|
if (patterns.length === 0) {
|
|
1524
1646
|
return void 0;
|
|
1525
1647
|
}
|
|
1526
|
-
const root =
|
|
1648
|
+
const root = path8.resolve(cwd);
|
|
1527
1649
|
return (absPath, isDir) => {
|
|
1528
|
-
const rel =
|
|
1650
|
+
const rel = path8.relative(root, absPath).split(path8.sep).join("/");
|
|
1529
1651
|
if (!rel || rel.startsWith("..")) {
|
|
1530
1652
|
return false;
|
|
1531
1653
|
}
|
|
@@ -1541,7 +1663,7 @@ function createIgnorePredicate(cwd) {
|
|
|
1541
1663
|
};
|
|
1542
1664
|
}
|
|
1543
1665
|
function isSensitivePath2(filePath) {
|
|
1544
|
-
const base =
|
|
1666
|
+
const base = path8.basename(filePath);
|
|
1545
1667
|
if (base === ".env.example" || base === ".env.sample" || base === ".env.template") {
|
|
1546
1668
|
return false;
|
|
1547
1669
|
}
|
|
@@ -1568,7 +1690,7 @@ var globTool = {
|
|
|
1568
1690
|
permission: "read",
|
|
1569
1691
|
parameters: schema3,
|
|
1570
1692
|
async execute(args, ctx) {
|
|
1571
|
-
const root =
|
|
1693
|
+
const root = path9.resolve(ctx.cwd, args.path ?? ".");
|
|
1572
1694
|
const files = await walkFiles(root, void 0, { ignore: createIgnorePredicate(ctx.cwd) });
|
|
1573
1695
|
const matched = files.filter((f) => matchesGlob(args.pattern, f.rel)).sort((a, b) => b.mtimeMs - a.mtimeMs).slice(0, MAX_RESULTS);
|
|
1574
1696
|
if (matched.length === 0) {
|
|
@@ -1580,7 +1702,7 @@ var globTool = {
|
|
|
1580
1702
|
|
|
1581
1703
|
// src/tools/fs/grep.ts
|
|
1582
1704
|
import { readFile as readFile4, stat as stat2 } from "fs/promises";
|
|
1583
|
-
import
|
|
1705
|
+
import path10 from "path";
|
|
1584
1706
|
import { z as z6 } from "zod";
|
|
1585
1707
|
var MAX_MATCHES = 250;
|
|
1586
1708
|
var schema4 = z6.object({
|
|
@@ -1601,12 +1723,12 @@ var grepTool = {
|
|
|
1601
1723
|
} catch (err) {
|
|
1602
1724
|
return { content: `Invalid regular expression: ${err.message}`, isError: true };
|
|
1603
1725
|
}
|
|
1604
|
-
const root =
|
|
1726
|
+
const root = path10.resolve(ctx.cwd, args.path ?? ".");
|
|
1605
1727
|
let files;
|
|
1606
1728
|
try {
|
|
1607
1729
|
const st = await stat2(root);
|
|
1608
1730
|
if (st.isFile()) {
|
|
1609
|
-
files = [{ abs: root, rel:
|
|
1731
|
+
files = [{ abs: root, rel: path10.basename(root), mtimeMs: st.mtimeMs }];
|
|
1610
1732
|
} else {
|
|
1611
1733
|
files = await walkFiles(root, void 0, { ignore: createIgnorePredicate(ctx.cwd) });
|
|
1612
1734
|
}
|
|
@@ -1653,7 +1775,7 @@ var grepTool = {
|
|
|
1653
1775
|
|
|
1654
1776
|
// src/tools/fs/read.ts
|
|
1655
1777
|
import { readFile as readFile5, stat as stat3 } from "fs/promises";
|
|
1656
|
-
import
|
|
1778
|
+
import path11 from "path";
|
|
1657
1779
|
import { z as z7 } from "zod";
|
|
1658
1780
|
var DEFAULT_LIMIT = 2e3;
|
|
1659
1781
|
var MAX_CHARS = 100 * 1024;
|
|
@@ -1668,7 +1790,7 @@ var readFileTool = {
|
|
|
1668
1790
|
permission: "read",
|
|
1669
1791
|
parameters: schema5,
|
|
1670
1792
|
async execute(args, ctx) {
|
|
1671
|
-
const filePath =
|
|
1793
|
+
const filePath = path11.resolve(ctx.cwd, args.path);
|
|
1672
1794
|
if (isSensitivePath2(filePath)) {
|
|
1673
1795
|
return { content: `Refused to read sensitive file: ${args.path}`, isError: true };
|
|
1674
1796
|
}
|
|
@@ -1714,7 +1836,7 @@ var readFileTool = {
|
|
|
1714
1836
|
|
|
1715
1837
|
// src/tools/fs/write.ts
|
|
1716
1838
|
import { mkdir as mkdir3, writeFile as writeFile4 } from "fs/promises";
|
|
1717
|
-
import
|
|
1839
|
+
import path12 from "path";
|
|
1718
1840
|
import { z as z8 } from "zod";
|
|
1719
1841
|
var schema6 = z8.object({
|
|
1720
1842
|
path: z8.string().describe("File path, absolute or relative to the working directory"),
|
|
@@ -1726,10 +1848,10 @@ var writeFileTool = {
|
|
|
1726
1848
|
permission: "write",
|
|
1727
1849
|
parameters: schema6,
|
|
1728
1850
|
async execute(args, ctx) {
|
|
1729
|
-
const filePath =
|
|
1851
|
+
const filePath = path12.resolve(ctx.cwd, args.path);
|
|
1730
1852
|
const snapshot = await captureSnapshot(filePath, "write_file");
|
|
1731
1853
|
try {
|
|
1732
|
-
await mkdir3(
|
|
1854
|
+
await mkdir3(path12.dirname(filePath), { recursive: true });
|
|
1733
1855
|
await writeFile4(filePath, args.content, "utf8");
|
|
1734
1856
|
} catch (err) {
|
|
1735
1857
|
return { content: `Failed to write ${args.path}: ${err.message}`, isError: true };
|
|
@@ -2155,7 +2277,7 @@ var MAX_SUBAGENT_DEPTH = 1;
|
|
|
2155
2277
|
var MAX_REPORT_CHARS = 2e4;
|
|
2156
2278
|
var SUBAGENT_PROMPT = "You are a subagent spawned to handle a focused subtask on behalf of the main agent. Work autonomously with the tools available to you, then finish with a concise report of what you found or did. Your final text is returned to the main agent as the tool result \u2014 make it self-contained.";
|
|
2157
2279
|
async function runSubagent(deps, args, cwd, signal) {
|
|
2158
|
-
const { AgentLoop: AgentLoop2 } = await import("./loop-
|
|
2280
|
+
const { AgentLoop: AgentLoop2 } = await import("./loop-XKC5HQBT.js");
|
|
2159
2281
|
const child = new AgentLoop2({
|
|
2160
2282
|
model: deps.model,
|
|
2161
2283
|
registry: createDefaultRegistry(),
|
|
@@ -2240,13 +2362,13 @@ function createSubagentTool(deps) {
|
|
|
2240
2362
|
|
|
2241
2363
|
// src/agent/user-memory.ts
|
|
2242
2364
|
import { appendFileSync, mkdirSync, readFileSync as readFileSync4, statSync as statSync4 } from "fs";
|
|
2243
|
-
import
|
|
2365
|
+
import path13 from "path";
|
|
2244
2366
|
import { z as z13 } from "zod";
|
|
2245
2367
|
var USER_MEMORY_MAX_CHARS = 32 * 1024;
|
|
2246
2368
|
var REMEMBER_TEXT_MAX_CHARS = 500;
|
|
2247
2369
|
var cache3 = /* @__PURE__ */ new Map();
|
|
2248
2370
|
function readUserMemory(home = starHome()) {
|
|
2249
|
-
const file =
|
|
2371
|
+
const file = path13.join(home, "MEMORY.md");
|
|
2250
2372
|
let mtimeMs;
|
|
2251
2373
|
try {
|
|
2252
2374
|
mtimeMs = statSync4(file).mtimeMs;
|
|
@@ -2278,7 +2400,7 @@ function readUserMemory(home = starHome()) {
|
|
|
2278
2400
|
}
|
|
2279
2401
|
function appendUserMemory(text, home = starHome()) {
|
|
2280
2402
|
mkdirSync(home, { recursive: true });
|
|
2281
|
-
appendFileSync(
|
|
2403
|
+
appendFileSync(path13.join(home, "MEMORY.md"), `- ${text}
|
|
2282
2404
|
`, "utf8");
|
|
2283
2405
|
}
|
|
2284
2406
|
function createRememberTool(deps = {}) {
|
|
@@ -2300,6 +2422,16 @@ function createRememberTool(deps = {}) {
|
|
|
2300
2422
|
var PLAN_MODE_PROMPT = "\n\nYou are currently in PLAN MODE. Research the task using only the read-only tools available to you, then present a concrete, step-by-step implementation plan as your final answer. You must not modify files, run shell commands, or otherwise change the system \u2014 write/exec tools are unavailable in this mode. Do not ask the user to run commands for you; note manual steps in the plan instead. The user will review your plan and approve it before any execution begins.";
|
|
2301
2423
|
var PENDING_WORK_PATTERN = /(?:我将|我会|我现在|接下来|下一步|下面(?:我|将)|稍后|随后|准备开始|现在开始|马上|即将|这就|待会|开始(?:执行|进行|处理|转换|动手|生成|写入|运行|创建)|(?:完成后|然后|接着)会|让我(?:们)?(?:来)?|I will|I'll|I am going to|I'm going to|I shall|let me|next,? I|I will now)/i;
|
|
2302
2424
|
var AUTO_CONTINUE_NUDGE = "[auto-continue] You ended your turn with words instead of actions. Do not describe or restate the plan \u2014 continue the task NOW by calling tools. Reply with text only if the task is already fully complete.";
|
|
2425
|
+
var EMPTY_REPLY_NUDGE = "[auto-continue] Your previous reply was empty \u2014 no text and no tool calls. If the task is fully complete, reply with one short confirmation; otherwise continue NOW by calling tools.";
|
|
2426
|
+
function isFilteredEmptyFinish(finishReason) {
|
|
2427
|
+
return finishReason === "content-filter";
|
|
2428
|
+
}
|
|
2429
|
+
function isSteerableEmptyFinish(finishReason) {
|
|
2430
|
+
return finishReason === "stop" || finishReason === "content-filter";
|
|
2431
|
+
}
|
|
2432
|
+
function finalNudgeWarning(autoContinues, maxAutoContinues) {
|
|
2433
|
+
return autoContinues >= maxAutoContinues ? " This is the final automatic continuation \u2014 if your next reply again has no tool calls, nudging stops and the turn is handed back to the user. Finish the work now or summarize what remains." : "";
|
|
2434
|
+
}
|
|
2303
2435
|
var COMPLETION_CHECK_TIMEOUT_MS = 3e4;
|
|
2304
2436
|
async function checkTaskComplete(model, request, finalReply, signal) {
|
|
2305
2437
|
const controller = new AbortController();
|
|
@@ -2335,21 +2467,6 @@ ${finalReply.slice(0, 2e3)}
|
|
|
2335
2467
|
}
|
|
2336
2468
|
}
|
|
2337
2469
|
var STREAM_RETRY_BASE_DELAY_MS = 1e3;
|
|
2338
|
-
var NON_RETRYABLE_ERROR_NAMES = /* @__PURE__ */ new Set([
|
|
2339
|
-
"AI_NoSuchToolError",
|
|
2340
|
-
"AI_InvalidToolArgumentsError",
|
|
2341
|
-
"AI_InvalidPromptError",
|
|
2342
|
-
"AI_NoSuchModelError",
|
|
2343
|
-
"AI_LoadAPIKeyError"
|
|
2344
|
-
]);
|
|
2345
|
-
function isRetryableStreamError(error) {
|
|
2346
|
-
if (NON_RETRYABLE_ERROR_NAMES.has(error.name)) return false;
|
|
2347
|
-
const status = error.statusCode;
|
|
2348
|
-
if (typeof status === "number") {
|
|
2349
|
-
return status === 408 || status === 429 || status >= 500;
|
|
2350
|
-
}
|
|
2351
|
-
return true;
|
|
2352
|
-
}
|
|
2353
2470
|
function sleep(ms, signal) {
|
|
2354
2471
|
if (signal.aborted) return Promise.resolve(false);
|
|
2355
2472
|
return new Promise((resolve) => {
|
|
@@ -2434,6 +2551,18 @@ var AgentLoop = class {
|
|
|
2434
2551
|
this.messages = reconcileToolCalls(messages);
|
|
2435
2552
|
this.turnMarkers = [];
|
|
2436
2553
|
}
|
|
2554
|
+
// Read-only counterpart of retractLastTurn: how many messages /undo would
|
|
2555
|
+
// drop and which snapshot turn's file changes it would revert (undefined =
|
|
2556
|
+
// no verified turn, so no file snapshots may be reverted). Mutates nothing,
|
|
2557
|
+
// so the REPL can show the preview before the user confirms.
|
|
2558
|
+
previewLastTurnRetraction() {
|
|
2559
|
+
const result = retractLastTurn([...this.messages]);
|
|
2560
|
+
if (result.removed === 0) return { removed: 0 };
|
|
2561
|
+
const userIndex = result.messages.length;
|
|
2562
|
+
const last = this.turnMarkers[this.turnMarkers.length - 1];
|
|
2563
|
+
const turn = last && last.userIndex === userIndex ? last.seq : void 0;
|
|
2564
|
+
return { removed: result.removed, turn };
|
|
2565
|
+
}
|
|
2437
2566
|
// Drops the final user message and everything after it, and persists the
|
|
2438
2567
|
// trimmed history so a later /resume does not bring the turn back.
|
|
2439
2568
|
// `turn` is the retracted turn's snapshot seq when it could be verified
|
|
@@ -2487,6 +2616,16 @@ var AgentLoop = class {
|
|
|
2487
2616
|
async persist(message) {
|
|
2488
2617
|
await this.opts.sessionStore?.append(message);
|
|
2489
2618
|
}
|
|
2619
|
+
// Replaces oversized image parts in the in-memory history with text
|
|
2620
|
+
// placeholders and rewrites the persisted session, so the failed request
|
|
2621
|
+
// can be resent and a later resume never carries the offending images.
|
|
2622
|
+
async stripOversizedImages() {
|
|
2623
|
+
const { messages, removed } = stripOversizedImages(this.messages);
|
|
2624
|
+
if (removed === 0) return 0;
|
|
2625
|
+
this.messages = messages;
|
|
2626
|
+
await this.opts.sessionStore?.replaceMessages([...this.messages]);
|
|
2627
|
+
return removed;
|
|
2628
|
+
}
|
|
2490
2629
|
// After the first assistant reply, kick off background title generation
|
|
2491
2630
|
// for the session. Runs once per loop; the store-level title check makes
|
|
2492
2631
|
// resumed sessions that already have a title a no-op.
|
|
@@ -2497,6 +2636,47 @@ var AgentLoop = class {
|
|
|
2497
2636
|
this.titleScheduled = true;
|
|
2498
2637
|
scheduleSessionTitle(store, input, this.opts.model);
|
|
2499
2638
|
}
|
|
2639
|
+
// A user interrupt (Esc) ends the turn mid-stream, before the normal
|
|
2640
|
+
// assistant-message persistence runs. Whatever text and tool calls already
|
|
2641
|
+
// arrived are still persisted — the text marked with "[interrupted]" so the
|
|
2642
|
+
// cut-off stays visible after a resume and the model can tell its reply was
|
|
2643
|
+
// cut short — and streamed tool calls are closed with synthetic results so
|
|
2644
|
+
// the stored history stays valid for the API.
|
|
2645
|
+
async *persistInterrupted(text, toolCalls) {
|
|
2646
|
+
if (text.length === 0 && toolCalls.length === 0) return;
|
|
2647
|
+
const assistantMessage = {
|
|
2648
|
+
role: "assistant",
|
|
2649
|
+
content: [
|
|
2650
|
+
...text ? [{ type: "text", text: `${text} [interrupted]` }] : [],
|
|
2651
|
+
...toolCalls.map((call) => ({
|
|
2652
|
+
type: "tool-call",
|
|
2653
|
+
toolCallId: call.id,
|
|
2654
|
+
toolName: call.name,
|
|
2655
|
+
args: call.args
|
|
2656
|
+
}))
|
|
2657
|
+
]
|
|
2658
|
+
};
|
|
2659
|
+
this.messages.push(assistantMessage);
|
|
2660
|
+
await this.persist(assistantMessage);
|
|
2661
|
+
for (const call of toolCalls) {
|
|
2662
|
+
const content = "Tool execution interrupted by user.";
|
|
2663
|
+
const synthetic = {
|
|
2664
|
+
role: "tool",
|
|
2665
|
+
content: [
|
|
2666
|
+
{
|
|
2667
|
+
type: "tool-result",
|
|
2668
|
+
toolCallId: call.id,
|
|
2669
|
+
toolName: call.name,
|
|
2670
|
+
result: content
|
|
2671
|
+
}
|
|
2672
|
+
]
|
|
2673
|
+
};
|
|
2674
|
+
this.messages.push(synthetic);
|
|
2675
|
+
await this.persist(synthetic).catch(() => {
|
|
2676
|
+
});
|
|
2677
|
+
yield { type: "tool-result", id: call.id, name: call.name, content, isError: true };
|
|
2678
|
+
}
|
|
2679
|
+
}
|
|
2500
2680
|
async *stream(input, signal, opts) {
|
|
2501
2681
|
this.syncSystemMessage();
|
|
2502
2682
|
const inputText = typeof input === "string" ? input : input.text;
|
|
@@ -2547,11 +2727,17 @@ ${finished.result}`
|
|
|
2547
2727
|
let text = "";
|
|
2548
2728
|
const toolCalls = [];
|
|
2549
2729
|
let failure = null;
|
|
2730
|
+
let lastFinishReason;
|
|
2731
|
+
let lastUsage;
|
|
2732
|
+
let sawReasoning = false;
|
|
2733
|
+
let oversizedImagesStripped = false;
|
|
2550
2734
|
for (let attempt = 0; ; attempt++) {
|
|
2551
2735
|
text = "";
|
|
2552
2736
|
toolCalls.length = 0;
|
|
2553
2737
|
failure = null;
|
|
2554
|
-
|
|
2738
|
+
lastFinishReason = void 0;
|
|
2739
|
+
lastUsage = void 0;
|
|
2740
|
+
sawReasoning = false;
|
|
2555
2741
|
const timeoutScale = Math.min(attempt + 1, 3);
|
|
2556
2742
|
try {
|
|
2557
2743
|
for await (const event of this.streamOnce(aiTools, signal, timeoutScale)) {
|
|
@@ -2559,12 +2745,14 @@ ${finished.result}`
|
|
|
2559
2745
|
text += event.text;
|
|
2560
2746
|
yield event;
|
|
2561
2747
|
} else if (event.type === "reasoning") {
|
|
2748
|
+
sawReasoning = true;
|
|
2562
2749
|
yield event;
|
|
2563
2750
|
} else if (event.type === "tool-call") {
|
|
2564
2751
|
toolCalls.push({ id: event.id, name: event.name, args: event.args });
|
|
2565
2752
|
yield event;
|
|
2566
2753
|
} else if (event.type === "finish") {
|
|
2567
|
-
|
|
2754
|
+
lastFinishReason = event.finishReason;
|
|
2755
|
+
lastUsage = event.usage;
|
|
2568
2756
|
yield event;
|
|
2569
2757
|
} else if (event.type === "error") {
|
|
2570
2758
|
failure = event.error;
|
|
@@ -2573,26 +2761,60 @@ ${finished.result}`
|
|
|
2573
2761
|
}
|
|
2574
2762
|
}
|
|
2575
2763
|
} catch (error) {
|
|
2576
|
-
if (signal.aborted)
|
|
2577
|
-
|
|
2764
|
+
if (!signal.aborted) {
|
|
2765
|
+
failure = error instanceof Error ? error : new Error(String(error));
|
|
2766
|
+
}
|
|
2767
|
+
}
|
|
2768
|
+
if (signal.aborted) {
|
|
2769
|
+
yield* this.persistInterrupted(text, toolCalls);
|
|
2770
|
+
return;
|
|
2578
2771
|
}
|
|
2579
|
-
if (signal.aborted) return;
|
|
2580
2772
|
if (!failure && (text.length > 0 || toolCalls.length > 0)) break;
|
|
2773
|
+
if (failure && !oversizedImagesStripped && isOversizedImageError(failure)) {
|
|
2774
|
+
oversizedImagesStripped = true;
|
|
2775
|
+
const removed = await this.stripOversizedImages();
|
|
2776
|
+
if (removed > 0) {
|
|
2777
|
+
yield {
|
|
2778
|
+
type: "notice",
|
|
2779
|
+
message: `Removed ${removed} oversized image(s) the model rejected; retrying the request.`
|
|
2780
|
+
};
|
|
2781
|
+
continue;
|
|
2782
|
+
}
|
|
2783
|
+
}
|
|
2581
2784
|
if (failure && !isRetryableStreamError(failure)) break;
|
|
2582
2785
|
if (attempt >= maxRetries) break;
|
|
2786
|
+
if (!failure && text.length === 0 && toolCalls.length === 0 && attempt >= 1 && (isFilteredEmptyFinish(lastFinishReason) || sawReasoning || lastUsage !== void 0 && lastUsage.completionTokens > 0))
|
|
2787
|
+
break;
|
|
2788
|
+
const delayMs = computeRetryDelayMs(attempt, baseDelay, failure);
|
|
2583
2789
|
yield {
|
|
2584
2790
|
type: "retry",
|
|
2585
2791
|
attempt: attempt + 2,
|
|
2586
2792
|
maxAttempts: maxRetries + 1,
|
|
2587
|
-
|
|
2793
|
+
delayMs,
|
|
2794
|
+
reason: failure ? summarizeStreamError(failure) : `empty response (finish: ${lastFinishReason ?? "none"})`
|
|
2588
2795
|
};
|
|
2589
|
-
if (!await sleep(
|
|
2796
|
+
if (!await sleep(delayMs, signal)) return;
|
|
2590
2797
|
}
|
|
2591
2798
|
if (failure) {
|
|
2592
2799
|
yield { type: "error", error: failure };
|
|
2593
2800
|
return;
|
|
2594
2801
|
}
|
|
2595
2802
|
if (text.length === 0 && toolCalls.length === 0) {
|
|
2803
|
+
if (isSteerableEmptyFinish(lastFinishReason) && config.permissionMode !== "plan" && autoContinues < config.maxAutoContinues) {
|
|
2804
|
+
autoContinues++;
|
|
2805
|
+
lastWasNudge = true;
|
|
2806
|
+
yield {
|
|
2807
|
+
type: "notice",
|
|
2808
|
+
message: `the model returned an empty reply; asking the model to continue (${autoContinues}/${config.maxAutoContinues}).`
|
|
2809
|
+
};
|
|
2810
|
+
const nudge = {
|
|
2811
|
+
role: "user",
|
|
2812
|
+
content: EMPTY_REPLY_NUDGE + finalNudgeWarning(autoContinues, config.maxAutoContinues)
|
|
2813
|
+
};
|
|
2814
|
+
this.messages.push(nudge);
|
|
2815
|
+
await this.persist(nudge);
|
|
2816
|
+
continue;
|
|
2817
|
+
}
|
|
2596
2818
|
yield {
|
|
2597
2819
|
type: "error",
|
|
2598
2820
|
error: new Error("The model returned an empty response after retries; ending the turn.")
|
|
@@ -2637,16 +2859,26 @@ ${finished.result}`
|
|
|
2637
2859
|
type: "notice",
|
|
2638
2860
|
message: `${reason}; asking the model to continue (${autoContinues}/${config.maxAutoContinues}).`
|
|
2639
2861
|
};
|
|
2640
|
-
const nudge = {
|
|
2862
|
+
const nudge = {
|
|
2863
|
+
role: "user",
|
|
2864
|
+
content: nudgeText + finalNudgeWarning(autoContinues, config.maxAutoContinues)
|
|
2865
|
+
};
|
|
2641
2866
|
this.messages.push(nudge);
|
|
2642
2867
|
await this.persist(nudge);
|
|
2643
2868
|
continue;
|
|
2644
2869
|
}
|
|
2870
|
+
if (reason !== null && config.permissionMode !== "plan") {
|
|
2871
|
+
yield {
|
|
2872
|
+
type: "notice",
|
|
2873
|
+
message: `${reason}; auto-continue limit reached, handing the turn back \u2014 reply "continue" to keep going.`
|
|
2874
|
+
};
|
|
2875
|
+
}
|
|
2645
2876
|
await this.runEventHooks("Stop");
|
|
2646
2877
|
return;
|
|
2647
2878
|
}
|
|
2648
2879
|
lastWasNudge = false;
|
|
2649
2880
|
usedToolsThisTurn = true;
|
|
2881
|
+
autoContinues = 0;
|
|
2650
2882
|
const answered = /* @__PURE__ */ new Set();
|
|
2651
2883
|
try {
|
|
2652
2884
|
for (const call of toolCalls) {
|
|
@@ -2760,7 +2992,7 @@ ${summary}`
|
|
|
2760
2992
|
}
|
|
2761
2993
|
}
|
|
2762
2994
|
async *streamOnce(aiTools, signal, timeoutScale = 1) {
|
|
2763
|
-
const { streamChat } = await import("./stream-
|
|
2995
|
+
const { streamChat } = await import("./stream-OY2MKWYE.js");
|
|
2764
2996
|
yield* streamChat({
|
|
2765
2997
|
model: this.opts.model,
|
|
2766
2998
|
messages: this.messages,
|
|
@@ -2870,6 +3102,8 @@ export {
|
|
|
2870
3102
|
collectCommitContext,
|
|
2871
3103
|
buildCommitPrompt,
|
|
2872
3104
|
collectWorkingDiff,
|
|
3105
|
+
MAX_IMAGE_DIMENSION,
|
|
3106
|
+
probeImageDimensions,
|
|
2873
3107
|
reconcileToolCalls,
|
|
2874
3108
|
isAllowedByRules,
|
|
2875
3109
|
buildAllowRule,
|
|
@@ -2877,17 +3111,15 @@ export {
|
|
|
2877
3111
|
listSnapshots,
|
|
2878
3112
|
clearSnapshots,
|
|
2879
3113
|
hydrateSnapshots,
|
|
3114
|
+
listTurnSnapshots,
|
|
3115
|
+
resolveSnapshotContent,
|
|
2880
3116
|
undoTurnSnapshots,
|
|
2881
3117
|
rewindToSnapshot,
|
|
2882
3118
|
TodoStore,
|
|
2883
3119
|
formatTodos,
|
|
2884
3120
|
parseTodoArgs,
|
|
2885
3121
|
loadTodos,
|
|
2886
|
-
|
|
2887
|
-
globalConfigPath,
|
|
2888
|
-
sessionsDir,
|
|
2889
|
-
userMemoryPath,
|
|
2890
|
-
projectConfigPath,
|
|
3122
|
+
resetTodos,
|
|
2891
3123
|
discoverSkills,
|
|
2892
3124
|
defaultTaskManager,
|
|
2893
3125
|
resolveShell,
|
|
@@ -2903,4 +3135,4 @@ export {
|
|
|
2903
3135
|
PLAN_MODE_PROMPT,
|
|
2904
3136
|
AgentLoop
|
|
2905
3137
|
};
|
|
2906
|
-
//# sourceMappingURL=chunk-
|
|
3138
|
+
//# sourceMappingURL=chunk-BV4PLM6X.js.map
|