@cryer/star-cli 0.2.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -73,7 +73,14 @@ apiKeyEnv = "OPENAI_API_KEY"
73
73
  name = "gpt"
74
74
  provider = "openai"
75
75
  model = "gpt-4o"
76
- # optional per-model pricing (USD per 1M tokens) — enables the $ estimate in /cost and /usage
76
+ # optional per-model context window — overrides the top-level contextMaxTokens
77
+ # for compaction and the ctx % in the status bar
78
+ # contextMaxTokens = 272000
79
+ # optional per-model pricing in USD per 1M tokens — enables the $ estimate in
80
+ # /cost and /usage. promptPrice prices input tokens (system prompt, history,
81
+ # @file contents, tool results — resent every turn, so the bulk of usage);
82
+ # completionPrice prices output tokens (the model's replies and tool calls —
83
+ # less volume, usually the pricier rate). BOTH fields are required together.
77
84
  promptPrice = 2.5
78
85
  completionPrice = 10
79
86
 
@@ -135,6 +142,7 @@ star --clear-sessions all delete every stored session
135
142
  | `/rewind [n]` | list file-change checkpoints, or rewind to just before checkpoint `n`: restore every file changed since then and retract the matching conversation messages (asks for confirmation first) |
136
143
  | `/init [force]` | scan the project and generate an AGENTS.md (LLM-polished when a model is available) |
137
144
  | `/doctor` | environment self-check (Node, shell, config, API key status, sessions dir writability) |
145
+ | `/skills` | list available skills (project scope overrides user scope) |
138
146
  | `/commit [instructions]` | analyze uncommitted changes and let the agent stage + commit them with a Conventional Commits message (git add/commit runs through the normal permission gate) |
139
147
  | `/diff` | show uncommitted changes client-side: `git status --short` plus a colored staged/unstaged diff (no model call; huge diffs are truncated at 2000 lines) |
140
148
  | `/copy [all]` | copy the last assistant reply to the clipboard (`all`: the whole conversation as plain text) |
@@ -146,7 +154,7 @@ Keys: `ESC` / `Ctrl+C` interrupts the current stream; on a permission prompt: `y
146
154
 
147
155
  While the model is working, a spinner (`- \ | /`) shows `star is thinking…`; reasoning models also stream a dimmed tail of their thinking (last ~200 chars), which collapses to a one-line summary once the answer starts.
148
156
 
149
- The status bar shows the working directory (full path on wide terminals), the git branch, the model, the permission mode, context usage as a percentage of `contextMaxTokens`, the session cost when the model has pricing configured, and total tokens.
157
+ The status bar shows the working directory (full path on wide terminals), the git branch, the model, the permission mode, context usage as a percentage of the model's context window (its own `contextMaxTokens` when set, else the top-level one), the session cost when the model has pricing configured, and total tokens.
150
158
 
151
159
  ## !shell passthrough
152
160
 
@@ -217,6 +225,19 @@ Review the following code and list concrete issues: $ARGUMENTS
217
225
 
218
226
  The REPL also checks npm for a newer release on startup (async, non-blocking; disable with `STAR_NO_UPDATE_CHECK=1`).
219
227
 
228
+ ## Skills
229
+
230
+ Skills are reusable instruction packs the agent loads on demand. Drop a `SKILL.md` into `.star/skills/<name>/` (project) or `~/.star-cli/skills/<name>/` (user); project skills override user skills of the same name. Each `SKILL.md` starts with frontmatter (`name` optional, defaults to the directory name; `description` required), followed by the instructions body:
231
+
232
+ ```markdown
233
+ ---
234
+ description: Review code for common issues
235
+ ---
236
+ Check error handling, naming, and test coverage. Report concrete findings with file:line references.
237
+ ```
238
+
239
+ Available skills are listed in the system prompt (name + description only, so they cost almost no tokens until used). When a request matches one, the agent calls the read-level `skill` tool to load the full body — supporting files next to the `SKILL.md` (scripts, templates, examples) are resolved relative to the skill's directory. Skills added mid-session are picked up on the next turn; `/skills` lists what's currently visible. Bodies are capped at 32KB.
240
+
220
241
  ## @file mentions
221
242
 
222
243
  Prefix a path with `@` in any prompt to attach its content (REPL and print mode alike):
@@ -237,7 +258,7 @@ Missing, binary, oversized (>100KB for text, >5MB for images), or sensitive file
237
258
 
238
259
  ## Built-in tools
239
260
 
240
- `read_file`, `write_file`, `edit_file`, `glob`, `grep`, `bash`, `web_fetch`, `web_search` (DuckDuckGo, no API key needed), `todo_read`, `todo_write`, `task_list`, `task_output`, `task_kill`, `subagent` — each declares a permission level (`read` / `write` / `exec`) enforced by the permission gate. Hard safety rules (dangerous shell commands, paths outside the working directory, secret files like `.env` / private keys) are denied in `ask` / `auto` / `readonly` and cannot be overridden by allow-rules.
261
+ `read_file`, `write_file`, `edit_file`, `glob`, `grep`, `bash`, `web_fetch`, `web_search` (DuckDuckGo, no API key needed), `todo_read`, `todo_write`, `task_list`, `task_output`, `task_kill`, `subagent`, `skill` — each declares a permission level (`read` / `write` / `exec`) enforced by the permission gate. Hard safety rules (dangerous shell commands, paths outside the working directory, secret files like `.env` / private keys) are denied in `ask` / `auto` / `readonly` and cannot be overridden by allow-rules.
241
262
 
242
263
  The `subagent` tool (`exec` level, so it is hidden in plan mode and denied in readonly) spawns a child agent loop with the same built-in tools to handle a focused, self-contained subtask — research, exploration, or an isolated change — and returns the child's final report as the tool result. Subagents run one level deep (a subagent cannot spawn further subagents), share the parent's permission mode and confirmation prompt, and their conversation is not persisted to the session.
243
264
 
@@ -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;
@@ -720,14 +728,150 @@ ${body}`;
720
728
  return block;
