@cryer/star-cli 0.2.2 → 0.3.1

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,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/agent/loop.ts
4
- import { tool as aiTool } from "ai";
4
+ import { tool as aiTool, generateText as generateText3 } from "ai";
5
5
 
6
6
  // src/context/compaction.ts
7
7
  import { generateText } from "ai";
@@ -235,6 +235,133 @@ ${unstaged}`);
235
235
  return { status, diff: text, truncated };
236
236
  }
237
237
 
238
+ // src/core/image.ts
239
+ var MAX_IMAGE_DIMENSION = 2e3;
240
+ var OVERSIZED_IMAGE_FALLBACK_BYTES = 5 * 1024 * 1024;
241
+ var IMAGE_REMOVED_PLACEHOLDER = "[image removed: too large for the model]";
242
+ var PNG_SIGNATURE = [137, 80, 78, 71, 13, 10, 26, 10];
243
+ function probePng(buf) {
244
+ if (buf.length < 24) return null;
245
+ for (let i = 0; i < PNG_SIGNATURE.length; i++) {
246
+ if (buf[i] !== PNG_SIGNATURE[i]) return null;
247
+ }
248
+ if (buf.toString("ascii", 12, 16) !== "IHDR") return null;
249
+ return { width: buf.readUInt32BE(16), height: buf.readUInt32BE(20) };
250
+ }
251
+ var JPEG_SOF_MARKERS = /* @__PURE__ */ new Set([
252
+ 192,
253
+ 193,
254
+ 194,
255
+ 195,
256
+ 197,
257
+ 198,
258
+ 199,
259
+ 201,
260
+ 202,
261
+ 203,
262
+ 205,
263
+ 206,
264
+ 207
265
+ ]);
266
+ function probeJpeg(buf) {
267
+ if (buf.length < 4 || buf[0] !== 255 || buf[1] !== 216) return null;
268
+ let offset = 2;
269
+ while (offset + 3 < buf.length) {
270
+ if (buf[offset] !== 255) return null;
271
+ const marker = buf[offset + 1];
272
+ if (marker === void 0) return null;
273
+ if (marker === 216 || marker === 1 || marker >= 208 && marker <= 215) {
274
+ offset += 2;
275
+ continue;
276
+ }
277
+ const length = buf.readUInt16BE(offset + 2);
278
+ if (length < 2) return null;
279
+ if (JPEG_SOF_MARKERS.has(marker)) {
280
+ if (offset + 9 > buf.length) return null;
281
+ return { height: buf.readUInt16BE(offset + 5), width: buf.readUInt16BE(offset + 7) };
282
+ }
283
+ offset += 2 + length;
284
+ }
285
+ return null;
286
+ }
287
+ function probeGif(buf) {
288
+ if (buf.length < 10) return null;
289
+ const signature = buf.toString("ascii", 0, 6);
290
+ if (signature !== "GIF87a" && signature !== "GIF89a") return null;
291
+ return { width: buf.readUInt16LE(6), height: buf.readUInt16LE(8) };
292
+ }
293
+ function probeWebp(buf) {
294
+ if (buf.length < 30) return null;
295
+ if (buf.toString("ascii", 0, 4) !== "RIFF" || buf.toString("ascii", 8, 12) !== "WEBP") {
296
+ return null;
297
+ }
298
+ const chunk = buf.toString("ascii", 12, 16);
299
+ if (chunk === "VP8X") {
300
+ return { width: 1 + buf.readUIntLE(24, 3), height: 1 + buf.readUIntLE(27, 3) };
301
+ }
302
+ if (chunk === "VP8 ") {
303
+ return {
304
+ width: buf.readUInt16LE(26) & 16383,
305
+ height: buf.readUInt16LE(28) & 16383
306
+ };
307
+ }
308
+ if (chunk === "VP8L") {
309
+ if (buf[20] !== 47) return null;
310
+ const b1 = buf[21];
311
+ const b2 = buf[22];
312
+ const b3 = buf[23];
313
+ const b4 = buf[24];
314
+ return {
315
+ width: 1 + ((b2 & 63) << 8 | b1),
316
+ height: 1 + ((b4 & 15) << 10 | b3 << 2 | (b2 & 192) >> 6)
317
+ };
318
+ }
319
+ return null;
320
+ }
321
+ function probeImageDimensions(buf) {
322
+ return probePng(buf) ?? probeJpeg(buf) ?? probeGif(buf) ?? probeWebp(buf);
323
+ }
324
+ var IMAGE_SIZE_KEYWORDS = /too[\s-]*(large|big)|exceeds?|dimensions?|resolution|size\s*limit|max(?:imum)?[\s-]*size/i;
325
+ function isOversizedImageError(error) {
326
+ const status = error.statusCode;
327
+ if (typeof status !== "number" || status < 400 || status >= 500) return false;
328
+ return /image/i.test(error.message) && IMAGE_SIZE_KEYWORDS.test(error.message);
329
+ }
330
+ function imagePartBytes(image) {
331
+ if (typeof image === "string") {
332
+ const base64 = image.startsWith("data:") ? image.slice(image.indexOf(",") + 1) : image;
333
+ try {
334
+ return Buffer.from(base64, "base64");
335
+ } catch {
336
+ return null;
337
+ }
338
+ }
339
+ if (image instanceof Uint8Array) return Buffer.from(image);
340
+ return null;
341
+ }
342
+ function isOversizedImageData(buf) {
343
+ const dims = probeImageDimensions(buf);
344
+ if (dims) return Math.max(dims.width, dims.height) > MAX_IMAGE_DIMENSION;
345
+ return buf.length > OVERSIZED_IMAGE_FALLBACK_BYTES;
346
+ }
347
+ function stripOversizedImages(messages) {
348
+ let removed = 0;
349
+ const result = messages.map((message) => {
350
+ if (!Array.isArray(message.content)) return message;
351
+ let changed = false;
352
+ const content = message.content.map((part) => {
353
+ if (part.type !== "image") return part;
354
+ const bytes = imagePartBytes(part.image);
355
+ if (!bytes || !isOversizedImageData(bytes)) return part;
356
+ changed = true;
357
+ removed++;
358
+ return { type: "text", text: IMAGE_REMOVED_PLACEHOLDER };
359
+ });
360
+ return changed ? { ...message, content } : message;
361
+ });
362
+ return { messages: removed > 0 ? result : messages, removed };
363
+ }
364
+
238
365
  // src/core/messages.ts
239
366
  var MISSING_TOOL_RESULT_TEXT = "tool result missing (session was interrupted)";
240
367
  function reconcileToolCalls(messages) {
@@ -664,6 +791,12 @@ async function revert(snapshot) {
664
791
  await rm(snapshot.path, { force: true });
665
792
  return `Deleted ${snapshot.path} (it did not exist before ${snapshot.toolName}).`;
666
793
  }
794
+ function listTurnSnapshots(turn) {
795
+ return stack.filter((snapshot) => snapshot.turn === turn).reverse();
796
+ }
797
+ function resolveSnapshotContent(snapshot) {
798
+ return resolveContent(snapshot);
799
+ }
667
800
  async function undoTurnSnapshots(turn) {
668
801
  const matches = [];
669
802
  for (let i = stack.length - 1; i >= 0; i--) {
@@ -702,14 +835,214 @@ async function rewindToSnapshot(id) {
702
835
  return { reverted, messageIndex };
703
836
  }
704
837
 
838
+ // src/tools/todo.ts
839
+ import { mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
840
+ import path3 from "path";
841
+ import { z } from "zod";
842
+ var todoItemSchema = z.object({
843
+ id: z.number().int(),
844
+ title: z.string(),
845
+ status: z.enum(["pending", "in_progress", "done"])
846
+ });
847
+ var writeSchema = z.object({
848
+ todos: z.array(todoItemSchema).describe("The full todo list, replacing the current one")
849
+ });
850
+ var readSchema = z.object({});
851
+ function fileFor(cwd) {
852
+ return path3.join(cwd, ".star", "todos.json");
853
+ }
854
+ var TodoStore = class {
855
+ items = /* @__PURE__ */ new Map();
856
+ list() {
857
+ return [...this.items.values()];
858
+ }
859
+ replace(items) {
860
+ this.items.clear();
861
+ for (const item of items) {
862
+ this.items.set(item.id, item);
863
+ }
864
+ }
865
+ async load(cwd) {
866
+ let raw;
867
+ try {
868
+ raw = await readFile2(fileFor(cwd), "utf8");
869
+ } catch {
870
+ return;
871
+ }
872
+ try {
873
+ const parsed = JSON.parse(raw);
874
+ if (Array.isArray(parsed)) {
875
+ this.replace(parsed.filter((it) => todoItemSchema.safeParse(it).success));
876
+ }
877
+ } catch {
878
+ }
879
+ }
880
+ async save(cwd) {
881
+ await mkdir2(path3.join(cwd, ".star"), { recursive: true });
882
+ await writeFile2(fileFor(cwd), `${JSON.stringify(this.list(), null, 2)}
883
+ `);
884
+ }
885
+ };
886
+ var SYMBOLS = {
887
+ pending: "[ ]",
888
+ in_progress: "[~]",
889
+ done: "[x]"
890
+ };
891
+ function formatTodos(items) {
892
+ if (items.length === 0) {
893
+ return "No todos.";
894
+ }
895
+ const lines = items.map((it) => `${SYMBOLS[it.status]} ${it.id}. ${it.title}`);
896
+ const width = Math.max(...lines.map((l) => l.length));
897
+ return lines.map((line, i) => {
898
+ const status = items[i]?.status;
899
+ return status === "pending" ? line : `${line.padEnd(width + 4)}(${status})`;
900
+ }).join("\n");
901
+ }
902
+ var defaultStore = new TodoStore();
903
+ function parseTodoArgs(args) {
904
+ const parsed = writeSchema.safeParse(args);
905
+ return parsed.success ? parsed.data.todos : null;
906
+ }
907
+ function pendingTodoTitles(args) {
908
+ return (parseTodoArgs(args) ?? []).filter((t) => t.status !== "done").map((t) => t.title);
909
+ }
910
+ async function loadTodos(cwd) {
911
+ await defaultStore.load(cwd);
912
+ return defaultStore.list();
913
+ }
914
+ function resetTodos() {
915
+ defaultStore.replace([]);
916
+ }
917
+ function createTodoTools(store = defaultStore) {
918
+ const todoWrite = {
919
+ name: "todo_write",
920
+ description: "Replace the current todo list with the given items. Each item has an id, title, and status (pending, in_progress, done).",
921
+ permission: "read",
922
+ parameters: writeSchema,
923
+ async execute(args, ctx) {
924
+ const parsed = writeSchema.safeParse(args);
925
+ if (!parsed.success) {
926
+ return { content: `Invalid todos: ${parsed.error.message}`, isError: true };
927
+ }
928
+ store.replace(parsed.data.todos);
929
+ await store.save(ctx.cwd);
930
+ const items = store.list();
931
+ const done = items.filter((it) => it.status === "done").length;
932
+ return { content: `Todo list updated (${done}/${items.length} done).` };
933
+ }
934
+ };
935
+ const todoRead = {
936
+ name: "todo_read",
937
+ description: "Read the current todo list.",
938
+ permission: "read",
939
+ parameters: readSchema,
940
+ async execute() {
941
+ return { content: formatTodos(store.list()) };
942
+ }
943
+ };
944
+ return [todoWrite, todoRead];
945
+ }
946
+
947
+ // src/agent/agent-tasks.ts
948
+ import { EventEmitter } from "events";
949
+ var AgentTaskManager = class extends EventEmitter {
950
+ records = /* @__PURE__ */ new Map();
951
+ counter = 0;
952
+ start(run, meta) {
953
+ const id = `agent-${++this.counter}`;
954
+ const rec = {
955
+ id,
956
+ prompt: meta.prompt,
957
+ description: meta.description,
958
+ status: "running",
959
+ startedAt: Date.now(),
960
+ result: "",
961
+ controller: new AbortController(),
962
+ notified: false
963
+ };
964
+ this.records.set(id, rec);
965
+ void run(rec.controller.signal).then((result) => this.finish(rec, "completed", result)).catch(
966
+ (error) => this.finish(rec, "failed", error instanceof Error ? error.message : String(error))
967
+ );
968
+ this.emitUpdate(rec);
969
+ return this.snapshot(rec);
970
+ }
971
+ get(id) {
972
+ const rec = this.records.get(id);
973
+ return rec ? this.snapshot(rec) : void 0;
974
+ }
975
+ list() {
976
+ return [...this.records.values()].map((rec) => this.snapshot(rec));
977
+ }
978
+ runningCount() {
979
+ let count = 0;
980
+ for (const rec of this.records.values()) {
981
+ if (rec.status === "running") count += 1;
982
+ }
983
+ return count;
984
+ }
985
+ kill(id) {
986
+ const rec = this.records.get(id);
987
+ if (!rec || rec.status !== "running") return void 0;
988
+ rec.controller.abort();
989
+ this.finish(rec, "stopped", rec.result || "Stopped by user.");
990
+ return this.snapshot(rec);
991
+ }
992
+ cleanup() {
993
+ const killed = [];
994
+ for (const rec of this.records.values()) {
995
+ if (rec.status === "running") {
996
+ rec.controller.abort();
997
+ this.finish(rec, "stopped", rec.result || "Stopped on shutdown.");
998
+ killed.push(this.snapshot(rec));
999
+ }
1000
+ }
1001
+ return killed;
1002
+ }
1003
+ // Finished tasks whose result has not yet been handed to the parent loop.
1004
+ drainNotifications() {
1005
+ const drained = [];
1006
+ for (const rec of this.records.values()) {
1007
+ if (rec.status !== "running" && !rec.notified) {
1008
+ rec.notified = true;
1009
+ drained.push(this.snapshot(rec));
1010
+ }
1011
+ }
1012
+ return drained;
1013
+ }
1014
+ finish(rec, status, result) {
1015
+ if (rec.status !== "running") return;
1016
+ rec.status = status;
1017
+ rec.result = result;
1018
+ rec.endedAt = Date.now();
1019
+ this.emitUpdate(rec);
1020
+ }
1021
+ emitUpdate(rec) {
1022
+ this.emit("update", this.snapshot(rec));
1023
+ }
1024
+ snapshot(rec) {
1025
+ return {
1026
+ id: rec.id,
1027
+ prompt: rec.prompt,
1028
+ description: rec.description,
1029
+ status: rec.status,
1030
+ startedAt: rec.startedAt,
1031
+ endedAt: rec.endedAt,
1032
+ result: rec.result
1033
+ };
1034
+ }
1035
+ };
1036
+ var defaultAgentTasks = new AgentTaskManager();
1037
+
705
1038
  // src/agent/project-memory.ts
706
1039
  import { readFileSync, statSync } from "fs";
707
- import path3 from "path";
1040
+ import path4 from "path";
708
1041
  var PROJECT_MEMORY_FILE = "AGENTS.md";
709
1042
  var PROJECT_MEMORY_MAX_CHARS = 32 * 1024;
710
1043
  var cache = /* @__PURE__ */ new Map();
711
1044
  function readProjectMemory(cwd) {
712
- const file = path3.join(cwd, PROJECT_MEMORY_FILE);
1045
+ const file = path4.join(cwd, PROJECT_MEMORY_FILE);
713
1046
  let mtimeMs;
714
1047
  try {
715
1048
  mtimeMs = statSync(file).mtimeMs;
@@ -739,26 +1072,29 @@ ${body}`;
739
1072
 
