@cryer/star-cli 0.1.0 → 0.1.2
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 +80 -11
- package/dist/main.js +2899 -664
- 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,8 +124,89 @@ 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";
|
|
174
|
+
|
|
175
|
+
// src/permissions/allow.ts
|
|
176
|
+
function parseAllowRule(rule) {
|
|
177
|
+
const match = /^([\w-]+)(?:\((.*)\))?$/.exec(rule.trim());
|
|
178
|
+
if (!match) return null;
|
|
179
|
+
return { toolName: match[1], pattern: match[2] };
|
|
180
|
+
}
|
|
181
|
+
function globMatch(pattern, value) {
|
|
182
|
+
const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
|
|
183
|
+
return new RegExp(`^${escaped}$`).test(value);
|
|
184
|
+
}
|
|
185
|
+
function requestTarget(req) {
|
|
186
|
+
if (typeof req.args !== "object" || req.args === null) return void 0;
|
|
187
|
+
const args = req.args;
|
|
188
|
+
const key = req.toolName === "bash" ? "command" : "path";
|
|
189
|
+
const value = args[key];
|
|
190
|
+
return typeof value === "string" ? value : void 0;
|
|
191
|
+
}
|
|
192
|
+
function matchAllowRule(rule, req) {
|
|
193
|
+
if (rule.toolName !== req.toolName) return false;
|
|
194
|
+
if (rule.pattern === void 0) return true;
|
|
195
|
+
const target = requestTarget(req);
|
|
196
|
+
return target !== void 0 && globMatch(rule.pattern, target);
|
|
197
|
+
}
|
|
198
|
+
function isAllowedByRules(rules, req) {
|
|
199
|
+
return rules.some((raw) => {
|
|
200
|
+
const rule = parseAllowRule(raw);
|
|
201
|
+
return rule !== null && matchAllowRule(rule, req);
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
function buildAllowRule(req) {
|
|
205
|
+
const target = requestTarget(req);
|
|
206
|
+
return target !== void 0 ? `${req.toolName}(${target})` : req.toolName;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// src/permissions/gate.ts
|
|
129
210
|
var FILE_TOOLS = /* @__PURE__ */ new Set(["write_file", "edit_file", "read_file"]);
|
|
130
211
|
var DANGEROUS_COMMAND_PATTERNS = [
|
|
131
212
|
/rm\s+-rf\s+\/(?:\s|$|;|&)/i,
|
|
@@ -138,6 +219,9 @@ var DANGEROUS_COMMAND_PATTERNS = [
|
|
|
138
219
|
/\breboot\b/i,
|
|
139
220
|
/\bpoweroff\b/i
|
|
140
221
|
];
|
|
222
|
+
function isDangerousCommand(command) {
|
|
223
|
+
return DANGEROUS_COMMAND_PATTERNS.some((pattern) => pattern.test(command));
|
|
224
|
+
}
|
|
141
225
|
function getStringArg(args, key) {
|
|
142
226
|
if (typeof args === "object" && args !== null) {
|
|
143
227
|
const value = args[key];
|
|
@@ -163,10 +247,13 @@ function isSensitivePath(p) {
|
|
|
163
247
|
}
|
|
164
248
|
return base === ".env" || base.startsWith(".env.") || base === "id_rsa" || base.endsWith(".pem");
|
|
165
249
|
}
|
|
166
|
-
function checkPermission(mode, req, ctx) {
|
|
250
|
+
function checkPermission(mode, req, ctx, allowRules = []) {
|
|
251
|
+
if (mode === "yolo") {
|
|
252
|
+
return "allow";
|
|
253
|
+
}
|
|
167
254
|
const command = getStringArg(req.args, "command");
|
|
168
255
|
const filePath = getStringArg(req.args, "path");
|
|
169
|
-
if (req.toolName === "bash" && command !== void 0 &&
|
|
256
|
+
if (req.toolName === "bash" && command !== void 0 && isDangerousCommand(command)) {
|
|
170
257
|
return "deny";
|
|
171
258
|
}
|
|
172
259
|
if (FILE_TOOLS.has(req.toolName) && filePath !== void 0) {
|
|
@@ -186,13 +273,78 @@ function checkPermission(mode, req, ctx) {
|
|
|
186
273
|
if (mode === "readonly") {
|
|
187
274
|
return req.level === "read" ? "allow" : "deny";
|
|
188
275
|
}
|
|
276
|
+
if (mode === "plan") {
|
|
277
|
+
return req.level === "read" ? "allow" : "deny";
|
|
278
|
+
}
|
|
279
|
+
if (isAllowedByRules(allowRules, req)) {
|
|
280
|
+
return "allow";
|
|
281
|
+
}
|
|
189
282
|
return req.level === "read" ? "allow" : "ask";
|
|
190
283
|
}
|
|
191
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
|
+
|
|
192
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.";
|
|
193
340
|
var AgentLoop = class {
|
|
194
341
|
messages = [];
|
|
195
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 = [];
|
|
196
348
|
confirmHandler;
|
|
197
349
|
constructor(opts) {
|
|
198
350
|
this.opts = opts;
|
|
@@ -204,15 +356,44 @@ var AgentLoop = class {
|
|
|
204
356
|
return this.messages;
|
|
205
357
|
}
|
|
206
358
|
async loadMessages(messages) {
|
|
207
|
-
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);
|
|
208
385
|
}
|
|
209
386
|
async persist(message) {
|
|
210
387
|
await this.opts.sessionStore?.append(message);
|
|
211
388
|
}
|
|
212
|
-
async *stream(input, signal) {
|
|
389
|
+
async *stream(input, signal, opts) {
|
|
390
|
+
this.syncSystemMessage();
|
|
213
391
|
const userMessage = { role: "user", content: input };
|
|
214
392
|
this.messages.push(userMessage);
|
|
215
|
-
|
|
393
|
+
this.turnMarkers.push({ seq: beginTurn(), userIndex: this.messages.length - 1 });
|
|
394
|
+
await this.persist(
|
|
395
|
+
opts?.persistAs !== void 0 ? { role: "user", content: opts.persistAs } : userMessage
|
|
396
|
+
);
|
|
216
397
|
const { config, registry, cwd } = this.opts;
|
|
217
398
|
const aiTools = this.buildAiTools();
|
|
218
399
|
for (let step = 0; step < config.maxSteps; step++) {
|
|
@@ -228,6 +409,8 @@ var AgentLoop = class {
|
|
|
228
409
|
if (event.type === "text-delta") {
|
|
229
410
|
text += event.text;
|
|
230
411
|
yield event;
|
|
412
|
+
} else if (event.type === "reasoning") {
|
|
413
|
+
yield event;
|
|
231
414
|
} else if (event.type === "tool-call") {
|
|
232
415
|
toolCalls.push({ id: event.id, name: event.name, args: event.args });
|
|
233
416
|
yield event;
|
|
@@ -239,6 +422,7 @@ var AgentLoop = class {
|
|
|
239
422
|
}
|
|
240
423
|
}
|
|
241
424
|
} catch (error) {
|
|
425
|
+
if (signal.aborted) return;
|
|
242
426
|
yield { type: "error", error: error instanceof Error ? error : new Error(String(error)) };
|
|
243
427
|
return;
|
|
244
428
|
}
|
|
@@ -258,35 +442,77 @@ var AgentLoop = class {
|
|
|
258
442
|
this.messages.push(assistantMessage);
|
|
259
443
|
await this.persist(assistantMessage);
|
|
260
444
|
if (toolCalls.length === 0) return;
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
const
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
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
|
+
}
|
|
283
492
|
}
|
|
493
|
+
if (signal.aborted) return;
|
|
284
494
|
}
|
|
285
495
|
yield {
|
|
286
496
|
type: "error",
|
|
287
497
|
error: new Error(`Max steps (${config.maxSteps}) reached, stopping.`)
|
|
288
498
|
};
|
|
289
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
|
+
}
|
|
290
516
|
async applyCompactionSummary(compacted) {
|
|
291
517
|
const { config, model } = this.opts;
|
|
292
518
|
if (config.contextCompaction !== "summary" || !model) {
|
|
@@ -308,17 +534,20 @@ ${summary}`
|
|
|
308
534
|
}
|
|
309
535
|
}
|
|
310
536
|
async *streamOnce(aiTools, signal) {
|
|
311
|
-
const { streamChat } = await import("./stream-
|
|
537
|
+
const { streamChat } = await import("./stream-Y55NRF2S.js");
|
|
312
538
|
yield* streamChat({
|
|
313
539
|
model: this.opts.model,
|
|
314
540
|
messages: this.messages,
|
|
315
541
|
tools: aiTools,
|
|
316
|
-
abortSignal: signal
|
|
542
|
+
abortSignal: signal,
|
|
543
|
+
idleTimeoutMs: this.opts.config.streamIdleTimeoutSec * 1e3
|
|
317
544
|
});
|
|
318
545
|
}
|
|
319
546
|
buildAiTools() {
|
|
547
|
+
const plan = this.opts.config.permissionMode === "plan";
|
|
320
548
|
const tools = {};
|
|
321
549
|
for (const t of this.opts.registry.list()) {
|
|
550
|
+
if (plan && t.permission !== "read") continue;
|
|
322
551
|
tools[t.name] = aiTool({
|
|
323
552
|
description: t.description,
|
|
324
553
|
parameters: t.parameters
|
|
@@ -335,17 +564,23 @@ ${summary}`
|
|
|
335
564
|
const decision = checkPermission(
|
|
336
565
|
config.permissionMode,
|
|
337
566
|
{ toolName: call.name, args: call.args, level: tool.permission },
|
|
338
|
-
{ cwd }
|
|
567
|
+
{ cwd },
|
|
568
|
+
config.permissions.allow
|
|
339
569
|
);
|
|
340
570
|
if (decision === "deny") {
|
|
341
571
|
return { content: `Permission denied for tool "${call.name}".`, isError: true };
|
|
342
572
|
}
|
|
343
573
|
if (decision === "ask") {
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
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
|
+
}
|
|
349
584
|
if (!approved) {
|
|
350
585
|
return { content: `User rejected tool "${call.name}".`, isError: true };
|
|
351
586
|
}
|
|
@@ -368,100 +603,36 @@ ${summary}`
|
|
|
368
603
|
}
|
|
369
604
|
};
|
|
370
605
|
|
|
371
|
-
// src/cli/
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
import { createOpenAICompatible } from "@ai-sdk/openai-compatible";
|
|
379
|
-
|
|
380
|
-
// src/llm/registry.ts
|
|
381
|
-
function listModels(config) {
|
|
382
|
-
return config.models;
|
|
383
|
-
}
|
|
384
|
-
function resolveModelConfig(config, modelName) {
|
|
385
|
-
const name = modelName ?? config.defaultModel;
|
|
386
|
-
const model = config.models.find((m) => m.name === name);
|
|
387
|
-
if (!model) {
|
|
388
|
-
const available = config.models.map((m) => m.name).join(", ") || "(none)";
|
|
389
|
-
throw new Error(`Model "${name}" not found. Available models: ${available}`);
|
|
390
|
-
}
|
|
391
|
-
return model;
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
// src/llm/provider.ts
|
|
395
|
-
function resolveApiKey(provider) {
|
|
396
|
-
const fromEnv = provider.apiKeyEnv ? process.env[provider.apiKeyEnv] : void 0;
|
|
397
|
-
const apiKey = fromEnv ?? provider.apiKey;
|
|
398
|
-
if (!apiKey) {
|
|
399
|
-
const hint = provider.apiKeyEnv ? `set ${provider.apiKeyEnv} or configure apiKey` : "configure apiKey";
|
|
400
|
-
throw new Error(`Missing API key for provider "${provider.name}": ${hint}`);
|
|
401
|
-
}
|
|
402
|
-
return apiKey;
|
|
403
|
-
}
|
|
404
|
-
function createModel(config, modelName) {
|
|
405
|
-
const modelConfig = resolveModelConfig(config, modelName);
|
|
406
|
-
const provider = config.providers.find((p) => p.name === modelConfig.provider);
|
|
407
|
-
if (!provider) {
|
|
408
|
-
const available = config.providers.map((p) => p.name).join(", ") || "(none)";
|
|
409
|
-
throw new Error(
|
|
410
|
-
`Provider "${modelConfig.provider}" for model "${modelConfig.name}" not found. Available providers: ${available}`
|
|
411
|
-
);
|
|
412
|
-
}
|
|
413
|
-
const apiKey = resolveApiKey(provider);
|
|
414
|
-
switch (provider.protocol) {
|
|
415
|
-
case "anthropic":
|
|
416
|
-
return createAnthropic({
|
|
417
|
-
baseURL: provider.baseURL,
|
|
418
|
-
apiKey,
|
|
419
|
-
headers: provider.headers
|
|
420
|
-
})(modelConfig.model);
|
|
421
|
-
case "openai-compatible":
|
|
422
|
-
return createOpenAICompatible({
|
|
423
|
-
name: provider.name,
|
|
424
|
-
baseURL: provider.baseURL,
|
|
425
|
-
apiKey,
|
|
426
|
-
headers: provider.headers
|
|
427
|
-
})(modelConfig.model);
|
|
428
|
-
case "openai-responses":
|
|
429
|
-
return createOpenAI({
|
|
430
|
-
baseURL: provider.baseURL,
|
|
431
|
-
apiKey,
|
|
432
|
-
headers: provider.headers
|
|
433
|
-
}).responses(modelConfig.model);
|
|
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);
|
|
434
613
|
}
|
|
614
|
+
return json.length > maxLength ? `${json.slice(0, maxLength)}...` : json;
|
|
435
615
|
}
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
// src/config/paths.ts
|
|
442
|
-
import os from "os";
|
|
443
|
-
import path2 from "path";
|
|
444
|
-
function starHome() {
|
|
445
|
-
return process.env.STAR_HOME ?? path2.join(os.homedir(), ".star-cli");
|
|
446
|
-
}
|
|
447
|
-
function globalConfigPath() {
|
|
448
|
-
return path2.join(starHome(), "config.toml");
|
|
449
|
-
}
|
|
450
|
-
function sessionsDir() {
|
|
451
|
-
return path2.join(starHome(), "sessions");
|
|
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 };
|
|
452
620
|
}
|
|
453
|
-
function
|
|
454
|
-
|
|
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) };
|
|
455
628
|
}
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
const suffix = Math.random().toString(16).slice(2, 8).padEnd(6, "0");
|
|
462
|
-
return `${stamp}-${suffix}`;
|
|
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;
|
|
463
634
|
}
|
|
464
|
-
function
|
|
635
|
+
function coreMessageText(message) {
|
|
465
636
|
const content = message.content;
|
|
466
637
|
if (typeof content === "string") return content;
|
|
467
638
|
if (Array.isArray(content)) {
|
|
@@ -469,90 +640,477 @@ function messageText(message) {
|
|
|
469
640
|
}
|
|
470
641
|
return "";
|
|
471
642
|
}
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
return path3.join(this.dir, "messages.jsonl");
|
|
486
|
-
}
|
|
487
|
-
static async create(cwd, model) {
|
|
488
|
-
const store = new _SessionStore(generateId(/* @__PURE__ */ new Date()));
|
|
489
|
-
store.pendingMeta = { cwd, model, createdAt: Date.now() };
|
|
490
|
-
return store;
|
|
491
|
-
}
|
|
492
|
-
static async open(id) {
|
|
493
|
-
const store = new _SessionStore(id);
|
|
494
|
-
try {
|
|
495
|
-
const raw = await fs.readFile(store.metaPath(), "utf8");
|
|
496
|
-
const meta = JSON.parse(raw);
|
|
497
|
-
if (meta.id !== id) return null;
|
|
498
|
-
store.initialized = true;
|
|
499
|
-
return store;
|
|
500
|
-
} catch {
|
|
501
|
-
return null;
|
|
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++;
|
|
502
656
|
}
|
|
503
657
|
}
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
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
|
+
|
|
668
|
+
// src/cli/mentions.ts
|
|
669
|
+
import { readFile as readFile2, stat as stat2 } from "fs/promises";
|
|
670
|
+
import path4 from "path";
|
|
671
|
+
|
|
672
|
+
// src/tools/fs/util.ts
|
|
673
|
+
import { readdir, stat } from "fs/promises";
|
|
674
|
+
import path3 from "path";
|
|
675
|
+
var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist"]);
|
|
676
|
+
async function walkFiles(root, skipDirs = SKIP_DIRS) {
|
|
677
|
+
const out = [];
|
|
678
|
+
async function walk(dir, relBase) {
|
|
679
|
+
const entries = await readdir(dir, { withFileTypes: true }).catch(() => null);
|
|
680
|
+
if (!entries) {
|
|
681
|
+
return;
|
|
510
682
|
}
|
|
511
|
-
const metas = [];
|
|
512
683
|
for (const entry of entries) {
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
if (
|
|
517
|
-
|
|
684
|
+
const abs = path3.join(dir, entry.name);
|
|
685
|
+
const rel = relBase ? `${relBase}/${entry.name}` : entry.name;
|
|
686
|
+
if (entry.isDirectory()) {
|
|
687
|
+
if (!skipDirs.has(entry.name)) {
|
|
688
|
+
await walk(abs, rel);
|
|
689
|
+
}
|
|
690
|
+
} else if (entry.isFile()) {
|
|
691
|
+
try {
|
|
692
|
+
const st = await stat(abs);
|
|
693
|
+
out.push({ abs, rel, mtimeMs: st.mtimeMs });
|
|
694
|
+
} catch {
|
|
695
|
+
}
|
|
518
696
|
}
|
|
519
697
|
}
|
|
520
|
-
return metas.sort((a, b) => b.updatedAt - a.updatedAt);
|
|
521
698
|
}
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
const
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
699
|
+
await walk(root, "");
|
|
700
|
+
return out;
|
|
701
|
+
}
|
|
702
|
+
var REGEX_SPECIALS = /[.+^${}()|[\]\\]/g;
|
|
703
|
+
function globToRegExp(pattern) {
|
|
704
|
+
let re = "";
|
|
705
|
+
let i = 0;
|
|
706
|
+
while (i < pattern.length) {
|
|
707
|
+
const c = pattern.charAt(i);
|
|
708
|
+
if (c === "*") {
|
|
709
|
+
if (pattern.charAt(i + 1) === "*") {
|
|
710
|
+
i += 2;
|
|
711
|
+
if (pattern.charAt(i) === "/") {
|
|
712
|
+
i += 1;
|
|
713
|
+
re += "(?:[^/]+/)*";
|
|
714
|
+
} else {
|
|
715
|
+
re += ".*";
|
|
716
|
+
}
|
|
717
|
+
} else {
|
|
718
|
+
re += "[^/]*";
|
|
719
|
+
i += 1;
|
|
720
|
+
}
|
|
721
|
+
} else if (c === "?") {
|
|
722
|
+
re += "[^/]";
|
|
723
|
+
i += 1;
|
|
724
|
+
} else {
|
|
725
|
+
re += c.replace(REGEX_SPECIALS, "\\$&");
|
|
726
|
+
i += 1;
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
return new RegExp(`^${re}$`);
|
|
730
|
+
}
|
|
731
|
+
function matchesGlob(pattern, relPath) {
|
|
732
|
+
const normalized = pattern.replace(/\\/g, "/");
|
|
733
|
+
if (!normalized.includes("/")) {
|
|
734
|
+
const base = relPath.split("/").pop() ?? relPath;
|
|
735
|
+
return globToRegExp(normalized).test(base);
|
|
736
|
+
}
|
|
737
|
+
return globToRegExp(normalized).test(relPath);
|
|
738
|
+
}
|
|
739
|
+
function isSensitivePath2(filePath) {
|
|
740
|
+
const base = path3.basename(filePath);
|
|
741
|
+
if (base === ".env.example" || base === ".env.sample" || base === ".env.template") {
|
|
742
|
+
return false;
|
|
743
|
+
}
|
|
744
|
+
if (base === ".env" || base.startsWith(".env.")) {
|
|
745
|
+
return true;
|
|
746
|
+
}
|
|
747
|
+
if (base === "id_rsa" || base.endsWith(".pem")) {
|
|
748
|
+
return true;
|
|
749
|
+
}
|
|
750
|
+
return false;
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
// src/cli/mentions.ts
|
|
754
|
+
var MAX_MENTION_BYTES = 100 * 1024;
|
|
755
|
+
var MENTION_RE = /(?<=^|\s)@([A-Za-z]:[\\/][A-Za-z0-9._\-/\\]*|[A-Za-z0-9._\-/\\]+)/g;
|
|
756
|
+
function parseMentions(text) {
|
|
757
|
+
const mentions = [];
|
|
758
|
+
const seen = /* @__PURE__ */ new Set();
|
|
759
|
+
const ranges = [];
|
|
760
|
+
for (const match of text.matchAll(MENTION_RE)) {
|
|
761
|
+
const mention = match[1];
|
|
762
|
+
const full = match[0];
|
|
763
|
+
if (mention === void 0 || full === void 0) continue;
|
|
764
|
+
ranges.push([match.index, match.index + full.length]);
|
|
765
|
+
if (!seen.has(mention)) {
|
|
766
|
+
seen.add(mention);
|
|
767
|
+
mentions.push(mention);
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
if (ranges.length === 0) {
|
|
771
|
+
return { cleanText: text, mentions };
|
|
772
|
+
}
|
|
773
|
+
let clean = "";
|
|
774
|
+
let last = 0;
|
|
775
|
+
for (const [start, end] of ranges) {
|
|
776
|
+
clean += text.slice(last, start);
|
|
777
|
+
last = end;
|
|
778
|
+
}
|
|
779
|
+
clean += text.slice(last);
|
|
780
|
+
const cleanText = clean.replace(/[ \t]+\n/g, "\n").replace(/\n{3,}/g, "\n\n").trim();
|
|
781
|
+
return { cleanText, mentions };
|
|
782
|
+
}
|
|
783
|
+
function isBinaryContent(buf) {
|
|
784
|
+
const probe = buf.subarray(0, 8e3);
|
|
785
|
+
return probe.includes(0);
|
|
786
|
+
}
|
|
787
|
+
async function resolveMentions(text, cwd) {
|
|
788
|
+
const { cleanText, mentions } = parseMentions(text);
|
|
789
|
+
if (mentions.length === 0) {
|
|
790
|
+
return { input: text, attached: [], skipped: [] };
|
|
791
|
+
}
|
|
792
|
+
const blocks = [];
|
|
793
|
+
const attached = [];
|
|
794
|
+
const skipped = [];
|
|
795
|
+
for (const mention of mentions) {
|
|
796
|
+
const abs = path4.resolve(cwd, mention);
|
|
797
|
+
if (isSensitivePath2(abs)) {
|
|
798
|
+
skipped.push({ path: mention, reason: "sensitive file" });
|
|
799
|
+
continue;
|
|
800
|
+
}
|
|
801
|
+
const st = await stat2(abs).catch(() => null);
|
|
802
|
+
if (!st) {
|
|
803
|
+
skipped.push({ path: mention, reason: "file not found" });
|
|
804
|
+
continue;
|
|
805
|
+
}
|
|
806
|
+
if (st.isDirectory()) {
|
|
807
|
+
skipped.push({ path: mention, reason: "is a directory" });
|
|
808
|
+
continue;
|
|
809
|
+
}
|
|
810
|
+
if (st.size > MAX_MENTION_BYTES) {
|
|
811
|
+
skipped.push({ path: mention, reason: `exceeds ${MAX_MENTION_BYTES / 1024}KB limit` });
|
|
812
|
+
continue;
|
|
813
|
+
}
|
|
814
|
+
const buf = await readFile2(abs).catch(() => null);
|
|
815
|
+
if (!buf) {
|
|
816
|
+
skipped.push({ path: mention, reason: "unreadable" });
|
|
817
|
+
continue;
|
|
818
|
+
}
|
|
819
|
+
if (isBinaryContent(buf)) {
|
|
820
|
+
skipped.push({ path: mention, reason: "binary file" });
|
|
821
|
+
continue;
|
|
822
|
+
}
|
|
823
|
+
attached.push(mention);
|
|
824
|
+
blocks.push(`--- @${mention} ---
|
|
825
|
+
${buf.toString("utf8")}
|
|
826
|
+
--- end ---`);
|
|
827
|
+
}
|
|
828
|
+
const input = [cleanText, ...blocks].filter((s) => s.length > 0).join("\n\n") || text;
|
|
829
|
+
return { input, attached, skipped };
|
|
830
|
+
}
|
|
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
|
+
|
|
875
|
+
// src/cli/repl.tsx
|
|
876
|
+
import { Box as Box8, Text as Text8, render, useApp, useInput as useInput4 } from "ink";
|
|
877
|
+
import { useCallback, useEffect, useMemo, useRef as useRef2, useState as useState2 } from "react";
|
|
878
|
+
|
|
879
|
+
// src/config/save.ts
|
|
880
|
+
import fs from "fs";
|
|
881
|
+
import path6 from "path";
|
|
882
|
+
import { parse, stringify } from "smol-toml";
|
|
883
|
+
|
|
884
|
+
// src/config/paths.ts
|
|
885
|
+
import os from "os";
|
|
886
|
+
import path5 from "path";
|
|
887
|
+
function starHome() {
|
|
888
|
+
return process.env.STAR_HOME ?? path5.join(os.homedir(), ".star-cli");
|
|
889
|
+
}
|
|
890
|
+
function globalConfigPath() {
|
|
891
|
+
return path5.join(starHome(), "config.toml");
|
|
892
|
+
}
|
|
893
|
+
function sessionsDir() {
|
|
894
|
+
return path5.join(starHome(), "sessions");
|
|
895
|
+
}
|
|
896
|
+
function projectConfigPath(cwd) {
|
|
897
|
+
return path5.join(cwd, ".star", "config.toml");
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
// src/config/save.ts
|
|
901
|
+
async function addAllowRule(rule) {
|
|
902
|
+
const filePath = globalConfigPath();
|
|
903
|
+
let raw = {};
|
|
904
|
+
try {
|
|
905
|
+
const content = await fs.promises.readFile(filePath, "utf8");
|
|
906
|
+
raw = parse(content);
|
|
907
|
+
} catch (error) {
|
|
908
|
+
if (error.code !== "ENOENT") throw error;
|
|
909
|
+
}
|
|
910
|
+
const permissions = typeof raw.permissions === "object" && raw.permissions !== null ? raw.permissions : {};
|
|
911
|
+
const allow = Array.isArray(permissions.allow) ? permissions.allow : [];
|
|
912
|
+
if (allow.includes(rule)) return false;
|
|
913
|
+
permissions.allow = [...allow, rule];
|
|
914
|
+
raw.permissions = permissions;
|
|
915
|
+
await fs.promises.mkdir(path6.dirname(filePath), { recursive: true });
|
|
916
|
+
await fs.promises.writeFile(filePath, stringify(raw), "utf8");
|
|
917
|
+
return true;
|
|
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
|
+
}
|
|
932
|
+
|
|
933
|
+
// src/llm/provider.ts
|
|
934
|
+
import { createAnthropic } from "@ai-sdk/anthropic";
|
|
935
|
+
import { createOpenAI } from "@ai-sdk/openai";
|
|
936
|
+
import { createOpenAICompatible } from "@ai-sdk/openai-compatible";
|
|
937
|
+
|
|
938
|
+
// src/llm/registry.ts
|
|
939
|
+
function listModels(config) {
|
|
940
|
+
return config.models;
|
|
941
|
+
}
|
|
942
|
+
function resolveModelConfig(config, modelName) {
|
|
943
|
+
const name = modelName ?? config.defaultModel;
|
|
944
|
+
const model = config.models.find((m) => m.name === name);
|
|
945
|
+
if (!model) {
|
|
946
|
+
const available = config.models.map((m) => m.name).join(", ") || "(none)";
|
|
947
|
+
throw new Error(`Model "${name}" not found. Available models: ${available}`);
|
|
948
|
+
}
|
|
949
|
+
return model;
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
// src/llm/provider.ts
|
|
953
|
+
function resolveApiKey(provider) {
|
|
954
|
+
const fromEnv = provider.apiKeyEnv ? process.env[provider.apiKeyEnv] : void 0;
|
|
955
|
+
const apiKey = fromEnv ?? provider.apiKey;
|
|
956
|
+
if (!apiKey) {
|
|
957
|
+
const hint = provider.apiKeyEnv ? `set ${provider.apiKeyEnv} or configure apiKey` : "configure apiKey";
|
|
958
|
+
throw new Error(`Missing API key for provider "${provider.name}": ${hint}`);
|
|
959
|
+
}
|
|
960
|
+
return apiKey;
|
|
961
|
+
}
|
|
962
|
+
function createModel(config, modelName) {
|
|
963
|
+
const modelConfig = resolveModelConfig(config, modelName);
|
|
964
|
+
const provider = config.providers.find((p) => p.name === modelConfig.provider);
|
|
965
|
+
if (!provider) {
|
|
966
|
+
const available = config.providers.map((p) => p.name).join(", ") || "(none)";
|
|
967
|
+
throw new Error(
|
|
968
|
+
`Provider "${modelConfig.provider}" for model "${modelConfig.name}" not found. Available providers: ${available}`
|
|
969
|
+
);
|
|
970
|
+
}
|
|
971
|
+
const apiKey = resolveApiKey(provider);
|
|
972
|
+
switch (provider.protocol) {
|
|
973
|
+
case "anthropic":
|
|
974
|
+
return createAnthropic({
|
|
975
|
+
baseURL: provider.baseURL,
|
|
976
|
+
apiKey,
|
|
977
|
+
headers: provider.headers
|
|
978
|
+
})(modelConfig.model);
|
|
979
|
+
case "openai-compatible":
|
|
980
|
+
return createOpenAICompatible({
|
|
981
|
+
name: provider.name,
|
|
982
|
+
baseURL: provider.baseURL,
|
|
983
|
+
apiKey,
|
|
984
|
+
headers: provider.headers
|
|
985
|
+
})(modelConfig.model);
|
|
986
|
+
case "openai-responses":
|
|
987
|
+
return createOpenAI({
|
|
988
|
+
baseURL: provider.baseURL,
|
|
989
|
+
apiKey,
|
|
990
|
+
headers: provider.headers
|
|
991
|
+
}).responses(modelConfig.model);
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
// src/session/store.ts
|
|
996
|
+
import fs2 from "fs/promises";
|
|
997
|
+
import path7 from "path";
|
|
998
|
+
function generateId(now) {
|
|
999
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
1000
|
+
const stamp = `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`;
|
|
1001
|
+
const suffix = Math.random().toString(16).slice(2, 8).padEnd(6, "0");
|
|
1002
|
+
return `${stamp}-${suffix}`;
|
|
1003
|
+
}
|
|
1004
|
+
function debugWarn(message) {
|
|
1005
|
+
if (process.env.STAR_DEBUG === "1") process.stderr.write(`[star-cli] ${message}
|
|
1006
|
+
`);
|
|
1007
|
+
}
|
|
1008
|
+
function messageText(message) {
|
|
1009
|
+
const content = message.content;
|
|
1010
|
+
if (typeof content === "string") return content;
|
|
1011
|
+
if (Array.isArray(content)) {
|
|
1012
|
+
return content.filter((part) => part.type === "text").map((part) => "text" in part ? part.text : "").join(" ");
|
|
1013
|
+
}
|
|
1014
|
+
return "";
|
|
1015
|
+
}
|
|
1016
|
+
var SessionStore = class _SessionStore {
|
|
1017
|
+
id;
|
|
1018
|
+
dir;
|
|
1019
|
+
pendingMeta = null;
|
|
1020
|
+
initialized = false;
|
|
1021
|
+
constructor(id) {
|
|
1022
|
+
this.id = id;
|
|
1023
|
+
this.dir = path7.join(sessionsDir(), id);
|
|
1024
|
+
}
|
|
1025
|
+
metaPath() {
|
|
1026
|
+
return path7.join(this.dir, "meta.json");
|
|
1027
|
+
}
|
|
1028
|
+
messagesPath() {
|
|
1029
|
+
return path7.join(this.dir, "messages.jsonl");
|
|
1030
|
+
}
|
|
1031
|
+
static async create(cwd, model) {
|
|
1032
|
+
const store = new _SessionStore(generateId(/* @__PURE__ */ new Date()));
|
|
1033
|
+
store.pendingMeta = { cwd, model, createdAt: Date.now() };
|
|
1034
|
+
return store;
|
|
1035
|
+
}
|
|
1036
|
+
static async open(id) {
|
|
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
|
+
}
|
|
1044
|
+
try {
|
|
1045
|
+
const meta = JSON.parse(raw);
|
|
1046
|
+
if (meta.id !== id) return null;
|
|
1047
|
+
} catch {
|
|
1048
|
+
debugWarn(`session ${id}: corrupt meta.json, using defaults`);
|
|
1049
|
+
}
|
|
1050
|
+
store.initialized = true;
|
|
1051
|
+
return store;
|
|
1052
|
+
}
|
|
1053
|
+
static async list(cwd) {
|
|
1054
|
+
let entries;
|
|
1055
|
+
try {
|
|
1056
|
+
entries = await fs2.readdir(sessionsDir());
|
|
1057
|
+
} catch {
|
|
1058
|
+
return [];
|
|
1059
|
+
}
|
|
1060
|
+
const metas = [];
|
|
1061
|
+
for (const entry of entries) {
|
|
1062
|
+
try {
|
|
1063
|
+
const raw = await fs2.readFile(path7.join(sessionsDir(), entry, "meta.json"), "utf8");
|
|
1064
|
+
const meta = JSON.parse(raw);
|
|
1065
|
+
if (meta.id === entry && (cwd === void 0 || meta.cwd === cwd)) metas.push(meta);
|
|
1066
|
+
} catch {
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
return metas.sort((a, b) => b.updatedAt - a.updatedAt);
|
|
1070
|
+
}
|
|
1071
|
+
async ensureInitialized() {
|
|
1072
|
+
if (this.initialized) return;
|
|
1073
|
+
const pending = this.pendingMeta;
|
|
1074
|
+
if (!pending) {
|
|
1075
|
+
this.initialized = true;
|
|
1076
|
+
return;
|
|
1077
|
+
}
|
|
1078
|
+
await fs2.mkdir(this.dir, { recursive: true });
|
|
1079
|
+
const meta = {
|
|
1080
|
+
id: this.id,
|
|
1081
|
+
title: "",
|
|
1082
|
+
model: pending.model,
|
|
1083
|
+
cwd: pending.cwd,
|
|
535
1084
|
createdAt: pending.createdAt,
|
|
536
1085
|
updatedAt: pending.createdAt
|
|
537
1086
|
};
|
|
538
|
-
await
|
|
1087
|
+
await fs2.writeFile(this.metaPath(), JSON.stringify(meta, null, 2));
|
|
539
1088
|
this.initialized = true;
|
|
540
1089
|
}
|
|
541
1090
|
async append(message) {
|
|
542
1091
|
await this.ensureInitialized();
|
|
543
|
-
await
|
|
1092
|
+
await fs2.appendFile(this.messagesPath(), `${JSON.stringify(message)}
|
|
544
1093
|
`);
|
|
545
1094
|
const meta = await this.meta();
|
|
546
1095
|
meta.updatedAt = Date.now();
|
|
547
1096
|
if (!meta.title && message.role === "user") {
|
|
548
1097
|
meta.title = messageText(message).slice(0, 60);
|
|
549
1098
|
}
|
|
550
|
-
await
|
|
1099
|
+
await fs2.writeFile(this.metaPath(), JSON.stringify(meta, null, 2));
|
|
1100
|
+
}
|
|
1101
|
+
async replaceMessages(messages) {
|
|
1102
|
+
await this.ensureInitialized();
|
|
1103
|
+
const content = messages.map((message) => JSON.stringify(message)).join("\n");
|
|
1104
|
+
await fs2.writeFile(this.messagesPath(), content ? `${content}
|
|
1105
|
+
` : "");
|
|
1106
|
+
const meta = await this.meta();
|
|
1107
|
+
meta.updatedAt = Date.now();
|
|
1108
|
+
await fs2.writeFile(this.metaPath(), JSON.stringify(meta, null, 2));
|
|
551
1109
|
}
|
|
552
1110
|
async messages() {
|
|
553
1111
|
let raw;
|
|
554
1112
|
try {
|
|
555
|
-
raw = await
|
|
1113
|
+
raw = await fs2.readFile(this.messagesPath(), "utf8");
|
|
556
1114
|
} catch {
|
|
557
1115
|
return [];
|
|
558
1116
|
}
|
|
@@ -563,21 +1121,31 @@ var SessionStore = class _SessionStore {
|
|
|
563
1121
|
try {
|
|
564
1122
|
messages.push(JSON.parse(trimmed));
|
|
565
1123
|
} catch {
|
|
1124
|
+
debugWarn(`session ${this.id}: skipping corrupt messages.jsonl line`);
|
|
566
1125
|
}
|
|
567
1126
|
}
|
|
568
1127
|
return messages;
|
|
569
1128
|
}
|
|
1129
|
+
fallbackMeta() {
|
|
1130
|
+
const now = Date.now();
|
|
1131
|
+
return { id: this.id, title: "", model: "", cwd: "", createdAt: now, updatedAt: now };
|
|
1132
|
+
}
|
|
570
1133
|
async meta() {
|
|
571
1134
|
await this.ensureInitialized();
|
|
572
|
-
|
|
573
|
-
|
|
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
|
+
}
|
|
574
1142
|
}
|
|
575
1143
|
async setTitle(title) {
|
|
576
1144
|
await this.ensureInitialized();
|
|
577
1145
|
const meta = await this.meta();
|
|
578
1146
|
meta.title = title;
|
|
579
1147
|
meta.updatedAt = Date.now();
|
|
580
|
-
await
|
|
1148
|
+
await fs2.writeFile(this.metaPath(), JSON.stringify(meta, null, 2));
|
|
581
1149
|
}
|
|
582
1150
|
async addUsage(delta) {
|
|
583
1151
|
await this.ensureInitialized();
|
|
@@ -593,7 +1161,7 @@ var SessionStore = class _SessionStore {
|
|
|
593
1161
|
usage.completionTokens += delta.completionTokens;
|
|
594
1162
|
usage.totalTokens += delta.totalTokens;
|
|
595
1163
|
meta.usage = usage;
|
|
596
|
-
await
|
|
1164
|
+
await fs2.writeFile(this.metaPath(), JSON.stringify(meta, null, 2));
|
|
597
1165
|
}
|
|
598
1166
|
};
|
|
599
1167
|
|
|
@@ -601,7 +1169,11 @@ var SessionStore = class _SessionStore {
|
|
|
601
1169
|
async function resumeSession(id) {
|
|
602
1170
|
const store = await SessionStore.open(id);
|
|
603
1171
|
if (!store) return null;
|
|
604
|
-
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
|
+
}
|
|
605
1177
|
return { meta, messages };
|
|
606
1178
|
}
|
|
607
1179
|
function relativeTime(timestamp) {
|
|
@@ -624,20 +1196,67 @@ function formatSessionList(metas) {
|
|
|
624
1196
|
}).join("\n");
|
|
625
1197
|
}
|
|
626
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
|
+
|
|
627
1246
|
// src/tools/bash.ts
|
|
628
1247
|
import { spawn } from "child_process";
|
|
629
1248
|
import { existsSync } from "fs";
|
|
630
|
-
import
|
|
1249
|
+
import path8 from "path";
|
|
631
1250
|
import { z } from "zod";
|
|
632
1251
|
var MAX_OUTPUT = 3e4;
|
|
633
1252
|
var DEFAULT_TIMEOUT = 120;
|
|
634
1253
|
var MAX_TIMEOUT = 600;
|
|
635
1254
|
function findOnPath(exe, exclude) {
|
|
636
|
-
for (const dir of (process.env.PATH ?? "").split(
|
|
1255
|
+
for (const dir of (process.env.PATH ?? "").split(path8.delimiter)) {
|
|
637
1256
|
if (!dir || exclude?.(dir)) {
|
|
638
1257
|
continue;
|
|
639
1258
|
}
|
|
640
|
-
const full =
|
|
1259
|
+
const full = path8.join(dir, exe);
|
|
641
1260
|
if (existsSync(full)) {
|
|
642
1261
|
return full;
|
|
643
1262
|
}
|
|
@@ -655,7 +1274,7 @@ function resolveShell() {
|
|
|
655
1274
|
}
|
|
656
1275
|
const gitExe = findOnPath("git.exe");
|
|
657
1276
|
if (gitExe) {
|
|
658
|
-
const sibling =
|
|
1277
|
+
const sibling = path8.join(path8.dirname(path8.dirname(gitExe)), "bin", "bash.exe");
|
|
659
1278
|
if (existsSync(sibling)) {
|
|
660
1279
|
return { shell: sibling, wrap: (c) => ["-c", c], label: "bash" };
|
|
661
1280
|
}
|
|
@@ -675,7 +1294,10 @@ function resolveShell() {
|
|
|
675
1294
|
var schema = z.object({
|
|
676
1295
|
command: z.string().describe("Shell command to execute"),
|
|
677
1296
|
timeout: z.number().positive().max(MAX_TIMEOUT).optional().describe("Timeout in seconds (default 120, max 600)"),
|
|
678
|
-
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
|
+
)
|
|
679
1301
|
});
|
|
680
1302
|
function killTree(child) {
|
|
681
1303
|
if (process.platform === "win32" && child.pid) {
|
|
@@ -700,10 +1322,23 @@ ${tail}`;
|
|
|
700
1322
|
}
|
|
701
1323
|
var bashTool = {
|
|
702
1324
|
name: "bash",
|
|
703
|
-
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.",
|
|
704
1326
|
permission: "exec",
|
|
705
1327
|
parameters: schema,
|
|
706
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
|
+
}
|
|
707
1342
|
const timeoutSeconds = Math.min(args.timeout ?? DEFAULT_TIMEOUT, MAX_TIMEOUT);
|
|
708
1343
|
const spec = resolveShell();
|
|
709
1344
|
return new Promise((resolve) => {
|
|
@@ -761,9 +1396,119 @@ Command timed out after ${timeoutSeconds}s`,
|
|
|
761
1396
|
}
|
|
762
1397
|
};
|
|
763
1398
|
|
|
1399
|
+
// src/tasks/manager.ts
|
|
1400
|
+
var MAX_TASK_OUTPUT = 1e5;
|
|
1401
|
+
var DEFAULT_TASK_TIMEOUT = 3600;
|
|
1402
|
+
var TRUNCATED_PREFIX = "[... earlier output truncated ...]\n";
|
|
1403
|
+
var TaskManager = class extends EventEmitter {
|
|
1404
|
+
records = /* @__PURE__ */ new Map();
|
|
1405
|
+
seq = 0;
|
|
1406
|
+
start(opts) {
|
|
1407
|
+
const id = `task-${++this.seq}`;
|
|
1408
|
+
const spec = resolveShell();
|
|
1409
|
+
const child = spawn2(spec.shell, spec.wrap(opts.command), {
|
|
1410
|
+
cwd: opts.cwd,
|
|
1411
|
+
windowsHide: true
|
|
1412
|
+
});
|
|
1413
|
+
const rec = {
|
|
1414
|
+
id,
|
|
1415
|
+
command: opts.command,
|
|
1416
|
+
description: opts.description,
|
|
1417
|
+
status: "running",
|
|
1418
|
+
startedAt: Date.now(),
|
|
1419
|
+
output: "",
|
|
1420
|
+
truncated: false,
|
|
1421
|
+
child
|
|
1422
|
+
};
|
|
1423
|
+
this.records.set(id, rec);
|
|
1424
|
+
const append = (d) => {
|
|
1425
|
+
rec.output += d.toString("utf8");
|
|
1426
|
+
if (rec.output.length > MAX_TASK_OUTPUT) {
|
|
1427
|
+
rec.output = rec.output.slice(-MAX_TASK_OUTPUT);
|
|
1428
|
+
rec.truncated = true;
|
|
1429
|
+
}
|
|
1430
|
+
};
|
|
1431
|
+
child.stdout.on("data", append);
|
|
1432
|
+
child.stderr.on("data", append);
|
|
1433
|
+
child.on("error", (err) => {
|
|
1434
|
+
append(Buffer.from(`Failed to start shell '${spec.label}': ${err.message}
|
|
1435
|
+
`));
|
|
1436
|
+
if (rec.status === "running") {
|
|
1437
|
+
this.finish(rec, "failed", null);
|
|
1438
|
+
}
|
|
1439
|
+
});
|
|
1440
|
+
child.on("close", (code) => {
|
|
1441
|
+
if (rec.status !== "running") return;
|
|
1442
|
+
this.finish(rec, code === 0 ? "completed" : "failed", code);
|
|
1443
|
+
});
|
|
1444
|
+
const timeoutSeconds = opts.timeoutSeconds ?? DEFAULT_TASK_TIMEOUT;
|
|
1445
|
+
rec.timer = setTimeout(() => this.terminate(rec, "timed_out"), timeoutSeconds * 1e3);
|
|
1446
|
+
rec.timer.unref();
|
|
1447
|
+
this.emitUpdate(rec);
|
|
1448
|
+
return this.snapshot(rec);
|
|
1449
|
+
}
|
|
1450
|
+
get(id) {
|
|
1451
|
+
const rec = this.records.get(id);
|
|
1452
|
+
return rec ? this.snapshot(rec) : void 0;
|
|
1453
|
+
}
|
|
1454
|
+
list() {
|
|
1455
|
+
return [...this.records.values()].map((rec) => this.snapshot(rec));
|
|
1456
|
+
}
|
|
1457
|
+
runningCount() {
|
|
1458
|
+
let count = 0;
|
|
1459
|
+
for (const rec of this.records.values()) {
|
|
1460
|
+
if (rec.status === "running") count += 1;
|
|
1461
|
+
}
|
|
1462
|
+
return count;
|
|
1463
|
+
}
|
|
1464
|
+
kill(id) {
|
|
1465
|
+
const rec = this.records.get(id);
|
|
1466
|
+
if (!rec || rec.status !== "running") return void 0;
|
|
1467
|
+
this.terminate(rec, "stopped");
|
|
1468
|
+
return this.snapshot(rec);
|
|
1469
|
+
}
|
|
1470
|
+
cleanup() {
|
|
1471
|
+
const killed = [];
|
|
1472
|
+
for (const rec of this.records.values()) {
|
|
1473
|
+
if (rec.status === "running") {
|
|
1474
|
+
this.terminate(rec, "stopped");
|
|
1475
|
+
killed.push(this.snapshot(rec));
|
|
1476
|
+
}
|
|
1477
|
+
}
|
|
1478
|
+
return killed;
|
|
1479
|
+
}
|
|
1480
|
+
terminate(rec, status) {
|
|
1481
|
+
killTree(rec.child);
|
|
1482
|
+
this.finish(rec, status, null);
|
|
1483
|
+
}
|
|
1484
|
+
finish(rec, status, exitCode) {
|
|
1485
|
+
if (rec.timer) clearTimeout(rec.timer);
|
|
1486
|
+
rec.status = status;
|
|
1487
|
+
rec.exitCode = exitCode;
|
|
1488
|
+
rec.endedAt = Date.now();
|
|
1489
|
+
this.emitUpdate(rec);
|
|
1490
|
+
}
|
|
1491
|
+
emitUpdate(rec) {
|
|
1492
|
+
this.emit("update", this.snapshot(rec));
|
|
1493
|
+
}
|
|
1494
|
+
snapshot(rec) {
|
|
1495
|
+
return {
|
|
1496
|
+
id: rec.id,
|
|
1497
|
+
command: rec.command,
|
|
1498
|
+
description: rec.description,
|
|
1499
|
+
status: rec.status,
|
|
1500
|
+
startedAt: rec.startedAt,
|
|
1501
|
+
endedAt: rec.endedAt,
|
|
1502
|
+
exitCode: rec.exitCode,
|
|
1503
|
+
output: rec.truncated ? TRUNCATED_PREFIX + rec.output : rec.output
|
|
1504
|
+
};
|
|
1505
|
+
}
|
|
1506
|
+
};
|
|
1507
|
+
var defaultTaskManager = new TaskManager();
|
|
1508
|
+
|
|
764
1509
|
// src/tools/fs/edit.ts
|
|
765
|
-
import { readFile, writeFile } from "fs/promises";
|
|
766
|
-
import
|
|
1510
|
+
import { readFile as readFile3, writeFile as writeFile2 } from "fs/promises";
|
|
1511
|
+
import path9 from "path";
|
|
767
1512
|
import { z as z2 } from "zod";
|
|
768
1513
|
var schema2 = z2.object({
|
|
769
1514
|
path: z2.string().describe("File path, absolute or relative to the working directory"),
|
|
@@ -777,120 +1522,49 @@ var editFileTool = {
|
|
|
777
1522
|
permission: "write",
|
|
778
1523
|
parameters: schema2,
|
|
779
1524
|
async execute(args, ctx) {
|
|
780
|
-
const filePath =
|
|
1525
|
+
const filePath = path9.resolve(ctx.cwd, args.path);
|
|
781
1526
|
let content;
|
|
782
1527
|
try {
|
|
783
|
-
content = await
|
|
1528
|
+
content = await readFile3(filePath, "utf8");
|
|
784
1529
|
} catch (err) {
|
|
785
1530
|
return { content: `Failed to read ${args.path}: ${err.message}`, isError: true };
|
|
786
1531
|
}
|
|
787
1532
|
let count = 0;
|
|
788
1533
|
let idx = content.indexOf(args.old_string);
|
|
789
|
-
while (idx !== -1) {
|
|
790
|
-
count += 1;
|
|
791
|
-
idx = content.indexOf(args.old_string, idx + args.old_string.length);
|
|
792
|
-
}
|
|
793
|
-
if (count === 0) {
|
|
794
|
-
return { content: `old_string not found in ${args.path}`, isError: true };
|
|
795
|
-
}
|
|
796
|
-
if (count > 1 && !args.replace_all) {
|
|
797
|
-
return {
|
|
798
|
-
content: `old_string matches ${count} locations in ${args.path}; provide more surrounding context or set replace_all`,
|
|
799
|
-
isError: true
|
|
800
|
-
};
|
|
801
|
-
}
|
|
802
|
-
const updated = args.replace_all ? content.split(args.old_string).join(args.new_string) : content.replace(args.old_string, args.new_string);
|
|
803
|
-
await writeFile(filePath, updated, "utf8");
|
|
804
|
-
return { content: `Edited ${args.path}: ${count} replacement${count > 1 ? "s" : ""}` };
|
|
805
|
-
}
|
|
806
|
-
};
|
|
807
|
-
|
|
808
|
-
// src/tools/fs/glob.ts
|
|
809
|
-
import path7 from "path";
|
|
810
|
-
import { z as z3 } from "zod";
|
|
811
|
-
|
|
812
|
-
// src/tools/fs/util.ts
|
|
813
|
-
import { readdir, stat } from "fs/promises";
|
|
814
|
-
import path6 from "path";
|
|
815
|
-
var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist"]);
|
|
816
|
-
async function walkFiles(root, skipDirs = SKIP_DIRS) {
|
|
817
|
-
const out = [];
|
|
818
|
-
async function walk(dir, relBase) {
|
|
819
|
-
const entries = await readdir(dir, { withFileTypes: true }).catch(() => null);
|
|
820
|
-
if (!entries) {
|
|
821
|
-
return;
|
|
822
|
-
}
|
|
823
|
-
for (const entry of entries) {
|
|
824
|
-
const abs = path6.join(dir, entry.name);
|
|
825
|
-
const rel = relBase ? `${relBase}/${entry.name}` : entry.name;
|
|
826
|
-
if (entry.isDirectory()) {
|
|
827
|
-
if (!skipDirs.has(entry.name)) {
|
|
828
|
-
await walk(abs, rel);
|
|
829
|
-
}
|
|
830
|
-
} else if (entry.isFile()) {
|
|
831
|
-
try {
|
|
832
|
-
const st = await stat(abs);
|
|
833
|
-
out.push({ abs, rel, mtimeMs: st.mtimeMs });
|
|
834
|
-
} catch {
|
|
835
|
-
}
|
|
836
|
-
}
|
|
837
|
-
}
|
|
838
|
-
}
|
|
839
|
-
await walk(root, "");
|
|
840
|
-
return out;
|
|
841
|
-
}
|
|
842
|
-
var REGEX_SPECIALS = /[.+^${}()|[\]\\]/g;
|
|
843
|
-
function globToRegExp(pattern) {
|
|
844
|
-
let re = "";
|
|
845
|
-
let i = 0;
|
|
846
|
-
while (i < pattern.length) {
|
|
847
|
-
const c = pattern.charAt(i);
|
|
848
|
-
if (c === "*") {
|
|
849
|
-
if (pattern.charAt(i + 1) === "*") {
|
|
850
|
-
i += 2;
|
|
851
|
-
if (pattern.charAt(i) === "/") {
|
|
852
|
-
i += 1;
|
|
853
|
-
re += "(?:[^/]+/)*";
|
|
854
|
-
} else {
|
|
855
|
-
re += ".*";
|
|
856
|
-
}
|
|
857
|
-
} else {
|
|
858
|
-
re += "[^/]*";
|
|
859
|
-
i += 1;
|
|
860
|
-
}
|
|
861
|
-
} else if (c === "?") {
|
|
862
|
-
re += "[^/]";
|
|
863
|
-
i += 1;
|
|
864
|
-
} else {
|
|
865
|
-
re += c.replace(REGEX_SPECIALS, "\\$&");
|
|
866
|
-
i += 1;
|
|
1534
|
+
while (idx !== -1) {
|
|
1535
|
+
count += 1;
|
|
1536
|
+
idx = content.indexOf(args.old_string, idx + args.old_string.length);
|
|
867
1537
|
}
|
|
1538
|
+
if (count === 0) {
|
|
1539
|
+
return { content: `old_string not found in ${args.path}`, isError: true };
|
|
1540
|
+
}
|
|
1541
|
+
if (count > 1 && !args.replace_all) {
|
|
1542
|
+
return {
|
|
1543
|
+
content: `old_string matches ${count} locations in ${args.path}; provide more surrounding context or set replace_all`,
|
|
1544
|
+
isError: true
|
|
1545
|
+
};
|
|
1546
|
+
}
|
|
1547
|
+
const updated = args.replace_all ? content.split(args.old_string).join(args.new_string) : content.replace(args.old_string, args.new_string);
|
|
1548
|
+
try {
|
|
1549
|
+
await writeFile2(filePath, updated, "utf8");
|
|
1550
|
+
} catch (err) {
|
|
1551
|
+
return { content: `Failed to write ${args.path}: ${err.message}`, isError: true };
|
|
1552
|
+
}
|
|
1553
|
+
pushSnapshot({
|
|
1554
|
+
path: filePath,
|
|
1555
|
+
existed: true,
|
|
1556
|
+
content,
|
|
1557
|
+
toolName: "edit_file",
|
|
1558
|
+
timestamp: Date.now(),
|
|
1559
|
+
turn: currentTurnSeq()
|
|
1560
|
+
});
|
|
1561
|
+
return { content: `Edited ${args.path}: ${count} replacement${count > 1 ? "s" : ""}` };
|
|
868
1562
|
}
|
|
869
|
-
|
|
870
|
-
}
|
|
871
|
-
function matchesGlob(pattern, relPath) {
|
|
872
|
-
const normalized = pattern.replace(/\\/g, "/");
|
|
873
|
-
if (!normalized.includes("/")) {
|
|
874
|
-
const base = relPath.split("/").pop() ?? relPath;
|
|
875
|
-
return globToRegExp(normalized).test(base);
|
|
876
|
-
}
|
|
877
|
-
return globToRegExp(normalized).test(relPath);
|
|
878
|
-
}
|
|
879
|
-
function isSensitivePath2(filePath) {
|
|
880
|
-
const base = path6.basename(filePath);
|
|
881
|
-
if (base === ".env.example" || base === ".env.sample" || base === ".env.template") {
|
|
882
|
-
return false;
|
|
883
|
-
}
|
|
884
|
-
if (base === ".env" || base.startsWith(".env.")) {
|
|
885
|
-
return true;
|
|
886
|
-
}
|
|
887
|
-
if (base === "id_rsa" || base.endsWith(".pem")) {
|
|
888
|
-
return true;
|
|
889
|
-
}
|
|
890
|
-
return false;
|
|
891
|
-
}
|
|
1563
|
+
};
|
|
892
1564
|
|
|
893
1565
|
// src/tools/fs/glob.ts
|
|
1566
|
+
import path10 from "path";
|
|
1567
|
+
import { z as z3 } from "zod";
|
|
894
1568
|
var MAX_RESULTS = 100;
|
|
895
1569
|
var schema3 = z3.object({
|
|
896
1570
|
pattern: z3.string().describe(
|
|
@@ -904,7 +1578,7 @@ var globTool = {
|
|
|
904
1578
|
permission: "read",
|
|
905
1579
|
parameters: schema3,
|
|
906
1580
|
async execute(args, ctx) {
|
|
907
|
-
const root =
|
|
1581
|
+
const root = path10.resolve(ctx.cwd, args.path ?? ".");
|
|
908
1582
|
const files = await walkFiles(root);
|
|
909
1583
|
const matched = files.filter((f) => matchesGlob(args.pattern, f.rel)).sort((a, b) => b.mtimeMs - a.mtimeMs).slice(0, MAX_RESULTS);
|
|
910
1584
|
if (matched.length === 0) {
|
|
@@ -915,8 +1589,8 @@ var globTool = {
|
|
|
915
1589
|
};
|
|
916
1590
|
|
|
917
1591
|
// src/tools/fs/grep.ts
|
|
918
|
-
import { readFile as
|
|
919
|
-
import
|
|
1592
|
+
import { readFile as readFile4, stat as stat3 } from "fs/promises";
|
|
1593
|
+
import path11 from "path";
|
|
920
1594
|
import { z as z4 } from "zod";
|
|
921
1595
|
var MAX_MATCHES = 250;
|
|
922
1596
|
var schema4 = z4.object({
|
|
@@ -937,12 +1611,12 @@ var grepTool = {
|
|
|
937
1611
|
} catch (err) {
|
|
938
1612
|
return { content: `Invalid regular expression: ${err.message}`, isError: true };
|
|
939
1613
|
}
|
|
940
|
-
const root =
|
|
1614
|
+
const root = path11.resolve(ctx.cwd, args.path ?? ".");
|
|
941
1615
|
let files;
|
|
942
1616
|
try {
|
|
943
|
-
const st = await
|
|
1617
|
+
const st = await stat3(root);
|
|
944
1618
|
if (st.isFile()) {
|
|
945
|
-
files = [{ abs: root, rel:
|
|
1619
|
+
files = [{ abs: root, rel: path11.basename(root), mtimeMs: st.mtimeMs }];
|
|
946
1620
|
} else {
|
|
947
1621
|
files = await walkFiles(root);
|
|
948
1622
|
}
|
|
@@ -958,7 +1632,7 @@ var grepTool = {
|
|
|
958
1632
|
outer: for (const file of files) {
|
|
959
1633
|
let buf;
|
|
960
1634
|
try {
|
|
961
|
-
buf = await
|
|
1635
|
+
buf = await readFile4(file.abs);
|
|
962
1636
|
} catch {
|
|
963
1637
|
continue;
|
|
964
1638
|
}
|
|
@@ -988,8 +1662,8 @@ var grepTool = {
|
|
|
988
1662
|
};
|
|
989
1663
|
|
|
990
1664
|
// src/tools/fs/read.ts
|
|
991
|
-
import { readFile as
|
|
992
|
-
import
|
|
1665
|
+
import { readFile as readFile5, stat as stat4 } from "fs/promises";
|
|
1666
|
+
import path12 from "path";
|
|
993
1667
|
import { z as z5 } from "zod";
|
|
994
1668
|
var DEFAULT_LIMIT = 2e3;
|
|
995
1669
|
var MAX_CHARS = 100 * 1024;
|
|
@@ -1004,18 +1678,18 @@ var readFileTool = {
|
|
|
1004
1678
|
permission: "read",
|
|
1005
1679
|
parameters: schema5,
|
|
1006
1680
|
async execute(args, ctx) {
|
|
1007
|
-
const filePath =
|
|
1681
|
+
const filePath = path12.resolve(ctx.cwd, args.path);
|
|
1008
1682
|
if (isSensitivePath2(filePath)) {
|
|
1009
1683
|
return { content: `Refused to read sensitive file: ${args.path}`, isError: true };
|
|
1010
1684
|
}
|
|
1011
|
-
const st = await
|
|
1685
|
+
const st = await stat4(filePath).catch(() => null);
|
|
1012
1686
|
if (!st) {
|
|
1013
1687
|
return { content: `File not found: ${args.path}`, isError: true };
|
|
1014
1688
|
}
|
|
1015
1689
|
if (st.isDirectory()) {
|
|
1016
1690
|
return { content: `Path is a directory, not a file: ${args.path}`, isError: true };
|
|
1017
1691
|
}
|
|
1018
|
-
const raw = await
|
|
1692
|
+
const raw = await readFile5(filePath, "utf8");
|
|
1019
1693
|
const lines = raw.split("\n");
|
|
1020
1694
|
const start = args.offset ?? 1;
|
|
1021
1695
|
const limit = args.limit ?? DEFAULT_LIMIT;
|
|
@@ -1049,8 +1723,8 @@ var readFileTool = {
|
|
|
1049
1723
|
};
|
|
1050
1724
|
|
|
1051
1725
|
// src/tools/fs/write.ts
|
|
1052
|
-
import { mkdir, writeFile as
|
|
1053
|
-
import
|
|
1726
|
+
import { mkdir as mkdir2, writeFile as writeFile3 } from "fs/promises";
|
|
1727
|
+
import path13 from "path";
|
|
1054
1728
|
import { z as z6 } from "zod";
|
|
1055
1729
|
var schema6 = z6.object({
|
|
1056
1730
|
path: z6.string().describe("File path, absolute or relative to the working directory"),
|
|
@@ -1062,32 +1736,84 @@ var writeFileTool = {
|
|
|
1062
1736
|
permission: "write",
|
|
1063
1737
|
parameters: schema6,
|
|
1064
1738
|
async execute(args, ctx) {
|
|
1065
|
-
const filePath =
|
|
1739
|
+
const filePath = path13.resolve(ctx.cwd, args.path);
|
|
1740
|
+
const snapshot = await captureSnapshot(filePath, "write_file");
|
|
1066
1741
|
try {
|
|
1067
|
-
await
|
|
1068
|
-
await
|
|
1742
|
+
await mkdir2(path13.dirname(filePath), { recursive: true });
|
|
1743
|
+
await writeFile3(filePath, args.content, "utf8");
|
|
1069
1744
|
} catch (err) {
|
|
1070
1745
|
return { content: `Failed to write ${args.path}: ${err.message}`, isError: true };
|
|
1071
1746
|
}
|
|
1747
|
+
pushSnapshot(snapshot);
|
|
1072
1748
|
return { content: `Wrote ${Buffer.byteLength(args.content, "utf8")} bytes to ${args.path}` };
|
|
1073
1749
|
}
|
|
1074
1750
|
};
|
|
1075
1751
|
|
|
1076
|
-
// src/tools/
|
|
1077
|
-
import { mkdir as mkdir2, readFile as readFile4, writeFile as writeFile3 } from "fs/promises";
|
|
1078
|
-
import path11 from "path";
|
|
1752
|
+
// src/tools/tasks.ts
|
|
1079
1753
|
import { z as z7 } from "zod";
|
|
1080
|
-
var
|
|
1081
|
-
id: z7.
|
|
1082
|
-
|
|
1083
|
-
|
|
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
|
+
|
|
1802
|
+
// src/tools/todo.ts
|
|
1803
|
+
import { mkdir as mkdir3, readFile as readFile6, writeFile as writeFile4 } from "fs/promises";
|
|
1804
|
+
import path14 from "path";
|
|
1805
|
+
import { z as z8 } from "zod";
|
|
1806
|
+
var todoItemSchema = z8.object({
|
|
1807
|
+
id: z8.number().int(),
|
|
1808
|
+
title: z8.string(),
|
|
1809
|
+
status: z8.enum(["pending", "in_progress", "done"])
|
|
1084
1810
|
});
|
|
1085
|
-
var writeSchema =
|
|
1086
|
-
todos:
|
|
1811
|
+
var writeSchema = z8.object({
|
|
1812
|
+
todos: z8.array(todoItemSchema).describe("The full todo list, replacing the current one")
|
|
1087
1813
|
});
|
|
1088
|
-
var readSchema =
|
|
1814
|
+
var readSchema = z8.object({});
|
|
1089
1815
|
function fileFor(cwd) {
|
|
1090
|
-
return
|
|
1816
|
+
return path14.join(cwd, ".star", "todos.json");
|
|
1091
1817
|
}
|
|
1092
1818
|
var TodoStore = class {
|
|
1093
1819
|
items = /* @__PURE__ */ new Map();
|
|
@@ -1103,7 +1829,7 @@ var TodoStore = class {
|
|
|
1103
1829
|
async load(cwd) {
|
|
1104
1830
|
let raw;
|
|
1105
1831
|
try {
|
|
1106
|
-
raw = await
|
|
1832
|
+
raw = await readFile6(fileFor(cwd), "utf8");
|
|
1107
1833
|
} catch {
|
|
1108
1834
|
return;
|
|
1109
1835
|
}
|
|
@@ -1116,8 +1842,8 @@ var TodoStore = class {
|
|
|
1116
1842
|
}
|
|
1117
1843
|
}
|
|
1118
1844
|
async save(cwd) {
|
|
1119
|
-
await
|
|
1120
|
-
await
|
|
1845
|
+
await mkdir3(path14.join(cwd, ".star"), { recursive: true });
|
|
1846
|
+
await writeFile4(fileFor(cwd), `${JSON.stringify(this.list(), null, 2)}
|
|
1121
1847
|
`);
|
|
1122
1848
|
}
|
|
1123
1849
|
};
|
|
@@ -1169,13 +1895,13 @@ function createTodoTools(store = defaultStore) {
|
|
|
1169
1895
|
}
|
|
1170
1896
|
|
|
1171
1897
|
// src/tools/web/fetch.ts
|
|
1172
|
-
import { z as
|
|
1898
|
+
import { z as z9 } from "zod";
|
|
1173
1899
|
var DEFAULT_MAX_CHARS = 2e4;
|
|
1174
1900
|
var MAX_BYTES = 2 * 1024 * 1024;
|
|
1175
1901
|
var TIMEOUT_MS = 3e4;
|
|
1176
|
-
var schema7 =
|
|
1177
|
-
url:
|
|
1178
|
-
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)")
|
|
1179
1905
|
});
|
|
1180
1906
|
function decodeEntities(s) {
|
|
1181
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) => {
|
|
@@ -1302,6 +2028,121 @@ var webFetchTool = {
|
|
|
1302
2028
|
}
|
|
1303
2029
|
};
|
|
1304
2030
|
|
|
2031
|
+
// src/tools/web/search.ts
|
|
2032
|
+
import { z as z10 } from "zod";
|
|
2033
|
+
var TIMEOUT_MS2 = 15e3;
|
|
2034
|
+
var DEFAULT_MAX_RESULTS = 5;
|
|
2035
|
+
var MAX_RESULTS_LIMIT = 10;
|
|
2036
|
+
var SEARCH_URL = "https://html.duckduckgo.com/html/?q=";
|
|
2037
|
+
var USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36";
|
|
2038
|
+
var schema8 = z10.object({
|
|
2039
|
+
query: z10.string().min(1).describe("The search query"),
|
|
2040
|
+
maxResults: z10.number().int().min(1).max(MAX_RESULTS_LIMIT).optional().describe(
|
|
2041
|
+
`Maximum number of results to return (default ${DEFAULT_MAX_RESULTS}, max ${MAX_RESULTS_LIMIT})`
|
|
2042
|
+
)
|
|
2043
|
+
});
|
|
2044
|
+
function stripTags(s) {
|
|
2045
|
+
return decodeEntities(s.replace(/<[^>]+>/g, " ")).replace(/\s+/g, " ").trim();
|
|
2046
|
+
}
|
|
2047
|
+
function normalizeUrl(href) {
|
|
2048
|
+
const decoded = decodeEntities(href);
|
|
2049
|
+
const uddg = decoded.match(/[?&]uddg=([^&]+)/);
|
|
2050
|
+
if (uddg?.[1]) {
|
|
2051
|
+
try {
|
|
2052
|
+
return decodeURIComponent(uddg[1]);
|
|
2053
|
+
} catch {
|
|
2054
|
+
return decoded;
|
|
2055
|
+
}
|
|
2056
|
+
}
|
|
2057
|
+
if (decoded.startsWith("//")) {
|
|
2058
|
+
return `https:${decoded}`;
|
|
2059
|
+
}
|
|
2060
|
+
return decoded;
|
|
2061
|
+
}
|
|
2062
|
+
function parseResults(html) {
|
|
2063
|
+
const results = [];
|
|
2064
|
+
const anchorRe = /<a\b[^>]*\bclass\s*=\s*"[^"]*\bresult__a\b[^"]*"[^>]*>[\s\S]*?<\/a>/gi;
|
|
2065
|
+
const anchors = [...html.matchAll(anchorRe)];
|
|
2066
|
+
for (const [i, anchor] of anchors.entries()) {
|
|
2067
|
+
const full = anchor[0];
|
|
2068
|
+
const hrefMatch = full.match(/href\s*=\s*"([^"]*)"/i);
|
|
2069
|
+
if (!hrefMatch?.[1]) {
|
|
2070
|
+
continue;
|
|
2071
|
+
}
|
|
2072
|
+
const url = normalizeUrl(hrefMatch[1]);
|
|
2073
|
+
const title = stripTags(full.replace(/^<a\b[^>]*>/i, "").replace(/<\/a>\s*$/i, ""));
|
|
2074
|
+
const blockStart = (anchor.index ?? 0) + full.length;
|
|
2075
|
+
const blockEnd = i + 1 < anchors.length ? anchors[i + 1]?.index ?? html.length : html.length;
|
|
2076
|
+
const block = html.slice(blockStart, blockEnd);
|
|
2077
|
+
const snippetMatch = block.match(
|
|
2078
|
+
/<(?:a|div)\b[^>]*\bclass\s*=\s*"[^"]*\bresult__snippet\b[^"]*"[^>]*>([\s\S]*?)<\/(?:a|div)>/i
|
|
2079
|
+
);
|
|
2080
|
+
const snippet = snippetMatch?.[1] ? stripTags(snippetMatch[1]) : "";
|
|
2081
|
+
if (title && url) {
|
|
2082
|
+
results.push({ title, url, snippet });
|
|
2083
|
+
}
|
|
2084
|
+
}
|
|
2085
|
+
return results;
|
|
2086
|
+
}
|
|
2087
|
+
var webSearchTool = {
|
|
2088
|
+
name: "web_search",
|
|
2089
|
+
description: "Search the web using DuckDuckGo and return a numbered list of results with title, URL and snippet. No API key required. Use web_fetch to read a result's full page.",
|
|
2090
|
+
permission: "read",
|
|
2091
|
+
parameters: schema8,
|
|
2092
|
+
async execute(args, ctx) {
|
|
2093
|
+
const maxResults = Math.min(args.maxResults ?? DEFAULT_MAX_RESULTS, MAX_RESULTS_LIMIT);
|
|
2094
|
+
const url = `${SEARCH_URL}${encodeURIComponent(args.query)}`;
|
|
2095
|
+
const timeoutSignal = AbortSignal.timeout(TIMEOUT_MS2);
|
|
2096
|
+
const signal = ctx.abortSignal ? AbortSignal.any([timeoutSignal, ctx.abortSignal]) : timeoutSignal;
|
|
2097
|
+
let res;
|
|
2098
|
+
try {
|
|
2099
|
+
res = await fetch(url, {
|
|
2100
|
+
signal,
|
|
2101
|
+
redirect: "follow",
|
|
2102
|
+
headers: { "user-agent": USER_AGENT, accept: "text/html" }
|
|
2103
|
+
});
|
|
2104
|
+
} catch (err) {
|
|
2105
|
+
if (ctx.abortSignal?.aborted) {
|
|
2106
|
+
return { content: "ERROR: request aborted", isError: true };
|
|
2107
|
+
}
|
|
2108
|
+
const name = err instanceof Error ? err.name : "";
|
|
2109
|
+
if (timeoutSignal.aborted || name === "TimeoutError") {
|
|
2110
|
+
return { content: `ERROR: search timed out after ${TIMEOUT_MS2 / 1e3}s`, isError: true };
|
|
2111
|
+
}
|
|
2112
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2113
|
+
return { content: `ERROR: search request failed: ${message}`, isError: true };
|
|
2114
|
+
}
|
|
2115
|
+
if (!res.ok) {
|
|
2116
|
+
await res.body?.cancel().catch(() => {
|
|
2117
|
+
});
|
|
2118
|
+
return {
|
|
2119
|
+
content: `ERROR: HTTP ${res.status} ${res.statusText} from DuckDuckGo`,
|
|
2120
|
+
isError: true
|
|
2121
|
+
};
|
|
2122
|
+
}
|
|
2123
|
+
let html;
|
|
2124
|
+
try {
|
|
2125
|
+
html = await res.text();
|
|
2126
|
+
} catch (err) {
|
|
2127
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2128
|
+
return { content: `ERROR: failed to read search response: ${message}`, isError: true };
|
|
2129
|
+
}
|
|
2130
|
+
const results = parseResults(html).slice(0, maxResults);
|
|
2131
|
+
if (results.length === 0) {
|
|
2132
|
+
return {
|
|
2133
|
+
content: `ERROR: no results found for "${args.query}" (the response may have been blocked or the page layout changed)`,
|
|
2134
|
+
isError: true
|
|
2135
|
+
};
|
|
2136
|
+
}
|
|
2137
|
+
const lines = results.map(
|
|
2138
|
+
(r, i) => `${i + 1}. ${r.title}
|
|
2139
|
+
${r.url}${r.snippet ? `
|
|
2140
|
+
${r.snippet}` : ""}`
|
|
2141
|
+
);
|
|
2142
|
+
return { content: lines.join("\n") };
|
|
2143
|
+
}
|
|
2144
|
+
};
|
|
2145
|
+
|
|
1305
2146
|
// src/tools/registry.ts
|
|
1306
2147
|
var ToolRegistry = class {
|
|
1307
2148
|
tools = /* @__PURE__ */ new Map();
|
|
@@ -1314,6 +2155,10 @@ var ToolRegistry = class {
|
|
|
1314
2155
|
grepTool,
|
|
1315
2156
|
bashTool,
|
|
1316
2157
|
webFetchTool,
|
|
2158
|
+
webSearchTool,
|
|
2159
|
+
taskListTool,
|
|
2160
|
+
taskOutputTool,
|
|
2161
|
+
taskKillTool,
|
|
1317
2162
|
...createTodoTools()
|
|
1318
2163
|
]) {
|
|
1319
2164
|
this.register(tool);
|
|
@@ -1342,6 +2187,155 @@ function createDefaultRegistry() {
|
|
|
1342
2187
|
return registry;
|
|
1343
2188
|
}
|
|
1344
2189
|
|
|
2190
|
+
// src/version.ts
|
|
2191
|
+
import fs3 from "fs";
|
|
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";
|
|
2211
|
+
var MIN_COMPACT_MESSAGES = 4;
|
|
2212
|
+
async function compactSession(opts) {
|
|
2213
|
+
const { backend, sessionStore, config, model } = opts;
|
|
2214
|
+
if (!(backend instanceof AgentLoop)) {
|
|
2215
|
+
return { message: "Current backend does not support compaction.", compacted: false };
|
|
2216
|
+
}
|
|
2217
|
+
const messages = [...backend.getMessages()];
|
|
2218
|
+
if (messages.length < MIN_COMPACT_MESSAGES) {
|
|
2219
|
+
return {
|
|
2220
|
+
message: `Nothing to compact: history has only ${messages.length} message(s).`,
|
|
2221
|
+
compacted: false
|
|
2222
|
+
};
|
|
2223
|
+
}
|
|
2224
|
+
const beforeTokens = estimateTokens(messages);
|
|
2225
|
+
const compacted = compactMessages(messages, config.contextMaxTokens);
|
|
2226
|
+
if (!compacted.compacted) {
|
|
2227
|
+
return {
|
|
2228
|
+
message: `Nothing to compact: ~${beforeTokens} estimated tokens, below the limit of ${config.contextMaxTokens}.`,
|
|
2229
|
+
compacted: false
|
|
2230
|
+
};
|
|
2231
|
+
}
|
|
2232
|
+
const next = await applyCompactionSummary(messages, compacted, config, model);
|
|
2233
|
+
await backend.loadMessages(next);
|
|
2234
|
+
await sessionStore?.replaceMessages(next);
|
|
2235
|
+
const afterTokens = estimateTokens(next);
|
|
2236
|
+
return {
|
|
2237
|
+
message: `Compacted context: ${messages.length} -> ${next.length} messages (~${beforeTokens} -> ~${afterTokens} estimated tokens).`,
|
|
2238
|
+
compacted: true,
|
|
2239
|
+
messages: next
|
|
2240
|
+
};
|
|
2241
|
+
}
|
|
2242
|
+
async function applyCompactionSummary(original, compacted, config, model) {
|
|
2243
|
+
if (config.contextCompaction !== "summary" || !model) {
|
|
2244
|
+
return compacted.messages;
|
|
2245
|
+
}
|
|
2246
|
+
const headCount = compacted.messages[0]?.role === "system" ? 1 : 0;
|
|
2247
|
+
const dropped = original.slice(headCount, headCount + compacted.droppedCount);
|
|
2248
|
+
try {
|
|
2249
|
+
const summary = await summarizeMessages(dropped, model);
|
|
2250
|
+
const messages = compacted.messages.slice();
|
|
2251
|
+
messages[headCount] = {
|
|
2252
|
+
role: "user",
|
|
2253
|
+
content: `[earlier conversation summarized]
|
|
2254
|
+
${summary}`
|
|
2255
|
+
};
|
|
2256
|
+
return messages;
|
|
2257
|
+
} catch {
|
|
2258
|
+
return compacted.messages;
|
|
2259
|
+
}
|
|
2260
|
+
}
|
|
2261
|
+
async function exportSession(opts) {
|
|
2262
|
+
const { backend, sessionStore, cwd, arg } = opts;
|
|
2263
|
+
if (!(backend instanceof AgentLoop)) {
|
|
2264
|
+
return "Current backend does not support exporting sessions.";
|
|
2265
|
+
}
|
|
2266
|
+
const messages = [...backend.getMessages()];
|
|
2267
|
+
if (messages.length === 0) {
|
|
2268
|
+
return "Nothing to export: the session has no messages.";
|
|
2269
|
+
}
|
|
2270
|
+
const sessionId = sessionStore?.id ?? `unknown-${Date.now()}`;
|
|
2271
|
+
const target = arg ? path16.resolve(cwd, arg) : path16.resolve(cwd, `star-session-${sessionId}.md`);
|
|
2272
|
+
const existed = await fileExists(target);
|
|
2273
|
+
await fs4.mkdir(path16.dirname(target), { recursive: true });
|
|
2274
|
+
await fs4.writeFile(target, renderSessionMarkdown(sessionId, messages));
|
|
2275
|
+
return existed ? `Exported ${messages.length} messages to ${target} (overwrote existing file).` : `Exported ${messages.length} messages to ${target}.`;
|
|
2276
|
+
}
|
|
2277
|
+
async function fileExists(target) {
|
|
2278
|
+
try {
|
|
2279
|
+
await fs4.access(target);
|
|
2280
|
+
return true;
|
|
2281
|
+
} catch {
|
|
2282
|
+
return false;
|
|
2283
|
+
}
|
|
2284
|
+
}
|
|
2285
|
+
function renderSessionMarkdown(sessionId, messages, exportedAt = /* @__PURE__ */ new Date()) {
|
|
2286
|
+
const lines = [
|
|
2287
|
+
`# Star CLI Session ${sessionId}`,
|
|
2288
|
+
"",
|
|
2289
|
+
`_Exported at ${exportedAt.toISOString()}_`,
|
|
2290
|
+
""
|
|
2291
|
+
];
|
|
2292
|
+
for (const message of messages) {
|
|
2293
|
+
lines.push(`## ${roleHeading(message.role)}`, "");
|
|
2294
|
+
lines.push(...renderContent(message), "");
|
|
2295
|
+
}
|
|
2296
|
+
return lines.join("\n");
|
|
2297
|
+
}
|
|
2298
|
+
function roleHeading(role) {
|
|
2299
|
+
return role.charAt(0).toUpperCase() + role.slice(1);
|
|
2300
|
+
}
|
|
2301
|
+
function renderContent(message) {
|
|
2302
|
+
const content = message.content;
|
|
2303
|
+
if (typeof content === "string") return [content];
|
|
2304
|
+
const lines = [];
|
|
2305
|
+
for (const part of content) {
|
|
2306
|
+
if (part.type === "text") {
|
|
2307
|
+
lines.push(part.text, "");
|
|
2308
|
+
} else if (part.type === "tool-call") {
|
|
2309
|
+
lines.push(
|
|
2310
|
+
`**Tool call: ${part.toolName}**`,
|
|
2311
|
+
"",
|
|
2312
|
+
"```json",
|
|
2313
|
+
JSON.stringify(part.args, null, 2),
|
|
2314
|
+
"```",
|
|
2315
|
+
""
|
|
2316
|
+
);
|
|
2317
|
+
} else if (part.type === "tool-result") {
|
|
2318
|
+
lines.push(
|
|
2319
|
+
`**Tool result: ${part.toolName}**`,
|
|
2320
|
+
"",
|
|
2321
|
+
"```",
|
|
2322
|
+
stringifyPart(part.result),
|
|
2323
|
+
"```",
|
|
2324
|
+
""
|
|
2325
|
+
);
|
|
2326
|
+
} else if (part.type === "reasoning") {
|
|
2327
|
+
lines.push(part.text, "");
|
|
2328
|
+
} else {
|
|
2329
|
+
lines.push(`[${part.type}]`, "");
|
|
2330
|
+
}
|
|
2331
|
+
}
|
|
2332
|
+
while (lines.length > 0 && lines[lines.length - 1] === "") lines.pop();
|
|
2333
|
+
return lines;
|
|
2334
|
+
}
|
|
2335
|
+
function stringifyPart(value) {
|
|
2336
|
+
return typeof value === "string" ? value : JSON.stringify(value, null, 2);
|
|
2337
|
+
}
|
|
2338
|
+
|
|
1345
2339
|
// src/cli/commands/builtin.ts
|
|
1346
2340
|
function registerBuiltinCommands(registry) {
|
|
1347
2341
|
registry.register({
|
|
@@ -1405,30 +2399,498 @@ ${lines.join("\n")}`);
|
|
|
1405
2399
|
}
|
|
1406
2400
|
}
|
|
1407
2401
|
});
|
|
1408
|
-
registry.register({
|
|
1409
|
-
name: "todo",
|
|
1410
|
-
description: "Show the current todo list",
|
|
1411
|
-
usage: "/todo",
|
|
1412
|
-
async run(_args, ctx) {
|
|
1413
|
-
ctx.addSystemMessage(await ctx.showTodos());
|
|
2402
|
+
registry.register({
|
|
2403
|
+
name: "todo",
|
|
2404
|
+
description: "Show the current todo list",
|
|
2405
|
+
usage: "/todo",
|
|
2406
|
+
async run(_args, ctx) {
|
|
2407
|
+
ctx.addSystemMessage(await ctx.showTodos());
|
|
2408
|
+
}
|
|
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
|
+
});
|
|
2418
|
+
registry.register({
|
|
2419
|
+
name: "cost",
|
|
2420
|
+
description: "Show API token usage for this session",
|
|
2421
|
+
usage: "/cost",
|
|
2422
|
+
run(_args, ctx) {
|
|
2423
|
+
ctx.addSystemMessage(ctx.showUsage());
|
|
2424
|
+
}
|
|
2425
|
+
});
|
|
2426
|
+
registry.register({
|
|
2427
|
+
name: "config",
|
|
2428
|
+
description: "Show the current configuration",
|
|
2429
|
+
usage: "/config",
|
|
2430
|
+
run(_args, ctx) {
|
|
2431
|
+
ctx.addSystemMessage(ctx.describeConfig());
|
|
2432
|
+
}
|
|
2433
|
+
});
|
|
2434
|
+
registry.register({
|
|
2435
|
+
name: "compact",
|
|
2436
|
+
description: "Compact the conversation history to free up context",
|
|
2437
|
+
usage: "/compact",
|
|
2438
|
+
async run(_args, ctx) {
|
|
2439
|
+
ctx.addSystemMessage(await ctx.compactContext());
|
|
2440
|
+
}
|
|
2441
|
+
});
|
|
2442
|
+
registry.register({
|
|
2443
|
+
name: "export",
|
|
2444
|
+
description: "Export the current session to a Markdown file",
|
|
2445
|
+
usage: "/export [path]",
|
|
2446
|
+
async run(args, ctx) {
|
|
2447
|
+
ctx.addSystemMessage(await ctx.exportSession(args));
|
|
2448
|
+
}
|
|
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
|
+
});
|
|
2466
|
+
registry.register({
|
|
2467
|
+
name: "undo",
|
|
2468
|
+
description: "Undo the last conversation turn: revert its file changes (write_file/edit_file) and retract its messages",
|
|
2469
|
+
usage: "/undo",
|
|
2470
|
+
async run(_args, ctx) {
|
|
2471
|
+
ctx.addSystemMessage(await ctx.undo());
|
|
2472
|
+
}
|
|
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
|
+
});
|
|
1414
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}`
|
|
1415
2667
|
});
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
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
|
|
1423
2680
|
});
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
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 {
|
|
1430
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.`
|
|
1431
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
|
+
};
|
|
1432
2894
|
}
|
|
1433
2895
|
|
|
1434
2896
|
// src/cli/commands/registry.ts
|
|
@@ -1460,21 +2922,67 @@ function parseSlashCommand(input) {
|
|
|
1460
2922
|
};
|
|
1461
2923
|
}
|
|
1462
2924
|
|
|
1463
|
-
// src/cli/components/
|
|
2925
|
+
// src/cli/components/ExitPlanPrompt.tsx
|
|
1464
2926
|
import { Box, Text, useInput } from "ink";
|
|
1465
|
-
import { useRef, useState } from "react";
|
|
1466
2927
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
1467
|
-
function
|
|
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";
|
|
2943
|
+
import { useRef, useState } from "react";
|
|
2944
|
+
|
|
2945
|
+
// src/cli/commands/suggest.ts
|
|
2946
|
+
var MAX_SUGGESTIONS = 5;
|
|
2947
|
+
function filterCommands(input, commands) {
|
|
2948
|
+
if (!input.startsWith("/")) return [];
|
|
2949
|
+
if (/\s/.test(input)) return [];
|
|
2950
|
+
const prefix = input.slice(1).toLowerCase();
|
|
2951
|
+
return commands.filter((cmd) => cmd.name.toLowerCase().startsWith(prefix)).sort((a, b) => a.name.localeCompare(b.name)).slice(0, MAX_SUGGESTIONS);
|
|
2952
|
+
}
|
|
2953
|
+
|
|
2954
|
+
// src/cli/components/InputBox.tsx
|
|
2955
|
+
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
2956
|
+
function InputBox({
|
|
2957
|
+
isStreaming,
|
|
2958
|
+
disabled,
|
|
2959
|
+
commands,
|
|
2960
|
+
onSubmit,
|
|
2961
|
+
onInterrupt,
|
|
2962
|
+
onExit
|
|
2963
|
+
}) {
|
|
1468
2964
|
const [value, setValue] = useState("");
|
|
1469
2965
|
const [cursor, setCursor] = useState(0);
|
|
1470
2966
|
const [history, setHistory] = useState([]);
|
|
2967
|
+
const [highlight, setHighlight] = useState(0);
|
|
2968
|
+
const [suggestionsDismissed, setSuggestionsDismissed] = useState(false);
|
|
1471
2969
|
const historyIndexRef = useRef(null);
|
|
1472
2970
|
const draftRef = useRef("");
|
|
1473
2971
|
const edit = (next, nextCursor) => {
|
|
1474
2972
|
setValue(next);
|
|
1475
2973
|
setCursor(Math.max(0, Math.min(nextCursor, next.length)));
|
|
2974
|
+
setHighlight(0);
|
|
2975
|
+
setSuggestionsDismissed(false);
|
|
1476
2976
|
};
|
|
1477
|
-
|
|
2977
|
+
const suggestions = isStreaming || disabled || suggestionsDismissed || !commands ? [] : filterCommands(value, commands);
|
|
2978
|
+
const activeIndex = suggestions.length === 0 ? 0 : Math.min(highlight, suggestions.length - 1);
|
|
2979
|
+
const completeHighlighted = () => {
|
|
2980
|
+
const cmd = suggestions[activeIndex];
|
|
2981
|
+
if (!cmd) return;
|
|
2982
|
+
const text = `/${cmd.name} `;
|
|
2983
|
+
edit(text, text.length);
|
|
2984
|
+
};
|
|
2985
|
+
useInput2((input, key) => {
|
|
1478
2986
|
if (key.ctrl && input === "c") {
|
|
1479
2987
|
if (isStreaming || disabled) {
|
|
1480
2988
|
onInterrupt();
|
|
@@ -1489,6 +2997,10 @@ function InputBox({ isStreaming, disabled, onSubmit, onInterrupt, onExit }) {
|
|
|
1489
2997
|
return;
|
|
1490
2998
|
}
|
|
1491
2999
|
if (isStreaming || disabled) return;
|
|
3000
|
+
if (key.escape) {
|
|
3001
|
+
if (suggestions.length > 0) setSuggestionsDismissed(true);
|
|
3002
|
+
return;
|
|
3003
|
+
}
|
|
1492
3004
|
if (key.return) {
|
|
1493
3005
|
const text = value.trim();
|
|
1494
3006
|
if (text.length > 0) {
|
|
@@ -1500,7 +3012,17 @@ function InputBox({ isStreaming, disabled, onSubmit, onInterrupt, onExit }) {
|
|
|
1500
3012
|
draftRef.current = "";
|
|
1501
3013
|
return;
|
|
1502
3014
|
}
|
|
1503
|
-
if (key.
|
|
3015
|
+
if (key.tab) {
|
|
3016
|
+
if (key.shift) return;
|
|
3017
|
+
if (suggestions.length > 0) {
|
|
3018
|
+
completeHighlighted();
|
|
3019
|
+
return;
|
|
3020
|
+
}
|
|
3021
|
+
} else if (key.upArrow) {
|
|
3022
|
+
if (suggestions.length > 0) {
|
|
3023
|
+
setHighlight((prev) => (prev - 1 + suggestions.length) % suggestions.length);
|
|
3024
|
+
return;
|
|
3025
|
+
}
|
|
1504
3026
|
if (history.length === 0) return;
|
|
1505
3027
|
if (historyIndexRef.current === null) {
|
|
1506
3028
|
draftRef.current = value;
|
|
@@ -1511,8 +3033,11 @@ function InputBox({ isStreaming, disabled, onSubmit, onInterrupt, onExit }) {
|
|
|
1511
3033
|
const entry = history[historyIndexRef.current] ?? "";
|
|
1512
3034
|
edit(entry, entry.length);
|
|
1513
3035
|
return;
|
|
1514
|
-
}
|
|
1515
|
-
|
|
3036
|
+
} else if (key.downArrow) {
|
|
3037
|
+
if (suggestions.length > 0) {
|
|
3038
|
+
setHighlight((prev) => (prev + 1) % suggestions.length);
|
|
3039
|
+
return;
|
|
3040
|
+
}
|
|
1516
3041
|
if (historyIndexRef.current === null) return;
|
|
1517
3042
|
if (historyIndexRef.current < history.length - 1) {
|
|
1518
3043
|
historyIndexRef.current += 1;
|
|
@@ -1523,13 +3048,15 @@ function InputBox({ isStreaming, disabled, onSubmit, onInterrupt, onExit }) {
|
|
|
1523
3048
|
edit(draftRef.current, draftRef.current.length);
|
|
1524
3049
|
}
|
|
1525
3050
|
return;
|
|
1526
|
-
}
|
|
1527
|
-
if (key.leftArrow) {
|
|
3051
|
+
} else if (key.leftArrow) {
|
|
1528
3052
|
setCursor((prev) => Math.max(0, prev - 1));
|
|
1529
3053
|
return;
|
|
1530
|
-
}
|
|
1531
|
-
|
|
1532
|
-
|
|
3054
|
+
} else if (key.rightArrow) {
|
|
3055
|
+
if (suggestions.length > 0 && cursor === value.length) {
|
|
3056
|
+
completeHighlighted();
|
|
3057
|
+
} else {
|
|
3058
|
+
setCursor((prev) => Math.min(value.length, prev + 1));
|
|
3059
|
+
}
|
|
1533
3060
|
return;
|
|
1534
3061
|
}
|
|
1535
3062
|
if (key.ctrl && input === "a") {
|
|
@@ -1567,169 +3094,457 @@ function InputBox({ isStreaming, disabled, onSubmit, onInterrupt, onExit }) {
|
|
|
1567
3094
|
const before = value.slice(0, cursor);
|
|
1568
3095
|
const at = value[cursor] ?? " ";
|
|
1569
3096
|
const after = value.slice(cursor + 1);
|
|
1570
|
-
return /* @__PURE__ */
|
|
1571
|
-
/* @__PURE__ */
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
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 })
|
|
3103
|
+
] }),
|
|
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}` })
|
|
3108
|
+
] }, cmd.name)
|
|
3109
|
+
) })
|
|
1575
3110
|
] });
|
|
1576
3111
|
}
|
|
1577
3112
|
|
|
1578
3113
|
// src/cli/components/MessageList.tsx
|
|
1579
|
-
import { Box as
|
|
1580
|
-
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";
|
|
1581
3117
|
var roleStyles = {
|
|
1582
3118
|
user: { label: "you", color: "cyan" },
|
|
1583
3119
|
assistant: { label: "star", color: "green" },
|
|
1584
3120
|
system: { label: "system", color: "yellow" },
|
|
1585
3121
|
tool: { label: "tool", color: "magenta" }
|
|
1586
3122
|
};
|
|
1587
|
-
|
|
1588
|
-
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
|
+
}
|
|
1589
3129
|
const style = roleStyles[message.role];
|
|
1590
|
-
return /* @__PURE__ */
|
|
1591
|
-
/* @__PURE__ */
|
|
1592
|
-
/* @__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 })
|
|
1593
3134
|
] }, message.id);
|
|
1594
3135
|
} });
|
|
1595
|
-
}
|
|
3136
|
+
});
|
|
1596
3137
|
|
|
1597
3138
|
// src/cli/components/PermissionPrompt.tsx
|
|
1598
|
-
import { Box as
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
}
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
}
|
|
1615
|
-
function coreMessageText(message) {
|
|
1616
|
-
const content = message.content;
|
|
1617
|
-
if (typeof content === "string") return content;
|
|
1618
|
-
if (Array.isArray(content)) {
|
|
1619
|
-
return content.filter((part) => part.type === "text").map((part) => "text" in part ? part.text : "").join(" ");
|
|
1620
|
-
}
|
|
1621
|
-
return "";
|
|
1622
|
-
}
|
|
1623
|
-
function buildDisplayMessages(messages) {
|
|
1624
|
-
const display = [];
|
|
1625
|
-
let collapsed = 0;
|
|
1626
|
-
for (const message of messages) {
|
|
1627
|
-
if (message.role === "user" || message.role === "assistant") {
|
|
1628
|
-
const text = coreMessageText(message);
|
|
1629
|
-
if (text) {
|
|
1630
|
-
display.push({ id: display.length, role: message.role, text });
|
|
1631
|
-
} else {
|
|
1632
|
-
collapsed++;
|
|
1633
|
-
}
|
|
1634
|
-
} else if (message.role === "tool") {
|
|
1635
|
-
collapsed++;
|
|
1636
|
-
}
|
|
1637
|
-
}
|
|
1638
|
-
if (collapsed > 0) {
|
|
1639
|
-
display.push({
|
|
1640
|
-
id: display.length,
|
|
1641
|
-
role: "system",
|
|
1642
|
-
text: `\u5DF2\u6062\u590D ${collapsed} \u6761\u5386\u53F2\u6D88\u606F`
|
|
1643
|
-
});
|
|
1644
|
-
}
|
|
1645
|
-
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
|
+
] });
|
|
1646
3155
|
}
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
1650
|
-
function PermissionPrompt({ request, onDecision }) {
|
|
1651
|
-
useInput2((input) => {
|
|
3156
|
+
function PermissionPrompt({ request, preview, onDecision }) {
|
|
3157
|
+
useInput3((input) => {
|
|
1652
3158
|
const ch = input.toLowerCase();
|
|
1653
3159
|
if (ch === "y") onDecision("yes");
|
|
1654
3160
|
else if (ch === "n") onDecision("no");
|
|
1655
3161
|
else if (ch === "a") onDecision("always");
|
|
1656
3162
|
});
|
|
1657
|
-
return /* @__PURE__ */
|
|
1658
|
-
/* @__PURE__ */
|
|
3163
|
+
return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", borderStyle: "round", borderColor: "yellow", paddingX: 1, children: [
|
|
3164
|
+
/* @__PURE__ */ jsxs4(Text4, { bold: true, color: "yellow", children: [
|
|
1659
3165
|
"Permission required: ",
|
|
1660
3166
|
request.toolName,
|
|
1661
3167
|
" (",
|
|
1662
3168
|
request.level,
|
|
1663
3169
|
")"
|
|
1664
3170
|
] }),
|
|
1665
|
-
/* @__PURE__ */
|
|
1666
|
-
|
|
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)" })
|
|
3182
|
+
] });
|
|
3183
|
+
}
|
|
3184
|
+
|
|
3185
|
+
// src/cli/components/StatusBar.tsx
|
|
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: [
|
|
3200
|
+
"model: ",
|
|
3201
|
+
model
|
|
3202
|
+
] }),
|
|
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: [
|
|
3211
|
+
tokens,
|
|
3212
|
+
" tokens"
|
|
3213
|
+
] }),
|
|
3214
|
+
/* @__PURE__ */ jsxs5(Text5, { dimColor: true, children: [
|
|
3215
|
+
"mode: ",
|
|
3216
|
+
permissionMode
|
|
3217
|
+
] })
|
|
3218
|
+
] });
|
|
3219
|
+
});
|
|
3220
|
+
|
|
3221
|
+
// src/cli/components/StreamingMessage.tsx
|
|
3222
|
+
import { Box as Box6, Text as Text6 } from "ink";
|
|
3223
|
+
import { memo as memo3 } from "react";
|
|
3224
|
+
import { jsx as jsx5, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
3225
|
+
var StreamingMessage = memo3(function StreamingMessage2({
|
|
3226
|
+
text,
|
|
3227
|
+
continuation
|
|
3228
|
+
}) {
|
|
3229
|
+
return /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", marginBottom: 1, children: [
|
|
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"
|
|
3258
|
+
] }),
|
|
3259
|
+
tail !== "" && /* @__PURE__ */ jsxs7(Text7, { dimColor: true, children: [
|
|
3260
|
+
" ",
|
|
3261
|
+
tail
|
|
3262
|
+
] })
|
|
1667
3263
|
] });
|
|
1668
3264
|
}
|
|
1669
3265
|
|
|
1670
|
-
// src/cli/components/
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
3266
|
+
// src/cli/components/ToolCallCard.tsx
|
|
3267
|
+
function formatToolCard(card) {
|
|
3268
|
+
const header = `${card.name} ${card.argsSummary}${card.isError ? " [error]" : ""}`;
|
|
3269
|
+
if (card.result === void 0) return header;
|
|
3270
|
+
const preview = previewLines(card.result, 10);
|
|
3271
|
+
return `${header}
|
|
3272
|
+
${preview.text}${preview.truncated ? "\n... (truncated)" : ""}`;
|
|
3273
|
+
}
|
|
3274
|
+
|
|
3275
|
+
// src/cli/cost.ts
|
|
3276
|
+
function formatDollars(cost) {
|
|
3277
|
+
let s = cost.toFixed(4).replace(/0+$/, "");
|
|
3278
|
+
if (s.endsWith(".")) {
|
|
3279
|
+
s += "00";
|
|
3280
|
+
} else if (s.split(".")[1]?.length === 1) {
|
|
3281
|
+
s += "0";
|
|
3282
|
+
}
|
|
3283
|
+
return s;
|
|
3284
|
+
}
|
|
3285
|
+
function estimateCost(usage, modelName, model) {
|
|
3286
|
+
if (model?.promptPrice === void 0 || model.completionPrice === void 0) {
|
|
3287
|
+
return `Estimated cost: unknown (no price configured for ${modelName})`;
|
|
3288
|
+
}
|
|
3289
|
+
const cost = (usage.promptTokens * model.promptPrice + usage.completionTokens * model.completionPrice) / 1e6;
|
|
3290
|
+
return `Estimated cost: $${formatDollars(cost)} (${model.name} @ $${model.promptPrice}/M prompt, $${model.completionPrice}/M completion)`;
|
|
3291
|
+
}
|
|
3292
|
+
|
|
3293
|
+
// src/cli/diff-preview.ts
|
|
3294
|
+
import { readFile as readFile7 } from "fs/promises";
|
|
3295
|
+
import path20 from "path";
|
|
3296
|
+
var CONTEXT_LINES = 2;
|
|
3297
|
+
var MAX_HUNK_LINES = 15;
|
|
3298
|
+
var MAX_TOTAL_LINES = 12;
|
|
3299
|
+
var NEW_FILE_PREVIEW_LINES = 10;
|
|
3300
|
+
var MAX_DP_CELLS = 25e4;
|
|
3301
|
+
function diffOps(oldText, newText) {
|
|
3302
|
+
const a = oldText.split("\n");
|
|
3303
|
+
const b = newText.split("\n");
|
|
3304
|
+
let start = 0;
|
|
3305
|
+
while (start < a.length && start < b.length && a[start] === b[start]) start += 1;
|
|
3306
|
+
let endA = a.length;
|
|
3307
|
+
let endB = b.length;
|
|
3308
|
+
while (endA > start && endB > start && a[endA - 1] === b[endB - 1]) {
|
|
3309
|
+
endA -= 1;
|
|
3310
|
+
endB -= 1;
|
|
3311
|
+
}
|
|
3312
|
+
const prefix = a.slice(0, start).map((text) => ({ kind: "context", text }));
|
|
3313
|
+
const suffix = a.slice(endA).map((text) => ({ kind: "context", text }));
|
|
3314
|
+
const midA = a.slice(start, endA);
|
|
3315
|
+
const midB = b.slice(start, endB);
|
|
3316
|
+
const prefixLen = prefix.length;
|
|
3317
|
+
const ops = [...prefix];
|
|
3318
|
+
if (midA.length * midB.length > MAX_DP_CELLS) {
|
|
3319
|
+
for (const text of midA) ops.push({ kind: "del", text });
|
|
3320
|
+
for (const text of midB) ops.push({ kind: "add", text });
|
|
3321
|
+
} else if (midA.length > 0 || midB.length > 0) {
|
|
3322
|
+
const rows = midA.length + 1;
|
|
3323
|
+
const cols = midB.length + 1;
|
|
3324
|
+
const dp = new Uint32Array(rows * cols);
|
|
3325
|
+
const cell = (r, c) => dp[r * cols + c] ?? 0;
|
|
3326
|
+
for (let i2 = 1; i2 < rows; i2++) {
|
|
3327
|
+
for (let j2 = 1; j2 < cols; j2++) {
|
|
3328
|
+
dp[i2 * cols + j2] = midA[i2 - 1] === midB[j2 - 1] ? cell(i2 - 1, j2 - 1) + 1 : Math.max(cell(i2 - 1, j2), cell(i2, j2 - 1));
|
|
3329
|
+
}
|
|
3330
|
+
}
|
|
3331
|
+
const lineAt = (lines, index) => lines[index] ?? "";
|
|
3332
|
+
const middle = [];
|
|
3333
|
+
let i = midA.length;
|
|
3334
|
+
let j = midB.length;
|
|
3335
|
+
while (i > 0 && j > 0) {
|
|
3336
|
+
if (midA[i - 1] === midB[j - 1]) {
|
|
3337
|
+
middle.push({ kind: "context", text: lineAt(midA, i - 1) });
|
|
3338
|
+
i -= 1;
|
|
3339
|
+
j -= 1;
|
|
3340
|
+
} else if (cell(i, j - 1) >= cell(i - 1, j)) {
|
|
3341
|
+
middle.push({ kind: "add", text: lineAt(midB, j - 1) });
|
|
3342
|
+
j -= 1;
|
|
3343
|
+
} else {
|
|
3344
|
+
middle.push({ kind: "del", text: lineAt(midA, i - 1) });
|
|
3345
|
+
i -= 1;
|
|
3346
|
+
}
|
|
3347
|
+
}
|
|
3348
|
+
while (i > 0) {
|
|
3349
|
+
middle.push({ kind: "del", text: lineAt(midA, i - 1) });
|
|
3350
|
+
i -= 1;
|
|
3351
|
+
}
|
|
3352
|
+
while (j > 0) {
|
|
3353
|
+
middle.push({ kind: "add", text: lineAt(midB, j - 1) });
|
|
3354
|
+
j -= 1;
|
|
3355
|
+
}
|
|
3356
|
+
middle.reverse();
|
|
3357
|
+
ops.push(...middle);
|
|
3358
|
+
}
|
|
3359
|
+
ops.push(...suffix);
|
|
3360
|
+
return ops.length === prefixLen + suffix.length ? [] : ops;
|
|
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
|
+
};
|
|
1688
3490
|
}
|
|
1689
3491
|
|
|
1690
|
-
// src/cli/
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
return
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
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);
|
|
1698
3505
|
}
|
|
1699
3506
|
|
|
1700
|
-
// src/cli/
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
function
|
|
1704
|
-
|
|
1705
|
-
const preview = card.result !== void 0 ? previewLines(card.result, 10) : null;
|
|
1706
|
-
return /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", marginBottom: 1, children: [
|
|
1707
|
-
/* @__PURE__ */ jsxs6(Text6, { bold: true, color, children: [
|
|
1708
|
-
"tool: ",
|
|
1709
|
-
card.name,
|
|
1710
|
-
card.isError ? " (error)" : ""
|
|
1711
|
-
] }),
|
|
1712
|
-
/* @__PURE__ */ jsx5(Text6, { dimColor: true, children: card.argsSummary }),
|
|
1713
|
-
preview && /* @__PURE__ */ jsxs6(Text6, { color, children: [
|
|
1714
|
-
preview.text,
|
|
1715
|
-
preview.truncated ? "\n... (truncated)" : ""
|
|
1716
|
-
] })
|
|
1717
|
-
] });
|
|
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) });
|
|
1718
3512
|
}
|
|
1719
|
-
function
|
|
1720
|
-
const
|
|
1721
|
-
|
|
1722
|
-
const
|
|
1723
|
-
|
|
1724
|
-
|
|
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
|
+
}
|
|
1725
3536
|
}
|
|
1726
3537
|
|
|
1727
3538
|
// src/cli/repl.tsx
|
|
1728
|
-
import { jsx as jsx6, jsxs as
|
|
1729
|
-
var
|
|
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";
|
|
1730
3541
|
function emptyUsage() {
|
|
1731
3542
|
return { requests: 0, promptTokens: 0, completionTokens: 0, totalTokens: 0 };
|
|
1732
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"];
|
|
1733
3548
|
function formatUsage(usage) {
|
|
1734
3549
|
return `API usage this session: ${usage.requests} requests, ${usage.promptTokens} prompt + ${usage.completionTokens} completion = ${usage.totalTokens} tokens`;
|
|
1735
3550
|
}
|
|
@@ -1749,48 +3564,129 @@ function Repl({
|
|
|
1749
3564
|
const [epoch, setEpoch] = useState2(0);
|
|
1750
3565
|
const [streamingText, setStreamingText] = useState2(null);
|
|
1751
3566
|
const [isStreaming, setIsStreaming] = useState2(false);
|
|
1752
|
-
const [
|
|
3567
|
+
const [thinking, setThinking] = useState2(false);
|
|
3568
|
+
const [thinkingText, setThinkingText] = useState2("");
|
|
3569
|
+
const [thoughtSummary, setThoughtSummary] = useState2(null);
|
|
3570
|
+
const [, setUsageVersion] = useState2(0);
|
|
1753
3571
|
const [modelName, setModelName] = useState2(model);
|
|
1754
3572
|
const [pending, setPending] = useState2(null);
|
|
1755
|
-
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);
|
|
1756
3578
|
const backendRef = useRef2(backend);
|
|
1757
3579
|
const nextIdRef = useRef2(initialDisplay.length);
|
|
1758
3580
|
const abortRef = useRef2(null);
|
|
1759
3581
|
const streamedRef = useRef2("");
|
|
1760
|
-
const
|
|
3582
|
+
const thinkingRef = useRef2(false);
|
|
3583
|
+
const reasoningRef = useRef2("");
|
|
3584
|
+
const tickerStopRef = useRef2(null);
|
|
3585
|
+
const flushedRef = useRef2({ streamed: "", reasoning: "" });
|
|
1761
3586
|
const toolCardsRef = useRef2(/* @__PURE__ */ new Map());
|
|
1762
3587
|
const pendingRef = useRef2(null);
|
|
1763
3588
|
const alwaysAllowedRef = useRef2(/* @__PURE__ */ new Set());
|
|
3589
|
+
const prevModeRef = useRef2(null);
|
|
3590
|
+
const planApprovalRef = useRef2(false);
|
|
1764
3591
|
const modelNameRef = useRef2(model);
|
|
1765
3592
|
const usageRef = useRef2(initialUsage ? { ...initialUsage } : emptyUsage());
|
|
1766
|
-
const
|
|
1767
|
-
|
|
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);
|
|
1768
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
|
+
);
|
|
1769
3627
|
const setPendingPermission = useCallback((p) => {
|
|
1770
3628
|
pendingRef.current = p;
|
|
1771
3629
|
setPending(p);
|
|
1772
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
|
+
);
|
|
1773
3648
|
const attachConfirmHandler = useCallback(
|
|
1774
3649
|
(target) => {
|
|
1775
|
-
target.confirmHandler = (req) => {
|
|
1776
|
-
if (alwaysAllowedRef.current
|
|
1777
|
-
return
|
|
3650
|
+
target.confirmHandler = async (req) => {
|
|
3651
|
+
if (isAllowedByRules([...alwaysAllowedRef.current], req)) {
|
|
3652
|
+
return true;
|
|
1778
3653
|
}
|
|
3654
|
+
const preview = await generateDiffPreview(req.toolName, req.args, cwd).catch(() => null);
|
|
1779
3655
|
return new Promise((resolve) => {
|
|
1780
|
-
setPendingPermission({ request: req, resolve });
|
|
3656
|
+
setPendingPermission({ request: req, preview, resolve });
|
|
1781
3657
|
});
|
|
1782
3658
|
};
|
|
1783
3659
|
},
|
|
1784
|
-
[setPendingPermission]
|
|
3660
|
+
[setPendingPermission, cwd]
|
|
1785
3661
|
);
|
|
1786
3662
|
useEffect(() => {
|
|
1787
3663
|
attachConfirmHandler(backendRef.current);
|
|
1788
3664
|
return () => {
|
|
1789
|
-
|
|
3665
|
+
tickerStopRef.current?.();
|
|
1790
3666
|
abortRef.current?.abort();
|
|
1791
3667
|
pendingRef.current?.resolve(false);
|
|
1792
3668
|
};
|
|
1793
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]);
|
|
1794
3690
|
const interrupt = useCallback(() => {
|
|
1795
3691
|
abortRef.current?.abort();
|
|
1796
3692
|
const p = pendingRef.current;
|
|
@@ -1799,20 +3695,46 @@ function Repl({
|
|
|
1799
3695
|
p.resolve(false);
|
|
1800
3696
|
}
|
|
1801
3697
|
}, [setPendingPermission]);
|
|
1802
|
-
|
|
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) => {
|
|
1803
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
|
+
}
|
|
1804
3717
|
});
|
|
1805
3718
|
const handleDecision = useCallback(
|
|
1806
3719
|
(decision) => {
|
|
1807
3720
|
const p = pendingRef.current;
|
|
1808
3721
|
if (!p) return;
|
|
1809
3722
|
if (decision === "always") {
|
|
1810
|
-
|
|
3723
|
+
const rule = buildAllowRule(p.request);
|
|
3724
|
+
alwaysAllowedRef.current.add(rule);
|
|
3725
|
+
void addAllowRule(rule).then((added) => {
|
|
3726
|
+
pushMessage(
|
|
3727
|
+
"system",
|
|
3728
|
+
added ? `Always allow ${rule} \u2014 saved to config` : `Always allow ${rule} (already in config)`
|
|
3729
|
+
);
|
|
3730
|
+
}).catch(() => {
|
|
3731
|
+
pushMessage("system", `Always allow ${rule} for this session (failed to save)`);
|
|
3732
|
+
});
|
|
1811
3733
|
}
|
|
1812
3734
|
setPendingPermission(null);
|
|
1813
3735
|
p.resolve(decision !== "no");
|
|
1814
3736
|
},
|
|
1815
|
-
[setPendingPermission]
|
|
3737
|
+
[setPendingPermission, pushMessage]
|
|
1816
3738
|
);
|
|
1817
3739
|
const switchModel = useCallback(
|
|
1818
3740
|
async (name) => {
|
|
@@ -1840,95 +3762,119 @@ function Repl({
|
|
|
1840
3762
|
},
|
|
1841
3763
|
[config, cwd, sessionStore, attachConfirmHandler]
|
|
1842
3764
|
);
|
|
1843
|
-
const resume = useCallback(
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
addSystemMessage: (text) => pushMessage("system", text),
|
|
1864
|
-
clearMessages: () => {
|
|
1865
|
-
setMessages([]);
|
|
1866
|
-
setEpoch((e) => e + 1);
|
|
1867
|
-
},
|
|
1868
|
-
exit: () => exit(),
|
|
1869
|
-
listModels: () => {
|
|
1870
|
-
const models = listModels(config);
|
|
1871
|
-
if (models.length === 0) return "No models configured.";
|
|
1872
|
-
const lines = models.map(
|
|
1873
|
-
(m) => `${m.name === modelNameRef.current ? "*" : " "} ${m.name} (${m.provider}/${m.model})`
|
|
1874
|
-
);
|
|
1875
|
-
return `Models (* = current):
|
|
1876
|
-
${lines.join("\n")}`;
|
|
1877
|
-
},
|
|
1878
|
-
switchModel,
|
|
1879
|
-
listSessions: async () => {
|
|
1880
|
-
const metas = await SessionStore.list(cwd);
|
|
1881
|
-
return metas.length === 0 ? "No sessions found for this directory." : formatSessionList(metas);
|
|
1882
|
-
},
|
|
1883
|
-
resumeSession: resume,
|
|
1884
|
-
showTodos: async () => {
|
|
1885
|
-
const store = new TodoStore();
|
|
1886
|
-
await store.load(cwd);
|
|
1887
|
-
return formatTodos(store.list());
|
|
1888
|
-
},
|
|
1889
|
-
showUsage: () => formatUsage(usageRef.current),
|
|
1890
|
-
describeConfig: () => [
|
|
1891
|
-
`defaultModel: ${config.defaultModel || "(none)"}`,
|
|
1892
|
-
`permissionMode: ${config.permissionMode}`,
|
|
1893
|
-
`providers (${config.providers.length}): ${config.providers.map((p) => p.name).join(", ") || "(none)"}`,
|
|
1894
|
-
`models (${config.models.length}): ${config.models.map((m) => m.name).join(", ") || "(none)"}`,
|
|
1895
|
-
`maxSteps: ${config.maxSteps}`,
|
|
1896
|
-
`contextMaxTokens: ${config.contextMaxTokens}`
|
|
1897
|
-
].join("\n")
|
|
1898
|
-
};
|
|
1899
|
-
const reg = new CommandRegistry();
|
|
1900
|
-
registerBuiltinCommands(reg);
|
|
1901
|
-
return Object.assign(reg, { ctx });
|
|
1902
|
-
}, [pushMessage, exit, config, cwd, switchModel, resume]);
|
|
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
|
+
);
|
|
1903
3785
|
const runStream = useCallback(
|
|
1904
3786
|
async (input) => {
|
|
1905
|
-
|
|
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
|
+
}
|
|
1906
3796
|
const controller = new AbortController();
|
|
1907
3797
|
abortRef.current = controller;
|
|
1908
3798
|
streamedRef.current = "";
|
|
3799
|
+
thinkingRef.current = true;
|
|
3800
|
+
reasoningRef.current = "";
|
|
3801
|
+
turnChunksRef.current = 0;
|
|
3802
|
+
setThinking(true);
|
|
3803
|
+
setThinkingText("");
|
|
3804
|
+
setThoughtSummary(null);
|
|
1909
3805
|
setStreamingText("");
|
|
1910
3806
|
setIsStreaming(true);
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
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
|
+
});
|
|
1914
3833
|
try {
|
|
1915
|
-
for await (const event of backendRef.current.stream(input, controller.signal
|
|
3834
|
+
for await (const event of backendRef.current.stream(resolved.input, controller.signal, {
|
|
3835
|
+
persistAs: input
|
|
3836
|
+
})) {
|
|
1916
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
|
+
}
|
|
1917
3847
|
streamedRef.current += event.text;
|
|
3848
|
+
} else if (event.type === "reasoning") {
|
|
3849
|
+
reasoningRef.current += event.text;
|
|
1918
3850
|
} else if (event.type === "tool-call") {
|
|
3851
|
+
commitStreamed(true);
|
|
1919
3852
|
toolCardsRef.current.set(event.id, {
|
|
1920
3853
|
id: event.id,
|
|
1921
3854
|
name: event.name,
|
|
1922
3855
|
argsSummary: summarizeArgs(event.args)
|
|
1923
3856
|
});
|
|
1924
|
-
|
|
3857
|
+
setActivity(`running ${event.name}: ${summarizeArgs(event.args, 60)}`);
|
|
1925
3858
|
} else if (event.type === "tool-result") {
|
|
1926
|
-
const card = toolCardsRef.current.get(event.id)
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
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("");
|
|
1932
3878
|
} else if (event.type === "finish") {
|
|
1933
3879
|
if (event.usage) {
|
|
1934
3880
|
const usage = usageRef.current;
|
|
@@ -1941,45 +3887,265 @@ ${lines.join("\n")}`;
|
|
|
1941
3887
|
});
|
|
1942
3888
|
}
|
|
1943
3889
|
} else if (event.type === "error") {
|
|
1944
|
-
pushMessage("system", `Error: ${event.error
|
|
3890
|
+
pushMessage("system", `Error: ${formatStreamError(event.error)}`);
|
|
1945
3891
|
}
|
|
1946
3892
|
}
|
|
1947
3893
|
} catch (error) {
|
|
1948
|
-
|
|
1949
|
-
|
|
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
|
+
);
|
|
1950
3900
|
}
|
|
1951
3901
|
} finally {
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
flushTimerRef.current = null;
|
|
1955
|
-
}
|
|
3902
|
+
tickerStopRef.current?.();
|
|
3903
|
+
tickerStopRef.current = null;
|
|
1956
3904
|
abortRef.current = null;
|
|
1957
3905
|
setIsStreaming(false);
|
|
3906
|
+
thinkingRef.current = false;
|
|
3907
|
+
reasoningRef.current = "";
|
|
3908
|
+
setThinking(false);
|
|
3909
|
+
setThinkingText("");
|
|
3910
|
+
setThoughtSummary(null);
|
|
3911
|
+
setActivity(null);
|
|
1958
3912
|
const finalText = streamedRef.current;
|
|
1959
3913
|
streamedRef.current = "";
|
|
1960
3914
|
setStreamingText(null);
|
|
3915
|
+
const interrupted = controller.signal.aborted;
|
|
1961
3916
|
if (finalText.length > 0) {
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
);
|
|
3917
|
+
pushAssistantChunk(`${finalText}${interrupted ? " [interrupted]" : ""}`, false);
|
|
3918
|
+
} else if (interrupted && turnChunksRef.current > 0) {
|
|
3919
|
+
pushAssistantChunk("[interrupted]", false);
|
|
1966
3920
|
}
|
|
1967
3921
|
for (const card of toolCardsRef.current.values()) {
|
|
1968
3922
|
pushMessage("tool", formatToolCard(card));
|
|
1969
3923
|
}
|
|
1970
3924
|
toolCardsRef.current.clear();
|
|
1971
|
-
|
|
3925
|
+
turnChunksRef.current = 0;
|
|
1972
3926
|
const p = pendingRef.current;
|
|
1973
3927
|
if (p) {
|
|
1974
3928
|
setPendingPermission(null);
|
|
1975
3929
|
p.resolve(false);
|
|
1976
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
|
+
);
|
|
3957
|
+
const registry = useMemo(() => {
|
|
3958
|
+
const ctx = {
|
|
3959
|
+
addSystemMessage: (text) => pushMessage("system", text),
|
|
3960
|
+
clearMessages: () => {
|
|
3961
|
+
redrawMessages([]);
|
|
3962
|
+
},
|
|
3963
|
+
exit: () => handleExit(),
|
|
3964
|
+
listModels: () => {
|
|
3965
|
+
const models = listModels(config);
|
|
3966
|
+
if (models.length === 0) return "No models configured.";
|
|
3967
|
+
const lines = models.map(
|
|
3968
|
+
(m) => `${m.name === modelNameRef.current ? "*" : " "} ${m.name} (${m.provider}/${m.model})`
|
|
3969
|
+
);
|
|
3970
|
+
return `Models (* = current):
|
|
3971
|
+
${lines.join("\n")}`;
|
|
3972
|
+
},
|
|
3973
|
+
switchModel,
|
|
3974
|
+
listSessions: async () => {
|
|
3975
|
+
const metas = await SessionStore.list(cwd);
|
|
3976
|
+
return metas.length === 0 ? "No sessions found for this directory." : formatSessionList(metas);
|
|
3977
|
+
},
|
|
3978
|
+
resumeSession: resume,
|
|
3979
|
+
showTodos: async () => {
|
|
3980
|
+
const store = new TodoStore();
|
|
3981
|
+
await store.load(cwd);
|
|
3982
|
+
return formatTodos(store.list());
|
|
3983
|
+
},
|
|
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
|
+
},
|
|
3990
|
+
compactContext: async () => {
|
|
3991
|
+
let summaryModel = null;
|
|
3992
|
+
try {
|
|
3993
|
+
summaryModel = createModel(config, modelNameRef.current);
|
|
3994
|
+
} catch {
|
|
3995
|
+
summaryModel = null;
|
|
3996
|
+
}
|
|
3997
|
+
const result = await compactSession({
|
|
3998
|
+
backend: backendRef.current,
|
|
3999
|
+
sessionStore,
|
|
4000
|
+
config,
|
|
4001
|
+
model: summaryModel
|
|
4002
|
+
});
|
|
4003
|
+
if (result.compacted && result.messages) {
|
|
4004
|
+
const display = buildDisplayMessages(result.messages);
|
|
4005
|
+
nextIdRef.current = display.length;
|
|
4006
|
+
applyMessages(display);
|
|
4007
|
+
}
|
|
4008
|
+
return result.message;
|
|
4009
|
+
},
|
|
4010
|
+
exportSession: (arg) => exportSession({ backend: backendRef.current, sessionStore, cwd, arg }),
|
|
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
|
+
},
|
|
4079
|
+
describeConfig: () => [
|
|
4080
|
+
`defaultModel: ${config.defaultModel || "(none)"}`,
|
|
4081
|
+
`permissionMode: ${config.permissionMode}`,
|
|
4082
|
+
`providers (${config.providers.length}): ${config.providers.map((p) => p.name).join(", ") || "(none)"}`,
|
|
4083
|
+
`models (${config.models.length}): ${config.models.map((m) => m.name).join(", ") || "(none)"}`,
|
|
4084
|
+
`maxSteps: ${config.maxSteps}`,
|
|
4085
|
+
`contextMaxTokens: ${config.contextMaxTokens}`,
|
|
4086
|
+
`streamIdleTimeoutSec: ${config.streamIdleTimeoutSec}`,
|
|
4087
|
+
`permissions.allow (${config.permissions.allow.length}): ${config.permissions.allow.join(", ") || "(none)"}`
|
|
4088
|
+
].join("\n")
|
|
4089
|
+
};
|
|
4090
|
+
const reg = new CommandRegistry();
|
|
4091
|
+
registerBuiltinCommands(reg);
|
|
4092
|
+
registerCustomCommands(reg, cwd);
|
|
4093
|
+
return Object.assign(reg, { ctx });
|
|
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()}`);
|
|
4110
|
+
const controller = new AbortController();
|
|
4111
|
+
abortRef.current = controller;
|
|
4112
|
+
try {
|
|
4113
|
+
const outcome = await executeShellBang(raw, cwd, controller.signal);
|
|
4114
|
+
if (!outcome.ok) {
|
|
4115
|
+
pushMessage(
|
|
4116
|
+
"system",
|
|
4117
|
+
outcome.reason === "dangerous" ? `Refused to run dangerous command: ${raw.trim()}` : "Usage: !<command>"
|
|
4118
|
+
);
|
|
4119
|
+
return;
|
|
4120
|
+
}
|
|
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);
|
|
4134
|
+
}
|
|
4135
|
+
} finally {
|
|
4136
|
+
if (abortRef.current === controller) {
|
|
4137
|
+
abortRef.current = null;
|
|
4138
|
+
}
|
|
1977
4139
|
}
|
|
1978
4140
|
},
|
|
1979
|
-
[
|
|
4141
|
+
[cwd, pushMessage]
|
|
1980
4142
|
);
|
|
1981
4143
|
const handleSubmit = useCallback(
|
|
1982
4144
|
(text) => {
|
|
4145
|
+
if (text.startsWith("!")) {
|
|
4146
|
+
void runShellBang(text.slice(1));
|
|
4147
|
+
return;
|
|
4148
|
+
}
|
|
1983
4149
|
if (text.startsWith("/")) {
|
|
1984
4150
|
const parsed = parseSlashCommand(text);
|
|
1985
4151
|
if (!parsed) return;
|
|
@@ -1993,36 +4159,59 @@ ${lines.join("\n")}`;
|
|
|
1993
4159
|
}
|
|
1994
4160
|
void runStream(text);
|
|
1995
4161
|
},
|
|
1996
|
-
[registry, pushMessage, runStream]
|
|
4162
|
+
[registry, pushMessage, runStream, runShellBang]
|
|
1997
4163
|
);
|
|
1998
|
-
const
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
4164
|
+
const commandHints = useMemo(
|
|
4165
|
+
() => registry.list().map((cmd) => ({ name: cmd.name, description: cmd.description })),
|
|
4166
|
+
[registry]
|
|
4167
|
+
);
|
|
4168
|
+
return /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", children: [
|
|
4169
|
+
/* @__PURE__ */ jsx6(MessageList, { messages }, `messages-${epoch}`),
|
|
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 }),
|
|
2004
4189
|
/* @__PURE__ */ jsx6(
|
|
2005
4190
|
InputBox,
|
|
2006
4191
|
{
|
|
2007
4192
|
isStreaming,
|
|
2008
|
-
disabled: pending !== null,
|
|
4193
|
+
disabled: pending !== null || planApproval,
|
|
4194
|
+
commands: commandHints,
|
|
2009
4195
|
onSubmit: handleSubmit,
|
|
2010
4196
|
onInterrupt: interrupt,
|
|
2011
|
-
onExit:
|
|
4197
|
+
onExit: handleExit
|
|
2012
4198
|
}
|
|
2013
4199
|
),
|
|
2014
4200
|
/* @__PURE__ */ jsx6(
|
|
2015
4201
|
StatusBar,
|
|
2016
4202
|
{
|
|
2017
4203
|
model: modelName,
|
|
2018
|
-
permissionMode,
|
|
2019
|
-
tokens: usageRef.current.totalTokens
|
|
2020
|
-
|
|
2021
|
-
|
|
4204
|
+
permissionMode: permissionModeState,
|
|
4205
|
+
tokens: usageRef.current.totalTokens,
|
|
4206
|
+
backgroundTasks: bgLabels
|
|
4207
|
+
}
|
|
2022
4208
|
)
|
|
2023
4209
|
] });
|
|
2024
4210
|
}
|
|
2025
4211
|
function renderRepl(backend, opts) {
|
|
4212
|
+
process.on("exit", () => {
|
|
4213
|
+
defaultTaskManager.cleanup();
|
|
4214
|
+
});
|
|
2026
4215
|
return render(
|
|
2027
4216
|
/* @__PURE__ */ jsx6(
|
|
2028
4217
|
Repl,
|
|
@@ -2041,33 +4230,43 @@ function renderRepl(backend, opts) {
|
|
|
2041
4230
|
}
|
|
2042
4231
|
|
|
2043
4232
|
// src/config/loader.ts
|
|
2044
|
-
import
|
|
2045
|
-
import { parse } from "smol-toml";
|
|
4233
|
+
import fs7 from "fs";
|
|
4234
|
+
import { parse as parse2 } from "smol-toml";
|
|
2046
4235
|
|
|
2047
4236
|
// src/config/schema.ts
|
|
2048
|
-
import { z as
|
|
2049
|
-
var ProviderConfigSchema =
|
|
2050
|
-
name:
|
|
2051
|
-
protocol:
|
|
2052
|
-
baseURL:
|
|
2053
|
-
apiKeyEnv:
|
|
2054
|
-
apiKey:
|
|
2055
|
-
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()
|
|
4245
|
+
});
|
|
4246
|
+
var ModelConfigSchema = z11.object({
|
|
4247
|
+
name: z11.string(),
|
|
4248
|
+
provider: z11.string(),
|
|
4249
|
+
model: z11.string(),
|
|
4250
|
+
maxTokens: z11.number().int().positive().optional(),
|
|
4251
|
+
promptPrice: z11.number().nonnegative().optional(),
|
|
4252
|
+
completionPrice: z11.number().nonnegative().optional()
|
|
2056
4253
|
});
|
|
2057
|
-
var
|
|
2058
|
-
|
|
2059
|
-
provider: z9.string(),
|
|
2060
|
-
model: z9.string(),
|
|
2061
|
-
maxTokens: z9.number().int().positive().optional()
|
|
4254
|
+
var PermissionsConfigSchema = z11.object({
|
|
4255
|
+
allow: z11.array(z11.string()).default([])
|
|
2062
4256
|
});
|
|
2063
|
-
var ConfigSchema =
|
|
2064
|
-
defaultModel:
|
|
2065
|
-
permissionMode:
|
|
2066
|
-
providers:
|
|
2067
|
-
models:
|
|
2068
|
-
maxSteps:
|
|
2069
|
-
contextMaxTokens:
|
|
2070
|
-
|
|
4257
|
+
var ConfigSchema = z11.object({
|
|
4258
|
+
defaultModel: z11.string().default(""),
|
|
4259
|
+
permissionMode: z11.enum(["auto", "ask", "readonly", "yolo", "plan"]).default("ask"),
|
|
4260
|
+
providers: z11.array(ProviderConfigSchema).default([]),
|
|
4261
|
+
models: z11.array(ModelConfigSchema).default([]),
|
|
4262
|
+
maxSteps: z11.number().int().positive().default(50),
|
|
4263
|
+
contextMaxTokens: z11.number().int().positive().default(1e5),
|
|
4264
|
+
// Seconds without any stream part before a stalled response is ended
|
|
4265
|
+
// gracefully (some relays never send the terminal chunks). Applies once
|
|
4266
|
+
// streaming has started; the first part gets a longer, fixed allowance.
|
|
4267
|
+
streamIdleTimeoutSec: z11.number().positive().default(20),
|
|
4268
|
+
contextCompaction: z11.enum(["summary", "truncate"]).default("summary"),
|
|
4269
|
+
permissions: PermissionsConfigSchema.default({ allow: [] })
|
|
2071
4270
|
});
|
|
2072
4271
|
|
|
2073
4272
|
// src/config/loader.ts
|
|
@@ -2075,7 +4274,7 @@ var PartialConfigSchema = ConfigSchema.partial();
|
|
|
2075
4274
|
function parseTomlFile(content, filePath) {
|
|
2076
4275
|
let raw;
|
|
2077
4276
|
try {
|
|
2078
|
-
raw =
|
|
4277
|
+
raw = parse2(content);
|
|
2079
4278
|
} catch (error) {
|
|
2080
4279
|
const message = error instanceof Error ? error.message : String(error);
|
|
2081
4280
|
throw new Error(`Failed to parse TOML in ${filePath}: ${message}`);
|
|
@@ -2097,8 +4296,8 @@ function applyOverrides(config, overrides) {
|
|
|
2097
4296
|
return merged;
|
|
2098
4297
|
}
|
|
2099
4298
|
function readTomlFileSync(filePath) {
|
|
2100
|
-
if (!
|
|
2101
|
-
const content =
|
|
4299
|
+
if (!fs7.existsSync(filePath)) return {};
|
|
4300
|
+
const content = fs7.readFileSync(filePath, "utf8");
|
|
2102
4301
|
return parseTomlFile(content, filePath);
|
|
2103
4302
|
}
|
|
2104
4303
|
function loadConfigSync(cwd, overrides) {
|
|
@@ -2125,59 +4324,95 @@ async function createLoop(config, modelName, cwd, sessionStore) {
|
|
|
2125
4324
|
sessionStore
|
|
2126
4325
|
});
|
|
2127
4326
|
}
|
|
2128
|
-
async function printMode(loop, prompt) {
|
|
4327
|
+
async function printMode(loop, prompt, cwd, json) {
|
|
2129
4328
|
const controller = new AbortController();
|
|
2130
4329
|
process.on("SIGINT", () => controller.abort());
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
4330
|
+
const resolved = await resolveMentions(prompt, cwd);
|
|
4331
|
+
if (resolved.attached.length > 0) {
|
|
4332
|
+
process.stderr.write(`[attached] ${resolved.attached.join(", ")}
|
|
4333
|
+
`);
|
|
4334
|
+
}
|
|
4335
|
+
for (const skip of resolved.skipped) {
|
|
4336
|
+
process.stderr.write(`[skipped] @${skip.path}: ${skip.reason}
|
|
4337
|
+
`);
|
|
4338
|
+
}
|
|
4339
|
+
const usage = new UsageTracker();
|
|
2135
4340
|
let exitCode = 0;
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
process.
|
|
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(`
|
|
2143
4358
|
[tool] ${event.name} ${JSON.stringify(event.args)}
|
|
2144
4359
|
`);
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
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}
|
|
2149
4365
|
`);
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
case "finish":
|
|
2153
|
-
if (event.usage) {
|
|
2154
|
-
requests += 1;
|
|
2155
|
-
promptTokens += event.usage.promptTokens;
|
|
2156
|
-
completionTokens += event.usage.completionTokens;
|
|
2157
|
-
totalTokens += event.usage.totalTokens;
|
|
4366
|
+
}
|
|
4367
|
+
break;
|
|
2158
4368
|
}
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
4369
|
+
case "finish":
|
|
4370
|
+
usage.add(event.usage);
|
|
4371
|
+
break;
|
|
4372
|
+
case "error":
|
|
4373
|
+
if (!json) process.stderr.write(`
|
|
4374
|
+
[error] ${formatStreamError(event.error)}
|
|
2163
4375
|
`);
|
|
2164
|
-
|
|
2165
|
-
|
|
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;
|
|
2166
4388
|
}
|
|
2167
|
-
if (exitCode !== 0) break;
|
|
2168
4389
|
}
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
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
|
|
2172
4399
|
`
|
|
2173
|
-
|
|
4400
|
+
);
|
|
4401
|
+
}
|
|
2174
4402
|
}
|
|
2175
|
-
process.stdout.write("\n");
|
|
4403
|
+
if (!json) process.stdout.write("\n");
|
|
4404
|
+
defaultTaskManager.cleanup();
|
|
2176
4405
|
return exitCode;
|
|
2177
4406
|
}
|
|
2178
4407
|
var program = new Command();
|
|
2179
|
-
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) => {
|
|
2180
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
|
+
}
|
|
2181
4416
|
let config;
|
|
2182
4417
|
try {
|
|
2183
4418
|
config = loadConfigSync(cwd, { model: opts.model, permissionMode: opts.permissionMode });
|
|
@@ -2215,7 +4450,7 @@ program.name("star").description("Star CLI \u2014 an AI agent command-line inter
|
|
|
2215
4450
|
await loop.loadMessages(resumed.messages);
|
|
2216
4451
|
}
|
|
2217
4452
|
if (opts.print) {
|
|
2218
|
-
process.exitCode = await printMode(loop, opts.print);
|
|
4453
|
+
process.exitCode = await printMode(loop, opts.print, cwd, Boolean(opts.json));
|
|
2219
4454
|
return;
|
|
2220
4455
|
}
|
|
2221
4456
|
renderRepl(loop, {
|