@cryer/star-cli 0.3.7 → 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.
@@ -1,15 +1,16 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ coreMessageText,
4
+ pruneTreeReposExcept,
5
+ removeTreeRepo
6
+ } from "./chunk-56ZIA57D.js";
2
7
  import {
3
8
  SKIP_DIRS,
4
9
  createIgnorePredicate,
5
10
  downsampleImageIfNeeded,
6
- envFilePath,
7
11
  isSensitivePath,
8
- pruneTreeReposExcept,
9
- removeTreeRepo,
10
- sessionsDir,
11
- wrapFetchWithActivity
12
- } from "./chunk-OOOW7FC4.js";
12
+ sessionsDir
13
+ } from "./chunk-A5PXPWHB.js";
13
14
 
14
15
  // src/cli/cost.ts
15
16
  function formatDollars(cost) {
@@ -53,6 +54,70 @@ function estimateCost(usage, modelName, model) {
53
54
  return `Estimated cost: $${formatDollars(cost)} (${model?.name} @ $${model?.promptPrice}/M prompt, $${model?.completionPrice}/M completion${cachePrice})`;
54
55
  }
55
56
 
57
+ // src/cli/icons.ts
58
+ var EMOJI_TERM_PROGRAMS = /* @__PURE__ */ new Set([
59
+ "vscode",
60
+ "WezTerm",
61
+ "iTerm.app",
62
+ "Apple_Terminal",
63
+ "ghostty",
64
+ "kitty",
65
+ "Hyper",
66
+ "WarpTerminal"
67
+ ]);
68
+ function resolveIconMode(env = process.env) {
69
+ const override = env.STAR_ICONS;
70
+ if (override === "emoji" || override === "plain") return override;
71
+ if (env.WT_SESSION) return "emoji";
72
+ if (EMOJI_TERM_PROGRAMS.has(env.TERM_PROGRAM ?? "")) return "emoji";
73
+ const term = env.TERM ?? "";
74
+ if (term.includes("kitty") || term.includes("alacritty")) return "emoji";
75
+ return "plain";
76
+ }
77
+ var EMOJI_TOOL_ICONS = {
78
+ bash: "\u{1F4BB}",
79
+ read_file: "\u{1F4C4}",
80
+ write_file: "\u{1F4DD}",
81
+ edit_file: "\u270F\uFE0F",
82
+ glob: "\u{1F4C2}",
83
+ grep: "\u{1F50D}",
84
+ web_search: "\u{1F50E}",
85
+ web_fetch: "\u{1F310}",
86
+ todo_write: "\u{1F4CB}",
87
+ todo_read: "\u{1F4CB}",
88
+ task_list: "\u{1F9F5}",
89
+ task_output: "\u{1F9F5}",
90
+ task_kill: "\u{1F6D1}",
91
+ subagent: "\u{1F916}",
92
+ skill: "\u2728",
93
+ remember: "\u{1F9E0}"
94
+ };
95
+ var PLAIN_TOOL_ICONS = {
96
+ bash: "$",
97
+ read_file: "\u2261",
98
+ write_file: "+",
99
+ edit_file: "\u270E",
100
+ glob: "*",
101
+ grep: "/",
102
+ web_search: "?",
103
+ web_fetch: "\u21D3",
104
+ todo_write: "\u2023",
105
+ todo_read: "\u2023",
106
+ task_list: "\xB6",
107
+ task_output: "\xB6",
108
+ task_kill: "\xD7",
109
+ subagent: "\u25C9",
110
+ skill: "\u2727",
111
+ remember: "\u203B"
112
+ };
113
+ var iconMode = resolveIconMode();
114
+ function toolIcon(name) {
115
+ const map = iconMode === "emoji" ? EMOJI_TOOL_ICONS : PLAIN_TOOL_ICONS;
116
+ return map[name] ?? (iconMode === "emoji" ? "\u{1F527}" : "\u25B8");
117
+ }
118
+ var thinkingIcon = iconMode === "emoji" ? "\u{1F4AD}" : "\u2727";
119
+ var folderIcon = iconMode === "emoji" ? "\u{1F4C1} " : "";
120
+
56
121
  // src/cli/terminal-text.ts
57
122
  var UNSAFE_CHARS = /[\x00-\x09\x0b-\x1f\x7f]/;
58
123
  var CONTROL_CHARS = /[\x00-\x08\x0b-\x1f\x7f]/g;
@@ -298,25 +363,48 @@ function formatStreamError(error) {
298
363
  }
299
364
  return error.message;
300
365
  }
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
366
  function buildDisplayMessages(messages) {
310
367
  const display = [];
311
368
  let collapsed = 0;
312
369
  for (const message of messages) {
313
370
  if (message.role === "user" || message.role === "assistant") {
314
- const text = coreMessageText(message);
315
- if (text) {
371
+ const content = message.content;
372
+ if (!Array.isArray(content)) {
373
+ const text = coreMessageText(message);
374
+ if (text) {
375
+ display.push({ id: display.length, role: message.role, text: toTerminalSafe(text) });
376
+ } else {
377
+ collapsed++;
378
+ }
379
+ continue;
380
+ }
381
+ let textParts = [];
382
+ let represented = false;
383
+ const flushText = () => {
384
+ const text = textParts.join(" ");
385
+ textParts = [];
386
+ if (!text) return;
387
+ represented = true;
316
388
  display.push({ id: display.length, role: message.role, text: toTerminalSafe(text) });
317
- } else {
318
- collapsed++;
389
+ };
390
+ for (const part of content) {
391
+ if (part.type === "text" && "text" in part) {
392
+ textParts.push(part.text);
393
+ } else if (part.type === "tool-call") {
394
+ flushText();
395
+ represented = true;
396
+ display.push({
397
+ id: display.length,
398
+ role: "tool",
399
+ text: toTerminalSafe(
400
+ `${toolIcon(part.toolName)} ${part.toolName} ${summarizeArgs(part.args)}`
401
+ ),
402
+ dim: true
403
+ });
404
+ }
319
405
  }
406
+ flushText();
407
+ if (!represented) collapsed++;
320
408
  } else if (message.role === "tool") {
321
409
  collapsed++;
322
410
  }
@@ -529,292 +617,15 @@ ${buf.toString("utf8")}
529
617
  return { input, attached, skipped, images };
530
618
  }
531
619
 
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
620
  // 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
621
  import fs2 from "fs/promises";
817
622
  import path3 from "path";
623
+
624
+ // src/session/store.ts
625
+ import { randomUUID } from "crypto";
626
+ import { existsSync } from "fs";
627
+ import fs from "fs/promises";
628
+ import path2 from "path";
818
629
  import { StringDecoder } from "string_decoder";
819
630
  function generateId(now) {
820
631
  const pad = (n) => String(n).padStart(2, "0");
@@ -822,6 +633,24 @@ function generateId(now) {
822
633
  const suffix = Math.random().toString(16).slice(2, 8).padEnd(6, "0");
823
634
  return `${stamp}-${suffix}`;
824
635
  }
636
+ var ID_COLLISION_MAX_ATTEMPTS = 5;
637
+ function generateUniqueId() {
638
+ let id = generateId(/* @__PURE__ */ new Date());
639
+ for (let attempt = 1; existsSync(path2.join(sessionsDir(), id)); attempt++) {
640
+ if (attempt >= ID_COLLISION_MAX_ATTEMPTS) {
641
+ const stamp = id.slice(0, id.lastIndexOf("-"));
642
+ return `${stamp}-${randomUUID().replaceAll("-", "").slice(0, 12)}`;
643
+ }
644
+ id = generateId(/* @__PURE__ */ new Date());
645
+ }
646
+ return id;
647
+ }
648
+ function normalizeCwdForCompare(cwd, platform = process.platform) {
649
+ if (platform !== "win32") return cwd;
650
+ const resolved = path2.win32.resolve(cwd);
651
+ const trimmed = resolved.replace(/[\\/]+$/, "");
652
+ return (trimmed === "" ? resolved : trimmed).toLowerCase();
653
+ }
825
654
  function debugWarn(message) {
826
655
  if (process.env.STAR_DEBUG === "1") process.stderr.write(`[star-cli] ${message}
827
656
  `);
@@ -861,6 +690,23 @@ function isFallbackMeta(meta) {
861
690
  }
862
691
  var META_FLUSH_INTERVAL_MS = 2e3;
863
692
  var MESSAGES_READ_CHUNK_BYTES = 64 * 1024;
693
+ var STAR_META_KEY = "__starMeta";
694
+ function serializeMessage(star) {
695
+ if (!star.meta) return JSON.stringify(star.message);
696
+ return JSON.stringify({ ...star.message, [STAR_META_KEY]: star.meta });
697
+ }
698
+ function parseMessageLine(line) {
699
+ try {
700
+ const parsed = JSON.parse(line);
701
+ if (parsed !== null && typeof parsed === "object" && STAR_META_KEY in parsed) {
702
+ const { [STAR_META_KEY]: meta, ...message } = parsed;
703
+ return { message, meta };
704
+ }
705
+ return { message: parsed };
706
+ } catch {
707
+ return null;
708
+ }
709
+ }
864
710
  function dayKey(date) {
865
711
  const pad = (n) => String(n).padStart(2, "0");
866
712
  return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
@@ -879,16 +725,16 @@ var SessionStore = class _SessionStore {
879
725
  metaDirty = false;
880
726
  constructor(id) {
881
727
  this.id = id;
882
- this.dir = path3.join(sessionsDir(), id);
728
+ this.dir = path2.join(sessionsDir(), id);
883
729
  }
884
730
  metaPath() {
885
- return path3.join(this.dir, "meta.json");
731
+ return path2.join(this.dir, "meta.json");
886
732
  }
887
733
  messagesPath() {
888
- return path3.join(this.dir, "messages.jsonl");
734
+ return path2.join(this.dir, "messages.jsonl");
889
735
  }
890
736
  static async create(cwd, model) {
891
- const store = new _SessionStore(generateId(/* @__PURE__ */ new Date()));
737
+ const store = new _SessionStore(generateUniqueId());
892
738
  store.pendingMeta = { cwd, model, createdAt: Date.now() };
893
739
  return store;
894
740
  }
@@ -896,7 +742,7 @@ var SessionStore = class _SessionStore {
896
742
  const store = new _SessionStore(id);
897
743
  let raw;
898
744
  try {
899
- raw = await fs2.readFile(store.metaPath(), "utf8");
745
+ raw = await fs.readFile(store.metaPath(), "utf8");
900
746
  } catch {
901
747
  return null;
902
748
  }
@@ -913,16 +759,19 @@ var SessionStore = class _SessionStore {
913
759
  static async list(cwd) {
914
760
  let entries;
915
761
  try {
916
- entries = await fs2.readdir(sessionsDir());
762
+ entries = await fs.readdir(sessionsDir());
917
763
  } catch {
918
764
  return [];
919
765
  }
766
+ const wanted = cwd === void 0 ? void 0 : normalizeCwdForCompare(cwd);
920
767
  const metas = await Promise.all(
921
768
  entries.map(async (entry) => {
922
769
  try {
923
- const raw = await fs2.readFile(path3.join(sessionsDir(), entry, "meta.json"), "utf8");
770
+ const raw = await fs.readFile(path2.join(sessionsDir(), entry, "meta.json"), "utf8");
924
771
  const meta = JSON.parse(raw);
925
- if (meta.id === entry && (cwd === void 0 || meta.cwd === cwd)) return meta;
772
+ if (meta.id === entry && (wanted === void 0 || normalizeCwdForCompare(meta.cwd) === wanted)) {
773
+ return meta;
774
+ }
926
775
  } catch {
927
776
  }
928
777
  return null;
@@ -945,11 +794,11 @@ var SessionStore = class _SessionStore {
945
794
  const run = this.metaWriteQueue.then(async () => {
946
795
  try {
947
796
  await withTransientFsRetry(async () => {
948
- await fs2.writeFile(tmp, JSON.stringify(meta, null, 2));
949
- await fs2.rename(tmp, this.metaPath());
797
+ await fs.writeFile(tmp, JSON.stringify(meta, null, 2));
798
+ await fs.rename(tmp, this.metaPath());
950
799
  });
951
800
  } catch (error) {
952
- await fs2.unlink(tmp).catch(() => {
801
+ await fs.unlink(tmp).catch(() => {
953
802
  });
954
803
  debugWarn(
955
804
  `session ${this.id}: meta write failed (${error.code ?? String(error)}), skipping`
@@ -1008,7 +857,7 @@ var SessionStore = class _SessionStore {
1008
857
  this.initialized = true;
1009
858
  return;
1010
859
  }
1011
- await fs2.mkdir(this.dir, { recursive: true, mode: 448 });
860
+ await fs.mkdir(this.dir, { recursive: true, mode: 448 });
1012
861
  await this.sweepTmpFiles();
1013
862
  const meta = {
1014
863
  id: this.id,
@@ -1029,22 +878,22 @@ var SessionStore = class _SessionStore {
1029
878
  async sweepTmpFiles() {
1030
879
  let entries;
1031
880
  try {
1032
- entries = await fs2.readdir(this.dir);
881
+ entries = await fs.readdir(this.dir);
1033
882
  } catch {
1034
883
  return;
1035
884
  }
1036
885
  await Promise.all(
1037
886
  entries.filter(
1038
887
  (entry) => entry.startsWith("meta.json.tmp-") || entry.startsWith("messages.jsonl.tmp-")
1039
- ).map((entry) => fs2.unlink(path3.join(this.dir, entry)).catch(() => {
888
+ ).map((entry) => fs.unlink(path2.join(this.dir, entry)).catch(() => {
1040
889
  }))
1041
890
  );
1042
891
  }
1043
- async append(message) {
892
+ async append(message, meta) {
1044
893
  await this.ensureInitialized();
1045
894
  try {
1046
895
  await withTransientFsRetry(
1047
- () => fs2.appendFile(this.messagesPath(), `${JSON.stringify(message)}
896
+ () => fs.appendFile(this.messagesPath(), `${serializeMessage({ message, meta })}
1048
897
  `)
1049
898
  );
1050
899
  } catch (error) {
@@ -1052,22 +901,22 @@ var SessionStore = class _SessionStore {
1052
901
  `session ${this.id}: message append failed (${error.code ?? String(error)}), message kept in memory only`
1053
902
  );
1054
903
  }
1055
- const meta = await this.meta();
1056
- meta.updatedAt = Date.now();
904
+ const sessionMeta = await this.meta();
905
+ sessionMeta.updatedAt = Date.now();
1057
906
  this.markMetaDirty();
1058
907
  }
1059
908
  async replaceMessages(messages) {
1060
909
  await this.ensureInitialized();
1061
- const content = messages.map((message) => JSON.stringify(message)).join("\n");
910
+ const content = messages.map(serializeMessage).join("\n");
1062
911
  const tmp = `${this.messagesPath()}.tmp-${process.pid}-${this.messagesTmpSeq++}`;
1063
912
  try {
1064
913
  await withTransientFsRetry(async () => {
1065
- await fs2.writeFile(tmp, content ? `${content}
914
+ await fs.writeFile(tmp, content ? `${content}
1066
915
  ` : "");
1067
- await fs2.rename(tmp, this.messagesPath());
916
+ await fs.rename(tmp, this.messagesPath());
1068
917
  });
1069
918
  } catch (error) {
1070
- await fs2.unlink(tmp).catch(() => {
919
+ await fs.unlink(tmp).catch(() => {
1071
920
  });
1072
921
  warn(
1073
922
  `session ${this.id}: history rewrite failed (${error.code ?? String(error)}), kept the previous messages.jsonl`
@@ -1078,9 +927,12 @@ var SessionStore = class _SessionStore {
1078
927
  await this.flushMetaNow();
1079
928
  }
1080
929
  async messages() {
930
+ return (await this.starMessages()).map((star) => star.message);
931
+ }
932
+ async starMessages() {
1081
933
  let handle = null;
1082
934
  try {
1083
- handle = await fs2.open(this.messagesPath(), "r");
935
+ handle = await fs.open(this.messagesPath(), "r");
1084
936
  const messages = [];
1085
937
  const decoder = new StringDecoder("utf8");
1086
938
  const buffer = Buffer.alloc(MESSAGES_READ_CHUNK_BYTES);
@@ -1088,9 +940,10 @@ var SessionStore = class _SessionStore {
1088
940
  const pushLine = (line) => {
1089
941
  const trimmed = line.trim();
1090
942
  if (!trimmed) return;
1091
- try {
1092
- messages.push(JSON.parse(trimmed));
1093
- } catch {
943
+ const parsed = parseMessageLine(trimmed);
944
+ if (parsed) {
945
+ messages.push(parsed);
946
+ } else {
1094
947
  debugWarn(`session ${this.id}: skipping corrupt messages.jsonl line`);
1095
948
  }
1096
949
  };
@@ -1128,7 +981,7 @@ var SessionStore = class _SessionStore {
1128
981
  await this.ensureInitialized();
1129
982
  if (this.cachedMeta) return this.cachedMeta;
1130
983
  try {
1131
- const raw = await fs2.readFile(this.metaPath(), "utf8");
984
+ const raw = await fs.readFile(this.metaPath(), "utf8");
1132
985
  this.cachedMeta = JSON.parse(raw);
1133
986
  return this.cachedMeta;
1134
987
  } catch {
@@ -1195,48 +1048,107 @@ var SessionStore = class _SessionStore {
1195
1048
  }
1196
1049
  };
1197
1050
 
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);
1051
+ // src/session/checkpoints.ts
1052
+ function checkpointsDir(sessionDir) {
1053
+ return path3.join(sessionDir, "checkpoints");
1054
+ }
1055
+ function indexPath(sessionDir) {
1056
+ return path3.join(checkpointsDir(sessionDir), "index.json");
1057
+ }
1058
+ var indexWriteQueues = /* @__PURE__ */ new Map();
1059
+ function enqueueIndexWrite(sessionDir, op) {
1060
+ const queued = indexWriteQueues.get(sessionDir) ?? Promise.resolve();
1061
+ const run = queued.then(op);
1062
+ const tail = run.then(
1063
+ () => {
1064
+ },
1065
+ () => {
1212
1066
  }
1213
- return removed2;
1067
+ );
1068
+ indexWriteQueues.set(sessionDir, tail);
1069
+ void tail.then(() => {
1070
+ if (indexWriteQueues.get(sessionDir) === tail) indexWriteQueues.delete(sessionDir);
1071
+ });
1072
+ return run;
1073
+ }
1074
+ var indexTmpSeq = 0;
1075
+ async function writeIndex(sessionDir, records) {
1076
+ const target = indexPath(sessionDir);
1077
+ const tmp = `${target}.tmp-${process.pid}-${indexTmpSeq++}`;
1078
+ try {
1079
+ await withTransientFsRetry(async () => {
1080
+ await fs2.writeFile(tmp, JSON.stringify(records, null, 2));
1081
+ await fs2.rename(tmp, target);
1082
+ });
1083
+ } catch (error) {
1084
+ await fs2.unlink(tmp).catch(() => {
1085
+ });
1086
+ throw error;
1214
1087
  }
1215
- let entries;
1088
+ }
1089
+ function checkpointContentPath(sessionDir, id) {
1090
+ return path3.join(checkpointsDir(sessionDir), `${id}.snapshot`);
1091
+ }
1092
+ async function listCheckpointRecords(sessionDir) {
1093
+ let raw;
1216
1094
  try {
1217
- entries = await fs3.readdir(sessionsDir());
1095
+ raw = await fs2.readFile(indexPath(sessionDir), "utf8");
1218
1096
  } catch {
1219
- return 0;
1097
+ return [];
1220
1098
  }
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;
1099
+ try {
1100
+ const parsed = JSON.parse(raw);
1101
+ return Array.isArray(parsed) ? parsed : [];
1102
+ } catch {
1103
+ process.stderr.write(
1104
+ `[star-cli] checkpoints index unreadable for session ${path3.basename(sessionDir)}, ignoring it
1105
+ `
1106
+ );
1107
+ return [];
1226
1108
  }
1227
- const survivors = await SessionStore.list();
1228
- await pruneTreeReposExcept(survivors.map((meta) => meta.cwd));
1229
- return removed;
1109
+ }
1110
+ async function appendCheckpointRecord(sessionDir, record, content) {
1111
+ await enqueueIndexWrite(sessionDir, async () => {
1112
+ await fs2.mkdir(checkpointsDir(sessionDir), { recursive: true, mode: 448 });
1113
+ if (record.existed && content !== null) {
1114
+ await fs2.writeFile(checkpointContentPath(sessionDir, record.id), content, "utf8");
1115
+ }
1116
+ const records = await listCheckpointRecords(sessionDir);
1117
+ records.push(record);
1118
+ await writeIndex(sessionDir, records);
1119
+ });
1120
+ }
1121
+ async function removeCheckpointRecords(sessionDir, ids) {
1122
+ if (ids.length === 0) return;
1123
+ await enqueueIndexWrite(sessionDir, async () => {
1124
+ const drop = new Set(ids);
1125
+ const records = await listCheckpointRecords(sessionDir);
1126
+ const kept = records.filter((record) => !drop.has(record.id));
1127
+ if (kept.length === records.length) return;
1128
+ await fs2.mkdir(checkpointsDir(sessionDir), { recursive: true, mode: 448 });
1129
+ await writeIndex(sessionDir, kept);
1130
+ for (const id of ids) {
1131
+ await fs2.rm(checkpointContentPath(sessionDir, id), { force: true });
1132
+ }
1133
+ });
1134
+ }
1135
+ async function loadSessionSnapshots(sessionDir) {
1136
+ const records = await listCheckpointRecords(sessionDir);
1137
+ return records.map((record) => ({
1138
+ ...record,
1139
+ content: null,
1140
+ contentFile: record.existed && !record.contentTooLarge ? checkpointContentPath(sessionDir, record.id) : void 0
1141
+ }));
1230
1142
  }
1231
1143
 
1232
1144
  // src/session/list.ts
1233
- import fs4 from "fs/promises";
1234
- import path5 from "path";
1145
+ import fs3 from "fs/promises";
1146
+ import path4 from "path";
1235
1147
  var COUNT_CHUNK_BYTES = 64 * 1024;
1236
1148
  async function countMessages(id) {
1237
1149
  let handle = null;
1238
1150
  try {
1239
- handle = await fs4.open(path5.join(sessionsDir(), id, "messages.jsonl"), "r");
1151
+ handle = await fs3.open(path4.join(sessionsDir(), id, "messages.jsonl"), "r");
1240
1152
  const buffer = Buffer.alloc(COUNT_CHUNK_BYTES);
1241
1153
  let count = 0;
1242
1154
  let lineHasContent = false;
@@ -1261,11 +1173,24 @@ async function countMessages(id) {
1261
1173
  });
1262
1174
  }
1263
1175
  }
1176
+ var LIST_COUNT_CONCURRENCY = 8;
1264
1177
  async function listSessionEntries(cwd) {
1265
1178
  const metas = await SessionStore.list(cwd);
1266
- return Promise.all(
1267
- metas.map(async (meta) => ({ meta, messageCount: await countMessages(meta.id) }))
1179
+ const entries = [];
1180
+ let next = 0;
1181
+ const worker = async () => {
1182
+ while (next < metas.length) {
1183
+ const index = next;
1184
+ next += 1;
1185
+ const meta = metas[index];
1186
+ if (!meta) continue;
1187
+ entries[index] = { meta, messageCount: await countMessages(meta.id) };
1188
+ }
1189
+ };
1190
+ await Promise.all(
1191
+ Array.from({ length: Math.min(LIST_COUNT_CONCURRENCY, metas.length) }, () => worker())
1268
1192
  );
1193
+ return entries;
1269
1194
  }
1270
1195
  async function findLatestSession(cwd) {
1271
1196
  const [latest] = await SessionStore.list(cwd);
@@ -1274,7 +1199,7 @@ async function findLatestSession(cwd) {
1274
1199
  async function sessionPreview(id, maxBytes = 4096) {
1275
1200
  let handle = null;
1276
1201
  try {
1277
- handle = await fs4.open(path5.join(sessionsDir(), id, "messages.jsonl"), "r");
1202
+ handle = await fs3.open(path4.join(sessionsDir(), id, "messages.jsonl"), "r");
1278
1203
  const buffer = Buffer.alloc(maxBytes);
1279
1204
  const { bytesRead } = await handle.read(buffer, 0, maxBytes, 0);
1280
1205
  const head = buffer.toString("utf8", 0, bytesRead);
@@ -1332,6 +1257,52 @@ function formatSessionEntries(entries, options = {}) {
1332
1257
  }).join("\n");
1333
1258
  }
1334
1259
 
1260
+ // src/cli/system-prompt.ts
1261
+ var SYSTEM_PROMPT = `You are Star CLI, an AI coding agent running in the user's terminal.
1262
+ You help with software engineering tasks: reading, writing and editing code, running shell commands, and managing todos.
1263
+ Be concise and direct. Use tools when they help accomplish the task.
1264
+ 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.
1265
+ The working directory is the user's project root; never touch files outside it without explicit instruction.
1266
+ 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.
1267
+ 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.
1268
+ 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.
1269
+ 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.
1270
+ 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.`;
1271
+
1272
+ // src/session/clear.ts
1273
+ import fs4 from "fs/promises";
1274
+ import path5 from "path";
1275
+ async function clearSessions(cwd, excludeId) {
1276
+ if (cwd !== void 0) {
1277
+ const metas = await SessionStore.list(cwd);
1278
+ let removed2 = 0;
1279
+ for (const meta of metas) {
1280
+ if (meta.id === excludeId) continue;
1281
+ await fs4.rm(path5.join(sessionsDir(), meta.id), { recursive: true, force: true });
1282
+ removed2 += 1;
1283
+ }
1284
+ if ((await SessionStore.list(cwd)).length === 0) {
1285
+ await removeTreeRepo(cwd);
1286
+ }
1287
+ return removed2;
1288
+ }
1289
+ let entries;
1290
+ try {
1291
+ entries = await fs4.readdir(sessionsDir());
1292
+ } catch {
1293
+ return 0;
1294
+ }
1295
+ let removed = 0;
1296
+ for (const entry of entries) {
1297
+ if (entry === excludeId) continue;
1298
+ await fs4.rm(path5.join(sessionsDir(), entry), { recursive: true, force: true });
1299
+ removed += 1;
1300
+ }
1301
+ const survivors = await SessionStore.list();
1302
+ await pruneTreeReposExcept(survivors.map((meta) => meta.cwd));
1303
+ return removed;
1304
+ }
1305
+
1335
1306
  // src/version.ts
1336
1307
  import fs5 from "fs";
1337
1308
  import path6 from "path";
@@ -1350,61 +1321,14 @@ function readPackageVersion() {
1350
1321
  }
1351
1322
  var VERSION = readPackageVersion();
1352
1323
 
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
1324
  export {
1404
1325
  formatDollars,
1405
1326
  formatTokens,
1406
1327
  computeCostUsd,
1407
1328
  estimateCost,
1329
+ toolIcon,
1330
+ thinkingIcon,
1331
+ folderIcon,
1408
1332
  renderMarkdown,
1409
1333
  toTerminalSafe,
1410
1334
  summarizeArgs,
@@ -1413,22 +1337,13 @@ export {
1413
1337
  formatElapsedSeconds,
1414
1338
  splitCommittableLines,
1415
1339
  formatStreamError,
1416
- coreMessageText,
1417
1340
  buildDisplayMessages,
1418
1341
  MAX_IMAGE_BYTES,
1419
1342
  imageMimeType,
1420
1343
  readImageInput,
1421
1344
  resolveMentions,
1422
- SYSTEM_PROMPT,
1423
- loadEnvFile,
1424
- upsertEnvContent,
1425
- listModels,
1426
- resolveModelConfig,
1427
- createModel,
1428
- reasoningEffortMetadata,
1429
1345
  loadSessionSnapshots,
1430
1346
  SessionStore,
1431
- clearSessions,
1432
1347
  listSessionEntries,
1433
1348
  findLatestSession,
1434
1349
  sessionPreview,
@@ -1436,6 +1351,8 @@ export {
1436
1351
  resolveSessionId,
1437
1352
  relativeTime,
1438
1353
  formatSessionEntries,
1354
+ SYSTEM_PROMPT,
1355
+ clearSessions,
1439
1356
  VERSION
1440
1357
  };
1441
- //# sourceMappingURL=chunk-4X25DNTF.js.map
1358
+ //# sourceMappingURL=chunk-VJFWTXWN.js.map