740
1073
  // src/agent/skills.ts
741
1074
  import { readFileSync as readFileSync2, readdirSync, statSync as statSync2 } from "fs";
742
- import path5 from "path";
743
- import { z } from "zod";
1075
+ import path6 from "path";
1076
+ import { z as z2 } from "zod";
744
1077
 
745
1078
  // src/config/paths.ts
746
1079
  import os from "os";
747
- import path4 from "path";
1080
+ import path5 from "path";
748
1081
  function starHome() {
749
- return process.env.STAR_HOME ?? path4.join(os.homedir(), ".star-cli");
1082
+ return process.env.STAR_HOME ?? path5.join(os.homedir(), ".star-cli");
750
1083
  }
751
1084
  function globalConfigPath() {
752
- return path4.join(starHome(), "config.toml");
1085
+ return path5.join(starHome(), "config.toml");
753
1086
  }
754
1087
  function sessionsDir() {
755
- return path4.join(starHome(), "sessions");
1088
+ return path5.join(starHome(), "sessions");
756
1089
  }
757
1090
  function userMemoryPath() {
758
- return path4.join(starHome(), "MEMORY.md");
1091
+ return path5.join(starHome(), "MEMORY.md");
1092
+ }
1093
+ function envFilePath() {
1094
+ return path5.join(starHome(), ".env");
759
1095
  }
760
1096
  function projectConfigPath(cwd) {
761
- return path4.join(cwd, ".star", "config.toml");
1097
+ return path5.join(cwd, ".star", "config.toml");
762
1098
  }
763
1099
 
764
1100
  // src/agent/skills.ts
@@ -791,7 +1127,7 @@ function parseSkillFile(filePath, dirName, scope) {
791
1127
  const name = parsed?.meta.name || dirName;
792
1128
  const description = parsed?.meta.description;
793
1129
  if (parsed && NAME_PATTERN.test(name) && description) {
794
- skill = { name, description, filePath, dir: path5.dirname(filePath), scope };
1130
+ skill = { name, description, filePath, dir: path6.dirname(filePath), scope };
795
1131
  }
796
1132
  } catch {
797
1133
  skill = null;
@@ -808,14 +1144,14 @@ function scanDir(base, scope, into) {
808
1144
  }
809
1145
  for (const entry of entries.sort()) {
810
1146
  if (!NAME_PATTERN.test(entry)) continue;
811
- const parsed = parseSkillFile(path5.join(base, entry, SKILL_FILE), entry, scope);
1147
+ const parsed = parseSkillFile(path6.join(base, entry, SKILL_FILE), entry, scope);
812
1148
  if (parsed) into.set(parsed.name, parsed);
813
1149
  }
814
1150
  }
