@cryer/star-cli 0.3.8 → 0.3.10

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,14 +1,16 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ coreMessageText,
4
+ pruneTreeReposExcept,
5
+ removeTreeRepo
6
+ } from "./chunk-IJGUW4PM.js";
2
7
  import {
3
8
  SKIP_DIRS,
4
9
  createIgnorePredicate,
5
10
  downsampleImageIfNeeded,
6
- envFilePath,
7
11
  isSensitivePath,
8
- pruneTreeReposExcept,
9
- removeTreeRepo,
10
12
  sessionsDir
11
- } from "./chunk-OHLA2DP7.js";
13
+ } from "./chunk-A5PXPWHB.js";
12
14
 
13
15
  // src/cli/cost.ts
14
16
  function formatDollars(cost) {
@@ -52,6 +54,70 @@ function estimateCost(usage, modelName, model) {
52
54
  return `Estimated cost: $${formatDollars(cost)} (${model?.name} @ $${model?.promptPrice}/M prompt, $${model?.completionPrice}/M completion${cachePrice})`;
53
55
  }
54
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
+
55
121
  // src/cli/terminal-text.ts
56
122
  var UNSAFE_CHARS = /[\x00-\x09\x0b-\x1f\x7f]/;
57
123
  var CONTROL_CHARS = /[\x00-\x08\x0b-\x1f\x7f]/g;
@@ -297,25 +363,48 @@ function formatStreamError(error) {
297
363
  }
298
364
  return error.message;
299
365
  }
300
- function coreMessageText(message) {
301
- const content = message.content;
302
- if (typeof content === "string") return content;
303
- if (Array.isArray(content)) {
304
- return content.filter((part) => part.type === "text").map((part) => "text" in part ? part.text : "").join(" ");
305
- }
306
- return "";
307
- }
308
366
  function buildDisplayMessages(messages) {
309
367
  const display = [];
310
368
  let collapsed = 0;
311
369
  for (const message of messages) {
312
370
  if (message.role === "user" || message.role === "assistant") {
313
- const text = coreMessageText(message);
314
- 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;
315
388
  display.push({ id: display.length, role: message.role, text: toTerminalSafe(text) });
316
- } else {
317
- 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
+ }
318
405
  }
406
+ flushText();
407
+ if (!represented) collapsed++;
319
408
  } else if (message.role === "tool") {
320
409
  collapsed++;
321
410
  }
@@ -528,96 +617,15 @@ ${buf.toString("utf8")}
528
617
  return { input, attached, skipped, images };
529
618
  }
530
619
 
531
- // src/cli/system-prompt.ts
532
- var SYSTEM_PROMPT = `You are Star CLI, an AI coding agent running in the user's terminal.
533
- You help with software engineering tasks: reading, writing and editing code, running shell commands, and managing todos.
534
- Be concise and direct. Use tools when they help accomplish the task.
535
- 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.
536
- The working directory is the user's project root; never touch files outside it without explicit instruction.
537
- 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.
538
- 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.
539
- 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.
540
- 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.
541
- 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.`;
542
-
543
620
  // src/session/checkpoints.ts
