@cryer/star-cli 0.2.0 → 0.2.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 +24 -3
- package/dist/{chunk-RLQXGX3N.js → chunk-GN236FD4.js} +394 -93
- package/dist/chunk-GN236FD4.js.map +1 -0
- package/dist/{loop-XIILV7GY.js → loop-ADVWZA62.js} +2 -2
- package/dist/main.js +727 -263
- package/dist/main.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-RLQXGX3N.js.map +0 -1
- /package/dist/{loop-XIILV7GY.js.map → loop-ADVWZA62.js.map} +0 -0
package/dist/main.js
CHANGED
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
SKIP_DIRS,
|
|
5
5
|
TodoStore,
|
|
6
6
|
WORKING_DIFF_MAX_LINES,
|
|
7
|
+
appendUserMemory,
|
|
7
8
|
bashTool,
|
|
8
9
|
buildAllowRule,
|
|
9
10
|
buildCommitPrompt,
|
|
@@ -12,25 +13,32 @@ import {
|
|
|
12
13
|
collectWorkingDiff,
|
|
13
14
|
compactMessages,
|
|
14
15
|
createDefaultRegistry,
|
|
16
|
+
createIgnorePredicate,
|
|
15
17
|
defaultTaskManager,
|
|
18
|
+
discoverSkills,
|
|
16
19
|
estimateTokens,
|
|
17
20
|
formatTaskFinished,
|
|
18
21
|
formatTaskList,
|
|
19
22
|
formatTaskStarted,
|
|
20
23
|
formatTodos,
|
|
21
24
|
getGitSummary,
|
|
25
|
+
globalConfigPath,
|
|
22
26
|
hydrateSnapshots,
|
|
23
27
|
isAllowedByRules,
|
|
24
28
|
isDangerousCommand,
|
|
25
29
|
isGitRepo,
|
|
26
30
|
isSensitivePath,
|
|
27
31
|
listSnapshots,
|
|
32
|
+
projectConfigPath,
|
|
28
33
|
reconcileToolCalls,
|
|
29
34
|
resolveShell,
|
|
30
35
|
rewindToSnapshot,
|
|
36
|
+
sessionsDir,
|
|
37
|
+
starHome,
|
|
31
38
|
summarizeMessages,
|
|
32
|
-
undoTurnSnapshots
|
|
33
|
-
|
|
39
|
+
undoTurnSnapshots,
|
|
40
|
+
userMemoryPath
|
|
41
|
+
} from "./chunk-GN236FD4.js";
|
|
34
42
|
|
|
35
43
|
// src/main.tsx
|
|
36
44
|
import { Command } from "commander";
|
|
@@ -264,32 +272,17 @@ var UsageTracker = class {
|
|
|
264
272
|
};
|
|
265
273
|
|
|
266
274
|
// src/cli/repl.tsx
|
|
267
|
-
import { Box as
|
|
268
|
-
import { useCallback, useEffect as useEffect2, useMemo, useRef as useRef2, useState as
|
|
275
|
+
import { Box as Box10, Text as Text11, render, useApp, useInput as useInput6 } from "ink";
|
|
276
|
+
import { useCallback, useEffect as useEffect2, useMemo, useRef as useRef2, useState as useState3 } from "react";
|
|
269
277
|
|
|
270
278
|
// src/config/save.ts
|
|
271
279
|
import fs from "fs";
|
|
272
|
-
import path3 from "path";
|
|
273
|
-
import { parse, stringify } from "smol-toml";
|
|
274
|
-
|
|
275
|
-
// src/config/paths.ts
|
|
276
|
-
import os from "os";
|
|
277
280
|
import path2 from "path";
|
|
278
|
-
|
|
279
|
-
return process.env.STAR_HOME ?? path2.join(os.homedir(), ".star-cli");
|
|
280
|
-
}
|
|
281
|
-
function globalConfigPath() {
|
|
282
|
-
return path2.join(starHome(), "config.toml");
|
|
283
|
-
}
|
|
284
|
-
function sessionsDir() {
|
|
285
|
-
return path2.join(starHome(), "sessions");
|
|
286
|
-
}
|
|
287
|
-
function projectConfigPath(cwd) {
|
|
288
|
-
return path2.join(cwd, ".star", "config.toml");
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
// src/config/save.ts
|
|
281
|
+
import { parse, stringify } from "smol-toml";
|
|
292
282
|
async function addAllowRule(rule) {
|
|
283
|
+
return addPermissionRule("allow", rule);
|
|
284
|
+
}
|
|
285
|
+
async function addPermissionRule(kind, rule) {
|
|
293
286
|
const filePath = globalConfigPath();
|
|
294
287
|
let raw = {};
|
|
295
288
|
try {
|
|
@@ -299,11 +292,11 @@ async function addAllowRule(rule) {
|
|
|
299
292
|
if (error.code !== "ENOENT") throw error;
|
|
300
293
|
}
|
|
301
294
|
const permissions = typeof raw.permissions === "object" && raw.permissions !== null ? raw.permissions : {};
|
|
302
|
-
const
|
|
303
|
-
if (
|
|
304
|
-
permissions
|
|
295
|
+
const rules = Array.isArray(permissions[kind]) ? permissions[kind] : [];
|
|
296
|
+
if (rules.includes(rule)) return false;
|
|
297
|
+
permissions[kind] = [...rules, rule];
|
|
305
298
|
raw.permissions = permissions;
|
|
306
|
-
await fs.promises.mkdir(
|
|
299
|
+
await fs.promises.mkdir(path2.dirname(filePath), { recursive: true });
|
|
307
300
|
await fs.promises.writeFile(filePath, stringify(raw), "utf8");
|
|
308
301
|
return true;
|
|
309
302
|
}
|
|
@@ -317,10 +310,80 @@ async function savePermissionMode(mode) {
|
|
|
317
310
|
if (error.code !== "ENOENT") throw error;
|
|
318
311
|
}
|
|
319
312
|
raw.permissionMode = mode;
|
|
320
|
-
await fs.promises.mkdir(
|
|
313
|
+
await fs.promises.mkdir(path2.dirname(filePath), { recursive: true });
|
|
321
314
|
await fs.promises.writeFile(filePath, stringify(raw), "utf8");
|
|
322
315
|
}
|
|
323
316
|
|
|
317
|
+
// src/config/schema.ts
|
|
318
|
+
import { z } from "zod";
|
|
319
|
+
var ProviderConfigSchema = z.object({
|
|
320
|
+
name: z.string(),
|
|
321
|
+
protocol: z.enum(["openai-compatible", "anthropic", "openai-responses"]).default("openai-compatible"),
|
|
322
|
+
baseURL: z.string(),
|
|
323
|
+
apiKeyEnv: z.string().optional(),
|
|
324
|
+
apiKey: z.string().optional(),
|
|
325
|
+
headers: z.record(z.string()).optional()
|
|
326
|
+
});
|
|
327
|
+
var ModelConfigSchema = z.object({
|
|
328
|
+
name: z.string(),
|
|
329
|
+
provider: z.string(),
|
|
330
|
+
model: z.string(),
|
|
331
|
+
maxTokens: z.number().int().positive().optional(),
|
|
332
|
+
// Per-model context window; overrides the top-level contextMaxTokens for
|
|
333
|
+
// compaction and the status bar when set.
|
|
334
|
+
contextMaxTokens: z.number().int().positive().optional(),
|
|
335
|
+
// Cost estimation needs BOTH prices (USD per 1M tokens); with only one set
|
|
336
|
+
// the model is treated as unpriced.
|
|
337
|
+
promptPrice: z.number().nonnegative().optional(),
|
|
338
|
+
completionPrice: z.number().nonnegative().optional()
|
|
339
|
+
});
|
|
340
|
+
var PermissionsConfigSchema = z.object({
|
|
341
|
+
allow: z.array(z.string()).default([]),
|
|
342
|
+
deny: z.array(z.string()).default([])
|
|
343
|
+
});
|
|
344
|
+
var HookConfigSchema = z.object({
|
|
345
|
+
event: z.enum(["PreToolUse", "PostToolUse", "Stop"]),
|
|
346
|
+
matcher: z.string().refine(
|
|
347
|
+
(pattern) => {
|
|
348
|
+
try {
|
|
349
|
+
new RegExp(pattern);
|
|
350
|
+
return true;
|
|
351
|
+
} catch {
|
|
352
|
+
return false;
|
|
353
|
+
}
|
|
354
|
+
},
|
|
355
|
+
{ message: "invalid regular expression" }
|
|
356
|
+
).optional(),
|
|
357
|
+
command: z.string().min(1),
|
|
358
|
+
timeoutSec: z.number().positive().default(30)
|
|
359
|
+
});
|
|
360
|
+
var ConfigSchema = z.object({
|
|
361
|
+
defaultModel: z.string().default(""),
|
|
362
|
+
permissionMode: z.enum(["auto", "ask", "readonly", "yolo", "plan"]).default("ask"),
|
|
363
|
+
providers: z.array(ProviderConfigSchema).default([]),
|
|
364
|
+
models: z.array(ModelConfigSchema).default([]),
|
|
365
|
+
maxSteps: z.number().int().positive().default(50),
|
|
366
|
+
contextMaxTokens: z.number().int().positive().default(1e5),
|
|
367
|
+
// Seconds without any stream part before a stalled response is ended
|
|
368
|
+
// gracefully (some relays never send the terminal chunks). Applies once
|
|
369
|
+
// streaming has started; the first part gets a longer, fixed allowance.
|
|
370
|
+
streamIdleTimeoutSec: z.number().positive().default(20),
|
|
371
|
+
contextCompaction: z.enum(["summary", "truncate"]).default("summary"),
|
|
372
|
+
// Optional per-session dollar budget; when set, the REPL stops the session
|
|
373
|
+
// once the estimated cost for the session reaches this amount.
|
|
374
|
+
sessionBudgetUsd: z.number().positive().optional(),
|
|
375
|
+
// Terminal bell (REPL only): ring when a turn takes longer than
|
|
376
|
+
// notifyBellThresholdSec, and when a background task finishes.
|
|
377
|
+
// STAR_NO_NOTIFY=1 disables without touching the config.
|
|
378
|
+
notifyBell: z.boolean().default(true),
|
|
379
|
+
notifyBellThresholdSec: z.number().positive().default(10),
|
|
380
|
+
permissions: PermissionsConfigSchema.default({ allow: [], deny: [] }),
|
|
381
|
+
hooks: z.array(HookConfigSchema).default([])
|
|
382
|
+
});
|
|
383
|
+
function contextWindowTokens(config, modelName) {
|
|
384
|
+
return config.models.find((m) => m.name === modelName)?.contextMaxTokens ?? config.contextMaxTokens;
|
|
385
|
+
}
|
|
386
|
+
|
|
324
387
|
// src/llm/provider.ts
|
|
325
388
|
import { createAnthropic } from "@ai-sdk/anthropic";
|
|
326
389
|
import { createOpenAI } from "@ai-sdk/openai";
|
|
@@ -385,15 +448,15 @@ function createModel(config, modelName) {
|
|
|
385
448
|
|
|
386
449
|
// src/session/checkpoints.ts
|
|
387
450
|
import fs2 from "fs/promises";
|
|
388
|
-
import
|
|
451
|
+
import path3 from "path";
|
|
389
452
|
function checkpointsDir(sessionDir) {
|
|
390
|
-
return
|
|
453
|
+
return path3.join(sessionDir, "checkpoints");
|
|
391
454
|
}
|
|
392
455
|
function indexPath(sessionDir) {
|
|
393
|
-
return
|
|
456
|
+
return path3.join(checkpointsDir(sessionDir), "index.json");
|
|
394
457
|
}
|
|
395
458
|
function checkpointContentPath(sessionDir, id) {
|
|
396
|
-
return
|
|
459
|
+
return path3.join(checkpointsDir(sessionDir), `${id}.snapshot`);
|
|
397
460
|
}
|
|
398
461
|
async function listCheckpointRecords(sessionDir) {
|
|
399
462
|
let raw;
|
|
@@ -441,11 +504,11 @@ async function loadSessionSnapshots(sessionDir) {
|
|
|
441
504
|
|
|
442
505
|
// src/session/clear.ts
|
|
443
506
|
import fs4 from "fs/promises";
|
|
444
|
-
import
|
|
507
|
+
import path5 from "path";
|
|
445
508
|
|
|
446
509
|
// src/session/store.ts
|
|
447
510
|
import fs3 from "fs/promises";
|
|
448
|
-
import
|
|
511
|
+
import path4 from "path";
|
|
449
512
|
function generateId(now) {
|
|
450
513
|
const pad = (n) => String(n).padStart(2, "0");
|
|
451
514
|
const stamp = `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`;
|
|
@@ -467,13 +530,13 @@ var SessionStore = class _SessionStore {
|
|
|
467
530
|
initialized = false;
|
|
468
531
|
constructor(id) {
|
|
469
532
|
this.id = id;
|
|
470
|
-
this.dir =
|
|
533
|
+
this.dir = path4.join(sessionsDir(), id);
|
|
471
534
|
}
|
|
472
535
|
metaPath() {
|
|
473
|
-
return
|
|
536
|
+
return path4.join(this.dir, "meta.json");
|
|
474
537
|
}
|
|
475
538
|
messagesPath() {
|
|
476
|
-
return
|
|
539
|
+
return path4.join(this.dir, "messages.jsonl");
|
|
477
540
|
}
|
|
478
541
|
static async create(cwd, model) {
|
|
479
542
|
const store = new _SessionStore(generateId(/* @__PURE__ */ new Date()));
|
|
@@ -507,7 +570,7 @@ var SessionStore = class _SessionStore {
|
|
|
507
570
|
const metas = [];
|
|
508
571
|
for (const entry of entries) {
|
|
509
572
|
try {
|
|
510
|
-
const raw = await fs3.readFile(
|
|
573
|
+
const raw = await fs3.readFile(path4.join(sessionsDir(), entry, "meta.json"), "utf8");
|
|
511
574
|
const meta = JSON.parse(raw);
|
|
512
575
|
if (meta.id === entry && (cwd === void 0 || meta.cwd === cwd)) metas.push(meta);
|
|
513
576
|
} catch {
|
|
@@ -634,7 +697,7 @@ async function clearSessions(cwd, excludeId) {
|
|
|
634
697
|
let removed2 = 0;
|
|
635
698
|
for (const meta of metas) {
|
|
636
699
|
if (meta.id === excludeId) continue;
|
|
637
|
-
await fs4.rm(
|
|
700
|
+
await fs4.rm(path5.join(sessionsDir(), meta.id), { recursive: true, force: true });
|
|
638
701
|
removed2 += 1;
|
|
639
702
|
}
|
|
640
703
|
return removed2;
|
|
@@ -648,7 +711,7 @@ async function clearSessions(cwd, excludeId) {
|
|
|
648
711
|
let removed = 0;
|
|
649
712
|
for (const entry of entries) {
|
|
650
713
|
if (entry === excludeId) continue;
|
|
651
|
-
await fs4.rm(
|
|
714
|
+
await fs4.rm(path5.join(sessionsDir(), entry), { recursive: true, force: true });
|
|
652
715
|
removed += 1;
|
|
653
716
|
}
|
|
654
717
|
return removed;
|
|
@@ -656,11 +719,11 @@ async function clearSessions(cwd, excludeId) {
|
|
|
656
719
|
|
|
657
720
|
// src/session/list.ts
|
|
658
721
|
import fs5 from "fs/promises";
|
|
659
|
-
import
|
|
722
|
+
import path6 from "path";
|
|
660
723
|
async function countMessages(id) {
|
|
661
724
|
let raw;
|
|
662
725
|
try {
|
|
663
|
-
raw = await fs5.readFile(
|
|
726
|
+
raw = await fs5.readFile(path6.join(sessionsDir(), id, "messages.jsonl"), "utf8");
|
|
664
727
|
} catch {
|
|
665
728
|
return 0;
|
|
666
729
|
}
|
|
@@ -744,10 +807,19 @@ function computeCostUsd(usage, model) {
|
|
|
744
807
|
}
|
|
745
808
|
function estimateCost(usage, modelName, model) {
|
|
746
809
|
const cost = computeCostUsd(usage, model);
|
|
747
|
-
if (cost === null
|
|
810
|
+
if (cost === null) {
|
|
811
|
+
if (model !== void 0) {
|
|
812
|
+
const missing = [
|
|
813
|
+
model.promptPrice === void 0 ? "promptPrice" : null,
|
|
814
|
+
model.completionPrice === void 0 ? "completionPrice" : null
|
|
815
|
+
].filter((f) => f !== null);
|
|
816
|
+
if (missing.length === 1) {
|
|
817
|
+
return `Estimated cost: unknown (${model.name} is missing ${missing[0]} \u2014 both prices are needed)`;
|
|
818
|
+
}
|
|
819
|
+
}
|
|
748
820
|
return `Estimated cost: unknown (no price configured for ${modelName})`;
|
|
749
821
|
}
|
|
750
|
-
return `Estimated cost: $${formatDollars(cost)} (${model
|
|
822
|
+
return `Estimated cost: $${formatDollars(cost)} (${model?.name} @ $${model?.promptPrice}/M prompt, $${model?.completionPrice}/M completion)`;
|
|
751
823
|
}
|
|
752
824
|
|
|
753
825
|
// src/session/usage.ts
|
|
@@ -909,11 +981,11 @@ function formatUsageDashboard(stats, models, options = {}) {
|
|
|
909
981
|
|
|
910
982
|
// src/version.ts
|
|
911
983
|
import fs6 from "fs";
|
|
912
|
-
import
|
|
984
|
+
import path7 from "path";
|
|
913
985
|
import { fileURLToPath } from "url";
|
|
914
986
|
function readPackageVersion() {
|
|
915
987
|
try {
|
|
916
|
-
const pkgPath =
|
|
988
|
+
const pkgPath = path7.join(path7.dirname(fileURLToPath(import.meta.url)), "..", "package.json");
|
|
917
989
|
const pkg = JSON.parse(fs6.readFileSync(pkgPath, "utf8"));
|
|
918
990
|
if (typeof pkg === "object" && pkg !== null) {
|
|
919
991
|
const version = pkg.version;
|
|
@@ -938,8 +1010,8 @@ function budgetState(costUsd, budgetUsd) {
|
|
|
938
1010
|
import { execFile } from "child_process";
|
|
939
1011
|
import { randomUUID } from "crypto";
|
|
940
1012
|
import { readFile as readFile2, unlink } from "fs/promises";
|
|
941
|
-
import
|
|
942
|
-
import
|
|
1013
|
+
import os from "os";
|
|
1014
|
+
import path8 from "path";
|
|
943
1015
|
import { promisify } from "util";
|
|
944
1016
|
var CLIPBOARD_TIMEOUT_MS = 5e3;
|
|
945
1017
|
var MAX_CLIPBOARD_IMAGE_BYTES = 10 * 1024 * 1024;
|
|
@@ -1013,7 +1085,7 @@ async function commandExists(command) {
|
|
|
1013
1085
|
}
|
|
1014
1086
|
}
|
|
1015
1087
|
async function readClipboardImage() {
|
|
1016
|
-
const tempFile =
|
|
1088
|
+
const tempFile = path8.join(os.tmpdir(), `star-clipboard-${randomUUID()}.png`);
|
|
1017
1089
|
const hasPngpaste = process.platform === "darwin" ? await commandExists("pngpaste") : false;
|
|
1018
1090
|
const plan = clipboardImagePlan(process.platform, tempFile, hasPngpaste);
|
|
1019
1091
|
if (!plan) return null;
|
|
@@ -1059,7 +1131,7 @@ async function copyText(text) {
|
|
|
1059
1131
|
|
|
1060
1132
|
// src/cli/commands/actions.ts
|
|
1061
1133
|
import fs7 from "fs/promises";
|
|
1062
|
-
import
|
|
1134
|
+
import path9 from "path";
|
|
1063
1135
|
var MIN_COMPACT_MESSAGES = 4;
|
|
1064
1136
|
async function compactSession(opts) {
|
|
1065
1137
|
const { backend, sessionStore, config, model } = opts;
|
|
@@ -1120,9 +1192,9 @@ async function exportSession(opts) {
|
|
|
1120
1192
|
return "Nothing to export: the session has no messages.";
|
|
1121
1193
|
}
|
|
1122
1194
|
const sessionId = sessionStore?.id ?? `unknown-${Date.now()}`;
|
|
1123
|
-
const target = arg ?
|
|
1195
|
+
const target = arg ? path9.resolve(cwd, arg) : path9.resolve(cwd, `star-session-${sessionId}.md`);
|
|
1124
1196
|
const existed = await fileExists(target);
|
|
1125
|
-
await fs7.mkdir(
|
|
1197
|
+
await fs7.mkdir(path9.dirname(target), { recursive: true });
|
|
1126
1198
|
await fs7.writeFile(target, renderSessionMarkdown(sessionId, messages));
|
|
1127
1199
|
return existed ? `Exported ${messages.length} messages to ${target} (overwrote existing file).` : `Exported ${messages.length} messages to ${target}.`;
|
|
1128
1200
|
}
|
|
@@ -1188,9 +1260,64 @@ function stringifyPart(value) {
|
|
|
1188
1260
|
return typeof value === "string" ? value : JSON.stringify(value, null, 2);
|
|
1189
1261
|
}
|
|
1190
1262
|
|
|
1263
|
+
// src/cli/commands/builtin.ts
|
|
1264
|
+
import { readFileSync } from "fs";
|
|
1265
|
+
|
|
1266
|
+
// src/session/search.ts
|
|
1267
|
+
var SNIPPET_LENGTH = 100;
|
|
1268
|
+
function flattenMessage(message) {
|
|
1269
|
+
const content = message.content;
|
|
1270
|
+
if (typeof content === "string") return content;
|
|
1271
|
+
if (!Array.isArray(content)) return "";
|
|
1272
|
+
const parts = [];
|
|
1273
|
+
for (const part of content) {
|
|
1274
|
+
if (part.type === "text") {
|
|
1275
|
+
parts.push(part.text);
|
|
1276
|
+
} else if (part.type === "tool-call") {
|
|
1277
|
+
parts.push(JSON.stringify(part.args));
|
|
1278
|
+
} else if (part.type === "tool-result") {
|
|
1279
|
+
parts.push(typeof part.result === "string" ? part.result : JSON.stringify(part.result));
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
return parts.join("\n");
|
|
1283
|
+
}
|
|
1284
|
+
function makeSnippet(text, query) {
|
|
1285
|
+
const flat = text.replace(/\s+/g, " ").trim();
|
|
1286
|
+
const index = flat.toLowerCase().indexOf(query.toLowerCase());
|
|
1287
|
+
if (index === -1 || flat.length <= SNIPPET_LENGTH) return flat.slice(0, SNIPPET_LENGTH);
|
|
1288
|
+
const side = Math.max(0, Math.floor((SNIPPET_LENGTH - query.length) / 2));
|
|
1289
|
+
let start = Math.max(0, index - side);
|
|
1290
|
+
const end = Math.min(flat.length, start + SNIPPET_LENGTH);
|
|
1291
|
+
start = Math.max(0, end - SNIPPET_LENGTH);
|
|
1292
|
+
return `${start > 0 ? "\u2026" : ""}${flat.slice(start, end)}${end < flat.length ? "\u2026" : ""}`;
|
|
1293
|
+
}
|
|
1294
|
+
async function searchSessions(query, limit = 10) {
|
|
1295
|
+
const hits = [];
|
|
1296
|
+
const needle = query.toLowerCase();
|
|
1297
|
+
for (const meta of await SessionStore.list()) {
|
|
1298
|
+
if (hits.length >= limit) break;
|
|
1299
|
+
const store = await SessionStore.open(meta.id);
|
|
1300
|
+
if (!store) continue;
|
|
1301
|
+
const text = (await store.messages()).map(flattenMessage).filter((part) => part.length > 0).join("\n");
|
|
1302
|
+
if (text.toLowerCase().includes(needle)) {
|
|
1303
|
+
hits.push({ meta, snippet: makeSnippet(text, query) });
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
return hits;
|
|
1307
|
+
}
|
|
1308
|
+
function formatSearchResults(hits, query) {
|
|
1309
|
+
if (hits.length === 0) return `No sessions matching "${query}".`;
|
|
1310
|
+
const lines = hits.map(({ meta, snippet }) => {
|
|
1311
|
+
const title = meta.title || "(untitled)";
|
|
1312
|
+
return `${shortSessionId(meta.id)} ${title} (${relativeTime(meta.updatedAt)})
|
|
1313
|
+
${snippet}`;
|
|
1314
|
+
});
|
|
1315
|
+
return [...lines, "Resume with /resume <id>"].join("\n");
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1191
1318
|
// src/cli/diff-preview.ts
|
|
1192
1319
|
import { readFile as readFile3 } from "fs/promises";
|
|
1193
|
-
import
|
|
1320
|
+
import path10 from "path";
|
|
1194
1321
|
var CONTEXT_LINES = 2;
|
|
1195
1322
|
var MAX_HUNK_LINES = 15;
|
|
1196
1323
|
var MAX_TOTAL_LINES = 12;
|
|
@@ -1351,7 +1478,7 @@ async function generateDiffPreview(toolName, args, cwd) {
|
|
|
1351
1478
|
if (toolName !== "edit_file" && toolName !== "write_file") return null;
|
|
1352
1479
|
const filePath = getStringArg(args, "path");
|
|
1353
1480
|
if (filePath === void 0) return null;
|
|
1354
|
-
const absolute =
|
|
1481
|
+
const absolute = path10.resolve(cwd, filePath);
|
|
1355
1482
|
let existing;
|
|
1356
1483
|
try {
|
|
1357
1484
|
existing = await readFile3(absolute, "utf8");
|
|
@@ -1375,31 +1502,41 @@ async function generateDiffPreview(toolName, args, cwd) {
|
|
|
1375
1502
|
|
|
1376
1503
|
// src/cli/commands/builtin.ts
|
|
1377
1504
|
function registerBuiltinCommands(registry) {
|
|
1378
|
-
registry.register(
|
|
1505
|
+
const register = (command) => registry.register(command);
|
|
1506
|
+
register({
|
|
1379
1507
|
name: "help",
|
|
1380
1508
|
description: "List available commands",
|
|
1381
1509
|
usage: "/help",
|
|
1510
|
+
category: "General",
|
|
1382
1511
|
run(_args, ctx) {
|
|
1383
|
-
const
|
|
1512
|
+
const groups = /* @__PURE__ */ new Map();
|
|
1513
|
+
for (const cmd of registry.list()) {
|
|
1514
|
+
const category = cmd.category ?? "Other";
|
|
1384
1515
|
const usage = cmd.usage ?? `/${cmd.name}`;
|
|
1385
|
-
|
|
1386
|
-
|
|
1516
|
+
const lines = groups.get(category) ?? [];
|
|
1517
|
+
lines.push(` ${usage} - ${cmd.description}`);
|
|
1518
|
+
groups.set(category, lines);
|
|
1519
|
+
}
|
|
1520
|
+
const sections = [...groups.entries()].map(([name, lines]) => [name, ...lines].join("\n"));
|
|
1387
1521
|
ctx.addSystemMessage(`Available commands:
|
|
1388
|
-
|
|
1522
|
+
|
|
1523
|
+
${sections.join("\n\n")}`);
|
|
1389
1524
|
}
|
|
1390
1525
|
});
|
|
1391
|
-
|
|
1526
|
+
register({
|
|
1392
1527
|
name: "clear",
|
|
1393
1528
|
description: "Clear message history",
|
|
1394
1529
|
usage: "/clear",
|
|
1530
|
+
category: "General",
|
|
1395
1531
|
run(_args, ctx) {
|
|
1396
1532
|
ctx.clearMessages();
|
|
1397
1533
|
}
|
|
1398
1534
|
});
|
|
1399
|
-
|
|
1535
|
+
register({
|
|
1400
1536
|
name: "new",
|
|
1401
1537
|
description: "Start a new session with a clean context",
|
|
1402
1538
|
usage: "/new",
|
|
1539
|
+
category: "Sessions",
|
|
1403
1540
|
async run(_args, ctx) {
|
|
1404
1541
|
if (!ctx.newSession) {
|
|
1405
1542
|
ctx.addSystemMessage("/new: this context cannot start a new session.");
|
|
@@ -1408,10 +1545,11 @@ ${lines.join("\n")}`);
|
|
|
1408
1545
|
ctx.addSystemMessage(await ctx.newSession());
|
|
1409
1546
|
}
|
|
1410
1547
|
});
|
|
1411
|
-
|
|
1548
|
+
register({
|
|
1412
1549
|
name: "clear-sessions",
|
|
1413
1550
|
description: "Delete stored sessions: this directory by default, every session with --all (the current session is kept)",
|
|
1414
1551
|
usage: "/clear-sessions [--all]",
|
|
1552
|
+
category: "Sessions",
|
|
1415
1553
|
async run(args, ctx) {
|
|
1416
1554
|
const arg = args.trim();
|
|
1417
1555
|
if (arg !== "" && arg !== "--all") {
|
|
@@ -1427,157 +1565,260 @@ ${lines.join("\n")}`);
|
|
|
1427
1565
|
ctx.addSystemMessage(await ctx.clearSessions(arg === "--all"));
|
|
1428
1566
|
}
|
|
1429
1567
|
});
|
|
1430
|
-
|
|
1568
|
+
register({
|
|
1431
1569
|
name: "exit",
|
|
1432
1570
|
description: "Exit the application",
|
|
1433
1571
|
usage: "/exit",
|
|
1572
|
+
category: "General",
|
|
1434
1573
|
run(_args, ctx) {
|
|
1435
1574
|
ctx.exit();
|
|
1436
1575
|
}
|
|
1437
1576
|
});
|
|
1438
|
-
|
|
1577
|
+
register({
|
|
1439
1578
|
name: "q",
|
|
1440
1579
|
description: "Exit the application (alias of /exit)",
|
|
1441
1580
|
usage: "/q",
|
|
1581
|
+
category: "General",
|
|
1442
1582
|
run(_args, ctx) {
|
|
1443
1583
|
ctx.exit();
|
|
1444
1584
|
}
|
|
1445
1585
|
});
|
|
1446
|
-
|
|
1586
|
+
register({
|
|
1447
1587
|
name: "model",
|
|
1448
|
-
description: "
|
|
1588
|
+
description: "Switch the current model (opens a picker when no name is given)",
|
|
1449
1589
|
usage: "/model [name]",
|
|
1590
|
+
category: "Settings",
|
|
1450
1591
|
async run(args, ctx) {
|
|
1451
1592
|
if (!args) {
|
|
1452
|
-
ctx.addSystemMessage(ctx.
|
|
1593
|
+
ctx.addSystemMessage(await ctx.pickModel());
|
|
1453
1594
|
} else {
|
|
1454
1595
|
ctx.addSystemMessage(await ctx.switchModel(args));
|
|
1455
1596
|
}
|
|
1456
1597
|
}
|
|
1457
1598
|
});
|
|
1458
|
-
|
|
1599
|
+
register({
|
|
1459
1600
|
name: "resume",
|
|
1460
|
-
description: "
|
|
1601
|
+
description: "Resume a session by id, or pick one from a list (--all for every directory)",
|
|
1461
1602
|
usage: "/resume [sessionId | --all]",
|
|
1603
|
+
category: "Sessions",
|
|
1462
1604
|
async run(args, ctx) {
|
|
1463
1605
|
const arg = args.trim();
|
|
1464
1606
|
if (arg === "--all") {
|
|
1465
|
-
ctx.addSystemMessage(await ctx.
|
|
1607
|
+
ctx.addSystemMessage(await ctx.pickSession(true));
|
|
1466
1608
|
} else if (!arg) {
|
|
1467
|
-
ctx.addSystemMessage(await ctx.
|
|
1609
|
+
ctx.addSystemMessage(await ctx.pickSession(false));
|
|
1468
1610
|
} else {
|
|
1469
1611
|
ctx.addSystemMessage(await ctx.resumeSession(arg));
|
|
1470
1612
|
}
|
|
1471
1613
|
}
|
|
1472
1614
|
});
|
|
1473
|
-
|
|
1615
|
+
register({
|
|
1616
|
+
name: "fork",
|
|
1617
|
+
description: "Fork the current session into a new one and switch to it",
|
|
1618
|
+
usage: "/fork",
|
|
1619
|
+
category: "Sessions",
|
|
1620
|
+
async run(_args, ctx) {
|
|
1621
|
+
ctx.addSystemMessage(await ctx.forkSession());
|
|
1622
|
+
}
|
|
1623
|
+
});
|
|
1624
|
+
register({
|
|
1625
|
+
name: "search",
|
|
1626
|
+
description: "Full-text search across all stored sessions",
|
|
1627
|
+
usage: "/search <query>",
|
|
1628
|
+
category: "Sessions",
|
|
1629
|
+
async run(args, ctx) {
|
|
1630
|
+
const query = args.trim();
|
|
1631
|
+
if (!query) {
|
|
1632
|
+
ctx.addSystemMessage("Usage: /search <query>");
|
|
1633
|
+
return;
|
|
1634
|
+
}
|
|
1635
|
+
ctx.addSystemMessage(formatSearchResults(await searchSessions(query), query));
|
|
1636
|
+
}
|
|
1637
|
+
});
|
|
1638
|
+
register({
|
|
1474
1639
|
name: "todo",
|
|
1475
1640
|
description: "Show the current todo list",
|
|
1476
1641
|
usage: "/todo",
|
|
1642
|
+
category: "Info",
|
|
1477
1643
|
async run(_args, ctx) {
|
|
1478
1644
|
ctx.addSystemMessage(await ctx.showTodos());
|
|
1479
1645
|
}
|
|
1480
1646
|
});
|
|
1481
|
-
|
|
1647
|
+
register({
|
|
1482
1648
|
name: "tasks",
|
|
1483
1649
|
description: "List background shell tasks",
|
|
1484
1650
|
usage: "/tasks",
|
|
1651
|
+
category: "Info",
|
|
1485
1652
|
run(_args, ctx) {
|
|
1486
1653
|
ctx.addSystemMessage(ctx.listTasks());
|
|
1487
1654
|
}
|
|
1488
1655
|
});
|
|
1489
|
-
|
|
1656
|
+
register({
|
|
1490
1657
|
name: "cost",
|
|
1491
1658
|
description: "Show API token usage for this session",
|
|
1492
1659
|
usage: "/cost",
|
|
1660
|
+
category: "Info",
|
|
1493
1661
|
run(_args, ctx) {
|
|
1494
1662
|
ctx.addSystemMessage(ctx.showUsage());
|
|
1495
1663
|
}
|
|
1496
1664
|
});
|
|
1497
|
-
|
|
1665
|
+
register({
|
|
1498
1666
|
name: "usage",
|
|
1499
1667
|
description: "Show token usage aggregated across all sessions",
|
|
1500
1668
|
usage: "/usage",
|
|
1669
|
+
category: "Info",
|
|
1501
1670
|
async run(_args, ctx) {
|
|
1502
1671
|
ctx.addSystemMessage(await ctx.showGlobalUsage());
|
|
1503
1672
|
}
|
|
1504
1673
|
});
|
|
1505
|
-
|
|
1674
|
+
register({
|
|
1506
1675
|
name: "config",
|
|
1507
1676
|
description: "Show the current configuration",
|
|
1508
1677
|
usage: "/config",
|
|
1678
|
+
category: "Info",
|
|
1509
1679
|
run(_args, ctx) {
|
|
1510
1680
|
ctx.addSystemMessage(ctx.describeConfig());
|
|
1511
1681
|
}
|
|
1512
1682
|
});
|
|
1513
|
-
|
|
1683
|
+
register({
|
|
1514
1684
|
name: "compact",
|
|
1515
1685
|
description: "Compact the conversation history to free up context",
|
|
1516
1686
|
usage: "/compact",
|
|
1687
|
+
category: "Sessions",
|
|
1517
1688
|
async run(_args, ctx) {
|
|
1518
1689
|
ctx.addSystemMessage(await ctx.compactContext());
|
|
1519
1690
|
}
|
|
1520
1691
|
});
|
|
1521
|
-
|
|
1692
|
+
register({
|
|
1522
1693
|
name: "export",
|
|
1523
1694
|
description: "Export the current session to a Markdown file",
|
|
1524
1695
|
usage: "/export [path]",
|
|
1696
|
+
category: "Sessions",
|
|
1525
1697
|
async run(args, ctx) {
|
|
1526
1698
|
ctx.addSystemMessage(await ctx.exportSession(args));
|
|
1527
1699
|
}
|
|
1528
1700
|
});
|
|
1529
|
-
|
|
1701
|
+
register({
|
|
1530
1702
|
name: "permission",
|
|
1531
|
-
description: "
|
|
1703
|
+
description: "Set the permission mode (opens a picker when no mode is given)",
|
|
1532
1704
|
usage: "/permission [ask|auto|readonly|yolo]",
|
|
1705
|
+
category: "Settings",
|
|
1533
1706
|
async run(args, ctx) {
|
|
1534
|
-
|
|
1707
|
+
const arg = args.trim();
|
|
1708
|
+
if (!arg) {
|
|
1709
|
+
ctx.addSystemMessage(await ctx.pickPermissionMode());
|
|
1710
|
+
} else {
|
|
1711
|
+
ctx.addSystemMessage(await ctx.permissionMode(arg));
|
|
1712
|
+
}
|
|
1535
1713
|
}
|
|
1536
1714
|
});
|
|
1537
|
-
|
|
1715
|
+
register({
|
|
1538
1716
|
name: "plan",
|
|
1539
1717
|
description: "Toggle plan mode: read-only research, then approve the plan before executing",
|
|
1540
1718
|
usage: "/plan",
|
|
1719
|
+
category: "Settings",
|
|
1541
1720
|
async run(_args, ctx) {
|
|
1542
1721
|
ctx.addSystemMessage(await ctx.planMode());
|
|
1543
1722
|
}
|
|
1544
1723
|
});
|
|
1545
|
-
|
|
1724
|
+
register({
|
|
1725
|
+
name: "memory",
|
|
1726
|
+
description: "Show user memory (MEMORY.md), or add an entry",
|
|
1727
|
+
usage: "/memory [add <text>]",
|
|
1728
|
+
category: "Settings",
|
|
1729
|
+
run(args, ctx) {
|
|
1730
|
+
const arg = args.trim();
|
|
1731
|
+
if (arg === "add" || arg.startsWith("add ")) {
|
|
1732
|
+
const text = arg.slice(3).trim();
|
|
1733
|
+
if (!text) {
|
|
1734
|
+
ctx.addSystemMessage("Usage: /memory add <text>");
|
|
1735
|
+
return;
|
|
1736
|
+
}
|
|
1737
|
+
appendUserMemory(text);
|
|
1738
|
+
ctx.addSystemMessage(`Saved to user memory: - ${text}`);
|
|
1739
|
+
return;
|
|
1740
|
+
}
|
|
1741
|
+
if (arg) {
|
|
1742
|
+
ctx.addSystemMessage(`Unknown argument "${arg}". Usage: /memory [add <text>]`);
|
|
1743
|
+
return;
|
|
1744
|
+
}
|
|
1745
|
+
const file = userMemoryPath();
|
|
1746
|
+
let content = "";
|
|
1747
|
+
try {
|
|
1748
|
+
content = readFileSync(file, "utf8").trim();
|
|
1749
|
+
} catch {
|
|
1750
|
+
content = "";
|
|
1751
|
+
}
|
|
1752
|
+
if (!content) {
|
|
1753
|
+
ctx.addSystemMessage(
|
|
1754
|
+
`No user memory yet (${file}).
|
|
1755
|
+
Add entries with /memory add <text> or ask the agent to remember something.`
|
|
1756
|
+
);
|
|
1757
|
+
return;
|
|
1758
|
+
}
|
|
1759
|
+
ctx.addSystemMessage(`User memory (${file}):
|
|
1760
|
+
${content}`);
|
|
1761
|
+
}
|
|
1762
|
+
});
|
|
1763
|
+
register({
|
|
1546
1764
|
name: "undo",
|
|
1547
1765
|
description: "Undo the last conversation turn: revert its file changes (write_file/edit_file) and retract its messages",
|
|
1548
1766
|
usage: "/undo",
|
|
1767
|
+
category: "Changes",
|
|
1549
1768
|
async run(_args, ctx) {
|
|
1550
1769
|
ctx.addSystemMessage(await ctx.undo());
|
|
1551
1770
|
}
|
|
1552
1771
|
});
|
|
1553
|
-
|
|
1772
|
+
register({
|
|
1554
1773
|
name: "rewind",
|
|
1555
1774
|
description: "List file-change checkpoints, or rewind to just before one: restore files and retract the conversation",
|
|
1556
1775
|
usage: "/rewind [n]",
|
|
1776
|
+
category: "Changes",
|
|
1557
1777
|
async run(args, ctx) {
|
|
1558
1778
|
ctx.addSystemMessage(await ctx.rewind(args.trim()));
|
|
1559
1779
|
}
|
|
1560
1780
|
});
|
|
1561
|
-
|
|
1781
|
+
register({
|
|
1562
1782
|
name: "init",
|
|
1563
1783
|
description: "Generate an AGENTS.md for the current project",
|
|
1564
1784
|
usage: "/init [force]",
|
|
1785
|
+
category: "Settings",
|
|
1565
1786
|
async run(args, ctx) {
|
|
1566
1787
|
ctx.addSystemMessage(await ctx.initProject(args));
|
|
1567
1788
|
}
|
|
1568
1789
|
});
|
|
1569
|
-
|
|
1790
|
+
register({
|
|
1570
1791
|
name: "doctor",
|
|
1571
1792
|
description: "Run environment and configuration checks",
|
|
1572
1793
|
usage: "/doctor",
|
|
1794
|
+
category: "Info",
|
|
1573
1795
|
async run(_args, ctx) {
|
|
1574
1796
|
ctx.addSystemMessage(await ctx.runDoctor());
|
|
1575
1797
|
}
|
|
1576
1798
|
});
|
|
1577
|
-
|
|
1799
|
+
register({
|
|
1800
|
+
name: "skills",
|
|
1801
|
+
description: "List available skills (project scope overrides user scope)",
|
|
1802
|
+
usage: "/skills",
|
|
1803
|
+
category: "Info",
|
|
1804
|
+
run(_args, ctx) {
|
|
1805
|
+
const skills = discoverSkills(ctx.cwd ?? process.cwd());
|
|
1806
|
+
if (skills.length === 0) {
|
|
1807
|
+
ctx.addSystemMessage(
|
|
1808
|
+
"No skills found. Add a SKILL.md under .star/skills/<name>/ (project) or ~/.star-cli/skills/<name>/ (user)."
|
|
1809
|
+
);
|
|
1810
|
+
return;
|
|
1811
|
+
}
|
|
1812
|
+
const lines = skills.map((s) => `${s.name} [${s.scope}] \u2014 ${s.description}`);
|
|
1813
|
+
ctx.addSystemMessage(`Available skills:
|
|
1814
|
+
${lines.join("\n")}`);
|
|
1815
|
+
}
|
|
1816
|
+
});
|
|
1817
|
+
register({
|
|
1578
1818
|
name: "commit",
|
|
1579
1819
|
description: "Analyze uncommitted changes and create a git commit (Conventional Commits)",
|
|
1580
1820
|
usage: "/commit [instructions]",
|
|
1821
|
+
category: "Changes",
|
|
1581
1822
|
async run(args, ctx) {
|
|
1582
1823
|
const cwd = ctx.cwd ?? process.cwd();
|
|
1583
1824
|
if (!isGitRepo(cwd)) {
|
|
@@ -1596,10 +1837,11 @@ ${lines.join("\n")}`);
|
|
|
1596
1837
|
ctx.addSystemMessage("/commit: this context cannot submit prompts to the model.");
|
|
1597
1838
|
}
|
|
1598
1839
|
});
|
|
1599
|
-
|
|
1840
|
+
register({
|
|
1600
1841
|
name: "copy",
|
|
1601
1842
|
description: "Copy the last assistant reply to the clipboard (`all` for the whole conversation)",
|
|
1602
1843
|
usage: "/copy [all]",
|
|
1844
|
+
category: "Sessions",
|
|
1603
1845
|
async run(args, ctx) {
|
|
1604
1846
|
const arg = args.trim();
|
|
1605
1847
|
if (arg !== "" && arg !== "all") {
|
|
@@ -1624,10 +1866,11 @@ ${lines.join("\n")}`);
|
|
|
1624
1866
|
);
|
|
1625
1867
|
}
|
|
1626
1868
|
});
|
|
1627
|
-
|
|
1869
|
+
register({
|
|
1628
1870
|
name: "diff",
|
|
1629
1871
|
description: "Show uncommitted changes (git status + colored diff)",
|
|
1630
1872
|
usage: "/diff",
|
|
1873
|
+
category: "Changes",
|
|
1631
1874
|
run(_args, ctx) {
|
|
1632
1875
|
const cwd = ctx.cwd ?? process.cwd();
|
|
1633
1876
|
if (!isGitRepo(cwd)) {
|
|
@@ -1663,15 +1906,15 @@ ${result.diff}${note ? `
|
|
|
1663
1906
|
}
|
|
1664
1907
|
|
|
1665
1908
|
// src/cli/commands/custom.ts
|
|
1666
|
-
import { readFileSync, readdirSync } from "fs";
|
|
1667
|
-
import
|
|
1909
|
+
import { readFileSync as readFileSync2, readdirSync } from "fs";
|
|
1910
|
+
import path11 from "path";
|
|
1668
1911
|
var NAME_PATTERN = /^[a-z0-9-]+$/;
|
|
1669
1912
|
var DESCRIPTION_PATTERN = /^<!--\s*description:\s*(.+?)\s*-->\s*$/;
|
|
1670
1913
|
var DEFAULT_DESCRIPTION = "Custom prompt command";
|
|
1671
1914
|
function parseCommandFile(filePath, name) {
|
|
1672
1915
|
let raw;
|
|
1673
1916
|
try {
|
|
1674
|
-
raw =
|
|
1917
|
+
raw = readFileSync2(filePath, "utf8");
|
|
1675
1918
|
} catch {
|
|
1676
1919
|
return null;
|
|
1677
1920
|
}
|
|
@@ -1699,14 +1942,14 @@ function scanDir(dir, into) {
|
|
|
1699
1942
|
if (!entry.endsWith(".md")) continue;
|
|
1700
1943
|
const name = entry.slice(0, -".md".length);
|
|
1701
1944
|
if (!NAME_PATTERN.test(name)) continue;
|
|
1702
|
-
const parsed = parseCommandFile(
|
|
1945
|
+
const parsed = parseCommandFile(path11.join(dir, entry), name);
|
|
1703
1946
|
if (parsed) into.set(name, parsed);
|
|
1704
1947
|
}
|
|
1705
1948
|
}
|
|
1706
1949
|
function loadCustomCommands(cwd, home = starHome()) {
|
|
1707
1950
|
const byName = /* @__PURE__ */ new Map();
|
|
1708
|
-
scanDir(
|
|
1709
|
-
scanDir(
|
|
1951
|
+
scanDir(path11.join(home, "commands"), byName);
|
|
1952
|
+
scanDir(path11.join(cwd, ".star", "commands"), byName);
|
|
1710
1953
|
return [...byName.values()];
|
|
1711
1954
|
}
|
|
1712
1955
|
function registerCustomCommands(registry, cwd, home) {
|
|
@@ -1717,6 +1960,7 @@ function registerCustomCommands(registry, cwd, home) {
|
|
|
1717
1960
|
name: command.name,
|
|
1718
1961
|
description: command.description,
|
|
1719
1962
|
usage: `/${command.name} [args]`,
|
|
1963
|
+
category: "Custom",
|
|
1720
1964
|
run(args, ctx) {
|
|
1721
1965
|
const prompt = command.template.split("$ARGUMENTS").join(args);
|
|
1722
1966
|
if (ctx.submitPrompt) {
|
|
@@ -1732,7 +1976,7 @@ function registerCustomCommands(registry, cwd, home) {
|
|
|
1732
1976
|
|
|
1733
1977
|
// src/cli/commands/doctor.ts
|
|
1734
1978
|
import fs8 from "fs/promises";
|
|
1735
|
-
import
|
|
1979
|
+
import path12 from "path";
|
|
1736
1980
|
var MIN_NODE_MAJOR = 20;
|
|
1737
1981
|
var REGISTRY_URL = "https://registry.npmjs.org/";
|
|
1738
1982
|
var REGISTRY_TIMEOUT_MS = 5e3;
|
|
@@ -1784,7 +2028,7 @@ async function runDoctor(opts) {
|
|
|
1784
2028
|
detail: `${spec.label} (${spec.shell})`
|
|
1785
2029
|
});
|
|
1786
2030
|
}
|
|
1787
|
-
const configPath =
|
|
2031
|
+
const configPath = path12.join(home, "config.toml");
|
|
1788
2032
|
const configExists = await pathExists(configPath);
|
|
1789
2033
|
checks.push({
|
|
1790
2034
|
name: "STAR_HOME / config",
|
|
@@ -1821,12 +2065,12 @@ async function runDoctor(opts) {
|
|
|
1821
2065
|
});
|
|
1822
2066
|
}
|
|
1823
2067
|
}
|
|
1824
|
-
const sessions =
|
|
2068
|
+
const sessions = path12.join(home, "sessions");
|
|
1825
2069
|
let writable = false;
|
|
1826
2070
|
let writeError = "";
|
|
1827
2071
|
try {
|
|
1828
2072
|
await fs8.mkdir(sessions, { recursive: true });
|
|
1829
|
-
const probe =
|
|
2073
|
+
const probe = path12.join(sessions, `.doctor-probe-${process.pid}`);
|
|
1830
2074
|
await fs8.writeFile(probe, "ok");
|
|
1831
2075
|
await fs8.unlink(probe);
|
|
1832
2076
|
writable = true;
|
|
@@ -1864,7 +2108,7 @@ function formatDoctorReport(report) {
|
|
|
1864
2108
|
|
|
1865
2109
|
// src/cli/commands/init-project.ts
|
|
1866
2110
|
import fs9 from "fs/promises";
|
|
1867
|
-
import
|
|
2111
|
+
import path13 from "path";
|
|
1868
2112
|
import { generateText } from "ai";
|
|
1869
2113
|
var IGNORED_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", "out", "coverage"]);
|
|
1870
2114
|
var MAX_ENTRIES = 20;
|
|
@@ -1879,7 +2123,7 @@ async function fileExists2(target) {
|
|
|
1879
2123
|
}
|
|
1880
2124
|
async function readPackageJson(cwd) {
|
|
1881
2125
|
try {
|
|
1882
|
-
const raw = await fs9.readFile(
|
|
2126
|
+
const raw = await fs9.readFile(path13.join(cwd, "package.json"), "utf8");
|
|
1883
2127
|
const parsed = JSON.parse(raw);
|
|
1884
2128
|
if (typeof parsed !== "object" || parsed === null) return null;
|
|
1885
2129
|
return parsed;
|
|
@@ -1896,10 +2140,10 @@ function stringRecord(value) {
|
|
|
1896
2140
|
return out;
|
|
1897
2141
|
}
|
|
1898
2142
|
async function detectPackageManager(cwd) {
|
|
1899
|
-
if (await fileExists2(
|
|
1900
|
-
if (await fileExists2(
|
|
1901
|
-
if (await fileExists2(
|
|
1902
|
-
if (await fileExists2(
|
|
2143
|
+
if (await fileExists2(path13.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
|
|
2144
|
+
if (await fileExists2(path13.join(cwd, "yarn.lock"))) return "yarn";
|
|
2145
|
+
if (await fileExists2(path13.join(cwd, "package-lock.json"))) return "npm";
|
|
2146
|
+
if (await fileExists2(path13.join(cwd, "bun.lockb"))) return "bun";
|
|
1903
2147
|
return null;
|
|
1904
2148
|
}
|
|
1905
2149
|
function inferCommands(pm, scripts) {
|
|
@@ -1933,7 +2177,7 @@ async function listEntries(cwd) {
|
|
|
1933
2177
|
async function readReadmeHead(cwd) {
|
|
1934
2178
|
for (const name of ["README.md", "readme.md", "README"]) {
|
|
1935
2179
|
try {
|
|
1936
|
-
const raw = await fs9.readFile(
|
|
2180
|
+
const raw = await fs9.readFile(path13.join(cwd, name), "utf8");
|
|
1937
2181
|
return raw.split("\n").map((line) => line.trim()).filter((line) => line.length > 0).slice(0, README_HEAD_LINES);
|
|
1938
2182
|
} catch {
|
|
1939
2183
|
}
|
|
@@ -1946,14 +2190,14 @@ async function scanProject(cwd) {
|
|
|
1946
2190
|
const scripts = stringRecord(pkg?.scripts);
|
|
1947
2191
|
return {
|
|
1948
2192
|
name: typeof pkg?.name === "string" ? pkg.name : null,
|
|
1949
|
-
dirName:
|
|
2193
|
+
dirName: path13.basename(cwd),
|
|
1950
2194
|
packageManager,
|
|
1951
2195
|
scripts,
|
|
1952
2196
|
dependencies: Object.keys(stringRecord(pkg?.dependencies)),
|
|
1953
2197
|
devDependencies: Object.keys(stringRecord(pkg?.devDependencies)),
|
|
1954
2198
|
commands: inferCommands(packageManager, scripts),
|
|
1955
|
-
hasTsconfig: await fileExists2(
|
|
1956
|
-
isGitRepo: await fileExists2(
|
|
2199
|
+
hasTsconfig: await fileExists2(path13.join(cwd, "tsconfig.json")),
|
|
2200
|
+
isGitRepo: await fileExists2(path13.join(cwd, ".git")),
|
|
1957
2201
|
readmeHead: await readReadmeHead(cwd),
|
|
1958
2202
|
entries: await listEntries(cwd)
|
|
1959
2203
|
};
|
|
@@ -2020,7 +2264,7 @@ Write the AGENTS.md.`
|
|
|
2020
2264
|
}
|
|
2021
2265
|
async function initProject(opts) {
|
|
2022
2266
|
const { cwd, args, model } = opts;
|
|
2023
|
-
const target =
|
|
2267
|
+
const target = path13.join(cwd, "AGENTS.md");
|
|
2024
2268
|
const force = args.split(/\s+/).includes("force");
|
|
2025
2269
|
if (!force && await fileExists2(target)) {
|
|
2026
2270
|
return {
|
|
@@ -2096,7 +2340,7 @@ function parseSlashCommand(input) {
|
|
|
2096
2340
|
}
|
|
2097
2341
|
|
|
2098
2342
|
// src/cli/commands/rewind.ts
|
|
2099
|
-
import
|
|
2343
|
+
import path14 from "path";
|
|
2100
2344
|
function planRewind(snapshots, id) {
|
|
2101
2345
|
const target = snapshots.find((snapshot) => snapshot.id === id);
|
|
2102
2346
|
if (!target) return null;
|
|
@@ -2110,7 +2354,7 @@ function planRewind(snapshots, id) {
|
|
|
2110
2354
|
return { target, affected, messageIndex };
|
|
2111
2355
|
}
|
|
2112
2356
|
function displayPath(filePath, cwd) {
|
|
2113
|
-
const relative =
|
|
2357
|
+
const relative = path14.relative(cwd, filePath);
|
|
2114
2358
|
return relative !== "" && !relative.startsWith("..") ? relative : filePath;
|
|
2115
2359
|
}
|
|
2116
2360
|
function formatTime(timestamp) {
|
|
@@ -2133,6 +2377,33 @@ function formatCheckpointList(snapshots, cwd) {
|
|
|
2133
2377
|
].join("\n");
|
|
2134
2378
|
}
|
|
2135
2379
|
|
|
2380
|
+
// src/cli/commands/suggest.ts
|
|
2381
|
+
var MAX_SUGGESTIONS = 5;
|
|
2382
|
+
function isSubsequence(query, target) {
|
|
2383
|
+
let i = 0;
|
|
2384
|
+
for (const ch of target) {
|
|
2385
|
+
if (ch === query[i]) i += 1;
|
|
2386
|
+
if (i === query.length) return true;
|
|
2387
|
+
}
|
|
2388
|
+
return query.length === 0;
|
|
2389
|
+
}
|
|
2390
|
+
function filterCommands(input, commands) {
|
|
2391
|
+
if (!input.startsWith("/")) return [];
|
|
2392
|
+
if (/\s/.test(input)) return [];
|
|
2393
|
+
const prefix = input.slice(1).toLowerCase();
|
|
2394
|
+
const byName = (a, b) => a.name.localeCompare(b.name);
|
|
2395
|
+
const prefixed = commands.filter((cmd) => cmd.name.toLowerCase().startsWith(prefix)).sort(byName);
|
|
2396
|
+
const fuzzy = commands.filter(
|
|
2397
|
+
(cmd) => !cmd.name.toLowerCase().startsWith(prefix) && isSubsequence(prefix, cmd.name.toLowerCase())
|
|
2398
|
+
).sort(byName);
|
|
2399
|
+
return [...prefixed, ...fuzzy].slice(0, MAX_SUGGESTIONS);
|
|
2400
|
+
}
|
|
2401
|
+
function didYouMeanSuffix(names) {
|
|
2402
|
+
if (names.length === 0) return "";
|
|
2403
|
+
const listed = names.slice(0, 3).map((name) => `/${name}`);
|
|
2404
|
+
return ` Did you mean: ${listed.join(", ")}?`;
|
|
2405
|
+
}
|
|
2406
|
+
|
|
2136
2407
|
// src/cli/components/ExitPlanPrompt.tsx
|
|
2137
2408
|
import { Box, Text, useInput } from "ink";
|
|
2138
2409
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
@@ -2153,18 +2424,9 @@ function ExitPlanPrompt({ onDecision }) {
|
|
|
2153
2424
|
import { Box as Box2, Text as Text2, useInput as useInput2 } from "ink";
|
|
2154
2425
|
import { useEffect, useRef, useState } from "react";
|
|
2155
2426
|
|
|
2156
|
-
// src/cli/commands/suggest.ts
|
|
2157
|
-
var MAX_SUGGESTIONS = 5;
|
|
2158
|
-
function filterCommands(input, commands) {
|
|
2159
|
-
if (!input.startsWith("/")) return [];
|
|
2160
|
-
if (/\s/.test(input)) return [];
|
|
2161
|
-
const prefix = input.slice(1).toLowerCase();
|
|
2162
|
-
return commands.filter((cmd) => cmd.name.toLowerCase().startsWith(prefix)).sort((a, b) => a.name.localeCompare(b.name)).slice(0, MAX_SUGGESTIONS);
|
|
2163
|
-
}
|
|
2164
|
-
|
|
2165
2427
|
// src/cli/path-suggest.ts
|
|
2166
2428
|
import { readdir } from "fs/promises";
|
|
2167
|
-
import
|
|
2429
|
+
import path15 from "path";
|
|
2168
2430
|
var MAX_SCAN_ENTRIES = 2e3;
|
|
2169
2431
|
function extractAtToken(value, cursor) {
|
|
2170
2432
|
const before = value.slice(0, cursor);
|
|
@@ -2179,9 +2441,10 @@ async function suggestPaths(token, cwd, maxScanEntries = MAX_SCAN_ENTRIES, maxSu
|
|
|
2179
2441
|
const dirPart = slash >= 0 ? normalized.slice(0, slash + 1) : "";
|
|
2180
2442
|
const basePrefix = slash >= 0 ? normalized.slice(slash + 1) : normalized;
|
|
2181
2443
|
if (dirPart.split("/").some((segment) => SKIP_DIRS.has(segment))) return [];
|
|
2182
|
-
const dirAbs =
|
|
2444
|
+
const dirAbs = path15.resolve(cwd, dirPart === "" ? "." : dirPart);
|
|
2183
2445
|
const entries = await readdir(dirAbs, { withFileTypes: true }).catch(() => null);
|
|
2184
2446
|
if (!entries) return [];
|
|
2447
|
+
const ignore = createIgnorePredicate(cwd);
|
|
2185
2448
|
const lower = basePrefix.toLowerCase();
|
|
2186
2449
|
const matches = [];
|
|
2187
2450
|
let scanned = 0;
|
|
@@ -2193,6 +2456,7 @@ async function suggestPaths(token, cwd, maxScanEntries = MAX_SCAN_ENTRIES, maxSu
|
|
|
2193
2456
|
if (isDir && SKIP_DIRS.has(entry.name)) continue;
|
|
2194
2457
|
if (entry.name.startsWith(".") && !basePrefix.startsWith(".")) continue;
|
|
2195
2458
|
if (!entry.name.toLowerCase().startsWith(lower)) continue;
|
|
2459
|
+
if (ignore?.(path15.join(dirAbs, entry.name), isDir)) continue;
|
|
2196
2460
|
matches.push({ path: `${dirPart}${entry.name}${isDir ? "/" : ""}`, isDir });
|
|
2197
2461
|
}
|
|
2198
2462
|
matches.sort((a, b) => a.path.localeCompare(b.path));
|
|
@@ -2207,6 +2471,7 @@ function InputBox({
|
|
|
2207
2471
|
commands,
|
|
2208
2472
|
cwd,
|
|
2209
2473
|
refill,
|
|
2474
|
+
initialHistory,
|
|
2210
2475
|
onSubmit,
|
|
2211
2476
|
onInterrupt,
|
|
2212
2477
|
onExit,
|
|
@@ -2214,7 +2479,7 @@ function InputBox({
|
|
|
2214
2479
|
}) {
|
|
2215
2480
|
const [value, setValue] = useState("");
|
|
2216
2481
|
const [cursor, setCursor] = useState(0);
|
|
2217
|
-
const [history, setHistory] = useState([]);
|
|
2482
|
+
const [history, setHistory] = useState(() => initialHistory ?? []);
|
|
2218
2483
|
const [highlight, setHighlight] = useState(0);
|
|
2219
2484
|
const [suggestionsDismissed, setSuggestionsDismissed] = useState(false);
|
|
2220
2485
|
const [pathSuggestions, setPathSuggestions] = useState([]);
|
|
@@ -2396,12 +2661,13 @@ function InputBox({
|
|
|
2396
2661
|
/* @__PURE__ */ jsx2(Text2, { inverse: true, children: at }),
|
|
2397
2662
|
/* @__PURE__ */ jsx2(Text2, { children: after })
|
|
2398
2663
|
] }),
|
|
2399
|
-
suggestions.length > 0 && /* @__PURE__ */ jsx2(Box2, { flexDirection: "column", paddingLeft: 2, children: suggestions.map(
|
|
2400
|
-
|
|
2401
|
-
|
|
2664
|
+
suggestions.length > 0 && /* @__PURE__ */ jsx2(Box2, { flexDirection: "column", paddingLeft: 2, children: suggestions.map((cmd, index) => {
|
|
2665
|
+
const label = cmd.usage ?? `/${cmd.name}`;
|
|
2666
|
+
return index === activeIndex ? /* @__PURE__ */ jsx2(Text2, { bold: true, inverse: true, children: `${label} - ${cmd.description}` }, cmd.name) : /* @__PURE__ */ jsxs2(Text2, { children: [
|
|
2667
|
+
/* @__PURE__ */ jsx2(Text2, { color: "cyan", children: label }),
|
|
2402
2668
|
/* @__PURE__ */ jsx2(Text2, { dimColor: true, children: ` - ${cmd.description}` })
|
|
2403
|
-
] }, cmd.name)
|
|
2404
|
-
) }),
|
|
2669
|
+
] }, cmd.name);
|
|
2670
|
+
}) }),
|
|
2405
2671
|
showPathSuggestions && /* @__PURE__ */ jsx2(Box2, { flexDirection: "column", paddingLeft: 2, children: pathSuggestions.map(
|
|
2406
2672
|
(sugg, index) => index === activeIndex ? /* @__PURE__ */ jsx2(Text2, { bold: true, inverse: true, children: `@${sugg.path}` }, sugg.path) : /* @__PURE__ */ jsxs2(Text2, { children: [
|
|
2407
2673
|
/* @__PURE__ */ jsx2(Text2, { color: "cyan", children: `@${sugg.path}` }),
|
|
@@ -2509,10 +2775,72 @@ function RewindConfirmPrompt({ summary, onDecision }) {
|
|
|
2509
2775
|
] });
|
|
2510
2776
|
}
|
|
2511
2777
|
|
|
2778
|
+
// src/cli/components/SelectPrompt.tsx
|
|
2779
|
+
import { Box as Box6, Text as Text7, useInput as useInput5 } from "ink";
|
|
2780
|
+
import { useState as useState2 } from "react";
|
|
2781
|
+
import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
2782
|
+
var SELECT_PROMPT_MAX_VISIBLE = 9;
|
|
2783
|
+
function SelectPrompt({ title, options, onSelect, onCancel }) {
|
|
2784
|
+
const [index, setIndex] = useState2(0);
|
|
2785
|
+
useInput5((input, key) => {
|
|
2786
|
+
if (key.escape) {
|
|
2787
|
+
onCancel();
|
|
2788
|
+
return;
|
|
2789
|
+
}
|
|
2790
|
+
if (key.return) {
|
|
2791
|
+
const option = options[index];
|
|
2792
|
+
if (option) onSelect(option.value);
|
|
2793
|
+
return;
|
|
2794
|
+
}
|
|
2795
|
+
if (key.downArrow || input === "j") {
|
|
2796
|
+
setIndex((i) => Math.min(i + 1, options.length - 1));
|
|
2797
|
+
} else if (key.upArrow || input === "k") {
|
|
2798
|
+
setIndex((i) => Math.max(i - 1, 0));
|
|
2799
|
+
}
|
|
2800
|
+
});
|
|
2801
|
+
const maxStart = Math.max(0, options.length - SELECT_PROMPT_MAX_VISIBLE);
|
|
2802
|
+
const start = Math.min(Math.max(0, index - Math.floor(SELECT_PROMPT_MAX_VISIBLE / 2)), maxStart);
|
|
2803
|
+
const visible = options.slice(start, start + SELECT_PROMPT_MAX_VISIBLE);
|
|
2804
|
+
const above = start;
|
|
2805
|
+
const below = options.length - start - visible.length;
|
|
2806
|
+
return /* @__PURE__ */ jsxs7(Box6, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1, children: [
|
|
2807
|
+
/* @__PURE__ */ jsx7(Text7, { bold: true, color: "cyan", children: title }),
|
|
2808
|
+
above > 0 && /* @__PURE__ */ jsxs7(Text7, { dimColor: true, children: [
|
|
2809
|
+
"\u2026 ",
|
|
2810
|
+
above,
|
|
2811
|
+
" more above"
|
|
2812
|
+
] }),
|
|
2813
|
+
visible.map((option, i) => {
|
|
2814
|
+
const active = start + i === index;
|
|
2815
|
+
return /* @__PURE__ */ jsxs7(Text7, { children: [
|
|
2816
|
+
/* @__PURE__ */ jsxs7(Text7, { bold: active, inverse: active, children: [
|
|
2817
|
+
active ? "\u276F " : " ",
|
|
2818
|
+
option.label
|
|
2819
|
+
] }),
|
|
2820
|
+
option.hint && /* @__PURE__ */ jsxs7(Text7, { dimColor: true, children: [
|
|
2821
|
+
" (",
|
|
2822
|
+
option.hint,
|
|
2823
|
+
")"
|
|
2824
|
+
] }),
|
|
2825
|
+
option.description && /* @__PURE__ */ jsxs7(Text7, { dimColor: true, children: [
|
|
2826
|
+
" \u2014 ",
|
|
2827
|
+
option.description
|
|
2828
|
+
] })
|
|
2829
|
+
] }, option.value);
|
|
2830
|
+
}),
|
|
2831
|
+
below > 0 && /* @__PURE__ */ jsxs7(Text7, { dimColor: true, children: [
|
|
2832
|
+
"\u2026 ",
|
|
2833
|
+
below,
|
|
2834
|
+
" more below"
|
|
2835
|
+
] }),
|
|
2836
|
+
/* @__PURE__ */ jsx7(Text7, { dimColor: true, children: "[\u2191/\u2193 or j/k] move [Enter] select [Esc] cancel" })
|
|
2837
|
+
] });
|
|
2838
|
+
}
|
|
2839
|
+
|
|
2512
2840
|
// src/cli/components/StatusBar.tsx
|
|
2513
|
-
import { Box as
|
|
2841
|
+
import { Box as Box7, Text as Text8, useStdout } from "ink";
|
|
2514
2842
|
import { memo as memo2 } from "react";
|
|
2515
|
-
import { jsxs as
|
|
2843
|
+
import { jsxs as jsxs8 } from "react/jsx-runtime";
|
|
2516
2844
|
var BG_LABEL_MAX = 40;
|
|
2517
2845
|
var WIDE_COLUMNS = 120;
|
|
2518
2846
|
function cwdLabel(cwd, wide) {
|
|
@@ -2534,36 +2862,36 @@ var StatusBar = memo2(function StatusBar2({
|
|
|
2534
2862
|
const wide = (stdout?.columns ?? 80) >= WIDE_COLUMNS;
|
|
2535
2863
|
const joined = backgroundTasks.join(", ");
|
|
2536
2864
|
const labels = joined.length > BG_LABEL_MAX ? `${joined.slice(0, BG_LABEL_MAX)}\u2026` : joined;
|
|
2537
|
-
return /* @__PURE__ */
|
|
2538
|
-
/* @__PURE__ */
|
|
2865
|
+
return /* @__PURE__ */ jsxs8(Box7, { justifyContent: "space-between", children: [
|
|
2866
|
+
/* @__PURE__ */ jsxs8(Text8, { dimColor: true, children: [
|
|
2539
2867
|
cwdLabel(cwd, wide),
|
|
2540
2868
|
gitBranch ? ` [${gitBranch}]` : ""
|
|
2541
2869
|
] }),
|
|
2542
|
-
backgroundTasks.length > 0 && /* @__PURE__ */
|
|
2870
|
+
backgroundTasks.length > 0 && /* @__PURE__ */ jsxs8(Text8, { color: "yellow", children: [
|
|
2543
2871
|
"bg: ",
|
|
2544
2872
|
backgroundTasks.length,
|
|
2545
2873
|
" (",
|
|
2546
2874
|
labels,
|
|
2547
2875
|
")"
|
|
2548
2876
|
] }),
|
|
2549
|
-
/* @__PURE__ */
|
|
2877
|
+
/* @__PURE__ */ jsxs8(Text8, { dimColor: true, children: [
|
|
2550
2878
|
"model: ",
|
|
2551
2879
|
model
|
|
2552
2880
|
] }),
|
|
2553
|
-
contextPercent != null && /* @__PURE__ */
|
|
2881
|
+
contextPercent != null && /* @__PURE__ */ jsxs8(Text8, { dimColor: true, children: [
|
|
2554
2882
|
"ctx: ",
|
|
2555
2883
|
contextPercent,
|
|
2556
2884
|
"%"
|
|
2557
2885
|
] }),
|
|
2558
|
-
sessionCostUsd != null && sessionCostUsd > 0 && /* @__PURE__ */
|
|
2886
|
+
sessionCostUsd != null && sessionCostUsd > 0 && /* @__PURE__ */ jsxs8(Text8, { dimColor: true, children: [
|
|
2559
2887
|
"$",
|
|
2560
2888
|
formatDollars(sessionCostUsd)
|
|
2561
2889
|
] }),
|
|
2562
|
-
/* @__PURE__ */
|
|
2890
|
+
/* @__PURE__ */ jsxs8(Text8, { dimColor: true, children: [
|
|
2563
2891
|
tokens2,
|
|
2564
2892
|
" tokens"
|
|
2565
2893
|
] }),
|
|
2566
|
-
/* @__PURE__ */
|
|
2894
|
+
/* @__PURE__ */ jsxs8(Text8, { dimColor: true, children: [
|
|
2567
2895
|
"mode: ",
|
|
2568
2896
|
permissionMode
|
|
2569
2897
|
] })
|
|
@@ -2571,22 +2899,22 @@ var StatusBar = memo2(function StatusBar2({
|
|
|
2571
2899
|
});
|
|
2572
2900
|
|
|
2573
2901
|
// src/cli/components/StreamingMessage.tsx
|
|
2574
|
-
import { Box as
|
|
2902
|
+
import { Box as Box8, Text as Text9 } from "ink";
|
|
2575
2903
|
import { memo as memo3 } from "react";
|
|
2576
|
-
import { jsx as
|
|
2904
|
+
import { jsx as jsx8, jsxs as jsxs9 } from "react/jsx-runtime";
|
|
2577
2905
|
var StreamingMessage = memo3(function StreamingMessage2({
|
|
2578
2906
|
text,
|
|
2579
2907
|
continuation
|
|
2580
2908
|
}) {
|
|
2581
|
-
return /* @__PURE__ */
|
|
2582
|
-
!continuation && /* @__PURE__ */
|
|
2583
|
-
/* @__PURE__ */
|
|
2909
|
+
return /* @__PURE__ */ jsxs9(Box8, { flexDirection: "column", marginBottom: 1, children: [
|
|
2910
|
+
!continuation && /* @__PURE__ */ jsx8(Text9, { bold: true, color: "green", children: "star" }),
|
|
2911
|
+
/* @__PURE__ */ jsx8(Text9, { color: "green", children: text })
|
|
2584
2912
|
] });
|
|
2585
2913
|
});
|
|
2586
2914
|
|
|
2587
2915
|
// src/cli/components/ThinkingIndicator.tsx
|
|
2588
|
-
import { Box as
|
|
2589
|
-
import { jsxs as
|
|
2916
|
+
import { Box as Box9, Text as Text10 } from "ink";
|
|
2917
|
+
import { jsxs as jsxs10 } from "react/jsx-runtime";
|
|
2590
2918
|
var SPINNER_FRAMES = ["-", "\\", "|", "/"];
|
|
2591
2919
|
var REASONING_TAIL_LENGTH = 200;
|
|
2592
2920
|
var THOUGHT_SUMMARY_LENGTH = 100;
|
|
@@ -2602,13 +2930,13 @@ function ThinkingIndicator({
|
|
|
2602
2930
|
}) {
|
|
2603
2931
|
const tail = reasoning ? truncateTail(reasoning, REASONING_TAIL_LENGTH) : "";
|
|
2604
2932
|
const spinner = SPINNER_FRAMES[frame % SPINNER_FRAMES.length];
|
|
2605
|
-
return /* @__PURE__ */
|
|
2606
|
-
/* @__PURE__ */
|
|
2933
|
+
return /* @__PURE__ */ jsxs10(Box9, { flexDirection: "column", children: [
|
|
2934
|
+
/* @__PURE__ */ jsxs10(Text10, { dimColor: true, children: [
|
|
2607
2935
|
spinner,
|
|
2608
2936
|
" ",
|
|
2609
2937
|
activity ?? "star is thinking\u2026"
|
|
2610
2938
|
] }),
|
|
2611
|
-
tail !== "" && /* @__PURE__ */
|
|
2939
|
+
tail !== "" && /* @__PURE__ */ jsxs10(Text10, { dimColor: true, children: [
|
|
2612
2940
|
" ",
|
|
2613
2941
|
tail
|
|
2614
2942
|
] })
|
|
@@ -2630,6 +2958,51 @@ function isDoubleEscape(previous, now, windowMs = DOUBLE_ESC_WINDOW_MS) {
|
|
|
2630
2958
|
return previous !== null && now - previous <= windowMs;
|
|
2631
2959
|
}
|
|
2632
2960
|
|
|
2961
|
+
// src/cli/history.ts
|
|
2962
|
+
import fs10 from "fs";
|
|
2963
|
+
import path16 from "path";
|
|
2964
|
+
var DEFAULT_LIMIT = 500;
|
|
2965
|
+
var MAX_FILE_LINES = 1e3;
|
|
2966
|
+
var TRIM_TO_LINES = 500;
|
|
2967
|
+
function historyPath() {
|
|
2968
|
+
return path16.join(starHome(), "history");
|
|
2969
|
+
}
|
|
2970
|
+
function readLines(file) {
|
|
2971
|
+
const raw = fs10.readFileSync(file, "utf8");
|
|
2972
|
+
return raw.split("\n").map((line) => line.replace(/\r$/, "")).filter((line) => line.trim().length > 0);
|
|
2973
|
+
}
|
|
2974
|
+
function loadHistory(limit = DEFAULT_LIMIT) {
|
|
2975
|
+
try {
|
|
2976
|
+
return readLines(historyPath()).slice(-limit);
|
|
2977
|
+
} catch {
|
|
2978
|
+
return [];
|
|
2979
|
+
}
|
|
2980
|
+
}
|
|
2981
|
+
function appendHistory(entry) {
|
|
2982
|
+
const line = entry.replace(/[\r\n]+/g, " ").trim();
|
|
2983
|
+
if (line.length === 0) return;
|
|
2984
|
+
try {
|
|
2985
|
+
const file = historyPath();
|
|
2986
|
+
let existing = [];
|
|
2987
|
+
try {
|
|
2988
|
+
existing = readLines(file);
|
|
2989
|
+
} catch {
|
|
2990
|
+
}
|
|
2991
|
+
if (existing[existing.length - 1] === line) return;
|
|
2992
|
+
if (existing.length >= MAX_FILE_LINES) {
|
|
2993
|
+
const kept = [...existing.slice(-(TRIM_TO_LINES - 1)), line];
|
|
2994
|
+
fs10.mkdirSync(path16.dirname(file), { recursive: true });
|
|
2995
|
+
fs10.writeFileSync(file, `${kept.join("\n")}
|
|
2996
|
+
`);
|
|
2997
|
+
return;
|
|
2998
|
+
}
|
|
2999
|
+
fs10.mkdirSync(path16.dirname(file), { recursive: true });
|
|
3000
|
+
fs10.appendFileSync(file, `${line}
|
|
3001
|
+
`);
|
|
3002
|
+
} catch {
|
|
3003
|
+
}
|
|
3004
|
+
}
|
|
3005
|
+
|
|
2633
3006
|
// src/cli/notify.ts
|
|
2634
3007
|
var BELL = "\x07";
|
|
2635
3008
|
function shouldNotifyBell(input, isTTY) {
|
|
@@ -2745,7 +3118,7 @@ async function checkForUpdate(currentVersion, fetcher = defaultFetcher2) {
|
|
|
2745
3118
|
}
|
|
2746
3119
|
|
|
2747
3120
|
// src/cli/repl.tsx
|
|
2748
|
-
import { jsx as
|
|
3121
|
+
import { jsx as jsx9, jsxs as jsxs11 } from "react/jsx-runtime";
|
|
2749
3122
|
var CLEAR_TERMINAL = process.platform === "win32" ? "\x1B[2J\x1B[0f" : "\x1B[2J\x1B[3J\x1B[H";
|
|
2750
3123
|
function emptyUsage() {
|
|
2751
3124
|
return { requests: 0, promptTokens: 0, completionTokens: 0, totalTokens: 0 };
|
|
@@ -2757,6 +3130,16 @@ var SESSION_MODE_CYCLE = ["ask", "auto", "readonly", "plan"];
|
|
|
2757
3130
|
function formatUsage(usage) {
|
|
2758
3131
|
return `API usage this session: ${usage.requests} requests, ${usage.promptTokens} prompt + ${usage.completionTokens} completion = ${usage.totalTokens} tokens`;
|
|
2759
3132
|
}
|
|
3133
|
+
var SESSION_PICKER_CAP = 20;
|
|
3134
|
+
async function sessionPreview(id) {
|
|
3135
|
+
const store = await SessionStore.open(id);
|
|
3136
|
+
if (!store) return null;
|
|
3137
|
+
const first = (await store.messages()).find((m) => m.role === "user");
|
|
3138
|
+
if (!first) return null;
|
|
3139
|
+
const text = coreMessageText(first).replace(/\s+/g, " ").trim();
|
|
3140
|
+
if (!text) return null;
|
|
3141
|
+
return text.length > 60 ? `${text.slice(0, 60)}\u2026` : text;
|
|
3142
|
+
}
|
|
2760
3143
|
function Repl({
|
|
2761
3144
|
backend,
|
|
2762
3145
|
model,
|
|
@@ -2768,23 +3151,24 @@ function Repl({
|
|
|
2768
3151
|
initialUsage
|
|
2769
3152
|
}) {
|
|
2770
3153
|
const { exit } = useApp();
|
|
2771
|
-
const [initialDisplay] =
|
|
2772
|
-
const [messages, setMessages] =
|
|
2773
|
-
const [epoch, setEpoch] =
|
|
2774
|
-
const [streamingText, setStreamingText] =
|
|
2775
|
-
const [isStreaming, setIsStreaming] =
|
|
2776
|
-
const [thinking, setThinking] =
|
|
2777
|
-
const [thinkingText, setThinkingText] =
|
|
2778
|
-
const [thoughtSummary, setThoughtSummary] =
|
|
2779
|
-
const [, setUsageVersion] =
|
|
2780
|
-
const [modelName, setModelName] =
|
|
2781
|
-
const [pending, setPending] =
|
|
2782
|
-
const [spinnerTick, setSpinnerTick] =
|
|
2783
|
-
const [activity, setActivity] =
|
|
2784
|
-
const [bgLabels, setBgLabels] =
|
|
2785
|
-
const [permissionModeState, setPermissionModeState] =
|
|
2786
|
-
const [planApproval, setPlanApprovalState] =
|
|
2787
|
-
const [pendingRewind, setPendingRewindState] =
|
|
3154
|
+
const [initialDisplay] = useState3(() => buildDisplayMessages(initialMessages ?? []));
|
|
3155
|
+
const [messages, setMessages] = useState3(initialDisplay);
|
|
3156
|
+
const [epoch, setEpoch] = useState3(0);
|
|
3157
|
+
const [streamingText, setStreamingText] = useState3(null);
|
|
3158
|
+
const [isStreaming, setIsStreaming] = useState3(false);
|
|
3159
|
+
const [thinking, setThinking] = useState3(false);
|
|
3160
|
+
const [thinkingText, setThinkingText] = useState3("");
|
|
3161
|
+
const [thoughtSummary, setThoughtSummary] = useState3(null);
|
|
3162
|
+
const [, setUsageVersion] = useState3(0);
|
|
3163
|
+
const [modelName, setModelName] = useState3(model);
|
|
3164
|
+
const [pending, setPending] = useState3(null);
|
|
3165
|
+
const [spinnerTick, setSpinnerTick] = useState3(0);
|
|
3166
|
+
const [activity, setActivity] = useState3(null);
|
|
3167
|
+
const [bgLabels, setBgLabels] = useState3(() => runningTaskLabels());
|
|
3168
|
+
const [permissionModeState, setPermissionModeState] = useState3(permissionMode);
|
|
3169
|
+
const [planApproval, setPlanApprovalState] = useState3(false);
|
|
3170
|
+
const [pendingRewind, setPendingRewindState] = useState3(null);
|
|
3171
|
+
const [picker, setPickerState] = useState3(null);
|
|
2788
3172
|
const backendRef = useRef2(backend);
|
|
2789
3173
|
const nextIdRef = useRef2(initialDisplay.length);
|
|
2790
3174
|
const abortRef = useRef2(null);
|
|
@@ -2799,21 +3183,23 @@ function Repl({
|
|
|
2799
3183
|
const prevModeRef = useRef2(null);
|
|
2800
3184
|
const planApprovalRef = useRef2(false);
|
|
2801
3185
|
const pendingRewindRef = useRef2(null);
|
|
3186
|
+
const pickerRef = useRef2(null);
|
|
2802
3187
|
const modelNameRef = useRef2(model);
|
|
2803
3188
|
const sessionStoreRef = useRef2(sessionStore);
|
|
2804
3189
|
const usageRef = useRef2(initialUsage ? { ...initialUsage } : emptyUsage());
|
|
2805
3190
|
const turnChunksRef = useRef2(0);
|
|
2806
3191
|
const messagesRef = useRef2(initialDisplay);
|
|
2807
|
-
const [queueItems, setQueueItems] =
|
|
3192
|
+
const [queueItems, setQueueItems] = useState3([]);
|
|
2808
3193
|
const queueRef = useRef2(new PromptQueue());
|
|
2809
|
-
const [pendingImages, setPendingImages] =
|
|
3194
|
+
const [pendingImages, setPendingImages] = useState3([]);
|
|
2810
3195
|
const pendingImagesRef = useRef2([]);
|
|
2811
3196
|
const imageSeqRef = useRef2(1);
|
|
2812
|
-
const [inputRefill, setInputRefill] =
|
|
3197
|
+
const [inputRefill, setInputRefill] = useState3();
|
|
2813
3198
|
const refillSeqRef = useRef2(0);
|
|
2814
3199
|
const lastEscRef = useRef2(null);
|
|
2815
|
-
const [gitBranch, setGitBranch] =
|
|
2816
|
-
const [contextPercent, setContextPercent] =
|
|
3200
|
+
const [gitBranch, setGitBranch] = useState3(null);
|
|
3201
|
+
const [contextPercent, setContextPercent] = useState3(null);
|
|
3202
|
+
const [initialHistory] = useState3(() => loadHistory());
|
|
2817
3203
|
const budgetWarnedRef = useRef2(false);
|
|
2818
3204
|
const budgetExceededRef = useRef2(false);
|
|
2819
3205
|
const runStreamRef = useRef2(
|
|
@@ -2863,6 +3249,34 @@ function Repl({
|
|
|
2863
3249
|
pendingRewindRef.current = p;
|
|
2864
3250
|
setPendingRewindState(p);
|
|
2865
3251
|
}, []);
|
|
3252
|
+
const setPicker = useCallback((p) => {
|
|
3253
|
+
pickerRef.current = p;
|
|
3254
|
+
setPickerState(p);
|
|
3255
|
+
}, []);
|
|
3256
|
+
const showPicker = useCallback(
|
|
3257
|
+
(title, options) => {
|
|
3258
|
+
if (pickerRef.current) return Promise.resolve(null);
|
|
3259
|
+
return new Promise((resolve) => {
|
|
3260
|
+
setPicker({ title, options, resolve });
|
|
3261
|
+
});
|
|
3262
|
+
},
|
|
3263
|
+
[setPicker]
|
|
3264
|
+
);
|
|
3265
|
+
const handlePickerSelect = useCallback(
|
|
3266
|
+
(value) => {
|
|
3267
|
+
const p = pickerRef.current;
|
|
3268
|
+
if (!p) return;
|
|
3269
|
+
setPicker(null);
|
|
3270
|
+
p.resolve(value);
|
|
3271
|
+
},
|
|
3272
|
+
[setPicker]
|
|
3273
|
+
);
|
|
3274
|
+
const handlePickerCancel = useCallback(() => {
|
|
3275
|
+
const p = pickerRef.current;
|
|
3276
|
+
if (!p) return;
|
|
3277
|
+
setPicker(null);
|
|
3278
|
+
p.resolve(null);
|
|
3279
|
+
}, [setPicker]);
|
|
2866
3280
|
const stageClipboardImage = useCallback((img) => {
|
|
2867
3281
|
pendingImagesRef.current = [
|
|
2868
3282
|
...pendingImagesRef.current,
|
|
@@ -3003,9 +3417,9 @@ function Repl({
|
|
|
3003
3417
|
}
|
|
3004
3418
|
exit();
|
|
3005
3419
|
}, [exit, pushMessage]);
|
|
3006
|
-
|
|
3420
|
+
useInput6((_input, key) => {
|
|
3007
3421
|
if (key.escape) {
|
|
3008
|
-
const busy = abortRef.current !== null || pendingRef.current !== null || planApprovalRef.current || pendingRewindRef.current !== null;
|
|
3422
|
+
const busy = abortRef.current !== null || pendingRef.current !== null || planApprovalRef.current || pendingRewindRef.current !== null || pickerRef.current !== null;
|
|
3009
3423
|
if (busy) {
|
|
3010
3424
|
lastEscRef.current = null;
|
|
3011
3425
|
interrupt();
|
|
@@ -3022,7 +3436,7 @@ function Repl({
|
|
|
3022
3436
|
}
|
|
3023
3437
|
lastEscRef.current = null;
|
|
3024
3438
|
if (key.shift && key.tab) {
|
|
3025
|
-
if (abortRef.current || pendingRef.current || planApprovalRef.current || pendingRewindRef.current)
|
|
3439
|
+
if (abortRef.current || pendingRef.current || planApprovalRef.current || pendingRewindRef.current || pickerRef.current)
|
|
3026
3440
|
return;
|
|
3027
3441
|
const current = config.permissionMode;
|
|
3028
3442
|
const index = SESSION_MODE_CYCLE.indexOf(current);
|
|
@@ -3060,7 +3474,8 @@ function Repl({
|
|
|
3060
3474
|
registry: createDefaultRegistry(),
|
|
3061
3475
|
config,
|
|
3062
3476
|
cwd,
|
|
3063
|
-
sessionStore: sessionStoreRef.current
|
|
3477
|
+
sessionStore: sessionStoreRef.current,
|
|
3478
|
+
contextMaxTokens: contextWindowTokens(config, name)
|
|
3064
3479
|
});
|
|
3065
3480
|
const prev = backendRef.current;
|
|
3066
3481
|
if (prev instanceof AgentLoop) {
|
|
@@ -3378,6 +3793,97 @@ ${lines.join("\n")}`;
|
|
|
3378
3793
|
return formatSessionEntries(entries, { showCwd: all });
|
|
3379
3794
|
},
|
|
3380
3795
|
resumeSession: resume,
|
|
3796
|
+
pickModel: async () => {
|
|
3797
|
+
const models = listModels(config);
|
|
3798
|
+
if (models.length === 0) return "No models configured.";
|
|
3799
|
+
const options = models.map((m) => {
|
|
3800
|
+
const parts = [`${m.provider}/${m.model}`, `${contextWindowTokens(config, m.name)} ctx`];
|
|
3801
|
+
if (m.promptPrice !== void 0 && m.completionPrice !== void 0) {
|
|
3802
|
+
parts.push(`$${m.promptPrice}/$${m.completionPrice} per 1M tokens`);
|
|
3803
|
+
}
|
|
3804
|
+
return {
|
|
3805
|
+
value: m.name,
|
|
3806
|
+
label: m.name,
|
|
3807
|
+
description: parts.join(" \xB7 "),
|
|
3808
|
+
hint: m.name === modelNameRef.current ? "current" : void 0
|
|
3809
|
+
};
|
|
3810
|
+
});
|
|
3811
|
+
const picked = await showPicker("Select a model", options);
|
|
3812
|
+
if (!picked || picked === modelNameRef.current) {
|
|
3813
|
+
return `Model unchanged \u2014 still "${modelNameRef.current}".`;
|
|
3814
|
+
}
|
|
3815
|
+
return switchModel(picked);
|
|
3816
|
+
},
|
|
3817
|
+
pickPermissionMode: async () => {
|
|
3818
|
+
const current = config.permissionMode;
|
|
3819
|
+
const descriptions = {
|
|
3820
|
+
ask: "reads allowed; writes/exec ask for confirmation",
|
|
3821
|
+
auto: "everything allowed except hard-denied dangerous commands/paths",
|
|
3822
|
+
readonly: "read-only; all writes/exec denied",
|
|
3823
|
+
yolo: "allow everything, never ask (disables ALL safety checks)",
|
|
3824
|
+
plan: "read-only research, then approve a plan before executing (this session only)"
|
|
3825
|
+
};
|
|
3826
|
+
const options = ["ask", "auto", "readonly", "yolo", "plan"].map((m) => ({
|
|
3827
|
+
value: m,
|
|
3828
|
+
label: m,
|
|
3829
|
+
description: descriptions[m],
|
|
3830
|
+
hint: m === current ? "current" : void 0
|
|
3831
|
+
}));
|
|
3832
|
+
const picked = await showPicker("Select a permission mode", options);
|
|
3833
|
+
if (!picked || picked === current) {
|
|
3834
|
+
return `Permission mode unchanged \u2014 still ${current}.`;
|
|
3835
|
+
}
|
|
3836
|
+
if (picked === "plan") return ctx.planMode();
|
|
3837
|
+
return ctx.permissionMode(picked);
|
|
3838
|
+
},
|
|
3839
|
+
pickSession: async (all) => {
|
|
3840
|
+
const entries = (await listSessionEntries(all ? void 0 : cwd)).slice(
|
|
3841
|
+
0,
|
|
3842
|
+
SESSION_PICKER_CAP
|
|
3843
|
+
);
|
|
3844
|
+
if (entries.length === 0) {
|
|
3845
|
+
return all ? "No sessions found." : "No sessions found for this directory.";
|
|
3846
|
+
}
|
|
3847
|
+
const options = await Promise.all(
|
|
3848
|
+
entries.map(async ({ meta, messageCount }) => {
|
|
3849
|
+
const parts = [`${messageCount} messages \xB7 ${relativeTime(meta.updatedAt)}`];
|
|
3850
|
+
const preview = await sessionPreview(meta.id);
|
|
3851
|
+
if (preview) parts.push(preview);
|
|
3852
|
+
if (all) parts.push(`[${meta.cwd}]`);
|
|
3853
|
+
return {
|
|
3854
|
+
value: meta.id,
|
|
3855
|
+
label: meta.title || shortSessionId(meta.id),
|
|
3856
|
+
description: parts.join(" \xB7 "),
|
|
3857
|
+
hint: meta.id === sessionStoreRef.current?.id ? "current" : void 0
|
|
3858
|
+
};
|
|
3859
|
+
})
|
|
3860
|
+
);
|
|
3861
|
+
const picked = await showPicker(
|
|
3862
|
+
all ? "Resume a session (all directories)" : "Resume a session",
|
|
3863
|
+
options
|
|
3864
|
+
);
|
|
3865
|
+
if (!picked) return "Session unchanged \u2014 picker cancelled.";
|
|
3866
|
+
return resume(picked);
|
|
3867
|
+
},
|
|
3868
|
+
forkSession: async () => {
|
|
3869
|
+
const current = backendRef.current;
|
|
3870
|
+
const store = sessionStoreRef.current;
|
|
3871
|
+
if (!(current instanceof AgentLoop) || !store) {
|
|
3872
|
+
return "Nothing to fork \u2014 no active session.";
|
|
3873
|
+
}
|
|
3874
|
+
const messages2 = await store.messages();
|
|
3875
|
+
if (messages2.length === 0) {
|
|
3876
|
+
return "Nothing to fork \u2014 the current session has no messages yet.";
|
|
3877
|
+
}
|
|
3878
|
+
const fork = await SessionStore.create(cwd, modelNameRef.current);
|
|
3879
|
+
await fork.replaceMessages(messages2);
|
|
3880
|
+
const oldMeta = await store.meta();
|
|
3881
|
+
const title = `Fork of ${oldMeta.title || shortSessionId(oldMeta.id)}`;
|
|
3882
|
+
await fork.setTitle(title);
|
|
3883
|
+
current.setSessionStore(fork);
|
|
3884
|
+
sessionStoreRef.current = fork;
|
|
3885
|
+
return `Forked into new session ${fork.id} ("${title}") and switched to it. Checkpoints and rewind history do not carry over.`;
|
|
3886
|
+
},
|
|
3381
3887
|
newSession: async () => {
|
|
3382
3888
|
const current = backendRef.current;
|
|
3383
3889
|
if (!(current instanceof AgentLoop)) {
|
|
@@ -3581,7 +4087,8 @@ ${plan.affected.length} file change(s) will be reverted, ${messageCount} message
|
|
|
3581
4087
|
applyMessages,
|
|
3582
4088
|
redrawMessages,
|
|
3583
4089
|
applySessionMode,
|
|
3584
|
-
setPendingRewind
|
|
4090
|
+
setPendingRewind,
|
|
4091
|
+
showPicker
|
|
3585
4092
|
]);
|
|
3586
4093
|
const runShellBang = useCallback(
|
|
3587
4094
|
async (raw) => {
|
|
@@ -3621,6 +4128,7 @@ ${plan.affected.length} file change(s) will be reverted, ${messageCount} message
|
|
|
3621
4128
|
);
|
|
3622
4129
|
const handleSubmit = useCallback(
|
|
3623
4130
|
(text) => {
|
|
4131
|
+
appendHistory(text);
|
|
3624
4132
|
if (text.startsWith("!")) {
|
|
3625
4133
|
if (abortRef.current) {
|
|
3626
4134
|
queueRef.current.enqueue({ text, images: [] });
|
|
@@ -3635,7 +4143,11 @@ ${plan.affected.length} file change(s) will be reverted, ${messageCount} message
|
|
|
3635
4143
|
if (!parsed) return;
|
|
3636
4144
|
const command = registry.get(parsed.name);
|
|
3637
4145
|
if (!command) {
|
|
3638
|
-
|
|
4146
|
+
const suggestions = registry.complete(parsed.name).map((cmd) => cmd.name);
|
|
4147
|
+
pushMessage(
|
|
4148
|
+
"system",
|
|
4149
|
+
`Unknown command: /${parsed.name} (try /help)${didYouMeanSuffix(suggestions)}`
|
|
4150
|
+
);
|
|
3639
4151
|
return;
|
|
3640
4152
|
}
|
|
3641
4153
|
void command.run(parsed.args, registry.ctx);
|
|
@@ -3661,24 +4173,28 @@ ${plan.affected.length} file change(s) will be reverted, ${messageCount} message
|
|
|
3661
4173
|
[registry, pushMessage, runStream, runShellBang, config, clearPendingImages, sessionCostUsd]
|
|
3662
4174
|
);
|
|
3663
4175
|
const commandHints = useMemo(
|
|
3664
|
-
() => registry.list().map((cmd) => ({
|
|
4176
|
+
() => registry.list().map((cmd) => ({
|
|
4177
|
+
name: cmd.name,
|
|
4178
|
+
description: cmd.description,
|
|
4179
|
+
usage: cmd.usage ?? `/${cmd.name}`
|
|
4180
|
+
})),
|
|
3665
4181
|
[registry]
|
|
3666
4182
|
);
|
|
3667
4183
|
useEffect2(() => {
|
|
3668
4184
|
setGitBranch(getGitSummary(cwd)?.branch ?? null);
|
|
3669
4185
|
const current = backendRef.current;
|
|
3670
4186
|
if (current instanceof AgentLoop) {
|
|
3671
|
-
|
|
3672
|
-
|
|
3673
|
-
);
|
|
4187
|
+
const window_ = contextWindowTokens(config, modelNameRef.current);
|
|
4188
|
+
const pct = estimateTokens([...current.getMessages()]) / window_ * 100;
|
|
4189
|
+
setContextPercent(pct < 10 ? Math.round(pct * 10) / 10 : Math.round(pct));
|
|
3674
4190
|
} else {
|
|
3675
4191
|
setContextPercent(null);
|
|
3676
4192
|
}
|
|
3677
4193
|
}, [cwd, config, isStreaming, epoch]);
|
|
3678
4194
|
const sessionCost = sessionCostUsd();
|
|
3679
|
-
return /* @__PURE__ */
|
|
3680
|
-
/* @__PURE__ */
|
|
3681
|
-
(thinking || activity !== null) && /* @__PURE__ */
|
|
4195
|
+
return /* @__PURE__ */ jsxs11(Box10, { flexDirection: "column", children: [
|
|
4196
|
+
/* @__PURE__ */ jsx9(MessageList, { messages }, `messages-${epoch}`),
|
|
4197
|
+
(thinking || activity !== null) && /* @__PURE__ */ jsx9(
|
|
3682
4198
|
ThinkingIndicator,
|
|
3683
4199
|
{
|
|
3684
4200
|
reasoning: thinkingText,
|
|
@@ -3686,9 +4202,9 @@ ${plan.affected.length} file change(s) will be reverted, ${messageCount} message
|
|
|
3686
4202
|
activity: activity ?? void 0
|
|
3687
4203
|
}
|
|
3688
4204
|
),
|
|
3689
|
-
!thinking && activity === null && thoughtSummary !== null && /* @__PURE__ */
|
|
3690
|
-
streamingText !== null && (streamingText !== "" || turnChunksRef.current === 0) && /* @__PURE__ */
|
|
3691
|
-
pending && /* @__PURE__ */
|
|
4205
|
+
!thinking && activity === null && thoughtSummary !== null && /* @__PURE__ */ jsx9(Text11, { dimColor: true, children: thoughtSummary }),
|
|
4206
|
+
streamingText !== null && (streamingText !== "" || turnChunksRef.current === 0) && /* @__PURE__ */ jsx9(StreamingMessage, { text: streamingText, continuation: turnChunksRef.current > 0 }),
|
|
4207
|
+
pending && /* @__PURE__ */ jsx9(
|
|
3692
4208
|
PermissionPrompt,
|
|
3693
4209
|
{
|
|
3694
4210
|
request: pending.request,
|
|
@@ -3696,32 +4212,42 @@ ${plan.affected.length} file change(s) will be reverted, ${messageCount} message
|
|
|
3696
4212
|
onDecision: handleDecision
|
|
3697
4213
|
}
|
|
3698
4214
|
),
|
|
3699
|
-
planApproval && /* @__PURE__ */
|
|
3700
|
-
pendingRewind && /* @__PURE__ */
|
|
3701
|
-
|
|
4215
|
+
planApproval && /* @__PURE__ */ jsx9(ExitPlanPrompt, { onDecision: handlePlanDecision }),
|
|
4216
|
+
pendingRewind && /* @__PURE__ */ jsx9(RewindConfirmPrompt, { summary: pendingRewind.summary, onDecision: handleRewindDecision }),
|
|
4217
|
+
picker && /* @__PURE__ */ jsx9(
|
|
4218
|
+
SelectPrompt,
|
|
4219
|
+
{
|
|
4220
|
+
title: picker.title,
|
|
4221
|
+
options: picker.options,
|
|
4222
|
+
onSelect: handlePickerSelect,
|
|
4223
|
+
onCancel: handlePickerCancel
|
|
4224
|
+
}
|
|
4225
|
+
),
|
|
4226
|
+
queueItems.map((item) => /* @__PURE__ */ jsxs11(Text11, { dimColor: true, children: [
|
|
3702
4227
|
"queued: ",
|
|
3703
4228
|
item.text.length > 80 ? `${item.text.slice(0, 80)}\u2026` : item.text
|
|
3704
4229
|
] }, item.id)),
|
|
3705
|
-
pendingImages.map((staged) => /* @__PURE__ */
|
|
4230
|
+
pendingImages.map((staged) => /* @__PURE__ */ jsxs11(Text11, { dimColor: true, children: [
|
|
3706
4231
|
"[image attached: ",
|
|
3707
4232
|
staged.image.path,
|
|
3708
4233
|
"]"
|
|
3709
4234
|
] }, staged.id)),
|
|
3710
|
-
/* @__PURE__ */
|
|
4235
|
+
/* @__PURE__ */ jsx9(
|
|
3711
4236
|
InputBox,
|
|
3712
4237
|
{
|
|
3713
4238
|
isStreaming,
|
|
3714
|
-
disabled: pending !== null || planApproval || pendingRewind !== null,
|
|
4239
|
+
disabled: pending !== null || planApproval || pendingRewind !== null || picker !== null,
|
|
3715
4240
|
commands: commandHints,
|
|
3716
4241
|
cwd,
|
|
3717
4242
|
refill: inputRefill,
|
|
4243
|
+
initialHistory,
|
|
3718
4244
|
onSubmit: handleSubmit,
|
|
3719
4245
|
onInterrupt: interrupt,
|
|
3720
4246
|
onExit: handleExit,
|
|
3721
4247
|
onPasteImage: handlePasteImage
|
|
3722
4248
|
}
|
|
3723
4249
|
),
|
|
3724
|
-
/* @__PURE__ */
|
|
4250
|
+
/* @__PURE__ */ jsx9(
|
|
3725
4251
|
StatusBar,
|
|
3726
4252
|
{
|
|
3727
4253
|
cwd,
|
|
@@ -3741,7 +4267,7 @@ function renderRepl(backend, opts) {
|
|
|
3741
4267
|
defaultTaskManager.cleanup();
|
|
3742
4268
|
});
|
|
3743
4269
|
return render(
|
|
3744
|
-
/* @__PURE__ */
|
|
4270
|
+
/* @__PURE__ */ jsx9(
|
|
3745
4271
|
Repl,
|
|
3746
4272
|
{
|
|
3747
4273
|
backend,
|
|
@@ -3758,71 +4284,8 @@ function renderRepl(backend, opts) {
|
|
|
3758
4284
|
}
|
|
3759
4285
|
|
|
3760
4286
|
// src/config/loader.ts
|
|
3761
|
-
import
|
|
4287
|
+
import fs11 from "fs";
|
|
3762
4288
|
import { parse as parse2 } from "smol-toml";
|
|
3763
|
-
|
|
3764
|
-
// src/config/schema.ts
|
|
3765
|
-
import { z } from "zod";
|
|
3766
|
-
var ProviderConfigSchema = z.object({
|
|
3767
|
-
name: z.string(),
|
|
3768
|
-
protocol: z.enum(["openai-compatible", "anthropic", "openai-responses"]).default("openai-compatible"),
|
|
3769
|
-
baseURL: z.string(),
|
|
3770
|
-
apiKeyEnv: z.string().optional(),
|
|
3771
|
-
apiKey: z.string().optional(),
|
|
3772
|
-
headers: z.record(z.string()).optional()
|
|
3773
|
-
});
|
|
3774
|
-
var ModelConfigSchema = z.object({
|
|
3775
|
-
name: z.string(),
|
|
3776
|
-
provider: z.string(),
|
|
3777
|
-
model: z.string(),
|
|
3778
|
-
maxTokens: z.number().int().positive().optional(),
|
|
3779
|
-
promptPrice: z.number().nonnegative().optional(),
|
|
3780
|
-
completionPrice: z.number().nonnegative().optional()
|
|
3781
|
-
});
|
|
3782
|
-
var PermissionsConfigSchema = z.object({
|
|
3783
|
-
allow: z.array(z.string()).default([])
|
|
3784
|
-
});
|
|
3785
|
-
var HookConfigSchema = z.object({
|
|
3786
|
-
event: z.enum(["PreToolUse", "PostToolUse", "Stop"]),
|
|
3787
|
-
matcher: z.string().refine(
|
|
3788
|
-
(pattern) => {
|
|
3789
|
-
try {
|
|
3790
|
-
new RegExp(pattern);
|
|
3791
|
-
return true;
|
|
3792
|
-
} catch {
|
|
3793
|
-
return false;
|
|
3794
|
-
}
|
|
3795
|
-
},
|
|
3796
|
-
{ message: "invalid regular expression" }
|
|
3797
|
-
).optional(),
|
|
3798
|
-
command: z.string().min(1),
|
|
3799
|
-
timeoutSec: z.number().positive().default(30)
|
|
3800
|
-
});
|
|
3801
|
-
var ConfigSchema = z.object({
|
|
3802
|
-
defaultModel: z.string().default(""),
|
|
3803
|
-
permissionMode: z.enum(["auto", "ask", "readonly", "yolo", "plan"]).default("ask"),
|
|
3804
|
-
providers: z.array(ProviderConfigSchema).default([]),
|
|
3805
|
-
models: z.array(ModelConfigSchema).default([]),
|
|
3806
|
-
maxSteps: z.number().int().positive().default(50),
|
|
3807
|
-
contextMaxTokens: z.number().int().positive().default(1e5),
|
|
3808
|
-
// Seconds without any stream part before a stalled response is ended
|
|
3809
|
-
// gracefully (some relays never send the terminal chunks). Applies once
|
|
3810
|
-
// streaming has started; the first part gets a longer, fixed allowance.
|
|
3811
|
-
streamIdleTimeoutSec: z.number().positive().default(20),
|
|
3812
|
-
contextCompaction: z.enum(["summary", "truncate"]).default("summary"),
|
|
3813
|
-
// Optional per-session dollar budget; when set, the REPL stops the session
|
|
3814
|
-
// once the estimated cost for the session reaches this amount.
|
|
3815
|
-
sessionBudgetUsd: z.number().positive().optional(),
|
|
3816
|
-
// Terminal bell (REPL only): ring when a turn takes longer than
|
|
3817
|
-
// notifyBellThresholdSec, and when a background task finishes.
|
|
3818
|
-
// STAR_NO_NOTIFY=1 disables without touching the config.
|
|
3819
|
-
notifyBell: z.boolean().default(true),
|
|
3820
|
-
notifyBellThresholdSec: z.number().positive().default(10),
|
|
3821
|
-
permissions: PermissionsConfigSchema.default({ allow: [] }),
|
|
3822
|
-
hooks: z.array(HookConfigSchema).default([])
|
|
3823
|
-
});
|
|
3824
|
-
|
|
3825
|
-
// src/config/loader.ts
|
|
3826
4289
|
var PartialConfigSchema = ConfigSchema.partial();
|
|
3827
4290
|
function parseTomlFile(content, filePath) {
|
|
3828
4291
|
let raw;
|
|
@@ -3849,8 +4312,8 @@ function applyOverrides(config, overrides) {
|
|
|
3849
4312
|
return merged;
|
|
3850
4313
|
}
|
|
3851
4314
|
function readTomlFileSync(filePath) {
|
|
3852
|
-
if (!
|
|
3853
|
-
const content =
|
|
4315
|
+
if (!fs11.existsSync(filePath)) return {};
|
|
4316
|
+
const content = fs11.readFileSync(filePath, "utf8");
|
|
3854
4317
|
return parseTomlFile(content, filePath);
|
|
3855
4318
|
}
|
|
3856
4319
|
function loadConfigSync(cwd, overrides) {
|
|
@@ -3874,7 +4337,8 @@ async function createLoop(config, modelName, cwd, sessionStore) {
|
|
|
3874
4337
|
config,
|
|
3875
4338
|
cwd,
|
|
3876
4339
|
system: SYSTEM_PROMPT,
|
|
3877
|
-
sessionStore
|
|
4340
|
+
sessionStore,
|
|
4341
|
+
contextMaxTokens: contextWindowTokens(config, modelName)
|
|
3878
4342
|
});
|
|
3879
4343
|
}
|
|
3880
4344
|
async function printMode(loop, prompt, cwd, json, imagePaths) {
|