@cryer/star-cli 0.1.1 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +52 -7
- package/dist/main.js +2080 -441
- package/dist/main.js.map +1 -1
- package/dist/stream-Y55NRF2S.js +82 -0
- package/dist/stream-Y55NRF2S.js.map +1 -0
- package/package.json +1 -1
- package/dist/stream-FJQZGETE.js +0 -51
- package/dist/stream-FJQZGETE.js.map +0 -1
package/dist/main.js
CHANGED
|
@@ -124,6 +124,51 @@ function compactMessages(messages, maxTokens) {
|
|
|
124
124
|
};
|
|
125
125
|
}
|
|
126
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
|
+
|
|
127
172
|
// src/permissions/gate.ts
|
|
128
173
|
import path from "path";
|
|
129
174
|
|
|
@@ -174,6 +219,9 @@ var DANGEROUS_COMMAND_PATTERNS = [
|
|
|
174
219
|
/\breboot\b/i,
|
|
175
220
|
/\bpoweroff\b/i
|
|
176
221
|
];
|
|
222
|
+
function isDangerousCommand(command) {
|
|
223
|
+
return DANGEROUS_COMMAND_PATTERNS.some((pattern) => pattern.test(command));
|
|
224
|
+
}
|
|
177
225
|
function getStringArg(args, key) {
|
|
178
226
|
if (typeof args === "object" && args !== null) {
|
|
179
227
|
const value = args[key];
|
|
@@ -200,9 +248,12 @@ function isSensitivePath(p) {
|
|
|
200
248
|
return base === ".env" || base.startsWith(".env.") || base === "id_rsa" || base.endsWith(".pem");
|
|
201
249
|
}
|
|
202
250
|
function checkPermission(mode, req, ctx, allowRules = []) {
|
|
251
|
+
if (mode === "yolo") {
|
|
252
|
+
return "allow";
|
|
253
|
+
}
|
|
203
254
|
const command = getStringArg(req.args, "command");
|
|
204
255
|
const filePath = getStringArg(req.args, "path");
|
|
205
|
-
if (req.toolName === "bash" && command !== void 0 &&
|
|
256
|
+
if (req.toolName === "bash" && command !== void 0 && isDangerousCommand(command)) {
|
|
206
257
|
return "deny";
|
|
207
258
|
}
|
|
208
259
|
if (FILE_TOOLS.has(req.toolName) && filePath !== void 0) {
|
|
@@ -222,16 +273,78 @@ function checkPermission(mode, req, ctx, allowRules = []) {
|
|
|
222
273
|
if (mode === "readonly") {
|
|
223
274
|
return req.level === "read" ? "allow" : "deny";
|
|
224
275
|
}
|
|
276
|
+
if (mode === "plan") {
|
|
277
|
+
return req.level === "read" ? "allow" : "deny";
|
|
278
|
+
}
|
|
225
279
|
if (isAllowedByRules(allowRules, req)) {
|
|
226
280
|
return "allow";
|
|
227
281
|
}
|
|
228
282
|
return req.level === "read" ? "allow" : "ask";
|
|
229
283
|
}
|
|
230
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
|
+
|
|
231
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.";
|
|
232
340
|
var AgentLoop = class {
|
|
233
341
|
messages = [];
|
|
234
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 = [];
|
|
235
348
|
confirmHandler;
|
|
236
349
|
constructor(opts) {
|
|
237
350
|
this.opts = opts;
|
|
@@ -243,14 +356,41 @@ var AgentLoop = class {
|
|
|
243
356
|
return this.messages;
|
|
244
357
|
}
|
|
245
358
|
async loadMessages(messages) {
|
|
246
|
-
this.messages = 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);
|
|
247
385
|
}
|
|
248
386
|
async persist(message) {
|
|
249
387
|
await this.opts.sessionStore?.append(message);
|
|
250
388
|
}
|
|
251
389
|
async *stream(input, signal, opts) {
|
|
390
|
+
this.syncSystemMessage();
|
|
252
391
|
const userMessage = { role: "user", content: input };
|
|
253
392
|
this.messages.push(userMessage);
|
|
393
|
+
this.turnMarkers.push({ seq: beginTurn(), userIndex: this.messages.length - 1 });
|
|
254
394
|
await this.persist(
|
|
255
395
|
opts?.persistAs !== void 0 ? { role: "user", content: opts.persistAs } : userMessage
|
|
256
396
|
);
|
|
@@ -269,6 +409,8 @@ var AgentLoop = class {
|
|
|
269
409
|
if (event.type === "text-delta") {
|
|
270
410
|
text += event.text;
|
|
271
411
|
yield event;
|
|
412
|
+
} else if (event.type === "reasoning") {
|
|
413
|
+
yield event;
|
|
272
414
|
} else if (event.type === "tool-call") {
|
|
273
415
|
toolCalls.push({ id: event.id, name: event.name, args: event.args });
|
|
274
416
|
yield event;
|
|
@@ -280,6 +422,7 @@ var AgentLoop = class {
|
|
|
280
422
|
}
|
|
281
423
|
}
|
|
282
424
|
} catch (error) {
|
|
425
|
+
if (signal.aborted) return;
|
|
283
426
|
yield { type: "error", error: error instanceof Error ? error : new Error(String(error)) };
|
|
284
427
|
return;
|
|
285
428
|
}
|
|
@@ -299,35 +442,77 @@ var AgentLoop = class {
|
|
|
299
442
|
this.messages.push(assistantMessage);
|
|
300
443
|
await this.persist(assistantMessage);
|
|
301
444
|
if (toolCalls.length === 0) return;
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
const
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
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
|
+
}
|
|
324
492
|
}
|
|
493
|
+
if (signal.aborted) return;
|
|
325
494
|
}
|
|
326
495
|
yield {
|
|
327
496
|
type: "error",
|
|
328
497
|
error: new Error(`Max steps (${config.maxSteps}) reached, stopping.`)
|
|
329
498
|
};
|
|
330
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
|
+
}
|
|
331
516
|
async applyCompactionSummary(compacted) {
|
|
332
517
|
const { config, model } = this.opts;
|
|
333
518
|
if (config.contextCompaction !== "summary" || !model) {
|
|
@@ -349,17 +534,20 @@ ${summary}`
|
|
|
349
534
|
}
|
|
350
535
|
}
|
|
351
536
|
async *streamOnce(aiTools, signal) {
|
|
352
|
-
const { streamChat } = await import("./stream-
|
|
537
|
+
const { streamChat } = await import("./stream-Y55NRF2S.js");
|
|
353
538
|
yield* streamChat({
|
|
354
539
|
model: this.opts.model,
|
|
355
540
|
messages: this.messages,
|
|
356
541
|
tools: aiTools,
|
|
357
|
-
abortSignal: signal
|
|
542
|
+
abortSignal: signal,
|
|
543
|
+
idleTimeoutMs: this.opts.config.streamIdleTimeoutSec * 1e3
|
|
358
544
|
});
|
|
359
545
|
}
|
|
360
546
|
buildAiTools() {
|
|
547
|
+
const plan = this.opts.config.permissionMode === "plan";
|
|
361
548
|
const tools = {};
|
|
362
549
|
for (const t of this.opts.registry.list()) {
|
|
550
|
+
if (plan && t.permission !== "read") continue;
|
|
363
551
|
tools[t.name] = aiTool({
|
|
364
552
|
description: t.description,
|
|
365
553
|
parameters: t.parameters
|
|
@@ -383,11 +571,16 @@ ${summary}`
|
|
|
383
571
|
return { content: `Permission denied for tool "${call.name}".`, isError: true };
|
|
384
572
|
}
|
|
385
573
|
if (decision === "ask") {
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
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
|
+
}
|
|
391
584
|
if (!approved) {
|
|
392
585
|
return { content: `User rejected tool "${call.name}".`, isError: true };
|
|
393
586
|
}
|
|
@@ -410,13 +603,75 @@ ${summary}`
|
|
|
410
603
|
}
|
|
411
604
|
};
|
|
412
605
|
|
|
606
|
+
// src/cli/format.ts
|
|
607
|
+
function summarizeArgs(args, maxLength = 120) {
|
|
608
|
+
let json;
|
|
609
|
+
try {
|
|
610
|
+
json = JSON.stringify(args) ?? String(args);
|
|
611
|
+
} catch {
|
|
612
|
+
json = String(args);
|
|
613
|
+
}
|
|
614
|
+
return json.length > maxLength ? `${json.slice(0, maxLength)}...` : json;
|
|
615
|
+
}
|
|
616
|
+
function previewLines(text, maxLines = 10) {
|
|
617
|
+
const lines = text.split("\n");
|
|
618
|
+
if (lines.length <= maxLines) return { text, truncated: false };
|
|
619
|
+
return { text: lines.slice(0, maxLines).join("\n"), truncated: true };
|
|
620
|
+
}
|
|
621
|
+
function splitCommittableLines(text, minCompleteLines) {
|
|
622
|
+
const lastNewline = text.lastIndexOf("\n");
|
|
623
|
+
if (lastNewline < 0) return null;
|
|
624
|
+
const complete = text.slice(0, lastNewline);
|
|
625
|
+
const completeLines = complete.split("\n").length;
|
|
626
|
+
if (completeLines < minCompleteLines) return null;
|
|
627
|
+
return { committed: complete, rest: text.slice(lastNewline + 1) };
|
|
628
|
+
}
|
|
629
|
+
function formatStreamError(error) {
|
|
630
|
+
if (error.message.includes("Unexpected end of JSON input")) {
|
|
631
|
+
return `${error.message} (response stream was truncated \u2014 try again)`;
|
|
632
|
+
}
|
|
633
|
+
return error.message;
|
|
634
|
+
}
|
|
635
|
+
function coreMessageText(message) {
|
|
636
|
+
const content = message.content;
|
|
637
|
+
if (typeof content === "string") return content;
|
|
638
|
+
if (Array.isArray(content)) {
|
|
639
|
+
return content.filter((part) => part.type === "text").map((part) => "text" in part ? part.text : "").join(" ");
|
|
640
|
+
}
|
|
641
|
+
return "";
|
|
642
|
+
}
|
|
643
|
+
function buildDisplayMessages(messages) {
|
|
644
|
+
const display = [];
|
|
645
|
+
let collapsed = 0;
|
|
646
|
+
for (const message of messages) {
|
|
647
|
+
if (message.role === "user" || message.role === "assistant") {
|
|
648
|
+
const text = coreMessageText(message);
|
|
649
|
+
if (text) {
|
|
650
|
+
display.push({ id: display.length, role: message.role, text });
|
|
651
|
+
} else {
|
|
652
|
+
collapsed++;
|
|
653
|
+
}
|
|
654
|
+
} else if (message.role === "tool") {
|
|
655
|
+
collapsed++;
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
if (collapsed > 0) {
|
|
659
|
+
display.push({
|
|
660
|
+
id: display.length,
|
|
661
|
+
role: "system",
|
|
662
|
+
text: `\u5DF2\u6062\u590D ${collapsed} \u6761\u5386\u53F2\u6D88\u606F`
|
|
663
|
+
});
|
|
664
|
+
}
|
|
665
|
+
return display;
|
|
666
|
+
}
|
|
667
|
+
|
|
413
668
|
// src/cli/mentions.ts
|
|
414
|
-
import { readFile, stat as stat2 } from "fs/promises";
|
|
415
|
-
import
|
|
669
|
+
import { readFile as readFile2, stat as stat2 } from "fs/promises";
|
|
670
|
+
import path4 from "path";
|
|
416
671
|
|
|
417
672
|
// src/tools/fs/util.ts
|
|
418
673
|
import { readdir, stat } from "fs/promises";
|
|
419
|
-
import
|
|
674
|
+
import path3 from "path";
|
|
420
675
|
var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist"]);
|
|
421
676
|
async function walkFiles(root, skipDirs = SKIP_DIRS) {
|
|
422
677
|
const out = [];
|
|
@@ -426,7 +681,7 @@ async function walkFiles(root, skipDirs = SKIP_DIRS) {
|
|
|
426
681
|
return;
|
|
427
682
|
}
|
|
428
683
|
for (const entry of entries) {
|
|
429
|
-
const abs =
|
|
684
|
+
const abs = path3.join(dir, entry.name);
|
|
430
685
|
const rel = relBase ? `${relBase}/${entry.name}` : entry.name;
|
|
431
686
|
if (entry.isDirectory()) {
|
|
432
687
|
if (!skipDirs.has(entry.name)) {
|
|
@@ -482,7 +737,7 @@ function matchesGlob(pattern, relPath) {
|
|
|
482
737
|
return globToRegExp(normalized).test(relPath);
|
|
483
738
|
}
|
|
484
739
|
function isSensitivePath2(filePath) {
|
|
485
|
-
const base =
|
|
740
|
+
const base = path3.basename(filePath);
|
|
486
741
|
if (base === ".env.example" || base === ".env.sample" || base === ".env.template") {
|
|
487
742
|
return false;
|
|
488
743
|
}
|
|
@@ -538,7 +793,7 @@ async function resolveMentions(text, cwd) {
|
|
|
538
793
|
const attached = [];
|
|
539
794
|
const skipped = [];
|
|
540
795
|
for (const mention of mentions) {
|
|
541
|
-
const abs =
|
|
796
|
+
const abs = path4.resolve(cwd, mention);
|
|
542
797
|
if (isSensitivePath2(abs)) {
|
|
543
798
|
skipped.push({ path: mention, reason: "sensitive file" });
|
|
544
799
|
continue;
|
|
@@ -556,7 +811,7 @@ async function resolveMentions(text, cwd) {
|
|
|
556
811
|
skipped.push({ path: mention, reason: `exceeds ${MAX_MENTION_BYTES / 1024}KB limit` });
|
|
557
812
|
continue;
|
|
558
813
|
}
|
|
559
|
-
const buf = await
|
|
814
|
+
const buf = await readFile2(abs).catch(() => null);
|
|
560
815
|
if (!buf) {
|
|
561
816
|
skipped.push({ path: mention, reason: "unreadable" });
|
|
562
817
|
continue;
|
|
@@ -574,29 +829,72 @@ ${buf.toString("utf8")}
|
|
|
574
829
|
return { input, attached, skipped };
|
|
575
830
|
}
|
|
576
831
|
|
|
832
|
+
// src/cli/print-json.ts
|
|
833
|
+
function eventToJsonLine(event) {
|
|
834
|
+
switch (event.type) {
|
|
835
|
+
case "finish":
|
|
836
|
+
return null;
|
|
837
|
+
case "error":
|
|
838
|
+
return JSON.stringify({ type: "error", error: { message: event.error.message } });
|
|
839
|
+
default:
|
|
840
|
+
return JSON.stringify(event);
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
var UsageTracker = class {
|
|
844
|
+
requests = 0;
|
|
845
|
+
promptTokens = 0;
|
|
846
|
+
completionTokens = 0;
|
|
847
|
+
totalTokens = 0;
|
|
848
|
+
add(usage) {
|
|
849
|
+
if (!usage) return;
|
|
850
|
+
this.requests += 1;
|
|
851
|
+
this.promptTokens += usage.promptTokens;
|
|
852
|
+
this.completionTokens += usage.completionTokens;
|
|
853
|
+
this.totalTokens += usage.totalTokens;
|
|
854
|
+
}
|
|
855
|
+
get totals() {
|
|
856
|
+
return {
|
|
857
|
+
requests: this.requests,
|
|
858
|
+
promptTokens: this.promptTokens,
|
|
859
|
+
completionTokens: this.completionTokens,
|
|
860
|
+
totalTokens: this.totalTokens
|
|
861
|
+
};
|
|
862
|
+
}
|
|
863
|
+
toJsonLine() {
|
|
864
|
+
if (this.requests === 0) return null;
|
|
865
|
+
return JSON.stringify({
|
|
866
|
+
type: "usage",
|
|
867
|
+
requests: this.requests,
|
|
868
|
+
promptTokens: this.promptTokens,
|
|
869
|
+
completionTokens: this.completionTokens,
|
|
870
|
+
totalTokens: this.totalTokens
|
|
871
|
+
});
|
|
872
|
+
}
|
|
873
|
+
};
|
|
874
|
+
|
|
577
875
|
// src/cli/repl.tsx
|
|
578
|
-
import { Box as
|
|
876
|
+
import { Box as Box8, Text as Text8, render, useApp, useInput as useInput4 } from "ink";
|
|
579
877
|
import { useCallback, useEffect, useMemo, useRef as useRef2, useState as useState2 } from "react";
|
|
580
878
|
|
|
581
879
|
// src/config/save.ts
|
|
582
880
|
import fs from "fs";
|
|
583
|
-
import
|
|
881
|
+
import path6 from "path";
|
|
584
882
|
import { parse, stringify } from "smol-toml";
|
|
585
883
|
|
|
586
884
|
// src/config/paths.ts
|
|
587
885
|
import os from "os";
|
|
588
|
-
import
|
|
886
|
+
import path5 from "path";
|
|
589
887
|
function starHome() {
|
|
590
|
-
return process.env.STAR_HOME ??
|
|
888
|
+
return process.env.STAR_HOME ?? path5.join(os.homedir(), ".star-cli");
|
|
591
889
|
}
|
|
592
890
|
function globalConfigPath() {
|
|
593
|
-
return
|
|
891
|
+
return path5.join(starHome(), "config.toml");
|
|
594
892
|
}
|
|
595
893
|
function sessionsDir() {
|
|
596
|
-
return
|
|
894
|
+
return path5.join(starHome(), "sessions");
|
|
597
895
|
}
|
|
598
896
|
function projectConfigPath(cwd) {
|
|
599
|
-
return
|
|
897
|
+
return path5.join(cwd, ".star", "config.toml");
|
|
600
898
|
}
|
|
601
899
|
|
|
602
900
|
// src/config/save.ts
|
|
@@ -614,10 +912,23 @@ async function addAllowRule(rule) {
|
|
|
614
912
|
if (allow.includes(rule)) return false;
|
|
615
913
|
permissions.allow = [...allow, rule];
|
|
616
914
|
raw.permissions = permissions;
|
|
617
|
-
await fs.promises.mkdir(
|
|
915
|
+
await fs.promises.mkdir(path6.dirname(filePath), { recursive: true });
|
|
618
916
|
await fs.promises.writeFile(filePath, stringify(raw), "utf8");
|
|
619
917
|
return true;
|
|
620
918
|
}
|
|
919
|
+
async function savePermissionMode(mode) {
|
|
920
|
+
const filePath = globalConfigPath();
|
|
921
|
+
let raw = {};
|
|
922
|
+
try {
|
|
923
|
+
const content = await fs.promises.readFile(filePath, "utf8");
|
|
924
|
+
raw = parse(content);
|
|
925
|
+
} catch (error) {
|
|
926
|
+
if (error.code !== "ENOENT") throw error;
|
|
927
|
+
}
|
|
928
|
+
raw.permissionMode = mode;
|
|
929
|
+
await fs.promises.mkdir(path6.dirname(filePath), { recursive: true });
|
|
930
|
+
await fs.promises.writeFile(filePath, stringify(raw), "utf8");
|
|
931
|
+
}
|
|
621
932
|
|
|
622
933
|
// src/llm/provider.ts
|
|
623
934
|
import { createAnthropic } from "@ai-sdk/anthropic";
|
|
@@ -683,13 +994,17 @@ function createModel(config, modelName) {
|
|
|
683
994
|
|
|
684
995
|
// src/session/store.ts
|
|
685
996
|
import fs2 from "fs/promises";
|
|
686
|
-
import
|
|
997
|
+
import path7 from "path";
|
|
687
998
|
function generateId(now) {
|
|
688
999
|
const pad = (n) => String(n).padStart(2, "0");
|
|
689
1000
|
const stamp = `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`;
|
|
690
1001
|
const suffix = Math.random().toString(16).slice(2, 8).padEnd(6, "0");
|
|
691
1002
|
return `${stamp}-${suffix}`;
|
|
692
1003
|
}
|
|
1004
|
+
function debugWarn(message) {
|
|
1005
|
+
if (process.env.STAR_DEBUG === "1") process.stderr.write(`[star-cli] ${message}
|
|
1006
|
+
`);
|
|
1007
|
+
}
|
|
693
1008
|
function messageText(message) {
|
|
694
1009
|
const content = message.content;
|
|
695
1010
|
if (typeof content === "string") return content;
|
|
@@ -705,13 +1020,13 @@ var SessionStore = class _SessionStore {
|
|
|
705
1020
|
initialized = false;
|
|
706
1021
|
constructor(id) {
|
|
707
1022
|
this.id = id;
|
|
708
|
-
this.dir =
|
|
1023
|
+
this.dir = path7.join(sessionsDir(), id);
|
|
709
1024
|
}
|
|
710
1025
|
metaPath() {
|
|
711
|
-
return
|
|
1026
|
+
return path7.join(this.dir, "meta.json");
|
|
712
1027
|
}
|
|
713
1028
|
messagesPath() {
|
|
714
|
-
return
|
|
1029
|
+
return path7.join(this.dir, "messages.jsonl");
|
|
715
1030
|
}
|
|
716
1031
|
static async create(cwd, model) {
|
|
717
1032
|
const store = new _SessionStore(generateId(/* @__PURE__ */ new Date()));
|
|
@@ -720,15 +1035,20 @@ var SessionStore = class _SessionStore {
|
|
|
720
1035
|
}
|
|
721
1036
|
static async open(id) {
|
|
722
1037
|
const store = new _SessionStore(id);
|
|
1038
|
+
let raw;
|
|
1039
|
+
try {
|
|
1040
|
+
raw = await fs2.readFile(store.metaPath(), "utf8");
|
|
1041
|
+
} catch {
|
|
1042
|
+
return null;
|
|
1043
|
+
}
|
|
723
1044
|
try {
|
|
724
|
-
const raw = await fs2.readFile(store.metaPath(), "utf8");
|
|
725
1045
|
const meta = JSON.parse(raw);
|
|
726
1046
|
if (meta.id !== id) return null;
|
|
727
|
-
store.initialized = true;
|
|
728
|
-
return store;
|
|
729
1047
|
} catch {
|
|
730
|
-
|
|
1048
|
+
debugWarn(`session ${id}: corrupt meta.json, using defaults`);
|
|
731
1049
|
}
|
|
1050
|
+
store.initialized = true;
|
|
1051
|
+
return store;
|
|
732
1052
|
}
|
|
733
1053
|
static async list(cwd) {
|
|
734
1054
|
let entries;
|
|
@@ -740,7 +1060,7 @@ var SessionStore = class _SessionStore {
|
|
|
740
1060
|
const metas = [];
|
|
741
1061
|
for (const entry of entries) {
|
|
742
1062
|
try {
|
|
743
|
-
const raw = await fs2.readFile(
|
|
1063
|
+
const raw = await fs2.readFile(path7.join(sessionsDir(), entry, "meta.json"), "utf8");
|
|
744
1064
|
const meta = JSON.parse(raw);
|
|
745
1065
|
if (meta.id === entry && (cwd === void 0 || meta.cwd === cwd)) metas.push(meta);
|
|
746
1066
|
} catch {
|
|
@@ -801,14 +1121,24 @@ var SessionStore = class _SessionStore {
|
|
|
801
1121
|
try {
|
|
802
1122
|
messages.push(JSON.parse(trimmed));
|
|
803
1123
|
} catch {
|
|
1124
|
+
debugWarn(`session ${this.id}: skipping corrupt messages.jsonl line`);
|
|
804
1125
|
}
|
|
805
1126
|
}
|
|
806
1127
|
return messages;
|
|
807
1128
|
}
|
|
1129
|
+
fallbackMeta() {
|
|
1130
|
+
const now = Date.now();
|
|
1131
|
+
return { id: this.id, title: "", model: "", cwd: "", createdAt: now, updatedAt: now };
|
|
1132
|
+
}
|
|
808
1133
|
async meta() {
|
|
809
1134
|
await this.ensureInitialized();
|
|
810
|
-
|
|
811
|
-
|
|
1135
|
+
try {
|
|
1136
|
+
const raw = await fs2.readFile(this.metaPath(), "utf8");
|
|
1137
|
+
return JSON.parse(raw);
|
|
1138
|
+
} catch {
|
|
1139
|
+
debugWarn(`session ${this.id}: unreadable meta.json, using defaults`);
|
|
1140
|
+
return this.fallbackMeta();
|
|
1141
|
+
}
|
|
812
1142
|
}
|
|
813
1143
|
async setTitle(title) {
|
|
814
1144
|
await this.ensureInitialized();
|
|
@@ -839,7 +1169,11 @@ var SessionStore = class _SessionStore {
|
|
|
839
1169
|
async function resumeSession(id) {
|
|
840
1170
|
const store = await SessionStore.open(id);
|
|
841
1171
|
if (!store) return null;
|
|
842
|
-
const [meta,
|
|
1172
|
+
const [meta, loaded] = await Promise.all([store.meta(), store.messages()]);
|
|
1173
|
+
const messages = reconcileToolCalls(loaded);
|
|
1174
|
+
if (messages.length !== loaded.length) {
|
|
1175
|
+
await store.replaceMessages(messages);
|
|
1176
|
+
}
|
|
843
1177
|
return { meta, messages };
|
|
844
1178
|
}
|
|
845
1179
|
function relativeTime(timestamp) {
|
|
@@ -862,20 +1196,67 @@ function formatSessionList(metas) {
|
|
|
862
1196
|
}).join("\n");
|
|
863
1197
|
}
|
|
864
1198
|
|
|
1199
|
+
// src/tasks/format.ts
|
|
1200
|
+
function formatDuration(startedAt, endedAt) {
|
|
1201
|
+
const seconds = Math.max(0, Math.round(((endedAt ?? Date.now()) - startedAt) / 1e3));
|
|
1202
|
+
if (seconds < 60) return `${seconds}s`;
|
|
1203
|
+
const minutes = Math.floor(seconds / 60);
|
|
1204
|
+
if (minutes < 60) return `${minutes}m${seconds % 60}s`;
|
|
1205
|
+
return `${Math.floor(minutes / 60)}h${minutes % 60}m`;
|
|
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}`;
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
// src/tasks/manager.ts
|
|
1243
|
+
import { spawn as spawn2 } from "child_process";
|
|
1244
|
+
import { EventEmitter } from "events";
|
|
1245
|
+
|
|
865
1246
|
// src/tools/bash.ts
|
|
866
1247
|
import { spawn } from "child_process";
|
|
867
1248
|
import { existsSync } from "fs";
|
|
868
|
-
import
|
|
1249
|
+
import path8 from "path";
|
|
869
1250
|
import { z } from "zod";
|
|
870
1251
|
var MAX_OUTPUT = 3e4;
|
|
871
1252
|
var DEFAULT_TIMEOUT = 120;
|
|
872
1253
|
var MAX_TIMEOUT = 600;
|
|
873
1254
|
function findOnPath(exe, exclude) {
|
|
874
|
-
for (const dir of (process.env.PATH ?? "").split(
|
|
1255
|
+
for (const dir of (process.env.PATH ?? "").split(path8.delimiter)) {
|
|
875
1256
|
if (!dir || exclude?.(dir)) {
|
|
876
1257
|
continue;
|
|
877
1258
|
}
|
|
878
|
-
const full =
|
|
1259
|
+
const full = path8.join(dir, exe);
|
|
879
1260
|
if (existsSync(full)) {
|
|
880
1261
|
return full;
|
|
881
1262
|
}
|
|
@@ -893,7 +1274,7 @@ function resolveShell() {
|
|
|
893
1274
|
}
|
|
894
1275
|
const gitExe = findOnPath("git.exe");
|
|
895
1276
|
if (gitExe) {
|
|
896
|
-
const sibling =
|
|
1277
|
+
const sibling = path8.join(path8.dirname(path8.dirname(gitExe)), "bin", "bash.exe");
|
|
897
1278
|
if (existsSync(sibling)) {
|
|
898
1279
|
return { shell: sibling, wrap: (c) => ["-c", c], label: "bash" };
|
|
899
1280
|
}
|
|
@@ -913,7 +1294,10 @@ function resolveShell() {
|
|
|
913
1294
|
var schema = z.object({
|
|
914
1295
|
command: z.string().describe("Shell command to execute"),
|
|
915
1296
|
timeout: z.number().positive().max(MAX_TIMEOUT).optional().describe("Timeout in seconds (default 120, max 600)"),
|
|
916
|
-
description: z.string().optional().describe("Short description of what the command does")
|
|
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
|
+
)
|
|
917
1301
|
});
|
|
918
1302
|
function killTree(child) {
|
|
919
1303
|
if (process.platform === "win32" && child.pid) {
|
|
@@ -938,10 +1322,23 @@ ${tail}`;
|
|
|
938
1322
|
}
|
|
939
1323
|
var bashTool = {
|
|
940
1324
|
name: "bash",
|
|
941
|
-
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.",
|
|
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.",
|
|
942
1326
|
permission: "exec",
|
|
943
1327
|
parameters: schema,
|
|
944
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
|
+
}
|
|
945
1342
|
const timeoutSeconds = Math.min(args.timeout ?? DEFAULT_TIMEOUT, MAX_TIMEOUT);
|
|
946
1343
|
const spec = resolveShell();
|
|
947
1344
|
return new Promise((resolve) => {
|
|
@@ -999,47 +1396,120 @@ Command timed out after ${timeoutSeconds}s`,
|
|
|
999
1396
|
}
|
|
1000
1397
|
};
|
|
1001
1398
|
|
|
1002
|
-
// src/
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
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);
|
|
1019
1449
|
}
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
stack.push(snapshot);
|
|
1024
|
-
if (stack.length > MAX_SNAPSHOTS) {
|
|
1025
|
-
stack.splice(0, stack.length - MAX_SNAPSHOTS);
|
|
1450
|
+
get(id) {
|
|
1451
|
+
const rec = this.records.get(id);
|
|
1452
|
+
return rec ? this.snapshot(rec) : void 0;
|
|
1026
1453
|
}
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
const snapshot = stack.pop();
|
|
1030
|
-
if (!snapshot) {
|
|
1031
|
-
return "Nothing to undo.";
|
|
1454
|
+
list() {
|
|
1455
|
+
return [...this.records.values()].map((rec) => this.snapshot(rec));
|
|
1032
1456
|
}
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
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;
|
|
1037
1463
|
}
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
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();
|
|
1041
1508
|
|
|
1042
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";
|
|
1043
1513
|
var schema2 = z2.object({
|
|
1044
1514
|
path: z2.string().describe("File path, absolute or relative to the working directory"),
|
|
1045
1515
|
old_string: z2.string().min(1).describe("Exact text to replace"),
|
|
@@ -1085,7 +1555,8 @@ var editFileTool = {
|
|
|
1085
1555
|
existed: true,
|
|
1086
1556
|
content,
|
|
1087
1557
|
toolName: "edit_file",
|
|
1088
|
-
timestamp: Date.now()
|
|
1558
|
+
timestamp: Date.now(),
|
|
1559
|
+
turn: currentTurnSeq()
|
|
1089
1560
|
});
|
|
1090
1561
|
return { content: `Edited ${args.path}: ${count} replacement${count > 1 ? "s" : ""}` };
|
|
1091
1562
|
}
|
|
@@ -1278,19 +1749,69 @@ var writeFileTool = {
|
|
|
1278
1749
|
}
|
|
1279
1750
|
};
|
|
1280
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.` });
|
|
1799
|
+
}
|
|
1800
|
+
};
|
|
1801
|
+
|
|
1281
1802
|
// src/tools/todo.ts
|
|
1282
1803
|
import { mkdir as mkdir3, readFile as readFile6, writeFile as writeFile4 } from "fs/promises";
|
|
1283
1804
|
import path14 from "path";
|
|
1284
|
-
import { z as
|
|
1285
|
-
var todoItemSchema =
|
|
1286
|
-
id:
|
|
1287
|
-
title:
|
|
1288
|
-
status:
|
|
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"])
|
|
1289
1810
|
});
|
|
1290
|
-
var writeSchema =
|
|
1291
|
-
todos:
|
|
1811
|
+
var writeSchema = z8.object({
|
|
1812
|
+
todos: z8.array(todoItemSchema).describe("The full todo list, replacing the current one")
|
|
1292
1813
|
});
|
|
1293
|
-
var readSchema =
|
|
1814
|
+
var readSchema = z8.object({});
|
|
1294
1815
|
function fileFor(cwd) {
|
|
1295
1816
|
return path14.join(cwd, ".star", "todos.json");
|
|
1296
1817
|
}
|
|
@@ -1374,13 +1895,13 @@ function createTodoTools(store = defaultStore) {
|
|
|
1374
1895
|
}
|
|
1375
1896
|
|
|
1376
1897
|
// src/tools/web/fetch.ts
|
|
1377
|
-
import { z as
|
|
1898
|
+
import { z as z9 } from "zod";
|
|
1378
1899
|
var DEFAULT_MAX_CHARS = 2e4;
|
|
1379
1900
|
var MAX_BYTES = 2 * 1024 * 1024;
|
|
1380
1901
|
var TIMEOUT_MS = 3e4;
|
|
1381
|
-
var schema7 =
|
|
1382
|
-
url:
|
|
1383
|
-
maxChars:
|
|
1902
|
+
var schema7 = z9.object({
|
|
1903
|
+
url: z9.string().describe("The http(s) URL to fetch"),
|
|
1904
|
+
maxChars: z9.number().int().positive().optional().describe("Maximum characters of text to return (default 20000)")
|
|
1384
1905
|
});
|
|
1385
1906
|
function decodeEntities(s) {
|
|
1386
1907
|
return s.replace(/&#(\d+);/g, (_, n) => String.fromCodePoint(Number(n))).replace(/&#x([0-9a-fA-F]+);/gi, (_, n) => String.fromCodePoint(Number.parseInt(n, 16))).replace(/&(quot|lt|gt|nbsp);/g, (m) => {
|
|
@@ -1508,15 +2029,15 @@ var webFetchTool = {
|
|
|
1508
2029
|
};
|
|
1509
2030
|
|
|
1510
2031
|
// src/tools/web/search.ts
|
|
1511
|
-
import { z as
|
|
2032
|
+
import { z as z10 } from "zod";
|
|
1512
2033
|
var TIMEOUT_MS2 = 15e3;
|
|
1513
2034
|
var DEFAULT_MAX_RESULTS = 5;
|
|
1514
2035
|
var MAX_RESULTS_LIMIT = 10;
|
|
1515
2036
|
var SEARCH_URL = "https://html.duckduckgo.com/html/?q=";
|
|
1516
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";
|
|
1517
|
-
var schema8 =
|
|
1518
|
-
query:
|
|
1519
|
-
maxResults:
|
|
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(
|
|
1520
2041
|
`Maximum number of results to return (default ${DEFAULT_MAX_RESULTS}, max ${MAX_RESULTS_LIMIT})`
|
|
1521
2042
|
)
|
|
1522
2043
|
});
|
|
@@ -1635,6 +2156,9 @@ var ToolRegistry = class {
|
|
|
1635
2156
|
bashTool,
|
|
1636
2157
|
webFetchTool,
|
|
1637
2158
|
webSearchTool,
|
|
2159
|
+
taskListTool,
|
|
2160
|
+
taskOutputTool,
|
|
2161
|
+
taskKillTool,
|
|
1638
2162
|
...createTodoTools()
|
|
1639
2163
|
]) {
|
|
1640
2164
|
this.register(tool);
|
|
@@ -1663,9 +2187,27 @@ function createDefaultRegistry() {
|
|
|
1663
2187
|
return registry;
|
|
1664
2188
|
}
|
|
1665
2189
|
|
|
1666
|
-
// src/
|
|
1667
|
-
import fs3 from "fs
|
|
2190
|
+
// src/version.ts
|
|
2191
|
+
import fs3 from "fs";
|
|
1668
2192
|
import path15 from "path";
|
|
2193
|
+
import { fileURLToPath } from "url";
|
|
2194
|
+
function readPackageVersion() {
|
|
2195
|
+
try {
|
|
2196
|
+
const pkgPath = path15.join(path15.dirname(fileURLToPath(import.meta.url)), "..", "package.json");
|
|
2197
|
+
const pkg = JSON.parse(fs3.readFileSync(pkgPath, "utf8"));
|
|
2198
|
+
if (typeof pkg === "object" && pkg !== null) {
|
|
2199
|
+
const version = pkg.version;
|
|
2200
|
+
if (typeof version === "string") return version;
|
|
2201
|
+
}
|
|
2202
|
+
} catch {
|
|
2203
|
+
}
|
|
2204
|
+
return "0.0.0";
|
|
2205
|
+
}
|
|
2206
|
+
var VERSION = readPackageVersion();
|
|
2207
|
+
|
|
2208
|
+
// src/cli/commands/actions.ts
|
|
2209
|
+
import fs4 from "fs/promises";
|
|
2210
|
+
import path16 from "path";
|
|
1669
2211
|
var MIN_COMPACT_MESSAGES = 4;
|
|
1670
2212
|
async function compactSession(opts) {
|
|
1671
2213
|
const { backend, sessionStore, config, model } = opts;
|
|
@@ -1726,15 +2268,15 @@ async function exportSession(opts) {
|
|
|
1726
2268
|
return "Nothing to export: the session has no messages.";
|
|
1727
2269
|
}
|
|
1728
2270
|
const sessionId = sessionStore?.id ?? `unknown-${Date.now()}`;
|
|
1729
|
-
const target = arg ?
|
|
2271
|
+
const target = arg ? path16.resolve(cwd, arg) : path16.resolve(cwd, `star-session-${sessionId}.md`);
|
|
1730
2272
|
const existed = await fileExists(target);
|
|
1731
|
-
await
|
|
1732
|
-
await
|
|
2273
|
+
await fs4.mkdir(path16.dirname(target), { recursive: true });
|
|
2274
|
+
await fs4.writeFile(target, renderSessionMarkdown(sessionId, messages));
|
|
1733
2275
|
return existed ? `Exported ${messages.length} messages to ${target} (overwrote existing file).` : `Exported ${messages.length} messages to ${target}.`;
|
|
1734
2276
|
}
|
|
1735
2277
|
async function fileExists(target) {
|
|
1736
2278
|
try {
|
|
1737
|
-
await
|
|
2279
|
+
await fs4.access(target);
|
|
1738
2280
|
return true;
|
|
1739
2281
|
} catch {
|
|
1740
2282
|
return false;
|
|
@@ -1865,6 +2407,14 @@ ${lines.join("\n")}`);
|
|
|
1865
2407
|
ctx.addSystemMessage(await ctx.showTodos());
|
|
1866
2408
|
}
|
|
1867
2409
|
});
|
|
2410
|
+
registry.register({
|
|
2411
|
+
name: "tasks",
|
|
2412
|
+
description: "List background shell tasks",
|
|
2413
|
+
usage: "/tasks",
|
|
2414
|
+
run(_args, ctx) {
|
|
2415
|
+
ctx.addSystemMessage(ctx.listTasks());
|
|
2416
|
+
}
|
|
2417
|
+
});
|
|
1868
2418
|
registry.register({
|
|
1869
2419
|
name: "cost",
|
|
1870
2420
|
description: "Show API token usage for this session",
|
|
@@ -1897,14 +2447,450 @@ ${lines.join("\n")}`);
|
|
|
1897
2447
|
ctx.addSystemMessage(await ctx.exportSession(args));
|
|
1898
2448
|
}
|
|
1899
2449
|
});
|
|
2450
|
+
registry.register({
|
|
2451
|
+
name: "permission",
|
|
2452
|
+
description: "Show or set the global permission mode (ask | auto | readonly | yolo)",
|
|
2453
|
+
usage: "/permission [ask|auto|readonly|yolo]",
|
|
2454
|
+
async run(args, ctx) {
|
|
2455
|
+
ctx.addSystemMessage(await ctx.permissionMode(args.trim()));
|
|
2456
|
+
}
|
|
2457
|
+
});
|
|
2458
|
+
registry.register({
|
|
2459
|
+
name: "plan",
|
|
2460
|
+
description: "Toggle plan mode: read-only research, then approve the plan before executing",
|
|
2461
|
+
usage: "/plan",
|
|
2462
|
+
async run(_args, ctx) {
|
|
2463
|
+
ctx.addSystemMessage(await ctx.planMode());
|
|
2464
|
+
}
|
|
2465
|
+
});
|
|
1900
2466
|
registry.register({
|
|
1901
2467
|
name: "undo",
|
|
1902
|
-
description: "
|
|
2468
|
+
description: "Undo the last conversation turn: revert its file changes (write_file/edit_file) and retract its messages",
|
|
1903
2469
|
usage: "/undo",
|
|
1904
2470
|
async run(_args, ctx) {
|
|
1905
2471
|
ctx.addSystemMessage(await ctx.undo());
|
|
1906
2472
|
}
|
|
1907
2473
|
});
|
|
2474
|
+
registry.register({
|
|
2475
|
+
name: "init",
|
|
2476
|
+
description: "Generate an AGENTS.md for the current project",
|
|
2477
|
+
usage: "/init [force]",
|
|
2478
|
+
async run(args, ctx) {
|
|
2479
|
+
ctx.addSystemMessage(await ctx.initProject(args));
|
|
2480
|
+
}
|
|
2481
|
+
});
|
|
2482
|
+
registry.register({
|
|
2483
|
+
name: "doctor",
|
|
2484
|
+
description: "Run environment and configuration checks",
|
|
2485
|
+
usage: "/doctor",
|
|
2486
|
+
async run(_args, ctx) {
|
|
2487
|
+
ctx.addSystemMessage(await ctx.runDoctor());
|
|
2488
|
+
}
|
|
2489
|
+
});
|
|
2490
|
+
}
|
|
2491
|
+
|
|
2492
|
+
// src/cli/commands/custom.ts
|
|
2493
|
+
import { readFileSync, readdirSync } from "fs";
|
|
2494
|
+
import path17 from "path";
|
|
2495
|
+
var NAME_PATTERN = /^[a-z0-9-]+$/;
|
|
2496
|
+
var DESCRIPTION_PATTERN = /^<!--\s*description:\s*(.+?)\s*-->\s*$/;
|
|
2497
|
+
var DEFAULT_DESCRIPTION = "Custom prompt command";
|
|
2498
|
+
function parseCommandFile(filePath, name) {
|
|
2499
|
+
let raw;
|
|
2500
|
+
try {
|
|
2501
|
+
raw = readFileSync(filePath, "utf8");
|
|
2502
|
+
} catch {
|
|
2503
|
+
return null;
|
|
2504
|
+
}
|
|
2505
|
+
const lines = raw.split(/\r?\n/);
|
|
2506
|
+
let description = DEFAULT_DESCRIPTION;
|
|
2507
|
+
const first = lines[0] ?? "";
|
|
2508
|
+
const match = first.match(DESCRIPTION_PATTERN);
|
|
2509
|
+
if (match) {
|
|
2510
|
+
description = match[1] ?? DEFAULT_DESCRIPTION;
|
|
2511
|
+
lines.shift();
|
|
2512
|
+
if (lines[0] !== void 0 && lines[0].trim() === "") lines.shift();
|
|
2513
|
+
}
|
|
2514
|
+
const template = lines.join("\n").trim();
|
|
2515
|
+
if (!template) return null;
|
|
2516
|
+
return { name, description, template, source: filePath };
|
|
2517
|
+
}
|
|
2518
|
+
function scanDir(dir, into) {
|
|
2519
|
+
let entries;
|
|
2520
|
+
try {
|
|
2521
|
+
entries = readdirSync(dir);
|
|
2522
|
+
} catch {
|
|
2523
|
+
return;
|
|
2524
|
+
}
|
|
2525
|
+
for (const entry of entries.sort()) {
|
|
2526
|
+
if (!entry.endsWith(".md")) continue;
|
|
2527
|
+
const name = entry.slice(0, -".md".length);
|
|
2528
|
+
if (!NAME_PATTERN.test(name)) continue;
|
|
2529
|
+
const parsed = parseCommandFile(path17.join(dir, entry), name);
|
|
2530
|
+
if (parsed) into.set(name, parsed);
|
|
2531
|
+
}
|
|
2532
|
+
}
|
|
2533
|
+
function loadCustomCommands(cwd, home = starHome()) {
|
|
2534
|
+
const byName = /* @__PURE__ */ new Map();
|
|
2535
|
+
scanDir(path17.join(home, "commands"), byName);
|
|
2536
|
+
scanDir(path17.join(cwd, ".star", "commands"), byName);
|
|
2537
|
+
return [...byName.values()];
|
|
2538
|
+
}
|
|
2539
|
+
function registerCustomCommands(registry, cwd, home) {
|
|
2540
|
+
const registered = [];
|
|
2541
|
+
for (const command of loadCustomCommands(cwd, home)) {
|
|
2542
|
+
if (registry.get(command.name)) continue;
|
|
2543
|
+
registry.register({
|
|
2544
|
+
name: command.name,
|
|
2545
|
+
description: command.description,
|
|
2546
|
+
usage: `/${command.name} [args]`,
|
|
2547
|
+
run(args, ctx) {
|
|
2548
|
+
const prompt = command.template.split("$ARGUMENTS").join(args);
|
|
2549
|
+
if (ctx.submitPrompt) {
|
|
2550
|
+
return ctx.submitPrompt(prompt);
|
|
2551
|
+
}
|
|
2552
|
+
ctx.addSystemMessage(`/${command.name}: this context cannot submit prompts to the model.`);
|
|
2553
|
+
}
|
|
2554
|
+
});
|
|
2555
|
+
registered.push(command);
|
|
2556
|
+
}
|
|
2557
|
+
return registered;
|
|
2558
|
+
}
|
|
2559
|
+
|
|
2560
|
+
// src/cli/commands/doctor.ts
|
|
2561
|
+
import fs5 from "fs/promises";
|
|
2562
|
+
import path18 from "path";
|
|
2563
|
+
var MIN_NODE_MAJOR = 20;
|
|
2564
|
+
var REGISTRY_URL = "https://registry.npmjs.org/";
|
|
2565
|
+
var REGISTRY_TIMEOUT_MS = 5e3;
|
|
2566
|
+
async function defaultFetcher(url, timeoutMs) {
|
|
2567
|
+
const res = await fetch(url, { signal: AbortSignal.timeout(timeoutMs) });
|
|
2568
|
+
return res.ok;
|
|
2569
|
+
}
|
|
2570
|
+
function nodeMajor(version) {
|
|
2571
|
+
const match = /^v?(\d+)/.exec(version);
|
|
2572
|
+
return match ? Number.parseInt(match[1] ?? "", 10) : null;
|
|
2573
|
+
}
|
|
2574
|
+
async function pathExists(target) {
|
|
2575
|
+
try {
|
|
2576
|
+
await fs5.access(target);
|
|
2577
|
+
return true;
|
|
2578
|
+
} catch {
|
|
2579
|
+
return false;
|
|
2580
|
+
}
|
|
2581
|
+
}
|
|
2582
|
+
function resolveApiKeyConfigured(config, providerName, env) {
|
|
2583
|
+
const provider = config.providers.find((p) => p.name === providerName);
|
|
2584
|
+
if (!provider) return null;
|
|
2585
|
+
const fromEnv = provider.apiKeyEnv ? env[provider.apiKeyEnv] : void 0;
|
|
2586
|
+
return Boolean(fromEnv ?? provider.apiKey);
|
|
2587
|
+
}
|
|
2588
|
+
async function runDoctor(opts) {
|
|
2589
|
+
const env = opts.env ?? process.env;
|
|
2590
|
+
const home = opts.home ?? starHome();
|
|
2591
|
+
const checks = [];
|
|
2592
|
+
const version = opts.nodeVersion ?? process.version;
|
|
2593
|
+
const major = nodeMajor(version);
|
|
2594
|
+
const nodeOk = major !== null && major >= MIN_NODE_MAJOR;
|
|
2595
|
+
checks.push({
|
|
2596
|
+
name: "Node.js version",
|
|
2597
|
+
ok: nodeOk,
|
|
2598
|
+
detail: nodeOk ? `${version} (>= ${MIN_NODE_MAJOR} required)` : `${version} \u2014 Node.js >= ${MIN_NODE_MAJOR} is required`
|
|
2599
|
+
});
|
|
2600
|
+
if (opts.shellLabel !== void 0 || opts.shellPath !== void 0) {
|
|
2601
|
+
checks.push({
|
|
2602
|
+
name: "Shell for bash tool",
|
|
2603
|
+
ok: true,
|
|
2604
|
+
detail: `${opts.shellLabel ?? "unknown"}${opts.shellPath ? ` (${opts.shellPath})` : ""}`
|
|
2605
|
+
});
|
|
2606
|
+
} else {
|
|
2607
|
+
const spec = resolveShell();
|
|
2608
|
+
checks.push({
|
|
2609
|
+
name: "Shell for bash tool",
|
|
2610
|
+
ok: true,
|
|
2611
|
+
detail: `${spec.label} (${spec.shell})`
|
|
2612
|
+
});
|
|
2613
|
+
}
|
|
2614
|
+
const configPath = path18.join(home, "config.toml");
|
|
2615
|
+
const configExists = await pathExists(configPath);
|
|
2616
|
+
checks.push({
|
|
2617
|
+
name: "STAR_HOME / config",
|
|
2618
|
+
ok: configExists,
|
|
2619
|
+
detail: configExists ? `STAR_HOME=${home}, config.toml found at ${configPath}` : `STAR_HOME=${home}, config.toml missing at ${configPath}`
|
|
2620
|
+
});
|
|
2621
|
+
const defaultModel = opts.config.defaultModel;
|
|
2622
|
+
if (!defaultModel) {
|
|
2623
|
+
checks.push({
|
|
2624
|
+
name: "Default model",
|
|
2625
|
+
ok: false,
|
|
2626
|
+
detail: "no defaultModel configured"
|
|
2627
|
+
});
|
|
2628
|
+
} else {
|
|
2629
|
+
const modelConfig = opts.config.models.find((m) => m.name === defaultModel);
|
|
2630
|
+
if (!modelConfig) {
|
|
2631
|
+
checks.push({
|
|
2632
|
+
name: "Default model",
|
|
2633
|
+
ok: false,
|
|
2634
|
+
detail: `model "${defaultModel}" not found in configured models`
|
|
2635
|
+
});
|
|
2636
|
+
} else {
|
|
2637
|
+
const provider = opts.config.providers.find((p) => p.name === modelConfig.provider);
|
|
2638
|
+
checks.push({
|
|
2639
|
+
name: "Default model",
|
|
2640
|
+
ok: provider !== void 0,
|
|
2641
|
+
detail: provider ? `${modelConfig.name} (${provider.name}/${modelConfig.model}, ${provider.baseURL})` : `provider "${modelConfig.provider}" for model "${modelConfig.name}" not found`
|
|
2642
|
+
});
|
|
2643
|
+
const keyConfigured = resolveApiKeyConfigured(opts.config, modelConfig.provider, env);
|
|
2644
|
+
checks.push({
|
|
2645
|
+
name: "Provider API key",
|
|
2646
|
+
ok: keyConfigured === true,
|
|
2647
|
+
detail: keyConfigured === null ? `provider "${modelConfig.provider}" not found` : keyConfigured ? `apiKey resolved for provider "${modelConfig.provider}"` : `apiKey missing for provider "${modelConfig.provider}"${provider?.apiKeyEnv ? ` (set ${provider.apiKeyEnv} or configure apiKey)` : ""}`
|
|
2648
|
+
});
|
|
2649
|
+
}
|
|
2650
|
+
}
|
|
2651
|
+
const sessions = path18.join(home, "sessions");
|
|
2652
|
+
let writable = false;
|
|
2653
|
+
let writeError = "";
|
|
2654
|
+
try {
|
|
2655
|
+
await fs5.mkdir(sessions, { recursive: true });
|
|
2656
|
+
const probe = path18.join(sessions, `.doctor-probe-${process.pid}`);
|
|
2657
|
+
await fs5.writeFile(probe, "ok");
|
|
2658
|
+
await fs5.unlink(probe);
|
|
2659
|
+
writable = true;
|
|
2660
|
+
} catch (error) {
|
|
2661
|
+
writeError = error instanceof Error ? error.message : String(error);
|
|
2662
|
+
}
|
|
2663
|
+
checks.push({
|
|
2664
|
+
name: "Sessions directory writable",
|
|
2665
|
+
ok: writable,
|
|
2666
|
+
detail: writable ? `${sessions} is writable` : `${sessions} not writable: ${writeError}`
|
|
2667
|
+
});
|
|
2668
|
+
const fetcher = opts.fetcher ?? defaultFetcher;
|
|
2669
|
+
let registryOk = false;
|
|
2670
|
+
try {
|
|
2671
|
+
registryOk = await fetcher(REGISTRY_URL, REGISTRY_TIMEOUT_MS);
|
|
2672
|
+
} catch {
|
|
2673
|
+
registryOk = false;
|
|
2674
|
+
}
|
|
2675
|
+
checks.push({
|
|
2676
|
+
name: "npm registry connectivity",
|
|
2677
|
+
ok: registryOk,
|
|
2678
|
+
detail: registryOk ? `${REGISTRY_URL} reachable` : `${REGISTRY_URL} unreachable (optional \u2014 only affects update checks)`,
|
|
2679
|
+
optional: true
|
|
2680
|
+
});
|
|
2681
|
+
const passed = checks.filter((c) => c.ok).length;
|
|
2682
|
+
return { checks, passed, failed: checks.length - passed };
|
|
2683
|
+
}
|
|
2684
|
+
function formatDoctorReport(report) {
|
|
2685
|
+
const lines = report.checks.map(
|
|
2686
|
+
(check) => `${check.ok ? "\u2713" : "\u2717"} ${check.name}: ${check.detail}`
|
|
2687
|
+
);
|
|
2688
|
+
lines.push("", `${report.passed} passed, ${report.failed} failed`);
|
|
2689
|
+
return lines.join("\n");
|
|
2690
|
+
}
|
|
2691
|
+
|
|
2692
|
+
// src/cli/commands/init-project.ts
|
|
2693
|
+
import fs6 from "fs/promises";
|
|
2694
|
+
import path19 from "path";
|
|
2695
|
+
import { generateText as generateText2 } from "ai";
|
|
2696
|
+
var IGNORED_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", "out", "coverage"]);
|
|
2697
|
+
var MAX_ENTRIES = 20;
|
|
2698
|
+
var README_HEAD_LINES = 5;
|
|
2699
|
+
async function fileExists2(target) {
|
|
2700
|
+
try {
|
|
2701
|
+
await fs6.access(target);
|
|
2702
|
+
return true;
|
|
2703
|
+
} catch {
|
|
2704
|
+
return false;
|
|
2705
|
+
}
|
|
2706
|
+
}
|
|
2707
|
+
async function readPackageJson(cwd) {
|
|
2708
|
+
try {
|
|
2709
|
+
const raw = await fs6.readFile(path19.join(cwd, "package.json"), "utf8");
|
|
2710
|
+
const parsed = JSON.parse(raw);
|
|
2711
|
+
if (typeof parsed !== "object" || parsed === null) return null;
|
|
2712
|
+
return parsed;
|
|
2713
|
+
} catch {
|
|
2714
|
+
return null;
|
|
2715
|
+
}
|
|
2716
|
+
}
|
|
2717
|
+
function stringRecord(value) {
|
|
2718
|
+
if (typeof value !== "object" || value === null) return {};
|
|
2719
|
+
const out = {};
|
|
2720
|
+
for (const [key, val] of Object.entries(value)) {
|
|
2721
|
+
if (typeof val === "string") out[key] = val;
|
|
2722
|
+
}
|
|
2723
|
+
return out;
|
|
2724
|
+
}
|
|
2725
|
+
async function detectPackageManager(cwd) {
|
|
2726
|
+
if (await fileExists2(path19.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
|
|
2727
|
+
if (await fileExists2(path19.join(cwd, "yarn.lock"))) return "yarn";
|
|
2728
|
+
if (await fileExists2(path19.join(cwd, "package-lock.json"))) return "npm";
|
|
2729
|
+
if (await fileExists2(path19.join(cwd, "bun.lockb"))) return "bun";
|
|
2730
|
+
return null;
|
|
2731
|
+
}
|
|
2732
|
+
function inferCommands(pm, scripts) {
|
|
2733
|
+
const runner = pm ?? "npm";
|
|
2734
|
+
const pick = (script) => script in scripts ? `${runner} ${script}` : null;
|
|
2735
|
+
return {
|
|
2736
|
+
test: pick("test"),
|
|
2737
|
+
build: pick("build"),
|
|
2738
|
+
lint: pick("lint"),
|
|
2739
|
+
typecheck: pick("typecheck")
|
|
2740
|
+
};
|
|
2741
|
+
}
|
|
2742
|
+
async function listEntries(cwd) {
|
|
2743
|
+
try {
|
|
2744
|
+
const dirents = await fs6.readdir(cwd, { withFileTypes: true });
|
|
2745
|
+
const entries = [];
|
|
2746
|
+
for (const dirent of dirents) {
|
|
2747
|
+
if (dirent.isDirectory()) {
|
|
2748
|
+
if (IGNORED_DIRS.has(dirent.name)) continue;
|
|
2749
|
+
entries.push(`${dirent.name}/`);
|
|
2750
|
+
} else if (!dirent.name.startsWith(".")) {
|
|
2751
|
+
entries.push(dirent.name);
|
|
2752
|
+
}
|
|
2753
|
+
if (entries.length >= MAX_ENTRIES) break;
|
|
2754
|
+
}
|
|
2755
|
+
return entries.sort();
|
|
2756
|
+
} catch {
|
|
2757
|
+
return [];
|
|
2758
|
+
}
|
|
2759
|
+
}
|
|
2760
|
+
async function readReadmeHead(cwd) {
|
|
2761
|
+
for (const name of ["README.md", "readme.md", "README"]) {
|
|
2762
|
+
try {
|
|
2763
|
+
const raw = await fs6.readFile(path19.join(cwd, name), "utf8");
|
|
2764
|
+
return raw.split("\n").map((line) => line.trim()).filter((line) => line.length > 0).slice(0, README_HEAD_LINES);
|
|
2765
|
+
} catch {
|
|
2766
|
+
}
|
|
2767
|
+
}
|
|
2768
|
+
return [];
|
|
2769
|
+
}
|
|
2770
|
+
async function scanProject(cwd) {
|
|
2771
|
+
const pkg = await readPackageJson(cwd);
|
|
2772
|
+
const packageManager = await detectPackageManager(cwd);
|
|
2773
|
+
const scripts = stringRecord(pkg?.scripts);
|
|
2774
|
+
return {
|
|
2775
|
+
name: typeof pkg?.name === "string" ? pkg.name : null,
|
|
2776
|
+
dirName: path19.basename(cwd),
|
|
2777
|
+
packageManager,
|
|
2778
|
+
scripts,
|
|
2779
|
+
dependencies: Object.keys(stringRecord(pkg?.dependencies)),
|
|
2780
|
+
devDependencies: Object.keys(stringRecord(pkg?.devDependencies)),
|
|
2781
|
+
commands: inferCommands(packageManager, scripts),
|
|
2782
|
+
hasTsconfig: await fileExists2(path19.join(cwd, "tsconfig.json")),
|
|
2783
|
+
isGitRepo: await fileExists2(path19.join(cwd, ".git")),
|
|
2784
|
+
readmeHead: await readReadmeHead(cwd),
|
|
2785
|
+
entries: await listEntries(cwd)
|
|
2786
|
+
};
|
|
2787
|
+
}
|
|
2788
|
+
function renderAgentsMd(facts) {
|
|
2789
|
+
const title = facts.name ?? facts.dirName;
|
|
2790
|
+
const lines = [
|
|
2791
|
+
"# AGENTS.md",
|
|
2792
|
+
"",
|
|
2793
|
+
"Guidance for AI agents working in this repository.",
|
|
2794
|
+
"",
|
|
2795
|
+
"## Project",
|
|
2796
|
+
"",
|
|
2797
|
+
facts.readmeHead.length > 0 ? facts.readmeHead.join(" ") : `${title}`,
|
|
2798
|
+
""
|
|
2799
|
+
];
|
|
2800
|
+
const commandLines = [];
|
|
2801
|
+
const push = (label, command) => {
|
|
2802
|
+
if (command) commandLines.push(`- \`${command}\` \u2014 ${label}`);
|
|
2803
|
+
};
|
|
2804
|
+
push("run tests", facts.commands.test);
|
|
2805
|
+
push("build", facts.commands.build);
|
|
2806
|
+
push("lint", facts.commands.lint);
|
|
2807
|
+
push("typecheck", facts.commands.typecheck);
|
|
2808
|
+
if (commandLines.length > 0) {
|
|
2809
|
+
lines.push("## Commands", "", ...commandLines, "");
|
|
2810
|
+
}
|
|
2811
|
+
lines.push("## Layout", "");
|
|
2812
|
+
for (const entry of facts.entries) {
|
|
2813
|
+
lines.push(`- \`${entry}\``);
|
|
2814
|
+
}
|
|
2815
|
+
lines.push("", "## Conventions", "");
|
|
2816
|
+
const conventions = [];
|
|
2817
|
+
if (facts.hasTsconfig) conventions.push("TypeScript project (tsconfig.json present).");
|
|
2818
|
+
if (facts.packageManager) conventions.push(`Use ${facts.packageManager} as the package manager.`);
|
|
2819
|
+
if (facts.isGitRepo) conventions.push("Git repository.");
|
|
2820
|
+
if (facts.dependencies.length > 0) {
|
|
2821
|
+
conventions.push(`Key dependencies: ${facts.dependencies.slice(0, 10).join(", ")}.`);
|
|
2822
|
+
}
|
|
2823
|
+
if (conventions.length === 0) conventions.push("No conventions detected yet \u2014 fill this in.");
|
|
2824
|
+
for (const item of conventions) lines.push(`- ${item}`);
|
|
2825
|
+
lines.push("");
|
|
2826
|
+
return lines.join("\n");
|
|
2827
|
+
}
|
|
2828
|
+
var INIT_SYSTEM_PROMPT = [
|
|
2829
|
+
"You are writing an AGENTS.md file that guides AI coding agents working in a repository.",
|
|
2830
|
+
"You are given scanned project facts as JSON.",
|
|
2831
|
+
"Produce a concise Markdown document with sections: Project, Commands, Layout, Conventions.",
|
|
2832
|
+
"Keep commands exactly as given in the facts. Do not invent tools, scripts, or directories.",
|
|
2833
|
+
"Output only the Markdown, no preamble, no code fences."
|
|
2834
|
+
].join("\n");
|
|
2835
|
+
async function polishWithModel(facts, model) {
|
|
2836
|
+
const { text } = await generateText2({
|
|
2837
|
+
model,
|
|
2838
|
+
system: INIT_SYSTEM_PROMPT,
|
|
2839
|
+
prompt: `Project facts:
|
|
2840
|
+
|
|
2841
|
+
${JSON.stringify(facts, null, 2)}
|
|
2842
|
+
|
|
2843
|
+
Write the AGENTS.md.`
|
|
2844
|
+
});
|
|
2845
|
+
const trimmed = text.trim();
|
|
2846
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
2847
|
+
}
|
|
2848
|
+
async function initProject(opts) {
|
|
2849
|
+
const { cwd, args, model } = opts;
|
|
2850
|
+
const target = path19.join(cwd, "AGENTS.md");
|
|
2851
|
+
const force = args.split(/\s+/).includes("force");
|
|
2852
|
+
if (!force && await fileExists2(target)) {
|
|
2853
|
+
return {
|
|
2854
|
+
message: `AGENTS.md already exists at ${target}. Use /init force to overwrite it.`,
|
|
2855
|
+
written: false,
|
|
2856
|
+
path: target,
|
|
2857
|
+
lines: 0,
|
|
2858
|
+
usedLlm: false
|
|
2859
|
+
};
|
|
2860
|
+
}
|
|
2861
|
+
const facts = await scanProject(cwd);
|
|
2862
|
+
let usedLlm = false;
|
|
2863
|
+
let content;
|
|
2864
|
+
let note = "";
|
|
2865
|
+
if (model) {
|
|
2866
|
+
try {
|
|
2867
|
+
const polished = await polishWithModel(facts, model);
|
|
2868
|
+
if (polished) {
|
|
2869
|
+
content = polished.endsWith("\n") ? polished : `${polished}
|
|
2870
|
+
`;
|
|
2871
|
+
usedLlm = true;
|
|
2872
|
+
} else {
|
|
2873
|
+
content = renderAgentsMd(facts);
|
|
2874
|
+
note = " Model returned an empty draft; used the template instead \u2014 review and refine manually.";
|
|
2875
|
+
}
|
|
2876
|
+
} catch {
|
|
2877
|
+
content = renderAgentsMd(facts);
|
|
2878
|
+
note = " Model refinement failed; used the template instead \u2014 review and refine manually.";
|
|
2879
|
+
}
|
|
2880
|
+
} else {
|
|
2881
|
+
content = renderAgentsMd(facts);
|
|
2882
|
+
note = " No model available; used the template \u2014 review and refine manually.";
|
|
2883
|
+
}
|
|
2884
|
+
await fs6.writeFile(target, content);
|
|
2885
|
+
const lines = content.split("\n").length;
|
|
2886
|
+
const how = usedLlm ? "generated with model refinement" : "generated from template";
|
|
2887
|
+
return {
|
|
2888
|
+
message: `Wrote ${target} (${lines} lines, ${how}).${note}`,
|
|
2889
|
+
written: true,
|
|
2890
|
+
path: target,
|
|
2891
|
+
lines,
|
|
2892
|
+
usedLlm
|
|
2893
|
+
};
|
|
1908
2894
|
}
|
|
1909
2895
|
|
|
1910
2896
|
// src/cli/commands/registry.ts
|
|
@@ -1936,8 +2922,24 @@ function parseSlashCommand(input) {
|
|
|
1936
2922
|
};
|
|
1937
2923
|
}
|
|
1938
2924
|
|
|
1939
|
-
// src/cli/components/
|
|
2925
|
+
// src/cli/components/ExitPlanPrompt.tsx
|
|
1940
2926
|
import { Box, Text, useInput } from "ink";
|
|
2927
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2928
|
+
function ExitPlanPrompt({ onDecision }) {
|
|
2929
|
+
useInput((input, key) => {
|
|
2930
|
+
const ch = input.toLowerCase();
|
|
2931
|
+
if (ch === "y") onDecision("yes");
|
|
2932
|
+
else if (ch === "n" || key.escape) onDecision("no");
|
|
2933
|
+
});
|
|
2934
|
+
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1, children: [
|
|
2935
|
+
/* @__PURE__ */ jsx(Text, { bold: true, color: "cyan", children: "Plan ready (plan mode)" }),
|
|
2936
|
+
/* @__PURE__ */ jsx(Text, { children: "The plan above was produced without making any changes." }),
|
|
2937
|
+
/* @__PURE__ */ jsx(Text, { children: "[y] approve and execute [n] keep refining (Esc = n)" })
|
|
2938
|
+
] });
|
|
2939
|
+
}
|
|
2940
|
+
|
|
2941
|
+
// src/cli/components/InputBox.tsx
|
|
2942
|
+
import { Box as Box2, Text as Text2, useInput as useInput2 } from "ink";
|
|
1941
2943
|
import { useRef, useState } from "react";
|
|
1942
2944
|
|
|
1943
2945
|
// src/cli/commands/suggest.ts
|
|
@@ -1950,7 +2952,7 @@ function filterCommands(input, commands) {
|
|
|
1950
2952
|
}
|
|
1951
2953
|
|
|
1952
2954
|
// src/cli/components/InputBox.tsx
|
|
1953
|
-
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2955
|
+
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
1954
2956
|
function InputBox({
|
|
1955
2957
|
isStreaming,
|
|
1956
2958
|
disabled,
|
|
@@ -1980,7 +2982,7 @@ function InputBox({
|
|
|
1980
2982
|
const text = `/${cmd.name} `;
|
|
1981
2983
|
edit(text, text.length);
|
|
1982
2984
|
};
|
|
1983
|
-
|
|
2985
|
+
useInput2((input, key) => {
|
|
1984
2986
|
if (key.ctrl && input === "c") {
|
|
1985
2987
|
if (isStreaming || disabled) {
|
|
1986
2988
|
onInterrupt();
|
|
@@ -2011,6 +3013,7 @@ function InputBox({
|
|
|
2011
3013
|
return;
|
|
2012
3014
|
}
|
|
2013
3015
|
if (key.tab) {
|
|
3016
|
+
if (key.shift) return;
|
|
2014
3017
|
if (suggestions.length > 0) {
|
|
2015
3018
|
completeHighlighted();
|
|
2016
3019
|
return;
|
|
@@ -2091,164 +3094,176 @@ function InputBox({
|
|
|
2091
3094
|
const before = value.slice(0, cursor);
|
|
2092
3095
|
const at = value[cursor] ?? " ";
|
|
2093
3096
|
const after = value.slice(cursor + 1);
|
|
2094
|
-
return /* @__PURE__ */
|
|
2095
|
-
/* @__PURE__ */
|
|
2096
|
-
/* @__PURE__ */
|
|
2097
|
-
/* @__PURE__ */
|
|
2098
|
-
/* @__PURE__ */
|
|
2099
|
-
/* @__PURE__ */
|
|
3097
|
+
return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", children: [
|
|
3098
|
+
/* @__PURE__ */ jsxs2(Box2, { borderStyle: "round", borderColor: "gray", paddingX: 1, children: [
|
|
3099
|
+
/* @__PURE__ */ jsx2(Text2, { color: "cyan", children: "> " }),
|
|
3100
|
+
/* @__PURE__ */ jsx2(Text2, { children: before }),
|
|
3101
|
+
/* @__PURE__ */ jsx2(Text2, { inverse: true, children: at }),
|
|
3102
|
+
/* @__PURE__ */ jsx2(Text2, { children: after })
|
|
2100
3103
|
] }),
|
|
2101
|
-
suggestions.length > 0 && /* @__PURE__ */
|
|
2102
|
-
(cmd, index) => index === activeIndex ? /* @__PURE__ */
|
|
2103
|
-
/* @__PURE__ */
|
|
2104
|
-
/* @__PURE__ */
|
|
3104
|
+
suggestions.length > 0 && /* @__PURE__ */ jsx2(Box2, { flexDirection: "column", paddingLeft: 2, children: suggestions.map(
|
|
3105
|
+
(cmd, index) => index === activeIndex ? /* @__PURE__ */ jsx2(Text2, { bold: true, inverse: true, children: `/${cmd.name} - ${cmd.description}` }, cmd.name) : /* @__PURE__ */ jsxs2(Text2, { children: [
|
|
3106
|
+
/* @__PURE__ */ jsx2(Text2, { color: "cyan", children: `/${cmd.name}` }),
|
|
3107
|
+
/* @__PURE__ */ jsx2(Text2, { dimColor: true, children: ` - ${cmd.description}` })
|
|
2105
3108
|
] }, cmd.name)
|
|
2106
3109
|
) })
|
|
2107
3110
|
] });
|
|
2108
3111
|
}
|
|
2109
3112
|
|
|
2110
3113
|
// src/cli/components/MessageList.tsx
|
|
2111
|
-
import { Box as
|
|
2112
|
-
import {
|
|
3114
|
+
import { Box as Box3, Static, Text as Text3 } from "ink";
|
|
3115
|
+
import { memo } from "react";
|
|
3116
|
+
import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
2113
3117
|
var roleStyles = {
|
|
2114
3118
|
user: { label: "you", color: "cyan" },
|
|
2115
3119
|
assistant: { label: "star", color: "green" },
|
|
2116
3120
|
system: { label: "system", color: "yellow" },
|
|
2117
3121
|
tool: { label: "tool", color: "magenta" }
|
|
2118
3122
|
};
|
|
2119
|
-
|
|
2120
|
-
return /* @__PURE__ */
|
|
3123
|
+
var MessageList = memo(function MessageList2({ messages }) {
|
|
3124
|
+
return /* @__PURE__ */ jsx3(Static, { items: messages, children: (message) => {
|
|
3125
|
+
const marginBottom = message.tight ? 0 : 1;
|
|
3126
|
+
if (message.role === "assistant-cont") {
|
|
3127
|
+
return /* @__PURE__ */ jsx3(Box3, { flexDirection: "column", marginBottom, children: /* @__PURE__ */ jsx3(Text3, { color: "green", children: message.text }) }, message.id);
|
|
3128
|
+
}
|
|
2121
3129
|
const style = roleStyles[message.role];
|
|
2122
|
-
return /* @__PURE__ */
|
|
2123
|
-
/* @__PURE__ */
|
|
2124
|
-
/* @__PURE__ */
|
|
2125
|
-
message.note && /* @__PURE__ */
|
|
3130
|
+
return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", marginBottom, children: [
|
|
3131
|
+
/* @__PURE__ */ jsx3(Text3, { bold: true, color: style.color, children: style.label }),
|
|
3132
|
+
/* @__PURE__ */ jsx3(Text3, { color: style.color, children: message.text }),
|
|
3133
|
+
message.note && /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: message.note })
|
|
2126
3134
|
] }, message.id);
|
|
2127
3135
|
} });
|
|
2128
|
-
}
|
|
3136
|
+
});
|
|
2129
3137
|
|
|
2130
3138
|
// src/cli/components/PermissionPrompt.tsx
|
|
2131
|
-
import { Box as
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
}
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
}
|
|
2148
|
-
function coreMessageText(message) {
|
|
2149
|
-
const content = message.content;
|
|
2150
|
-
if (typeof content === "string") return content;
|
|
2151
|
-
if (Array.isArray(content)) {
|
|
2152
|
-
return content.filter((part) => part.type === "text").map((part) => "text" in part ? part.text : "").join(" ");
|
|
2153
|
-
}
|
|
2154
|
-
return "";
|
|
2155
|
-
}
|
|
2156
|
-
function buildDisplayMessages(messages) {
|
|
2157
|
-
const display = [];
|
|
2158
|
-
let collapsed = 0;
|
|
2159
|
-
for (const message of messages) {
|
|
2160
|
-
if (message.role === "user" || message.role === "assistant") {
|
|
2161
|
-
const text = coreMessageText(message);
|
|
2162
|
-
if (text) {
|
|
2163
|
-
display.push({ id: display.length, role: message.role, text });
|
|
2164
|
-
} else {
|
|
2165
|
-
collapsed++;
|
|
2166
|
-
}
|
|
2167
|
-
} else if (message.role === "tool") {
|
|
2168
|
-
collapsed++;
|
|
2169
|
-
}
|
|
2170
|
-
}
|
|
2171
|
-
if (collapsed > 0) {
|
|
2172
|
-
display.push({
|
|
2173
|
-
id: display.length,
|
|
2174
|
-
role: "system",
|
|
2175
|
-
text: `\u5DF2\u6062\u590D ${collapsed} \u6761\u5386\u53F2\u6D88\u606F`
|
|
2176
|
-
});
|
|
2177
|
-
}
|
|
2178
|
-
return display;
|
|
3139
|
+
import { Box as Box4, Text as Text4, useInput as useInput3 } from "ink";
|
|
3140
|
+
import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
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
|
+
] });
|
|
2179
3155
|
}
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
2183
|
-
function PermissionPrompt({ request, onDecision }) {
|
|
2184
|
-
useInput2((input) => {
|
|
3156
|
+
function PermissionPrompt({ request, preview, onDecision }) {
|
|
3157
|
+
useInput3((input) => {
|
|
2185
3158
|
const ch = input.toLowerCase();
|
|
2186
3159
|
if (ch === "y") onDecision("yes");
|
|
2187
3160
|
else if (ch === "n") onDecision("no");
|
|
2188
3161
|
else if (ch === "a") onDecision("always");
|
|
2189
3162
|
});
|
|
2190
|
-
return /* @__PURE__ */
|
|
2191
|
-
/* @__PURE__ */
|
|
3163
|
+
return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", borderStyle: "round", borderColor: "yellow", paddingX: 1, children: [
|
|
3164
|
+
/* @__PURE__ */ jsxs4(Text4, { bold: true, color: "yellow", children: [
|
|
2192
3165
|
"Permission required: ",
|
|
2193
3166
|
request.toolName,
|
|
2194
3167
|
" (",
|
|
2195
3168
|
request.level,
|
|
2196
3169
|
")"
|
|
2197
3170
|
] }),
|
|
2198
|
-
/* @__PURE__ */
|
|
2199
|
-
|
|
3171
|
+
preview ? /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", children: [
|
|
3172
|
+
/* @__PURE__ */ jsxs4(Text4, { dimColor: true, children: [
|
|
3173
|
+
preview.type === "new-file" ? "New file: " : "",
|
|
3174
|
+
preview.label
|
|
3175
|
+
] }),
|
|
3176
|
+
preview.lines.map((line, index) => (
|
|
3177
|
+
// biome-ignore lint/suspicious/noArrayIndexKey: diff lines have no stable identity
|
|
3178
|
+
/* @__PURE__ */ jsx4(DiffLineView, { line }, index)
|
|
3179
|
+
))
|
|
3180
|
+
] }) : /* @__PURE__ */ jsx4(Text4, { children: summarizeArgs(request.args) }),
|
|
3181
|
+
/* @__PURE__ */ jsx4(Text4, { children: "[y] allow [n] deny [a] always (saved to config)" })
|
|
2200
3182
|
] });
|
|
2201
3183
|
}
|
|
2202
3184
|
|
|
2203
3185
|
// src/cli/components/StatusBar.tsx
|
|
2204
|
-
import { Box as
|
|
2205
|
-
import {
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
3186
|
+
import { Box as Box5, Text as Text5 } from "ink";
|
|
3187
|
+
import { memo as memo2 } from "react";
|
|
3188
|
+
import { jsxs as jsxs5 } from "react/jsx-runtime";
|
|
3189
|
+
var BG_LABEL_MAX = 40;
|
|
3190
|
+
var StatusBar = memo2(function StatusBar2({
|
|
3191
|
+
model,
|
|
3192
|
+
permissionMode,
|
|
3193
|
+
tokens,
|
|
3194
|
+
backgroundTasks = []
|
|
3195
|
+
}) {
|
|
3196
|
+
const joined = backgroundTasks.join(", ");
|
|
3197
|
+
const labels = joined.length > BG_LABEL_MAX ? `${joined.slice(0, BG_LABEL_MAX)}\u2026` : joined;
|
|
3198
|
+
return /* @__PURE__ */ jsxs5(Box5, { justifyContent: "space-between", children: [
|
|
3199
|
+
/* @__PURE__ */ jsxs5(Text5, { dimColor: true, children: [
|
|
2209
3200
|
"model: ",
|
|
2210
3201
|
model
|
|
2211
3202
|
] }),
|
|
2212
|
-
/* @__PURE__ */
|
|
3203
|
+
backgroundTasks.length > 0 && /* @__PURE__ */ jsxs5(Text5, { color: "yellow", children: [
|
|
3204
|
+
"bg: ",
|
|
3205
|
+
backgroundTasks.length,
|
|
3206
|
+
" (",
|
|
3207
|
+
labels,
|
|
3208
|
+
")"
|
|
3209
|
+
] }),
|
|
3210
|
+
/* @__PURE__ */ jsxs5(Text5, { dimColor: true, children: [
|
|
2213
3211
|
tokens,
|
|
2214
3212
|
" tokens"
|
|
2215
3213
|
] }),
|
|
2216
|
-
/* @__PURE__ */
|
|
3214
|
+
/* @__PURE__ */ jsxs5(Text5, { dimColor: true, children: [
|
|
2217
3215
|
"mode: ",
|
|
2218
3216
|
permissionMode
|
|
2219
3217
|
] })
|
|
2220
3218
|
] });
|
|
2221
|
-
}
|
|
3219
|
+
});
|
|
2222
3220
|
|
|
2223
3221
|
// src/cli/components/StreamingMessage.tsx
|
|
2224
|
-
import { Box as Box5, Text as Text5 } from "ink";
|
|
2225
|
-
import { jsx as jsx4, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
2226
|
-
function StreamingMessage({ text }) {
|
|
2227
|
-
return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", marginBottom: 1, children: [
|
|
2228
|
-
/* @__PURE__ */ jsx4(Text5, { bold: true, color: "green", children: "star" }),
|
|
2229
|
-
/* @__PURE__ */ jsx4(Text5, { color: "green", children: text })
|
|
2230
|
-
] });
|
|
2231
|
-
}
|
|
2232
|
-
|
|
2233
|
-
// src/cli/components/ToolCallCard.tsx
|
|
2234
3222
|
import { Box as Box6, Text as Text6 } from "ink";
|
|
3223
|
+
import { memo as memo3 } from "react";
|
|
2235
3224
|
import { jsx as jsx5, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
3225
|
+
var StreamingMessage = memo3(function StreamingMessage2({
|
|
3226
|
+
text,
|
|
3227
|
+
continuation
|
|
3228
|
+
}) {
|
|
2239
3229
|
return /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", marginBottom: 1, children: [
|
|
2240
|
-
/* @__PURE__ */
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
3230
|
+
!continuation && /* @__PURE__ */ jsx5(Text6, { bold: true, color: "green", children: "star" }),
|
|
3231
|
+
/* @__PURE__ */ jsx5(Text6, { color: "green", children: text })
|
|
3232
|
+
] });
|
|
3233
|
+
});
|
|
3234
|
+
|
|
3235
|
+
// src/cli/components/ThinkingIndicator.tsx
|
|
3236
|
+
import { Box as Box7, Text as Text7 } from "ink";
|
|
3237
|
+
import { jsxs as jsxs7 } from "react/jsx-runtime";
|
|
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"
|
|
2244
3258
|
] }),
|
|
2245
|
-
/* @__PURE__ */
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
preview.truncated ? "\n... (truncated)" : ""
|
|
3259
|
+
tail !== "" && /* @__PURE__ */ jsxs7(Text7, { dimColor: true, children: [
|
|
3260
|
+
" ",
|
|
3261
|
+
tail
|
|
2249
3262
|
] })
|
|
2250
3263
|
] });
|
|
2251
3264
|
}
|
|
3265
|
+
|
|
3266
|
+
// src/cli/components/ToolCallCard.tsx
|
|
2252
3267
|
function formatToolCard(card) {
|
|
2253
3268
|
const header = `${card.name} ${card.argsSummary}${card.isError ? " [error]" : ""}`;
|
|
2254
3269
|
if (card.result === void 0) return header;
|
|
@@ -2257,73 +3272,421 @@ function formatToolCard(card) {
|
|
|
2257
3272
|
${preview.text}${preview.truncated ? "\n... (truncated)" : ""}`;
|
|
2258
3273
|
}
|
|
2259
3274
|
|
|
2260
|
-
// src/cli/
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
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;
|
|
2265
3284
|
}
|
|
2266
|
-
function
|
|
2267
|
-
|
|
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)`;
|
|
2268
3291
|
}
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
const
|
|
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;
|
|
3361
|
+
}
|
|
3362
|
+
function hunkRanges(ops) {
|
|
3363
|
+
const changes = [];
|
|
3364
|
+
for (let i = 0; i < ops.length; i++) {
|
|
3365
|
+
const op = ops[i];
|
|
3366
|
+
if (op && op.kind !== "context") changes.push(i);
|
|
3367
|
+
}
|
|
3368
|
+
const ranges = [];
|
|
3369
|
+
let groupStart = 0;
|
|
3370
|
+
for (let k = 1; k <= changes.length; k++) {
|
|
3371
|
+
const cur = changes[k];
|
|
3372
|
+
const prev = changes[k - 1];
|
|
3373
|
+
const first = changes[groupStart];
|
|
3374
|
+
if (prev === void 0 || first === void 0) break;
|
|
3375
|
+
if (cur === void 0 || cur - prev > CONTEXT_LINES * 2 + 1) {
|
|
3376
|
+
ranges.push([
|
|
3377
|
+
Math.max(0, first - CONTEXT_LINES),
|
|
3378
|
+
Math.min(ops.length, prev + CONTEXT_LINES + 1)
|
|
3379
|
+
]);
|
|
3380
|
+
groupStart = k;
|
|
3381
|
+
}
|
|
3382
|
+
}
|
|
3383
|
+
return ranges;
|
|
3384
|
+
}
|
|
3385
|
+
function buildDiffLines(oldText, newText) {
|
|
3386
|
+
const ops = diffOps(oldText, newText);
|
|
3387
|
+
if (ops.length === 0) return [];
|
|
3388
|
+
const ranges = hunkRanges(ops);
|
|
3389
|
+
const lines = [];
|
|
3390
|
+
let omitted = 0;
|
|
3391
|
+
for (const [rangeIndex, [start, end]] of ranges.entries()) {
|
|
3392
|
+
if (rangeIndex > 0) lines.push({ kind: "marker", text: "\xB7\xB7\xB7" });
|
|
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;
|
|
3410
|
+
}
|
|
3411
|
+
function getStringArg2(args, key) {
|
|
3412
|
+
if (typeof args === "object" && args !== null) {
|
|
3413
|
+
const value = args[key];
|
|
3414
|
+
if (typeof value === "string") return value;
|
|
3415
|
+
}
|
|
3416
|
+
return void 0;
|
|
3417
|
+
}
|
|
3418
|
+
function applyEdit(content, oldString, newString, replaceAll) {
|
|
3419
|
+
let count = 0;
|
|
3420
|
+
let idx = content.indexOf(oldString);
|
|
3421
|
+
while (idx !== -1) {
|
|
3422
|
+
count += 1;
|
|
3423
|
+
idx = content.indexOf(oldString, idx + oldString.length);
|
|
3424
|
+
}
|
|
3425
|
+
if (count === 0) return null;
|
|
3426
|
+
if (count > 1 && !replaceAll) return null;
|
|
3427
|
+
return replaceAll ? content.split(oldString).join(newString) : content.replace(oldString, newString);
|
|
3428
|
+
}
|
|
3429
|
+
function newFilePreview(content, label) {
|
|
3430
|
+
const all = content.split("\n");
|
|
3431
|
+
const shown = all.slice(0, NEW_FILE_PREVIEW_LINES);
|
|
3432
|
+
const lines = shown.map((text) => ({ kind: "add", text }));
|
|
3433
|
+
if (all.length > shown.length) {
|
|
3434
|
+
lines.push({ kind: "marker", text: `... (${all.length - shown.length} more lines)` });
|
|
3435
|
+
}
|
|
3436
|
+
return { type: "new-file", label, lines };
|
|
3437
|
+
}
|
|
3438
|
+
async function generateDiffPreview(toolName, args, cwd) {
|
|
3439
|
+
if (toolName !== "edit_file" && toolName !== "write_file") return null;
|
|
3440
|
+
const filePath = getStringArg2(args, "path");
|
|
3441
|
+
if (filePath === void 0) return null;
|
|
3442
|
+
const absolute = path20.resolve(cwd, filePath);
|
|
3443
|
+
let existing;
|
|
3444
|
+
try {
|
|
3445
|
+
existing = await readFile7(absolute, "utf8");
|
|
3446
|
+
} catch {
|
|
3447
|
+
existing = null;
|
|
3448
|
+
}
|
|
3449
|
+
if (toolName === "edit_file") {
|
|
3450
|
+
const oldString = getStringArg2(args, "old_string");
|
|
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) };
|
|
3457
|
+
}
|
|
3458
|
+
const content = getStringArg2(args, "content");
|
|
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
|
+
}
|
|
3463
|
+
|
|
3464
|
+
// src/cli/shell-bang.ts
|
|
3465
|
+
var SHELL_BANG_DISPLAY_MAX_CHARS = 3e3;
|
|
3466
|
+
function truncateShellOutput(output, max = SHELL_BANG_DISPLAY_MAX_CHARS) {
|
|
3467
|
+
if (output.length <= max) return output;
|
|
3468
|
+
const half = Math.floor(max / 2);
|
|
3469
|
+
return `${output.slice(0, half)}
|
|
3470
|
+
... [${output.length - max} characters truncated] ...
|
|
3471
|
+
${output.slice(-half)}`;
|
|
3472
|
+
}
|
|
3473
|
+
function buildShellContextMessage(command, output) {
|
|
3474
|
+
return `The user ran \`!${command}\` directly in the shell (no permission prompt). Output:
|
|
3475
|
+
${output}`;
|
|
3476
|
+
}
|
|
3477
|
+
async function executeShellBang(raw, cwd, signal) {
|
|
3478
|
+
const command = raw.trim();
|
|
3479
|
+
if (!command) return { ok: false, reason: "empty" };
|
|
3480
|
+
if (isDangerousCommand(command)) return { ok: false, reason: "dangerous" };
|
|
3481
|
+
const result = await bashTool.execute({ command }, { cwd, abortSignal: signal });
|
|
3482
|
+
const output = truncateShellOutput(result.content);
|
|
3483
|
+
return {
|
|
3484
|
+
ok: true,
|
|
3485
|
+
command,
|
|
3486
|
+
output,
|
|
3487
|
+
isError: result.isError ?? false,
|
|
3488
|
+
contextMessage: buildShellContextMessage(command, output)
|
|
3489
|
+
};
|
|
3490
|
+
}
|
|
3491
|
+
|
|
3492
|
+
// src/cli/ticker.ts
|
|
3493
|
+
var TICK_MS = 100;
|
|
3494
|
+
function nextFlush(prev, next) {
|
|
3495
|
+
if (prev.streamed === next.streamed && prev.reasoning === next.reasoning) return null;
|
|
3496
|
+
return next;
|
|
3497
|
+
}
|
|
3498
|
+
function startTicker(onTick, intervalMs = TICK_MS) {
|
|
3499
|
+
let tick = 0;
|
|
3500
|
+
const timer = setInterval(() => {
|
|
3501
|
+
tick += 1;
|
|
3502
|
+
onTick(tick);
|
|
3503
|
+
}, intervalMs);
|
|
3504
|
+
return () => clearInterval(timer);
|
|
3505
|
+
}
|
|
3506
|
+
|
|
3507
|
+
// src/cli/update-check.ts
|
|
3508
|
+
var REGISTRY_URL2 = "https://registry.npmjs.org/@cryer%2fstar-cli/latest";
|
|
3509
|
+
var TIMEOUT_MS3 = 5e3;
|
|
3510
|
+
async function defaultFetcher2(url) {
|
|
3511
|
+
return fetch(url, { signal: AbortSignal.timeout(TIMEOUT_MS3) });
|
|
3512
|
+
}
|
|
3513
|
+
function isNewerVersion(latest, current) {
|
|
3514
|
+
const parse3 = (v) => v.split(".").slice(0, 3).map((part) => Number.parseInt(part, 10));
|
|
3515
|
+
const a = parse3(latest);
|
|
3516
|
+
const b = parse3(current);
|
|
3517
|
+
for (let i = 0; i < 3; i++) {
|
|
3518
|
+
const x = a[i] ?? 0;
|
|
3519
|
+
const y = b[i] ?? 0;
|
|
3520
|
+
if (Number.isNaN(x) || Number.isNaN(y)) return false;
|
|
3521
|
+
if (x !== y) return x > y;
|
|
3522
|
+
}
|
|
3523
|
+
return false;
|
|
3524
|
+
}
|
|
3525
|
+
async function checkForUpdate(currentVersion, fetcher = defaultFetcher2) {
|
|
3526
|
+
try {
|
|
3527
|
+
const res = await fetcher(REGISTRY_URL2);
|
|
3528
|
+
if (!res.ok) return null;
|
|
3529
|
+
const data = await res.json();
|
|
3530
|
+
const latest = typeof data === "object" && data !== null ? data.version : null;
|
|
3531
|
+
if (typeof latest !== "string" || !isNewerVersion(latest, currentVersion)) return null;
|
|
3532
|
+
return `New version available: ${currentVersion} -> ${latest} \u2014 run: npm i -g @cryer/star-cli`;
|
|
3533
|
+
} catch {
|
|
3534
|
+
return null;
|
|
3535
|
+
}
|
|
3536
|
+
}
|
|
3537
|
+
|
|
3538
|
+
// src/cli/repl.tsx
|
|
3539
|
+
import { jsx as jsx6, jsxs as jsxs8 } from "react/jsx-runtime";
|
|
3540
|
+
var CLEAR_TERMINAL = process.platform === "win32" ? "\x1B[2J\x1B[0f" : "\x1B[2J\x1B[3J\x1B[H";
|
|
3541
|
+
function emptyUsage() {
|
|
3542
|
+
return { requests: 0, promptTokens: 0, completionTokens: 0, totalTokens: 0 };
|
|
3543
|
+
}
|
|
3544
|
+
function runningTaskLabels() {
|
|
3545
|
+
return defaultTaskManager.list().filter((t) => t.status === "running").map((t) => t.description ?? t.command);
|
|
3546
|
+
}
|
|
3547
|
+
var SESSION_MODE_CYCLE = ["ask", "auto", "readonly", "plan"];
|
|
3548
|
+
function formatUsage(usage) {
|
|
3549
|
+
return `API usage this session: ${usage.requests} requests, ${usage.promptTokens} prompt + ${usage.completionTokens} completion = ${usage.totalTokens} tokens`;
|
|
3550
|
+
}
|
|
3551
|
+
function Repl({
|
|
3552
|
+
backend,
|
|
3553
|
+
model,
|
|
3554
|
+
permissionMode,
|
|
3555
|
+
config,
|
|
3556
|
+
cwd,
|
|
3557
|
+
sessionStore,
|
|
3558
|
+
initialMessages,
|
|
3559
|
+
initialUsage
|
|
3560
|
+
}) {
|
|
3561
|
+
const { exit } = useApp();
|
|
2280
3562
|
const [initialDisplay] = useState2(() => buildDisplayMessages(initialMessages ?? []));
|
|
2281
3563
|
const [messages, setMessages] = useState2(initialDisplay);
|
|
2282
3564
|
const [epoch, setEpoch] = useState2(0);
|
|
2283
3565
|
const [streamingText, setStreamingText] = useState2(null);
|
|
2284
3566
|
const [isStreaming, setIsStreaming] = useState2(false);
|
|
2285
|
-
const [
|
|
3567
|
+
const [thinking, setThinking] = useState2(false);
|
|
3568
|
+
const [thinkingText, setThinkingText] = useState2("");
|
|
3569
|
+
const [thoughtSummary, setThoughtSummary] = useState2(null);
|
|
3570
|
+
const [, setUsageVersion] = useState2(0);
|
|
2286
3571
|
const [modelName, setModelName] = useState2(model);
|
|
2287
3572
|
const [pending, setPending] = useState2(null);
|
|
2288
|
-
const [
|
|
3573
|
+
const [spinnerTick, setSpinnerTick] = useState2(0);
|
|
3574
|
+
const [activity, setActivity] = useState2(null);
|
|
3575
|
+
const [bgLabels, setBgLabels] = useState2(() => runningTaskLabels());
|
|
3576
|
+
const [permissionModeState, setPermissionModeState] = useState2(permissionMode);
|
|
3577
|
+
const [planApproval, setPlanApprovalState] = useState2(false);
|
|
2289
3578
|
const backendRef = useRef2(backend);
|
|
2290
3579
|
const nextIdRef = useRef2(initialDisplay.length);
|
|
2291
3580
|
const abortRef = useRef2(null);
|
|
2292
3581
|
const streamedRef = useRef2("");
|
|
2293
|
-
const
|
|
3582
|
+
const thinkingRef = useRef2(false);
|
|
3583
|
+
const reasoningRef = useRef2("");
|
|
3584
|
+
const tickerStopRef = useRef2(null);
|
|
3585
|
+
const flushedRef = useRef2({ streamed: "", reasoning: "" });
|
|
2294
3586
|
const toolCardsRef = useRef2(/* @__PURE__ */ new Map());
|
|
2295
3587
|
const pendingRef = useRef2(null);
|
|
2296
3588
|
const alwaysAllowedRef = useRef2(/* @__PURE__ */ new Set());
|
|
3589
|
+
const prevModeRef = useRef2(null);
|
|
3590
|
+
const planApprovalRef = useRef2(false);
|
|
2297
3591
|
const modelNameRef = useRef2(model);
|
|
2298
3592
|
const usageRef = useRef2(initialUsage ? { ...initialUsage } : emptyUsage());
|
|
2299
|
-
const
|
|
2300
|
-
|
|
3593
|
+
const turnChunksRef = useRef2(0);
|
|
3594
|
+
const messagesRef = useRef2(initialDisplay);
|
|
3595
|
+
const pushMessage = useCallback(
|
|
3596
|
+
(role, text, note, tight) => {
|
|
3597
|
+
setMessages((prev) => {
|
|
3598
|
+
const next = [...prev, { id: nextIdRef.current++, role, text, note, tight }];
|
|
3599
|
+
messagesRef.current = next;
|
|
3600
|
+
return next;
|
|
3601
|
+
});
|
|
3602
|
+
},
|
|
3603
|
+
[]
|
|
3604
|
+
);
|
|
3605
|
+
const applyMessages = useCallback((next) => {
|
|
3606
|
+
messagesRef.current = next;
|
|
3607
|
+
setMessages(next);
|
|
3608
|
+
setEpoch((e) => e + 1);
|
|
2301
3609
|
}, []);
|
|
3610
|
+
const redrawMessages = useCallback(
|
|
3611
|
+
(next) => {
|
|
3612
|
+
if (process.stdout.isTTY) {
|
|
3613
|
+
process.stdout.write(CLEAR_TERMINAL);
|
|
3614
|
+
}
|
|
3615
|
+
applyMessages(next);
|
|
3616
|
+
},
|
|
3617
|
+
[applyMessages]
|
|
3618
|
+
);
|
|
3619
|
+
const pushAssistantChunk = useCallback(
|
|
3620
|
+
(text, tight) => {
|
|
3621
|
+
const role = turnChunksRef.current === 0 ? "assistant" : "assistant-cont";
|
|
3622
|
+
turnChunksRef.current += 1;
|
|
3623
|
+
pushMessage(role, text, void 0, tight);
|
|
3624
|
+
},
|
|
3625
|
+
[pushMessage]
|
|
3626
|
+
);
|
|
2302
3627
|
const setPendingPermission = useCallback((p) => {
|
|
2303
3628
|
pendingRef.current = p;
|
|
2304
3629
|
setPending(p);
|
|
2305
3630
|
}, []);
|
|
3631
|
+
const setPlanApproval = useCallback((v) => {
|
|
3632
|
+
planApprovalRef.current = v;
|
|
3633
|
+
setPlanApprovalState(v);
|
|
3634
|
+
}, []);
|
|
3635
|
+
const applySessionMode = useCallback(
|
|
3636
|
+
(mode) => {
|
|
3637
|
+
if (mode === config.permissionMode) return;
|
|
3638
|
+
if (mode === "plan") {
|
|
3639
|
+
prevModeRef.current = config.permissionMode;
|
|
3640
|
+
} else if (config.permissionMode === "plan") {
|
|
3641
|
+
prevModeRef.current = null;
|
|
3642
|
+
}
|
|
3643
|
+
config.permissionMode = mode;
|
|
3644
|
+
setPermissionModeState(mode);
|
|
3645
|
+
},
|
|
3646
|
+
[config]
|
|
3647
|
+
);
|
|
2306
3648
|
const attachConfirmHandler = useCallback(
|
|
2307
3649
|
(target) => {
|
|
2308
|
-
target.confirmHandler = (req) => {
|
|
3650
|
+
target.confirmHandler = async (req) => {
|
|
2309
3651
|
if (isAllowedByRules([...alwaysAllowedRef.current], req)) {
|
|
2310
|
-
return
|
|
3652
|
+
return true;
|
|
2311
3653
|
}
|
|
3654
|
+
const preview = await generateDiffPreview(req.toolName, req.args, cwd).catch(() => null);
|
|
2312
3655
|
return new Promise((resolve) => {
|
|
2313
|
-
setPendingPermission({ request: req, resolve });
|
|
3656
|
+
setPendingPermission({ request: req, preview, resolve });
|
|
2314
3657
|
});
|
|
2315
3658
|
};
|
|
2316
3659
|
},
|
|
2317
|
-
[setPendingPermission]
|
|
3660
|
+
[setPendingPermission, cwd]
|
|
2318
3661
|
);
|
|
2319
3662
|
useEffect(() => {
|
|
2320
3663
|
attachConfirmHandler(backendRef.current);
|
|
2321
3664
|
return () => {
|
|
2322
|
-
|
|
3665
|
+
tickerStopRef.current?.();
|
|
2323
3666
|
abortRef.current?.abort();
|
|
2324
3667
|
pendingRef.current?.resolve(false);
|
|
2325
3668
|
};
|
|
2326
3669
|
}, [attachConfirmHandler]);
|
|
3670
|
+
useEffect(() => {
|
|
3671
|
+
if (process.env.STAR_NO_UPDATE_CHECK === "1") return;
|
|
3672
|
+
void checkForUpdate(VERSION).then((message) => {
|
|
3673
|
+
if (message) pushMessage("system", message);
|
|
3674
|
+
});
|
|
3675
|
+
}, [pushMessage]);
|
|
3676
|
+
useEffect(() => {
|
|
3677
|
+
const onUpdate = (task) => {
|
|
3678
|
+
setBgLabels(runningTaskLabels());
|
|
3679
|
+
pushMessage(
|
|
3680
|
+
"system",
|
|
3681
|
+
task.status === "running" ? formatTaskStarted(task) : formatTaskFinished(task)
|
|
3682
|
+
);
|
|
3683
|
+
};
|
|
3684
|
+
defaultTaskManager.on("update", onUpdate);
|
|
3685
|
+
return () => {
|
|
3686
|
+
defaultTaskManager.off("update", onUpdate);
|
|
3687
|
+
defaultTaskManager.cleanup();
|
|
3688
|
+
};
|
|
3689
|
+
}, [pushMessage]);
|
|
2327
3690
|
const interrupt = useCallback(() => {
|
|
2328
3691
|
abortRef.current?.abort();
|
|
2329
3692
|
const p = pendingRef.current;
|
|
@@ -2332,8 +3695,25 @@ function Repl({
|
|
|
2332
3695
|
p.resolve(false);
|
|
2333
3696
|
}
|
|
2334
3697
|
}, [setPendingPermission]);
|
|
2335
|
-
|
|
3698
|
+
const handleExit = useCallback(() => {
|
|
3699
|
+
const killed = defaultTaskManager.cleanup();
|
|
3700
|
+
if (killed.length > 0) {
|
|
3701
|
+
pushMessage(
|
|
3702
|
+
"system",
|
|
3703
|
+
`Stopped ${killed.length} background task(s): ${killed.map((t) => t.id).join(", ")}`
|
|
3704
|
+
);
|
|
3705
|
+
}
|
|
3706
|
+
exit();
|
|
3707
|
+
}, [exit, pushMessage]);
|
|
3708
|
+
useInput4((_input, key) => {
|
|
2336
3709
|
if (key.escape) interrupt();
|
|
3710
|
+
if (key.shift && key.tab) {
|
|
3711
|
+
if (abortRef.current || pendingRef.current || planApprovalRef.current) return;
|
|
3712
|
+
const current = config.permissionMode;
|
|
3713
|
+
const index = SESSION_MODE_CYCLE.indexOf(current);
|
|
3714
|
+
const next = SESSION_MODE_CYCLE[(index + 1) % SESSION_MODE_CYCLE.length] ?? "ask";
|
|
3715
|
+
applySessionMode(next);
|
|
3716
|
+
}
|
|
2337
3717
|
});
|
|
2338
3718
|
const handleDecision = useCallback(
|
|
2339
3719
|
(decision) => {
|
|
@@ -2382,32 +3762,205 @@ function Repl({
|
|
|
2382
3762
|
},
|
|
2383
3763
|
[config, cwd, sessionStore, attachConfirmHandler]
|
|
2384
3764
|
);
|
|
2385
|
-
const resume = useCallback(
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
3765
|
+
const resume = useCallback(
|
|
3766
|
+
async (id) => {
|
|
3767
|
+
const current = backendRef.current;
|
|
3768
|
+
if (!(current instanceof AgentLoop)) {
|
|
3769
|
+
return "Current backend does not support resuming sessions.";
|
|
3770
|
+
}
|
|
3771
|
+
const resumed = await resumeSession(id);
|
|
3772
|
+
if (!resumed) {
|
|
3773
|
+
return `Session not found: ${id}`;
|
|
3774
|
+
}
|
|
3775
|
+
await current.loadMessages(resumed.messages);
|
|
3776
|
+
const display = buildDisplayMessages(resumed.messages);
|
|
3777
|
+
nextIdRef.current = display.length;
|
|
3778
|
+
applyMessages(display);
|
|
3779
|
+
usageRef.current = resumed.meta.usage ? { ...resumed.meta.usage } : emptyUsage();
|
|
3780
|
+
setUsageVersion((v) => v + 1);
|
|
3781
|
+
return `Resumed session ${id} (${resumed.messages.length} messages).`;
|
|
3782
|
+
},
|
|
3783
|
+
[applyMessages]
|
|
3784
|
+
);
|
|
3785
|
+
const runStream = useCallback(
|
|
3786
|
+
async (input) => {
|
|
3787
|
+
const resolved = await resolveMentions(input, cwd);
|
|
3788
|
+
pushMessage(
|
|
3789
|
+
"user",
|
|
3790
|
+
input,
|
|
3791
|
+
resolved.attached.length > 0 ? `attached: ${resolved.attached.join(", ")}` : void 0
|
|
3792
|
+
);
|
|
3793
|
+
for (const skip of resolved.skipped) {
|
|
3794
|
+
pushMessage("system", `Skipped @${skip.path}: ${skip.reason}`);
|
|
3795
|
+
}
|
|
3796
|
+
const controller = new AbortController();
|
|
3797
|
+
abortRef.current = controller;
|
|
3798
|
+
streamedRef.current = "";
|
|
3799
|
+
thinkingRef.current = true;
|
|
3800
|
+
reasoningRef.current = "";
|
|
3801
|
+
turnChunksRef.current = 0;
|
|
3802
|
+
setThinking(true);
|
|
3803
|
+
setThinkingText("");
|
|
3804
|
+
setThoughtSummary(null);
|
|
3805
|
+
setStreamingText("");
|
|
3806
|
+
setIsStreaming(true);
|
|
3807
|
+
flushedRef.current = { streamed: "", reasoning: "" };
|
|
3808
|
+
const commitStreamed = (tight) => {
|
|
3809
|
+
if (streamedRef.current.length === 0) return;
|
|
3810
|
+
pushAssistantChunk(streamedRef.current, tight);
|
|
3811
|
+
streamedRef.current = "";
|
|
3812
|
+
flushedRef.current = { ...flushedRef.current, streamed: "" };
|
|
3813
|
+
setStreamingText("");
|
|
3814
|
+
};
|
|
3815
|
+
tickerStopRef.current = startTicker((tick) => {
|
|
3816
|
+
setSpinnerTick(tick);
|
|
3817
|
+
const split = splitCommittableLines(streamedRef.current, 8);
|
|
3818
|
+
if (split) {
|
|
3819
|
+
pushAssistantChunk(split.committed, true);
|
|
3820
|
+
streamedRef.current = split.rest;
|
|
3821
|
+
flushedRef.current = { ...flushedRef.current, streamed: "" };
|
|
3822
|
+
}
|
|
3823
|
+
const next = {
|
|
3824
|
+
streamed: streamedRef.current,
|
|
3825
|
+
reasoning: reasoningRef.current
|
|
3826
|
+
};
|
|
3827
|
+
if (nextFlush(flushedRef.current, next) !== null) {
|
|
3828
|
+
flushedRef.current = next;
|
|
3829
|
+
setStreamingText(next.streamed);
|
|
3830
|
+
setThinkingText(next.reasoning);
|
|
3831
|
+
}
|
|
3832
|
+
});
|
|
3833
|
+
try {
|
|
3834
|
+
for await (const event of backendRef.current.stream(resolved.input, controller.signal, {
|
|
3835
|
+
persistAs: input
|
|
3836
|
+
})) {
|
|
3837
|
+
if (event.type === "text-delta") {
|
|
3838
|
+
if (thinkingRef.current) {
|
|
3839
|
+
thinkingRef.current = false;
|
|
3840
|
+
setThinking(false);
|
|
3841
|
+
if (reasoningRef.current.length > 0) {
|
|
3842
|
+
setThoughtSummary(
|
|
3843
|
+
`thought: ${truncateTail(reasoningRef.current, THOUGHT_SUMMARY_LENGTH)}`
|
|
3844
|
+
);
|
|
3845
|
+
}
|
|
3846
|
+
}
|
|
3847
|
+
streamedRef.current += event.text;
|
|
3848
|
+
} else if (event.type === "reasoning") {
|
|
3849
|
+
reasoningRef.current += event.text;
|
|
3850
|
+
} else if (event.type === "tool-call") {
|
|
3851
|
+
commitStreamed(true);
|
|
3852
|
+
toolCardsRef.current.set(event.id, {
|
|
3853
|
+
id: event.id,
|
|
3854
|
+
name: event.name,
|
|
3855
|
+
argsSummary: summarizeArgs(event.args)
|
|
3856
|
+
});
|
|
3857
|
+
setActivity(`running ${event.name}: ${summarizeArgs(event.args, 60)}`);
|
|
3858
|
+
} else if (event.type === "tool-result") {
|
|
3859
|
+
const card = toolCardsRef.current.get(event.id) ?? {
|
|
3860
|
+
id: event.id,
|
|
3861
|
+
name: event.name,
|
|
3862
|
+
argsSummary: ""
|
|
3863
|
+
};
|
|
3864
|
+
toolCardsRef.current.delete(event.id);
|
|
3865
|
+
pushMessage(
|
|
3866
|
+
"tool",
|
|
3867
|
+
formatToolCard({
|
|
3868
|
+
...card,
|
|
3869
|
+
result: event.content,
|
|
3870
|
+
isError: event.isError ?? false
|
|
3871
|
+
})
|
|
3872
|
+
);
|
|
3873
|
+
setActivity(null);
|
|
3874
|
+
thinkingRef.current = true;
|
|
3875
|
+
reasoningRef.current = "";
|
|
3876
|
+
setThinking(true);
|
|
3877
|
+
setThinkingText("");
|
|
3878
|
+
} else if (event.type === "finish") {
|
|
3879
|
+
if (event.usage) {
|
|
3880
|
+
const usage = usageRef.current;
|
|
3881
|
+
usage.requests += 1;
|
|
3882
|
+
usage.promptTokens += event.usage.promptTokens;
|
|
3883
|
+
usage.completionTokens += event.usage.completionTokens;
|
|
3884
|
+
usage.totalTokens += event.usage.totalTokens;
|
|
3885
|
+
setUsageVersion((v) => v + 1);
|
|
3886
|
+
sessionStore?.addUsage(event.usage).catch(() => {
|
|
3887
|
+
});
|
|
3888
|
+
}
|
|
3889
|
+
} else if (event.type === "error") {
|
|
3890
|
+
pushMessage("system", `Error: ${formatStreamError(event.error)}`);
|
|
3891
|
+
}
|
|
3892
|
+
}
|
|
3893
|
+
} catch (error) {
|
|
3894
|
+
const aborted = controller.signal.aborted || error instanceof DOMException && error.name === "AbortError";
|
|
3895
|
+
if (!aborted) {
|
|
3896
|
+
pushMessage(
|
|
3897
|
+
"system",
|
|
3898
|
+
`Error: ${error instanceof Error ? formatStreamError(error) : String(error)}`
|
|
3899
|
+
);
|
|
3900
|
+
}
|
|
3901
|
+
} finally {
|
|
3902
|
+
tickerStopRef.current?.();
|
|
3903
|
+
tickerStopRef.current = null;
|
|
3904
|
+
abortRef.current = null;
|
|
3905
|
+
setIsStreaming(false);
|
|
3906
|
+
thinkingRef.current = false;
|
|
3907
|
+
reasoningRef.current = "";
|
|
3908
|
+
setThinking(false);
|
|
3909
|
+
setThinkingText("");
|
|
3910
|
+
setThoughtSummary(null);
|
|
3911
|
+
setActivity(null);
|
|
3912
|
+
const finalText = streamedRef.current;
|
|
3913
|
+
streamedRef.current = "";
|
|
3914
|
+
setStreamingText(null);
|
|
3915
|
+
const interrupted = controller.signal.aborted;
|
|
3916
|
+
if (finalText.length > 0) {
|
|
3917
|
+
pushAssistantChunk(`${finalText}${interrupted ? " [interrupted]" : ""}`, false);
|
|
3918
|
+
} else if (interrupted && turnChunksRef.current > 0) {
|
|
3919
|
+
pushAssistantChunk("[interrupted]", false);
|
|
3920
|
+
}
|
|
3921
|
+
for (const card of toolCardsRef.current.values()) {
|
|
3922
|
+
pushMessage("tool", formatToolCard(card));
|
|
3923
|
+
}
|
|
3924
|
+
toolCardsRef.current.clear();
|
|
3925
|
+
turnChunksRef.current = 0;
|
|
3926
|
+
const p = pendingRef.current;
|
|
3927
|
+
if (p) {
|
|
3928
|
+
setPendingPermission(null);
|
|
3929
|
+
p.resolve(false);
|
|
3930
|
+
}
|
|
3931
|
+
if (!interrupted && config.permissionMode === "plan") {
|
|
3932
|
+
setPlanApproval(true);
|
|
3933
|
+
}
|
|
3934
|
+
}
|
|
3935
|
+
},
|
|
3936
|
+
[
|
|
3937
|
+
pushMessage,
|
|
3938
|
+
pushAssistantChunk,
|
|
3939
|
+
setPendingPermission,
|
|
3940
|
+
setPlanApproval,
|
|
3941
|
+
sessionStore,
|
|
3942
|
+
cwd,
|
|
3943
|
+
config
|
|
3944
|
+
]
|
|
3945
|
+
);
|
|
3946
|
+
const handlePlanDecision = useCallback(
|
|
3947
|
+
(decision) => {
|
|
3948
|
+
setPlanApproval(false);
|
|
3949
|
+
if (decision === "no") return;
|
|
3950
|
+
const restore = prevModeRef.current ?? "ask";
|
|
3951
|
+
applySessionMode(restore);
|
|
3952
|
+
pushMessage("system", `Plan approved \u2014 permission mode restored to ${restore}.`);
|
|
3953
|
+
void runStream("The plan above is approved. Proceed with the implementation.");
|
|
3954
|
+
},
|
|
3955
|
+
[applySessionMode, pushMessage, runStream, setPlanApproval]
|
|
3956
|
+
);
|
|
2403
3957
|
const registry = useMemo(() => {
|
|
2404
3958
|
const ctx = {
|
|
2405
3959
|
addSystemMessage: (text) => pushMessage("system", text),
|
|
2406
3960
|
clearMessages: () => {
|
|
2407
|
-
|
|
2408
|
-
setEpoch((e) => e + 1);
|
|
3961
|
+
redrawMessages([]);
|
|
2409
3962
|
},
|
|
2410
|
-
exit: () =>
|
|
3963
|
+
exit: () => handleExit(),
|
|
2411
3964
|
listModels: () => {
|
|
2412
3965
|
const models = listModels(config);
|
|
2413
3966
|
if (models.length === 0) return "No models configured.";
|
|
@@ -2428,7 +3981,12 @@ ${lines.join("\n")}`;
|
|
|
2428
3981
|
await store.load(cwd);
|
|
2429
3982
|
return formatTodos(store.list());
|
|
2430
3983
|
},
|
|
2431
|
-
|
|
3984
|
+
listTasks: () => formatTaskList(defaultTaskManager.list()),
|
|
3985
|
+
showUsage: () => {
|
|
3986
|
+
const modelConfig = config.models.find((m) => m.name === modelNameRef.current);
|
|
3987
|
+
return `${formatUsage(usageRef.current)}
|
|
3988
|
+
${estimateCost(usageRef.current, modelNameRef.current, modelConfig)}`;
|
|
3989
|
+
},
|
|
2432
3990
|
compactContext: async () => {
|
|
2433
3991
|
let summaryModel = null;
|
|
2434
3992
|
try {
|
|
@@ -2445,13 +4003,79 @@ ${lines.join("\n")}`;
|
|
|
2445
4003
|
if (result.compacted && result.messages) {
|
|
2446
4004
|
const display = buildDisplayMessages(result.messages);
|
|
2447
4005
|
nextIdRef.current = display.length;
|
|
2448
|
-
|
|
2449
|
-
setEpoch((e) => e + 1);
|
|
4006
|
+
applyMessages(display);
|
|
2450
4007
|
}
|
|
2451
4008
|
return result.message;
|
|
2452
4009
|
},
|
|
2453
4010
|
exportSession: (arg) => exportSession({ backend: backendRef.current, sessionStore, cwd, arg }),
|
|
2454
|
-
undo: () =>
|
|
4011
|
+
undo: async () => {
|
|
4012
|
+
const current = backendRef.current;
|
|
4013
|
+
if (!(current instanceof AgentLoop)) {
|
|
4014
|
+
return "Nothing to undo.";
|
|
4015
|
+
}
|
|
4016
|
+
const { removed, turn } = await current.retractLastTurn();
|
|
4017
|
+
if (removed === 0) {
|
|
4018
|
+
return "Nothing to undo (no conversation turn to retract).";
|
|
4019
|
+
}
|
|
4020
|
+
const reverted = turn !== void 0 ? await undoTurnSnapshots(turn) : [];
|
|
4021
|
+
const visible = messagesRef.current;
|
|
4022
|
+
let cut = -1;
|
|
4023
|
+
for (let i = visible.length - 1; i >= 0; i--) {
|
|
4024
|
+
if (visible[i]?.role === "user") {
|
|
4025
|
+
cut = i;
|
|
4026
|
+
break;
|
|
4027
|
+
}
|
|
4028
|
+
}
|
|
4029
|
+
redrawMessages(cut >= 0 ? visible.slice(0, cut) : visible);
|
|
4030
|
+
return [...reverted, `Retracted the last conversation turn (${removed} messages).`].join(
|
|
4031
|
+
"\n"
|
|
4032
|
+
);
|
|
4033
|
+
},
|
|
4034
|
+
permissionMode: async (args) => {
|
|
4035
|
+
const current = config.permissionMode;
|
|
4036
|
+
if (!args) {
|
|
4037
|
+
const mark = (m) => m === current ? "*" : " ";
|
|
4038
|
+
return [
|
|
4039
|
+
"Permission modes (* = current):",
|
|
4040
|
+
`${mark("ask")} ask \u2014 reads allowed; writes/exec ask for confirmation`,
|
|
4041
|
+
`${mark("auto")} auto \u2014 everything allowed except hard-denied dangerous commands/paths`,
|
|
4042
|
+
`${mark("readonly")} readonly \u2014 read-only; all writes/exec denied`,
|
|
4043
|
+
`${mark("yolo")} yolo \u2014 allow everything, never ask (disables ALL safety checks)`
|
|
4044
|
+
].join("\n");
|
|
4045
|
+
}
|
|
4046
|
+
const mode = args.toLowerCase();
|
|
4047
|
+
if (!["ask", "auto", "readonly", "yolo"].includes(mode)) {
|
|
4048
|
+
return `Unknown permission mode: ${args} (expected ask | auto | readonly | yolo)`;
|
|
4049
|
+
}
|
|
4050
|
+
config.permissionMode = mode;
|
|
4051
|
+
prevModeRef.current = null;
|
|
4052
|
+
setPermissionModeState(mode);
|
|
4053
|
+
await savePermissionMode(mode);
|
|
4054
|
+
return mode === "yolo" ? "Permission mode set to yolo \u2014 ALL safety checks disabled, tools run without asking. Saved to config." : `Permission mode set to ${mode}. Saved to config.`;
|
|
4055
|
+
},
|
|
4056
|
+
planMode: async () => {
|
|
4057
|
+
if (config.permissionMode === "plan") {
|
|
4058
|
+
const restore = prevModeRef.current ?? "ask";
|
|
4059
|
+
applySessionMode(restore);
|
|
4060
|
+
return `Exited plan mode \u2014 permission mode back to ${restore} (this session only).`;
|
|
4061
|
+
}
|
|
4062
|
+
applySessionMode("plan");
|
|
4063
|
+
return "Entered plan mode: the agent researches with read-only tools and presents a plan for approval before anything is executed. /plan again to exit.";
|
|
4064
|
+
},
|
|
4065
|
+
initProject: async (args) => {
|
|
4066
|
+
let model2 = null;
|
|
4067
|
+
try {
|
|
4068
|
+
model2 = createModel(config, modelNameRef.current);
|
|
4069
|
+
} catch {
|
|
4070
|
+
model2 = null;
|
|
4071
|
+
}
|
|
4072
|
+
const result = await initProject({ cwd, args, model: model2 });
|
|
4073
|
+
return result.message;
|
|
4074
|
+
},
|
|
4075
|
+
runDoctor: async () => formatDoctorReport(await runDoctor({ cwd, config })),
|
|
4076
|
+
submitPrompt: async (text) => {
|
|
4077
|
+
await runStream(text);
|
|
4078
|
+
},
|
|
2455
4079
|
describeConfig: () => [
|
|
2456
4080
|
`defaultModel: ${config.defaultModel || "(none)"}`,
|
|
2457
4081
|
`permissionMode: ${config.permissionMode}`,
|
|
@@ -2459,103 +4083,69 @@ ${lines.join("\n")}`;
|
|
|
2459
4083
|
`models (${config.models.length}): ${config.models.map((m) => m.name).join(", ") || "(none)"}`,
|
|
2460
4084
|
`maxSteps: ${config.maxSteps}`,
|
|
2461
4085
|
`contextMaxTokens: ${config.contextMaxTokens}`,
|
|
4086
|
+
`streamIdleTimeoutSec: ${config.streamIdleTimeoutSec}`,
|
|
2462
4087
|
`permissions.allow (${config.permissions.allow.length}): ${config.permissions.allow.join(", ") || "(none)"}`
|
|
2463
4088
|
].join("\n")
|
|
2464
4089
|
};
|
|
2465
4090
|
const reg = new CommandRegistry();
|
|
2466
4091
|
registerBuiltinCommands(reg);
|
|
4092
|
+
registerCustomCommands(reg, cwd);
|
|
2467
4093
|
return Object.assign(reg, { ctx });
|
|
2468
|
-
}, [
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
4094
|
+
}, [
|
|
4095
|
+
pushMessage,
|
|
4096
|
+
handleExit,
|
|
4097
|
+
config,
|
|
4098
|
+
cwd,
|
|
4099
|
+
switchModel,
|
|
4100
|
+
resume,
|
|
4101
|
+
sessionStore,
|
|
4102
|
+
runStream,
|
|
4103
|
+
applyMessages,
|
|
4104
|
+
redrawMessages,
|
|
4105
|
+
applySessionMode
|
|
4106
|
+
]);
|
|
4107
|
+
const runShellBang = useCallback(
|
|
4108
|
+
async (raw) => {
|
|
4109
|
+
pushMessage("user", `!${raw.trim()}`);
|
|
2480
4110
|
const controller = new AbortController();
|
|
2481
4111
|
abortRef.current = controller;
|
|
2482
|
-
streamedRef.current = "";
|
|
2483
|
-
setStreamingText("");
|
|
2484
|
-
setIsStreaming(true);
|
|
2485
|
-
flushTimerRef.current = setInterval(() => {
|
|
2486
|
-
setStreamingText(streamedRef.current);
|
|
2487
|
-
}, FLUSH_INTERVAL_MS);
|
|
2488
4112
|
try {
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
})) {
|
|
2492
|
-
if (event.type === "text-delta") {
|
|
2493
|
-
streamedRef.current += event.text;
|
|
2494
|
-
} else if (event.type === "tool-call") {
|
|
2495
|
-
toolCardsRef.current.set(event.id, {
|
|
2496
|
-
id: event.id,
|
|
2497
|
-
name: event.name,
|
|
2498
|
-
argsSummary: summarizeArgs(event.args)
|
|
2499
|
-
});
|
|
2500
|
-
setCardsVersion((v) => v + 1);
|
|
2501
|
-
} else if (event.type === "tool-result") {
|
|
2502
|
-
const card = toolCardsRef.current.get(event.id);
|
|
2503
|
-
if (card) {
|
|
2504
|
-
card.result = event.content;
|
|
2505
|
-
card.isError = event.isError ?? false;
|
|
2506
|
-
setCardsVersion((v) => v + 1);
|
|
2507
|
-
}
|
|
2508
|
-
} else if (event.type === "finish") {
|
|
2509
|
-
if (event.usage) {
|
|
2510
|
-
const usage = usageRef.current;
|
|
2511
|
-
usage.requests += 1;
|
|
2512
|
-
usage.promptTokens += event.usage.promptTokens;
|
|
2513
|
-
usage.completionTokens += event.usage.completionTokens;
|
|
2514
|
-
usage.totalTokens += event.usage.totalTokens;
|
|
2515
|
-
setUsageVersion((v) => v + 1);
|
|
2516
|
-
sessionStore?.addUsage(event.usage).catch(() => {
|
|
2517
|
-
});
|
|
2518
|
-
}
|
|
2519
|
-
} else if (event.type === "error") {
|
|
2520
|
-
pushMessage("system", `Error: ${event.error.message}`);
|
|
2521
|
-
}
|
|
2522
|
-
}
|
|
2523
|
-
} catch (error) {
|
|
2524
|
-
if (!(error instanceof DOMException && error.name === "AbortError")) {
|
|
2525
|
-
pushMessage("system", `Error: ${error instanceof Error ? error.message : String(error)}`);
|
|
2526
|
-
}
|
|
2527
|
-
} finally {
|
|
2528
|
-
if (flushTimerRef.current !== null) {
|
|
2529
|
-
clearInterval(flushTimerRef.current);
|
|
2530
|
-
flushTimerRef.current = null;
|
|
2531
|
-
}
|
|
2532
|
-
abortRef.current = null;
|
|
2533
|
-
setIsStreaming(false);
|
|
2534
|
-
const finalText = streamedRef.current;
|
|
2535
|
-
streamedRef.current = "";
|
|
2536
|
-
setStreamingText(null);
|
|
2537
|
-
if (finalText.length > 0) {
|
|
4113
|
+
const outcome = await executeShellBang(raw, cwd, controller.signal);
|
|
4114
|
+
if (!outcome.ok) {
|
|
2538
4115
|
pushMessage(
|
|
2539
|
-
"
|
|
2540
|
-
|
|
4116
|
+
"system",
|
|
4117
|
+
outcome.reason === "dangerous" ? `Refused to run dangerous command: ${raw.trim()}` : "Usage: !<command>"
|
|
2541
4118
|
);
|
|
4119
|
+
return;
|
|
2542
4120
|
}
|
|
2543
|
-
|
|
2544
|
-
|
|
4121
|
+
pushMessage(
|
|
4122
|
+
"tool",
|
|
4123
|
+
formatToolCard({
|
|
4124
|
+
id: `bang-${nextIdRef.current}`,
|
|
4125
|
+
name: "bash",
|
|
4126
|
+
argsSummary: outcome.command,
|
|
4127
|
+
result: outcome.output,
|
|
4128
|
+
isError: outcome.isError
|
|
4129
|
+
})
|
|
4130
|
+
);
|
|
4131
|
+
const current = backendRef.current;
|
|
4132
|
+
if (current instanceof AgentLoop) {
|
|
4133
|
+
await current.appendContextMessage(outcome.contextMessage);
|
|
2545
4134
|
}
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
if (p) {
|
|
2550
|
-
setPendingPermission(null);
|
|
2551
|
-
p.resolve(false);
|
|
4135
|
+
} finally {
|
|
4136
|
+
if (abortRef.current === controller) {
|
|
4137
|
+
abortRef.current = null;
|
|
2552
4138
|
}
|
|
2553
4139
|
}
|
|
2554
4140
|
},
|
|
2555
|
-
[
|
|
4141
|
+
[cwd, pushMessage]
|
|
2556
4142
|
);
|
|
2557
4143
|
const handleSubmit = useCallback(
|
|
2558
4144
|
(text) => {
|
|
4145
|
+
if (text.startsWith("!")) {
|
|
4146
|
+
void runShellBang(text.slice(1));
|
|
4147
|
+
return;
|
|
4148
|
+
}
|
|
2559
4149
|
if (text.startsWith("/")) {
|
|
2560
4150
|
const parsed = parseSlashCommand(text);
|
|
2561
4151
|
if (!parsed) return;
|
|
@@ -2569,41 +4159,59 @@ ${lines.join("\n")}`;
|
|
|
2569
4159
|
}
|
|
2570
4160
|
void runStream(text);
|
|
2571
4161
|
},
|
|
2572
|
-
[registry, pushMessage, runStream]
|
|
4162
|
+
[registry, pushMessage, runStream, runShellBang]
|
|
2573
4163
|
);
|
|
2574
4164
|
const commandHints = useMemo(
|
|
2575
4165
|
() => registry.list().map((cmd) => ({ name: cmd.name, description: cmd.description })),
|
|
2576
4166
|
[registry]
|
|
2577
4167
|
);
|
|
2578
|
-
|
|
2579
|
-
return /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", children: [
|
|
4168
|
+
return /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", children: [
|
|
2580
4169
|
/* @__PURE__ */ jsx6(MessageList, { messages }, `messages-${epoch}`),
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
4170
|
+
(thinking || activity !== null) && /* @__PURE__ */ jsx6(
|
|
4171
|
+
ThinkingIndicator,
|
|
4172
|
+
{
|
|
4173
|
+
reasoning: thinkingText,
|
|
4174
|
+
frame: spinnerTick,
|
|
4175
|
+
activity: activity ?? void 0
|
|
4176
|
+
}
|
|
4177
|
+
),
|
|
4178
|
+
!thinking && activity === null && thoughtSummary !== null && /* @__PURE__ */ jsx6(Text8, { dimColor: true, children: thoughtSummary }),
|
|
4179
|
+
streamingText !== null && (streamingText !== "" || turnChunksRef.current === 0) && /* @__PURE__ */ jsx6(StreamingMessage, { text: streamingText, continuation: turnChunksRef.current > 0 }),
|
|
4180
|
+
pending && /* @__PURE__ */ jsx6(
|
|
4181
|
+
PermissionPrompt,
|
|
4182
|
+
{
|
|
4183
|
+
request: pending.request,
|
|
4184
|
+
preview: pending.preview,
|
|
4185
|
+
onDecision: handleDecision
|
|
4186
|
+
}
|
|
4187
|
+
),
|
|
4188
|
+
planApproval && /* @__PURE__ */ jsx6(ExitPlanPrompt, { onDecision: handlePlanDecision }),
|
|
2584
4189
|
/* @__PURE__ */ jsx6(
|
|
2585
4190
|
InputBox,
|
|
2586
4191
|
{
|
|
2587
4192
|
isStreaming,
|
|
2588
|
-
disabled: pending !== null,
|
|
4193
|
+
disabled: pending !== null || planApproval,
|
|
2589
4194
|
commands: commandHints,
|
|
2590
4195
|
onSubmit: handleSubmit,
|
|
2591
4196
|
onInterrupt: interrupt,
|
|
2592
|
-
onExit:
|
|
4197
|
+
onExit: handleExit
|
|
2593
4198
|
}
|
|
2594
4199
|
),
|
|
2595
4200
|
/* @__PURE__ */ jsx6(
|
|
2596
4201
|
StatusBar,
|
|
2597
4202
|
{
|
|
2598
4203
|
model: modelName,
|
|
2599
|
-
permissionMode,
|
|
2600
|
-
tokens: usageRef.current.totalTokens
|
|
2601
|
-
|
|
2602
|
-
|
|
4204
|
+
permissionMode: permissionModeState,
|
|
4205
|
+
tokens: usageRef.current.totalTokens,
|
|
4206
|
+
backgroundTasks: bgLabels
|
|
4207
|
+
}
|
|
2603
4208
|
)
|
|
2604
4209
|
] });
|
|
2605
4210
|
}
|
|
2606
4211
|
function renderRepl(backend, opts) {
|
|
4212
|
+
process.on("exit", () => {
|
|
4213
|
+
defaultTaskManager.cleanup();
|
|
4214
|
+
});
|
|
2607
4215
|
return render(
|
|
2608
4216
|
/* @__PURE__ */ jsx6(
|
|
2609
4217
|
Repl,
|
|
@@ -2622,36 +4230,42 @@ function renderRepl(backend, opts) {
|
|
|
2622
4230
|
}
|
|
2623
4231
|
|
|
2624
4232
|
// src/config/loader.ts
|
|
2625
|
-
import
|
|
4233
|
+
import fs7 from "fs";
|
|
2626
4234
|
import { parse as parse2 } from "smol-toml";
|
|
2627
4235
|
|
|
2628
4236
|
// src/config/schema.ts
|
|
2629
|
-
import { z as
|
|
2630
|
-
var ProviderConfigSchema =
|
|
2631
|
-
name:
|
|
2632
|
-
protocol:
|
|
2633
|
-
baseURL:
|
|
2634
|
-
apiKeyEnv:
|
|
2635
|
-
apiKey:
|
|
2636
|
-
headers:
|
|
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()
|
|
2637
4245
|
});
|
|
2638
|
-
var ModelConfigSchema =
|
|
2639
|
-
name:
|
|
2640
|
-
provider:
|
|
2641
|
-
model:
|
|
2642
|
-
maxTokens:
|
|
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()
|
|
2643
4253
|
});
|
|
2644
|
-
var PermissionsConfigSchema =
|
|
2645
|
-
allow:
|
|
4254
|
+
var PermissionsConfigSchema = z11.object({
|
|
4255
|
+
allow: z11.array(z11.string()).default([])
|
|
2646
4256
|
});
|
|
2647
|
-
var ConfigSchema =
|
|
2648
|
-
defaultModel:
|
|
2649
|
-
permissionMode:
|
|
2650
|
-
providers:
|
|
2651
|
-
models:
|
|
2652
|
-
maxSteps:
|
|
2653
|
-
contextMaxTokens:
|
|
2654
|
-
|
|
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"),
|
|
2655
4269
|
permissions: PermissionsConfigSchema.default({ allow: [] })
|
|
2656
4270
|
});
|
|
2657
4271
|
|
|
@@ -2682,8 +4296,8 @@ function applyOverrides(config, overrides) {
|
|
|
2682
4296
|
return merged;
|
|
2683
4297
|
}
|
|
2684
4298
|
function readTomlFileSync(filePath) {
|
|
2685
|
-
if (!
|
|
2686
|
-
const content =
|
|
4299
|
+
if (!fs7.existsSync(filePath)) return {};
|
|
4300
|
+
const content = fs7.readFileSync(filePath, "utf8");
|
|
2687
4301
|
return parseTomlFile(content, filePath);
|
|
2688
4302
|
}
|
|
2689
4303
|
function loadConfigSync(cwd, overrides) {
|
|
@@ -2710,7 +4324,7 @@ async function createLoop(config, modelName, cwd, sessionStore) {
|
|
|
2710
4324
|
sessionStore
|
|
2711
4325
|
});
|
|
2712
4326
|
}
|
|
2713
|
-
async function printMode(loop, prompt, cwd) {
|
|
4327
|
+
async function printMode(loop, prompt, cwd, json) {
|
|
2714
4328
|
const controller = new AbortController();
|
|
2715
4329
|
process.on("SIGINT", () => controller.abort());
|
|
2716
4330
|
const resolved = await resolveMentions(prompt, cwd);
|
|
@@ -2722,58 +4336,83 @@ async function printMode(loop, prompt, cwd) {
|
|
|
2722
4336
|
process.stderr.write(`[skipped] @${skip.path}: ${skip.reason}
|
|
2723
4337
|
`);
|
|
2724
4338
|
}
|
|
2725
|
-
|
|
2726
|
-
let promptTokens = 0;
|
|
2727
|
-
let completionTokens = 0;
|
|
2728
|
-
let totalTokens = 0;
|
|
4339
|
+
const usage = new UsageTracker();
|
|
2729
4340
|
let exitCode = 0;
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
4341
|
+
try {
|
|
4342
|
+
for await (const event of loop.stream(resolved.input, controller.signal, {
|
|
4343
|
+
persistAs: prompt
|
|
4344
|
+
})) {
|
|
4345
|
+
if (json) {
|
|
4346
|
+
const line = eventToJsonLine(event);
|
|
4347
|
+
if (line) process.stdout.write(`${line}
|
|
4348
|
+
`);
|
|
4349
|
+
}
|
|
4350
|
+
switch (event.type) {
|
|
4351
|
+
case "text-delta":
|
|
4352
|
+
if (!json) process.stdout.write(event.text);
|
|
4353
|
+
break;
|
|
4354
|
+
case "reasoning":
|
|
4355
|
+
break;
|
|
4356
|
+
case "tool-call":
|
|
4357
|
+
if (!json) process.stderr.write(`
|
|
2739
4358
|
[tool] ${event.name} ${JSON.stringify(event.args)}
|
|
2740
4359
|
`);
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
|
|
2744
|
-
|
|
4360
|
+
break;
|
|
4361
|
+
case "tool-result": {
|
|
4362
|
+
if (!json) {
|
|
4363
|
+
const preview = event.content.length > 500 ? `${event.content.slice(0, 500)}... (truncated)` : event.content;
|
|
4364
|
+
process.stderr.write(`[result] ${event.isError ? "ERROR: " : ""}${preview}
|
|
2745
4365
|
`);
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
case "finish":
|
|
2749
|
-
if (event.usage) {
|
|
2750
|
-
requests += 1;
|
|
2751
|
-
promptTokens += event.usage.promptTokens;
|
|
2752
|
-
completionTokens += event.usage.completionTokens;
|
|
2753
|
-
totalTokens += event.usage.totalTokens;
|
|
4366
|
+
}
|
|
4367
|
+
break;
|
|
2754
4368
|
}
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
4369
|
+
case "finish":
|
|
4370
|
+
usage.add(event.usage);
|
|
4371
|
+
break;
|
|
4372
|
+
case "error":
|
|
4373
|
+
if (!json) process.stderr.write(`
|
|
4374
|
+
[error] ${formatStreamError(event.error)}
|
|
2759
4375
|
`);
|
|
2760
|
-
|
|
2761
|
-
|
|
4376
|
+
exitCode = 1;
|
|
4377
|
+
break;
|
|
4378
|
+
}
|
|
4379
|
+
if (exitCode !== 0) break;
|
|
4380
|
+
}
|
|
4381
|
+
} catch (error) {
|
|
4382
|
+
if (!(error instanceof DOMException && error.name === "AbortError")) {
|
|
4383
|
+
const message = error instanceof Error ? formatStreamError(error) : String(error);
|
|
4384
|
+
process.stderr.write(`
|
|
4385
|
+
[error] ${message}
|
|
4386
|
+
`);
|
|
4387
|
+
exitCode = 1;
|
|
2762
4388
|
}
|
|
2763
|
-
if (exitCode !== 0) break;
|
|
2764
4389
|
}
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
4390
|
+
const usageLine = usage.toJsonLine();
|
|
4391
|
+
if (usageLine) {
|
|
4392
|
+
if (json) {
|
|
4393
|
+
process.stdout.write(`${usageLine}
|
|
4394
|
+
`);
|
|
4395
|
+
} else {
|
|
4396
|
+
const t = usage.totals;
|
|
4397
|
+
process.stderr.write(
|
|
4398
|
+
`[usage] ${t.requests} requests, ${t.promptTokens} prompt + ${t.completionTokens} completion = ${t.totalTokens} tokens
|
|
2768
4399
|
`
|
|
2769
|
-
|
|
4400
|
+
);
|
|
4401
|
+
}
|
|
2770
4402
|
}
|
|
2771
|
-
process.stdout.write("\n");
|
|
4403
|
+
if (!json) process.stdout.write("\n");
|
|
4404
|
+
defaultTaskManager.cleanup();
|
|
2772
4405
|
return exitCode;
|
|
2773
4406
|
}
|
|
2774
4407
|
var program = new Command();
|
|
2775
|
-
program.name("star").description("Star CLI \u2014 an AI agent command-line interface").version(
|
|
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("-r, --resume <sessionId>", "resume a previous session").action(async (opts) => {
|
|
2776
4409
|
const cwd = process.cwd();
|
|
4410
|
+
if (opts.json && opts.print === void 0) {
|
|
4411
|
+
console.error(
|
|
4412
|
+
'--json requires print mode: use star -p "..." --json (the interactive REPL does not support JSON output).'
|
|
4413
|
+
);
|
|
4414
|
+
process.exit(1);
|
|
4415
|
+
}
|
|
2777
4416
|
let config;
|
|
2778
4417
|
try {
|
|
2779
4418
|
config = loadConfigSync(cwd, { model: opts.model, permissionMode: opts.permissionMode });
|
|
@@ -2811,7 +4450,7 @@ program.name("star").description("Star CLI \u2014 an AI agent command-line inter
|
|
|
2811
4450
|
await loop.loadMessages(resumed.messages);
|
|
2812
4451
|
}
|
|
2813
4452
|
if (opts.print) {
|
|
2814
|
-
process.exitCode = await printMode(loop, opts.print, cwd);
|
|
4453
|
+
process.exitCode = await printMode(loop, opts.print, cwd, Boolean(opts.json));
|
|
2815
4454
|
return;
|
|
2816
4455
|
}
|
|
2817
4456
|
renderRepl(loop, {
|