@cryer/star-cli 0.2.0 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -10,23 +10,31 @@ import { generateText } from "ai";
10
10
  var CHARS_PER_TOKEN = 4;
11
11
  var MESSAGE_OVERHEAD = 4;
12
12
  var IMAGE_PART_TOKENS = 1024;
13
+ var CJK_RE = /[\u3000-\u30FF\u3400-\u4DBF\u4E00-\u9FFF\uF900-\uFAFF\uFF00-\uFFEF]/;
14
+ function textWeight(text) {
15
+ let cjk = 0;
16
+ for (const ch of text) {
17
+ if (CJK_RE.test(ch)) cjk++;
18
+ }
19
+ return text.length + cjk * (CHARS_PER_TOKEN - 1);
20
+ }
13
21
  function contentCharLength(message) {
14
22
  if (message.role === "tool") {
15
23
  let chars2 = 0;
16
24
  for (const part of message.content) {
17
- chars2 += JSON.stringify(part.result ?? null).length;
25
+ chars2 += textWeight(JSON.stringify(part.result ?? null));
18
26
  }
19
27
  return chars2;
20
28
  }
21
29
  if (typeof message.content === "string") {
22
- return message.content.length;
30
+ return textWeight(message.content);
23
31
  }
24
32
  let chars = 0;
25
33
  for (const part of message.content) {
26
34
  if (part.type === "text" || part.type === "reasoning") {
27
- chars += part.text.length;
35
+ chars += textWeight(part.text);
28
36
  } else if (part.type === "tool-call") {
29
- chars += part.toolName.length + JSON.stringify(part.args ?? null).length;
37
+ chars += textWeight(part.toolName) + textWeight(JSON.stringify(part.args ?? null));
30
38
  }
31
39
  }
32
40
  return chars;
@@ -425,6 +433,12 @@ function matchAllowRule(rule, req) {
425
433
  return target !== void 0 && globMatch(rule.pattern, target);
426
434
  }
427
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) {
428
442
  return rules.some((raw) => {
429
443
  const rule = parseAllowRule(raw);
430
444
  return rule !== null && matchAllowRule(rule, req);
@@ -476,7 +490,7 @@ function isSensitivePath(p) {
476
490
  }
477
491
  return base === ".env" || base.startsWith(".env.") || base === "id_rsa" || base.endsWith(".pem");
478
492
  }
479
- function checkPermission(mode, req, ctx, allowRules = []) {
493
+ function checkPermission(mode, req, ctx, allowRules = [], denyRules = []) {
480
494
  if (mode === "yolo") {
481
495
  return "allow";
482
496
  }
@@ -496,6 +510,9 @@ function checkPermission(mode, req, ctx, allowRules = []) {
496
510
  return "deny";
497
511
  }
498
512
  }
513
+ if (isDeniedByRules(denyRules, req)) {
514
+ return "deny";
515
+ }
499
516
  if (mode === "auto") {
500
517
  return "allow";
501
518
  }
@@ -720,14 +737,153 @@ ${body}`;
720
737
  return block;
721
738
  }
722
739
 
740
+ // src/agent/skills.ts
741
+ import { readFileSync as readFileSync2, readdirSync, statSync as statSync2 } from "fs";
742
+ import path5 from "path";
743
+ import { z } from "zod";
744
+
745
+ // src/config/paths.ts
746
+ import os from "os";
747
+ import path4 from "path";
748
+ function starHome() {
749
+ return process.env.STAR_HOME ?? path4.join(os.homedir(), ".star-cli");
750
+ }
751
+ function globalConfigPath() {
752
+ return path4.join(starHome(), "config.toml");
753
+ }
754
+ function sessionsDir() {
755
+ return path4.join(starHome(), "sessions");
756
+ }
757
+ function userMemoryPath() {
758
+ return path4.join(starHome(), "MEMORY.md");
759
+ }
760
+ function projectConfigPath(cwd) {
761
+ return path4.join(cwd, ".star", "config.toml");
762
+ }
763
+
764
+ // src/agent/skills.ts
765
+ var SKILL_FILE = "SKILL.md";
766
+ var SKILL_MAX_CHARS = 32 * 1024;
767
+ var NAME_PATTERN = /^[a-z0-9-]+$/;
768
+ function parseFrontmatter(raw) {
769
+ const match = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/);
770
+ if (!match) return null;
771
+ const meta = {};
772
+ for (const line of (match[1] ?? "").split(/\r?\n/)) {
773
+ const kv = line.match(/^([A-Za-z][A-Za-z0-9_-]*):\s*(.*)$/);
774
+ if (kv?.[1]) meta[kv[1]] = (kv[2] ?? "").trim();
775
+ }
776
+ return { meta, body: raw.slice(match[0].length).trim() };
777
+ }
778
+ var cache2 = /* @__PURE__ */ new Map();
779
+ function parseSkillFile(filePath, dirName, scope) {
780
+ let mtimeMs;
781
+ try {
782
+ mtimeMs = statSync2(filePath).mtimeMs;
783
+ } catch {
784
+ return null;
785
+ }
786
+ const hit = cache2.get(filePath);
787
+ if (hit && hit.mtimeMs === mtimeMs) return hit.skill;
788
+ let skill = null;
789
+ try {
790
+ const parsed = parseFrontmatter(readFileSync2(filePath, "utf8"));
791
+ const name = parsed?.meta.name || dirName;
792
+ const description = parsed?.meta.description;
793
+ if (parsed && NAME_PATTERN.test(name) && description) {
794
+ skill = { name, description, filePath, dir: path5.dirname(filePath), scope };
795
+ }
796
+ } catch {
797
+ skill = null;
798
+ }
799
+ cache2.set(filePath, { mtimeMs, skill });
800
+ return skill;
801
+ }
802
+ function scanDir(base, scope, into) {
803
+ let entries;
804
+ try {
805
+ entries = readdirSync(base);
806
+ } catch {
807
+ return;
808
+ }
809
+ for (const entry of entries.sort()) {
810
+ if (!NAME_PATTERN.test(entry)) continue;
811
+ const parsed = parseSkillFile(path5.join(base, entry, SKILL_FILE), entry, scope);
812
+ if (parsed) into.set(parsed.name, parsed);
813
+ }
814
+ }
815
+ function discoverSkills(cwd, home = starHome()) {
816
+ const byName = /* @__PURE__ */ new Map();
817
+ scanDir(path5.join(home, "skills"), "user", byName);
818
+ scanDir(path5.join(cwd, ".star", "skills"), "project", byName);
819
+ return [...byName.values()];
820
+ }
821
+ function formatSkillsBlock(skills) {
822
+ const lines = skills.map((s) => `- ${s.name}: ${s.description}`);
823
+ return [
824
+ "# Available skills",
825
+ "When the user's request matches one of these skills, call the skill tool with its name to load the full instructions before proceeding:",
826
+ ...lines
827
+ ].join("\n");
828
+ }
829
+ function loadSkillBody(skill) {
830
+ let parsed = null;
831
+ try {
832
+ parsed = parseFrontmatter(readFileSync2(skill.filePath, "utf8"));
833
+ } catch {
834
+ return null;
835
+ }
836
+ if (!parsed?.body) return null;
837
+ if (parsed.body.length > SKILL_MAX_CHARS) {
838
+ return `${parsed.body.slice(0, SKILL_MAX_CHARS)}
839
+
840
+ [skill truncated to ${SKILL_MAX_CHARS} characters]`;
841
+ }
842
+ return parsed.body;
843
+ }
844
+ function createSkillTool(deps) {
845
+ return {
846
+ name: "skill",
847
+ 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
+ permission: "read",
849
+ parameters: z.object({
850
+ name: z.string().describe("Name of the skill to load, as listed in the system prompt.")
851
+ }),
852
+ async execute(args) {
853
+ const skills = discoverSkills(deps.cwd, deps.home);
854
+ const skill = skills.find((s) => s.name === args.name);
855
+ if (!skill) {
856
+ const available = skills.map((s) => s.name).join(", ") || "none";
857
+ return {
858
+ content: `Unknown skill: "${args.name}". Available skills: ${available}`,
859
+ isError: true
860
+ };
861
+ }
862
+ const body = loadSkillBody(skill);
863
+ if (!body) {
864
+ return { content: `Skill "${skill.name}" is empty or unreadable.`, isError: true };
865
+ }
866
+ return {
867
+ content: [
868
+ `# Skill: ${skill.name}`,
869
+ "",
870
+ body,
871
+ "",
872
+ `Base directory for relative file references in this skill: ${skill.dir}`
873
+ ].join("\n")
874
+ };
875
+ }
876
+ };
877
+ }
878
+
723
879
  // src/agent/subagent.ts