721
729
  }
722
730
 
731
+ // src/agent/skills.ts
732
+ import { readFileSync as readFileSync2, readdirSync, statSync as statSync2 } from "fs";
733
+ import path5 from "path";
734
+ import { z } from "zod";
735
+
736
+ // src/config/paths.ts
737
+ import os from "os";
738
+ import path4 from "path";
739
+ function starHome() {
740
+ return process.env.STAR_HOME ?? path4.join(os.homedir(), ".star-cli");
741
+ }
742
+ function globalConfigPath() {
743
+ return path4.join(starHome(), "config.toml");
744
+ }
745
+ function sessionsDir() {
746
+ return path4.join(starHome(), "sessions");
747
+ }
748
+ function projectConfigPath(cwd) {
749
+ return path4.join(cwd, ".star", "config.toml");
750
+ }
751
+
752
+ // src/agent/skills.ts
753
+ var SKILL_FILE = "SKILL.md";
754
+ var SKILL_MAX_CHARS = 32 * 1024;
755
+ var NAME_PATTERN = /^[a-z0-9-]+$/;
756
+ function parseFrontmatter(raw) {
757
+ const match = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/);
758
+ if (!match) return null;
759
+ const meta = {};
760
+ for (const line of (match[1] ?? "").split(/\r?\n/)) {
761
+ const kv = line.match(/^([A-Za-z][A-Za-z0-9_-]*):\s*(.*)$/);
762
+ if (kv?.[1]) meta[kv[1]] = (kv[2] ?? "").trim();
763
+ }
764
+ return { meta, body: raw.slice(match[0].length).trim() };
765
+ }
766
+ var cache2 = /* @__PURE__ */ new Map();
767
+ function parseSkillFile(filePath, dirName, scope) {
768
+ let mtimeMs;
769
+ try {
770
+ mtimeMs = statSync2(filePath).mtimeMs;
771
+ } catch {
772
+ return null;
773
+ }
774
+ const hit = cache2.get(filePath);
775
+ if (hit && hit.mtimeMs === mtimeMs) return hit.skill;
776
+ let skill = null;
777
+ try {
778
+ const parsed = parseFrontmatter(readFileSync2(filePath, "utf8"));
779
+ const name = parsed?.meta.name || dirName;
780
+ const description = parsed?.meta.description;
781
+ if (parsed && NAME_PATTERN.test(name) && description) {
782
+ skill = { name, description, filePath, dir: path5.dirname(filePath), scope };
783
+ }
784
+ } catch {
785
+ skill = null;
786
+ }
787
+ cache2.set(filePath, { mtimeMs, skill });
788
+ return skill;
789
+ }
790
+ function scanDir(base, scope, into) {
791
+ let entries;
792
+ try {
793
+ entries = readdirSync(base);
794
+ } catch {
795
+ return;
796
+ }
797
+ for (const entry of entries.sort()) {
798
+ if (!NAME_PATTERN.test(entry)) continue;
799
+ const parsed = parseSkillFile(path5.join(base, entry, SKILL_FILE), entry, scope);
800
+ if (parsed) into.set(parsed.name, parsed);
801
+ }
802
+ }
803
+ function discoverSkills(cwd, home = starHome()) {
804
+ const byName = /* @__PURE__ */ new Map();
805
+ scanDir(path5.join(home, "skills"), "user", byName);
806
+ scanDir(path5.join(cwd, ".star", "skills"), "project", byName);
807
+ return [...byName.values()];
808
+ }
809
+ function formatSkillsBlock(skills) {
810
+ const lines = skills.map((s) => `- ${s.name}: ${s.description}`);
811
+ return [
812
+ "# Available skills",
813
+ "When the user's request matches one of these skills, call the skill tool with its name to load the full instructions before proceeding:",
814
+ ...lines
815
+ ].join("\n");
816
+ }
817
+ function loadSkillBody(skill) {
818
+ let parsed = null;
819
+ try {
820
+ parsed = parseFrontmatter(readFileSync2(skill.filePath, "utf8"));
821
+ } catch {
822
+ return null;
823
+ }
824
+ if (!parsed?.body) return null;
825
+ if (parsed.body.length > SKILL_MAX_CHARS) {
826
+ return `${parsed.body.slice(0, SKILL_MAX_CHARS)}
827
+
828
+ [skill truncated to ${SKILL_MAX_CHARS} characters]`;
829
+ }
830
+ return parsed.body;
831
+ }
832
+ function createSkillTool(deps) {
833
+ return {
834
+ name: "skill",
835
+ 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
+ permission: "read",
837
+ parameters: z.object({
838
+ name: z.string().describe("Name of the skill to load, as listed in the system prompt.")
839
+ }),
840
+ async execute(args) {
841
+ const skills = discoverSkills(deps.cwd, deps.home);
842
+ const skill = skills.find((s) => s.name === args.name);
843
+ if (!skill) {
844
+ const available = skills.map((s) => s.name).join(", ") || "none";
845
+ return {
846
+ content: `Unknown skill: "${args.name}". Available skills: ${available}`,
847
+ isError: true
848
+ };
849
+ }
850
+ const body = loadSkillBody(skill);
851
+ if (!body) {
852
+ return { content: `Skill "${skill.name}" is empty or unreadable.`, isError: true };
853
+ }
854
+ return {
855
+ content: [
856
+ `# Skill: ${skill.name}`,
857
+ "",
858
+ body,
859
+ "",
860
+ `Base directory for relative file references in this skill: ${skill.dir}`
861
+ ].join("\n")
862
+ };
863
+ }
864
+ };
865
+ }
866
+
723
867
  // src/agent/subagent.ts
