@cryer/star-cli 0.2.1 → 0.3.0

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";
@@ -433,6 +433,12 @@ function matchAllowRule(rule, req) {
433
433
  return target !== void 0 && globMatch(rule.pattern, target);
434
434
  }
435
435
  function isAllowedByRules(rules, req) {
436
+ return matchesAnyRule(rules, req);
437
+ }
438
+ function isDeniedByRules(rules, req) {
439
+ return matchesAnyRule(rules, req);
440
+ }
441
+ function matchesAnyRule(rules, req) {
436
442
  return rules.some((raw) => {
437
443
  const rule = parseAllowRule(raw);
438
444
  return rule !== null && matchAllowRule(rule, req);
@@ -484,7 +490,7 @@ function isSensitivePath(p) {
484
490
  }
485
491
  return base === ".env" || base.startsWith(".env.") || base === "id_rsa" || base.endsWith(".pem");
486
492
  }
487
- function checkPermission(mode, req, ctx, allowRules = []) {
493
+ function checkPermission(mode, req, ctx, allowRules = [], denyRules = []) {
488
494
  if (mode === "yolo") {
489
495
  return "allow";
490
496
  }
@@ -504,6 +510,9 @@ function checkPermission(mode, req, ctx, allowRules = []) {
504
510
  return "deny";
505
511
  }
506
512
  }
513
+ if (isDeniedByRules(denyRules, req)) {
514
+ return "deny";
515
+ }
507
516
  if (mode === "auto") {
508
517
  return "allow";
509
518
  }
@@ -693,14 +702,211 @@ async function rewindToSnapshot(id) {
693
702
  return { reverted, messageIndex };
694
703
  }
695
704
 
705
+ // src/tools/todo.ts
706
+ import { mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
707
+ import path3 from "path";
708
+ import { z } from "zod";
709
+ var todoItemSchema = z.object({
710
+ id: z.number().int(),
711
+ title: z.string(),
712
+ status: z.enum(["pending", "in_progress", "done"])
713
+ });
714
+ var writeSchema = z.object({
715
+ todos: z.array(todoItemSchema).describe("The full todo list, replacing the current one")
716
+ });
717
+ var readSchema = z.object({});
718
+ function fileFor(cwd) {
719
+ return path3.join(cwd, ".star", "todos.json");
720
+ }
721
+ var TodoStore = class {
722
+ items = /* @__PURE__ */ new Map();
723
+ list() {
724
+ return [...this.items.values()];
725
+ }
726
+ replace(items) {
727
+ this.items.clear();
728
+ for (const item of items) {
729
+ this.items.set(item.id, item);
730
+ }
731
+ }
732
+ async load(cwd) {
733
+ let raw;
734
+ try {
735
+ raw = await readFile2(fileFor(cwd), "utf8");
736
+ } catch {
737
+ return;
738
+ }
739
+ try {
740
+ const parsed = JSON.parse(raw);
741
+ if (Array.isArray(parsed)) {
742
+ this.replace(parsed.filter((it) => todoItemSchema.safeParse(it).success));
743
+ }
744
+ } catch {
745
+ }
746
+ }
747
+ async save(cwd) {
748
+ await mkdir2(path3.join(cwd, ".star"), { recursive: true });
749
+ await writeFile2(fileFor(cwd), `${JSON.stringify(this.list(), null, 2)}
750
+ `);
751
+ }
752
+ };
753
+ var SYMBOLS = {
754
+ pending: "[ ]",
755
+ in_progress: "[~]",
756
+ done: "[x]"
757
+ };
758
+ function formatTodos(items) {
759
+ if (items.length === 0) {
760
+ return "No todos.";
761
+ }
762
+ const lines = items.map((it) => `${SYMBOLS[it.status]} ${it.id}. ${it.title}`);
763
+ const width = Math.max(...lines.map((l) => l.length));
764
+ return lines.map((line, i) => {
765
+ const status = items[i]?.status;
766
+ return status === "pending" ? line : `${line.padEnd(width + 4)}(${status})`;
767
+ }).join("\n");
768
+ }
769
+ var defaultStore = new TodoStore();
770
+ function parseTodoArgs(args) {
771
+ const parsed = writeSchema.safeParse(args);
772
+ return parsed.success ? parsed.data.todos : null;
773
+ }
774
+ function pendingTodoTitles(args) {
775
+ return (parseTodoArgs(args) ?? []).filter((t) => t.status !== "done").map((t) => t.title);
776
+ }
777
+ async function loadTodos(cwd) {
778
+ await defaultStore.load(cwd);
779
+ return defaultStore.list();
780
+ }
781
+ function createTodoTools(store = defaultStore) {
782
+ const todoWrite = {
783
+ name: "todo_write",
784
+ description: "Replace the current todo list with the given items. Each item has an id, title, and status (pending, in_progress, done).",
785
+ permission: "read",
786
+ parameters: writeSchema,
787
+ async execute(args, ctx) {
788
+ const parsed = writeSchema.safeParse(args);
789
+ if (!parsed.success) {
790
+ return { content: `Invalid todos: ${parsed.error.message}`, isError: true };
791
+ }
792
+ await store.load(ctx.cwd);
793
+ store.replace(parsed.data.todos);
794
+ await store.save(ctx.cwd);
795
+ return { content: formatTodos(store.list()) };
796
+ }
797
+ };
798
+ const todoRead = {
799
+ name: "todo_read",
800
+ description: "Read the current todo list.",
801
+ permission: "read",
802
+ parameters: readSchema,
803
+ async execute(_args, ctx) {
804
+ await store.load(ctx.cwd);
805
+ return { content: formatTodos(store.list()) };
806
+ }
807
+ };
808
+ return [todoWrite, todoRead];
809
+ }
810
+
811
+ // src/agent/agent-tasks.ts
812
+ import { EventEmitter } from "events";
813
+ var AgentTaskManager = class extends EventEmitter {
814
+ records = /* @__PURE__ */ new Map();
815
+ counter = 0;
816
+ start(run, meta) {
817
+ const id = `agent-${++this.counter}`;
818
+ const rec = {
819
+ id,
820
+ prompt: meta.prompt,
821
+ description: meta.description,
822
+ status: "running",
823
+ startedAt: Date.now(),
824
+ result: "",
825
+ controller: new AbortController(),
826
+ notified: false
827
+ };
828
+ this.records.set(id, rec);
829
+ void run(rec.controller.signal).then((result) => this.finish(rec, "completed", result)).catch(
830
+ (error) => this.finish(rec, "failed", error instanceof Error ? error.message : String(error))
831
+ );
832
+ this.emitUpdate(rec);
833
+ return this.snapshot(rec);
834
+ }
835
+ get(id) {
836
+ const rec = this.records.get(id);
837
+ return rec ? this.snapshot(rec) : void 0;
838
+ }
839
+ list() {
840
+ return [...this.records.values()].map((rec) => this.snapshot(rec));
841
+ }
842
+ runningCount() {
843
+ let count = 0;
844
+ for (const rec of this.records.values()) {
845
+ if (rec.status === "running") count += 1;
846
+ }
847
+ return count;
848
+ }
849
+ kill(id) {
850
+ const rec = this.records.get(id);
851
+ if (!rec || rec.status !== "running") return void 0;
852
+ rec.controller.abort();
853
+ this.finish(rec, "stopped", rec.result || "Stopped by user.");
854
+ return this.snapshot(rec);
855
+ }
856
+ cleanup() {
857
+ const killed = [];
858
+ for (const rec of this.records.values()) {
859
+ if (rec.status === "running") {
860
+ rec.controller.abort();
861
+ this.finish(rec, "stopped", rec.result || "Stopped on shutdown.");
862
+ killed.push(this.snapshot(rec));
863
+ }
864
+ }
865
+ return killed;
866
+ }
867
+ // Finished tasks whose result has not yet been handed to the parent loop.
868
+ drainNotifications() {
869
+ const drained = [];
870
+ for (const rec of this.records.values()) {
871
+ if (rec.status !== "running" && !rec.notified) {
872
+ rec.notified = true;
873
+ drained.push(this.snapshot(rec));
874
+ }
875
+ }
876
+ return drained;
877
+ }
878
+ finish(rec, status, result) {
879
+ if (rec.status !== "running") return;
880
+ rec.status = status;
881
+ rec.result = result;
882
+ rec.endedAt = Date.now();
883
+ this.emitUpdate(rec);
884
+ }
885
+ emitUpdate(rec) {
886
+ this.emit("update", this.snapshot(rec));
887
+ }
888
+ snapshot(rec) {
889
+ return {
890
+ id: rec.id,
891
+ prompt: rec.prompt,
892
+ description: rec.description,
893
+ status: rec.status,
894
+ startedAt: rec.startedAt,
895
+ endedAt: rec.endedAt,
896
+ result: rec.result
897
+ };
898
+ }
899
+ };
900
+ var defaultAgentTasks = new AgentTaskManager();
901
+
696
902
  // src/agent/project-memory.ts
697
903
  import { readFileSync, statSync } from "fs";
698
- import path3 from "path";
904
+ import path4 from "path";
699
905
  var PROJECT_MEMORY_FILE = "AGENTS.md";
700
906
  var PROJECT_MEMORY_MAX_CHARS = 32 * 1024;
701
907
  var cache = /* @__PURE__ */ new Map();
702
908
  function readProjectMemory(cwd) {
703
- const file = path3.join(cwd, PROJECT_MEMORY_FILE);
909
+ const file = path4.join(cwd, PROJECT_MEMORY_FILE);
704
910
  let mtimeMs;
705
911
  try {
706
912
  mtimeMs = statSync(file).mtimeMs;
@@ -730,23 +936,26 @@ ${body}`;
730
936
 
731
937
  // src/agent/skills.ts
732
938
  import { readFileSync as readFileSync2, readdirSync, statSync as statSync2 } from "fs";
733
- import path5 from "path";
734
- import { z } from "zod";
939
+ import path6 from "path";
940
+ import { z as z2 } from "zod";
735
941
 
736
942
  // src/config/paths.ts
737
943
  import os from "os";
738
- import path4 from "path";
944
+ import path5 from "path";
739
945
  function starHome() {
740
- return process.env.STAR_HOME ?? path4.join(os.homedir(), ".star-cli");
946
+ return process.env.STAR_HOME ?? path5.join(os.homedir(), ".star-cli");
741
947
  }
742
948
  function globalConfigPath() {
743
- return path4.join(starHome(), "config.toml");
949
+ return path5.join(starHome(), "config.toml");
744
950
  }
745
951
  function sessionsDir() {
746
- return path4.join(starHome(), "sessions");
952
+ return path5.join(starHome(), "sessions");
953
+ }
954
+ function userMemoryPath() {
955
+ return path5.join(starHome(), "MEMORY.md");
747
956
  }
748
957
  function projectConfigPath(cwd) {
749
- return path4.join(cwd, ".star", "config.toml");
958
+ return path5.join(cwd, ".star", "config.toml");
750
959
  }
751
960
 
752
961
  // src/agent/skills.ts
@@ -779,7 +988,7 @@ function parseSkillFile(filePath, dirName, scope) {
779
988
  const name = parsed?.meta.name || dirName;
780
989
  const description = parsed?.meta.description;
781
990
  if (parsed && NAME_PATTERN.test(name) && description) {
782
- skill = { name, description, filePath, dir: path5.dirname(filePath), scope };
991
+ skill = { name, description, filePath, dir: path6.dirname(filePath), scope };
783
992
  }
784
993
  } catch {
785
994
  skill = null;
@@ -796,14 +1005,14 @@ function scanDir(base, scope, into) {
796
1005
  }
797
1006
  for (const entry of entries.sort()) {
798
1007
  if (!NAME_PATTERN.test(entry)) continue;
799
- const parsed = parseSkillFile(path5.join(base, entry, SKILL_FILE), entry, scope);
1008
+ const parsed = parseSkillFile(path6.join(base, entry, SKILL_FILE), entry, scope);
800
1009
  if (parsed) into.set(parsed.name, parsed);
801
1010
  }
802
1011
  }
803
1012
  function discoverSkills(cwd, home = starHome()) {
804
1013
  const byName = /* @__PURE__ */ new Map();
805
- scanDir(path5.join(home, "skills"), "user", byName);
806
- scanDir(path5.join(cwd, ".star", "skills"), "project", byName);
1014
+ scanDir(path6.join(home, "skills"), "user", byName);
1015
+ scanDir(path6.join(cwd, ".star", "skills"), "project", byName);
807
1016
  return [...byName.values()];
808
1017
  }
809
1018
  function formatSkillsBlock(skills) {
@@ -834,8 +1043,8 @@ function createSkillTool(deps) {
834
1043
  name: "skill",
835
1044
  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.",
836
1045
  permission: "read",
837
- parameters: z.object({
838
- name: z.string().describe("Name of the skill to load, as listed in the system prompt.")
1046
+ parameters: z2.object({
1047
+ name: z2.string().describe("Name of the skill to load, as listed in the system prompt.")
839
1048
  }),
840
1049
  async execute(args) {
841
1050
  const skills = discoverSkills(deps.cwd, deps.home);
@@ -870,16 +1079,16 @@ import { z as z12 } from "zod";
870
1079
  // src/tools/bash.ts
871
1080
  import { spawn as spawn3 } from "child_process";
872
1081
  import { existsSync } from "fs";
873
- import path6 from "path";
874
- import { z as z2 } from "zod";
1082
+ import path7 from "path";
1083
+ import { z as z3 } from "zod";
875
1084
 
876
1085
  // src/tasks/manager.ts
877
1086
  import { spawn as spawn2 } from "child_process";
878
- import { EventEmitter } from "events";
1087
+ import { EventEmitter as EventEmitter2 } from "events";
879
1088
  var MAX_TASK_OUTPUT = 1e5;
880
1089
  var DEFAULT_TASK_TIMEOUT = 3600;
881
1090
  var TRUNCATED_PREFIX = "[... earlier output truncated ...]\n";
882
- var TaskManager = class extends EventEmitter {
1091
+ var TaskManager = class extends EventEmitter2 {
883
1092
  records = /* @__PURE__ */ new Map();
884
1093
  seq = 0;
885
1094
  start(opts) {
@@ -990,11 +1199,11 @@ var MAX_OUTPUT = 3e4;
990
1199
  var DEFAULT_TIMEOUT = 120;
991
1200
  var MAX_TIMEOUT = 600;
992
1201
  function findOnPath(exe, exclude) {
993
- for (const dir of (process.env.PATH ?? "").split(path6.delimiter)) {
1202
+ for (const dir of (process.env.PATH ?? "").split(path7.delimiter)) {
994
1203
  if (!dir || exclude?.(dir)) {
995
1204
  continue;
996
1205
  }
997
- const full = path6.join(dir, exe);
1206
+ const full = path7.join(dir, exe);
998
1207
  if (existsSync(full)) {
999
1208
  return full;
1000
1209
  }
@@ -1012,7 +1221,7 @@ function resolveShell() {
1012
1221
  }
1013
1222
  const gitExe = findOnPath("git.exe");
1014
1223
  if (gitExe) {
1015
- const sibling = path6.join(path6.dirname(path6.dirname(gitExe)), "bin", "bash.exe");
1224
+ const sibling = path7.join(path7.dirname(path7.dirname(gitExe)), "bin", "bash.exe");
1016
1225
  if (existsSync(sibling)) {
1017
1226
  return { shell: sibling, wrap: (c) => ["-c", c], label: "bash" };
1018
1227
  }
@@ -1029,11 +1238,11 @@ function resolveShell() {
1029
1238
  const comspec = process.env.ComSpec ?? "cmd.exe";
1030
1239
  return { shell: comspec, wrap: (c) => ["/d", "/s", "/c", c], label: "cmd" };
1031
1240
  }
1032
- var schema = z2.object({
1033
- command: z2.string().describe("Shell command to execute"),
1034
- timeout: z2.number().positive().max(MAX_TIMEOUT).optional().describe("Timeout in seconds (default 120, max 600)"),
1035
- description: z2.string().optional().describe("Short description of what the command does"),
1036
- run_in_background: z2.boolean().optional().describe(
1241
+ var schema = z3.object({
1242
+ command: z3.string().describe("Shell command to execute"),
1243
+ timeout: z3.number().positive().max(MAX_TIMEOUT).optional().describe("Timeout in seconds (default 120, max 600)"),
1244
+ description: z3.string().optional().describe("Short description of what the command does"),
1245
+ run_in_background: z3.boolean().optional().describe(
1037
1246
  "Run the command in the background and return a task id immediately (default false). Use task_output/task_kill to inspect or stop it."
1038
1247
  )
1039
1248
  });
@@ -1135,14 +1344,14 @@ Command timed out after ${timeoutSeconds}s`,
1135
1344
  };
1136
1345
 
1137
1346
  // src/tools/fs/edit.ts
1138
- import { readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
1139
- import path7 from "path";
1140
- import { z as z3 } from "zod";
1141
- var schema2 = z3.object({
1142
- path: z3.string().describe("File path, absolute or relative to the working directory"),
1143
- old_string: z3.string().min(1).describe("Exact text to replace"),
1144
- new_string: z3.string().describe("Replacement text"),
1145
- replace_all: z3.boolean().optional().describe("Replace every occurrence instead of requiring a unique match")
1347
+ import { readFile as readFile3, writeFile as writeFile3 } from "fs/promises";
1348
+ import path8 from "path";
1349
+ import { z as z4 } from "zod";
1350
+ var schema2 = z4.object({
1351
+ path: z4.string().describe("File path, absolute or relative to the working directory"),
1352
+ old_string: z4.string().min(1).describe("Exact text to replace"),
1353
+ new_string: z4.string().describe("Replacement text"),
1354
+ replace_all: z4.boolean().optional().describe("Replace every occurrence instead of requiring a unique match")
1146
1355
  });
1147
1356
  var editFileTool = {
1148
1357
  name: "edit_file",
@@ -1150,10 +1359,10 @@ var editFileTool = {
1150
1359
  permission: "write",
1151
1360
  parameters: schema2,
1152
1361
  async execute(args, ctx) {
1153
- const filePath = path7.resolve(ctx.cwd, args.path);
1362
+ const filePath = path8.resolve(ctx.cwd, args.path);
1154
1363
  let content;
1155
1364
  try {
1156
- content = await readFile2(filePath, "utf8");
1365
+ content = await readFile3(filePath, "utf8");
1157
1366
  } catch (err) {
1158
1367
  return { content: `Failed to read ${args.path}: ${err.message}`, isError: true };
1159
1368
  }
@@ -1174,7 +1383,7 @@ var editFileTool = {
1174
1383
  }
1175
1384
  const updated = args.replace_all ? content.split(args.old_string).join(args.new_string) : content.replace(args.old_string, args.new_string);
1176
1385
  try {
1177
- await writeFile2(filePath, updated, "utf8");
1386
+ await writeFile3(filePath, updated, "utf8");
1178
1387
  } catch (err) {
1179
1388
  return { content: `Failed to write ${args.path}: ${err.message}`, isError: true };
1180
1389
  }
@@ -1193,14 +1402,15 @@ var editFileTool = {
1193
1402
  };
1194
1403
 
1195
1404
  // src/tools/fs/glob.ts
1196
- import path9 from "path";
1197
- import { z as z4 } from "zod";
1405
+ import path10 from "path";
1406
+ import { z as z5 } from "zod";
1198
1407
 
1199
1408
  // src/tools/fs/util.ts
1409
+ import { readFileSync as readFileSync3, statSync as statSync3 } from "fs";
1200
1410
  import { readdir, stat } from "fs/promises";
1201
- import path8 from "path";
1411
+ import path9 from "path";
1202
1412
  var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist"]);
1203
- async function walkFiles(root, skipDirs = SKIP_DIRS) {
1413
+ async function walkFiles(root, skipDirs = SKIP_DIRS, options = {}) {
1204
1414
  const out = [];
1205
1415
  async function walk(dir, relBase) {
1206
1416
  const entries = await readdir(dir, { withFileTypes: true }).catch(() => null);
@@ -1208,13 +1418,17 @@ async function walkFiles(root, skipDirs = SKIP_DIRS) {
1208
1418
  return;
1209
1419
  }
1210
1420
  for (const entry of entries) {
1211
- const abs = path8.join(dir, entry.name);
1421
+ const abs = path9.join(dir, entry.name);
1212
1422
  const rel = relBase ? `${relBase}/${entry.name}` : entry.name;
1213
1423
  if (entry.isDirectory()) {
1214
- if (!skipDirs.has(entry.name)) {
1215
- await walk(abs, rel);
1424
+ if (skipDirs.has(entry.name) || options.ignore?.(abs, true)) {
1425
+ continue;
1216
1426
  }
1427
+ await walk(abs, rel);
1217
1428
  } else if (entry.isFile()) {
1429
+ if (options.ignore?.(abs, false)) {
1430
+ continue;
1431
+ }
1218
1432
  try {
1219
1433
  const st = await stat(abs);
1220
1434
  out.push({ abs, rel, mtimeMs: st.mtimeMs });
@@ -1263,8 +1477,71 @@ function matchesGlob(pattern, relPath) {
1263
1477
  }
1264
1478
  return globToRegExp(normalized).test(relPath);
1265
1479
  }
1480
+ var IGNORE_FILE = ".starignore";
1481
+ var ignoreCache = /* @__PURE__ */ new Map();
1482
+ function parseIgnorePatterns(body) {
1483
+ const out = [];
1484
+ for (const raw of body.split("\n")) {
1485
+ const line = raw.trim();
1486
+ if (!line || line.startsWith("#")) {
1487
+ continue;
1488
+ }
1489
+ if (line.endsWith("/")) {
1490
+ const pattern = line.slice(0, -1);
1491
+ if (pattern) {
1492
+ out.push({ pattern, dirOnly: true });
1493
+ }
1494
+ } else {
1495
+ out.push({ pattern: line, dirOnly: false });
1496
+ }
1497
+ }
1498
+ return out;
1499
+ }
1500
+ function loadIgnorePatterns(dir) {
1501
+ const file = path9.join(dir, IGNORE_FILE);
1502
+ let mtimeMs;
1503
+ try {
1504
+ mtimeMs = statSync3(file).mtimeMs;
1505
+ } catch {
1506
+ return [];
1507
+ }
1508
+ const hit = ignoreCache.get(file);
1509
+ if (hit && hit.mtimeMs === mtimeMs) {
1510
+ return hit.patterns;
1511
+ }
1512
+ let patterns = [];
1513
+ try {
1514
+ patterns = parseIgnorePatterns(readFileSync3(file, "utf8"));
1515
+ } catch {
1516
+ patterns = [];
1517
+ }
1518
+ ignoreCache.set(file, { mtimeMs, patterns });
1519
+ return patterns;
1520
+ }
1521
+ function createIgnorePredicate(cwd) {
1522
+ const patterns = loadIgnorePatterns(cwd);
1523
+ if (patterns.length === 0) {
1524
+ return void 0;
1525
+ }
1526
+ const root = path9.resolve(cwd);
1527
+ return (absPath, isDir) => {
1528
+ const rel = path9.relative(root, absPath).split(path9.sep).join("/");
1529
+ if (!rel || rel.startsWith("..")) {
1530
+ return false;
1531
+ }
1532
+ for (const { pattern, dirOnly } of patterns) {
1533
+ if (dirOnly && !isDir) {
1534
+ continue;
1535
+ }
1536
+ if (matchesGlob(pattern, rel)) {
1537
+ return true;
1538
+ }
1539
+ }
1540
+ return false;
1541
+ };
1542
+ }
1266
1543
  function isSensitivePath2(filePath) {
1267
- const base = path8.basename(filePath);
1544
+ const base = path9.basename(filePath);
1268
1545
  if (base === ".env.example" || base === ".env.sample" || base === ".env.template") {
1269
1546
  return false;
1270
1547
  }
@@ -1279,11 +1556,11 @@ function isSensitivePath2(filePath) {
1279
1556
 
1280
1557
  // src/tools/fs/glob.ts
1281
1558
  var MAX_RESULTS = 100;
1282
- var schema3 = z4.object({
1283
- pattern: z4.string().describe(
1559
+ var schema3 = z5.object({
1560
+ pattern: z5.string().describe(
1284
1561
  "Glob pattern, e.g. 'src/**/*.ts'. Supports **, * and ?. A bare pattern like '*.ts' matches basenames recursively."
1285
1562
  ),
1286
- path: z4.string().optional().describe("Directory to search, defaults to the working directory")
1563
+ path: z5.string().optional().describe("Directory to search, defaults to the working directory")
1287
1564
  });
1288
1565
  var globTool = {
1289
1566
  name: "glob",
@@ -1291,8 +1568,8 @@ var globTool = {
1291
1568
  permission: "read",
1292
1569
  parameters: schema3,
1293
1570
  async execute(args, ctx) {
1294
- const root = path9.resolve(ctx.cwd, args.path ?? ".");
1295
- const files = await walkFiles(root);
1571
+ const root = path10.resolve(ctx.cwd, args.path ?? ".");
1572
+ const files = await walkFiles(root, void 0, { ignore: createIgnorePredicate(ctx.cwd) });
1296
1573
  const matched = files.filter((f) => matchesGlob(args.pattern, f.rel)).sort((a, b) => b.mtimeMs - a.mtimeMs).slice(0, MAX_RESULTS);
1297
1574
  if (matched.length === 0) {
1298
1575
  return { content: `No files matched pattern: ${args.pattern}` };
@@ -1302,15 +1579,15 @@ var globTool = {
1302
1579
  };
1303
1580
 
1304
1581
  // src/tools/fs/grep.ts
1305
- import { readFile as readFile3, stat as stat2 } from "fs/promises";
1306
- import path10 from "path";
1307
- import { z as z5 } from "zod";
1582
+ import { readFile as readFile4, stat as stat2 } from "fs/promises";
1583
+ import path11 from "path";
1584
+ import { z as z6 } from "zod";
1308
1585
  var MAX_MATCHES = 250;
1309
- var schema4 = z5.object({
1310
- pattern: z5.string().describe("Regular expression to search for"),
1311
- path: z5.string().optional().describe("File or directory to search, defaults to the working directory"),
1312
- glob: z5.string().optional().describe("Glob pattern to filter which files are searched, e.g. '*.ts'"),
1313
- ignoreCase: z5.boolean().optional().describe("Case-insensitive matching")
1586
+ var schema4 = z6.object({
1587
+ pattern: z6.string().describe("Regular expression to search for"),
1588
+ path: z6.string().optional().describe("File or directory to search, defaults to the working directory"),
1589
+ glob: z6.string().optional().describe("Glob pattern to filter which files are searched, e.g. '*.ts'"),
1590
+ ignoreCase: z6.boolean().optional().describe("Case-insensitive matching")
1314
1591
  });
1315
1592
  var grepTool = {
1316
1593
  name: "grep",
@@ -1324,14 +1601,14 @@ var grepTool = {
1324
1601
  } catch (err) {
1325
1602
  return { content: `Invalid regular expression: ${err.message}`, isError: true };
1326
1603
  }
1327
- const root = path10.resolve(ctx.cwd, args.path ?? ".");
1604
+ const root = path11.resolve(ctx.cwd, args.path ?? ".");
1328
1605
  let files;
1329
1606
  try {
1330
1607
  const st = await stat2(root);
1331
1608
  if (st.isFile()) {
1332
- files = [{ abs: root, rel: path10.basename(root), mtimeMs: st.mtimeMs }];
1609
+ files = [{ abs: root, rel: path11.basename(root), mtimeMs: st.mtimeMs }];
1333
1610
  } else {
1334
- files = await walkFiles(root);
1611
+ files = await walkFiles(root, void 0, { ignore: createIgnorePredicate(ctx.cwd) });
1335
1612
  }
1336
1613
  } catch {
1337
1614
  return { content: `Path not found: ${args.path ?? "."}`, isError: true };
@@ -1345,7 +1622,7 @@ var grepTool = {
1345
1622
  outer: for (const file of files) {
1346
1623
  let buf;
1347
1624
  try {
1348
- buf = await readFile3(file.abs);
1625
+ buf = await readFile4(file.abs);
1349
1626
  } catch {
1350
1627
  continue;
1351
1628
  }
@@ -1375,15 +1652,15 @@ var grepTool = {
1375
1652
  };
1376
1653
 
1377
1654
  // src/tools/fs/read.ts
1378
- import { readFile as readFile4, stat as stat3 } from "fs/promises";
1379
- import path11 from "path";
1380
- import { z as z6 } from "zod";
1655
+ import { readFile as readFile5, stat as stat3 } from "fs/promises";
1656
+ import path12 from "path";
1657
+ import { z as z7 } from "zod";
1381
1658
  var DEFAULT_LIMIT = 2e3;
1382
1659
  var MAX_CHARS = 100 * 1024;
1383
- var schema5 = z6.object({
1384
- path: z6.string().describe("File path, absolute or relative to the working directory"),
1385
- offset: z6.number().int().positive().optional().describe("1-based line number to start reading from"),
1386
- limit: z6.number().int().positive().optional().describe("Maximum number of lines to read (default 2000)")
1660
+ var schema5 = z7.object({
1661
+ path: z7.string().describe("File path, absolute or relative to the working directory"),
1662
+ offset: z7.number().int().positive().optional().describe("1-based line number to start reading from"),
1663
+ limit: z7.number().int().positive().optional().describe("Maximum number of lines to read (default 2000)")
1387
1664
  });
1388
1665
  var readFileTool = {
1389
1666
  name: "read_file",
@@ -1391,7 +1668,7 @@ var readFileTool = {
1391
1668
  permission: "read",
1392
1669
  parameters: schema5,
1393
1670
  async execute(args, ctx) {
1394
- const filePath = path11.resolve(ctx.cwd, args.path);
1671
+ const filePath = path12.resolve(ctx.cwd, args.path);
1395
1672
  if (isSensitivePath2(filePath)) {
1396
1673
  return { content: `Refused to read sensitive file: ${args.path}`, isError: true };
1397
1674
  }
@@ -1402,7 +1679,7 @@ var readFileTool = {
1402
1679
  if (st.isDirectory()) {
1403
1680
  return { content: `Path is a directory, not a file: ${args.path}`, isError: true };
1404
1681
  }
1405
- const raw = await readFile4(filePath, "utf8");
1682
+ const raw = await readFile5(filePath, "utf8");
1406
1683
  const lines = raw.split("\n");
1407
1684
  const start = args.offset ?? 1;
1408
1685
  const limit = args.limit ?? DEFAULT_LIMIT;
@@ -1436,12 +1713,12 @@ var readFileTool = {
1436
1713
  };
1437
1714
 
1438
1715
  // src/tools/fs/write.ts
1439
- import { mkdir as mkdir2, writeFile as writeFile3 } from "fs/promises";
1440
- import path12 from "path";
1441
- import { z as z7 } from "zod";
1442
- var schema6 = z7.object({
1443
- path: z7.string().describe("File path, absolute or relative to the working directory"),
1444
- content: z7.string().describe("Full content to write to the file")
1716
+ import { mkdir as mkdir3, writeFile as writeFile4 } from "fs/promises";
1717
+ import path13 from "path";
1718
+ import { z as z8 } from "zod";
1719
+ var schema6 = z8.object({
1720
+ path: z8.string().describe("File path, absolute or relative to the working directory"),
1721
+ content: z8.string().describe("Full content to write to the file")
1445
1722
  });
1446
1723
  var writeFileTool = {
1447
1724
  name: "write_file",
@@ -1449,11 +1726,11 @@ var writeFileTool = {
1449
1726
  permission: "write",
1450
1727
  parameters: schema6,
1451
1728
  async execute(args, ctx) {
1452
- const filePath = path12.resolve(ctx.cwd, args.path);
1729
+ const filePath = path13.resolve(ctx.cwd, args.path);
1453
1730
  const snapshot = await captureSnapshot(filePath, "write_file");
1454
1731
  try {
1455
- await mkdir2(path12.dirname(filePath), { recursive: true });
1456
- await writeFile3(filePath, args.content, "utf8");
1732
+ await mkdir3(path13.dirname(filePath), { recursive: true });
1733
+ await writeFile4(filePath, args.content, "utf8");
1457
1734
  } catch (err) {
1458
1735
  return { content: `Failed to write ${args.path}: ${err.message}`, isError: true };
1459
1736
  }
@@ -1463,7 +1740,7 @@ var writeFileTool = {
1463
1740
  };
1464
1741
 
1465
1742
  // src/tools/tasks.ts
1466
- import { z as z8 } from "zod";
1743
+ import { z as z9 } from "zod";
1467
1744
 
1468
1745
  // src/tasks/format.ts
1469
1746
  function formatDuration(startedAt, endedAt) {
@@ -1509,40 +1786,59 @@ function formatTaskFinished(task) {
1509
1786
  }
1510
1787
 
1511
1788
  // src/tools/tasks.ts
1512
- var idSchema = z8.object({
1513
- id: z8.string().describe("Task id, e.g. task-1")
1789
+ var idSchema = z9.object({
1790
+ id: z9.string().describe("Task id, e.g. task-1 or agent-1")
1514
1791
  });
1792
+ function formatAgentTaskLine(task) {
1793
+ const duration = formatDuration(task.startedAt, task.endedAt);
1794
+ const label = task.description ?? task.prompt.slice(0, 60);
1795
+ const status = task.status === "running" ? `running ${duration}` : `${task.status} after ${duration}`;
1796
+ return `${task.id} [${status}] ${label}`;
1797
+ }
1515
1798
  var taskListTool = {
1516
1799
  name: "task_list",
1517
- description: "List background shell tasks with status, runtime, and exit code.",
1800
+ description: "List background tasks (shell commands and subagents) with status and runtime.",
1518
1801
  permission: "read",
1519
- parameters: z8.object({}),
1802
+ parameters: z9.object({}),
1520
1803
  execute() {
1521
- return Promise.resolve({ content: formatTaskList(defaultTaskManager.list()) });
1804
+ const shell = formatTaskList(defaultTaskManager.list());
1805
+ const agents = defaultAgentTasks.list();
1806
+ const agentLines = agents.length === 0 ? "No background subagents." : `Background subagents:
1807
+ ${agents.map(formatAgentTaskLine).join("\n")}`;
1808
+ return Promise.resolve({ content: `${shell}
1809
+ ${agentLines}` });
1522
1810
  }
1523
1811
  };
1524
1812
  var taskOutputTool = {
1525
1813
  name: "task_output",
1526
- description: "Read the current output of a background shell task (works while still running).",
1814
+ description: "Read the current output of a background task (shell command or subagent; works while still running).",
1527
1815
  permission: "read",
1528
1816
  parameters: idSchema,
1529
1817
  execute(args) {
1530
- const task = defaultTaskManager.get(args.id);
1531
- if (!task) {
1532
- return Promise.resolve({ content: `Unknown task: ${args.id}`, isError: true });
1818
+ const shell = defaultTaskManager.get(args.id);
1819
+ if (shell) {
1820
+ const body = shell.output.replace(/\s+$/, "") || "(no output yet)";
1821
+ return Promise.resolve({ content: `${formatTaskLine(shell)}
1822
+ ${body}` });
1533
1823
  }
1534
- const body = task.output.replace(/\s+$/, "") || "(no output yet)";
1535
- return Promise.resolve({ content: `${formatTaskLine(task)}
1824
+ const agent = defaultAgentTasks.get(args.id);
1825
+ if (agent) {
1826
+ const body = agent.status === "running" ? "(still running)" : agent.result.replace(/\s+$/, "") || "(no report)";
1827
+ return Promise.resolve({ content: `${formatAgentTaskLine(agent)}
1536
1828
  ${body}` });
1829
+ }
1830
+ return Promise.resolve({ content: `Unknown task: ${args.id}`, isError: true });
1537
1831
  }
1538
1832
  };
1539
1833
  var taskKillTool = {
1540
1834
  name: "task_kill",
1541
- description: "Stop a running background shell task (kills the whole process tree).",
1835
+ description: "Stop a running background task (shell command process tree or subagent).",
1542
1836
  permission: "exec",
1543
1837
  parameters: idSchema,
1544
1838
  execute(args) {
1545
- const task = defaultTaskManager.get(args.id);
1839
+ const shell = defaultTaskManager.get(args.id);
1840
+ const agent = shell ? void 0 : defaultAgentTasks.get(args.id);
1841
+ const task = shell ?? agent;
1546
1842
  if (!task) {
1547
1843
  return Promise.resolve({ content: `Unknown task: ${args.id}`, isError: true });
1548
1844
  }
@@ -1552,106 +1848,15 @@ var taskKillTool = {
1552
1848
  isError: true
1553
1849
  });
1554
1850
  }
1555
- defaultTaskManager.kill(args.id);
1851
+ if (shell) {
1852
+ defaultTaskManager.kill(args.id);
1853
+ } else {
1854
+ defaultAgentTasks.kill(args.id);
1855
+ }
1556
1856
  return Promise.resolve({ content: `Task ${args.id} stopped.` });
1557
1857
  }
1558
1858
  };
1559
1859
 
1560
- // src/tools/todo.ts
1561
- import { mkdir as mkdir3, readFile as readFile5, writeFile as writeFile4 } from "fs/promises";
1562
- import path13 from "path";
1563
- import { z as z9 } from "zod";
1564
- var todoItemSchema = z9.object({
1565
- id: z9.number().int(),
1566
- title: z9.string(),
1567
- status: z9.enum(["pending", "in_progress", "done"])
1568
- });
1569
- var writeSchema = z9.object({
1570
- todos: z9.array(todoItemSchema).describe("The full todo list, replacing the current one")
1571
- });
1572
- var readSchema = z9.object({});
1573
- function fileFor(cwd) {
1574
- return path13.join(cwd, ".star", "todos.json");
1575
- }
1576
- var TodoStore = class {
1577
- items = /* @__PURE__ */ new Map();
1578
- list() {
1579
- return [...this.items.values()];
1580
- }
1581
- replace(items) {
1582
- this.items.clear();
1583
- for (const item of items) {
1584
- this.items.set(item.id, item);
1585
- }
1586
- }
1587
- async load(cwd) {
1588
- let raw;
1589
- try {
1590
- raw = await readFile5(fileFor(cwd), "utf8");
1591
- } catch {
1592
- return;
1593
- }
1594
- try {
1595
- const parsed = JSON.parse(raw);
1596
- if (Array.isArray(parsed)) {
1597
- this.replace(parsed.filter((it) => todoItemSchema.safeParse(it).success));
1598
- }
1599
- } catch {
1600
- }
1601
- }
1602
- async save(cwd) {
1603
- await mkdir3(path13.join(cwd, ".star"), { recursive: true });
1604
- await writeFile4(fileFor(cwd), `${JSON.stringify(this.list(), null, 2)}
1605
- `);
1606
- }
1607
- };
1608
- var SYMBOLS = {
1609
- pending: "[ ]",
1610
- in_progress: "[~]",
1611
- done: "[x]"
1612
- };
1613
- function formatTodos(items) {
1614
- if (items.length === 0) {
1615
- return "No todos.";
1616
- }
1617
- const lines = items.map((it) => `${SYMBOLS[it.status]} ${it.id}. ${it.title}`);
1618
- const width = Math.max(...lines.map((l) => l.length));
1619
- return lines.map((line, i) => {
1620
- const status = items[i]?.status;
1621
- return status === "pending" ? line : `${line.padEnd(width + 4)}(${status})`;
1622
- }).join("\n");
1623
- }
1624
- var defaultStore = new TodoStore();
1625
- function createTodoTools(store = defaultStore) {
1626
- const todoWrite = {
1627
- name: "todo_write",
1628
- description: "Replace the current todo list with the given items. Each item has an id, title, and status (pending, in_progress, done).",
1629
- permission: "read",
1630
- parameters: writeSchema,
1631
- async execute(args, ctx) {
1632
- const parsed = writeSchema.safeParse(args);
1633
- if (!parsed.success) {
1634
- return { content: `Invalid todos: ${parsed.error.message}`, isError: true };
1635
- }
1636
- await store.load(ctx.cwd);
1637
- store.replace(parsed.data.todos);
1638
- await store.save(ctx.cwd);
1639
- return { content: formatTodos(store.list()) };
1640
- }
1641
- };
1642
- const todoRead = {
1643
- name: "todo_read",
1644
- description: "Read the current todo list.",
1645
- permission: "read",
1646
- parameters: readSchema,
1647
- async execute(_args, ctx) {
1648
- await store.load(ctx.cwd);
1649
- return { content: formatTodos(store.list()) };
1650
- }
1651
- };
1652
- return [todoWrite, todoRead];
1653
- }
1654
-
1655
1860
  // src/tools/web/fetch.ts
1656
1861
  import { z as z10 } from "zod";
1657
1862
  var DEFAULT_MAX_CHARS = 2e4;
@@ -1949,75 +2154,216 @@ function createDefaultRegistry() {
1949
2154
  var MAX_SUBAGENT_DEPTH = 1;
1950
2155
  var MAX_REPORT_CHARS = 2e4;
1951
2156
  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
+ async function runSubagent(deps, args, cwd, signal) {
2158
+ const { AgentLoop: AgentLoop2 } = await import("./loop-EIODKYPT.js");
2159
+ const child = new AgentLoop2({
2160
+ model: deps.model,
2161
+ registry: createDefaultRegistry(),
2162
+ config: deps.config,
2163
+ cwd,
2164
+ system: deps.system ? `${deps.system}
2165
+
2166
+ ${SUBAGENT_PROMPT}` : SUBAGENT_PROMPT,
2167
+ subagentDepth: deps.depth + 1
2168
+ });
2169
+ const confirmHandler = deps.getConfirmHandler?.();
2170
+ if (confirmHandler) child.confirmHandler = confirmHandler;
2171
+ let toolCalls = 0;
2172
+ let error = null;
2173
+ try {
2174
+ for await (const event of child.stream(args.prompt, signal)) {
2175
+ if (event.type === "tool-result") {
2176
+ toolCalls++;
2177
+ } else if (event.type === "error") {
2178
+ error = event.error.message;
2179
+ break;
2180
+ }
2181
+ }
2182
+ } catch (e) {
2183
+ error = e instanceof Error ? e.message : String(e);
2184
+ }
2185
+ let report = "";
2186
+ const messages = child.getMessages();
2187
+ for (let i = messages.length - 1; i >= 0; i--) {
2188
+ const message = messages[i];
2189
+ if (message?.role !== "assistant" || !Array.isArray(message.content)) continue;
2190
+ const text = message.content.filter((part) => part.type === "text").map((part) => part.text).join("");
2191
+ if (text) {
2192
+ report = text;
2193
+ break;
2194
+ }
2195
+ }
2196
+ if (report.length > MAX_REPORT_CHARS) {
2197
+ report = `${report.slice(0, MAX_REPORT_CHARS)}
2198
+ ... (truncated)`;
2199
+ }
2200
+ const label = args.description ? ` "${args.description}"` : "";
2201
+ const stats = `[subagent${label}: ${toolCalls} tool call(s)]`;
2202
+ if (error) {
2203
+ throw new Error([`Subagent failed: ${error}`, stats, report].filter(Boolean).join("\n"));
2204
+ }
2205
+ return `${stats}
2206
+ ${report || "(subagent produced no text output)"}`;
2207
+ }
1952
2208
  function createSubagentTool(deps) {
1953
2209
  return {
1954
2210
  name: "subagent",
1955
- 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.",
2211
+ 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).",
1956
2212
  permission: "exec",
1957
2213
  parameters: z12.object({
1958
2214
  prompt: z12.string().describe("Complete, self-contained instructions for the subagent."),
1959
- description: z12.string().optional().describe("Short label for the subtask, shown in the result header.")
2215
+ description: z12.string().optional().describe("Short label for the subtask, shown in the result header."),
2216
+ run_in_background: z12.boolean().optional().describe(
2217
+ "Run concurrently in the background and return a task id immediately (default false). Use for independent subtasks that can run in parallel."
2218
+ )
1960
2219
  }),
1961
2220
  async execute(args, ctx) {
1962
- const { AgentLoop: AgentLoop2 } = await import("./loop-R3RQCNFT.js");
1963
- const child = new AgentLoop2({
1964
- model: deps.model,
1965
- registry: createDefaultRegistry(),
1966
- config: deps.config,
1967
- cwd: ctx.cwd,
1968
- system: deps.system ? `${deps.system}
1969
-
1970
- ${SUBAGENT_PROMPT}` : SUBAGENT_PROMPT,
1971
- subagentDepth: deps.depth + 1
1972
- });
1973
- const confirmHandler = deps.getConfirmHandler?.();
1974
- if (confirmHandler) child.confirmHandler = confirmHandler;
2221
+ if (args.run_in_background) {
2222
+ const task = defaultAgentTasks.start((signal2) => runSubagent(deps, args, ctx.cwd, signal2), {
2223
+ prompt: args.prompt,
2224
+ description: args.description
2225
+ });
2226
+ const label = args.description ? ` "${args.description}"` : "";
2227
+ return {
2228
+ 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.`
2229
+ };
2230
+ }
1975
2231
  const signal = ctx.abortSignal ?? new AbortController().signal;
1976
- let toolCalls = 0;
1977
- let error = null;
1978
2232
  try {
1979
- for await (const event of child.stream(args.prompt, signal)) {
1980
- if (event.type === "tool-result") {
1981
- toolCalls++;
1982
- } else if (event.type === "error") {
1983
- error = event.error.message;
1984
- break;
1985
- }
1986
- }
2233
+ return { content: await runSubagent(deps, args, ctx.cwd, signal) };
1987
2234
  } catch (e) {
1988
- error = e instanceof Error ? e.message : String(e);
2235
+ return { content: e instanceof Error ? e.message : String(e), isError: true };
1989
2236
  }
1990
- let report = "";
1991
- const messages = child.getMessages();
1992
- for (let i = messages.length - 1; i >= 0; i--) {
1993
- const message = messages[i];
1994
- if (message?.role !== "assistant" || !Array.isArray(message.content)) continue;
1995
- const text = message.content.filter((part) => part.type === "text").map((part) => part.text).join("");
1996
- if (text) {
1997
- report = text;
1998
- break;
1999
- }
2000
- }
2001
- if (report.length > MAX_REPORT_CHARS) {
2002
- report = `${report.slice(0, MAX_REPORT_CHARS)}
2003
- ... (truncated)`;
2004
- }
2005
- const label = args.description ? ` "${args.description}"` : "";
2006
- const stats = `[subagent${label}: ${toolCalls} tool call(s)]`;
2007
- if (error) {
2008
- return {
2009
- content: [`Subagent failed: ${error}`, stats, report].filter(Boolean).join("\n"),
2010
- isError: true
2011
- };
2237
+ }
2238
+ };
2239
+ }
2240
+
2241
+ // src/agent/user-memory.ts
2242
+ import { appendFileSync, mkdirSync, readFileSync as readFileSync4, statSync as statSync4 } from "fs";
2243
+ import path14 from "path";
2244
+ import { z as z13 } from "zod";
2245
+ var USER_MEMORY_MAX_CHARS = 32 * 1024;
2246
+ var REMEMBER_TEXT_MAX_CHARS = 500;
2247
+ var cache3 = /* @__PURE__ */ new Map();
2248
+ function readUserMemory(home = starHome()) {
2249
+ const file = path14.join(home, "MEMORY.md");
2250
+ let mtimeMs;
2251
+ try {
2252
+ mtimeMs = statSync4(file).mtimeMs;
2253
+ } catch {
2254
+ return null;
2255
+ }
2256
+ const hit = cache3.get(file);
2257
+ if (hit && hit.mtimeMs === mtimeMs) return hit.block;
2258
+ let block = null;
2259
+ try {
2260
+ let body = readFileSync4(file, "utf8").trim();
2261
+ if (body) {
2262
+ if (body.length > USER_MEMORY_MAX_CHARS) {
2263
+ body = `${body.slice(0, USER_MEMORY_MAX_CHARS)}
2264
+
2265
+ [MEMORY.md truncated to ${USER_MEMORY_MAX_CHARS} characters]`;
2012
2266
  }
2013
- return { content: `${stats}
2014
- ${report || "(subagent produced no text output)"}` };
2267
+ block = [
2268
+ "# User memory (MEMORY.md)",
2269
+ "The following notes were saved by the user across sessions. Respect them unless they conflict with the current request.",
2270
+ body
2271
+ ].join("\n");
2272
+ }
2273
+ } catch {
2274
+ block = null;
2275
+ }
2276
+ cache3.set(file, { mtimeMs, block });
2277
+ return block;
2278
+ }
2279
+ function appendUserMemory(text, home = starHome()) {
2280
+ mkdirSync(home, { recursive: true });
2281
+ appendFileSync(path14.join(home, "MEMORY.md"), `- ${text}
2282
+ `, "utf8");
2283
+ }
2284
+ function createRememberTool(deps = {}) {
2285
+ return {
2286
+ name: "remember",
2287
+ description: `Save a note to the user's long-term memory (${userMemoryPath()}), injected into every future session. ONLY use when the user explicitly asks to remember or save something. Never use autonomously.`,
2288
+ permission: "write",
2289
+ parameters: z13.object({
2290
+ text: z13.string().max(REMEMBER_TEXT_MAX_CHARS).describe("The note to save, as a single concise sentence.")
2291
+ }),
2292
+ async execute(args) {
2293
+ appendUserMemory(args.text, deps.home);
2294
+ return { content: `Saved to user memory: ${args.text}` };
2015
2295
  }
2016
2296
  };
2017
2297
  }
2018
2298
 
2019
2299
  // src/agent/loop.ts
2020
2300
  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
+ 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
+ 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.";
2303
+ var COMPLETION_CHECK_TIMEOUT_MS = 3e4;
2304
+ async function checkTaskComplete(model, request, finalReply, signal) {
2305
+ const controller = new AbortController();
2306
+ const timer = setTimeout(() => controller.abort(), COMPLETION_CHECK_TIMEOUT_MS);
2307
+ const onAbort = () => controller.abort();
2308
+ signal.addEventListener("abort", onAbort, { once: true });
2309
+ try {
2310
+ const { text } = await generateText3({
2311
+ model,
2312
+ maxTokens: 8,
2313
+ abortSignal: controller.signal,
2314
+ prompt: [
2315
+ "An AI coding agent was given this task by the user:",
2316
+ `<task>
2317
+ ${request.slice(0, 2e3)}
2318
+ </task>`,
2319
+ "The agent has stopped calling tools and ended with this reply:",
2320
+ `<reply>
2321
+ ${finalReply.slice(0, 2e3)}
2322
+ </reply>`,
2323
+ "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."
2324
+ ].join("\n")
2325
+ });
2326
+ const verdict = text.trim().toUpperCase();
2327
+ if (verdict.startsWith("NOT_DONE")) return false;
2328
+ if (verdict.startsWith("DONE")) return true;
2329
+ return null;
2330
+ } catch {
2331
+ return null;
2332
+ } finally {
2333
+ clearTimeout(timer);
2334
+ signal.removeEventListener("abort", onAbort);
2335
+ }
2336
+ }
2337
+ 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
+ function sleep(ms, signal) {
2354
+ if (signal.aborted) return Promise.resolve(false);
2355
+ return new Promise((resolve) => {
2356
+ const onAbort = () => {
2357
+ clearTimeout(timer);
2358
+ resolve(false);
2359
+ };
2360
+ const timer = setTimeout(() => {
2361
+ signal.removeEventListener("abort", onAbort);
2362
+ resolve(true);
2363
+ }, ms);
2364
+ signal.addEventListener("abort", onAbort, { once: true });
2365
+ });
2366
+ }
2021
2367
  var AgentLoop = class {
2022
2368
  messages = [];
2023
2369
  opts;
@@ -2041,6 +2387,7 @@ var AgentLoop = class {
2041
2387
  this.bindSnapshotHooks(opts.sessionStore);
2042
2388
  }
2043
2389
  opts.registry?.register(createSkillTool({ cwd: opts.cwd }));
2390
+ opts.registry?.register(createRememberTool());
2044
2391
  const depth = opts.subagentDepth ?? 0;
2045
2392
  if (opts.registry && depth < MAX_SUBAGENT_DEPTH) {
2046
2393
  opts.registry.register(
@@ -2152,7 +2499,7 @@ var AgentLoop = class {
2152
2499
  }
2153
2500
  async *stream(input, signal, opts) {
2154
2501
  this.syncSystemMessage();
2155
- const text = typeof input === "string" ? input : input.text;
2502
+ const inputText = typeof input === "string" ? input : input.text;
2156
2503
  const images = typeof input === "string" ? [] : input.images;
2157
2504
  const userMessage = images.length > 0 ? {
2158
2505
  role: "user",
@@ -2162,9 +2509,9 @@ var AgentLoop = class {
2162
2509
  image: img.data,
2163
2510
  mimeType: img.mimeType
2164
2511
  })),
2165
- { type: "text", text }
2512
+ { type: "text", text: inputText }
2166
2513
  ]
2167
- } : { role: "user", content: text };
2514
+ } : { role: "user", content: inputText };
2168
2515
  this.messages.push(userMessage);
2169
2516
  const seq = (this.opts.subagentDepth ?? 0) === 0 ? beginTurn(this.messages.length - 1) : currentTurnSeq();
2170
2517
  this.turnMarkers.push({ seq, userIndex: this.messages.length - 1 });
@@ -2173,42 +2520,89 @@ var AgentLoop = class {
2173
2520
  );
2174
2521
  const { config, registry, cwd } = this.opts;
2175
2522
  const aiTools = this.buildAiTools();
2523
+ let autoContinues = 0;
2524
+ let lastWasNudge = false;
2525
+ let usedToolsThisTurn = false;
2526
+ let openTodos = [];
2176
2527
  for (let step = 0; step < config.maxSteps; step++) {
2528
+ if ((this.opts.subagentDepth ?? 0) === 0) {
2529
+ for (const finished of defaultAgentTasks.drainNotifications()) {
2530
+ const label = finished.description ? ` "${finished.description}"` : "";
2531
+ const note = {
2532
+ role: "user",
2533
+ content: `[background subagent ${finished.id}${label} ${finished.status}]
2534
+ ${finished.result}`
2535
+ };
2536
+ this.messages.push(note);
2537
+ await this.persist(note);
2538
+ }
2539
+ }
2177
2540
  const maxTokens = this.opts.contextMaxTokens ?? config.contextMaxTokens;
2178
2541
  const compacted = compactMessages([...this.messages], maxTokens);
2179
2542
  if (compacted.compacted) {
2180
2543
  this.messages = await this.applyCompactionSummary(compacted);
2181
2544
  }
2182
- let text2 = "";
2545
+ const maxRetries = Math.max(0, config.streamMaxRetries);
2546
+ const baseDelay = this.opts.retryDelayMs ?? STREAM_RETRY_BASE_DELAY_MS;
2547
+ let text = "";
2183
2548
  const toolCalls = [];
2184
- let failed = false;
2185
- try {
2186
- for await (const event of this.streamOnce(aiTools, signal)) {
2187
- if (event.type === "text-delta") {
2188
- text2 += event.text;
2189
- yield event;
2190
- } else if (event.type === "reasoning") {
2191
- yield event;
2192
- } else if (event.type === "tool-call") {
2193
- toolCalls.push({ id: event.id, name: event.name, args: event.args });
2194
- yield event;
2195
- } else if (event.type === "error") {
2196
- failed = true;
2197
- yield event;
2198
- } else {
2199
- yield event;
2549
+ let failure = null;
2550
+ for (let attempt = 0; ; attempt++) {
2551
+ text = "";
2552
+ toolCalls.length = 0;
2553
+ failure = null;
2554
+ let finishReason;
2555
+ const timeoutScale = Math.min(attempt + 1, 3);
2556
+ try {
2557
+ for await (const event of this.streamOnce(aiTools, signal, timeoutScale)) {
2558
+ if (event.type === "text-delta") {
2559
+ text += event.text;
2560
+ yield event;
2561
+ } else if (event.type === "reasoning") {
2562
+ yield event;
2563
+ } else if (event.type === "tool-call") {
2564
+ toolCalls.push({ id: event.id, name: event.name, args: event.args });
2565
+ yield event;
2566
+ } else if (event.type === "finish") {
2567
+ finishReason = event.finishReason;
2568
+ yield event;
2569
+ } else if (event.type === "error") {
2570
+ failure = event.error;
2571
+ } else {
2572
+ yield event;
2573
+ }
2200
2574
  }
2575
+ } catch (error) {
2576
+ if (signal.aborted) return;
2577
+ failure = error instanceof Error ? error : new Error(String(error));
2201
2578
  }
2202
- } catch (error) {
2203
2579
  if (signal.aborted) return;
2204
- yield { type: "error", error: error instanceof Error ? error : new Error(String(error)) };
2580
+ if (!failure && (text.length > 0 || toolCalls.length > 0)) break;
2581
+ if (failure && !isRetryableStreamError(failure)) break;
2582
+ if (attempt >= maxRetries) break;
2583
+ yield {
2584
+ type: "retry",
2585
+ attempt: attempt + 2,
2586
+ maxAttempts: maxRetries + 1,
2587
+ reason: failure ? failure.message : `empty response (finish: ${finishReason ?? "none"})`
2588
+ };
2589
+ if (!await sleep(baseDelay * 2 ** attempt, signal)) return;
2590
+ }
2591
+ if (failure) {
2592
+ yield { type: "error", error: failure };
2593
+ return;
2594
+ }
2595
+ if (text.length === 0 && toolCalls.length === 0) {
2596
+ yield {
2597
+ type: "error",
2598
+ error: new Error("The model returned an empty response after retries; ending the turn.")
2599
+ };
2205
2600
  return;
2206
2601
  }
2207
- if (failed) return;
2208
2602
  const assistantMessage = {
2209
2603
  role: "assistant",
2210
2604
  content: [
2211
- ...text2 ? [{ type: "text", text: text2 }] : [],
2605
+ ...text ? [{ type: "text", text }] : [],
2212
2606
  ...toolCalls.map((call) => ({
2213
2607
  type: "tool-call",
2214
2608
  toolCallId: call.id,
@@ -2219,11 +2613,40 @@ var AgentLoop = class {
2219
2613
  };
2220
2614
  this.messages.push(assistantMessage);
2221
2615
  await this.persist(assistantMessage);
2222
- this.maybeScheduleTitle(text2);
2616
+ this.maybeScheduleTitle(text);
2223
2617
  if (toolCalls.length === 0) {
2618
+ let reason = null;
2619
+ let nudgeText = AUTO_CONTINUE_NUDGE;
2620
+ if (openTodos.length > 0) {
2621
+ reason = `${openTodos.length} todo item(s) still open`;
2622
+ nudgeText = `[auto-continue] You ended your turn with unfinished todo item(s): ${openTodos.map((t) => `"${t}"`).join(
2623
+ ", "
2624
+ )}. Complete them now with tool calls, or update the list with todo_write if they are no longer needed.`;
2625
+ } else if (lastWasNudge) {
2626
+ reason = "reply still had no tool calls";
2627
+ } else if (PENDING_WORK_PATTERN.test(text)) {
2628
+ reason = "reply announced unfinished work";
2629
+ } else if (usedToolsThisTurn && !signal.aborted) {
2630
+ const complete = await checkTaskComplete(this.opts.model, inputText, text, signal);
2631
+ if (complete === false) reason = "completion check reports the task unfinished";
2632
+ }
2633
+ if (reason !== null && config.permissionMode !== "plan" && autoContinues < config.maxAutoContinues) {
2634
+ autoContinues++;
2635
+ lastWasNudge = true;
2636
+ yield {
2637
+ type: "notice",
2638
+ message: `${reason}; asking the model to continue (${autoContinues}/${config.maxAutoContinues}).`
2639
+ };
2640
+ const nudge = { role: "user", content: nudgeText };
2641
+ this.messages.push(nudge);
2642
+ await this.persist(nudge);
2643
+ continue;
2644
+ }
2224
2645
  await this.runEventHooks("Stop");
2225
2646
  return;
2226
2647
  }
2648
+ lastWasNudge = false;
2649
+ usedToolsThisTurn = true;
2227
2650
  const answered = /* @__PURE__ */ new Set();
2228
2651
  try {
2229
2652
  for (const call of toolCalls) {
@@ -2243,6 +2666,9 @@ var AgentLoop = class {
2243
2666
  this.messages.push(toolMessage);
2244
2667
  await this.persist(toolMessage);
2245
2668
  answered.add(call.id);
2669
+ if (call.name === "todo_write" && !result.isError) {
2670
+ openTodos = pendingTodoTitles(call.args);
2671
+ }
2246
2672
  yield {
2247
2673
  type: "tool-result",
2248
2674
  id: call.id,
@@ -2285,8 +2711,9 @@ var AgentLoop = class {
2285
2711
  // /model switch) replaced the history with one that has none. Git context
2286
2712
  // (branch, dirty count, recent commits) is refreshed here too, so the model
2287
2713
  // always sees the current repo state; any git failure is silently skipped.
2288
- // Project memory (AGENTS.md in the cwd) is appended as a delimited block;
2289
- // readProjectMemory caches by mtime, so this stays cheap per turn. The
2714
+ // Project memory (AGENTS.md in the cwd) and user memory (~/.star-cli/
2715
+ // MEMORY.md) are appended as delimited blocks; both cache by mtime, so this
2716
+ // stays cheap per turn. The
2290
2717
  // skills listing is refreshed here as well, so skills added mid-session
2291
2718
  // show up in the next turn's system prompt.
2292
2719
  syncSystemMessage() {
@@ -2297,6 +2724,8 @@ var AgentLoop = class {
2297
2724
  if (plan) parts.push(PLAN_MODE_PROMPT.trim());
2298
2725
  const memory = readProjectMemory(this.opts.cwd);
2299
2726
  if (memory) parts.push(memory);
2727
+ const userMemory = readUserMemory();
2728
+ if (userMemory) parts.push(userMemory);
2300
2729
  const skills = discoverSkills(this.opts.cwd);
2301
2730
  if (skills.length > 0) parts.push(formatSkillsBlock(skills));
2302
2731
  const git = getGitSummary(this.opts.cwd);
@@ -2330,14 +2759,15 @@ ${summary}`
2330
2759
  return compacted.messages;
2331
2760
  }
2332
2761
  }
2333
- async *streamOnce(aiTools, signal) {
2762
+ async *streamOnce(aiTools, signal, timeoutScale = 1) {
2334
2763
  const { streamChat } = await import("./stream-Y55NRF2S.js");
2335
2764
  yield* streamChat({
2336
2765
  model: this.opts.model,
2337
2766
  messages: this.messages,
2338
2767
  tools: aiTools,
2339
2768
  abortSignal: signal,
2340
- idleTimeoutMs: this.opts.config.streamIdleTimeoutSec * 1e3
2769
+ idleTimeoutMs: this.opts.config.streamIdleTimeoutSec * 1e3 * timeoutScale,
2770
+ firstPartTimeoutMs: this.opts.config.streamFirstChunkTimeoutSec * 1e3 * timeoutScale
2341
2771
  });
2342
2772
  }
2343
2773
  buildAiTools() {
@@ -2378,7 +2808,8 @@ ${summary}`
2378
2808
  config.permissionMode,
2379
2809
  { toolName: call.name, args: call.args, level: tool.permission },
2380
2810
  { cwd },
2381
- config.permissions.allow
2811
+ config.permissions.allow,
2812
+ config.permissions.deny
2382
2813
  );
2383
2814
  if (decision === "deny") {
2384
2815
  return { content: `Permission denied for tool "${call.name}".`, isError: true };
@@ -2429,6 +2860,7 @@ ${summary}`
2429
2860
  };
2430
2861
 
2431
2862
  export {
2863
+ defaultAgentTasks,
2432
2864
  estimateTokens,
2433
2865
  summarizeMessages,
2434
2866
  compactMessages,
@@ -2447,23 +2879,28 @@ export {
2447
2879
  hydrateSnapshots,
2448
2880
  undoTurnSnapshots,
2449
2881
  rewindToSnapshot,
2882
+ TodoStore,
2883
+ formatTodos,
2884
+ parseTodoArgs,
2885
+ loadTodos,
2450
2886
  starHome,
2451
2887
  globalConfigPath,
2452
2888
  sessionsDir,
2889
+ userMemoryPath,
2453
2890
  projectConfigPath,
2454
2891
  discoverSkills,
2455
2892
  defaultTaskManager,
2456
2893
  resolveShell,
2457
2894
  bashTool,
2458
2895
  SKIP_DIRS,
2896
+ createIgnorePredicate,
2459
2897
  isSensitivePath2 as isSensitivePath,
2460
2898
  formatTaskList,
2461
2899
  formatTaskStarted,
2462
2900
  formatTaskFinished,
2463
- TodoStore,
2464
- formatTodos,
2465
2901
  createDefaultRegistry,
2902
+ appendUserMemory,
2466
2903
  PLAN_MODE_PROMPT,
2467
2904
  AgentLoop
2468
2905
  };
2469
- //# sourceMappingURL=chunk-MJ2O7NWI.js.map
2906
+ //# sourceMappingURL=chunk-DZWTIMJ2.js.map