544
- import fs from "fs/promises";
545
- import path2 from "path";
546
- function checkpointsDir(sessionDir) {
547
- return path2.join(sessionDir, "checkpoints");
548
- }
549
- function indexPath(sessionDir) {
550
- return path2.join(checkpointsDir(sessionDir), "index.json");
551
- }
552
- var indexWriteQueues = /* @__PURE__ */ new Map();
553
- function enqueueIndexWrite(sessionDir, op) {
554
- const queued = indexWriteQueues.get(sessionDir) ?? Promise.resolve();
555
- const run = queued.then(op);
556
- indexWriteQueues.set(
557
- sessionDir,
558
- run.then(
559
- () => {
560
- },
561
- () => {
562
- }
563
- )
564
- );
565
- return run;
566
- }
567
- function checkpointContentPath(sessionDir, id) {
568
- return path2.join(checkpointsDir(sessionDir), `${id}.snapshot`);
569
- }
570
- async function listCheckpointRecords(sessionDir) {
571
- let raw;
572
- try {
573
- raw = await fs.readFile(indexPath(sessionDir), "utf8");
574
- } catch {
575
- return [];
576
- }
577
- try {
578
- const parsed = JSON.parse(raw);
579
- return Array.isArray(parsed) ? parsed : [];
580
- } catch {
581
- return [];
582
- }
583
- }
584
- async function appendCheckpointRecord(sessionDir, record, content) {
585
- await enqueueIndexWrite(sessionDir, async () => {
586
- await fs.mkdir(checkpointsDir(sessionDir), { recursive: true, mode: 448 });
587
- if (record.existed && content !== null) {
588
- await fs.writeFile(checkpointContentPath(sessionDir, record.id), content, "utf8");
589
- }
590
- const records = await listCheckpointRecords(sessionDir);
591
- records.push(record);
592
- await fs.writeFile(indexPath(sessionDir), JSON.stringify(records, null, 2));
593
- });
594
- }
595
- async function removeCheckpointRecords(sessionDir, ids) {
596
- if (ids.length === 0) return;
597
- await enqueueIndexWrite(sessionDir, async () => {
598
- const drop = new Set(ids);
599
- const records = await listCheckpointRecords(sessionDir);
600
- const kept = records.filter((record) => !drop.has(record.id));
601
- if (kept.length === records.length) return;
602
- await fs.mkdir(checkpointsDir(sessionDir), { recursive: true, mode: 448 });
603
- await fs.writeFile(indexPath(sessionDir), JSON.stringify(kept, null, 2));
604
- for (const id of ids) {
605
- await fs.rm(checkpointContentPath(sessionDir, id), { force: true });
606
- }
607
- });
608
- }
609
- async function loadSessionSnapshots(sessionDir) {
610
- const records = await listCheckpointRecords(sessionDir);
611
- return records.map((record) => ({
612
- ...record,
613
- content: null,
614
- contentFile: record.existed && !record.contentTooLarge ? checkpointContentPath(sessionDir, record.id) : void 0
615
- }));
616
- }
617
-
618
- // src/session/store.ts
619
621
  import fs2 from "fs/promises";
620
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";
621
629
  import { StringDecoder } from "string_decoder";
