@cryer/star-cli 0.3.1 → 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 +3 -1
- package/README.zh-CN.md +317 -0
- package/dist/{chunk-K4SIXWCI.js → chunk-3L6QKJM4.js} +1368 -716
- package/dist/chunk-3L6QKJM4.js.map +1 -0
- package/dist/chunk-B6P4VUBE.js +122 -0
- package/dist/chunk-B6P4VUBE.js.map +1 -0
- package/dist/{loop-ED6AAMWV.js → loop-TQ7GFTTE.js} +3 -2
- package/dist/main.js +724 -209
- package/dist/main.js.map +1 -1
- package/dist/stream-H4BYBVAZ.js +184 -0
- package/dist/stream-H4BYBVAZ.js.map +1 -0
- package/package.json +16 -25
- package/dist/chunk-K4SIXWCI.js.map +0 -1
- package/dist/stream-YQCRUBO3.js +0 -106
- package/dist/stream-YQCRUBO3.js.map +0 -1
- /package/dist/{loop-ED6AAMWV.js.map → loop-TQ7GFTTE.js.map} +0 -0
package/dist/main.js
CHANGED
|
@@ -17,15 +17,14 @@ import {
|
|
|
17
17
|
createIgnorePredicate,
|
|
18
18
|
defaultAgentTasks,
|
|
19
19
|
defaultTaskManager,
|
|
20
|
+
diffTreeNames,
|
|
20
21
|
discoverSkills,
|
|
21
|
-
envFilePath,
|
|
22
22
|
estimateTokens,
|
|
23
23
|
formatTaskFinished,
|
|
24
24
|
formatTaskList,
|
|
25
25
|
formatTaskStarted,
|
|
26
26
|
formatTodos,
|
|
27
27
|
getGitSummary,
|
|
28
|
-
globalConfigPath,
|
|
29
28
|
hydrateSnapshots,
|
|
30
29
|
isAllowedByRules,
|
|
31
30
|
isDangerousCommand,
|
|
@@ -36,22 +35,34 @@ import {
|
|
|
36
35
|
loadTodos,
|
|
37
36
|
parseTodoArgs,
|
|
38
37
|
probeImageDimensions,
|
|
39
|
-
projectConfigPath,
|
|
40
38
|
reconcileToolCalls,
|
|
41
39
|
resetTodos,
|
|
42
40
|
resolveShell,
|
|
43
41
|
resolveSnapshotContent,
|
|
44
42
|
rewindToSnapshot,
|
|
43
|
+
summarizeMessages
|
|
44
|
+
} from "./chunk-3L6QKJM4.js";
|
|
45
|
+
import {
|
|
46
|
+
envFilePath,
|
|
47
|
+
globalConfigPath,
|
|
48
|
+
projectConfigPath,
|
|
45
49
|
sessionsDir,
|
|
46
50
|
starHome,
|
|
47
|
-
summarizeMessages,
|
|
48
|
-
undoTurnSnapshots,
|
|
49
51
|
userMemoryPath
|
|
50
|
-
} from "./chunk-
|
|
52
|
+
} from "./chunk-B6P4VUBE.js";
|
|
51
53
|
|
|
52
54
|
// src/main.tsx
|
|
53
55
|
import { Command } from "commander";
|
|
54
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
|
+
|
|
55
66
|
// src/cli/format.ts
|
|
56
67
|
function summarizeArgs(args, maxLength = 120) {
|
|
57
68
|
let json;
|
|
@@ -96,7 +107,7 @@ function buildDisplayMessages(messages) {
|
|
|
96
107
|
if (message.role === "user" || message.role === "assistant") {
|
|
97
108
|
const text = coreMessageText(message);
|
|
98
109
|
if (text) {
|
|
99
|
-
display.push({ id: display.length, role: message.role, text });
|
|
110
|
+
display.push({ id: display.length, role: message.role, text: toTerminalSafe(text) });
|
|
100
111
|
} else {
|
|
101
112
|
collapsed++;
|
|
102
113
|
}
|
|
@@ -108,7 +119,7 @@ function buildDisplayMessages(messages) {
|
|
|
108
119
|
display.push({
|
|
109
120
|
id: display.length,
|
|
110
121
|
role: "system",
|
|
111
|
-
text:
|
|
122
|
+
text: `Restored ${collapsed} history message(s) not shown here.`
|
|
112
123
|
});
|
|
113
124
|
}
|
|
114
125
|
return display;
|
|
@@ -166,7 +177,15 @@ function resizeImagePlan(platform, inputFile, outputFile, maxDimension, tools =
|
|
|
166
177
|
if (tools.hasConvert) {
|
|
167
178
|
return {
|
|
168
179
|
command: "convert",
|
|
169
|
-
|
|
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
|
+
]
|
|
170
189
|
};
|
|
171
190
|
}
|
|
172
191
|
if (tools.hasFfmpeg) {
|
|
@@ -234,6 +253,7 @@ var MAX_MENTION_BYTES = 100 * 1024;
|
|
|
234
253
|
var MAX_IMAGE_BYTES = 5 * 1024 * 1024;
|
|
235
254
|
var MAX_DIR_MENTION_ENTRIES = 200;
|
|
236
255
|
var MAX_DIR_MENTION_DEPTH = 10;
|
|
256
|
+
var MAX_DIR_MENTION_TOTAL = 5e3;
|
|
237
257
|
var IMAGE_MIME_TYPES = {
|
|
238
258
|
".png": "image/png",
|
|
239
259
|
".jpg": "image/jpeg",
|
|
@@ -259,7 +279,7 @@ async function readImageInput(filePath, cwd) {
|
|
|
259
279
|
});
|
|
260
280
|
return result.image;
|
|
261
281
|
}
|
|
262
|
-
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;
|
|
263
283
|
function parseMentions(text) {
|
|
264
284
|
const mentions = [];
|
|
265
285
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -292,13 +312,18 @@ function isBinaryContent(buf) {
|
|
|
292
312
|
return probe.includes(0);
|
|
293
313
|
}
|
|
294
314
|
async function listDirEntries(dir, prefix, depth, state, ignore) {
|
|
315
|
+
if (state.capped) return;
|
|
295
316
|
const entries = await readdir(dir, { withFileTypes: true }).catch(() => null);
|
|
296
317
|
if (!entries) return;
|
|
297
|
-
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) => {
|
|
298
319
|
const dirDiff = Number(b.isDirectory()) - Number(a.isDirectory());
|
|
299
320
|
return dirDiff !== 0 ? dirDiff : a.name.localeCompare(b.name);
|
|
300
321
|
});
|
|
301
322
|
for (let i = 0; i < visible.length; i++) {
|
|
323
|
+
if (state.total >= MAX_DIR_MENTION_TOTAL) {
|
|
324
|
+
state.capped = true;
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
302
327
|
const entry = visible[i];
|
|
303
328
|
if (!entry) continue;
|
|
304
329
|
state.total += 1;
|
|
@@ -323,11 +348,13 @@ async function listDirEntries(dir, prefix, depth, state, ignore) {
|
|
|
323
348
|
}
|
|
324
349
|
async function directoryBlock(abs, mention, ignore) {
|
|
325
350
|
const display = `${mention.replace(/\\/g, "/").replace(/\/+$/, "")}/`;
|
|
326
|
-
const state = { lines: [`${display}`], shown: 0, total: 0 };
|
|
351
|
+
const state = { lines: [`${display}`], shown: 0, total: 0, capped: false };
|
|
327
352
|
const probe = await readdir(abs).catch(() => null);
|
|
328
353
|
if (!probe) return null;
|
|
329
354
|
await listDirEntries(abs, "", 1, state, ignore);
|
|
330
|
-
if (state.
|
|
355
|
+
if (state.capped) {
|
|
356
|
+
state.lines.push("... (truncated, many more entries)");
|
|
357
|
+
} else if (state.total > state.shown) {
|
|
331
358
|
state.lines.push(
|
|
332
359
|
`... (truncated, ${state.total - state.shown} more entries \u2014 use glob/grep tools or mention a subdirectory to see more)`
|
|
333
360
|
);
|
|
@@ -471,11 +498,56 @@ import { parse, stringify } from "smol-toml";
|
|
|
471
498
|
async function addAllowRule(rule) {
|
|
472
499
|
return addPermissionRule("allow", rule);
|
|
473
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
|
+
}
|
|
474
545
|
async function addPermissionRule(kind, rule) {
|
|
475
546
|
const filePath = globalConfigPath();
|
|
547
|
+
let content = "";
|
|
476
548
|
let raw = {};
|
|
477
549
|
try {
|
|
478
|
-
|
|
550
|
+
content = await fs.promises.readFile(filePath, "utf8");
|
|
479
551
|
raw = parse(content);
|
|
480
552
|
} catch (error) {
|
|
481
553
|
if (error.code !== "ENOENT") throw error;
|
|
@@ -483,30 +555,57 @@ async function addPermissionRule(kind, rule) {
|
|
|
483
555
|
const permissions = typeof raw.permissions === "object" && raw.permissions !== null ? raw.permissions : {};
|
|
484
556
|
const rules = Array.isArray(permissions[kind]) ? permissions[kind] : [];
|
|
485
557
|
if (rules.includes(rule)) return false;
|
|
486
|
-
|
|
487
|
-
raw.permissions = permissions;
|
|
558
|
+
const nextRules = [...rules, rule];
|
|
488
559
|
await fs.promises.mkdir(path3.dirname(filePath), { recursive: true });
|
|
489
|
-
|
|
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));
|
|
490
568
|
return true;
|
|
491
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
|
+
}
|
|
492
589
|
async function savePermissionMode(mode) {
|
|
493
590
|
const filePath = globalConfigPath();
|
|
494
|
-
let
|
|
591
|
+
let content = "";
|
|
495
592
|
try {
|
|
496
|
-
|
|
497
|
-
raw = parse(content);
|
|
593
|
+
content = await fs.promises.readFile(filePath, "utf8");
|
|
498
594
|
} catch (error) {
|
|
499
595
|
if (error.code !== "ENOENT") throw error;
|
|
500
596
|
}
|
|
501
|
-
raw.permissionMode = mode;
|
|
502
597
|
await fs.promises.mkdir(path3.dirname(filePath), { recursive: true });
|
|
503
|
-
await
|
|
598
|
+
await writeFileAtomic(filePath, upsertTopLevelKeyText(content, "permissionMode", mode));
|
|
504
599
|
}
|
|
505
600
|
|
|
506
601
|
// src/config/schema.ts
|
|
507
602
|
import { z } from "zod";
|
|
508
603
|
var ProviderConfigSchema = z.object({
|
|
509
604
|
name: z.string(),
|
|
605
|
+
// openai-responses talks to the Responses API (/responses) instead of chat
|
|
606
|
+
// completions — the wire format codex uses, and the native API of
|
|
607
|
+
// reasoning-model relays that otherwise hide reasoning behind empty chat
|
|
608
|
+
// completions. streamChat sends strictSchemas:false + store:false with it.
|
|
510
609
|
protocol: z.enum(["openai-compatible", "anthropic", "openai-responses"]).default("openai-compatible"),
|
|
511
610
|
baseURL: z.string(),
|
|
512
611
|
apiKeyEnv: z.string().optional(),
|
|
@@ -524,7 +623,11 @@ var ModelConfigSchema = z.object({
|
|
|
524
623
|
// Cost estimation needs BOTH prices (USD per 1M tokens); with only one set
|
|
525
624
|
// the model is treated as unpriced.
|
|
526
625
|
promptPrice: z.number().nonnegative().optional(),
|
|
527
|
-
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()
|
|
528
631
|
});
|
|
529
632
|
var PermissionsConfigSchema = z.object({
|
|
530
633
|
allow: z.array(z.string()).default([]),
|
|
@@ -563,14 +666,16 @@ var ConfigSchema = z.object({
|
|
|
563
666
|
// Extra attempts per model request when a stream fails transiently
|
|
564
667
|
// (network error, 429/5xx, idle watchdog cutoff) or comes back empty, so a
|
|
565
668
|
// relay hiccup does not silently end a half-finished turn. 0 disables.
|
|
566
|
-
//
|
|
567
|
-
//
|
|
669
|
+
// The wait between attempts honors Retry-After headers and otherwise backs
|
|
670
|
+
// off exponentially with jitter (llm/retry.ts); retries after a
|
|
671
|
+
// timeout/empty failure also scale both stream timeouts up (attempt number
|
|
672
|
+
// ×, capped at 3x) since the relay is likely overloaded.
|
|
568
673
|
streamMaxRetries: z.number().int().min(0).default(3),
|
|
569
674
|
// How many consecutive unproductive replies may be nudged before the turn
|
|
570
675
|
// is handed back: a text-only reply that announces pending work ("我将…",
|
|
571
|
-
// "I will…") or answers a nudge with more words, or an empty reply
|
|
572
|
-
//
|
|
573
|
-
//
|
|
676
|
+
// "I will…") or answers a nudge with more words, or an empty reply that
|
|
677
|
+
// survived the stream retry budget (steered with a nudge — a changed
|
|
678
|
+
// request — since identical resends already failed). Tool calls reset the
|
|
574
679
|
// count, so a productive task keeps going; exhaustion surfaces a notice
|
|
575
680
|
// instead of stopping silently. Never fires in plan mode. 0 disables.
|
|
576
681
|
maxAutoContinues: z.number().int().min(0).default(2),
|
|
@@ -584,7 +689,17 @@ var ConfigSchema = z.object({
|
|
|
584
689
|
notifyBell: z.boolean().default(true),
|
|
585
690
|
notifyBellThresholdSec: z.number().positive().default(10),
|
|
586
691
|
permissions: PermissionsConfigSchema.default({ allow: [], deny: [] }),
|
|
587
|
-
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)
|
|
588
703
|
});
|
|
589
704
|
function contextWindowTokens(config, modelName) {
|
|
590
705
|
return config.models.find((m) => m.name === modelName)?.contextMaxTokens ?? config.contextMaxTokens;
|
|
@@ -609,6 +724,74 @@ function resolveModelConfig(config, modelName) {
|
|
|
609
724
|
return model;
|
|
610
725
|
}
|
|
611
726
|
|
|
727
|
+
// src/llm/sse-normalize.ts
|
|
728
|
+
var SseNormalizeTransform = class extends TransformStream {
|
|
729
|
+
constructor() {
|
|
730
|
+
let buffer = "";
|
|
731
|
+
let pending = null;
|
|
732
|
+
const flushPending = (controller) => {
|
|
733
|
+
if (pending === null) return;
|
|
734
|
+
controller.enqueue(`data: ${pending}
|
|
735
|
+
|
|
736
|
+
`);
|
|
737
|
+
pending = null;
|
|
738
|
+
};
|
|
739
|
+
const handleLine = (raw, controller) => {
|
|
740
|
+
const line = raw.endsWith("\r") ? raw.slice(0, -1) : raw;
|
|
741
|
+
if (line === "") {
|
|
742
|
+
flushPending(controller);
|
|
743
|
+
return;
|
|
744
|
+
}
|
|
745
|
+
if (line.startsWith("data:")) {
|
|
746
|
+
flushPending(controller);
|
|
747
|
+
pending = line.slice(5).replace(/^ /, "");
|
|
748
|
+
return;
|
|
749
|
+
}
|
|
750
|
+
if (line.startsWith("{") && pending !== null) {
|
|
751
|
+
flushPending(controller);
|
|
752
|
+
pending = line;
|
|
753
|
+
return;
|
|
754
|
+
}
|
|
755
|
+
flushPending(controller);
|
|
756
|
+
controller.enqueue(`${line}
|
|
757
|
+
`);
|
|
758
|
+
};
|
|
759
|
+
super({
|
|
760
|
+
transform(chunk, controller) {
|
|
761
|
+
buffer += chunk;
|
|
762
|
+
let index = buffer.indexOf("\n");
|
|
763
|
+
while (index !== -1) {
|
|
764
|
+
handleLine(buffer.slice(0, index), controller);
|
|
765
|
+
buffer = buffer.slice(index + 1);
|
|
766
|
+
index = buffer.indexOf("\n");
|
|
767
|
+
}
|
|
768
|
+
},
|
|
769
|
+
flush(controller) {
|
|
770
|
+
if (buffer.length > 0) handleLine(buffer, controller);
|
|
771
|
+
flushPending(controller);
|
|
772
|
+
}
|
|
773
|
+
});
|
|
774
|
+
}
|
|
775
|
+
};
|
|
776
|
+
function createSseNormalizingFetch(baseFetch = fetch) {
|
|
777
|
+
return async (input, init) => {
|
|
778
|
+
const response = await baseFetch(input, init);
|
|
779
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
780
|
+
if (!response.body || !contentType.includes("text/event-stream")) {
|
|
781
|
+
return response;
|
|
782
|
+
}
|
|
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");
|
|
787
|
+
return new Response(body, {
|
|
788
|
+
status: response.status,
|
|
789
|
+
statusText: response.statusText,
|
|
790
|
+
headers
|
|
791
|
+
});
|
|
792
|
+
};
|
|
793
|
+
}
|
|
794
|
+
|
|
612
795
|
// src/llm/provider.ts
|
|
613
796
|
function resolveApiKey(provider) {
|
|
614
797
|
const fromEnv = provider.apiKeyEnv ? process.env[provider.apiKeyEnv] : void 0;
|
|
@@ -642,12 +825,16 @@ function createModel(config, modelName) {
|
|
|
642
825
|
baseURL: provider.baseURL,
|
|
643
826
|
apiKey,
|
|
644
827
|
headers: provider.headers
|
|
645
|
-
})(modelConfig.model);
|
|
828
|
+
})(modelConfig.model, {}, { includeUsage: true });
|
|
646
829
|
case "openai-responses":
|
|
647
830
|
return createOpenAI({
|
|
648
831
|
baseURL: provider.baseURL,
|
|
649
832
|
apiKey,
|
|
650
|
-
headers: provider.headers
|
|
833
|
+
headers: provider.headers,
|
|
834
|
+
// Relays fronting the Responses API sometimes batch several JSON
|
|
835
|
+
// events into one SSE data payload, which the SDK's parser cannot
|
|
836
|
+
// read; normalize the wire format back to one event per data line.
|
|
837
|
+
fetch: createSseNormalizingFetch()
|
|
651
838
|
}).responses(modelConfig.model);
|
|
652
839
|
}
|
|
653
840
|
}
|
|
@@ -661,6 +848,21 @@ function checkpointsDir(sessionDir) {
|
|
|
661
848
|
function indexPath(sessionDir) {
|
|
662
849
|
return path4.join(checkpointsDir(sessionDir), "index.json");
|
|
663
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
|
+
}
|
|
664
866
|
function checkpointContentPath(sessionDir, id) {
|
|
665
867
|
return path4.join(checkpointsDir(sessionDir), `${id}.snapshot`);
|
|
666
868
|
}
|
|
@@ -679,32 +881,36 @@ async function listCheckpointRecords(sessionDir) {
|
|
|
679
881
|
}
|
|
680
882
|
}
|
|
681
883
|
async function appendCheckpointRecord(sessionDir, record, content) {
|
|
682
|
-
await
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
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
|
+
});
|
|
689
893
|
}
|
|
690
894
|
async function removeCheckpointRecords(sessionDir, ids) {
|
|
691
895
|
if (ids.length === 0) return;
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
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
|
+
});
|
|
701
907
|
}
|
|
702
908
|
async function loadSessionSnapshots(sessionDir) {
|
|
703
909
|
const records = await listCheckpointRecords(sessionDir);
|
|
704
910
|
return records.map((record) => ({
|
|
705
911
|
...record,
|
|
706
912
|
content: null,
|
|
707
|
-
contentFile: record.existed ? checkpointContentPath(sessionDir, record.id) : void 0
|
|
913
|
+
contentFile: record.existed && !record.contentTooLarge ? checkpointContentPath(sessionDir, record.id) : void 0
|
|
708
914
|
}));
|
|
709
915
|
}
|
|
710
916
|
|
|
@@ -725,6 +931,10 @@ function debugWarn(message) {
|
|
|
725
931
|
if (process.env.STAR_DEBUG === "1") process.stderr.write(`[star-cli] ${message}
|
|
726
932
|
`);
|
|
727
933
|
}
|
|
934
|
+
var FALLBACK_META = /* @__PURE__ */ Symbol("star.fallbackMeta");
|
|
935
|
+
function isFallbackMeta(meta) {
|
|
936
|
+
return meta[FALLBACK_META] === true;
|
|
937
|
+
}
|
|
728
938
|
function dayKey(date) {
|
|
729
939
|
const pad = (n) => String(n).padStart(2, "0");
|
|
730
940
|
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
|
|
@@ -734,6 +944,9 @@ var SessionStore = class _SessionStore {
|
|
|
734
944
|
dir;
|
|
735
945
|
pendingMeta = null;
|
|
736
946
|
initialized = false;
|
|
947
|
+
cachedMeta = null;
|
|
948
|
+
metaTmpSeq = 0;
|
|
949
|
+
metaWriteQueue = Promise.resolve();
|
|
737
950
|
constructor(id) {
|
|
738
951
|
this.id = id;
|
|
739
952
|
this.dir = path5.join(sessionsDir(), id);
|
|
@@ -763,6 +976,7 @@ var SessionStore = class _SessionStore {
|
|
|
763
976
|
} catch {
|
|
764
977
|
debugWarn(`session ${id}: corrupt meta.json, using defaults`);
|
|
765
978
|
}
|
|
979
|
+
await store.sweepTmpFiles();
|
|
766
980
|
store.initialized = true;
|
|
767
981
|
return store;
|
|
768
982
|
}
|
|
@@ -773,16 +987,40 @@ var SessionStore = class _SessionStore {
|
|
|
773
987
|
} catch {
|
|
774
988
|
return [];
|
|
775
989
|
}
|
|
776
|
-
const metas =
|
|
777
|
-
|
|
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 () => {
|
|
778
1012
|
try {
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
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;
|
|
783
1019
|
}
|
|
784
|
-
}
|
|
785
|
-
|
|
1020
|
+
});
|
|
1021
|
+
this.metaWriteQueue = run.catch(() => {
|
|
1022
|
+
});
|
|
1023
|
+
return run;
|
|
786
1024
|
}
|
|
787
1025
|
async ensureInitialized() {
|
|
788
1026
|
if (this.initialized) return;
|
|
@@ -791,7 +1029,8 @@ var SessionStore = class _SessionStore {
|
|
|
791
1029
|
this.initialized = true;
|
|
792
1030
|
return;
|
|
793
1031
|
}
|
|
794
|
-
await fs3.mkdir(this.dir, { recursive: true });
|
|
1032
|
+
await fs3.mkdir(this.dir, { recursive: true, mode: 448 });
|
|
1033
|
+
await this.sweepTmpFiles();
|
|
795
1034
|
const meta = {
|
|
796
1035
|
id: this.id,
|
|
797
1036
|
title: "",
|
|
@@ -800,16 +1039,31 @@ var SessionStore = class _SessionStore {
|
|
|
800
1039
|
createdAt: pending.createdAt,
|
|
801
1040
|
updatedAt: pending.createdAt
|
|
802
1041
|
};
|
|
803
|
-
await
|
|
1042
|
+
await this.writeMeta(meta);
|
|
1043
|
+
this.cachedMeta = meta;
|
|
804
1044
|
this.initialized = true;
|
|
805
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
|
+
}
|
|
806
1060
|
async append(message) {
|
|
807
1061
|
await this.ensureInitialized();
|
|
808
1062
|
await fs3.appendFile(this.messagesPath(), `${JSON.stringify(message)}
|
|
809
1063
|
`);
|
|
810
1064
|
const meta = await this.meta();
|
|
811
1065
|
meta.updatedAt = Date.now();
|
|
812
|
-
await
|
|
1066
|
+
await this.writeMeta(meta);
|
|
813
1067
|
}
|
|
814
1068
|
async replaceMessages(messages) {
|
|
815
1069
|
await this.ensureInitialized();
|
|
@@ -818,7 +1072,7 @@ var SessionStore = class _SessionStore {
|
|
|
818
1072
|
` : "");
|
|
819
1073
|
const meta = await this.meta();
|
|
820
1074
|
meta.updatedAt = Date.now();
|
|
821
|
-
await
|
|
1075
|
+
await this.writeMeta(meta);
|
|
822
1076
|
}
|
|
823
1077
|
async messages() {
|
|
824
1078
|
let raw;
|
|
@@ -841,13 +1095,24 @@ var SessionStore = class _SessionStore {
|
|
|
841
1095
|
}
|
|
842
1096
|
fallbackMeta() {
|
|
843
1097
|
const now = Date.now();
|
|
844
|
-
|
|
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;
|
|
845
1108
|
}
|
|
846
1109
|
async meta() {
|
|
847
1110
|
await this.ensureInitialized();
|
|
1111
|
+
if (this.cachedMeta) return this.cachedMeta;
|
|
848
1112
|
try {
|
|
849
1113
|
const raw = await fs3.readFile(this.metaPath(), "utf8");
|
|
850
|
-
|
|
1114
|
+
this.cachedMeta = JSON.parse(raw);
|
|
1115
|
+
return this.cachedMeta;
|
|
851
1116
|
} catch {
|
|
852
1117
|
debugWarn(`session ${this.id}: unreadable meta.json, using defaults`);
|
|
853
1118
|
return this.fallbackMeta();
|
|
@@ -858,7 +1123,7 @@ var SessionStore = class _SessionStore {
|
|
|
858
1123
|
const meta = await this.meta();
|
|
859
1124
|
meta.title = title;
|
|
860
1125
|
meta.updatedAt = Date.now();
|
|
861
|
-
await
|
|
1126
|
+
await this.writeMeta(meta);
|
|
862
1127
|
}
|
|
863
1128
|
async appendCheckpoint(record, content) {
|
|
864
1129
|
await this.ensureInitialized();
|
|
@@ -883,6 +1148,12 @@ var SessionStore = class _SessionStore {
|
|
|
883
1148
|
usage.promptTokens += delta.promptTokens;
|
|
884
1149
|
usage.completionTokens += delta.completionTokens;
|
|
885
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
|
+
}
|
|
886
1157
|
meta.usage = usage;
|
|
887
1158
|
const day = dayKey(/* @__PURE__ */ new Date());
|
|
888
1159
|
const byDay = meta.usageByDay ?? {};
|
|
@@ -892,7 +1163,7 @@ var SessionStore = class _SessionStore {
|
|
|
892
1163
|
bucket.totalTokens += delta.totalTokens;
|
|
893
1164
|
byDay[day] = bucket;
|
|
894
1165
|
meta.usageByDay = byDay;
|
|
895
|
-
await
|
|
1166
|
+
await this.writeMeta(meta);
|
|
896
1167
|
}
|
|
897
1168
|
};
|
|
898
1169
|
|
|
@@ -949,6 +1220,33 @@ async function findLatestSession(cwd) {
|
|
|
949
1220
|
const [latest] = await SessionStore.list(cwd);
|
|
950
1221
|
return latest ?? null;
|
|
951
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
|
+
}
|
|
952
1250
|
function shortSessionId(id) {
|
|
953
1251
|
const dash = id.lastIndexOf("-");
|
|
954
1252
|
return dash === -1 ? id : id.slice(dash + 1);
|
|
@@ -965,20 +1263,20 @@ async function resolveSessionId(query) {
|
|
|
965
1263
|
function relativeTime(timestamp) {
|
|
966
1264
|
const diff = Date.now() - timestamp;
|
|
967
1265
|
const minutes = Math.floor(diff / 6e4);
|
|
968
|
-
if (minutes < 1) return "
|
|
969
|
-
if (minutes < 60) return `${minutes}
|
|
1266
|
+
if (minutes < 1) return "just now";
|
|
1267
|
+
if (minutes < 60) return `${minutes}m ago`;
|
|
970
1268
|
const hours = Math.floor(minutes / 60);
|
|
971
|
-
if (hours < 24) return `${hours}
|
|
1269
|
+
if (hours < 24) return `${hours}h ago`;
|
|
972
1270
|
const days = Math.floor(hours / 24);
|
|
973
|
-
if (days < 30) return `${days}
|
|
1271
|
+
if (days < 30) return `${days}d ago`;
|
|
974
1272
|
const months = Math.floor(days / 30);
|
|
975
|
-
if (months < 12) return `${months}
|
|
976
|
-
return `${Math.floor(months / 12)}
|
|
1273
|
+
if (months < 12) return `${months}mo ago`;
|
|
1274
|
+
return `${Math.floor(months / 12)}y ago`;
|
|
977
1275
|
}
|
|
978
1276
|
function formatSessionEntries(entries, options = {}) {
|
|
979
1277
|
return entries.map(({ meta, messageCount }) => {
|
|
980
|
-
const title = meta.title || "(
|
|
981
|
-
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)}`;
|
|
982
1280
|
return options.showCwd ? `${base} [${meta.cwd}]` : base;
|
|
983
1281
|
}).join("\n");
|
|
984
1282
|
}
|
|
@@ -1009,7 +1307,9 @@ function computeCostUsd(usage, model) {
|
|
|
1009
1307
|
if (model?.promptPrice === void 0 || model.completionPrice === void 0) {
|
|
1010
1308
|
return null;
|
|
1011
1309
|
}
|
|
1012
|
-
|
|
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;
|
|
1013
1313
|
}
|
|
1014
1314
|
function estimateCost(usage, modelName, model) {
|
|
1015
1315
|
const cost = computeCostUsd(usage, model);
|
|
@@ -1025,7 +1325,8 @@ function estimateCost(usage, modelName, model) {
|
|
|
1025
1325
|
}
|
|
1026
1326
|
return `Estimated cost: unknown (no price configured for ${modelName})`;
|
|
1027
1327
|
}
|
|
1028
|
-
|
|
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})`;
|
|
1029
1330
|
}
|
|
1030
1331
|
|
|
1031
1332
|
// src/session/usage.ts
|
|
@@ -1101,6 +1402,12 @@ function aggregateUsage(metas) {
|
|
|
1101
1402
|
entry.sessions += 1;
|
|
1102
1403
|
entry.requests += requestCount;
|
|
1103
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
|
+
}
|
|
1104
1411
|
models.set(name, entry);
|
|
1105
1412
|
}
|
|
1106
1413
|
return {
|
|
@@ -1155,7 +1462,7 @@ function formatUsageDashboard(stats, models, options = {}) {
|
|
|
1155
1462
|
const configured = new Map(models.map((m) => [m.name, m]));
|
|
1156
1463
|
const rows = stats.models.map((entry) => {
|
|
1157
1464
|
const cfg = configured.get(entry.model);
|
|
1158
|
-
const cost =
|
|
1465
|
+
const cost = computeCostUsd(entry, cfg);
|
|
1159
1466
|
return { entry, cost };
|
|
1160
1467
|
});
|
|
1161
1468
|
const header = ["model", "requests", "prompt", "completion", "total", "cost"];
|
|
@@ -1229,7 +1536,7 @@ function cacheHitPercent(totals) {
|
|
|
1229
1536
|
// src/cli/clipboard.ts
|
|
1230
1537
|
import { execFile as execFile2 } from "child_process";
|
|
1231
1538
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
1232
|
-
import { readFile as readFile3, unlink as unlink2 } from "fs/promises";
|
|
1539
|
+
import { readFile as readFile3, stat as stat2, unlink as unlink2 } from "fs/promises";
|
|
1233
1540
|
import os2 from "os";
|
|
1234
1541
|
import path9 from "path";
|
|
1235
1542
|
import { promisify as promisify2 } from "util";
|
|
@@ -1308,6 +1615,13 @@ async function finalizeClipboardImage(image) {
|
|
|
1308
1615
|
const result = await downsampleImageIfNeeded(image);
|
|
1309
1616
|
return { image: result.image, oversized: result.oversized && !result.resized };
|
|
1310
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
|
+
}
|
|
1311
1625
|
async function readClipboardImage() {
|
|
1312
1626
|
const tempFile = path9.join(os2.tmpdir(), `star-clipboard-${randomUUID2()}.png`);
|
|
1313
1627
|
const hasPngpaste = process.platform === "darwin" ? await commandExists2("pngpaste") : false;
|
|
@@ -1320,22 +1634,18 @@ async function readClipboardImage() {
|
|
|
1320
1634
|
maxBuffer: MAX_CLIPBOARD_IMAGE_BYTES,
|
|
1321
1635
|
encoding: "buffer"
|
|
1322
1636
|
});
|
|
1323
|
-
const
|
|
1324
|
-
if (
|
|
1637
|
+
const buf = stdout;
|
|
1638
|
+
if (buf.length === 0) return null;
|
|
1325
1639
|
return finalizeClipboardImage({
|
|
1326
1640
|
path: "clipboard.png",
|
|
1327
1641
|
mimeType: "image/png",
|
|
1328
|
-
data:
|
|
1642
|
+
data: buf.toString("base64")
|
|
1329
1643
|
});
|
|
1330
1644
|
}
|
|
1331
1645
|
await execFileAsync2(plan.command, plan.args, { timeout: CLIPBOARD_TIMEOUT_MS });
|
|
1332
|
-
const
|
|
1333
|
-
if (!
|
|
1334
|
-
return finalizeClipboardImage(
|
|
1335
|
-
path: "clipboard.png",
|
|
1336
|
-
mimeType: "image/png",
|
|
1337
|
-
data: buf.toString("base64")
|
|
1338
|
-
});
|
|
1646
|
+
const image = await readClipboardTempFile(tempFile);
|
|
1647
|
+
if (!image) return null;
|
|
1648
|
+
return finalizeClipboardImage(image);
|
|
1339
1649
|
} catch {
|
|
1340
1650
|
return null;
|
|
1341
1651
|
} finally {
|
|
@@ -1695,7 +2005,7 @@ function applyEdit(content, oldString, newString, replaceAll) {
|
|
|
1695
2005
|
}
|
|
1696
2006
|
if (count === 0) return null;
|
|
1697
2007
|
if (count > 1 && !replaceAll) return null;
|
|
1698
|
-
return replaceAll ? content.split(oldString).join(newString) : content.replace(oldString, newString);
|
|
2008
|
+
return replaceAll ? content.split(oldString).join(newString) : content.replace(oldString, () => newString);
|
|
1699
2009
|
}
|
|
1700
2010
|
function newFilePreview(content, label) {
|
|
1701
2011
|
const all = content.split("\n");
|
|
@@ -1994,13 +2304,22 @@ ${content}`);
|
|
|
1994
2304
|
});
|
|
1995
2305
|
register({
|
|
1996
2306
|
name: "undo",
|
|
1997
|
-
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",
|
|
1998
2308
|
usage: "/undo",
|
|
1999
2309
|
category: "Changes",
|
|
2000
2310
|
async run(_args, ctx) {
|
|
2001
2311
|
ctx.addSystemMessage(await ctx.undo());
|
|
2002
2312
|
}
|
|
2003
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
|
+
});
|
|
2004
2323
|
register({
|
|
2005
2324
|
name: "rewind",
|
|
2006
2325
|
description: "List file-change checkpoints, or rewind to just before one: restore files and retract the conversation",
|
|
@@ -2272,19 +2591,19 @@ function maskApiKey(key) {
|
|
|
2272
2591
|
if (key.length <= 8) return "\u2022\u2022\u2022\u2022\u2022\u2022";
|
|
2273
2592
|
return `${key.slice(0, 3)}\u2026${key.slice(-4)}`;
|
|
2274
2593
|
}
|
|
2275
|
-
function
|
|
2594
|
+
function tomlString2(value) {
|
|
2276
2595
|
return JSON.stringify(value);
|
|
2277
2596
|
}
|
|
2278
2597
|
function buildProviderTomlBlock(provider) {
|
|
2279
2598
|
return [
|
|
2280
2599
|
"[[providers]]",
|
|
2281
|
-
`name = ${
|
|
2600
|
+
`name = ${tomlString2(provider.name)}`,
|
|
2282
2601
|
"# wire format: openai-compatible | anthropic | openai-responses",
|
|
2283
|
-
`protocol = ${
|
|
2284
|
-
`baseURL = ${
|
|
2602
|
+
`protocol = ${tomlString2(provider.protocol)}`,
|
|
2603
|
+
`baseURL = ${tomlString2(provider.baseURL)}`,
|
|
2285
2604
|
"# environment variable holding the API key; the key itself lives in",
|
|
2286
2605
|
"# ~/.star-cli/.env (written by /connect), never in this file",
|
|
2287
|
-
`apiKeyEnv = ${
|
|
2606
|
+
`apiKeyEnv = ${tomlString2(provider.apiKeyEnv ?? "")}`,
|
|
2288
2607
|
"# inline key instead of apiKeyEnv (not recommended):",
|
|
2289
2608
|
'# apiKey = "sk-..."',
|
|
2290
2609
|
"# extra HTTP headers sent to the provider:",
|
|
@@ -2296,9 +2615,9 @@ function buildProviderTomlBlock(provider) {
|
|
|
2296
2615
|
function buildModelTomlBlock(model) {
|
|
2297
2616
|
return [
|
|
2298
2617
|
"[[models]]",
|
|
2299
|
-
`name = ${
|
|
2300
|
-
`provider = ${
|
|
2301
|
-
`model = ${
|
|
2618
|
+
`name = ${tomlString2(model.name)}`,
|
|
2619
|
+
`provider = ${tomlString2(model.provider)}`,
|
|
2620
|
+
`model = ${tomlString2(model.model)}`,
|
|
2302
2621
|
"# per-model context window in tokens \u2014 overrides the top-level",
|
|
2303
2622
|
"# contextMaxTokens for compaction and the ctx % in the status bar",
|
|
2304
2623
|
`contextMaxTokens = ${model.contextMaxTokens ?? CONNECT_DEFAULT_CONTEXT_TOKENS}`,
|
|
@@ -2366,7 +2685,7 @@ function saveConnection(answers, configPath = globalConfigPath(), envPath = envF
|
|
|
2366
2685
|
return { provider, model, configPath, envPath };
|
|
2367
2686
|
}
|
|
2368
2687
|
function setDefaultModelInToml(content, modelName) {
|
|
2369
|
-
const line = `defaultModel = ${
|
|
2688
|
+
const line = `defaultModel = ${tomlString2(modelName)}`;
|
|
2370
2689
|
const lines = content.split("\n");
|
|
2371
2690
|
let inTopLevel = true;
|
|
2372
2691
|
for (let i = 0; i < lines.length; i++) {
|
|
@@ -2806,6 +3125,20 @@ async function initProject(opts) {
|
|
|
2806
3125
|
};
|
|
2807
3126
|
}
|
|
2808
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
|
+
|
|
2809
3142
|
// src/cli/commands/registry.ts
|
|
2810
3143
|
var CommandRegistry = class {
|
|
2811
3144
|
commands = /* @__PURE__ */ new Map();
|
|
@@ -2921,6 +3254,16 @@ async function buildUndoDiffs(snapshots, cwd) {
|
|
|
2921
3254
|
}
|
|
2922
3255
|
return diffs;
|
|
2923
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
|
+
}
|
|
2924
3267
|
|
|
2925
3268
|
// src/cli/components/ConnectWizard.tsx
|
|
2926
3269
|
import { Box as Box4, Text as Text4, useInput as useInput4 } from "ink";
|
|
@@ -3855,18 +4198,19 @@ import { memo } from "react";
|
|
|
3855
4198
|
import { Text as Text6 } from "ink";
|
|
3856
4199
|
import { Fragment, jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
3857
4200
|
function DiffLineView({ line }) {
|
|
4201
|
+
const text = toTerminalSafe(line.text);
|
|
3858
4202
|
if (line.kind === "add") return /* @__PURE__ */ jsxs6(Text6, { color: "green", children: [
|
|
3859
4203
|
"+ ",
|
|
3860
|
-
|
|
4204
|
+
text
|
|
3861
4205
|
] });
|
|
3862
4206
|
if (line.kind === "del") return /* @__PURE__ */ jsxs6(Text6, { color: "red", children: [
|
|
3863
4207
|
"- ",
|
|
3864
|
-
|
|
4208
|
+
text
|
|
3865
4209
|
] });
|
|
3866
|
-
if (line.kind === "marker") return /* @__PURE__ */ jsx6(Text6, { dimColor: true, children:
|
|
4210
|
+
if (line.kind === "marker") return /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: text });
|
|
3867
4211
|
return /* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
|
|
3868
4212
|
" ",
|
|
3869
|
-
|
|
4213
|
+
text
|
|
3870
4214
|
] });
|
|
3871
4215
|
}
|
|
3872
4216
|
function DiffLines({ lines }) {
|
|
@@ -3910,6 +4254,17 @@ var MessageList = memo(function MessageList2({ messages }) {
|
|
|
3910
4254
|
// src/cli/components/PermissionPrompt.tsx
|
|
3911
4255
|
import { Box as Box7, Text as Text8, useInput as useInput6 } from "ink";
|
|
3912
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
|
+
}
|
|
3913
4268
|
function PermissionPrompt({ request, preview, onDecision }) {
|
|
3914
4269
|
useInput6((input) => {
|
|
3915
4270
|
const ch = input.toLowerCase();
|
|
@@ -3917,6 +4272,7 @@ function PermissionPrompt({ request, preview, onDecision }) {
|
|
|
3917
4272
|
else if (ch === "n") onDecision("no");
|
|
3918
4273
|
else if (ch === "a") onDecision("always");
|
|
3919
4274
|
});
|
|
4275
|
+
const command = request.toolName === "bash" ? bashCommand(request.args) : null;
|
|
3920
4276
|
return /* @__PURE__ */ jsxs8(Box7, { flexDirection: "column", borderStyle: "round", borderColor: "yellow", paddingX: 1, children: [
|
|
3921
4277
|
/* @__PURE__ */ jsxs8(Text8, { bold: true, color: "yellow", children: [
|
|
3922
4278
|
"Permission required: ",
|
|
@@ -3928,10 +4284,10 @@ function PermissionPrompt({ request, preview, onDecision }) {
|
|
|
3928
4284
|
preview ? /* @__PURE__ */ jsxs8(Box7, { flexDirection: "column", children: [
|
|
3929
4285
|
/* @__PURE__ */ jsxs8(Text8, { dimColor: true, children: [
|
|
3930
4286
|
preview.type === "new-file" ? "New file: " : "",
|
|
3931
|
-
preview.label
|
|
4287
|
+
toTerminalSafe(preview.label)
|
|
3932
4288
|
] }),
|
|
3933
4289
|
/* @__PURE__ */ jsx8(DiffLines, { lines: preview.lines })
|
|
3934
|
-
] }) : /* @__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)) }),
|
|
3935
4291
|
/* @__PURE__ */ jsx8(Text8, { children: "[y] allow [n] deny [a] always (saved to config)" })
|
|
3936
4292
|
] });
|
|
3937
4293
|
}
|
|
@@ -3990,7 +4346,7 @@ var StatusBar = memo2(function StatusBar2({
|
|
|
3990
4346
|
}) {
|
|
3991
4347
|
const { stdout } = useStdout2();
|
|
3992
4348
|
const wide = (stdout?.columns ?? 80) >= WIDE_COLUMNS;
|
|
3993
|
-
const joined = backgroundTasks.join(", ");
|
|
4349
|
+
const joined = backgroundTasks.map((label) => toTerminalSafe(label)).join(", ");
|
|
3994
4350
|
const labels = joined.length > BG_LABEL_MAX ? `${joined.slice(0, BG_LABEL_MAX)}\u2026` : joined;
|
|
3995
4351
|
return /* @__PURE__ */ jsxs10(Box9, { justifyContent: "space-between", children: [
|
|
3996
4352
|
/* @__PURE__ */ jsxs10(Text10, { dimColor: true, children: [
|
|
@@ -4079,34 +4435,59 @@ function ThinkingIndicator({
|
|
|
4079
4435
|
|
|
4080
4436
|
// src/cli/components/TodoPanel.tsx
|
|
4081
4437
|
import { Box as Box12, Text as Text13 } from "ink";
|
|
4082
|
-
import {
|
|
4438
|
+
import { jsxs as jsxs13 } from "react/jsx-runtime";
|
|
4083
4439
|
var SYMBOLS = {
|
|
4084
4440
|
pending: "[ ]",
|
|
4085
4441
|
in_progress: "[~]",
|
|
4086
4442
|
done: "[x]"
|
|
4087
4443
|
};
|
|
4444
|
+
var MAX_VISIBLE_TODOS = 10;
|
|
4445
|
+
function visibleTodoWindow(todos, max = MAX_VISIBLE_TODOS) {
|
|
4446
|
+
if (todos.length <= max) return { items: [...todos], hiddenBefore: 0, hiddenAfter: 0 };
|
|
4447
|
+
const active = todos.findIndex((todo) => todo.status === "in_progress");
|
|
4448
|
+
const start = active > 0 ? Math.min(active, todos.length - max) : 0;
|
|
4449
|
+
const items = todos.slice(start, start + max);
|
|
4450
|
+
return {
|
|
4451
|
+
items,
|
|
4452
|
+
hiddenBefore: start,
|
|
4453
|
+
hiddenAfter: todos.length - start - items.length
|
|
4454
|
+
};
|
|
4455
|
+
}
|
|
4088
4456
|
function TodoPanel({ todos }) {
|
|
4089
4457
|
if (todos.length === 0) return null;
|
|
4090
|
-
|
|
4091
|
-
|
|
4092
|
-
{
|
|
4093
|
-
|
|
4094
|
-
|
|
4095
|
-
|
|
4096
|
-
|
|
4097
|
-
|
|
4098
|
-
|
|
4099
|
-
|
|
4100
|
-
|
|
4101
|
-
|
|
4102
|
-
|
|
4458
|
+
const { items, hiddenBefore, hiddenAfter } = visibleTodoWindow(todos);
|
|
4459
|
+
return /* @__PURE__ */ jsxs13(Box12, { flexDirection: "column", children: [
|
|
4460
|
+
hiddenBefore > 0 && /* @__PURE__ */ jsxs13(Text13, { dimColor: true, children: [
|
|
4461
|
+
" \u2026 ",
|
|
4462
|
+
hiddenBefore,
|
|
4463
|
+
" more above"
|
|
4464
|
+
] }),
|
|
4465
|
+
items.map((todo) => /* @__PURE__ */ jsxs13(
|
|
4466
|
+
Text13,
|
|
4467
|
+
{
|
|
4468
|
+
dimColor: todo.status === "done",
|
|
4469
|
+
color: todo.status === "in_progress" ? "cyan" : void 0,
|
|
4470
|
+
children: [
|
|
4471
|
+
SYMBOLS[todo.status],
|
|
4472
|
+
" ",
|
|
4473
|
+
toTerminalSafe(todo.title)
|
|
4474
|
+
]
|
|
4475
|
+
},
|
|
4476
|
+
todo.id
|
|
4477
|
+
)),
|
|
4478
|
+
hiddenAfter > 0 && /* @__PURE__ */ jsxs13(Text13, { dimColor: true, children: [
|
|
4479
|
+
" \u2026 ",
|
|
4480
|
+
hiddenAfter,
|
|
4481
|
+
" more"
|
|
4482
|
+
] })
|
|
4483
|
+
] });
|
|
4103
4484
|
}
|
|
4104
4485
|
|
|
4105
4486
|
// src/cli/components/ToolCallCard.tsx
|
|
4106
4487
|
function formatToolCard(card) {
|
|
4107
4488
|
const header = `${card.name} ${card.argsSummary}${card.isError ? " [error]" : ""}`;
|
|
4108
4489
|
if (card.result === void 0) return header;
|
|
4109
|
-
const preview = previewLines(card.result, 10);
|
|
4490
|
+
const preview = previewLines(toTerminalSafe(card.result), 10);
|
|
4110
4491
|
return `${header}
|
|
4111
4492
|
${preview.text}${preview.truncated ? "\n... (truncated)" : ""}`;
|
|
4112
4493
|
}
|
|
@@ -4243,6 +4624,18 @@ async function executeShellBang(raw, cwd, signal) {
|
|
|
4243
4624
|
};
|
|
4244
4625
|
}
|
|
4245
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
|
+
|
|
4246
4639
|
// src/cli/ticker.ts
|
|
4247
4640
|
var TICK_MS = 100;
|
|
4248
4641
|
function nextFlush(prev, next) {
|
|
@@ -4290,7 +4683,7 @@ async function checkForUpdate(currentVersion, fetcher = defaultFetcher2) {
|
|
|
4290
4683
|
}
|
|
4291
4684
|
|
|
4292
4685
|
// src/cli/repl.tsx
|
|
4293
|
-
import { jsx as
|
|
4686
|
+
import { jsx as jsx11, jsxs as jsxs14 } from "react/jsx-runtime";
|
|
4294
4687
|
var CLEAR_TERMINAL = process.platform === "win32" ? "\x1B[2J\x1B[0f" : "\x1B[2J\x1B[3J\x1B[H";
|
|
4295
4688
|
function emptyUsage() {
|
|
4296
4689
|
return { requests: 0, promptTokens: 0, completionTokens: 0, totalTokens: 0 };
|
|
@@ -4301,19 +4694,25 @@ function runningTaskLabels() {
|
|
|
4301
4694
|
return [...shells, ...agents];
|
|
4302
4695
|
}
|
|
4303
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
|
+
]);
|
|
4304
4710
|
function formatUsage(usage) {
|
|
4305
4711
|
return `API usage this session: ${usage.requests} requests, ${usage.promptTokens} prompt + ${usage.completionTokens} completion = ${usage.totalTokens} tokens`;
|
|
4306
4712
|
}
|
|
4307
4713
|
var SESSION_PICKER_CAP = 20;
|
|
4308
|
-
|
|
4309
|
-
|
|
4310
|
-
if (!store) return null;
|
|
4311
|
-
const first = (await store.messages()).find((m) => m.role === "user");
|
|
4312
|
-
if (!first) return null;
|
|
4313
|
-
const text = coreMessageText(first).replace(/\s+/g, " ").trim();
|
|
4314
|
-
if (!text) return null;
|
|
4315
|
-
return text.length > 60 ? `${text.slice(0, 60)}\u2026` : text;
|
|
4316
|
-
}
|
|
4714
|
+
var SESSION_PREVIEW_MAX = 60;
|
|
4715
|
+
var MAX_VISIBLE_QUEUED = 5;
|
|
4317
4716
|
function Repl({
|
|
4318
4717
|
backend,
|
|
4319
4718
|
model,
|
|
@@ -4349,12 +4748,14 @@ function Repl({
|
|
|
4349
4748
|
const nextIdRef = useRef5(initialDisplay.length);
|
|
4350
4749
|
const abortRef = useRef5(null);
|
|
4351
4750
|
const streamedRef = useRef5("");
|
|
4751
|
+
const lastCommittedLenRef = useRef5(0);
|
|
4352
4752
|
const thinkingRef = useRef5(false);
|
|
4353
4753
|
const reasoningRef = useRef5("");
|
|
4354
4754
|
const tickerStopRef = useRef5(null);
|
|
4355
4755
|
const flushedRef = useRef5({ streamed: "", reasoning: "" });
|
|
4356
4756
|
const toolCardsRef = useRef5(/* @__PURE__ */ new Map());
|
|
4357
4757
|
const pendingRef = useRef5(null);
|
|
4758
|
+
const permissionChainRef = useRef5(Promise.resolve());
|
|
4358
4759
|
const alwaysAllowedRef = useRef5(/* @__PURE__ */ new Set());
|
|
4359
4760
|
const prevModeRef = useRef5(null);
|
|
4360
4761
|
const planApprovalRef = useRef5(false);
|
|
@@ -4363,6 +4764,10 @@ function Repl({
|
|
|
4363
4764
|
const pendingConnectRef = useRef5(null);
|
|
4364
4765
|
const modelNameRef = useRef5(model);
|
|
4365
4766
|
const sessionStoreRef = useRef5(sessionStore);
|
|
4767
|
+
const configRef = useRef5(config);
|
|
4768
|
+
useEffect2(() => {
|
|
4769
|
+
configRef.current = config;
|
|
4770
|
+
}, [config]);
|
|
4366
4771
|
const usageRef = useRef5(initialUsage ? { ...initialUsage } : emptyUsage());
|
|
4367
4772
|
const turnChunksRef = useRef5(0);
|
|
4368
4773
|
const messagesRef = useRef5(initialDisplay);
|
|
@@ -4538,14 +4943,22 @@ function Repl({
|
|
|
4538
4943
|
);
|
|
4539
4944
|
const attachConfirmHandler = useCallback(
|
|
4540
4945
|
(target) => {
|
|
4541
|
-
target.confirmHandler =
|
|
4946
|
+
target.confirmHandler = (req) => {
|
|
4542
4947
|
if (isAllowedByRules([...alwaysAllowedRef.current], req)) {
|
|
4543
|
-
return true;
|
|
4948
|
+
return Promise.resolve(true);
|
|
4544
4949
|
}
|
|
4545
|
-
const
|
|
4546
|
-
|
|
4547
|
-
|
|
4548
|
-
|
|
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;
|
|
4549
4962
|
};
|
|
4550
4963
|
target.onHookWarning = (message) => pushMessage("system", message);
|
|
4551
4964
|
},
|
|
@@ -4576,27 +4989,29 @@ function Repl({
|
|
|
4576
4989
|
setBgLabels(runningTaskLabels());
|
|
4577
4990
|
pushMessage(
|
|
4578
4991
|
"system",
|
|
4579
|
-
|
|
4992
|
+
toTerminalSafe(
|
|
4993
|
+
task.status === "running" ? formatTaskStarted(task) : formatTaskFinished(task)
|
|
4994
|
+
)
|
|
4580
4995
|
);
|
|
4581
4996
|
if (task.status !== "running") {
|
|
4582
4997
|
notifyBell({
|
|
4583
|
-
enabled:
|
|
4584
|
-
thresholdSec:
|
|
4998
|
+
enabled: configRef.current.notifyBell,
|
|
4999
|
+
thresholdSec: configRef.current.notifyBellThresholdSec,
|
|
4585
5000
|
noNotifyEnv: process.env.STAR_NO_NOTIFY === "1"
|
|
4586
5001
|
});
|
|
4587
5002
|
}
|
|
4588
5003
|
};
|
|
4589
5004
|
const onAgentUpdate = (task) => {
|
|
4590
5005
|
setBgLabels(runningTaskLabels());
|
|
4591
|
-
const label = task.description ? `: ${task.description}` : "";
|
|
5006
|
+
const label = task.description ? `: ${toTerminalSafe(task.description)}` : "";
|
|
4592
5007
|
pushMessage(
|
|
4593
5008
|
"system",
|
|
4594
5009
|
task.status === "running" ? `Background subagent ${task.id} started${label}` : `Background subagent ${task.id} ${task.status}${label}`
|
|
4595
5010
|
);
|
|
4596
5011
|
if (task.status !== "running") {
|
|
4597
5012
|
notifyBell({
|
|
4598
|
-
enabled:
|
|
4599
|
-
thresholdSec:
|
|
5013
|
+
enabled: configRef.current.notifyBell,
|
|
5014
|
+
thresholdSec: configRef.current.notifyBellThresholdSec,
|
|
4600
5015
|
noNotifyEnv: process.env.STAR_NO_NOTIFY === "1"
|
|
4601
5016
|
});
|
|
4602
5017
|
}
|
|
@@ -4609,7 +5024,7 @@ function Repl({
|
|
|
4609
5024
|
defaultTaskManager.cleanup();
|
|
4610
5025
|
defaultAgentTasks.cleanup();
|
|
4611
5026
|
};
|
|
4612
|
-
}, [pushMessage
|
|
5027
|
+
}, [pushMessage]);
|
|
4613
5028
|
const interrupt = useCallback(() => {
|
|
4614
5029
|
abortRef.current?.abort();
|
|
4615
5030
|
const p = pendingRef.current;
|
|
@@ -4648,9 +5063,11 @@ function Repl({
|
|
|
4648
5063
|
const handleExit = useCallback(() => {
|
|
4649
5064
|
const killed = defaultTaskManager.cleanup();
|
|
4650
5065
|
const killedAgents = defaultAgentTasks.cleanup();
|
|
4651
|
-
const stopped =
|
|
4652
|
-
if (stopped
|
|
4653
|
-
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;
|
|
4654
5071
|
}
|
|
4655
5072
|
exit();
|
|
4656
5073
|
}, [exit, pushMessage]);
|
|
@@ -4711,6 +5128,7 @@ function Repl({
|
|
|
4711
5128
|
registry: createDefaultRegistry(),
|
|
4712
5129
|
config,
|
|
4713
5130
|
cwd,
|
|
5131
|
+
system: SYSTEM_PROMPT,
|
|
4714
5132
|
sessionStore: sessionStoreRef.current,
|
|
4715
5133
|
contextMaxTokens: contextWindowTokens(config, name)
|
|
4716
5134
|
});
|
|
@@ -4752,12 +5170,15 @@ function Repl({
|
|
|
4752
5170
|
if (!resumed) {
|
|
4753
5171
|
return `Session not found: ${id}`;
|
|
4754
5172
|
}
|
|
4755
|
-
await current.loadMessages(resumed.messages);
|
|
4756
|
-
setTodos(await loadTodos(cwd));
|
|
4757
5173
|
const store = await SessionStore.open(resolvedId);
|
|
4758
|
-
if (store) {
|
|
4759
|
-
|
|
5174
|
+
if (!store) {
|
|
5175
|
+
return `Session not found: ${id}`;
|
|
4760
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));
|
|
4761
5182
|
const display = buildDisplayMessages(resumed.messages);
|
|
4762
5183
|
nextIdRef.current = display.length;
|
|
4763
5184
|
applyMessages(display);
|
|
@@ -4783,6 +5204,7 @@ function Repl({
|
|
|
4783
5204
|
abortRef.current = controller;
|
|
4784
5205
|
const turnStartedAt = Date.now();
|
|
4785
5206
|
streamedRef.current = "";
|
|
5207
|
+
lastCommittedLenRef.current = 0;
|
|
4786
5208
|
thinkingRef.current = true;
|
|
4787
5209
|
reasoningRef.current = "";
|
|
4788
5210
|
turnChunksRef.current = 0;
|
|
@@ -4793,22 +5215,25 @@ function Repl({
|
|
|
4793
5215
|
setIsStreaming(true);
|
|
4794
5216
|
flushedRef.current = { streamed: "", reasoning: "" };
|
|
4795
5217
|
const commitStreamed = (tight) => {
|
|
4796
|
-
|
|
4797
|
-
|
|
5218
|
+
const uncommitted = streamedRef.current.slice(lastCommittedLenRef.current);
|
|
5219
|
+
if (uncommitted.length > 0) {
|
|
5220
|
+
pushAssistantChunk(uncommitted, tight);
|
|
5221
|
+
}
|
|
4798
5222
|
streamedRef.current = "";
|
|
5223
|
+
lastCommittedLenRef.current = 0;
|
|
4799
5224
|
flushedRef.current = { ...flushedRef.current, streamed: "" };
|
|
4800
5225
|
setStreamingText("");
|
|
4801
5226
|
};
|
|
4802
5227
|
tickerStopRef.current = startTicker((tick) => {
|
|
4803
5228
|
setSpinnerTick(tick);
|
|
4804
|
-
const
|
|
5229
|
+
const uncommitted = streamedRef.current.slice(lastCommittedLenRef.current);
|
|
5230
|
+
const split = splitCommittableLines(uncommitted, 8);
|
|
4805
5231
|
if (split) {
|
|
4806
5232
|
pushAssistantChunk(split.committed, true);
|
|
4807
|
-
|
|
4808
|
-
flushedRef.current = { ...flushedRef.current, streamed: "" };
|
|
5233
|
+
lastCommittedLenRef.current += split.committed.length + 1;
|
|
4809
5234
|
}
|
|
4810
5235
|
const next = {
|
|
4811
|
-
streamed: streamedRef.current,
|
|
5236
|
+
streamed: streamedRef.current.slice(lastCommittedLenRef.current),
|
|
4812
5237
|
reasoning: reasoningRef.current
|
|
4813
5238
|
};
|
|
4814
5239
|
if (nextFlush(flushedRef.current, next) !== null) {
|
|
@@ -4835,9 +5260,9 @@ function Repl({
|
|
|
4835
5260
|
);
|
|
4836
5261
|
}
|
|
4837
5262
|
}
|
|
4838
|
-
streamedRef.current += event.text;
|
|
5263
|
+
streamedRef.current += toTerminalSafe(event.text);
|
|
4839
5264
|
} else if (event.type === "reasoning") {
|
|
4840
|
-
reasoningRef.current += event.text;
|
|
5265
|
+
reasoningRef.current += toTerminalSafe(event.text);
|
|
4841
5266
|
} else if (event.type === "tool-call") {
|
|
4842
5267
|
commitStreamed(true);
|
|
4843
5268
|
toolCardsRef.current.set(event.id, {
|
|
@@ -4877,6 +5302,12 @@ function Repl({
|
|
|
4877
5302
|
usage.promptTokens += event.usage.promptTokens;
|
|
4878
5303
|
usage.completionTokens += event.usage.completionTokens;
|
|
4879
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
|
+
}
|
|
4880
5311
|
if (cacheUsageReported(event.usage)) {
|
|
4881
5312
|
if (!usage.cache) usage.cache = { cachedTokens: 0, promptTokens: 0 };
|
|
4882
5313
|
addToCacheTotals(usage.cache, event.usage);
|
|
@@ -4886,9 +5317,13 @@ function Repl({
|
|
|
4886
5317
|
});
|
|
4887
5318
|
}
|
|
4888
5319
|
} else if (event.type === "retry") {
|
|
5320
|
+
streamedRef.current = streamedRef.current.slice(0, lastCommittedLenRef.current);
|
|
5321
|
+
flushedRef.current = { ...flushedRef.current, streamed: "" };
|
|
5322
|
+
setStreamingText("");
|
|
5323
|
+
const wait = event.delayMs !== void 0 && event.delayMs >= 1e3 ? ` in ${Math.round(event.delayMs / 1e3)}s` : "";
|
|
4889
5324
|
pushMessage(
|
|
4890
5325
|
"system",
|
|
4891
|
-
`Request failed, retrying (${event.attempt}/${event.maxAttempts}): ${event.reason}`
|
|
5326
|
+
`Request failed, retrying (${event.attempt}/${event.maxAttempts})${wait}: ${event.reason}`
|
|
4892
5327
|
);
|
|
4893
5328
|
} else if (event.type === "notice") {
|
|
4894
5329
|
pushMessage("system", event.message);
|
|
@@ -4915,8 +5350,9 @@ function Repl({
|
|
|
4915
5350
|
setThinkingText("");
|
|
4916
5351
|
setThoughtSummary(null);
|
|
4917
5352
|
setActivity(null);
|
|
4918
|
-
const finalText = streamedRef.current;
|
|
5353
|
+
const finalText = streamedRef.current.slice(lastCommittedLenRef.current);
|
|
4919
5354
|
streamedRef.current = "";
|
|
5355
|
+
lastCommittedLenRef.current = 0;
|
|
4920
5356
|
setStreamingText(null);
|
|
4921
5357
|
const interrupted = controller.signal.aborted;
|
|
4922
5358
|
notifyBell({
|
|
@@ -5099,10 +5535,8 @@ ${lines.join("\n")}`;
|
|
|
5099
5535
|
return ctx.permissionMode(picked);
|
|
5100
5536
|
},
|
|
5101
5537
|
pickSession: async (all) => {
|
|
5102
|
-
const
|
|
5103
|
-
|
|
5104
|
-
SESSION_PICKER_CAP
|
|
5105
|
-
);
|
|
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);
|
|
5106
5540
|
if (entries.length === 0) {
|
|
5107
5541
|
return all ? "No sessions found." : "No sessions found for this directory.";
|
|
5108
5542
|
}
|
|
@@ -5110,13 +5544,16 @@ ${lines.join("\n")}`;
|
|
|
5110
5544
|
entries.map(async ({ meta, messageCount }) => {
|
|
5111
5545
|
const parts = [`${messageCount} messages \xB7 ${relativeTime(meta.updatedAt)}`];
|
|
5112
5546
|
const preview = await sessionPreview(meta.id);
|
|
5113
|
-
if (preview)
|
|
5547
|
+
if (preview) {
|
|
5548
|
+
parts.push(
|
|
5549
|
+
preview.length > SESSION_PREVIEW_MAX ? `${preview.slice(0, SESSION_PREVIEW_MAX)}\u2026` : preview
|
|
5550
|
+
);
|
|
5551
|
+
}
|
|
5114
5552
|
if (all) parts.push(`[${meta.cwd}]`);
|
|
5115
5553
|
return {
|
|
5116
5554
|
value: meta.id,
|
|
5117
5555
|
label: meta.title || shortSessionId(meta.id),
|
|
5118
|
-
description: parts.join(" \xB7 ")
|
|
5119
|
-
hint: meta.id === sessionStoreRef.current?.id ? "current" : void 0
|
|
5556
|
+
description: parts.join(" \xB7 ")
|
|
5120
5557
|
};
|
|
5121
5558
|
})
|
|
5122
5559
|
);
|
|
@@ -5229,15 +5666,16 @@ ${estimateCost(usageRef.current, modelNameRef.current, modelConfig)}`;
|
|
|
5229
5666
|
if (preview.removed === 0) {
|
|
5230
5667
|
return "Nothing to undo (no conversation turn to retract).";
|
|
5231
5668
|
}
|
|
5232
|
-
const diffs = await buildUndoDiffs(
|
|
5669
|
+
const diffs = preview.tree ? await buildUndoTreeDiffs(cwd, preview.tree) : await buildUndoDiffs(
|
|
5233
5670
|
preview.turn !== void 0 ? listTurnSnapshots(preview.turn) : [],
|
|
5234
5671
|
cwd
|
|
5235
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.`;
|
|
5236
5674
|
const confirmed = await new Promise((resolve) => {
|
|
5237
5675
|
setPendingRewind({
|
|
5238
5676
|
title: "Undo last turn",
|
|
5239
5677
|
confirmLabel: "undo",
|
|
5240
|
-
summary
|
|
5678
|
+
summary,
|
|
5241
5679
|
diffs,
|
|
5242
5680
|
resolve
|
|
5243
5681
|
});
|
|
@@ -5245,11 +5683,10 @@ ${estimateCost(usageRef.current, modelNameRef.current, modelConfig)}`;
|
|
|
5245
5683
|
if (!confirmed) {
|
|
5246
5684
|
return "Undo cancelled.";
|
|
5247
5685
|
}
|
|
5248
|
-
const
|
|
5249
|
-
if (removed === 0) {
|
|
5686
|
+
const outcome = await current.undoLastTurn();
|
|
5687
|
+
if (outcome.removed === 0) {
|
|
5250
5688
|
return "Nothing to undo (no conversation turn to retract).";
|
|
5251
5689
|
}
|
|
5252
|
-
const reverted = turn !== void 0 ? await undoTurnSnapshots(turn) : [];
|
|
5253
5690
|
const visible = messagesRef.current;
|
|
5254
5691
|
let cut = -1;
|
|
5255
5692
|
for (let i = visible.length - 1; i >= 0; i--) {
|
|
@@ -5259,9 +5696,36 @@ ${estimateCost(usageRef.current, modelNameRef.current, modelConfig)}`;
|
|
|
5259
5696
|
}
|
|
5260
5697
|
}
|
|
5261
5698
|
redrawMessages(cut >= 0 ? visible.slice(0, cut) : visible);
|
|
5262
|
-
return [
|
|
5263
|
-
|
|
5264
|
-
|
|
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);
|
|
5265
5729
|
},
|
|
5266
5730
|
rewind: async (args) => {
|
|
5267
5731
|
if (!args) {
|
|
@@ -5444,6 +5908,10 @@ ${plan.affected.length} file change(s) will be reverted, ${messageCount} message
|
|
|
5444
5908
|
);
|
|
5445
5909
|
return;
|
|
5446
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
|
+
}
|
|
5447
5915
|
void command.run(parsed.args, registry.ctx);
|
|
5448
5916
|
return;
|
|
5449
5917
|
}
|
|
@@ -5487,8 +5955,8 @@ ${plan.affected.length} file change(s) will be reverted, ${messageCount} message
|
|
|
5487
5955
|
}, [cwd, config, isStreaming, epoch]);
|
|
5488
5956
|
const sessionCost = sessionCostUsd();
|
|
5489
5957
|
return /* @__PURE__ */ jsxs14(Box13, { flexDirection: "column", children: [
|
|
5490
|
-
/* @__PURE__ */
|
|
5491
|
-
(thinking || activity !== null) && /* @__PURE__ */
|
|
5958
|
+
/* @__PURE__ */ jsx11(MessageList, { messages }, `messages-${epoch}`),
|
|
5959
|
+
(thinking || activity !== null) && /* @__PURE__ */ jsx11(
|
|
5492
5960
|
ThinkingIndicator,
|
|
5493
5961
|
{
|
|
5494
5962
|
reasoning: thinkingText,
|
|
@@ -5496,9 +5964,9 @@ ${plan.affected.length} file change(s) will be reverted, ${messageCount} message
|
|
|
5496
5964
|
activity: activity ?? void 0
|
|
5497
5965
|
}
|
|
5498
5966
|
),
|
|
5499
|
-
!thinking && activity === null && thoughtSummary !== null && /* @__PURE__ */
|
|
5500
|
-
streamingText !== null && (streamingText !== "" || turnChunksRef.current === 0) && /* @__PURE__ */
|
|
5501
|
-
pending && /* @__PURE__ */
|
|
5967
|
+
!thinking && activity === null && thoughtSummary !== null && /* @__PURE__ */ jsx11(Text14, { dimColor: true, children: thoughtSummary }),
|
|
5968
|
+
streamingText !== null && (streamingText !== "" || turnChunksRef.current === 0) && /* @__PURE__ */ jsx11(StreamingMessage, { text: streamingText, continuation: turnChunksRef.current > 0 }),
|
|
5969
|
+
pending && /* @__PURE__ */ jsx11(
|
|
5502
5970
|
PermissionPrompt,
|
|
5503
5971
|
{
|
|
5504
5972
|
request: pending.request,
|
|
@@ -5506,8 +5974,8 @@ ${plan.affected.length} file change(s) will be reverted, ${messageCount} message
|
|
|
5506
5974
|
onDecision: handleDecision
|
|
5507
5975
|
}
|
|
5508
5976
|
),
|
|
5509
|
-
planApproval && /* @__PURE__ */
|
|
5510
|
-
pendingRewind && /* @__PURE__ */
|
|
5977
|
+
planApproval && /* @__PURE__ */ jsx11(ExitPlanPrompt, { onDecision: handlePlanDecision }),
|
|
5978
|
+
pendingRewind && /* @__PURE__ */ jsx11(
|
|
5511
5979
|
RewindConfirmPrompt,
|
|
5512
5980
|
{
|
|
5513
5981
|
summary: pendingRewind.summary,
|
|
@@ -5517,7 +5985,7 @@ ${plan.affected.length} file change(s) will be reverted, ${messageCount} message
|
|
|
5517
5985
|
onDecision: handleRewindDecision
|
|
5518
5986
|
}
|
|
5519
5987
|
),
|
|
5520
|
-
picker && /* @__PURE__ */
|
|
5988
|
+
picker && /* @__PURE__ */ jsx11(
|
|
5521
5989
|
SelectPrompt,
|
|
5522
5990
|
{
|
|
5523
5991
|
title: picker.title,
|
|
@@ -5526,7 +5994,7 @@ ${plan.affected.length} file change(s) will be reverted, ${messageCount} message
|
|
|
5526
5994
|
onCancel: handlePickerCancel
|
|
5527
5995
|
}
|
|
5528
5996
|
),
|
|
5529
|
-
pendingConnect && /* @__PURE__ */
|
|
5997
|
+
pendingConnect && /* @__PURE__ */ jsx11(
|
|
5530
5998
|
ConnectWizard,
|
|
5531
5999
|
{
|
|
5532
6000
|
existingProviderNames: config.providers.map((p) => p.name),
|
|
@@ -5538,17 +6006,22 @@ ${plan.affected.length} file change(s) will be reverted, ${messageCount} message
|
|
|
5538
6006
|
onCancel: cancelConnect
|
|
5539
6007
|
}
|
|
5540
6008
|
),
|
|
5541
|
-
queueItems.map((item) => /* @__PURE__ */ jsxs14(Text14, { dimColor: true, children: [
|
|
6009
|
+
queueItems.slice(0, MAX_VISIBLE_QUEUED).map((item) => /* @__PURE__ */ jsxs14(Text14, { dimColor: true, children: [
|
|
5542
6010
|
"queued: ",
|
|
5543
6011
|
item.text.length > 80 ? `${item.text.slice(0, 80)}\u2026` : item.text
|
|
5544
6012
|
] }, item.id)),
|
|
6013
|
+
queueItems.length > MAX_VISIBLE_QUEUED && /* @__PURE__ */ jsxs14(Text14, { dimColor: true, children: [
|
|
6014
|
+
"\u2026 ",
|
|
6015
|
+
queueItems.length - MAX_VISIBLE_QUEUED,
|
|
6016
|
+
" more queued"
|
|
6017
|
+
] }),
|
|
5545
6018
|
pendingImages.map((staged) => /* @__PURE__ */ jsxs14(Text14, { dimColor: true, children: [
|
|
5546
6019
|
"[image attached: ",
|
|
5547
6020
|
staged.image.path,
|
|
5548
6021
|
"]"
|
|
5549
6022
|
] }, staged.id)),
|
|
5550
|
-
/* @__PURE__ */
|
|
5551
|
-
/* @__PURE__ */
|
|
6023
|
+
/* @__PURE__ */ jsx11(TodoPanel, { todos }),
|
|
6024
|
+
/* @__PURE__ */ jsx11(
|
|
5552
6025
|
InputBox,
|
|
5553
6026
|
{
|
|
5554
6027
|
isStreaming,
|
|
@@ -5563,7 +6036,7 @@ ${plan.affected.length} file change(s) will be reverted, ${messageCount} message
|
|
|
5563
6036
|
onPasteImage: handlePasteImage
|
|
5564
6037
|
}
|
|
5565
6038
|
),
|
|
5566
|
-
/* @__PURE__ */
|
|
6039
|
+
/* @__PURE__ */ jsx11(
|
|
5567
6040
|
StatusBar,
|
|
5568
6041
|
{
|
|
5569
6042
|
cwd,
|
|
@@ -5584,7 +6057,7 @@ function renderRepl(backend, opts) {
|
|
|
5584
6057
|
defaultTaskManager.cleanup();
|
|
5585
6058
|
});
|
|
5586
6059
|
return render(
|
|
5587
|
-
/* @__PURE__ */
|
|
6060
|
+
/* @__PURE__ */ jsx11(
|
|
5588
6061
|
Repl,
|
|
5589
6062
|
{
|
|
5590
6063
|
backend,
|
|
@@ -5621,6 +6094,37 @@ function parseTomlFile(content, filePath) {
|
|
|
5621
6094
|
function mergeConfig(base, override) {
|
|
5622
6095
|
return { ...base, ...override };
|
|
5623
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
|
+
}
|
|
5624
6128
|
function applyOverrides(config, overrides) {
|
|
5625
6129
|
if (!overrides) return config;
|
|
5626
6130
|
const merged = { ...config };
|
|
@@ -5636,22 +6140,13 @@ function readTomlFileSync(filePath) {
|
|
|
5636
6140
|
function loadConfigSync(cwd, overrides) {
|
|
5637
6141
|
loadEnvFile();
|
|
5638
6142
|
const global = readTomlFileSync(globalConfigPath());
|
|
5639
|
-
const
|
|
6143
|
+
const projectPath = projectConfigPath(cwd);
|
|
6144
|
+
const project = sanitizeProjectConfig(readTomlFileSync(projectPath), projectPath);
|
|
5640
6145
|
const merged = applyOverrides(mergeConfig(global, project), overrides);
|
|
5641
6146
|
return ConfigSchema.parse(merged);
|
|
5642
6147
|
}
|
|
5643
6148
|
|
|
5644
6149
|
// src/main.tsx
|
|
5645
|
-
var SYSTEM_PROMPT = `You are Star CLI, an AI coding agent running in the user's terminal.
|
|
5646
|
-
You help with software engineering tasks: reading, writing and editing code, running shell commands, and managing todos.
|
|
5647
|
-
Be concise and direct. Use tools when they help accomplish the task.
|
|
5648
|
-
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.
|
|
5649
|
-
The working directory is the user's project root; never touch files outside it without explicit instruction.
|
|
5650
|
-
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.
|
|
5651
|
-
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.
|
|
5652
|
-
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.
|
|
5653
|
-
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.
|
|
5654
|
-
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.`;
|
|
5655
6150
|
async function createLoop(config, modelName, cwd, sessionStore) {
|
|
5656
6151
|
const model = createModel(config, modelName);
|
|
5657
6152
|
const registry = createDefaultRegistry();
|
|
@@ -5665,7 +6160,7 @@ async function createLoop(config, modelName, cwd, sessionStore) {
|
|
|
5665
6160
|
contextMaxTokens: contextWindowTokens(config, modelName)
|
|
5666
6161
|
});
|
|
5667
6162
|
}
|
|
5668
|
-
async function printMode(loop, prompt, cwd, json, imagePaths) {
|
|
6163
|
+
async function printMode(loop, prompt, cwd, json, imagePaths, sessionStore) {
|
|
5669
6164
|
const flagImages = [];
|
|
5670
6165
|
for (const imagePath of imagePaths) {
|
|
5671
6166
|
if (!imageMimeType(imagePath)) {
|
|
@@ -5734,15 +6229,22 @@ async function printMode(loop, prompt, cwd, json, imagePaths) {
|
|
|
5734
6229
|
}
|
|
5735
6230
|
case "finish":
|
|
5736
6231
|
usage.add(event.usage);
|
|
6232
|
+
if (event.usage) {
|
|
6233
|
+
sessionStore?.addUsage(event.usage).catch(() => {
|
|
6234
|
+
});
|
|
6235
|
+
}
|
|
5737
6236
|
break;
|
|
5738
|
-
case "retry":
|
|
5739
|
-
if (!json)
|
|
6237
|
+
case "retry": {
|
|
6238
|
+
if (!json) {
|
|
6239
|
+
const wait = event.delayMs !== void 0 && event.delayMs >= 1e3 ? ` in ${Math.round(event.delayMs / 1e3)}s` : "";
|
|
5740
6240
|
process.stderr.write(
|
|
5741
6241
|
`
|
|
5742
|
-
[retry ${event.attempt}/${event.maxAttempts}] ${event.reason}
|
|
6242
|
+
[retry ${event.attempt}/${event.maxAttempts}${wait}] ${event.reason}
|
|
5743
6243
|
`
|
|
5744
6244
|
);
|
|
6245
|
+
}
|
|
5745
6246
|
break;
|
|
6247
|
+
}
|
|
5746
6248
|
case "notice":
|
|
5747
6249
|
if (!json) process.stderr.write(`
|
|
5748
6250
|
[notice] ${event.message}
|
|
@@ -5780,8 +6282,15 @@ async function printMode(loop, prompt, cwd, json, imagePaths) {
|
|
|
5780
6282
|
}
|
|
5781
6283
|
}
|
|
5782
6284
|
if (!json) process.stdout.write("\n");
|
|
5783
|
-
defaultTaskManager.cleanup();
|
|
5784
|
-
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
|
+
}
|
|
5785
6294
|
return exitCode;
|
|
5786
6295
|
}
|
|
5787
6296
|
var program = new Command();
|
|
@@ -5859,12 +6368,17 @@ program.name("star").description("Star CLI \u2014 an AI agent command-line inter
|
|
|
5859
6368
|
}
|
|
5860
6369
|
}
|
|
5861
6370
|
if (resumeId) {
|
|
5862
|
-
|
|
5863
|
-
if (!
|
|
6371
|
+
sessionStore = await SessionStore.open(resumeId);
|
|
6372
|
+
if (!sessionStore) {
|
|
5864
6373
|
console.error(`Session not found: ${resumeId}`);
|
|
5865
6374
|
process.exit(1);
|
|
5866
6375
|
}
|
|
5867
|
-
|
|
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 };
|
|
5868
6382
|
} else if (!opts.print) {
|
|
5869
6383
|
sessionStore = await SessionStore.create(cwd, modelName);
|
|
5870
6384
|
}
|
|
@@ -5888,7 +6402,8 @@ program.name("star").description("Star CLI \u2014 an AI agent command-line inter
|
|
|
5888
6402
|
opts.print,
|
|
5889
6403
|
cwd,
|
|
5890
6404
|
Boolean(opts.json),
|
|
5891
|
-
opts.image ?? []
|
|
6405
|
+
opts.image ?? [],
|
|
6406
|
+
sessionStore
|
|
5892
6407
|
);
|
|
5893
6408
|
return;
|
|
5894
6409
|
}
|