@cryer/star-cli 0.3.2 → 0.3.4
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 +5 -1
- package/README.zh-CN.md +319 -0
- package/dist/{chunk-LADQCYKL.js → chunk-B6P4VUBE.js} +16 -7
- package/dist/chunk-B6P4VUBE.js.map +1 -0
- package/dist/{chunk-BV4PLM6X.js → chunk-K3R42LOX.js} +1487 -670
- package/dist/chunk-K3R42LOX.js.map +1 -0
- package/dist/{loop-XKC5HQBT.js → loop-SDZVEQND.js} +3 -3
- package/dist/main.js +579 -285
- 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 +25 -16
- 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-SDZVEQND.js.map} +0 -0
package/dist/main.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
AgentLoop,
|
|
4
|
+
ConfigSchema,
|
|
4
5
|
MAX_IMAGE_DIMENSION,
|
|
5
6
|
SKIP_DIRS,
|
|
6
7
|
TodoStore,
|
|
@@ -13,12 +14,15 @@ import {
|
|
|
13
14
|
collectCommitContext,
|
|
14
15
|
collectWorkingDiff,
|
|
15
16
|
compactMessages,
|
|
17
|
+
contextWindowTokens,
|
|
16
18
|
createDefaultRegistry,
|
|
17
19
|
createIgnorePredicate,
|
|
18
20
|
defaultAgentTasks,
|
|
19
21
|
defaultTaskManager,
|
|
22
|
+
diffTreeNames,
|
|
20
23
|
discoverSkills,
|
|
21
24
|
estimateTokens,
|
|
25
|
+
formatAgentTaskUpdate,
|
|
22
26
|
formatTaskFinished,
|
|
23
27
|
formatTaskList,
|
|
24
28
|
formatTaskStarted,
|
|
@@ -39,9 +43,8 @@ import {
|
|
|
39
43
|
resolveShell,
|
|
40
44
|
resolveSnapshotContent,
|
|
41
45
|
rewindToSnapshot,
|
|
42
|
-
summarizeMessages
|
|
43
|
-
|
|
44
|
-
} from "./chunk-BV4PLM6X.js";
|
|
46
|
+
summarizeMessages
|
|
47
|
+
} from "./chunk-K3R42LOX.js";
|
|
45
48
|
import {
|
|
46
49
|
envFilePath,
|
|
47
50
|
globalConfigPath,
|
|
@@ -49,11 +52,20 @@ import {
|
|
|
49
52
|
sessionsDir,
|
|
50
53
|
starHome,
|
|
51
54
|
userMemoryPath
|
|
52
|
-
} from "./chunk-
|
|
55
|
+
} from "./chunk-B6P4VUBE.js";
|
|
53
56
|
|
|
54
57
|
// src/main.tsx
|
|
55
58
|
import { Command } from "commander";
|
|
56
59
|
|
|
60
|
+
// src/cli/terminal-text.ts
|
|
61
|
+
var UNSAFE_CHARS = /[\x00-\x09\x0b-\x1f\x7f]/;
|
|
62
|
+
var CONTROL_CHARS = /[\x00-\x08\x0b-\x1f\x7f]/g;
|
|
63
|
+
var TABS = /\t/g;
|
|
64
|
+
function toTerminalSafe(text) {
|
|
65
|
+
if (!UNSAFE_CHARS.test(text)) return text;
|
|
66
|
+
return text.replace(TABS, " ").replace(CONTROL_CHARS, "");
|
|
67
|
+
}
|
|
68
|
+
|
|
57
69
|
// src/cli/format.ts
|
|
58
70
|
function summarizeArgs(args, maxLength = 120) {
|
|
59
71
|
let json;
|
|
@@ -98,7 +110,7 @@ function buildDisplayMessages(messages) {
|
|
|
98
110
|
if (message.role === "user" || message.role === "assistant") {
|
|
99
111
|
const text = coreMessageText(message);
|
|
100
112
|
if (text) {
|
|
101
|
-
display.push({ id: display.length, role: message.role, text });
|
|
113
|
+
display.push({ id: display.length, role: message.role, text: toTerminalSafe(text) });
|
|
102
114
|
} else {
|
|
103
115
|
collapsed++;
|
|
104
116
|
}
|
|
@@ -110,7 +122,7 @@ function buildDisplayMessages(messages) {
|
|
|
110
122
|
display.push({
|
|
111
123
|
id: display.length,
|
|
112
124
|
role: "system",
|
|
113
|
-
text:
|
|
125
|
+
text: `Restored ${collapsed} history message(s) not shown here.`
|
|
114
126
|
});
|
|
115
127
|
}
|
|
116
128
|
return display;
|
|
@@ -168,7 +180,15 @@ function resizeImagePlan(platform, inputFile, outputFile, maxDimension, tools =
|
|
|
168
180
|
if (tools.hasConvert) {
|
|
169
181
|
return {
|
|
170
182
|
command: "convert",
|
|
171
|
-
|
|
183
|
+
// -auto-orient keeps EXIF-rotated photos upright after the resize
|
|
184
|
+
// strips their orientation metadata.
|
|
185
|
+
args: [
|
|
186
|
+
inputFile,
|
|
187
|
+
"-auto-orient",
|
|
188
|
+
"-resize",
|
|
189
|
+
`${maxDimension}x${maxDimension}>`,
|
|
190
|
+
outputFile
|
|
191
|
+
]
|
|
172
192
|
};
|
|
173
193
|
}
|
|
174
194
|
if (tools.hasFfmpeg) {
|
|
@@ -236,6 +256,7 @@ var MAX_MENTION_BYTES = 100 * 1024;
|
|
|
236
256
|
var MAX_IMAGE_BYTES = 5 * 1024 * 1024;
|
|
237
257
|
var MAX_DIR_MENTION_ENTRIES = 200;
|
|
238
258
|
var MAX_DIR_MENTION_DEPTH = 10;
|
|
259
|
+
var MAX_DIR_MENTION_TOTAL = 5e3;
|
|
239
260
|
var IMAGE_MIME_TYPES = {
|
|
240
261
|
".png": "image/png",
|
|
241
262
|
".jpg": "image/jpeg",
|
|
@@ -261,7 +282,7 @@ async function readImageInput(filePath, cwd) {
|
|
|
261
282
|
});
|
|
262
283
|
return result.image;
|
|
263
284
|
}
|
|
264
|
-
var MENTION_RE = /(?<=^|\s)@([A-Za-z]:[\\/][A-Za-z0-9._
|
|
285
|
+
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
286
|
function parseMentions(text) {
|
|
266
287
|
const mentions = [];
|
|
267
288
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -294,13 +315,18 @@ function isBinaryContent(buf) {
|
|
|
294
315
|
return probe.includes(0);
|
|
295
316
|
}
|
|
296
317
|
async function listDirEntries(dir, prefix, depth, state, ignore) {
|
|
318
|
+
if (state.capped) return;
|
|
297
319
|
const entries = await readdir(dir, { withFileTypes: true }).catch(() => null);
|
|
298
320
|
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) => {
|
|
321
|
+
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
322
|
const dirDiff = Number(b.isDirectory()) - Number(a.isDirectory());
|
|
301
323
|
return dirDiff !== 0 ? dirDiff : a.name.localeCompare(b.name);
|
|
302
324
|
});
|
|
303
325
|
for (let i = 0; i < visible.length; i++) {
|
|
326
|
+
if (state.total >= MAX_DIR_MENTION_TOTAL) {
|
|
327
|
+
state.capped = true;
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
304
330
|
const entry = visible[i];
|
|
305
331
|
if (!entry) continue;
|
|
306
332
|
state.total += 1;
|
|
@@ -325,11 +351,13 @@ async function listDirEntries(dir, prefix, depth, state, ignore) {
|
|
|
325
351
|
}
|
|
326
352
|
async function directoryBlock(abs, mention, ignore) {
|
|
327
353
|
const display = `${mention.replace(/\\/g, "/").replace(/\/+$/, "")}/`;
|
|
328
|
-
const state = { lines: [`${display}`], shown: 0, total: 0 };
|
|
354
|
+
const state = { lines: [`${display}`], shown: 0, total: 0, capped: false };
|
|
329
355
|
const probe = await readdir(abs).catch(() => null);
|
|
330
356
|
if (!probe) return null;
|
|
331
357
|
await listDirEntries(abs, "", 1, state, ignore);
|
|
332
|
-
if (state.
|
|
358
|
+
if (state.capped) {
|
|
359
|
+
state.lines.push("... (truncated, many more entries)");
|
|
360
|
+
} else if (state.total > state.shown) {
|
|
333
361
|
state.lines.push(
|
|
334
362
|
`... (truncated, ${state.total - state.shown} more entries \u2014 use glob/grep tools or mention a subdirectory to see more)`
|
|
335
363
|
);
|
|
@@ -473,11 +501,56 @@ import { parse, stringify } from "smol-toml";
|
|
|
473
501
|
async function addAllowRule(rule) {
|
|
474
502
|
return addPermissionRule("allow", rule);
|
|
475
503
|
}
|
|
504
|
+
function tomlString(value) {
|
|
505
|
+
return JSON.stringify(value);
|
|
506
|
+
}
|
|
507
|
+
async function writeFileAtomic(filePath, content) {
|
|
508
|
+
const tmpPath = `${filePath}.${process.pid}.tmp`;
|
|
509
|
+
await fs.promises.writeFile(tmpPath, content, "utf8");
|
|
510
|
+
await fs.promises.rename(tmpPath, filePath);
|
|
511
|
+
}
|
|
512
|
+
function upsertPermissionRuleText(content, kind, rules, hadPermissionsTable) {
|
|
513
|
+
const newLine = `${kind} = [${rules.map(tomlString).join(", ")}]`;
|
|
514
|
+
const lines = content.split("\n");
|
|
515
|
+
let sectionStart = -1;
|
|
516
|
+
let sectionEnd = lines.length;
|
|
517
|
+
for (let i = 0; i < lines.length; i++) {
|
|
518
|
+
const line = lines[i] ?? "";
|
|
519
|
+
if (!/^\s*\[/.test(line)) continue;
|
|
520
|
+
if (sectionStart === -1) {
|
|
521
|
+
if (/^\s*\[\s*permissions\s*\]\s*(?:#.*)?$/.test(line)) sectionStart = i;
|
|
522
|
+
} else {
|
|
523
|
+
sectionEnd = i;
|
|
524
|
+
break;
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
if (sectionStart === -1) {
|
|
528
|
+
if (hadPermissionsTable) return null;
|
|
529
|
+
const separator = content === "" ? "" : content.endsWith("\n") ? "\n" : "\n\n";
|
|
530
|
+
return `${content}${separator}[permissions]
|
|
531
|
+
${newLine}
|
|
532
|
+
`;
|
|
533
|
+
}
|
|
534
|
+
const inlineArray = new RegExp(`^(\\s*)${kind}\\s*=\\s*\\[.*\\](\\s*(?:#.*)?)$`);
|
|
535
|
+
const anyKey = new RegExp(`^\\s*${kind}\\s*=`);
|
|
536
|
+
for (let i = sectionStart + 1; i < sectionEnd; i++) {
|
|
537
|
+
const line = lines[i] ?? "";
|
|
538
|
+
const match = inlineArray.exec(line);
|
|
539
|
+
if (match) {
|
|
540
|
+
lines[i] = `${match[1]}${newLine}${match[2]}`;
|
|
541
|
+
return lines.join("\n");
|
|
542
|
+
}
|
|
543
|
+
if (anyKey.test(line)) return null;
|
|
544
|
+
}
|
|
545
|
+
lines.splice(sectionStart + 1, 0, newLine);
|
|
546
|
+
return lines.join("\n");
|
|
547
|
+
}
|
|
476
548
|
async function addPermissionRule(kind, rule) {
|
|
477
549
|
const filePath = globalConfigPath();
|
|
550
|
+
let content = "";
|
|
478
551
|
let raw = {};
|
|
479
552
|
try {
|
|
480
|
-
|
|
553
|
+
content = await fs.promises.readFile(filePath, "utf8");
|
|
481
554
|
raw = parse(content);
|
|
482
555
|
} catch (error) {
|
|
483
556
|
if (error.code !== "ENOENT") throw error;
|
|
@@ -485,117 +558,47 @@ async function addPermissionRule(kind, rule) {
|
|
|
485
558
|
const permissions = typeof raw.permissions === "object" && raw.permissions !== null ? raw.permissions : {};
|
|
486
559
|
const rules = Array.isArray(permissions[kind]) ? permissions[kind] : [];
|
|
487
560
|
if (rules.includes(rule)) return false;
|
|
488
|
-
|
|
489
|
-
raw.permissions = permissions;
|
|
561
|
+
const nextRules = [...rules, rule];
|
|
490
562
|
await fs.promises.mkdir(path3.dirname(filePath), { recursive: true });
|
|
491
|
-
|
|
563
|
+
const updated = upsertPermissionRuleText(content, kind, nextRules, raw.permissions !== void 0);
|
|
564
|
+
if (updated !== null) {
|
|
565
|
+
await writeFileAtomic(filePath, updated);
|
|
566
|
+
return true;
|
|
567
|
+
}
|
|
568
|
+
permissions[kind] = nextRules;
|
|
569
|
+
raw.permissions = permissions;
|
|
570
|
+
await writeFileAtomic(filePath, stringify(raw));
|
|
492
571
|
return true;
|
|
493
572
|
}
|
|
573
|
+
function upsertTopLevelKeyText(content, key, value) {
|
|
574
|
+
const newLine = `${key} = ${tomlString(value)}`;
|
|
575
|
+
if (content.trim() === "") return `${newLine}
|
|
576
|
+
`;
|
|
577
|
+
const lines = content.split("\n");
|
|
578
|
+
const firstTable = lines.findIndex((line) => /^\s*\[/.test(line));
|
|
579
|
+
const end = firstTable === -1 ? lines.length : firstTable;
|
|
580
|
+
const keyLine = new RegExp(`^\\s*${key}\\s*=`);
|
|
581
|
+
for (let i = 0; i < end; i++) {
|
|
582
|
+
const line = lines[i] ?? "";
|
|
583
|
+
if (keyLine.test(line)) {
|
|
584
|
+
const comment = /\s+#.*$/.exec(line);
|
|
585
|
+
lines[i] = `${newLine}${comment?.[0] ?? ""}`;
|
|
586
|
+
return lines.join("\n");
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
lines.splice(end, 0, newLine);
|
|
590
|
+
return lines.join("\n");
|
|
591
|
+
}
|
|
494
592
|
async function savePermissionMode(mode) {
|
|
495
593
|
const filePath = globalConfigPath();
|
|
496
|
-
let
|
|
594
|
+
let content = "";
|
|
497
595
|
try {
|
|
498
|
-
|
|
499
|
-
raw = parse(content);
|
|
596
|
+
content = await fs.promises.readFile(filePath, "utf8");
|
|
500
597
|
} catch (error) {
|
|
501
598
|
if (error.code !== "ENOENT") throw error;
|
|
502
599
|
}
|
|
503
|
-
raw.permissionMode = mode;
|
|
504
600
|
await fs.promises.mkdir(path3.dirname(filePath), { recursive: true });
|
|
505
|
-
await
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
// src/config/schema.ts
|
|
509
|
-
import { z } from "zod";
|
|
510
|
-
var ProviderConfigSchema = z.object({
|
|
511
|
-
name: z.string(),
|
|
512
|
-
// openai-responses talks to the Responses API (/responses) instead of chat
|
|
513
|
-
// completions — the wire format codex uses, and the native API of
|
|
514
|
-
// reasoning-model relays that otherwise hide reasoning behind empty chat
|
|
515
|
-
// completions. streamChat sends strictSchemas:false + store:false with it.
|
|
516
|
-
protocol: z.enum(["openai-compatible", "anthropic", "openai-responses"]).default("openai-compatible"),
|
|
517
|
-
baseURL: z.string(),
|
|
518
|
-
apiKeyEnv: z.string().optional(),
|
|
519
|
-
apiKey: z.string().optional(),
|
|
520
|
-
headers: z.record(z.string()).optional()
|
|
521
|
-
});
|
|
522
|
-
var ModelConfigSchema = z.object({
|
|
523
|
-
name: z.string(),
|
|
524
|
-
provider: z.string(),
|
|
525
|
-
model: z.string(),
|
|
526
|
-
maxTokens: z.number().int().positive().optional(),
|
|
527
|
-
// Per-model context window; overrides the top-level contextMaxTokens for
|
|
528
|
-
// compaction and the status bar when set.
|
|
529
|
-
contextMaxTokens: z.number().int().positive().optional(),
|
|
530
|
-
// Cost estimation needs BOTH prices (USD per 1M tokens); with only one set
|
|
531
|
-
// the model is treated as unpriced.
|
|
532
|
-
promptPrice: z.number().nonnegative().optional(),
|
|
533
|
-
completionPrice: z.number().nonnegative().optional()
|
|
534
|
-
});
|
|
535
|
-
var PermissionsConfigSchema = z.object({
|
|
536
|
-
allow: z.array(z.string()).default([]),
|
|
537
|
-
deny: z.array(z.string()).default([])
|
|
538
|
-
});
|
|
539
|
-
var HookConfigSchema = z.object({
|
|
540
|
-
event: z.enum(["PreToolUse", "PostToolUse", "Stop"]),
|
|
541
|
-
matcher: z.string().refine(
|
|
542
|
-
(pattern) => {
|
|
543
|
-
try {
|
|
544
|
-
new RegExp(pattern);
|
|
545
|
-
return true;
|
|
546
|
-
} catch {
|
|
547
|
-
return false;
|
|
548
|
-
}
|
|
549
|
-
},
|
|
550
|
-
{ message: "invalid regular expression" }
|
|
551
|
-
).optional(),
|
|
552
|
-
command: z.string().min(1),
|
|
553
|
-
timeoutSec: z.number().positive().default(30)
|
|
554
|
-
});
|
|
555
|
-
var ConfigSchema = z.object({
|
|
556
|
-
defaultModel: z.string().default(""),
|
|
557
|
-
permissionMode: z.enum(["auto", "ask", "readonly", "yolo", "plan"]).default("ask"),
|
|
558
|
-
providers: z.array(ProviderConfigSchema).default([]),
|
|
559
|
-
models: z.array(ModelConfigSchema).default([]),
|
|
560
|
-
maxSteps: z.number().int().positive().default(50),
|
|
561
|
-
contextMaxTokens: z.number().int().positive().default(1e5),
|
|
562
|
-
// Seconds without any stream part before a stalled response is ended
|
|
563
|
-
// gracefully (some relays never send the terminal chunks). Applies once
|
|
564
|
-
// streaming has started; the first part gets a longer, fixed allowance.
|
|
565
|
-
streamIdleTimeoutSec: z.number().positive().default(20),
|
|
566
|
-
// Seconds to wait for the very first stream part before giving up; slow
|
|
567
|
-
// thinking-model relays can buffer for minutes before sending anything.
|
|
568
|
-
streamFirstChunkTimeoutSec: z.number().positive().default(300),
|
|
569
|
-
// Extra attempts per model request when a stream fails transiently
|
|
570
|
-
// (network error, 429/5xx, idle watchdog cutoff) or comes back empty, so a
|
|
571
|
-
// relay hiccup does not silently end a half-finished turn. 0 disables.
|
|
572
|
-
// The wait between attempts honors Retry-After headers and otherwise backs
|
|
573
|
-
// off exponentially with jitter (llm/retry.ts); retries after a
|
|
574
|
-
// timeout/empty failure also scale both stream timeouts up (attempt number
|
|
575
|
-
// ×, capped at 3x) since the relay is likely overloaded.
|
|
576
|
-
streamMaxRetries: z.number().int().min(0).default(3),
|
|
577
|
-
// How many consecutive unproductive replies may be nudged before the turn
|
|
578
|
-
// is handed back: a text-only reply that announces pending work ("我将…",
|
|
579
|
-
// "I will…") or answers a nudge with more words, or an empty reply that
|
|
580
|
-
// survived the stream retry budget (steered with a nudge — a changed
|
|
581
|
-
// request — since identical resends already failed). Tool calls reset the
|
|
582
|
-
// count, so a productive task keeps going; exhaustion surfaces a notice
|
|
583
|
-
// instead of stopping silently. Never fires in plan mode. 0 disables.
|
|
584
|
-
maxAutoContinues: z.number().int().min(0).default(2),
|
|
585
|
-
contextCompaction: z.enum(["summary", "truncate"]).default("summary"),
|
|
586
|
-
// Optional per-session dollar budget; when set, the REPL stops the session
|
|
587
|
-
// once the estimated cost for the session reaches this amount.
|
|
588
|
-
sessionBudgetUsd: z.number().positive().optional(),
|
|
589
|
-
// Terminal bell (REPL only): ring when a turn takes longer than
|
|
590
|
-
// notifyBellThresholdSec, and when a background task finishes.
|
|
591
|
-
// STAR_NO_NOTIFY=1 disables without touching the config.
|
|
592
|
-
notifyBell: z.boolean().default(true),
|
|
593
|
-
notifyBellThresholdSec: z.number().positive().default(10),
|
|
594
|
-
permissions: PermissionsConfigSchema.default({ allow: [], deny: [] }),
|
|
595
|
-
hooks: z.array(HookConfigSchema).default([])
|
|
596
|
-
});
|
|
597
|
-
function contextWindowTokens(config, modelName) {
|
|
598
|
-
return config.models.find((m) => m.name === modelName)?.contextMaxTokens ?? config.contextMaxTokens;
|
|
601
|
+
await writeFileAtomic(filePath, upsertTopLevelKeyText(content, "permissionMode", mode));
|
|
599
602
|
}
|
|
600
603
|
|
|
601
604
|
// src/llm/provider.ts
|
|
@@ -674,10 +677,13 @@ function createSseNormalizingFetch(baseFetch = fetch) {
|
|
|
674
677
|
return response;
|
|
675
678
|
}
|
|
676
679
|
const body = response.body.pipeThrough(new TextDecoderStream()).pipeThrough(new SseNormalizeTransform()).pipeThrough(new TextEncoderStream());
|
|
680
|
+
const headers = new Headers(response.headers);
|
|
681
|
+
headers.delete("content-length");
|
|
682
|
+
headers.delete("content-encoding");
|
|
677
683
|
return new Response(body, {
|
|
678
684
|
status: response.status,
|
|
679
685
|
statusText: response.statusText,
|
|
680
|
-
headers
|
|
686
|
+
headers
|
|
681
687
|
});
|
|
682
688
|
};
|
|
683
689
|
}
|
|
@@ -715,7 +721,7 @@ function createModel(config, modelName) {
|
|
|
715
721
|
baseURL: provider.baseURL,
|
|
716
722
|
apiKey,
|
|
717
723
|
headers: provider.headers
|
|
718
|
-
})(modelConfig.model);
|
|
724
|
+
})(modelConfig.model, {}, { includeUsage: true });
|
|
719
725
|
case "openai-responses":
|
|
720
726
|
return createOpenAI({
|
|
721
727
|
baseURL: provider.baseURL,
|
|
@@ -738,6 +744,21 @@ function checkpointsDir(sessionDir) {
|
|
|
738
744
|
function indexPath(sessionDir) {
|
|
739
745
|
return path4.join(checkpointsDir(sessionDir), "index.json");
|
|
740
746
|
}
|
|
747
|
+
var indexWriteQueues = /* @__PURE__ */ new Map();
|
|
748
|
+
function enqueueIndexWrite(sessionDir, op) {
|
|
749
|
+
const queued = indexWriteQueues.get(sessionDir) ?? Promise.resolve();
|
|
750
|
+
const run = queued.then(op);
|
|
751
|
+
indexWriteQueues.set(
|
|
752
|
+
sessionDir,
|
|
753
|
+
run.then(
|
|
754
|
+
() => {
|
|
755
|
+
},
|
|
756
|
+
() => {
|
|
757
|
+
}
|
|
758
|
+
)
|
|
759
|
+
);
|
|
760
|
+
return run;
|
|
761
|
+
}
|
|
741
762
|
function checkpointContentPath(sessionDir, id) {
|
|
742
763
|
return path4.join(checkpointsDir(sessionDir), `${id}.snapshot`);
|
|
743
764
|
}
|
|
@@ -756,32 +777,36 @@ async function listCheckpointRecords(sessionDir) {
|
|
|
756
777
|
}
|
|
757
778
|
}
|
|
758
779
|
async function appendCheckpointRecord(sessionDir, record, content) {
|
|
759
|
-
await
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
780
|
+
await enqueueIndexWrite(sessionDir, async () => {
|
|
781
|
+
await fs2.mkdir(checkpointsDir(sessionDir), { recursive: true, mode: 448 });
|
|
782
|
+
if (record.existed && content !== null) {
|
|
783
|
+
await fs2.writeFile(checkpointContentPath(sessionDir, record.id), content, "utf8");
|
|
784
|
+
}
|
|
785
|
+
const records = await listCheckpointRecords(sessionDir);
|
|
786
|
+
records.push(record);
|
|
787
|
+
await fs2.writeFile(indexPath(sessionDir), JSON.stringify(records, null, 2));
|
|
788
|
+
});
|
|
766
789
|
}
|
|
767
790
|
async function removeCheckpointRecords(sessionDir, ids) {
|
|
768
791
|
if (ids.length === 0) return;
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
792
|
+
await enqueueIndexWrite(sessionDir, async () => {
|
|
793
|
+
const drop = new Set(ids);
|
|
794
|
+
const records = await listCheckpointRecords(sessionDir);
|
|
795
|
+
const kept = records.filter((record) => !drop.has(record.id));
|
|
796
|
+
if (kept.length === records.length) return;
|
|
797
|
+
await fs2.mkdir(checkpointsDir(sessionDir), { recursive: true, mode: 448 });
|
|
798
|
+
await fs2.writeFile(indexPath(sessionDir), JSON.stringify(kept, null, 2));
|
|
799
|
+
for (const id of ids) {
|
|
800
|
+
await fs2.rm(checkpointContentPath(sessionDir, id), { force: true });
|
|
801
|
+
}
|
|
802
|
+
});
|
|
778
803
|
}
|
|
779
804
|
async function loadSessionSnapshots(sessionDir) {
|
|
780
805
|
const records = await listCheckpointRecords(sessionDir);
|
|
781
806
|
return records.map((record) => ({
|
|
782
807
|
...record,
|
|
783
808
|
content: null,
|
|
784
|
-
contentFile: record.existed ? checkpointContentPath(sessionDir, record.id) : void 0
|
|
809
|
+
contentFile: record.existed && !record.contentTooLarge ? checkpointContentPath(sessionDir, record.id) : void 0
|
|
785
810
|
}));
|
|
786
811
|
}
|
|
787
812
|
|
|
@@ -802,6 +827,10 @@ function debugWarn(message) {
|
|
|
802
827
|
if (process.env.STAR_DEBUG === "1") process.stderr.write(`[star-cli] ${message}
|
|
803
828
|
`);
|
|
804
829
|
}
|
|
830
|
+
var FALLBACK_META = /* @__PURE__ */ Symbol("star.fallbackMeta");
|
|
831
|
+
function isFallbackMeta(meta) {
|
|
832
|
+
return meta[FALLBACK_META] === true;
|
|
833
|
+
}
|
|
805
834
|
function dayKey(date) {
|
|
806
835
|
const pad = (n) => String(n).padStart(2, "0");
|
|
807
836
|
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
|
|
@@ -811,6 +840,9 @@ var SessionStore = class _SessionStore {
|
|
|
811
840
|
dir;
|
|
812
841
|
pendingMeta = null;
|
|
813
842
|
initialized = false;
|
|
843
|
+
cachedMeta = null;
|
|
844
|
+
metaTmpSeq = 0;
|
|
845
|
+
metaWriteQueue = Promise.resolve();
|
|
814
846
|
constructor(id) {
|
|
815
847
|
this.id = id;
|
|
816
848
|
this.dir = path5.join(sessionsDir(), id);
|
|
@@ -840,6 +872,7 @@ var SessionStore = class _SessionStore {
|
|
|
840
872
|
} catch {
|
|
841
873
|
debugWarn(`session ${id}: corrupt meta.json, using defaults`);
|
|
842
874
|
}
|
|
875
|
+
await store.sweepTmpFiles();
|
|
843
876
|
store.initialized = true;
|
|
844
877
|
return store;
|
|
845
878
|
}
|
|
@@ -850,16 +883,40 @@ var SessionStore = class _SessionStore {
|
|
|
850
883
|
} catch {
|
|
851
884
|
return [];
|
|
852
885
|
}
|
|
853
|
-
const metas =
|
|
854
|
-
|
|
886
|
+
const metas = await Promise.all(
|
|
887
|
+
entries.map(async (entry) => {
|
|
888
|
+
try {
|
|
889
|
+
const raw = await fs3.readFile(path5.join(sessionsDir(), entry, "meta.json"), "utf8");
|
|
890
|
+
const meta = JSON.parse(raw);
|
|
891
|
+
if (meta.id === entry && (cwd === void 0 || meta.cwd === cwd)) return meta;
|
|
892
|
+
} catch {
|
|
893
|
+
}
|
|
894
|
+
return null;
|
|
895
|
+
})
|
|
896
|
+
);
|
|
897
|
+
return metas.filter((meta) => meta !== null).sort((a, b) => b.updatedAt - a.updatedAt);
|
|
898
|
+
}
|
|
899
|
+
// Meta writes are serialized per instance and land atomically (tmp file +
|
|
900
|
+
// rename) over a cached in-memory meta: the fire-and-forget title/usage
|
|
901
|
+
// writers race the loop's own appends, and a truncated read once turned
|
|
902
|
+
// fallbackMeta's empty defaults into a persisted wipe of model/cwd/usage.
|
|
903
|
+
// A branded fallback meta is never written back for the same reason.
|
|
904
|
+
writeMeta(meta) {
|
|
905
|
+
if (isFallbackMeta(meta)) return Promise.resolve();
|
|
906
|
+
const tmp = `${this.metaPath()}.tmp-${process.pid}-${this.metaTmpSeq++}`;
|
|
907
|
+
const run = this.metaWriteQueue.then(async () => {
|
|
855
908
|
try {
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
909
|
+
await fs3.writeFile(tmp, JSON.stringify(meta, null, 2));
|
|
910
|
+
await fs3.rename(tmp, this.metaPath());
|
|
911
|
+
} catch (error) {
|
|
912
|
+
await fs3.unlink(tmp).catch(() => {
|
|
913
|
+
});
|
|
914
|
+
throw error;
|
|
860
915
|
}
|
|
861
|
-
}
|
|
862
|
-
|
|
916
|
+
});
|
|
917
|
+
this.metaWriteQueue = run.catch(() => {
|
|
918
|
+
});
|
|
919
|
+
return run;
|
|
863
920
|
}
|
|
864
921
|
async ensureInitialized() {
|
|
865
922
|
if (this.initialized) return;
|
|
@@ -868,7 +925,8 @@ var SessionStore = class _SessionStore {
|
|
|
868
925
|
this.initialized = true;
|
|
869
926
|
return;
|
|
870
927
|
}
|
|
871
|
-
await fs3.mkdir(this.dir, { recursive: true });
|
|
928
|
+
await fs3.mkdir(this.dir, { recursive: true, mode: 448 });
|
|
929
|
+
await this.sweepTmpFiles();
|
|
872
930
|
const meta = {
|
|
873
931
|
id: this.id,
|
|
874
932
|
title: "",
|
|
@@ -877,16 +935,31 @@ var SessionStore = class _SessionStore {
|
|
|
877
935
|
createdAt: pending.createdAt,
|
|
878
936
|
updatedAt: pending.createdAt
|
|
879
937
|
};
|
|
880
|
-
await
|
|
938
|
+
await this.writeMeta(meta);
|
|
939
|
+
this.cachedMeta = meta;
|
|
881
940
|
this.initialized = true;
|
|
882
941
|
}
|
|
942
|
+
// Removes meta.json.tmp-* orphans left behind by a crashed meta write
|
|
943
|
+
// (tmp file + rename); best-effort, the directory may not even exist yet.
|
|
944
|
+
async sweepTmpFiles() {
|
|
945
|
+
let entries;
|
|
946
|
+
try {
|
|
947
|
+
entries = await fs3.readdir(this.dir);
|
|
948
|
+
} catch {
|
|
949
|
+
return;
|
|
950
|
+
}
|
|
951
|
+
await Promise.all(
|
|
952
|
+
entries.filter((entry) => entry.startsWith("meta.json.tmp-")).map((entry) => fs3.unlink(path5.join(this.dir, entry)).catch(() => {
|
|
953
|
+
}))
|
|
954
|
+
);
|
|
955
|
+
}
|
|
883
956
|
async append(message) {
|
|
884
957
|
await this.ensureInitialized();
|
|
885
958
|
await fs3.appendFile(this.messagesPath(), `${JSON.stringify(message)}
|
|
886
959
|
`);
|
|
887
960
|
const meta = await this.meta();
|
|
888
961
|
meta.updatedAt = Date.now();
|
|
889
|
-
await
|
|
962
|
+
await this.writeMeta(meta);
|
|
890
963
|
}
|
|
891
964
|
async replaceMessages(messages) {
|
|
892
965
|
await this.ensureInitialized();
|
|
@@ -895,7 +968,7 @@ var SessionStore = class _SessionStore {
|
|
|
895
968
|
` : "");
|
|
896
969
|
const meta = await this.meta();
|
|
897
970
|
meta.updatedAt = Date.now();
|
|
898
|
-
await
|
|
971
|
+
await this.writeMeta(meta);
|
|
899
972
|
}
|
|
900
973
|
async messages() {
|
|
901
974
|
let raw;
|
|
@@ -918,13 +991,24 @@ var SessionStore = class _SessionStore {
|
|
|
918
991
|
}
|
|
919
992
|
fallbackMeta() {
|
|
920
993
|
const now = Date.now();
|
|
921
|
-
|
|
994
|
+
const meta = {
|
|
995
|
+
id: this.id,
|
|
996
|
+
title: "",
|
|
997
|
+
model: "",
|
|
998
|
+
cwd: "",
|
|
999
|
+
createdAt: now,
|
|
1000
|
+
updatedAt: now
|
|
1001
|
+
};
|
|
1002
|
+
Object.defineProperty(meta, FALLBACK_META, { value: true, enumerable: false });
|
|
1003
|
+
return meta;
|
|
922
1004
|
}
|
|
923
1005
|
async meta() {
|
|
924
1006
|
await this.ensureInitialized();
|
|
1007
|
+
if (this.cachedMeta) return this.cachedMeta;
|
|
925
1008
|
try {
|
|
926
1009
|
const raw = await fs3.readFile(this.metaPath(), "utf8");
|
|
927
|
-
|
|
1010
|
+
this.cachedMeta = JSON.parse(raw);
|
|
1011
|
+
return this.cachedMeta;
|
|
928
1012
|
} catch {
|
|
929
1013
|
debugWarn(`session ${this.id}: unreadable meta.json, using defaults`);
|
|
930
1014
|
return this.fallbackMeta();
|
|
@@ -935,7 +1019,7 @@ var SessionStore = class _SessionStore {
|
|
|
935
1019
|
const meta = await this.meta();
|
|
936
1020
|
meta.title = title;
|
|
937
1021
|
meta.updatedAt = Date.now();
|
|
938
|
-
await
|
|
1022
|
+
await this.writeMeta(meta);
|
|
939
1023
|
}
|
|
940
1024
|
async appendCheckpoint(record, content) {
|
|
941
1025
|
await this.ensureInitialized();
|
|
@@ -960,6 +1044,12 @@ var SessionStore = class _SessionStore {
|
|
|
960
1044
|
usage.promptTokens += delta.promptTokens;
|
|
961
1045
|
usage.completionTokens += delta.completionTokens;
|
|
962
1046
|
usage.totalTokens += delta.totalTokens;
|
|
1047
|
+
if (delta.cachedPromptTokens) {
|
|
1048
|
+
usage.cachedPromptTokens = (usage.cachedPromptTokens ?? 0) + delta.cachedPromptTokens;
|
|
1049
|
+
}
|
|
1050
|
+
if (delta.cacheReadInputTokens) {
|
|
1051
|
+
usage.cacheReadInputTokens = (usage.cacheReadInputTokens ?? 0) + delta.cacheReadInputTokens;
|
|
1052
|
+
}
|
|
963
1053
|
meta.usage = usage;
|
|
964
1054
|
const day = dayKey(/* @__PURE__ */ new Date());
|
|
965
1055
|
const byDay = meta.usageByDay ?? {};
|
|
@@ -969,7 +1059,7 @@ var SessionStore = class _SessionStore {
|
|
|
969
1059
|
bucket.totalTokens += delta.totalTokens;
|
|
970
1060
|
byDay[day] = bucket;
|
|
971
1061
|
meta.usageByDay = byDay;
|
|
972
|
-
await
|
|
1062
|
+
await this.writeMeta(meta);
|
|
973
1063
|
}
|
|
974
1064
|
};
|
|
975
1065
|
|
|
@@ -1026,6 +1116,33 @@ async function findLatestSession(cwd) {
|
|
|
1026
1116
|
const [latest] = await SessionStore.list(cwd);
|
|
1027
1117
|
return latest ?? null;
|
|
1028
1118
|
}
|
|
1119
|
+
async function sessionPreview(id, maxBytes = 4096) {
|
|
1120
|
+
let handle = null;
|
|
1121
|
+
try {
|
|
1122
|
+
handle = await fs5.open(path7.join(sessionsDir(), id, "messages.jsonl"), "r");
|
|
1123
|
+
const buffer = Buffer.alloc(maxBytes);
|
|
1124
|
+
const { bytesRead } = await handle.read(buffer, 0, maxBytes, 0);
|
|
1125
|
+
const head = buffer.toString("utf8", 0, bytesRead);
|
|
1126
|
+
for (const line of head.split("\n")) {
|
|
1127
|
+
const trimmed = line.trim();
|
|
1128
|
+
if (!trimmed) continue;
|
|
1129
|
+
try {
|
|
1130
|
+
const message = JSON.parse(trimmed);
|
|
1131
|
+
if (message.role !== "user") continue;
|
|
1132
|
+
const text = typeof message.content === "string" ? message.content : message.content.filter((part) => part.type === "text").map((part) => part.type === "text" ? part.text : "").join(" ");
|
|
1133
|
+
const collapsed = text.replace(/\s+/g, " ").trim();
|
|
1134
|
+
if (collapsed) return collapsed;
|
|
1135
|
+
} catch {
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
return null;
|
|
1139
|
+
} catch {
|
|
1140
|
+
return null;
|
|
1141
|
+
} finally {
|
|
1142
|
+
await handle?.close().catch(() => {
|
|
1143
|
+
});
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1029
1146
|
function shortSessionId(id) {
|
|
1030
1147
|
const dash = id.lastIndexOf("-");
|
|
1031
1148
|
return dash === -1 ? id : id.slice(dash + 1);
|
|
@@ -1042,20 +1159,20 @@ async function resolveSessionId(query) {
|
|
|
1042
1159
|
function relativeTime(timestamp) {
|
|
1043
1160
|
const diff = Date.now() - timestamp;
|
|
1044
1161
|
const minutes = Math.floor(diff / 6e4);
|
|
1045
|
-
if (minutes < 1) return "
|
|
1046
|
-
if (minutes < 60) return `${minutes}
|
|
1162
|
+
if (minutes < 1) return "just now";
|
|
1163
|
+
if (minutes < 60) return `${minutes}m ago`;
|
|
1047
1164
|
const hours = Math.floor(minutes / 60);
|
|
1048
|
-
if (hours < 24) return `${hours}
|
|
1165
|
+
if (hours < 24) return `${hours}h ago`;
|
|
1049
1166
|
const days = Math.floor(hours / 24);
|
|
1050
|
-
if (days < 30) return `${days}
|
|
1167
|
+
if (days < 30) return `${days}d ago`;
|
|
1051
1168
|
const months = Math.floor(days / 30);
|
|
1052
|
-
if (months < 12) return `${months}
|
|
1053
|
-
return `${Math.floor(months / 12)}
|
|
1169
|
+
if (months < 12) return `${months}mo ago`;
|
|
1170
|
+
return `${Math.floor(months / 12)}y ago`;
|
|
1054
1171
|
}
|
|
1055
1172
|
function formatSessionEntries(entries, options = {}) {
|
|
1056
1173
|
return entries.map(({ meta, messageCount }) => {
|
|
1057
|
-
const title = meta.title || "(
|
|
1058
|
-
const base = `${shortSessionId(meta.id)} ${title} ${messageCount}
|
|
1174
|
+
const title = meta.title || "(untitled)";
|
|
1175
|
+
const base = `${shortSessionId(meta.id)} ${title} ${messageCount} messages updated ${relativeTime(meta.updatedAt)}`;
|
|
1059
1176
|
return options.showCwd ? `${base} [${meta.cwd}]` : base;
|
|
1060
1177
|
}).join("\n");
|
|
1061
1178
|
}
|
|
@@ -1086,7 +1203,9 @@ function computeCostUsd(usage, model) {
|
|
|
1086
1203
|
if (model?.promptPrice === void 0 || model.completionPrice === void 0) {
|
|
1087
1204
|
return null;
|
|
1088
1205
|
}
|
|
1089
|
-
|
|
1206
|
+
const cached = Math.min(Math.max(usage.cachedPromptTokens ?? 0, 0), usage.promptTokens);
|
|
1207
|
+
const promptUsd = (usage.promptTokens - cached) * model.promptPrice + cached * (model.cacheReadPrice ?? model.promptPrice) + (usage.cacheReadInputTokens ?? 0) * (model.cacheReadPrice ?? 0);
|
|
1208
|
+
return (promptUsd + usage.completionTokens * model.completionPrice) / 1e6;
|
|
1090
1209
|
}
|
|
1091
1210
|
function estimateCost(usage, modelName, model) {
|
|
1092
1211
|
const cost = computeCostUsd(usage, model);
|
|
@@ -1102,7 +1221,8 @@ function estimateCost(usage, modelName, model) {
|
|
|
1102
1221
|
}
|
|
1103
1222
|
return `Estimated cost: unknown (no price configured for ${modelName})`;
|
|
1104
1223
|
}
|
|
1105
|
-
|
|
1224
|
+
const cachePrice = model?.cacheReadPrice !== void 0 ? `, $${model.cacheReadPrice}/M cache read` : "";
|
|
1225
|
+
return `Estimated cost: $${formatDollars(cost)} (${model?.name} @ $${model?.promptPrice}/M prompt, $${model?.completionPrice}/M completion${cachePrice})`;
|
|
1106
1226
|
}
|
|
1107
1227
|
|
|
1108
1228
|
// src/session/usage.ts
|
|
@@ -1178,6 +1298,12 @@ function aggregateUsage(metas) {
|
|
|
1178
1298
|
entry.sessions += 1;
|
|
1179
1299
|
entry.requests += requestCount;
|
|
1180
1300
|
addTo(entry, totals);
|
|
1301
|
+
if (hasTotals && meta.usage) {
|
|
1302
|
+
const cached = tokens(meta.usage.cachedPromptTokens);
|
|
1303
|
+
const cacheRead = tokens(meta.usage.cacheReadInputTokens);
|
|
1304
|
+
if (cached > 0) entry.cachedPromptTokens = (entry.cachedPromptTokens ?? 0) + cached;
|
|
1305
|
+
if (cacheRead > 0) entry.cacheReadInputTokens = (entry.cacheReadInputTokens ?? 0) + cacheRead;
|
|
1306
|
+
}
|
|
1181
1307
|
models.set(name, entry);
|
|
1182
1308
|
}
|
|
1183
1309
|
return {
|
|
@@ -1232,7 +1358,7 @@ function formatUsageDashboard(stats, models, options = {}) {
|
|
|
1232
1358
|
const configured = new Map(models.map((m) => [m.name, m]));
|
|
1233
1359
|
const rows = stats.models.map((entry) => {
|
|
1234
1360
|
const cfg = configured.get(entry.model);
|
|
1235
|
-
const cost =
|
|
1361
|
+
const cost = computeCostUsd(entry, cfg);
|
|
1236
1362
|
return { entry, cost };
|
|
1237
1363
|
});
|
|
1238
1364
|
const header = ["model", "requests", "prompt", "completion", "total", "cost"];
|
|
@@ -1306,7 +1432,7 @@ function cacheHitPercent(totals) {
|
|
|
1306
1432
|
// src/cli/clipboard.ts
|
|
1307
1433
|
import { execFile as execFile2 } from "child_process";
|
|
1308
1434
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
1309
|
-
import { readFile as readFile3, unlink as unlink2 } from "fs/promises";
|
|
1435
|
+
import { readFile as readFile3, stat as stat2, unlink as unlink2 } from "fs/promises";
|
|
1310
1436
|
import os2 from "os";
|
|
1311
1437
|
import path9 from "path";
|
|
1312
1438
|
import { promisify as promisify2 } from "util";
|
|
@@ -1385,6 +1511,13 @@ async function finalizeClipboardImage(image) {
|
|
|
1385
1511
|
const result = await downsampleImageIfNeeded(image);
|
|
1386
1512
|
return { image: result.image, oversized: result.oversized && !result.resized };
|
|
1387
1513
|
}
|
|
1514
|
+
async function readClipboardTempFile(tempFile) {
|
|
1515
|
+
const st = await stat2(tempFile).catch(() => null);
|
|
1516
|
+
if (!st || st.size === 0 || st.size > MAX_CLIPBOARD_IMAGE_BYTES) return null;
|
|
1517
|
+
const buf = await readFile3(tempFile).catch(() => null);
|
|
1518
|
+
if (!buf || buf.length === 0) return null;
|
|
1519
|
+
return { path: "clipboard.png", mimeType: "image/png", data: buf.toString("base64") };
|
|
1520
|
+
}
|
|
1388
1521
|
async function readClipboardImage() {
|
|
1389
1522
|
const tempFile = path9.join(os2.tmpdir(), `star-clipboard-${randomUUID2()}.png`);
|
|
1390
1523
|
const hasPngpaste = process.platform === "darwin" ? await commandExists2("pngpaste") : false;
|
|
@@ -1397,22 +1530,18 @@ async function readClipboardImage() {
|
|
|
1397
1530
|
maxBuffer: MAX_CLIPBOARD_IMAGE_BYTES,
|
|
1398
1531
|
encoding: "buffer"
|
|
1399
1532
|
});
|
|
1400
|
-
const
|
|
1401
|
-
if (
|
|
1533
|
+
const buf = stdout;
|
|
1534
|
+
if (buf.length === 0) return null;
|
|
1402
1535
|
return finalizeClipboardImage({
|
|
1403
1536
|
path: "clipboard.png",
|
|
1404
1537
|
mimeType: "image/png",
|
|
1405
|
-
data:
|
|
1538
|
+
data: buf.toString("base64")
|
|
1406
1539
|
});
|
|
1407
1540
|
}
|
|
1408
1541
|
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
|
-
});
|
|
1542
|
+
const image = await readClipboardTempFile(tempFile);
|
|
1543
|
+
if (!image) return null;
|
|
1544
|
+
return finalizeClipboardImage(image);
|
|
1416
1545
|
} catch {
|
|
1417
1546
|
return null;
|
|
1418
1547
|
} finally {
|
|
@@ -1455,10 +1584,10 @@ async function compactSession(opts) {
|
|
|
1455
1584
|
};
|
|
1456
1585
|
}
|
|
1457
1586
|
const beforeTokens = estimateTokens(messages);
|
|
1458
|
-
const compacted = compactMessages(messages, config.contextMaxTokens);
|
|
1587
|
+
const compacted = compactMessages(messages, config.contextMaxTokens, { force: true });
|
|
1459
1588
|
if (!compacted.compacted) {
|
|
1460
1589
|
return {
|
|
1461
|
-
message: `Nothing to compact:
|
|
1590
|
+
message: `Nothing to compact: history is too short to drop a whole turn (~${beforeTokens} estimated tokens).`,
|
|
1462
1591
|
compacted: false
|
|
1463
1592
|
};
|
|
1464
1593
|
}
|
|
@@ -1772,7 +1901,7 @@ function applyEdit(content, oldString, newString, replaceAll) {
|
|
|
1772
1901
|
}
|
|
1773
1902
|
if (count === 0) return null;
|
|
1774
1903
|
if (count > 1 && !replaceAll) return null;
|
|
1775
|
-
return replaceAll ? content.split(oldString).join(newString) : content.replace(oldString, newString);
|
|
1904
|
+
return replaceAll ? content.split(oldString).join(newString) : content.replace(oldString, () => newString);
|
|
1776
1905
|
}
|
|
1777
1906
|
function newFilePreview(content, label) {
|
|
1778
1907
|
const all = content.split("\n");
|
|
@@ -2071,13 +2200,22 @@ ${content}`);
|
|
|
2071
2200
|
});
|
|
2072
2201
|
register({
|
|
2073
2202
|
name: "undo",
|
|
2074
|
-
description: "Undo the last conversation turn: revert its file changes (
|
|
2203
|
+
description: "Undo the last conversation turn: revert its file changes (any tool, bash included, when git snapshots are on) and retract its messages",
|
|
2075
2204
|
usage: "/undo",
|
|
2076
2205
|
category: "Changes",
|
|
2077
2206
|
async run(_args, ctx) {
|
|
2078
2207
|
ctx.addSystemMessage(await ctx.undo());
|
|
2079
2208
|
}
|
|
2080
2209
|
});
|
|
2210
|
+
register({
|
|
2211
|
+
name: "redo",
|
|
2212
|
+
description: "Redo the last git-snapshot undo: restore the working tree to its pre-undo state",
|
|
2213
|
+
usage: "/redo",
|
|
2214
|
+
category: "Changes",
|
|
2215
|
+
async run(_args, ctx) {
|
|
2216
|
+
ctx.addSystemMessage(await ctx.redo());
|
|
2217
|
+
}
|
|
2218
|
+
});
|
|
2081
2219
|
register({
|
|
2082
2220
|
name: "rewind",
|
|
2083
2221
|
description: "List file-change checkpoints, or rewind to just before one: restore files and retract the conversation",
|
|
@@ -2349,19 +2487,19 @@ function maskApiKey(key) {
|
|
|
2349
2487
|
if (key.length <= 8) return "\u2022\u2022\u2022\u2022\u2022\u2022";
|
|
2350
2488
|
return `${key.slice(0, 3)}\u2026${key.slice(-4)}`;
|
|
2351
2489
|
}
|
|
2352
|
-
function
|
|
2490
|
+
function tomlString2(value) {
|
|
2353
2491
|
return JSON.stringify(value);
|
|
2354
2492
|
}
|
|
2355
2493
|
function buildProviderTomlBlock(provider) {
|
|
2356
2494
|
return [
|
|
2357
2495
|
"[[providers]]",
|
|
2358
|
-
`name = ${
|
|
2496
|
+
`name = ${tomlString2(provider.name)}`,
|
|
2359
2497
|
"# wire format: openai-compatible | anthropic | openai-responses",
|
|
2360
|
-
`protocol = ${
|
|
2361
|
-
`baseURL = ${
|
|
2498
|
+
`protocol = ${tomlString2(provider.protocol)}`,
|
|
2499
|
+
`baseURL = ${tomlString2(provider.baseURL)}`,
|
|
2362
2500
|
"# environment variable holding the API key; the key itself lives in",
|
|
2363
2501
|
"# ~/.star-cli/.env (written by /connect), never in this file",
|
|
2364
|
-
`apiKeyEnv = ${
|
|
2502
|
+
`apiKeyEnv = ${tomlString2(provider.apiKeyEnv ?? "")}`,
|
|
2365
2503
|
"# inline key instead of apiKeyEnv (not recommended):",
|
|
2366
2504
|
'# apiKey = "sk-..."',
|
|
2367
2505
|
"# extra HTTP headers sent to the provider:",
|
|
@@ -2373,9 +2511,9 @@ function buildProviderTomlBlock(provider) {
|
|
|
2373
2511
|
function buildModelTomlBlock(model) {
|
|
2374
2512
|
return [
|
|
2375
2513
|
"[[models]]",
|
|
2376
|
-
`name = ${
|
|
2377
|
-
`provider = ${
|
|
2378
|
-
`model = ${
|
|
2514
|
+
`name = ${tomlString2(model.name)}`,
|
|
2515
|
+
`provider = ${tomlString2(model.provider)}`,
|
|
2516
|
+
`model = ${tomlString2(model.model)}`,
|
|
2379
2517
|
"# per-model context window in tokens \u2014 overrides the top-level",
|
|
2380
2518
|
"# contextMaxTokens for compaction and the ctx % in the status bar",
|
|
2381
2519
|
`contextMaxTokens = ${model.contextMaxTokens ?? CONNECT_DEFAULT_CONTEXT_TOKENS}`,
|
|
@@ -2443,7 +2581,7 @@ function saveConnection(answers, configPath = globalConfigPath(), envPath = envF
|
|
|
2443
2581
|
return { provider, model, configPath, envPath };
|
|
2444
2582
|
}
|
|
2445
2583
|
function setDefaultModelInToml(content, modelName) {
|
|
2446
|
-
const line = `defaultModel = ${
|
|
2584
|
+
const line = `defaultModel = ${tomlString2(modelName)}`;
|
|
2447
2585
|
const lines = content.split("\n");
|
|
2448
2586
|
let inTopLevel = true;
|
|
2449
2587
|
for (let i = 0; i < lines.length; i++) {
|
|
@@ -2883,6 +3021,20 @@ async function initProject(opts) {
|
|
|
2883
3021
|
};
|
|
2884
3022
|
}
|
|
2885
3023
|
|
|
3024
|
+
// src/cli/commands/redo.ts
|
|
3025
|
+
function formatRedoSummary(entry) {
|
|
3026
|
+
const label = entry.label !== "" ? ` of "${entry.label}"` : "";
|
|
3027
|
+
return `Restore the working tree to its state before the undo${label} (tree ${entry.tree.slice(0, 8)}). Conversation messages are not restored.`;
|
|
3028
|
+
}
|
|
3029
|
+
function formatRedoResult(result) {
|
|
3030
|
+
if (!result.ok) {
|
|
3031
|
+
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.`;
|
|
3032
|
+
}
|
|
3033
|
+
const files = result.files === null ? "unknown number of" : `${result.files}`;
|
|
3034
|
+
const label = result.label !== "" ? ` \u2014 "${result.label}"` : "";
|
|
3035
|
+
return `Redone: restored ${files} file(s) to the pre-undo state (tree ${result.tree.slice(0, 8)})${label}.`;
|
|
3036
|
+
}
|
|
3037
|
+
|
|
2886
3038
|
// src/cli/commands/registry.ts
|
|
2887
3039
|
var CommandRegistry = class {
|
|
2888
3040
|
commands = /* @__PURE__ */ new Map();
|
|
@@ -2998,6 +3150,16 @@ async function buildUndoDiffs(snapshots, cwd) {
|
|
|
2998
3150
|
}
|
|
2999
3151
|
return diffs;
|
|
3000
3152
|
}
|
|
3153
|
+
async function buildUndoTreeDiffs(cwd, tree) {
|
|
3154
|
+
const names = await diffTreeNames(cwd, tree);
|
|
3155
|
+
if (!names || names.length === 0) return [];
|
|
3156
|
+
return [
|
|
3157
|
+
{
|
|
3158
|
+
label: "Working-tree files restoring the turn's start will touch:",
|
|
3159
|
+
lines: names.map((name) => ({ kind: "marker", text: name }))
|
|
3160
|
+
}
|
|
3161
|
+
];
|
|
3162
|
+
}
|
|
3001
3163
|
|
|
3002
3164
|
// src/cli/components/ConnectWizard.tsx
|
|
3003
3165
|
import { Box as Box4, Text as Text4, useInput as useInput4 } from "ink";
|
|
@@ -3932,18 +4094,19 @@ import { memo } from "react";
|
|
|
3932
4094
|
import { Text as Text6 } from "ink";
|
|
3933
4095
|
import { Fragment, jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
3934
4096
|
function DiffLineView({ line }) {
|
|
4097
|
+
const text = toTerminalSafe(line.text);
|
|
3935
4098
|
if (line.kind === "add") return /* @__PURE__ */ jsxs6(Text6, { color: "green", children: [
|
|
3936
4099
|
"+ ",
|
|
3937
|
-
|
|
4100
|
+
text
|
|
3938
4101
|
] });
|
|
3939
4102
|
if (line.kind === "del") return /* @__PURE__ */ jsxs6(Text6, { color: "red", children: [
|
|
3940
4103
|
"- ",
|
|
3941
|
-
|
|
4104
|
+
text
|
|
3942
4105
|
] });
|
|
3943
|
-
if (line.kind === "marker") return /* @__PURE__ */ jsx6(Text6, { dimColor: true, children:
|
|
4106
|
+
if (line.kind === "marker") return /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: text });
|
|
3944
4107
|
return /* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
|
|
3945
4108
|
" ",
|
|
3946
|
-
|
|
4109
|
+
text
|
|
3947
4110
|
] });
|
|
3948
4111
|
}
|
|
3949
4112
|
function DiffLines({ lines }) {
|
|
@@ -3987,6 +4150,17 @@ var MessageList = memo(function MessageList2({ messages }) {
|
|
|
3987
4150
|
// src/cli/components/PermissionPrompt.tsx
|
|
3988
4151
|
import { Box as Box7, Text as Text8, useInput as useInput6 } from "ink";
|
|
3989
4152
|
import { jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
|
|
4153
|
+
var BASH_COMMAND_MAX = 1e3;
|
|
4154
|
+
function middleTruncate(text, max) {
|
|
4155
|
+
if (text.length <= max) return text;
|
|
4156
|
+
const half = Math.floor((max - 1) / 2);
|
|
4157
|
+
return `${text.slice(0, half)}\u2026${text.slice(text.length - half)}`;
|
|
4158
|
+
}
|
|
4159
|
+
function bashCommand(args) {
|
|
4160
|
+
if (typeof args !== "object" || args === null) return null;
|
|
4161
|
+
const command = args.command;
|
|
4162
|
+
return typeof command === "string" ? command : null;
|
|
4163
|
+
}
|
|
3990
4164
|
function PermissionPrompt({ request, preview, onDecision }) {
|
|
3991
4165
|
useInput6((input) => {
|
|
3992
4166
|
const ch = input.toLowerCase();
|
|
@@ -3994,6 +4168,7 @@ function PermissionPrompt({ request, preview, onDecision }) {
|
|
|
3994
4168
|
else if (ch === "n") onDecision("no");
|
|
3995
4169
|
else if (ch === "a") onDecision("always");
|
|
3996
4170
|
});
|
|
4171
|
+
const command = request.toolName === "bash" ? bashCommand(request.args) : null;
|
|
3997
4172
|
return /* @__PURE__ */ jsxs8(Box7, { flexDirection: "column", borderStyle: "round", borderColor: "yellow", paddingX: 1, children: [
|
|
3998
4173
|
/* @__PURE__ */ jsxs8(Text8, { bold: true, color: "yellow", children: [
|
|
3999
4174
|
"Permission required: ",
|
|
@@ -4005,10 +4180,10 @@ function PermissionPrompt({ request, preview, onDecision }) {
|
|
|
4005
4180
|
preview ? /* @__PURE__ */ jsxs8(Box7, { flexDirection: "column", children: [
|
|
4006
4181
|
/* @__PURE__ */ jsxs8(Text8, { dimColor: true, children: [
|
|
4007
4182
|
preview.type === "new-file" ? "New file: " : "",
|
|
4008
|
-
preview.label
|
|
4183
|
+
toTerminalSafe(preview.label)
|
|
4009
4184
|
] }),
|
|
4010
4185
|
/* @__PURE__ */ jsx8(DiffLines, { lines: preview.lines })
|
|
4011
|
-
] }) : /* @__PURE__ */ jsx8(Text8, { children: summarizeArgs(request.args) }),
|
|
4186
|
+
] }) : command !== null ? /* @__PURE__ */ jsx8(Text8, { children: toTerminalSafe(middleTruncate(command, BASH_COMMAND_MAX)) }) : /* @__PURE__ */ jsx8(Text8, { children: toTerminalSafe(summarizeArgs(request.args)) }),
|
|
4012
4187
|
/* @__PURE__ */ jsx8(Text8, { children: "[y] allow [n] deny [a] always (saved to config)" })
|
|
4013
4188
|
] });
|
|
4014
4189
|
}
|
|
@@ -4067,7 +4242,7 @@ var StatusBar = memo2(function StatusBar2({
|
|
|
4067
4242
|
}) {
|
|
4068
4243
|
const { stdout } = useStdout2();
|
|
4069
4244
|
const wide = (stdout?.columns ?? 80) >= WIDE_COLUMNS;
|
|
4070
|
-
const joined = backgroundTasks.join(", ");
|
|
4245
|
+
const joined = backgroundTasks.map((label) => toTerminalSafe(label)).join(", ");
|
|
4071
4246
|
const labels = joined.length > BG_LABEL_MAX ? `${joined.slice(0, BG_LABEL_MAX)}\u2026` : joined;
|
|
4072
4247
|
return /* @__PURE__ */ jsxs10(Box9, { justifyContent: "space-between", children: [
|
|
4073
4248
|
/* @__PURE__ */ jsxs10(Text10, { dimColor: true, children: [
|
|
@@ -4156,17 +4331,6 @@ function ThinkingIndicator({
|
|
|
4156
4331
|
|
|
4157
4332
|
// src/cli/components/TodoPanel.tsx
|
|
4158
4333
|
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
4334
|
import { jsxs as jsxs13 } from "react/jsx-runtime";
|
|
4171
4335
|
var SYMBOLS = {
|
|
4172
4336
|
pending: "[ ]",
|
|
@@ -4356,6 +4520,18 @@ async function executeShellBang(raw, cwd, signal) {
|
|
|
4356
4520
|
};
|
|
4357
4521
|
}
|
|
4358
4522
|
|
|
4523
|
+
// src/cli/system-prompt.ts
|
|
4524
|
+
var SYSTEM_PROMPT = `You are Star CLI, an AI coding agent running in the user's terminal.
|
|
4525
|
+
You help with software engineering tasks: reading, writing and editing code, running shell commands, and managing todos.
|
|
4526
|
+
Be concise and direct. Use tools when they help accomplish the task.
|
|
4527
|
+
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.
|
|
4528
|
+
The working directory is the user's project root; never touch files outside it without explicit instruction.
|
|
4529
|
+
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.
|
|
4530
|
+
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.
|
|
4531
|
+
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.
|
|
4532
|
+
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.
|
|
4533
|
+
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.`;
|
|
4534
|
+
|
|
4359
4535
|
// src/cli/ticker.ts
|
|
4360
4536
|
var TICK_MS = 100;
|
|
4361
4537
|
function nextFlush(prev, next) {
|
|
@@ -4414,20 +4590,25 @@ function runningTaskLabels() {
|
|
|
4414
4590
|
return [...shells, ...agents];
|
|
4415
4591
|
}
|
|
4416
4592
|
var SESSION_MODE_CYCLE = ["ask", "auto", "readonly", "plan"];
|
|
4593
|
+
var BUSY_BLOCKED_COMMANDS = /* @__PURE__ */ new Set([
|
|
4594
|
+
"new",
|
|
4595
|
+
"resume",
|
|
4596
|
+
"undo",
|
|
4597
|
+
"redo",
|
|
4598
|
+
"rewind",
|
|
4599
|
+
"compact",
|
|
4600
|
+
"fork",
|
|
4601
|
+
"clear-sessions",
|
|
4602
|
+
"model",
|
|
4603
|
+
"connect",
|
|
4604
|
+
"memory"
|
|
4605
|
+
]);
|
|
4417
4606
|
function formatUsage(usage) {
|
|
4418
4607
|
return `API usage this session: ${usage.requests} requests, ${usage.promptTokens} prompt + ${usage.completionTokens} completion = ${usage.totalTokens} tokens`;
|
|
4419
4608
|
}
|
|
4420
4609
|
var SESSION_PICKER_CAP = 20;
|
|
4610
|
+
var SESSION_PREVIEW_MAX = 60;
|
|
4421
4611
|
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
4612
|
function Repl({
|
|
4432
4613
|
backend,
|
|
4433
4614
|
model,
|
|
@@ -4463,12 +4644,14 @@ function Repl({
|
|
|
4463
4644
|
const nextIdRef = useRef5(initialDisplay.length);
|
|
4464
4645
|
const abortRef = useRef5(null);
|
|
4465
4646
|
const streamedRef = useRef5("");
|
|
4647
|
+
const lastCommittedLenRef = useRef5(0);
|
|
4466
4648
|
const thinkingRef = useRef5(false);
|
|
4467
4649
|
const reasoningRef = useRef5("");
|
|
4468
4650
|
const tickerStopRef = useRef5(null);
|
|
4469
4651
|
const flushedRef = useRef5({ streamed: "", reasoning: "" });
|
|
4470
4652
|
const toolCardsRef = useRef5(/* @__PURE__ */ new Map());
|
|
4471
4653
|
const pendingRef = useRef5(null);
|
|
4654
|
+
const permissionChainRef = useRef5(Promise.resolve());
|
|
4472
4655
|
const alwaysAllowedRef = useRef5(/* @__PURE__ */ new Set());
|
|
4473
4656
|
const prevModeRef = useRef5(null);
|
|
4474
4657
|
const planApprovalRef = useRef5(false);
|
|
@@ -4477,6 +4660,10 @@ function Repl({
|
|
|
4477
4660
|
const pendingConnectRef = useRef5(null);
|
|
4478
4661
|
const modelNameRef = useRef5(model);
|
|
4479
4662
|
const sessionStoreRef = useRef5(sessionStore);
|
|
4663
|
+
const configRef = useRef5(config);
|
|
4664
|
+
useEffect2(() => {
|
|
4665
|
+
configRef.current = config;
|
|
4666
|
+
}, [config]);
|
|
4480
4667
|
const usageRef = useRef5(initialUsage ? { ...initialUsage } : emptyUsage());
|
|
4481
4668
|
const turnChunksRef = useRef5(0);
|
|
4482
4669
|
const messagesRef = useRef5(initialDisplay);
|
|
@@ -4652,14 +4839,22 @@ function Repl({
|
|
|
4652
4839
|
);
|
|
4653
4840
|
const attachConfirmHandler = useCallback(
|
|
4654
4841
|
(target) => {
|
|
4655
|
-
target.confirmHandler =
|
|
4842
|
+
target.confirmHandler = (req) => {
|
|
4656
4843
|
if (isAllowedByRules([...alwaysAllowedRef.current], req)) {
|
|
4657
|
-
return true;
|
|
4844
|
+
return Promise.resolve(true);
|
|
4658
4845
|
}
|
|
4659
|
-
const
|
|
4660
|
-
|
|
4661
|
-
|
|
4662
|
-
|
|
4846
|
+
const ask = async () => {
|
|
4847
|
+
const preview = await generateDiffPreview(req.toolName, req.args, cwd).catch(() => null);
|
|
4848
|
+
return new Promise((resolve) => {
|
|
4849
|
+
setPendingPermission({ request: req, preview, resolve });
|
|
4850
|
+
});
|
|
4851
|
+
};
|
|
4852
|
+
const decision = permissionChainRef.current.then(ask);
|
|
4853
|
+
permissionChainRef.current = decision.then(
|
|
4854
|
+
() => void 0,
|
|
4855
|
+
() => void 0
|
|
4856
|
+
);
|
|
4857
|
+
return decision;
|
|
4663
4858
|
};
|
|
4664
4859
|
target.onHookWarning = (message) => pushMessage("system", message);
|
|
4665
4860
|
},
|
|
@@ -4690,27 +4885,25 @@ function Repl({
|
|
|
4690
4885
|
setBgLabels(runningTaskLabels());
|
|
4691
4886
|
pushMessage(
|
|
4692
4887
|
"system",
|
|
4693
|
-
|
|
4888
|
+
toTerminalSafe(
|
|
4889
|
+
task.status === "running" ? formatTaskStarted(task) : formatTaskFinished(task)
|
|
4890
|
+
)
|
|
4694
4891
|
);
|
|
4695
4892
|
if (task.status !== "running") {
|
|
4696
4893
|
notifyBell({
|
|
4697
|
-
enabled:
|
|
4698
|
-
thresholdSec:
|
|
4894
|
+
enabled: configRef.current.notifyBell,
|
|
4895
|
+
thresholdSec: configRef.current.notifyBellThresholdSec,
|
|
4699
4896
|
noNotifyEnv: process.env.STAR_NO_NOTIFY === "1"
|
|
4700
4897
|
});
|
|
4701
4898
|
}
|
|
4702
4899
|
};
|
|
4703
4900
|
const onAgentUpdate = (task) => {
|
|
4704
4901
|
setBgLabels(runningTaskLabels());
|
|
4705
|
-
|
|
4706
|
-
pushMessage(
|
|
4707
|
-
"system",
|
|
4708
|
-
task.status === "running" ? `Background subagent ${task.id} started${label}` : `Background subagent ${task.id} ${task.status}${label}`
|
|
4709
|
-
);
|
|
4902
|
+
pushMessage("system", toTerminalSafe(formatAgentTaskUpdate(task)));
|
|
4710
4903
|
if (task.status !== "running") {
|
|
4711
4904
|
notifyBell({
|
|
4712
|
-
enabled:
|
|
4713
|
-
thresholdSec:
|
|
4905
|
+
enabled: configRef.current.notifyBell,
|
|
4906
|
+
thresholdSec: configRef.current.notifyBellThresholdSec,
|
|
4714
4907
|
noNotifyEnv: process.env.STAR_NO_NOTIFY === "1"
|
|
4715
4908
|
});
|
|
4716
4909
|
}
|
|
@@ -4723,7 +4916,7 @@ function Repl({
|
|
|
4723
4916
|
defaultTaskManager.cleanup();
|
|
4724
4917
|
defaultAgentTasks.cleanup();
|
|
4725
4918
|
};
|
|
4726
|
-
}, [pushMessage
|
|
4919
|
+
}, [pushMessage]);
|
|
4727
4920
|
const interrupt = useCallback(() => {
|
|
4728
4921
|
abortRef.current?.abort();
|
|
4729
4922
|
const p = pendingRef.current;
|
|
@@ -4762,9 +4955,11 @@ function Repl({
|
|
|
4762
4955
|
const handleExit = useCallback(() => {
|
|
4763
4956
|
const killed = defaultTaskManager.cleanup();
|
|
4764
4957
|
const killedAgents = defaultAgentTasks.cleanup();
|
|
4765
|
-
const stopped =
|
|
4766
|
-
if (stopped
|
|
4767
|
-
pushMessage("system",
|
|
4958
|
+
const stopped = killed.length + killedAgents.length;
|
|
4959
|
+
if (stopped > 0) {
|
|
4960
|
+
pushMessage("system", `${stopped} background task(s) still running; reports will be lost.`);
|
|
4961
|
+
setTimeout(exit, 50);
|
|
4962
|
+
return;
|
|
4768
4963
|
}
|
|
4769
4964
|
exit();
|
|
4770
4965
|
}, [exit, pushMessage]);
|
|
@@ -4825,6 +5020,7 @@ function Repl({
|
|
|
4825
5020
|
registry: createDefaultRegistry(),
|
|
4826
5021
|
config,
|
|
4827
5022
|
cwd,
|
|
5023
|
+
system: SYSTEM_PROMPT,
|
|
4828
5024
|
sessionStore: sessionStoreRef.current,
|
|
4829
5025
|
contextMaxTokens: contextWindowTokens(config, name)
|
|
4830
5026
|
});
|
|
@@ -4866,12 +5062,15 @@ function Repl({
|
|
|
4866
5062
|
if (!resumed) {
|
|
4867
5063
|
return `Session not found: ${id}`;
|
|
4868
5064
|
}
|
|
4869
|
-
await current.loadMessages(resumed.messages);
|
|
4870
|
-
setTodos(await loadTodos(cwd));
|
|
4871
5065
|
const store = await SessionStore.open(resolvedId);
|
|
4872
|
-
if (store) {
|
|
4873
|
-
|
|
5066
|
+
if (!store) {
|
|
5067
|
+
return `Session not found: ${id}`;
|
|
4874
5068
|
}
|
|
5069
|
+
await current.loadMessages(resumed.messages);
|
|
5070
|
+
current.setSessionStore(store);
|
|
5071
|
+
sessionStoreRef.current = store;
|
|
5072
|
+
setTodos(await loadTodos(cwd));
|
|
5073
|
+
hydrateSnapshots(await loadSessionSnapshots(store.dir));
|
|
4875
5074
|
const display = buildDisplayMessages(resumed.messages);
|
|
4876
5075
|
nextIdRef.current = display.length;
|
|
4877
5076
|
applyMessages(display);
|
|
@@ -4897,6 +5096,7 @@ function Repl({
|
|
|
4897
5096
|
abortRef.current = controller;
|
|
4898
5097
|
const turnStartedAt = Date.now();
|
|
4899
5098
|
streamedRef.current = "";
|
|
5099
|
+
lastCommittedLenRef.current = 0;
|
|
4900
5100
|
thinkingRef.current = true;
|
|
4901
5101
|
reasoningRef.current = "";
|
|
4902
5102
|
turnChunksRef.current = 0;
|
|
@@ -4907,22 +5107,25 @@ function Repl({
|
|
|
4907
5107
|
setIsStreaming(true);
|
|
4908
5108
|
flushedRef.current = { streamed: "", reasoning: "" };
|
|
4909
5109
|
const commitStreamed = (tight) => {
|
|
4910
|
-
|
|
4911
|
-
|
|
5110
|
+
const uncommitted = streamedRef.current.slice(lastCommittedLenRef.current);
|
|
5111
|
+
if (uncommitted.length > 0) {
|
|
5112
|
+
pushAssistantChunk(uncommitted, tight);
|
|
5113
|
+
}
|
|
4912
5114
|
streamedRef.current = "";
|
|
5115
|
+
lastCommittedLenRef.current = 0;
|
|
4913
5116
|
flushedRef.current = { ...flushedRef.current, streamed: "" };
|
|
4914
5117
|
setStreamingText("");
|
|
4915
5118
|
};
|
|
4916
5119
|
tickerStopRef.current = startTicker((tick) => {
|
|
4917
5120
|
setSpinnerTick(tick);
|
|
4918
|
-
const
|
|
5121
|
+
const uncommitted = streamedRef.current.slice(lastCommittedLenRef.current);
|
|
5122
|
+
const split = splitCommittableLines(uncommitted, 8);
|
|
4919
5123
|
if (split) {
|
|
4920
5124
|
pushAssistantChunk(split.committed, true);
|
|
4921
|
-
|
|
4922
|
-
flushedRef.current = { ...flushedRef.current, streamed: "" };
|
|
5125
|
+
lastCommittedLenRef.current += split.committed.length + 1;
|
|
4923
5126
|
}
|
|
4924
5127
|
const next = {
|
|
4925
|
-
streamed: streamedRef.current,
|
|
5128
|
+
streamed: streamedRef.current.slice(lastCommittedLenRef.current),
|
|
4926
5129
|
reasoning: reasoningRef.current
|
|
4927
5130
|
};
|
|
4928
5131
|
if (nextFlush(flushedRef.current, next) !== null) {
|
|
@@ -4991,6 +5194,12 @@ function Repl({
|
|
|
4991
5194
|
usage.promptTokens += event.usage.promptTokens;
|
|
4992
5195
|
usage.completionTokens += event.usage.completionTokens;
|
|
4993
5196
|
usage.totalTokens += event.usage.totalTokens;
|
|
5197
|
+
if (event.usage.cachedPromptTokens) {
|
|
5198
|
+
usage.cachedPromptTokens = (usage.cachedPromptTokens ?? 0) + event.usage.cachedPromptTokens;
|
|
5199
|
+
}
|
|
5200
|
+
if (event.usage.cacheReadInputTokens) {
|
|
5201
|
+
usage.cacheReadInputTokens = (usage.cacheReadInputTokens ?? 0) + event.usage.cacheReadInputTokens;
|
|
5202
|
+
}
|
|
4994
5203
|
if (cacheUsageReported(event.usage)) {
|
|
4995
5204
|
if (!usage.cache) usage.cache = { cachedTokens: 0, promptTokens: 0 };
|
|
4996
5205
|
addToCacheTotals(usage.cache, event.usage);
|
|
@@ -5000,6 +5209,9 @@ function Repl({
|
|
|
5000
5209
|
});
|
|
5001
5210
|
}
|
|
5002
5211
|
} else if (event.type === "retry") {
|
|
5212
|
+
streamedRef.current = streamedRef.current.slice(0, lastCommittedLenRef.current);
|
|
5213
|
+
flushedRef.current = { ...flushedRef.current, streamed: "" };
|
|
5214
|
+
setStreamingText("");
|
|
5003
5215
|
const wait = event.delayMs !== void 0 && event.delayMs >= 1e3 ? ` in ${Math.round(event.delayMs / 1e3)}s` : "";
|
|
5004
5216
|
pushMessage(
|
|
5005
5217
|
"system",
|
|
@@ -5030,8 +5242,9 @@ function Repl({
|
|
|
5030
5242
|
setThinkingText("");
|
|
5031
5243
|
setThoughtSummary(null);
|
|
5032
5244
|
setActivity(null);
|
|
5033
|
-
const finalText = streamedRef.current;
|
|
5245
|
+
const finalText = streamedRef.current.slice(lastCommittedLenRef.current);
|
|
5034
5246
|
streamedRef.current = "";
|
|
5247
|
+
lastCommittedLenRef.current = 0;
|
|
5035
5248
|
setStreamingText(null);
|
|
5036
5249
|
const interrupted = controller.signal.aborted;
|
|
5037
5250
|
notifyBell({
|
|
@@ -5214,10 +5427,8 @@ ${lines.join("\n")}`;
|
|
|
5214
5427
|
return ctx.permissionMode(picked);
|
|
5215
5428
|
},
|
|
5216
5429
|
pickSession: async (all) => {
|
|
5217
|
-
const
|
|
5218
|
-
|
|
5219
|
-
SESSION_PICKER_CAP
|
|
5220
|
-
);
|
|
5430
|
+
const currentId = sessionStoreRef.current?.id;
|
|
5431
|
+
const entries = (await listSessionEntries(all ? void 0 : cwd)).filter((entry) => entry.meta.id !== currentId).slice(0, SESSION_PICKER_CAP);
|
|
5221
5432
|
if (entries.length === 0) {
|
|
5222
5433
|
return all ? "No sessions found." : "No sessions found for this directory.";
|
|
5223
5434
|
}
|
|
@@ -5225,13 +5436,16 @@ ${lines.join("\n")}`;
|
|
|
5225
5436
|
entries.map(async ({ meta, messageCount }) => {
|
|
5226
5437
|
const parts = [`${messageCount} messages \xB7 ${relativeTime(meta.updatedAt)}`];
|
|
5227
5438
|
const preview = await sessionPreview(meta.id);
|
|
5228
|
-
if (preview)
|
|
5439
|
+
if (preview) {
|
|
5440
|
+
parts.push(
|
|
5441
|
+
preview.length > SESSION_PREVIEW_MAX ? `${preview.slice(0, SESSION_PREVIEW_MAX)}\u2026` : preview
|
|
5442
|
+
);
|
|
5443
|
+
}
|
|
5229
5444
|
if (all) parts.push(`[${meta.cwd}]`);
|
|
5230
5445
|
return {
|
|
5231
5446
|
value: meta.id,
|
|
5232
5447
|
label: meta.title || shortSessionId(meta.id),
|
|
5233
|
-
description: parts.join(" \xB7 ")
|
|
5234
|
-
hint: meta.id === sessionStoreRef.current?.id ? "current" : void 0
|
|
5448
|
+
description: parts.join(" \xB7 ")
|
|
5235
5449
|
};
|
|
5236
5450
|
})
|
|
5237
5451
|
);
|
|
@@ -5316,18 +5530,26 @@ ${estimateCost(usageRef.current, modelNameRef.current, modelConfig)}`;
|
|
|
5316
5530
|
} catch {
|
|
5317
5531
|
summaryModel = null;
|
|
5318
5532
|
}
|
|
5319
|
-
|
|
5320
|
-
|
|
5321
|
-
|
|
5322
|
-
|
|
5323
|
-
|
|
5324
|
-
|
|
5325
|
-
|
|
5326
|
-
|
|
5327
|
-
|
|
5328
|
-
|
|
5533
|
+
tickerStopRef.current = startTicker((tick) => setSpinnerTick(tick));
|
|
5534
|
+
setActivity("compacting context\u2026");
|
|
5535
|
+
try {
|
|
5536
|
+
const result = await compactSession({
|
|
5537
|
+
backend: backendRef.current,
|
|
5538
|
+
sessionStore: sessionStoreRef.current,
|
|
5539
|
+
config,
|
|
5540
|
+
model: summaryModel
|
|
5541
|
+
});
|
|
5542
|
+
if (result.compacted && result.messages) {
|
|
5543
|
+
const display = buildDisplayMessages(result.messages);
|
|
5544
|
+
nextIdRef.current = display.length;
|
|
5545
|
+
applyMessages(display);
|
|
5546
|
+
}
|
|
5547
|
+
return result.message;
|
|
5548
|
+
} finally {
|
|
5549
|
+
tickerStopRef.current?.();
|
|
5550
|
+
tickerStopRef.current = null;
|
|
5551
|
+
setActivity(null);
|
|
5329
5552
|
}
|
|
5330
|
-
return result.message;
|
|
5331
5553
|
},
|
|
5332
5554
|
exportSession: (arg) => exportSession({
|
|
5333
5555
|
backend: backendRef.current,
|
|
@@ -5344,15 +5566,16 @@ ${estimateCost(usageRef.current, modelNameRef.current, modelConfig)}`;
|
|
|
5344
5566
|
if (preview.removed === 0) {
|
|
5345
5567
|
return "Nothing to undo (no conversation turn to retract).";
|
|
5346
5568
|
}
|
|
5347
|
-
const diffs = await buildUndoDiffs(
|
|
5569
|
+
const diffs = preview.tree ? await buildUndoTreeDiffs(cwd, preview.tree) : await buildUndoDiffs(
|
|
5348
5570
|
preview.turn !== void 0 ? listTurnSnapshots(preview.turn) : [],
|
|
5349
5571
|
cwd
|
|
5350
5572
|
);
|
|
5573
|
+
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
5574
|
const confirmed = await new Promise((resolve) => {
|
|
5352
5575
|
setPendingRewind({
|
|
5353
5576
|
title: "Undo last turn",
|
|
5354
5577
|
confirmLabel: "undo",
|
|
5355
|
-
summary
|
|
5578
|
+
summary,
|
|
5356
5579
|
diffs,
|
|
5357
5580
|
resolve
|
|
5358
5581
|
});
|
|
@@ -5360,11 +5583,10 @@ ${estimateCost(usageRef.current, modelNameRef.current, modelConfig)}`;
|
|
|
5360
5583
|
if (!confirmed) {
|
|
5361
5584
|
return "Undo cancelled.";
|
|
5362
5585
|
}
|
|
5363
|
-
const
|
|
5364
|
-
if (removed === 0) {
|
|
5586
|
+
const outcome = await current.undoLastTurn();
|
|
5587
|
+
if (outcome.removed === 0) {
|
|
5365
5588
|
return "Nothing to undo (no conversation turn to retract).";
|
|
5366
5589
|
}
|
|
5367
|
-
const reverted = turn !== void 0 ? await undoTurnSnapshots(turn) : [];
|
|
5368
5590
|
const visible = messagesRef.current;
|
|
5369
5591
|
let cut = -1;
|
|
5370
5592
|
for (let i = visible.length - 1; i >= 0; i--) {
|
|
@@ -5374,9 +5596,36 @@ ${estimateCost(usageRef.current, modelNameRef.current, modelConfig)}`;
|
|
|
5374
5596
|
}
|
|
5375
5597
|
}
|
|
5376
5598
|
redrawMessages(cut >= 0 ? visible.slice(0, cut) : visible);
|
|
5377
|
-
return [
|
|
5378
|
-
|
|
5379
|
-
|
|
5599
|
+
return [
|
|
5600
|
+
...outcome.reverted,
|
|
5601
|
+
`Retracted the last conversation turn (${outcome.removed} messages).`
|
|
5602
|
+
].join("\n");
|
|
5603
|
+
},
|
|
5604
|
+
redo: async () => {
|
|
5605
|
+
const current = backendRef.current;
|
|
5606
|
+
if (!(current instanceof AgentLoop)) {
|
|
5607
|
+
return "Nothing to redo.";
|
|
5608
|
+
}
|
|
5609
|
+
const entry = current.peekRedo();
|
|
5610
|
+
if (!entry) {
|
|
5611
|
+
return "Nothing to redo.";
|
|
5612
|
+
}
|
|
5613
|
+
const confirmed = await new Promise((resolve) => {
|
|
5614
|
+
setPendingRewind({
|
|
5615
|
+
title: "Redo last undo",
|
|
5616
|
+
confirmLabel: "redo",
|
|
5617
|
+
summary: formatRedoSummary(entry),
|
|
5618
|
+
resolve
|
|
5619
|
+
});
|
|
5620
|
+
});
|
|
5621
|
+
if (!confirmed) {
|
|
5622
|
+
return "Redo cancelled.";
|
|
5623
|
+
}
|
|
5624
|
+
const result = await current.redoLastUndo();
|
|
5625
|
+
if (!result) {
|
|
5626
|
+
return "Nothing to redo.";
|
|
5627
|
+
}
|
|
5628
|
+
return formatRedoResult(result);
|
|
5380
5629
|
},
|
|
5381
5630
|
rewind: async (args) => {
|
|
5382
5631
|
if (!args) {
|
|
@@ -5472,6 +5721,7 @@ ${plan.affected.length} file change(s) will be reverted, ${messageCount} message
|
|
|
5472
5721
|
`models (${config.models.length}): ${config.models.map((m) => m.name).join(", ") || "(none)"}`,
|
|
5473
5722
|
`maxSteps: ${config.maxSteps}`,
|
|
5474
5723
|
`contextMaxTokens: ${config.contextMaxTokens}`,
|
|
5724
|
+
`compactThresholdTokens: ${config.compactThresholdTokens ?? "(context window)"}`,
|
|
5475
5725
|
`streamIdleTimeoutSec: ${config.streamIdleTimeoutSec}`,
|
|
5476
5726
|
`streamFirstChunkTimeoutSec: ${config.streamFirstChunkTimeoutSec}`,
|
|
5477
5727
|
`streamMaxRetries: ${config.streamMaxRetries}`,
|
|
@@ -5559,6 +5809,10 @@ ${plan.affected.length} file change(s) will be reverted, ${messageCount} message
|
|
|
5559
5809
|
);
|
|
5560
5810
|
return;
|
|
5561
5811
|
}
|
|
5812
|
+
if (abortRef.current && BUSY_BLOCKED_COMMANDS.has(parsed.name)) {
|
|
5813
|
+
pushMessage("system", "Busy \u2014 wait for the turn to finish or press Esc to interrupt it.");
|
|
5814
|
+
return;
|
|
5815
|
+
}
|
|
5562
5816
|
void command.run(parsed.args, registry.ctx);
|
|
5563
5817
|
return;
|
|
5564
5818
|
}
|
|
@@ -5741,6 +5995,38 @@ function parseTomlFile(content, filePath) {
|
|
|
5741
5995
|
function mergeConfig(base, override) {
|
|
5742
5996
|
return { ...base, ...override };
|
|
5743
5997
|
}
|
|
5998
|
+
var PROJECT_ALLOWED_KEYS = /* @__PURE__ */ new Set([
|
|
5999
|
+
"defaultModel",
|
|
6000
|
+
"models",
|
|
6001
|
+
"maxSteps",
|
|
6002
|
+
"contextMaxTokens",
|
|
6003
|
+
"compactThresholdTokens",
|
|
6004
|
+
"streamIdleTimeoutSec",
|
|
6005
|
+
"streamFirstChunkTimeoutSec",
|
|
6006
|
+
"streamMaxRetries",
|
|
6007
|
+
"maxAutoContinues",
|
|
6008
|
+
"contextCompaction",
|
|
6009
|
+
"sessionBudgetUsd",
|
|
6010
|
+
"notifyBell",
|
|
6011
|
+
"notifyBellThresholdSec",
|
|
6012
|
+
"doomLoopThreshold",
|
|
6013
|
+
"gitSnapshots"
|
|
6014
|
+
]);
|
|
6015
|
+
function sanitizeProjectConfig(project, filePath) {
|
|
6016
|
+
const sanitized = {};
|
|
6017
|
+
for (const [key, value] of Object.entries(project)) {
|
|
6018
|
+
if (value === void 0) continue;
|
|
6019
|
+
if (PROJECT_ALLOWED_KEYS.has(key)) {
|
|
6020
|
+
sanitized[key] = value;
|
|
6021
|
+
} else {
|
|
6022
|
+
process.stderr.write(
|
|
6023
|
+
`[star] ignoring "${key}" in ${filePath}: project config cannot set this key
|
|
6024
|
+
`
|
|
6025
|
+
);
|
|
6026
|
+
}
|
|
6027
|
+
}
|
|
6028
|
+
return sanitized;
|
|
6029
|
+
}
|
|
5744
6030
|
function applyOverrides(config, overrides) {
|
|
5745
6031
|
if (!overrides) return config;
|
|
5746
6032
|
const merged = { ...config };
|
|
@@ -5756,22 +6042,13 @@ function readTomlFileSync(filePath) {
|
|
|
5756
6042
|
function loadConfigSync(cwd, overrides) {
|
|
5757
6043
|
loadEnvFile();
|
|
5758
6044
|
const global = readTomlFileSync(globalConfigPath());
|
|
5759
|
-
const
|
|
6045
|
+
const projectPath = projectConfigPath(cwd);
|
|
6046
|
+
const project = sanitizeProjectConfig(readTomlFileSync(projectPath), projectPath);
|
|
5760
6047
|
const merged = applyOverrides(mergeConfig(global, project), overrides);
|
|
5761
6048
|
return ConfigSchema.parse(merged);
|
|
5762
6049
|
}
|
|
5763
6050
|
|
|
5764
6051
|
// 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
6052
|
async function createLoop(config, modelName, cwd, sessionStore) {
|
|
5776
6053
|
const model = createModel(config, modelName);
|
|
5777
6054
|
const registry = createDefaultRegistry();
|
|
@@ -5785,7 +6062,7 @@ async function createLoop(config, modelName, cwd, sessionStore) {
|
|
|
5785
6062
|
contextMaxTokens: contextWindowTokens(config, modelName)
|
|
5786
6063
|
});
|
|
5787
6064
|
}
|
|
5788
|
-
async function printMode(loop, prompt, cwd, json, imagePaths) {
|
|
6065
|
+
async function printMode(loop, prompt, cwd, json, imagePaths, sessionStore) {
|
|
5789
6066
|
const flagImages = [];
|
|
5790
6067
|
for (const imagePath of imagePaths) {
|
|
5791
6068
|
if (!imageMimeType(imagePath)) {
|
|
@@ -5854,6 +6131,10 @@ async function printMode(loop, prompt, cwd, json, imagePaths) {
|
|
|
5854
6131
|
}
|
|
5855
6132
|
case "finish":
|
|
5856
6133
|
usage.add(event.usage);
|
|
6134
|
+
if (event.usage) {
|
|
6135
|
+
sessionStore?.addUsage(event.usage).catch(() => {
|
|
6136
|
+
});
|
|
6137
|
+
}
|
|
5857
6138
|
break;
|
|
5858
6139
|
case "retry": {
|
|
5859
6140
|
if (!json) {
|
|
@@ -5903,8 +6184,15 @@ async function printMode(loop, prompt, cwd, json, imagePaths) {
|
|
|
5903
6184
|
}
|
|
5904
6185
|
}
|
|
5905
6186
|
if (!json) process.stdout.write("\n");
|
|
5906
|
-
defaultTaskManager.cleanup();
|
|
5907
|
-
defaultAgentTasks.cleanup();
|
|
6187
|
+
const killedShells = defaultTaskManager.cleanup();
|
|
6188
|
+
const killedAgents = defaultAgentTasks.cleanup();
|
|
6189
|
+
const stopped = killedShells.length + killedAgents.length;
|
|
6190
|
+
if (stopped > 0) {
|
|
6191
|
+
process.stderr.write(
|
|
6192
|
+
`[info] ${stopped} background task(s) were still running and have been stopped.
|
|
6193
|
+
`
|
|
6194
|
+
);
|
|
6195
|
+
}
|
|
5908
6196
|
return exitCode;
|
|
5909
6197
|
}
|
|
5910
6198
|
var program = new Command();
|
|
@@ -5982,12 +6270,17 @@ program.name("star").description("Star CLI \u2014 an AI agent command-line inter
|
|
|
5982
6270
|
}
|
|
5983
6271
|
}
|
|
5984
6272
|
if (resumeId) {
|
|
5985
|
-
|
|
5986
|
-
if (!
|
|
6273
|
+
sessionStore = await SessionStore.open(resumeId);
|
|
6274
|
+
if (!sessionStore) {
|
|
5987
6275
|
console.error(`Session not found: ${resumeId}`);
|
|
5988
6276
|
process.exit(1);
|
|
5989
6277
|
}
|
|
5990
|
-
|
|
6278
|
+
const [meta, loaded] = await Promise.all([sessionStore.meta(), sessionStore.messages()]);
|
|
6279
|
+
const messages = reconcileToolCalls(loaded);
|
|
6280
|
+
if (messages.length !== loaded.length) {
|
|
6281
|
+
await sessionStore.replaceMessages(messages);
|
|
6282
|
+
}
|
|
6283
|
+
resumed = { meta, messages };
|
|
5991
6284
|
} else if (!opts.print) {
|
|
5992
6285
|
sessionStore = await SessionStore.create(cwd, modelName);
|
|
5993
6286
|
}
|
|
@@ -6011,7 +6304,8 @@ program.name("star").description("Star CLI \u2014 an AI agent command-line inter
|
|
|
6011
6304
|
opts.print,
|
|
6012
6305
|
cwd,
|
|
6013
6306
|
Boolean(opts.json),
|
|
6014
|
-
opts.image ?? []
|
|
6307
|
+
opts.image ?? [],
|
|
6308
|
+
sessionStore
|
|
6015
6309
|
);
|
|
6016
6310
|
return;
|
|
6017
6311
|
}
|