@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.
- package/README.md +16 -2
- package/README.zh-CN.md +15 -2
- package/dist/chunk-4IRY44TG.js +34 -0
- package/dist/chunk-4IRY44TG.js.map +1 -0
- package/dist/chunk-A5PXPWHB.js +2014 -0
- package/dist/chunk-A5PXPWHB.js.map +1 -0
- package/dist/chunk-IJGUW4PM.js +504 -0
- package/dist/chunk-IJGUW4PM.js.map +1 -0
- package/dist/chunk-MNVCCOI2.js +3510 -0
- package/dist/chunk-MNVCCOI2.js.map +1 -0
- package/dist/chunk-MVAMPVTA.js +232 -0
- package/dist/chunk-MVAMPVTA.js.map +1 -0
- package/dist/{chunk-IYWYSFSF.js → chunk-PDFC7QVW.js} +339 -218
- package/dist/chunk-PDFC7QVW.js.map +1 -0
- package/dist/chunk-URC7JXKX.js +1414 -0
- package/dist/chunk-URC7JXKX.js.map +1 -0
- package/dist/loop-TW2FHL7J.js +17 -0
- package/dist/main.js +257 -79
- package/dist/main.js.map +1 -1
- package/dist/provider-EWMUPBLH.js +11 -0
- package/dist/provider-EWMUPBLH.js.map +1 -0
- package/dist/{repl-YA23CK5R.js → repl-GBVIW5QR.js} +435 -262
- package/dist/repl-GBVIW5QR.js.map +1 -0
- package/dist/tools-7EBGCYYE.js +18 -0
- package/dist/tools-7EBGCYYE.js.map +1 -0
- package/package.json +18 -26
- package/dist/chunk-IYWYSFSF.js.map +0 -1
- package/dist/chunk-OHLA2DP7.js +0 -5849
- package/dist/chunk-OHLA2DP7.js.map +0 -1
- package/dist/loop-2MOFLXTG.js +0 -12
- package/dist/repl-YA23CK5R.js.map +0 -1
- /package/dist/{loop-2MOFLXTG.js.map → loop-TW2FHL7J.js.map} +0 -0
|
@@ -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-
|
|
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
|
|
314
|
-
if (
|
|
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
|
-
}
|
|
317
|
-
|
|
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 =
|
|
728
|
+
this.dir = path2.join(sessionsDir(), id);
|
|
686
729
|
}
|
|
687
730
|
metaPath() {
|
|
688
|
-
return
|
|
731
|
+
return path2.join(this.dir, "meta.json");
|
|
689
732
|
}
|
|
690
733
|
messagesPath() {
|
|
691
|
-
return
|
|
734
|
+
return path2.join(this.dir, "messages.jsonl");
|
|
692
735
|
}
|
|
693
736
|
static async create(cwd, model) {
|
|
694
|
-
const store = new _SessionStore(
|
|
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
|
|
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
|
|
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
|
|
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 && (
|
|
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
|
|
752
|
-
await
|
|
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
|
|
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
|
|
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
|
|
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) =>
|
|
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
|
-
() =>
|
|
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
|
|
859
|
-
|
|
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(
|
|
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
|
|
914
|
+
await fs.writeFile(tmp, content ? `${content}
|
|
869
915
|
` : "");
|
|
870
|
-
await
|
|
916
|
+
await fs.rename(tmp, this.messagesPath());
|
|
871
917
|
});
|
|
872
918
|
} catch (error) {
|
|
873
|
-
await
|
|
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
|
|
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
|
-
|
|
895
|
-
|
|
896
|
-
|
|
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
|
|
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/
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1095
|
+
raw = await fs2.readFile(indexPath(sessionDir), "utf8");
|
|
1021
1096
|
} catch {
|
|
1022
|
-
return
|
|
1097
|
+
return [];
|
|
1023
1098
|
}
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
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
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
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
|
|
1037
|
-
import
|
|
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
|
|
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
|
-
|
|
1070
|
-
|
|
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
|
|
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-
|
|
1361
|
+
//# sourceMappingURL=chunk-PDFC7QVW.js.map
|