@cryer/star-cli 0.3.2 → 0.3.3
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 +2 -0
- package/README.zh-CN.md +317 -0
- package/dist/{chunk-BV4PLM6X.js → chunk-3L6QKJM4.js} +1350 -671
- package/dist/chunk-3L6QKJM4.js.map +1 -0
- package/dist/{chunk-LADQCYKL.js → chunk-B6P4VUBE.js} +16 -7
- package/dist/chunk-B6P4VUBE.js.map +1 -0
- package/dist/{loop-XKC5HQBT.js → loop-TQ7GFTTE.js} +3 -3
- package/dist/main.js +569 -177
- package/dist/main.js.map +1 -1
- package/dist/{stream-OY2MKWYE.js → stream-H4BYBVAZ.js} +22 -4
- package/dist/stream-H4BYBVAZ.js.map +1 -0
- package/package.json +1 -1
- package/dist/chunk-BV4PLM6X.js.map +0 -1
- package/dist/chunk-LADQCYKL.js.map +0 -1
- package/dist/stream-OY2MKWYE.js.map +0 -1
- /package/dist/{loop-XKC5HQBT.js.map → loop-TQ7GFTTE.js.map} +0 -0
package/dist/main.js
CHANGED
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
createIgnorePredicate,
|
|
18
18
|
defaultAgentTasks,
|
|
19
19
|
defaultTaskManager,
|
|
20
|
+
diffTreeNames,
|
|
20
21
|
discoverSkills,
|
|
21
22
|
estimateTokens,
|
|
22
23
|
formatTaskFinished,
|
|
@@ -39,9 +40,8 @@ import {
|
|
|
39
40
|
resolveShell,
|
|
40
41
|
resolveSnapshotContent,
|
|
41
42
|
rewindToSnapshot,
|
|
42
|
-
summarizeMessages
|
|
43
|
-
|
|
44
|
-
} from "./chunk-BV4PLM6X.js";
|
|
43
|
+
summarizeMessages
|
|
44
|
+
} from "./chunk-3L6QKJM4.js";
|
|
45
45
|
import {
|
|
46
46
|
envFilePath,
|
|
47
47
|
globalConfigPath,
|
|
@@ -49,11 +49,20 @@ import {
|
|
|
49
49
|
sessionsDir,
|
|
50
50
|
starHome,
|
|
51
51
|
userMemoryPath
|
|
52
|
-
} from "./chunk-
|
|
52
|
+
} from "./chunk-B6P4VUBE.js";
|
|
53
53
|
|
|
54
54
|
// src/main.tsx
|
|
55
55
|
import { Command } from "commander";
|
|
56
56
|
|
|
57
|
+
// src/cli/terminal-text.ts
|
|
58
|
+
var UNSAFE_CHARS = /[\x00-\x09\x0b-\x1f\x7f]/;
|
|
59
|
+
var CONTROL_CHARS = /[\x00-\x08\x0b-\x1f\x7f]/g;
|
|
60
|
+
var TABS = /\t/g;
|
|
61
|
+
function toTerminalSafe(text) {
|
|
62
|
+
if (!UNSAFE_CHARS.test(text)) return text;
|
|
63
|
+
return text.replace(TABS, " ").replace(CONTROL_CHARS, "");
|
|
64
|
+
}
|
|
65
|
+
|
|
57
66
|
// src/cli/format.ts
|
|
58
67
|
function summarizeArgs(args, maxLength = 120) {
|
|
59
68
|
let json;
|
|
@@ -98,7 +107,7 @@ function buildDisplayMessages(messages) {
|
|
|
98
107
|
if (message.role === "user" || message.role === "assistant") {
|
|
99
108
|
const text = coreMessageText(message);
|
|
100
109
|
if (text) {
|
|
101
|
-
display.push({ id: display.length, role: message.role, text });
|
|
110
|
+
display.push({ id: display.length, role: message.role, text: toTerminalSafe(text) });
|
|
102
111
|
} else {
|
|
103
112
|
collapsed++;
|
|
104
113
|
}
|
|
@@ -110,7 +119,7 @@ function buildDisplayMessages(messages) {
|
|
|
110
119
|
display.push({
|
|
111
120
|
id: display.length,
|
|
112
121
|
role: "system",
|
|
113
|
-
text:
|
|
122
|
+
text: `Restored ${collapsed} history message(s) not shown here.`
|
|
114
123
|
});
|
|
115
124
|
}
|
|
116
125
|
return display;
|
|
@@ -168,7 +177,15 @@ function resizeImagePlan(platform, inputFile, outputFile, maxDimension, tools =
|
|
|
168
177
|
if (tools.hasConvert) {
|
|
169
178
|
return {
|
|
170
179
|
command: "convert",
|
|
171
|
-
|
|
180
|
+
// -auto-orient keeps EXIF-rotated photos upright after the resize
|
|
181
|
+
// strips their orientation metadata.
|
|
182
|
+
args: [
|
|
183
|
+
inputFile,
|
|
184
|
+
"-auto-orient",
|
|
185
|
+
"-resize",
|
|
186
|
+
`${maxDimension}x${maxDimension}>`,
|
|
187
|
+
outputFile
|
|
188
|
+
]
|
|
172
189
|
};
|
|
173
190
|
}
|
|
174
191
|
if (tools.hasFfmpeg) {
|
|
@@ -236,6 +253,7 @@ var MAX_MENTION_BYTES = 100 * 1024;
|
|
|
236
253
|
var MAX_IMAGE_BYTES = 5 * 1024 * 1024;
|
|
237
254
|
var MAX_DIR_MENTION_ENTRIES = 200;
|
|
238
255
|
var MAX_DIR_MENTION_DEPTH = 10;
|
|
256
|
+
var MAX_DIR_MENTION_TOTAL = 5e3;
|
|
239
257
|
var IMAGE_MIME_TYPES = {
|
|
240
258
|
".png": "image/png",
|
|
241
259
|
".jpg": "image/jpeg",
|
|
@@ -261,7 +279,7 @@ async function readImageInput(filePath, cwd) {
|
|
|
261
279
|
});
|
|
262
280
|
return result.image;
|
|
263
281
|
}
|
|
264
|
-
var MENTION_RE = /(?<=^|\s)@([A-Za-z]:[\\/][A-Za-z0-9._
|
|
282
|
+
var MENTION_RE = /(?<=^|\s)@([A-Za-z]:[\\/][A-Za-z0-9._\-/\\\u2e80-\u9fff\uac00-\ud7af\uf900-\ufaff\uff00-\uffef]*|[A-Za-z0-9._\-/\\\u2e80-\u9fff\uac00-\ud7af\uf900-\ufaff\uff00-\uffef]+)/g;
|
|
265
283
|
function parseMentions(text) {
|
|
266
284
|
const mentions = [];
|
|
267
285
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -294,13 +312,18 @@ function isBinaryContent(buf) {
|
|
|
294
312
|
return probe.includes(0);
|
|
295
313
|
}
|
|
296
314
|
async function listDirEntries(dir, prefix, depth, state, ignore) {
|
|
315
|
+
if (state.capped) return;
|
|
297
316
|
const entries = await readdir(dir, { withFileTypes: true }).catch(() => null);
|
|
298
317
|
if (!entries) return;
|
|
299
|
-
const visible = entries.filter((entry) => entry.isDirectory() || entry.isFile()).filter((entry) => !isSensitivePath(entry.name)).filter((entry) => !ignore?.(path2.join(dir, entry.name), entry.isDirectory())).sort((a, b) => {
|
|
318
|
+
const visible = entries.filter((entry) => entry.isDirectory() || entry.isFile()).filter((entry) => !(entry.isDirectory() && SKIP_DIRS.has(entry.name))).filter((entry) => !isSensitivePath(entry.name)).filter((entry) => !ignore?.(path2.join(dir, entry.name), entry.isDirectory())).sort((a, b) => {
|
|
300
319
|
const dirDiff = Number(b.isDirectory()) - Number(a.isDirectory());
|
|
301
320
|
return dirDiff !== 0 ? dirDiff : a.name.localeCompare(b.name);
|
|
302
321
|
});
|
|
303
322
|
for (let i = 0; i < visible.length; i++) {
|
|
323
|
+
if (state.total >= MAX_DIR_MENTION_TOTAL) {
|
|
324
|
+
state.capped = true;
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
304
327
|
const entry = visible[i];
|
|
305
328
|
if (!entry) continue;
|
|
306
329
|
state.total += 1;
|
|
@@ -325,11 +348,13 @@ async function listDirEntries(dir, prefix, depth, state, ignore) {
|
|
|
325
348
|
}
|
|
326
349
|
async function directoryBlock(abs, mention, ignore) {
|
|
327
350
|
const display = `${mention.replace(/\\/g, "/").replace(/\/+$/, "")}/`;
|
|
328
|
-
const state = { lines: [`${display}`], shown: 0, total: 0 };
|
|
351
|
+
const state = { lines: [`${display}`], shown: 0, total: 0, capped: false };
|
|
329
352
|
const probe = await readdir(abs).catch(() => null);
|
|
330
353
|
if (!probe) return null;
|
|
331
354
|
await listDirEntries(abs, "", 1, state, ignore);
|
|
332
|
-
if (state.
|
|
355
|
+
if (state.capped) {
|
|
356
|
+
state.lines.push("... (truncated, many more entries)");
|
|
357
|
+
} else if (state.total > state.shown) {
|
|
333
358
|
state.lines.push(
|
|
334
359
|
`... (truncated, ${state.total - state.shown} more entries \u2014 use glob/grep tools or mention a subdirectory to see more)`
|
|
335
360
|
);
|
|
@@ -473,11 +498,56 @@ import { parse, stringify } from "smol-toml";
|
|
|
473
498
|
async function addAllowRule(rule) {
|
|
474
499
|
return addPermissionRule("allow", rule);
|
|
475
500
|
}
|
|
501
|
+
function tomlString(value) {
|
|
502
|
+
return JSON.stringify(value);
|
|
503
|
+
}
|
|
504
|
+
async function writeFileAtomic(filePath, content) {
|
|
505
|
+
const tmpPath = `${filePath}.${process.pid}.tmp`;
|
|
506
|
+
await fs.promises.writeFile(tmpPath, content, "utf8");
|
|
507
|
+
await fs.promises.rename(tmpPath, filePath);
|
|
508
|
+
}
|
|
509
|
+
function upsertPermissionRuleText(content, kind, rules, hadPermissionsTable) {
|
|
510
|
+
const newLine = `${kind} = [${rules.map(tomlString).join(", ")}]`;
|
|
511
|
+
const lines = content.split("\n");
|
|
512
|
+
let sectionStart = -1;
|
|
513
|
+
let sectionEnd = lines.length;
|
|
514
|
+
for (let i = 0; i < lines.length; i++) {
|
|
515
|
+
const line = lines[i] ?? "";
|
|
516
|
+
if (!/^\s*\[/.test(line)) continue;
|
|
517
|
+
if (sectionStart === -1) {
|
|
518
|
+
if (/^\s*\[\s*permissions\s*\]\s*(?:#.*)?$/.test(line)) sectionStart = i;
|
|
519
|
+
} else {
|
|
520
|
+
sectionEnd = i;
|
|
521
|
+
break;
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
if (sectionStart === -1) {
|
|
525
|
+
if (hadPermissionsTable) return null;
|
|
526
|
+
const separator = content === "" ? "" : content.endsWith("\n") ? "\n" : "\n\n";
|
|
527
|
+
return `${content}${separator}[permissions]
|
|
528
|
+
${newLine}
|
|
529
|
+
`;
|
|
530
|
+
}
|
|
531
|
+
const inlineArray = new RegExp(`^(\\s*)${kind}\\s*=\\s*\\[.*\\](\\s*(?:#.*)?)$`);
|
|
532
|
+
const anyKey = new RegExp(`^\\s*${kind}\\s*=`);
|
|
533
|
+
for (let i = sectionStart + 1; i < sectionEnd; i++) {
|
|
534
|
+
const line = lines[i] ?? "";
|
|
535
|
+
const match = inlineArray.exec(line);
|
|
536
|
+
if (match) {
|
|
537
|
+
lines[i] = `${match[1]}${newLine}${match[2]}`;
|
|
538
|
+
return lines.join("\n");
|
|
539
|
+
}
|
|
540
|
+
if (anyKey.test(line)) return null;
|
|
541
|
+
}
|
|
542
|
+
lines.splice(sectionStart + 1, 0, newLine);
|
|
543
|
+
return lines.join("\n");
|
|
544
|
+
}
|
|
476
545
|
async function addPermissionRule(kind, rule) {
|
|
477
546
|
const filePath = globalConfigPath();
|
|
547
|
+
let content = "";
|
|
478
548
|
let raw = {};
|
|
479
549
|
try {
|
|
480
|
-
|
|
550
|
+
content = await fs.promises.readFile(filePath, "utf8");
|
|
481
551
|
raw = parse(content);
|
|
482
552
|
} catch (error) {
|
|
483
553
|
if (error.code !== "ENOENT") throw error;
|
|
@@ -485,24 +555,47 @@ async function addPermissionRule(kind, rule) {
|
|
|
485
555
|
const permissions = typeof raw.permissions === "object" && raw.permissions !== null ? raw.permissions : {};
|
|
486
556
|
const rules = Array.isArray(permissions[kind]) ? permissions[kind] : [];
|
|
487
557
|
if (rules.includes(rule)) return false;
|
|
488
|
-
|
|
489
|
-
raw.permissions = permissions;
|
|
558
|
+
const nextRules = [...rules, rule];
|
|
490
559
|
await fs.promises.mkdir(path3.dirname(filePath), { recursive: true });
|
|
491
|
-
|
|
560
|
+
const updated = upsertPermissionRuleText(content, kind, nextRules, raw.permissions !== void 0);
|
|
561
|
+
if (updated !== null) {
|
|
562
|
+
await writeFileAtomic(filePath, updated);
|
|
563
|
+
return true;
|
|
564
|
+
}
|
|
565
|
+
permissions[kind] = nextRules;
|
|
566
|
+
raw.permissions = permissions;
|
|
567
|
+
await writeFileAtomic(filePath, stringify(raw));
|
|
492
568
|
return true;
|
|
493
569
|
}
|
|
570
|
+
function upsertTopLevelKeyText(content, key, value) {
|
|
571
|
+
const newLine = `${key} = ${tomlString(value)}`;
|
|
572
|
+
if (content.trim() === "") return `${newLine}
|
|
573
|
+
`;
|
|
574
|
+
const lines = content.split("\n");
|
|
575
|
+
const firstTable = lines.findIndex((line) => /^\s*\[/.test(line));
|
|
576
|
+
const end = firstTable === -1 ? lines.length : firstTable;
|
|
577
|
+
const keyLine = new RegExp(`^\\s*${key}\\s*=`);
|
|
578
|
+
for (let i = 0; i < end; i++) {
|
|
579
|
+
const line = lines[i] ?? "";
|
|
580
|
+
if (keyLine.test(line)) {
|
|
581
|
+
const comment = /\s+#.*$/.exec(line);
|
|
582
|
+
lines[i] = `${newLine}${comment?.[0] ?? ""}`;
|
|
583
|
+
return lines.join("\n");
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
lines.splice(end, 0, newLine);
|
|
587
|
+
return lines.join("\n");
|
|
588
|
+
}
|
|
494
589
|
async function savePermissionMode(mode) {
|
|
495
590
|
const filePath = globalConfigPath();
|
|
496
|
-
let
|
|
591
|
+
let content = "";
|
|
497
592
|
try {
|
|
498
|
-
|
|
499
|
-
raw = parse(content);
|
|
593
|
+
content = await fs.promises.readFile(filePath, "utf8");
|
|
500
594
|
} catch (error) {
|
|
501
595
|
if (error.code !== "ENOENT") throw error;
|
|
502
596
|
}
|
|
503
|
-
raw.permissionMode = mode;
|
|
504
597
|
await fs.promises.mkdir(path3.dirname(filePath), { recursive: true });
|
|
505
|
-
await
|
|
598
|
+
await writeFileAtomic(filePath, upsertTopLevelKeyText(content, "permissionMode", mode));
|
|
506
599
|
}
|
|
507
600
|
|
|
508
601
|
// src/config/schema.ts
|
|
@@ -530,7 +623,11 @@ var ModelConfigSchema = z.object({
|
|
|
530
623
|
// Cost estimation needs BOTH prices (USD per 1M tokens); with only one set
|
|
531
624
|
// the model is treated as unpriced.
|
|
532
625
|
promptPrice: z.number().nonnegative().optional(),
|
|
533
|
-
completionPrice: z.number().nonnegative().optional()
|
|
626
|
+
completionPrice: z.number().nonnegative().optional(),
|
|
627
|
+
// Optional price (USD per 1M tokens) for cache-read prompt tokens. When
|
|
628
|
+
// unset, OpenAI-style cached tokens bill at promptPrice and Anthropic-style
|
|
629
|
+
// cache reads are not billed — set this for accurate cache pricing.
|
|
630
|
+
cacheReadPrice: z.number().nonnegative().optional()
|
|
534
631
|
});
|
|
535
632
|
var PermissionsConfigSchema = z.object({
|
|
536
633
|
allow: z.array(z.string()).default([]),
|
|
@@ -592,7 +689,17 @@ var ConfigSchema = z.object({
|
|
|
592
689
|
notifyBell: z.boolean().default(true),
|
|
593
690
|
notifyBellThresholdSec: z.number().positive().default(10),
|
|
594
691
|
permissions: PermissionsConfigSchema.default({ allow: [], deny: [] }),
|
|
595
|
-
hooks: z.array(HookConfigSchema).default([])
|
|
692
|
+
hooks: z.array(HookConfigSchema).default([]),
|
|
693
|
+
// Consecutive identical tool calls (same tool + same arguments) allowed
|
|
694
|
+
// before the loop refuses to execute the repeat and tells the model to
|
|
695
|
+
// change approach — a guard against agents stuck retrying a failing call
|
|
696
|
+
// (opencode's doom_loop). 0 disables.
|
|
697
|
+
doomLoopThreshold: z.number().int().min(0).default(3),
|
|
698
|
+
// Track the whole working tree with an internal git repo (outside the
|
|
699
|
+
// user's .git) at every turn boundary, so /undo and /redo can restore
|
|
700
|
+
// changes made by ANY tool (bash included), not just write_file/edit_file.
|
|
701
|
+
// Falls back silently to per-file snapshots when git is unavailable.
|
|
702
|
+
gitSnapshots: z.boolean().default(true)
|
|
596
703
|
});
|
|
597
704
|
function contextWindowTokens(config, modelName) {
|
|
598
705
|
return config.models.find((m) => m.name === modelName)?.contextMaxTokens ?? config.contextMaxTokens;
|
|
@@ -674,10 +781,13 @@ function createSseNormalizingFetch(baseFetch = fetch) {
|
|
|
674
781
|
return response;
|
|
675
782
|
}
|
|
676
783
|
const body = response.body.pipeThrough(new TextDecoderStream()).pipeThrough(new SseNormalizeTransform()).pipeThrough(new TextEncoderStream());
|
|
784
|
+
const headers = new Headers(response.headers);
|
|
785
|
+
headers.delete("content-length");
|
|
786
|
+
headers.delete("content-encoding");
|
|
677
787
|
return new Response(body, {
|
|
678
788
|
status: response.status,
|
|
679
789
|
statusText: response.statusText,
|
|
680
|
-
headers
|
|
790
|
+
headers
|
|
681
791
|
});
|
|
682
792
|
};
|
|
683
793
|
}
|
|
@@ -715,7 +825,7 @@ function createModel(config, modelName) {
|
|
|
715
825
|
baseURL: provider.baseURL,
|
|
716
826
|
apiKey,
|
|
717
827
|
headers: provider.headers
|
|
718
|
-
})(modelConfig.model);
|
|
828
|
+
})(modelConfig.model, {}, { includeUsage: true });
|
|
719
829
|
case "openai-responses":
|
|
720
830
|
return createOpenAI({
|
|
721
831
|
baseURL: provider.baseURL,
|
|
@@ -738,6 +848,21 @@ function checkpointsDir(sessionDir) {
|
|
|
738
848
|
function indexPath(sessionDir) {
|
|
739
849
|
return path4.join(checkpointsDir(sessionDir), "index.json");
|
|
740
850
|
}
|
|
851
|
+
var indexWriteQueues = /* @__PURE__ */ new Map();
|
|
852
|
+
function enqueueIndexWrite(sessionDir, op) {
|
|
853
|
+
const queued = indexWriteQueues.get(sessionDir) ?? Promise.resolve();
|
|
854
|
+
const run = queued.then(op);
|
|
855
|
+
indexWriteQueues.set(
|
|
856
|
+
sessionDir,
|
|
857
|
+
run.then(
|
|
858
|
+
() => {
|
|
859
|
+
},
|
|
860
|
+
() => {
|
|
861
|
+
}
|
|
862
|
+
)
|
|
863
|
+
);
|
|
864
|
+
return run;
|
|
865
|
+
}
|
|
741
866
|
function checkpointContentPath(sessionDir, id) {
|
|
742
867
|
return path4.join(checkpointsDir(sessionDir), `${id}.snapshot`);
|
|
743
868
|
}
|
|
@@ -756,32 +881,36 @@ async function listCheckpointRecords(sessionDir) {
|
|
|
756
881
|
}
|
|
757
882
|
}
|
|
758
883
|
async function appendCheckpointRecord(sessionDir, record, content) {
|
|
759
|
-
await
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
884
|
+
await enqueueIndexWrite(sessionDir, async () => {
|
|
885
|
+
await fs2.mkdir(checkpointsDir(sessionDir), { recursive: true, mode: 448 });
|
|
886
|
+
if (record.existed && content !== null) {
|
|
887
|
+
await fs2.writeFile(checkpointContentPath(sessionDir, record.id), content, "utf8");
|
|
888
|
+
}
|
|
889
|
+
const records = await listCheckpointRecords(sessionDir);
|
|
890
|
+
records.push(record);
|
|
891
|
+
await fs2.writeFile(indexPath(sessionDir), JSON.stringify(records, null, 2));
|
|
892
|
+
});
|
|
766
893
|
}
|
|
767
894
|
async function removeCheckpointRecords(sessionDir, ids) {
|
|
768
895
|
if (ids.length === 0) return;
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
896
|
+
await enqueueIndexWrite(sessionDir, async () => {
|
|
897
|
+
const drop = new Set(ids);
|
|
898
|
+
const records = await listCheckpointRecords(sessionDir);
|
|
899
|
+
const kept = records.filter((record) => !drop.has(record.id));
|
|
900
|
+
if (kept.length === records.length) return;
|
|
901
|
+
await fs2.mkdir(checkpointsDir(sessionDir), { recursive: true, mode: 448 });
|
|
902
|
+
await fs2.writeFile(indexPath(sessionDir), JSON.stringify(kept, null, 2));
|
|
903
|
+
for (const id of ids) {
|
|
904
|
+
await fs2.rm(checkpointContentPath(sessionDir, id), { force: true });
|
|
905
|
+
}
|
|
906
|
+
});
|
|
778
907
|
}
|
|
779
908
|
async function loadSessionSnapshots(sessionDir) {
|
|
780
909
|
const records = await listCheckpointRecords(sessionDir);
|
|
781
910
|
return records.map((record) => ({
|
|
782
911
|
...record,
|
|
783
912
|
content: null,
|
|
784
|
-
contentFile: record.existed ? checkpointContentPath(sessionDir, record.id) : void 0
|
|
913
|
+
contentFile: record.existed && !record.contentTooLarge ? checkpointContentPath(sessionDir, record.id) : void 0
|
|
785
914
|
}));
|
|
786
915
|
}
|
|
787
916
|
|
|
@@ -802,6 +931,10 @@ function debugWarn(message) {
|
|
|
802
931
|
if (process.env.STAR_DEBUG === "1") process.stderr.write(`[star-cli] ${message}
|
|
803
932
|
`);
|
|
804
933
|
}
|
|
934
|
+
var FALLBACK_META = /* @__PURE__ */ Symbol("star.fallbackMeta");
|
|
935
|
+
function isFallbackMeta(meta) {
|
|
936
|
+
return meta[FALLBACK_META] === true;
|
|
937
|
+
}
|
|
805
938
|
function dayKey(date) {
|
|
806
939
|
const pad = (n) => String(n).padStart(2, "0");
|
|
807
940
|
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
|
|
@@ -811,6 +944,9 @@ var SessionStore = class _SessionStore {
|
|
|
811
944
|
dir;
|
|
812
945
|
pendingMeta = null;
|
|
813
946
|
initialized = false;
|
|
947
|
+
cachedMeta = null;
|
|
948
|
+
metaTmpSeq = 0;
|
|
949
|
+
metaWriteQueue = Promise.resolve();
|
|
814
950
|
constructor(id) {
|
|
815
951
|
this.id = id;
|
|
816
952
|
this.dir = path5.join(sessionsDir(), id);
|
|
@@ -840,6 +976,7 @@ var SessionStore = class _SessionStore {
|
|
|
840
976
|
} catch {
|
|
841
977
|
debugWarn(`session ${id}: corrupt meta.json, using defaults`);
|
|
842
978
|
}
|
|
979
|
+
await store.sweepTmpFiles();
|
|
843
980
|
store.initialized = true;
|
|
844
981
|
return store;
|
|
845
982
|
}
|
|
@@ -850,16 +987,40 @@ var SessionStore = class _SessionStore {
|
|
|
850
987
|
} catch {
|
|
851
988
|
return [];
|
|
852
989
|
}
|
|
853
|
-
const metas =
|
|
854
|
-
|
|
990
|
+
const metas = await Promise.all(
|
|
991
|
+
entries.map(async (entry) => {
|
|
992
|
+
try {
|
|
993
|
+
const raw = await fs3.readFile(path5.join(sessionsDir(), entry, "meta.json"), "utf8");
|
|
994
|
+
const meta = JSON.parse(raw);
|
|
995
|
+
if (meta.id === entry && (cwd === void 0 || meta.cwd === cwd)) return meta;
|
|
996
|
+
} catch {
|
|
997
|
+
}
|
|
998
|
+
return null;
|
|
999
|
+
})
|
|
1000
|
+
);
|
|
1001
|
+
return metas.filter((meta) => meta !== null).sort((a, b) => b.updatedAt - a.updatedAt);
|
|
1002
|
+
}
|
|
1003
|
+
// Meta writes are serialized per instance and land atomically (tmp file +
|
|
1004
|
+
// rename) over a cached in-memory meta: the fire-and-forget title/usage
|
|
1005
|
+
// writers race the loop's own appends, and a truncated read once turned
|
|
1006
|
+
// fallbackMeta's empty defaults into a persisted wipe of model/cwd/usage.
|
|
1007
|
+
// A branded fallback meta is never written back for the same reason.
|
|
1008
|
+
writeMeta(meta) {
|
|
1009
|
+
if (isFallbackMeta(meta)) return Promise.resolve();
|
|
1010
|
+
const tmp = `${this.metaPath()}.tmp-${process.pid}-${this.metaTmpSeq++}`;
|
|
1011
|
+
const run = this.metaWriteQueue.then(async () => {
|
|
855
1012
|
try {
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
1013
|
+
await fs3.writeFile(tmp, JSON.stringify(meta, null, 2));
|
|
1014
|
+
await fs3.rename(tmp, this.metaPath());
|
|
1015
|
+
} catch (error) {
|
|
1016
|
+
await fs3.unlink(tmp).catch(() => {
|
|
1017
|
+
});
|
|
1018
|
+
throw error;
|
|
860
1019
|
}
|
|
861
|
-
}
|
|
862
|
-
|
|
1020
|
+
});
|
|
1021
|
+
this.metaWriteQueue = run.catch(() => {
|
|
1022
|
+
});
|
|
1023
|
+
return run;
|
|
863
1024
|
}
|
|
864
1025
|
async ensureInitialized() {
|
|
865
1026
|
if (this.initialized) return;
|
|
@@ -868,7 +1029,8 @@ var SessionStore = class _SessionStore {
|
|
|
868
1029
|
this.initialized = true;
|
|
869
1030
|
return;
|
|
870
1031
|
}
|
|
871
|
-
await fs3.mkdir(this.dir, { recursive: true });
|
|
1032
|
+
await fs3.mkdir(this.dir, { recursive: true, mode: 448 });
|
|
1033
|
+
await this.sweepTmpFiles();
|
|
872
1034
|
const meta = {
|
|
873
1035
|
id: this.id,
|
|
874
1036
|
title: "",
|
|
@@ -877,16 +1039,31 @@ var SessionStore = class _SessionStore {
|
|
|
877
1039
|
createdAt: pending.createdAt,
|
|
878
1040
|
updatedAt: pending.createdAt
|
|
879
1041
|
};
|
|
880
|
-
await
|
|
1042
|
+
await this.writeMeta(meta);
|
|
1043
|
+
this.cachedMeta = meta;
|
|
881
1044
|
this.initialized = true;
|
|
882
1045
|
}
|
|
1046
|
+
// Removes meta.json.tmp-* orphans left behind by a crashed meta write
|
|
1047
|
+
// (tmp file + rename); best-effort, the directory may not even exist yet.
|
|
1048
|
+
async sweepTmpFiles() {
|
|
1049
|
+
let entries;
|
|
1050
|
+
try {
|
|
1051
|
+
entries = await fs3.readdir(this.dir);
|
|
1052
|
+
} catch {
|
|
1053
|
+
return;
|
|
1054
|
+
}
|
|
1055
|
+
await Promise.all(
|
|
1056
|
+
entries.filter((entry) => entry.startsWith("meta.json.tmp-")).map((entry) => fs3.unlink(path5.join(this.dir, entry)).catch(() => {
|
|
1057
|
+
}))
|
|
1058
|
+
);
|
|
1059
|
+
}
|
|
883
1060
|
async append(message) {
|
|
884
1061
|
await this.ensureInitialized();
|
|
885
1062
|
await fs3.appendFile(this.messagesPath(), `${JSON.stringify(message)}
|
|
886
1063
|
`);
|
|
887
1064
|
const meta = await this.meta();
|
|
888
1065
|
meta.updatedAt = Date.now();
|
|
889
|
-
await
|
|
1066
|
+
await this.writeMeta(meta);
|
|
890
1067
|
}
|
|
891
1068
|
async replaceMessages(messages) {
|
|
892
1069
|
await this.ensureInitialized();
|
|
@@ -895,7 +1072,7 @@ var SessionStore = class _SessionStore {
|
|
|
895
1072
|
` : "");
|
|
896
1073
|
const meta = await this.meta();
|
|
897
1074
|
meta.updatedAt = Date.now();
|
|
898
|
-
await
|
|
1075
|
+
await this.writeMeta(meta);
|
|
899
1076
|
}
|
|
900
1077
|
async messages() {
|
|
901
1078
|
let raw;
|
|
@@ -918,13 +1095,24 @@ var SessionStore = class _SessionStore {
|
|
|
918
1095
|
}
|
|
919
1096
|
fallbackMeta() {
|
|
920
1097
|
const now = Date.now();
|
|
921
|
-
|
|
1098
|
+
const meta = {
|
|
1099
|
+
id: this.id,
|
|
1100
|
+
title: "",
|
|
1101
|
+
model: "",
|
|
1102
|
+
cwd: "",
|
|
1103
|
+
createdAt: now,
|
|
1104
|
+
updatedAt: now
|
|
1105
|
+
};
|
|
1106
|
+
Object.defineProperty(meta, FALLBACK_META, { value: true, enumerable: false });
|
|
1107
|
+
return meta;
|
|
922
1108
|
}
|
|
923
1109
|
async meta() {
|
|
924
1110
|
await this.ensureInitialized();
|
|
1111
|
+
if (this.cachedMeta) return this.cachedMeta;
|
|
925
1112
|
try {
|
|
926
1113
|
const raw = await fs3.readFile(this.metaPath(), "utf8");
|
|
927
|
-
|
|
1114
|
+
this.cachedMeta = JSON.parse(raw);
|
|
1115
|
+
return this.cachedMeta;
|
|
928
1116
|
} catch {
|
|
929
1117
|
debugWarn(`session ${this.id}: unreadable meta.json, using defaults`);
|
|
930
1118
|
return this.fallbackMeta();
|
|
@@ -935,7 +1123,7 @@ var SessionStore = class _SessionStore {
|
|
|
935
1123
|
const meta = await this.meta();
|
|
936
1124
|
meta.title = title;
|
|
937
1125
|
meta.updatedAt = Date.now();
|
|
938
|
-
await
|
|
1126
|
+
await this.writeMeta(meta);
|
|
939
1127
|
}
|
|
940
1128
|
async appendCheckpoint(record, content) {
|
|
941
1129
|
await this.ensureInitialized();
|
|
@@ -960,6 +1148,12 @@ var SessionStore = class _SessionStore {
|
|
|
960
1148
|
usage.promptTokens += delta.promptTokens;
|
|
961
1149
|
usage.completionTokens += delta.completionTokens;
|
|
962
1150
|
usage.totalTokens += delta.totalTokens;
|
|
1151
|
+
if (delta.cachedPromptTokens) {
|
|
1152
|
+
usage.cachedPromptTokens = (usage.cachedPromptTokens ?? 0) + delta.cachedPromptTokens;
|
|
1153
|
+
}
|
|
1154
|
+
if (delta.cacheReadInputTokens) {
|
|
1155
|
+
usage.cacheReadInputTokens = (usage.cacheReadInputTokens ?? 0) + delta.cacheReadInputTokens;
|
|
1156
|
+
}
|
|
963
1157
|
meta.usage = usage;
|
|
964
1158
|
const day = dayKey(/* @__PURE__ */ new Date());
|
|
965
1159
|
const byDay = meta.usageByDay ?? {};
|
|
@@ -969,7 +1163,7 @@ var SessionStore = class _SessionStore {
|
|
|
969
1163
|
bucket.totalTokens += delta.totalTokens;
|
|
970
1164
|
byDay[day] = bucket;
|
|
971
1165
|
meta.usageByDay = byDay;
|
|
972
|
-
await
|
|
1166
|
+
await this.writeMeta(meta);
|
|
973
1167
|
}
|
|
974
1168
|
};
|
|
975
1169
|
|
|
@@ -1026,6 +1220,33 @@ async function findLatestSession(cwd) {
|
|
|
1026
1220
|
const [latest] = await SessionStore.list(cwd);
|
|
1027
1221
|
return latest ?? null;
|
|
1028
1222
|
}
|
|
1223
|
+
async function sessionPreview(id, maxBytes = 4096) {
|
|
1224
|
+
let handle = null;
|
|
1225
|
+
try {
|
|
1226
|
+
handle = await fs5.open(path7.join(sessionsDir(), id, "messages.jsonl"), "r");
|
|
1227
|
+
const buffer = Buffer.alloc(maxBytes);
|
|
1228
|
+
const { bytesRead } = await handle.read(buffer, 0, maxBytes, 0);
|
|
1229
|
+
const head = buffer.toString("utf8", 0, bytesRead);
|
|
1230
|
+
for (const line of head.split("\n")) {
|
|
1231
|
+
const trimmed = line.trim();
|
|
1232
|
+
if (!trimmed) continue;
|
|
1233
|
+
try {
|
|
1234
|
+
const message = JSON.parse(trimmed);
|
|
1235
|
+
if (message.role !== "user") continue;
|
|
1236
|
+
const text = typeof message.content === "string" ? message.content : message.content.filter((part) => part.type === "text").map((part) => part.type === "text" ? part.text : "").join(" ");
|
|
1237
|
+
const collapsed = text.replace(/\s+/g, " ").trim();
|
|
1238
|
+
if (collapsed) return collapsed;
|
|
1239
|
+
} catch {
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
return null;
|
|
1243
|
+
} catch {
|
|
1244
|
+
return null;
|
|
1245
|
+
} finally {
|
|
1246
|
+
await handle?.close().catch(() => {
|
|
1247
|
+
});
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1029
1250
|
function shortSessionId(id) {
|
|
1030
1251
|
const dash = id.lastIndexOf("-");
|
|
1031
1252
|
return dash === -1 ? id : id.slice(dash + 1);
|
|
@@ -1042,20 +1263,20 @@ async function resolveSessionId(query) {
|
|
|
1042
1263
|
function relativeTime(timestamp) {
|
|
1043
1264
|
const diff = Date.now() - timestamp;
|
|
1044
1265
|
const minutes = Math.floor(diff / 6e4);
|
|
1045
|
-
if (minutes < 1) return "
|
|
1046
|
-
if (minutes < 60) return `${minutes}
|
|
1266
|
+
if (minutes < 1) return "just now";
|
|
1267
|
+
if (minutes < 60) return `${minutes}m ago`;
|
|
1047
1268
|
const hours = Math.floor(minutes / 60);
|
|
1048
|
-
if (hours < 24) return `${hours}
|
|
1269
|
+
if (hours < 24) return `${hours}h ago`;
|
|
1049
1270
|
const days = Math.floor(hours / 24);
|
|
1050
|
-
if (days < 30) return `${days}
|
|
1271
|
+
if (days < 30) return `${days}d ago`;
|
|
1051
1272
|
const months = Math.floor(days / 30);
|
|
1052
|
-
if (months < 12) return `${months}
|
|
1053
|
-
return `${Math.floor(months / 12)}
|
|
1273
|
+
if (months < 12) return `${months}mo ago`;
|
|
1274
|
+
return `${Math.floor(months / 12)}y ago`;
|
|
1054
1275
|
}
|
|
1055
1276
|
function formatSessionEntries(entries, options = {}) {
|
|
1056
1277
|
return entries.map(({ meta, messageCount }) => {
|
|
1057
|
-
const title = meta.title || "(
|
|
1058
|
-
const base = `${shortSessionId(meta.id)} ${title} ${messageCount}
|
|
1278
|
+
const title = meta.title || "(untitled)";
|
|
1279
|
+
const base = `${shortSessionId(meta.id)} ${title} ${messageCount} messages updated ${relativeTime(meta.updatedAt)}`;
|
|
1059
1280
|
return options.showCwd ? `${base} [${meta.cwd}]` : base;
|
|
1060
1281
|
}).join("\n");
|
|
1061
1282
|
}
|
|
@@ -1086,7 +1307,9 @@ function computeCostUsd(usage, model) {
|
|
|
1086
1307
|
if (model?.promptPrice === void 0 || model.completionPrice === void 0) {
|
|
1087
1308
|
return null;
|
|
1088
1309
|
}
|
|
1089
|
-
|
|
1310
|
+
const cached = Math.min(Math.max(usage.cachedPromptTokens ?? 0, 0), usage.promptTokens);
|
|
1311
|
+
const promptUsd = (usage.promptTokens - cached) * model.promptPrice + cached * (model.cacheReadPrice ?? model.promptPrice) + (usage.cacheReadInputTokens ?? 0) * (model.cacheReadPrice ?? 0);
|
|
1312
|
+
return (promptUsd + usage.completionTokens * model.completionPrice) / 1e6;
|
|
1090
1313
|
}
|
|
1091
1314
|
function estimateCost(usage, modelName, model) {
|
|
1092
1315
|
const cost = computeCostUsd(usage, model);
|
|
@@ -1102,7 +1325,8 @@ function estimateCost(usage, modelName, model) {
|
|
|
1102
1325
|
}
|
|
1103
1326
|
return `Estimated cost: unknown (no price configured for ${modelName})`;
|
|
1104
1327
|
}
|
|
1105
|
-
|
|
1328
|
+
const cachePrice = model?.cacheReadPrice !== void 0 ? `, $${model.cacheReadPrice}/M cache read` : "";
|
|
1329
|
+
return `Estimated cost: $${formatDollars(cost)} (${model?.name} @ $${model?.promptPrice}/M prompt, $${model?.completionPrice}/M completion${cachePrice})`;
|
|
1106
1330
|
}
|
|
1107
1331
|
|
|
1108
1332
|
// src/session/usage.ts
|
|
@@ -1178,6 +1402,12 @@ function aggregateUsage(metas) {
|
|
|
1178
1402
|
entry.sessions += 1;
|
|
1179
1403
|
entry.requests += requestCount;
|
|
1180
1404
|
addTo(entry, totals);
|
|
1405
|
+
if (hasTotals && meta.usage) {
|
|
1406
|
+
const cached = tokens(meta.usage.cachedPromptTokens);
|
|
1407
|
+
const cacheRead = tokens(meta.usage.cacheReadInputTokens);
|
|
1408
|
+
if (cached > 0) entry.cachedPromptTokens = (entry.cachedPromptTokens ?? 0) + cached;
|
|
1409
|
+
if (cacheRead > 0) entry.cacheReadInputTokens = (entry.cacheReadInputTokens ?? 0) + cacheRead;
|
|
1410
|
+
}
|
|
1181
1411
|
models.set(name, entry);
|
|
1182
1412
|
}
|
|
1183
1413
|
return {
|
|
@@ -1232,7 +1462,7 @@ function formatUsageDashboard(stats, models, options = {}) {
|
|
|
1232
1462
|
const configured = new Map(models.map((m) => [m.name, m]));
|
|
1233
1463
|
const rows = stats.models.map((entry) => {
|
|
1234
1464
|
const cfg = configured.get(entry.model);
|
|
1235
|
-
const cost =
|
|
1465
|
+
const cost = computeCostUsd(entry, cfg);
|
|
1236
1466
|
return { entry, cost };
|
|
1237
1467
|
});
|
|
1238
1468
|
const header = ["model", "requests", "prompt", "completion", "total", "cost"];
|
|
@@ -1306,7 +1536,7 @@ function cacheHitPercent(totals) {
|
|
|
1306
1536
|
// src/cli/clipboard.ts
|
|
1307
1537
|
import { execFile as execFile2 } from "child_process";
|
|
1308
1538
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
1309
|
-
import { readFile as readFile3, unlink as unlink2 } from "fs/promises";
|
|
1539
|
+
import { readFile as readFile3, stat as stat2, unlink as unlink2 } from "fs/promises";
|
|
1310
1540
|
import os2 from "os";
|
|
1311
1541
|
import path9 from "path";
|
|
1312
1542
|
import { promisify as promisify2 } from "util";
|
|
@@ -1385,6 +1615,13 @@ async function finalizeClipboardImage(image) {
|
|
|
1385
1615
|
const result = await downsampleImageIfNeeded(image);
|
|
1386
1616
|
return { image: result.image, oversized: result.oversized && !result.resized };
|
|
1387
1617
|
}
|
|
1618
|
+
async function readClipboardTempFile(tempFile) {
|
|
1619
|
+
const st = await stat2(tempFile).catch(() => null);
|
|
1620
|
+
if (!st || st.size === 0 || st.size > MAX_CLIPBOARD_IMAGE_BYTES) return null;
|
|
1621
|
+
const buf = await readFile3(tempFile).catch(() => null);
|
|
1622
|
+
if (!buf || buf.length === 0) return null;
|
|
1623
|
+
return { path: "clipboard.png", mimeType: "image/png", data: buf.toString("base64") };
|
|
1624
|
+
}
|
|
1388
1625
|
async function readClipboardImage() {
|
|
1389
1626
|
const tempFile = path9.join(os2.tmpdir(), `star-clipboard-${randomUUID2()}.png`);
|
|
1390
1627
|
const hasPngpaste = process.platform === "darwin" ? await commandExists2("pngpaste") : false;
|
|
@@ -1397,22 +1634,18 @@ async function readClipboardImage() {
|
|
|
1397
1634
|
maxBuffer: MAX_CLIPBOARD_IMAGE_BYTES,
|
|
1398
1635
|
encoding: "buffer"
|
|
1399
1636
|
});
|
|
1400
|
-
const
|
|
1401
|
-
if (
|
|
1637
|
+
const buf = stdout;
|
|
1638
|
+
if (buf.length === 0) return null;
|
|
1402
1639
|
return finalizeClipboardImage({
|
|
1403
1640
|
path: "clipboard.png",
|
|
1404
1641
|
mimeType: "image/png",
|
|
1405
|
-
data:
|
|
1642
|
+
data: buf.toString("base64")
|
|
1406
1643
|
});
|
|
1407
1644
|
}
|
|
1408
1645
|
await execFileAsync2(plan.command, plan.args, { timeout: CLIPBOARD_TIMEOUT_MS });
|
|
1409
|
-
const
|
|
1410
|
-
if (!
|
|
1411
|
-
return finalizeClipboardImage(
|
|
1412
|
-
path: "clipboard.png",
|
|
1413
|
-
mimeType: "image/png",
|
|
1414
|
-
data: buf.toString("base64")
|
|
1415
|
-
});
|
|
1646
|
+
const image = await readClipboardTempFile(tempFile);
|
|
1647
|
+
if (!image) return null;
|
|
1648
|
+
return finalizeClipboardImage(image);
|
|
1416
1649
|
} catch {
|
|
1417
1650
|
return null;
|
|
1418
1651
|
} finally {
|
|
@@ -1772,7 +2005,7 @@ function applyEdit(content, oldString, newString, replaceAll) {
|
|
|
1772
2005
|
}
|
|
1773
2006
|
if (count === 0) return null;
|
|
1774
2007
|
if (count > 1 && !replaceAll) return null;
|
|
1775
|
-
return replaceAll ? content.split(oldString).join(newString) : content.replace(oldString, newString);
|
|
2008
|
+
return replaceAll ? content.split(oldString).join(newString) : content.replace(oldString, () => newString);
|
|
1776
2009
|
}
|
|
1777
2010
|
function newFilePreview(content, label) {
|
|
1778
2011
|
const all = content.split("\n");
|
|
@@ -2071,13 +2304,22 @@ ${content}`);
|
|
|
2071
2304
|
});
|
|
2072
2305
|
register({
|
|
2073
2306
|
name: "undo",
|
|
2074
|
-
description: "Undo the last conversation turn: revert its file changes (
|
|
2307
|
+
description: "Undo the last conversation turn: revert its file changes (any tool, bash included, when git snapshots are on) and retract its messages",
|
|
2075
2308
|
usage: "/undo",
|
|
2076
2309
|
category: "Changes",
|
|
2077
2310
|
async run(_args, ctx) {
|
|
2078
2311
|
ctx.addSystemMessage(await ctx.undo());
|
|
2079
2312
|
}
|
|
2080
2313
|
});
|
|
2314
|
+
register({
|
|
2315
|
+
name: "redo",
|
|
2316
|
+
description: "Redo the last git-snapshot undo: restore the working tree to its pre-undo state",
|
|
2317
|
+
usage: "/redo",
|
|
2318
|
+
category: "Changes",
|
|
2319
|
+
async run(_args, ctx) {
|
|
2320
|
+
ctx.addSystemMessage(await ctx.redo());
|
|
2321
|
+
}
|
|
2322
|
+
});
|
|
2081
2323
|
register({
|
|
2082
2324
|
name: "rewind",
|
|
2083
2325
|
description: "List file-change checkpoints, or rewind to just before one: restore files and retract the conversation",
|
|
@@ -2349,19 +2591,19 @@ function maskApiKey(key) {
|
|
|
2349
2591
|
if (key.length <= 8) return "\u2022\u2022\u2022\u2022\u2022\u2022";
|
|
2350
2592
|
return `${key.slice(0, 3)}\u2026${key.slice(-4)}`;
|
|
2351
2593
|
}
|
|
2352
|
-
function
|
|
2594
|
+
function tomlString2(value) {
|
|
2353
2595
|
return JSON.stringify(value);
|
|
2354
2596
|
}
|
|
2355
2597
|
function buildProviderTomlBlock(provider) {
|
|
2356
2598
|
return [
|
|
2357
2599
|
"[[providers]]",
|
|
2358
|
-
`name = ${
|
|
2600
|
+
`name = ${tomlString2(provider.name)}`,
|
|
2359
2601
|
"# wire format: openai-compatible | anthropic | openai-responses",
|
|
2360
|
-
`protocol = ${
|
|
2361
|
-
`baseURL = ${
|
|
2602
|
+
`protocol = ${tomlString2(provider.protocol)}`,
|
|
2603
|
+
`baseURL = ${tomlString2(provider.baseURL)}`,
|
|
2362
2604
|
"# environment variable holding the API key; the key itself lives in",
|
|
2363
2605
|
"# ~/.star-cli/.env (written by /connect), never in this file",
|
|
2364
|
-
`apiKeyEnv = ${
|
|
2606
|
+
`apiKeyEnv = ${tomlString2(provider.apiKeyEnv ?? "")}`,
|
|
2365
2607
|
"# inline key instead of apiKeyEnv (not recommended):",
|
|
2366
2608
|
'# apiKey = "sk-..."',
|
|
2367
2609
|
"# extra HTTP headers sent to the provider:",
|
|
@@ -2373,9 +2615,9 @@ function buildProviderTomlBlock(provider) {
|
|
|
2373
2615
|
function buildModelTomlBlock(model) {
|
|
2374
2616
|
return [
|
|
2375
2617
|
"[[models]]",
|
|
2376
|
-
`name = ${
|
|
2377
|
-
`provider = ${
|
|
2378
|
-
`model = ${
|
|
2618
|
+
`name = ${tomlString2(model.name)}`,
|
|
2619
|
+
`provider = ${tomlString2(model.provider)}`,
|
|
2620
|
+
`model = ${tomlString2(model.model)}`,
|
|
2379
2621
|
"# per-model context window in tokens \u2014 overrides the top-level",
|
|
2380
2622
|
"# contextMaxTokens for compaction and the ctx % in the status bar",
|
|
2381
2623
|
`contextMaxTokens = ${model.contextMaxTokens ?? CONNECT_DEFAULT_CONTEXT_TOKENS}`,
|
|
@@ -2443,7 +2685,7 @@ function saveConnection(answers, configPath = globalConfigPath(), envPath = envF
|
|
|
2443
2685
|
return { provider, model, configPath, envPath };
|
|
2444
2686
|
}
|
|
2445
2687
|
function setDefaultModelInToml(content, modelName) {
|
|
2446
|
-
const line = `defaultModel = ${
|
|
2688
|
+
const line = `defaultModel = ${tomlString2(modelName)}`;
|
|
2447
2689
|
const lines = content.split("\n");
|
|
2448
2690
|
let inTopLevel = true;
|
|
2449
2691
|
for (let i = 0; i < lines.length; i++) {
|
|
@@ -2883,6 +3125,20 @@ async function initProject(opts) {
|
|
|
2883
3125
|
};
|
|
2884
3126
|
}
|
|
2885
3127
|
|
|
3128
|
+
// src/cli/commands/redo.ts
|
|
3129
|
+
function formatRedoSummary(entry) {
|
|
3130
|
+
const label = entry.label !== "" ? ` of "${entry.label}"` : "";
|
|
3131
|
+
return `Restore the working tree to its state before the undo${label} (tree ${entry.tree.slice(0, 8)}). Conversation messages are not restored.`;
|
|
3132
|
+
}
|
|
3133
|
+
function formatRedoResult(result) {
|
|
3134
|
+
if (!result.ok) {
|
|
3135
|
+
return `Redo failed \u2014 git could not restore the snapshot (tree ${result.tree.slice(0, 8)}). The redo entry was kept; try again or check that git is working.`;
|
|
3136
|
+
}
|
|
3137
|
+
const files = result.files === null ? "unknown number of" : `${result.files}`;
|
|
3138
|
+
const label = result.label !== "" ? ` \u2014 "${result.label}"` : "";
|
|
3139
|
+
return `Redone: restored ${files} file(s) to the pre-undo state (tree ${result.tree.slice(0, 8)})${label}.`;
|
|
3140
|
+
}
|
|
3141
|
+
|
|
2886
3142
|
// src/cli/commands/registry.ts
|
|
2887
3143
|
var CommandRegistry = class {
|
|
2888
3144
|
commands = /* @__PURE__ */ new Map();
|
|
@@ -2998,6 +3254,16 @@ async function buildUndoDiffs(snapshots, cwd) {
|
|
|
2998
3254
|
}
|
|
2999
3255
|
return diffs;
|
|
3000
3256
|
}
|
|
3257
|
+
async function buildUndoTreeDiffs(cwd, tree) {
|
|
3258
|
+
const names = await diffTreeNames(cwd, tree);
|
|
3259
|
+
if (!names || names.length === 0) return [];
|
|
3260
|
+
return [
|
|
3261
|
+
{
|
|
3262
|
+
label: "Working-tree files restoring the turn's start will touch:",
|
|
3263
|
+
lines: names.map((name) => ({ kind: "marker", text: name }))
|
|
3264
|
+
}
|
|
3265
|
+
];
|
|
3266
|
+
}
|
|
3001
3267
|
|
|
3002
3268
|
// src/cli/components/ConnectWizard.tsx
|
|
3003
3269
|
import { Box as Box4, Text as Text4, useInput as useInput4 } from "ink";
|
|
@@ -3932,18 +4198,19 @@ import { memo } from "react";
|
|
|
3932
4198
|
import { Text as Text6 } from "ink";
|
|
3933
4199
|
import { Fragment, jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
3934
4200
|
function DiffLineView({ line }) {
|
|
4201
|
+
const text = toTerminalSafe(line.text);
|
|
3935
4202
|
if (line.kind === "add") return /* @__PURE__ */ jsxs6(Text6, { color: "green", children: [
|
|
3936
4203
|
"+ ",
|
|
3937
|
-
|
|
4204
|
+
text
|
|
3938
4205
|
] });
|
|
3939
4206
|
if (line.kind === "del") return /* @__PURE__ */ jsxs6(Text6, { color: "red", children: [
|
|
3940
4207
|
"- ",
|
|
3941
|
-
|
|
4208
|
+
text
|
|
3942
4209
|
] });
|
|
3943
|
-
if (line.kind === "marker") return /* @__PURE__ */ jsx6(Text6, { dimColor: true, children:
|
|
4210
|
+
if (line.kind === "marker") return /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: text });
|
|
3944
4211
|
return /* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
|
|
3945
4212
|
" ",
|
|
3946
|
-
|
|
4213
|
+
text
|
|
3947
4214
|
] });
|
|
3948
4215
|
}
|
|
3949
4216
|
function DiffLines({ lines }) {
|
|
@@ -3987,6 +4254,17 @@ var MessageList = memo(function MessageList2({ messages }) {
|
|
|
3987
4254
|
// src/cli/components/PermissionPrompt.tsx
|
|
3988
4255
|
import { Box as Box7, Text as Text8, useInput as useInput6 } from "ink";
|
|
3989
4256
|
import { jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
|
|
4257
|
+
var BASH_COMMAND_MAX = 1e3;
|
|
4258
|
+
function middleTruncate(text, max) {
|
|
4259
|
+
if (text.length <= max) return text;
|
|
4260
|
+
const half = Math.floor((max - 1) / 2);
|
|
4261
|
+
return `${text.slice(0, half)}\u2026${text.slice(text.length - half)}`;
|
|
4262
|
+
}
|
|
4263
|
+
function bashCommand(args) {
|
|
4264
|
+
if (typeof args !== "object" || args === null) return null;
|
|
4265
|
+
const command = args.command;
|
|
4266
|
+
return typeof command === "string" ? command : null;
|
|
4267
|
+
}
|
|
3990
4268
|
function PermissionPrompt({ request, preview, onDecision }) {
|
|
3991
4269
|
useInput6((input) => {
|
|
3992
4270
|
const ch = input.toLowerCase();
|
|
@@ -3994,6 +4272,7 @@ function PermissionPrompt({ request, preview, onDecision }) {
|
|
|
3994
4272
|
else if (ch === "n") onDecision("no");
|
|
3995
4273
|
else if (ch === "a") onDecision("always");
|
|
3996
4274
|
});
|
|
4275
|
+
const command = request.toolName === "bash" ? bashCommand(request.args) : null;
|
|
3997
4276
|
return /* @__PURE__ */ jsxs8(Box7, { flexDirection: "column", borderStyle: "round", borderColor: "yellow", paddingX: 1, children: [
|
|
3998
4277
|
/* @__PURE__ */ jsxs8(Text8, { bold: true, color: "yellow", children: [
|
|
3999
4278
|
"Permission required: ",
|
|
@@ -4005,10 +4284,10 @@ function PermissionPrompt({ request, preview, onDecision }) {
|
|
|
4005
4284
|
preview ? /* @__PURE__ */ jsxs8(Box7, { flexDirection: "column", children: [
|
|
4006
4285
|
/* @__PURE__ */ jsxs8(Text8, { dimColor: true, children: [
|
|
4007
4286
|
preview.type === "new-file" ? "New file: " : "",
|
|
4008
|
-
preview.label
|
|
4287
|
+
toTerminalSafe(preview.label)
|
|
4009
4288
|
] }),
|
|
4010
4289
|
/* @__PURE__ */ jsx8(DiffLines, { lines: preview.lines })
|
|
4011
|
-
] }) : /* @__PURE__ */ jsx8(Text8, { children: summarizeArgs(request.args) }),
|
|
4290
|
+
] }) : command !== null ? /* @__PURE__ */ jsx8(Text8, { children: toTerminalSafe(middleTruncate(command, BASH_COMMAND_MAX)) }) : /* @__PURE__ */ jsx8(Text8, { children: toTerminalSafe(summarizeArgs(request.args)) }),
|
|
4012
4291
|
/* @__PURE__ */ jsx8(Text8, { children: "[y] allow [n] deny [a] always (saved to config)" })
|
|
4013
4292
|
] });
|
|
4014
4293
|
}
|
|
@@ -4067,7 +4346,7 @@ var StatusBar = memo2(function StatusBar2({
|
|
|
4067
4346
|
}) {
|
|
4068
4347
|
const { stdout } = useStdout2();
|
|
4069
4348
|
const wide = (stdout?.columns ?? 80) >= WIDE_COLUMNS;
|
|
4070
|
-
const joined = backgroundTasks.join(", ");
|
|
4349
|
+
const joined = backgroundTasks.map((label) => toTerminalSafe(label)).join(", ");
|
|
4071
4350
|
const labels = joined.length > BG_LABEL_MAX ? `${joined.slice(0, BG_LABEL_MAX)}\u2026` : joined;
|
|
4072
4351
|
return /* @__PURE__ */ jsxs10(Box9, { justifyContent: "space-between", children: [
|
|
4073
4352
|
/* @__PURE__ */ jsxs10(Text10, { dimColor: true, children: [
|
|
@@ -4156,17 +4435,6 @@ function ThinkingIndicator({
|
|
|
4156
4435
|
|
|
4157
4436
|
// src/cli/components/TodoPanel.tsx
|
|
4158
4437
|
import { Box as Box12, Text as Text13 } from "ink";
|
|
4159
|
-
|
|
4160
|
-
// src/cli/terminal-text.ts
|
|
4161
|
-
var UNSAFE_CHARS = /[\x00-\x09\x0b-\x1f\x7f]/;
|
|
4162
|
-
var CONTROL_CHARS = /[\x00-\x08\x0b-\x1f\x7f]/g;
|
|
4163
|
-
var TABS = /\t/g;
|
|
4164
|
-
function toTerminalSafe(text) {
|
|
4165
|
-
if (!UNSAFE_CHARS.test(text)) return text;
|
|
4166
|
-
return text.replace(TABS, " ").replace(CONTROL_CHARS, "");
|
|
4167
|
-
}
|
|
4168
|
-
|
|
4169
|
-
// src/cli/components/TodoPanel.tsx
|
|
4170
4438
|
import { jsxs as jsxs13 } from "react/jsx-runtime";
|
|
4171
4439
|
var SYMBOLS = {
|
|
4172
4440
|
pending: "[ ]",
|
|
@@ -4356,6 +4624,18 @@ async function executeShellBang(raw, cwd, signal) {
|
|
|
4356
4624
|
};
|
|
4357
4625
|
}
|
|
4358
4626
|
|
|
4627
|
+
// src/cli/system-prompt.ts
|
|
4628
|
+
var SYSTEM_PROMPT = `You are Star CLI, an AI coding agent running in the user's terminal.
|
|
4629
|
+
You help with software engineering tasks: reading, writing and editing code, running shell commands, and managing todos.
|
|
4630
|
+
Be concise and direct. Use tools when they help accomplish the task.
|
|
4631
|
+
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.
|
|
4632
|
+
The working directory is the user's project root; never touch files outside it without explicit instruction.
|
|
4633
|
+
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.
|
|
4634
|
+
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.
|
|
4635
|
+
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.
|
|
4636
|
+
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.
|
|
4637
|
+
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.`;
|
|
4638
|
+
|
|
4359
4639
|
// src/cli/ticker.ts
|
|
4360
4640
|
var TICK_MS = 100;
|
|
4361
4641
|
function nextFlush(prev, next) {
|
|
@@ -4414,20 +4694,25 @@ function runningTaskLabels() {
|
|
|
4414
4694
|
return [...shells, ...agents];
|
|
4415
4695
|
}
|
|
4416
4696
|
var SESSION_MODE_CYCLE = ["ask", "auto", "readonly", "plan"];
|
|
4697
|
+
var BUSY_BLOCKED_COMMANDS = /* @__PURE__ */ new Set([
|
|
4698
|
+
"new",
|
|
4699
|
+
"resume",
|
|
4700
|
+
"undo",
|
|
4701
|
+
"redo",
|
|
4702
|
+
"rewind",
|
|
4703
|
+
"compact",
|
|
4704
|
+
"fork",
|
|
4705
|
+
"clear-sessions",
|
|
4706
|
+
"model",
|
|
4707
|
+
"connect",
|
|
4708
|
+
"memory"
|
|
4709
|
+
]);
|
|
4417
4710
|
function formatUsage(usage) {
|
|
4418
4711
|
return `API usage this session: ${usage.requests} requests, ${usage.promptTokens} prompt + ${usage.completionTokens} completion = ${usage.totalTokens} tokens`;
|
|
4419
4712
|
}
|
|
4420
4713
|
var SESSION_PICKER_CAP = 20;
|
|
4714
|
+
var SESSION_PREVIEW_MAX = 60;
|
|
4421
4715
|
var MAX_VISIBLE_QUEUED = 5;
|
|
4422
|
-
async function sessionPreview(id) {
|
|
4423
|
-
const store = await SessionStore.open(id);
|
|
4424
|
-
if (!store) return null;
|
|
4425
|
-
const first = (await store.messages()).find((m) => m.role === "user");
|
|
4426
|
-
if (!first) return null;
|
|
4427
|
-
const text = coreMessageText(first).replace(/\s+/g, " ").trim();
|
|
4428
|
-
if (!text) return null;
|
|
4429
|
-
return text.length > 60 ? `${text.slice(0, 60)}\u2026` : text;
|
|
4430
|
-
}
|
|
4431
4716
|
function Repl({
|
|
4432
4717
|
backend,
|
|
4433
4718
|
model,
|
|
@@ -4463,12 +4748,14 @@ function Repl({
|
|
|
4463
4748
|
const nextIdRef = useRef5(initialDisplay.length);
|
|
4464
4749
|
const abortRef = useRef5(null);
|
|
4465
4750
|
const streamedRef = useRef5("");
|
|
4751
|
+
const lastCommittedLenRef = useRef5(0);
|
|
4466
4752
|
const thinkingRef = useRef5(false);
|
|
4467
4753
|
const reasoningRef = useRef5("");
|
|
4468
4754
|
const tickerStopRef = useRef5(null);
|
|
4469
4755
|
const flushedRef = useRef5({ streamed: "", reasoning: "" });
|
|
4470
4756
|
const toolCardsRef = useRef5(/* @__PURE__ */ new Map());
|
|
4471
4757
|
const pendingRef = useRef5(null);
|
|
4758
|
+
const permissionChainRef = useRef5(Promise.resolve());
|
|
4472
4759
|
const alwaysAllowedRef = useRef5(/* @__PURE__ */ new Set());
|
|
4473
4760
|
const prevModeRef = useRef5(null);
|
|
4474
4761
|
const planApprovalRef = useRef5(false);
|
|
@@ -4477,6 +4764,10 @@ function Repl({
|
|
|
4477
4764
|
const pendingConnectRef = useRef5(null);
|
|
4478
4765
|
const modelNameRef = useRef5(model);
|
|
4479
4766
|
const sessionStoreRef = useRef5(sessionStore);
|
|
4767
|
+
const configRef = useRef5(config);
|
|
4768
|
+
useEffect2(() => {
|
|
4769
|
+
configRef.current = config;
|
|
4770
|
+
}, [config]);
|
|
4480
4771
|
const usageRef = useRef5(initialUsage ? { ...initialUsage } : emptyUsage());
|
|
4481
4772
|
const turnChunksRef = useRef5(0);
|
|
4482
4773
|
const messagesRef = useRef5(initialDisplay);
|
|
@@ -4652,14 +4943,22 @@ function Repl({
|
|
|
4652
4943
|
);
|
|
4653
4944
|
const attachConfirmHandler = useCallback(
|
|
4654
4945
|
(target) => {
|
|
4655
|
-
target.confirmHandler =
|
|
4946
|
+
target.confirmHandler = (req) => {
|
|
4656
4947
|
if (isAllowedByRules([...alwaysAllowedRef.current], req)) {
|
|
4657
|
-
return true;
|
|
4948
|
+
return Promise.resolve(true);
|
|
4658
4949
|
}
|
|
4659
|
-
const
|
|
4660
|
-
|
|
4661
|
-
|
|
4662
|
-
|
|
4950
|
+
const ask = async () => {
|
|
4951
|
+
const preview = await generateDiffPreview(req.toolName, req.args, cwd).catch(() => null);
|
|
4952
|
+
return new Promise((resolve) => {
|
|
4953
|
+
setPendingPermission({ request: req, preview, resolve });
|
|
4954
|
+
});
|
|
4955
|
+
};
|
|
4956
|
+
const decision = permissionChainRef.current.then(ask);
|
|
4957
|
+
permissionChainRef.current = decision.then(
|
|
4958
|
+
() => void 0,
|
|
4959
|
+
() => void 0
|
|
4960
|
+
);
|
|
4961
|
+
return decision;
|
|
4663
4962
|
};
|
|
4664
4963
|
target.onHookWarning = (message) => pushMessage("system", message);
|
|
4665
4964
|
},
|
|
@@ -4690,27 +4989,29 @@ function Repl({
|
|
|
4690
4989
|
setBgLabels(runningTaskLabels());
|
|
4691
4990
|
pushMessage(
|
|
4692
4991
|
"system",
|
|
4693
|
-
|
|
4992
|
+
toTerminalSafe(
|
|
4993
|
+
task.status === "running" ? formatTaskStarted(task) : formatTaskFinished(task)
|
|
4994
|
+
)
|
|
4694
4995
|
);
|
|
4695
4996
|
if (task.status !== "running") {
|
|
4696
4997
|
notifyBell({
|
|
4697
|
-
enabled:
|
|
4698
|
-
thresholdSec:
|
|
4998
|
+
enabled: configRef.current.notifyBell,
|
|
4999
|
+
thresholdSec: configRef.current.notifyBellThresholdSec,
|
|
4699
5000
|
noNotifyEnv: process.env.STAR_NO_NOTIFY === "1"
|
|
4700
5001
|
});
|
|
4701
5002
|
}
|
|
4702
5003
|
};
|
|
4703
5004
|
const onAgentUpdate = (task) => {
|
|
4704
5005
|
setBgLabels(runningTaskLabels());
|
|
4705
|
-
const label = task.description ? `: ${task.description}` : "";
|
|
5006
|
+
const label = task.description ? `: ${toTerminalSafe(task.description)}` : "";
|
|
4706
5007
|
pushMessage(
|
|
4707
5008
|
"system",
|
|
4708
5009
|
task.status === "running" ? `Background subagent ${task.id} started${label}` : `Background subagent ${task.id} ${task.status}${label}`
|
|
4709
5010
|
);
|
|
4710
5011
|
if (task.status !== "running") {
|
|
4711
5012
|
notifyBell({
|
|
4712
|
-
enabled:
|
|
4713
|
-
thresholdSec:
|
|
5013
|
+
enabled: configRef.current.notifyBell,
|
|
5014
|
+
thresholdSec: configRef.current.notifyBellThresholdSec,
|
|
4714
5015
|
noNotifyEnv: process.env.STAR_NO_NOTIFY === "1"
|
|
4715
5016
|
});
|
|
4716
5017
|
}
|
|
@@ -4723,7 +5024,7 @@ function Repl({
|
|
|
4723
5024
|
defaultTaskManager.cleanup();
|
|
4724
5025
|
defaultAgentTasks.cleanup();
|
|
4725
5026
|
};
|
|
4726
|
-
}, [pushMessage
|
|
5027
|
+
}, [pushMessage]);
|
|
4727
5028
|
const interrupt = useCallback(() => {
|
|
4728
5029
|
abortRef.current?.abort();
|
|
4729
5030
|
const p = pendingRef.current;
|
|
@@ -4762,9 +5063,11 @@ function Repl({
|
|
|
4762
5063
|
const handleExit = useCallback(() => {
|
|
4763
5064
|
const killed = defaultTaskManager.cleanup();
|
|
4764
5065
|
const killedAgents = defaultAgentTasks.cleanup();
|
|
4765
|
-
const stopped =
|
|
4766
|
-
if (stopped
|
|
4767
|
-
pushMessage("system",
|
|
5066
|
+
const stopped = killed.length + killedAgents.length;
|
|
5067
|
+
if (stopped > 0) {
|
|
5068
|
+
pushMessage("system", `${stopped} background task(s) still running; reports will be lost.`);
|
|
5069
|
+
setTimeout(exit, 50);
|
|
5070
|
+
return;
|
|
4768
5071
|
}
|
|
4769
5072
|
exit();
|
|
4770
5073
|
}, [exit, pushMessage]);
|
|
@@ -4825,6 +5128,7 @@ function Repl({
|
|
|
4825
5128
|
registry: createDefaultRegistry(),
|
|
4826
5129
|
config,
|
|
4827
5130
|
cwd,
|
|
5131
|
+
system: SYSTEM_PROMPT,
|
|
4828
5132
|
sessionStore: sessionStoreRef.current,
|
|
4829
5133
|
contextMaxTokens: contextWindowTokens(config, name)
|
|
4830
5134
|
});
|
|
@@ -4866,12 +5170,15 @@ function Repl({
|
|
|
4866
5170
|
if (!resumed) {
|
|
4867
5171
|
return `Session not found: ${id}`;
|
|
4868
5172
|
}
|
|
4869
|
-
await current.loadMessages(resumed.messages);
|
|
4870
|
-
setTodos(await loadTodos(cwd));
|
|
4871
5173
|
const store = await SessionStore.open(resolvedId);
|
|
4872
|
-
if (store) {
|
|
4873
|
-
|
|
5174
|
+
if (!store) {
|
|
5175
|
+
return `Session not found: ${id}`;
|
|
4874
5176
|
}
|
|
5177
|
+
await current.loadMessages(resumed.messages);
|
|
5178
|
+
current.setSessionStore(store);
|
|
5179
|
+
sessionStoreRef.current = store;
|
|
5180
|
+
setTodos(await loadTodos(cwd));
|
|
5181
|
+
hydrateSnapshots(await loadSessionSnapshots(store.dir));
|
|
4875
5182
|
const display = buildDisplayMessages(resumed.messages);
|
|
4876
5183
|
nextIdRef.current = display.length;
|
|
4877
5184
|
applyMessages(display);
|
|
@@ -4897,6 +5204,7 @@ function Repl({
|
|
|
4897
5204
|
abortRef.current = controller;
|
|
4898
5205
|
const turnStartedAt = Date.now();
|
|
4899
5206
|
streamedRef.current = "";
|
|
5207
|
+
lastCommittedLenRef.current = 0;
|
|
4900
5208
|
thinkingRef.current = true;
|
|
4901
5209
|
reasoningRef.current = "";
|
|
4902
5210
|
turnChunksRef.current = 0;
|
|
@@ -4907,22 +5215,25 @@ function Repl({
|
|
|
4907
5215
|
setIsStreaming(true);
|
|
4908
5216
|
flushedRef.current = { streamed: "", reasoning: "" };
|
|
4909
5217
|
const commitStreamed = (tight) => {
|
|
4910
|
-
|
|
4911
|
-
|
|
5218
|
+
const uncommitted = streamedRef.current.slice(lastCommittedLenRef.current);
|
|
5219
|
+
if (uncommitted.length > 0) {
|
|
5220
|
+
pushAssistantChunk(uncommitted, tight);
|
|
5221
|
+
}
|
|
4912
5222
|
streamedRef.current = "";
|
|
5223
|
+
lastCommittedLenRef.current = 0;
|
|
4913
5224
|
flushedRef.current = { ...flushedRef.current, streamed: "" };
|
|
4914
5225
|
setStreamingText("");
|
|
4915
5226
|
};
|
|
4916
5227
|
tickerStopRef.current = startTicker((tick) => {
|
|
4917
5228
|
setSpinnerTick(tick);
|
|
4918
|
-
const
|
|
5229
|
+
const uncommitted = streamedRef.current.slice(lastCommittedLenRef.current);
|
|
5230
|
+
const split = splitCommittableLines(uncommitted, 8);
|
|
4919
5231
|
if (split) {
|
|
4920
5232
|
pushAssistantChunk(split.committed, true);
|
|
4921
|
-
|
|
4922
|
-
flushedRef.current = { ...flushedRef.current, streamed: "" };
|
|
5233
|
+
lastCommittedLenRef.current += split.committed.length + 1;
|
|
4923
5234
|
}
|
|
4924
5235
|
const next = {
|
|
4925
|
-
streamed: streamedRef.current,
|
|
5236
|
+
streamed: streamedRef.current.slice(lastCommittedLenRef.current),
|
|
4926
5237
|
reasoning: reasoningRef.current
|
|
4927
5238
|
};
|
|
4928
5239
|
if (nextFlush(flushedRef.current, next) !== null) {
|
|
@@ -4991,6 +5302,12 @@ function Repl({
|
|
|
4991
5302
|
usage.promptTokens += event.usage.promptTokens;
|
|
4992
5303
|
usage.completionTokens += event.usage.completionTokens;
|
|
4993
5304
|
usage.totalTokens += event.usage.totalTokens;
|
|
5305
|
+
if (event.usage.cachedPromptTokens) {
|
|
5306
|
+
usage.cachedPromptTokens = (usage.cachedPromptTokens ?? 0) + event.usage.cachedPromptTokens;
|
|
5307
|
+
}
|
|
5308
|
+
if (event.usage.cacheReadInputTokens) {
|
|
5309
|
+
usage.cacheReadInputTokens = (usage.cacheReadInputTokens ?? 0) + event.usage.cacheReadInputTokens;
|
|
5310
|
+
}
|
|
4994
5311
|
if (cacheUsageReported(event.usage)) {
|
|
4995
5312
|
if (!usage.cache) usage.cache = { cachedTokens: 0, promptTokens: 0 };
|
|
4996
5313
|
addToCacheTotals(usage.cache, event.usage);
|
|
@@ -5000,6 +5317,9 @@ function Repl({
|
|
|
5000
5317
|
});
|
|
5001
5318
|
}
|
|
5002
5319
|
} else if (event.type === "retry") {
|
|
5320
|
+
streamedRef.current = streamedRef.current.slice(0, lastCommittedLenRef.current);
|
|
5321
|
+
flushedRef.current = { ...flushedRef.current, streamed: "" };
|
|
5322
|
+
setStreamingText("");
|
|
5003
5323
|
const wait = event.delayMs !== void 0 && event.delayMs >= 1e3 ? ` in ${Math.round(event.delayMs / 1e3)}s` : "";
|
|
5004
5324
|
pushMessage(
|
|
5005
5325
|
"system",
|
|
@@ -5030,8 +5350,9 @@ function Repl({
|
|
|
5030
5350
|
setThinkingText("");
|
|
5031
5351
|
setThoughtSummary(null);
|
|
5032
5352
|
setActivity(null);
|
|
5033
|
-
const finalText = streamedRef.current;
|
|
5353
|
+
const finalText = streamedRef.current.slice(lastCommittedLenRef.current);
|
|
5034
5354
|
streamedRef.current = "";
|
|
5355
|
+
lastCommittedLenRef.current = 0;
|
|
5035
5356
|
setStreamingText(null);
|
|
5036
5357
|
const interrupted = controller.signal.aborted;
|
|
5037
5358
|
notifyBell({
|
|
@@ -5214,10 +5535,8 @@ ${lines.join("\n")}`;
|
|
|
5214
5535
|
return ctx.permissionMode(picked);
|
|
5215
5536
|
},
|
|
5216
5537
|
pickSession: async (all) => {
|
|
5217
|
-
const
|
|
5218
|
-
|
|
5219
|
-
SESSION_PICKER_CAP
|
|
5220
|
-
);
|
|
5538
|
+
const currentId = sessionStoreRef.current?.id;
|
|
5539
|
+
const entries = (await listSessionEntries(all ? void 0 : cwd)).filter((entry) => entry.meta.id !== currentId).slice(0, SESSION_PICKER_CAP);
|
|
5221
5540
|
if (entries.length === 0) {
|
|
5222
5541
|
return all ? "No sessions found." : "No sessions found for this directory.";
|
|
5223
5542
|
}
|
|
@@ -5225,13 +5544,16 @@ ${lines.join("\n")}`;
|
|
|
5225
5544
|
entries.map(async ({ meta, messageCount }) => {
|
|
5226
5545
|
const parts = [`${messageCount} messages \xB7 ${relativeTime(meta.updatedAt)}`];
|
|
5227
5546
|
const preview = await sessionPreview(meta.id);
|
|
5228
|
-
if (preview)
|
|
5547
|
+
if (preview) {
|
|
5548
|
+
parts.push(
|
|
5549
|
+
preview.length > SESSION_PREVIEW_MAX ? `${preview.slice(0, SESSION_PREVIEW_MAX)}\u2026` : preview
|
|
5550
|
+
);
|
|
5551
|
+
}
|
|
5229
5552
|
if (all) parts.push(`[${meta.cwd}]`);
|
|
5230
5553
|
return {
|
|
5231
5554
|
value: meta.id,
|
|
5232
5555
|
label: meta.title || shortSessionId(meta.id),
|
|
5233
|
-
description: parts.join(" \xB7 ")
|
|
5234
|
-
hint: meta.id === sessionStoreRef.current?.id ? "current" : void 0
|
|
5556
|
+
description: parts.join(" \xB7 ")
|
|
5235
5557
|
};
|
|
5236
5558
|
})
|
|
5237
5559
|
);
|
|
@@ -5344,15 +5666,16 @@ ${estimateCost(usageRef.current, modelNameRef.current, modelConfig)}`;
|
|
|
5344
5666
|
if (preview.removed === 0) {
|
|
5345
5667
|
return "Nothing to undo (no conversation turn to retract).";
|
|
5346
5668
|
}
|
|
5347
|
-
const diffs = await buildUndoDiffs(
|
|
5669
|
+
const diffs = preview.tree ? await buildUndoTreeDiffs(cwd, preview.tree) : await buildUndoDiffs(
|
|
5348
5670
|
preview.turn !== void 0 ? listTurnSnapshots(preview.turn) : [],
|
|
5349
5671
|
cwd
|
|
5350
5672
|
);
|
|
5673
|
+
const summary = preview.tree ? `${preview.removed} message(s) will be retracted, and the working tree restored to the turn's start (git snapshot \u2014 covers changes from any tool, bash included).` : `${preview.removed} message(s) will be retracted, ${diffs.length} file change(s) reverted.`;
|
|
5351
5674
|
const confirmed = await new Promise((resolve) => {
|
|
5352
5675
|
setPendingRewind({
|
|
5353
5676
|
title: "Undo last turn",
|
|
5354
5677
|
confirmLabel: "undo",
|
|
5355
|
-
summary
|
|
5678
|
+
summary,
|
|
5356
5679
|
diffs,
|
|
5357
5680
|
resolve
|
|
5358
5681
|
});
|
|
@@ -5360,11 +5683,10 @@ ${estimateCost(usageRef.current, modelNameRef.current, modelConfig)}`;
|
|
|
5360
5683
|
if (!confirmed) {
|
|
5361
5684
|
return "Undo cancelled.";
|
|
5362
5685
|
}
|
|
5363
|
-
const
|
|
5364
|
-
if (removed === 0) {
|
|
5686
|
+
const outcome = await current.undoLastTurn();
|
|
5687
|
+
if (outcome.removed === 0) {
|
|
5365
5688
|
return "Nothing to undo (no conversation turn to retract).";
|
|
5366
5689
|
}
|
|
5367
|
-
const reverted = turn !== void 0 ? await undoTurnSnapshots(turn) : [];
|
|
5368
5690
|
const visible = messagesRef.current;
|
|
5369
5691
|
let cut = -1;
|
|
5370
5692
|
for (let i = visible.length - 1; i >= 0; i--) {
|
|
@@ -5374,9 +5696,36 @@ ${estimateCost(usageRef.current, modelNameRef.current, modelConfig)}`;
|
|
|
5374
5696
|
}
|
|
5375
5697
|
}
|
|
5376
5698
|
redrawMessages(cut >= 0 ? visible.slice(0, cut) : visible);
|
|
5377
|
-
return [
|
|
5378
|
-
|
|
5379
|
-
|
|
5699
|
+
return [
|
|
5700
|
+
...outcome.reverted,
|
|
5701
|
+
`Retracted the last conversation turn (${outcome.removed} messages).`
|
|
5702
|
+
].join("\n");
|
|
5703
|
+
},
|
|
5704
|
+
redo: async () => {
|
|
5705
|
+
const current = backendRef.current;
|
|
5706
|
+
if (!(current instanceof AgentLoop)) {
|
|
5707
|
+
return "Nothing to redo.";
|
|
5708
|
+
}
|
|
5709
|
+
const entry = current.peekRedo();
|
|
5710
|
+
if (!entry) {
|
|
5711
|
+
return "Nothing to redo.";
|
|
5712
|
+
}
|
|
5713
|
+
const confirmed = await new Promise((resolve) => {
|
|
5714
|
+
setPendingRewind({
|
|
5715
|
+
title: "Redo last undo",
|
|
5716
|
+
confirmLabel: "redo",
|
|
5717
|
+
summary: formatRedoSummary(entry),
|
|
5718
|
+
resolve
|
|
5719
|
+
});
|
|
5720
|
+
});
|
|
5721
|
+
if (!confirmed) {
|
|
5722
|
+
return "Redo cancelled.";
|
|
5723
|
+
}
|
|
5724
|
+
const result = await current.redoLastUndo();
|
|
5725
|
+
if (!result) {
|
|
5726
|
+
return "Nothing to redo.";
|
|
5727
|
+
}
|
|
5728
|
+
return formatRedoResult(result);
|
|
5380
5729
|
},
|
|
5381
5730
|
rewind: async (args) => {
|
|
5382
5731
|
if (!args) {
|
|
@@ -5559,6 +5908,10 @@ ${plan.affected.length} file change(s) will be reverted, ${messageCount} message
|
|
|
5559
5908
|
);
|
|
5560
5909
|
return;
|
|
5561
5910
|
}
|
|
5911
|
+
if (abortRef.current && BUSY_BLOCKED_COMMANDS.has(parsed.name)) {
|
|
5912
|
+
pushMessage("system", "Busy \u2014 wait for the turn to finish or press Esc to interrupt it.");
|
|
5913
|
+
return;
|
|
5914
|
+
}
|
|
5562
5915
|
void command.run(parsed.args, registry.ctx);
|
|
5563
5916
|
return;
|
|
5564
5917
|
}
|
|
@@ -5741,6 +6094,37 @@ function parseTomlFile(content, filePath) {
|
|
|
5741
6094
|
function mergeConfig(base, override) {
|
|
5742
6095
|
return { ...base, ...override };
|
|
5743
6096
|
}
|
|
6097
|
+
var PROJECT_ALLOWED_KEYS = /* @__PURE__ */ new Set([
|
|
6098
|
+
"defaultModel",
|
|
6099
|
+
"models",
|
|
6100
|
+
"maxSteps",
|
|
6101
|
+
"contextMaxTokens",
|
|
6102
|
+
"streamIdleTimeoutSec",
|
|
6103
|
+
"streamFirstChunkTimeoutSec",
|
|
6104
|
+
"streamMaxRetries",
|
|
6105
|
+
"maxAutoContinues",
|
|
6106
|
+
"contextCompaction",
|
|
6107
|
+
"sessionBudgetUsd",
|
|
6108
|
+
"notifyBell",
|
|
6109
|
+
"notifyBellThresholdSec",
|
|
6110
|
+
"doomLoopThreshold",
|
|
6111
|
+
"gitSnapshots"
|
|
6112
|
+
]);
|
|
6113
|
+
function sanitizeProjectConfig(project, filePath) {
|
|
6114
|
+
const sanitized = {};
|
|
6115
|
+
for (const [key, value] of Object.entries(project)) {
|
|
6116
|
+
if (value === void 0) continue;
|
|
6117
|
+
if (PROJECT_ALLOWED_KEYS.has(key)) {
|
|
6118
|
+
sanitized[key] = value;
|
|
6119
|
+
} else {
|
|
6120
|
+
process.stderr.write(
|
|
6121
|
+
`[star] ignoring "${key}" in ${filePath}: project config cannot set this key
|
|
6122
|
+
`
|
|
6123
|
+
);
|
|
6124
|
+
}
|
|
6125
|
+
}
|
|
6126
|
+
return sanitized;
|
|
6127
|
+
}
|
|
5744
6128
|
function applyOverrides(config, overrides) {
|
|
5745
6129
|
if (!overrides) return config;
|
|
5746
6130
|
const merged = { ...config };
|
|
@@ -5756,22 +6140,13 @@ function readTomlFileSync(filePath) {
|
|
|
5756
6140
|
function loadConfigSync(cwd, overrides) {
|
|
5757
6141
|
loadEnvFile();
|
|
5758
6142
|
const global = readTomlFileSync(globalConfigPath());
|
|
5759
|
-
const
|
|
6143
|
+
const projectPath = projectConfigPath(cwd);
|
|
6144
|
+
const project = sanitizeProjectConfig(readTomlFileSync(projectPath), projectPath);
|
|
5760
6145
|
const merged = applyOverrides(mergeConfig(global, project), overrides);
|
|
5761
6146
|
return ConfigSchema.parse(merged);
|
|
5762
6147
|
}
|
|
5763
6148
|
|
|
5764
6149
|
// src/main.tsx
|
|
5765
|
-
var SYSTEM_PROMPT = `You are Star CLI, an AI coding agent running in the user's terminal.
|
|
5766
|
-
You help with software engineering tasks: reading, writing and editing code, running shell commands, and managing todos.
|
|
5767
|
-
Be concise and direct. Use tools when they help accomplish the task.
|
|
5768
|
-
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.
|
|
5769
|
-
The working directory is the user's project root; never touch files outside it without explicit instruction.
|
|
5770
|
-
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.
|
|
5771
|
-
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.
|
|
5772
|
-
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.
|
|
5773
|
-
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.
|
|
5774
|
-
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.`;
|
|
5775
6150
|
async function createLoop(config, modelName, cwd, sessionStore) {
|
|
5776
6151
|
const model = createModel(config, modelName);
|
|
5777
6152
|
const registry = createDefaultRegistry();
|
|
@@ -5785,7 +6160,7 @@ async function createLoop(config, modelName, cwd, sessionStore) {
|
|
|
5785
6160
|
contextMaxTokens: contextWindowTokens(config, modelName)
|
|
5786
6161
|
});
|
|
5787
6162
|
}
|
|
5788
|
-
async function printMode(loop, prompt, cwd, json, imagePaths) {
|
|
6163
|
+
async function printMode(loop, prompt, cwd, json, imagePaths, sessionStore) {
|
|
5789
6164
|
const flagImages = [];
|
|
5790
6165
|
for (const imagePath of imagePaths) {
|
|
5791
6166
|
if (!imageMimeType(imagePath)) {
|
|
@@ -5854,6 +6229,10 @@ async function printMode(loop, prompt, cwd, json, imagePaths) {
|
|
|
5854
6229
|
}
|
|
5855
6230
|
case "finish":
|
|
5856
6231
|
usage.add(event.usage);
|
|
6232
|
+
if (event.usage) {
|
|
6233
|
+
sessionStore?.addUsage(event.usage).catch(() => {
|
|
6234
|
+
});
|
|
6235
|
+
}
|
|
5857
6236
|
break;
|
|
5858
6237
|
case "retry": {
|
|
5859
6238
|
if (!json) {
|
|
@@ -5903,8 +6282,15 @@ async function printMode(loop, prompt, cwd, json, imagePaths) {
|
|
|
5903
6282
|
}
|
|
5904
6283
|
}
|
|
5905
6284
|
if (!json) process.stdout.write("\n");
|
|
5906
|
-
defaultTaskManager.cleanup();
|
|
5907
|
-
defaultAgentTasks.cleanup();
|
|
6285
|
+
const killedShells = defaultTaskManager.cleanup();
|
|
6286
|
+
const killedAgents = defaultAgentTasks.cleanup();
|
|
6287
|
+
const stopped = killedShells.length + killedAgents.length;
|
|
6288
|
+
if (stopped > 0) {
|
|
6289
|
+
process.stderr.write(
|
|
6290
|
+
`[info] ${stopped} background task(s) were still running and have been stopped.
|
|
6291
|
+
`
|
|
6292
|
+
);
|
|
6293
|
+
}
|
|
5908
6294
|
return exitCode;
|
|
5909
6295
|
}
|
|
5910
6296
|
var program = new Command();
|
|
@@ -5982,12 +6368,17 @@ program.name("star").description("Star CLI \u2014 an AI agent command-line inter
|
|
|
5982
6368
|
}
|
|
5983
6369
|
}
|
|
5984
6370
|
if (resumeId) {
|
|
5985
|
-
|
|
5986
|
-
if (!
|
|
6371
|
+
sessionStore = await SessionStore.open(resumeId);
|
|
6372
|
+
if (!sessionStore) {
|
|
5987
6373
|
console.error(`Session not found: ${resumeId}`);
|
|
5988
6374
|
process.exit(1);
|
|
5989
6375
|
}
|
|
5990
|
-
|
|
6376
|
+
const [meta, loaded] = await Promise.all([sessionStore.meta(), sessionStore.messages()]);
|
|
6377
|
+
const messages = reconcileToolCalls(loaded);
|
|
6378
|
+
if (messages.length !== loaded.length) {
|
|
6379
|
+
await sessionStore.replaceMessages(messages);
|
|
6380
|
+
}
|
|
6381
|
+
resumed = { meta, messages };
|
|
5991
6382
|
} else if (!opts.print) {
|
|
5992
6383
|
sessionStore = await SessionStore.create(cwd, modelName);
|
|
5993
6384
|
}
|
|
@@ -6011,7 +6402,8 @@ program.name("star").description("Star CLI \u2014 an AI agent command-line inter
|
|
|
6011
6402
|
opts.print,
|
|
6012
6403
|
cwd,
|
|
6013
6404
|
Boolean(opts.json),
|
|
6014
|
-
opts.image ?? []
|
|
6405
|
+
opts.image ?? [],
|
|
6406
|
+
sessionStore
|
|
6015
6407
|
);
|
|
6016
6408
|
return;
|
|
6017
6409
|
}
|