815
1151
  function discoverSkills(cwd, home = starHome()) {
816
1152
  const byName = /* @__PURE__ */ new Map();
817
- scanDir(path5.join(home, "skills"), "user", byName);
818
- scanDir(path5.join(cwd, ".star", "skills"), "project", byName);
1153
+ scanDir(path6.join(home, "skills"), "user", byName);
1154
+ scanDir(path6.join(cwd, ".star", "skills"), "project", byName);
819
1155
  return [...byName.values()];
820
1156
  }
821
1157
  function formatSkillsBlock(skills) {
@@ -846,8 +1182,8 @@ function createSkillTool(deps) {
846
1182
  name: "skill",
847
1183
  description: "Load the full instructions of a skill by name. Available skills are listed in the system prompt when present; call this before starting work that matches one of them.",
848
1184
  permission: "read",
849
- parameters: z.object({
850
- name: z.string().describe("Name of the skill to load, as listed in the system prompt.")
1185
+ parameters: z2.object({
1186
+ name: z2.string().describe("Name of the skill to load, as listed in the system prompt.")
851
1187
  }),
852
1188
  async execute(args) {
853
1189
  const skills = discoverSkills(deps.cwd, deps.home);
@@ -882,16 +1218,16 @@ import { z as z12 } from "zod";
882
1218
  // src/tools/bash.ts
883
1219
  import { spawn as spawn3 } from "child_process";
884
1220
  import { existsSync } from "fs";
885
- import path6 from "path";
886
- import { z as z2 } from "zod";
1221
+ import path7 from "path";
1222
+ import { z as z3 } from "zod";
887
1223
 
888
1224
  // src/tasks/manager.ts
889
1225
  import { spawn as spawn2 } from "child_process";
890
- import { EventEmitter } from "events";
1226
+ import { EventEmitter as EventEmitter2 } from "events";
891
1227
  var MAX_TASK_OUTPUT = 1e5;
892
1228
  var DEFAULT_TASK_TIMEOUT = 3600;
893
1229
  var TRUNCATED_PREFIX = "[... earlier output truncated ...]\n";
894
- var TaskManager = class extends EventEmitter {
1230
+ var TaskManager = class extends EventEmitter2 {
895
1231
  records = /* @__PURE__ */ new Map();
896
1232
  seq = 0;
897
1233
  start(opts) {
@@ -1002,11 +1338,11 @@ var MAX_OUTPUT = 3e4;
1002
1338
  var DEFAULT_TIMEOUT = 120;
1003
1339
  var MAX_TIMEOUT = 600;
1004
1340
  function findOnPath(exe, exclude) {
1005
- for (const dir of (process.env.PATH ?? "").split(path6.delimiter)) {
1341
+ for (const dir of (process.env.PATH ?? "").split(path7.delimiter)) {
1006
1342
  if (!dir || exclude?.(dir)) {
1007
1343
  continue;
1008
1344
  }
1009
- const full = path6.join(dir, exe);
1345
+ const full = path7.join(dir, exe);
1010
1346
  if (existsSync(full)) {
1011
1347
  return full;
1012
1348
  }
@@ -1024,7 +1360,7 @@ function resolveShell() {
1024
1360
  }
1025
1361
  const gitExe = findOnPath("git.exe");
1026
1362
  if (gitExe) {
1027
- const sibling = path6.join(path6.dirname(path6.dirname(gitExe)), "bin", "bash.exe");
1363
+ const sibling = path7.join(path7.dirname(path7.dirname(gitExe)), "bin", "bash.exe");
1028
1364
  if (existsSync(sibling)) {
1029
1365
  return { shell: sibling, wrap: (c) => ["-c", c], label: "bash" };
1030
1366
  }
@@ -1041,11 +1377,11 @@ function resolveShell() {
1041
1377
  const comspec = process.env.ComSpec ?? "cmd.exe";
1042
1378
  return { shell: comspec, wrap: (c) => ["/d", "/s", "/c", c], label: "cmd" };
1043
1379
  }
1044
- var schema = z2.object({
1045
- command: z2.string().describe("Shell command to execute"),
1046
- timeout: z2.number().positive().max(MAX_TIMEOUT).optional().describe("Timeout in seconds (default 120, max 600)"),
1047
- description: z2.string().optional().describe("Short description of what the command does"),
1048
- run_in_background: z2.boolean().optional().describe(
1380
+ var schema = z3.object({
1381
+ command: z3.string().describe("Shell command to execute"),
1382
+ timeout: z3.number().positive().max(MAX_TIMEOUT).optional().describe("Timeout in seconds (default 120, max 600)"),
1383
+ description: z3.string().optional().describe("Short description of what the command does"),
1384
+ run_in_background: z3.boolean().optional().describe(
1049
1385
  "Run the command in the background and return a task id immediately (default false). Use task_output/task_kill to inspect or stop it."
1050
1386
  )
1051
1387
  });
@@ -1147,14 +1483,14 @@ Command timed out after ${timeoutSeconds}s`,
1147
1483
  };
1148
1484
 
1149
1485
  // src/tools/fs/edit.ts
1150
- import { readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
1151
- import path7 from "path";
1152
- import { z as z3 } from "zod";
1153
- var schema2 = z3.object({
1154
- path: z3.string().describe("File path, absolute or relative to the working directory"),
1155
- old_string: z3.string().min(1).describe("Exact text to replace"),
1156
- new_string: z3.string().describe("Replacement text"),
1157
- replace_all: z3.boolean().optional().describe("Replace every occurrence instead of requiring a unique match")
1486
+ import { readFile as readFile3, writeFile as writeFile3 } from "fs/promises";
1487
+ import path8 from "path";
1488
+ import { z as z4 } from "zod";
1489
+ var schema2 = z4.object({
1490
+ path: z4.string().describe("File path, absolute or relative to the working directory"),
1491
+ old_string: z4.string().min(1).describe("Exact text to replace"),
1492
+ new_string: z4.string().describe("Replacement text"),
1493
+ replace_all: z4.boolean().optional().describe("Replace every occurrence instead of requiring a unique match")
1158
1494
  });
1159
1495
  var editFileTool = {
1160
1496
  name: "edit_file",
@@ -1162,10 +1498,10 @@ var editFileTool = {
1162
1498
  permission: "write",
1163
1499
  parameters: schema2,
1164
1500
  async execute(args, ctx) {
1165
- const filePath = path7.resolve(ctx.cwd, args.path);
1501
+ const filePath = path8.resolve(ctx.cwd, args.path);
1166
1502
  let content;
1167
1503
  try {
1168
- content = await readFile2(filePath, "utf8");
1504
+ content = await readFile3(filePath, "utf8");
1169
1505
  } catch (err) {
1170
1506
  return { content: `Failed to read ${args.path}: ${err.message}`, isError: true };
1171
1507
  }
@@ -1186,7 +1522,7 @@ var editFileTool = {
1186
1522
  }
1187
1523
  const updated = args.replace_all ? content.split(args.old_string).join(args.new_string) : content.replace(args.old_string, args.new_string);
1188
1524
  try {
1189
- await writeFile2(filePath, updated, "utf8");
1525
+ await writeFile3(filePath, updated, "utf8");
1190
1526
  } catch (err) {
1191
1527
  return { content: `Failed to write ${args.path}: ${err.message}`, isError: true };
1192
1528
  }
@@ -1205,13 +1541,13 @@ var editFileTool = {
1205
1541
  };
1206
1542
 
1207
1543
  // src/tools/fs/glob.ts
1208
- import path9 from "path";
1209
- import { z as z4 } from "zod";
1544
+ import path10 from "path";
1545
+ import { z as z5 } from "zod";
1210
1546
 
1211
1547
  // src/tools/fs/util.ts
1212
1548
  import { readFileSync as readFileSync3, statSync as statSync3 } from "fs";
1213
1549
  import { readdir, stat } from "fs/promises";
1214
- import path8 from "path";
1550
+ import path9 from "path";
1215
1551
  var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist"]);
1216
1552
  async function walkFiles(root, skipDirs = SKIP_DIRS, options = {}) {
1217
1553
  const out = [];
@@ -1221,7 +1557,7 @@ async function walkFiles(root, skipDirs = SKIP_DIRS, options = {}) {
1221
1557
  return;
1222
1558
  }
1223
1559
  for (const entry of entries) {
1224
- const abs = path8.join(dir, entry.name);
1560
+ const abs = path9.join(dir, entry.name);
1225
1561
  const rel = relBase ? `${relBase}/${entry.name}` : entry.name;
1226
1562
  if (entry.isDirectory()) {
1227
1563
  if (skipDirs.has(entry.name) || options.ignore?.(abs, true)) {
@@ -1301,7 +1637,7 @@ function parseIgnorePatterns(body) {
1301
1637
  return out;
1302
1638
  }
1303
1639
  function loadIgnorePatterns(dir) {
1304
- const file = path8.join(dir, IGNORE_FILE);
1640
+ const file = path9.join(dir, IGNORE_FILE);
1305
1641
  let mtimeMs;
1306
1642
  try {
1307
1643
  mtimeMs = statSync3(file).mtimeMs;
@@ -1326,9 +1662,9 @@ function createIgnorePredicate(cwd) {
1326
1662
  if (patterns.length === 0) {
1327
1663
  return void 0;
1328
1664
  }
1329
- const root = path8.resolve(cwd);
1665
+ const root = path9.resolve(cwd);
1330
1666
  return (absPath, isDir) => {
1331
- const rel = path8.relative(root, absPath).split(path8.sep).join("/");
1667
+ const rel = path9.relative(root, absPath).split(path9.sep).join("/");
1332
1668
  if (!rel || rel.startsWith("..")) {
1333
1669
  return false;
1334
1670
  }
@@ -1344,7 +1680,7 @@ function createIgnorePredicate(cwd) {
1344
1680
  };
1345
1681
  }
1346
1682
  function isSensitivePath2(filePath) {
1347
- const base = path8.basename(filePath);
1683
+ const base = path9.basename(filePath);
1348
1684
  if (base === ".env.example" || base === ".env.sample" || base === ".env.template") {
1349
1685
  return false;
1350
1686
  }
@@ -1359,11 +1695,11 @@ function isSensitivePath2(filePath) {
1359
1695
 
1360
1696
  // src/tools/fs/glob.ts
1361
1697
  var MAX_RESULTS = 100;
1362
- var schema3 = z4.object({
1363
- pattern: z4.string().describe(
1698
+ var schema3 = z5.object({
1699
+ pattern: z5.string().describe(
1364
1700
  "Glob pattern, e.g. 'src/**/*.ts'. Supports **, * and ?. A bare pattern like '*.ts' matches basenames recursively."
1365
1701
  ),
1366
- path: z4.string().optional().describe("Directory to search, defaults to the working directory")
1702
+ path: z5.string().optional().describe("Directory to search, defaults to the working directory")
1367
1703
  });
1368
1704
  var globTool = {
1369
1705
  name: "glob",
@@ -1371,7 +1707,7 @@ var globTool = {
1371
1707
  permission: "read",
1372
1708
  parameters: schema3,
1373
1709
  async execute(args, ctx) {
1374
- const root = path9.resolve(ctx.cwd, args.path ?? ".");
1710
+ const root = path10.resolve(ctx.cwd, args.path ?? ".");
1375
1711
  const files = await walkFiles(root, void 0, { ignore: createIgnorePredicate(ctx.cwd) });
1376
1712
  const matched = files.filter((f) => matchesGlob(args.pattern, f.rel)).sort((a, b) => b.mtimeMs - a.mtimeMs).slice(0, MAX_RESULTS);
1377
1713
  if (matched.length === 0) {
@@ -1382,15 +1718,15 @@ var globTool = {
1382
1718
  };
1383
1719
 
1384
1720
  // src/tools/fs/grep.ts
1385
- import { readFile as readFile3, stat as stat2 } from "fs/promises";
1386
- import path10 from "path";
1387
- import { z as z5 } from "zod";
1721
+ import { readFile as readFile4, stat as stat2 } from "fs/promises";
1722
+ import path11 from "path";
1723
+ import { z as z6 } from "zod";
1388
1724
  var MAX_MATCHES = 250;
1389
- var schema4 = z5.object({
1390
- pattern: z5.string().describe("Regular expression to search for"),
1391
- path: z5.string().optional().describe("File or directory to search, defaults to the working directory"),
1392
- glob: z5.string().optional().describe("Glob pattern to filter which files are searched, e.g. '*.ts'"),
1393
- ignoreCase: z5.boolean().optional().describe("Case-insensitive matching")
1725
+ var schema4 = z6.object({
1726
+ pattern: z6.string().describe("Regular expression to search for"),
1727
+ path: z6.string().optional().describe("File or directory to search, defaults to the working directory"),
1728
+ glob: z6.string().optional().describe("Glob pattern to filter which files are searched, e.g. '*.ts'"),
1729
+ ignoreCase: z6.boolean().optional().describe("Case-insensitive matching")
1394
1730
  });
1395
1731
  var grepTool = {
1396
1732
  name: "grep",
@@ -1404,12 +1740,12 @@ var grepTool = {
1404
1740
  } catch (err) {
1405
1741
  return { content: `Invalid regular expression: ${err.message}`, isError: true };
1406
1742
  }
1407
- const root = path10.resolve(ctx.cwd, args.path ?? ".");
1743
+ const root = path11.resolve(ctx.cwd, args.path ?? ".");
1408
1744
  let files;
1409
1745
  try {
1410
1746
  const st = await stat2(root);
1411
1747
  if (st.isFile()) {
1412
- files = [{ abs: root, rel: path10.basename(root), mtimeMs: st.mtimeMs }];
1748
+ files = [{ abs: root, rel: path11.basename(root), mtimeMs: st.mtimeMs }];
1413
1749
  } else {
1414
1750
  files = await walkFiles(root, void 0, { ignore: createIgnorePredicate(ctx.cwd) });
1415
1751
  }
@@ -1425,7 +1761,7 @@ var grepTool = {
1425
1761
  outer: for (const file of files) {
1426
1762
  let buf;
1427
1763
  try {
1428
- buf = await readFile3(file.abs);
1764
+ buf = await readFile4(file.abs);
1429
1765
  } catch {
1430
1766
  continue;
1431
1767
  }
@@ -1455,15 +1791,15 @@ var grepTool = {
1455
1791
  };
1456
1792
 
1457
1793
  // src/tools/fs/read.ts
1458
- import { readFile as readFile4, stat as stat3 } from "fs/promises";
1459
- import path11 from "path";
1460
- import { z as z6 } from "zod";
1794
+ import { readFile as readFile5, stat as stat3 } from "fs/promises";
1795
+ import path12 from "path";
1796
+ import { z as z7 } from "zod";
1461
1797
  var DEFAULT_LIMIT = 2e3;
1462
1798
  var MAX_CHARS = 100 * 1024;
1463
- var schema5 = z6.object({
1464
- path: z6.string().describe("File path, absolute or relative to the working directory"),
1465
- offset: z6.number().int().positive().optional().describe("1-based line number to start reading from"),
1466
- limit: z6.number().int().positive().optional().describe("Maximum number of lines to read (default 2000)")
1799
+ var schema5 = z7.object({
1800
+ path: z7.string().describe("File path, absolute or relative to the working directory"),
1801
+ offset: z7.number().int().positive().optional().describe("1-based line number to start reading from"),
1802
+ limit: z7.number().int().positive().optional().describe("Maximum number of lines to read (default 2000)")
1467
1803
  });
1468
1804
  var readFileTool = {
1469
1805
  name: "read_file",
@@ -1471,7 +1807,7 @@ var readFileTool = {
1471
1807
  permission: "read",
1472
1808
  parameters: schema5,
1473
1809
  async execute(args, ctx) {
1474
- const filePath = path11.resolve(ctx.cwd, args.path);
1810
+ const filePath = path12.resolve(ctx.cwd, args.path);
1475
1811
  if (isSensitivePath2(filePath)) {
1476
1812
  return { content: `Refused to read sensitive file: ${args.path}`, isError: true };
1477
1813
  }
@@ -1482,7 +1818,7 @@ var readFileTool = {
1482
1818
  if (st.isDirectory()) {
1483
1819
  return { content: `Path is a directory, not a file: ${args.path}`, isError: true };
1484
1820
  }
1485
- const raw = await readFile4(filePath, "utf8");
1821
+ const raw = await readFile5(filePath, "utf8");
1486
1822
  const lines = raw.split("\n");
1487
1823
  const start = args.offset ?? 1;
1488
1824
  const limit = args.limit ?? DEFAULT_LIMIT;
@@ -1516,12 +1852,12 @@ var readFileTool = {
1516
1852
  };
1517
1853
 
1518
1854
  // src/tools/fs/write.ts
1519
- import { mkdir as mkdir2, writeFile as writeFile3 } from "fs/promises";
1520
- import path12 from "path";
1521
- import { z as z7 } from "zod";
1522
- var schema6 = z7.object({
1523
- path: z7.string().describe("File path, absolute or relative to the working directory"),
1524
- content: z7.string().describe("Full content to write to the file")
1855
+ import { mkdir as mkdir3, writeFile as writeFile4 } from "fs/promises";
1856
+ import path13 from "path";
1857
+ import { z as z8 } from "zod";
1858
+ var schema6 = z8.object({
1859
+ path: z8.string().describe("File path, absolute or relative to the working directory"),
1860
+ content: z8.string().describe("Full content to write to the file")
1525
1861
  });
1526
1862
  var writeFileTool = {
1527
1863
  name: "write_file",
@@ -1529,11 +1865,11 @@ var writeFileTool = {
1529
1865
  permission: "write",
1530
1866
  parameters: schema6,
1531
1867
  async execute(args, ctx) {
1532
- const filePath = path12.resolve(ctx.cwd, args.path);
1868
+ const filePath = path13.resolve(ctx.cwd, args.path);
1533
1869
  const snapshot = await captureSnapshot(filePath, "write_file");
1534
1870
  try {
1535
- await mkdir2(path12.dirname(filePath), { recursive: true });
1536
- await writeFile3(filePath, args.content, "utf8");
1871
+ await mkdir3(path13.dirname(filePath), { recursive: true });
1872
+ await writeFile4(filePath, args.content, "utf8");
1537
1873
  } catch (err) {
1538
1874
  return { content: `Failed to write ${args.path}: ${err.message}`, isError: true };
1539
1875
  }
@@ -1543,7 +1879,7 @@ var writeFileTool = {
1543
1879
  };
1544
1880
 
1545
1881
  // src/tools/tasks.ts
1546
- import { z as z8 } from "zod";
1882
+ import { z as z9 } from "zod";
1547
1883
 
1548
1884
  // src/tasks/format.ts
1549
1885
  function formatDuration(startedAt, endedAt) {
@@ -1589,40 +1925,59 @@ function formatTaskFinished(task) {
1589
1925
  }
1590
1926
 
1591
1927
  // src/tools/tasks.ts
1592
- var idSchema = z8.object({
1593
- id: z8.string().describe("Task id, e.g. task-1")
1928
+ var idSchema = z9.object({
1929
+ id: z9.string().describe("Task id, e.g. task-1 or agent-1")
1594
1930
  });
1931
+ function formatAgentTaskLine(task) {
1932
+ const duration = formatDuration(task.startedAt, task.endedAt);
1933
+ const label = task.description ?? task.prompt.slice(0, 60);
1934
+ const status = task.status === "running" ? `running ${duration}` : `${task.status} after ${duration}`;
1935
+ return `${task.id} [${status}] ${label}`;
1936
+ }
1595
1937
  var taskListTool = {
1596
1938
  name: "task_list",
1597
- description: "List background shell tasks with status, runtime, and exit code.",
1939
+ description: "List background tasks (shell commands and subagents) with status and runtime.",
1598
1940
  permission: "read",
1599
- parameters: z8.object({}),
1941
+ parameters: z9.object({}),
1600
1942
  execute() {
1601
- return Promise.resolve({ content: formatTaskList(defaultTaskManager.list()) });
1943
+ const shell = formatTaskList(defaultTaskManager.list());
1944
+ const agents = defaultAgentTasks.list();
1945
+ const agentLines = agents.length === 0 ? "No background subagents." : `Background subagents:
1946
+ ${agents.map(formatAgentTaskLine).join("\n")}`;
1947
+ return Promise.resolve({ content: `${shell}
1948
+ ${agentLines}` });
1602
1949
  }
1603
1950
  };
1604
1951
  var taskOutputTool = {
1605
1952
  name: "task_output",
1606
- description: "Read the current output of a background shell task (works while still running).",
1953
+ description: "Read the current output of a background task (shell command or subagent; works while still running).",
1607
1954
  permission: "read",
1608
1955
  parameters: idSchema,
1609
1956
  execute(args) {
1610
- const task = defaultTaskManager.get(args.id);
1611
- if (!task) {
1612
- return Promise.resolve({ content: `Unknown task: ${args.id}`, isError: true });
1957
+ const shell = defaultTaskManager.get(args.id);
1958
+ if (shell) {
1959
+ const body = shell.output.replace(/\s+$/, "") || "(no output yet)";
1960
+ return Promise.resolve({ content: `${formatTaskLine(shell)}
1961
+ ${body}` });
1613
1962
  }
1614
- const body = task.output.replace(/\s+$/, "") || "(no output yet)";
1615
- return Promise.resolve({ content: `${formatTaskLine(task)}
1963
+ const agent = defaultAgentTasks.get(args.id);
1964
+ if (agent) {
1965
+ const body = agent.status === "running" ? "(still running)" : agent.result.replace(/\s+$/, "") || "(no report)";
1966
+ return Promise.resolve({ content: `${formatAgentTaskLine(agent)}
1616
1967
  ${body}` });
1968
+ }
1969
+ return Promise.resolve({ content: `Unknown task: ${args.id}`, isError: true });
1617
1970
  }
1618
1971
  };
1619
1972
  var taskKillTool = {
1620
1973
  name: "task_kill",
1621
- description: "Stop a running background shell task (kills the whole process tree).",
1974
+ description: "Stop a running background task (shell command process tree or subagent).",
1622
1975
  permission: "exec",
1623
1976
  parameters: idSchema,
1624
1977
  execute(args) {
1625
- const task = defaultTaskManager.get(args.id);
1978
+ const shell = defaultTaskManager.get(args.id);
1979
+ const agent = shell ? void 0 : defaultAgentTasks.get(args.id);
1980
+ const task = shell ?? agent;
1626
1981
  if (!task) {
1627
1982
  return Promise.resolve({ content: `Unknown task: ${args.id}`, isError: true });
1628
1983
  }
@@ -1632,105 +1987,14 @@ var taskKillTool = {
1632
1987
  isError: true
1633
1988
  });
1634
1989
  }
1635
- defaultTaskManager.kill(args.id);
1636
- return Promise.resolve({ content: `Task ${args.id} stopped.` });
1637
- }
1638
- };
1639
-
1640
- // src/tools/todo.ts
1641
- import { mkdir as mkdir3, readFile as readFile5, writeFile as writeFile4 } from "fs/promises";
1642
- import path13 from "path";
1643
- import { z as z9 } from "zod";
1644
- var todoItemSchema = z9.object({
1645
- id: z9.number().int(),
1646
- title: z9.string(),
1647
- status: z9.enum(["pending", "in_progress", "done"])
1648
- });
1649
- var writeSchema = z9.object({
1650
- todos: z9.array(todoItemSchema).describe("The full todo list, replacing the current one")
1651
- });
1652
- var readSchema = z9.object({});
1653
- function fileFor(cwd) {
1654
- return path13.join(cwd, ".star", "todos.json");
1655
- }
1656
- var TodoStore = class {
1657
- items = /* @__PURE__ */ new Map();
1658
- list() {
1659
- return [...this.items.values()];
1660
- }
1661
- replace(items) {
1662
- this.items.clear();
1663
- for (const item of items) {
1664
- this.items.set(item.id, item);
1665
- }
1666
- }
1667
- async load(cwd) {
1668
- let raw;
1669
- try {
1670
- raw = await readFile5(fileFor(cwd), "utf8");
1671
- } catch {
1672
- return;
1673
- }
1674
- try {
1675
- const parsed = JSON.parse(raw);
1676
- if (Array.isArray(parsed)) {
1677
- this.replace(parsed.filter((it) => todoItemSchema.safeParse(it).success));
1678
- }
1679
- } catch {
1990
+ if (shell) {
1991
+ defaultTaskManager.kill(args.id);
1992
+ } else {
1993
+ defaultAgentTasks.kill(args.id);
1680
1994
  }
1681
- }
1682
- async save(cwd) {
1683
- await mkdir3(path13.join(cwd, ".star"), { recursive: true });
1684
- await writeFile4(fileFor(cwd), `${JSON.stringify(this.list(), null, 2)}
1685
- `);
1995
+ return Promise.resolve({ content: `Task ${args.id} stopped.` });
1686
1996
  }
1687
1997
  };
1688
- var SYMBOLS = {
1689
- pending: "[ ]",
1690
- in_progress: "[~]",
1691
- done: "[x]"
1692
- };
1693
- function formatTodos(items) {
1694
- if (items.length === 0) {
1695
- return "No todos.";
1696
- }
1697
- const lines = items.map((it) => `${SYMBOLS[it.status]} ${it.id}. ${it.title}`);
1698
- const width = Math.max(...lines.map((l) => l.length));
1699
- return lines.map((line, i) => {
1700
- const status = items[i]?.status;
1701
- return status === "pending" ? line : `${line.padEnd(width + 4)}(${status})`;
1702
- }).join("\n");
1703
- }
1704
- var defaultStore = new TodoStore();
1705
- function createTodoTools(store = defaultStore) {
1706
- const todoWrite = {
1707
- name: "todo_write",
1708
- description: "Replace the current todo list with the given items. Each item has an id, title, and status (pending, in_progress, done).",
1709
- permission: "read",
1710
- parameters: writeSchema,
1711
- async execute(args, ctx) {
1712
- const parsed = writeSchema.safeParse(args);
1713
- if (!parsed.success) {
1714
- return { content: `Invalid todos: ${parsed.error.message}`, isError: true };
1715
- }
1716
- await store.load(ctx.cwd);
1717
- store.replace(parsed.data.todos);
1718
- await store.save(ctx.cwd);
1719
- return { content: formatTodos(store.list()) };
1720
- }
1721
- };
1722
- const todoRead = {
1723
- name: "todo_read",
1724
- description: "Read the current todo list.",
1725
- permission: "read",
1726
- parameters: readSchema,
1727
- async execute(_args, ctx) {
1728
- await store.load(ctx.cwd);
1729
- return { content: formatTodos(store.list()) };
1730
- }
1731
- };
1732
- return [todoWrite, todoRead];
1733
- }
1734
1998
 
1735
1999
  // src/tools/web/fetch.ts
1736
2000
  import { z as z10 } from "zod";
@@ -2029,69 +2293,86 @@ function createDefaultRegistry() {
2029
2293
  var MAX_SUBAGENT_DEPTH = 1;
2030
2294
  var MAX_REPORT_CHARS = 2e4;
2031
2295
  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.";
2296
+ async function runSubagent(deps, args, cwd, signal) {
2297
+ const { AgentLoop: AgentLoop2 } = await import("./loop-ED6AAMWV.js");
2298
+ const child = new AgentLoop2({
2299
+ model: deps.model,
2300
+ registry: createDefaultRegistry(),
2301
+ config: deps.config,
2302
+ cwd,
2303
+ system: deps.system ? `${deps.system}
2304
+
2305
+ ${SUBAGENT_PROMPT}` : SUBAGENT_PROMPT,
2306
+ subagentDepth: deps.depth + 1
2307
+ });
2308
+ const confirmHandler = deps.getConfirmHandler?.();
2309
+ if (confirmHandler) child.confirmHandler = confirmHandler;
2310
+ let toolCalls = 0;
2311
+ let error = null;
2312
+ try {
2313
+ for await (const event of child.stream(args.prompt, signal)) {
2314
+ if (event.type === "tool-result") {
2315
+ toolCalls++;
2316
+ } else if (event.type === "error") {
2317
+ error = event.error.message;
2318
+ break;
2319
+ }
2320
+ }
2321
+ } catch (e) {
2322
+ error = e instanceof Error ? e.message : String(e);
2323
+ }
2324
+ let report = "";
2325
+ const messages = child.getMessages();
2326
+ for (let i = messages.length - 1; i >= 0; i--) {
2327
+ const message = messages[i];
2328
+ if (message?.role !== "assistant" || !Array.isArray(message.content)) continue;
2329
+ const text = message.content.filter((part) => part.type === "text").map((part) => part.text).join("");
2330
+ if (text) {
2331
+ report = text;
2332
+ break;
2333
+ }
2334
+ }
2335
+ if (report.length > MAX_REPORT_CHARS) {
2336
+ report = `${report.slice(0, MAX_REPORT_CHARS)}
2337
+ ... (truncated)`;
2338
+ }
2339
+ const label = args.description ? ` "${args.description}"` : "";
2340
+ const stats = `[subagent${label}: ${toolCalls} tool call(s)]`;
2341
+ if (error) {
2342
+ throw new Error([`Subagent failed: ${error}`, stats, report].filter(Boolean).join("\n"));
2343
+ }
2344
+ return `${stats}
2345
+ ${report || "(subagent produced no text output)"}`;
2346
+ }
2032
2347
  function createSubagentTool(deps) {
2033
2348
  return {
2034
2349
  name: "subagent",
2035
- description: "Spawn a subagent with its own agent loop to handle a focused subtask (research, exploration, or an isolated change). The subagent has the same tools but cannot spawn further subagents. Returns the subagent's final report. Prefer this for self-contained work that would otherwise clutter the main conversation.",
2350
+ description: "Spawn a subagent with its own agent loop to handle a focused subtask (research, exploration, or an isolated change). The subagent has the same tools but cannot spawn further subagents. Returns the subagent's final report. Prefer this for self-contained work that would otherwise clutter the main conversation. Set run_in_background to start it without blocking: independent subtasks then run concurrently and their reports are delivered to you automatically when they finish (also inspectable via task_list/task_output/task_kill).",
2036
2351
  permission: "exec",
2037
2352
  parameters: z12.object({
2038
2353
  prompt: z12.string().describe("Complete, self-contained instructions for the subagent."),
2039
- description: z12.string().optional().describe("Short label for the subtask, shown in the result header.")
2354
+ description: z12.string().optional().describe("Short label for the subtask, shown in the result header."),
2355
+ run_in_background: z12.boolean().optional().describe(
2356
+ "Run concurrently in the background and return a task id immediately (default false). Use for independent subtasks that can run in parallel."
2357
+ )
2040
2358
  }),
2041
2359
  async execute(args, ctx) {
2042
- const { AgentLoop: AgentLoop2 } = await import("./loop-ADVWZA62.js");
2043
- const child = new AgentLoop2({
2044
- model: deps.model,
2045
- registry: createDefaultRegistry(),
2046
- config: deps.config,
2047
- cwd: ctx.cwd,
2048
- system: deps.system ? `${deps.system}
2049
-
2050
- ${SUBAGENT_PROMPT}` : SUBAGENT_PROMPT,
2051
- subagentDepth: deps.depth + 1
2052
- });
2053
- const confirmHandler = deps.getConfirmHandler?.();
2054
- if (confirmHandler) child.confirmHandler = confirmHandler;
2360
+ if (args.run_in_background) {
2361
+ const task = defaultAgentTasks.start((signal2) => runSubagent(deps, args, ctx.cwd, signal2), {
2362
+ prompt: args.prompt,
2363
+ description: args.description
2364
+ });
2365
+ const label = args.description ? ` "${args.description}"` : "";
2366
+ return {
2367
+ content: `Background subagent ${task.id}${label} started. Its report will be delivered as a message when it finishes; use task_list/task_output to check progress.`
2368
+ };
2369
+ }
2055
2370
  const signal = ctx.abortSignal ?? new AbortController().signal;
2056
- let toolCalls = 0;
2057
- let error = null;
2058
2371
  try {
2059
- for await (const event of child.stream(args.prompt, signal)) {
2060
- if (event.type === "tool-result") {
2061
- toolCalls++;
2062
- } else if (event.type === "error") {
2063
- error = event.error.message;
2064
- break;
2065
- }
2066
- }
2372
+ return { content: await runSubagent(deps, args, ctx.cwd, signal) };
2067
2373
  } catch (e) {
2068
- error = e instanceof Error ? e.message : String(e);
2069
- }
2070
- let report = "";
2071
- const messages = child.getMessages();
2072
- for (let i = messages.length - 1; i >= 0; i--) {
2073
- const message = messages[i];
2074
- if (message?.role !== "assistant" || !Array.isArray(message.content)) continue;
2075
- const text = message.content.filter((part) => part.type === "text").map((part) => part.text).join("");
2076
- if (text) {
2077
- report = text;
2078
- break;
2079
- }
2080
- }
2081
- if (report.length > MAX_REPORT_CHARS) {
2082
- report = `${report.slice(0, MAX_REPORT_CHARS)}
2083
- ... (truncated)`;
2374
+ return { content: e instanceof Error ? e.message : String(e), isError: true };
2084
2375
  }
2085
- const label = args.description ? ` "${args.description}"` : "";
2086
- const stats = `[subagent${label}: ${toolCalls} tool call(s)]`;
2087
- if (error) {
2088
- return {
2089
- content: [`Subagent failed: ${error}`, stats, report].filter(Boolean).join("\n"),
2090
- isError: true
2091
- };
2092
- }
2093
- return { content: `${stats}
2094
- ${report || "(subagent produced no text output)"}` };
2095
2376
  }
2096
2377
  };
2097
2378
  }
@@ -2156,6 +2437,79 @@ function createRememberTool(deps = {}) {
2156
2437
 
2157
2438
  // src/agent/loop.ts
2158
2439
  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.";
2440
+ 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;
2441
+ 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.";
2442
+ 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.";
2443
+ function isSteerableEmptyFinish(finishReason) {
2444
+ return finishReason === "stop" || finishReason === "content-filter";
2445
+ }
2446
+ function finalNudgeWarning(autoContinues, maxAutoContinues) {
2447
+ 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." : "";
2448
+ }
2449
+ var COMPLETION_CHECK_TIMEOUT_MS = 3e4;
2450
+ async function checkTaskComplete(model, request, finalReply, signal) {
2451
+ const controller = new AbortController();
2452
+ const timer = setTimeout(() => controller.abort(), COMPLETION_CHECK_TIMEOUT_MS);
2453
+ const onAbort = () => controller.abort();
2454
+ signal.addEventListener("abort", onAbort, { once: true });
2455
+ try {
2456
+ const { text } = await generateText3({
2457
+ model,
2458
+ maxTokens: 8,
2459
+ abortSignal: controller.signal,
2460
+ prompt: [
2461
+ "An AI coding agent was given this task by the user:",
2462
+ `<task>
2463
+ ${request.slice(0, 2e3)}
2464
+ </task>`,
2465
+ "The agent has stopped calling tools and ended with this reply:",
2466
+ `<reply>
2467
+ ${finalReply.slice(0, 2e3)}
2468
+ </reply>`,
2469
+ "Has the agent fully completed the task \u2014 every requested action actually performed, and the result verified by running a check (tests, build, or inspecting the produced output) rather than assumed? Or is it only describing, planning, or reporting progress with work still left? Answer with exactly one word: DONE or NOT_DONE."
2470
+ ].join("\n")
2471
+ });
2472
+ const verdict = text.trim().toUpperCase();
2473
+ if (verdict.startsWith("NOT_DONE")) return false;
2474
+ if (verdict.startsWith("DONE")) return true;
2475
+ return null;
2476
+ } catch {
2477
+ return null;
2478
+ } finally {
2479
+ clearTimeout(timer);
2480
+ signal.removeEventListener("abort", onAbort);
2481
+ }
2482
+ }
2483
+ var STREAM_RETRY_BASE_DELAY_MS = 1e3;
2484
+ var NON_RETRYABLE_ERROR_NAMES = /* @__PURE__ */ new Set([
2485
+ "AI_NoSuchToolError",
2486
+ "AI_InvalidToolArgumentsError",
2487
+ "AI_InvalidPromptError",
2488
+ "AI_NoSuchModelError",
2489
+ "AI_LoadAPIKeyError"
2490
+ ]);
2491
+ function isRetryableStreamError(error) {
2492
+ if (NON_RETRYABLE_ERROR_NAMES.has(error.name)) return false;
2493
+ const status = error.statusCode;
2494
+ if (typeof status === "number") {
2495
+ return status === 408 || status === 429 || status >= 500;
2496
+ }
2497
+ return true;
2498
+ }
2499
+ function sleep(ms, signal) {
2500
+ if (signal.aborted) return Promise.resolve(false);
2501
+ return new Promise((resolve) => {
2502
+ const onAbort = () => {
2503
+ clearTimeout(timer);
2504
+ resolve(false);
2505
+ };
2506
+ const timer = setTimeout(() => {
2507
+ signal.removeEventListener("abort", onAbort);
2508
+ resolve(true);
2509
+ }, ms);
2510
+ signal.addEventListener("abort", onAbort, { once: true });
2511
+ });
2512
+ }
2159
2513
  var AgentLoop = class {
2160
2514
  messages = [];
2161
2515
  opts;
@@ -2226,6 +2580,18 @@ var AgentLoop = class {
2226
2580
  this.messages = reconcileToolCalls(messages);
2227
2581
  this.turnMarkers = [];
2228
2582
  }
2583
+ // Read-only counterpart of retractLastTurn: how many messages /undo would
2584
+ // drop and which snapshot turn's file changes it would revert (undefined =
2585
+ // no verified turn, so no file snapshots may be reverted). Mutates nothing,
2586
+ // so the REPL can show the preview before the user confirms.
2587
+ previewLastTurnRetraction() {
2588
+ const result = retractLastTurn([...this.messages]);
2589
+ if (result.removed === 0) return { removed: 0 };
2590
+ const userIndex = result.messages.length;
2591
+ const last = this.turnMarkers[this.turnMarkers.length - 1];
2592
+ const turn = last && last.userIndex === userIndex ? last.seq : void 0;
2593
+ return { removed: result.removed, turn };
2594
+ }
2229
2595
  // Drops the final user message and everything after it, and persists the
2230
2596
  // trimmed history so a later /resume does not bring the turn back.
2231
2597
  // `turn` is the retracted turn's snapshot seq when it could be verified
@@ -2279,6 +2645,16 @@ var AgentLoop = class {
2279
2645
  async persist(message) {
2280
2646
  await this.opts.sessionStore?.append(message);
2281
2647
  }
2648
+ // Replaces oversized image parts in the in-memory history with text
2649
+ // placeholders and rewrites the persisted session, so the failed request
2650
+ // can be resent and a later resume never carries the offending images.
2651
+ async stripOversizedImages() {
2652
+ const { messages, removed } = stripOversizedImages(this.messages);
2653
+ if (removed === 0) return 0;
2654
+ this.messages = messages;
2655
+ await this.opts.sessionStore?.replaceMessages([...this.messages]);
2656
+ return removed;
2657
+ }
2282
2658
  // After the first assistant reply, kick off background title generation
2283
2659
  // for the session. Runs once per loop; the store-level title check makes
2284
2660
  // resumed sessions that already have a title a no-op.
@@ -2289,9 +2665,50 @@ var AgentLoop = class {
2289
2665
  this.titleScheduled = true;
2290
2666
  scheduleSessionTitle(store, input, this.opts.model);
2291
2667
  }
2668
+ // A user interrupt (Esc) ends the turn mid-stream, before the normal
2669
+ // assistant-message persistence runs. Whatever text and tool calls already
2670
+ // arrived are still persisted — the text marked with "[interrupted]" so the
2671
+ // cut-off stays visible after a resume and the model can tell its reply was
2672
+ // cut short — and streamed tool calls are closed with synthetic results so
2673
+ // the stored history stays valid for the API.
2674
+ async *persistInterrupted(text, toolCalls) {
2675
+ if (text.length === 0 && toolCalls.length === 0) return;
2676
+ const assistantMessage = {
2677
+ role: "assistant",
2678
+ content: [
2679
+ ...text ? [{ type: "text", text: `${text} [interrupted]` }] : [],
2680
+ ...toolCalls.map((call) => ({
2681
+ type: "tool-call",
2682
+ toolCallId: call.id,
2683
+ toolName: call.name,
2684
+ args: call.args
2685
+ }))
2686
+ ]
2687
+ };
2688
+ this.messages.push(assistantMessage);
2689
+ await this.persist(assistantMessage);
2690
+ for (const call of toolCalls) {
2691
+ const content = "Tool execution interrupted by user.";
2692
+ const synthetic = {
2693
+ role: "tool",
2694
+ content: [
2695
+ {
2696
+ type: "tool-result",
2697
+ toolCallId: call.id,
2698
+ toolName: call.name,
2699
+ result: content
2700
+ }
2701
+ ]
2702
+ };
2703
+ this.messages.push(synthetic);
2704
+ await this.persist(synthetic).catch(() => {
2705
+ });
2706
+ yield { type: "tool-result", id: call.id, name: call.name, content, isError: true };
2707
+ }
2708
+ }
2292
2709
  async *stream(input, signal, opts) {
2293
2710
  this.syncSystemMessage();
2294
- const text = typeof input === "string" ? input : input.text;
2711
+ const inputText = typeof input === "string" ? input : input.text;
2295
2712
  const images = typeof input === "string" ? [] : input.images;
2296
2713
  const userMessage = images.length > 0 ? {
2297
2714
  role: "user",
@@ -2301,9 +2718,9 @@ var AgentLoop = class {
2301
2718
  image: img.data,
2302
2719
  mimeType: img.mimeType
2303
2720
  })),
2304
- { type: "text", text }
2721
+ { type: "text", text: inputText }
2305
2722
  ]
2306
- } : { role: "user", content: text };
2723
+ } : { role: "user", content: inputText };
2307
2724
  this.messages.push(userMessage);
2308
2725
  const seq = (this.opts.subagentDepth ?? 0) === 0 ? beginTurn(this.messages.length - 1) : currentTurnSeq();
2309
2726
  this.turnMarkers.push({ seq, userIndex: this.messages.length - 1 });
@@ -2312,42 +2729,123 @@ var AgentLoop = class {
2312
2729
  );
2313
2730
  const { config, registry, cwd } = this.opts;
2314
2731
  const aiTools = this.buildAiTools();
2732
+ let autoContinues = 0;
2733
+ let lastWasNudge = false;
2734
+ let usedToolsThisTurn = false;
2735
+ let openTodos = [];
2315
2736
  for (let step = 0; step < config.maxSteps; step++) {
2737
+ if ((this.opts.subagentDepth ?? 0) === 0) {
2738
+ for (const finished of defaultAgentTasks.drainNotifications()) {
2739
+ const label = finished.description ? ` "${finished.description}"` : "";
2740
+ const note = {
2741
+ role: "user",
2742
+ content: `[background subagent ${finished.id}${label} ${finished.status}]
2743
+ ${finished.result}`
2744
+ };
2745
+ this.messages.push(note);
2746
+ await this.persist(note);
2747
+ }
2748
+ }
2316
2749
  const maxTokens = this.opts.contextMaxTokens ?? config.contextMaxTokens;
2317
2750
  const compacted = compactMessages([...this.messages], maxTokens);
2318
2751
  if (compacted.compacted) {
2319
2752
  this.messages = await this.applyCompactionSummary(compacted);
2320
2753
  }
2321
- let text2 = "";
2754
+ const maxRetries = Math.max(0, config.streamMaxRetries);
2755
+ const baseDelay = this.opts.retryDelayMs ?? STREAM_RETRY_BASE_DELAY_MS;
2756
+ let text = "";
2322
2757
  const toolCalls = [];
2323
- let failed = false;
2324
- try {
2325
- for await (const event of this.streamOnce(aiTools, signal)) {
2326
- if (event.type === "text-delta") {
2327
- text2 += event.text;
2328
- yield event;
2329
- } else if (event.type === "reasoning") {
2330
- yield event;
2331
- } else if (event.type === "tool-call") {
2332
- toolCalls.push({ id: event.id, name: event.name, args: event.args });
2333
- yield event;
2334
- } else if (event.type === "error") {
2335
- failed = true;
2336
- yield event;
2337
- } else {
2338
- yield event;
2758
+ let failure = null;
2759
+ let lastFinishReason;
2760
+ let oversizedImagesStripped = false;
2761
+ for (let attempt = 0; ; attempt++) {
2762
+ text = "";
2763
+ toolCalls.length = 0;
2764
+ failure = null;
2765
+ lastFinishReason = void 0;
2766
+ const timeoutScale = Math.min(attempt + 1, 3);
2767
+ try {
2768
+ for await (const event of this.streamOnce(aiTools, signal, timeoutScale)) {
2769
+ if (event.type === "text-delta") {
2770
+ text += event.text;
2771
+ yield event;
2772
+ } else if (event.type === "reasoning") {
2773
+ yield event;
2774
+ } else if (event.type === "tool-call") {
2775
+ toolCalls.push({ id: event.id, name: event.name, args: event.args });
2776
+ yield event;
2777
+ } else if (event.type === "finish") {
2778
+ lastFinishReason = event.finishReason;
2779
+ yield event;
2780
+ } else if (event.type === "error") {
2781
+ failure = event.error;
2782
+ } else {
2783
+ yield event;
2784
+ }
2785
+ }
2786
+ } catch (error) {
2787
+ if (!signal.aborted) {
2788
+ failure = error instanceof Error ? error : new Error(String(error));
2789
+ }
2790
+ }
2791
+ if (signal.aborted) {
2792
+ yield* this.persistInterrupted(text, toolCalls);
2793
+ return;
2794
+ }
2795
+ if (!failure && (text.length > 0 || toolCalls.length > 0)) break;
2796
+ if (failure && !oversizedImagesStripped && isOversizedImageError(failure)) {
2797
+ oversizedImagesStripped = true;
2798
+ const removed = await this.stripOversizedImages();
2799
+ if (removed > 0) {
2800
+ yield {
2801
+ type: "notice",
2802
+ message: `Removed ${removed} oversized image(s) the model rejected; retrying the request.`
2803
+ };
2804
+ continue;
2339
2805
  }
2340
2806
  }
2341
- } catch (error) {
2342
- if (signal.aborted) return;
2343
- yield { type: "error", error: error instanceof Error ? error : new Error(String(error)) };
2807
+ if (failure && !isRetryableStreamError(failure)) break;
2808
+ if (attempt >= maxRetries) break;
2809
+ if (!failure && text.length === 0 && toolCalls.length === 0 && attempt >= 1 && isSteerableEmptyFinish(lastFinishReason))
2810
+ break;
2811
+ yield {
2812
+ type: "retry",
2813
+ attempt: attempt + 2,
2814
+ maxAttempts: maxRetries + 1,
2815
+ reason: failure ? failure.message : `empty response (finish: ${lastFinishReason ?? "none"})`
2816
+ };
2817
+ if (!await sleep(baseDelay * 2 ** attempt, signal)) return;
2818
+ }
2819
+ if (failure) {
2820
+ yield { type: "error", error: failure };
2821
+ return;
2822
+ }
2823
+ if (text.length === 0 && toolCalls.length === 0) {
2824
+ if (isSteerableEmptyFinish(lastFinishReason) && config.permissionMode !== "plan" && autoContinues < config.maxAutoContinues) {
2825
+ autoContinues++;
2826
+ lastWasNudge = true;
2827
+ yield {
2828
+ type: "notice",
2829
+ message: `the model returned an empty reply; asking the model to continue (${autoContinues}/${config.maxAutoContinues}).`
2830
+ };
2831
+ const nudge = {
2832
+ role: "user",
2833
+ content: EMPTY_REPLY_NUDGE + finalNudgeWarning(autoContinues, config.maxAutoContinues)
2834
+ };
2835
+ this.messages.push(nudge);
2836
+ await this.persist(nudge);
2837
+ continue;
2838
+ }
2839
+ yield {
2840
+ type: "error",
2841
+ error: new Error("The model returned an empty response after retries; ending the turn.")
2842
+ };
2344
2843
  return;
2345
2844
  }
2346
- if (failed) return;
2347
2845
  const assistantMessage = {
2348
2846
  role: "assistant",
2349
2847
  content: [
2350
- ...text2 ? [{ type: "text", text: text2 }] : [],
2848
+ ...text ? [{ type: "text", text }] : [],
2351
2849
  ...toolCalls.map((call) => ({
2352
2850
  type: "tool-call",
2353
2851
  toolCallId: call.id,
@@ -2358,11 +2856,50 @@ var AgentLoop = class {
2358
2856
  };
2359
2857
  this.messages.push(assistantMessage);
2360
2858
  await this.persist(assistantMessage);
2361
- this.maybeScheduleTitle(text2);
2859
+ this.maybeScheduleTitle(text);
2362
2860
  if (toolCalls.length === 0) {
2861
+ let reason = null;
2862
+ let nudgeText = AUTO_CONTINUE_NUDGE;
2863
+ if (openTodos.length > 0) {
2864
+ reason = `${openTodos.length} todo item(s) still open`;
2865
+ nudgeText = `[auto-continue] You ended your turn with unfinished todo item(s): ${openTodos.map((t) => `"${t}"`).join(
2866
+ ", "
2867
+ )}. Complete them now with tool calls, or update the list with todo_write if they are no longer needed.`;
2868
+ } else if (lastWasNudge) {
2869
+ reason = "reply still had no tool calls";
2870
+ } else if (PENDING_WORK_PATTERN.test(text)) {
2871
+ reason = "reply announced unfinished work";
2872
+ } else if (usedToolsThisTurn && !signal.aborted) {
2873
+ const complete = await checkTaskComplete(this.opts.model, inputText, text, signal);
2874
+ if (complete === false) reason = "completion check reports the task unfinished";
2875
+ }
2876
+ if (reason !== null && config.permissionMode !== "plan" && autoContinues < config.maxAutoContinues) {
2877
+ autoContinues++;
2878
+ lastWasNudge = true;
2879
+ yield {
2880
+ type: "notice",
2881
+ message: `${reason}; asking the model to continue (${autoContinues}/${config.maxAutoContinues}).`
2882
+ };
2883
+ const nudge = {
2884
+ role: "user",
2885
+ content: nudgeText + finalNudgeWarning(autoContinues, config.maxAutoContinues)
2886
+ };
2887
+ this.messages.push(nudge);
2888
+ await this.persist(nudge);
2889
+ continue;
2890
+ }
2891
+ if (reason !== null && config.permissionMode !== "plan") {
2892
+ yield {
2893
+ type: "notice",
2894
+ message: `${reason}; auto-continue limit reached, handing the turn back \u2014 reply "continue" to keep going.`
2895
+ };
2896
+ }
2363
2897
  await this.runEventHooks("Stop");
2364
2898
  return;
2365
2899
  }
2900
+ lastWasNudge = false;
2901
+ usedToolsThisTurn = true;
2902
+ autoContinues = 0;
2366
2903
  const answered = /* @__PURE__ */ new Set();
2367
2904
  try {
2368
2905
  for (const call of toolCalls) {
@@ -2382,6 +2919,9 @@ var AgentLoop = class {
2382
2919
  this.messages.push(toolMessage);
2383
2920
  await this.persist(toolMessage);
2384
2921
  answered.add(call.id);
2922
+ if (call.name === "todo_write" && !result.isError) {
2923
+ openTodos = pendingTodoTitles(call.args);
2924
+ }
2385
2925
  yield {
2386
2926
  type: "tool-result",
2387
2927
  id: call.id,
@@ -2472,14 +3012,15 @@ ${summary}`
2472
3012
  return compacted.messages;
2473
3013
  }
2474
3014
  }
2475
- async *streamOnce(aiTools, signal) {
2476
- const { streamChat } = await import("./stream-Y55NRF2S.js");
3015
+ async *streamOnce(aiTools, signal, timeoutScale = 1) {
3016
+ const { streamChat } = await import("./stream-YQCRUBO3.js");
2477
3017
  yield* streamChat({
2478
3018
  model: this.opts.model,
2479
3019
  messages: this.messages,
2480
3020
  tools: aiTools,
2481
3021
  abortSignal: signal,
2482
- idleTimeoutMs: this.opts.config.streamIdleTimeoutSec * 1e3
3022
+ idleTimeoutMs: this.opts.config.streamIdleTimeoutSec * 1e3 * timeoutScale,
3023
+ firstPartTimeoutMs: this.opts.config.streamFirstChunkTimeoutSec * 1e3 * timeoutScale
2483
3024
  });
2484
3025
  }
2485
3026
  buildAiTools() {
@@ -2572,6 +3113,7 @@ ${summary}`
2572
3113
  };
2573
3114
 
2574
3115
  export {
3116
+ defaultAgentTasks,
2575
3117
  estimateTokens,
2576
3118
  summarizeMessages,
2577
3119
  compactMessages,
@@ -2581,6 +3123,8 @@ export {
2581
3123
  collectCommitContext,
2582
3124
  buildCommitPrompt,
2583
3125
  collectWorkingDiff,
3126
+ MAX_IMAGE_DIMENSION,
3127
+ probeImageDimensions,
2584
3128
  reconcileToolCalls,
2585
3129
  isAllowedByRules,
2586
3130
  buildAllowRule,
@@ -2588,12 +3132,20 @@ export {
2588
3132
  listSnapshots,
2589
3133
  clearSnapshots,
2590
3134
  hydrateSnapshots,
3135
+ listTurnSnapshots,
3136
+ resolveSnapshotContent,
2591
3137
  undoTurnSnapshots,
2592
3138
  rewindToSnapshot,
3139
+ TodoStore,
3140
+ formatTodos,
3141
+ parseTodoArgs,
3142
+ loadTodos,
3143
+ resetTodos,
2593
3144
  starHome,
2594
3145
  globalConfigPath,
2595
3146
  sessionsDir,
2596
3147
  userMemoryPath,
3148
+ envFilePath,
2597
3149
  projectConfigPath,
2598
3150
  discoverSkills,
2599
3151
  defaultTaskManager,
@@ -2605,11 +3157,9 @@ export {
2605
3157
  formatTaskList,
2606
3158
  formatTaskStarted,
2607
3159
  formatTaskFinished,
2608
- TodoStore,
2609
- formatTodos,
2610
3160
  createDefaultRegistry,
2611
3161
  appendUserMemory,
2612
3162
  PLAN_MODE_PROMPT,
2613
3163
  AgentLoop
2614
3164
  };
2615
- //# sourceMappingURL=chunk-GN236FD4.js.map
3165
+ //# sourceMappingURL=chunk-K4SIXWCI.js.map