@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/dist/main.js CHANGED
@@ -13,24 +13,29 @@ import {
13
13
  compactMessages,
14
14
  createDefaultRegistry,
15
15
  defaultTaskManager,
16
+ discoverSkills,
16
17
  estimateTokens,
17
18
  formatTaskFinished,
18
19
  formatTaskList,
19
20
  formatTaskStarted,
20
21
  formatTodos,
21
22
  getGitSummary,
23
+ globalConfigPath,
22
24
  hydrateSnapshots,
23
25
  isAllowedByRules,
24
26
  isDangerousCommand,
25
27
  isGitRepo,
26
28
  isSensitivePath,
27
29
  listSnapshots,
30
+ projectConfigPath,
28
31
  reconcileToolCalls,
29
32
  resolveShell,
30
33
  rewindToSnapshot,
34
+ sessionsDir,
35
+ starHome,
31
36
  summarizeMessages,
32
37
  undoTurnSnapshots
33
- } from "./chunk-RLQXGX3N.js";
38
+ } from "./chunk-MJ2O7NWI.js";
34
39
 
35
40
  // src/main.tsx
36
41
  import { Command } from "commander";
@@ -269,26 +274,8 @@ import { useCallback, useEffect as useEffect2, useMemo, useRef as useRef2, useSt
269
274
 
270
275
  // src/config/save.ts
271
276
  import fs from "fs";
272
- import path3 from "path";
273
- import { parse, stringify } from "smol-toml";
274
-
275
- // src/config/paths.ts
276
- import os from "os";
277
277
  import path2 from "path";
278
- function starHome() {
279
- return process.env.STAR_HOME ?? path2.join(os.homedir(), ".star-cli");
280
- }
281
- function globalConfigPath() {
282
- return path2.join(starHome(), "config.toml");
283
- }
284
- function sessionsDir() {
285
- return path2.join(starHome(), "sessions");
286
- }
287
- function projectConfigPath(cwd) {
288
- return path2.join(cwd, ".star", "config.toml");
289
- }
290
-
291
- // src/config/save.ts
278
+ import { parse, stringify } from "smol-toml";
292
279
  async function addAllowRule(rule) {
293
280
  const filePath = globalConfigPath();
294
281
  let raw = {};
@@ -303,7 +290,7 @@ async function addAllowRule(rule) {
303
290
  if (allow.includes(rule)) return false;
304
291
  permissions.allow = [...allow, rule];
305
292
  raw.permissions = permissions;
306
- await fs.promises.mkdir(path3.dirname(filePath), { recursive: true });
293
+ await fs.promises.mkdir(path2.dirname(filePath), { recursive: true });
307
294
  await fs.promises.writeFile(filePath, stringify(raw), "utf8");
308
295
  return true;
309
296
  }
@@ -317,10 +304,79 @@ async function savePermissionMode(mode) {
317
304
  if (error.code !== "ENOENT") throw error;
318
305
  }
319
306
  raw.permissionMode = mode;
320
- await fs.promises.mkdir(path3.dirname(filePath), { recursive: true });
307
+ await fs.promises.mkdir(path2.dirname(filePath), { recursive: true });
321
308
  await fs.promises.writeFile(filePath, stringify(raw), "utf8");
322
309
  }
323
310
 
311
+ // src/config/schema.ts
312
+ import { z } from "zod";
313
+ var ProviderConfigSchema = z.object({
314
+ name: z.string(),
315
+ protocol: z.enum(["openai-compatible", "anthropic", "openai-responses"]).default("openai-compatible"),
316
+ baseURL: z.string(),
317
+ apiKeyEnv: z.string().optional(),
318
+ apiKey: z.string().optional(),
319
+ headers: z.record(z.string()).optional()
320
+ });
321
+ var ModelConfigSchema = z.object({
322
+ name: z.string(),
323
+ provider: z.string(),
324
+ model: z.string(),
325
+ maxTokens: z.number().int().positive().optional(),
326
+ // Per-model context window; overrides the top-level contextMaxTokens for
327
+ // compaction and the status bar when set.
328
+ contextMaxTokens: z.number().int().positive().optional(),
329
+ // Cost estimation needs BOTH prices (USD per 1M tokens); with only one set
330
+ // the model is treated as unpriced.
331
+ promptPrice: z.number().nonnegative().optional(),
332
+ completionPrice: z.number().nonnegative().optional()
333
+ });
334
+ var PermissionsConfigSchema = z.object({
335
+ allow: z.array(z.string()).default([])
336
+ });
337
+ var HookConfigSchema = z.object({
338
+ event: z.enum(["PreToolUse", "PostToolUse", "Stop"]),
339
+ matcher: z.string().refine(
340
+ (pattern) => {
341
+ try {
342
+ new RegExp(pattern);
343
+ return true;
344
+ } catch {
345
+ return false;
346
+ }
347
+ },
348
+ { message: "invalid regular expression" }
349
+ ).optional(),
350
+ command: z.string().min(1),
351
+ timeoutSec: z.number().positive().default(30)
352
+ });
353
+ var ConfigSchema = z.object({
354
+ defaultModel: z.string().default(""),
355
+ permissionMode: z.enum(["auto", "ask", "readonly", "yolo", "plan"]).default("ask"),
356
+ providers: z.array(ProviderConfigSchema).default([]),
357
+ models: z.array(ModelConfigSchema).default([]),
358
+ maxSteps: z.number().int().positive().default(50),
359
+ contextMaxTokens: z.number().int().positive().default(1e5),
360
+ // Seconds without any stream part before a stalled response is ended
361
+ // gracefully (some relays never send the terminal chunks). Applies once
362
+ // streaming has started; the first part gets a longer, fixed allowance.
363
+ streamIdleTimeoutSec: z.number().positive().default(20),
364
+ contextCompaction: z.enum(["summary", "truncate"]).default("summary"),
365
+ // Optional per-session dollar budget; when set, the REPL stops the session
366
+ // once the estimated cost for the session reaches this amount.
367
+ sessionBudgetUsd: z.number().positive().optional(),
368
+ // Terminal bell (REPL only): ring when a turn takes longer than
369
+ // notifyBellThresholdSec, and when a background task finishes.
370
+ // STAR_NO_NOTIFY=1 disables without touching the config.
371
+ notifyBell: z.boolean().default(true),
372
+ notifyBellThresholdSec: z.number().positive().default(10),
373
+ permissions: PermissionsConfigSchema.default({ allow: [] }),
374
+ hooks: z.array(HookConfigSchema).default([])
375
+ });
376
+ function contextWindowTokens(config, modelName) {
377
+ return config.models.find((m) => m.name === modelName)?.contextMaxTokens ?? config.contextMaxTokens;
378
+ }
379
+
324
380
  // src/llm/provider.ts
325
381
  import { createAnthropic } from "@ai-sdk/anthropic";
326
382
  import { createOpenAI } from "@ai-sdk/openai";
@@ -385,15 +441,15 @@ function createModel(config, modelName) {
385
441
 
386
442
  // src/session/checkpoints.ts
387
443
  import fs2 from "fs/promises";
388
- import path4 from "path";
444
+ import path3 from "path";
389
445
  function checkpointsDir(sessionDir) {
390
- return path4.join(sessionDir, "checkpoints");
446
+ return path3.join(sessionDir, "checkpoints");
391
447
  }
392
448
  function indexPath(sessionDir) {
393
- return path4.join(checkpointsDir(sessionDir), "index.json");
449
+ return path3.join(checkpointsDir(sessionDir), "index.json");
394
450
  }
395
451
  function checkpointContentPath(sessionDir, id) {
396
- return path4.join(checkpointsDir(sessionDir), `${id}.snapshot`);
452
+ return path3.join(checkpointsDir(sessionDir), `${id}.snapshot`);
397
453
  }
398
454
  async function listCheckpointRecords(sessionDir) {
399
455
  let raw;
@@ -441,11 +497,11 @@ async function loadSessionSnapshots(sessionDir) {
441
497
 
442
498
  // src/session/clear.ts
443
499
  import fs4 from "fs/promises";
444
- import path6 from "path";
500
+ import path5 from "path";
445
501
 
446
502
  // src/session/store.ts
447
503
  import fs3 from "fs/promises";
448
- import path5 from "path";
504
+ import path4 from "path";
449
505
  function generateId(now) {
450
506
  const pad = (n) => String(n).padStart(2, "0");
451
507
  const stamp = `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`;
@@ -467,13 +523,13 @@ var SessionStore = class _SessionStore {
467
523
  initialized = false;
468
524
  constructor(id) {
469
525
  this.id = id;
470
- this.dir = path5.join(sessionsDir(), id);
526
+ this.dir = path4.join(sessionsDir(), id);
471
527
  }
472
528
  metaPath() {
473
- return path5.join(this.dir, "meta.json");
529
+ return path4.join(this.dir, "meta.json");
474
530
  }
475
531
  messagesPath() {
476
- return path5.join(this.dir, "messages.jsonl");
532
+ return path4.join(this.dir, "messages.jsonl");
477
533
  }
478
534
  static async create(cwd, model) {
479
535
  const store = new _SessionStore(generateId(/* @__PURE__ */ new Date()));
@@ -507,7 +563,7 @@ var SessionStore = class _SessionStore {
507
563
  const metas = [];
508
564
  for (const entry of entries) {
509
565
  try {
510
- const raw = await fs3.readFile(path5.join(sessionsDir(), entry, "meta.json"), "utf8");
566
+ const raw = await fs3.readFile(path4.join(sessionsDir(), entry, "meta.json"), "utf8");
511
567
  const meta = JSON.parse(raw);
512
568
  if (meta.id === entry && (cwd === void 0 || meta.cwd === cwd)) metas.push(meta);
513
569
  } catch {
@@ -634,7 +690,7 @@ async function clearSessions(cwd, excludeId) {
634
690
  let removed2 = 0;
635
691
  for (const meta of metas) {
636
692
  if (meta.id === excludeId) continue;
637
- await fs4.rm(path6.join(sessionsDir(), meta.id), { recursive: true, force: true });
693
+ await fs4.rm(path5.join(sessionsDir(), meta.id), { recursive: true, force: true });
638
694
  removed2 += 1;
639
695
  }
640
696
  return removed2;
@@ -648,7 +704,7 @@ async function clearSessions(cwd, excludeId) {
648
704
  let removed = 0;
649
705
  for (const entry of entries) {
650
706
  if (entry === excludeId) continue;
651
- await fs4.rm(path6.join(sessionsDir(), entry), { recursive: true, force: true });
707
+ await fs4.rm(path5.join(sessionsDir(), entry), { recursive: true, force: true });
652
708
  removed += 1;
653
709
  }
654
710
  return removed;
@@ -656,11 +712,11 @@ async function clearSessions(cwd, excludeId) {
656
712
 
657
713
  // src/session/list.ts
658
714
  import fs5 from "fs/promises";
659
- import path7 from "path";
715
+ import path6 from "path";
660
716
  async function countMessages(id) {
661
717
  let raw;
662
718
  try {
663
- raw = await fs5.readFile(path7.join(sessionsDir(), id, "messages.jsonl"), "utf8");
719
+ raw = await fs5.readFile(path6.join(sessionsDir(), id, "messages.jsonl"), "utf8");
664
720
  } catch {
665
721
  return 0;
666
722
  }
@@ -744,10 +800,19 @@ function computeCostUsd(usage, model) {
744
800
  }
745
801
  function estimateCost(usage, modelName, model) {
746
802
  const cost = computeCostUsd(usage, model);
747
- if (cost === null || model === void 0) {
803
+ if (cost === null) {
804
+ if (model !== void 0) {
805
+ const missing = [
806
+ model.promptPrice === void 0 ? "promptPrice" : null,
807
+ model.completionPrice === void 0 ? "completionPrice" : null
808
+ ].filter((f) => f !== null);
809
+ if (missing.length === 1) {
810
+ return `Estimated cost: unknown (${model.name} is missing ${missing[0]} \u2014 both prices are needed)`;
811
+ }
812
+ }
748
813
  return `Estimated cost: unknown (no price configured for ${modelName})`;
749
814
  }
750
- return `Estimated cost: $${formatDollars(cost)} (${model.name} @ $${model.promptPrice}/M prompt, $${model.completionPrice}/M completion)`;
815
+ return `Estimated cost: $${formatDollars(cost)} (${model?.name} @ $${model?.promptPrice}/M prompt, $${model?.completionPrice}/M completion)`;
751
816
  }
752
817
 
753
818
  // src/session/usage.ts
@@ -909,11 +974,11 @@ function formatUsageDashboard(stats, models, options = {}) {
909
974
 
910
975
  // src/version.ts
911
976
  import fs6 from "fs";
912
- import path8 from "path";
977
+ import path7 from "path";
913
978
  import { fileURLToPath } from "url";
914
979
  function readPackageVersion() {
915
980
  try {
916
- const pkgPath = path8.join(path8.dirname(fileURLToPath(import.meta.url)), "..", "package.json");
981
+ const pkgPath = path7.join(path7.dirname(fileURLToPath(import.meta.url)), "..", "package.json");
917
982
  const pkg = JSON.parse(fs6.readFileSync(pkgPath, "utf8"));
918
983
  if (typeof pkg === "object" && pkg !== null) {
919
984
  const version = pkg.version;
@@ -938,8 +1003,8 @@ function budgetState(costUsd, budgetUsd) {
938
1003
  import { execFile } from "child_process";
939
1004
  import { randomUUID } from "crypto";
940
1005
  import { readFile as readFile2, unlink } from "fs/promises";
941
- import os2 from "os";
942
- import path9 from "path";
1006
+ import os from "os";
1007
+ import path8 from "path";
943
1008
  import { promisify } from "util";
944
1009
  var CLIPBOARD_TIMEOUT_MS = 5e3;
945
1010
  var MAX_CLIPBOARD_IMAGE_BYTES = 10 * 1024 * 1024;
@@ -1013,7 +1078,7 @@ async function commandExists(command) {
1013
1078
  }
1014
1079
  }
1015
1080
  async function readClipboardImage() {
1016
- const tempFile = path9.join(os2.tmpdir(), `star-clipboard-${randomUUID()}.png`);
1081
+ const tempFile = path8.join(os.tmpdir(), `star-clipboard-${randomUUID()}.png`);
1017
1082
  const hasPngpaste = process.platform === "darwin" ? await commandExists("pngpaste") : false;
1018
1083
  const plan = clipboardImagePlan(process.platform, tempFile, hasPngpaste);
1019
1084
  if (!plan) return null;
@@ -1059,7 +1124,7 @@ async function copyText(text) {
1059
1124
 
1060
1125
  // src/cli/commands/actions.ts
1061
1126
  import fs7 from "fs/promises";
1062
- import path10 from "path";
1127
+ import path9 from "path";
1063
1128
  var MIN_COMPACT_MESSAGES = 4;
1064
1129
  async function compactSession(opts) {
1065
1130
  const { backend, sessionStore, config, model } = opts;
@@ -1120,9 +1185,9 @@ async function exportSession(opts) {
1120
1185
  return "Nothing to export: the session has no messages.";
1121
1186
  }
1122
1187
  const sessionId = sessionStore?.id ?? `unknown-${Date.now()}`;
1123
- const target = arg ? path10.resolve(cwd, arg) : path10.resolve(cwd, `star-session-${sessionId}.md`);
1188
+ const target = arg ? path9.resolve(cwd, arg) : path9.resolve(cwd, `star-session-${sessionId}.md`);
1124
1189
  const existed = await fileExists(target);
1125
- await fs7.mkdir(path10.dirname(target), { recursive: true });
1190
+ await fs7.mkdir(path9.dirname(target), { recursive: true });
1126
1191
  await fs7.writeFile(target, renderSessionMarkdown(sessionId, messages));
1127
1192
  return existed ? `Exported ${messages.length} messages to ${target} (overwrote existing file).` : `Exported ${messages.length} messages to ${target}.`;
1128
1193
  }
@@ -1190,7 +1255,7 @@ function stringifyPart(value) {
1190
1255
 
1191
1256
  // src/cli/diff-preview.ts
1192
1257
  import { readFile as readFile3 } from "fs/promises";
1193
- import path11 from "path";
1258
+ import path10 from "path";
1194
1259
  var CONTEXT_LINES = 2;
1195
1260
  var MAX_HUNK_LINES = 15;
1196
1261
  var MAX_TOTAL_LINES = 12;
@@ -1351,7 +1416,7 @@ async function generateDiffPreview(toolName, args, cwd) {
1351
1416
  if (toolName !== "edit_file" && toolName !== "write_file") return null;
1352
1417
  const filePath = getStringArg(args, "path");
1353
1418
  if (filePath === void 0) return null;
1354
- const absolute = path11.resolve(cwd, filePath);
1419
+ const absolute = path10.resolve(cwd, filePath);
1355
1420
  let existing;
1356
1421
  try {
1357
1422
  existing = await readFile3(absolute, "utf8");
@@ -1574,6 +1639,23 @@ ${lines.join("\n")}`);
1574
1639
  ctx.addSystemMessage(await ctx.runDoctor());
1575
1640
  }
1576
1641
  });
1642
+ registry.register({
1643
+ name: "skills",
1644
+ description: "List available skills (project scope overrides user scope)",
1645
+ usage: "/skills",
1646
+ run(_args, ctx) {
1647
+ const skills = discoverSkills(ctx.cwd ?? process.cwd());
1648
+ if (skills.length === 0) {
1649
+ ctx.addSystemMessage(
1650
+ "No skills found. Add a SKILL.md under .star/skills/<name>/ (project) or ~/.star-cli/skills/<name>/ (user)."
1651
+ );
1652
+ return;
1653
+ }
1654
+ const lines = skills.map((s) => `${s.name} [${s.scope}] \u2014 ${s.description}`);
1655
+ ctx.addSystemMessage(`Available skills:
1656
+ ${lines.join("\n")}`);
1657
+ }
1658
+ });
1577
1659
  registry.register({
1578
1660
  name: "commit",
1579
1661
  description: "Analyze uncommitted changes and create a git commit (Conventional Commits)",
@@ -1664,7 +1746,7 @@ ${result.diff}${note ? `
1664
1746
 
1665
1747
  // src/cli/commands/custom.ts
1666
1748
  import { readFileSync, readdirSync } from "fs";
1667
- import path12 from "path";
1749
+ import path11 from "path";
1668
1750
  var NAME_PATTERN = /^[a-z0-9-]+$/;
1669
1751
  var DESCRIPTION_PATTERN = /^<!--\s*description:\s*(.+?)\s*-->\s*$/;
1670
1752
  var DEFAULT_DESCRIPTION = "Custom prompt command";
@@ -1699,14 +1781,14 @@ function scanDir(dir, into) {
1699
1781
  if (!entry.endsWith(".md")) continue;
1700
1782
  const name = entry.slice(0, -".md".length);
1701
1783
  if (!NAME_PATTERN.test(name)) continue;
1702
- const parsed = parseCommandFile(path12.join(dir, entry), name);
1784
+ const parsed = parseCommandFile(path11.join(dir, entry), name);
1703
1785
  if (parsed) into.set(name, parsed);
1704
1786
  }
1705
1787
  }
1706
1788
  function loadCustomCommands(cwd, home = starHome()) {
1707
1789
  const byName = /* @__PURE__ */ new Map();
1708
- scanDir(path12.join(home, "commands"), byName);
1709
- scanDir(path12.join(cwd, ".star", "commands"), byName);
1790
+ scanDir(path11.join(home, "commands"), byName);
1791
+ scanDir(path11.join(cwd, ".star", "commands"), byName);
1710
1792
  return [...byName.values()];
1711
1793
  }
1712
1794
  function registerCustomCommands(registry, cwd, home) {
@@ -1732,7 +1814,7 @@ function registerCustomCommands(registry, cwd, home) {
1732
1814
 
1733
1815
  // src/cli/commands/doctor.ts
1734
1816
  import fs8 from "fs/promises";
1735
- import path13 from "path";
1817
+ import path12 from "path";
1736
1818
  var MIN_NODE_MAJOR = 20;
1737
1819
  var REGISTRY_URL = "https://registry.npmjs.org/";
1738
1820
  var REGISTRY_TIMEOUT_MS = 5e3;
@@ -1784,7 +1866,7 @@ async function runDoctor(opts) {
1784
1866
  detail: `${spec.label} (${spec.shell})`
1785
1867
  });
1786
1868
  }
1787
- const configPath = path13.join(home, "config.toml");
1869
+ const configPath = path12.join(home, "config.toml");
1788
1870
  const configExists = await pathExists(configPath);
1789
1871
  checks.push({
1790
1872
  name: "STAR_HOME / config",
@@ -1821,12 +1903,12 @@ async function runDoctor(opts) {
1821
1903
  });
1822
1904
  }
1823
1905
  }
1824
- const sessions = path13.join(home, "sessions");
1906
+ const sessions = path12.join(home, "sessions");
1825
1907
  let writable = false;
1826
1908
  let writeError = "";
1827
1909
  try {
1828
1910
  await fs8.mkdir(sessions, { recursive: true });
1829
- const probe = path13.join(sessions, `.doctor-probe-${process.pid}`);
1911
+ const probe = path12.join(sessions, `.doctor-probe-${process.pid}`);
1830
1912
  await fs8.writeFile(probe, "ok");
1831
1913
  await fs8.unlink(probe);
1832
1914
  writable = true;
@@ -1864,7 +1946,7 @@ function formatDoctorReport(report) {
1864
1946
 
1865
1947
  // src/cli/commands/init-project.ts
1866
1948
  import fs9 from "fs/promises";
1867
- import path14 from "path";
1949
+ import path13 from "path";
1868
1950
  import { generateText } from "ai";
1869
1951
  var IGNORED_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", "out", "coverage"]);
1870
1952
  var MAX_ENTRIES = 20;
@@ -1879,7 +1961,7 @@ async function fileExists2(target) {
1879
1961
  }
1880
1962
  async function readPackageJson(cwd) {
1881
1963
  try {
1882
- const raw = await fs9.readFile(path14.join(cwd, "package.json"), "utf8");
1964
+ const raw = await fs9.readFile(path13.join(cwd, "package.json"), "utf8");
1883
1965
  const parsed = JSON.parse(raw);
1884
1966
  if (typeof parsed !== "object" || parsed === null) return null;
1885
1967
  return parsed;
@@ -1896,10 +1978,10 @@ function stringRecord(value) {
1896
1978
  return out;
1897
1979
  }
1898
1980
  async function detectPackageManager(cwd) {
1899
- if (await fileExists2(path14.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
1900
- if (await fileExists2(path14.join(cwd, "yarn.lock"))) return "yarn";
1901
- if (await fileExists2(path14.join(cwd, "package-lock.json"))) return "npm";
1902
- if (await fileExists2(path14.join(cwd, "bun.lockb"))) return "bun";
1981
+ if (await fileExists2(path13.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
1982
+ if (await fileExists2(path13.join(cwd, "yarn.lock"))) return "yarn";
1983
+ if (await fileExists2(path13.join(cwd, "package-lock.json"))) return "npm";
1984
+ if (await fileExists2(path13.join(cwd, "bun.lockb"))) return "bun";
1903
1985
  return null;
1904
1986
  }
1905
1987
  function inferCommands(pm, scripts) {
@@ -1933,7 +2015,7 @@ async function listEntries(cwd) {
1933
2015
  async function readReadmeHead(cwd) {
1934
2016
  for (const name of ["README.md", "readme.md", "README"]) {
1935
2017
  try {
1936
- const raw = await fs9.readFile(path14.join(cwd, name), "utf8");
2018
+ const raw = await fs9.readFile(path13.join(cwd, name), "utf8");
1937
2019
  return raw.split("\n").map((line) => line.trim()).filter((line) => line.length > 0).slice(0, README_HEAD_LINES);
1938
2020
  } catch {
1939
2021
  }
@@ -1946,14 +2028,14 @@ async function scanProject(cwd) {
1946
2028
  const scripts = stringRecord(pkg?.scripts);
1947
2029
  return {
1948
2030
  name: typeof pkg?.name === "string" ? pkg.name : null,
1949
- dirName: path14.basename(cwd),
2031
+ dirName: path13.basename(cwd),
1950
2032
  packageManager,
1951
2033
  scripts,
1952
2034
  dependencies: Object.keys(stringRecord(pkg?.dependencies)),
1953
2035
  devDependencies: Object.keys(stringRecord(pkg?.devDependencies)),
1954
2036
  commands: inferCommands(packageManager, scripts),
1955
- hasTsconfig: await fileExists2(path14.join(cwd, "tsconfig.json")),
1956
- isGitRepo: await fileExists2(path14.join(cwd, ".git")),
2037
+ hasTsconfig: await fileExists2(path13.join(cwd, "tsconfig.json")),
2038
+ isGitRepo: await fileExists2(path13.join(cwd, ".git")),
1957
2039
  readmeHead: await readReadmeHead(cwd),
1958
2040
  entries: await listEntries(cwd)
1959
2041
  };
@@ -2020,7 +2102,7 @@ Write the AGENTS.md.`
2020
2102
  }
2021
2103
  async function initProject(opts) {
2022
2104
  const { cwd, args, model } = opts;
2023
- const target = path14.join(cwd, "AGENTS.md");
2105
+ const target = path13.join(cwd, "AGENTS.md");
2024
2106
  const force = args.split(/\s+/).includes("force");
2025
2107
  if (!force && await fileExists2(target)) {
2026
2108
  return {
@@ -2096,7 +2178,7 @@ function parseSlashCommand(input) {
2096
2178
  }
2097
2179
 
2098
2180
  // src/cli/commands/rewind.ts
2099
- import path15 from "path";
2181
+ import path14 from "path";
2100
2182
  function planRewind(snapshots, id) {
2101
2183
  const target = snapshots.find((snapshot) => snapshot.id === id);
2102
2184
  if (!target) return null;
@@ -2110,7 +2192,7 @@ function planRewind(snapshots, id) {
2110
2192
  return { target, affected, messageIndex };
2111
2193
  }
2112
2194
  function displayPath(filePath, cwd) {
2113
- const relative = path15.relative(cwd, filePath);
2195
+ const relative = path14.relative(cwd, filePath);
2114
2196
  return relative !== "" && !relative.startsWith("..") ? relative : filePath;
2115
2197
  }
2116
2198
  function formatTime(timestamp) {
@@ -2164,7 +2246,7 @@ function filterCommands(input, commands) {
2164
2246
 
2165
2247
  // src/cli/path-suggest.ts
2166
2248
  import { readdir } from "fs/promises";
2167
- import path16 from "path";
2249
+ import path15 from "path";
2168
2250
  var MAX_SCAN_ENTRIES = 2e3;
2169
2251
  function extractAtToken(value, cursor) {
2170
2252
  const before = value.slice(0, cursor);
@@ -2179,7 +2261,7 @@ async function suggestPaths(token, cwd, maxScanEntries = MAX_SCAN_ENTRIES, maxSu
2179
2261
  const dirPart = slash >= 0 ? normalized.slice(0, slash + 1) : "";
2180
2262
  const basePrefix = slash >= 0 ? normalized.slice(slash + 1) : normalized;
2181
2263
  if (dirPart.split("/").some((segment) => SKIP_DIRS.has(segment))) return [];
2182
- const dirAbs = path16.resolve(cwd, dirPart === "" ? "." : dirPart);
2264
+ const dirAbs = path15.resolve(cwd, dirPart === "" ? "." : dirPart);
2183
2265
  const entries = await readdir(dirAbs, { withFileTypes: true }).catch(() => null);
2184
2266
  if (!entries) return [];
2185
2267
  const lower = basePrefix.toLowerCase();
@@ -3060,7 +3142,8 @@ function Repl({
3060
3142
  registry: createDefaultRegistry(),
3061
3143
  config,
3062
3144
  cwd,
3063
- sessionStore: sessionStoreRef.current
3145
+ sessionStore: sessionStoreRef.current,
3146
+ contextMaxTokens: contextWindowTokens(config, name)
3064
3147
  });
3065
3148
  const prev = backendRef.current;
3066
3149
  if (prev instanceof AgentLoop) {
@@ -3668,9 +3751,9 @@ ${plan.affected.length} file change(s) will be reverted, ${messageCount} message
3668
3751
  setGitBranch(getGitSummary(cwd)?.branch ?? null);
3669
3752
  const current = backendRef.current;
3670
3753
  if (current instanceof AgentLoop) {
3671
- setContextPercent(
3672
- Math.round(estimateTokens([...current.getMessages()]) / config.contextMaxTokens * 100)
3673
- );
3754
+ const window_ = contextWindowTokens(config, modelNameRef.current);
3755
+ const pct = estimateTokens([...current.getMessages()]) / window_ * 100;
3756
+ setContextPercent(pct < 10 ? Math.round(pct * 10) / 10 : Math.round(pct));
3674
3757
  } else {
3675
3758
  setContextPercent(null);
3676
3759
  }
@@ -3760,69 +3843,6 @@ function renderRepl(backend, opts) {
3760
3843
  // src/config/loader.ts
3761
3844
  import fs10 from "fs";
3762
3845
  import { parse as parse2 } from "smol-toml";
3763
-
3764
- // src/config/schema.ts
3765
- import { z } from "zod";
3766
- var ProviderConfigSchema = z.object({
3767
- name: z.string(),
3768
- protocol: z.enum(["openai-compatible", "anthropic", "openai-responses"]).default("openai-compatible"),
3769
- baseURL: z.string(),
3770
- apiKeyEnv: z.string().optional(),
3771
- apiKey: z.string().optional(),
3772
- headers: z.record(z.string()).optional()
3773
- });
3774
- var ModelConfigSchema = z.object({
3775
- name: z.string(),
3776
- provider: z.string(),
3777
- model: z.string(),
3778
- maxTokens: z.number().int().positive().optional(),
3779
- promptPrice: z.number().nonnegative().optional(),
3780
- completionPrice: z.number().nonnegative().optional()
3781
- });
3782
- var PermissionsConfigSchema = z.object({
3783
- allow: z.array(z.string()).default([])
3784
- });
3785
- var HookConfigSchema = z.object({
3786
- event: z.enum(["PreToolUse", "PostToolUse", "Stop"]),
3787
- matcher: z.string().refine(
3788
- (pattern) => {
3789
- try {
3790
- new RegExp(pattern);
3791
- return true;
3792
- } catch {
3793
- return false;
3794
- }
3795
- },
3796
- { message: "invalid regular expression" }
3797
- ).optional(),
3798
- command: z.string().min(1),
3799
- timeoutSec: z.number().positive().default(30)
3800
- });
3801
- var ConfigSchema = z.object({
3802
- defaultModel: z.string().default(""),
3803
- permissionMode: z.enum(["auto", "ask", "readonly", "yolo", "plan"]).default("ask"),
3804
- providers: z.array(ProviderConfigSchema).default([]),
3805
- models: z.array(ModelConfigSchema).default([]),
3806
- maxSteps: z.number().int().positive().default(50),
3807
- contextMaxTokens: z.number().int().positive().default(1e5),
3808
- // Seconds without any stream part before a stalled response is ended
3809
- // gracefully (some relays never send the terminal chunks). Applies once
3810
- // streaming has started; the first part gets a longer, fixed allowance.
3811
- streamIdleTimeoutSec: z.number().positive().default(20),
3812
- contextCompaction: z.enum(["summary", "truncate"]).default("summary"),
3813
- // Optional per-session dollar budget; when set, the REPL stops the session
3814
- // once the estimated cost for the session reaches this amount.
3815
- sessionBudgetUsd: z.number().positive().optional(),
3816
- // Terminal bell (REPL only): ring when a turn takes longer than
3817
- // notifyBellThresholdSec, and when a background task finishes.
3818
- // STAR_NO_NOTIFY=1 disables without touching the config.
3819
- notifyBell: z.boolean().default(true),
3820
- notifyBellThresholdSec: z.number().positive().default(10),
3821
- permissions: PermissionsConfigSchema.default({ allow: [] }),
3822
- hooks: z.array(HookConfigSchema).default([])
3823
- });
3824
-
3825
- // src/config/loader.ts
3826
3846
  var PartialConfigSchema = ConfigSchema.partial();
3827
3847
  function parseTomlFile(content, filePath) {
3828
3848
  let raw;
@@ -3874,7 +3894,8 @@ async function createLoop(config, modelName, cwd, sessionStore) {
3874
3894
  config,
3875
3895
  cwd,
3876
3896
  system: SYSTEM_PROMPT,
3877
- sessionStore
3897
+ sessionStore,
3898
+ contextMaxTokens: contextWindowTokens(config, modelName)
3878
3899
  });
3879
3900
  }
3880
3901
  async function printMode(loop, prompt, cwd, json, imagePaths) {