724
- import { z as z11 } from "zod";
880
+ import { z as z12 } from "zod";
725
881
 
726
882
  // src/tools/bash.ts
727
883
  import { spawn as spawn3 } from "child_process";
728
884
  import { existsSync } from "fs";
729
- import path4 from "path";
730
- import { z } from "zod";
885
+ import path6 from "path";
886
+ import { z as z2 } from "zod";
731
887
 
732
888
  // src/tasks/manager.ts
733
889
  import { spawn as spawn2 } from "child_process";
@@ -846,11 +1002,11 @@ var MAX_OUTPUT = 3e4;
846
1002
  var DEFAULT_TIMEOUT = 120;
847
1003
  var MAX_TIMEOUT = 600;
848
1004
  function findOnPath(exe, exclude) {
849
- for (const dir of (process.env.PATH ?? "").split(path4.delimiter)) {
1005
+ for (const dir of (process.env.PATH ?? "").split(path6.delimiter)) {
850
1006
  if (!dir || exclude?.(dir)) {
851
1007
  continue;
852
1008
  }
853
- const full = path4.join(dir, exe);
1009
+ const full = path6.join(dir, exe);
854
1010
  if (existsSync(full)) {
855
1011
  return full;
856
1012
  }
@@ -868,7 +1024,7 @@ function resolveShell() {
868
1024
  }
869
1025
  const gitExe = findOnPath("git.exe");
870
1026
  if (gitExe) {
871
- const sibling = path4.join(path4.dirname(path4.dirname(gitExe)), "bin", "bash.exe");
1027
+ const sibling = path6.join(path6.dirname(path6.dirname(gitExe)), "bin", "bash.exe");
872
1028
  if (existsSync(sibling)) {
873
1029
  return { shell: sibling, wrap: (c) => ["-c", c], label: "bash" };
874
1030
  }
@@ -885,11 +1041,11 @@ function resolveShell() {
885
1041
  const comspec = process.env.ComSpec ?? "cmd.exe";
886
1042
  return { shell: comspec, wrap: (c) => ["/d", "/s", "/c", c], label: "cmd" };
887
1043
  }
888
- var schema = z.object({
889
- command: z.string().describe("Shell command to execute"),
890
- timeout: z.number().positive().max(MAX_TIMEOUT).optional().describe("Timeout in seconds (default 120, max 600)"),
891
- description: z.string().optional().describe("Short description of what the command does"),
892
- run_in_background: z.boolean().optional().describe(
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(
893
1049
  "Run the command in the background and return a task id immediately (default false). Use task_output/task_kill to inspect or stop it."
894
1050
  )
895
1051
  });
@@ -992,13 +1148,13 @@ Command timed out after ${timeoutSeconds}s`,
992
1148
 
993
1149
  // src/tools/fs/edit.ts
994
1150
  import { readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
995
- import path5 from "path";
996
- import { z as z2 } from "zod";
997
- var schema2 = z2.object({
998
- path: z2.string().describe("File path, absolute or relative to the working directory"),
999
- old_string: z2.string().min(1).describe("Exact text to replace"),
1000
- new_string: z2.string().describe("Replacement text"),
1001
- replace_all: z2.boolean().optional().describe("Replace every occurrence instead of requiring a unique match")
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")
1002
1158
  });
1003
1159
  var editFileTool = {
1004
1160
  name: "edit_file",
@@ -1006,7 +1162,7 @@ var editFileTool = {
1006
1162
  permission: "write",
1007
1163
  parameters: schema2,
1008
1164
  async execute(args, ctx) {
1009
- const filePath = path5.resolve(ctx.cwd, args.path);
1165
+ const filePath = path7.resolve(ctx.cwd, args.path);
1010
1166
  let content;
1011
1167
  try {
1012
1168
  content = await readFile2(filePath, "utf8");
@@ -1049,14 +1205,15 @@ var editFileTool = {
1049
1205
  };
1050
1206
 
1051
1207
  // src/tools/fs/glob.ts
1052
- import path7 from "path";
1053
- import { z as z3 } from "zod";
1208
+ import path9 from "path";
1209
+ import { z as z4 } from "zod";
1054
1210
 
1055
1211
  // src/tools/fs/util.ts
1212
+ import { readFileSync as readFileSync3, statSync as statSync3 } from "fs";
1056
1213
  import { readdir, stat } from "fs/promises";
1057
- import path6 from "path";
1214
+ import path8 from "path";
1058
1215
  var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist"]);
1059
- async function walkFiles(root, skipDirs = SKIP_DIRS) {
1216
+ async function walkFiles(root, skipDirs = SKIP_DIRS, options = {}) {
1060
1217
  const out = [];
1061
1218
  async function walk(dir, relBase) {
1062
1219
  const entries = await readdir(dir, { withFileTypes: true }).catch(() => null);
@@ -1064,13 +1221,17 @@ async function walkFiles(root, skipDirs = SKIP_DIRS) {
1064
1221
  return;
1065
1222
  }
1066
1223
  for (const entry of entries) {
1067
- const abs = path6.join(dir, entry.name);
1224
+ const abs = path8.join(dir, entry.name);
1068
1225
  const rel = relBase ? `${relBase}/${entry.name}` : entry.name;
1069
1226
  if (entry.isDirectory()) {
1070
- if (!skipDirs.has(entry.name)) {
1071
- await walk(abs, rel);
1227
+ if (skipDirs.has(entry.name) || options.ignore?.(abs, true)) {
1228
+ continue;
1072
1229
  }
1230
+ await walk(abs, rel);
1073
1231
  } else if (entry.isFile()) {
1232
+ if (options.ignore?.(abs, false)) {
1233
+ continue;
1234
+ }
1074
1235
  try {
1075
1236
  const st = await stat(abs);
1076
1237
  out.push({ abs, rel, mtimeMs: st.mtimeMs });
@@ -1119,8 +1280,71 @@ function matchesGlob(pattern, relPath) {
1119
1280
  }
1120
1281
  return globToRegExp(normalized).test(relPath);
1121
1282
  }
1283
+ var IGNORE_FILE = ".starignore";
1284
+ var ignoreCache = /* @__PURE__ */ new Map();
1285
+ function parseIgnorePatterns(body) {
1286
+ const out = [];
1287
+ for (const raw of body.split("\n")) {
1288
+ const line = raw.trim();
1289
+ if (!line || line.startsWith("#")) {
1290
+ continue;
1291
+ }
1292
+ if (line.endsWith("/")) {
1293
+ const pattern = line.slice(0, -1);
1294
+ if (pattern) {
1295
+ out.push({ pattern, dirOnly: true });
1296
+ }
1297
+ } else {
1298
+ out.push({ pattern: line, dirOnly: false });
1299
+ }
1300
+ }
1301
+ return out;
1302
+ }
1303
+ function loadIgnorePatterns(dir) {
1304
+ const file = path8.join(dir, IGNORE_FILE);
1305
+ let mtimeMs;
1306
+ try {
1307
+ mtimeMs = statSync3(file).mtimeMs;
1308
+ } catch {
1309
+ return [];
1310
+ }
1311
+ const hit = ignoreCache.get(file);
1312
+ if (hit && hit.mtimeMs === mtimeMs) {
1313
+ return hit.patterns;
1314
+ }
1315
+ let patterns = [];
1316
+ try {
1317
+ patterns = parseIgnorePatterns(readFileSync3(file, "utf8"));
1318
+ } catch {
1319
+ patterns = [];
1320
+ }
1321
+ ignoreCache.set(file, { mtimeMs, patterns });
1322
+ return patterns;
1323
+ }
1324
+ function createIgnorePredicate(cwd) {
1325
+ const patterns = loadIgnorePatterns(cwd);
1326
+ if (patterns.length === 0) {
1327
+ return void 0;
1328
+ }
1329
+ const root = path8.resolve(cwd);
1330
+ return (absPath, isDir) => {
1331
+ const rel = path8.relative(root, absPath).split(path8.sep).join("/");
1332
+ if (!rel || rel.startsWith("..")) {
1333
+ return false;
1334
+ }
1335
+ for (const { pattern, dirOnly } of patterns) {
1336
+ if (dirOnly && !isDir) {
1337
+ continue;
1338
+ }
1339
+ if (matchesGlob(pattern, rel)) {
1340
+ return true;
1341
+ }
1342
+ }
1343
+ return false;
1344
+ };
1345
+ }
1122
1346
  function isSensitivePath2(filePath) {
1123
- const base = path6.basename(filePath);
1347
+ const base = path8.basename(filePath);
1124
1348
  if (base === ".env.example" || base === ".env.sample" || base === ".env.template") {
1125
1349
  return false;
1126
1350
  }
@@ -1135,11 +1359,11 @@ function isSensitivePath2(filePath) {
1135
1359
 
1136
1360
  // src/tools/fs/glob.ts
1137
1361
  var MAX_RESULTS = 100;
1138
- var schema3 = z3.object({
1139
- pattern: z3.string().describe(
1362
+ var schema3 = z4.object({
1363
+ pattern: z4.string().describe(
1140
1364
  "Glob pattern, e.g. 'src/**/*.ts'. Supports **, * and ?. A bare pattern like '*.ts' matches basenames recursively."
1141
1365
  ),
1142
- path: z3.string().optional().describe("Directory to search, defaults to the working directory")
1366
+ path: z4.string().optional().describe("Directory to search, defaults to the working directory")
1143
1367
  });
1144
1368
  var globTool = {
1145
1369
  name: "glob",
@@ -1147,8 +1371,8 @@ var globTool = {
1147
1371
  permission: "read",
1148
1372
  parameters: schema3,
1149
1373
  async execute(args, ctx) {
1150
- const root = path7.resolve(ctx.cwd, args.path ?? ".");
1151
- const files = await walkFiles(root);
1374
+ const root = path9.resolve(ctx.cwd, args.path ?? ".");
1375
+ const files = await walkFiles(root, void 0, { ignore: createIgnorePredicate(ctx.cwd) });
1152
1376
  const matched = files.filter((f) => matchesGlob(args.pattern, f.rel)).sort((a, b) => b.mtimeMs - a.mtimeMs).slice(0, MAX_RESULTS);
1153
1377
  if (matched.length === 0) {
1154
1378
  return { content: `No files matched pattern: ${args.pattern}` };
@@ -1159,14 +1383,14 @@ var globTool = {
1159
1383
 
1160
1384
  // src/tools/fs/grep.ts
1161
1385
  import { readFile as readFile3, stat as stat2 } from "fs/promises";
1162
- import path8 from "path";
1163
- import { z as z4 } from "zod";
1386
+ import path10 from "path";
1387
+ import { z as z5 } from "zod";
1164
1388
  var MAX_MATCHES = 250;
1165
- var schema4 = z4.object({
1166
- pattern: z4.string().describe("Regular expression to search for"),
1167
- path: z4.string().optional().describe("File or directory to search, defaults to the working directory"),
1168
- glob: z4.string().optional().describe("Glob pattern to filter which files are searched, e.g. '*.ts'"),
1169
- ignoreCase: z4.boolean().optional().describe("Case-insensitive matching")
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")
1170
1394
  });
1171
1395
  var grepTool = {
1172
1396
  name: "grep",
@@ -1180,14 +1404,14 @@ var grepTool = {
1180
1404
  } catch (err) {
1181
1405
  return { content: `Invalid regular expression: ${err.message}`, isError: true };
1182
1406
  }
1183
- const root = path8.resolve(ctx.cwd, args.path ?? ".");
1407
+ const root = path10.resolve(ctx.cwd, args.path ?? ".");
1184
1408
  let files;
1185
1409
  try {
1186
1410
  const st = await stat2(root);
1187
1411
  if (st.isFile()) {
1188
- files = [{ abs: root, rel: path8.basename(root), mtimeMs: st.mtimeMs }];
1412
+ files = [{ abs: root, rel: path10.basename(root), mtimeMs: st.mtimeMs }];
1189
1413
  } else {
1190
- files = await walkFiles(root);
1414
+ files = await walkFiles(root, void 0, { ignore: createIgnorePredicate(ctx.cwd) });
1191
1415
  }
1192
1416
  } catch {
1193
1417
  return { content: `Path not found: ${args.path ?? "."}`, isError: true };
@@ -1232,14 +1456,14 @@ var grepTool = {
1232
1456
 
1233
1457
  // src/tools/fs/read.ts
1234
1458
  import { readFile as readFile4, stat as stat3 } from "fs/promises";
1235
- import path9 from "path";
1236
- import { z as z5 } from "zod";
1459
+ import path11 from "path";
1460
+ import { z as z6 } from "zod";
1237
1461
  var DEFAULT_LIMIT = 2e3;
1238
1462
  var MAX_CHARS = 100 * 1024;
1239
- var schema5 = z5.object({
1240
- path: z5.string().describe("File path, absolute or relative to the working directory"),
1241
- offset: z5.number().int().positive().optional().describe("1-based line number to start reading from"),
1242
- limit: z5.number().int().positive().optional().describe("Maximum number of lines to read (default 2000)")
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)")
1243
1467
  });
1244
1468
  var readFileTool = {
1245
1469
  name: "read_file",
@@ -1247,7 +1471,7 @@ var readFileTool = {
1247
1471
  permission: "read",
1248
1472
  parameters: schema5,
1249
1473
  async execute(args, ctx) {
1250
- const filePath = path9.resolve(ctx.cwd, args.path);
1474
+ const filePath = path11.resolve(ctx.cwd, args.path);
1251
1475
  if (isSensitivePath2(filePath)) {
1252
1476
  return { content: `Refused to read sensitive file: ${args.path}`, isError: true };
1253
1477
  }
@@ -1293,11 +1517,11 @@ var readFileTool = {
1293
1517
 
1294
1518
  // src/tools/fs/write.ts
1295
1519
  import { mkdir as mkdir2, writeFile as writeFile3 } from "fs/promises";
1296
- import path10 from "path";
1297
- import { z as z6 } from "zod";
1298
- var schema6 = z6.object({
1299
- path: z6.string().describe("File path, absolute or relative to the working directory"),
1300
- content: z6.string().describe("Full content to write to the file")
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")
1301
1525
  });
1302
1526
  var writeFileTool = {
1303
1527
  name: "write_file",
@@ -1305,10 +1529,10 @@ var writeFileTool = {
1305
1529
  permission: "write",
1306
1530
  parameters: schema6,
1307
1531
  async execute(args, ctx) {
1308
- const filePath = path10.resolve(ctx.cwd, args.path);
1532
+ const filePath = path12.resolve(ctx.cwd, args.path);
1309
1533
  const snapshot = await captureSnapshot(filePath, "write_file");
1310
1534
  try {
1311
- await mkdir2(path10.dirname(filePath), { recursive: true });
1535
+ await mkdir2(path12.dirname(filePath), { recursive: true });
1312
1536
  await writeFile3(filePath, args.content, "utf8");
1313
1537
  } catch (err) {
1314
1538
  return { content: `Failed to write ${args.path}: ${err.message}`, isError: true };
@@ -1319,7 +1543,7 @@ var writeFileTool = {
1319
1543
  };
1320
1544
 
1321
1545
  // src/tools/tasks.ts
1322
- import { z as z7 } from "zod";
1546
+ import { z as z8 } from "zod";
1323
1547
 
1324
1548
  // src/tasks/format.ts
1325
1549
  function formatDuration(startedAt, endedAt) {
@@ -1365,14 +1589,14 @@ function formatTaskFinished(task) {
1365
1589
  }
1366
1590
 
1367
1591
  // src/tools/tasks.ts
1368
- var idSchema = z7.object({
1369
- id: z7.string().describe("Task id, e.g. task-1")
1592
+ var idSchema = z8.object({
1593
+ id: z8.string().describe("Task id, e.g. task-1")
1370
1594
  });
1371
1595
  var taskListTool = {
1372
1596
  name: "task_list",
1373
1597
  description: "List background shell tasks with status, runtime, and exit code.",
1374
1598
  permission: "read",
1375
- parameters: z7.object({}),
1599
+ parameters: z8.object({}),
1376
1600
  execute() {
1377
1601
  return Promise.resolve({ content: formatTaskList(defaultTaskManager.list()) });
1378
1602
  }
@@ -1415,19 +1639,19 @@ var taskKillTool = {
1415
1639
 
1416
1640
  // src/tools/todo.ts
1417
1641
  import { mkdir as mkdir3, readFile as readFile5, writeFile as writeFile4 } from "fs/promises";
1418
- import path11 from "path";
1419
- import { z as z8 } from "zod";
1420
- var todoItemSchema = z8.object({
1421
- id: z8.number().int(),
1422
- title: z8.string(),
1423
- status: z8.enum(["pending", "in_progress", "done"])
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"])
1424
1648
  });
1425
- var writeSchema = z8.object({
1426
- todos: z8.array(todoItemSchema).describe("The full todo list, replacing the current one")
1649
+ var writeSchema = z9.object({
1650
+ todos: z9.array(todoItemSchema).describe("The full todo list, replacing the current one")
1427
1651
  });
1428
- var readSchema = z8.object({});
1652
+ var readSchema = z9.object({});
1429
1653
  function fileFor(cwd) {
1430
- return path11.join(cwd, ".star", "todos.json");
1654
+ return path13.join(cwd, ".star", "todos.json");
1431
1655
  }
1432
1656
  var TodoStore = class {
1433
1657
  items = /* @__PURE__ */ new Map();
@@ -1456,7 +1680,7 @@ var TodoStore = class {
1456
1680
  }
1457
1681
  }
1458
1682
  async save(cwd) {
1459
- await mkdir3(path11.join(cwd, ".star"), { recursive: true });
1683
+ await mkdir3(path13.join(cwd, ".star"), { recursive: true });
1460
1684
  await writeFile4(fileFor(cwd), `${JSON.stringify(this.list(), null, 2)}
1461
1685
  `);
1462
1686
  }
@@ -1509,13 +1733,13 @@ function createTodoTools(store = defaultStore) {
1509
1733
  }
1510
1734
 
1511
1735
  // src/tools/web/fetch.ts
1512
- import { z as z9 } from "zod";
1736
+ import { z as z10 } from "zod";
1513
1737
  var DEFAULT_MAX_CHARS = 2e4;
1514
1738
  var MAX_BYTES = 2 * 1024 * 1024;
1515
1739
  var TIMEOUT_MS = 3e4;
1516
- var schema7 = z9.object({
1517
- url: z9.string().describe("The http(s) URL to fetch"),
1518
- maxChars: z9.number().int().positive().optional().describe("Maximum characters of text to return (default 20000)")
1740
+ var schema7 = z10.object({
1741
+ url: z10.string().describe("The http(s) URL to fetch"),
1742
+ maxChars: z10.number().int().positive().optional().describe("Maximum characters of text to return (default 20000)")
1519
1743
  });
1520
1744
  function decodeEntities(s) {
1521
1745
  return s.replace(/&#(\d+);/g, (_, n) => String.fromCodePoint(Number(n))).replace(/&#x([0-9a-fA-F]+);/gi, (_, n) => String.fromCodePoint(Number.parseInt(n, 16))).replace(/&(quot|lt|gt|nbsp);/g, (m) => {
@@ -1643,15 +1867,15 @@ var webFetchTool = {
1643
1867
  };
1644
1868
 
1645
1869
  // src/tools/web/search.ts
1646
- import { z as z10 } from "zod";
1870
+ import { z as z11 } from "zod";
1647
1871
  var TIMEOUT_MS2 = 15e3;
1648
1872
  var DEFAULT_MAX_RESULTS = 5;
1649
1873
  var MAX_RESULTS_LIMIT = 10;
1650
1874
  var SEARCH_URL = "https://html.duckduckgo.com/html/?q=";
1651
1875
  var USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36";
1652
- var schema8 = z10.object({
1653
- query: z10.string().min(1).describe("The search query"),
1654
- maxResults: z10.number().int().min(1).max(MAX_RESULTS_LIMIT).optional().describe(
1876
+ var schema8 = z11.object({
1877
+ query: z11.string().min(1).describe("The search query"),
1878
+ maxResults: z11.number().int().min(1).max(MAX_RESULTS_LIMIT).optional().describe(
1655
1879
  `Maximum number of results to return (default ${DEFAULT_MAX_RESULTS}, max ${MAX_RESULTS_LIMIT})`
1656
1880
  )
1657
1881
  });
@@ -1810,12 +2034,12 @@ function createSubagentTool(deps) {
1810
2034
  name: "subagent",
1811
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.",
1812
2036
  permission: "exec",
1813
- parameters: z11.object({
1814
- prompt: z11.string().describe("Complete, self-contained instructions for the subagent."),
1815
- description: z11.string().optional().describe("Short label for the subtask, shown in the result header.")
2037
+ parameters: z12.object({
2038
+ 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.")
1816
2040
  }),
1817
2041
  async execute(args, ctx) {
1818
- const { AgentLoop: AgentLoop2 } = await import("./loop-XIILV7GY.js");
2042
+ const { AgentLoop: AgentLoop2 } = await import("./loop-ADVWZA62.js");
1819
2043
  const child = new AgentLoop2({
1820
2044
  model: deps.model,
1821
2045
  registry: createDefaultRegistry(),
@@ -1872,6 +2096,64 @@ ${report || "(subagent produced no text output)"}` };
1872
2096
  };
1873
2097
  }
1874
2098
 
2099
+ // src/agent/user-memory.ts
2100
+ import { appendFileSync, mkdirSync, readFileSync as readFileSync4, statSync as statSync4 } from "fs";
2101
+ import path14 from "path";
2102
+ import { z as z13 } from "zod";
2103
+ var USER_MEMORY_MAX_CHARS = 32 * 1024;
2104
+ var REMEMBER_TEXT_MAX_CHARS = 500;
2105
+ var cache3 = /* @__PURE__ */ new Map();
2106
+ function readUserMemory(home = starHome()) {
2107
+ const file = path14.join(home, "MEMORY.md");
2108
+ let mtimeMs;
2109
+ try {
2110
+ mtimeMs = statSync4(file).mtimeMs;
2111
+ } catch {
2112
+ return null;
2113
+ }
2114
+ const hit = cache3.get(file);
2115
+ if (hit && hit.mtimeMs === mtimeMs) return hit.block;
2116
+ let block = null;
2117
+ try {
2118
+ let body = readFileSync4(file, "utf8").trim();
2119
+ if (body) {
2120
+ if (body.length > USER_MEMORY_MAX_CHARS) {
2121
+ body = `${body.slice(0, USER_MEMORY_MAX_CHARS)}
2122
+
2123
+ [MEMORY.md truncated to ${USER_MEMORY_MAX_CHARS} characters]`;
2124
+ }
2125
+ block = [
2126
+ "# User memory (MEMORY.md)",
2127
+ "The following notes were saved by the user across sessions. Respect them unless they conflict with the current request.",
2128
+ body
2129
+ ].join("\n");
2130
+ }
2131
+ } catch {
2132
+ block = null;
2133
+ }
2134
+ cache3.set(file, { mtimeMs, block });
2135
+ return block;
2136
+ }
2137
+ function appendUserMemory(text, home = starHome()) {
2138
+ mkdirSync(home, { recursive: true });
2139
+ appendFileSync(path14.join(home, "MEMORY.md"), `- ${text}
2140
+ `, "utf8");
2141
+ }
2142
+ function createRememberTool(deps = {}) {
2143
+ return {
2144
+ name: "remember",
2145
+ 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.`,
2146
+ permission: "write",
2147
+ parameters: z13.object({
2148
+ text: z13.string().max(REMEMBER_TEXT_MAX_CHARS).describe("The note to save, as a single concise sentence.")
2149
+ }),
2150
+ async execute(args) {
2151
+ appendUserMemory(args.text, deps.home);
2152
+ return { content: `Saved to user memory: ${args.text}` };
2153
+ }
2154
+ };
2155
+ }
2156
+
1875
2157
  // src/agent/loop.ts
1876
2158
  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.";
1877
2159
  var AgentLoop = class {
@@ -1896,6 +2178,8 @@ var AgentLoop = class {
1896
2178
  if (opts.sessionStore) {
1897
2179
  this.bindSnapshotHooks(opts.sessionStore);
1898
2180
  }
2181
+ opts.registry?.register(createSkillTool({ cwd: opts.cwd }));
2182
+ opts.registry?.register(createRememberTool());
1899
2183
  const depth = opts.subagentDepth ?? 0;
1900
2184
  if (opts.registry && depth < MAX_SUBAGENT_DEPTH) {
1901
2185
  opts.registry.register(
@@ -2029,7 +2313,8 @@ var AgentLoop = class {
2029
2313
  const { config, registry, cwd } = this.opts;
2030
2314
  const aiTools = this.buildAiTools();
2031
2315
  for (let step = 0; step < config.maxSteps; step++) {
2032
- const compacted = compactMessages([...this.messages], config.contextMaxTokens);
2316
+ const maxTokens = this.opts.contextMaxTokens ?? config.contextMaxTokens;
2317
+ const compacted = compactMessages([...this.messages], maxTokens);
2033
2318
  if (compacted.compacted) {
2034
2319
  this.messages = await this.applyCompactionSummary(compacted);
2035
2320
  }
@@ -2139,8 +2424,11 @@ var AgentLoop = class {
2139
2424
  // /model switch) replaced the history with one that has none. Git context
2140
2425
  // (branch, dirty count, recent commits) is refreshed here too, so the model
2141
2426
  // always sees the current repo state; any git failure is silently skipped.
2142
- // Project memory (AGENTS.md in the cwd) is appended as a delimited block;
2143
- // readProjectMemory caches by mtime, so this stays cheap per turn.
2427
+ // Project memory (AGENTS.md in the cwd) and user memory (~/.star-cli/
2428
+ // MEMORY.md) are appended as delimited blocks; both cache by mtime, so this
2429
+ // stays cheap per turn. The
2430
+ // skills listing is refreshed here as well, so skills added mid-session
2431
+ // show up in the next turn's system prompt.
2144
2432
  syncSystemMessage() {
2145
2433
  const base = this.opts.system;
2146
2434
  const plan = this.opts.config.permissionMode === "plan";
@@ -2149,6 +2437,10 @@ var AgentLoop = class {
2149
2437
  if (plan) parts.push(PLAN_MODE_PROMPT.trim());
2150
2438
  const memory = readProjectMemory(this.opts.cwd);
2151
2439
  if (memory) parts.push(memory);
2440
+ const userMemory = readUserMemory();
2441
+ if (userMemory) parts.push(userMemory);
2442
+ const skills = discoverSkills(this.opts.cwd);
2443
+ if (skills.length > 0) parts.push(formatSkillsBlock(skills));
2152
2444
  const git = getGitSummary(this.opts.cwd);
2153
2445
  if (git) parts.push(formatGitSummary(git));
2154
2446
  if (parts.length === 0) return;
@@ -2228,7 +2520,8 @@ ${summary}`
2228
2520
  config.permissionMode,
2229
2521
  { toolName: call.name, args: call.args, level: tool.permission },
2230
2522
  { cwd },
2231
- config.permissions.allow
2523
+ config.permissions.allow,
2524
+ config.permissions.deny
2232
2525
  );
2233
2526
  if (decision === "deny") {
2234
2527
  return { content: `Permission denied for tool "${call.name}".`, isError: true };
@@ -2297,10 +2590,17 @@ export {
2297
2590
  hydrateSnapshots,
2298
2591
  undoTurnSnapshots,
2299
2592
  rewindToSnapshot,
2593
+ starHome,
2594
+ globalConfigPath,
2595
+ sessionsDir,
2596
+ userMemoryPath,
2597
+ projectConfigPath,
2598
+ discoverSkills,
2300
2599
  defaultTaskManager,
2301
2600
  resolveShell,
2302
2601
  bashTool,
2303
2602
  SKIP_DIRS,
2603
+ createIgnorePredicate,
2304
2604
  isSensitivePath2 as isSensitivePath,
2305
2605
  formatTaskList,
2306
2606
  formatTaskStarted,
@@ -2308,7 +2608,8 @@ export {
2308
2608
  TodoStore,
2309
2609
  formatTodos,
2310
2610
  createDefaultRegistry,
2611
+ appendUserMemory,
2311
2612
  PLAN_MODE_PROMPT,
2312
2613
  AgentLoop
2313
2614
  };
2314
- //# sourceMappingURL=chunk-RLQXGX3N.js.map
2615
+ //# sourceMappingURL=chunk-GN236FD4.js.map