@cryer/star-cli 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +37 -8
- package/dist/main.js +808 -212
- package/dist/main.js.map +1 -1
- package/package.json +1 -1
package/dist/main.js
CHANGED
|
@@ -126,6 +126,42 @@ function compactMessages(messages, maxTokens) {
|
|
|
126
126
|
|
|
127
127
|
// src/permissions/gate.ts
|
|
128
128
|
import path from "path";
|
|
129
|
+
|
|
130
|
+
// src/permissions/allow.ts
|
|
131
|
+
function parseAllowRule(rule) {
|
|
132
|
+
const match = /^([\w-]+)(?:\((.*)\))?$/.exec(rule.trim());
|
|
133
|
+
if (!match) return null;
|
|
134
|
+
return { toolName: match[1], pattern: match[2] };
|
|
135
|
+
}
|
|
136
|
+
function globMatch(pattern, value) {
|
|
137
|
+
const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
|
|
138
|
+
return new RegExp(`^${escaped}$`).test(value);
|
|
139
|
+
}
|
|
140
|
+
function requestTarget(req) {
|
|
141
|
+
if (typeof req.args !== "object" || req.args === null) return void 0;
|
|
142
|
+
const args = req.args;
|
|
143
|
+
const key = req.toolName === "bash" ? "command" : "path";
|
|
144
|
+
const value = args[key];
|
|
145
|
+
return typeof value === "string" ? value : void 0;
|
|
146
|
+
}
|
|
147
|
+
function matchAllowRule(rule, req) {
|
|
148
|
+
if (rule.toolName !== req.toolName) return false;
|
|
149
|
+
if (rule.pattern === void 0) return true;
|
|
150
|
+
const target = requestTarget(req);
|
|
151
|
+
return target !== void 0 && globMatch(rule.pattern, target);
|
|
152
|
+
}
|
|
153
|
+
function isAllowedByRules(rules, req) {
|
|
154
|
+
return rules.some((raw) => {
|
|
155
|
+
const rule = parseAllowRule(raw);
|
|
156
|
+
return rule !== null && matchAllowRule(rule, req);
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
function buildAllowRule(req) {
|
|
160
|
+
const target = requestTarget(req);
|
|
161
|
+
return target !== void 0 ? `${req.toolName}(${target})` : req.toolName;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// src/permissions/gate.ts
|
|
129
165
|
var FILE_TOOLS = /* @__PURE__ */ new Set(["write_file", "edit_file", "read_file"]);
|
|
130
166
|
var DANGEROUS_COMMAND_PATTERNS = [
|
|
131
167
|
/rm\s+-rf\s+\/(?:\s|$|;|&)/i,
|
|
@@ -163,7 +199,7 @@ function isSensitivePath(p) {
|
|
|
163
199
|
}
|
|
164
200
|
return base === ".env" || base.startsWith(".env.") || base === "id_rsa" || base.endsWith(".pem");
|
|
165
201
|
}
|
|
166
|
-
function checkPermission(mode, req, ctx) {
|
|
202
|
+
function checkPermission(mode, req, ctx, allowRules = []) {
|
|
167
203
|
const command = getStringArg(req.args, "command");
|
|
168
204
|
const filePath = getStringArg(req.args, "path");
|
|
169
205
|
if (req.toolName === "bash" && command !== void 0 && DANGEROUS_COMMAND_PATTERNS.some((pattern) => pattern.test(command))) {
|
|
@@ -186,6 +222,9 @@ function checkPermission(mode, req, ctx) {
|
|
|
186
222
|
if (mode === "readonly") {
|
|
187
223
|
return req.level === "read" ? "allow" : "deny";
|
|
188
224
|
}
|
|
225
|
+
if (isAllowedByRules(allowRules, req)) {
|
|
226
|
+
return "allow";
|
|
227
|
+
}
|
|
189
228
|
return req.level === "read" ? "allow" : "ask";
|
|
190
229
|
}
|
|
191
230
|
|
|
@@ -209,10 +248,12 @@ var AgentLoop = class {
|
|
|
209
248
|
async persist(message) {
|
|
210
249
|
await this.opts.sessionStore?.append(message);
|
|
211
250
|
}
|
|
212
|
-
async *stream(input, signal) {
|
|
251
|
+
async *stream(input, signal, opts) {
|
|
213
252
|
const userMessage = { role: "user", content: input };
|
|
214
253
|
this.messages.push(userMessage);
|
|
215
|
-
await this.persist(
|
|
254
|
+
await this.persist(
|
|
255
|
+
opts?.persistAs !== void 0 ? { role: "user", content: opts.persistAs } : userMessage
|
|
256
|
+
);
|
|
216
257
|
const { config, registry, cwd } = this.opts;
|
|
217
258
|
const aiTools = this.buildAiTools();
|
|
218
259
|
for (let step = 0; step < config.maxSteps; step++) {
|
|
@@ -335,7 +376,8 @@ ${summary}`
|
|
|
335
376
|
const decision = checkPermission(
|
|
336
377
|
config.permissionMode,
|
|
337
378
|
{ toolName: call.name, args: call.args, level: tool.permission },
|
|
338
|
-
{ cwd }
|
|
379
|
+
{ cwd },
|
|
380
|
+
config.permissions.allow
|
|
339
381
|
);
|
|
340
382
|
if (decision === "deny") {
|
|
341
383
|
return { content: `Permission denied for tool "${call.name}".`, isError: true };
|
|
@@ -368,10 +410,215 @@ ${summary}`
|
|
|
368
410
|
}
|
|
369
411
|
};
|
|
370
412
|
|
|
413
|
+
// src/cli/mentions.ts
|
|
414
|
+
import { readFile, stat as stat2 } from "fs/promises";
|
|
415
|
+
import path3 from "path";
|
|
416
|
+
|
|
417
|
+
// src/tools/fs/util.ts
|
|
418
|
+
import { readdir, stat } from "fs/promises";
|
|
419
|
+
import path2 from "path";
|
|
420
|
+
var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist"]);
|
|
421
|
+
async function walkFiles(root, skipDirs = SKIP_DIRS) {
|
|
422
|
+
const out = [];
|
|
423
|
+
async function walk(dir, relBase) {
|
|
424
|
+
const entries = await readdir(dir, { withFileTypes: true }).catch(() => null);
|
|
425
|
+
if (!entries) {
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
for (const entry of entries) {
|
|
429
|
+
const abs = path2.join(dir, entry.name);
|
|
430
|
+
const rel = relBase ? `${relBase}/${entry.name}` : entry.name;
|
|
431
|
+
if (entry.isDirectory()) {
|
|
432
|
+
if (!skipDirs.has(entry.name)) {
|
|
433
|
+
await walk(abs, rel);
|
|
434
|
+
}
|
|
435
|
+
} else if (entry.isFile()) {
|
|
436
|
+
try {
|
|
437
|
+
const st = await stat(abs);
|
|
438
|
+
out.push({ abs, rel, mtimeMs: st.mtimeMs });
|
|
439
|
+
} catch {
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
await walk(root, "");
|
|
445
|
+
return out;
|
|
446
|
+
}
|
|
447
|
+
var REGEX_SPECIALS = /[.+^${}()|[\]\\]/g;
|
|
448
|
+
function globToRegExp(pattern) {
|
|
449
|
+
let re = "";
|
|
450
|
+
let i = 0;
|
|
451
|
+
while (i < pattern.length) {
|
|
452
|
+
const c = pattern.charAt(i);
|
|
453
|
+
if (c === "*") {
|
|
454
|
+
if (pattern.charAt(i + 1) === "*") {
|
|
455
|
+
i += 2;
|
|
456
|
+
if (pattern.charAt(i) === "/") {
|
|
457
|
+
i += 1;
|
|
458
|
+
re += "(?:[^/]+/)*";
|
|
459
|
+
} else {
|
|
460
|
+
re += ".*";
|
|
461
|
+
}
|
|
462
|
+
} else {
|
|
463
|
+
re += "[^/]*";
|
|
464
|
+
i += 1;
|
|
465
|
+
}
|
|
466
|
+
} else if (c === "?") {
|
|
467
|
+
re += "[^/]";
|
|
468
|
+
i += 1;
|
|
469
|
+
} else {
|
|
470
|
+
re += c.replace(REGEX_SPECIALS, "\\$&");
|
|
471
|
+
i += 1;
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
return new RegExp(`^${re}$`);
|
|
475
|
+
}
|
|
476
|
+
function matchesGlob(pattern, relPath) {
|
|
477
|
+
const normalized = pattern.replace(/\\/g, "/");
|
|
478
|
+
if (!normalized.includes("/")) {
|
|
479
|
+
const base = relPath.split("/").pop() ?? relPath;
|
|
480
|
+
return globToRegExp(normalized).test(base);
|
|
481
|
+
}
|
|
482
|
+
return globToRegExp(normalized).test(relPath);
|
|
483
|
+
}
|
|
484
|
+
function isSensitivePath2(filePath) {
|
|
485
|
+
const base = path2.basename(filePath);
|
|
486
|
+
if (base === ".env.example" || base === ".env.sample" || base === ".env.template") {
|
|
487
|
+
return false;
|
|
488
|
+
}
|
|
489
|
+
if (base === ".env" || base.startsWith(".env.")) {
|
|
490
|
+
return true;
|
|
491
|
+
}
|
|
492
|
+
if (base === "id_rsa" || base.endsWith(".pem")) {
|
|
493
|
+
return true;
|
|
494
|
+
}
|
|
495
|
+
return false;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// src/cli/mentions.ts
|
|
499
|
+
var MAX_MENTION_BYTES = 100 * 1024;
|
|
500
|
+
var MENTION_RE = /(?<=^|\s)@([A-Za-z]:[\\/][A-Za-z0-9._\-/\\]*|[A-Za-z0-9._\-/\\]+)/g;
|
|
501
|
+
function parseMentions(text) {
|
|
502
|
+
const mentions = [];
|
|
503
|
+
const seen = /* @__PURE__ */ new Set();
|
|
504
|
+
const ranges = [];
|
|
505
|
+
for (const match of text.matchAll(MENTION_RE)) {
|
|
506
|
+
const mention = match[1];
|
|
507
|
+
const full = match[0];
|
|
508
|
+
if (mention === void 0 || full === void 0) continue;
|
|
509
|
+
ranges.push([match.index, match.index + full.length]);
|
|
510
|
+
if (!seen.has(mention)) {
|
|
511
|
+
seen.add(mention);
|
|
512
|
+
mentions.push(mention);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
if (ranges.length === 0) {
|
|
516
|
+
return { cleanText: text, mentions };
|
|
517
|
+
}
|
|
518
|
+
let clean = "";
|
|
519
|
+
let last = 0;
|
|
520
|
+
for (const [start, end] of ranges) {
|
|
521
|
+
clean += text.slice(last, start);
|
|
522
|
+
last = end;
|
|
523
|
+
}
|
|
524
|
+
clean += text.slice(last);
|
|
525
|
+
const cleanText = clean.replace(/[ \t]+\n/g, "\n").replace(/\n{3,}/g, "\n\n").trim();
|
|
526
|
+
return { cleanText, mentions };
|
|
527
|
+
}
|
|
528
|
+
function isBinaryContent(buf) {
|
|
529
|
+
const probe = buf.subarray(0, 8e3);
|
|
530
|
+
return probe.includes(0);
|
|
531
|
+
}
|
|
532
|
+
async function resolveMentions(text, cwd) {
|
|
533
|
+
const { cleanText, mentions } = parseMentions(text);
|
|
534
|
+
if (mentions.length === 0) {
|
|
535
|
+
return { input: text, attached: [], skipped: [] };
|
|
536
|
+
}
|
|
537
|
+
const blocks = [];
|
|
538
|
+
const attached = [];
|
|
539
|
+
const skipped = [];
|
|
540
|
+
for (const mention of mentions) {
|
|
541
|
+
const abs = path3.resolve(cwd, mention);
|
|
542
|
+
if (isSensitivePath2(abs)) {
|
|
543
|
+
skipped.push({ path: mention, reason: "sensitive file" });
|
|
544
|
+
continue;
|
|
545
|
+
}
|
|
546
|
+
const st = await stat2(abs).catch(() => null);
|
|
547
|
+
if (!st) {
|
|
548
|
+
skipped.push({ path: mention, reason: "file not found" });
|
|
549
|
+
continue;
|
|
550
|
+
}
|
|
551
|
+
if (st.isDirectory()) {
|
|
552
|
+
skipped.push({ path: mention, reason: "is a directory" });
|
|
553
|
+
continue;
|
|
554
|
+
}
|
|
555
|
+
if (st.size > MAX_MENTION_BYTES) {
|
|
556
|
+
skipped.push({ path: mention, reason: `exceeds ${MAX_MENTION_BYTES / 1024}KB limit` });
|
|
557
|
+
continue;
|
|
558
|
+
}
|
|
559
|
+
const buf = await readFile(abs).catch(() => null);
|
|
560
|
+
if (!buf) {
|
|
561
|
+
skipped.push({ path: mention, reason: "unreadable" });
|
|
562
|
+
continue;
|
|
563
|
+
}
|
|
564
|
+
if (isBinaryContent(buf)) {
|
|
565
|
+
skipped.push({ path: mention, reason: "binary file" });
|
|
566
|
+
continue;
|
|
567
|
+
}
|
|
568
|
+
attached.push(mention);
|
|
569
|
+
blocks.push(`--- @${mention} ---
|
|
570
|
+
${buf.toString("utf8")}
|
|
571
|
+
--- end ---`);
|
|
572
|
+
}
|
|
573
|
+
const input = [cleanText, ...blocks].filter((s) => s.length > 0).join("\n\n") || text;
|
|
574
|
+
return { input, attached, skipped };
|
|
575
|
+
}
|
|
576
|
+
|
|
371
577
|
// src/cli/repl.tsx
|
|
372
578
|
import { Box as Box7, render, useApp, useInput as useInput3 } from "ink";
|
|
373
579
|
import { useCallback, useEffect, useMemo, useRef as useRef2, useState as useState2 } from "react";
|
|
374
580
|
|
|
581
|
+
// src/config/save.ts
|
|
582
|
+
import fs from "fs";
|
|
583
|
+
import path5 from "path";
|
|
584
|
+
import { parse, stringify } from "smol-toml";
|
|
585
|
+
|
|
586
|
+
// src/config/paths.ts
|
|
587
|
+
import os from "os";
|
|
588
|
+
import path4 from "path";
|
|
589
|
+
function starHome() {
|
|
590
|
+
return process.env.STAR_HOME ?? path4.join(os.homedir(), ".star-cli");
|
|
591
|
+
}
|
|
592
|
+
function globalConfigPath() {
|
|
593
|
+
return path4.join(starHome(), "config.toml");
|
|
594
|
+
}
|
|
595
|
+
function sessionsDir() {
|
|
596
|
+
return path4.join(starHome(), "sessions");
|
|
597
|
+
}
|
|
598
|
+
function projectConfigPath(cwd) {
|
|
599
|
+
return path4.join(cwd, ".star", "config.toml");
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
// src/config/save.ts
|
|
603
|
+
async function addAllowRule(rule) {
|
|
604
|
+
const filePath = globalConfigPath();
|
|
605
|
+
let raw = {};
|
|
606
|
+
try {
|
|
607
|
+
const content = await fs.promises.readFile(filePath, "utf8");
|
|
608
|
+
raw = parse(content);
|
|
609
|
+
} catch (error) {
|
|
610
|
+
if (error.code !== "ENOENT") throw error;
|
|
611
|
+
}
|
|
612
|
+
const permissions = typeof raw.permissions === "object" && raw.permissions !== null ? raw.permissions : {};
|
|
613
|
+
const allow = Array.isArray(permissions.allow) ? permissions.allow : [];
|
|
614
|
+
if (allow.includes(rule)) return false;
|
|
615
|
+
permissions.allow = [...allow, rule];
|
|
616
|
+
raw.permissions = permissions;
|
|
617
|
+
await fs.promises.mkdir(path5.dirname(filePath), { recursive: true });
|
|
618
|
+
await fs.promises.writeFile(filePath, stringify(raw), "utf8");
|
|
619
|
+
return true;
|
|
620
|
+
}
|
|
621
|
+
|
|
375
622
|
// src/llm/provider.ts
|
|
376
623
|
import { createAnthropic } from "@ai-sdk/anthropic";
|
|
377
624
|
import { createOpenAI } from "@ai-sdk/openai";
|
|
@@ -435,26 +682,8 @@ function createModel(config, modelName) {
|
|
|
435
682
|
}
|
|
436
683
|
|
|
437
684
|
// src/session/store.ts
|
|
438
|
-
import
|
|
439
|
-
import
|
|
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");
|
|
452
|
-
}
|
|
453
|
-
function projectConfigPath(cwd) {
|
|
454
|
-
return path2.join(cwd, ".star", "config.toml");
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
// src/session/store.ts
|
|
685
|
+
import fs2 from "fs/promises";
|
|
686
|
+
import path6 from "path";
|
|
458
687
|
function generateId(now) {
|
|
459
688
|
const pad = (n) => String(n).padStart(2, "0");
|
|
460
689
|
const stamp = `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`;
|
|
@@ -476,13 +705,13 @@ var SessionStore = class _SessionStore {
|
|
|
476
705
|
initialized = false;
|
|
477
706
|
constructor(id) {
|
|
478
707
|
this.id = id;
|
|
479
|
-
this.dir =
|
|
708
|
+
this.dir = path6.join(sessionsDir(), id);
|
|
480
709
|
}
|
|
481
710
|
metaPath() {
|
|
482
|
-
return
|
|
711
|
+
return path6.join(this.dir, "meta.json");
|
|
483
712
|
}
|
|
484
713
|
messagesPath() {
|
|
485
|
-
return
|
|
714
|
+
return path6.join(this.dir, "messages.jsonl");
|
|
486
715
|
}
|
|
487
716
|
static async create(cwd, model) {
|
|
488
717
|
const store = new _SessionStore(generateId(/* @__PURE__ */ new Date()));
|
|
@@ -492,7 +721,7 @@ var SessionStore = class _SessionStore {
|
|
|
492
721
|
static async open(id) {
|
|
493
722
|
const store = new _SessionStore(id);
|
|
494
723
|
try {
|
|
495
|
-
const raw = await
|
|
724
|
+
const raw = await fs2.readFile(store.metaPath(), "utf8");
|
|
496
725
|
const meta = JSON.parse(raw);
|
|
497
726
|
if (meta.id !== id) return null;
|
|
498
727
|
store.initialized = true;
|
|
@@ -504,14 +733,14 @@ var SessionStore = class _SessionStore {
|
|
|
504
733
|
static async list(cwd) {
|
|
505
734
|
let entries;
|
|
506
735
|
try {
|
|
507
|
-
entries = await
|
|
736
|
+
entries = await fs2.readdir(sessionsDir());
|
|
508
737
|
} catch {
|
|
509
738
|
return [];
|
|
510
739
|
}
|
|
511
740
|
const metas = [];
|
|
512
741
|
for (const entry of entries) {
|
|
513
742
|
try {
|
|
514
|
-
const raw = await
|
|
743
|
+
const raw = await fs2.readFile(path6.join(sessionsDir(), entry, "meta.json"), "utf8");
|
|
515
744
|
const meta = JSON.parse(raw);
|
|
516
745
|
if (meta.id === entry && (cwd === void 0 || meta.cwd === cwd)) metas.push(meta);
|
|
517
746
|
} catch {
|
|
@@ -526,7 +755,7 @@ var SessionStore = class _SessionStore {
|
|
|
526
755
|
this.initialized = true;
|
|
527
756
|
return;
|
|
528
757
|
}
|
|
529
|
-
await
|
|
758
|
+
await fs2.mkdir(this.dir, { recursive: true });
|
|
530
759
|
const meta = {
|
|
531
760
|
id: this.id,
|
|
532
761
|
title: "",
|
|
@@ -535,24 +764,33 @@ var SessionStore = class _SessionStore {
|
|
|
535
764
|
createdAt: pending.createdAt,
|
|
536
765
|
updatedAt: pending.createdAt
|
|
537
766
|
};
|
|
538
|
-
await
|
|
767
|
+
await fs2.writeFile(this.metaPath(), JSON.stringify(meta, null, 2));
|
|
539
768
|
this.initialized = true;
|
|
540
769
|
}
|
|
541
770
|
async append(message) {
|
|
542
771
|
await this.ensureInitialized();
|
|
543
|
-
await
|
|
772
|
+
await fs2.appendFile(this.messagesPath(), `${JSON.stringify(message)}
|
|
544
773
|
`);
|
|
545
774
|
const meta = await this.meta();
|
|
546
775
|
meta.updatedAt = Date.now();
|
|
547
776
|
if (!meta.title && message.role === "user") {
|
|
548
777
|
meta.title = messageText(message).slice(0, 60);
|
|
549
778
|
}
|
|
550
|
-
await
|
|
779
|
+
await fs2.writeFile(this.metaPath(), JSON.stringify(meta, null, 2));
|
|
780
|
+
}
|
|
781
|
+
async replaceMessages(messages) {
|
|
782
|
+
await this.ensureInitialized();
|
|
783
|
+
const content = messages.map((message) => JSON.stringify(message)).join("\n");
|
|
784
|
+
await fs2.writeFile(this.messagesPath(), content ? `${content}
|
|
785
|
+
` : "");
|
|
786
|
+
const meta = await this.meta();
|
|
787
|
+
meta.updatedAt = Date.now();
|
|
788
|
+
await fs2.writeFile(this.metaPath(), JSON.stringify(meta, null, 2));
|
|
551
789
|
}
|
|
552
790
|
async messages() {
|
|
553
791
|
let raw;
|
|
554
792
|
try {
|
|
555
|
-
raw = await
|
|
793
|
+
raw = await fs2.readFile(this.messagesPath(), "utf8");
|
|
556
794
|
} catch {
|
|
557
795
|
return [];
|
|
558
796
|
}
|
|
@@ -569,7 +807,7 @@ var SessionStore = class _SessionStore {
|
|
|
569
807
|
}
|
|
570
808
|
async meta() {
|
|
571
809
|
await this.ensureInitialized();
|
|
572
|
-
const raw = await
|
|
810
|
+
const raw = await fs2.readFile(this.metaPath(), "utf8");
|
|
573
811
|
return JSON.parse(raw);
|
|
574
812
|
}
|
|
575
813
|
async setTitle(title) {
|
|
@@ -577,7 +815,7 @@ var SessionStore = class _SessionStore {
|
|
|
577
815
|
const meta = await this.meta();
|
|
578
816
|
meta.title = title;
|
|
579
817
|
meta.updatedAt = Date.now();
|
|
580
|
-
await
|
|
818
|
+
await fs2.writeFile(this.metaPath(), JSON.stringify(meta, null, 2));
|
|
581
819
|
}
|
|
582
820
|
async addUsage(delta) {
|
|
583
821
|
await this.ensureInitialized();
|
|
@@ -593,7 +831,7 @@ var SessionStore = class _SessionStore {
|
|
|
593
831
|
usage.completionTokens += delta.completionTokens;
|
|
594
832
|
usage.totalTokens += delta.totalTokens;
|
|
595
833
|
meta.usage = usage;
|
|
596
|
-
await
|
|
834
|
+
await fs2.writeFile(this.metaPath(), JSON.stringify(meta, null, 2));
|
|
597
835
|
}
|
|
598
836
|
};
|
|
599
837
|
|
|
@@ -627,17 +865,17 @@ function formatSessionList(metas) {
|
|
|
627
865
|
// src/tools/bash.ts
|
|
628
866
|
import { spawn } from "child_process";
|
|
629
867
|
import { existsSync } from "fs";
|
|
630
|
-
import
|
|
868
|
+
import path7 from "path";
|
|
631
869
|
import { z } from "zod";
|
|
632
870
|
var MAX_OUTPUT = 3e4;
|
|
633
871
|
var DEFAULT_TIMEOUT = 120;
|
|
634
872
|
var MAX_TIMEOUT = 600;
|
|
635
873
|
function findOnPath(exe, exclude) {
|
|
636
|
-
for (const dir of (process.env.PATH ?? "").split(
|
|
874
|
+
for (const dir of (process.env.PATH ?? "").split(path7.delimiter)) {
|
|
637
875
|
if (!dir || exclude?.(dir)) {
|
|
638
876
|
continue;
|
|
639
877
|
}
|
|
640
|
-
const full =
|
|
878
|
+
const full = path7.join(dir, exe);
|
|
641
879
|
if (existsSync(full)) {
|
|
642
880
|
return full;
|
|
643
881
|
}
|
|
@@ -655,7 +893,7 @@ function resolveShell() {
|
|
|
655
893
|
}
|
|
656
894
|
const gitExe = findOnPath("git.exe");
|
|
657
895
|
if (gitExe) {
|
|
658
|
-
const sibling =
|
|
896
|
+
const sibling = path7.join(path7.dirname(path7.dirname(gitExe)), "bin", "bash.exe");
|
|
659
897
|
if (existsSync(sibling)) {
|
|
660
898
|
return { shell: sibling, wrap: (c) => ["-c", c], label: "bash" };
|
|
661
899
|
}
|
|
@@ -762,9 +1000,46 @@ Command timed out after ${timeoutSeconds}s`,
|
|
|
762
1000
|
};
|
|
763
1001
|
|
|
764
1002
|
// src/tools/fs/edit.ts
|
|
765
|
-
import { readFile, writeFile } from "fs/promises";
|
|
766
|
-
import
|
|
1003
|
+
import { readFile as readFile3, writeFile as writeFile2 } from "fs/promises";
|
|
1004
|
+
import path9 from "path";
|
|
767
1005
|
import { z as z2 } from "zod";
|
|
1006
|
+
|
|
1007
|
+
// src/tools/fs/snapshots.ts
|
|
1008
|
+
import { mkdir, readFile as readFile2, rm, writeFile } from "fs/promises";
|
|
1009
|
+
import path8 from "path";
|
|
1010
|
+
var MAX_SNAPSHOTS = 50;
|
|
1011
|
+
var stack = [];
|
|
1012
|
+
async function captureSnapshot(filePath, toolName) {
|
|
1013
|
+
let content = null;
|
|
1014
|
+
let existed = false;
|
|
1015
|
+
try {
|
|
1016
|
+
content = await readFile2(filePath, "utf8");
|
|
1017
|
+
existed = true;
|
|
1018
|
+
} catch {
|
|
1019
|
+
}
|
|
1020
|
+
return { path: filePath, existed, content, toolName, timestamp: Date.now() };
|
|
1021
|
+
}
|
|
1022
|
+
function pushSnapshot(snapshot) {
|
|
1023
|
+
stack.push(snapshot);
|
|
1024
|
+
if (stack.length > MAX_SNAPSHOTS) {
|
|
1025
|
+
stack.splice(0, stack.length - MAX_SNAPSHOTS);
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
async function undoLastSnapshot() {
|
|
1029
|
+
const snapshot = stack.pop();
|
|
1030
|
+
if (!snapshot) {
|
|
1031
|
+
return "Nothing to undo.";
|
|
1032
|
+
}
|
|
1033
|
+
if (snapshot.existed && snapshot.content !== null) {
|
|
1034
|
+
await mkdir(path8.dirname(snapshot.path), { recursive: true });
|
|
1035
|
+
await writeFile(snapshot.path, snapshot.content, "utf8");
|
|
1036
|
+
return `Restored ${snapshot.path} to its state before ${snapshot.toolName}.`;
|
|
1037
|
+
}
|
|
1038
|
+
await rm(snapshot.path, { force: true });
|
|
1039
|
+
return `Deleted ${snapshot.path} (it did not exist before ${snapshot.toolName}).`;
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
// src/tools/fs/edit.ts
|
|
768
1043
|
var schema2 = z2.object({
|
|
769
1044
|
path: z2.string().describe("File path, absolute or relative to the working directory"),
|
|
770
1045
|
old_string: z2.string().min(1).describe("Exact text to replace"),
|
|
@@ -777,10 +1052,10 @@ var editFileTool = {
|
|
|
777
1052
|
permission: "write",
|
|
778
1053
|
parameters: schema2,
|
|
779
1054
|
async execute(args, ctx) {
|
|
780
|
-
const filePath =
|
|
1055
|
+
const filePath = path9.resolve(ctx.cwd, args.path);
|
|
781
1056
|
let content;
|
|
782
1057
|
try {
|
|
783
|
-
content = await
|
|
1058
|
+
content = await readFile3(filePath, "utf8");
|
|
784
1059
|
} catch (err) {
|
|
785
1060
|
return { content: `Failed to read ${args.path}: ${err.message}`, isError: true };
|
|
786
1061
|
}
|
|
@@ -800,97 +1075,25 @@ var editFileTool = {
|
|
|
800
1075
|
};
|
|
801
1076
|
}
|
|
802
1077
|
const updated = args.replace_all ? content.split(args.old_string).join(args.new_string) : content.replace(args.old_string, args.new_string);
|
|
803
|
-
|
|
1078
|
+
try {
|
|
1079
|
+
await writeFile2(filePath, updated, "utf8");
|
|
1080
|
+
} catch (err) {
|
|
1081
|
+
return { content: `Failed to write ${args.path}: ${err.message}`, isError: true };
|
|
1082
|
+
}
|
|
1083
|
+
pushSnapshot({
|
|
1084
|
+
path: filePath,
|
|
1085
|
+
existed: true,
|
|
1086
|
+
content,
|
|
1087
|
+
toolName: "edit_file",
|
|
1088
|
+
timestamp: Date.now()
|
|
1089
|
+
});
|
|
804
1090
|
return { content: `Edited ${args.path}: ${count} replacement${count > 1 ? "s" : ""}` };
|
|
805
1091
|
}
|
|
806
1092
|
};
|
|
807
1093
|
|
|
808
1094
|
// src/tools/fs/glob.ts
|
|
809
|
-
import
|
|
1095
|
+
import path10 from "path";
|
|
810
1096
|
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;
|
|
867
|
-
}
|
|
868
|
-
}
|
|
869
|
-
return new RegExp(`^${re}$`);
|
|
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
|
-
}
|
|
892
|
-
|
|
893
|
-
// src/tools/fs/glob.ts
|
|
894
1097
|
var MAX_RESULTS = 100;
|
|
895
1098
|
var schema3 = z3.object({
|
|
896
1099
|
pattern: z3.string().describe(
|
|
@@ -904,7 +1107,7 @@ var globTool = {
|
|
|
904
1107
|
permission: "read",
|
|
905
1108
|
parameters: schema3,
|
|
906
1109
|
async execute(args, ctx) {
|
|
907
|
-
const root =
|
|
1110
|
+
const root = path10.resolve(ctx.cwd, args.path ?? ".");
|
|
908
1111
|
const files = await walkFiles(root);
|
|
909
1112
|
const matched = files.filter((f) => matchesGlob(args.pattern, f.rel)).sort((a, b) => b.mtimeMs - a.mtimeMs).slice(0, MAX_RESULTS);
|
|
910
1113
|
if (matched.length === 0) {
|
|
@@ -915,8 +1118,8 @@ var globTool = {
|
|
|
915
1118
|
};
|
|
916
1119
|
|
|
917
1120
|
// src/tools/fs/grep.ts
|
|
918
|
-
import { readFile as
|
|
919
|
-
import
|
|
1121
|
+
import { readFile as readFile4, stat as stat3 } from "fs/promises";
|
|
1122
|
+
import path11 from "path";
|
|
920
1123
|
import { z as z4 } from "zod";
|
|
921
1124
|
var MAX_MATCHES = 250;
|
|
922
1125
|
var schema4 = z4.object({
|
|
@@ -937,12 +1140,12 @@ var grepTool = {
|
|
|
937
1140
|
} catch (err) {
|
|
938
1141
|
return { content: `Invalid regular expression: ${err.message}`, isError: true };
|
|
939
1142
|
}
|
|
940
|
-
const root =
|
|
1143
|
+
const root = path11.resolve(ctx.cwd, args.path ?? ".");
|
|
941
1144
|
let files;
|
|
942
1145
|
try {
|
|
943
|
-
const st = await
|
|
1146
|
+
const st = await stat3(root);
|
|
944
1147
|
if (st.isFile()) {
|
|
945
|
-
files = [{ abs: root, rel:
|
|
1148
|
+
files = [{ abs: root, rel: path11.basename(root), mtimeMs: st.mtimeMs }];
|
|
946
1149
|
} else {
|
|
947
1150
|
files = await walkFiles(root);
|
|
948
1151
|
}
|
|
@@ -958,7 +1161,7 @@ var grepTool = {
|
|
|
958
1161
|
outer: for (const file of files) {
|
|
959
1162
|
let buf;
|
|
960
1163
|
try {
|
|
961
|
-
buf = await
|
|
1164
|
+
buf = await readFile4(file.abs);
|
|
962
1165
|
} catch {
|
|
963
1166
|
continue;
|
|
964
1167
|
}
|
|
@@ -988,8 +1191,8 @@ var grepTool = {
|
|
|
988
1191
|
};
|
|
989
1192
|
|
|
990
1193
|
// src/tools/fs/read.ts
|
|
991
|
-
import { readFile as
|
|
992
|
-
import
|
|
1194
|
+
import { readFile as readFile5, stat as stat4 } from "fs/promises";
|
|
1195
|
+
import path12 from "path";
|
|
993
1196
|
import { z as z5 } from "zod";
|
|
994
1197
|
var DEFAULT_LIMIT = 2e3;
|
|
995
1198
|
var MAX_CHARS = 100 * 1024;
|
|
@@ -1004,18 +1207,18 @@ var readFileTool = {
|
|
|
1004
1207
|
permission: "read",
|
|
1005
1208
|
parameters: schema5,
|
|
1006
1209
|
async execute(args, ctx) {
|
|
1007
|
-
const filePath =
|
|
1210
|
+
const filePath = path12.resolve(ctx.cwd, args.path);
|
|
1008
1211
|
if (isSensitivePath2(filePath)) {
|
|
1009
1212
|
return { content: `Refused to read sensitive file: ${args.path}`, isError: true };
|
|
1010
1213
|
}
|
|
1011
|
-
const st = await
|
|
1214
|
+
const st = await stat4(filePath).catch(() => null);
|
|
1012
1215
|
if (!st) {
|
|
1013
1216
|
return { content: `File not found: ${args.path}`, isError: true };
|
|
1014
1217
|
}
|
|
1015
1218
|
if (st.isDirectory()) {
|
|
1016
1219
|
return { content: `Path is a directory, not a file: ${args.path}`, isError: true };
|
|
1017
1220
|
}
|
|
1018
|
-
const raw = await
|
|
1221
|
+
const raw = await readFile5(filePath, "utf8");
|
|
1019
1222
|
const lines = raw.split("\n");
|
|
1020
1223
|
const start = args.offset ?? 1;
|
|
1021
1224
|
const limit = args.limit ?? DEFAULT_LIMIT;
|
|
@@ -1049,8 +1252,8 @@ var readFileTool = {
|
|
|
1049
1252
|
};
|
|
1050
1253
|
|
|
1051
1254
|
// src/tools/fs/write.ts
|
|
1052
|
-
import { mkdir, writeFile as
|
|
1053
|
-
import
|
|
1255
|
+
import { mkdir as mkdir2, writeFile as writeFile3 } from "fs/promises";
|
|
1256
|
+
import path13 from "path";
|
|
1054
1257
|
import { z as z6 } from "zod";
|
|
1055
1258
|
var schema6 = z6.object({
|
|
1056
1259
|
path: z6.string().describe("File path, absolute or relative to the working directory"),
|
|
@@ -1062,20 +1265,22 @@ var writeFileTool = {
|
|
|
1062
1265
|
permission: "write",
|
|
1063
1266
|
parameters: schema6,
|
|
1064
1267
|
async execute(args, ctx) {
|
|
1065
|
-
const filePath =
|
|
1268
|
+
const filePath = path13.resolve(ctx.cwd, args.path);
|
|
1269
|
+
const snapshot = await captureSnapshot(filePath, "write_file");
|
|
1066
1270
|
try {
|
|
1067
|
-
await
|
|
1068
|
-
await
|
|
1271
|
+
await mkdir2(path13.dirname(filePath), { recursive: true });
|
|
1272
|
+
await writeFile3(filePath, args.content, "utf8");
|
|
1069
1273
|
} catch (err) {
|
|
1070
1274
|
return { content: `Failed to write ${args.path}: ${err.message}`, isError: true };
|
|
1071
1275
|
}
|
|
1276
|
+
pushSnapshot(snapshot);
|
|
1072
1277
|
return { content: `Wrote ${Buffer.byteLength(args.content, "utf8")} bytes to ${args.path}` };
|
|
1073
1278
|
}
|
|
1074
1279
|
};
|
|
1075
1280
|
|
|
1076
1281
|
// src/tools/todo.ts
|
|
1077
|
-
import { mkdir as
|
|
1078
|
-
import
|
|
1282
|
+
import { mkdir as mkdir3, readFile as readFile6, writeFile as writeFile4 } from "fs/promises";
|
|
1283
|
+
import path14 from "path";
|
|
1079
1284
|
import { z as z7 } from "zod";
|
|
1080
1285
|
var todoItemSchema = z7.object({
|
|
1081
1286
|
id: z7.number().int(),
|
|
@@ -1087,7 +1292,7 @@ var writeSchema = z7.object({
|
|
|
1087
1292
|
});
|
|
1088
1293
|
var readSchema = z7.object({});
|
|
1089
1294
|
function fileFor(cwd) {
|
|
1090
|
-
return
|
|
1295
|
+
return path14.join(cwd, ".star", "todos.json");
|
|
1091
1296
|
}
|
|
1092
1297
|
var TodoStore = class {
|
|
1093
1298
|
items = /* @__PURE__ */ new Map();
|
|
@@ -1103,7 +1308,7 @@ var TodoStore = class {
|
|
|
1103
1308
|
async load(cwd) {
|
|
1104
1309
|
let raw;
|
|
1105
1310
|
try {
|
|
1106
|
-
raw = await
|
|
1311
|
+
raw = await readFile6(fileFor(cwd), "utf8");
|
|
1107
1312
|
} catch {
|
|
1108
1313
|
return;
|
|
1109
1314
|
}
|
|
@@ -1116,8 +1321,8 @@ var TodoStore = class {
|
|
|
1116
1321
|
}
|
|
1117
1322
|
}
|
|
1118
1323
|
async save(cwd) {
|
|
1119
|
-
await
|
|
1120
|
-
await
|
|
1324
|
+
await mkdir3(path14.join(cwd, ".star"), { recursive: true });
|
|
1325
|
+
await writeFile4(fileFor(cwd), `${JSON.stringify(this.list(), null, 2)}
|
|
1121
1326
|
`);
|
|
1122
1327
|
}
|
|
1123
1328
|
};
|
|
@@ -1302,6 +1507,121 @@ var webFetchTool = {
|
|
|
1302
1507
|
}
|
|
1303
1508
|
};
|
|
1304
1509
|
|
|
1510
|
+
// src/tools/web/search.ts
|
|
1511
|
+
import { z as z9 } from "zod";
|
|
1512
|
+
var TIMEOUT_MS2 = 15e3;
|
|
1513
|
+
var DEFAULT_MAX_RESULTS = 5;
|
|
1514
|
+
var MAX_RESULTS_LIMIT = 10;
|
|
1515
|
+
var SEARCH_URL = "https://html.duckduckgo.com/html/?q=";
|
|
1516
|
+
var USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36";
|
|
1517
|
+
var schema8 = z9.object({
|
|
1518
|
+
query: z9.string().min(1).describe("The search query"),
|
|
1519
|
+
maxResults: z9.number().int().min(1).max(MAX_RESULTS_LIMIT).optional().describe(
|
|
1520
|
+
`Maximum number of results to return (default ${DEFAULT_MAX_RESULTS}, max ${MAX_RESULTS_LIMIT})`
|
|
1521
|
+
)
|
|
1522
|
+
});
|
|
1523
|
+
function stripTags(s) {
|
|
1524
|
+
return decodeEntities(s.replace(/<[^>]+>/g, " ")).replace(/\s+/g, " ").trim();
|
|
1525
|
+
}
|
|
1526
|
+
function normalizeUrl(href) {
|
|
1527
|
+
const decoded = decodeEntities(href);
|
|
1528
|
+
const uddg = decoded.match(/[?&]uddg=([^&]+)/);
|
|
1529
|
+
if (uddg?.[1]) {
|
|
1530
|
+
try {
|
|
1531
|
+
return decodeURIComponent(uddg[1]);
|
|
1532
|
+
} catch {
|
|
1533
|
+
return decoded;
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1536
|
+
if (decoded.startsWith("//")) {
|
|
1537
|
+
return `https:${decoded}`;
|
|
1538
|
+
}
|
|
1539
|
+
return decoded;
|
|
1540
|
+
}
|
|
1541
|
+
function parseResults(html) {
|
|
1542
|
+
const results = [];
|
|
1543
|
+
const anchorRe = /<a\b[^>]*\bclass\s*=\s*"[^"]*\bresult__a\b[^"]*"[^>]*>[\s\S]*?<\/a>/gi;
|
|
1544
|
+
const anchors = [...html.matchAll(anchorRe)];
|
|
1545
|
+
for (const [i, anchor] of anchors.entries()) {
|
|
1546
|
+
const full = anchor[0];
|
|
1547
|
+
const hrefMatch = full.match(/href\s*=\s*"([^"]*)"/i);
|
|
1548
|
+
if (!hrefMatch?.[1]) {
|
|
1549
|
+
continue;
|
|
1550
|
+
}
|
|
1551
|
+
const url = normalizeUrl(hrefMatch[1]);
|
|
1552
|
+
const title = stripTags(full.replace(/^<a\b[^>]*>/i, "").replace(/<\/a>\s*$/i, ""));
|
|
1553
|
+
const blockStart = (anchor.index ?? 0) + full.length;
|
|
1554
|
+
const blockEnd = i + 1 < anchors.length ? anchors[i + 1]?.index ?? html.length : html.length;
|
|
1555
|
+
const block = html.slice(blockStart, blockEnd);
|
|
1556
|
+
const snippetMatch = block.match(
|
|
1557
|
+
/<(?:a|div)\b[^>]*\bclass\s*=\s*"[^"]*\bresult__snippet\b[^"]*"[^>]*>([\s\S]*?)<\/(?:a|div)>/i
|
|
1558
|
+
);
|
|
1559
|
+
const snippet = snippetMatch?.[1] ? stripTags(snippetMatch[1]) : "";
|
|
1560
|
+
if (title && url) {
|
|
1561
|
+
results.push({ title, url, snippet });
|
|
1562
|
+
}
|
|
1563
|
+
}
|
|
1564
|
+
return results;
|
|
1565
|
+
}
|
|
1566
|
+
var webSearchTool = {
|
|
1567
|
+
name: "web_search",
|
|
1568
|
+
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.",
|
|
1569
|
+
permission: "read",
|
|
1570
|
+
parameters: schema8,
|
|
1571
|
+
async execute(args, ctx) {
|
|
1572
|
+
const maxResults = Math.min(args.maxResults ?? DEFAULT_MAX_RESULTS, MAX_RESULTS_LIMIT);
|
|
1573
|
+
const url = `${SEARCH_URL}${encodeURIComponent(args.query)}`;
|
|
1574
|
+
const timeoutSignal = AbortSignal.timeout(TIMEOUT_MS2);
|
|
1575
|
+
const signal = ctx.abortSignal ? AbortSignal.any([timeoutSignal, ctx.abortSignal]) : timeoutSignal;
|
|
1576
|
+
let res;
|
|
1577
|
+
try {
|
|
1578
|
+
res = await fetch(url, {
|
|
1579
|
+
signal,
|
|
1580
|
+
redirect: "follow",
|
|
1581
|
+
headers: { "user-agent": USER_AGENT, accept: "text/html" }
|
|
1582
|
+
});
|
|
1583
|
+
} catch (err) {
|
|
1584
|
+
if (ctx.abortSignal?.aborted) {
|
|
1585
|
+
return { content: "ERROR: request aborted", isError: true };
|
|
1586
|
+
}
|
|
1587
|
+
const name = err instanceof Error ? err.name : "";
|
|
1588
|
+
if (timeoutSignal.aborted || name === "TimeoutError") {
|
|
1589
|
+
return { content: `ERROR: search timed out after ${TIMEOUT_MS2 / 1e3}s`, isError: true };
|
|
1590
|
+
}
|
|
1591
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1592
|
+
return { content: `ERROR: search request failed: ${message}`, isError: true };
|
|
1593
|
+
}
|
|
1594
|
+
if (!res.ok) {
|
|
1595
|
+
await res.body?.cancel().catch(() => {
|
|
1596
|
+
});
|
|
1597
|
+
return {
|
|
1598
|
+
content: `ERROR: HTTP ${res.status} ${res.statusText} from DuckDuckGo`,
|
|
1599
|
+
isError: true
|
|
1600
|
+
};
|
|
1601
|
+
}
|
|
1602
|
+
let html;
|
|
1603
|
+
try {
|
|
1604
|
+
html = await res.text();
|
|
1605
|
+
} catch (err) {
|
|
1606
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1607
|
+
return { content: `ERROR: failed to read search response: ${message}`, isError: true };
|
|
1608
|
+
}
|
|
1609
|
+
const results = parseResults(html).slice(0, maxResults);
|
|
1610
|
+
if (results.length === 0) {
|
|
1611
|
+
return {
|
|
1612
|
+
content: `ERROR: no results found for "${args.query}" (the response may have been blocked or the page layout changed)`,
|
|
1613
|
+
isError: true
|
|
1614
|
+
};
|
|
1615
|
+
}
|
|
1616
|
+
const lines = results.map(
|
|
1617
|
+
(r, i) => `${i + 1}. ${r.title}
|
|
1618
|
+
${r.url}${r.snippet ? `
|
|
1619
|
+
${r.snippet}` : ""}`
|
|
1620
|
+
);
|
|
1621
|
+
return { content: lines.join("\n") };
|
|
1622
|
+
}
|
|
1623
|
+
};
|
|
1624
|
+
|
|
1305
1625
|
// src/tools/registry.ts
|
|
1306
1626
|
var ToolRegistry = class {
|
|
1307
1627
|
tools = /* @__PURE__ */ new Map();
|
|
@@ -1314,6 +1634,7 @@ var ToolRegistry = class {
|
|
|
1314
1634
|
grepTool,
|
|
1315
1635
|
bashTool,
|
|
1316
1636
|
webFetchTool,
|
|
1637
|
+
webSearchTool,
|
|
1317
1638
|
...createTodoTools()
|
|
1318
1639
|
]) {
|
|
1319
1640
|
this.register(tool);
|
|
@@ -1342,6 +1663,137 @@ function createDefaultRegistry() {
|
|
|
1342
1663
|
return registry;
|
|
1343
1664
|
}
|
|
1344
1665
|
|
|
1666
|
+
// src/cli/commands/actions.ts
|
|
1667
|
+
import fs3 from "fs/promises";
|
|
1668
|
+
import path15 from "path";
|
|
1669
|
+
var MIN_COMPACT_MESSAGES = 4;
|
|
1670
|
+
async function compactSession(opts) {
|
|
1671
|
+
const { backend, sessionStore, config, model } = opts;
|
|
1672
|
+
if (!(backend instanceof AgentLoop)) {
|
|
1673
|
+
return { message: "Current backend does not support compaction.", compacted: false };
|
|
1674
|
+
}
|
|
1675
|
+
const messages = [...backend.getMessages()];
|
|
1676
|
+
if (messages.length < MIN_COMPACT_MESSAGES) {
|
|
1677
|
+
return {
|
|
1678
|
+
message: `Nothing to compact: history has only ${messages.length} message(s).`,
|
|
1679
|
+
compacted: false
|
|
1680
|
+
};
|
|
1681
|
+
}
|
|
1682
|
+
const beforeTokens = estimateTokens(messages);
|
|
1683
|
+
const compacted = compactMessages(messages, config.contextMaxTokens);
|
|
1684
|
+
if (!compacted.compacted) {
|
|
1685
|
+
return {
|
|
1686
|
+
message: `Nothing to compact: ~${beforeTokens} estimated tokens, below the limit of ${config.contextMaxTokens}.`,
|
|
1687
|
+
compacted: false
|
|
1688
|
+
};
|
|
1689
|
+
}
|
|
1690
|
+
const next = await applyCompactionSummary(messages, compacted, config, model);
|
|
1691
|
+
await backend.loadMessages(next);
|
|
1692
|
+
await sessionStore?.replaceMessages(next);
|
|
1693
|
+
const afterTokens = estimateTokens(next);
|
|
1694
|
+
return {
|
|
1695
|
+
message: `Compacted context: ${messages.length} -> ${next.length} messages (~${beforeTokens} -> ~${afterTokens} estimated tokens).`,
|
|
1696
|
+
compacted: true,
|
|
1697
|
+
messages: next
|
|
1698
|
+
};
|
|
1699
|
+
}
|
|
1700
|
+
async function applyCompactionSummary(original, compacted, config, model) {
|
|
1701
|
+
if (config.contextCompaction !== "summary" || !model) {
|
|
1702
|
+
return compacted.messages;
|
|
1703
|
+
}
|
|
1704
|
+
const headCount = compacted.messages[0]?.role === "system" ? 1 : 0;
|
|
1705
|
+
const dropped = original.slice(headCount, headCount + compacted.droppedCount);
|
|
1706
|
+
try {
|
|
1707
|
+
const summary = await summarizeMessages(dropped, model);
|
|
1708
|
+
const messages = compacted.messages.slice();
|
|
1709
|
+
messages[headCount] = {
|
|
1710
|
+
role: "user",
|
|
1711
|
+
content: `[earlier conversation summarized]
|
|
1712
|
+
${summary}`
|
|
1713
|
+
};
|
|
1714
|
+
return messages;
|
|
1715
|
+
} catch {
|
|
1716
|
+
return compacted.messages;
|
|
1717
|
+
}
|
|
1718
|
+
}
|
|
1719
|
+
async function exportSession(opts) {
|
|
1720
|
+
const { backend, sessionStore, cwd, arg } = opts;
|
|
1721
|
+
if (!(backend instanceof AgentLoop)) {
|
|
1722
|
+
return "Current backend does not support exporting sessions.";
|
|
1723
|
+
}
|
|
1724
|
+
const messages = [...backend.getMessages()];
|
|
1725
|
+
if (messages.length === 0) {
|
|
1726
|
+
return "Nothing to export: the session has no messages.";
|
|
1727
|
+
}
|
|
1728
|
+
const sessionId = sessionStore?.id ?? `unknown-${Date.now()}`;
|
|
1729
|
+
const target = arg ? path15.resolve(cwd, arg) : path15.resolve(cwd, `star-session-${sessionId}.md`);
|
|
1730
|
+
const existed = await fileExists(target);
|
|
1731
|
+
await fs3.mkdir(path15.dirname(target), { recursive: true });
|
|
1732
|
+
await fs3.writeFile(target, renderSessionMarkdown(sessionId, messages));
|
|
1733
|
+
return existed ? `Exported ${messages.length} messages to ${target} (overwrote existing file).` : `Exported ${messages.length} messages to ${target}.`;
|
|
1734
|
+
}
|
|
1735
|
+
async function fileExists(target) {
|
|
1736
|
+
try {
|
|
1737
|
+
await fs3.access(target);
|
|
1738
|
+
return true;
|
|
1739
|
+
} catch {
|
|
1740
|
+
return false;
|
|
1741
|
+
}
|
|
1742
|
+
}
|
|
1743
|
+
function renderSessionMarkdown(sessionId, messages, exportedAt = /* @__PURE__ */ new Date()) {
|
|
1744
|
+
const lines = [
|
|
1745
|
+
`# Star CLI Session ${sessionId}`,
|
|
1746
|
+
"",
|
|
1747
|
+
`_Exported at ${exportedAt.toISOString()}_`,
|
|
1748
|
+
""
|
|
1749
|
+
];
|
|
1750
|
+
for (const message of messages) {
|
|
1751
|
+
lines.push(`## ${roleHeading(message.role)}`, "");
|
|
1752
|
+
lines.push(...renderContent(message), "");
|
|
1753
|
+
}
|
|
1754
|
+
return lines.join("\n");
|
|
1755
|
+
}
|
|
1756
|
+
function roleHeading(role) {
|
|
1757
|
+
return role.charAt(0).toUpperCase() + role.slice(1);
|
|
1758
|
+
}
|
|
1759
|
+
function renderContent(message) {
|
|
1760
|
+
const content = message.content;
|
|
1761
|
+
if (typeof content === "string") return [content];
|
|
1762
|
+
const lines = [];
|
|
1763
|
+
for (const part of content) {
|
|
1764
|
+
if (part.type === "text") {
|
|
1765
|
+
lines.push(part.text, "");
|
|
1766
|
+
} else if (part.type === "tool-call") {
|
|
1767
|
+
lines.push(
|
|
1768
|
+
`**Tool call: ${part.toolName}**`,
|
|
1769
|
+
"",
|
|
1770
|
+
"```json",
|
|
1771
|
+
JSON.stringify(part.args, null, 2),
|
|
1772
|
+
"```",
|
|
1773
|
+
""
|
|
1774
|
+
);
|
|
1775
|
+
} else if (part.type === "tool-result") {
|
|
1776
|
+
lines.push(
|
|
1777
|
+
`**Tool result: ${part.toolName}**`,
|
|
1778
|
+
"",
|
|
1779
|
+
"```",
|
|
1780
|
+
stringifyPart(part.result),
|
|
1781
|
+
"```",
|
|
1782
|
+
""
|
|
1783
|
+
);
|
|
1784
|
+
} else if (part.type === "reasoning") {
|
|
1785
|
+
lines.push(part.text, "");
|
|
1786
|
+
} else {
|
|
1787
|
+
lines.push(`[${part.type}]`, "");
|
|
1788
|
+
}
|
|
1789
|
+
}
|
|
1790
|
+
while (lines.length > 0 && lines[lines.length - 1] === "") lines.pop();
|
|
1791
|
+
return lines;
|
|
1792
|
+
}
|
|
1793
|
+
function stringifyPart(value) {
|
|
1794
|
+
return typeof value === "string" ? value : JSON.stringify(value, null, 2);
|
|
1795
|
+
}
|
|
1796
|
+
|
|
1345
1797
|
// src/cli/commands/builtin.ts
|
|
1346
1798
|
function registerBuiltinCommands(registry) {
|
|
1347
1799
|
registry.register({
|
|
@@ -1429,6 +1881,30 @@ ${lines.join("\n")}`);
|
|
|
1429
1881
|
ctx.addSystemMessage(ctx.describeConfig());
|
|
1430
1882
|
}
|
|
1431
1883
|
});
|
|
1884
|
+
registry.register({
|
|
1885
|
+
name: "compact",
|
|
1886
|
+
description: "Compact the conversation history to free up context",
|
|
1887
|
+
usage: "/compact",
|
|
1888
|
+
async run(_args, ctx) {
|
|
1889
|
+
ctx.addSystemMessage(await ctx.compactContext());
|
|
1890
|
+
}
|
|
1891
|
+
});
|
|
1892
|
+
registry.register({
|
|
1893
|
+
name: "export",
|
|
1894
|
+
description: "Export the current session to a Markdown file",
|
|
1895
|
+
usage: "/export [path]",
|
|
1896
|
+
async run(args, ctx) {
|
|
1897
|
+
ctx.addSystemMessage(await ctx.exportSession(args));
|
|
1898
|
+
}
|
|
1899
|
+
});
|
|
1900
|
+
registry.register({
|
|
1901
|
+
name: "undo",
|
|
1902
|
+
description: "Revert the last file change made by write_file or edit_file",
|
|
1903
|
+
usage: "/undo",
|
|
1904
|
+
async run(_args, ctx) {
|
|
1905
|
+
ctx.addSystemMessage(await ctx.undo());
|
|
1906
|
+
}
|
|
1907
|
+
});
|
|
1432
1908
|
}
|
|
1433
1909
|
|
|
1434
1910
|
// src/cli/commands/registry.ts
|
|
@@ -1463,16 +1939,46 @@ function parseSlashCommand(input) {
|
|
|
1463
1939
|
// src/cli/components/InputBox.tsx
|
|
1464
1940
|
import { Box, Text, useInput } from "ink";
|
|
1465
1941
|
import { useRef, useState } from "react";
|
|
1942
|
+
|
|
1943
|
+
// src/cli/commands/suggest.ts
|
|
1944
|
+
var MAX_SUGGESTIONS = 5;
|
|
1945
|
+
function filterCommands(input, commands) {
|
|
1946
|
+
if (!input.startsWith("/")) return [];
|
|
1947
|
+
if (/\s/.test(input)) return [];
|
|
1948
|
+
const prefix = input.slice(1).toLowerCase();
|
|
1949
|
+
return commands.filter((cmd) => cmd.name.toLowerCase().startsWith(prefix)).sort((a, b) => a.name.localeCompare(b.name)).slice(0, MAX_SUGGESTIONS);
|
|
1950
|
+
}
|
|
1951
|
+
|
|
1952
|
+
// src/cli/components/InputBox.tsx
|
|
1466
1953
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
1467
|
-
function InputBox({
|
|
1954
|
+
function InputBox({
|
|
1955
|
+
isStreaming,
|
|
1956
|
+
disabled,
|
|
1957
|
+
commands,
|
|
1958
|
+
onSubmit,
|
|
1959
|
+
onInterrupt,
|
|
1960
|
+
onExit
|
|
1961
|
+
}) {
|
|
1468
1962
|
const [value, setValue] = useState("");
|
|
1469
1963
|
const [cursor, setCursor] = useState(0);
|
|
1470
1964
|
const [history, setHistory] = useState([]);
|
|
1965
|
+
const [highlight, setHighlight] = useState(0);
|
|
1966
|
+
const [suggestionsDismissed, setSuggestionsDismissed] = useState(false);
|
|
1471
1967
|
const historyIndexRef = useRef(null);
|
|
1472
1968
|
const draftRef = useRef("");
|
|
1473
1969
|
const edit = (next, nextCursor) => {
|
|
1474
1970
|
setValue(next);
|
|
1475
1971
|
setCursor(Math.max(0, Math.min(nextCursor, next.length)));
|
|
1972
|
+
setHighlight(0);
|
|
1973
|
+
setSuggestionsDismissed(false);
|
|
1974
|
+
};
|
|
1975
|
+
const suggestions = isStreaming || disabled || suggestionsDismissed || !commands ? [] : filterCommands(value, commands);
|
|
1976
|
+
const activeIndex = suggestions.length === 0 ? 0 : Math.min(highlight, suggestions.length - 1);
|
|
1977
|
+
const completeHighlighted = () => {
|
|
1978
|
+
const cmd = suggestions[activeIndex];
|
|
1979
|
+
if (!cmd) return;
|
|
1980
|
+
const text = `/${cmd.name} `;
|
|
1981
|
+
edit(text, text.length);
|
|
1476
1982
|
};
|
|
1477
1983
|
useInput((input, key) => {
|
|
1478
1984
|
if (key.ctrl && input === "c") {
|
|
@@ -1489,6 +1995,10 @@ function InputBox({ isStreaming, disabled, onSubmit, onInterrupt, onExit }) {
|
|
|
1489
1995
|
return;
|
|
1490
1996
|
}
|
|
1491
1997
|
if (isStreaming || disabled) return;
|
|
1998
|
+
if (key.escape) {
|
|
1999
|
+
if (suggestions.length > 0) setSuggestionsDismissed(true);
|
|
2000
|
+
return;
|
|
2001
|
+
}
|
|
1492
2002
|
if (key.return) {
|
|
1493
2003
|
const text = value.trim();
|
|
1494
2004
|
if (text.length > 0) {
|
|
@@ -1500,7 +2010,16 @@ function InputBox({ isStreaming, disabled, onSubmit, onInterrupt, onExit }) {
|
|
|
1500
2010
|
draftRef.current = "";
|
|
1501
2011
|
return;
|
|
1502
2012
|
}
|
|
1503
|
-
if (key.
|
|
2013
|
+
if (key.tab) {
|
|
2014
|
+
if (suggestions.length > 0) {
|
|
2015
|
+
completeHighlighted();
|
|
2016
|
+
return;
|
|
2017
|
+
}
|
|
2018
|
+
} else if (key.upArrow) {
|
|
2019
|
+
if (suggestions.length > 0) {
|
|
2020
|
+
setHighlight((prev) => (prev - 1 + suggestions.length) % suggestions.length);
|
|
2021
|
+
return;
|
|
2022
|
+
}
|
|
1504
2023
|
if (history.length === 0) return;
|
|
1505
2024
|
if (historyIndexRef.current === null) {
|
|
1506
2025
|
draftRef.current = value;
|
|
@@ -1511,8 +2030,11 @@ function InputBox({ isStreaming, disabled, onSubmit, onInterrupt, onExit }) {
|
|
|
1511
2030
|
const entry = history[historyIndexRef.current] ?? "";
|
|
1512
2031
|
edit(entry, entry.length);
|
|
1513
2032
|
return;
|
|
1514
|
-
}
|
|
1515
|
-
|
|
2033
|
+
} else if (key.downArrow) {
|
|
2034
|
+
if (suggestions.length > 0) {
|
|
2035
|
+
setHighlight((prev) => (prev + 1) % suggestions.length);
|
|
2036
|
+
return;
|
|
2037
|
+
}
|
|
1516
2038
|
if (historyIndexRef.current === null) return;
|
|
1517
2039
|
if (historyIndexRef.current < history.length - 1) {
|
|
1518
2040
|
historyIndexRef.current += 1;
|
|
@@ -1523,13 +2045,15 @@ function InputBox({ isStreaming, disabled, onSubmit, onInterrupt, onExit }) {
|
|
|
1523
2045
|
edit(draftRef.current, draftRef.current.length);
|
|
1524
2046
|
}
|
|
1525
2047
|
return;
|
|
1526
|
-
}
|
|
1527
|
-
if (key.leftArrow) {
|
|
2048
|
+
} else if (key.leftArrow) {
|
|
1528
2049
|
setCursor((prev) => Math.max(0, prev - 1));
|
|
1529
2050
|
return;
|
|
1530
|
-
}
|
|
1531
|
-
|
|
1532
|
-
|
|
2051
|
+
} else if (key.rightArrow) {
|
|
2052
|
+
if (suggestions.length > 0 && cursor === value.length) {
|
|
2053
|
+
completeHighlighted();
|
|
2054
|
+
} else {
|
|
2055
|
+
setCursor((prev) => Math.min(value.length, prev + 1));
|
|
2056
|
+
}
|
|
1533
2057
|
return;
|
|
1534
2058
|
}
|
|
1535
2059
|
if (key.ctrl && input === "a") {
|
|
@@ -1567,11 +2091,19 @@ function InputBox({ isStreaming, disabled, onSubmit, onInterrupt, onExit }) {
|
|
|
1567
2091
|
const before = value.slice(0, cursor);
|
|
1568
2092
|
const at = value[cursor] ?? " ";
|
|
1569
2093
|
const after = value.slice(cursor + 1);
|
|
1570
|
-
return /* @__PURE__ */ jsxs(Box, {
|
|
1571
|
-
/* @__PURE__ */
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
2094
|
+
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
|
|
2095
|
+
/* @__PURE__ */ jsxs(Box, { borderStyle: "round", borderColor: "gray", paddingX: 1, children: [
|
|
2096
|
+
/* @__PURE__ */ jsx(Text, { color: "cyan", children: "> " }),
|
|
2097
|
+
/* @__PURE__ */ jsx(Text, { children: before }),
|
|
2098
|
+
/* @__PURE__ */ jsx(Text, { inverse: true, children: at }),
|
|
2099
|
+
/* @__PURE__ */ jsx(Text, { children: after })
|
|
2100
|
+
] }),
|
|
2101
|
+
suggestions.length > 0 && /* @__PURE__ */ jsx(Box, { flexDirection: "column", paddingLeft: 2, children: suggestions.map(
|
|
2102
|
+
(cmd, index) => index === activeIndex ? /* @__PURE__ */ jsx(Text, { bold: true, inverse: true, children: `/${cmd.name} - ${cmd.description}` }, cmd.name) : /* @__PURE__ */ jsxs(Text, { children: [
|
|
2103
|
+
/* @__PURE__ */ jsx(Text, { color: "cyan", children: `/${cmd.name}` }),
|
|
2104
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: ` - ${cmd.description}` })
|
|
2105
|
+
] }, cmd.name)
|
|
2106
|
+
) })
|
|
1575
2107
|
] });
|
|
1576
2108
|
}
|
|
1577
2109
|
|
|
@@ -1589,7 +2121,8 @@ function MessageList({ messages }) {
|
|
|
1589
2121
|
const style = roleStyles[message.role];
|
|
1590
2122
|
return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", marginBottom: 1, children: [
|
|
1591
2123
|
/* @__PURE__ */ jsx2(Text2, { bold: true, color: style.color, children: style.label }),
|
|
1592
|
-
/* @__PURE__ */ jsx2(Text2, { color: style.color, children: message.text })
|
|
2124
|
+
/* @__PURE__ */ jsx2(Text2, { color: style.color, children: message.text }),
|
|
2125
|
+
message.note && /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: message.note })
|
|
1593
2126
|
] }, message.id);
|
|
1594
2127
|
} });
|
|
1595
2128
|
}
|
|
@@ -1663,7 +2196,7 @@ function PermissionPrompt({ request, onDecision }) {
|
|
|
1663
2196
|
")"
|
|
1664
2197
|
] }),
|
|
1665
2198
|
/* @__PURE__ */ jsx3(Text3, { children: summarizeArgs(request.args) }),
|
|
1666
|
-
/* @__PURE__ */ jsx3(Text3, { children: "[y] allow [n] deny [a] always" })
|
|
2199
|
+
/* @__PURE__ */ jsx3(Text3, { children: "[y] allow [n] deny [a] always (saved to config)" })
|
|
1667
2200
|
] });
|
|
1668
2201
|
}
|
|
1669
2202
|
|
|
@@ -1763,8 +2296,8 @@ function Repl({
|
|
|
1763
2296
|
const alwaysAllowedRef = useRef2(/* @__PURE__ */ new Set());
|
|
1764
2297
|
const modelNameRef = useRef2(model);
|
|
1765
2298
|
const usageRef = useRef2(initialUsage ? { ...initialUsage } : emptyUsage());
|
|
1766
|
-
const pushMessage = useCallback((role, text) => {
|
|
1767
|
-
setMessages((prev) => [...prev, { id: nextIdRef.current++, role, text }]);
|
|
2299
|
+
const pushMessage = useCallback((role, text, note) => {
|
|
2300
|
+
setMessages((prev) => [...prev, { id: nextIdRef.current++, role, text, note }]);
|
|
1768
2301
|
}, []);
|
|
1769
2302
|
const setPendingPermission = useCallback((p) => {
|
|
1770
2303
|
pendingRef.current = p;
|
|
@@ -1773,7 +2306,7 @@ function Repl({
|
|
|
1773
2306
|
const attachConfirmHandler = useCallback(
|
|
1774
2307
|
(target) => {
|
|
1775
2308
|
target.confirmHandler = (req) => {
|
|
1776
|
-
if (alwaysAllowedRef.current
|
|
2309
|
+
if (isAllowedByRules([...alwaysAllowedRef.current], req)) {
|
|
1777
2310
|
return Promise.resolve(true);
|
|
1778
2311
|
}
|
|
1779
2312
|
return new Promise((resolve) => {
|
|
@@ -1807,12 +2340,21 @@ function Repl({
|
|
|
1807
2340
|
const p = pendingRef.current;
|
|
1808
2341
|
if (!p) return;
|
|
1809
2342
|
if (decision === "always") {
|
|
1810
|
-
|
|
2343
|
+
const rule = buildAllowRule(p.request);
|
|
2344
|
+
alwaysAllowedRef.current.add(rule);
|
|
2345
|
+
void addAllowRule(rule).then((added) => {
|
|
2346
|
+
pushMessage(
|
|
2347
|
+
"system",
|
|
2348
|
+
added ? `Always allow ${rule} \u2014 saved to config` : `Always allow ${rule} (already in config)`
|
|
2349
|
+
);
|
|
2350
|
+
}).catch(() => {
|
|
2351
|
+
pushMessage("system", `Always allow ${rule} for this session (failed to save)`);
|
|
2352
|
+
});
|
|
1811
2353
|
}
|
|
1812
2354
|
setPendingPermission(null);
|
|
1813
2355
|
p.resolve(decision !== "no");
|
|
1814
2356
|
},
|
|
1815
|
-
[setPendingPermission]
|
|
2357
|
+
[setPendingPermission, pushMessage]
|
|
1816
2358
|
);
|
|
1817
2359
|
const switchModel = useCallback(
|
|
1818
2360
|
async (name) => {
|
|
@@ -1887,22 +2429,54 @@ ${lines.join("\n")}`;
|
|
|
1887
2429
|
return formatTodos(store.list());
|
|
1888
2430
|
},
|
|
1889
2431
|
showUsage: () => formatUsage(usageRef.current),
|
|
2432
|
+
compactContext: async () => {
|
|
2433
|
+
let summaryModel = null;
|
|
2434
|
+
try {
|
|
2435
|
+
summaryModel = createModel(config, modelNameRef.current);
|
|
2436
|
+
} catch {
|
|
2437
|
+
summaryModel = null;
|
|
2438
|
+
}
|
|
2439
|
+
const result = await compactSession({
|
|
2440
|
+
backend: backendRef.current,
|
|
2441
|
+
sessionStore,
|
|
2442
|
+
config,
|
|
2443
|
+
model: summaryModel
|
|
2444
|
+
});
|
|
2445
|
+
if (result.compacted && result.messages) {
|
|
2446
|
+
const display = buildDisplayMessages(result.messages);
|
|
2447
|
+
nextIdRef.current = display.length;
|
|
2448
|
+
setMessages(display);
|
|
2449
|
+
setEpoch((e) => e + 1);
|
|
2450
|
+
}
|
|
2451
|
+
return result.message;
|
|
2452
|
+
},
|
|
2453
|
+
exportSession: (arg) => exportSession({ backend: backendRef.current, sessionStore, cwd, arg }),
|
|
2454
|
+
undo: () => undoLastSnapshot(),
|
|
1890
2455
|
describeConfig: () => [
|
|
1891
2456
|
`defaultModel: ${config.defaultModel || "(none)"}`,
|
|
1892
2457
|
`permissionMode: ${config.permissionMode}`,
|
|
1893
2458
|
`providers (${config.providers.length}): ${config.providers.map((p) => p.name).join(", ") || "(none)"}`,
|
|
1894
2459
|
`models (${config.models.length}): ${config.models.map((m) => m.name).join(", ") || "(none)"}`,
|
|
1895
2460
|
`maxSteps: ${config.maxSteps}`,
|
|
1896
|
-
`contextMaxTokens: ${config.contextMaxTokens}
|
|
2461
|
+
`contextMaxTokens: ${config.contextMaxTokens}`,
|
|
2462
|
+
`permissions.allow (${config.permissions.allow.length}): ${config.permissions.allow.join(", ") || "(none)"}`
|
|
1897
2463
|
].join("\n")
|
|
1898
2464
|
};
|
|
1899
2465
|
const reg = new CommandRegistry();
|
|
1900
2466
|
registerBuiltinCommands(reg);
|
|
1901
2467
|
return Object.assign(reg, { ctx });
|
|
1902
|
-
}, [pushMessage, exit, config, cwd, switchModel, resume]);
|
|
2468
|
+
}, [pushMessage, exit, config, cwd, switchModel, resume, sessionStore]);
|
|
1903
2469
|
const runStream = useCallback(
|
|
1904
2470
|
async (input) => {
|
|
1905
|
-
|
|
2471
|
+
const resolved = await resolveMentions(input, cwd);
|
|
2472
|
+
pushMessage(
|
|
2473
|
+
"user",
|
|
2474
|
+
input,
|
|
2475
|
+
resolved.attached.length > 0 ? `attached: ${resolved.attached.join(", ")}` : void 0
|
|
2476
|
+
);
|
|
2477
|
+
for (const skip of resolved.skipped) {
|
|
2478
|
+
pushMessage("system", `Skipped @${skip.path}: ${skip.reason}`);
|
|
2479
|
+
}
|
|
1906
2480
|
const controller = new AbortController();
|
|
1907
2481
|
abortRef.current = controller;
|
|
1908
2482
|
streamedRef.current = "";
|
|
@@ -1912,7 +2486,9 @@ ${lines.join("\n")}`;
|
|
|
1912
2486
|
setStreamingText(streamedRef.current);
|
|
1913
2487
|
}, FLUSH_INTERVAL_MS);
|
|
1914
2488
|
try {
|
|
1915
|
-
for await (const event of backendRef.current.stream(input, controller.signal
|
|
2489
|
+
for await (const event of backendRef.current.stream(resolved.input, controller.signal, {
|
|
2490
|
+
persistAs: input
|
|
2491
|
+
})) {
|
|
1916
2492
|
if (event.type === "text-delta") {
|
|
1917
2493
|
streamedRef.current += event.text;
|
|
1918
2494
|
} else if (event.type === "tool-call") {
|
|
@@ -1976,7 +2552,7 @@ ${lines.join("\n")}`;
|
|
|
1976
2552
|
}
|
|
1977
2553
|
}
|
|
1978
2554
|
},
|
|
1979
|
-
[pushMessage, setPendingPermission, sessionStore]
|
|
2555
|
+
[pushMessage, setPendingPermission, sessionStore, cwd]
|
|
1980
2556
|
);
|
|
1981
2557
|
const handleSubmit = useCallback(
|
|
1982
2558
|
(text) => {
|
|
@@ -1995,10 +2571,14 @@ ${lines.join("\n")}`;
|
|
|
1995
2571
|
},
|
|
1996
2572
|
[registry, pushMessage, runStream]
|
|
1997
2573
|
);
|
|
2574
|
+
const commandHints = useMemo(
|
|
2575
|
+
() => registry.list().map((cmd) => ({ name: cmd.name, description: cmd.description })),
|
|
2576
|
+
[registry]
|
|
2577
|
+
);
|
|
1998
2578
|
const cards = [...toolCardsRef.current.values()];
|
|
1999
2579
|
return /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", children: [
|
|
2000
|
-
/* @__PURE__ */ jsx6(MessageList, { messages }, epoch),
|
|
2001
|
-
cards.length > 0 && /* @__PURE__ */ jsx6(Box7, { flexDirection: "column", children: cards.map((card) => /* @__PURE__ */ jsx6(ToolCallCard, { card }, card.id)) }, cardsVersion),
|
|
2580
|
+
/* @__PURE__ */ jsx6(MessageList, { messages }, `messages-${epoch}`),
|
|
2581
|
+
cards.length > 0 && /* @__PURE__ */ jsx6(Box7, { flexDirection: "column", children: cards.map((card) => /* @__PURE__ */ jsx6(ToolCallCard, { card }, card.id)) }, `cards-${cardsVersion}`),
|
|
2002
2582
|
streamingText !== null && /* @__PURE__ */ jsx6(StreamingMessage, { text: streamingText }),
|
|
2003
2583
|
pending && /* @__PURE__ */ jsx6(PermissionPrompt, { request: pending.request, onDecision: handleDecision }),
|
|
2004
2584
|
/* @__PURE__ */ jsx6(
|
|
@@ -2006,6 +2586,7 @@ ${lines.join("\n")}`;
|
|
|
2006
2586
|
{
|
|
2007
2587
|
isStreaming,
|
|
2008
2588
|
disabled: pending !== null,
|
|
2589
|
+
commands: commandHints,
|
|
2009
2590
|
onSubmit: handleSubmit,
|
|
2010
2591
|
onInterrupt: interrupt,
|
|
2011
2592
|
onExit: exit
|
|
@@ -2018,7 +2599,7 @@ ${lines.join("\n")}`;
|
|
|
2018
2599
|
permissionMode,
|
|
2019
2600
|
tokens: usageRef.current.totalTokens
|
|
2020
2601
|
},
|
|
2021
|
-
usageVersion
|
|
2602
|
+
`status-${usageVersion}`
|
|
2022
2603
|
)
|
|
2023
2604
|
] });
|
|
2024
2605
|
}
|
|
@@ -2041,33 +2622,37 @@ function renderRepl(backend, opts) {
|
|
|
2041
2622
|
}
|
|
2042
2623
|
|
|
2043
2624
|
// src/config/loader.ts
|
|
2044
|
-
import
|
|
2045
|
-
import { parse } from "smol-toml";
|
|
2625
|
+
import fs4 from "fs";
|
|
2626
|
+
import { parse as parse2 } from "smol-toml";
|
|
2046
2627
|
|
|
2047
2628
|
// src/config/schema.ts
|
|
2048
|
-
import { z as
|
|
2049
|
-
var ProviderConfigSchema =
|
|
2050
|
-
name:
|
|
2051
|
-
protocol:
|
|
2052
|
-
baseURL:
|
|
2053
|
-
apiKeyEnv:
|
|
2054
|
-
apiKey:
|
|
2055
|
-
headers:
|
|
2629
|
+
import { z as z10 } from "zod";
|
|
2630
|
+
var ProviderConfigSchema = z10.object({
|
|
2631
|
+
name: z10.string(),
|
|
2632
|
+
protocol: z10.enum(["openai-compatible", "anthropic", "openai-responses"]).default("openai-compatible"),
|
|
2633
|
+
baseURL: z10.string(),
|
|
2634
|
+
apiKeyEnv: z10.string().optional(),
|
|
2635
|
+
apiKey: z10.string().optional(),
|
|
2636
|
+
headers: z10.record(z10.string()).optional()
|
|
2056
2637
|
});
|
|
2057
|
-
var ModelConfigSchema =
|
|
2058
|
-
name:
|
|
2059
|
-
provider:
|
|
2060
|
-
model:
|
|
2061
|
-
maxTokens:
|
|
2638
|
+
var ModelConfigSchema = z10.object({
|
|
2639
|
+
name: z10.string(),
|
|
2640
|
+
provider: z10.string(),
|
|
2641
|
+
model: z10.string(),
|
|
2642
|
+
maxTokens: z10.number().int().positive().optional()
|
|
2062
2643
|
});
|
|
2063
|
-
var
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2644
|
+
var PermissionsConfigSchema = z10.object({
|
|
2645
|
+
allow: z10.array(z10.string()).default([])
|
|
2646
|
+
});
|
|
2647
|
+
var ConfigSchema = z10.object({
|
|
2648
|
+
defaultModel: z10.string().default(""),
|
|
2649
|
+
permissionMode: z10.enum(["auto", "ask", "readonly"]).default("ask"),
|
|
2650
|
+
providers: z10.array(ProviderConfigSchema).default([]),
|
|
2651
|
+
models: z10.array(ModelConfigSchema).default([]),
|
|
2652
|
+
maxSteps: z10.number().int().positive().default(50),
|
|
2653
|
+
contextMaxTokens: z10.number().int().positive().default(1e5),
|
|
2654
|
+
contextCompaction: z10.enum(["summary", "truncate"]).default("summary"),
|
|
2655
|
+
permissions: PermissionsConfigSchema.default({ allow: [] })
|
|
2071
2656
|
});
|
|
2072
2657
|
|
|
2073
2658
|
// src/config/loader.ts
|
|
@@ -2075,7 +2660,7 @@ var PartialConfigSchema = ConfigSchema.partial();
|
|
|
2075
2660
|
function parseTomlFile(content, filePath) {
|
|
2076
2661
|
let raw;
|
|
2077
2662
|
try {
|
|
2078
|
-
raw =
|
|
2663
|
+
raw = parse2(content);
|
|
2079
2664
|
} catch (error) {
|
|
2080
2665
|
const message = error instanceof Error ? error.message : String(error);
|
|
2081
2666
|
throw new Error(`Failed to parse TOML in ${filePath}: ${message}`);
|
|
@@ -2097,8 +2682,8 @@ function applyOverrides(config, overrides) {
|
|
|
2097
2682
|
return merged;
|
|
2098
2683
|
}
|
|
2099
2684
|
function readTomlFileSync(filePath) {
|
|
2100
|
-
if (!
|
|
2101
|
-
const content =
|
|
2685
|
+
if (!fs4.existsSync(filePath)) return {};
|
|
2686
|
+
const content = fs4.readFileSync(filePath, "utf8");
|
|
2102
2687
|
return parseTomlFile(content, filePath);
|
|
2103
2688
|
}
|
|
2104
2689
|
function loadConfigSync(cwd, overrides) {
|
|
@@ -2125,15 +2710,26 @@ async function createLoop(config, modelName, cwd, sessionStore) {
|
|
|
2125
2710
|
sessionStore
|
|
2126
2711
|
});
|
|
2127
2712
|
}
|
|
2128
|
-
async function printMode(loop, prompt) {
|
|
2713
|
+
async function printMode(loop, prompt, cwd) {
|
|
2129
2714
|
const controller = new AbortController();
|
|
2130
2715
|
process.on("SIGINT", () => controller.abort());
|
|
2716
|
+
const resolved = await resolveMentions(prompt, cwd);
|
|
2717
|
+
if (resolved.attached.length > 0) {
|
|
2718
|
+
process.stderr.write(`[attached] ${resolved.attached.join(", ")}
|
|
2719
|
+
`);
|
|
2720
|
+
}
|
|
2721
|
+
for (const skip of resolved.skipped) {
|
|
2722
|
+
process.stderr.write(`[skipped] @${skip.path}: ${skip.reason}
|
|
2723
|
+
`);
|
|
2724
|
+
}
|
|
2131
2725
|
let requests = 0;
|
|
2132
2726
|
let promptTokens = 0;
|
|
2133
2727
|
let completionTokens = 0;
|
|
2134
2728
|
let totalTokens = 0;
|
|
2135
2729
|
let exitCode = 0;
|
|
2136
|
-
for await (const event of loop.stream(
|
|
2730
|
+
for await (const event of loop.stream(resolved.input, controller.signal, {
|
|
2731
|
+
persistAs: prompt
|
|
2732
|
+
})) {
|
|
2137
2733
|
switch (event.type) {
|
|
2138
2734
|
case "text-delta":
|
|
2139
2735
|
process.stdout.write(event.text);
|
|
@@ -2215,7 +2811,7 @@ program.name("star").description("Star CLI \u2014 an AI agent command-line inter
|
|
|
2215
2811
|
await loop.loadMessages(resumed.messages);
|
|
2216
2812
|
}
|
|
2217
2813
|
if (opts.print) {
|
|
2218
|
-
process.exitCode = await printMode(loop, opts.print);
|
|
2814
|
+
process.exitCode = await printMode(loop, opts.print, cwd);
|
|
2219
2815
|
return;
|
|
2220
2816
|
}
|
|
2221
2817
|
renderRepl(loop, {
|