622
630
  function generateId(now) {
623
631
  const pad = (n) => String(n).padStart(2, "0");
@@ -625,6 +633,24 @@ function generateId(now) {
625
633
  const suffix = Math.random().toString(16).slice(2, 8).padEnd(6, "0");
626
634
  return `${stamp}-${suffix}`;
627
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
+ }
628
654
  function debugWarn(message) {
629
655
  if (process.env.STAR_DEBUG === "1") process.stderr.write(`[star-cli] ${message}
630
656
  `);
@@ -664,6 +690,23 @@ function isFallbackMeta(meta) {
664
690
  }
665
691
  var META_FLUSH_INTERVAL_MS = 2e3;
666
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
+ }
667
710
  function dayKey(date) {
668
711
  const pad = (n) => String(n).padStart(2, "0");
669
712
  return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
@@ -682,16 +725,16 @@ var SessionStore = class _SessionStore {
682
725
  metaDirty = false;
683
726
  constructor(id) {
684
727
  this.id = id;
685
- this.dir = path3.join(sessionsDir(), id);
728
+ this.dir = path2.join(sessionsDir(), id);
686
729
  }
687
730
  metaPath() {
688
- return path3.join(this.dir, "meta.json");
731
+ return path2.join(this.dir, "meta.json");
689
732
  }
690
733
  messagesPath() {
691
- return path3.join(this.dir, "messages.jsonl");
734
+ return path2.join(this.dir, "messages.jsonl");
692
735
  }
693
736
  static async create(cwd, model) {
694
- const store = new _SessionStore(generateId(/* @__PURE__ */ new Date()));
737
+ const store = new _SessionStore(generateUniqueId());
695
738
  store.pendingMeta = { cwd, model, createdAt: Date.now() };
696
739
  return store;
697
740
  }
@@ -699,7 +742,7 @@ var SessionStore = class _SessionStore {
699
742
  const store = new _SessionStore(id);
700
743
  let raw;
701
744
  try {
702
- raw = await fs2.readFile(store.metaPath(), "utf8");
745
+ raw = await fs.readFile(store.metaPath(), "utf8");
703
746
  } catch {
704
747
  return null;
705
748
  }
@@ -716,16 +759,19 @@ var SessionStore = class _SessionStore {
716
759
  static async list(cwd) {
717
760
  let entries;
718
761
  try {
719
- entries = await fs2.readdir(sessionsDir());
762
+ entries = await fs.readdir(sessionsDir());
720
763
  } catch {
721
764
  return [];
722
765
  }
766
+ const wanted = cwd === void 0 ? void 0 : normalizeCwdForCompare(cwd);
723
767
  const metas = await Promise.all(
724
768
  entries.map(async (entry) => {
725
769
  try {
726
- 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");
727
771
  const meta = JSON.parse(raw);
728
- 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
+ }
729
775
  } catch {
730
776
  }
731
777
  return null;
@@ -748,11 +794,11 @@ var SessionStore = class _SessionStore {
748
794
  const run = this.metaWriteQueue.then(async () => {
749
795
  try {
750
796
  await withTransientFsRetry(async () => {
751
- await fs2.writeFile(tmp, JSON.stringify(meta, null, 2));
752
- await fs2.rename(tmp, this.metaPath());
797
+ await fs.writeFile(tmp, JSON.stringify(meta, null, 2));
798
+ await fs.rename(tmp, this.metaPath());
753
799
  });
754
800
  } catch (error) {
755
- await fs2.unlink(tmp).catch(() => {
801
+ await fs.unlink(tmp).catch(() => {
756
802
  });
757
803
  debugWarn(
758
804
  `session ${this.id}: meta write failed (${error.code ?? String(error)}), skipping`
@@ -811,7 +857,7 @@ var SessionStore = class _SessionStore {
811
857
  this.initialized = true;
812
858
  return;
813
859
  }
814
- await fs2.mkdir(this.dir, { recursive: true, mode: 448 });
860
+ await fs.mkdir(this.dir, { recursive: true, mode: 448 });
815
861
  await this.sweepTmpFiles();
816
862
  const meta = {
817
863
  id: this.id,
@@ -832,22 +878,22 @@ var SessionStore = class _SessionStore {
832
878
  async sweepTmpFiles() {
833
879
  let entries;
834
880
  try {
835
- entries = await fs2.readdir(this.dir);
881
+ entries = await fs.readdir(this.dir);
836
882
  } catch {
837
883
  return;
838
884
  }
839
885
  await Promise.all(
840
886
  entries.filter(
841
887
  (entry) => entry.startsWith("meta.json.tmp-") || entry.startsWith("messages.jsonl.tmp-")
842
- ).map((entry) => fs2.unlink(path3.join(this.dir, entry)).catch(() => {
888
+ ).map((entry) => fs.unlink(path2.join(this.dir, entry)).catch(() => {
843
889
  }))
844
890
  );
845
891
  }
846
- async append(message) {
892
+ async append(message, meta) {
847
893
  await this.ensureInitialized();
848
894
  try {
849
895
  await withTransientFsRetry(
850
- () => fs2.appendFile(this.messagesPath(), `${JSON.stringify(message)}
896
+ () => fs.appendFile(this.messagesPath(), `${serializeMessage({ message, meta })}
851
897
  `)
852
898
  );
853
899
  } catch (error) {
@@ -855,22 +901,22 @@ var SessionStore = class _SessionStore {
855
901
  `session ${this.id}: message append failed (${error.code ?? String(error)}), message kept in memory only`
856
902
  );
857
903
  }
858
- const meta = await this.meta();
859
- meta.updatedAt = Date.now();
904
+ const sessionMeta = await this.meta();
905
+ sessionMeta.updatedAt = Date.now();
860
906
  this.markMetaDirty();
861
907
  }
862
908
  async replaceMessages(messages) {
863
909
  await this.ensureInitialized();
864
- const content = messages.map((message) => JSON.stringify(message)).join("\n");
910
+ const content = messages.map(serializeMessage).join("\n");
865
911
  const tmp = `${this.messagesPath()}.tmp-${process.pid}-${this.messagesTmpSeq++}`;
866
912
  try {
867
913
  await withTransientFsRetry(async () => {
868
- await fs2.writeFile(tmp, content ? `${content}
914
+ await fs.writeFile(tmp, content ? `${content}
869
915
  ` : "");
870
- await fs2.rename(tmp, this.messagesPath());
916
+ await fs.rename(tmp, this.messagesPath());
871
917
  });
872
918
  } catch (error) {
873
- await fs2.unlink(tmp).catch(() => {
919
+ await fs.unlink(tmp).catch(() => {
874
920
  });
875
921
  warn(
876
922
  `session ${this.id}: history rewrite failed (${error.code ?? String(error)}), kept the previous messages.jsonl`
@@ -881,9 +927,12 @@ var SessionStore = class _SessionStore {
881
927
  await this.flushMetaNow();
882
928
  }
883
929
  async messages() {
930
+ return (await this.starMessages()).map((star) => star.message);
931
+ }
932
+ async starMessages() {
884
933
  let handle = null;
885
934
  try {
886
- handle = await fs2.open(this.messagesPath(), "r");
935
+ handle = await fs.open(this.messagesPath(), "r");
887
936
  const messages = [];
888
937
  const decoder = new StringDecoder("utf8");
889
938
  const buffer = Buffer.alloc(MESSAGES_READ_CHUNK_BYTES);
@@ -891,9 +940,10 @@ var SessionStore = class _SessionStore {
891
940
  const pushLine = (line) => {
892
941
  const trimmed = line.trim();
893
942
  if (!trimmed) return;
894
- try {
895
- messages.push(JSON.parse(trimmed));
896
- } catch {
943
+ const parsed = parseMessageLine(trimmed);
944
+ if (parsed) {
945
+ messages.push(parsed);
946
+ } else {
897
947
  debugWarn(`session ${this.id}: skipping corrupt messages.jsonl line`);
898
948
  }
899
949
  };
@@ -931,7 +981,7 @@ var SessionStore = class _SessionStore {
931
981
  await this.ensureInitialized();
932
982
  if (this.cachedMeta) return this.cachedMeta;
933
983
  try {
934
- const raw = await fs2.readFile(this.metaPath(), "utf8");
984
+ const raw = await fs.readFile(this.metaPath(), "utf8");
935
985
  this.cachedMeta = JSON.parse(raw);
936
986
  return this.cachedMeta;
937
987
  } catch {
@@ -998,48 +1048,107 @@ var SessionStore = class _SessionStore {
998
1048
  }
999
1049
  };
1000
1050
 
1001
- // src/session/clear.ts
1002
- import fs3 from "fs/promises";
1003
- import path4 from "path";
1004
- async function clearSessions(cwd, excludeId) {
1005
- if (cwd !== void 0) {
1006
- const metas = await SessionStore.list(cwd);
1007
- let removed2 = 0;
1008
- for (const meta of metas) {
1009
- if (meta.id === excludeId) continue;
1010
- await fs3.rm(path4.join(sessionsDir(), meta.id), { recursive: true, force: true });
1011
- removed2 += 1;
1012
- }
1013
- if ((await SessionStore.list(cwd)).length === 0) {
1014
- 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
+ () => {
1015
1066
  }
1016
- 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;
1017
1087
  }
1018
- 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;
1019
1094
  try {
1020
- entries = await fs3.readdir(sessionsDir());
1095
+ raw = await fs2.readFile(indexPath(sessionDir), "utf8");
1021
1096
  } catch {
1022
- return 0;
1097
+ return [];
1023
1098
  }
1024
- let removed = 0;
1025
- for (const entry of entries) {
1026
- if (entry === excludeId) continue;
1027
- await fs3.rm(path4.join(sessionsDir(), entry), { recursive: true, force: true });
1028
- 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 [];
1029
1108
  }
1030
- const survivors = await SessionStore.list();
1031
- await pruneTreeReposExcept(survivors.map((meta) => meta.cwd));
1032
- 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
+ }));
1033
1142
  }
1034
1143
 
1035
1144
  // src/session/list.ts
1036
- import fs4 from "fs/promises";
1037
- import path5 from "path";
1145
+ import fs3 from "fs/promises";
1146
+ import path4 from "path";
1038
1147
  var COUNT_CHUNK_BYTES = 64 * 1024;
1039
1148
  async function countMessages(id) {
1040
1149
  let handle = null;
1041
1150
  try {
1042
- handle = await fs4.open(path5.join(sessionsDir(), id, "messages.jsonl"), "r");
1151
+ handle = await fs3.open(path4.join(sessionsDir(), id, "messages.jsonl"), "r");
1043
1152
  const buffer = Buffer.alloc(COUNT_CHUNK_BYTES);
1044
1153
  let count = 0;
1045
1154
  let lineHasContent = false;
@@ -1064,11 +1173,24 @@ async function countMessages(id) {
1064
1173
  });
1065
1174
  }
1066
1175
  }
1176
+ var LIST_COUNT_CONCURRENCY = 8;
1067
1177
  async function listSessionEntries(cwd) {
1068
1178
  const metas = await SessionStore.list(cwd);
1069
- return Promise.all(
1070
- 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())
1071
1192
  );
1193
+ return entries;
1072
1194
  }
1073
1195
  async function findLatestSession(cwd) {
1074
1196
  const [latest] = await SessionStore.list(cwd);
@@ -1077,7 +1199,7 @@ async function findLatestSession(cwd) {
1077
1199
  async function sessionPreview(id, maxBytes = 4096) {
1078
1200
  let handle = null;
1079
1201
  try {
1080
- handle = await fs4.open(path5.join(sessionsDir(), id, "messages.jsonl"), "r");
1202
+ handle = await fs3.open(path4.join(sessionsDir(), id, "messages.jsonl"), "r");
1081
1203
  const buffer = Buffer.alloc(maxBytes);
1082
1204
  const { bytesRead } = await handle.read(buffer, 0, maxBytes, 0);
1083
1205
  const head = buffer.toString("utf8", 0, bytesRead);
@@ -1135,6 +1257,55 @@ function formatSessionEntries(entries, options = {}) {
1135
1257
  }).join("\n");
1136
1258
  }
1137
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
+ When you add a feature or change behavior, write the unit tests that cover it in the same change, matching the project's test framework, layout, and naming; if the project has no test setup at all, say so instead of silently skipping tests.
1269
+ Never consider the task finished until you have verified the result yourself: run the project's full verification \u2014 the whole test suite, not only the tests you touched, plus the type check, build, or lint the project defines \u2014 and if verification fails or reveals gaps, keep fixing until everything passes; a change that breaks other tests is not done.
1270
+ Before the final verification, sweep your own changes for leftovers: debug prints, commented-out code, half-finished edits.
1271
+ 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.
1272
+ When you report completion, state the evidence: which checks you ran and that they passed \u2014 never claim success you did not observe.
1273
+ 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.`;
1274
+
1275
+ // src/session/clear.ts
1276
+ import fs4 from "fs/promises";
1277
+ import path5 from "path";
1278
+ async function clearSessions(cwd, excludeId) {
1279
+ if (cwd !== void 0) {
1280
+ const metas = await SessionStore.list(cwd);
1281
+ let removed2 = 0;
1282
+ for (const meta of metas) {
1283
+ if (meta.id === excludeId) continue;
1284
+ await fs4.rm(path5.join(sessionsDir(), meta.id), { recursive: true, force: true });
1285
+ removed2 += 1;
1286
+ }
1287
+ if ((await SessionStore.list(cwd)).length === 0) {
1288
+ await removeTreeRepo(cwd);
1289
+ }
1290
+ return removed2;
1291
+ }
1292
+ let entries;
1293
+ try {
1294
+ entries = await fs4.readdir(sessionsDir());
1295
+ } catch {
1296
+ return 0;
1297
+ }
1298
+ let removed = 0;
1299
+ for (const entry of entries) {
1300
+ if (entry === excludeId) continue;
1301
+ await fs4.rm(path5.join(sessionsDir(), entry), { recursive: true, force: true });
1302
+ removed += 1;
1303
+ }
1304
+ const survivors = await SessionStore.list();
1305
+ await pruneTreeReposExcept(survivors.map((meta) => meta.cwd));
1306
+ return removed;
1307
+ }
1308
+
1138
1309
  // src/version.ts
1139
1310
  import fs5 from "fs";
1140
1311
  import path6 from "path";
@@ -1153,61 +1324,14 @@ function readPackageVersion() {
1153
1324
  }
1154
1325
  var VERSION = readPackageVersion();
1155
1326
 
1156
- // src/config/env.ts
1157
- import fs6 from "fs";
1158
- var ENV_KEY_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
1159
- function parseEnvContent(content) {
1160
- const vars = {};
1161
- for (const rawLine of content.split(/\r?\n/)) {
1162
- const line = rawLine.trim();
1163
- if (!line || line.startsWith("#")) continue;
1164
- const eq = line.indexOf("=");
1165
- if (eq <= 0) continue;
1166
- const key = line.slice(0, eq).trim();
1167
- if (!ENV_KEY_PATTERN.test(key)) continue;
1168
- let value = line.slice(eq + 1).trim();
1169
- if (value.length >= 2 && (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'"))) {
1170
- value = value.slice(1, -1);
1171
- }
1172
- vars[key] = value;
1173
- }
1174
- return vars;
1175
- }
1176
- function loadEnvFile(filePath = envFilePath()) {
1177
- let content;
1178
- try {
1179
- content = fs6.readFileSync(filePath, "utf8");
1180
- } catch {
1181
- return;
1182
- }
1183
- for (const [key, value] of Object.entries(parseEnvContent(content))) {
1184
- if (process.env[key] === void 0) {
1185
- process.env[key] = value;
1186
- }
1187
- }
1188
- }
1189
- function upsertEnvContent(content, key, value) {
1190
- const entry = `${key}=${value.replace(/[\r\n]/g, "")}`;
1191
- const pattern = new RegExp(`^\\s*${key}\\s*=`);
1192
- const lines = content.split("\n");
1193
- while (lines.length > 0 && (lines[lines.length - 1] ?? "").trim() === "") lines.pop();
1194
- for (let i = 0; i < lines.length; i++) {
1195
- if (pattern.test(lines[i] ?? "")) {
1196
- lines[i] = entry;
1197
- return `${lines.join("\n")}
1198
- `;
1199
- }
1200
- }
1201
- lines.push(entry);
1202
- return `${lines.join("\n")}
1203
- `;
1204
- }
1205
-
1206
1327
  export {
1207
1328
  formatDollars,
1208
1329
  formatTokens,
1209
1330
  computeCostUsd,
1210
1331
  estimateCost,
1332
+ toolIcon,
1333
+ thinkingIcon,
1334
+ folderIcon,
1211
1335
  renderMarkdown,
1212
1336
  toTerminalSafe,
1213
1337
  summarizeArgs,
@@ -1216,18 +1340,13 @@ export {
1216
1340
  formatElapsedSeconds,
1217
1341
  splitCommittableLines,
1218
1342
  formatStreamError,
1219
- coreMessageText,
1220
1343
  buildDisplayMessages,
1221
1344
  MAX_IMAGE_BYTES,
1222
1345
  imageMimeType,
1223
1346
  readImageInput,
1224
1347
  resolveMentions,
1225
- SYSTEM_PROMPT,
1226
- loadEnvFile,
1227
- upsertEnvContent,
1228
1348
  loadSessionSnapshots,
1229
1349
  SessionStore,
1230
- clearSessions,
1231
1350
  listSessionEntries,
1232
1351
  findLatestSession,
1233
1352
  sessionPreview,
@@ -1235,6 +1354,8 @@ export {
1235
1354
  resolveSessionId,
1236
1355
  relativeTime,
1237
1356
  formatSessionEntries,
1357
+ SYSTEM_PROMPT,
1358
+ clearSessions,
1238
1359
  VERSION
1239
1360
  };
1240
- //# sourceMappingURL=chunk-IYWYSFSF.js.map
1361
+ //# sourceMappingURL=chunk-PDFC7QVW.js.map