@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.
@@ -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
- return { content: formatTodos(store.list()) };
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(_args, ctx) {
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
- function starHome() {
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: path6.dirname(filePath), scope };
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(path6.join(base, entry, SKILL_FILE), entry, scope);
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(path6.join(home, "skills"), "user", byName);
1015
- scanDir(path6.join(cwd, ".star", "skills"), "project", byName);
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 path7 from "path";
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(path7.delimiter)) {
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 = path7.join(dir, exe);
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 = path7.join(path7.dirname(path7.dirname(gitExe)), "bin", "bash.exe");
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 path8 from "path";
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 = path8.resolve(ctx.cwd, args.path);
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 path10 from "path";
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 path9 from "path";
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 = path9.join(dir, entry.name);
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 = path9.join(dir, IGNORE_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 = path9.resolve(cwd);
1648
+ const root = path8.resolve(cwd);
1527
1649
  return (absPath, isDir) => {
1528
- const rel = path9.relative(root, absPath).split(path9.sep).join("/");
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 = path9.basename(filePath);
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 = path10.resolve(ctx.cwd, args.path ?? ".");
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 path11 from "path";
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 = path11.resolve(ctx.cwd, args.path ?? ".");
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: path11.basename(root), mtimeMs: st.mtimeMs }];
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 path12 from "path";
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 = path12.resolve(ctx.cwd, args.path);
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 path13 from "path";
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 = path13.resolve(ctx.cwd, args.path);
1851
+ const filePath = path12.resolve(ctx.cwd, args.path);
1730
1852
  const snapshot = await captureSnapshot(filePath, "write_file");
1731
1853
  try {
1732
- await mkdir3(path13.dirname(filePath), { recursive: true });
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-EIODKYPT.js");
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 path14 from "path";
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 = path14.join(home, "MEMORY.md");
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(path14.join(home, "MEMORY.md"), `- ${text}
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
- let finishReason;
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
- finishReason = event.finishReason;
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) return;
2577
- failure = error instanceof Error ? error : new Error(String(error));
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
- reason: failure ? failure.message : `empty response (finish: ${finishReason ?? "none"})`
2793
+ delayMs,
2794
+ reason: failure ? summarizeStreamError(failure) : `empty response (finish: ${lastFinishReason ?? "none"})`
2588
2795
  };
2589
- if (!await sleep(baseDelay * 2 ** attempt, signal)) return;
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 = { role: "user", content: nudgeText };
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-Y55NRF2S.js");
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
- starHome,
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-DZWTIMJ2.js.map
3138
+ //# sourceMappingURL=chunk-BV4PLM6X.js.map