@cryer/star-cli 0.1.3 → 0.2.1
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 +123 -10
- package/dist/chunk-MJ2O7NWI.js +2469 -0
- package/dist/chunk-MJ2O7NWI.js.map +1 -0
- package/dist/loop-R3RQCNFT.js +10 -0
- package/dist/loop-R3RQCNFT.js.map +1 -0
- package/dist/main.js +1803 -2146
- package/dist/main.js.map +1 -1
- package/package.json +1 -1
package/dist/main.js
CHANGED
|
@@ -1,608 +1,45 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
AgentLoop,
|
|
4
|
+
SKIP_DIRS,
|
|
5
|
+
TodoStore,
|
|
6
|
+
WORKING_DIFF_MAX_LINES,
|
|
7
|
+
bashTool,
|
|
8
|
+
buildAllowRule,
|
|
9
|
+
buildCommitPrompt,
|
|
10
|
+
clearSnapshots,
|
|
11
|
+
collectCommitContext,
|
|
12
|
+
collectWorkingDiff,
|
|
13
|
+
compactMessages,
|
|
14
|
+
createDefaultRegistry,
|
|
15
|
+
defaultTaskManager,
|
|
16
|
+
discoverSkills,
|
|
17
|
+
estimateTokens,
|
|
18
|
+
formatTaskFinished,
|
|
19
|
+
formatTaskList,
|
|
20
|
+
formatTaskStarted,
|
|
21
|
+
formatTodos,
|
|
22
|
+
getGitSummary,
|
|
23
|
+
globalConfigPath,
|
|
24
|
+
hydrateSnapshots,
|
|
25
|
+
isAllowedByRules,
|
|
26
|
+
isDangerousCommand,
|
|
27
|
+
isGitRepo,
|
|
28
|
+
isSensitivePath,
|
|
29
|
+
listSnapshots,
|
|
30
|
+
projectConfigPath,
|
|
31
|
+
reconcileToolCalls,
|
|
32
|
+
resolveShell,
|
|
33
|
+
rewindToSnapshot,
|
|
34
|
+
sessionsDir,
|
|
35
|
+
starHome,
|
|
36
|
+
summarizeMessages,
|
|
37
|
+
undoTurnSnapshots
|
|
38
|
+
} from "./chunk-MJ2O7NWI.js";
|
|
2
39
|
|
|
3
40
|
// src/main.tsx
|
|
4
41
|
import { Command } from "commander";
|
|
5
42
|
|
|
6
|
-
// src/agent/loop.ts
|
|
7
|
-
import { tool as aiTool } from "ai";
|
|
8
|
-
|
|
9
|
-
// src/context/compaction.ts
|
|
10
|
-
import { generateText } from "ai";
|
|
11
|
-
|
|
12
|
-
// src/context/tokens.ts
|
|
13
|
-
var CHARS_PER_TOKEN = 4;
|
|
14
|
-
var MESSAGE_OVERHEAD = 4;
|
|
15
|
-
function contentCharLength(message) {
|
|
16
|
-
if (message.role === "tool") {
|
|
17
|
-
let chars2 = 0;
|
|
18
|
-
for (const part of message.content) {
|
|
19
|
-
chars2 += JSON.stringify(part.result ?? null).length;
|
|
20
|
-
}
|
|
21
|
-
return chars2;
|
|
22
|
-
}
|
|
23
|
-
if (typeof message.content === "string") {
|
|
24
|
-
return message.content.length;
|
|
25
|
-
}
|
|
26
|
-
let chars = 0;
|
|
27
|
-
for (const part of message.content) {
|
|
28
|
-
if (part.type === "text" || part.type === "reasoning") {
|
|
29
|
-
chars += part.text.length;
|
|
30
|
-
} else if (part.type === "tool-call") {
|
|
31
|
-
chars += part.toolName.length + JSON.stringify(part.args ?? null).length;
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
return chars;
|
|
35
|
-
}
|
|
36
|
-
function estimateMessageTokens(message) {
|
|
37
|
-
return Math.ceil(contentCharLength(message) / CHARS_PER_TOKEN) + MESSAGE_OVERHEAD;
|
|
38
|
-
}
|
|
39
|
-
function estimateTokens(messages) {
|
|
40
|
-
return messages.reduce((total, message) => total + estimateMessageTokens(message), 0);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
// src/context/compaction.ts
|
|
44
|
-
var MIN_KEPT_MESSAGES = 4;
|
|
45
|
-
function placeholderMessage(droppedCount) {
|
|
46
|
-
return {
|
|
47
|
-
role: "user",
|
|
48
|
-
content: `[context compacted: ${droppedCount} earlier messages dropped]`
|
|
49
|
-
};
|
|
50
|
-
}
|
|
51
|
-
var SUMMARY_SYSTEM_PROMPT = [
|
|
52
|
-
"You are summarizing an AI coding agent's conversation for context compaction.",
|
|
53
|
-
"Write a concise summary that preserves:",
|
|
54
|
-
"- the user's goals and requests",
|
|
55
|
-
"- decisions made and their rationale",
|
|
56
|
-
"- files and code touched (paths, key changes)",
|
|
57
|
-
"- tool results that matter (errors, key outputs)",
|
|
58
|
-
"- outstanding TODOs and next steps",
|
|
59
|
-
"Output only the summary, no preamble."
|
|
60
|
-
].join("\n");
|
|
61
|
-
function serializeMessage(message) {
|
|
62
|
-
if (typeof message.content === "string") {
|
|
63
|
-
return `${message.role}: ${message.content}`;
|
|
64
|
-
}
|
|
65
|
-
const parts = message.content.map((part) => {
|
|
66
|
-
if (part.type === "text") return part.text;
|
|
67
|
-
if (part.type === "tool-call")
|
|
68
|
-
return `[tool-call ${part.toolName}] ${JSON.stringify(part.args)}`;
|
|
69
|
-
if (part.type === "tool-result")
|
|
70
|
-
return `[tool-result ${part.toolName}] ${JSON.stringify(part.result)}`;
|
|
71
|
-
return `[${part.type}]`;
|
|
72
|
-
});
|
|
73
|
-
return `${message.role}: ${parts.join("\n")}`;
|
|
74
|
-
}
|
|
75
|
-
async function summarizeMessages(messages, model) {
|
|
76
|
-
const transcript = messages.map(serializeMessage).join("\n");
|
|
77
|
-
const { text } = await generateText({
|
|
78
|
-
model,
|
|
79
|
-
system: SUMMARY_SYSTEM_PROMPT,
|
|
80
|
-
prompt: `Summarize this conversation so far:
|
|
81
|
-
|
|
82
|
-
${transcript}`
|
|
83
|
-
});
|
|
84
|
-
return text.trim();
|
|
85
|
-
}
|
|
86
|
-
function compactMessages(messages, maxTokens) {
|
|
87
|
-
if (estimateTokens(messages) <= maxTokens) {
|
|
88
|
-
return { messages, compacted: false, droppedCount: 0 };
|
|
89
|
-
}
|
|
90
|
-
const first = messages[0];
|
|
91
|
-
const hasSystem = first?.role === "system";
|
|
92
|
-
const head = hasSystem && first ? [first] : [];
|
|
93
|
-
const rest = hasSystem ? messages.slice(1) : messages.slice();
|
|
94
|
-
const turns = [];
|
|
95
|
-
for (const message of rest) {
|
|
96
|
-
const current = turns[turns.length - 1];
|
|
97
|
-
if (message.role === "user" || !current) {
|
|
98
|
-
turns.push([message]);
|
|
99
|
-
} else {
|
|
100
|
-
current.push(message);
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
let droppedCount = 0;
|
|
104
|
-
let turnIndex = 0;
|
|
105
|
-
while (turnIndex < turns.length) {
|
|
106
|
-
const turn = turns[turnIndex];
|
|
107
|
-
if (rest.length - droppedCount - turn.length < MIN_KEPT_MESSAGES) {
|
|
108
|
-
break;
|
|
109
|
-
}
|
|
110
|
-
droppedCount += turn.length;
|
|
111
|
-
turnIndex += 1;
|
|
112
|
-
const candidate = [...head, placeholderMessage(droppedCount), ...rest.slice(droppedCount)];
|
|
113
|
-
if (estimateTokens(candidate) <= maxTokens) {
|
|
114
|
-
break;
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
if (droppedCount === 0) {
|
|
118
|
-
return { messages, compacted: false, droppedCount: 0 };
|
|
119
|
-
}
|
|
120
|
-
return {
|
|
121
|
-
messages: [...head, placeholderMessage(droppedCount), ...rest.slice(droppedCount)],
|
|
122
|
-
compacted: true,
|
|
123
|
-
droppedCount
|
|
124
|
-
};
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
// src/core/messages.ts
|
|
128
|
-
var MISSING_TOOL_RESULT_TEXT = "tool result missing (session was interrupted)";
|
|
129
|
-
function reconcileToolCalls(messages) {
|
|
130
|
-
const result = [];
|
|
131
|
-
let pending = [];
|
|
132
|
-
const flushPending = () => {
|
|
133
|
-
for (const call of pending) {
|
|
134
|
-
result.push({
|
|
135
|
-
role: "tool",
|
|
136
|
-
content: [
|
|
137
|
-
{
|
|
138
|
-
type: "tool-result",
|
|
139
|
-
toolCallId: call.toolCallId,
|
|
140
|
-
toolName: call.toolName,
|
|
141
|
-
result: MISSING_TOOL_RESULT_TEXT
|
|
142
|
-
}
|
|
143
|
-
]
|
|
144
|
-
});
|
|
145
|
-
}
|
|
146
|
-
pending = [];
|
|
147
|
-
};
|
|
148
|
-
for (const message of messages) {
|
|
149
|
-
if (message.role === "assistant" && Array.isArray(message.content)) {
|
|
150
|
-
flushPending();
|
|
151
|
-
pending = message.content.filter((part) => part.type === "tool-call").map((part) => ({ toolCallId: part.toolCallId, toolName: part.toolName }));
|
|
152
|
-
} else if (message.role === "tool" && Array.isArray(message.content) && pending.length > 0) {
|
|
153
|
-
const answered = new Set(message.content.map((part) => part.toolCallId));
|
|
154
|
-
pending = pending.filter((call) => !answered.has(call.toolCallId));
|
|
155
|
-
} else {
|
|
156
|
-
flushPending();
|
|
157
|
-
}
|
|
158
|
-
result.push(message);
|
|
159
|
-
}
|
|
160
|
-
flushPending();
|
|
161
|
-
return result;
|
|
162
|
-
}
|
|
163
|
-
function retractLastTurn(messages) {
|
|
164
|
-
for (let i = messages.length - 1; i >= 0; i--) {
|
|
165
|
-
if (messages[i]?.role === "user") {
|
|
166
|
-
return { messages: messages.slice(0, i), removed: messages.length - i };
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
return { messages, removed: 0 };
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
// src/permissions/gate.ts
|
|
173
|
-
import path from "path";
|
|
174
|
-
|
|
175
|
-
// src/permissions/allow.ts
|
|
176
|
-
function parseAllowRule(rule) {
|
|
177
|
-
const match = /^([\w-]+)(?:\((.*)\))?$/.exec(rule.trim());
|
|
178
|
-
if (!match) return null;
|
|
179
|
-
return { toolName: match[1], pattern: match[2] };
|
|
180
|
-
}
|
|
181
|
-
function globMatch(pattern, value) {
|
|
182
|
-
const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
|
|
183
|
-
return new RegExp(`^${escaped}$`).test(value);
|
|
184
|
-
}
|
|
185
|
-
function requestTarget(req) {
|
|
186
|
-
if (typeof req.args !== "object" || req.args === null) return void 0;
|
|
187
|
-
const args = req.args;
|
|
188
|
-
const key = req.toolName === "bash" ? "command" : "path";
|
|
189
|
-
const value = args[key];
|
|
190
|
-
return typeof value === "string" ? value : void 0;
|
|
191
|
-
}
|
|
192
|
-
function matchAllowRule(rule, req) {
|
|
193
|
-
if (rule.toolName !== req.toolName) return false;
|
|
194
|
-
if (rule.pattern === void 0) return true;
|
|
195
|
-
const target = requestTarget(req);
|
|
196
|
-
return target !== void 0 && globMatch(rule.pattern, target);
|
|
197
|
-
}
|
|
198
|
-
function isAllowedByRules(rules, req) {
|
|
199
|
-
return rules.some((raw) => {
|
|
200
|
-
const rule = parseAllowRule(raw);
|
|
201
|
-
return rule !== null && matchAllowRule(rule, req);
|
|
202
|
-
});
|
|
203
|
-
}
|
|
204
|
-
function buildAllowRule(req) {
|
|
205
|
-
const target = requestTarget(req);
|
|
206
|
-
return target !== void 0 ? `${req.toolName}(${target})` : req.toolName;
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
// src/permissions/gate.ts
|
|
210
|
-
var FILE_TOOLS = /* @__PURE__ */ new Set(["write_file", "edit_file", "read_file"]);
|
|
211
|
-
var DANGEROUS_COMMAND_PATTERNS = [
|
|
212
|
-
/rm\s+-rf\s+\/(?:\s|$|;|&)/i,
|
|
213
|
-
/rm\s+-rf\s+~(?:\s|\/|$|;|&)/i,
|
|
214
|
-
/:\(\)\s*\{\s*:\|:&\s*\}\s*;:/,
|
|
215
|
-
/\bmkfs\b/i,
|
|
216
|
-
/dd\s+[^|;]*\bif=/i,
|
|
217
|
-
/>\s*\/dev\/sd[a-z]/i,
|
|
218
|
-
/\bshutdown\b/i,
|
|
219
|
-
/\breboot\b/i,
|
|
220
|
-
/\bpoweroff\b/i
|
|
221
|
-
];
|
|
222
|
-
function isDangerousCommand(command) {
|
|
223
|
-
return DANGEROUS_COMMAND_PATTERNS.some((pattern) => pattern.test(command));
|
|
224
|
-
}
|
|
225
|
-
function getStringArg(args, key) {
|
|
226
|
-
if (typeof args === "object" && args !== null) {
|
|
227
|
-
const value = args[key];
|
|
228
|
-
if (typeof value === "string") {
|
|
229
|
-
return value;
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
return void 0;
|
|
233
|
-
}
|
|
234
|
-
function normalizeAbsolute(p, cwd) {
|
|
235
|
-
const abs = path.isAbsolute(p) ? path.normalize(p) : path.resolve(cwd, p);
|
|
236
|
-
return abs.replace(/\\/g, "/").replace(/\/+$/, "").toLowerCase();
|
|
237
|
-
}
|
|
238
|
-
function isOutsideCwd(p, cwd) {
|
|
239
|
-
const abs = normalizeAbsolute(p, cwd);
|
|
240
|
-
const base = normalizeAbsolute(cwd, cwd);
|
|
241
|
-
return abs !== base && !abs.startsWith(`${base}/`);
|
|
242
|
-
}
|
|
243
|
-
function isSensitivePath(p) {
|
|
244
|
-
const base = p.replace(/\\/g, "/").split("/").pop()?.toLowerCase() ?? "";
|
|
245
|
-
if (base === ".env.example" || base === ".env.sample" || base === ".env.template") {
|
|
246
|
-
return false;
|
|
247
|
-
}
|
|
248
|
-
return base === ".env" || base.startsWith(".env.") || base === "id_rsa" || base.endsWith(".pem");
|
|
249
|
-
}
|
|
250
|
-
function checkPermission(mode, req, ctx, allowRules = []) {
|
|
251
|
-
if (mode === "yolo") {
|
|
252
|
-
return "allow";
|
|
253
|
-
}
|
|
254
|
-
const command = getStringArg(req.args, "command");
|
|
255
|
-
const filePath = getStringArg(req.args, "path");
|
|
256
|
-
if (req.toolName === "bash" && command !== void 0 && isDangerousCommand(command)) {
|
|
257
|
-
return "deny";
|
|
258
|
-
}
|
|
259
|
-
if (FILE_TOOLS.has(req.toolName) && filePath !== void 0) {
|
|
260
|
-
if ((req.toolName === "write_file" || req.toolName === "edit_file") && isSensitivePath(filePath)) {
|
|
261
|
-
return "deny";
|
|
262
|
-
}
|
|
263
|
-
if (isOutsideCwd(filePath, ctx.cwd)) {
|
|
264
|
-
if (mode === "ask" && req.level === "read") {
|
|
265
|
-
return "ask";
|
|
266
|
-
}
|
|
267
|
-
return "deny";
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
if (mode === "auto") {
|
|
271
|
-
return "allow";
|
|
272
|
-
}
|
|
273
|
-
if (mode === "readonly") {
|
|
274
|
-
return req.level === "read" ? "allow" : "deny";
|
|
275
|
-
}
|
|
276
|
-
if (mode === "plan") {
|
|
277
|
-
return req.level === "read" ? "allow" : "deny";
|
|
278
|
-
}
|
|
279
|
-
if (isAllowedByRules(allowRules, req)) {
|
|
280
|
-
return "allow";
|
|
281
|
-
}
|
|
282
|
-
return req.level === "read" ? "allow" : "ask";
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
// src/tools/fs/snapshots.ts
|
|
286
|
-
import { mkdir, readFile, rm, writeFile } from "fs/promises";
|
|
287
|
-
import path2 from "path";
|
|
288
|
-
var MAX_SNAPSHOTS = 50;
|
|
289
|
-
var stack = [];
|
|
290
|
-
var activeTurn = 0;
|
|
291
|
-
function beginTurn() {
|
|
292
|
-
return ++activeTurn;
|
|
293
|
-
}
|
|
294
|
-
function currentTurnSeq() {
|
|
295
|
-
return activeTurn;
|
|
296
|
-
}
|
|
297
|
-
async function captureSnapshot(filePath, toolName) {
|
|
298
|
-
let content = null;
|
|
299
|
-
let existed = false;
|
|
300
|
-
try {
|
|
301
|
-
content = await readFile(filePath, "utf8");
|
|
302
|
-
existed = true;
|
|
303
|
-
} catch {
|
|
304
|
-
}
|
|
305
|
-
return { path: filePath, existed, content, toolName, timestamp: Date.now(), turn: activeTurn };
|
|
306
|
-
}
|
|
307
|
-
function pushSnapshot(snapshot) {
|
|
308
|
-
stack.push(snapshot);
|
|
309
|
-
if (stack.length > MAX_SNAPSHOTS) {
|
|
310
|
-
stack.splice(0, stack.length - MAX_SNAPSHOTS);
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
async function revert(snapshot) {
|
|
314
|
-
if (snapshot.existed && snapshot.content !== null) {
|
|
315
|
-
await mkdir(path2.dirname(snapshot.path), { recursive: true });
|
|
316
|
-
await writeFile(snapshot.path, snapshot.content, "utf8");
|
|
317
|
-
return `Restored ${snapshot.path} to its state before ${snapshot.toolName}.`;
|
|
318
|
-
}
|
|
319
|
-
await rm(snapshot.path, { force: true });
|
|
320
|
-
return `Deleted ${snapshot.path} (it did not exist before ${snapshot.toolName}).`;
|
|
321
|
-
}
|
|
322
|
-
async function undoTurnSnapshots(turn) {
|
|
323
|
-
const matches = [];
|
|
324
|
-
for (let i = stack.length - 1; i >= 0; i--) {
|
|
325
|
-
const snapshot = stack[i];
|
|
326
|
-
if (snapshot && snapshot.turn === turn) {
|
|
327
|
-
matches.push(snapshot);
|
|
328
|
-
stack.splice(i, 1);
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
const messages = [];
|
|
332
|
-
for (const snapshot of matches) {
|
|
333
|
-
messages.push(await revert(snapshot));
|
|
334
|
-
}
|
|
335
|
-
return messages;
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
// src/agent/loop.ts
|
|
339
|
-
var PLAN_MODE_PROMPT = "\n\nYou are currently in PLAN MODE. Research the task using only the read-only tools available to you, then present a concrete, step-by-step implementation plan as your final answer. You must not modify files, run shell commands, or otherwise change the system \u2014 write/exec tools are unavailable in this mode. Do not ask the user to run commands for you; note manual steps in the plan instead. The user will review your plan and approve it before any execution begins.";
|
|
340
|
-
var AgentLoop = class {
|
|
341
|
-
messages = [];
|
|
342
|
-
opts;
|
|
343
|
-
// Seq + user-message index of each turn started via stream(); lets /undo
|
|
344
|
-
// match a retracted turn to the file snapshots it produced. Cleared when
|
|
345
|
-
// history is replaced wholesale (resume/compact), because indices no longer
|
|
346
|
-
// line up — in that case /undo only retracts messages, never wrong files.
|
|
347
|
-
turnMarkers = [];
|
|
348
|
-
confirmHandler;
|
|
349
|
-
constructor(opts) {
|
|
350
|
-
this.opts = opts;
|
|
351
|
-
if (opts.system) {
|
|
352
|
-
this.messages.push({ role: "system", content: opts.system });
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
getMessages() {
|
|
356
|
-
return this.messages;
|
|
357
|
-
}
|
|
358
|
-
async loadMessages(messages) {
|
|
359
|
-
this.messages = reconcileToolCalls(messages);
|
|
360
|
-
this.turnMarkers = [];
|
|
361
|
-
}
|
|
362
|
-
// Drops the final user message and everything after it, and persists the
|
|
363
|
-
// trimmed history so a later /resume does not bring the turn back.
|
|
364
|
-
// `turn` is the retracted turn's snapshot seq when it could be verified
|
|
365
|
-
// against the marker recorded at turn start — undefined means the caller
|
|
366
|
-
// must not revert any file snapshots.
|
|
367
|
-
async retractLastTurn() {
|
|
368
|
-
const result = retractLastTurn([...this.messages]);
|
|
369
|
-
if (result.removed === 0) return { removed: 0 };
|
|
370
|
-
const userIndex = result.messages.length;
|
|
371
|
-
this.messages = result.messages;
|
|
372
|
-
await this.opts.sessionStore?.replaceMessages([...this.messages]);
|
|
373
|
-
const last = this.turnMarkers[this.turnMarkers.length - 1];
|
|
374
|
-
let turn;
|
|
375
|
-
if (last && last.userIndex === userIndex) {
|
|
376
|
-
turn = last.seq;
|
|
377
|
-
this.turnMarkers.pop();
|
|
378
|
-
}
|
|
379
|
-
return { removed: result.removed, turn };
|
|
380
|
-
}
|
|
381
|
-
async appendContextMessage(text, role = "user") {
|
|
382
|
-
const message = { role, content: text };
|
|
383
|
-
this.messages.push(message);
|
|
384
|
-
await this.persist(message);
|
|
385
|
-
}
|
|
386
|
-
async persist(message) {
|
|
387
|
-
await this.opts.sessionStore?.append(message);
|
|
388
|
-
}
|
|
389
|
-
async *stream(input, signal, opts) {
|
|
390
|
-
this.syncSystemMessage();
|
|
391
|
-
const userMessage = { role: "user", content: input };
|
|
392
|
-
this.messages.push(userMessage);
|
|
393
|
-
this.turnMarkers.push({ seq: beginTurn(), userIndex: this.messages.length - 1 });
|
|
394
|
-
await this.persist(
|
|
395
|
-
opts?.persistAs !== void 0 ? { role: "user", content: opts.persistAs } : userMessage
|
|
396
|
-
);
|
|
397
|
-
const { config, registry, cwd } = this.opts;
|
|
398
|
-
const aiTools = this.buildAiTools();
|
|
399
|
-
for (let step = 0; step < config.maxSteps; step++) {
|
|
400
|
-
const compacted = compactMessages([...this.messages], config.contextMaxTokens);
|
|
401
|
-
if (compacted.compacted) {
|
|
402
|
-
this.messages = await this.applyCompactionSummary(compacted);
|
|
403
|
-
}
|
|
404
|
-
let text = "";
|
|
405
|
-
const toolCalls = [];
|
|
406
|
-
let failed = false;
|
|
407
|
-
try {
|
|
408
|
-
for await (const event of this.streamOnce(aiTools, signal)) {
|
|
409
|
-
if (event.type === "text-delta") {
|
|
410
|
-
text += event.text;
|
|
411
|
-
yield event;
|
|
412
|
-
} else if (event.type === "reasoning") {
|
|
413
|
-
yield event;
|
|
414
|
-
} else if (event.type === "tool-call") {
|
|
415
|
-
toolCalls.push({ id: event.id, name: event.name, args: event.args });
|
|
416
|
-
yield event;
|
|
417
|
-
} else if (event.type === "error") {
|
|
418
|
-
failed = true;
|
|
419
|
-
yield event;
|
|
420
|
-
} else {
|
|
421
|
-
yield event;
|
|
422
|
-
}
|
|
423
|
-
}
|
|
424
|
-
} catch (error) {
|
|
425
|
-
if (signal.aborted) return;
|
|
426
|
-
yield { type: "error", error: error instanceof Error ? error : new Error(String(error)) };
|
|
427
|
-
return;
|
|
428
|
-
}
|
|
429
|
-
if (failed) return;
|
|
430
|
-
const assistantMessage = {
|
|
431
|
-
role: "assistant",
|
|
432
|
-
content: [
|
|
433
|
-
...text ? [{ type: "text", text }] : [],
|
|
434
|
-
...toolCalls.map((call) => ({
|
|
435
|
-
type: "tool-call",
|
|
436
|
-
toolCallId: call.id,
|
|
437
|
-
toolName: call.name,
|
|
438
|
-
args: call.args
|
|
439
|
-
}))
|
|
440
|
-
]
|
|
441
|
-
};
|
|
442
|
-
this.messages.push(assistantMessage);
|
|
443
|
-
await this.persist(assistantMessage);
|
|
444
|
-
if (toolCalls.length === 0) return;
|
|
445
|
-
const answered = /* @__PURE__ */ new Set();
|
|
446
|
-
try {
|
|
447
|
-
for (const call of toolCalls) {
|
|
448
|
-
if (signal.aborted) break;
|
|
449
|
-
const result = await this.executeTool(call, signal);
|
|
450
|
-
const toolMessage = {
|
|
451
|
-
role: "tool",
|
|
452
|
-
content: [
|
|
453
|
-
{
|
|
454
|
-
type: "tool-result",
|
|
455
|
-
toolCallId: call.id,
|
|
456
|
-
toolName: call.name,
|
|
457
|
-
result: result.content
|
|
458
|
-
}
|
|
459
|
-
]
|
|
460
|
-
};
|
|
461
|
-
this.messages.push(toolMessage);
|
|
462
|
-
await this.persist(toolMessage);
|
|
463
|
-
answered.add(call.id);
|
|
464
|
-
yield {
|
|
465
|
-
type: "tool-result",
|
|
466
|
-
id: call.id,
|
|
467
|
-
name: call.name,
|
|
468
|
-
content: result.content,
|
|
469
|
-
isError: result.isError
|
|
470
|
-
};
|
|
471
|
-
}
|
|
472
|
-
} finally {
|
|
473
|
-
for (const call of toolCalls) {
|
|
474
|
-
if (answered.has(call.id)) continue;
|
|
475
|
-
const content = signal.aborted ? "Tool execution interrupted by user." : "Tool execution interrupted before a result was produced.";
|
|
476
|
-
const synthetic = {
|
|
477
|
-
role: "tool",
|
|
478
|
-
content: [
|
|
479
|
-
{
|
|
480
|
-
type: "tool-result",
|
|
481
|
-
toolCallId: call.id,
|
|
482
|
-
toolName: call.name,
|
|
483
|
-
result: content
|
|
484
|
-
}
|
|
485
|
-
]
|
|
486
|
-
};
|
|
487
|
-
this.messages.push(synthetic);
|
|
488
|
-
await this.persist(synthetic).catch(() => {
|
|
489
|
-
});
|
|
490
|
-
yield { type: "tool-result", id: call.id, name: call.name, content, isError: true };
|
|
491
|
-
}
|
|
492
|
-
}
|
|
493
|
-
if (signal.aborted) return;
|
|
494
|
-
}
|
|
495
|
-
yield {
|
|
496
|
-
type: "error",
|
|
497
|
-
error: new Error(`Max steps (${config.maxSteps}) reached, stopping.`)
|
|
498
|
-
};
|
|
499
|
-
}
|
|
500
|
-
// The permission mode can change at runtime, so the system message is
|
|
501
|
-
// recomputed at the start of every turn instead of being frozen by the
|
|
502
|
-
// constructor. Also restores a system message after loadMessages() (resume,
|
|
503
|
-
// /model switch) replaced the history with one that has none.
|
|
504
|
-
syncSystemMessage() {
|
|
505
|
-
const base = this.opts.system;
|
|
506
|
-
const plan = this.opts.config.permissionMode === "plan";
|
|
507
|
-
const content = plan ? base ? base + PLAN_MODE_PROMPT : PLAN_MODE_PROMPT.trim() : base ?? null;
|
|
508
|
-
if (content === null) return;
|
|
509
|
-
const head = this.messages[0];
|
|
510
|
-
if (head?.role === "system" && typeof head.content === "string") {
|
|
511
|
-
if (head.content !== content) head.content = content;
|
|
512
|
-
} else if (head?.role !== "system") {
|
|
513
|
-
this.messages.unshift({ role: "system", content });
|
|
514
|
-
}
|
|
515
|
-
}
|
|
516
|
-
async applyCompactionSummary(compacted) {
|
|
517
|
-
const { config, model } = this.opts;
|
|
518
|
-
if (config.contextCompaction !== "summary" || !model) {
|
|
519
|
-
return compacted.messages;
|
|
520
|
-
}
|
|
521
|
-
const headCount = compacted.messages[0]?.role === "system" ? 1 : 0;
|
|
522
|
-
const dropped = this.messages.slice(headCount, headCount + compacted.droppedCount);
|
|
523
|
-
try {
|
|
524
|
-
const summary = await summarizeMessages(dropped, model);
|
|
525
|
-
const messages = compacted.messages.slice();
|
|
526
|
-
messages[headCount] = {
|
|
527
|
-
role: "user",
|
|
528
|
-
content: `[earlier conversation summarized]
|
|
529
|
-
${summary}`
|
|
530
|
-
};
|
|
531
|
-
return messages;
|
|
532
|
-
} catch {
|
|
533
|
-
return compacted.messages;
|
|
534
|
-
}
|
|
535
|
-
}
|
|
536
|
-
async *streamOnce(aiTools, signal) {
|
|
537
|
-
const { streamChat } = await import("./stream-Y55NRF2S.js");
|
|
538
|
-
yield* streamChat({
|
|
539
|
-
model: this.opts.model,
|
|
540
|
-
messages: this.messages,
|
|
541
|
-
tools: aiTools,
|
|
542
|
-
abortSignal: signal,
|
|
543
|
-
idleTimeoutMs: this.opts.config.streamIdleTimeoutSec * 1e3
|
|
544
|
-
});
|
|
545
|
-
}
|
|
546
|
-
buildAiTools() {
|
|
547
|
-
const plan = this.opts.config.permissionMode === "plan";
|
|
548
|
-
const tools = {};
|
|
549
|
-
for (const t of this.opts.registry.list()) {
|
|
550
|
-
if (plan && t.permission !== "read") continue;
|
|
551
|
-
tools[t.name] = aiTool({
|
|
552
|
-
description: t.description,
|
|
553
|
-
parameters: t.parameters
|
|
554
|
-
});
|
|
555
|
-
}
|
|
556
|
-
return tools;
|
|
557
|
-
}
|
|
558
|
-
async executeTool(call, signal) {
|
|
559
|
-
const { registry, config, cwd } = this.opts;
|
|
560
|
-
const tool = registry.get(call.name);
|
|
561
|
-
if (!tool) {
|
|
562
|
-
return { content: `Unknown tool: ${call.name}`, isError: true };
|
|
563
|
-
}
|
|
564
|
-
const decision = checkPermission(
|
|
565
|
-
config.permissionMode,
|
|
566
|
-
{ toolName: call.name, args: call.args, level: tool.permission },
|
|
567
|
-
{ cwd },
|
|
568
|
-
config.permissions.allow
|
|
569
|
-
);
|
|
570
|
-
if (decision === "deny") {
|
|
571
|
-
return { content: `Permission denied for tool "${call.name}".`, isError: true };
|
|
572
|
-
}
|
|
573
|
-
if (decision === "ask") {
|
|
574
|
-
let approved = false;
|
|
575
|
-
try {
|
|
576
|
-
approved = this.confirmHandler ? await this.confirmHandler({
|
|
577
|
-
toolName: call.name,
|
|
578
|
-
args: call.args,
|
|
579
|
-
level: tool.permission
|
|
580
|
-
}) : false;
|
|
581
|
-
} catch {
|
|
582
|
-
approved = false;
|
|
583
|
-
}
|
|
584
|
-
if (!approved) {
|
|
585
|
-
return { content: `User rejected tool "${call.name}".`, isError: true };
|
|
586
|
-
}
|
|
587
|
-
}
|
|
588
|
-
const parsed = tool.parameters.safeParse(call.args);
|
|
589
|
-
if (!parsed.success) {
|
|
590
|
-
return { content: `Invalid arguments: ${parsed.error.message}`, isError: true };
|
|
591
|
-
}
|
|
592
|
-
try {
|
|
593
|
-
return await tool.execute(parsed.data, { cwd, abortSignal: signal });
|
|
594
|
-
} catch (error) {
|
|
595
|
-
if (signal.aborted) {
|
|
596
|
-
return { content: "Tool execution aborted.", isError: true };
|
|
597
|
-
}
|
|
598
|
-
return {
|
|
599
|
-
content: `Tool error: ${error instanceof Error ? error.message : String(error)}`,
|
|
600
|
-
isError: true
|
|
601
|
-
};
|
|
602
|
-
}
|
|
603
|
-
}
|
|
604
|
-
};
|
|
605
|
-
|
|
606
43
|
// src/cli/format.ts
|
|
607
44
|
function summarizeArgs(args, maxLength = 120) {
|
|
608
45
|
let json;
|
|
@@ -666,92 +103,30 @@ function buildDisplayMessages(messages) {
|
|
|
666
103
|
}
|
|
667
104
|
|
|
668
105
|
// src/cli/mentions.ts
|
|
669
|
-
import { readFile
|
|
670
|
-
import
|
|
671
|
-
|
|
672
|
-
// src/tools/fs/util.ts
|
|
673
|
-
import { readdir, stat } from "fs/promises";
|
|
674
|
-
import path3 from "path";
|
|
675
|
-
var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist"]);
|
|
676
|
-
async function walkFiles(root, skipDirs = SKIP_DIRS) {
|
|
677
|
-
const out = [];
|
|
678
|
-
async function walk(dir, relBase) {
|
|
679
|
-
const entries = await readdir(dir, { withFileTypes: true }).catch(() => null);
|
|
680
|
-
if (!entries) {
|
|
681
|
-
return;
|
|
682
|
-
}
|
|
683
|
-
for (const entry of entries) {
|
|
684
|
-
const abs = path3.join(dir, entry.name);
|
|
685
|
-
const rel = relBase ? `${relBase}/${entry.name}` : entry.name;
|
|
686
|
-
if (entry.isDirectory()) {
|
|
687
|
-
if (!skipDirs.has(entry.name)) {
|
|
688
|
-
await walk(abs, rel);
|
|
689
|
-
}
|
|
690
|
-
} else if (entry.isFile()) {
|
|
691
|
-
try {
|
|
692
|
-
const st = await stat(abs);
|
|
693
|
-
out.push({ abs, rel, mtimeMs: st.mtimeMs });
|
|
694
|
-
} catch {
|
|
695
|
-
}
|
|
696
|
-
}
|
|
697
|
-
}
|
|
698
|
-
}
|
|
699
|
-
await walk(root, "");
|
|
700
|
-
return out;
|
|
701
|
-
}
|
|
702
|
-
var REGEX_SPECIALS = /[.+^${}()|[\]\\]/g;
|
|
703
|
-
function globToRegExp(pattern) {
|
|
704
|
-
let re = "";
|
|
705
|
-
let i = 0;
|
|
706
|
-
while (i < pattern.length) {
|
|
707
|
-
const c = pattern.charAt(i);
|
|
708
|
-
if (c === "*") {
|
|
709
|
-
if (pattern.charAt(i + 1) === "*") {
|
|
710
|
-
i += 2;
|
|
711
|
-
if (pattern.charAt(i) === "/") {
|
|
712
|
-
i += 1;
|
|
713
|
-
re += "(?:[^/]+/)*";
|
|
714
|
-
} else {
|
|
715
|
-
re += ".*";
|
|
716
|
-
}
|
|
717
|
-
} else {
|
|
718
|
-
re += "[^/]*";
|
|
719
|
-
i += 1;
|
|
720
|
-
}
|
|
721
|
-
} else if (c === "?") {
|
|
722
|
-
re += "[^/]";
|
|
723
|
-
i += 1;
|
|
724
|
-
} else {
|
|
725
|
-
re += c.replace(REGEX_SPECIALS, "\\$&");
|
|
726
|
-
i += 1;
|
|
727
|
-
}
|
|
728
|
-
}
|
|
729
|
-
return new RegExp(`^${re}$`);
|
|
730
|
-
}
|
|
731
|
-
function matchesGlob(pattern, relPath) {
|
|
732
|
-
const normalized = pattern.replace(/\\/g, "/");
|
|
733
|
-
if (!normalized.includes("/")) {
|
|
734
|
-
const base = relPath.split("/").pop() ?? relPath;
|
|
735
|
-
return globToRegExp(normalized).test(base);
|
|
736
|
-
}
|
|
737
|
-
return globToRegExp(normalized).test(relPath);
|
|
738
|
-
}
|
|
739
|
-
function isSensitivePath2(filePath) {
|
|
740
|
-
const base = path3.basename(filePath);
|
|
741
|
-
if (base === ".env.example" || base === ".env.sample" || base === ".env.template") {
|
|
742
|
-
return false;
|
|
743
|
-
}
|
|
744
|
-
if (base === ".env" || base.startsWith(".env.")) {
|
|
745
|
-
return true;
|
|
746
|
-
}
|
|
747
|
-
if (base === "id_rsa" || base.endsWith(".pem")) {
|
|
748
|
-
return true;
|
|
749
|
-
}
|
|
750
|
-
return false;
|
|
751
|
-
}
|
|
752
|
-
|
|
753
|
-
// src/cli/mentions.ts
|
|
106
|
+
import { readFile, stat } from "fs/promises";
|
|
107
|
+
import path from "path";
|
|
754
108
|
var MAX_MENTION_BYTES = 100 * 1024;
|
|
109
|
+
var MAX_IMAGE_BYTES = 5 * 1024 * 1024;
|
|
110
|
+
var IMAGE_MIME_TYPES = {
|
|
111
|
+
".png": "image/png",
|
|
112
|
+
".jpg": "image/jpeg",
|
|
113
|
+
".jpeg": "image/jpeg",
|
|
114
|
+
".gif": "image/gif",
|
|
115
|
+
".webp": "image/webp"
|
|
116
|
+
};
|
|
117
|
+
function imageMimeType(filePath) {
|
|
118
|
+
return IMAGE_MIME_TYPES[path.extname(filePath).toLowerCase()] ?? null;
|
|
119
|
+
}
|
|
120
|
+
async function readImageInput(filePath, cwd) {
|
|
121
|
+
const mimeType = imageMimeType(filePath);
|
|
122
|
+
if (!mimeType) return null;
|
|
123
|
+
const abs = path.resolve(cwd, filePath);
|
|
124
|
+
const st = await stat(abs).catch(() => null);
|
|
125
|
+
if (!st || !st.isFile() || st.size > MAX_IMAGE_BYTES) return null;
|
|
126
|
+
const buf = await readFile(abs).catch(() => null);
|
|
127
|
+
if (!buf) return null;
|
|
128
|
+
return { path: filePath, mimeType, data: buf.toString("base64") };
|
|
129
|
+
}
|
|
755
130
|
var MENTION_RE = /(?<=^|\s)@([A-Za-z]:[\\/][A-Za-z0-9._\-/\\]*|[A-Za-z0-9._\-/\\]+)/g;
|
|
756
131
|
function parseMentions(text) {
|
|
757
132
|
const mentions = [];
|
|
@@ -787,18 +162,39 @@ function isBinaryContent(buf) {
|
|
|
787
162
|
async function resolveMentions(text, cwd) {
|
|
788
163
|
const { cleanText, mentions } = parseMentions(text);
|
|
789
164
|
if (mentions.length === 0) {
|
|
790
|
-
return { input: text, attached: [], skipped: [] };
|
|
165
|
+
return { input: text, attached: [], skipped: [], images: [] };
|
|
791
166
|
}
|
|
792
167
|
const blocks = [];
|
|
793
168
|
const attached = [];
|
|
794
169
|
const skipped = [];
|
|
170
|
+
const images = [];
|
|
795
171
|
for (const mention of mentions) {
|
|
796
|
-
const abs =
|
|
797
|
-
if (
|
|
172
|
+
const abs = path.resolve(cwd, mention);
|
|
173
|
+
if (isSensitivePath(abs)) {
|
|
798
174
|
skipped.push({ path: mention, reason: "sensitive file" });
|
|
799
175
|
continue;
|
|
800
176
|
}
|
|
801
|
-
const
|
|
177
|
+
const mimeType = imageMimeType(mention);
|
|
178
|
+
if (mimeType) {
|
|
179
|
+
const st2 = await stat(abs).catch(() => null);
|
|
180
|
+
if (!st2 || !st2.isFile()) {
|
|
181
|
+
skipped.push({ path: mention, reason: st2 ? "is a directory" : "file not found" });
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
if (st2.size > MAX_IMAGE_BYTES) {
|
|
185
|
+
skipped.push({ path: mention, reason: "image too large" });
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
const buf2 = await readFile(abs).catch(() => null);
|
|
189
|
+
if (!buf2) {
|
|
190
|
+
skipped.push({ path: mention, reason: "unreadable" });
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
attached.push(mention);
|
|
194
|
+
images.push({ path: mention, mimeType, data: buf2.toString("base64") });
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
const st = await stat(abs).catch(() => null);
|
|
802
198
|
if (!st) {
|
|
803
199
|
skipped.push({ path: mention, reason: "file not found" });
|
|
804
200
|
continue;
|
|
@@ -811,7 +207,7 @@ async function resolveMentions(text, cwd) {
|
|
|
811
207
|
skipped.push({ path: mention, reason: `exceeds ${MAX_MENTION_BYTES / 1024}KB limit` });
|
|
812
208
|
continue;
|
|
813
209
|
}
|
|
814
|
-
const buf = await
|
|
210
|
+
const buf = await readFile(abs).catch(() => null);
|
|
815
211
|
if (!buf) {
|
|
816
212
|
skipped.push({ path: mention, reason: "unreadable" });
|
|
817
213
|
continue;
|
|
@@ -826,7 +222,7 @@ ${buf.toString("utf8")}
|
|
|
826
222
|
--- end ---`);
|
|
827
223
|
}
|
|
828
224
|
const input = [cleanText, ...blocks].filter((s) => s.length > 0).join("\n\n") || text;
|
|
829
|
-
return { input, attached, skipped };
|
|
225
|
+
return { input, attached, skipped, images };
|
|
830
226
|
}
|
|
831
227
|
|
|
832
228
|
// src/cli/print-json.ts
|
|
@@ -873,31 +269,13 @@ var UsageTracker = class {
|
|
|
873
269
|
};
|
|
874
270
|
|
|
875
271
|
// src/cli/repl.tsx
|
|
876
|
-
import { Box as
|
|
877
|
-
import { useCallback, useEffect, useMemo, useRef as useRef2, useState as useState2 } from "react";
|
|
272
|
+
import { Box as Box9, Text as Text10, render, useApp, useInput as useInput5 } from "ink";
|
|
273
|
+
import { useCallback, useEffect as useEffect2, useMemo, useRef as useRef2, useState as useState2 } from "react";
|
|
878
274
|
|
|
879
275
|
// src/config/save.ts
|
|
880
276
|
import fs from "fs";
|
|
881
|
-
import
|
|
277
|
+
import path2 from "path";
|
|
882
278
|
import { parse, stringify } from "smol-toml";
|
|
883
|
-
|
|
884
|
-
// src/config/paths.ts
|
|
885
|
-
import os from "os";
|
|
886
|
-
import path5 from "path";
|
|
887
|
-
function starHome() {
|
|
888
|
-
return process.env.STAR_HOME ?? path5.join(os.homedir(), ".star-cli");
|
|
889
|
-
}
|
|
890
|
-
function globalConfigPath() {
|
|
891
|
-
return path5.join(starHome(), "config.toml");
|
|
892
|
-
}
|
|
893
|
-
function sessionsDir() {
|
|
894
|
-
return path5.join(starHome(), "sessions");
|
|
895
|
-
}
|
|
896
|
-
function projectConfigPath(cwd) {
|
|
897
|
-
return path5.join(cwd, ".star", "config.toml");
|
|
898
|
-
}
|
|
899
|
-
|
|
900
|
-
// src/config/save.ts
|
|
901
279
|
async function addAllowRule(rule) {
|
|
902
280
|
const filePath = globalConfigPath();
|
|
903
281
|
let raw = {};
|
|
@@ -912,7 +290,7 @@ async function addAllowRule(rule) {
|
|
|
912
290
|
if (allow.includes(rule)) return false;
|
|
913
291
|
permissions.allow = [...allow, rule];
|
|
914
292
|
raw.permissions = permissions;
|
|
915
|
-
await fs.promises.mkdir(
|
|
293
|
+
await fs.promises.mkdir(path2.dirname(filePath), { recursive: true });
|
|
916
294
|
await fs.promises.writeFile(filePath, stringify(raw), "utf8");
|
|
917
295
|
return true;
|
|
918
296
|
}
|
|
@@ -926,10 +304,79 @@ async function savePermissionMode(mode) {
|
|
|
926
304
|
if (error.code !== "ENOENT") throw error;
|
|
927
305
|
}
|
|
928
306
|
raw.permissionMode = mode;
|
|
929
|
-
await fs.promises.mkdir(
|
|
307
|
+
await fs.promises.mkdir(path2.dirname(filePath), { recursive: true });
|
|
930
308
|
await fs.promises.writeFile(filePath, stringify(raw), "utf8");
|
|
931
309
|
}
|
|
932
310
|
|
|
311
|
+
// src/config/schema.ts
|
|
312
|
+
import { z } from "zod";
|
|
313
|
+
var ProviderConfigSchema = z.object({
|
|
314
|
+
name: z.string(),
|
|
315
|
+
protocol: z.enum(["openai-compatible", "anthropic", "openai-responses"]).default("openai-compatible"),
|
|
316
|
+
baseURL: z.string(),
|
|
317
|
+
apiKeyEnv: z.string().optional(),
|
|
318
|
+
apiKey: z.string().optional(),
|
|
319
|
+
headers: z.record(z.string()).optional()
|
|
320
|
+
});
|
|
321
|
+
var ModelConfigSchema = z.object({
|
|
322
|
+
name: z.string(),
|
|
323
|
+
provider: z.string(),
|
|
324
|
+
model: z.string(),
|
|
325
|
+
maxTokens: z.number().int().positive().optional(),
|
|
326
|
+
// Per-model context window; overrides the top-level contextMaxTokens for
|
|
327
|
+
// compaction and the status bar when set.
|
|
328
|
+
contextMaxTokens: z.number().int().positive().optional(),
|
|
329
|
+
// Cost estimation needs BOTH prices (USD per 1M tokens); with only one set
|
|
330
|
+
// the model is treated as unpriced.
|
|
331
|
+
promptPrice: z.number().nonnegative().optional(),
|
|
332
|
+
completionPrice: z.number().nonnegative().optional()
|
|
333
|
+
});
|
|
334
|
+
var PermissionsConfigSchema = z.object({
|
|
335
|
+
allow: z.array(z.string()).default([])
|
|
336
|
+
});
|
|
337
|
+
var HookConfigSchema = z.object({
|
|
338
|
+
event: z.enum(["PreToolUse", "PostToolUse", "Stop"]),
|
|
339
|
+
matcher: z.string().refine(
|
|
340
|
+
(pattern) => {
|
|
341
|
+
try {
|
|
342
|
+
new RegExp(pattern);
|
|
343
|
+
return true;
|
|
344
|
+
} catch {
|
|
345
|
+
return false;
|
|
346
|
+
}
|
|
347
|
+
},
|
|
348
|
+
{ message: "invalid regular expression" }
|
|
349
|
+
).optional(),
|
|
350
|
+
command: z.string().min(1),
|
|
351
|
+
timeoutSec: z.number().positive().default(30)
|
|
352
|
+
});
|
|
353
|
+
var ConfigSchema = z.object({
|
|
354
|
+
defaultModel: z.string().default(""),
|
|
355
|
+
permissionMode: z.enum(["auto", "ask", "readonly", "yolo", "plan"]).default("ask"),
|
|
356
|
+
providers: z.array(ProviderConfigSchema).default([]),
|
|
357
|
+
models: z.array(ModelConfigSchema).default([]),
|
|
358
|
+
maxSteps: z.number().int().positive().default(50),
|
|
359
|
+
contextMaxTokens: z.number().int().positive().default(1e5),
|
|
360
|
+
// Seconds without any stream part before a stalled response is ended
|
|
361
|
+
// gracefully (some relays never send the terminal chunks). Applies once
|
|
362
|
+
// streaming has started; the first part gets a longer, fixed allowance.
|
|
363
|
+
streamIdleTimeoutSec: z.number().positive().default(20),
|
|
364
|
+
contextCompaction: z.enum(["summary", "truncate"]).default("summary"),
|
|
365
|
+
// Optional per-session dollar budget; when set, the REPL stops the session
|
|
366
|
+
// once the estimated cost for the session reaches this amount.
|
|
367
|
+
sessionBudgetUsd: z.number().positive().optional(),
|
|
368
|
+
// Terminal bell (REPL only): ring when a turn takes longer than
|
|
369
|
+
// notifyBellThresholdSec, and when a background task finishes.
|
|
370
|
+
// STAR_NO_NOTIFY=1 disables without touching the config.
|
|
371
|
+
notifyBell: z.boolean().default(true),
|
|
372
|
+
notifyBellThresholdSec: z.number().positive().default(10),
|
|
373
|
+
permissions: PermissionsConfigSchema.default({ allow: [] }),
|
|
374
|
+
hooks: z.array(HookConfigSchema).default([])
|
|
375
|
+
});
|
|
376
|
+
function contextWindowTokens(config, modelName) {
|
|
377
|
+
return config.models.find((m) => m.name === modelName)?.contextMaxTokens ?? config.contextMaxTokens;
|
|
378
|
+
}
|
|
379
|
+
|
|
933
380
|
// src/llm/provider.ts
|
|
934
381
|
import { createAnthropic } from "@ai-sdk/anthropic";
|
|
935
382
|
import { createOpenAI } from "@ai-sdk/openai";
|
|
@@ -992,9 +439,69 @@ function createModel(config, modelName) {
|
|
|
992
439
|
}
|
|
993
440
|
}
|
|
994
441
|
|
|
995
|
-
// src/session/
|
|
442
|
+
// src/session/checkpoints.ts
|
|
996
443
|
import fs2 from "fs/promises";
|
|
997
|
-
import
|
|
444
|
+
import path3 from "path";
|
|
445
|
+
function checkpointsDir(sessionDir) {
|
|
446
|
+
return path3.join(sessionDir, "checkpoints");
|
|
447
|
+
}
|
|
448
|
+
function indexPath(sessionDir) {
|
|
449
|
+
return path3.join(checkpointsDir(sessionDir), "index.json");
|
|
450
|
+
}
|
|
451
|
+
function checkpointContentPath(sessionDir, id) {
|
|
452
|
+
return path3.join(checkpointsDir(sessionDir), `${id}.snapshot`);
|
|
453
|
+
}
|
|
454
|
+
async function listCheckpointRecords(sessionDir) {
|
|
455
|
+
let raw;
|
|
456
|
+
try {
|
|
457
|
+
raw = await fs2.readFile(indexPath(sessionDir), "utf8");
|
|
458
|
+
} catch {
|
|
459
|
+
return [];
|
|
460
|
+
}
|
|
461
|
+
try {
|
|
462
|
+
const parsed = JSON.parse(raw);
|
|
463
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
464
|
+
} catch {
|
|
465
|
+
return [];
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
async function appendCheckpointRecord(sessionDir, record, content) {
|
|
469
|
+
await fs2.mkdir(checkpointsDir(sessionDir), { recursive: true });
|
|
470
|
+
if (record.existed && content !== null) {
|
|
471
|
+
await fs2.writeFile(checkpointContentPath(sessionDir, record.id), content, "utf8");
|
|
472
|
+
}
|
|
473
|
+
const records = await listCheckpointRecords(sessionDir);
|
|
474
|
+
records.push(record);
|
|
475
|
+
await fs2.writeFile(indexPath(sessionDir), JSON.stringify(records, null, 2));
|
|
476
|
+
}
|
|
477
|
+
async function removeCheckpointRecords(sessionDir, ids) {
|
|
478
|
+
if (ids.length === 0) return;
|
|
479
|
+
const drop = new Set(ids);
|
|
480
|
+
const records = await listCheckpointRecords(sessionDir);
|
|
481
|
+
const kept = records.filter((record) => !drop.has(record.id));
|
|
482
|
+
if (kept.length === records.length) return;
|
|
483
|
+
await fs2.mkdir(checkpointsDir(sessionDir), { recursive: true });
|
|
484
|
+
await fs2.writeFile(indexPath(sessionDir), JSON.stringify(kept, null, 2));
|
|
485
|
+
for (const id of ids) {
|
|
486
|
+
await fs2.rm(checkpointContentPath(sessionDir, id), { force: true });
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
async function loadSessionSnapshots(sessionDir) {
|
|
490
|
+
const records = await listCheckpointRecords(sessionDir);
|
|
491
|
+
return records.map((record) => ({
|
|
492
|
+
...record,
|
|
493
|
+
content: null,
|
|
494
|
+
contentFile: record.existed ? checkpointContentPath(sessionDir, record.id) : void 0
|
|
495
|
+
}));
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// src/session/clear.ts
|
|
499
|
+
import fs4 from "fs/promises";
|
|
500
|
+
import path5 from "path";
|
|
501
|
+
|
|
502
|
+
// src/session/store.ts
|
|
503
|
+
import fs3 from "fs/promises";
|
|
504
|
+
import path4 from "path";
|
|
998
505
|
function generateId(now) {
|
|
999
506
|
const pad = (n) => String(n).padStart(2, "0");
|
|
1000
507
|
const stamp = `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`;
|
|
@@ -1005,13 +512,9 @@ function debugWarn(message) {
|
|
|
1005
512
|
if (process.env.STAR_DEBUG === "1") process.stderr.write(`[star-cli] ${message}
|
|
1006
513
|
`);
|
|
1007
514
|
}
|
|
1008
|
-
function
|
|
1009
|
-
const
|
|
1010
|
-
|
|
1011
|
-
if (Array.isArray(content)) {
|
|
1012
|
-
return content.filter((part) => part.type === "text").map((part) => "text" in part ? part.text : "").join(" ");
|
|
1013
|
-
}
|
|
1014
|
-
return "";
|
|
515
|
+
function dayKey(date) {
|
|
516
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
517
|
+
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
|
|
1015
518
|
}
|
|
1016
519
|
var SessionStore = class _SessionStore {
|
|
1017
520
|
id;
|
|
@@ -1020,13 +523,13 @@ var SessionStore = class _SessionStore {
|
|
|
1020
523
|
initialized = false;
|
|
1021
524
|
constructor(id) {
|
|
1022
525
|
this.id = id;
|
|
1023
|
-
this.dir =
|
|
526
|
+
this.dir = path4.join(sessionsDir(), id);
|
|
1024
527
|
}
|
|
1025
528
|
metaPath() {
|
|
1026
|
-
return
|
|
529
|
+
return path4.join(this.dir, "meta.json");
|
|
1027
530
|
}
|
|
1028
531
|
messagesPath() {
|
|
1029
|
-
return
|
|
532
|
+
return path4.join(this.dir, "messages.jsonl");
|
|
1030
533
|
}
|
|
1031
534
|
static async create(cwd, model) {
|
|
1032
535
|
const store = new _SessionStore(generateId(/* @__PURE__ */ new Date()));
|
|
@@ -1037,7 +540,7 @@ var SessionStore = class _SessionStore {
|
|
|
1037
540
|
const store = new _SessionStore(id);
|
|
1038
541
|
let raw;
|
|
1039
542
|
try {
|
|
1040
|
-
raw = await
|
|
543
|
+
raw = await fs3.readFile(store.metaPath(), "utf8");
|
|
1041
544
|
} catch {
|
|
1042
545
|
return null;
|
|
1043
546
|
}
|
|
@@ -1053,14 +556,14 @@ var SessionStore = class _SessionStore {
|
|
|
1053
556
|
static async list(cwd) {
|
|
1054
557
|
let entries;
|
|
1055
558
|
try {
|
|
1056
|
-
entries = await
|
|
559
|
+
entries = await fs3.readdir(sessionsDir());
|
|
1057
560
|
} catch {
|
|
1058
561
|
return [];
|
|
1059
562
|
}
|
|
1060
563
|
const metas = [];
|
|
1061
564
|
for (const entry of entries) {
|
|
1062
565
|
try {
|
|
1063
|
-
const raw = await
|
|
566
|
+
const raw = await fs3.readFile(path4.join(sessionsDir(), entry, "meta.json"), "utf8");
|
|
1064
567
|
const meta = JSON.parse(raw);
|
|
1065
568
|
if (meta.id === entry && (cwd === void 0 || meta.cwd === cwd)) metas.push(meta);
|
|
1066
569
|
} catch {
|
|
@@ -1075,7 +578,7 @@ var SessionStore = class _SessionStore {
|
|
|
1075
578
|
this.initialized = true;
|
|
1076
579
|
return;
|
|
1077
580
|
}
|
|
1078
|
-
await
|
|
581
|
+
await fs3.mkdir(this.dir, { recursive: true });
|
|
1079
582
|
const meta = {
|
|
1080
583
|
id: this.id,
|
|
1081
584
|
title: "",
|
|
@@ -1084,33 +587,30 @@ var SessionStore = class _SessionStore {
|
|
|
1084
587
|
createdAt: pending.createdAt,
|
|
1085
588
|
updatedAt: pending.createdAt
|
|
1086
589
|
};
|
|
1087
|
-
await
|
|
590
|
+
await fs3.writeFile(this.metaPath(), JSON.stringify(meta, null, 2));
|
|
1088
591
|
this.initialized = true;
|
|
1089
592
|
}
|
|
1090
593
|
async append(message) {
|
|
1091
594
|
await this.ensureInitialized();
|
|
1092
|
-
await
|
|
595
|
+
await fs3.appendFile(this.messagesPath(), `${JSON.stringify(message)}
|
|
1093
596
|
`);
|
|
1094
597
|
const meta = await this.meta();
|
|
1095
598
|
meta.updatedAt = Date.now();
|
|
1096
|
-
|
|
1097
|
-
meta.title = messageText(message).slice(0, 60);
|
|
1098
|
-
}
|
|
1099
|
-
await fs2.writeFile(this.metaPath(), JSON.stringify(meta, null, 2));
|
|
599
|
+
await fs3.writeFile(this.metaPath(), JSON.stringify(meta, null, 2));
|
|
1100
600
|
}
|
|
1101
601
|
async replaceMessages(messages) {
|
|
1102
602
|
await this.ensureInitialized();
|
|
1103
603
|
const content = messages.map((message) => JSON.stringify(message)).join("\n");
|
|
1104
|
-
await
|
|
604
|
+
await fs3.writeFile(this.messagesPath(), content ? `${content}
|
|
1105
605
|
` : "");
|
|
1106
606
|
const meta = await this.meta();
|
|
1107
607
|
meta.updatedAt = Date.now();
|
|
1108
|
-
await
|
|
608
|
+
await fs3.writeFile(this.metaPath(), JSON.stringify(meta, null, 2));
|
|
1109
609
|
}
|
|
1110
610
|
async messages() {
|
|
1111
611
|
let raw;
|
|
1112
612
|
try {
|
|
1113
|
-
raw = await
|
|
613
|
+
raw = await fs3.readFile(this.messagesPath(), "utf8");
|
|
1114
614
|
} catch {
|
|
1115
615
|
return [];
|
|
1116
616
|
}
|
|
@@ -1133,7 +633,7 @@ var SessionStore = class _SessionStore {
|
|
|
1133
633
|
async meta() {
|
|
1134
634
|
await this.ensureInitialized();
|
|
1135
635
|
try {
|
|
1136
|
-
const raw = await
|
|
636
|
+
const raw = await fs3.readFile(this.metaPath(), "utf8");
|
|
1137
637
|
return JSON.parse(raw);
|
|
1138
638
|
} catch {
|
|
1139
639
|
debugWarn(`session ${this.id}: unreadable meta.json, using defaults`);
|
|
@@ -1145,7 +645,17 @@ var SessionStore = class _SessionStore {
|
|
|
1145
645
|
const meta = await this.meta();
|
|
1146
646
|
meta.title = title;
|
|
1147
647
|
meta.updatedAt = Date.now();
|
|
1148
|
-
await
|
|
648
|
+
await fs3.writeFile(this.metaPath(), JSON.stringify(meta, null, 2));
|
|
649
|
+
}
|
|
650
|
+
async appendCheckpoint(record, content) {
|
|
651
|
+
await this.ensureInitialized();
|
|
652
|
+
await appendCheckpointRecord(this.dir, record, content);
|
|
653
|
+
}
|
|
654
|
+
async listCheckpoints() {
|
|
655
|
+
return listCheckpointRecords(this.dir);
|
|
656
|
+
}
|
|
657
|
+
async removeCheckpoints(ids) {
|
|
658
|
+
await removeCheckpointRecords(this.dir, ids);
|
|
1149
659
|
}
|
|
1150
660
|
async addUsage(delta) {
|
|
1151
661
|
await this.ensureInitialized();
|
|
@@ -1161,20 +671,83 @@ var SessionStore = class _SessionStore {
|
|
|
1161
671
|
usage.completionTokens += delta.completionTokens;
|
|
1162
672
|
usage.totalTokens += delta.totalTokens;
|
|
1163
673
|
meta.usage = usage;
|
|
1164
|
-
|
|
674
|
+
const day = dayKey(/* @__PURE__ */ new Date());
|
|
675
|
+
const byDay = meta.usageByDay ?? {};
|
|
676
|
+
const bucket = byDay[day] ?? { promptTokens: 0, completionTokens: 0, totalTokens: 0 };
|
|
677
|
+
bucket.promptTokens += delta.promptTokens;
|
|
678
|
+
bucket.completionTokens += delta.completionTokens;
|
|
679
|
+
bucket.totalTokens += delta.totalTokens;
|
|
680
|
+
byDay[day] = bucket;
|
|
681
|
+
meta.usageByDay = byDay;
|
|
682
|
+
await fs3.writeFile(this.metaPath(), JSON.stringify(meta, null, 2));
|
|
1165
683
|
}
|
|
1166
684
|
};
|
|
1167
685
|
|
|
1168
|
-
// src/session/
|
|
1169
|
-
async function
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
686
|
+
// src/session/clear.ts
|
|
687
|
+
async function clearSessions(cwd, excludeId) {
|
|
688
|
+
if (cwd !== void 0) {
|
|
689
|
+
const metas = await SessionStore.list(cwd);
|
|
690
|
+
let removed2 = 0;
|
|
691
|
+
for (const meta of metas) {
|
|
692
|
+
if (meta.id === excludeId) continue;
|
|
693
|
+
await fs4.rm(path5.join(sessionsDir(), meta.id), { recursive: true, force: true });
|
|
694
|
+
removed2 += 1;
|
|
695
|
+
}
|
|
696
|
+
return removed2;
|
|
1176
697
|
}
|
|
1177
|
-
|
|
698
|
+
let entries;
|
|
699
|
+
try {
|
|
700
|
+
entries = await fs4.readdir(sessionsDir());
|
|
701
|
+
} catch {
|
|
702
|
+
return 0;
|
|
703
|
+
}
|
|
704
|
+
let removed = 0;
|
|
705
|
+
for (const entry of entries) {
|
|
706
|
+
if (entry === excludeId) continue;
|
|
707
|
+
await fs4.rm(path5.join(sessionsDir(), entry), { recursive: true, force: true });
|
|
708
|
+
removed += 1;
|
|
709
|
+
}
|
|
710
|
+
return removed;
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
// src/session/list.ts
|
|
714
|
+
import fs5 from "fs/promises";
|
|
715
|
+
import path6 from "path";
|
|
716
|
+
async function countMessages(id) {
|
|
717
|
+
let raw;
|
|
718
|
+
try {
|
|
719
|
+
raw = await fs5.readFile(path6.join(sessionsDir(), id, "messages.jsonl"), "utf8");
|
|
720
|
+
} catch {
|
|
721
|
+
return 0;
|
|
722
|
+
}
|
|
723
|
+
let count = 0;
|
|
724
|
+
for (const line of raw.split("\n")) {
|
|
725
|
+
if (line.trim()) count += 1;
|
|
726
|
+
}
|
|
727
|
+
return count;
|
|
728
|
+
}
|
|
729
|
+
async function listSessionEntries(cwd) {
|
|
730
|
+
const metas = await SessionStore.list(cwd);
|
|
731
|
+
return Promise.all(
|
|
732
|
+
metas.map(async (meta) => ({ meta, messageCount: await countMessages(meta.id) }))
|
|
733
|
+
);
|
|
734
|
+
}
|
|
735
|
+
async function findLatestSession(cwd) {
|
|
736
|
+
const [latest] = await SessionStore.list(cwd);
|
|
737
|
+
return latest ?? null;
|
|
738
|
+
}
|
|
739
|
+
function shortSessionId(id) {
|
|
740
|
+
const dash = id.lastIndexOf("-");
|
|
741
|
+
return dash === -1 ? id : id.slice(dash + 1);
|
|
742
|
+
}
|
|
743
|
+
async function resolveSessionId(query) {
|
|
744
|
+
const metas = await SessionStore.list();
|
|
745
|
+
const exact = metas.find((meta) => meta.id === query);
|
|
746
|
+
if (exact) return exact.id;
|
|
747
|
+
const matches = metas.filter(
|
|
748
|
+
(meta) => shortSessionId(meta.id) === query || meta.id.startsWith(query)
|
|
749
|
+
);
|
|
750
|
+
return matches.length === 1 ? matches[0]?.id ?? null : null;
|
|
1178
751
|
}
|
|
1179
752
|
function relativeTime(timestamp) {
|
|
1180
753
|
const diff = Date.now() - timestamp;
|
|
@@ -1189,1012 +762,224 @@ function relativeTime(timestamp) {
|
|
|
1189
762
|
if (months < 12) return `${months} \u4E2A\u6708\u524D`;
|
|
1190
763
|
return `${Math.floor(months / 12)} \u5E74\u524D`;
|
|
1191
764
|
}
|
|
1192
|
-
function
|
|
1193
|
-
return
|
|
765
|
+
function formatSessionEntries(entries, options = {}) {
|
|
766
|
+
return entries.map(({ meta, messageCount }) => {
|
|
1194
767
|
const title = meta.title || "(\u65E0\u6807\u9898)";
|
|
1195
|
-
|
|
768
|
+
const base = `${shortSessionId(meta.id)} ${title} ${messageCount} \u6761\u6D88\u606F \u66F4\u65B0\u4E8E ${relativeTime(meta.updatedAt)}`;
|
|
769
|
+
return options.showCwd ? `${base} [${meta.cwd}]` : base;
|
|
1196
770
|
}).join("\n");
|
|
1197
771
|
}
|
|
1198
772
|
|
|
1199
|
-
// src/
|
|
1200
|
-
function
|
|
1201
|
-
const
|
|
1202
|
-
if (
|
|
1203
|
-
const
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
function formatStatus(task) {
|
|
1208
|
-
const duration = formatDuration(task.startedAt, task.endedAt);
|
|
1209
|
-
if (task.status === "running") return `running ${duration}`;
|
|
1210
|
-
const exit = task.exitCode === null || task.exitCode === void 0 ? "" : `, exit ${task.exitCode}`;
|
|
1211
|
-
return `${task.status}${exit} after ${duration}`;
|
|
1212
|
-
}
|
|
1213
|
-
function formatTaskLine(task) {
|
|
1214
|
-
const label = task.description ? `${task.description} (${task.command})` : task.command;
|
|
1215
|
-
return `${task.id} [${formatStatus(task)}] ${label}`;
|
|
1216
|
-
}
|
|
1217
|
-
function formatTaskList(tasks) {
|
|
1218
|
-
if (tasks.length === 0) return "No background tasks.";
|
|
1219
|
-
return `Background tasks:
|
|
1220
|
-
${tasks.map(formatTaskLine).join("\n")}`;
|
|
1221
|
-
}
|
|
1222
|
-
function formatTaskStarted(task) {
|
|
1223
|
-
const label = task.description ? `${task.description} (${task.command})` : task.command;
|
|
1224
|
-
return `Background task ${task.id} started: ${label}`;
|
|
1225
|
-
}
|
|
1226
|
-
function formatTaskFinished(task) {
|
|
1227
|
-
const exit = task.exitCode === null || task.exitCode === void 0 ? "" : ` (exit ${task.exitCode})`;
|
|
1228
|
-
switch (task.status) {
|
|
1229
|
-
case "completed":
|
|
1230
|
-
return `Background task ${task.id} finished${exit}: ${task.command}`;
|
|
1231
|
-
case "failed":
|
|
1232
|
-
return `Background task ${task.id} failed${exit}: ${task.command}`;
|
|
1233
|
-
case "timed_out":
|
|
1234
|
-
return `Background task ${task.id} timed out: ${task.command}`;
|
|
1235
|
-
case "stopped":
|
|
1236
|
-
return `Background task ${task.id} stopped: ${task.command}`;
|
|
1237
|
-
default:
|
|
1238
|
-
return `Background task ${task.id}: ${task.command}`;
|
|
773
|
+
// src/session/resume.ts
|
|
774
|
+
async function resumeSession(id) {
|
|
775
|
+
const store = await SessionStore.open(id);
|
|
776
|
+
if (!store) return null;
|
|
777
|
+
const [meta, loaded] = await Promise.all([store.meta(), store.messages()]);
|
|
778
|
+
const messages = reconcileToolCalls(loaded);
|
|
779
|
+
if (messages.length !== loaded.length) {
|
|
780
|
+
await store.replaceMessages(messages);
|
|
1239
781
|
}
|
|
782
|
+
return { meta, messages };
|
|
1240
783
|
}
|
|
1241
784
|
|
|
1242
|
-
// src/
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
import path8 from "path";
|
|
1250
|
-
import { z } from "zod";
|
|
1251
|
-
var MAX_OUTPUT = 3e4;
|
|
1252
|
-
var DEFAULT_TIMEOUT = 120;
|
|
1253
|
-
var MAX_TIMEOUT = 600;
|
|
1254
|
-
function findOnPath(exe, exclude) {
|
|
1255
|
-
for (const dir of (process.env.PATH ?? "").split(path8.delimiter)) {
|
|
1256
|
-
if (!dir || exclude?.(dir)) {
|
|
1257
|
-
continue;
|
|
1258
|
-
}
|
|
1259
|
-
const full = path8.join(dir, exe);
|
|
1260
|
-
if (existsSync(full)) {
|
|
1261
|
-
return full;
|
|
1262
|
-
}
|
|
1263
|
-
}
|
|
1264
|
-
return null;
|
|
1265
|
-
}
|
|
1266
|
-
function resolveShell() {
|
|
1267
|
-
if (process.platform !== "win32") {
|
|
1268
|
-
return { shell: "sh", wrap: (c) => ["-c", c], label: "sh" };
|
|
1269
|
-
}
|
|
1270
|
-
const isWslStub = (dir) => /\\(system32|windowsapps)\\?$/i.test(dir.trim());
|
|
1271
|
-
const fromPath = findOnPath("bash.exe", isWslStub);
|
|
1272
|
-
if (fromPath) {
|
|
1273
|
-
return { shell: fromPath, wrap: (c) => ["-c", c], label: "bash" };
|
|
1274
|
-
}
|
|
1275
|
-
const gitExe = findOnPath("git.exe");
|
|
1276
|
-
if (gitExe) {
|
|
1277
|
-
const sibling = path8.join(path8.dirname(path8.dirname(gitExe)), "bin", "bash.exe");
|
|
1278
|
-
if (existsSync(sibling)) {
|
|
1279
|
-
return { shell: sibling, wrap: (c) => ["-c", c], label: "bash" };
|
|
1280
|
-
}
|
|
1281
|
-
}
|
|
1282
|
-
for (const candidate of [
|
|
1283
|
-
"C:\\Program Files\\Git\\bin\\bash.exe",
|
|
1284
|
-
"C:\\Program Files (x86)\\Git\\bin\\bash.exe",
|
|
1285
|
-
"D:\\Git\\bin\\bash.exe"
|
|
1286
|
-
]) {
|
|
1287
|
-
if (existsSync(candidate)) {
|
|
1288
|
-
return { shell: candidate, wrap: (c) => ["-c", c], label: "bash" };
|
|
1289
|
-
}
|
|
785
|
+
// src/cli/cost.ts
|
|
786
|
+
function formatDollars(cost) {
|
|
787
|
+
let s = cost.toFixed(4).replace(/0+$/, "");
|
|
788
|
+
if (s.endsWith(".")) {
|
|
789
|
+
s += "00";
|
|
790
|
+
} else if (s.split(".")[1]?.length === 1) {
|
|
791
|
+
s += "0";
|
|
1290
792
|
}
|
|
1291
|
-
|
|
1292
|
-
return { shell: comspec, wrap: (c) => ["/d", "/s", "/c", c], label: "cmd" };
|
|
793
|
+
return s;
|
|
1293
794
|
}
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
description: z.string().optional().describe("Short description of what the command does"),
|
|
1298
|
-
run_in_background: z.boolean().optional().describe(
|
|
1299
|
-
"Run the command in the background and return a task id immediately (default false). Use task_output/task_kill to inspect or stop it."
|
|
1300
|
-
)
|
|
1301
|
-
});
|
|
1302
|
-
function killTree(child) {
|
|
1303
|
-
if (process.platform === "win32" && child.pid) {
|
|
1304
|
-
spawn("taskkill", ["/pid", String(child.pid), "/t", "/f"], {
|
|
1305
|
-
windowsHide: true,
|
|
1306
|
-
stdio: "ignore"
|
|
1307
|
-
}).unref();
|
|
1308
|
-
return;
|
|
1309
|
-
}
|
|
1310
|
-
child.kill("SIGKILL");
|
|
1311
|
-
}
|
|
1312
|
-
function truncateMiddle(s) {
|
|
1313
|
-
if (s.length <= MAX_OUTPUT) {
|
|
1314
|
-
return s;
|
|
1315
|
-
}
|
|
1316
|
-
const half = Math.floor(MAX_OUTPUT / 2);
|
|
1317
|
-
const head = s.slice(0, half);
|
|
1318
|
-
const tail = s.slice(-half);
|
|
1319
|
-
return `${head}
|
|
1320
|
-
... [${s.length - MAX_OUTPUT} characters truncated] ...
|
|
1321
|
-
${tail}`;
|
|
1322
|
-
}
|
|
1323
|
-
var bashTool = {
|
|
1324
|
-
name: "bash",
|
|
1325
|
-
description: "Execute a shell command (Git Bash on Windows when available, otherwise cmd; sh elsewhere). Stdout and stderr are merged. Output is truncated to 30000 characters. Set run_in_background for long-running commands; it returns a task id for task_list/task_output/task_kill.",
|
|
1326
|
-
permission: "exec",
|
|
1327
|
-
parameters: schema,
|
|
1328
|
-
execute(args, ctx) {
|
|
1329
|
-
if (args.run_in_background) {
|
|
1330
|
-
const task = defaultTaskManager.start({
|
|
1331
|
-
command: args.command,
|
|
1332
|
-
description: args.description,
|
|
1333
|
-
cwd: ctx.cwd,
|
|
1334
|
-
timeoutSeconds: args.timeout
|
|
1335
|
-
});
|
|
1336
|
-
return Promise.resolve({
|
|
1337
|
-
content: `Background task started: ${task.id}
|
|
1338
|
-
command: ${args.command}
|
|
1339
|
-
description: ${args.description ?? "(none)"}`
|
|
1340
|
-
});
|
|
1341
|
-
}
|
|
1342
|
-
const timeoutSeconds = Math.min(args.timeout ?? DEFAULT_TIMEOUT, MAX_TIMEOUT);
|
|
1343
|
-
const spec = resolveShell();
|
|
1344
|
-
return new Promise((resolve) => {
|
|
1345
|
-
const child = spawn(spec.shell, spec.wrap(args.command), {
|
|
1346
|
-
cwd: ctx.cwd,
|
|
1347
|
-
windowsHide: true
|
|
1348
|
-
});
|
|
1349
|
-
let output = "";
|
|
1350
|
-
let settled = false;
|
|
1351
|
-
const finish = (result) => {
|
|
1352
|
-
if (settled) {
|
|
1353
|
-
return;
|
|
1354
|
-
}
|
|
1355
|
-
settled = true;
|
|
1356
|
-
clearTimeout(timer);
|
|
1357
|
-
ctx.abortSignal?.removeEventListener("abort", onAbort);
|
|
1358
|
-
resolve(result);
|
|
1359
|
-
};
|
|
1360
|
-
const onAbort = () => {
|
|
1361
|
-
killTree(child);
|
|
1362
|
-
finish({ content: `${truncateMiddle(output)}
|
|
1363
|
-
Command aborted`, isError: true });
|
|
1364
|
-
};
|
|
1365
|
-
const timer = setTimeout(() => {
|
|
1366
|
-
killTree(child);
|
|
1367
|
-
finish({
|
|
1368
|
-
content: `${truncateMiddle(output)}
|
|
1369
|
-
Command timed out after ${timeoutSeconds}s`,
|
|
1370
|
-
isError: true
|
|
1371
|
-
});
|
|
1372
|
-
}, timeoutSeconds * 1e3);
|
|
1373
|
-
child.stdout.on("data", (d) => {
|
|
1374
|
-
output += d.toString("utf8");
|
|
1375
|
-
});
|
|
1376
|
-
child.stderr.on("data", (d) => {
|
|
1377
|
-
output += d.toString("utf8");
|
|
1378
|
-
});
|
|
1379
|
-
child.on("error", (err) => {
|
|
1380
|
-
finish({ content: `Failed to start shell '${spec.label}': ${err.message}`, isError: true });
|
|
1381
|
-
});
|
|
1382
|
-
ctx.abortSignal?.addEventListener("abort", onAbort);
|
|
1383
|
-
child.on("close", (code) => {
|
|
1384
|
-
const trimmed = output.replace(/\s+$/, "");
|
|
1385
|
-
if (code === 0) {
|
|
1386
|
-
finish({ content: truncateMiddle(trimmed) || "(no output)" });
|
|
1387
|
-
return;
|
|
1388
|
-
}
|
|
1389
|
-
const body = truncateMiddle(trimmed);
|
|
1390
|
-
finish({
|
|
1391
|
-
content: `${body}${body ? "\n" : ""}Exit code: ${code ?? "unknown"}`,
|
|
1392
|
-
isError: true
|
|
1393
|
-
});
|
|
1394
|
-
});
|
|
1395
|
-
});
|
|
1396
|
-
}
|
|
1397
|
-
};
|
|
1398
|
-
|
|
1399
|
-
// src/tasks/manager.ts
|
|
1400
|
-
var MAX_TASK_OUTPUT = 1e5;
|
|
1401
|
-
var DEFAULT_TASK_TIMEOUT = 3600;
|
|
1402
|
-
var TRUNCATED_PREFIX = "[... earlier output truncated ...]\n";
|
|
1403
|
-
var TaskManager = class extends EventEmitter {
|
|
1404
|
-
records = /* @__PURE__ */ new Map();
|
|
1405
|
-
seq = 0;
|
|
1406
|
-
start(opts) {
|
|
1407
|
-
const id = `task-${++this.seq}`;
|
|
1408
|
-
const spec = resolveShell();
|
|
1409
|
-
const child = spawn2(spec.shell, spec.wrap(opts.command), {
|
|
1410
|
-
cwd: opts.cwd,
|
|
1411
|
-
windowsHide: true
|
|
1412
|
-
});
|
|
1413
|
-
const rec = {
|
|
1414
|
-
id,
|
|
1415
|
-
command: opts.command,
|
|
1416
|
-
description: opts.description,
|
|
1417
|
-
status: "running",
|
|
1418
|
-
startedAt: Date.now(),
|
|
1419
|
-
output: "",
|
|
1420
|
-
truncated: false,
|
|
1421
|
-
child
|
|
1422
|
-
};
|
|
1423
|
-
this.records.set(id, rec);
|
|
1424
|
-
const append = (d) => {
|
|
1425
|
-
rec.output += d.toString("utf8");
|
|
1426
|
-
if (rec.output.length > MAX_TASK_OUTPUT) {
|
|
1427
|
-
rec.output = rec.output.slice(-MAX_TASK_OUTPUT);
|
|
1428
|
-
rec.truncated = true;
|
|
1429
|
-
}
|
|
1430
|
-
};
|
|
1431
|
-
child.stdout.on("data", append);
|
|
1432
|
-
child.stderr.on("data", append);
|
|
1433
|
-
child.on("error", (err) => {
|
|
1434
|
-
append(Buffer.from(`Failed to start shell '${spec.label}': ${err.message}
|
|
1435
|
-
`));
|
|
1436
|
-
if (rec.status === "running") {
|
|
1437
|
-
this.finish(rec, "failed", null);
|
|
1438
|
-
}
|
|
1439
|
-
});
|
|
1440
|
-
child.on("close", (code) => {
|
|
1441
|
-
if (rec.status !== "running") return;
|
|
1442
|
-
this.finish(rec, code === 0 ? "completed" : "failed", code);
|
|
1443
|
-
});
|
|
1444
|
-
const timeoutSeconds = opts.timeoutSeconds ?? DEFAULT_TASK_TIMEOUT;
|
|
1445
|
-
rec.timer = setTimeout(() => this.terminate(rec, "timed_out"), timeoutSeconds * 1e3);
|
|
1446
|
-
rec.timer.unref();
|
|
1447
|
-
this.emitUpdate(rec);
|
|
1448
|
-
return this.snapshot(rec);
|
|
1449
|
-
}
|
|
1450
|
-
get(id) {
|
|
1451
|
-
const rec = this.records.get(id);
|
|
1452
|
-
return rec ? this.snapshot(rec) : void 0;
|
|
1453
|
-
}
|
|
1454
|
-
list() {
|
|
1455
|
-
return [...this.records.values()].map((rec) => this.snapshot(rec));
|
|
1456
|
-
}
|
|
1457
|
-
runningCount() {
|
|
1458
|
-
let count = 0;
|
|
1459
|
-
for (const rec of this.records.values()) {
|
|
1460
|
-
if (rec.status === "running") count += 1;
|
|
1461
|
-
}
|
|
1462
|
-
return count;
|
|
1463
|
-
}
|
|
1464
|
-
kill(id) {
|
|
1465
|
-
const rec = this.records.get(id);
|
|
1466
|
-
if (!rec || rec.status !== "running") return void 0;
|
|
1467
|
-
this.terminate(rec, "stopped");
|
|
1468
|
-
return this.snapshot(rec);
|
|
1469
|
-
}
|
|
1470
|
-
cleanup() {
|
|
1471
|
-
const killed = [];
|
|
1472
|
-
for (const rec of this.records.values()) {
|
|
1473
|
-
if (rec.status === "running") {
|
|
1474
|
-
this.terminate(rec, "stopped");
|
|
1475
|
-
killed.push(this.snapshot(rec));
|
|
1476
|
-
}
|
|
1477
|
-
}
|
|
1478
|
-
return killed;
|
|
1479
|
-
}
|
|
1480
|
-
terminate(rec, status) {
|
|
1481
|
-
killTree(rec.child);
|
|
1482
|
-
this.finish(rec, status, null);
|
|
1483
|
-
}
|
|
1484
|
-
finish(rec, status, exitCode) {
|
|
1485
|
-
if (rec.timer) clearTimeout(rec.timer);
|
|
1486
|
-
rec.status = status;
|
|
1487
|
-
rec.exitCode = exitCode;
|
|
1488
|
-
rec.endedAt = Date.now();
|
|
1489
|
-
this.emitUpdate(rec);
|
|
1490
|
-
}
|
|
1491
|
-
emitUpdate(rec) {
|
|
1492
|
-
this.emit("update", this.snapshot(rec));
|
|
1493
|
-
}
|
|
1494
|
-
snapshot(rec) {
|
|
1495
|
-
return {
|
|
1496
|
-
id: rec.id,
|
|
1497
|
-
command: rec.command,
|
|
1498
|
-
description: rec.description,
|
|
1499
|
-
status: rec.status,
|
|
1500
|
-
startedAt: rec.startedAt,
|
|
1501
|
-
endedAt: rec.endedAt,
|
|
1502
|
-
exitCode: rec.exitCode,
|
|
1503
|
-
output: rec.truncated ? TRUNCATED_PREFIX + rec.output : rec.output
|
|
1504
|
-
};
|
|
1505
|
-
}
|
|
1506
|
-
};
|
|
1507
|
-
var defaultTaskManager = new TaskManager();
|
|
1508
|
-
|
|
1509
|
-
// src/tools/fs/edit.ts
|
|
1510
|
-
import { readFile as readFile3, writeFile as writeFile2 } from "fs/promises";
|
|
1511
|
-
import path9 from "path";
|
|
1512
|
-
import { z as z2 } from "zod";
|
|
1513
|
-
var schema2 = z2.object({
|
|
1514
|
-
path: z2.string().describe("File path, absolute or relative to the working directory"),
|
|
1515
|
-
old_string: z2.string().min(1).describe("Exact text to replace"),
|
|
1516
|
-
new_string: z2.string().describe("Replacement text"),
|
|
1517
|
-
replace_all: z2.boolean().optional().describe("Replace every occurrence instead of requiring a unique match")
|
|
1518
|
-
});
|
|
1519
|
-
var editFileTool = {
|
|
1520
|
-
name: "edit_file",
|
|
1521
|
-
description: "Replace an exact string in a file. Fails if old_string is not found or matches multiple locations unless replace_all is set.",
|
|
1522
|
-
permission: "write",
|
|
1523
|
-
parameters: schema2,
|
|
1524
|
-
async execute(args, ctx) {
|
|
1525
|
-
const filePath = path9.resolve(ctx.cwd, args.path);
|
|
1526
|
-
let content;
|
|
1527
|
-
try {
|
|
1528
|
-
content = await readFile3(filePath, "utf8");
|
|
1529
|
-
} catch (err) {
|
|
1530
|
-
return { content: `Failed to read ${args.path}: ${err.message}`, isError: true };
|
|
1531
|
-
}
|
|
1532
|
-
let count = 0;
|
|
1533
|
-
let idx = content.indexOf(args.old_string);
|
|
1534
|
-
while (idx !== -1) {
|
|
1535
|
-
count += 1;
|
|
1536
|
-
idx = content.indexOf(args.old_string, idx + args.old_string.length);
|
|
1537
|
-
}
|
|
1538
|
-
if (count === 0) {
|
|
1539
|
-
return { content: `old_string not found in ${args.path}`, isError: true };
|
|
1540
|
-
}
|
|
1541
|
-
if (count > 1 && !args.replace_all) {
|
|
1542
|
-
return {
|
|
1543
|
-
content: `old_string matches ${count} locations in ${args.path}; provide more surrounding context or set replace_all`,
|
|
1544
|
-
isError: true
|
|
1545
|
-
};
|
|
1546
|
-
}
|
|
1547
|
-
const updated = args.replace_all ? content.split(args.old_string).join(args.new_string) : content.replace(args.old_string, args.new_string);
|
|
1548
|
-
try {
|
|
1549
|
-
await writeFile2(filePath, updated, "utf8");
|
|
1550
|
-
} catch (err) {
|
|
1551
|
-
return { content: `Failed to write ${args.path}: ${err.message}`, isError: true };
|
|
1552
|
-
}
|
|
1553
|
-
pushSnapshot({
|
|
1554
|
-
path: filePath,
|
|
1555
|
-
existed: true,
|
|
1556
|
-
content,
|
|
1557
|
-
toolName: "edit_file",
|
|
1558
|
-
timestamp: Date.now(),
|
|
1559
|
-
turn: currentTurnSeq()
|
|
1560
|
-
});
|
|
1561
|
-
return { content: `Edited ${args.path}: ${count} replacement${count > 1 ? "s" : ""}` };
|
|
1562
|
-
}
|
|
1563
|
-
};
|
|
1564
|
-
|
|
1565
|
-
// src/tools/fs/glob.ts
|
|
1566
|
-
import path10 from "path";
|
|
1567
|
-
import { z as z3 } from "zod";
|
|
1568
|
-
var MAX_RESULTS = 100;
|
|
1569
|
-
var schema3 = z3.object({
|
|
1570
|
-
pattern: z3.string().describe(
|
|
1571
|
-
"Glob pattern, e.g. 'src/**/*.ts'. Supports **, * and ?. A bare pattern like '*.ts' matches basenames recursively."
|
|
1572
|
-
),
|
|
1573
|
-
path: z3.string().optional().describe("Directory to search, defaults to the working directory")
|
|
1574
|
-
});
|
|
1575
|
-
var globTool = {
|
|
1576
|
-
name: "glob",
|
|
1577
|
-
description: "Find files by glob pattern. Skips node_modules, .git and dist. Returns paths sorted by modification time, newest first, up to 100 results.",
|
|
1578
|
-
permission: "read",
|
|
1579
|
-
parameters: schema3,
|
|
1580
|
-
async execute(args, ctx) {
|
|
1581
|
-
const root = path10.resolve(ctx.cwd, args.path ?? ".");
|
|
1582
|
-
const files = await walkFiles(root);
|
|
1583
|
-
const matched = files.filter((f) => matchesGlob(args.pattern, f.rel)).sort((a, b) => b.mtimeMs - a.mtimeMs).slice(0, MAX_RESULTS);
|
|
1584
|
-
if (matched.length === 0) {
|
|
1585
|
-
return { content: `No files matched pattern: ${args.pattern}` };
|
|
1586
|
-
}
|
|
1587
|
-
return { content: matched.map((f) => f.rel).join("\n") };
|
|
1588
|
-
}
|
|
1589
|
-
};
|
|
1590
|
-
|
|
1591
|
-
// src/tools/fs/grep.ts
|
|
1592
|
-
import { readFile as readFile4, stat as stat3 } from "fs/promises";
|
|
1593
|
-
import path11 from "path";
|
|
1594
|
-
import { z as z4 } from "zod";
|
|
1595
|
-
var MAX_MATCHES = 250;
|
|
1596
|
-
var schema4 = z4.object({
|
|
1597
|
-
pattern: z4.string().describe("Regular expression to search for"),
|
|
1598
|
-
path: z4.string().optional().describe("File or directory to search, defaults to the working directory"),
|
|
1599
|
-
glob: z4.string().optional().describe("Glob pattern to filter which files are searched, e.g. '*.ts'"),
|
|
1600
|
-
ignoreCase: z4.boolean().optional().describe("Case-insensitive matching")
|
|
1601
|
-
});
|
|
1602
|
-
var grepTool = {
|
|
1603
|
-
name: "grep",
|
|
1604
|
-
description: "Search file contents with a regular expression. Outputs 'file:line:content', up to 250 matches. Skips binary files, node_modules and .git.",
|
|
1605
|
-
permission: "read",
|
|
1606
|
-
parameters: schema4,
|
|
1607
|
-
async execute(args, ctx) {
|
|
1608
|
-
let re;
|
|
1609
|
-
try {
|
|
1610
|
-
re = new RegExp(args.pattern, args.ignoreCase ? "i" : "");
|
|
1611
|
-
} catch (err) {
|
|
1612
|
-
return { content: `Invalid regular expression: ${err.message}`, isError: true };
|
|
1613
|
-
}
|
|
1614
|
-
const root = path11.resolve(ctx.cwd, args.path ?? ".");
|
|
1615
|
-
let files;
|
|
1616
|
-
try {
|
|
1617
|
-
const st = await stat3(root);
|
|
1618
|
-
if (st.isFile()) {
|
|
1619
|
-
files = [{ abs: root, rel: path11.basename(root), mtimeMs: st.mtimeMs }];
|
|
1620
|
-
} else {
|
|
1621
|
-
files = await walkFiles(root);
|
|
1622
|
-
}
|
|
1623
|
-
} catch {
|
|
1624
|
-
return { content: `Path not found: ${args.path ?? "."}`, isError: true };
|
|
1625
|
-
}
|
|
1626
|
-
if (args.glob) {
|
|
1627
|
-
const glob = args.glob;
|
|
1628
|
-
files = files.filter((f) => matchesGlob(glob, f.rel));
|
|
1629
|
-
}
|
|
1630
|
-
const out = [];
|
|
1631
|
-
let truncated = false;
|
|
1632
|
-
outer: for (const file of files) {
|
|
1633
|
-
let buf;
|
|
1634
|
-
try {
|
|
1635
|
-
buf = await readFile4(file.abs);
|
|
1636
|
-
} catch {
|
|
1637
|
-
continue;
|
|
1638
|
-
}
|
|
1639
|
-
if (buf.includes(0)) {
|
|
1640
|
-
continue;
|
|
1641
|
-
}
|
|
1642
|
-
const lines = buf.toString("utf8").split("\n");
|
|
1643
|
-
for (let i = 0; i < lines.length; i++) {
|
|
1644
|
-
const line = lines[i] ?? "";
|
|
1645
|
-
if (re.test(line)) {
|
|
1646
|
-
out.push(`${file.rel}:${i + 1}:${line}`);
|
|
1647
|
-
if (out.length >= MAX_MATCHES) {
|
|
1648
|
-
truncated = true;
|
|
1649
|
-
break outer;
|
|
1650
|
-
}
|
|
1651
|
-
}
|
|
1652
|
-
}
|
|
1653
|
-
}
|
|
1654
|
-
if (out.length === 0) {
|
|
1655
|
-
return { content: `No matches for pattern: ${args.pattern}` };
|
|
1656
|
-
}
|
|
1657
|
-
if (truncated) {
|
|
1658
|
-
out.push(`... (truncated: more than ${MAX_MATCHES} matches)`);
|
|
1659
|
-
}
|
|
1660
|
-
return { content: out.join("\n") };
|
|
1661
|
-
}
|
|
1662
|
-
};
|
|
1663
|
-
|
|
1664
|
-
// src/tools/fs/read.ts
|
|
1665
|
-
import { readFile as readFile5, stat as stat4 } from "fs/promises";
|
|
1666
|
-
import path12 from "path";
|
|
1667
|
-
import { z as z5 } from "zod";
|
|
1668
|
-
var DEFAULT_LIMIT = 2e3;
|
|
1669
|
-
var MAX_CHARS = 100 * 1024;
|
|
1670
|
-
var schema5 = z5.object({
|
|
1671
|
-
path: z5.string().describe("File path, absolute or relative to the working directory"),
|
|
1672
|
-
offset: z5.number().int().positive().optional().describe("1-based line number to start reading from"),
|
|
1673
|
-
limit: z5.number().int().positive().optional().describe("Maximum number of lines to read (default 2000)")
|
|
1674
|
-
});
|
|
1675
|
-
var readFileTool = {
|
|
1676
|
-
name: "read_file",
|
|
1677
|
-
description: "Read a text file and return its contents with line numbers. Output is truncated to 2000 lines or 100KB by default.",
|
|
1678
|
-
permission: "read",
|
|
1679
|
-
parameters: schema5,
|
|
1680
|
-
async execute(args, ctx) {
|
|
1681
|
-
const filePath = path12.resolve(ctx.cwd, args.path);
|
|
1682
|
-
if (isSensitivePath2(filePath)) {
|
|
1683
|
-
return { content: `Refused to read sensitive file: ${args.path}`, isError: true };
|
|
1684
|
-
}
|
|
1685
|
-
const st = await stat4(filePath).catch(() => null);
|
|
1686
|
-
if (!st) {
|
|
1687
|
-
return { content: `File not found: ${args.path}`, isError: true };
|
|
1688
|
-
}
|
|
1689
|
-
if (st.isDirectory()) {
|
|
1690
|
-
return { content: `Path is a directory, not a file: ${args.path}`, isError: true };
|
|
1691
|
-
}
|
|
1692
|
-
const raw = await readFile5(filePath, "utf8");
|
|
1693
|
-
const lines = raw.split("\n");
|
|
1694
|
-
const start = args.offset ?? 1;
|
|
1695
|
-
const limit = args.limit ?? DEFAULT_LIMIT;
|
|
1696
|
-
const end = Math.min(lines.length, start - 1 + limit);
|
|
1697
|
-
const out = [];
|
|
1698
|
-
let size = 0;
|
|
1699
|
-
let truncatedBySize = false;
|
|
1700
|
-
for (let i = start - 1; i < end; i++) {
|
|
1701
|
-
const line = lines[i];
|
|
1702
|
-
if (line === void 0) {
|
|
1703
|
-
break;
|
|
1704
|
-
}
|
|
1705
|
-
const numbered = `${i + 1} ${line}`;
|
|
1706
|
-
if (size + numbered.length > MAX_CHARS) {
|
|
1707
|
-
truncatedBySize = true;
|
|
1708
|
-
break;
|
|
1709
|
-
}
|
|
1710
|
-
out.push(numbered);
|
|
1711
|
-
size += numbered.length;
|
|
1712
|
-
}
|
|
1713
|
-
const lastShown = start - 1 + out.length;
|
|
1714
|
-
if (truncatedBySize) {
|
|
1715
|
-
out.push(
|
|
1716
|
-
`... (truncated: output exceeds 100KB, showing lines ${start}-${lastShown} of ${lines.length})`
|
|
1717
|
-
);
|
|
1718
|
-
} else if (end < lines.length) {
|
|
1719
|
-
out.push(`... (truncated: showing lines ${start}-${end} of ${lines.length})`);
|
|
1720
|
-
}
|
|
1721
|
-
return { content: out.join("\n") };
|
|
1722
|
-
}
|
|
1723
|
-
};
|
|
1724
|
-
|
|
1725
|
-
// src/tools/fs/write.ts
|
|
1726
|
-
import { mkdir as mkdir2, writeFile as writeFile3 } from "fs/promises";
|
|
1727
|
-
import path13 from "path";
|
|
1728
|
-
import { z as z6 } from "zod";
|
|
1729
|
-
var schema6 = z6.object({
|
|
1730
|
-
path: z6.string().describe("File path, absolute or relative to the working directory"),
|
|
1731
|
-
content: z6.string().describe("Full content to write to the file")
|
|
1732
|
-
});
|
|
1733
|
-
var writeFileTool = {
|
|
1734
|
-
name: "write_file",
|
|
1735
|
-
description: "Write content to a file, creating parent directories as needed. Overwrites existing files.",
|
|
1736
|
-
permission: "write",
|
|
1737
|
-
parameters: schema6,
|
|
1738
|
-
async execute(args, ctx) {
|
|
1739
|
-
const filePath = path13.resolve(ctx.cwd, args.path);
|
|
1740
|
-
const snapshot = await captureSnapshot(filePath, "write_file");
|
|
1741
|
-
try {
|
|
1742
|
-
await mkdir2(path13.dirname(filePath), { recursive: true });
|
|
1743
|
-
await writeFile3(filePath, args.content, "utf8");
|
|
1744
|
-
} catch (err) {
|
|
1745
|
-
return { content: `Failed to write ${args.path}: ${err.message}`, isError: true };
|
|
1746
|
-
}
|
|
1747
|
-
pushSnapshot(snapshot);
|
|
1748
|
-
return { content: `Wrote ${Buffer.byteLength(args.content, "utf8")} bytes to ${args.path}` };
|
|
1749
|
-
}
|
|
1750
|
-
};
|
|
1751
|
-
|
|
1752
|
-
// src/tools/tasks.ts
|
|
1753
|
-
import { z as z7 } from "zod";
|
|
1754
|
-
var idSchema = z7.object({
|
|
1755
|
-
id: z7.string().describe("Task id, e.g. task-1")
|
|
1756
|
-
});
|
|
1757
|
-
var taskListTool = {
|
|
1758
|
-
name: "task_list",
|
|
1759
|
-
description: "List background shell tasks with status, runtime, and exit code.",
|
|
1760
|
-
permission: "read",
|
|
1761
|
-
parameters: z7.object({}),
|
|
1762
|
-
execute() {
|
|
1763
|
-
return Promise.resolve({ content: formatTaskList(defaultTaskManager.list()) });
|
|
1764
|
-
}
|
|
1765
|
-
};
|
|
1766
|
-
var taskOutputTool = {
|
|
1767
|
-
name: "task_output",
|
|
1768
|
-
description: "Read the current output of a background shell task (works while still running).",
|
|
1769
|
-
permission: "read",
|
|
1770
|
-
parameters: idSchema,
|
|
1771
|
-
execute(args) {
|
|
1772
|
-
const task = defaultTaskManager.get(args.id);
|
|
1773
|
-
if (!task) {
|
|
1774
|
-
return Promise.resolve({ content: `Unknown task: ${args.id}`, isError: true });
|
|
1775
|
-
}
|
|
1776
|
-
const body = task.output.replace(/\s+$/, "") || "(no output yet)";
|
|
1777
|
-
return Promise.resolve({ content: `${formatTaskLine(task)}
|
|
1778
|
-
${body}` });
|
|
1779
|
-
}
|
|
1780
|
-
};
|
|
1781
|
-
var taskKillTool = {
|
|
1782
|
-
name: "task_kill",
|
|
1783
|
-
description: "Stop a running background shell task (kills the whole process tree).",
|
|
1784
|
-
permission: "exec",
|
|
1785
|
-
parameters: idSchema,
|
|
1786
|
-
execute(args) {
|
|
1787
|
-
const task = defaultTaskManager.get(args.id);
|
|
1788
|
-
if (!task) {
|
|
1789
|
-
return Promise.resolve({ content: `Unknown task: ${args.id}`, isError: true });
|
|
1790
|
-
}
|
|
1791
|
-
if (task.status !== "running") {
|
|
1792
|
-
return Promise.resolve({
|
|
1793
|
-
content: `Task ${args.id} is not running (status: ${task.status}).`,
|
|
1794
|
-
isError: true
|
|
1795
|
-
});
|
|
1796
|
-
}
|
|
1797
|
-
defaultTaskManager.kill(args.id);
|
|
1798
|
-
return Promise.resolve({ content: `Task ${args.id} stopped.` });
|
|
795
|
+
function computeCostUsd(usage, model) {
|
|
796
|
+
if (model?.promptPrice === void 0 || model.completionPrice === void 0) {
|
|
797
|
+
return null;
|
|
1799
798
|
}
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
// src/tools/todo.ts
|
|
1803
|
-
import { mkdir as mkdir3, readFile as readFile6, writeFile as writeFile4 } from "fs/promises";
|
|
1804
|
-
import path14 from "path";
|
|
1805
|
-
import { z as z8 } from "zod";
|
|
1806
|
-
var todoItemSchema = z8.object({
|
|
1807
|
-
id: z8.number().int(),
|
|
1808
|
-
title: z8.string(),
|
|
1809
|
-
status: z8.enum(["pending", "in_progress", "done"])
|
|
1810
|
-
});
|
|
1811
|
-
var writeSchema = z8.object({
|
|
1812
|
-
todos: z8.array(todoItemSchema).describe("The full todo list, replacing the current one")
|
|
1813
|
-
});
|
|
1814
|
-
var readSchema = z8.object({});
|
|
1815
|
-
function fileFor(cwd) {
|
|
1816
|
-
return path14.join(cwd, ".star", "todos.json");
|
|
799
|
+
return (usage.promptTokens * model.promptPrice + usage.completionTokens * model.completionPrice) / 1e6;
|
|
1817
800
|
}
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
}
|
|
1829
|
-
async load(cwd) {
|
|
1830
|
-
let raw;
|
|
1831
|
-
try {
|
|
1832
|
-
raw = await readFile6(fileFor(cwd), "utf8");
|
|
1833
|
-
} catch {
|
|
1834
|
-
return;
|
|
1835
|
-
}
|
|
1836
|
-
try {
|
|
1837
|
-
const parsed = JSON.parse(raw);
|
|
1838
|
-
if (Array.isArray(parsed)) {
|
|
1839
|
-
this.replace(parsed.filter((it) => todoItemSchema.safeParse(it).success));
|
|
801
|
+
function estimateCost(usage, modelName, model) {
|
|
802
|
+
const cost = computeCostUsd(usage, model);
|
|
803
|
+
if (cost === null) {
|
|
804
|
+
if (model !== void 0) {
|
|
805
|
+
const missing = [
|
|
806
|
+
model.promptPrice === void 0 ? "promptPrice" : null,
|
|
807
|
+
model.completionPrice === void 0 ? "completionPrice" : null
|
|
808
|
+
].filter((f) => f !== null);
|
|
809
|
+
if (missing.length === 1) {
|
|
810
|
+
return `Estimated cost: unknown (${model.name} is missing ${missing[0]} \u2014 both prices are needed)`;
|
|
1840
811
|
}
|
|
1841
|
-
} catch {
|
|
1842
812
|
}
|
|
813
|
+
return `Estimated cost: unknown (no price configured for ${modelName})`;
|
|
1843
814
|
}
|
|
1844
|
-
|
|
1845
|
-
await mkdir3(path14.join(cwd, ".star"), { recursive: true });
|
|
1846
|
-
await writeFile4(fileFor(cwd), `${JSON.stringify(this.list(), null, 2)}
|
|
1847
|
-
`);
|
|
1848
|
-
}
|
|
1849
|
-
};
|
|
1850
|
-
var SYMBOLS = {
|
|
1851
|
-
pending: "[ ]",
|
|
1852
|
-
in_progress: "[~]",
|
|
1853
|
-
done: "[x]"
|
|
1854
|
-
};
|
|
1855
|
-
function formatTodos(items) {
|
|
1856
|
-
if (items.length === 0) {
|
|
1857
|
-
return "No todos.";
|
|
1858
|
-
}
|
|
1859
|
-
const lines = items.map((it) => `${SYMBOLS[it.status]} ${it.id}. ${it.title}`);
|
|
1860
|
-
const width = Math.max(...lines.map((l) => l.length));
|
|
1861
|
-
return lines.map((line, i) => {
|
|
1862
|
-
const status = items[i]?.status;
|
|
1863
|
-
return status === "pending" ? line : `${line.padEnd(width + 4)}(${status})`;
|
|
1864
|
-
}).join("\n");
|
|
1865
|
-
}
|
|
1866
|
-
var defaultStore = new TodoStore();
|
|
1867
|
-
function createTodoTools(store = defaultStore) {
|
|
1868
|
-
const todoWrite = {
|
|
1869
|
-
name: "todo_write",
|
|
1870
|
-
description: "Replace the current todo list with the given items. Each item has an id, title, and status (pending, in_progress, done).",
|
|
1871
|
-
permission: "read",
|
|
1872
|
-
parameters: writeSchema,
|
|
1873
|
-
async execute(args, ctx) {
|
|
1874
|
-
const parsed = writeSchema.safeParse(args);
|
|
1875
|
-
if (!parsed.success) {
|
|
1876
|
-
return { content: `Invalid todos: ${parsed.error.message}`, isError: true };
|
|
1877
|
-
}
|
|
1878
|
-
await store.load(ctx.cwd);
|
|
1879
|
-
store.replace(parsed.data.todos);
|
|
1880
|
-
await store.save(ctx.cwd);
|
|
1881
|
-
return { content: formatTodos(store.list()) };
|
|
1882
|
-
}
|
|
1883
|
-
};
|
|
1884
|
-
const todoRead = {
|
|
1885
|
-
name: "todo_read",
|
|
1886
|
-
description: "Read the current todo list.",
|
|
1887
|
-
permission: "read",
|
|
1888
|
-
parameters: readSchema,
|
|
1889
|
-
async execute(_args, ctx) {
|
|
1890
|
-
await store.load(ctx.cwd);
|
|
1891
|
-
return { content: formatTodos(store.list()) };
|
|
1892
|
-
}
|
|
1893
|
-
};
|
|
1894
|
-
return [todoWrite, todoRead];
|
|
815
|
+
return `Estimated cost: $${formatDollars(cost)} (${model?.name} @ $${model?.promptPrice}/M prompt, $${model?.completionPrice}/M completion)`;
|
|
1895
816
|
}
|
|
1896
817
|
|
|
1897
|
-
// src/
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
case "<":
|
|
1912
|
-
return "<";
|
|
1913
|
-
case ">":
|
|
1914
|
-
return ">";
|
|
1915
|
-
case " ":
|
|
1916
|
-
return " ";
|
|
1917
|
-
default:
|
|
1918
|
-
return m;
|
|
1919
|
-
}
|
|
1920
|
-
}).replace(/&/g, "&");
|
|
1921
|
-
}
|
|
1922
|
-
function htmlToText(html) {
|
|
1923
|
-
const noBlocks = html.replace(/<(script|style|noscript|template)\b[^>]*>[\s\S]*?<\/\1>/gi, " ");
|
|
1924
|
-
const withBreaks = noBlocks.replace(
|
|
1925
|
-
/<br\s*\/?>|<\/(p|div|li|tr|h[1-6]|section|article|header|footer|blockquote|pre)>/gi,
|
|
1926
|
-
"\n"
|
|
1927
|
-
);
|
|
1928
|
-
const stripped = decodeEntities(withBreaks.replace(/<[^>]+>/g, " "));
|
|
1929
|
-
return stripped.split("\n").map((line) => line.replace(/\s+/g, " ").trim()).join("\n").replace(/\n{3,}/g, "\n\n").trim();
|
|
818
|
+
// src/session/usage.ts
|
|
819
|
+
var USAGE_MAX_DAYS = 14;
|
|
820
|
+
function tokens(value) {
|
|
821
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : 0;
|
|
822
|
+
}
|
|
823
|
+
function bucketOf(value) {
|
|
824
|
+
const record = typeof value === "object" && value !== null ? value : {};
|
|
825
|
+
const promptTokens = tokens(record.promptTokens);
|
|
826
|
+
const completionTokens = tokens(record.completionTokens);
|
|
827
|
+
return {
|
|
828
|
+
promptTokens,
|
|
829
|
+
completionTokens,
|
|
830
|
+
totalTokens: tokens(record.totalTokens) || promptTokens + completionTokens
|
|
831
|
+
};
|
|
1930
832
|
}
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
833
|
+
function addTo(target, source) {
|
|
834
|
+
target.promptTokens += source.promptTokens;
|
|
835
|
+
target.completionTokens += source.completionTokens;
|
|
836
|
+
target.totalTokens += source.totalTokens;
|
|
837
|
+
}
|
|
838
|
+
function aggregateUsage(metas) {
|
|
839
|
+
const days = /* @__PURE__ */ new Map();
|
|
840
|
+
const models = /* @__PURE__ */ new Map();
|
|
841
|
+
const legacy = { promptTokens: 0, completionTokens: 0, totalTokens: 0 };
|
|
842
|
+
let hasLegacy = false;
|
|
843
|
+
let sessionsWithUsage = 0;
|
|
844
|
+
let requests = 0;
|
|
845
|
+
const grand = { promptTokens: 0, completionTokens: 0, totalTokens: 0 };
|
|
846
|
+
for (const meta of metas) {
|
|
847
|
+
const dayBuckets = [];
|
|
848
|
+
if (meta.usageByDay !== null && typeof meta.usageByDay === "object") {
|
|
849
|
+
for (const [date, value] of Object.entries(meta.usageByDay)) {
|
|
850
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(date)) continue;
|
|
851
|
+
dayBuckets.push([date, bucketOf(value)]);
|
|
1949
852
|
}
|
|
1950
853
|
}
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
}
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
async execute(args, ctx) {
|
|
1971
|
-
let url;
|
|
1972
|
-
try {
|
|
1973
|
-
url = new URL(args.url);
|
|
1974
|
-
} catch {
|
|
1975
|
-
return { content: `ERROR: invalid URL: ${args.url}`, isError: true };
|
|
1976
|
-
}
|
|
1977
|
-
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
1978
|
-
return {
|
|
1979
|
-
content: `ERROR: only http(s) URLs are supported, got: ${url.protocol}`,
|
|
1980
|
-
isError: true
|
|
854
|
+
const daySum = { promptTokens: 0, completionTokens: 0, totalTokens: 0 };
|
|
855
|
+
for (const [, bucket] of dayBuckets) addTo(daySum, bucket);
|
|
856
|
+
const hasTotals = meta.usage !== null && typeof meta.usage === "object";
|
|
857
|
+
const totals = hasTotals ? bucketOf(meta.usage) : daySum;
|
|
858
|
+
const requestCount = hasTotals && meta.usage ? tokens(meta.usage.requests) : 0;
|
|
859
|
+
if (!hasTotals && dayBuckets.length === 0) continue;
|
|
860
|
+
sessionsWithUsage += 1;
|
|
861
|
+
addTo(grand, totals);
|
|
862
|
+
requests += requestCount;
|
|
863
|
+
for (const [date, bucket] of dayBuckets) {
|
|
864
|
+
const acc = days.get(date) ?? { promptTokens: 0, completionTokens: 0, totalTokens: 0 };
|
|
865
|
+
addTo(acc, bucket);
|
|
866
|
+
days.set(date, acc);
|
|
867
|
+
}
|
|
868
|
+
if (hasTotals) {
|
|
869
|
+
const rest = {
|
|
870
|
+
promptTokens: Math.max(0, totals.promptTokens - daySum.promptTokens),
|
|
871
|
+
completionTokens: Math.max(0, totals.completionTokens - daySum.completionTokens),
|
|
872
|
+
totalTokens: Math.max(0, totals.totalTokens - daySum.totalTokens)
|
|
1981
873
|
};
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
const signal = ctx.abortSignal ? AbortSignal.any([timeoutSignal, ctx.abortSignal]) : timeoutSignal;
|
|
1986
|
-
let res;
|
|
1987
|
-
try {
|
|
1988
|
-
res = await fetch(url, { signal, redirect: "follow" });
|
|
1989
|
-
} catch (err) {
|
|
1990
|
-
if (ctx.abortSignal?.aborted) {
|
|
1991
|
-
return { content: "ERROR: request aborted", isError: true };
|
|
1992
|
-
}
|
|
1993
|
-
if (timeoutSignal.aborted) {
|
|
1994
|
-
return { content: `ERROR: request timed out after ${TIMEOUT_MS / 1e3}s`, isError: true };
|
|
874
|
+
if (rest.promptTokens > 0 || rest.completionTokens > 0 || rest.totalTokens > 0) {
|
|
875
|
+
addTo(legacy, rest);
|
|
876
|
+
hasLegacy = true;
|
|
1995
877
|
}
|
|
1996
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
1997
|
-
return { content: `ERROR: failed to fetch ${args.url}: ${message}`, isError: true };
|
|
1998
|
-
}
|
|
1999
|
-
if (!res.ok) {
|
|
2000
|
-
await res.body?.cancel().catch(() => {
|
|
2001
|
-
});
|
|
2002
|
-
return {
|
|
2003
|
-
content: `ERROR: HTTP ${res.status} ${res.statusText} for ${args.url}`,
|
|
2004
|
-
isError: true
|
|
2005
|
-
};
|
|
2006
|
-
}
|
|
2007
|
-
const [rawType = ""] = (res.headers.get("content-type") ?? "text/plain").split(";");
|
|
2008
|
-
const contentType = rawType.trim().toLowerCase();
|
|
2009
|
-
const isHtml = contentType === "text/html" || contentType === "application/xhtml+xml";
|
|
2010
|
-
const isText = contentType.startsWith("text/") || contentType === "application/json" || contentType === "application/xml" || contentType.endsWith("+json") || contentType.endsWith("+xml");
|
|
2011
|
-
if (!isHtml && !isText) {
|
|
2012
|
-
await res.body?.cancel().catch(() => {
|
|
2013
|
-
});
|
|
2014
|
-
return {
|
|
2015
|
-
content: `ERROR: unsupported content type: ${contentType} (${args.url})`,
|
|
2016
|
-
isError: true
|
|
2017
|
-
};
|
|
2018
|
-
}
|
|
2019
|
-
let body;
|
|
2020
|
-
try {
|
|
2021
|
-
body = await readBody(res, maxChars);
|
|
2022
|
-
} catch (err) {
|
|
2023
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
2024
|
-
return { content: `ERROR: failed to read response body: ${message}`, isError: true };
|
|
2025
|
-
}
|
|
2026
|
-
const text = isHtml ? htmlToText(body) : body;
|
|
2027
|
-
return { content: truncate(text, maxChars) };
|
|
2028
|
-
}
|
|
2029
|
-
};
|
|
2030
|
-
|
|
2031
|
-
// src/tools/web/search.ts
|
|
2032
|
-
import { z as z10 } from "zod";
|
|
2033
|
-
var TIMEOUT_MS2 = 15e3;
|
|
2034
|
-
var DEFAULT_MAX_RESULTS = 5;
|
|
2035
|
-
var MAX_RESULTS_LIMIT = 10;
|
|
2036
|
-
var SEARCH_URL = "https://html.duckduckgo.com/html/?q=";
|
|
2037
|
-
var USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36";
|
|
2038
|
-
var schema8 = z10.object({
|
|
2039
|
-
query: z10.string().min(1).describe("The search query"),
|
|
2040
|
-
maxResults: z10.number().int().min(1).max(MAX_RESULTS_LIMIT).optional().describe(
|
|
2041
|
-
`Maximum number of results to return (default ${DEFAULT_MAX_RESULTS}, max ${MAX_RESULTS_LIMIT})`
|
|
2042
|
-
)
|
|
2043
|
-
});
|
|
2044
|
-
function stripTags(s) {
|
|
2045
|
-
return decodeEntities(s.replace(/<[^>]+>/g, " ")).replace(/\s+/g, " ").trim();
|
|
2046
|
-
}
|
|
2047
|
-
function normalizeUrl(href) {
|
|
2048
|
-
const decoded = decodeEntities(href);
|
|
2049
|
-
const uddg = decoded.match(/[?&]uddg=([^&]+)/);
|
|
2050
|
-
if (uddg?.[1]) {
|
|
2051
|
-
try {
|
|
2052
|
-
return decodeURIComponent(uddg[1]);
|
|
2053
|
-
} catch {
|
|
2054
|
-
return decoded;
|
|
2055
|
-
}
|
|
2056
|
-
}
|
|
2057
|
-
if (decoded.startsWith("//")) {
|
|
2058
|
-
return `https:${decoded}`;
|
|
2059
|
-
}
|
|
2060
|
-
return decoded;
|
|
2061
|
-
}
|
|
2062
|
-
function parseResults(html) {
|
|
2063
|
-
const results = [];
|
|
2064
|
-
const anchorRe = /<a\b[^>]*\bclass\s*=\s*"[^"]*\bresult__a\b[^"]*"[^>]*>[\s\S]*?<\/a>/gi;
|
|
2065
|
-
const anchors = [...html.matchAll(anchorRe)];
|
|
2066
|
-
for (const [i, anchor] of anchors.entries()) {
|
|
2067
|
-
const full = anchor[0];
|
|
2068
|
-
const hrefMatch = full.match(/href\s*=\s*"([^"]*)"/i);
|
|
2069
|
-
if (!hrefMatch?.[1]) {
|
|
2070
|
-
continue;
|
|
2071
|
-
}
|
|
2072
|
-
const url = normalizeUrl(hrefMatch[1]);
|
|
2073
|
-
const title = stripTags(full.replace(/^<a\b[^>]*>/i, "").replace(/<\/a>\s*$/i, ""));
|
|
2074
|
-
const blockStart = (anchor.index ?? 0) + full.length;
|
|
2075
|
-
const blockEnd = i + 1 < anchors.length ? anchors[i + 1]?.index ?? html.length : html.length;
|
|
2076
|
-
const block = html.slice(blockStart, blockEnd);
|
|
2077
|
-
const snippetMatch = block.match(
|
|
2078
|
-
/<(?:a|div)\b[^>]*\bclass\s*=\s*"[^"]*\bresult__snippet\b[^"]*"[^>]*>([\s\S]*?)<\/(?:a|div)>/i
|
|
2079
|
-
);
|
|
2080
|
-
const snippet = snippetMatch?.[1] ? stripTags(snippetMatch[1]) : "";
|
|
2081
|
-
if (title && url) {
|
|
2082
|
-
results.push({ title, url, snippet });
|
|
2083
878
|
}
|
|
879
|
+
const name = typeof meta.model === "string" && meta.model.trim() ? meta.model : "unknown";
|
|
880
|
+
const entry = models.get(name) ?? {
|
|
881
|
+
model: name,
|
|
882
|
+
sessions: 0,
|
|
883
|
+
requests: 0,
|
|
884
|
+
promptTokens: 0,
|
|
885
|
+
completionTokens: 0,
|
|
886
|
+
totalTokens: 0
|
|
887
|
+
};
|
|
888
|
+
entry.sessions += 1;
|
|
889
|
+
entry.requests += requestCount;
|
|
890
|
+
addTo(entry, totals);
|
|
891
|
+
models.set(name, entry);
|
|
2084
892
|
}
|
|
2085
|
-
return
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
};
|
|
2122
|
-
}
|
|
2123
|
-
let html;
|
|
2124
|
-
try {
|
|
2125
|
-
html = await res.text();
|
|
2126
|
-
} catch (err) {
|
|
2127
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
2128
|
-
return { content: `ERROR: failed to read search response: ${message}`, isError: true };
|
|
2129
|
-
}
|
|
2130
|
-
const results = parseResults(html).slice(0, maxResults);
|
|
2131
|
-
if (results.length === 0) {
|
|
2132
|
-
return {
|
|
2133
|
-
content: `ERROR: no results found for "${args.query}" (the response may have been blocked or the page layout changed)`,
|
|
2134
|
-
isError: true
|
|
2135
|
-
};
|
|
2136
|
-
}
|
|
2137
|
-
const lines = results.map(
|
|
2138
|
-
(r, i) => `${i + 1}. ${r.title}
|
|
2139
|
-
${r.url}${r.snippet ? `
|
|
2140
|
-
${r.snippet}` : ""}`
|
|
893
|
+
return {
|
|
894
|
+
sessions: metas.length,
|
|
895
|
+
sessionsWithUsage,
|
|
896
|
+
requests,
|
|
897
|
+
promptTokens: grand.promptTokens,
|
|
898
|
+
completionTokens: grand.completionTokens,
|
|
899
|
+
totalTokens: grand.totalTokens,
|
|
900
|
+
days: [...days.entries()].map(([date, bucket]) => ({ date, ...bucket })).sort((a, b) => a.date.localeCompare(b.date)),
|
|
901
|
+
legacy: hasLegacy ? legacy : null,
|
|
902
|
+
models: [...models.values()].sort((a, b) => b.totalTokens - a.totalTokens)
|
|
903
|
+
};
|
|
904
|
+
}
|
|
905
|
+
async function collectUsageStats() {
|
|
906
|
+
return aggregateUsage(await SessionStore.list());
|
|
907
|
+
}
|
|
908
|
+
function fmt(n) {
|
|
909
|
+
return n.toLocaleString("en-US");
|
|
910
|
+
}
|
|
911
|
+
var BAR_WIDTH = 30;
|
|
912
|
+
function formatUsageDashboard(stats, models, options = {}) {
|
|
913
|
+
if (stats.sessions === 0) return "No sessions found.";
|
|
914
|
+
const lines = [
|
|
915
|
+
"Token usage across all sessions",
|
|
916
|
+
"",
|
|
917
|
+
`Total: ${stats.sessions} sessions (${stats.sessionsWithUsage} with usage) \xB7 ${fmt(stats.requests)} requests \xB7 ${fmt(stats.promptTokens)} prompt + ${fmt(stats.completionTokens)} completion = ${fmt(stats.totalTokens)} tokens`
|
|
918
|
+
];
|
|
919
|
+
if (stats.sessionsWithUsage === 0) {
|
|
920
|
+
lines.push("", "No token usage recorded yet.");
|
|
921
|
+
return lines.join("\n");
|
|
922
|
+
}
|
|
923
|
+
const maxDays = options.maxDays ?? USAGE_MAX_DAYS;
|
|
924
|
+
const recent = stats.days.slice(-maxDays);
|
|
925
|
+
if (recent.length > 0) {
|
|
926
|
+
lines.push("");
|
|
927
|
+
lines.push(
|
|
928
|
+
stats.days.length > maxDays ? `By day (last ${maxDays} of ${stats.days.length} days with usage):` : "By day:"
|
|
2141
929
|
);
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
// src/tools/registry.ts
|
|
2147
|
-
var ToolRegistry = class {
|
|
2148
|
-
tools = /* @__PURE__ */ new Map();
|
|
2149
|
-
constructor() {
|
|
2150
|
-
for (const tool of [
|
|
2151
|
-
readFileTool,
|
|
2152
|
-
writeFileTool,
|
|
2153
|
-
editFileTool,
|
|
2154
|
-
globTool,
|
|
2155
|
-
grepTool,
|
|
2156
|
-
bashTool,
|
|
2157
|
-
webFetchTool,
|
|
2158
|
-
webSearchTool,
|
|
2159
|
-
taskListTool,
|
|
2160
|
-
taskOutputTool,
|
|
2161
|
-
taskKillTool,
|
|
2162
|
-
...createTodoTools()
|
|
2163
|
-
]) {
|
|
2164
|
-
this.register(tool);
|
|
930
|
+
const maxTotal = Math.max(...recent.map((day) => day.totalTokens));
|
|
931
|
+
for (const day of recent) {
|
|
932
|
+
const barLen = maxTotal > 0 ? Math.max(1, Math.round(day.totalTokens / maxTotal * BAR_WIDTH)) : 0;
|
|
933
|
+
lines.push(`${day.date} ${fmt(day.totalTokens).padStart(12)} ${"\u2588".repeat(barLen)}`);
|
|
2165
934
|
}
|
|
2166
935
|
}
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
return this.tools.get(name);
|
|
2172
|
-
}
|
|
2173
|
-
list() {
|
|
2174
|
-
return [...this.tools.values()];
|
|
2175
|
-
}
|
|
2176
|
-
names() {
|
|
2177
|
-
return [...this.tools.keys()];
|
|
936
|
+
if (stats.legacy) {
|
|
937
|
+
lines.push(
|
|
938
|
+
`Earlier usage without per-day records: ${fmt(stats.legacy.totalTokens)} tokens (${fmt(stats.legacy.promptTokens)} prompt + ${fmt(stats.legacy.completionTokens)} completion)`
|
|
939
|
+
);
|
|
2178
940
|
}
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
941
|
+
lines.push("", "By model:");
|
|
942
|
+
const configured = new Map(models.map((m) => [m.name, m]));
|
|
943
|
+
const rows = stats.models.map((entry) => {
|
|
944
|
+
const cfg = configured.get(entry.model);
|
|
945
|
+
const cost = cfg?.promptPrice !== void 0 && cfg.completionPrice !== void 0 ? (entry.promptTokens * cfg.promptPrice + entry.completionTokens * cfg.completionPrice) / 1e6 : null;
|
|
946
|
+
return { entry, cost };
|
|
947
|
+
});
|
|
948
|
+
const header = ["model", "requests", "prompt", "completion", "total", "cost"];
|
|
949
|
+
const cells = rows.map(({ entry, cost }) => [
|
|
950
|
+
entry.model,
|
|
951
|
+
fmt(entry.requests),
|
|
952
|
+
fmt(entry.promptTokens),
|
|
953
|
+
fmt(entry.completionTokens),
|
|
954
|
+
fmt(entry.totalTokens),
|
|
955
|
+
cost === null ? "\u2014" : `$${formatDollars(cost)}`
|
|
956
|
+
]);
|
|
957
|
+
const widths = header.map(
|
|
958
|
+
(h, i) => Math.max(h.length, ...cells.map((row) => row[i]?.length ?? 0))
|
|
959
|
+
);
|
|
960
|
+
const renderRow = (row) => row.map((cell, i) => i === 0 ? cell.padEnd(widths[0] ?? 0) : cell.padStart(widths[i] ?? 0)).join(" ");
|
|
961
|
+
lines.push(renderRow(header));
|
|
962
|
+
for (const row of cells) lines.push(renderRow(row));
|
|
963
|
+
const pricedCosts = rows.filter((row) => row.cost !== null).map((row) => row.cost);
|
|
964
|
+
const unpriced = rows.filter((row) => row.cost === null).map((row) => row.entry.model);
|
|
965
|
+
if (pricedCosts.length > 0) {
|
|
966
|
+
const total = pricedCosts.reduce((sum, cost) => sum + cost, 0);
|
|
967
|
+
const note = unpriced.length > 0 ? ` (excludes ${unpriced.join(", ")} \u2014 no price configured)` : "";
|
|
968
|
+
lines.push("", `Estimated cost: $${formatDollars(total)}${note}`);
|
|
969
|
+
} else {
|
|
970
|
+
lines.push("", "Estimated cost: unknown (no per-model pricing configured)");
|
|
2186
971
|
}
|
|
2187
|
-
return
|
|
972
|
+
return lines.join("\n");
|
|
2188
973
|
}
|
|
2189
974
|
|
|
2190
975
|
// src/version.ts
|
|
2191
|
-
import
|
|
2192
|
-
import
|
|
976
|
+
import fs6 from "fs";
|
|
977
|
+
import path7 from "path";
|
|
2193
978
|
import { fileURLToPath } from "url";
|
|
2194
979
|
function readPackageVersion() {
|
|
2195
980
|
try {
|
|
2196
|
-
const pkgPath =
|
|
2197
|
-
const pkg = JSON.parse(
|
|
981
|
+
const pkgPath = path7.join(path7.dirname(fileURLToPath(import.meta.url)), "..", "package.json");
|
|
982
|
+
const pkg = JSON.parse(fs6.readFileSync(pkgPath, "utf8"));
|
|
2198
983
|
if (typeof pkg === "object" && pkg !== null) {
|
|
2199
984
|
const version = pkg.version;
|
|
2200
985
|
if (typeof version === "string") return version;
|
|
@@ -2205,9 +990,141 @@ function readPackageVersion() {
|
|
|
2205
990
|
}
|
|
2206
991
|
var VERSION = readPackageVersion();
|
|
2207
992
|
|
|
993
|
+
// src/cli/budget.ts
|
|
994
|
+
var BUDGET_WARN_RATIO = 0.8;
|
|
995
|
+
function budgetState(costUsd, budgetUsd) {
|
|
996
|
+
if (budgetUsd === void 0 || costUsd === null) return "ok";
|
|
997
|
+
if (costUsd >= budgetUsd) return "exceeded";
|
|
998
|
+
if (costUsd >= budgetUsd * BUDGET_WARN_RATIO) return "warn";
|
|
999
|
+
return "ok";
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
// src/cli/clipboard.ts
|
|
1003
|
+
import { execFile } from "child_process";
|
|
1004
|
+
import { randomUUID } from "crypto";
|
|
1005
|
+
import { readFile as readFile2, unlink } from "fs/promises";
|
|
1006
|
+
import os from "os";
|
|
1007
|
+
import path8 from "path";
|
|
1008
|
+
import { promisify } from "util";
|
|
1009
|
+
var CLIPBOARD_TIMEOUT_MS = 5e3;
|
|
1010
|
+
var MAX_CLIPBOARD_IMAGE_BYTES = 10 * 1024 * 1024;
|
|
1011
|
+
var execFileAsync = promisify(execFile);
|
|
1012
|
+
function windowsClipboardImageScript(tempFile) {
|
|
1013
|
+
const escaped = tempFile.replace(/'/g, "''");
|
|
1014
|
+
return [
|
|
1015
|
+
"Add-Type -AssemblyName System.Windows.Forms",
|
|
1016
|
+
"$img = [Windows.Forms.Clipboard]::GetImage()",
|
|
1017
|
+
"if ($null -ne $img) {",
|
|
1018
|
+
` $img.Save('${escaped}', [System.Drawing.Imaging.ImageFormat]::Png)`,
|
|
1019
|
+
" $img.Dispose()",
|
|
1020
|
+
"}"
|
|
1021
|
+
].join("; ");
|
|
1022
|
+
}
|
|
1023
|
+
function macOsClipboardImageLines(tempFile) {
|
|
1024
|
+
const posix = tempFile.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
1025
|
+
return [
|
|
1026
|
+
"set pngData to the clipboard as \xABclass PNGf\xBB",
|
|
1027
|
+
`set theFile to open for access (POSIX file "${posix}") with write permission`,
|
|
1028
|
+
"write pngData to theFile",
|
|
1029
|
+
"close access theFile"
|
|
1030
|
+
];
|
|
1031
|
+
}
|
|
1032
|
+
function clipboardImagePlan(platform, tempFile, hasPngpaste) {
|
|
1033
|
+
if (platform === "win32") {
|
|
1034
|
+
return {
|
|
1035
|
+
kind: "temp-file",
|
|
1036
|
+
command: "powershell.exe",
|
|
1037
|
+
args: [
|
|
1038
|
+
"-NoProfile",
|
|
1039
|
+
"-NonInteractive",
|
|
1040
|
+
"-STA",
|
|
1041
|
+
"-Command",
|
|
1042
|
+
windowsClipboardImageScript(tempFile)
|
|
1043
|
+
]
|
|
1044
|
+
};
|
|
1045
|
+
}
|
|
1046
|
+
if (platform === "darwin") {
|
|
1047
|
+
if (hasPngpaste) {
|
|
1048
|
+
return { kind: "temp-file", command: "pngpaste", args: [tempFile] };
|
|
1049
|
+
}
|
|
1050
|
+
return {
|
|
1051
|
+
kind: "temp-file",
|
|
1052
|
+
command: "osascript",
|
|
1053
|
+
args: macOsClipboardImageLines(tempFile).flatMap((line) => ["-e", line])
|
|
1054
|
+
};
|
|
1055
|
+
}
|
|
1056
|
+
if (platform === "linux") {
|
|
1057
|
+
return {
|
|
1058
|
+
kind: "stdout",
|
|
1059
|
+
command: "xclip",
|
|
1060
|
+
args: ["-selection", "clipboard", "-t", "image/png", "-o"]
|
|
1061
|
+
};
|
|
1062
|
+
}
|
|
1063
|
+
return null;
|
|
1064
|
+
}
|
|
1065
|
+
function clipboardCopyCommand(platform) {
|
|
1066
|
+
if (platform === "win32") return { command: "clip.exe", args: [] };
|
|
1067
|
+
if (platform === "darwin") return { command: "pbcopy", args: [] };
|
|
1068
|
+
if (platform === "linux") return { command: "xclip", args: ["-selection", "clipboard"] };
|
|
1069
|
+
return null;
|
|
1070
|
+
}
|
|
1071
|
+
async function commandExists(command) {
|
|
1072
|
+
const checker = process.platform === "win32" ? "where" : "which";
|
|
1073
|
+
try {
|
|
1074
|
+
await execFileAsync(checker, [command], { timeout: CLIPBOARD_TIMEOUT_MS });
|
|
1075
|
+
return true;
|
|
1076
|
+
} catch {
|
|
1077
|
+
return false;
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
async function readClipboardImage() {
|
|
1081
|
+
const tempFile = path8.join(os.tmpdir(), `star-clipboard-${randomUUID()}.png`);
|
|
1082
|
+
const hasPngpaste = process.platform === "darwin" ? await commandExists("pngpaste") : false;
|
|
1083
|
+
const plan = clipboardImagePlan(process.platform, tempFile, hasPngpaste);
|
|
1084
|
+
if (!plan) return null;
|
|
1085
|
+
try {
|
|
1086
|
+
if (plan.kind === "stdout") {
|
|
1087
|
+
const { stdout } = await execFileAsync(plan.command, plan.args, {
|
|
1088
|
+
timeout: CLIPBOARD_TIMEOUT_MS,
|
|
1089
|
+
maxBuffer: MAX_CLIPBOARD_IMAGE_BYTES,
|
|
1090
|
+
encoding: "buffer"
|
|
1091
|
+
});
|
|
1092
|
+
const buf2 = stdout;
|
|
1093
|
+
if (buf2.length === 0) return null;
|
|
1094
|
+
return { path: "clipboard.png", mimeType: "image/png", data: buf2.toString("base64") };
|
|
1095
|
+
}
|
|
1096
|
+
await execFileAsync(plan.command, plan.args, { timeout: CLIPBOARD_TIMEOUT_MS });
|
|
1097
|
+
const buf = await readFile2(tempFile).catch(() => null);
|
|
1098
|
+
if (!buf || buf.length === 0) return null;
|
|
1099
|
+
return { path: "clipboard.png", mimeType: "image/png", data: buf.toString("base64") };
|
|
1100
|
+
} catch {
|
|
1101
|
+
return null;
|
|
1102
|
+
} finally {
|
|
1103
|
+
if (plan.kind === "temp-file") {
|
|
1104
|
+
await unlink(tempFile).catch(() => {
|
|
1105
|
+
});
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
async function copyText(text) {
|
|
1110
|
+
const plan = clipboardCopyCommand(process.platform);
|
|
1111
|
+
if (!plan) return false;
|
|
1112
|
+
return new Promise((resolve) => {
|
|
1113
|
+
const child = execFile(
|
|
1114
|
+
plan.command,
|
|
1115
|
+
plan.args,
|
|
1116
|
+
{ timeout: CLIPBOARD_TIMEOUT_MS },
|
|
1117
|
+
(error) => resolve(error === null)
|
|
1118
|
+
);
|
|
1119
|
+
child.stdin?.on("error", () => {
|
|
1120
|
+
});
|
|
1121
|
+
child.stdin?.end(text);
|
|
1122
|
+
});
|
|
1123
|
+
}
|
|
1124
|
+
|
|
2208
1125
|
// src/cli/commands/actions.ts
|
|
2209
|
-
import
|
|
2210
|
-
import
|
|
1126
|
+
import fs7 from "fs/promises";
|
|
1127
|
+
import path9 from "path";
|
|
2211
1128
|
var MIN_COMPACT_MESSAGES = 4;
|
|
2212
1129
|
async function compactSession(opts) {
|
|
2213
1130
|
const { backend, sessionStore, config, model } = opts;
|
|
@@ -2268,15 +1185,15 @@ async function exportSession(opts) {
|
|
|
2268
1185
|
return "Nothing to export: the session has no messages.";
|
|
2269
1186
|
}
|
|
2270
1187
|
const sessionId = sessionStore?.id ?? `unknown-${Date.now()}`;
|
|
2271
|
-
const target = arg ?
|
|
1188
|
+
const target = arg ? path9.resolve(cwd, arg) : path9.resolve(cwd, `star-session-${sessionId}.md`);
|
|
2272
1189
|
const existed = await fileExists(target);
|
|
2273
|
-
await
|
|
2274
|
-
await
|
|
1190
|
+
await fs7.mkdir(path9.dirname(target), { recursive: true });
|
|
1191
|
+
await fs7.writeFile(target, renderSessionMarkdown(sessionId, messages));
|
|
2275
1192
|
return existed ? `Exported ${messages.length} messages to ${target} (overwrote existing file).` : `Exported ${messages.length} messages to ${target}.`;
|
|
2276
1193
|
}
|
|
2277
1194
|
async function fileExists(target) {
|
|
2278
1195
|
try {
|
|
2279
|
-
await
|
|
1196
|
+
await fs7.access(target);
|
|
2280
1197
|
return true;
|
|
2281
1198
|
} catch {
|
|
2282
1199
|
return false;
|
|
@@ -2336,6 +1253,191 @@ function stringifyPart(value) {
|
|
|
2336
1253
|
return typeof value === "string" ? value : JSON.stringify(value, null, 2);
|
|
2337
1254
|
}
|
|
2338
1255
|
|
|
1256
|
+
// src/cli/diff-preview.ts
|
|
1257
|
+
import { readFile as readFile3 } from "fs/promises";
|
|
1258
|
+
import path10 from "path";
|
|
1259
|
+
var CONTEXT_LINES = 2;
|
|
1260
|
+
var MAX_HUNK_LINES = 15;
|
|
1261
|
+
var MAX_TOTAL_LINES = 12;
|
|
1262
|
+
var NEW_FILE_PREVIEW_LINES = 10;
|
|
1263
|
+
var MAX_DP_CELLS = 25e4;
|
|
1264
|
+
function diffOps(oldText, newText) {
|
|
1265
|
+
const a = oldText.split("\n");
|
|
1266
|
+
const b = newText.split("\n");
|
|
1267
|
+
let start = 0;
|
|
1268
|
+
while (start < a.length && start < b.length && a[start] === b[start]) start += 1;
|
|
1269
|
+
let endA = a.length;
|
|
1270
|
+
let endB = b.length;
|
|
1271
|
+
while (endA > start && endB > start && a[endA - 1] === b[endB - 1]) {
|
|
1272
|
+
endA -= 1;
|
|
1273
|
+
endB -= 1;
|
|
1274
|
+
}
|
|
1275
|
+
const prefix = a.slice(0, start).map((text) => ({ kind: "context", text }));
|
|
1276
|
+
const suffix = a.slice(endA).map((text) => ({ kind: "context", text }));
|
|
1277
|
+
const midA = a.slice(start, endA);
|
|
1278
|
+
const midB = b.slice(start, endB);
|
|
1279
|
+
const prefixLen = prefix.length;
|
|
1280
|
+
const ops = [...prefix];
|
|
1281
|
+
if (midA.length * midB.length > MAX_DP_CELLS) {
|
|
1282
|
+
for (const text of midA) ops.push({ kind: "del", text });
|
|
1283
|
+
for (const text of midB) ops.push({ kind: "add", text });
|
|
1284
|
+
} else if (midA.length > 0 || midB.length > 0) {
|
|
1285
|
+
const rows = midA.length + 1;
|
|
1286
|
+
const cols = midB.length + 1;
|
|
1287
|
+
const dp = new Uint32Array(rows * cols);
|
|
1288
|
+
const cell = (r, c) => dp[r * cols + c] ?? 0;
|
|
1289
|
+
for (let i2 = 1; i2 < rows; i2++) {
|
|
1290
|
+
for (let j2 = 1; j2 < cols; j2++) {
|
|
1291
|
+
dp[i2 * cols + j2] = midA[i2 - 1] === midB[j2 - 1] ? cell(i2 - 1, j2 - 1) + 1 : Math.max(cell(i2 - 1, j2), cell(i2, j2 - 1));
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
const lineAt = (lines, index) => lines[index] ?? "";
|
|
1295
|
+
const middle = [];
|
|
1296
|
+
let i = midA.length;
|
|
1297
|
+
let j = midB.length;
|
|
1298
|
+
while (i > 0 && j > 0) {
|
|
1299
|
+
if (midA[i - 1] === midB[j - 1]) {
|
|
1300
|
+
middle.push({ kind: "context", text: lineAt(midA, i - 1) });
|
|
1301
|
+
i -= 1;
|
|
1302
|
+
j -= 1;
|
|
1303
|
+
} else if (cell(i, j - 1) >= cell(i - 1, j)) {
|
|
1304
|
+
middle.push({ kind: "add", text: lineAt(midB, j - 1) });
|
|
1305
|
+
j -= 1;
|
|
1306
|
+
} else {
|
|
1307
|
+
middle.push({ kind: "del", text: lineAt(midA, i - 1) });
|
|
1308
|
+
i -= 1;
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1311
|
+
while (i > 0) {
|
|
1312
|
+
middle.push({ kind: "del", text: lineAt(midA, i - 1) });
|
|
1313
|
+
i -= 1;
|
|
1314
|
+
}
|
|
1315
|
+
while (j > 0) {
|
|
1316
|
+
middle.push({ kind: "add", text: lineAt(midB, j - 1) });
|
|
1317
|
+
j -= 1;
|
|
1318
|
+
}
|
|
1319
|
+
middle.reverse();
|
|
1320
|
+
ops.push(...middle);
|
|
1321
|
+
}
|
|
1322
|
+
ops.push(...suffix);
|
|
1323
|
+
return ops.length === prefixLen + suffix.length ? [] : ops;
|
|
1324
|
+
}
|
|
1325
|
+
function hunkRanges(ops) {
|
|
1326
|
+
const changes = [];
|
|
1327
|
+
for (let i = 0; i < ops.length; i++) {
|
|
1328
|
+
const op = ops[i];
|
|
1329
|
+
if (op && op.kind !== "context") changes.push(i);
|
|
1330
|
+
}
|
|
1331
|
+
const ranges = [];
|
|
1332
|
+
let groupStart = 0;
|
|
1333
|
+
for (let k = 1; k <= changes.length; k++) {
|
|
1334
|
+
const cur = changes[k];
|
|
1335
|
+
const prev = changes[k - 1];
|
|
1336
|
+
const first = changes[groupStart];
|
|
1337
|
+
if (prev === void 0 || first === void 0) break;
|
|
1338
|
+
if (cur === void 0 || cur - prev > CONTEXT_LINES * 2 + 1) {
|
|
1339
|
+
ranges.push([
|
|
1340
|
+
Math.max(0, first - CONTEXT_LINES),
|
|
1341
|
+
Math.min(ops.length, prev + CONTEXT_LINES + 1)
|
|
1342
|
+
]);
|
|
1343
|
+
groupStart = k;
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
return ranges;
|
|
1347
|
+
}
|
|
1348
|
+
function buildDiffLines(oldText, newText) {
|
|
1349
|
+
const ops = diffOps(oldText, newText);
|
|
1350
|
+
if (ops.length === 0) return [];
|
|
1351
|
+
const ranges = hunkRanges(ops);
|
|
1352
|
+
const lines = [];
|
|
1353
|
+
let omitted = 0;
|
|
1354
|
+
for (const [rangeIndex, [start, end]] of ranges.entries()) {
|
|
1355
|
+
if (rangeIndex > 0) lines.push({ kind: "marker", text: "\xB7\xB7\xB7" });
|
|
1356
|
+
const hunk = ops.slice(start, end);
|
|
1357
|
+
let shown = hunk;
|
|
1358
|
+
if (hunk.length > MAX_HUNK_LINES) {
|
|
1359
|
+
const headCount = Math.ceil(MAX_HUNK_LINES / 2);
|
|
1360
|
+
shown = [...hunk.slice(0, headCount), ...hunk.slice(headCount - MAX_HUNK_LINES)];
|
|
1361
|
+
}
|
|
1362
|
+
if (hunk.length > shown.length) {
|
|
1363
|
+
omitted += hunk.length - shown.length;
|
|
1364
|
+
}
|
|
1365
|
+
for (const op of shown) lines.push(op);
|
|
1366
|
+
}
|
|
1367
|
+
if (lines.length > MAX_TOTAL_LINES) {
|
|
1368
|
+
omitted += lines.length - MAX_TOTAL_LINES;
|
|
1369
|
+
lines.length = MAX_TOTAL_LINES;
|
|
1370
|
+
}
|
|
1371
|
+
if (omitted > 0) lines.push({ kind: "marker", text: `... (${omitted} more lines)` });
|
|
1372
|
+
return lines;
|
|
1373
|
+
}
|
|
1374
|
+
function parseGitDiffLines(diffText) {
|
|
1375
|
+
return diffText.split("\n").map((raw) => {
|
|
1376
|
+
if (raw.startsWith("+") && !raw.startsWith("+++")) {
|
|
1377
|
+
return { kind: "add", text: raw.slice(1) };
|
|
1378
|
+
}
|
|
1379
|
+
if (raw.startsWith("-") && !raw.startsWith("---")) {
|
|
1380
|
+
return { kind: "del", text: raw.slice(1) };
|
|
1381
|
+
}
|
|
1382
|
+
if (raw.startsWith("@@") || raw.startsWith("diff ") || raw.startsWith("index ") || raw.startsWith("#") || raw.startsWith("+++") || raw.startsWith("---")) {
|
|
1383
|
+
return { kind: "marker", text: raw };
|
|
1384
|
+
}
|
|
1385
|
+
return { kind: "context", text: raw.startsWith(" ") ? raw.slice(1) : raw };
|
|
1386
|
+
});
|
|
1387
|
+
}
|
|
1388
|
+
function getStringArg(args, key) {
|
|
1389
|
+
if (typeof args === "object" && args !== null) {
|
|
1390
|
+
const value = args[key];
|
|
1391
|
+
if (typeof value === "string") return value;
|
|
1392
|
+
}
|
|
1393
|
+
return void 0;
|
|
1394
|
+
}
|
|
1395
|
+
function applyEdit(content, oldString, newString, replaceAll) {
|
|
1396
|
+
let count = 0;
|
|
1397
|
+
let idx = content.indexOf(oldString);
|
|
1398
|
+
while (idx !== -1) {
|
|
1399
|
+
count += 1;
|
|
1400
|
+
idx = content.indexOf(oldString, idx + oldString.length);
|
|
1401
|
+
}
|
|
1402
|
+
if (count === 0) return null;
|
|
1403
|
+
if (count > 1 && !replaceAll) return null;
|
|
1404
|
+
return replaceAll ? content.split(oldString).join(newString) : content.replace(oldString, newString);
|
|
1405
|
+
}
|
|
1406
|
+
function newFilePreview(content, label) {
|
|
1407
|
+
const all = content.split("\n");
|
|
1408
|
+
const shown = all.slice(0, NEW_FILE_PREVIEW_LINES);
|
|
1409
|
+
const lines = shown.map((text) => ({ kind: "add", text }));
|
|
1410
|
+
if (all.length > shown.length) {
|
|
1411
|
+
lines.push({ kind: "marker", text: `... (${all.length - shown.length} more lines)` });
|
|
1412
|
+
}
|
|
1413
|
+
return { type: "new-file", label, lines };
|
|
1414
|
+
}
|
|
1415
|
+
async function generateDiffPreview(toolName, args, cwd) {
|
|
1416
|
+
if (toolName !== "edit_file" && toolName !== "write_file") return null;
|
|
1417
|
+
const filePath = getStringArg(args, "path");
|
|
1418
|
+
if (filePath === void 0) return null;
|
|
1419
|
+
const absolute = path10.resolve(cwd, filePath);
|
|
1420
|
+
let existing;
|
|
1421
|
+
try {
|
|
1422
|
+
existing = await readFile3(absolute, "utf8");
|
|
1423
|
+
} catch {
|
|
1424
|
+
existing = null;
|
|
1425
|
+
}
|
|
1426
|
+
if (toolName === "edit_file") {
|
|
1427
|
+
const oldString = getStringArg(args, "old_string");
|
|
1428
|
+
const newString = getStringArg(args, "new_string");
|
|
1429
|
+
if (existing === null || oldString === void 0 || newString === void 0) return null;
|
|
1430
|
+
const replaceAll = typeof args === "object" && args !== null && args.replace_all === true;
|
|
1431
|
+
const updated = applyEdit(existing, oldString, newString, replaceAll);
|
|
1432
|
+
if (updated === null) return null;
|
|
1433
|
+
return { type: "diff", label: filePath, lines: buildDiffLines(existing, updated) };
|
|
1434
|
+
}
|
|
1435
|
+
const content = getStringArg(args, "content");
|
|
1436
|
+
if (content === void 0) return null;
|
|
1437
|
+
if (existing === null) return newFilePreview(content, filePath);
|
|
1438
|
+
return { type: "diff", label: filePath, lines: buildDiffLines(existing, content) };
|
|
1439
|
+
}
|
|
1440
|
+
|
|
2339
1441
|
// src/cli/commands/builtin.ts
|
|
2340
1442
|
function registerBuiltinCommands(registry) {
|
|
2341
1443
|
registry.register({
|
|
@@ -2359,6 +1461,37 @@ ${lines.join("\n")}`);
|
|
|
2359
1461
|
ctx.clearMessages();
|
|
2360
1462
|
}
|
|
2361
1463
|
});
|
|
1464
|
+
registry.register({
|
|
1465
|
+
name: "new",
|
|
1466
|
+
description: "Start a new session with a clean context",
|
|
1467
|
+
usage: "/new",
|
|
1468
|
+
async run(_args, ctx) {
|
|
1469
|
+
if (!ctx.newSession) {
|
|
1470
|
+
ctx.addSystemMessage("/new: this context cannot start a new session.");
|
|
1471
|
+
return;
|
|
1472
|
+
}
|
|
1473
|
+
ctx.addSystemMessage(await ctx.newSession());
|
|
1474
|
+
}
|
|
1475
|
+
});
|
|
1476
|
+
registry.register({
|
|
1477
|
+
name: "clear-sessions",
|
|
1478
|
+
description: "Delete stored sessions: this directory by default, every session with --all (the current session is kept)",
|
|
1479
|
+
usage: "/clear-sessions [--all]",
|
|
1480
|
+
async run(args, ctx) {
|
|
1481
|
+
const arg = args.trim();
|
|
1482
|
+
if (arg !== "" && arg !== "--all") {
|
|
1483
|
+
ctx.addSystemMessage(
|
|
1484
|
+
`/clear-sessions: unknown argument "${arg}". Usage: /clear-sessions [--all]`
|
|
1485
|
+
);
|
|
1486
|
+
return;
|
|
1487
|
+
}
|
|
1488
|
+
if (!ctx.clearSessions) {
|
|
1489
|
+
ctx.addSystemMessage("/clear-sessions: this context cannot delete sessions.");
|
|
1490
|
+
return;
|
|
1491
|
+
}
|
|
1492
|
+
ctx.addSystemMessage(await ctx.clearSessions(arg === "--all"));
|
|
1493
|
+
}
|
|
1494
|
+
});
|
|
2362
1495
|
registry.register({
|
|
2363
1496
|
name: "exit",
|
|
2364
1497
|
description: "Exit the application",
|
|
@@ -2390,12 +1523,15 @@ ${lines.join("\n")}`);
|
|
|
2390
1523
|
registry.register({
|
|
2391
1524
|
name: "resume",
|
|
2392
1525
|
description: "List sessions or resume a session by id",
|
|
2393
|
-
usage: "/resume [sessionId]",
|
|
1526
|
+
usage: "/resume [sessionId | --all]",
|
|
2394
1527
|
async run(args, ctx) {
|
|
2395
|
-
|
|
1528
|
+
const arg = args.trim();
|
|
1529
|
+
if (arg === "--all") {
|
|
1530
|
+
ctx.addSystemMessage(await ctx.listSessions(true));
|
|
1531
|
+
} else if (!arg) {
|
|
2396
1532
|
ctx.addSystemMessage(await ctx.listSessions());
|
|
2397
1533
|
} else {
|
|
2398
|
-
ctx.addSystemMessage(await ctx.resumeSession(
|
|
1534
|
+
ctx.addSystemMessage(await ctx.resumeSession(arg));
|
|
2399
1535
|
}
|
|
2400
1536
|
}
|
|
2401
1537
|
});
|
|
@@ -2423,6 +1559,14 @@ ${lines.join("\n")}`);
|
|
|
2423
1559
|
ctx.addSystemMessage(ctx.showUsage());
|
|
2424
1560
|
}
|
|
2425
1561
|
});
|
|
1562
|
+
registry.register({
|
|
1563
|
+
name: "usage",
|
|
1564
|
+
description: "Show token usage aggregated across all sessions",
|
|
1565
|
+
usage: "/usage",
|
|
1566
|
+
async run(_args, ctx) {
|
|
1567
|
+
ctx.addSystemMessage(await ctx.showGlobalUsage());
|
|
1568
|
+
}
|
|
1569
|
+
});
|
|
2426
1570
|
registry.register({
|
|
2427
1571
|
name: "config",
|
|
2428
1572
|
description: "Show the current configuration",
|
|
@@ -2471,6 +1615,14 @@ ${lines.join("\n")}`);
|
|
|
2471
1615
|
ctx.addSystemMessage(await ctx.undo());
|
|
2472
1616
|
}
|
|
2473
1617
|
});
|
|
1618
|
+
registry.register({
|
|
1619
|
+
name: "rewind",
|
|
1620
|
+
description: "List file-change checkpoints, or rewind to just before one: restore files and retract the conversation",
|
|
1621
|
+
usage: "/rewind [n]",
|
|
1622
|
+
async run(args, ctx) {
|
|
1623
|
+
ctx.addSystemMessage(await ctx.rewind(args.trim()));
|
|
1624
|
+
}
|
|
1625
|
+
});
|
|
2474
1626
|
registry.register({
|
|
2475
1627
|
name: "init",
|
|
2476
1628
|
description: "Generate an AGENTS.md for the current project",
|
|
@@ -2487,11 +1639,114 @@ ${lines.join("\n")}`);
|
|
|
2487
1639
|
ctx.addSystemMessage(await ctx.runDoctor());
|
|
2488
1640
|
}
|
|
2489
1641
|
});
|
|
1642
|
+
registry.register({
|
|
1643
|
+
name: "skills",
|
|
1644
|
+
description: "List available skills (project scope overrides user scope)",
|
|
1645
|
+
usage: "/skills",
|
|
1646
|
+
run(_args, ctx) {
|
|
1647
|
+
const skills = discoverSkills(ctx.cwd ?? process.cwd());
|
|
1648
|
+
if (skills.length === 0) {
|
|
1649
|
+
ctx.addSystemMessage(
|
|
1650
|
+
"No skills found. Add a SKILL.md under .star/skills/<name>/ (project) or ~/.star-cli/skills/<name>/ (user)."
|
|
1651
|
+
);
|
|
1652
|
+
return;
|
|
1653
|
+
}
|
|
1654
|
+
const lines = skills.map((s) => `${s.name} [${s.scope}] \u2014 ${s.description}`);
|
|
1655
|
+
ctx.addSystemMessage(`Available skills:
|
|
1656
|
+
${lines.join("\n")}`);
|
|
1657
|
+
}
|
|
1658
|
+
});
|
|
1659
|
+
registry.register({
|
|
1660
|
+
name: "commit",
|
|
1661
|
+
description: "Analyze uncommitted changes and create a git commit (Conventional Commits)",
|
|
1662
|
+
usage: "/commit [instructions]",
|
|
1663
|
+
async run(args, ctx) {
|
|
1664
|
+
const cwd = ctx.cwd ?? process.cwd();
|
|
1665
|
+
if (!isGitRepo(cwd)) {
|
|
1666
|
+
ctx.addSystemMessage("/commit: not a git repository \u2014 nothing to do.");
|
|
1667
|
+
return;
|
|
1668
|
+
}
|
|
1669
|
+
const context = collectCommitContext(cwd);
|
|
1670
|
+
if (!context) {
|
|
1671
|
+
ctx.addSystemMessage("/commit: working tree clean \u2014 nothing to commit.");
|
|
1672
|
+
return;
|
|
1673
|
+
}
|
|
1674
|
+
const prompt = buildCommitPrompt(context, args);
|
|
1675
|
+
if (ctx.submitPrompt) {
|
|
1676
|
+
return ctx.submitPrompt(prompt);
|
|
1677
|
+
}
|
|
1678
|
+
ctx.addSystemMessage("/commit: this context cannot submit prompts to the model.");
|
|
1679
|
+
}
|
|
1680
|
+
});
|
|
1681
|
+
registry.register({
|
|
1682
|
+
name: "copy",
|
|
1683
|
+
description: "Copy the last assistant reply to the clipboard (`all` for the whole conversation)",
|
|
1684
|
+
usage: "/copy [all]",
|
|
1685
|
+
async run(args, ctx) {
|
|
1686
|
+
const arg = args.trim();
|
|
1687
|
+
if (arg !== "" && arg !== "all") {
|
|
1688
|
+
ctx.addSystemMessage(`/copy: unknown argument "${arg}". Usage: /copy [all]`);
|
|
1689
|
+
return;
|
|
1690
|
+
}
|
|
1691
|
+
if (!ctx.conversationText || !ctx.copyToClipboard) {
|
|
1692
|
+
ctx.addSystemMessage("/copy: this context cannot access the clipboard.");
|
|
1693
|
+
return;
|
|
1694
|
+
}
|
|
1695
|
+
const scope = arg === "all" ? "all" : "last";
|
|
1696
|
+
const text = ctx.conversationText(scope);
|
|
1697
|
+
if (!text) {
|
|
1698
|
+
ctx.addSystemMessage(
|
|
1699
|
+
scope === "all" ? "Nothing to copy yet." : "No assistant reply yet \u2014 nothing to copy."
|
|
1700
|
+
);
|
|
1701
|
+
return;
|
|
1702
|
+
}
|
|
1703
|
+
const ok = await ctx.copyToClipboard(text);
|
|
1704
|
+
ctx.addSystemMessage(
|
|
1705
|
+
ok ? `Copied ${text.length} chars to clipboard.` : "Copy failed \u2014 no clipboard command available on this platform."
|
|
1706
|
+
);
|
|
1707
|
+
}
|
|
1708
|
+
});
|
|
1709
|
+
registry.register({
|
|
1710
|
+
name: "diff",
|
|
1711
|
+
description: "Show uncommitted changes (git status + colored diff)",
|
|
1712
|
+
usage: "/diff",
|
|
1713
|
+
run(_args, ctx) {
|
|
1714
|
+
const cwd = ctx.cwd ?? process.cwd();
|
|
1715
|
+
if (!isGitRepo(cwd)) {
|
|
1716
|
+
ctx.addSystemMessage("/diff: not a git repository \u2014 nothing to show.");
|
|
1717
|
+
return;
|
|
1718
|
+
}
|
|
1719
|
+
const result = collectWorkingDiff(cwd);
|
|
1720
|
+
if (!result || result.status === "") {
|
|
1721
|
+
ctx.addSystemMessage("Working tree clean \u2014 no uncommitted changes.");
|
|
1722
|
+
return;
|
|
1723
|
+
}
|
|
1724
|
+
const header = `git status --short:
|
|
1725
|
+
${result.status}`;
|
|
1726
|
+
if (result.diff === "") {
|
|
1727
|
+
ctx.addSystemMessage(
|
|
1728
|
+
`${header}
|
|
1729
|
+
|
|
1730
|
+
Only untracked files \u2014 no tracked modifications to diff.`
|
|
1731
|
+
);
|
|
1732
|
+
return;
|
|
1733
|
+
}
|
|
1734
|
+
const note = result.truncated ? `diff truncated at ${WORKING_DIFF_MAX_LINES} lines` : void 0;
|
|
1735
|
+
if (ctx.showDiff) {
|
|
1736
|
+
ctx.showDiff(header, parseGitDiffLines(result.diff), note);
|
|
1737
|
+
return;
|
|
1738
|
+
}
|
|
1739
|
+
ctx.addSystemMessage(`${header}
|
|
1740
|
+
|
|
1741
|
+
${result.diff}${note ? `
|
|
1742
|
+
... (${note})` : ""}`);
|
|
1743
|
+
}
|
|
1744
|
+
});
|
|
2490
1745
|
}
|
|
2491
1746
|
|
|
2492
1747
|
// src/cli/commands/custom.ts
|
|
2493
1748
|
import { readFileSync, readdirSync } from "fs";
|
|
2494
|
-
import
|
|
1749
|
+
import path11 from "path";
|
|
2495
1750
|
var NAME_PATTERN = /^[a-z0-9-]+$/;
|
|
2496
1751
|
var DESCRIPTION_PATTERN = /^<!--\s*description:\s*(.+?)\s*-->\s*$/;
|
|
2497
1752
|
var DEFAULT_DESCRIPTION = "Custom prompt command";
|
|
@@ -2526,14 +1781,14 @@ function scanDir(dir, into) {
|
|
|
2526
1781
|
if (!entry.endsWith(".md")) continue;
|
|
2527
1782
|
const name = entry.slice(0, -".md".length);
|
|
2528
1783
|
if (!NAME_PATTERN.test(name)) continue;
|
|
2529
|
-
const parsed = parseCommandFile(
|
|
1784
|
+
const parsed = parseCommandFile(path11.join(dir, entry), name);
|
|
2530
1785
|
if (parsed) into.set(name, parsed);
|
|
2531
1786
|
}
|
|
2532
1787
|
}
|
|
2533
1788
|
function loadCustomCommands(cwd, home = starHome()) {
|
|
2534
1789
|
const byName = /* @__PURE__ */ new Map();
|
|
2535
|
-
scanDir(
|
|
2536
|
-
scanDir(
|
|
1790
|
+
scanDir(path11.join(home, "commands"), byName);
|
|
1791
|
+
scanDir(path11.join(cwd, ".star", "commands"), byName);
|
|
2537
1792
|
return [...byName.values()];
|
|
2538
1793
|
}
|
|
2539
1794
|
function registerCustomCommands(registry, cwd, home) {
|
|
@@ -2558,8 +1813,8 @@ function registerCustomCommands(registry, cwd, home) {
|
|
|
2558
1813
|
}
|
|
2559
1814
|
|
|
2560
1815
|
// src/cli/commands/doctor.ts
|
|
2561
|
-
import
|
|
2562
|
-
import
|
|
1816
|
+
import fs8 from "fs/promises";
|
|
1817
|
+
import path12 from "path";
|
|
2563
1818
|
var MIN_NODE_MAJOR = 20;
|
|
2564
1819
|
var REGISTRY_URL = "https://registry.npmjs.org/";
|
|
2565
1820
|
var REGISTRY_TIMEOUT_MS = 5e3;
|
|
@@ -2573,7 +1828,7 @@ function nodeMajor(version) {
|
|
|
2573
1828
|
}
|
|
2574
1829
|
async function pathExists(target) {
|
|
2575
1830
|
try {
|
|
2576
|
-
await
|
|
1831
|
+
await fs8.access(target);
|
|
2577
1832
|
return true;
|
|
2578
1833
|
} catch {
|
|
2579
1834
|
return false;
|
|
@@ -2611,7 +1866,7 @@ async function runDoctor(opts) {
|
|
|
2611
1866
|
detail: `${spec.label} (${spec.shell})`
|
|
2612
1867
|
});
|
|
2613
1868
|
}
|
|
2614
|
-
const configPath =
|
|
1869
|
+
const configPath = path12.join(home, "config.toml");
|
|
2615
1870
|
const configExists = await pathExists(configPath);
|
|
2616
1871
|
checks.push({
|
|
2617
1872
|
name: "STAR_HOME / config",
|
|
@@ -2648,14 +1903,14 @@ async function runDoctor(opts) {
|
|
|
2648
1903
|
});
|
|
2649
1904
|
}
|
|
2650
1905
|
}
|
|
2651
|
-
const sessions =
|
|
1906
|
+
const sessions = path12.join(home, "sessions");
|
|
2652
1907
|
let writable = false;
|
|
2653
1908
|
let writeError = "";
|
|
2654
1909
|
try {
|
|
2655
|
-
await
|
|
2656
|
-
const probe =
|
|
2657
|
-
await
|
|
2658
|
-
await
|
|
1910
|
+
await fs8.mkdir(sessions, { recursive: true });
|
|
1911
|
+
const probe = path12.join(sessions, `.doctor-probe-${process.pid}`);
|
|
1912
|
+
await fs8.writeFile(probe, "ok");
|
|
1913
|
+
await fs8.unlink(probe);
|
|
2659
1914
|
writable = true;
|
|
2660
1915
|
} catch (error) {
|
|
2661
1916
|
writeError = error instanceof Error ? error.message : String(error);
|
|
@@ -2690,15 +1945,15 @@ function formatDoctorReport(report) {
|
|
|
2690
1945
|
}
|
|
2691
1946
|
|
|
2692
1947
|
// src/cli/commands/init-project.ts
|
|
2693
|
-
import
|
|
2694
|
-
import
|
|
2695
|
-
import { generateText
|
|
1948
|
+
import fs9 from "fs/promises";
|
|
1949
|
+
import path13 from "path";
|
|
1950
|
+
import { generateText } from "ai";
|
|
2696
1951
|
var IGNORED_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", "out", "coverage"]);
|
|
2697
1952
|
var MAX_ENTRIES = 20;
|
|
2698
1953
|
var README_HEAD_LINES = 5;
|
|
2699
1954
|
async function fileExists2(target) {
|
|
2700
1955
|
try {
|
|
2701
|
-
await
|
|
1956
|
+
await fs9.access(target);
|
|
2702
1957
|
return true;
|
|
2703
1958
|
} catch {
|
|
2704
1959
|
return false;
|
|
@@ -2706,7 +1961,7 @@ async function fileExists2(target) {
|
|
|
2706
1961
|
}
|
|
2707
1962
|
async function readPackageJson(cwd) {
|
|
2708
1963
|
try {
|
|
2709
|
-
const raw = await
|
|
1964
|
+
const raw = await fs9.readFile(path13.join(cwd, "package.json"), "utf8");
|
|
2710
1965
|
const parsed = JSON.parse(raw);
|
|
2711
1966
|
if (typeof parsed !== "object" || parsed === null) return null;
|
|
2712
1967
|
return parsed;
|
|
@@ -2723,10 +1978,10 @@ function stringRecord(value) {
|
|
|
2723
1978
|
return out;
|
|
2724
1979
|
}
|
|
2725
1980
|
async function detectPackageManager(cwd) {
|
|
2726
|
-
if (await fileExists2(
|
|
2727
|
-
if (await fileExists2(
|
|
2728
|
-
if (await fileExists2(
|
|
2729
|
-
if (await fileExists2(
|
|
1981
|
+
if (await fileExists2(path13.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
|
|
1982
|
+
if (await fileExists2(path13.join(cwd, "yarn.lock"))) return "yarn";
|
|
1983
|
+
if (await fileExists2(path13.join(cwd, "package-lock.json"))) return "npm";
|
|
1984
|
+
if (await fileExists2(path13.join(cwd, "bun.lockb"))) return "bun";
|
|
2730
1985
|
return null;
|
|
2731
1986
|
}
|
|
2732
1987
|
function inferCommands(pm, scripts) {
|
|
@@ -2741,7 +1996,7 @@ function inferCommands(pm, scripts) {
|
|
|
2741
1996
|
}
|
|
2742
1997
|
async function listEntries(cwd) {
|
|
2743
1998
|
try {
|
|
2744
|
-
const dirents = await
|
|
1999
|
+
const dirents = await fs9.readdir(cwd, { withFileTypes: true });
|
|
2745
2000
|
const entries = [];
|
|
2746
2001
|
for (const dirent of dirents) {
|
|
2747
2002
|
if (dirent.isDirectory()) {
|
|
@@ -2760,7 +2015,7 @@ async function listEntries(cwd) {
|
|
|
2760
2015
|
async function readReadmeHead(cwd) {
|
|
2761
2016
|
for (const name of ["README.md", "readme.md", "README"]) {
|
|
2762
2017
|
try {
|
|
2763
|
-
const raw = await
|
|
2018
|
+
const raw = await fs9.readFile(path13.join(cwd, name), "utf8");
|
|
2764
2019
|
return raw.split("\n").map((line) => line.trim()).filter((line) => line.length > 0).slice(0, README_HEAD_LINES);
|
|
2765
2020
|
} catch {
|
|
2766
2021
|
}
|
|
@@ -2773,14 +2028,14 @@ async function scanProject(cwd) {
|
|
|
2773
2028
|
const scripts = stringRecord(pkg?.scripts);
|
|
2774
2029
|
return {
|
|
2775
2030
|
name: typeof pkg?.name === "string" ? pkg.name : null,
|
|
2776
|
-
dirName:
|
|
2031
|
+
dirName: path13.basename(cwd),
|
|
2777
2032
|
packageManager,
|
|
2778
2033
|
scripts,
|
|
2779
2034
|
dependencies: Object.keys(stringRecord(pkg?.dependencies)),
|
|
2780
2035
|
devDependencies: Object.keys(stringRecord(pkg?.devDependencies)),
|
|
2781
2036
|
commands: inferCommands(packageManager, scripts),
|
|
2782
|
-
hasTsconfig: await fileExists2(
|
|
2783
|
-
isGitRepo: await fileExists2(
|
|
2037
|
+
hasTsconfig: await fileExists2(path13.join(cwd, "tsconfig.json")),
|
|
2038
|
+
isGitRepo: await fileExists2(path13.join(cwd, ".git")),
|
|
2784
2039
|
readmeHead: await readReadmeHead(cwd),
|
|
2785
2040
|
entries: await listEntries(cwd)
|
|
2786
2041
|
};
|
|
@@ -2833,7 +2088,7 @@ var INIT_SYSTEM_PROMPT = [
|
|
|
2833
2088
|
"Output only the Markdown, no preamble, no code fences."
|
|
2834
2089
|
].join("\n");
|
|
2835
2090
|
async function polishWithModel(facts, model) {
|
|
2836
|
-
const { text } = await
|
|
2091
|
+
const { text } = await generateText({
|
|
2837
2092
|
model,
|
|
2838
2093
|
system: INIT_SYSTEM_PROMPT,
|
|
2839
2094
|
prompt: `Project facts:
|
|
@@ -2847,7 +2102,7 @@ Write the AGENTS.md.`
|
|
|
2847
2102
|
}
|
|
2848
2103
|
async function initProject(opts) {
|
|
2849
2104
|
const { cwd, args, model } = opts;
|
|
2850
|
-
const target =
|
|
2105
|
+
const target = path13.join(cwd, "AGENTS.md");
|
|
2851
2106
|
const force = args.split(/\s+/).includes("force");
|
|
2852
2107
|
if (!force && await fileExists2(target)) {
|
|
2853
2108
|
return {
|
|
@@ -2881,7 +2136,7 @@ async function initProject(opts) {
|
|
|
2881
2136
|
content = renderAgentsMd(facts);
|
|
2882
2137
|
note = " No model available; used the template \u2014 review and refine manually.";
|
|
2883
2138
|
}
|
|
2884
|
-
await
|
|
2139
|
+
await fs9.writeFile(target, content);
|
|
2885
2140
|
const lines = content.split("\n").length;
|
|
2886
2141
|
const how = usedLlm ? "generated with model refinement" : "generated from template";
|
|
2887
2142
|
return {
|
|
@@ -2922,6 +2177,44 @@ function parseSlashCommand(input) {
|
|
|
2922
2177
|
};
|
|
2923
2178
|
}
|
|
2924
2179
|
|
|
2180
|
+
// src/cli/commands/rewind.ts
|
|
2181
|
+
import path14 from "path";
|
|
2182
|
+
function planRewind(snapshots, id) {
|
|
2183
|
+
const target = snapshots.find((snapshot) => snapshot.id === id);
|
|
2184
|
+
if (!target) return null;
|
|
2185
|
+
const affected = snapshots.filter((snapshot) => snapshot.id >= id);
|
|
2186
|
+
let messageIndex = null;
|
|
2187
|
+
for (const snapshot of affected) {
|
|
2188
|
+
if (snapshot.messageIndex >= 0) {
|
|
2189
|
+
messageIndex = messageIndex === null ? snapshot.messageIndex : Math.min(messageIndex, snapshot.messageIndex);
|
|
2190
|
+
}
|
|
2191
|
+
}
|
|
2192
|
+
return { target, affected, messageIndex };
|
|
2193
|
+
}
|
|
2194
|
+
function displayPath(filePath, cwd) {
|
|
2195
|
+
const relative = path14.relative(cwd, filePath);
|
|
2196
|
+
return relative !== "" && !relative.startsWith("..") ? relative : filePath;
|
|
2197
|
+
}
|
|
2198
|
+
function formatTime(timestamp) {
|
|
2199
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
2200
|
+
const date = new Date(timestamp);
|
|
2201
|
+
return `${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
|
|
2202
|
+
}
|
|
2203
|
+
function formatCheckpointList(snapshots, cwd) {
|
|
2204
|
+
if (snapshots.length === 0) {
|
|
2205
|
+
return "No checkpoints yet \u2014 checkpoints are recorded on every successful write_file/edit_file.";
|
|
2206
|
+
}
|
|
2207
|
+
const lines = snapshots.map((snapshot) => {
|
|
2208
|
+
const label = snapshot.existed ? snapshot.toolName : `${snapshot.toolName} (new file)`;
|
|
2209
|
+
return `#${snapshot.id} ${formatTime(snapshot.timestamp)} ${label} ${displayPath(snapshot.path, cwd)}`;
|
|
2210
|
+
});
|
|
2211
|
+
return [
|
|
2212
|
+
`Checkpoints (${snapshots.length}):`,
|
|
2213
|
+
...lines,
|
|
2214
|
+
"Use /rewind <n> to restore files and conversation to just before checkpoint n."
|
|
2215
|
+
].join("\n");
|
|
2216
|
+
}
|
|
2217
|
+
|
|
2925
2218
|
// src/cli/components/ExitPlanPrompt.tsx
|
|
2926
2219
|
import { Box, Text, useInput } from "ink";
|
|
2927
2220
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
@@ -2940,7 +2233,7 @@ function ExitPlanPrompt({ onDecision }) {
|
|
|
2940
2233
|
|
|
2941
2234
|
// src/cli/components/InputBox.tsx
|
|
2942
2235
|
import { Box as Box2, Text as Text2, useInput as useInput2 } from "ink";
|
|
2943
|
-
import { useRef, useState } from "react";
|
|
2236
|
+
import { useEffect, useRef, useState } from "react";
|
|
2944
2237
|
|
|
2945
2238
|
// src/cli/commands/suggest.ts
|
|
2946
2239
|
var MAX_SUGGESTIONS = 5;
|
|
@@ -2951,36 +2244,116 @@ function filterCommands(input, commands) {
|
|
|
2951
2244
|
return commands.filter((cmd) => cmd.name.toLowerCase().startsWith(prefix)).sort((a, b) => a.name.localeCompare(b.name)).slice(0, MAX_SUGGESTIONS);
|
|
2952
2245
|
}
|
|
2953
2246
|
|
|
2247
|
+
// src/cli/path-suggest.ts
|
|
2248
|
+
import { readdir } from "fs/promises";
|
|
2249
|
+
import path15 from "path";
|
|
2250
|
+
var MAX_SCAN_ENTRIES = 2e3;
|
|
2251
|
+
function extractAtToken(value, cursor) {
|
|
2252
|
+
const before = value.slice(0, cursor);
|
|
2253
|
+
const match = /(?:^|\s)@(\S*)$/.exec(before);
|
|
2254
|
+
if (!match) return null;
|
|
2255
|
+
const token = match[1] ?? "";
|
|
2256
|
+
return { token, start: cursor - token.length - 1, end: cursor };
|
|
2257
|
+
}
|
|
2258
|
+
async function suggestPaths(token, cwd, maxScanEntries = MAX_SCAN_ENTRIES, maxSuggestions = MAX_SUGGESTIONS) {
|
|
2259
|
+
const normalized = token.replace(/\\/g, "/");
|
|
2260
|
+
const slash = normalized.lastIndexOf("/");
|
|
2261
|
+
const dirPart = slash >= 0 ? normalized.slice(0, slash + 1) : "";
|
|
2262
|
+
const basePrefix = slash >= 0 ? normalized.slice(slash + 1) : normalized;
|
|
2263
|
+
if (dirPart.split("/").some((segment) => SKIP_DIRS.has(segment))) return [];
|
|
2264
|
+
const dirAbs = path15.resolve(cwd, dirPart === "" ? "." : dirPart);
|
|
2265
|
+
const entries = await readdir(dirAbs, { withFileTypes: true }).catch(() => null);
|
|
2266
|
+
if (!entries) return [];
|
|
2267
|
+
const lower = basePrefix.toLowerCase();
|
|
2268
|
+
const matches = [];
|
|
2269
|
+
let scanned = 0;
|
|
2270
|
+
for (const entry of entries) {
|
|
2271
|
+
if (scanned >= maxScanEntries) break;
|
|
2272
|
+
scanned += 1;
|
|
2273
|
+
const isDir = entry.isDirectory();
|
|
2274
|
+
if (!isDir && !entry.isFile()) continue;
|
|
2275
|
+
if (isDir && SKIP_DIRS.has(entry.name)) continue;
|
|
2276
|
+
if (entry.name.startsWith(".") && !basePrefix.startsWith(".")) continue;
|
|
2277
|
+
if (!entry.name.toLowerCase().startsWith(lower)) continue;
|
|
2278
|
+
matches.push({ path: `${dirPart}${entry.name}${isDir ? "/" : ""}`, isDir });
|
|
2279
|
+
}
|
|
2280
|
+
matches.sort((a, b) => a.path.localeCompare(b.path));
|
|
2281
|
+
return matches.slice(0, maxSuggestions);
|
|
2282
|
+
}
|
|
2283
|
+
|
|
2954
2284
|
// src/cli/components/InputBox.tsx
|
|
2955
2285
|
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
2956
2286
|
function InputBox({
|
|
2957
2287
|
isStreaming,
|
|
2958
2288
|
disabled,
|
|
2959
2289
|
commands,
|
|
2290
|
+
cwd,
|
|
2291
|
+
refill,
|
|
2960
2292
|
onSubmit,
|
|
2961
2293
|
onInterrupt,
|
|
2962
|
-
onExit
|
|
2294
|
+
onExit,
|
|
2295
|
+
onPasteImage
|
|
2963
2296
|
}) {
|
|
2964
2297
|
const [value, setValue] = useState("");
|
|
2965
2298
|
const [cursor, setCursor] = useState(0);
|
|
2966
2299
|
const [history, setHistory] = useState([]);
|
|
2967
2300
|
const [highlight, setHighlight] = useState(0);
|
|
2968
2301
|
const [suggestionsDismissed, setSuggestionsDismissed] = useState(false);
|
|
2302
|
+
const [pathSuggestions, setPathSuggestions] = useState([]);
|
|
2969
2303
|
const historyIndexRef = useRef(null);
|
|
2970
2304
|
const draftRef = useRef("");
|
|
2305
|
+
const refillSeqRef = useRef(0);
|
|
2971
2306
|
const edit = (next, nextCursor) => {
|
|
2972
2307
|
setValue(next);
|
|
2973
2308
|
setCursor(Math.max(0, Math.min(nextCursor, next.length)));
|
|
2974
2309
|
setHighlight(0);
|
|
2975
2310
|
setSuggestionsDismissed(false);
|
|
2976
2311
|
};
|
|
2977
|
-
|
|
2978
|
-
|
|
2312
|
+
useEffect(() => {
|
|
2313
|
+
if (refill && refill.seq !== refillSeqRef.current) {
|
|
2314
|
+
refillSeqRef.current = refill.seq;
|
|
2315
|
+
edit(refill.text, refill.text.length);
|
|
2316
|
+
}
|
|
2317
|
+
});
|
|
2318
|
+
const suggestionsEnabled = !isStreaming && !disabled && !suggestionsDismissed;
|
|
2319
|
+
const suggestions = suggestionsEnabled && commands ? filterCommands(value, commands) : [];
|
|
2320
|
+
useEffect(() => {
|
|
2321
|
+
if (!suggestionsEnabled || !cwd) {
|
|
2322
|
+
setPathSuggestions([]);
|
|
2323
|
+
return;
|
|
2324
|
+
}
|
|
2325
|
+
const token = extractAtToken(value, cursor);
|
|
2326
|
+
if (!token) {
|
|
2327
|
+
setPathSuggestions([]);
|
|
2328
|
+
return;
|
|
2329
|
+
}
|
|
2330
|
+
let cancelled = false;
|
|
2331
|
+
void suggestPaths(token.token, cwd).then((result) => {
|
|
2332
|
+
if (!cancelled) setPathSuggestions(result);
|
|
2333
|
+
});
|
|
2334
|
+
return () => {
|
|
2335
|
+
cancelled = true;
|
|
2336
|
+
};
|
|
2337
|
+
}, [value, cursor, cwd, suggestionsEnabled]);
|
|
2338
|
+
const showPathSuggestions = suggestions.length === 0 && pathSuggestions.length > 0;
|
|
2339
|
+
const suggestionCount = suggestions.length > 0 ? suggestions.length : pathSuggestions.length;
|
|
2340
|
+
const activeIndex = suggestionCount === 0 ? 0 : Math.min(highlight, suggestionCount - 1);
|
|
2979
2341
|
const completeHighlighted = () => {
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
|
|
2342
|
+
if (suggestions.length > 0) {
|
|
2343
|
+
const cmd = suggestions[activeIndex];
|
|
2344
|
+
if (!cmd) return;
|
|
2345
|
+
const text = `/${cmd.name} `;
|
|
2346
|
+
edit(text, text.length);
|
|
2347
|
+
return;
|
|
2348
|
+
}
|
|
2349
|
+
const sugg = pathSuggestions[activeIndex];
|
|
2350
|
+
const token = extractAtToken(value, cursor);
|
|
2351
|
+
if (!sugg || !token) return;
|
|
2352
|
+
const replacement = `@${sugg.path}${sugg.isDir ? "" : " "}`;
|
|
2353
|
+
edit(
|
|
2354
|
+
value.slice(0, token.start) + replacement + value.slice(token.end),
|
|
2355
|
+
token.start + replacement.length
|
|
2356
|
+
);
|
|
2984
2357
|
};
|
|
2985
2358
|
useInput2((input, key) => {
|
|
2986
2359
|
if (key.ctrl && input === "c") {
|
|
@@ -2996,9 +2369,13 @@ function InputBox({
|
|
|
2996
2369
|
onExit();
|
|
2997
2370
|
return;
|
|
2998
2371
|
}
|
|
2999
|
-
if (
|
|
2372
|
+
if ((key.meta || key.ctrl) && input === "v") {
|
|
2373
|
+
onPasteImage?.();
|
|
2374
|
+
return;
|
|
2375
|
+
}
|
|
2376
|
+
if (disabled) return;
|
|
3000
2377
|
if (key.escape) {
|
|
3001
|
-
if (
|
|
2378
|
+
if (!isStreaming && suggestionCount > 0) setSuggestionsDismissed(true);
|
|
3002
2379
|
return;
|
|
3003
2380
|
}
|
|
3004
2381
|
if (key.return) {
|
|
@@ -3014,13 +2391,13 @@ function InputBox({
|
|
|
3014
2391
|
}
|
|
3015
2392
|
if (key.tab) {
|
|
3016
2393
|
if (key.shift) return;
|
|
3017
|
-
if (
|
|
2394
|
+
if (suggestionCount > 0) {
|
|
3018
2395
|
completeHighlighted();
|
|
3019
2396
|
return;
|
|
3020
2397
|
}
|
|
3021
2398
|
} else if (key.upArrow) {
|
|
3022
|
-
if (
|
|
3023
|
-
setHighlight((prev) => (prev - 1 +
|
|
2399
|
+
if (suggestionCount > 0) {
|
|
2400
|
+
setHighlight((prev) => (prev - 1 + suggestionCount) % suggestionCount);
|
|
3024
2401
|
return;
|
|
3025
2402
|
}
|
|
3026
2403
|
if (history.length === 0) return;
|
|
@@ -3034,8 +2411,8 @@ function InputBox({
|
|
|
3034
2411
|
edit(entry, entry.length);
|
|
3035
2412
|
return;
|
|
3036
2413
|
} else if (key.downArrow) {
|
|
3037
|
-
if (
|
|
3038
|
-
setHighlight((prev) => (prev + 1) %
|
|
2414
|
+
if (suggestionCount > 0) {
|
|
2415
|
+
setHighlight((prev) => (prev + 1) % suggestionCount);
|
|
3039
2416
|
return;
|
|
3040
2417
|
}
|
|
3041
2418
|
if (historyIndexRef.current === null) return;
|
|
@@ -3052,7 +2429,7 @@ function InputBox({
|
|
|
3052
2429
|
setCursor((prev) => Math.max(0, prev - 1));
|
|
3053
2430
|
return;
|
|
3054
2431
|
} else if (key.rightArrow) {
|
|
3055
|
-
if (
|
|
2432
|
+
if (suggestionCount > 0 && cursor === value.length) {
|
|
3056
2433
|
completeHighlighted();
|
|
3057
2434
|
} else {
|
|
3058
2435
|
setCursor((prev) => Math.min(value.length, prev + 1));
|
|
@@ -3106,14 +2483,47 @@ function InputBox({
|
|
|
3106
2483
|
/* @__PURE__ */ jsx2(Text2, { color: "cyan", children: `/${cmd.name}` }),
|
|
3107
2484
|
/* @__PURE__ */ jsx2(Text2, { dimColor: true, children: ` - ${cmd.description}` })
|
|
3108
2485
|
] }, cmd.name)
|
|
2486
|
+
) }),
|
|
2487
|
+
showPathSuggestions && /* @__PURE__ */ jsx2(Box2, { flexDirection: "column", paddingLeft: 2, children: pathSuggestions.map(
|
|
2488
|
+
(sugg, index) => index === activeIndex ? /* @__PURE__ */ jsx2(Text2, { bold: true, inverse: true, children: `@${sugg.path}` }, sugg.path) : /* @__PURE__ */ jsxs2(Text2, { children: [
|
|
2489
|
+
/* @__PURE__ */ jsx2(Text2, { color: "cyan", children: `@${sugg.path}` }),
|
|
2490
|
+
sugg.isDir && /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: " - directory" })
|
|
2491
|
+
] }, sugg.path)
|
|
3109
2492
|
) })
|
|
3110
2493
|
] });
|
|
3111
2494
|
}
|
|
3112
2495
|
|
|
3113
2496
|
// src/cli/components/MessageList.tsx
|
|
3114
|
-
import { Box as Box3, Static, Text as
|
|
2497
|
+
import { Box as Box3, Static, Text as Text4 } from "ink";
|
|
3115
2498
|
import { memo } from "react";
|
|
3116
|
-
|
|
2499
|
+
|
|
2500
|
+
// src/cli/components/DiffLines.tsx
|
|
2501
|
+
import { Text as Text3 } from "ink";
|
|
2502
|
+
import { Fragment, jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
2503
|
+
function DiffLineView({ line }) {
|
|
2504
|
+
if (line.kind === "add") return /* @__PURE__ */ jsxs3(Text3, { color: "green", children: [
|
|
2505
|
+
"+ ",
|
|
2506
|
+
line.text
|
|
2507
|
+
] });
|
|
2508
|
+
if (line.kind === "del") return /* @__PURE__ */ jsxs3(Text3, { color: "red", children: [
|
|
2509
|
+
"- ",
|
|
2510
|
+
line.text
|
|
2511
|
+
] });
|
|
2512
|
+
if (line.kind === "marker") return /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: line.text });
|
|
2513
|
+
return /* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
|
|
2514
|
+
" ",
|
|
2515
|
+
line.text
|
|
2516
|
+
] });
|
|
2517
|
+
}
|
|
2518
|
+
function DiffLines({ lines }) {
|
|
2519
|
+
return /* @__PURE__ */ jsx3(Fragment, { children: lines.map((line, index) => (
|
|
2520
|
+
// biome-ignore lint/suspicious/noArrayIndexKey: diff lines have no stable identity
|
|
2521
|
+
/* @__PURE__ */ jsx3(DiffLineView, { line }, index)
|
|
2522
|
+
)) });
|
|
2523
|
+
}
|
|
2524
|
+
|
|
2525
|
+
// src/cli/components/MessageList.tsx
|
|
2526
|
+
import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
3117
2527
|
var roleStyles = {
|
|
3118
2528
|
user: { label: "you", color: "cyan" },
|
|
3119
2529
|
assistant: { label: "star", color: "green" },
|
|
@@ -3121,38 +2531,24 @@ var roleStyles = {
|
|
|
3121
2531
|
tool: { label: "tool", color: "magenta" }
|
|
3122
2532
|
};
|
|
3123
2533
|
var MessageList = memo(function MessageList2({ messages }) {
|
|
3124
|
-
return /* @__PURE__ */
|
|
2534
|
+
return /* @__PURE__ */ jsx4(Static, { items: messages, children: (message) => {
|
|
3125
2535
|
const marginBottom = message.tight ? 0 : 1;
|
|
3126
2536
|
if (message.role === "assistant-cont") {
|
|
3127
|
-
return /* @__PURE__ */
|
|
2537
|
+
return /* @__PURE__ */ jsx4(Box3, { flexDirection: "column", marginBottom, children: /* @__PURE__ */ jsx4(Text4, { color: "green", children: message.text }) }, message.id);
|
|
3128
2538
|
}
|
|
3129
2539
|
const style = roleStyles[message.role];
|
|
3130
|
-
return /* @__PURE__ */
|
|
3131
|
-
/* @__PURE__ */
|
|
3132
|
-
/* @__PURE__ */
|
|
3133
|
-
message.
|
|
2540
|
+
return /* @__PURE__ */ jsxs4(Box3, { flexDirection: "column", marginBottom, children: [
|
|
2541
|
+
/* @__PURE__ */ jsx4(Text4, { bold: true, color: style.color, children: style.label }),
|
|
2542
|
+
/* @__PURE__ */ jsx4(Text4, { color: style.color, children: message.text }),
|
|
2543
|
+
message.diff && /* @__PURE__ */ jsx4(DiffLines, { lines: message.diff }),
|
|
2544
|
+
message.note && /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: message.note })
|
|
3134
2545
|
] }, message.id);
|
|
3135
2546
|
} });
|
|
3136
2547
|
});
|
|
3137
2548
|
|
|
3138
2549
|
// src/cli/components/PermissionPrompt.tsx
|
|
3139
|
-
import { Box as Box4, Text as
|
|
3140
|
-
import { jsx as
|
|
3141
|
-
function DiffLineView({ line }) {
|
|
3142
|
-
if (line.kind === "add") return /* @__PURE__ */ jsxs4(Text4, { color: "green", children: [
|
|
3143
|
-
"+ ",
|
|
3144
|
-
line.text
|
|
3145
|
-
] });
|
|
3146
|
-
if (line.kind === "del") return /* @__PURE__ */ jsxs4(Text4, { color: "red", children: [
|
|
3147
|
-
"- ",
|
|
3148
|
-
line.text
|
|
3149
|
-
] });
|
|
3150
|
-
if (line.kind === "marker") return /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: line.text });
|
|
3151
|
-
return /* @__PURE__ */ jsxs4(Text4, { dimColor: true, children: [
|
|
3152
|
-
" ",
|
|
3153
|
-
line.text
|
|
3154
|
-
] });
|
|
3155
|
-
}
|
|
2550
|
+
import { Box as Box4, Text as Text5, useInput as useInput3 } from "ink";
|
|
2551
|
+
import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
3156
2552
|
function PermissionPrompt({ request, preview, onDecision }) {
|
|
3157
2553
|
useInput3((input) => {
|
|
3158
2554
|
const ch = input.toLowerCase();
|
|
@@ -3160,58 +2556,96 @@ function PermissionPrompt({ request, preview, onDecision }) {
|
|
|
3160
2556
|
else if (ch === "n") onDecision("no");
|
|
3161
2557
|
else if (ch === "a") onDecision("always");
|
|
3162
2558
|
});
|
|
3163
|
-
return /* @__PURE__ */
|
|
3164
|
-
/* @__PURE__ */
|
|
2559
|
+
return /* @__PURE__ */ jsxs5(Box4, { flexDirection: "column", borderStyle: "round", borderColor: "yellow", paddingX: 1, children: [
|
|
2560
|
+
/* @__PURE__ */ jsxs5(Text5, { bold: true, color: "yellow", children: [
|
|
3165
2561
|
"Permission required: ",
|
|
3166
2562
|
request.toolName,
|
|
3167
2563
|
" (",
|
|
3168
2564
|
request.level,
|
|
3169
2565
|
")"
|
|
3170
2566
|
] }),
|
|
3171
|
-
preview ? /* @__PURE__ */
|
|
3172
|
-
/* @__PURE__ */
|
|
2567
|
+
preview ? /* @__PURE__ */ jsxs5(Box4, { flexDirection: "column", children: [
|
|
2568
|
+
/* @__PURE__ */ jsxs5(Text5, { dimColor: true, children: [
|
|
3173
2569
|
preview.type === "new-file" ? "New file: " : "",
|
|
3174
2570
|
preview.label
|
|
3175
2571
|
] }),
|
|
3176
|
-
preview.lines
|
|
3177
|
-
|
|
3178
|
-
|
|
3179
|
-
|
|
3180
|
-
|
|
3181
|
-
|
|
2572
|
+
/* @__PURE__ */ jsx5(DiffLines, { lines: preview.lines })
|
|
2573
|
+
] }) : /* @__PURE__ */ jsx5(Text5, { children: summarizeArgs(request.args) }),
|
|
2574
|
+
/* @__PURE__ */ jsx5(Text5, { children: "[y] allow [n] deny [a] always (saved to config)" })
|
|
2575
|
+
] });
|
|
2576
|
+
}
|
|
2577
|
+
|
|
2578
|
+
// src/cli/components/RewindConfirmPrompt.tsx
|
|
2579
|
+
import { Box as Box5, Text as Text6, useInput as useInput4 } from "ink";
|
|
2580
|
+
import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
2581
|
+
function RewindConfirmPrompt({ summary, onDecision }) {
|
|
2582
|
+
useInput4((input, key) => {
|
|
2583
|
+
const ch = input.toLowerCase();
|
|
2584
|
+
if (ch === "y") onDecision("yes");
|
|
2585
|
+
else if (ch === "n" || key.escape) onDecision("no");
|
|
2586
|
+
});
|
|
2587
|
+
return /* @__PURE__ */ jsxs6(Box5, { flexDirection: "column", borderStyle: "round", borderColor: "yellow", paddingX: 1, children: [
|
|
2588
|
+
/* @__PURE__ */ jsx6(Text6, { bold: true, color: "yellow", children: "Rewind checkpoints" }),
|
|
2589
|
+
/* @__PURE__ */ jsx6(Text6, { children: summary }),
|
|
2590
|
+
/* @__PURE__ */ jsx6(Text6, { children: "[y] rewind [n] cancel (Esc = n)" })
|
|
3182
2591
|
] });
|
|
3183
2592
|
}
|
|
3184
2593
|
|
|
3185
2594
|
// src/cli/components/StatusBar.tsx
|
|
3186
|
-
import { Box as
|
|
2595
|
+
import { Box as Box6, Text as Text7, useStdout } from "ink";
|
|
3187
2596
|
import { memo as memo2 } from "react";
|
|
3188
|
-
import { jsxs as
|
|
2597
|
+
import { jsxs as jsxs7 } from "react/jsx-runtime";
|
|
3189
2598
|
var BG_LABEL_MAX = 40;
|
|
2599
|
+
var WIDE_COLUMNS = 120;
|
|
2600
|
+
function cwdLabel(cwd, wide) {
|
|
2601
|
+
if (wide) return cwd;
|
|
2602
|
+
const base = cwd.replace(/[\\/]+$/, "").split(/[\\/]/).pop();
|
|
2603
|
+
return base && base.length > 0 ? base : cwd;
|
|
2604
|
+
}
|
|
3190
2605
|
var StatusBar = memo2(function StatusBar2({
|
|
2606
|
+
cwd,
|
|
3191
2607
|
model,
|
|
3192
2608
|
permissionMode,
|
|
3193
|
-
tokens,
|
|
2609
|
+
tokens: tokens2,
|
|
2610
|
+
gitBranch,
|
|
2611
|
+
contextPercent,
|
|
2612
|
+
sessionCostUsd,
|
|
3194
2613
|
backgroundTasks = []
|
|
3195
2614
|
}) {
|
|
2615
|
+
const { stdout } = useStdout();
|
|
2616
|
+
const wide = (stdout?.columns ?? 80) >= WIDE_COLUMNS;
|
|
3196
2617
|
const joined = backgroundTasks.join(", ");
|
|
3197
2618
|
const labels = joined.length > BG_LABEL_MAX ? `${joined.slice(0, BG_LABEL_MAX)}\u2026` : joined;
|
|
3198
|
-
return /* @__PURE__ */
|
|
3199
|
-
/* @__PURE__ */
|
|
3200
|
-
|
|
3201
|
-
|
|
2619
|
+
return /* @__PURE__ */ jsxs7(Box6, { justifyContent: "space-between", children: [
|
|
2620
|
+
/* @__PURE__ */ jsxs7(Text7, { dimColor: true, children: [
|
|
2621
|
+
cwdLabel(cwd, wide),
|
|
2622
|
+
gitBranch ? ` [${gitBranch}]` : ""
|
|
3202
2623
|
] }),
|
|
3203
|
-
backgroundTasks.length > 0 && /* @__PURE__ */
|
|
2624
|
+
backgroundTasks.length > 0 && /* @__PURE__ */ jsxs7(Text7, { color: "yellow", children: [
|
|
3204
2625
|
"bg: ",
|
|
3205
2626
|
backgroundTasks.length,
|
|
3206
2627
|
" (",
|
|
3207
2628
|
labels,
|
|
3208
2629
|
")"
|
|
3209
2630
|
] }),
|
|
3210
|
-
/* @__PURE__ */
|
|
3211
|
-
|
|
2631
|
+
/* @__PURE__ */ jsxs7(Text7, { dimColor: true, children: [
|
|
2632
|
+
"model: ",
|
|
2633
|
+
model
|
|
2634
|
+
] }),
|
|
2635
|
+
contextPercent != null && /* @__PURE__ */ jsxs7(Text7, { dimColor: true, children: [
|
|
2636
|
+
"ctx: ",
|
|
2637
|
+
contextPercent,
|
|
2638
|
+
"%"
|
|
2639
|
+
] }),
|
|
2640
|
+
sessionCostUsd != null && sessionCostUsd > 0 && /* @__PURE__ */ jsxs7(Text7, { dimColor: true, children: [
|
|
2641
|
+
"$",
|
|
2642
|
+
formatDollars(sessionCostUsd)
|
|
2643
|
+
] }),
|
|
2644
|
+
/* @__PURE__ */ jsxs7(Text7, { dimColor: true, children: [
|
|
2645
|
+
tokens2,
|
|
3212
2646
|
" tokens"
|
|
3213
2647
|
] }),
|
|
3214
|
-
/* @__PURE__ */
|
|
2648
|
+
/* @__PURE__ */ jsxs7(Text7, { dimColor: true, children: [
|
|
3215
2649
|
"mode: ",
|
|
3216
2650
|
permissionMode
|
|
3217
2651
|
] })
|
|
@@ -3219,247 +2653,104 @@ var StatusBar = memo2(function StatusBar2({
|
|
|
3219
2653
|
});
|
|
3220
2654
|
|
|
3221
2655
|
// src/cli/components/StreamingMessage.tsx
|
|
3222
|
-
import { Box as
|
|
2656
|
+
import { Box as Box7, Text as Text8 } from "ink";
|
|
3223
2657
|
import { memo as memo3 } from "react";
|
|
3224
|
-
import { jsx as
|
|
2658
|
+
import { jsx as jsx7, jsxs as jsxs8 } from "react/jsx-runtime";
|
|
3225
2659
|
var StreamingMessage = memo3(function StreamingMessage2({
|
|
3226
2660
|
text,
|
|
3227
2661
|
continuation
|
|
3228
2662
|
}) {
|
|
3229
|
-
return /* @__PURE__ */
|
|
3230
|
-
!continuation && /* @__PURE__ */
|
|
3231
|
-
/* @__PURE__ */
|
|
2663
|
+
return /* @__PURE__ */ jsxs8(Box7, { flexDirection: "column", marginBottom: 1, children: [
|
|
2664
|
+
!continuation && /* @__PURE__ */ jsx7(Text8, { bold: true, color: "green", children: "star" }),
|
|
2665
|
+
/* @__PURE__ */ jsx7(Text8, { color: "green", children: text })
|
|
3232
2666
|
] });
|
|
3233
2667
|
});
|
|
3234
|
-
|
|
3235
|
-
// src/cli/components/ThinkingIndicator.tsx
|
|
3236
|
-
import { Box as
|
|
3237
|
-
import { jsxs as
|
|
3238
|
-
var SPINNER_FRAMES = ["-", "\\", "|", "/"];
|
|
3239
|
-
var REASONING_TAIL_LENGTH = 200;
|
|
3240
|
-
var THOUGHT_SUMMARY_LENGTH = 100;
|
|
3241
|
-
function truncateTail(text, max) {
|
|
3242
|
-
const flat = text.replace(/\s+/g, " ").trim();
|
|
3243
|
-
if (flat.length <= max) return flat;
|
|
3244
|
-
return `\u2026${flat.slice(flat.length - max)}`;
|
|
3245
|
-
}
|
|
3246
|
-
function ThinkingIndicator({
|
|
3247
|
-
reasoning,
|
|
3248
|
-
frame = 0,
|
|
3249
|
-
activity
|
|
3250
|
-
}) {
|
|
3251
|
-
const tail = reasoning ? truncateTail(reasoning, REASONING_TAIL_LENGTH) : "";
|
|
3252
|
-
const spinner = SPINNER_FRAMES[frame % SPINNER_FRAMES.length];
|
|
3253
|
-
return /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", children: [
|
|
3254
|
-
/* @__PURE__ */ jsxs7(Text7, { dimColor: true, children: [
|
|
3255
|
-
spinner,
|
|
3256
|
-
" ",
|
|
3257
|
-
activity ?? "star is thinking\u2026"
|
|
3258
|
-
] }),
|
|
3259
|
-
tail !== "" && /* @__PURE__ */ jsxs7(Text7, { dimColor: true, children: [
|
|
3260
|
-
" ",
|
|
3261
|
-
tail
|
|
3262
|
-
] })
|
|
3263
|
-
] });
|
|
3264
|
-
}
|
|
3265
|
-
|
|
3266
|
-
// src/cli/components/ToolCallCard.tsx
|
|
3267
|
-
function formatToolCard(card) {
|
|
3268
|
-
const header = `${card.name} ${card.argsSummary}${card.isError ? " [error]" : ""}`;
|
|
3269
|
-
if (card.result === void 0) return header;
|
|
3270
|
-
const preview = previewLines(card.result, 10);
|
|
3271
|
-
return `${header}
|
|
3272
|
-
${preview.text}${preview.truncated ? "\n... (truncated)" : ""}`;
|
|
3273
|
-
}
|
|
3274
|
-
|
|
3275
|
-
// src/cli/cost.ts
|
|
3276
|
-
function formatDollars(cost) {
|
|
3277
|
-
let s = cost.toFixed(4).replace(/0+$/, "");
|
|
3278
|
-
if (s.endsWith(".")) {
|
|
3279
|
-
s += "00";
|
|
3280
|
-
} else if (s.split(".")[1]?.length === 1) {
|
|
3281
|
-
s += "0";
|
|
3282
|
-
}
|
|
3283
|
-
return s;
|
|
3284
|
-
}
|
|
3285
|
-
function estimateCost(usage, modelName, model) {
|
|
3286
|
-
if (model?.promptPrice === void 0 || model.completionPrice === void 0) {
|
|
3287
|
-
return `Estimated cost: unknown (no price configured for ${modelName})`;
|
|
3288
|
-
}
|
|
3289
|
-
const cost = (usage.promptTokens * model.promptPrice + usage.completionTokens * model.completionPrice) / 1e6;
|
|
3290
|
-
return `Estimated cost: $${formatDollars(cost)} (${model.name} @ $${model.promptPrice}/M prompt, $${model.completionPrice}/M completion)`;
|
|
3291
|
-
}
|
|
3292
|
-
|
|
3293
|
-
// src/cli/diff-preview.ts
|
|
3294
|
-
import { readFile as readFile7 } from "fs/promises";
|
|
3295
|
-
import path20 from "path";
|
|
3296
|
-
var CONTEXT_LINES = 2;
|
|
3297
|
-
var MAX_HUNK_LINES = 15;
|
|
3298
|
-
var MAX_TOTAL_LINES = 12;
|
|
3299
|
-
var NEW_FILE_PREVIEW_LINES = 10;
|
|
3300
|
-
var MAX_DP_CELLS = 25e4;
|
|
3301
|
-
function diffOps(oldText, newText) {
|
|
3302
|
-
const a = oldText.split("\n");
|
|
3303
|
-
const b = newText.split("\n");
|
|
3304
|
-
let start = 0;
|
|
3305
|
-
while (start < a.length && start < b.length && a[start] === b[start]) start += 1;
|
|
3306
|
-
let endA = a.length;
|
|
3307
|
-
let endB = b.length;
|
|
3308
|
-
while (endA > start && endB > start && a[endA - 1] === b[endB - 1]) {
|
|
3309
|
-
endA -= 1;
|
|
3310
|
-
endB -= 1;
|
|
3311
|
-
}
|
|
3312
|
-
const prefix = a.slice(0, start).map((text) => ({ kind: "context", text }));
|
|
3313
|
-
const suffix = a.slice(endA).map((text) => ({ kind: "context", text }));
|
|
3314
|
-
const midA = a.slice(start, endA);
|
|
3315
|
-
const midB = b.slice(start, endB);
|
|
3316
|
-
const prefixLen = prefix.length;
|
|
3317
|
-
const ops = [...prefix];
|
|
3318
|
-
if (midA.length * midB.length > MAX_DP_CELLS) {
|
|
3319
|
-
for (const text of midA) ops.push({ kind: "del", text });
|
|
3320
|
-
for (const text of midB) ops.push({ kind: "add", text });
|
|
3321
|
-
} else if (midA.length > 0 || midB.length > 0) {
|
|
3322
|
-
const rows = midA.length + 1;
|
|
3323
|
-
const cols = midB.length + 1;
|
|
3324
|
-
const dp = new Uint32Array(rows * cols);
|
|
3325
|
-
const cell = (r, c) => dp[r * cols + c] ?? 0;
|
|
3326
|
-
for (let i2 = 1; i2 < rows; i2++) {
|
|
3327
|
-
for (let j2 = 1; j2 < cols; j2++) {
|
|
3328
|
-
dp[i2 * cols + j2] = midA[i2 - 1] === midB[j2 - 1] ? cell(i2 - 1, j2 - 1) + 1 : Math.max(cell(i2 - 1, j2), cell(i2, j2 - 1));
|
|
3329
|
-
}
|
|
3330
|
-
}
|
|
3331
|
-
const lineAt = (lines, index) => lines[index] ?? "";
|
|
3332
|
-
const middle = [];
|
|
3333
|
-
let i = midA.length;
|
|
3334
|
-
let j = midB.length;
|
|
3335
|
-
while (i > 0 && j > 0) {
|
|
3336
|
-
if (midA[i - 1] === midB[j - 1]) {
|
|
3337
|
-
middle.push({ kind: "context", text: lineAt(midA, i - 1) });
|
|
3338
|
-
i -= 1;
|
|
3339
|
-
j -= 1;
|
|
3340
|
-
} else if (cell(i, j - 1) >= cell(i - 1, j)) {
|
|
3341
|
-
middle.push({ kind: "add", text: lineAt(midB, j - 1) });
|
|
3342
|
-
j -= 1;
|
|
3343
|
-
} else {
|
|
3344
|
-
middle.push({ kind: "del", text: lineAt(midA, i - 1) });
|
|
3345
|
-
i -= 1;
|
|
3346
|
-
}
|
|
3347
|
-
}
|
|
3348
|
-
while (i > 0) {
|
|
3349
|
-
middle.push({ kind: "del", text: lineAt(midA, i - 1) });
|
|
3350
|
-
i -= 1;
|
|
3351
|
-
}
|
|
3352
|
-
while (j > 0) {
|
|
3353
|
-
middle.push({ kind: "add", text: lineAt(midB, j - 1) });
|
|
3354
|
-
j -= 1;
|
|
3355
|
-
}
|
|
3356
|
-
middle.reverse();
|
|
3357
|
-
ops.push(...middle);
|
|
3358
|
-
}
|
|
3359
|
-
ops.push(...suffix);
|
|
3360
|
-
return ops.length === prefixLen + suffix.length ? [] : ops;
|
|
2668
|
+
|
|
2669
|
+
// src/cli/components/ThinkingIndicator.tsx
|
|
2670
|
+
import { Box as Box8, Text as Text9 } from "ink";
|
|
2671
|
+
import { jsxs as jsxs9 } from "react/jsx-runtime";
|
|
2672
|
+
var SPINNER_FRAMES = ["-", "\\", "|", "/"];
|
|
2673
|
+
var REASONING_TAIL_LENGTH = 200;
|
|
2674
|
+
var THOUGHT_SUMMARY_LENGTH = 100;
|
|
2675
|
+
function truncateTail(text, max) {
|
|
2676
|
+
const flat = text.replace(/\s+/g, " ").trim();
|
|
2677
|
+
if (flat.length <= max) return flat;
|
|
2678
|
+
return `\u2026${flat.slice(flat.length - max)}`;
|
|
3361
2679
|
}
|
|
3362
|
-
function
|
|
3363
|
-
|
|
3364
|
-
|
|
3365
|
-
|
|
3366
|
-
|
|
3367
|
-
|
|
3368
|
-
const
|
|
3369
|
-
|
|
3370
|
-
|
|
3371
|
-
|
|
3372
|
-
|
|
3373
|
-
|
|
3374
|
-
|
|
3375
|
-
|
|
3376
|
-
|
|
3377
|
-
|
|
3378
|
-
|
|
3379
|
-
|
|
3380
|
-
groupStart = k;
|
|
3381
|
-
}
|
|
3382
|
-
}
|
|
3383
|
-
return ranges;
|
|
2680
|
+
function ThinkingIndicator({
|
|
2681
|
+
reasoning,
|
|
2682
|
+
frame = 0,
|
|
2683
|
+
activity
|
|
2684
|
+
}) {
|
|
2685
|
+
const tail = reasoning ? truncateTail(reasoning, REASONING_TAIL_LENGTH) : "";
|
|
2686
|
+
const spinner = SPINNER_FRAMES[frame % SPINNER_FRAMES.length];
|
|
2687
|
+
return /* @__PURE__ */ jsxs9(Box8, { flexDirection: "column", children: [
|
|
2688
|
+
/* @__PURE__ */ jsxs9(Text9, { dimColor: true, children: [
|
|
2689
|
+
spinner,
|
|
2690
|
+
" ",
|
|
2691
|
+
activity ?? "star is thinking\u2026"
|
|
2692
|
+
] }),
|
|
2693
|
+
tail !== "" && /* @__PURE__ */ jsxs9(Text9, { dimColor: true, children: [
|
|
2694
|
+
" ",
|
|
2695
|
+
tail
|
|
2696
|
+
] })
|
|
2697
|
+
] });
|
|
3384
2698
|
}
|
|
3385
|
-
|
|
3386
|
-
|
|
3387
|
-
|
|
3388
|
-
const
|
|
3389
|
-
|
|
3390
|
-
|
|
3391
|
-
|
|
3392
|
-
|
|
3393
|
-
const hunk = ops.slice(start, end);
|
|
3394
|
-
let shown = hunk;
|
|
3395
|
-
if (hunk.length > MAX_HUNK_LINES) {
|
|
3396
|
-
const headCount = Math.ceil(MAX_HUNK_LINES / 2);
|
|
3397
|
-
shown = [...hunk.slice(0, headCount), ...hunk.slice(headCount - MAX_HUNK_LINES)];
|
|
3398
|
-
}
|
|
3399
|
-
if (hunk.length > shown.length) {
|
|
3400
|
-
omitted += hunk.length - shown.length;
|
|
3401
|
-
}
|
|
3402
|
-
for (const op of shown) lines.push(op);
|
|
3403
|
-
}
|
|
3404
|
-
if (lines.length > MAX_TOTAL_LINES) {
|
|
3405
|
-
omitted += lines.length - MAX_TOTAL_LINES;
|
|
3406
|
-
lines.length = MAX_TOTAL_LINES;
|
|
3407
|
-
}
|
|
3408
|
-
if (omitted > 0) lines.push({ kind: "marker", text: `... (${omitted} more lines)` });
|
|
3409
|
-
return lines;
|
|
2699
|
+
|
|
2700
|
+
// src/cli/components/ToolCallCard.tsx
|
|
2701
|
+
function formatToolCard(card) {
|
|
2702
|
+
const header = `${card.name} ${card.argsSummary}${card.isError ? " [error]" : ""}`;
|
|
2703
|
+
if (card.result === void 0) return header;
|
|
2704
|
+
const preview = previewLines(card.result, 10);
|
|
2705
|
+
return `${header}
|
|
2706
|
+
${preview.text}${preview.truncated ? "\n... (truncated)" : ""}`;
|
|
3410
2707
|
}
|
|
3411
|
-
|
|
3412
|
-
|
|
3413
|
-
|
|
3414
|
-
|
|
3415
|
-
|
|
3416
|
-
return void 0;
|
|
2708
|
+
|
|
2709
|
+
// src/cli/double-esc.ts
|
|
2710
|
+
var DOUBLE_ESC_WINDOW_MS = 600;
|
|
2711
|
+
function isDoubleEscape(previous, now, windowMs = DOUBLE_ESC_WINDOW_MS) {
|
|
2712
|
+
return previous !== null && now - previous <= windowMs;
|
|
3417
2713
|
}
|
|
3418
|
-
|
|
3419
|
-
|
|
3420
|
-
|
|
3421
|
-
|
|
3422
|
-
|
|
3423
|
-
|
|
3424
|
-
|
|
3425
|
-
|
|
3426
|
-
if (count > 1 && !replaceAll) return null;
|
|
3427
|
-
return replaceAll ? content.split(oldString).join(newString) : content.replace(oldString, newString);
|
|
2714
|
+
|
|
2715
|
+
// src/cli/notify.ts
|
|
2716
|
+
var BELL = "\x07";
|
|
2717
|
+
function shouldNotifyBell(input, isTTY) {
|
|
2718
|
+
if (!input.enabled || input.noNotifyEnv || !isTTY) return false;
|
|
2719
|
+
if (input.interrupted) return false;
|
|
2720
|
+
if (input.elapsedMs !== void 0 && input.elapsedMs < input.thresholdSec * 1e3) return false;
|
|
2721
|
+
return true;
|
|
3428
2722
|
}
|
|
3429
|
-
function
|
|
3430
|
-
const
|
|
3431
|
-
|
|
3432
|
-
|
|
3433
|
-
|
|
3434
|
-
lines.push({ kind: "marker", text: `... (${all.length - shown.length} more lines)` });
|
|
3435
|
-
}
|
|
3436
|
-
return { type: "new-file", label, lines };
|
|
2723
|
+
function notifyBell(input, deps = {}) {
|
|
2724
|
+
const isTTY = deps.isTTY ?? Boolean(process.stdout.isTTY);
|
|
2725
|
+
if (!shouldNotifyBell(input, isTTY)) return false;
|
|
2726
|
+
(deps.write ?? ((text) => process.stdout.write(text)))(BELL);
|
|
2727
|
+
return true;
|
|
3437
2728
|
}
|
|
3438
|
-
|
|
3439
|
-
|
|
3440
|
-
|
|
3441
|
-
|
|
3442
|
-
|
|
3443
|
-
|
|
3444
|
-
|
|
3445
|
-
|
|
3446
|
-
}
|
|
3447
|
-
|
|
2729
|
+
|
|
2730
|
+
// src/cli/queue.ts
|
|
2731
|
+
var PromptQueue = class {
|
|
2732
|
+
items = [];
|
|
2733
|
+
nextId = 1;
|
|
2734
|
+
enqueue(prompt) {
|
|
2735
|
+
this.items.push({ id: this.nextId++, ...prompt });
|
|
2736
|
+
return this.items.length;
|
|
2737
|
+
}
|
|
2738
|
+
dequeue() {
|
|
2739
|
+
return this.items.shift();
|
|
2740
|
+
}
|
|
2741
|
+
// Empties the queue and returns what was dropped (for the "cleared N" note).
|
|
2742
|
+
clear() {
|
|
2743
|
+
const removed = this.items;
|
|
2744
|
+
this.items = [];
|
|
2745
|
+
return removed;
|
|
2746
|
+
}
|
|
2747
|
+
get size() {
|
|
2748
|
+
return this.items.length;
|
|
3448
2749
|
}
|
|
3449
|
-
|
|
3450
|
-
|
|
3451
|
-
const newString = getStringArg2(args, "new_string");
|
|
3452
|
-
if (existing === null || oldString === void 0 || newString === void 0) return null;
|
|
3453
|
-
const replaceAll = typeof args === "object" && args !== null && args.replace_all === true;
|
|
3454
|
-
const updated = applyEdit(existing, oldString, newString, replaceAll);
|
|
3455
|
-
if (updated === null) return null;
|
|
3456
|
-
return { type: "diff", label: filePath, lines: buildDiffLines(existing, updated) };
|
|
2750
|
+
list() {
|
|
2751
|
+
return [...this.items];
|
|
3457
2752
|
}
|
|
3458
|
-
|
|
3459
|
-
if (content === void 0) return null;
|
|
3460
|
-
if (existing === null) return newFilePreview(content, filePath);
|
|
3461
|
-
return { type: "diff", label: filePath, lines: buildDiffLines(existing, content) };
|
|
3462
|
-
}
|
|
2753
|
+
};
|
|
3463
2754
|
|
|
3464
2755
|
// src/cli/shell-bang.ts
|
|
3465
2756
|
var SHELL_BANG_DISPLAY_MAX_CHARS = 3e3;
|
|
@@ -3506,9 +2797,9 @@ function startTicker(onTick, intervalMs = TICK_MS) {
|
|
|
3506
2797
|
|
|
3507
2798
|
// src/cli/update-check.ts
|
|
3508
2799
|
var REGISTRY_URL2 = "https://registry.npmjs.org/@cryer%2fstar-cli/latest";
|
|
3509
|
-
var
|
|
2800
|
+
var TIMEOUT_MS = 5e3;
|
|
3510
2801
|
async function defaultFetcher2(url) {
|
|
3511
|
-
return fetch(url, { signal: AbortSignal.timeout(
|
|
2802
|
+
return fetch(url, { signal: AbortSignal.timeout(TIMEOUT_MS) });
|
|
3512
2803
|
}
|
|
3513
2804
|
function isNewerVersion(latest, current) {
|
|
3514
2805
|
const parse3 = (v) => v.split(".").slice(0, 3).map((part) => Number.parseInt(part, 10));
|
|
@@ -3536,7 +2827,7 @@ async function checkForUpdate(currentVersion, fetcher = defaultFetcher2) {
|
|
|
3536
2827
|
}
|
|
3537
2828
|
|
|
3538
2829
|
// src/cli/repl.tsx
|
|
3539
|
-
import { jsx as
|
|
2830
|
+
import { jsx as jsx8, jsxs as jsxs10 } from "react/jsx-runtime";
|
|
3540
2831
|
var CLEAR_TERMINAL = process.platform === "win32" ? "\x1B[2J\x1B[0f" : "\x1B[2J\x1B[3J\x1B[H";
|
|
3541
2832
|
function emptyUsage() {
|
|
3542
2833
|
return { requests: 0, promptTokens: 0, completionTokens: 0, totalTokens: 0 };
|
|
@@ -3575,6 +2866,7 @@ function Repl({
|
|
|
3575
2866
|
const [bgLabels, setBgLabels] = useState2(() => runningTaskLabels());
|
|
3576
2867
|
const [permissionModeState, setPermissionModeState] = useState2(permissionMode);
|
|
3577
2868
|
const [planApproval, setPlanApprovalState] = useState2(false);
|
|
2869
|
+
const [pendingRewind, setPendingRewindState] = useState2(null);
|
|
3578
2870
|
const backendRef = useRef2(backend);
|
|
3579
2871
|
const nextIdRef = useRef2(initialDisplay.length);
|
|
3580
2872
|
const abortRef = useRef2(null);
|
|
@@ -3588,10 +2880,27 @@ function Repl({
|
|
|
3588
2880
|
const alwaysAllowedRef = useRef2(/* @__PURE__ */ new Set());
|
|
3589
2881
|
const prevModeRef = useRef2(null);
|
|
3590
2882
|
const planApprovalRef = useRef2(false);
|
|
2883
|
+
const pendingRewindRef = useRef2(null);
|
|
3591
2884
|
const modelNameRef = useRef2(model);
|
|
2885
|
+
const sessionStoreRef = useRef2(sessionStore);
|
|
3592
2886
|
const usageRef = useRef2(initialUsage ? { ...initialUsage } : emptyUsage());
|
|
3593
2887
|
const turnChunksRef = useRef2(0);
|
|
3594
2888
|
const messagesRef = useRef2(initialDisplay);
|
|
2889
|
+
const [queueItems, setQueueItems] = useState2([]);
|
|
2890
|
+
const queueRef = useRef2(new PromptQueue());
|
|
2891
|
+
const [pendingImages, setPendingImages] = useState2([]);
|
|
2892
|
+
const pendingImagesRef = useRef2([]);
|
|
2893
|
+
const imageSeqRef = useRef2(1);
|
|
2894
|
+
const [inputRefill, setInputRefill] = useState2();
|
|
2895
|
+
const refillSeqRef = useRef2(0);
|
|
2896
|
+
const lastEscRef = useRef2(null);
|
|
2897
|
+
const [gitBranch, setGitBranch] = useState2(null);
|
|
2898
|
+
const [contextPercent, setContextPercent] = useState2(null);
|
|
2899
|
+
const budgetWarnedRef = useRef2(false);
|
|
2900
|
+
const budgetExceededRef = useRef2(false);
|
|
2901
|
+
const runStreamRef = useRef2(
|
|
2902
|
+
null
|
|
2903
|
+
);
|
|
3595
2904
|
const pushMessage = useCallback(
|
|
3596
2905
|
(role, text, note, tight) => {
|
|
3597
2906
|
setMessages((prev) => {
|
|
@@ -3632,6 +2941,42 @@ function Repl({
|
|
|
3632
2941
|
planApprovalRef.current = v;
|
|
3633
2942
|
setPlanApprovalState(v);
|
|
3634
2943
|
}, []);
|
|
2944
|
+
const setPendingRewind = useCallback((p) => {
|
|
2945
|
+
pendingRewindRef.current = p;
|
|
2946
|
+
setPendingRewindState(p);
|
|
2947
|
+
}, []);
|
|
2948
|
+
const stageClipboardImage = useCallback((img) => {
|
|
2949
|
+
pendingImagesRef.current = [
|
|
2950
|
+
...pendingImagesRef.current,
|
|
2951
|
+
{ id: imageSeqRef.current++, image: img }
|
|
2952
|
+
];
|
|
2953
|
+
setPendingImages(pendingImagesRef.current);
|
|
2954
|
+
}, []);
|
|
2955
|
+
const clearPendingImages = useCallback(() => {
|
|
2956
|
+
pendingImagesRef.current = [];
|
|
2957
|
+
setPendingImages([]);
|
|
2958
|
+
}, []);
|
|
2959
|
+
const sessionCostUsd = useCallback(
|
|
2960
|
+
() => computeCostUsd(
|
|
2961
|
+
usageRef.current,
|
|
2962
|
+
config.models.find((m) => m.name === modelNameRef.current)
|
|
2963
|
+
),
|
|
2964
|
+
[config]
|
|
2965
|
+
);
|
|
2966
|
+
const handlePasteImage = useCallback(() => {
|
|
2967
|
+
void readClipboardImage().then((img) => {
|
|
2968
|
+
if (img) stageClipboardImage(img);
|
|
2969
|
+
});
|
|
2970
|
+
}, [stageClipboardImage]);
|
|
2971
|
+
const handleRewindDecision = useCallback(
|
|
2972
|
+
(decision) => {
|
|
2973
|
+
const p = pendingRewindRef.current;
|
|
2974
|
+
if (!p) return;
|
|
2975
|
+
setPendingRewind(null);
|
|
2976
|
+
p.resolve(decision === "yes");
|
|
2977
|
+
},
|
|
2978
|
+
[setPendingRewind]
|
|
2979
|
+
);
|
|
3635
2980
|
const applySessionMode = useCallback(
|
|
3636
2981
|
(mode) => {
|
|
3637
2982
|
if (mode === config.permissionMode) return;
|
|
@@ -3656,10 +3001,11 @@ function Repl({
|
|
|
3656
3001
|
setPendingPermission({ request: req, preview, resolve });
|
|
3657
3002
|
});
|
|
3658
3003
|
};
|
|
3004
|
+
target.onHookWarning = (message) => pushMessage("system", message);
|
|
3659
3005
|
},
|
|
3660
|
-
[setPendingPermission, cwd]
|
|
3006
|
+
[setPendingPermission, cwd, pushMessage]
|
|
3661
3007
|
);
|
|
3662
|
-
|
|
3008
|
+
useEffect2(() => {
|
|
3663
3009
|
attachConfirmHandler(backendRef.current);
|
|
3664
3010
|
return () => {
|
|
3665
3011
|
tickerStopRef.current?.();
|
|
@@ -3667,26 +3013,33 @@ function Repl({
|
|
|
3667
3013
|
pendingRef.current?.resolve(false);
|
|
3668
3014
|
};
|
|
3669
3015
|
}, [attachConfirmHandler]);
|
|
3670
|
-
|
|
3016
|
+
useEffect2(() => {
|
|
3671
3017
|
if (process.env.STAR_NO_UPDATE_CHECK === "1") return;
|
|
3672
3018
|
void checkForUpdate(VERSION).then((message) => {
|
|
3673
3019
|
if (message) pushMessage("system", message);
|
|
3674
3020
|
});
|
|
3675
3021
|
}, [pushMessage]);
|
|
3676
|
-
|
|
3022
|
+
useEffect2(() => {
|
|
3677
3023
|
const onUpdate = (task) => {
|
|
3678
3024
|
setBgLabels(runningTaskLabels());
|
|
3679
3025
|
pushMessage(
|
|
3680
3026
|
"system",
|
|
3681
3027
|
task.status === "running" ? formatTaskStarted(task) : formatTaskFinished(task)
|
|
3682
3028
|
);
|
|
3029
|
+
if (task.status !== "running") {
|
|
3030
|
+
notifyBell({
|
|
3031
|
+
enabled: config.notifyBell,
|
|
3032
|
+
thresholdSec: config.notifyBellThresholdSec,
|
|
3033
|
+
noNotifyEnv: process.env.STAR_NO_NOTIFY === "1"
|
|
3034
|
+
});
|
|
3035
|
+
}
|
|
3683
3036
|
};
|
|
3684
3037
|
defaultTaskManager.on("update", onUpdate);
|
|
3685
3038
|
return () => {
|
|
3686
3039
|
defaultTaskManager.off("update", onUpdate);
|
|
3687
3040
|
defaultTaskManager.cleanup();
|
|
3688
3041
|
};
|
|
3689
|
-
}, [pushMessage]);
|
|
3042
|
+
}, [pushMessage, config]);
|
|
3690
3043
|
const interrupt = useCallback(() => {
|
|
3691
3044
|
abortRef.current?.abort();
|
|
3692
3045
|
const p = pendingRef.current;
|
|
@@ -3694,7 +3047,34 @@ function Repl({
|
|
|
3694
3047
|
setPendingPermission(null);
|
|
3695
3048
|
p.resolve(false);
|
|
3696
3049
|
}
|
|
3697
|
-
|
|
3050
|
+
const cleared = queueRef.current.clear();
|
|
3051
|
+
if (cleared.length > 0) {
|
|
3052
|
+
setQueueItems([]);
|
|
3053
|
+
pushMessage("system", `cleared ${cleared.length} queued message(s)`);
|
|
3054
|
+
}
|
|
3055
|
+
}, [setPendingPermission, pushMessage]);
|
|
3056
|
+
const editLastMessage = useCallback(async () => {
|
|
3057
|
+
const current = backendRef.current;
|
|
3058
|
+
if (typeof current.retractLastTurn !== "function") return;
|
|
3059
|
+
const visible = messagesRef.current;
|
|
3060
|
+
let cut = -1;
|
|
3061
|
+
let text = "";
|
|
3062
|
+
for (let i = visible.length - 1; i >= 0; i--) {
|
|
3063
|
+
const msg = visible[i];
|
|
3064
|
+
if (msg?.role === "user") {
|
|
3065
|
+
cut = i;
|
|
3066
|
+
text = msg.text;
|
|
3067
|
+
break;
|
|
3068
|
+
}
|
|
3069
|
+
}
|
|
3070
|
+
if (cut < 0) return;
|
|
3071
|
+
const { removed } = await current.retractLastTurn();
|
|
3072
|
+
if (removed === 0) return;
|
|
3073
|
+
redrawMessages(visible.slice(0, cut));
|
|
3074
|
+
refillSeqRef.current += 1;
|
|
3075
|
+
setInputRefill({ text, seq: refillSeqRef.current });
|
|
3076
|
+
pushMessage("system", "last message restored for editing");
|
|
3077
|
+
}, [pushMessage, redrawMessages]);
|
|
3698
3078
|
const handleExit = useCallback(() => {
|
|
3699
3079
|
const killed = defaultTaskManager.cleanup();
|
|
3700
3080
|
if (killed.length > 0) {
|
|
@@ -3705,10 +3085,27 @@ function Repl({
|
|
|
3705
3085
|
}
|
|
3706
3086
|
exit();
|
|
3707
3087
|
}, [exit, pushMessage]);
|
|
3708
|
-
|
|
3709
|
-
if (key.escape)
|
|
3088
|
+
useInput5((_input, key) => {
|
|
3089
|
+
if (key.escape) {
|
|
3090
|
+
const busy = abortRef.current !== null || pendingRef.current !== null || planApprovalRef.current || pendingRewindRef.current !== null;
|
|
3091
|
+
if (busy) {
|
|
3092
|
+
lastEscRef.current = null;
|
|
3093
|
+
interrupt();
|
|
3094
|
+
return;
|
|
3095
|
+
}
|
|
3096
|
+
const now = Date.now();
|
|
3097
|
+
if (isDoubleEscape(lastEscRef.current, now)) {
|
|
3098
|
+
lastEscRef.current = null;
|
|
3099
|
+
void editLastMessage();
|
|
3100
|
+
} else {
|
|
3101
|
+
lastEscRef.current = now;
|
|
3102
|
+
}
|
|
3103
|
+
return;
|
|
3104
|
+
}
|
|
3105
|
+
lastEscRef.current = null;
|
|
3710
3106
|
if (key.shift && key.tab) {
|
|
3711
|
-
if (abortRef.current || pendingRef.current || planApprovalRef.current)
|
|
3107
|
+
if (abortRef.current || pendingRef.current || planApprovalRef.current || pendingRewindRef.current)
|
|
3108
|
+
return;
|
|
3712
3109
|
const current = config.permissionMode;
|
|
3713
3110
|
const index = SESSION_MODE_CYCLE.indexOf(current);
|
|
3714
3111
|
const next = SESSION_MODE_CYCLE[(index + 1) % SESSION_MODE_CYCLE.length] ?? "ask";
|
|
@@ -3745,7 +3142,8 @@ function Repl({
|
|
|
3745
3142
|
registry: createDefaultRegistry(),
|
|
3746
3143
|
config,
|
|
3747
3144
|
cwd,
|
|
3748
|
-
sessionStore
|
|
3145
|
+
sessionStore: sessionStoreRef.current,
|
|
3146
|
+
contextMaxTokens: contextWindowTokens(config, name)
|
|
3749
3147
|
});
|
|
3750
3148
|
const prev = backendRef.current;
|
|
3751
3149
|
if (prev instanceof AgentLoop) {
|
|
@@ -3760,7 +3158,7 @@ function Repl({
|
|
|
3760
3158
|
return `Failed to switch model: ${error instanceof Error ? error.message : String(error)}`;
|
|
3761
3159
|
}
|
|
3762
3160
|
},
|
|
3763
|
-
[config, cwd,
|
|
3161
|
+
[config, cwd, attachConfirmHandler]
|
|
3764
3162
|
);
|
|
3765
3163
|
const resume = useCallback(
|
|
3766
3164
|
async (id) => {
|
|
@@ -3768,33 +3166,43 @@ function Repl({
|
|
|
3768
3166
|
if (!(current instanceof AgentLoop)) {
|
|
3769
3167
|
return "Current backend does not support resuming sessions.";
|
|
3770
3168
|
}
|
|
3771
|
-
const
|
|
3169
|
+
const resolvedId = await resolveSessionId(id);
|
|
3170
|
+
if (!resolvedId) {
|
|
3171
|
+
return `Session not found: ${id}`;
|
|
3172
|
+
}
|
|
3173
|
+
const resumed = await resumeSession(resolvedId);
|
|
3772
3174
|
if (!resumed) {
|
|
3773
3175
|
return `Session not found: ${id}`;
|
|
3774
3176
|
}
|
|
3775
3177
|
await current.loadMessages(resumed.messages);
|
|
3178
|
+
const store = await SessionStore.open(resolvedId);
|
|
3179
|
+
if (store) {
|
|
3180
|
+
hydrateSnapshots(await loadSessionSnapshots(store.dir));
|
|
3181
|
+
}
|
|
3776
3182
|
const display = buildDisplayMessages(resumed.messages);
|
|
3777
3183
|
nextIdRef.current = display.length;
|
|
3778
3184
|
applyMessages(display);
|
|
3779
3185
|
usageRef.current = resumed.meta.usage ? { ...resumed.meta.usage } : emptyUsage();
|
|
3780
3186
|
setUsageVersion((v) => v + 1);
|
|
3781
|
-
return `Resumed session ${
|
|
3187
|
+
return `Resumed session ${resolvedId} (${resumed.messages.length} messages).`;
|
|
3782
3188
|
},
|
|
3783
3189
|
[applyMessages]
|
|
3784
3190
|
);
|
|
3785
3191
|
const runStream = useCallback(
|
|
3786
|
-
async (input) => {
|
|
3192
|
+
async (input, extraImages = []) => {
|
|
3787
3193
|
const resolved = await resolveMentions(input, cwd);
|
|
3194
|
+
const images = [...resolved.images, ...extraImages];
|
|
3788
3195
|
pushMessage(
|
|
3789
3196
|
"user",
|
|
3790
3197
|
input,
|
|
3791
|
-
resolved.attached.length > 0 ? `attached: ${resolved.attached.join(", ")}` : void 0
|
|
3198
|
+
resolved.attached.length + extraImages.length > 0 ? `attached: ${[...resolved.attached, ...extraImages.map((img) => img.path)].join(", ")}` : void 0
|
|
3792
3199
|
);
|
|
3793
3200
|
for (const skip of resolved.skipped) {
|
|
3794
3201
|
pushMessage("system", `Skipped @${skip.path}: ${skip.reason}`);
|
|
3795
3202
|
}
|
|
3796
3203
|
const controller = new AbortController();
|
|
3797
3204
|
abortRef.current = controller;
|
|
3205
|
+
const turnStartedAt = Date.now();
|
|
3798
3206
|
streamedRef.current = "";
|
|
3799
3207
|
thinkingRef.current = true;
|
|
3800
3208
|
reasoningRef.current = "";
|
|
@@ -3831,9 +3239,13 @@ function Repl({
|
|
|
3831
3239
|
}
|
|
3832
3240
|
});
|
|
3833
3241
|
try {
|
|
3834
|
-
for await (const event of backendRef.current.stream(
|
|
3835
|
-
|
|
3836
|
-
|
|
3242
|
+
for await (const event of backendRef.current.stream(
|
|
3243
|
+
images.length > 0 ? { text: resolved.input, images } : resolved.input,
|
|
3244
|
+
controller.signal,
|
|
3245
|
+
{
|
|
3246
|
+
persistAs: extraImages.length > 0 ? `${input}${" [clipboard image]".repeat(extraImages.length)}` : input
|
|
3247
|
+
}
|
|
3248
|
+
)) {
|
|
3837
3249
|
if (event.type === "text-delta") {
|
|
3838
3250
|
if (thinkingRef.current) {
|
|
3839
3251
|
thinkingRef.current = false;
|
|
@@ -3883,7 +3295,7 @@ function Repl({
|
|
|
3883
3295
|
usage.completionTokens += event.usage.completionTokens;
|
|
3884
3296
|
usage.totalTokens += event.usage.totalTokens;
|
|
3885
3297
|
setUsageVersion((v) => v + 1);
|
|
3886
|
-
|
|
3298
|
+
sessionStoreRef.current?.addUsage(event.usage).catch(() => {
|
|
3887
3299
|
});
|
|
3888
3300
|
}
|
|
3889
3301
|
} else if (event.type === "error") {
|
|
@@ -3913,6 +3325,13 @@ function Repl({
|
|
|
3913
3325
|
streamedRef.current = "";
|
|
3914
3326
|
setStreamingText(null);
|
|
3915
3327
|
const interrupted = controller.signal.aborted;
|
|
3328
|
+
notifyBell({
|
|
3329
|
+
enabled: config.notifyBell,
|
|
3330
|
+
thresholdSec: config.notifyBellThresholdSec,
|
|
3331
|
+
noNotifyEnv: process.env.STAR_NO_NOTIFY === "1",
|
|
3332
|
+
interrupted,
|
|
3333
|
+
elapsedMs: Date.now() - turnStartedAt
|
|
3334
|
+
});
|
|
3916
3335
|
if (finalText.length > 0) {
|
|
3917
3336
|
pushAssistantChunk(`${finalText}${interrupted ? " [interrupted]" : ""}`, false);
|
|
3918
3337
|
} else if (interrupted && turnChunksRef.current > 0) {
|
|
@@ -3931,6 +3350,37 @@ function Repl({
|
|
|
3931
3350
|
if (!interrupted && config.permissionMode === "plan") {
|
|
3932
3351
|
setPlanApproval(true);
|
|
3933
3352
|
}
|
|
3353
|
+
const cost = sessionCostUsd();
|
|
3354
|
+
const budget = config.sessionBudgetUsd;
|
|
3355
|
+
const status = budgetState(cost, budget);
|
|
3356
|
+
if (status === "warn" && !budgetWarnedRef.current) {
|
|
3357
|
+
budgetWarnedRef.current = true;
|
|
3358
|
+
pushMessage(
|
|
3359
|
+
"system",
|
|
3360
|
+
`Session cost $${formatDollars(cost ?? 0)} has passed 80% of the $${formatDollars(budget ?? 0)} session budget (sessionBudgetUsd).`
|
|
3361
|
+
);
|
|
3362
|
+
}
|
|
3363
|
+
if (status === "exceeded" && !budgetExceededRef.current) {
|
|
3364
|
+
budgetExceededRef.current = true;
|
|
3365
|
+
pushMessage(
|
|
3366
|
+
"system",
|
|
3367
|
+
`Session budget exceeded: $${formatDollars(cost ?? 0)} of $${formatDollars(budget ?? 0)} (sessionBudgetUsd). New prompts are blocked \u2014 raise sessionBudgetUsd in the config or start a /new session.`
|
|
3368
|
+
);
|
|
3369
|
+
}
|
|
3370
|
+
const nextPrompt = queueRef.current.dequeue();
|
|
3371
|
+
if (nextPrompt) {
|
|
3372
|
+
if (status === "exceeded") {
|
|
3373
|
+
const dropped = queueRef.current.clear().length + 1;
|
|
3374
|
+
setQueueItems([]);
|
|
3375
|
+
pushMessage(
|
|
3376
|
+
"system",
|
|
3377
|
+
`Dropped ${dropped} queued message(s) \u2014 session budget exceeded.`
|
|
3378
|
+
);
|
|
3379
|
+
} else {
|
|
3380
|
+
setQueueItems(queueRef.current.list());
|
|
3381
|
+
void runStreamRef.current?.(nextPrompt.text, nextPrompt.images);
|
|
3382
|
+
}
|
|
3383
|
+
}
|
|
3934
3384
|
}
|
|
3935
3385
|
},
|
|
3936
3386
|
[
|
|
@@ -3938,11 +3388,14 @@ function Repl({
|
|
|
3938
3388
|
pushAssistantChunk,
|
|
3939
3389
|
setPendingPermission,
|
|
3940
3390
|
setPlanApproval,
|
|
3941
|
-
sessionStore,
|
|
3942
3391
|
cwd,
|
|
3943
|
-
config
|
|
3392
|
+
config,
|
|
3393
|
+
sessionCostUsd
|
|
3944
3394
|
]
|
|
3945
3395
|
);
|
|
3396
|
+
useEffect2(() => {
|
|
3397
|
+
runStreamRef.current = runStream;
|
|
3398
|
+
}, [runStream]);
|
|
3946
3399
|
const handlePlanDecision = useCallback(
|
|
3947
3400
|
(decision) => {
|
|
3948
3401
|
setPlanApproval(false);
|
|
@@ -3956,10 +3409,39 @@ function Repl({
|
|
|
3956
3409
|
);
|
|
3957
3410
|
const registry = useMemo(() => {
|
|
3958
3411
|
const ctx = {
|
|
3412
|
+
cwd,
|
|
3959
3413
|
addSystemMessage: (text) => pushMessage("system", text),
|
|
3414
|
+
showDiff: (text, lines, note) => {
|
|
3415
|
+
setMessages((prev) => {
|
|
3416
|
+
const next = [
|
|
3417
|
+
...prev,
|
|
3418
|
+
{ id: nextIdRef.current++, role: "system", text, note, diff: lines }
|
|
3419
|
+
];
|
|
3420
|
+
messagesRef.current = next;
|
|
3421
|
+
return next;
|
|
3422
|
+
});
|
|
3423
|
+
},
|
|
3960
3424
|
clearMessages: () => {
|
|
3961
3425
|
redrawMessages([]);
|
|
3962
3426
|
},
|
|
3427
|
+
conversationText: (scope) => {
|
|
3428
|
+
const current = backendRef.current;
|
|
3429
|
+
if (!(current instanceof AgentLoop)) return null;
|
|
3430
|
+
const messages2 = current.getMessages();
|
|
3431
|
+
if (scope === "all") {
|
|
3432
|
+
const parts = messages2.filter((m) => m.role === "user" || m.role === "assistant").map((m) => coreMessageText(m)).filter((text) => text.length > 0);
|
|
3433
|
+
return parts.length > 0 ? parts.join("\n\n") : null;
|
|
3434
|
+
}
|
|
3435
|
+
for (let i = messages2.length - 1; i >= 0; i--) {
|
|
3436
|
+
const message = messages2[i];
|
|
3437
|
+
if (message?.role === "assistant") {
|
|
3438
|
+
const text = coreMessageText(message);
|
|
3439
|
+
if (text) return text;
|
|
3440
|
+
}
|
|
3441
|
+
}
|
|
3442
|
+
return null;
|
|
3443
|
+
},
|
|
3444
|
+
copyToClipboard: (text) => copyText(text),
|
|
3963
3445
|
exit: () => handleExit(),
|
|
3964
3446
|
listModels: () => {
|
|
3965
3447
|
const models = listModels(config);
|
|
@@ -3971,11 +3453,42 @@ function Repl({
|
|
|
3971
3453
|
${lines.join("\n")}`;
|
|
3972
3454
|
},
|
|
3973
3455
|
switchModel,
|
|
3974
|
-
listSessions: async () => {
|
|
3975
|
-
const
|
|
3976
|
-
|
|
3456
|
+
listSessions: async (all) => {
|
|
3457
|
+
const entries = await listSessionEntries(all ? void 0 : cwd);
|
|
3458
|
+
if (entries.length === 0) {
|
|
3459
|
+
return all ? "No sessions found." : "No sessions found for this directory.";
|
|
3460
|
+
}
|
|
3461
|
+
return formatSessionEntries(entries, { showCwd: all });
|
|
3977
3462
|
},
|
|
3978
3463
|
resumeSession: resume,
|
|
3464
|
+
newSession: async () => {
|
|
3465
|
+
const current = backendRef.current;
|
|
3466
|
+
if (!(current instanceof AgentLoop)) {
|
|
3467
|
+
return "Current backend does not support starting a new session.";
|
|
3468
|
+
}
|
|
3469
|
+
const store = await SessionStore.create(cwd, modelNameRef.current);
|
|
3470
|
+
current.setSessionStore(store);
|
|
3471
|
+
sessionStoreRef.current = store;
|
|
3472
|
+
const [first] = current.getMessages();
|
|
3473
|
+
await current.loadMessages(first?.role === "system" ? [first] : []);
|
|
3474
|
+
clearSnapshots();
|
|
3475
|
+
redrawMessages([]);
|
|
3476
|
+
usageRef.current = emptyUsage();
|
|
3477
|
+
budgetWarnedRef.current = false;
|
|
3478
|
+
budgetExceededRef.current = false;
|
|
3479
|
+
setUsageVersion((v) => v + 1);
|
|
3480
|
+
return `Started a new session (${store.id}) with a clean context.`;
|
|
3481
|
+
},
|
|
3482
|
+
clearSessions: async (all) => {
|
|
3483
|
+
const currentId = sessionStoreRef.current?.id;
|
|
3484
|
+
const removed = await clearSessions(all ? void 0 : cwd, currentId);
|
|
3485
|
+
if (removed === 0) {
|
|
3486
|
+
return all ? "No stored sessions to delete." : "No stored sessions for this directory.";
|
|
3487
|
+
}
|
|
3488
|
+
const scope = all ? "across all directories" : "for this directory";
|
|
3489
|
+
const kept = currentId ? " The current session is still active." : "";
|
|
3490
|
+
return `Deleted ${removed} session(s) ${scope}.${kept}`;
|
|
3491
|
+
},
|
|
3979
3492
|
showTodos: async () => {
|
|
3980
3493
|
const store = new TodoStore();
|
|
3981
3494
|
await store.load(cwd);
|
|
@@ -3987,6 +3500,7 @@ ${lines.join("\n")}`;
|
|
|
3987
3500
|
return `${formatUsage(usageRef.current)}
|
|
3988
3501
|
${estimateCost(usageRef.current, modelNameRef.current, modelConfig)}`;
|
|
3989
3502
|
},
|
|
3503
|
+
showGlobalUsage: async () => formatUsageDashboard(await collectUsageStats(), config.models),
|
|
3990
3504
|
compactContext: async () => {
|
|
3991
3505
|
let summaryModel = null;
|
|
3992
3506
|
try {
|
|
@@ -3996,7 +3510,7 @@ ${estimateCost(usageRef.current, modelNameRef.current, modelConfig)}`;
|
|
|
3996
3510
|
}
|
|
3997
3511
|
const result = await compactSession({
|
|
3998
3512
|
backend: backendRef.current,
|
|
3999
|
-
sessionStore,
|
|
3513
|
+
sessionStore: sessionStoreRef.current,
|
|
4000
3514
|
config,
|
|
4001
3515
|
model: summaryModel
|
|
4002
3516
|
});
|
|
@@ -4007,7 +3521,12 @@ ${estimateCost(usageRef.current, modelNameRef.current, modelConfig)}`;
|
|
|
4007
3521
|
}
|
|
4008
3522
|
return result.message;
|
|
4009
3523
|
},
|
|
4010
|
-
exportSession: (arg) => exportSession({
|
|
3524
|
+
exportSession: (arg) => exportSession({
|
|
3525
|
+
backend: backendRef.current,
|
|
3526
|
+
sessionStore: sessionStoreRef.current,
|
|
3527
|
+
cwd,
|
|
3528
|
+
arg
|
|
3529
|
+
}),
|
|
4011
3530
|
undo: async () => {
|
|
4012
3531
|
const current = backendRef.current;
|
|
4013
3532
|
if (!(current instanceof AgentLoop)) {
|
|
@@ -4031,6 +3550,48 @@ ${estimateCost(usageRef.current, modelNameRef.current, modelConfig)}`;
|
|
|
4031
3550
|
"\n"
|
|
4032
3551
|
);
|
|
4033
3552
|
},
|
|
3553
|
+
rewind: async (args) => {
|
|
3554
|
+
if (!args) {
|
|
3555
|
+
return formatCheckpointList(listSnapshots(), cwd);
|
|
3556
|
+
}
|
|
3557
|
+
const id = Number.parseInt(args, 10);
|
|
3558
|
+
if (!Number.isFinite(id) || String(id) !== args) {
|
|
3559
|
+
return `Usage: /rewind <n> \u2014 "${args}" is not a checkpoint number.`;
|
|
3560
|
+
}
|
|
3561
|
+
const plan = planRewind(listSnapshots(), id);
|
|
3562
|
+
if (!plan) {
|
|
3563
|
+
return `Checkpoint #${id} not found (it may already have been rewound). /rewind lists the current checkpoints.`;
|
|
3564
|
+
}
|
|
3565
|
+
const current = backendRef.current;
|
|
3566
|
+
const messageCount = plan.messageIndex !== null && current instanceof AgentLoop ? current.countRetraction(plan.messageIndex) : 0;
|
|
3567
|
+
const confirmed = await new Promise((resolve) => {
|
|
3568
|
+
setPendingRewind({
|
|
3569
|
+
summary: `Rewind to just before checkpoint #${id} (${plan.target.toolName} ${plan.target.path}):
|
|
3570
|
+
${plan.affected.length} file change(s) will be reverted, ${messageCount} message(s) retracted.`,
|
|
3571
|
+
resolve
|
|
3572
|
+
});
|
|
3573
|
+
});
|
|
3574
|
+
if (!confirmed) {
|
|
3575
|
+
return "Rewind cancelled.";
|
|
3576
|
+
}
|
|
3577
|
+
const result = await rewindToSnapshot(id);
|
|
3578
|
+
if (!result) {
|
|
3579
|
+
return `Checkpoint #${id} is no longer available.`;
|
|
3580
|
+
}
|
|
3581
|
+
let retracted = 0;
|
|
3582
|
+
if (result.messageIndex !== null && current instanceof AgentLoop) {
|
|
3583
|
+
retracted = await current.retractFromIndex(result.messageIndex);
|
|
3584
|
+
if (retracted > 0) {
|
|
3585
|
+
const display = buildDisplayMessages([...current.getMessages()]);
|
|
3586
|
+
nextIdRef.current = display.length;
|
|
3587
|
+
redrawMessages(display);
|
|
3588
|
+
}
|
|
3589
|
+
}
|
|
3590
|
+
return [
|
|
3591
|
+
...result.reverted,
|
|
3592
|
+
`Rewound to before checkpoint #${id}: ${result.reverted.length} file change(s) reverted, ${retracted} message(s) retracted.`
|
|
3593
|
+
].join("\n");
|
|
3594
|
+
},
|
|
4034
3595
|
permissionMode: async (args) => {
|
|
4035
3596
|
const current = config.permissionMode;
|
|
4036
3597
|
if (!args) {
|
|
@@ -4084,7 +3645,8 @@ ${estimateCost(usageRef.current, modelNameRef.current, modelConfig)}`;
|
|
|
4084
3645
|
`maxSteps: ${config.maxSteps}`,
|
|
4085
3646
|
`contextMaxTokens: ${config.contextMaxTokens}`,
|
|
4086
3647
|
`streamIdleTimeoutSec: ${config.streamIdleTimeoutSec}`,
|
|
4087
|
-
`permissions.allow (${config.permissions.allow.length}): ${config.permissions.allow.join(", ") || "(none)"}
|
|
3648
|
+
`permissions.allow (${config.permissions.allow.length}): ${config.permissions.allow.join(", ") || "(none)"}`,
|
|
3649
|
+
`hooks (${config.hooks.length}): ${config.hooks.map((h) => h.event).join(", ") || "(none)"}`
|
|
4088
3650
|
].join("\n")
|
|
4089
3651
|
};
|
|
4090
3652
|
const reg = new CommandRegistry();
|
|
@@ -4098,11 +3660,11 @@ ${estimateCost(usageRef.current, modelNameRef.current, modelConfig)}`;
|
|
|
4098
3660
|
cwd,
|
|
4099
3661
|
switchModel,
|
|
4100
3662
|
resume,
|
|
4101
|
-
sessionStore,
|
|
4102
3663
|
runStream,
|
|
4103
3664
|
applyMessages,
|
|
4104
3665
|
redrawMessages,
|
|
4105
|
-
applySessionMode
|
|
3666
|
+
applySessionMode,
|
|
3667
|
+
setPendingRewind
|
|
4106
3668
|
]);
|
|
4107
3669
|
const runShellBang = useCallback(
|
|
4108
3670
|
async (raw) => {
|
|
@@ -4143,6 +3705,11 @@ ${estimateCost(usageRef.current, modelNameRef.current, modelConfig)}`;
|
|
|
4143
3705
|
const handleSubmit = useCallback(
|
|
4144
3706
|
(text) => {
|
|
4145
3707
|
if (text.startsWith("!")) {
|
|
3708
|
+
if (abortRef.current) {
|
|
3709
|
+
queueRef.current.enqueue({ text, images: [] });
|
|
3710
|
+
setQueueItems(queueRef.current.list());
|
|
3711
|
+
return;
|
|
3712
|
+
}
|
|
4146
3713
|
void runShellBang(text.slice(1));
|
|
4147
3714
|
return;
|
|
4148
3715
|
}
|
|
@@ -4157,17 +3724,44 @@ ${estimateCost(usageRef.current, modelNameRef.current, modelConfig)}`;
|
|
|
4157
3724
|
void command.run(parsed.args, registry.ctx);
|
|
4158
3725
|
return;
|
|
4159
3726
|
}
|
|
4160
|
-
|
|
3727
|
+
const budget = config.sessionBudgetUsd;
|
|
3728
|
+
if (budgetState(sessionCostUsd(), budget) === "exceeded") {
|
|
3729
|
+
pushMessage(
|
|
3730
|
+
"system",
|
|
3731
|
+
`Session budget of $${formatDollars(budget ?? 0)} (sessionBudgetUsd) is exceeded \u2014 prompts are blocked. Slash commands still work; raise sessionBudgetUsd in the config or start a /new session.`
|
|
3732
|
+
);
|
|
3733
|
+
return;
|
|
3734
|
+
}
|
|
3735
|
+
const images = pendingImagesRef.current.map((staged) => staged.image);
|
|
3736
|
+
if (images.length > 0) clearPendingImages();
|
|
3737
|
+
if (abortRef.current) {
|
|
3738
|
+
queueRef.current.enqueue({ text, images });
|
|
3739
|
+
setQueueItems(queueRef.current.list());
|
|
3740
|
+
return;
|
|
3741
|
+
}
|
|
3742
|
+
void runStream(text, images);
|
|
4161
3743
|
},
|
|
4162
|
-
[registry, pushMessage, runStream, runShellBang]
|
|
3744
|
+
[registry, pushMessage, runStream, runShellBang, config, clearPendingImages, sessionCostUsd]
|
|
4163
3745
|
);
|
|
4164
3746
|
const commandHints = useMemo(
|
|
4165
3747
|
() => registry.list().map((cmd) => ({ name: cmd.name, description: cmd.description })),
|
|
4166
3748
|
[registry]
|
|
4167
3749
|
);
|
|
4168
|
-
|
|
4169
|
-
|
|
4170
|
-
|
|
3750
|
+
useEffect2(() => {
|
|
3751
|
+
setGitBranch(getGitSummary(cwd)?.branch ?? null);
|
|
3752
|
+
const current = backendRef.current;
|
|
3753
|
+
if (current instanceof AgentLoop) {
|
|
3754
|
+
const window_ = contextWindowTokens(config, modelNameRef.current);
|
|
3755
|
+
const pct = estimateTokens([...current.getMessages()]) / window_ * 100;
|
|
3756
|
+
setContextPercent(pct < 10 ? Math.round(pct * 10) / 10 : Math.round(pct));
|
|
3757
|
+
} else {
|
|
3758
|
+
setContextPercent(null);
|
|
3759
|
+
}
|
|
3760
|
+
}, [cwd, config, isStreaming, epoch]);
|
|
3761
|
+
const sessionCost = sessionCostUsd();
|
|
3762
|
+
return /* @__PURE__ */ jsxs10(Box9, { flexDirection: "column", children: [
|
|
3763
|
+
/* @__PURE__ */ jsx8(MessageList, { messages }, `messages-${epoch}`),
|
|
3764
|
+
(thinking || activity !== null) && /* @__PURE__ */ jsx8(
|
|
4171
3765
|
ThinkingIndicator,
|
|
4172
3766
|
{
|
|
4173
3767
|
reasoning: thinkingText,
|
|
@@ -4175,9 +3769,9 @@ ${estimateCost(usageRef.current, modelNameRef.current, modelConfig)}`;
|
|
|
4175
3769
|
activity: activity ?? void 0
|
|
4176
3770
|
}
|
|
4177
3771
|
),
|
|
4178
|
-
!thinking && activity === null && thoughtSummary !== null && /* @__PURE__ */
|
|
4179
|
-
streamingText !== null && (streamingText !== "" || turnChunksRef.current === 0) && /* @__PURE__ */
|
|
4180
|
-
pending && /* @__PURE__ */
|
|
3772
|
+
!thinking && activity === null && thoughtSummary !== null && /* @__PURE__ */ jsx8(Text10, { dimColor: true, children: thoughtSummary }),
|
|
3773
|
+
streamingText !== null && (streamingText !== "" || turnChunksRef.current === 0) && /* @__PURE__ */ jsx8(StreamingMessage, { text: streamingText, continuation: turnChunksRef.current > 0 }),
|
|
3774
|
+
pending && /* @__PURE__ */ jsx8(
|
|
4181
3775
|
PermissionPrompt,
|
|
4182
3776
|
{
|
|
4183
3777
|
request: pending.request,
|
|
@@ -4185,24 +3779,41 @@ ${estimateCost(usageRef.current, modelNameRef.current, modelConfig)}`;
|
|
|
4185
3779
|
onDecision: handleDecision
|
|
4186
3780
|
}
|
|
4187
3781
|
),
|
|
4188
|
-
planApproval && /* @__PURE__ */
|
|
4189
|
-
/* @__PURE__ */
|
|
3782
|
+
planApproval && /* @__PURE__ */ jsx8(ExitPlanPrompt, { onDecision: handlePlanDecision }),
|
|
3783
|
+
pendingRewind && /* @__PURE__ */ jsx8(RewindConfirmPrompt, { summary: pendingRewind.summary, onDecision: handleRewindDecision }),
|
|
3784
|
+
queueItems.map((item) => /* @__PURE__ */ jsxs10(Text10, { dimColor: true, children: [
|
|
3785
|
+
"queued: ",
|
|
3786
|
+
item.text.length > 80 ? `${item.text.slice(0, 80)}\u2026` : item.text
|
|
3787
|
+
] }, item.id)),
|
|
3788
|
+
pendingImages.map((staged) => /* @__PURE__ */ jsxs10(Text10, { dimColor: true, children: [
|
|
3789
|
+
"[image attached: ",
|
|
3790
|
+
staged.image.path,
|
|
3791
|
+
"]"
|
|
3792
|
+
] }, staged.id)),
|
|
3793
|
+
/* @__PURE__ */ jsx8(
|
|
4190
3794
|
InputBox,
|
|
4191
3795
|
{
|
|
4192
3796
|
isStreaming,
|
|
4193
|
-
disabled: pending !== null || planApproval,
|
|
3797
|
+
disabled: pending !== null || planApproval || pendingRewind !== null,
|
|
4194
3798
|
commands: commandHints,
|
|
3799
|
+
cwd,
|
|
3800
|
+
refill: inputRefill,
|
|
4195
3801
|
onSubmit: handleSubmit,
|
|
4196
3802
|
onInterrupt: interrupt,
|
|
4197
|
-
onExit: handleExit
|
|
3803
|
+
onExit: handleExit,
|
|
3804
|
+
onPasteImage: handlePasteImage
|
|
4198
3805
|
}
|
|
4199
3806
|
),
|
|
4200
|
-
/* @__PURE__ */
|
|
3807
|
+
/* @__PURE__ */ jsx8(
|
|
4201
3808
|
StatusBar,
|
|
4202
3809
|
{
|
|
3810
|
+
cwd,
|
|
4203
3811
|
model: modelName,
|
|
4204
3812
|
permissionMode: permissionModeState,
|
|
4205
3813
|
tokens: usageRef.current.totalTokens,
|
|
3814
|
+
gitBranch,
|
|
3815
|
+
contextPercent,
|
|
3816
|
+
sessionCostUsd: sessionCost,
|
|
4206
3817
|
backgroundTasks: bgLabels
|
|
4207
3818
|
}
|
|
4208
3819
|
)
|
|
@@ -4213,7 +3824,7 @@ function renderRepl(backend, opts) {
|
|
|
4213
3824
|
defaultTaskManager.cleanup();
|
|
4214
3825
|
});
|
|
4215
3826
|
return render(
|
|
4216
|
-
/* @__PURE__ */
|
|
3827
|
+
/* @__PURE__ */ jsx8(
|
|
4217
3828
|
Repl,
|
|
4218
3829
|
{
|
|
4219
3830
|
backend,
|
|
@@ -4230,46 +3841,8 @@ function renderRepl(backend, opts) {
|
|
|
4230
3841
|
}
|
|
4231
3842
|
|
|
4232
3843
|
// src/config/loader.ts
|
|
4233
|
-
import
|
|
3844
|
+
import fs10 from "fs";
|
|
4234
3845
|
import { parse as parse2 } from "smol-toml";
|
|
4235
|
-
|
|
4236
|
-
// src/config/schema.ts
|
|
4237
|
-
import { z as z11 } from "zod";
|
|
4238
|
-
var ProviderConfigSchema = z11.object({
|
|
4239
|
-
name: z11.string(),
|
|
4240
|
-
protocol: z11.enum(["openai-compatible", "anthropic", "openai-responses"]).default("openai-compatible"),
|
|
4241
|
-
baseURL: z11.string(),
|
|
4242
|
-
apiKeyEnv: z11.string().optional(),
|
|
4243
|
-
apiKey: z11.string().optional(),
|
|
4244
|
-
headers: z11.record(z11.string()).optional()
|
|
4245
|
-
});
|
|
4246
|
-
var ModelConfigSchema = z11.object({
|
|
4247
|
-
name: z11.string(),
|
|
4248
|
-
provider: z11.string(),
|
|
4249
|
-
model: z11.string(),
|
|
4250
|
-
maxTokens: z11.number().int().positive().optional(),
|
|
4251
|
-
promptPrice: z11.number().nonnegative().optional(),
|
|
4252
|
-
completionPrice: z11.number().nonnegative().optional()
|
|
4253
|
-
});
|
|
4254
|
-
var PermissionsConfigSchema = z11.object({
|
|
4255
|
-
allow: z11.array(z11.string()).default([])
|
|
4256
|
-
});
|
|
4257
|
-
var ConfigSchema = z11.object({
|
|
4258
|
-
defaultModel: z11.string().default(""),
|
|
4259
|
-
permissionMode: z11.enum(["auto", "ask", "readonly", "yolo", "plan"]).default("ask"),
|
|
4260
|
-
providers: z11.array(ProviderConfigSchema).default([]),
|
|
4261
|
-
models: z11.array(ModelConfigSchema).default([]),
|
|
4262
|
-
maxSteps: z11.number().int().positive().default(50),
|
|
4263
|
-
contextMaxTokens: z11.number().int().positive().default(1e5),
|
|
4264
|
-
// Seconds without any stream part before a stalled response is ended
|
|
4265
|
-
// gracefully (some relays never send the terminal chunks). Applies once
|
|
4266
|
-
// streaming has started; the first part gets a longer, fixed allowance.
|
|
4267
|
-
streamIdleTimeoutSec: z11.number().positive().default(20),
|
|
4268
|
-
contextCompaction: z11.enum(["summary", "truncate"]).default("summary"),
|
|
4269
|
-
permissions: PermissionsConfigSchema.default({ allow: [] })
|
|
4270
|
-
});
|
|
4271
|
-
|
|
4272
|
-
// src/config/loader.ts
|
|
4273
3846
|
var PartialConfigSchema = ConfigSchema.partial();
|
|
4274
3847
|
function parseTomlFile(content, filePath) {
|
|
4275
3848
|
let raw;
|
|
@@ -4296,8 +3869,8 @@ function applyOverrides(config, overrides) {
|
|
|
4296
3869
|
return merged;
|
|
4297
3870
|
}
|
|
4298
3871
|
function readTomlFileSync(filePath) {
|
|
4299
|
-
if (!
|
|
4300
|
-
const content =
|
|
3872
|
+
if (!fs10.existsSync(filePath)) return {};
|
|
3873
|
+
const content = fs10.readFileSync(filePath, "utf8");
|
|
4301
3874
|
return parseTomlFile(content, filePath);
|
|
4302
3875
|
}
|
|
4303
3876
|
function loadConfigSync(cwd, overrides) {
|
|
@@ -4321,12 +3894,36 @@ async function createLoop(config, modelName, cwd, sessionStore) {
|
|
|
4321
3894
|
config,
|
|
4322
3895
|
cwd,
|
|
4323
3896
|
system: SYSTEM_PROMPT,
|
|
4324
|
-
sessionStore
|
|
3897
|
+
sessionStore,
|
|
3898
|
+
contextMaxTokens: contextWindowTokens(config, modelName)
|
|
4325
3899
|
});
|
|
4326
3900
|
}
|
|
4327
|
-
async function printMode(loop, prompt, cwd, json) {
|
|
3901
|
+
async function printMode(loop, prompt, cwd, json, imagePaths) {
|
|
3902
|
+
const flagImages = [];
|
|
3903
|
+
for (const imagePath of imagePaths) {
|
|
3904
|
+
if (!imageMimeType(imagePath)) {
|
|
3905
|
+
process.stderr.write(
|
|
3906
|
+
`[error] unsupported image type: ${imagePath} (supported: png, jpg, jpeg, gif, webp)
|
|
3907
|
+
`
|
|
3908
|
+
);
|
|
3909
|
+
return 1;
|
|
3910
|
+
}
|
|
3911
|
+
const image = await readImageInput(imagePath, cwd);
|
|
3912
|
+
if (!image) {
|
|
3913
|
+
process.stderr.write(
|
|
3914
|
+
`[error] cannot read image: ${imagePath} (missing, unreadable, or larger than ${MAX_IMAGE_BYTES / (1024 * 1024)}MB)
|
|
3915
|
+
`
|
|
3916
|
+
);
|
|
3917
|
+
return 1;
|
|
3918
|
+
}
|
|
3919
|
+
flagImages.push(image);
|
|
3920
|
+
}
|
|
4328
3921
|
const controller = new AbortController();
|
|
4329
3922
|
process.on("SIGINT", () => controller.abort());
|
|
3923
|
+
loop.onHookWarning = (message) => {
|
|
3924
|
+
process.stderr.write(`[hook] ${message}
|
|
3925
|
+
`);
|
|
3926
|
+
};
|
|
4330
3927
|
const resolved = await resolveMentions(prompt, cwd);
|
|
4331
3928
|
if (resolved.attached.length > 0) {
|
|
4332
3929
|
process.stderr.write(`[attached] ${resolved.attached.join(", ")}
|
|
@@ -4336,10 +3933,12 @@ async function printMode(loop, prompt, cwd, json) {
|
|
|
4336
3933
|
process.stderr.write(`[skipped] @${skip.path}: ${skip.reason}
|
|
4337
3934
|
`);
|
|
4338
3935
|
}
|
|
3936
|
+
const images = [...flagImages, ...resolved.images];
|
|
3937
|
+
const input = images.length > 0 ? { text: resolved.input, images } : resolved.input;
|
|
4339
3938
|
const usage = new UsageTracker();
|
|
4340
3939
|
let exitCode = 0;
|
|
4341
3940
|
try {
|
|
4342
|
-
for await (const event of loop.stream(
|
|
3941
|
+
for await (const event of loop.stream(input, controller.signal, {
|
|
4343
3942
|
persistAs: prompt
|
|
4344
3943
|
})) {
|
|
4345
3944
|
if (json) {
|
|
@@ -4405,7 +4004,15 @@ async function printMode(loop, prompt, cwd, json) {
|
|
|
4405
4004
|
return exitCode;
|
|
4406
4005
|
}
|
|
4407
4006
|
var program = new Command();
|
|
4408
|
-
program.name("star").description("Star CLI \u2014 an AI agent command-line interface").version(VERSION).option("-m, --model <model>", "model to use").option("--permission-mode <mode>", "permission mode: ask | auto | readonly | yolo | plan").option("-p, --print <prompt>", "non-interactive print mode").option("--json", "output NDJSON events on stdout (print mode only)").option(
|
|
4007
|
+
program.name("star").description("Star CLI \u2014 an AI agent command-line interface").version(VERSION).option("-m, --model <model>", "model to use").option("--permission-mode <mode>", "permission mode: ask | auto | readonly | yolo | plan").option("-p, --print <prompt>", "non-interactive print mode").option("--json", "output NDJSON events on stdout (print mode only)").option(
|
|
4008
|
+
"--image <path>",
|
|
4009
|
+
"attach an image file (png/jpg/jpeg/gif/webp, max 5MB); repeatable",
|
|
4010
|
+
(value, previous) => [...previous, value],
|
|
4011
|
+
[]
|
|
4012
|
+
).option("-r, --resume [sessionId]", "resume a previous session (lists sessions when no id given)").option("-c, --continue", "continue the most recent session for the current directory").option(
|
|
4013
|
+
"--clear-sessions [scope]",
|
|
4014
|
+
"delete stored sessions for the current directory ('all' deletes every session) and exit"
|
|
4015
|
+
).action(async (opts) => {
|
|
4409
4016
|
const cwd = process.cwd();
|
|
4410
4017
|
if (opts.json && opts.print === void 0) {
|
|
4411
4018
|
console.error(
|
|
@@ -4413,6 +4020,32 @@ program.name("star").description("Star CLI \u2014 an AI agent command-line inter
|
|
|
4413
4020
|
);
|
|
4414
4021
|
process.exit(1);
|
|
4415
4022
|
}
|
|
4023
|
+
if (opts.resume !== void 0 && opts.continue) {
|
|
4024
|
+
console.error("Options --resume and --continue are mutually exclusive: pick one.");
|
|
4025
|
+
process.exit(1);
|
|
4026
|
+
}
|
|
4027
|
+
if (opts.resume === true) {
|
|
4028
|
+
const entries = await listSessionEntries(cwd);
|
|
4029
|
+
if (entries.length === 0) {
|
|
4030
|
+
console.error("No sessions found for this directory.");
|
|
4031
|
+
} else {
|
|
4032
|
+
console.log(formatSessionEntries(entries));
|
|
4033
|
+
}
|
|
4034
|
+
process.exit(0);
|
|
4035
|
+
}
|
|
4036
|
+
if (opts.clearSessions !== void 0) {
|
|
4037
|
+
const scope = opts.clearSessions;
|
|
4038
|
+
if (scope !== true && scope !== "all") {
|
|
4039
|
+
console.error(`Invalid scope: ${scope}. Use --clear-sessions or --clear-sessions all.`);
|
|
4040
|
+
process.exit(1);
|
|
4041
|
+
}
|
|
4042
|
+
const all = scope === "all";
|
|
4043
|
+
const removed = await clearSessions(all ? void 0 : cwd);
|
|
4044
|
+
console.log(
|
|
4045
|
+
all ? `Deleted ${removed} session(s) across all directories.` : `Deleted ${removed} session(s) for this directory.`
|
|
4046
|
+
);
|
|
4047
|
+
process.exit(0);
|
|
4048
|
+
}
|
|
4416
4049
|
let config;
|
|
4417
4050
|
try {
|
|
4418
4051
|
config = loadConfigSync(cwd, { model: opts.model, permissionMode: opts.permissionMode });
|
|
@@ -4429,13 +4062,28 @@ program.name("star").description("Star CLI \u2014 an AI agent command-line inter
|
|
|
4429
4062
|
}
|
|
4430
4063
|
let sessionStore = null;
|
|
4431
4064
|
let resumed = null;
|
|
4432
|
-
|
|
4433
|
-
|
|
4434
|
-
|
|
4065
|
+
let resumeId = null;
|
|
4066
|
+
if (typeof opts.resume === "string") {
|
|
4067
|
+
resumeId = await resolveSessionId(opts.resume);
|
|
4068
|
+
if (!resumeId) {
|
|
4435
4069
|
console.error(`Session not found: ${opts.resume}`);
|
|
4436
4070
|
process.exit(1);
|
|
4437
4071
|
}
|
|
4438
|
-
|
|
4072
|
+
} else if (opts.continue) {
|
|
4073
|
+
const latest = await findLatestSession(cwd);
|
|
4074
|
+
if (latest) {
|
|
4075
|
+
resumeId = latest.id;
|
|
4076
|
+
} else {
|
|
4077
|
+
process.stderr.write("No previous session for this directory \u2014 starting a new session.\n");
|
|
4078
|
+
}
|
|
4079
|
+
}
|
|
4080
|
+
if (resumeId) {
|
|
4081
|
+
resumed = await resumeSession(resumeId);
|
|
4082
|
+
if (!resumed) {
|
|
4083
|
+
console.error(`Session not found: ${resumeId}`);
|
|
4084
|
+
process.exit(1);
|
|
4085
|
+
}
|
|
4086
|
+
sessionStore = await SessionStore.open(resumeId);
|
|
4439
4087
|
} else if (!opts.print) {
|
|
4440
4088
|
sessionStore = await SessionStore.create(cwd, modelName);
|
|
4441
4089
|
}
|
|
@@ -4448,9 +4096,18 @@ program.name("star").description("Star CLI \u2014 an AI agent command-line inter
|
|
|
4448
4096
|
}
|
|
4449
4097
|
if (resumed) {
|
|
4450
4098
|
await loop.loadMessages(resumed.messages);
|
|
4099
|
+
if (sessionStore) {
|
|
4100
|
+
hydrateSnapshots(await loadSessionSnapshots(sessionStore.dir));
|
|
4101
|
+
}
|
|
4451
4102
|
}
|
|
4452
4103
|
if (opts.print) {
|
|
4453
|
-
process.exitCode = await printMode(
|
|
4104
|
+
process.exitCode = await printMode(
|
|
4105
|
+
loop,
|
|
4106
|
+
opts.print,
|
|
4107
|
+
cwd,
|
|
4108
|
+
Boolean(opts.json),
|
|
4109
|
+
opts.image ?? []
|
|
4110
|
+
);
|
|
4454
4111
|
return;
|
|
4455
4112
|
}
|
|
4456
4113
|
renderRepl(loop, {
|