@cryer/star-cli 0.3.8 → 0.3.9

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
@@ -116,6 +116,11 @@ model = "gpt-4o" # model id sent to the API
116
116
  # provider (low/medium/high are common; some add minimal/max...).
117
117
  # Unset = server default. Ignored for anthropic. The /model
118
118
  # picker also offers these levels after a model pick.
119
+ # vision = false # mark a text-only model: read_image/screenshot then decline with
120
+ # a text error instead of attaching images the endpoint would
121
+ # reject. Unset = images allowed. (Without it, a 4xx rejecting
122
+ # images outright still self-heals: the images are stripped from
123
+ # the history and the request retried once.)
119
124
  # streamIdleTimeoutSec = 60 # per-model overrides of the global stream watchdog timeouts
120
125
  # streamFirstChunkTimeoutSec = 600
121
126
  # optional per-model pricing in USD per 1M tokens — enables the $ estimate in /cost and /usage.
package/README.zh-CN.md CHANGED
@@ -114,6 +114,10 @@ model = "gpt-4o" # 发送给 API 的模型 id
114
114
  # minimal/max 等),因此接受任意字符串,由服务端校验。
115
115
  # 不设置 = 服务端默认。anthropic 协议下忽略。
116
116
  # /model 选完模型后也会接着让你选择推理强度。
117
+ # vision = false # 标记纯语言模型:read_image/screenshot 会直接返回文本错误,
118
+ # 不再把图片塞进历史导致端点报 4xx。不设置 = 允许图片。(即使
119
+ # 不设置,端点明确拒绝图片的 4xx 也会自愈:剥掉历史中的图片后
120
+ # 自动重试一次。)
117
121
  # streamIdleTimeoutSec = 60 # 按模型覆盖全局的流看门狗超时
118
122
  # streamFirstChunkTimeoutSec = 600
119
123
  # 可选的按模型定价(美元/1M token)——启用 /cost 和 /usage 中的美元估算。