724
- import { z as z11 } from "zod";
868
+ import { z as z12 } from "zod";
725
869
 
726
870
  // src/tools/bash.ts
727
871
  import { spawn as spawn3 } from "child_process";
728
872
  import { existsSync } from "fs";
729
- import path4 from "path";
730
- import { z } from "zod";
873
+ import path6 from "path";
874
+ import { z as z2 } from "zod";
731
875
 
732
876
  // src/tasks/manager.ts
733
877
  import { spawn as spawn2 } from "child_process";
@@ -846,11 +990,11 @@ var MAX_OUTPUT = 3e4;
846
990
  var DEFAULT_TIMEOUT = 120;
847
991
  var MAX_TIMEOUT = 600;
848
992
  function findOnPath(exe, exclude) {
849
- for (const dir of (process.env.PATH ?? "").split(path4.delimiter)) {
993
+ for (const dir of (process.env.PATH ?? "").split(path6.delimiter)) {
850
994
  if (!dir || exclude?.(dir)) {
851
995
  continue;
852
996
  }
853
- const full = path4.join(dir, exe);
997
+ const full = path6.join(dir, exe);
854
998
  if (existsSync(full)) {
855
999
  return full;
856
1000
  }
@@ -868,7 +1012,7 @@ function resolveShell() {
868
1012
  }
869
1013
  const gitExe = findOnPath("git.exe");
870
1014
  if (gitExe) {
871
- const sibling = path4.join(path4.dirname(path4.dirname(gitExe)), "bin", "bash.exe");
1015
+ const sibling = path6.join(path6.dirname(path6.dirname(gitExe)), "bin", "bash.exe");
872
1016
  if (existsSync(sibling)) {
873
1017
  return { shell: sibling, wrap: (c) => ["-c", c], label: "bash" };
874
1018
  }
@@ -885,11 +1029,11 @@ function resolveShell() {
885
1029
  const comspec = process.env.ComSpec ?? "cmd.exe";
886
1030
  return { shell: comspec, wrap: (c) => ["/d", "/s", "/c", c], label: "cmd" };
887
1031
  }
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(
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(
893
1037
  "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
1038
  )
895
1039
  });
@@ -992,13 +1136,13 @@ Command timed out after ${timeoutSeconds}s`,
992
1136
 
993
1137
  // src/tools/fs/edit.ts
994
1138
  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")
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")
1002
1146
  });
1003
1147
  var editFileTool = {
1004
1148
  name: "edit_file",
@@ -1006,7 +1150,7 @@ var editFileTool = {
1006
1150
  permission: "write",
1007
1151
  parameters: schema2,
1008
1152
  async execute(args, ctx) {
1009
- const filePath = path5.resolve(ctx.cwd, args.path);
1153
+ const filePath = path7.resolve(ctx.cwd, args.path);
1010
1154
  let content;
1011
1155
  try {
1012
1156
  content = await readFile2(filePath, "utf8");
@@ -1049,12 +1193,12 @@ var editFileTool = {
1049
1193
  };
1050
1194
 
1051
1195
  // src/tools/fs/glob.ts
1052
- import path7 from "path";
1053
- import { z as z3 } from "zod";
1196
+ import path9 from "path";
1197
+ import { z as z4 } from "zod";
1054
1198
 
1055
1199
  // src/tools/fs/util.ts
1056
1200
  import { readdir, stat } from "fs/promises";
1057
- import path6 from "path";
1201
+ import path8 from "path";
1058
1202
  var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist"]);
1059
1203
  async function walkFiles(root, skipDirs = SKIP_DIRS) {
1060
1204
  const out = [];
@@ -1064,7 +1208,7 @@ async function walkFiles(root, skipDirs = SKIP_DIRS) {
1064
1208
  return;
1065
1209
  }
1066
1210
  for (const entry of entries) {
1067
- const abs = path6.join(dir, entry.name);
1211
+ const abs = path8.join(dir, entry.name);
1068
1212
  const rel = relBase ? `${relBase}/${entry.name}` : entry.name;
1069
1213
  if (entry.isDirectory()) {
1070
1214
  if (!skipDirs.has(entry.name)) {
@@ -1120,7 +1264,7 @@ function matchesGlob(pattern, relPath) {
1120
1264
  return globToRegExp(normalized).test(relPath);
1121
1265
  }
1122
1266
  function isSensitivePath2(filePath) {
1123
- const base = path6.basename(filePath);
1267
+ const base = path8.basename(filePath);
1124
1268
  if (base === ".env.example" || base === ".env.sample" || base === ".env.template") {
1125
1269
  return false;
1126
1270
  }
@@ -1135,11 +1279,11 @@ function isSensitivePath2(filePath) {
1135
1279
 
1136
1280
  // src/tools/fs/glob.ts
1137
1281
  var MAX_RESULTS = 100;
1138
- var schema3 = z3.object({
1139
- pattern: z3.string().describe(
1282
+ var schema3 = z4.object({
1283
+ pattern: z4.string().describe(
1140
1284
  "Glob pattern, e.g. 'src/**/*.ts'. Supports **, * and ?. A bare pattern like '*.ts' matches basenames recursively."
1141
1285
  ),
1142
- path: z3.string().optional().describe("Directory to search, defaults to the working directory")
1286
+ path: z4.string().optional().describe("Directory to search, defaults to the working directory")
1143
1287
  });
1144
1288
  var globTool = {
1145
1289
  name: "glob",
@@ -1147,7 +1291,7 @@ var globTool = {
1147
1291
  permission: "read",
1148
1292
  parameters: schema3,
1149
1293
  async execute(args, ctx) {
1150
- const root = path7.resolve(ctx.cwd, args.path ?? ".");
1294
+ const root = path9.resolve(ctx.cwd, args.path ?? ".");
1151
1295
  const files = await walkFiles(root);
1152
1296
  const matched = files.filter((f) => matchesGlob(args.pattern, f.rel)).sort((a, b) => b.mtimeMs - a.mtimeMs).slice(0, MAX_RESULTS);
1153
1297
  if (matched.length === 0) {
@@ -1159,14 +1303,14 @@ var globTool = {
1159
1303
 
1160
1304
  // src/tools/fs/grep.ts
1161
1305
  import { readFile as readFile3, stat as stat2 } from "fs/promises";
1162
- import path8 from "path";
1163
- import { z as z4 } from "zod";
1306
+ import path10 from "path";
1307
+ import { z as z5 } from "zod";
1164
1308
  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")
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")
1170
1314
  });
1171
1315
  var grepTool = {
1172
1316
  name: "grep",
@@ -1180,12 +1324,12 @@ var grepTool = {
1180
1324
  } catch (err) {
1181
1325
  return { content: `Invalid regular expression: ${err.message}`, isError: true };
1182
1326
  }
1183
- const root = path8.resolve(ctx.cwd, args.path ?? ".");
1327
+ const root = path10.resolve(ctx.cwd, args.path ?? ".");
1184
1328
  let files;
1185
1329
  try {
1186
1330
  const st = await stat2(root);
1187
1331
  if (st.isFile()) {
1188
- files = [{ abs: root, rel: path8.basename(root), mtimeMs: st.mtimeMs }];
1332
+ files = [{ abs: root, rel: path10.basename(root), mtimeMs: st.mtimeMs }];
1189
1333
  } else {
1190
1334
  files = await walkFiles(root);
1191
1335
  }
@@ -1232,14 +1376,14 @@ var grepTool = {
1232
1376
 
1233
1377
  // src/tools/fs/read.ts
1234
1378
  import { readFile as readFile4, stat as stat3 } from "fs/promises";
1235
- import path9 from "path";
1236
- import { z as z5 } from "zod";
1379
+ import path11 from "path";
1380
+ import { z as z6 } from "zod";
1237
1381
  var DEFAULT_LIMIT = 2e3;
1238
1382
  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)")
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)")
1243
1387
  });
1244
1388
  var readFileTool = {
1245
1389
  name: "read_file",
@@ -1247,7 +1391,7 @@ var readFileTool = {
1247
1391
  permission: "read",
1248
1392
  parameters: schema5,
1249
1393
  async execute(args, ctx) {
1250
- const filePath = path9.resolve(ctx.cwd, args.path);
1394
+ const filePath = path11.resolve(ctx.cwd, args.path);
1251
1395
  if (isSensitivePath2(filePath)) {
1252
1396
  return { content: `Refused to read sensitive file: ${args.path}`, isError: true };
1253
1397
  }
@@ -1293,11 +1437,11 @@ var readFileTool = {
1293
1437
 
1294
1438
  // src/tools/fs/write.ts
1295
1439
  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")
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")
1301
1445
  });
1302
1446
  var writeFileTool = {
1303
1447
  name: "write_file",
@@ -1305,10 +1449,10 @@ var writeFileTool = {
1305
1449
  permission: "write",
1306
1450
  parameters: schema6,
1307
1451
  async execute(args, ctx) {
1308
- const filePath = path10.resolve(ctx.cwd, args.path);
1452
+ const filePath = path12.resolve(ctx.cwd, args.path);
1309
1453
  const snapshot = await captureSnapshot(filePath, "write_file");
1310
1454
  try {
1311
- await mkdir2(path10.dirname(filePath), { recursive: true });
1455
+ await mkdir2(path12.dirname(filePath), { recursive: true });
1312
1456
  await writeFile3(filePath, args.content, "utf8");
1313
1457
  } catch (err) {
1314
1458
  return { content: `Failed to write ${args.path}: ${err.message}`, isError: true };
@@ -1319,7 +1463,7 @@ var writeFileTool = {
1319
1463
  };
1320
1464
 
1321
1465
  // src/tools/tasks.ts
1322
- import { z as z7 } from "zod";
1466
+ import { z as z8 } from "zod";
1323
1467
 
1324
1468
  // src/tasks/format.ts
1325
1469
  function formatDuration(startedAt, endedAt) {
@@ -1365,14 +1509,14 @@ function formatTaskFinished(task) {
1365
1509
  }
1366
1510
 
1367
1511
  // src/tools/tasks.ts
1368
- var idSchema = z7.object({
1369
- id: z7.string().describe("Task id, e.g. task-1")
1512
+ var idSchema = z8.object({
1513
+ id: z8.string().describe("Task id, e.g. task-1")
1370
1514
  });
1371
1515
  var taskListTool = {
1372
1516
  name: "task_list",
1373
1517
  description: "List background shell tasks with status, runtime, and exit code.",
1374
1518
  permission: "read",
1375
- parameters: z7.object({}),
1519
+ parameters: z8.object({}),
1376
1520
  execute() {
1377
1521
  return Promise.resolve({ content: formatTaskList(defaultTaskManager.list()) });
1378
1522
  }
@@ -1415,19 +1559,19 @@ var taskKillTool = {
1415
1559
 
1416
1560
  // src/tools/todo.ts
1417
1561
  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"])
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"])
1424
1568
  });
1425
- var writeSchema = z8.object({
1426
- todos: z8.array(todoItemSchema).describe("The full todo list, replacing the current one")
1569
+ var writeSchema = z9.object({
1570
+ todos: z9.array(todoItemSchema).describe("The full todo list, replacing the current one")
1427
1571
  });
1428
- var readSchema = z8.object({});
1572
+ var readSchema = z9.object({});
1429
1573
  function fileFor(cwd) {
1430
- return path11.join(cwd, ".star", "todos.json");
1574
+ return path13.join(cwd, ".star", "todos.json");
1431
1575
  }
1432
1576
  var TodoStore = class {
1433
1577
  items = /* @__PURE__ */ new Map();
@@ -1456,7 +1600,7 @@ var TodoStore = class {
1456
1600
  }
1457
1601
  }
1458
1602
  async save(cwd) {
1459
- await mkdir3(path11.join(cwd, ".star"), { recursive: true });
1603
+ await mkdir3(path13.join(cwd, ".star"), { recursive: true });
1460
1604
  await writeFile4(fileFor(cwd), `${JSON.stringify(this.list(), null, 2)}
1461
1605
  `);
1462
1606
  }
@@ -1509,13 +1653,13 @@ function createTodoTools(store = defaultStore) {
1509
1653
  }
1510
1654
 
1511
1655
  // src/tools/web/fetch.ts
1512
- import { z as z9 } from "zod";
1656
+ import { z as z10 } from "zod";
1513
1657
  var DEFAULT_MAX_CHARS = 2e4;
1514
1658
  var MAX_BYTES = 2 * 1024 * 1024;
1515
1659
  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)")
1660
+ var schema7 = z10.object({
1661
+ url: z10.string().describe("The http(s) URL to fetch"),
1662
+ maxChars: z10.number().int().positive().optional().describe("Maximum characters of text to return (default 20000)")
1519
1663
  });
1520
1664
  function decodeEntities(s) {
1521
1665
  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 +1787,15 @@ var webFetchTool = {
1643
1787
  };
1644
1788
 
1645
1789
  // src/tools/web/search.ts
1646
- import { z as z10 } from "zod";
1790
+ import { z as z11 } from "zod";
1647
1791
  var TIMEOUT_MS2 = 15e3;
1648
1792
  var DEFAULT_MAX_RESULTS = 5;
1649
1793
  var MAX_RESULTS_LIMIT = 10;
1650
1794
  var SEARCH_URL = "https://html.duckduckgo.com/html/?q=";
1651
1795
  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(
1796
+ var schema8 = z11.object({
1797
+ query: z11.string().min(1).describe("The search query"),
1798
+ maxResults: z11.number().int().min(1).max(MAX_RESULTS_LIMIT).optional().describe(
1655
1799
  `Maximum number of results to return (default ${DEFAULT_MAX_RESULTS}, max ${MAX_RESULTS_LIMIT})`
1656
1800
  )
1657
1801
  });
@@ -1810,12 +1954,12 @@ function createSubagentTool(deps) {
1810
1954
  name: "subagent",
1811
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.",
1812
1956
  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.")
1957
+ parameters: z12.object({
1958
+ 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.")
1816
1960
  }),
1817
1961
  async execute(args, ctx) {
1818
- const { AgentLoop: AgentLoop2 } = await import("./loop-XIILV7GY.js");
1962
+ const { AgentLoop: AgentLoop2 } = await import("./loop-R3RQCNFT.js");
1819
1963
  const child = new AgentLoop2({
1820
1964
  model: deps.model,
1821
1965
  registry: createDefaultRegistry(),
@@ -1896,6 +2040,7 @@ var AgentLoop = class {
1896
2040
  if (opts.sessionStore) {
1897
2041
  this.bindSnapshotHooks(opts.sessionStore);
1898
2042
  }
2043
+ opts.registry?.register(createSkillTool({ cwd: opts.cwd }));
1899
2044
  const depth = opts.subagentDepth ?? 0;
1900
2045
  if (opts.registry && depth < MAX_SUBAGENT_DEPTH) {
1901
2046
  opts.registry.register(
@@ -2029,7 +2174,8 @@ var AgentLoop = class {
2029
2174
  const { config, registry, cwd } = this.opts;
2030
2175
  const aiTools = this.buildAiTools();
2031
2176
  for (let step = 0; step < config.maxSteps; step++) {
2032
- const compacted = compactMessages([...this.messages], config.contextMaxTokens);
2177
+ const maxTokens = this.opts.contextMaxTokens ?? config.contextMaxTokens;
2178
+ const compacted = compactMessages([...this.messages], maxTokens);
2033
2179
  if (compacted.compacted) {
2034
2180
  this.messages = await this.applyCompactionSummary(compacted);
2035
2181
  }
@@ -2140,7 +2286,9 @@ var AgentLoop = class {
2140
2286
  // (branch, dirty count, recent commits) is refreshed here too, so the model
2141
2287
  // always sees the current repo state; any git failure is silently skipped.
2142
2288
  // Project memory (AGENTS.md in the cwd) is appended as a delimited block;
2143
- // readProjectMemory caches by mtime, so this stays cheap per turn.
2289
+ // readProjectMemory caches by mtime, so this stays cheap per turn. The
2290
+ // skills listing is refreshed here as well, so skills added mid-session
2291
+ // show up in the next turn's system prompt.
2144
2292
  syncSystemMessage() {
2145
2293
  const base = this.opts.system;
2146
2294
  const plan = this.opts.config.permissionMode === "plan";
@@ -2149,6 +2297,8 @@ var AgentLoop = class {
2149
2297
  if (plan) parts.push(PLAN_MODE_PROMPT.trim());
2150
2298
  const memory = readProjectMemory(this.opts.cwd);
2151
2299
  if (memory) parts.push(memory);
2300
+ const skills = discoverSkills(this.opts.cwd);
2301
+ if (skills.length > 0) parts.push(formatSkillsBlock(skills));
2152
2302
  const git = getGitSummary(this.opts.cwd);
2153
2303
  if (git) parts.push(formatGitSummary(git));
2154
2304
  if (parts.length === 0) return;
@@ -2297,6 +2447,11 @@ export {
2297
2447
  hydrateSnapshots,
2298
2448
  undoTurnSnapshots,
2299
2449
  rewindToSnapshot,
2450
+ starHome,
2451
+ globalConfigPath,
2452
+ sessionsDir,
2453
+ projectConfigPath,
2454
+ discoverSkills,
2300
2455
  defaultTaskManager,
2301
2456
  resolveShell,
2302
2457
  bashTool,
@@ -2311,4 +2466,4 @@ export {
2311
2466
  PLAN_MODE_PROMPT,
2312
2467
  AgentLoop
2313
2468
  };
2314
- //# sourceMappingURL=chunk-RLQXGX3N.js.map
2469
+ //# sourceMappingURL=chunk-MJ2O7NWI.js.map