@cryer/star-cli 0.3.6 → 0.3.7

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.
@@ -0,0 +1,1441 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ SKIP_DIRS,
4
+ createIgnorePredicate,
5
+ downsampleImageIfNeeded,
6
+ envFilePath,
7
+ isSensitivePath,
8
+ pruneTreeReposExcept,
9
+ removeTreeRepo,
10
+ sessionsDir,
11
+ wrapFetchWithActivity
12
+ } from "./chunk-OOOW7FC4.js";
13
+
14
+ // src/cli/cost.ts
15
+ function formatDollars(cost) {
16
+ let s = cost.toFixed(4).replace(/0+$/, "");
17
+ if (s.endsWith(".")) {
18
+ s += "00";
19
+ } else if (s.split(".")[1]?.length === 1) {
20
+ s += "0";
21
+ }
22
+ return s;
23
+ }
24
+ function formatTokens(n) {
25
+ if (!Number.isFinite(n)) return String(n);
26
+ if (n < 1e3) return String(n);
27
+ if (n < 1e6) return `${(n / 1e3).toFixed(2)}K`;
28
+ return `${(n / 1e6).toFixed(2)}M`;
29
+ }
30
+ function computeCostUsd(usage, model) {
31
+ if (model?.promptPrice === void 0 || model.completionPrice === void 0) {
32
+ return null;
33
+ }
34
+ const cached = Math.min(Math.max(usage.cachedPromptTokens ?? 0, 0), usage.promptTokens);
35
+ const promptUsd = (usage.promptTokens - cached) * model.promptPrice + cached * (model.cacheReadPrice ?? model.promptPrice) + (usage.cacheReadInputTokens ?? 0) * (model.cacheReadPrice ?? 0);
36
+ return (promptUsd + usage.completionTokens * model.completionPrice) / 1e6;
37
+ }
38
+ function estimateCost(usage, modelName, model) {
39
+ const cost = computeCostUsd(usage, model);
40
+ if (cost === null) {
41
+ if (model !== void 0) {
42
+ const missing = [
43
+ model.promptPrice === void 0 ? "promptPrice" : null,
44
+ model.completionPrice === void 0 ? "completionPrice" : null
45
+ ].filter((f) => f !== null);
46
+ if (missing.length === 1) {
47
+ return `Estimated cost: unknown (${model.name} is missing ${missing[0]} \u2014 both prices are needed)`;
48
+ }
49
+ }
50
+ return `Estimated cost: unknown (no price configured for ${modelName})`;
51
+ }
52
+ const cachePrice = model?.cacheReadPrice !== void 0 ? `, $${model.cacheReadPrice}/M cache read` : "";
53
+ return `Estimated cost: $${formatDollars(cost)} (${model?.name} @ $${model?.promptPrice}/M prompt, $${model?.completionPrice}/M completion${cachePrice})`;
54
+ }
55
+
56
+ // src/cli/terminal-text.ts
57
+ var UNSAFE_CHARS = /[\x00-\x09\x0b-\x1f\x7f]/;
58
+ var CONTROL_CHARS = /[\x00-\x08\x0b-\x1f\x7f]/g;
59
+ var TABS = /\t/g;
60
+ function toTerminalSafe(text) {
61
+ if (!UNSAFE_CHARS.test(text)) return text;
62
+ return text.replace(TABS, " ").replace(CONTROL_CHARS, "");
63
+ }
64
+
65
+ // src/cli/markdown.ts
66
+ var ESC = "\x1B[";
67
+ var RESET = `${ESC}0m`;
68
+ function styled(open, text, base) {
69
+ const reopen = base ? `${ESC}${base}m` : "";
70
+ return `${ESC}${open}m${text}${RESET}${reopen}`;
71
+ }
72
+ var bold = (t, b) => styled("1", t, b);
73
+ var dim = (t, b) => styled("2", t, b);
74
+ var italic = (t, b) => styled("3", t, b);
75
+ var underline = (t, b) => styled("4", t, b);
76
+ var strike = (t, b) => styled("9", t, b);
77
+ var yellow = (t, b) => styled("33", t, b);
78
+ var cyanBold = (t, b) => styled("1;36", t, b);
79
+ function cellWidth(text) {
80
+ let width = 0;
81
+ for (const ch of text) {
82
+ const cp = ch.codePointAt(0) ?? 0;
83
+ if (cp >= 768 && cp <= 879 || cp === 8205 || cp >= 65024 && cp <= 65039) {
84
+ continue;
85
+ }
86
+ if (cp >= 4352 && cp <= 4447 || cp >= 11904 && cp <= 40959 || cp >= 40960 && cp <= 42191 || cp >= 44032 && cp <= 55203 || cp >= 63744 && cp <= 64255 || cp >= 65072 && cp <= 65103 || cp >= 65280 && cp <= 65376 || cp >= 65504 && cp <= 65510 || cp >= 126976 && cp <= 129791 || cp >= 131072 && cp <= 262141) {
87
+ width += 2;
88
+ } else {
89
+ width += 1;
90
+ }
91
+ }
92
+ return width;
93
+ }
94
+ var IMAGE_RE = /!\[([^\]]*)\]\(([^)\s]+)\)/g;
95
+ var LINK_RE = /\[([^\]]+)\]\(([^)\s]+)\)/g;
96
+ var BOLD_RE = /\*\*([^*\n]+)\*\*|__([^_\n]+)__/g;
97
+ var STRIKE_RE = /~~([^~\n]+)~~/g;
98
+ var STAR_ITALIC_RE = /\*([^*\n]+)\*/g;
99
+ var UNDERSCORE_ITALIC_RE = /(^|[^\w])_([^_\n]+)_(?=[^\w]|$)/g;
100
+ function renderInline(text, base = "") {
101
+ return text.split(/`([^`\n]+)`/).map((segment, index) => {
102
+ if (index % 2 === 1) return yellow(segment, base);
103
+ return segment.replace(IMAGE_RE, (_m, alt) => dim(`[image: ${alt}]`, base)).replace(
104
+ LINK_RE,
105
+ (_m, label, url) => `${underline(label, base)}${dim(` (${url})`, base)}`
106
+ ).replace(BOLD_RE, (_m, star, under) => bold(star ?? under, base)).replace(STRIKE_RE, (_m, t) => strike(t, base)).replace(STAR_ITALIC_RE, (_m, t) => italic(t, base)).replace(UNDERSCORE_ITALIC_RE, (_m, pre, t) => pre + italic(t, base));
107
+ }).join("");
108
+ }
109
+ var HEADING_RE = /^#{1,6}\s+(.*)$/;
110
+ var HR_RE = /^\s{0,3}(?:-{3,}|\*{3,}|_{3,})\s*$/;
111
+ var QUOTE_RE = /^\s{0,3}>\s?(.*)$/;
112
+ var FENCE_RE = /^\s{0,3}(```+|~~~+)/;
113
+ var TABLE_SEPARATOR_CELL_RE = /^:?-{3,}:?$/;
114
+ function isTableSeparator(line) {
115
+ const cells = splitTableRow(line);
116
+ return cells !== null && cells.length > 0 && cells.every((cell) => TABLE_SEPARATOR_CELL_RE.test(cell.trim()));
117
+ }
118
+ function splitTableRow(line) {
119
+ const trimmed = line.trim();
120
+ if (!trimmed.includes("|")) return null;
121
+ const stripped = trimmed.replace(/^\|/, "").replace(/\|$/, "");
122
+ return stripped.split("|").map((cell) => cell.trim());
123
+ }
124
+ var MIN_COLUMN_WIDTH = 4;
125
+ function wrapCell(text, width) {
126
+ const lines = [];
127
+ let current = "";
128
+ let currentWidth = 0;
129
+ for (const ch of text) {
130
+ const w = cellWidth(ch);
131
+ if (currentWidth > 0 && currentWidth + w > width) {
132
+ const lastSpace = current.lastIndexOf(" ");
133
+ if (lastSpace > 0 && ch !== " ") {
134
+ lines.push(current.slice(0, lastSpace));
135
+ current = current.slice(lastSpace + 1) + ch;
136
+ } else {
137
+ lines.push(current);
138
+ current = ch === " " ? "" : ch;
139
+ }
140
+ currentWidth = cellWidth(current);
141
+ } else {
142
+ current += ch;
143
+ currentWidth += w;
144
+ }
145
+ }
146
+ if (current.length > 0 || lines.length === 0) lines.push(current);
147
+ return lines;
148
+ }
149
+ function renderTable(lines, base, maxWidth) {
150
+ const rows = [];
151
+ for (const line of lines) {
152
+ const cells = splitTableRow(line);
153
+ if (cells) rows.push(cells);
154
+ }
155
+ if (rows.length < 2) return lines;
156
+ const columnCount = Math.max(...rows.map((row) => row.length));
157
+ const widths = Array.from(
158
+ { length: columnCount },
159
+ (_, column) => Math.max(1, ...rows.map((row) => cellWidth(row[column] ?? "")))
160
+ );
161
+ const totalWidth = () => 2 + widths.reduce((sum, width) => sum + width, 0) + 3 * (columnCount - 1);
162
+ while (totalWidth() > maxWidth) {
163
+ const widest = widths.indexOf(Math.max(...widths));
164
+ if (widths[widest] === void 0 || widths[widest] <= MIN_COLUMN_WIDTH) break;
165
+ widths[widest] -= 1;
166
+ }
167
+ const joiner = ` ${dim("\u2502", base)} `;
168
+ const formatRow = (row, style) => {
169
+ const wrapped = widths.map((width, column) => wrapCell(row[column] ?? "", width));
170
+ const height = Math.max(...wrapped.map((cell) => cell.length));
171
+ const out = [];
172
+ for (let line = 0; line < height; line++) {
173
+ out.push(
174
+ ` ${widths.map((width, column) => {
175
+ const raw = wrapped[column]?.[line] ?? "";
176
+ return style(raw) + " ".repeat(Math.max(0, width - cellWidth(raw)));
177
+ }).join(joiner).trimEnd()}`
178
+ );
179
+ }
180
+ return out;
181
+ };
182
+ const separator = dim(` ${widths.map((width) => "\u2500".repeat(width)).join("\u2500\u253C\u2500")}`, base);
183
+ const [header, , ...body] = rows;
184
+ return [
185
+ ...formatRow(header, (cell) => bold(cell, base)),
186
+ separator,
187
+ ...body.flatMap((row, index) => {
188
+ const lines2 = formatRow(row, (cell) => renderInline(cell, base));
189
+ return index < body.length - 1 ? [...lines2, separator] : lines2;
190
+ })
191
+ ];
192
+ }
193
+ function committableLineCount(lines) {
194
+ let inFence = false;
195
+ let safe = 0;
196
+ for (let i = 0; i < lines.length; i++) {
197
+ const line = lines[i] ?? "";
198
+ if (FENCE_RE.test(line)) inFence = !inFence;
199
+ const isPipe = line.includes("|");
200
+ const nextIsPipe = i + 1 >= lines.length || (lines[i + 1] ?? "").includes("|");
201
+ if (!inFence && !(isPipe && nextIsPipe)) safe = i + 1;
202
+ }
203
+ return safe;
204
+ }
205
+ function renderMarkdown(text, base = "", maxWidth = 80) {
206
+ const lines = text.split("\n");
207
+ const out = [];
208
+ let inFence = false;
209
+ for (let i = 0; i < lines.length; i++) {
210
+ const line = lines[i] ?? "";
211
+ if (inFence) {
212
+ if (FENCE_RE.test(line)) {
213
+ inFence = false;
214
+ } else {
215
+ out.push(`${dim(" \u258E", base)}${line}`);
216
+ }
217
+ continue;
218
+ }
219
+ const fence = FENCE_RE.exec(line);
220
+ if (fence) {
221
+ inFence = true;
222
+ const lang = line.trim().slice(fence[1]?.length ?? 0).trim();
223
+ if (lang) out.push(dim(` \u258E ${lang}`, base));
224
+ continue;
225
+ }
226
+ const heading = HEADING_RE.exec(line);
227
+ if (heading) {
228
+ out.push(cyanBold(heading[1] ?? "", base));
229
+ continue;
230
+ }
231
+ if (HR_RE.test(line)) {
232
+ out.push(dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500", base));
233
+ continue;
234
+ }
235
+ const quote = QUOTE_RE.exec(line);
236
+ if (quote) {
237
+ out.push(`${dim(" \u258E", base)}${italic(renderInline(quote[1] ?? "", base), base)}`);
238
+ continue;
239
+ }
240
+ const headerCells = splitTableRow(line);
241
+ if (headerCells && i + 1 < lines.length && isTableSeparator(lines[i + 1] ?? "")) {
242
+ const block = [line];
243
+ let j = i + 1;
244
+ while (j < lines.length && splitTableRow(lines[j] ?? "") !== null) {
245
+ block.push(lines[j] ?? "");
246
+ j += 1;
247
+ }
248
+ out.push(...renderTable(block, base, maxWidth));
249
+ i = j - 1;
250
+ continue;
251
+ }
252
+ out.push(renderInline(line, base));
253
+ }
254
+ return out.join("\n");
255
+ }
256
+
257
+ // src/cli/format.ts
258
+ function summarizeArgs(args, maxLength = 120) {
259
+ let json;
260
+ try {
261
+ json = JSON.stringify(args) ?? String(args);
262
+ } catch {
263
+ json = String(args);
264
+ }
265
+ return json.length > maxLength ? `${json.slice(0, maxLength)}...` : json;
266
+ }
267
+ function previewLines(text, maxLines = 10) {
268
+ const lines = text.split("\n");
269
+ if (lines.length <= maxLines) return { text, truncated: false };
270
+ return { text: lines.slice(0, maxLines).join("\n"), truncated: true };
271
+ }
272
+ function formatByteCount(bytes) {
273
+ if (bytes < 1024) return `${bytes} B`;
274
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
275
+ return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
276
+ }
277
+ function formatElapsedSeconds(seconds) {
278
+ const total = Math.max(0, Math.floor(seconds));
279
+ if (total < 60) return `${total}s`;
280
+ const minutes = Math.floor(total / 60);
281
+ const secs = total % 60;
282
+ if (minutes < 60) return `${minutes}m${secs}s`;
283
+ return `${Math.floor(minutes / 60)}h${minutes % 60}m${secs}s`;
284
+ }
285
+ function splitCommittableLines(text, minCompleteLines) {
286
+ const lastNewline = text.lastIndexOf("\n");
287
+ if (lastNewline < 0) return null;
288
+ const complete = text.slice(0, lastNewline);
289
+ const lines = complete.split("\n");
290
+ const count = committableLineCount(lines);
291
+ if (count < minCompleteLines) return null;
292
+ const committed = lines.slice(0, count).join("\n");
293
+ return { committed, rest: text.slice(committed.length + 1) };
294
+ }
295
+ function formatStreamError(error) {
296
+ if (error.message.includes("Unexpected end of JSON input")) {
297
+ return `${error.message} (response stream was truncated \u2014 try again)`;
298
+ }
299
+ return error.message;
300
+ }
301
+ function coreMessageText(message) {
302
+ const content = message.content;
303
+ if (typeof content === "string") return content;
304
+ if (Array.isArray(content)) {
305
+ return content.filter((part) => part.type === "text").map((part) => "text" in part ? part.text : "").join(" ");
306
+ }
307
+ return "";
308
+ }
309
+ function buildDisplayMessages(messages) {
310
+ const display = [];
311
+ let collapsed = 0;
312
+ for (const message of messages) {
313
+ if (message.role === "user" || message.role === "assistant") {
314
+ const text = coreMessageText(message);
315
+ if (text) {
316
+ display.push({ id: display.length, role: message.role, text: toTerminalSafe(text) });
317
+ } else {
318
+ collapsed++;
319
+ }
320
+ } else if (message.role === "tool") {
321
+ collapsed++;
322
+ }
323
+ }
324
+ if (collapsed > 0) {
325
+ display.push({
326
+ id: display.length,
327
+ role: "system",
328
+ text: `Restored ${collapsed} history message(s) not shown here.`
329
+ });
330
+ }
331
+ return display;
332
+ }
333
+
334
+ // src/cli/mentions.ts
335
+ import { readFile, readdir, stat } from "fs/promises";
336
+ import path from "path";
337
+ var MAX_MENTION_BYTES = 100 * 1024;
338
+ var MAX_IMAGE_BYTES = 5 * 1024 * 1024;
339
+ var MAX_DIR_MENTION_ENTRIES = 200;
340
+ var MAX_DIR_MENTION_DEPTH = 10;
341
+ var MAX_DIR_MENTION_TOTAL = 5e3;
342
+ var IMAGE_MIME_TYPES = {
343
+ ".png": "image/png",
344
+ ".jpg": "image/jpeg",
345
+ ".jpeg": "image/jpeg",
346
+ ".gif": "image/gif",
347
+ ".webp": "image/webp"
348
+ };
349
+ function imageMimeType(filePath) {
350
+ return IMAGE_MIME_TYPES[path.extname(filePath).toLowerCase()] ?? null;
351
+ }
352
+ async function readImageInput(filePath, cwd) {
353
+ const mimeType = imageMimeType(filePath);
354
+ if (!mimeType) return null;
355
+ const abs = path.resolve(cwd, filePath);
356
+ const st = await stat(abs).catch(() => null);
357
+ if (!st || !st.isFile() || st.size > MAX_IMAGE_BYTES) return null;
358
+ const buf = await readFile(abs).catch(() => null);
359
+ if (!buf) return null;
360
+ const result = await downsampleImageIfNeeded({
361
+ path: filePath,
362
+ mimeType,
363
+ data: buf.toString("base64")
364
+ });
365
+ return result.image;
366
+ }
367
+ var MENTION_RE = /(?<=^|\s)@([A-Za-z]:[\\/][A-Za-z0-9._\-/\\\u2e80-\u9fff\uac00-\ud7af\uf900-\ufaff\uff00-\uffef]*|[A-Za-z0-9._\-/\\\u2e80-\u9fff\uac00-\ud7af\uf900-\ufaff\uff00-\uffef]+)/g;
368
+ function parseMentions(text) {
369
+ const mentions = [];
370
+ const seen = /* @__PURE__ */ new Set();
371
+ const ranges = [];
372
+ for (const match of text.matchAll(MENTION_RE)) {
373
+ const mention = match[1];
374
+ const full = match[0];
375
+ if (mention === void 0 || full === void 0) continue;
376
+ ranges.push([match.index, match.index + full.length]);
377
+ if (!seen.has(mention)) {
378
+ seen.add(mention);
379
+ mentions.push(mention);
380
+ }
381
+ }
382
+ if (ranges.length === 0) {
383
+ return { cleanText: text, mentions };
384
+ }
385
+ let clean = "";
386
+ let last = 0;
387
+ for (const [start, end] of ranges) {
388
+ clean += text.slice(last, start);
389
+ last = end;
390
+ }
391
+ clean += text.slice(last);
392
+ const cleanText = clean.replace(/[ \t]+\n/g, "\n").replace(/\n{3,}/g, "\n\n").trim();
393
+ return { cleanText, mentions };
394
+ }
395
+ function isBinaryContent(buf) {
396
+ const probe = buf.subarray(0, 8e3);
397
+ return probe.includes(0);
398
+ }
399
+ async function listDirEntries(dir, prefix, depth, state, ignore) {
400
+ if (state.capped) return;
401
+ const entries = await readdir(dir, { withFileTypes: true }).catch(() => null);
402
+ if (!entries) return;
403
+ const visible = entries.filter((entry) => entry.isDirectory() || entry.isFile()).filter((entry) => !(entry.isDirectory() && SKIP_DIRS.has(entry.name))).filter((entry) => !isSensitivePath(entry.name)).filter((entry) => !ignore?.(path.join(dir, entry.name), entry.isDirectory())).sort((a, b) => {
404
+ const dirDiff = Number(b.isDirectory()) - Number(a.isDirectory());
405
+ return dirDiff !== 0 ? dirDiff : a.name.localeCompare(b.name);
406
+ });
407
+ for (let i = 0; i < visible.length; i++) {
408
+ if (state.total >= MAX_DIR_MENTION_TOTAL) {
409
+ state.capped = true;
410
+ return;
411
+ }
412
+ const entry = visible[i];
413
+ if (!entry) continue;
414
+ state.total += 1;
415
+ const last = i === visible.length - 1;
416
+ const isDir = entry.isDirectory();
417
+ if (state.shown < MAX_DIR_MENTION_ENTRIES) {
418
+ state.shown += 1;
419
+ state.lines.push(`${prefix}${last ? "\u2514\u2500\u2500 " : "\u251C\u2500\u2500 "}${entry.name}${isDir ? "/" : ""}`);
420
+ if (isDir && depth < MAX_DIR_MENTION_DEPTH) {
421
+ await listDirEntries(
422
+ path.join(dir, entry.name),
423
+ `${prefix}${last ? " " : "\u2502 "}`,
424
+ depth + 1,
425
+ state,
426
+ ignore
427
+ );
428
+ }
429
+ } else if (isDir && depth < MAX_DIR_MENTION_DEPTH) {
430
+ await listDirEntries(path.join(dir, entry.name), prefix, depth + 1, state, ignore);
431
+ }
432
+ }
433
+ }
434
+ async function directoryBlock(abs, mention, ignore) {
435
+ const display = `${mention.replace(/\\/g, "/").replace(/\/+$/, "")}/`;
436
+ const state = { lines: [`${display}`], shown: 0, total: 0, capped: false };
437
+ const probe = await readdir(abs).catch(() => null);
438
+ if (!probe) return null;
439
+ await listDirEntries(abs, "", 1, state, ignore);
440
+ if (state.capped) {
441
+ state.lines.push("... (truncated, many more entries)");
442
+ } else if (state.total > state.shown) {
443
+ state.lines.push(
444
+ `... (truncated, ${state.total - state.shown} more entries \u2014 use glob/grep tools or mention a subdirectory to see more)`
445
+ );
446
+ }
447
+ return `--- @${display} (directory) ---
448
+ ${state.lines.join("\n")}
449
+ --- end ---`;
450
+ }
451
+ async function resolveMentions(text, cwd) {
452
+ const { cleanText, mentions } = parseMentions(text);
453
+ if (mentions.length === 0) {
454
+ return { input: text, attached: [], skipped: [], images: [] };
455
+ }
456
+ const blocks = [];
457
+ const attached = [];
458
+ const skipped = [];
459
+ const images = [];
460
+ let ignore;
461
+ for (const mention of mentions) {
462
+ const abs = path.resolve(cwd, mention);
463
+ if (isSensitivePath(abs)) {
464
+ skipped.push({ path: mention, reason: "sensitive file" });
465
+ continue;
466
+ }
467
+ const mimeType = imageMimeType(mention);
468
+ if (mimeType) {
469
+ const st2 = await stat(abs).catch(() => null);
470
+ if (!st2 || !st2.isFile()) {
471
+ skipped.push({ path: mention, reason: st2 ? "is a directory" : "file not found" });
472
+ continue;
473
+ }
474
+ if (st2.size > MAX_IMAGE_BYTES) {
475
+ skipped.push({ path: mention, reason: "image too large" });
476
+ continue;
477
+ }
478
+ const buf2 = await readFile(abs).catch(() => null);
479
+ if (!buf2) {
480
+ skipped.push({ path: mention, reason: "unreadable" });
481
+ continue;
482
+ }
483
+ const result = await downsampleImageIfNeeded({
484
+ path: mention,
485
+ mimeType,
486
+ data: buf2.toString("base64")
487
+ });
488
+ attached.push(mention);
489
+ images.push(result.image);
490
+ continue;
491
+ }
492
+ const st = await stat(abs).catch(() => null);
493
+ if (!st) {
494
+ skipped.push({ path: mention, reason: "file not found" });
495
+ continue;
496
+ }
497
+ if (st.isDirectory()) {
498
+ if (ignore === void 0) {
499
+ ignore = createIgnorePredicate(cwd) ?? null;
500
+ }
501
+ const block = await directoryBlock(abs, mention, ignore ?? void 0);
502
+ if (!block) {
503
+ skipped.push({ path: mention, reason: "unreadable" });
504
+ continue;
505
+ }
506
+ attached.push(mention);
507
+ blocks.push(block);
508
+ continue;
509
+ }
510
+ if (st.size > MAX_MENTION_BYTES) {
511
+ skipped.push({ path: mention, reason: `exceeds ${MAX_MENTION_BYTES / 1024}KB limit` });
512
+ continue;
513
+ }
514
+ const buf = await readFile(abs).catch(() => null);
515
+ if (!buf) {
516
+ skipped.push({ path: mention, reason: "unreadable" });
517
+ continue;
518
+ }
519
+ if (isBinaryContent(buf)) {
520
+ skipped.push({ path: mention, reason: "binary file" });
521
+ continue;
522
+ }
523
+ attached.push(mention);
524
+ blocks.push(`--- @${mention} ---
525
+ ${buf.toString("utf8")}
526
+ --- end ---`);
527
+ }
528
+ const input = [cleanText, ...blocks].filter((s) => s.length > 0).join("\n\n") || text;
529
+ return { input, attached, skipped, images };
530
+ }
531
+
532
+ // src/cli/system-prompt.ts
533
+ var SYSTEM_PROMPT = `You are Star CLI, an AI coding agent running in the user's terminal.
534
+ You help with software engineering tasks: reading, writing and editing code, running shell commands, and managing todos.
535
+ Be concise and direct. Use tools when they help accomplish the task.
536
+ For any task with two or more steps, start by creating a todo list with todo_write and keep it updated as you progress; only mark an item completed after you have verified its result.
537
+ The working directory is the user's project root; never touch files outside it without explicit instruction.
538
+ For conversion, batch-processing, or other scriptable tasks, write a script file into the working directory and run it instead of pasting long inline code into the shell; once the script's output is verified, delete the script unless the user asked to keep it.
539
+ When a tool call or command fails, read the error output, work out the cause, and try again with a corrected or alternative approach \u2014 never repeat an identical failing call without changing something.
540
+ Never consider the task finished until you have verified the result yourself: run the tests, the build, or a check command that proves the output is correct, and if verification fails or reveals gaps, keep fixing until it passes.
541
+ Do not end your turn while the task is still incomplete; keep going until it is done or you are genuinely blocked, and if you are blocked, state exactly what is missing.
542
+ Never end a reply by announcing what you will do next \u2014 either do it now with tool calls, or do not mention it. Narrating future actions is not progress.`;
543
+
544
+ // src/llm/registry.ts
545
+ function listModels(config) {
546
+ return config.models;
547
+ }
548
+ function resolveModelConfig(config, modelName) {
549
+ const name = modelName ?? config.defaultModel;
550
+ const model = config.models.find((m) => m.name === name);
551
+ if (!model) {
552
+ const available = config.models.map((m) => m.name).join(", ") || "(none)";
553
+ throw new Error(`Model "${name}" not found. Available models: ${available}`);
554
+ }
555
+ return model;
556
+ }
557
+
558
+ // src/llm/provider.ts
559
+ import { createAnthropic } from "@ai-sdk/anthropic";
560
+ import { createOpenAI } from "@ai-sdk/openai";
561
+ import { createOpenAICompatible } from "@ai-sdk/openai-compatible";
562
+
563
+ // src/llm/sse-normalize.ts
564
+ function splitJsonPrefix(text) {
565
+ if (!text.startsWith("{")) return null;
566
+ let depth = 0;
567
+ let inString = false;
568
+ let escaped = false;
569
+ for (let i = 0; i < text.length; i++) {
570
+ const ch = text[i];
571
+ if (inString) {
572
+ if (escaped) escaped = false;
573
+ else if (ch === "\\") escaped = true;
574
+ else if (ch === '"') inString = false;
575
+ continue;
576
+ }
577
+ if (ch === '"') inString = true;
578
+ else if (ch === "{") depth += 1;
579
+ else if (ch === "}") {
580
+ depth -= 1;
581
+ if (depth === 0) return [text.slice(0, i + 1), text.slice(i + 1).trimStart()];
582
+ }
583
+ }
584
+ return null;
585
+ }
586
+ var SseNormalizeTransform = class extends TransformStream {
587
+ constructor() {
588
+ let buffer = "";
589
+ let pending = null;
590
+ const flushPending = (controller) => {
591
+ if (pending === null) return;
592
+ controller.enqueue(`data: ${pending}
593
+
594
+ `);
595
+ pending = null;
596
+ };
597
+ const emitPayload = (payload, controller) => {
598
+ let rest = payload;
599
+ for (; ; ) {
600
+ const split = splitJsonPrefix(rest);
601
+ if (!split) {
602
+ pending = rest;
603
+ return;
604
+ }
605
+ const [json, tail] = split;
606
+ if (tail === "") {
607
+ pending = json;
608
+ return;
609
+ }
610
+ controller.enqueue(`data: ${json}
611
+
612
+ `);
613
+ if (tail.startsWith("data:")) {
614
+ rest = tail.slice(5).replace(/^ /, "");
615
+ continue;
616
+ }
617
+ if (tail.startsWith("{")) {
618
+ rest = tail;
619
+ continue;
620
+ }
621
+ controller.enqueue(`${tail}
622
+ `);
623
+ return;
624
+ }
625
+ };
626
+ const handleLine = (raw, controller) => {
627
+ const line = raw.endsWith("\r") ? raw.slice(0, -1) : raw;
628
+ if (line === "") {
629
+ flushPending(controller);
630
+ return;
631
+ }
632
+ if (line.startsWith("data:")) {
633
+ flushPending(controller);
634
+ emitPayload(line.slice(5).replace(/^ /, ""), controller);
635
+ return;
636
+ }
637
+ if (line.startsWith("{") && pending !== null) {
638
+ flushPending(controller);
639
+ emitPayload(line, controller);
640
+ return;
641
+ }
642
+ flushPending(controller);
643
+ controller.enqueue(`${line}
644
+ `);
645
+ };
646
+ super({
647
+ transform(chunk, controller) {
648
+ buffer += chunk;
649
+ let index = buffer.indexOf("\n");
650
+ while (index !== -1) {
651
+ handleLine(buffer.slice(0, index), controller);
652
+ buffer = buffer.slice(index + 1);
653
+ index = buffer.indexOf("\n");
654
+ }
655
+ },
656
+ flush(controller) {
657
+ if (buffer.length > 0) handleLine(buffer, controller);
658
+ flushPending(controller);
659
+ }
660
+ });
661
+ }
662
+ };
663
+ function createSseNormalizingFetch(baseFetch = fetch) {
664
+ return async (input, init) => {
665
+ const response = await baseFetch(input, init);
666
+ const contentType = response.headers.get("content-type") ?? "";
667
+ if (!response.body || !contentType.includes("text/event-stream")) {
668
+ return response;
669
+ }
670
+ const body = response.body.pipeThrough(new TextDecoderStream()).pipeThrough(new SseNormalizeTransform()).pipeThrough(new TextEncoderStream());
671
+ const headers = new Headers(response.headers);
672
+ headers.delete("content-length");
673
+ headers.delete("content-encoding");
674
+ return new Response(body, {
675
+ status: response.status,
676
+ statusText: response.statusText,
677
+ headers
678
+ });
679
+ };
680
+ }
681
+
682
+ // src/llm/provider.ts
683
+ function resolveApiKey(provider) {
684
+ const fromEnv = provider.apiKeyEnv ? process.env[provider.apiKeyEnv] : void 0;
685
+ const apiKey = fromEnv ?? provider.apiKey;
686
+ if (!apiKey) {
687
+ const hint = provider.apiKeyEnv ? `set ${provider.apiKeyEnv} or configure apiKey` : "configure apiKey";
688
+ throw new Error(`Missing API key for provider "${provider.name}": ${hint}`);
689
+ }
690
+ return apiKey;
691
+ }
692
+ function createModel(config, modelName) {
693
+ const modelConfig = resolveModelConfig(config, modelName);
694
+ const provider = config.providers.find((p) => p.name === modelConfig.provider);
695
+ if (!provider) {
696
+ const available = config.providers.map((p) => p.name).join(", ") || "(none)";
697
+ throw new Error(
698
+ `Provider "${modelConfig.provider}" for model "${modelConfig.name}" not found. Available providers: ${available}`
699
+ );
700
+ }
701
+ const apiKey = resolveApiKey(provider);
702
+ const trackedFetch = wrapFetchWithActivity();
703
+ switch (provider.protocol) {
704
+ case "anthropic":
705
+ return createAnthropic({
706
+ baseURL: provider.baseURL,
707
+ apiKey,
708
+ headers: provider.headers,
709
+ fetch: trackedFetch
710
+ })(modelConfig.model);
711
+ case "openai-compatible":
712
+ return createOpenAICompatible({
713
+ name: provider.name,
714
+ baseURL: provider.baseURL,
715
+ apiKey,
716
+ headers: provider.headers,
717
+ fetch: trackedFetch
718
+ })(modelConfig.model, {}, { includeUsage: true });
719
+ case "openai-responses":
720
+ return createOpenAI({
721
+ baseURL: provider.baseURL,
722
+ apiKey,
723
+ headers: provider.headers,
724
+ // Relays fronting the Responses API sometimes batch several JSON
725
+ // events into one SSE data payload, which the SDK's parser cannot
726
+ // read; normalize the wire format back to one event per data line.
727
+ fetch: createSseNormalizingFetch(trackedFetch)
728
+ }).responses(modelConfig.model);
729
+ }
730
+ }
731
+ function reasoningEffortMetadata(config, modelName) {
732
+ const modelConfig = resolveModelConfig(config, modelName);
733
+ if (!modelConfig.reasoningEffort) return void 0;
734
+ const provider = config.providers.find((p) => p.name === modelConfig.provider);
735
+ if (!provider || provider.protocol === "anthropic") return void 0;
736
+ const key = provider.protocol === "openai-responses" ? "openai" : provider.name;
737
+ return { [key]: { reasoningEffort: modelConfig.reasoningEffort } };
738
+ }
739
+
740
+ // src/session/checkpoints.ts
741
+ import fs from "fs/promises";
742
+ import path2 from "path";
743
+ function checkpointsDir(sessionDir) {
744
+ return path2.join(sessionDir, "checkpoints");
745
+ }
746
+ function indexPath(sessionDir) {
747
+ return path2.join(checkpointsDir(sessionDir), "index.json");
748
+ }
749
+ var indexWriteQueues = /* @__PURE__ */ new Map();
750
+ function enqueueIndexWrite(sessionDir, op) {
751
+ const queued = indexWriteQueues.get(sessionDir) ?? Promise.resolve();
752
+ const run = queued.then(op);
753
+ indexWriteQueues.set(
754
+ sessionDir,
755
+ run.then(
756
+ () => {
757
+ },
758
+ () => {
759
+ }
760
+ )
761
+ );
762
+ return run;
763
+ }
764
+ function checkpointContentPath(sessionDir, id) {
765
+ return path2.join(checkpointsDir(sessionDir), `${id}.snapshot`);
766
+ }
767
+ async function listCheckpointRecords(sessionDir) {
768
+ let raw;
769
+ try {
770
+ raw = await fs.readFile(indexPath(sessionDir), "utf8");
771
+ } catch {
772
+ return [];
773
+ }
774
+ try {
775
+ const parsed = JSON.parse(raw);
776
+ return Array.isArray(parsed) ? parsed : [];
777
+ } catch {
778
+ return [];
779
+ }
780
+ }
781
+ async function appendCheckpointRecord(sessionDir, record, content) {
782
+ await enqueueIndexWrite(sessionDir, async () => {
783
+ await fs.mkdir(checkpointsDir(sessionDir), { recursive: true, mode: 448 });
784
+ if (record.existed && content !== null) {
785
+ await fs.writeFile(checkpointContentPath(sessionDir, record.id), content, "utf8");
786
+ }
787
+ const records = await listCheckpointRecords(sessionDir);
788
+ records.push(record);
789
+ await fs.writeFile(indexPath(sessionDir), JSON.stringify(records, null, 2));
790
+ });
791
+ }
792
+ async function removeCheckpointRecords(sessionDir, ids) {
793
+ if (ids.length === 0) return;
794
+ await enqueueIndexWrite(sessionDir, async () => {
795
+ const drop = new Set(ids);
796
+ const records = await listCheckpointRecords(sessionDir);
797
+ const kept = records.filter((record) => !drop.has(record.id));
798
+ if (kept.length === records.length) return;
799
+ await fs.mkdir(checkpointsDir(sessionDir), { recursive: true, mode: 448 });
800
+ await fs.writeFile(indexPath(sessionDir), JSON.stringify(kept, null, 2));
801
+ for (const id of ids) {
802
+ await fs.rm(checkpointContentPath(sessionDir, id), { force: true });
803
+ }
804
+ });
805
+ }
806
+ async function loadSessionSnapshots(sessionDir) {
807
+ const records = await listCheckpointRecords(sessionDir);
808
+ return records.map((record) => ({
809
+ ...record,
810
+ content: null,
811
+ contentFile: record.existed && !record.contentTooLarge ? checkpointContentPath(sessionDir, record.id) : void 0
812
+ }));
813
+ }
814
+
815
+ // src/session/store.ts
816
+ import fs2 from "fs/promises";
817
+ import path3 from "path";
818
+ import { StringDecoder } from "string_decoder";
819
+ function generateId(now) {
820
+ const pad = (n) => String(n).padStart(2, "0");
821
+ const stamp = `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`;
822
+ const suffix = Math.random().toString(16).slice(2, 8).padEnd(6, "0");
823
+ return `${stamp}-${suffix}`;
824
+ }
825
+ function debugWarn(message) {
826
+ if (process.env.STAR_DEBUG === "1") process.stderr.write(`[star-cli] ${message}
827
+ `);
828
+ }
829
+ function warn(message) {
830
+ process.stderr.write(`[star-cli] ${message}
831
+ `);
832
+ }
833
+ var FS_WRITE_MAX_ATTEMPTS = 5;
834
+ var FS_WRITE_RETRY_DELAY_MS = 25;
835
+ function isTransientFsError(error) {
836
+ const code = error.code;
837
+ return code === "EPERM" || code === "EACCES" || code === "EBUSY";
838
+ }
839
+ async function withTransientFsRetry(operation) {
840
+ let lastError;
841
+ for (let attempt = 0; attempt < FS_WRITE_MAX_ATTEMPTS; attempt++) {
842
+ try {
843
+ await operation();
844
+ return;
845
+ } catch (error) {
846
+ lastError = error;
847
+ if (isTransientFsError(error) && attempt + 1 < FS_WRITE_MAX_ATTEMPTS) {
848
+ await new Promise(
849
+ (resolve) => setTimeout(resolve, FS_WRITE_RETRY_DELAY_MS * (attempt + 1))
850
+ );
851
+ } else {
852
+ break;
853
+ }
854
+ }
855
+ }
856
+ throw lastError;
857
+ }
858
+ var FALLBACK_META = /* @__PURE__ */ Symbol("star.fallbackMeta");
859
+ function isFallbackMeta(meta) {
860
+ return meta[FALLBACK_META] === true;
861
+ }
862
+ var META_FLUSH_INTERVAL_MS = 2e3;
863
+ var MESSAGES_READ_CHUNK_BYTES = 64 * 1024;
864
+ function dayKey(date) {
865
+ const pad = (n) => String(n).padStart(2, "0");
866
+ return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
867
+ }
868
+ var SessionStore = class _SessionStore {
869
+ id;
870
+ dir;
871
+ pendingMeta = null;
872
+ initialized = false;
873
+ cachedMeta = null;
874
+ metaTmpSeq = 0;
875
+ messagesTmpSeq = 0;
876
+ metaWriteQueue = Promise.resolve();
877
+ metaLastFlushAt = 0;
878
+ metaFlushTimer = null;
879
+ metaDirty = false;
880
+ constructor(id) {
881
+ this.id = id;
882
+ this.dir = path3.join(sessionsDir(), id);
883
+ }
884
+ metaPath() {
885
+ return path3.join(this.dir, "meta.json");
886
+ }
887
+ messagesPath() {
888
+ return path3.join(this.dir, "messages.jsonl");
889
+ }
890
+ static async create(cwd, model) {
891
+ const store = new _SessionStore(generateId(/* @__PURE__ */ new Date()));
892
+ store.pendingMeta = { cwd, model, createdAt: Date.now() };
893
+ return store;
894
+ }
895
+ static async open(id) {
896
+ const store = new _SessionStore(id);
897
+ let raw;
898
+ try {
899
+ raw = await fs2.readFile(store.metaPath(), "utf8");
900
+ } catch {
901
+ return null;
902
+ }
903
+ try {
904
+ const meta = JSON.parse(raw);
905
+ if (meta.id !== id) return null;
906
+ } catch {
907
+ debugWarn(`session ${id}: corrupt meta.json, using defaults`);
908
+ }
909
+ await store.sweepTmpFiles();
910
+ store.initialized = true;
911
+ return store;
912
+ }
913
+ static async list(cwd) {
914
+ let entries;
915
+ try {
916
+ entries = await fs2.readdir(sessionsDir());
917
+ } catch {
918
+ return [];
919
+ }
920
+ const metas = await Promise.all(
921
+ entries.map(async (entry) => {
922
+ try {
923
+ const raw = await fs2.readFile(path3.join(sessionsDir(), entry, "meta.json"), "utf8");
924
+ const meta = JSON.parse(raw);
925
+ if (meta.id === entry && (cwd === void 0 || meta.cwd === cwd)) return meta;
926
+ } catch {
927
+ }
928
+ return null;
929
+ })
930
+ );
931
+ return metas.filter((meta) => meta !== null).sort((a, b) => b.updatedAt - a.updatedAt);
932
+ }
933
+ // Meta writes are serialized per instance and land atomically (tmp file +
934
+ // rename) over a cached in-memory meta: the fire-and-forget title/usage
935
+ // writers race the loop's own appends, and a truncated read once turned
936
+ // fallbackMeta's empty defaults into a persisted wipe of model/cwd/usage.
937
+ // A branded fallback meta is never written back for the same reason.
938
+ // A write that still fails after retries is dropped rather than thrown:
939
+ // meta.json is bookkeeping the cached meta rebuilds on the next write,
940
+ // while the conversation itself lives in messages.jsonl — killing the
941
+ // agent turn over a stale updatedAt would be the wrong trade.
942
+ writeMeta(meta) {
943
+ if (isFallbackMeta(meta)) return Promise.resolve();
944
+ const tmp = `${this.metaPath()}.tmp-${process.pid}-${this.metaTmpSeq++}`;
945
+ const run = this.metaWriteQueue.then(async () => {
946
+ try {
947
+ await withTransientFsRetry(async () => {
948
+ await fs2.writeFile(tmp, JSON.stringify(meta, null, 2));
949
+ await fs2.rename(tmp, this.metaPath());
950
+ });
951
+ } catch (error) {
952
+ await fs2.unlink(tmp).catch(() => {
953
+ });
954
+ debugWarn(
955
+ `session ${this.id}: meta write failed (${error.code ?? String(error)}), skipping`
956
+ );
957
+ }
958
+ });
959
+ this.metaWriteQueue = run.catch(() => {
960
+ });
961
+ return run;
962
+ }
963
+ // Debounced flush for append-driven updatedAt bumps. The trailing timer is
964
+ // unref'd so a pending flush never keeps the process alive; close() flushes
965
+ // synchronously on a clean exit, and a hard exit simply drops the trailing
966
+ // bump — meta is bookkeeping.
967
+ markMetaDirty() {
968
+ this.metaDirty = true;
969
+ if (this.metaFlushTimer) return;
970
+ const delay = Math.max(0, META_FLUSH_INTERVAL_MS - (Date.now() - this.metaLastFlushAt));
971
+ if (delay === 0) {
972
+ void this.flushMetaIfDirty();
973
+ return;
974
+ }
975
+ this.metaFlushTimer = setTimeout(() => {
976
+ this.metaFlushTimer = null;
977
+ void this.flushMetaIfDirty();
978
+ }, delay);
979
+ this.metaFlushTimer.unref();
980
+ }
981
+ async flushMetaIfDirty() {
982
+ if (!this.metaDirty) return;
983
+ await this.flushMetaNow();
984
+ }
985
+ // Immediate flush for substantive meta changes; cancels a pending debounced
986
+ // flush, which the write makes redundant either way (it serializes the live
987
+ // cached meta).
988
+ async flushMetaNow() {
989
+ if (this.metaFlushTimer) {
990
+ clearTimeout(this.metaFlushTimer);
991
+ this.metaFlushTimer = null;
992
+ }
993
+ this.metaDirty = false;
994
+ this.metaLastFlushAt = Date.now();
995
+ if (this.cachedMeta) await this.writeMeta(this.cachedMeta);
996
+ }
997
+ // Flushes a pending debounced meta write and waits for in-flight writes to
998
+ // settle; call when the store is dropped so a clean exit keeps the trailing
999
+ // updatedAt bump.
1000
+ async close() {
1001
+ await this.flushMetaIfDirty();
1002
+ await this.metaWriteQueue;
1003
+ }
1004
+ async ensureInitialized() {
1005
+ if (this.initialized) return;
1006
+ const pending = this.pendingMeta;
1007
+ if (!pending) {
1008
+ this.initialized = true;
1009
+ return;
1010
+ }
1011
+ await fs2.mkdir(this.dir, { recursive: true, mode: 448 });
1012
+ await this.sweepTmpFiles();
1013
+ const meta = {
1014
+ id: this.id,
1015
+ title: "",
1016
+ model: pending.model,
1017
+ cwd: pending.cwd,
1018
+ createdAt: pending.createdAt,
1019
+ updatedAt: pending.createdAt
1020
+ };
1021
+ await this.writeMeta(meta);
1022
+ this.cachedMeta = meta;
1023
+ this.metaLastFlushAt = Date.now();
1024
+ this.initialized = true;
1025
+ }
1026
+ // Removes tmp orphans left behind by crashed atomic writes (both meta.json
1027
+ // and messages.jsonl write tmp file + rename); best-effort, the directory
1028
+ // may not even exist yet.
1029
+ async sweepTmpFiles() {
1030
+ let entries;
1031
+ try {
1032
+ entries = await fs2.readdir(this.dir);
1033
+ } catch {
1034
+ return;
1035
+ }
1036
+ await Promise.all(
1037
+ entries.filter(
1038
+ (entry) => entry.startsWith("meta.json.tmp-") || entry.startsWith("messages.jsonl.tmp-")
1039
+ ).map((entry) => fs2.unlink(path3.join(this.dir, entry)).catch(() => {
1040
+ }))
1041
+ );
1042
+ }
1043
+ async append(message) {
1044
+ await this.ensureInitialized();
1045
+ try {
1046
+ await withTransientFsRetry(
1047
+ () => fs2.appendFile(this.messagesPath(), `${JSON.stringify(message)}
1048
+ `)
1049
+ );
1050
+ } catch (error) {
1051
+ warn(
1052
+ `session ${this.id}: message append failed (${error.code ?? String(error)}), message kept in memory only`
1053
+ );
1054
+ }
1055
+ const meta = await this.meta();
1056
+ meta.updatedAt = Date.now();
1057
+ this.markMetaDirty();
1058
+ }
1059
+ async replaceMessages(messages) {
1060
+ await this.ensureInitialized();
1061
+ const content = messages.map((message) => JSON.stringify(message)).join("\n");
1062
+ const tmp = `${this.messagesPath()}.tmp-${process.pid}-${this.messagesTmpSeq++}`;
1063
+ try {
1064
+ await withTransientFsRetry(async () => {
1065
+ await fs2.writeFile(tmp, content ? `${content}
1066
+ ` : "");
1067
+ await fs2.rename(tmp, this.messagesPath());
1068
+ });
1069
+ } catch (error) {
1070
+ await fs2.unlink(tmp).catch(() => {
1071
+ });
1072
+ warn(
1073
+ `session ${this.id}: history rewrite failed (${error.code ?? String(error)}), kept the previous messages.jsonl`
1074
+ );
1075
+ }
1076
+ const meta = await this.meta();
1077
+ meta.updatedAt = Date.now();
1078
+ await this.flushMetaNow();
1079
+ }
1080
+ async messages() {
1081
+ let handle = null;
1082
+ try {
1083
+ handle = await fs2.open(this.messagesPath(), "r");
1084
+ const messages = [];
1085
+ const decoder = new StringDecoder("utf8");
1086
+ const buffer = Buffer.alloc(MESSAGES_READ_CHUNK_BYTES);
1087
+ let tail = "";
1088
+ const pushLine = (line) => {
1089
+ const trimmed = line.trim();
1090
+ if (!trimmed) return;
1091
+ try {
1092
+ messages.push(JSON.parse(trimmed));
1093
+ } catch {
1094
+ debugWarn(`session ${this.id}: skipping corrupt messages.jsonl line`);
1095
+ }
1096
+ };
1097
+ while (true) {
1098
+ const { bytesRead } = await handle.read(buffer, 0, buffer.length, null);
1099
+ if (bytesRead === 0) break;
1100
+ const chunk = tail + decoder.write(buffer.subarray(0, bytesRead));
1101
+ const lines = chunk.split("\n");
1102
+ tail = lines.pop() ?? "";
1103
+ for (const line of lines) pushLine(line);
1104
+ }
1105
+ pushLine(tail + decoder.end());
1106
+ return messages;
1107
+ } catch {
1108
+ return [];
1109
+ } finally {
1110
+ await handle?.close().catch(() => {
1111
+ });
1112
+ }
1113
+ }
1114
+ fallbackMeta() {
1115
+ const now = Date.now();
1116
+ const meta = {
1117
+ id: this.id,
1118
+ title: "",
1119
+ model: "",
1120
+ cwd: "",
1121
+ createdAt: now,
1122
+ updatedAt: now
1123
+ };
1124
+ Object.defineProperty(meta, FALLBACK_META, { value: true, enumerable: false });
1125
+ return meta;
1126
+ }
1127
+ async meta() {
1128
+ await this.ensureInitialized();
1129
+ if (this.cachedMeta) return this.cachedMeta;
1130
+ try {
1131
+ const raw = await fs2.readFile(this.metaPath(), "utf8");
1132
+ this.cachedMeta = JSON.parse(raw);
1133
+ return this.cachedMeta;
1134
+ } catch {
1135
+ debugWarn(`session ${this.id}: unreadable meta.json, using defaults`);
1136
+ return this.fallbackMeta();
1137
+ }
1138
+ }
1139
+ async setTitle(title) {
1140
+ await this.ensureInitialized();
1141
+ const meta = await this.meta();
1142
+ meta.title = title;
1143
+ meta.updatedAt = Date.now();
1144
+ await this.flushMetaNow();
1145
+ }
1146
+ // /model swaps the live loop mid-session; keep the recorded model in sync
1147
+ // or the session list and /usage attribute the whole session to whichever
1148
+ // model happened to be active at creation.
1149
+ async setModel(model) {
1150
+ await this.ensureInitialized();
1151
+ const meta = await this.meta();
1152
+ meta.model = model;
1153
+ meta.updatedAt = Date.now();
1154
+ await this.flushMetaNow();
1155
+ }
1156
+ async appendCheckpoint(record, content) {
1157
+ await this.ensureInitialized();
1158
+ await appendCheckpointRecord(this.dir, record, content);
1159
+ }
1160
+ async listCheckpoints() {
1161
+ return listCheckpointRecords(this.dir);
1162
+ }
1163
+ async removeCheckpoints(ids) {
1164
+ await removeCheckpointRecords(this.dir, ids);
1165
+ }
1166
+ async addUsage(delta) {
1167
+ await this.ensureInitialized();
1168
+ const meta = await this.meta();
1169
+ const usage = meta.usage ?? {
1170
+ requests: 0,
1171
+ promptTokens: 0,
1172
+ completionTokens: 0,
1173
+ totalTokens: 0
1174
+ };
1175
+ usage.requests += 1;
1176
+ usage.promptTokens += delta.promptTokens;
1177
+ usage.completionTokens += delta.completionTokens;
1178
+ usage.totalTokens += delta.totalTokens;
1179
+ if (delta.cachedPromptTokens) {
1180
+ usage.cachedPromptTokens = (usage.cachedPromptTokens ?? 0) + delta.cachedPromptTokens;
1181
+ }
1182
+ if (delta.cacheReadInputTokens) {
1183
+ usage.cacheReadInputTokens = (usage.cacheReadInputTokens ?? 0) + delta.cacheReadInputTokens;
1184
+ }
1185
+ meta.usage = usage;
1186
+ const day = dayKey(/* @__PURE__ */ new Date());
1187
+ const byDay = meta.usageByDay ?? {};
1188
+ const bucket = byDay[day] ?? { promptTokens: 0, completionTokens: 0, totalTokens: 0 };
1189
+ bucket.promptTokens += delta.promptTokens;
1190
+ bucket.completionTokens += delta.completionTokens;
1191
+ bucket.totalTokens += delta.totalTokens;
1192
+ byDay[day] = bucket;
1193
+ meta.usageByDay = byDay;
1194
+ await this.flushMetaNow();
1195
+ }
1196
+ };
1197
+
1198
+ // src/session/clear.ts
1199
+ import fs3 from "fs/promises";
1200
+ import path4 from "path";
1201
+ async function clearSessions(cwd, excludeId) {
1202
+ if (cwd !== void 0) {
1203
+ const metas = await SessionStore.list(cwd);
1204
+ let removed2 = 0;
1205
+ for (const meta of metas) {
1206
+ if (meta.id === excludeId) continue;
1207
+ await fs3.rm(path4.join(sessionsDir(), meta.id), { recursive: true, force: true });
1208
+ removed2 += 1;
1209
+ }
1210
+ if ((await SessionStore.list(cwd)).length === 0) {
1211
+ await removeTreeRepo(cwd);
1212
+ }
1213
+ return removed2;
1214
+ }
1215
+ let entries;
1216
+ try {
1217
+ entries = await fs3.readdir(sessionsDir());
1218
+ } catch {
1219
+ return 0;
1220
+ }
1221
+ let removed = 0;
1222
+ for (const entry of entries) {
1223
+ if (entry === excludeId) continue;
1224
+ await fs3.rm(path4.join(sessionsDir(), entry), { recursive: true, force: true });
1225
+ removed += 1;
1226
+ }
1227
+ const survivors = await SessionStore.list();
1228
+ await pruneTreeReposExcept(survivors.map((meta) => meta.cwd));
1229
+ return removed;
1230
+ }
1231
+
1232
+ // src/session/list.ts
1233
+ import fs4 from "fs/promises";
1234
+ import path5 from "path";
1235
+ var COUNT_CHUNK_BYTES = 64 * 1024;
1236
+ async function countMessages(id) {
1237
+ let handle = null;
1238
+ try {
1239
+ handle = await fs4.open(path5.join(sessionsDir(), id, "messages.jsonl"), "r");
1240
+ const buffer = Buffer.alloc(COUNT_CHUNK_BYTES);
1241
+ let count = 0;
1242
+ let lineHasContent = false;
1243
+ while (true) {
1244
+ const { bytesRead } = await handle.read(buffer, 0, COUNT_CHUNK_BYTES, null);
1245
+ if (bytesRead === 0) break;
1246
+ for (const byte of buffer.subarray(0, bytesRead)) {
1247
+ if (byte === 10) {
1248
+ if (lineHasContent) count += 1;
1249
+ lineHasContent = false;
1250
+ } else if (byte !== 13 && byte !== 32 && byte !== 9) {
1251
+ lineHasContent = true;
1252
+ }
1253
+ }
1254
+ }
1255
+ if (lineHasContent) count += 1;
1256
+ return count;
1257
+ } catch {
1258
+ return 0;
1259
+ } finally {
1260
+ await handle?.close().catch(() => {
1261
+ });
1262
+ }
1263
+ }
1264
+ async function listSessionEntries(cwd) {
1265
+ const metas = await SessionStore.list(cwd);
1266
+ return Promise.all(
1267
+ metas.map(async (meta) => ({ meta, messageCount: await countMessages(meta.id) }))
1268
+ );
1269
+ }
1270
+ async function findLatestSession(cwd) {
1271
+ const [latest] = await SessionStore.list(cwd);
1272
+ return latest ?? null;
1273
+ }
1274
+ async function sessionPreview(id, maxBytes = 4096) {
1275
+ let handle = null;
1276
+ try {
1277
+ handle = await fs4.open(path5.join(sessionsDir(), id, "messages.jsonl"), "r");
1278
+ const buffer = Buffer.alloc(maxBytes);
1279
+ const { bytesRead } = await handle.read(buffer, 0, maxBytes, 0);
1280
+ const head = buffer.toString("utf8", 0, bytesRead);
1281
+ for (const line of head.split("\n")) {
1282
+ const trimmed = line.trim();
1283
+ if (!trimmed) continue;
1284
+ try {
1285
+ const message = JSON.parse(trimmed);
1286
+ if (message.role !== "user") continue;
1287
+ const text = typeof message.content === "string" ? message.content : message.content.filter((part) => part.type === "text").map((part) => part.type === "text" ? part.text : "").join(" ");
1288
+ const collapsed = text.replace(/\s+/g, " ").trim();
1289
+ if (collapsed) return collapsed;
1290
+ } catch {
1291
+ }
1292
+ }
1293
+ return null;
1294
+ } catch {
1295
+ return null;
1296
+ } finally {
1297
+ await handle?.close().catch(() => {
1298
+ });
1299
+ }
1300
+ }
1301
+ function shortSessionId(id) {
1302
+ const dash = id.lastIndexOf("-");
1303
+ return dash === -1 ? id : id.slice(dash + 1);
1304
+ }
1305
+ async function resolveSessionId(query) {
1306
+ const metas = await SessionStore.list();
1307
+ const exact = metas.find((meta) => meta.id === query);
1308
+ if (exact) return exact.id;
1309
+ const matches = metas.filter(
1310
+ (meta) => shortSessionId(meta.id) === query || meta.id.startsWith(query)
1311
+ );
1312
+ return matches.length === 1 ? matches[0]?.id ?? null : null;
1313
+ }
1314
+ function relativeTime(timestamp) {
1315
+ const diff = Date.now() - timestamp;
1316
+ const minutes = Math.floor(diff / 6e4);
1317
+ if (minutes < 1) return "just now";
1318
+ if (minutes < 60) return `${minutes}m ago`;
1319
+ const hours = Math.floor(minutes / 60);
1320
+ if (hours < 24) return `${hours}h ago`;
1321
+ const days = Math.floor(hours / 24);
1322
+ if (days < 30) return `${days}d ago`;
1323
+ const months = Math.floor(days / 30);
1324
+ if (months < 12) return `${months}mo ago`;
1325
+ return `${Math.floor(months / 12)}y ago`;
1326
+ }
1327
+ function formatSessionEntries(entries, options = {}) {
1328
+ return entries.map(({ meta, messageCount }) => {
1329
+ const title = meta.title || "(untitled)";
1330
+ const base = `${shortSessionId(meta.id)} ${title} ${messageCount} messages updated ${relativeTime(meta.updatedAt)}`;
1331
+ return options.showCwd ? `${base} [${meta.cwd}]` : base;
1332
+ }).join("\n");
1333
+ }
1334
+
1335
+ // src/version.ts
1336
+ import fs5 from "fs";
1337
+ import path6 from "path";
1338
+ import { fileURLToPath } from "url";
1339
+ function readPackageVersion() {
1340
+ try {
1341
+ const pkgPath = path6.join(path6.dirname(fileURLToPath(import.meta.url)), "..", "package.json");
1342
+ const pkg = JSON.parse(fs5.readFileSync(pkgPath, "utf8"));
1343
+ if (typeof pkg === "object" && pkg !== null) {
1344
+ const version = pkg.version;
1345
+ if (typeof version === "string") return version;
1346
+ }
1347
+ } catch {
1348
+ }
1349
+ return "0.0.0";
1350
+ }
1351
+ var VERSION = readPackageVersion();
1352
+
1353
+ // src/config/env.ts
1354
+ import fs6 from "fs";
1355
+ var ENV_KEY_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
1356
+ function parseEnvContent(content) {
1357
+ const vars = {};
1358
+ for (const rawLine of content.split(/\r?\n/)) {
1359
+ const line = rawLine.trim();
1360
+ if (!line || line.startsWith("#")) continue;
1361
+ const eq = line.indexOf("=");
1362
+ if (eq <= 0) continue;
1363
+ const key = line.slice(0, eq).trim();
1364
+ if (!ENV_KEY_PATTERN.test(key)) continue;
1365
+ let value = line.slice(eq + 1).trim();
1366
+ if (value.length >= 2 && (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'"))) {
1367
+ value = value.slice(1, -1);
1368
+ }
1369
+ vars[key] = value;
1370
+ }
1371
+ return vars;
1372
+ }
1373
+ function loadEnvFile(filePath = envFilePath()) {
1374
+ let content;
1375
+ try {
1376
+ content = fs6.readFileSync(filePath, "utf8");
1377
+ } catch {
1378
+ return;
1379
+ }
1380
+ for (const [key, value] of Object.entries(parseEnvContent(content))) {
1381
+ if (process.env[key] === void 0) {
1382
+ process.env[key] = value;
1383
+ }
1384
+ }
1385
+ }
1386
+ function upsertEnvContent(content, key, value) {
1387
+ const entry = `${key}=${value.replace(/[\r\n]/g, "")}`;
1388
+ const pattern = new RegExp(`^\\s*${key}\\s*=`);
1389
+ const lines = content.split("\n");
1390
+ while (lines.length > 0 && (lines[lines.length - 1] ?? "").trim() === "") lines.pop();
1391
+ for (let i = 0; i < lines.length; i++) {
1392
+ if (pattern.test(lines[i] ?? "")) {
1393
+ lines[i] = entry;
1394
+ return `${lines.join("\n")}
1395
+ `;
1396
+ }
1397
+ }
1398
+ lines.push(entry);
1399
+ return `${lines.join("\n")}
1400
+ `;
1401
+ }
1402
+
1403
+ export {
1404
+ formatDollars,
1405
+ formatTokens,
1406
+ computeCostUsd,
1407
+ estimateCost,
1408
+ renderMarkdown,
1409
+ toTerminalSafe,
1410
+ summarizeArgs,
1411
+ previewLines,
1412
+ formatByteCount,
1413
+ formatElapsedSeconds,
1414
+ splitCommittableLines,
1415
+ formatStreamError,
1416
+ coreMessageText,
1417
+ buildDisplayMessages,
1418
+ MAX_IMAGE_BYTES,
1419
+ imageMimeType,
1420
+ readImageInput,
1421
+ resolveMentions,
1422
+ SYSTEM_PROMPT,
1423
+ loadEnvFile,
1424
+ upsertEnvContent,
1425
+ listModels,
1426
+ resolveModelConfig,
1427
+ createModel,
1428
+ reasoningEffortMetadata,
1429
+ loadSessionSnapshots,
1430
+ SessionStore,
1431
+ clearSessions,
1432
+ listSessionEntries,
1433
+ findLatestSession,
1434
+ sessionPreview,
1435
+ shortSessionId,
1436
+ resolveSessionId,
1437
+ relativeTime,
1438
+ formatSessionEntries,
1439
+ VERSION
1440
+ };
1441
+ //# sourceMappingURL=chunk-4X25DNTF.js.map