@@ -0,0 +1,34 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/llm/registry.ts
4
+ function listModels(config) {
5
+ return config.models;
6
+ }
7
+ function resolveModelConfig(config, modelName) {
8
+ const name = modelName ?? config.defaultModel;
9
+ const model = config.models.find((m) => m.name === name);
10
+ if (!model) {
11
+ const available = config.models.map((m) => m.name).join(", ") || "(none)";
12
+ throw new Error(`Model "${name}" not found. Available models: ${available}`);
13
+ }
14
+ return model;
15
+ }
16
+ function resolveStartupModel(config, sessionModel) {
17
+ if (sessionModel) {
18
+ if (config.models.some((m) => m.name === sessionModel)) {
19
+ return { name: sessionModel, notice: null };
20
+ }
21
+ return {
22
+ name: config.defaultModel,
23
+ notice: `Session model "${sessionModel}" not found in config \u2014 using default model.`
24
+ };
25
+ }
26
+ return { name: config.defaultModel, notice: null };
27
+ }
28
+
29
+ export {
30
+ listModels,
31
+ resolveModelConfig,
32
+ resolveStartupModel
33
+ };
34
+ //# sourceMappingURL=chunk-4IRY44TG.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/llm/registry.ts"],"sourcesContent":["import type { ModelConfig, StarConfig } from \"../config/schema\";\n\nexport function listModels(config: StarConfig): ModelConfig[] {\n return config.models;\n}\n\nexport function resolveModelConfig(config: StarConfig, modelName?: string): ModelConfig {\n const name = modelName ?? config.defaultModel;\n const model = config.models.find((m) => m.name === name);\n if (!model) {\n const available = config.models.map((m) => m.name).join(\", \") || \"(none)\";\n throw new Error(`Model \"${name}\" not found. Available models: ${available}`);\n }\n return model;\n}\n\n// Pick the model at startup. An explicit --model already sits in\n// config.defaultModel (loader override), so it wins naturally; otherwise a\n// resumed session's recorded model is restored when it still exists in the\n// config. A stale recorded model falls back to the default with a notice.\nexport function resolveStartupModel(\n config: StarConfig,\n sessionModel?: string | null,\n): { name: string | undefined; notice: string | null } {\n if (sessionModel) {\n if (config.models.some((m) => m.name === sessionModel)) {\n return { name: sessionModel, notice: null };\n }\n return {\n name: config.defaultModel,\n notice: `Session model \"${sessionModel}\" not found in config — using default model.`,\n };\n }\n return { name: config.defaultModel, notice: null };\n}\n"],"mappings":";;;AAEO,SAAS,WAAW,QAAmC;AAC5D,SAAO,OAAO;AAChB;AAEO,SAAS,mBAAmB,QAAoB,WAAiC;AACtF,QAAM,OAAO,aAAa,OAAO;AACjC,QAAM,QAAQ,OAAO,OAAO,KAAK,CAAC,MAAM,EAAE,SAAS,IAAI;AACvD,MAAI,CAAC,OAAO;AACV,UAAM,YAAY,OAAO,OAAO,IAAI,CAAC,MAAM,EAAE,IAAI,EAAE,KAAK,IAAI,KAAK;AACjE,UAAM,IAAI,MAAM,UAAU,IAAI,kCAAkC,SAAS,EAAE;AAAA,EAC7E;AACA,SAAO;AACT;AAMO,SAAS,oBACd,QACA,cACqD;AACrD,MAAI,cAAc;AAChB,QAAI,OAAO,OAAO,KAAK,CAAC,MAAM,EAAE,SAAS,YAAY,GAAG;AACtD,aAAO,EAAE,MAAM,cAAc,QAAQ,KAAK;AAAA,IAC5C;AACA,WAAO;AAAA,MACL,MAAM,OAAO;AAAA,MACb,QAAQ,kBAAkB,YAAY;AAAA,IACxC;AAAA,EACF;AACA,SAAO,EAAE,MAAM,OAAO,cAAc,QAAQ,KAAK;AACnD;","names":[]}
@@ -0,0 +1,493 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ gitTreesDir,
4
+ isSensitivePath
5
+ } from "./chunk-A5PXPWHB.js";
6
+
7
+ // src/core/messages.ts
8
+ function toStarMessage(message) {
9
+ return "role" in message ? { message } : message;
10
+ }
11
+ function toCoreMessages(messages) {
12
+ return messages.map((star) => star.message);
13
+ }
14
+ function isSyntheticUserMessage(star) {
15
+ return star.message.role === "user" && star.meta?.synthetic !== void 0;
16
+ }
17
+ function coreMessageText(message) {
18
+ if (!message) return "";
19
+ const content = message.content;
20
+ if (typeof content === "string") return content;
21
+ if (Array.isArray(content)) {
22
+ return content.filter((part) => part.type === "text").map((part) => "text" in part ? part.text : "").join(" ");
23
+ }
24
+ return "";
25
+ }
26
+ var MISSING_TOOL_RESULT_TEXT = "tool result missing (session was interrupted)";
27
+ var INTERRUPTED_TOOL_RESULT_TEXT = "Tool execution interrupted by user.";
28
+ var UNFINISHED_TOOL_RESULT_TEXT = "Tool execution interrupted before a result was produced.";
29
+ function reconcileStarMessages(messages) {
30
+ const result = [];
31
+ let pending = [];
32
+ const flushPending = () => {
33
+ for (const call of pending) {
34
+ result.push({
35
+ message: {
36
+ role: "tool",
37
+ content: [
38
+ {
39
+ type: "tool-result",
40
+ toolCallId: call.toolCallId,
41
+ toolName: call.toolName,
42
+ result: MISSING_TOOL_RESULT_TEXT
43
+ }
44
+ ]
45
+ }
46
+ });
47
+ }
48
+ pending = [];
49
+ };
50
+ for (const star of messages) {
51
+ const message = star.message;
52
+ if (message.role === "assistant" && Array.isArray(message.content)) {
53
+ flushPending();
54
+ pending = message.content.filter((part) => part.type === "tool-call").map((part) => ({ toolCallId: part.toolCallId, toolName: part.toolName }));
55
+ } else if (message.role === "tool" && Array.isArray(message.content) && pending.length > 0) {
56
+ const answered = new Set(message.content.map((part) => part.toolCallId));
57
+ pending = pending.filter((call) => !answered.has(call.toolCallId));
58
+ } else {
59
+ flushPending();
60
+ }
61
+ result.push(star);
62
+ }
63
+ flushPending();
64
+ return result;
65
+ }
66
+ function retractLastTurn(messages) {
67
+ for (let i = messages.length - 1; i >= 0; i--) {
68
+ const star = messages[i];
69
+ if (star && star.message.role === "user" && !isSyntheticUserMessage(star)) {
70
+ return { messages: messages.slice(0, i), removed: messages.length - i };
71
+ }
72
+ }
73
+ return { messages, removed: 0 };
74
+ }
75
+
76
+ // src/config/schema.ts
77
+ import { z } from "zod";
78
+ var ProviderConfigSchema = z.object({
79
+ name: z.string(),
80
+ // openai-responses talks to the Responses API (/responses) instead of chat
81
+ // completions — the wire format codex uses, and the native API of
82
+ // reasoning-model relays that otherwise hide reasoning behind empty chat
83
+ // completions. streamChat sends strictSchemas:false + store:false with it.
84
+ protocol: z.enum(["openai-compatible", "anthropic", "openai-responses"]).default("openai-compatible"),
85
+ baseURL: z.string(),
86
+ apiKeyEnv: z.string().optional(),
87
+ apiKey: z.string().optional(),
88
+ headers: z.record(z.string()).optional()
89
+ });
90
+ var ModelConfigSchema = z.object({
91
+ name: z.string(),
92
+ provider: z.string(),
93
+ model: z.string(),
94
+ maxTokens: z.number().int().positive().optional(),
95
+ // Per-model context window; overrides the top-level contextMaxTokens for
96
+ // compaction and the status bar when set.
97
+ contextMaxTokens: z.number().int().positive().optional(),
98
+ // Reasoning intensity for thinking models, forwarded verbatim as
99
+ // OpenAI-style reasoning_effort on every request (llm/provider.ts
100
+ // reasoningEffortMetadata) — level naming varies by model/provider
101
+ // (low|medium|high are common; some add minimal/max/xhigh/none), so any
102
+ // non-empty string is accepted and the server validates it. Unset = the
103
+ // request carries no effort field and the server-side default applies.
104
+ // Anthropic maps effort to a thinking budget instead — ignored there.
105
+ reasoningEffort: z.string().min(1).optional(),
106
+ // Sampling temperature forwarded on every request. Unset = the AI SDK's
107
+ // own default applies (ai@4 sends 0 — it does NOT omit the field). Some
108
+ // endpoints reject anything but one explicit value (e.g. kimi-for-coding:
109
+ // "invalid temperature: only 1 is allowed") — set it here. No upper
110
+ // bound: ranges vary by provider and the server validates.
111
+ temperature: z.number().min(0).optional(),
112
+ // Per-model overrides of the top-level stream watchdog timeouts (unset =
113
+ // the global values apply). Raise streamIdleTimeoutSec for relays that
114
+ // buffer long generations and stall mid-stream between flushes.
115
+ streamIdleTimeoutSec: z.number().positive().optional(),
116
+ streamFirstChunkTimeoutSec: z.number().positive().optional(),
117
+ // Text-only models: set false so image-producing tools (read_image,
118
+ // screenshot) decline with a text error instead of attaching images the
119
+ // endpoint would reject with an unrecoverable 4xx. Unset = images allowed.
120
+ vision: z.boolean().optional(),
121
+ // Cost estimation needs BOTH prices (USD per 1M tokens); with only one set
122
+ // the model is treated as unpriced.
123
+ promptPrice: z.number().nonnegative().optional(),
124
+ completionPrice: z.number().nonnegative().optional(),
125
+ // Optional price (USD per 1M tokens) for cache-read prompt tokens. When
126
+ // unset, OpenAI-style cached tokens bill at promptPrice and Anthropic-style
127
+ // cache reads are not billed — set this for accurate cache pricing.
128
+ cacheReadPrice: z.number().nonnegative().optional()
129
+ });
130
+ var PermissionsConfigSchema = z.object({
131
+ allow: z.array(z.string()).default([]),
132
+ deny: z.array(z.string()).default([]),
133
+ // Rules that force a confirmation prompt in auto mode (same tool(glob)
134
+ // syntax as allow/deny) — e.g. ask = ["bash(git push *)"]. Priority:
135
+ // deny > ask > allow > mode default. Only meaningful in auto mode (ask
136
+ // mode already prompts; readonly/plan are unaffected; yolo bypasses all).
137
+ ask: z.array(z.string()).default([]),
138
+ // Extra likely-secret file globs extending core/sensitive.ts's hardcoded
139
+ // list (same single-* glob semantics as rules, matched case-insensitively
140
+ // against the basename and the full normalized path). Global config only —
141
+ // the project-config sandbox drops the whole [permissions] table.
142
+ sensitive: z.array(z.string()).default([])
143
+ });
144
+ var HookConfigSchema = z.object({
145
+ event: z.enum(["PreToolUse", "PostToolUse", "Stop"]),
146
+ matcher: z.string().refine(
147
+ (pattern) => {
148
+ try {
149
+ new RegExp(pattern);
150
+ return true;
151
+ } catch {
152
+ return false;
153
+ }
154
+ },
155
+ { message: "invalid regular expression" }
156
+ ).optional(),
157
+ command: z.string().min(1),
158
+ timeoutSec: z.number().positive().default(30)
159
+ });
160
+ var ConfigSchema = z.object({
161
+ defaultModel: z.string().default(""),
162
+ // Optional [[models]] name for cheap auxiliary LLM calls (compaction
163
+ // summaries, session titles, turn completion checks) — Claude Code routes
164
+ // these to a Haiku-class model. Unset = the active main model does them.
165
+ smallModel: z.string().optional(),
166
+ permissionMode: z.enum(["auto", "ask", "readonly", "yolo", "plan"]).default("ask"),
167
+ providers: z.array(ProviderConfigSchema).default([]),
168
+ models: z.array(ModelConfigSchema).default([]),
169
+ // Cap on model round-trips ("steps") per turn between progress checkpoints.
170
+ // One step is a streamed reply plus every tool call it asked for — stream
171
+ // retries and auto-continue nudges do not consume steps. Reaching the cap
172
+ // while tools still execute resets the budget with a notice; the turn stops
173
+ // only when no tool ran since the last checkpoint (the doom-loop
174
+ // signature). 0 disables the cap entirely (opencode/codex have none).
175
+ maxSteps: z.number().int().min(0).default(100),
176
+ contextMaxTokens: z.number().int().positive().default(1e5),
177
+ // Token threshold at which the agent loop auto-compacts the history.
178
+ // Defaults to the effective context window (compact only when full); set a
179
+ // lower value to compact earlier and keep headroom. Manual /compact ignores
180
+ // this — it always compacts.
181
+ compactThresholdTokens: z.number().int().positive().optional(),
182
+ // Seconds without any stream part before a stalled response is ended
183
+ // gracefully (some relays never send the terminal chunks). Applies once
184
+ // content has started streaming; the wait for the first content part gets
185
+ // a longer allowance (control/metadata parts don't count as content).
186
+ streamIdleTimeoutSec: z.number().positive().default(20),
187
+ // Seconds to wait for the very first content part before giving up; slow
188
+ // thinking-model endpoints can stay silent for minutes after accepting
189
+ // the request.
190
+ streamFirstChunkTimeoutSec: z.number().positive().default(300),
191
+ // Extra attempts per model request when a stream fails transiently
192
+ // (network error, 429/5xx, idle watchdog cutoff) or comes back empty, so a
193
+ // relay hiccup does not silently end a half-finished turn. 0 disables.
194
+ // The wait between attempts honors Retry-After headers and otherwise backs
195
+ // off exponentially with jitter (llm/retry.ts); retries after a
196
+ // timeout/empty failure also scale both stream timeouts up (attempt number
197
+ // ×, capped at 3x) since the relay is likely overloaded. Default 5 matches
198
+ // opencode/Codex retry budgets and rides out flaky relays (中转站) better
199
+ // than a minimal budget.
200
+ streamMaxRetries: z.number().int().min(0).default(5),
201
+ // How many consecutive unproductive replies may be nudged before the turn
202
+ // is handed back: a text-only reply that announces pending work ("我将…",
203
+ // "I will…") or answers a nudge with more words, or an empty reply that
204
+ // survived the stream retry budget (steered with a nudge — a changed
205
+ // request — since identical resends already failed). Tool calls reset the
206
+ // count, so a productive task keeps going; exhaustion surfaces a notice
207
+ // instead of stopping silently. Never fires in plan mode. 0 disables.
208
+ maxAutoContinues: z.number().int().min(0).default(2),
209
+ contextCompaction: z.enum(["summary", "truncate"]).default("summary"),
210
+ // Optional per-session dollar budget; when set, the REPL stops the session
211
+ // once the estimated cost for the session reaches this amount.
212
+ sessionBudgetUsd: z.number().positive().optional(),
213
+ // Terminal bell (REPL only): ring when a turn takes longer than
214
+ // notifyBellThresholdSec, and when a background task finishes.
215
+ // STAR_NO_NOTIFY=1 disables without touching the config.
216
+ notifyBell: z.boolean().default(true),
217
+ notifyBellThresholdSec: z.number().positive().default(10),
218
+ permissions: PermissionsConfigSchema.default({ allow: [], deny: [] }),
219
+ hooks: z.array(HookConfigSchema).default([]),
220
+ // Consecutive identical tool calls (same tool + same arguments) allowed
221
+ // before the loop refuses to execute the repeat and tells the model to
222
+ // change approach — a guard against agents stuck retrying a failing call
223
+ // (opencode's doom_loop). 0 disables.
224
+ doomLoopThreshold: z.number().int().min(0).default(3),
225
+ // Track the whole working tree with an internal git repo (outside the
226
+ // user's .git) at every turn boundary, so /undo and /redo can restore
227
+ // changes made by ANY tool (bash included), not just write_file/edit_file.
228
+ // Falls back silently to per-file snapshots when git is unavailable.
229
+ gitSnapshots: z.boolean().default(true),
230
+ // web_fetch SSRF guard escape hatch: by default target hosts are resolved
231
+ // before connecting and private/reserved ranges are refused (loopback,
232
+ // RFC1918, link-local incl. the cloud metadata address 169.254.169.254,
233
+ // ULA, and localhost/.local/.internal names; redirect hops re-checked).
234
+ // true disables the guard for intranet use. Global config only — not in
235
+ // the project-config sandbox whitelist.
236
+ webFetchAllowPrivateHosts: z.boolean().default(false)
237
+ });
238
+ function contextWindowTokens(config, modelName) {
239
+ return config.models.find((m) => m.name === modelName)?.contextMaxTokens ?? config.contextMaxTokens;
240
+ }
241
+ function resolveCompactThreshold(config, windowTokens) {
242
+ return Math.min(config.compactThresholdTokens ?? windowTokens, windowTokens);
243
+ }
244
+
245
+ // src/snapshot/git-tree.ts
246
+ import { execFile } from "child_process";
247
+ import { randomBytes } from "crypto";
248
+ import { existsSync } from "fs";
249
+ import { mkdir, readdir, rm, writeFile } from "fs/promises";
250
+ import os from "os";
251
+ import path from "path";
252
+ var GIT_TIMEOUT_MS = 1e4;
253
+ var GC_TIMEOUT_MS = 6e4;
254
+ var GIT_MAX_BUFFER = 8 * 1024 * 1024;
255
+ var GC_INTERVAL = 50;
256
+ var TRACK_FAILURE_TTL_MS = 5 * 60 * 1e3;
257
+ var MAX_PINNED_REFS = 50;
258
+ var PINNED_REF_PREFIX = "refs/star/";
259
+ var SENSITIVE_EXCLUDE_PATTERNS = [
260
+ ".env",
261
+ ".env.*",
262
+ "!.env.example",
263
+ "!.env.sample",
264
+ "!.env.template",
265
+ "id_rsa",
266
+ "id_dsa",
267
+ "id_ecdsa",
268
+ "id_ed25519",
269
+ ".npmrc",
270
+ ".netrc",
271
+ ".pgpass",
272
+ "*.pem",
273
+ "*.key",
274
+ "*.p12",
275
+ "*.pfx",
276
+ "**/.aws/credentials",
277
+ "**/.kube/config"
278
+ ];
279
+ var GIT_CONFIG_ARGS = [
280
+ "-c",
281
+ "core.autocrlf=false",
282
+ "-c",
283
+ "core.eol=native",
284
+ "-c",
285
+ "filter.lfs.process=",
286
+ "-c",
287
+ "filter.lfs.smudge=",
288
+ "-c",
289
+ "filter.lfs.clean="
290
+ ];
291
+ function gitEnv() {
292
+ const env = {};
293
+ for (const [key, value] of Object.entries(process.env)) {
294
+ if (value !== void 0 && !key.toUpperCase().startsWith("GIT_")) env[key] = value;
295
+ }
296
+ return env;
297
+ }
298
+ function runGit(args, timeoutMs = GIT_TIMEOUT_MS) {
299
+ return new Promise((resolve) => {
300
+ execFile(
301
+ "git",
302
+ [...GIT_CONFIG_ARGS, ...args],
303
+ { timeout: timeoutMs, maxBuffer: GIT_MAX_BUFFER, encoding: "utf8", env: gitEnv() },
304
+ (error, stdout) => {
305
+ resolve(error ? null : stdout.trim());
306
+ }
307
+ );
308
+ });
309
+ }
310
+ var gitAvailable = null;
311
+ var trackFailureUntil = /* @__PURE__ */ new Map();
312
+ var trackCounts = /* @__PURE__ */ new Map();
313
+ var scrubbedRepos = /* @__PURE__ */ new Set();
314
+ async function isGitAvailable() {
315
+ if (gitAvailable === null) {
316
+ gitAvailable = await runGit(["--version"]) !== null;
317
+ }
318
+ return gitAvailable;
319
+ }
320
+ function treeRepoDir(cwd) {
321
+ const key = Buffer.from(path.resolve(cwd), "utf8").toString("base64url");
322
+ return path.join(gitTreesDir(), key);
323
+ }
324
+ async function removeTreeRepo(cwd) {
325
+ const dir = treeRepoDir(cwd);
326
+ trackFailureUntil.delete(path.resolve(cwd));
327
+ trackCounts.delete(dir);
328
+ scrubbedRepos.delete(dir);
329
+ await rm(dir, { recursive: true, force: true }).catch(() => {
330
+ });
331
+ }
332
+ async function pruneTreeReposExcept(remainingCwds) {
333
+ let entries;
334
+ try {
335
+ entries = await readdir(gitTreesDir());
336
+ } catch {
337
+ return;
338
+ }
339
+ const keep = /* @__PURE__ */ new Set();
340
+ for (const cwd of remainingCwds) keep.add(path.basename(treeRepoDir(cwd)));
341
+ await Promise.all(
342
+ entries.filter((entry) => !keep.has(entry)).map(
343
+ (entry) => rm(path.join(gitTreesDir(), entry), { recursive: true, force: true }).catch(() => {
344
+ })
345
+ )
346
+ );
347
+ }
348
+ function isGuardedDir(cwd) {
349
+ const resolved = path.resolve(cwd);
350
+ const home = path.resolve(os.homedir());
351
+ const same = (a, b) => process.platform === "win32" ? a.toLowerCase() === b.toLowerCase() : a === b;
352
+ return same(resolved, home) || same(resolved, path.parse(resolved).root);
353
+ }
354
+ async function ensureTreeRepo(cwd) {
355
+ const dir = treeRepoDir(cwd);
356
+ try {
357
+ await mkdir(dir, { recursive: true, mode: 448 });
358
+ if (!existsSync(path.join(dir, "HEAD"))) {
359
+ const out = await runGit(["init", "--bare", dir]);
360
+ if (out === null) return null;
361
+ await runGit([`--git-dir=${dir}`, "config", "core.autocrlf", "false"]);
362
+ }
363
+ await mkdir(path.join(dir, "info"), { recursive: true });
364
+ await writeFile(
365
+ path.join(dir, "info", "exclude"),
366
+ `${SENSITIVE_EXCLUDE_PATTERNS.join("\n")}
367
+ `,
368
+ "utf8"
369
+ );
370
+ } catch {
371
+ return null;
372
+ }
373
+ return dir;
374
+ }
375
+ function repoArgs(dir, cwd) {
376
+ return [`--git-dir=${dir}`, `--work-tree=${cwd}`];
377
+ }
378
+ async function untrackExcluded(dir, cwd) {
379
+ const base = repoArgs(dir, cwd);
380
+ const listed = await runGit([...base, "ls-files", "-z", "-c", "-i", "--exclude-standard"]);
381
+ if (listed === null) return false;
382
+ const files = listed.split("\0").filter(Boolean);
383
+ if (files.length === 0) return true;
384
+ const removed = await runGit([
385
+ ...base,
386
+ "rm",
387
+ "--cached",
388
+ "--ignore-unmatch",
389
+ "-q",
390
+ "--",
391
+ ...files.map((file) => `:(literal)${file}`)
392
+ ]);
393
+ return removed !== null;
394
+ }
395
+ function trackTree(cwd) {
396
+ const key = path.resolve(cwd);
397
+ const next = (trackChains.get(key) ?? Promise.resolve(null)).then(() => trackTreeGuarded(cwd));
398
+ trackChains.set(key, next);
399
+ return next;
400
+ }
401
+ var trackChains = /* @__PURE__ */ new Map();
402
+ async function trackTreeGuarded(cwd) {
403
+ if (isGuardedDir(cwd)) return null;
404
+ const key = path.resolve(cwd);
405
+ const failedUntil = trackFailureUntil.get(key);
406
+ if (failedUntil !== void 0 && Date.now() < failedUntil) return null;
407
+ const tree = await trackTreeUncached(cwd);
408
+ if (tree === null) {
409
+ trackFailureUntil.set(key, Date.now() + TRACK_FAILURE_TTL_MS);
410
+ return null;
411
+ }
412
+ trackFailureUntil.delete(key);
413
+ return tree;
414
+ }
415
+ async function pinTree(base, tree) {
416
+ const ref = `${PINNED_REF_PREFIX}${Date.now()}-${randomBytes(4).toString("hex")}`;
417
+ if (await runGit([...base, "update-ref", ref, tree]) === null) return;
418
+ const listed = await runGit([...base, "for-each-ref", "--format=%(refname)", PINNED_REF_PREFIX]);
419
+ if (listed === null) return;
420
+ const refs = listed.split("\n").filter(Boolean);
421
+ const stale = refs.slice(0, Math.max(0, refs.length - MAX_PINNED_REFS));
422
+ await Promise.all(stale.map((name) => runGit([...base, "update-ref", "-d", name])));
423
+ }
424
+ async function trackTreeUncached(cwd) {
425
+ if (!await isGitAvailable()) return null;
426
+ const dir = await ensureTreeRepo(cwd);
427
+ if (!dir) return null;
428
+ const base = repoArgs(dir, cwd);
429
+ if (!scrubbedRepos.has(dir)) {
430
+ if (!await untrackExcluded(dir, cwd)) return null;
431
+ scrubbedRepos.add(dir);
432
+ }
433
+ if (await runGit([...base, "add", "-A"]) === null) return null;
434
+ const tree = await runGit([...base, "write-tree"]);
435
+ if (tree === null) return null;
436
+ await pinTree(base, tree);
437
+ const count = (trackCounts.get(dir) ?? 0) + 1;
438
+ trackCounts.set(dir, count);
439
+ if (count % GC_INTERVAL === 0) {
440
+ await runGit([`--git-dir=${dir}`, "gc", "--auto"], GC_TIMEOUT_MS);
441
+ }
442
+ return tree;
443
+ }
444
+ async function restoreTree(cwd, hash) {
445
+ if (isGuardedDir(cwd)) return false;
446
+ if (!await isGitAvailable()) return false;
447
+ const dir = await ensureTreeRepo(cwd);
448
+ if (!dir) return false;
449
+ const base = repoArgs(dir, cwd);
450
+ if (await runGit([...base, "read-tree", hash]) === null) return false;
451
+ if (!await untrackExcluded(dir, cwd)) return false;
452
+ if (await runGit([...base, "checkout-index", "-f", "-a"]) === null) return false;
453
+ if (await runGit([...base, "clean", "-fd"]) === null) return false;
454
+ return true;
455
+ }
456
+ async function diffTreeNames(cwd, hash) {
457
+ if (!await isGitAvailable()) return null;
458
+ const dir = treeRepoDir(cwd);
459
+ if (!existsSync(path.join(dir, "HEAD"))) return null;
460
+ const base = repoArgs(dir, cwd);
461
+ const changed = await runGit([...base, "diff", "--name-only", hash]);
462
+ const untracked = await runGit([...base, "ls-files", "--others", "--exclude-standard"]);
463
+ if (changed === null || untracked === null) return null;
464
+ const names = /* @__PURE__ */ new Set();
465
+ for (const line of [...changed.split("\n"), ...untracked.split("\n")]) {
466
+ if (line !== "" && !isSensitivePath(line)) names.add(line);
467
+ }
468
+ return [...names];
469
+ }
470
+
471
+ export {
472
+ toStarMessage,
473
+ toCoreMessages,
474
+ isSyntheticUserMessage,
475
+ coreMessageText,
476
+ INTERRUPTED_TOOL_RESULT_TEXT,
477
+ UNFINISHED_TOOL_RESULT_TEXT,
478
+ reconcileStarMessages,
479
+ retractLastTurn,
480
+ ProviderConfigSchema,
481
+ ModelConfigSchema,
482
+ PermissionsConfigSchema,
483
+ HookConfigSchema,
484
+ ConfigSchema,
485
+ contextWindowTokens,
486
+ resolveCompactThreshold,
487
+ removeTreeRepo,
488
+ pruneTreeReposExcept,
489
+ trackTree,
490
+ restoreTree,
491
+ diffTreeNames
492
+ };
493
+ //# sourceMappingURL=chunk-56ZIA57D.js.map