@kernelonpanic/kitcode 1.0.1-beta → 1.2.0
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 +281 -280
- package/README.ru.md +276 -275
- package/dist/index.js +1419 -412
- package/dist/index.js.map +1 -1
- package/package.json +9 -1
package/dist/index.js
CHANGED
|
@@ -9,6 +9,14 @@ import { existsSync } from "fs";
|
|
|
9
9
|
|
|
10
10
|
// src/config/schema.ts
|
|
11
11
|
import { z } from "zod";
|
|
12
|
+
var reservedRecordKeys = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
|
|
13
|
+
var safeRecordKeySchema = z.string().min(1).max(256).refine((value) => !reservedRecordKeys.has(value) && !/[\0-\x1f\x7f]/.test(value), {
|
|
14
|
+
message: "contains a reserved or unsafe key"
|
|
15
|
+
});
|
|
16
|
+
var providerIdSchema = safeRecordKeySchema.regex(
|
|
17
|
+
/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/,
|
|
18
|
+
"must start with a letter or digit and contain only letters, digits, dots, underscores, or dashes"
|
|
19
|
+
);
|
|
12
20
|
function isAllowedEndpointUrl(value) {
|
|
13
21
|
try {
|
|
14
22
|
const url = new URL(value);
|
|
@@ -62,7 +70,7 @@ var diagnosticsSchema = z.object({
|
|
|
62
70
|
commands: z.array(z.string().trim().min(1).max(2e4)).max(8).default([])
|
|
63
71
|
});
|
|
64
72
|
var updatesSchema = z.object({
|
|
65
|
-
checkOnStart: z.boolean().default(
|
|
73
|
+
checkOnStart: z.boolean().default(true)
|
|
66
74
|
});
|
|
67
75
|
var configSchema = z.object({
|
|
68
76
|
version: z.literal(1).default(1),
|
|
@@ -79,11 +87,11 @@ var configSchema = z.object({
|
|
|
79
87
|
}),
|
|
80
88
|
diagnostics: diagnosticsSchema.default({ autoRun: true, commands: [] }),
|
|
81
89
|
updates: updatesSchema.default({ checkOnStart: false }),
|
|
82
|
-
providers: z.record(
|
|
83
|
-
permissions: z.record(
|
|
84
|
-
mcp: z.record(
|
|
90
|
+
providers: z.record(providerIdSchema, providerConfigSchema).default({}),
|
|
91
|
+
permissions: z.record(safeRecordKeySchema, permissionModeSchema).default({}),
|
|
92
|
+
mcp: z.record(safeRecordKeySchema, mcpServerSchema).default({})
|
|
85
93
|
});
|
|
86
|
-
var authSchema = z.record(
|
|
94
|
+
var authSchema = z.record(providerIdSchema, z.string());
|
|
87
95
|
function defaultConfig() {
|
|
88
96
|
return configSchema.parse({});
|
|
89
97
|
}
|
|
@@ -380,8 +388,9 @@ function perMillion(value) {
|
|
|
380
388
|
}
|
|
381
389
|
function positiveInt(value) {
|
|
382
390
|
const parsed = toNumber(value);
|
|
383
|
-
if (parsed === void 0
|
|
384
|
-
|
|
391
|
+
if (parsed === void 0) return void 0;
|
|
392
|
+
const integer = Math.trunc(parsed);
|
|
393
|
+
return integer >= 1 ? integer : void 0;
|
|
385
394
|
}
|
|
386
395
|
function toNumber(value) {
|
|
387
396
|
if (typeof value === "number") return Number.isFinite(value) ? value : void 0;
|
|
@@ -401,7 +410,12 @@ async function detectProvider(rawUrl, apiKey2, opts = {}) {
|
|
|
401
410
|
if (!isAllowedEndpointUrl(baseUrl)) {
|
|
402
411
|
throw new Error("API URL must use https; plain http is only allowed for localhost or 127.0.0.1.");
|
|
403
412
|
}
|
|
404
|
-
const
|
|
413
|
+
const candidateId = opts.name ?? providerIdFromUrl(baseUrl);
|
|
414
|
+
const parsedId = providerIdSchema.safeParse(candidateId);
|
|
415
|
+
if (!parsedId.success) {
|
|
416
|
+
throw new Error(`Invalid provider name "${candidateId}": ${parsedId.error.issues[0]?.message ?? "invalid name"}`);
|
|
417
|
+
}
|
|
418
|
+
const id = parsedId.data;
|
|
405
419
|
if (hostname(baseUrl) === anthropicHost) {
|
|
406
420
|
const probed = await probe(
|
|
407
421
|
fetchImpl,
|
|
@@ -453,7 +467,8 @@ function providerIdFromUrl(url) {
|
|
|
453
467
|
}
|
|
454
468
|
const labels = host.split(".");
|
|
455
469
|
const label = labels.find((part) => part !== "api" && part !== "") ?? "provider";
|
|
456
|
-
|
|
470
|
+
const candidate = label.replace(/[^a-z0-9-]/g, "-").replace(/^-+|-+$/g, "") || "provider";
|
|
471
|
+
return providerIdSchema.safeParse(candidate).success ? candidate : `provider-${candidate}`;
|
|
457
472
|
}
|
|
458
473
|
function isAddressLiteral(host) {
|
|
459
474
|
return host === "localhost" || /^[0-9.]+$/.test(host) || host.includes(":");
|
|
@@ -651,13 +666,14 @@ async function isFile(file) {
|
|
|
651
666
|
|
|
652
667
|
// src/config/store.ts
|
|
653
668
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
654
|
-
import { chmod as chmod2, readFile as readFile2, rename as rename2, rm as rm2, writeFile as writeFile2 } from "fs/promises";
|
|
669
|
+
import { chmod as chmod2, mkdir as mkdir2, readFile as readFile2, rename as rename2, rm as rm2, writeFile as writeFile2 } from "fs/promises";
|
|
655
670
|
import path3 from "path";
|
|
656
671
|
|
|
657
672
|
// src/config/trust.ts
|
|
658
673
|
import { randomUUID } from "crypto";
|
|
659
674
|
import { readFile, realpath, rename, rm, writeFile } from "fs/promises";
|
|
660
675
|
import path2 from "path";
|
|
676
|
+
var trustLock = Promise.resolve();
|
|
661
677
|
var emptyTrust = () => ({ version: 1, workspaces: [] });
|
|
662
678
|
async function canonicalWorkspace(cwd) {
|
|
663
679
|
const resolved = await workspaceRootFor(cwd);
|
|
@@ -670,22 +686,30 @@ async function isWorkspaceTrusted(cwd) {
|
|
|
670
686
|
}
|
|
671
687
|
async function trustWorkspace(cwd) {
|
|
672
688
|
const workspace = await canonicalWorkspace(cwd);
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
trust.workspaces.
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
689
|
+
await withTrustLock(async () => {
|
|
690
|
+
const trust = await readTrust();
|
|
691
|
+
if (!trust.workspaces.includes(workspace)) {
|
|
692
|
+
trust.workspaces.push(workspace);
|
|
693
|
+
trust.workspaces.sort();
|
|
694
|
+
await writeTrust(trust);
|
|
695
|
+
}
|
|
696
|
+
});
|
|
679
697
|
return workspace;
|
|
680
698
|
}
|
|
681
699
|
async function revokeWorkspaceTrust(cwd) {
|
|
682
700
|
const workspace = await canonicalWorkspace(cwd);
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
701
|
+
await withTrustLock(async () => {
|
|
702
|
+
const trust = await readTrust();
|
|
703
|
+
const workspaces = trust.workspaces.filter((entry) => entry !== workspace);
|
|
704
|
+
if (workspaces.length !== trust.workspaces.length) await writeTrust({ version: 1, workspaces });
|
|
705
|
+
});
|
|
686
706
|
return workspace;
|
|
687
707
|
}
|
|
688
708
|
async function loadTrust() {
|
|
709
|
+
await trustLock;
|
|
710
|
+
return readTrust();
|
|
711
|
+
}
|
|
712
|
+
async function readTrust() {
|
|
689
713
|
let parsed;
|
|
690
714
|
try {
|
|
691
715
|
parsed = JSON.parse(await readFile(trustPath, "utf8"));
|
|
@@ -701,7 +725,7 @@ async function loadTrust() {
|
|
|
701
725
|
workspaces: value.workspaces.filter((entry) => typeof entry === "string")
|
|
702
726
|
};
|
|
703
727
|
}
|
|
704
|
-
async function
|
|
728
|
+
async function writeTrust(value) {
|
|
705
729
|
await ensureDir(path2.dirname(trustPath));
|
|
706
730
|
const temp = `${trustPath}.${randomUUID()}.tmp`;
|
|
707
731
|
try {
|
|
@@ -713,6 +737,19 @@ async function saveTrust(value) {
|
|
|
713
737
|
throw error;
|
|
714
738
|
}
|
|
715
739
|
}
|
|
740
|
+
async function withTrustLock(action) {
|
|
741
|
+
const previous = trustLock;
|
|
742
|
+
let release;
|
|
743
|
+
trustLock = new Promise((resolve3) => {
|
|
744
|
+
release = resolve3;
|
|
745
|
+
});
|
|
746
|
+
await previous;
|
|
747
|
+
try {
|
|
748
|
+
return await action();
|
|
749
|
+
} finally {
|
|
750
|
+
release();
|
|
751
|
+
}
|
|
752
|
+
}
|
|
716
753
|
|
|
717
754
|
// src/config/store.ts
|
|
718
755
|
var active;
|
|
@@ -742,7 +779,7 @@ async function loadConfigAt(location) {
|
|
|
742
779
|
}
|
|
743
780
|
async function saveConfig(config) {
|
|
744
781
|
const location = await configLocation();
|
|
745
|
-
await writeJsonAtomic(location.path, config);
|
|
782
|
+
await writeJsonAtomic(location.path, config, location.scope === "project" ? void 0 : 384);
|
|
746
783
|
}
|
|
747
784
|
async function initProjectConfig(dir) {
|
|
748
785
|
const location = { path: projectConfigPath(dir), scope: "project" };
|
|
@@ -805,17 +842,24 @@ async function readJson(file) {
|
|
|
805
842
|
}
|
|
806
843
|
}
|
|
807
844
|
async function writeJsonAtomic(file, value, mode) {
|
|
808
|
-
|
|
845
|
+
const parent = path3.dirname(file);
|
|
846
|
+
if (isInside(homeDir, parent)) await ensureDir(parent);
|
|
847
|
+
else await mkdir2(parent, { recursive: true });
|
|
809
848
|
const temp = `${file}.${randomUUID2()}.tmp`;
|
|
810
849
|
try {
|
|
811
850
|
await writeFile2(temp, `${JSON.stringify(value, null, 2)}
|
|
812
851
|
`, { encoding: "utf8", mode });
|
|
813
852
|
await rename2(temp, file);
|
|
853
|
+
if (mode !== void 0) await chmod2(file, mode);
|
|
814
854
|
} catch (error) {
|
|
815
855
|
await rm2(temp, { force: true });
|
|
816
856
|
throw error;
|
|
817
857
|
}
|
|
818
858
|
}
|
|
859
|
+
function isInside(root, candidate) {
|
|
860
|
+
const relative2 = path3.relative(path3.resolve(root), path3.resolve(candidate));
|
|
861
|
+
return relative2 === "" || !path3.isAbsolute(relative2) && relative2.split(path3.sep)[0] !== "..";
|
|
862
|
+
}
|
|
819
863
|
function formatIssues(file, error) {
|
|
820
864
|
const lines = error.issues.map(
|
|
821
865
|
(issue) => ` ${issue.path.join(".") || "(root)"}: ${issue.message}`
|
|
@@ -844,6 +888,8 @@ async function addProvider(url, key, options = {}) {
|
|
|
844
888
|
}
|
|
845
889
|
const config = options.local ? await loadProjectConfig(cwd) : (await loadRuntimeConfig(cwd)).config;
|
|
846
890
|
const auth = await loadAuth();
|
|
891
|
+
const configBefore = structuredClone(config);
|
|
892
|
+
const authBefore = { ...auth };
|
|
847
893
|
config.providers[detected.id] = detected.config;
|
|
848
894
|
auth[detected.id] = key;
|
|
849
895
|
const models = detected.models;
|
|
@@ -851,8 +897,16 @@ async function addProvider(url, key, options = {}) {
|
|
|
851
897
|
const chosen = pickDefault(models);
|
|
852
898
|
if (chosen) config.model = formatModelRef(detected.id, chosen);
|
|
853
899
|
}
|
|
854
|
-
|
|
855
|
-
|
|
900
|
+
try {
|
|
901
|
+
await saveConfig(config);
|
|
902
|
+
await saveAuth(auth);
|
|
903
|
+
} catch (error) {
|
|
904
|
+
Object.assign(config, configBefore);
|
|
905
|
+
for (const id of Object.keys(auth)) delete auth[id];
|
|
906
|
+
Object.assign(auth, authBefore);
|
|
907
|
+
await Promise.allSettled([saveConfig(config), saveAuth(auth)]);
|
|
908
|
+
throw error;
|
|
909
|
+
}
|
|
856
910
|
const location = await configLocation();
|
|
857
911
|
const protocol = detected.config.type === "anthropic" ? "Anthropic" : "OpenAI-compatible";
|
|
858
912
|
const rows = [
|
|
@@ -959,7 +1013,7 @@ function skipControlString(text, from, bellTerminates) {
|
|
|
959
1013
|
|
|
960
1014
|
// src/app/runtime.ts
|
|
961
1015
|
import path13 from "path";
|
|
962
|
-
import { mkdir as
|
|
1016
|
+
import { mkdir as mkdir6 } from "fs/promises";
|
|
963
1017
|
|
|
964
1018
|
// src/core/session.ts
|
|
965
1019
|
import { randomBytes } from "crypto";
|
|
@@ -985,8 +1039,13 @@ async function saveSession(state) {
|
|
|
985
1039
|
await ensureDir(sessionsDir);
|
|
986
1040
|
const file = path4.join(sessionsDir, `${state.id}.json`);
|
|
987
1041
|
const temp = `${file}.${process.pid}.${randomBytes(4).toString("hex")}.tmp`;
|
|
988
|
-
|
|
989
|
-
|
|
1042
|
+
try {
|
|
1043
|
+
await writeFile3(temp, JSON.stringify(state), { encoding: "utf8", mode: 384 });
|
|
1044
|
+
await atomicRename(temp, file);
|
|
1045
|
+
} catch (error) {
|
|
1046
|
+
await unlink(temp).catch(() => void 0);
|
|
1047
|
+
throw error;
|
|
1048
|
+
}
|
|
990
1049
|
}
|
|
991
1050
|
async function atomicRename(from, to, retries = 5) {
|
|
992
1051
|
for (let attempt = 0; attempt < retries; attempt++) {
|
|
@@ -1160,7 +1219,9 @@ function readState(raw) {
|
|
|
1160
1219
|
}
|
|
1161
1220
|
if (typeof parsed !== "object" || parsed === null) return null;
|
|
1162
1221
|
const state = parsed;
|
|
1163
|
-
if (typeof state.id !== "string" || !
|
|
1222
|
+
if (typeof state.id !== "string" || !validSessionId(state.id)) return null;
|
|
1223
|
+
const messages = readMessages(state.messages);
|
|
1224
|
+
if (!messages) return null;
|
|
1164
1225
|
return {
|
|
1165
1226
|
id: state.id,
|
|
1166
1227
|
title: typeof state.title === "string" ? state.title.slice(0, 120) : void 0,
|
|
@@ -1168,11 +1229,59 @@ function readState(raw) {
|
|
|
1168
1229
|
createdAt: typeof state.createdAt === "string" ? state.createdAt : "",
|
|
1169
1230
|
updatedAt: typeof state.updatedAt === "string" ? state.updatedAt : "",
|
|
1170
1231
|
model: typeof state.model === "string" ? state.model : "",
|
|
1171
|
-
messages
|
|
1172
|
-
usage:
|
|
1232
|
+
messages,
|
|
1233
|
+
usage: readUsageEntries(state.usage),
|
|
1173
1234
|
context: readContextUsage(state.context)
|
|
1174
1235
|
};
|
|
1175
1236
|
}
|
|
1237
|
+
function readMessages(value) {
|
|
1238
|
+
if (!Array.isArray(value)) return null;
|
|
1239
|
+
const messages = [];
|
|
1240
|
+
for (const item of value) {
|
|
1241
|
+
if (typeof item !== "object" || item === null) return null;
|
|
1242
|
+
const candidate = item;
|
|
1243
|
+
if (candidate.role !== "user" && candidate.role !== "assistant") return null;
|
|
1244
|
+
if (!Array.isArray(candidate.content)) return null;
|
|
1245
|
+
const content = [];
|
|
1246
|
+
for (const block of candidate.content) {
|
|
1247
|
+
if (!validContentBlock(block)) return null;
|
|
1248
|
+
content.push(block);
|
|
1249
|
+
}
|
|
1250
|
+
messages.push({ role: candidate.role, content });
|
|
1251
|
+
}
|
|
1252
|
+
return messages;
|
|
1253
|
+
}
|
|
1254
|
+
function validContentBlock(value) {
|
|
1255
|
+
if (typeof value !== "object" || value === null) return false;
|
|
1256
|
+
const block = value;
|
|
1257
|
+
switch (block.type) {
|
|
1258
|
+
case "text":
|
|
1259
|
+
case "thinking":
|
|
1260
|
+
return typeof block.text === "string";
|
|
1261
|
+
case "tool_use":
|
|
1262
|
+
return typeof block.id === "string" && typeof block.name === "string";
|
|
1263
|
+
case "tool_result":
|
|
1264
|
+
return typeof block.toolUseId === "string" && typeof block.content === "string" && (block.isError === void 0 || typeof block.isError === "boolean");
|
|
1265
|
+
case "image":
|
|
1266
|
+
return typeof block.name === "string" && typeof block.data === "string" && block.data.length <= MAX_SESSION_BYTES * 2 && ["image/jpeg", "image/png", "image/gif", "image/webp"].includes(String(block.mediaType));
|
|
1267
|
+
case "file":
|
|
1268
|
+
return typeof block.name === "string" && typeof block.mediaType === "string" && typeof block.text === "string";
|
|
1269
|
+
default:
|
|
1270
|
+
return false;
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
function readUsageEntries(value) {
|
|
1274
|
+
if (!Array.isArray(value)) return [];
|
|
1275
|
+
return value.flatMap((item) => {
|
|
1276
|
+
if (typeof item !== "object" || item === null) return [];
|
|
1277
|
+
const candidate = item;
|
|
1278
|
+
const usage = readUsage(candidate.usage);
|
|
1279
|
+
if (typeof candidate.model !== "string" || !usage || !Number.isInteger(candidate.requests) || candidate.requests < 0) {
|
|
1280
|
+
return [];
|
|
1281
|
+
}
|
|
1282
|
+
return [{ model: candidate.model, usage, requests: candidate.requests, costUsd: null }];
|
|
1283
|
+
});
|
|
1284
|
+
}
|
|
1176
1285
|
async function resolveExportTarget(destination, state) {
|
|
1177
1286
|
const resolved = path4.resolve(destination);
|
|
1178
1287
|
const info = await stat2(resolved).catch(() => null);
|
|
@@ -1233,7 +1342,9 @@ function readUsage(value) {
|
|
|
1233
1342
|
if (typeof value !== "object" || value === null) return void 0;
|
|
1234
1343
|
const usage = value;
|
|
1235
1344
|
const fields = ["input", "output", "cacheWrite", "cacheRead"];
|
|
1236
|
-
if (fields.some(
|
|
1345
|
+
if (fields.some(
|
|
1346
|
+
(field2) => typeof usage[field2] !== "number" || !Number.isFinite(usage[field2]) || usage[field2] < 0
|
|
1347
|
+
)) {
|
|
1237
1348
|
return void 0;
|
|
1238
1349
|
}
|
|
1239
1350
|
return {
|
|
@@ -1264,10 +1375,13 @@ async function resolveSessionId(query) {
|
|
|
1264
1375
|
);
|
|
1265
1376
|
}
|
|
1266
1377
|
function assertSessionId(id) {
|
|
1267
|
-
if (
|
|
1378
|
+
if (!validSessionId(id)) {
|
|
1268
1379
|
throw new Error(`Invalid session id: ${id}`);
|
|
1269
1380
|
}
|
|
1270
1381
|
}
|
|
1382
|
+
function validSessionId(id) {
|
|
1383
|
+
return /^[A-Za-z0-9_-]{1,240}$/.test(id);
|
|
1384
|
+
}
|
|
1271
1385
|
|
|
1272
1386
|
// src/core/agent.ts
|
|
1273
1387
|
var MAX_PAUSE_RESUMES = 5;
|
|
@@ -1443,11 +1557,20 @@ function retryAfterFromError(error) {
|
|
|
1443
1557
|
}
|
|
1444
1558
|
function sleep(ms, signal) {
|
|
1445
1559
|
return new Promise((resolve3, reject) => {
|
|
1446
|
-
|
|
1447
|
-
|
|
1560
|
+
if (signal.aborted) {
|
|
1561
|
+
reject(signal.reason ?? new Error("Aborted"));
|
|
1562
|
+
return;
|
|
1563
|
+
}
|
|
1564
|
+
const finish = () => {
|
|
1565
|
+
signal.removeEventListener("abort", onAbort);
|
|
1566
|
+
resolve3();
|
|
1567
|
+
};
|
|
1568
|
+
const onAbort = () => {
|
|
1448
1569
|
clearTimeout(id);
|
|
1449
1570
|
reject(signal.reason ?? new Error("Aborted"));
|
|
1450
|
-
}
|
|
1571
|
+
};
|
|
1572
|
+
const id = setTimeout(finish, ms);
|
|
1573
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
1451
1574
|
});
|
|
1452
1575
|
}
|
|
1453
1576
|
function estimateRequestTokens(cfg, messages) {
|
|
@@ -1466,9 +1589,7 @@ function estimateRequestTokens(cfg, messages) {
|
|
|
1466
1589
|
async function runToolCalls(cfg, content, hooks, signal) {
|
|
1467
1590
|
const calls = content.filter((block) => block.type === "tool_use");
|
|
1468
1591
|
const results = [];
|
|
1469
|
-
for (const call of calls)
|
|
1470
|
-
results.push(await runOneCall(cfg, call, hooks, signal));
|
|
1471
|
-
}
|
|
1592
|
+
for (const call of calls) results.push(await runOneCall(cfg, call, hooks, signal));
|
|
1472
1593
|
return results;
|
|
1473
1594
|
}
|
|
1474
1595
|
async function runOneCall(cfg, call, hooks, signal) {
|
|
@@ -1596,6 +1717,7 @@ function costOf(usage, pricing) {
|
|
|
1596
1717
|
}
|
|
1597
1718
|
|
|
1598
1719
|
// src/core/budget.ts
|
|
1720
|
+
var UNKNOWN_MODEL_PRICING = { input: 3, output: 15 };
|
|
1599
1721
|
function createTurnBudget(limits, resolvePricing = pricingFor) {
|
|
1600
1722
|
let usage = emptyUsage();
|
|
1601
1723
|
let costUsd = 0;
|
|
@@ -1631,11 +1753,12 @@ function createTurnBudget(limits, resolvePricing = pricingFor) {
|
|
|
1631
1753
|
};
|
|
1632
1754
|
}
|
|
1633
1755
|
const pricing = resolvePricing(request.modelRef);
|
|
1634
|
-
if (
|
|
1756
|
+
if (current.costUsd !== null) {
|
|
1635
1757
|
const remainingUsd = limits.maxCostUsdPerTurn - current.costUsd;
|
|
1636
|
-
const
|
|
1758
|
+
const effectivePricing = pricing ?? UNKNOWN_MODEL_PRICING;
|
|
1759
|
+
const inputUsd = estimatedInput * effectivePricing.input / 1e6;
|
|
1637
1760
|
const affordableOutput = Math.floor(
|
|
1638
|
-
(remainingUsd - inputUsd) * 1e6 /
|
|
1761
|
+
(remainingUsd - inputUsd) * 1e6 / effectivePricing.output
|
|
1639
1762
|
);
|
|
1640
1763
|
outputLimit = Math.min(outputLimit, affordableOutput);
|
|
1641
1764
|
if (outputLimit < 1) {
|
|
@@ -1648,9 +1771,10 @@ function createTurnBudget(limits, resolvePricing = pricingFor) {
|
|
|
1648
1771
|
return { allowed: true, maxOutputTokens: Math.floor(outputLimit) };
|
|
1649
1772
|
},
|
|
1650
1773
|
record(modelRef, next) {
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1774
|
+
const normalized = normalizeUsage(next);
|
|
1775
|
+
usage = addUsage(usage, normalized);
|
|
1776
|
+
const priced = costOf(normalized, resolvePricing(modelRef) ?? UNKNOWN_MODEL_PRICING);
|
|
1777
|
+
costUsd = (costUsd ?? 0) + (priced ?? 0);
|
|
1654
1778
|
},
|
|
1655
1779
|
snapshot
|
|
1656
1780
|
};
|
|
@@ -1673,7 +1797,7 @@ import { createReadStream } from "fs";
|
|
|
1673
1797
|
import {
|
|
1674
1798
|
chmod as chmod3,
|
|
1675
1799
|
lstat,
|
|
1676
|
-
mkdir as
|
|
1800
|
+
mkdir as mkdir3,
|
|
1677
1801
|
readFile as readFile4,
|
|
1678
1802
|
readdir as readdir2,
|
|
1679
1803
|
rename as rename4,
|
|
@@ -1813,7 +1937,7 @@ function beginCheckpoint(options) {
|
|
|
1813
1937
|
lastCheckpointTimestamp = Math.max(Date.now(), lastCheckpointTimestamp + 1);
|
|
1814
1938
|
const id = `${String(lastCheckpointTimestamp).padStart(13, "0")}-${randomBytes2(4).toString("hex")}`;
|
|
1815
1939
|
await writeCheckpoint(path5.join(sessionDir, `${id}.json`), stored);
|
|
1816
|
-
|
|
1940
|
+
pruneCheckpoints(sessionDir).catch(() => void 0);
|
|
1817
1941
|
return { id, paths: entries.map((entry) => entry.path) };
|
|
1818
1942
|
}
|
|
1819
1943
|
};
|
|
@@ -1915,7 +2039,7 @@ async function restoreFile(root, relativePath, snapshot, expected) {
|
|
|
1915
2039
|
const safe = resolveInside(root, relativePath);
|
|
1916
2040
|
if (!safe.ok || safe.relative !== relativePath) throw new Error("The restore path changed.");
|
|
1917
2041
|
const data = decodeSnapshot(snapshot);
|
|
1918
|
-
await
|
|
2042
|
+
await mkdir3(path5.dirname(safe.path), { recursive: true });
|
|
1919
2043
|
const rechecked = resolveInside(root, relativePath);
|
|
1920
2044
|
if (!rechecked.ok || rechecked.path !== safe.path || rechecked.relative !== relativePath) {
|
|
1921
2045
|
throw new Error("The restore path changed while preparing its parent directory.");
|
|
@@ -2315,7 +2439,7 @@ async function isFile2(file) {
|
|
|
2315
2439
|
|
|
2316
2440
|
// src/core/attachments.ts
|
|
2317
2441
|
import { execFile } from "child_process";
|
|
2318
|
-
import { readFile as readFile6, stat as stat5 } from "fs/promises";
|
|
2442
|
+
import { lstat as lstat2, readFile as readFile6, stat as stat5 } from "fs/promises";
|
|
2319
2443
|
import os2 from "os";
|
|
2320
2444
|
import path7 from "path";
|
|
2321
2445
|
import { fileURLToPath } from "url";
|
|
@@ -2328,6 +2452,8 @@ var SENSITIVE_AUTO_NAMES = /* @__PURE__ */ new Set([
|
|
|
2328
2452
|
".netrc",
|
|
2329
2453
|
".npmrc",
|
|
2330
2454
|
".pypirc",
|
|
2455
|
+
".pgpass",
|
|
2456
|
+
".my.cnf",
|
|
2331
2457
|
"auth.json",
|
|
2332
2458
|
"credentials.json",
|
|
2333
2459
|
"id_dsa",
|
|
@@ -2337,11 +2463,17 @@ var SENSITIVE_AUTO_NAMES = /* @__PURE__ */ new Set([
|
|
|
2337
2463
|
"kitcode.json",
|
|
2338
2464
|
"secret.json",
|
|
2339
2465
|
"secrets.json",
|
|
2340
|
-
"tokens.json"
|
|
2466
|
+
"tokens.json",
|
|
2467
|
+
"kubeconfig",
|
|
2468
|
+
"service-account.json"
|
|
2341
2469
|
]);
|
|
2342
|
-
var SENSITIVE_AUTO_EXTENSIONS = /* @__PURE__ */ new Set([".jks", ".key", ".keystore", ".p12", ".pem", ".pfx"]);
|
|
2470
|
+
var SENSITIVE_AUTO_EXTENSIONS = /* @__PURE__ */ new Set([".jks", ".key", ".keystore", ".p12", ".pem", ".pfx", ".p8", ".der"]);
|
|
2343
2471
|
async function loadAttachment(cwd, requestedPath) {
|
|
2344
2472
|
const resolved = resolveAttachmentPath(cwd, requestedPath);
|
|
2473
|
+
const linkInfo = await lstat2(resolved).catch(() => null);
|
|
2474
|
+
if (linkInfo?.isSymbolicLink()) {
|
|
2475
|
+
throw new Error(`Cannot attach symbolic links: ${path7.basename(resolved)}`);
|
|
2476
|
+
}
|
|
2345
2477
|
const info = await stat5(resolved).catch((error) => {
|
|
2346
2478
|
if (error.code === "ENOENT") throw new Error(`Attachment not found: ${resolved}`);
|
|
2347
2479
|
throw error;
|
|
@@ -2351,17 +2483,22 @@ async function loadAttachment(cwd, requestedPath) {
|
|
|
2351
2483
|
}
|
|
2352
2484
|
async function loadAutomaticAttachment(cwd, requestedPath) {
|
|
2353
2485
|
if (!looksLikeAttachmentPath(requestedPath)) return null;
|
|
2354
|
-
const
|
|
2355
|
-
const
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
});
|
|
2359
|
-
if (!info?.isFile()) return null;
|
|
2486
|
+
const requested = resolveAttachmentPath(cwd, requestedPath);
|
|
2487
|
+
const safe = resolveInside(cwd, requested);
|
|
2488
|
+
if (!safe.ok || safe.relative === "") return null;
|
|
2489
|
+
const resolved = safe.path;
|
|
2360
2490
|
if (isSensitiveAutomaticPath(resolved)) {
|
|
2361
2491
|
throw new Error(
|
|
2362
2492
|
`For safety, sensitive-looking files must be attached explicitly with /attach: ${path7.basename(resolved)}`
|
|
2363
2493
|
);
|
|
2364
2494
|
}
|
|
2495
|
+
const linkInfo = await lstat2(resolved).catch(() => null);
|
|
2496
|
+
if (linkInfo?.isSymbolicLink()) return null;
|
|
2497
|
+
const info = await stat5(resolved).catch((error) => {
|
|
2498
|
+
if (error.code === "ENOENT" || error.code === "ENOTDIR") return null;
|
|
2499
|
+
throw error;
|
|
2500
|
+
});
|
|
2501
|
+
if (!info?.isFile()) return null;
|
|
2365
2502
|
return loadResolvedAttachment(resolved, info.size);
|
|
2366
2503
|
}
|
|
2367
2504
|
function looksLikeAttachmentPath(value) {
|
|
@@ -2622,7 +2759,7 @@ function normalizeInputPath(value) {
|
|
|
2622
2759
|
return value.slice(1, -1);
|
|
2623
2760
|
}
|
|
2624
2761
|
}
|
|
2625
|
-
return value.replace(/\\([\\ "'()\[\]
|
|
2762
|
+
return value.replace(/\\([\\ "'()\[\]&;#$!])/g, (_match, character) => character);
|
|
2626
2763
|
}
|
|
2627
2764
|
function safeName(value) {
|
|
2628
2765
|
return value.replace(/[\r\n\0-\x1f\x7f]/g, "").slice(0, 200) || "attachment";
|
|
@@ -2633,47 +2770,90 @@ function formatBytes(value) {
|
|
|
2633
2770
|
|
|
2634
2771
|
// src/core/compact.ts
|
|
2635
2772
|
var KEEP_USER_TURNS = 2;
|
|
2636
|
-
var
|
|
2637
|
-
var
|
|
2638
|
-
var
|
|
2773
|
+
var SINGLE_REQUEST_MAX_CHARS = 8e4;
|
|
2774
|
+
var CHUNK_TARGET_CHARS = 35e3;
|
|
2775
|
+
var CHUNK_OVERLAP_MESSAGES = 2;
|
|
2776
|
+
var CHUNK_OVERLAP_MAX_CHARS = 4e3;
|
|
2777
|
+
var MERGE_TARGET_CHARS = 6e4;
|
|
2778
|
+
var MAX_USER_TEXT_CHARS = 12e3;
|
|
2779
|
+
var MAX_ASSISTANT_TEXT_CHARS = 6e3;
|
|
2780
|
+
var MAX_FILE_BLOCK_CHARS = 2e3;
|
|
2781
|
+
var MAX_TOOL_RESULT_CHARS = 1200;
|
|
2782
|
+
var MAX_TOOL_ERROR_CHARS = 2e3;
|
|
2783
|
+
var MAX_TOOL_INPUT_CHARS = 800;
|
|
2784
|
+
var SINGLE_MAX_TOKENS = 768;
|
|
2785
|
+
var CHUNK_MAX_TOKENS = 512;
|
|
2786
|
+
var MERGE_MAX_TOKENS = 1024;
|
|
2787
|
+
var CHUNK_CONCURRENCY = 3;
|
|
2788
|
+
var SYSTEM_SUMMARIZE = "You are summarizing a coding conversation so another agent can continue the work. Extract and preserve ONLY: concrete requirements, user preferences, files changed, commands run and their results, errors encountered, bugs found, architectural decisions, and unfinished work. Be terse and factual. Do NOT include greetings, acknowledgments, chain-of-thought, or step-by-step narration. Return only the summary.";
|
|
2789
|
+
var SYSTEM_MERGE = "Merge these partial summaries of a coding conversation into one coherent summary. Remove duplicates. Preserve: requirements, files changed, commands run, errors, decisions, and unfinished work. Be terse. Return only the merged summary.";
|
|
2639
2790
|
async function compactHistory(options) {
|
|
2640
2791
|
const cut = compactCutIndex(options.history);
|
|
2641
2792
|
if (cut <= 0) {
|
|
2642
2793
|
return { history: options.history, compacted: false, removedMessages: 0 };
|
|
2643
2794
|
}
|
|
2644
|
-
const
|
|
2645
|
-
|
|
2795
|
+
const oldMessages = options.history.slice(0, cut);
|
|
2796
|
+
const renderedMessages = renderMessages(oldMessages);
|
|
2797
|
+
const rendered = renderedMessages.join("\n\n");
|
|
2798
|
+
if (!rendered.trim()) {
|
|
2646
2799
|
return { history: options.history, compacted: false, removedMessages: 0 };
|
|
2647
2800
|
}
|
|
2648
|
-
let summary
|
|
2649
|
-
let finalText2 = "";
|
|
2801
|
+
let summary;
|
|
2650
2802
|
let usage;
|
|
2651
2803
|
let rateLimits;
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
2804
|
+
if (rendered.length <= SINGLE_REQUEST_MAX_CHARS) {
|
|
2805
|
+
const requestMax = singleOutputLimit(options.maxTokens, options.maxTotalOutputTokens);
|
|
2806
|
+
const result = await summarizeText(
|
|
2807
|
+
options.provider,
|
|
2808
|
+
options.model,
|
|
2809
|
+
options.maxTokens,
|
|
2810
|
+
options.signal,
|
|
2811
|
+
rendered,
|
|
2812
|
+
SYSTEM_SUMMARIZE,
|
|
2813
|
+
requestMax
|
|
2814
|
+
);
|
|
2815
|
+
summary = result.text;
|
|
2816
|
+
usage = result.usage;
|
|
2817
|
+
rateLimits = result.rateLimits;
|
|
2818
|
+
} else {
|
|
2819
|
+
const chunks = chunkRenderedMessages(
|
|
2820
|
+
renderedMessages,
|
|
2821
|
+
CHUNK_TARGET_CHARS,
|
|
2822
|
+
CHUNK_OVERLAP_MESSAGES,
|
|
2823
|
+
CHUNK_OVERLAP_MAX_CHARS
|
|
2824
|
+
);
|
|
2825
|
+
const limits = chunkOutputLimits(
|
|
2826
|
+
chunks.length,
|
|
2827
|
+
options.maxTokens,
|
|
2828
|
+
options.maxTotalOutputTokens
|
|
2829
|
+
);
|
|
2830
|
+
const chunkResults = await summarizeSourcesParallel(
|
|
2831
|
+
options.provider,
|
|
2832
|
+
options.model,
|
|
2833
|
+
options.maxTokens,
|
|
2834
|
+
options.signal,
|
|
2835
|
+
chunks,
|
|
2836
|
+
SYSTEM_SUMMARIZE,
|
|
2837
|
+
limits.chunk
|
|
2838
|
+
);
|
|
2839
|
+
const chunkUsages = chunkResults.map((result) => result.usage).filter(Boolean);
|
|
2840
|
+
usage = chunkUsages.length > 0 ? mergeUsage(chunkUsages) : void 0;
|
|
2841
|
+
rateLimits = [...chunkResults].reverse().find((r) => r.rateLimits)?.rateLimits;
|
|
2842
|
+
const mergeResult = await mergePartialSummaries(
|
|
2843
|
+
options.provider,
|
|
2844
|
+
options.model,
|
|
2845
|
+
options.maxTokens,
|
|
2846
|
+
options.signal,
|
|
2847
|
+
chunkResults.map((result) => result.text),
|
|
2848
|
+
limits.merge
|
|
2849
|
+
);
|
|
2850
|
+
summary = mergeResult.text;
|
|
2851
|
+
if (mergeResult.usage) {
|
|
2852
|
+
usage = usage ? mergeUsage([usage, mergeResult.usage]) : mergeResult.usage;
|
|
2674
2853
|
}
|
|
2854
|
+
if (mergeResult.rateLimits) rateLimits = mergeResult.rateLimits;
|
|
2675
2855
|
}
|
|
2676
|
-
summary =
|
|
2856
|
+
summary = summary.trim();
|
|
2677
2857
|
if (!summary) throw new Error("The provider returned an empty context summary.");
|
|
2678
2858
|
const prefix = [
|
|
2679
2859
|
{
|
|
@@ -2705,11 +2885,34 @@ function compactCutIndex(history) {
|
|
|
2705
2885
|
if (userTurns.length <= KEEP_USER_TURNS) return 0;
|
|
2706
2886
|
return userTurns[userTurns.length - KEEP_USER_TURNS] ?? 0;
|
|
2707
2887
|
}
|
|
2708
|
-
function
|
|
2888
|
+
function estimateCompactBudget(history, maxTokens = MERGE_MAX_TOKENS) {
|
|
2709
2889
|
const cut = compactCutIndex(history);
|
|
2710
|
-
if (cut <= 0) return 1;
|
|
2711
|
-
const
|
|
2712
|
-
|
|
2890
|
+
if (cut <= 0) return { inputTokens: 1, maxOutputTokens: 0 };
|
|
2891
|
+
const renderedMessages = renderMessages(history.slice(0, cut));
|
|
2892
|
+
const rendered = renderedMessages.join("\n\n");
|
|
2893
|
+
if (rendered.length <= SINGLE_REQUEST_MAX_CHARS) {
|
|
2894
|
+
return {
|
|
2895
|
+
inputTokens: Math.max(1, rendered.length + 1e3),
|
|
2896
|
+
maxOutputTokens: Math.max(1, Math.min(SINGLE_MAX_TOKENS, Math.floor(maxTokens)))
|
|
2897
|
+
};
|
|
2898
|
+
}
|
|
2899
|
+
const chunks = chunkRenderedMessages(
|
|
2900
|
+
renderedMessages,
|
|
2901
|
+
CHUNK_TARGET_CHARS,
|
|
2902
|
+
CHUNK_OVERLAP_MESSAGES,
|
|
2903
|
+
CHUNK_OVERLAP_MAX_CHARS
|
|
2904
|
+
);
|
|
2905
|
+
const chunkInput = chunks.reduce((total, chunk) => total + chunk.length, 0);
|
|
2906
|
+
const chunkOutput = Math.max(1, Math.min(CHUNK_MAX_TOKENS, Math.floor(maxTokens)));
|
|
2907
|
+
const mergeOutput = Math.max(1, Math.min(MERGE_MAX_TOKENS, Math.floor(maxTokens)));
|
|
2908
|
+
const mergeRequests = Math.max(0, chunks.length - 1);
|
|
2909
|
+
const totalOutput = chunks.length * chunkOutput + mergeRequests * mergeOutput;
|
|
2910
|
+
const intermediateOutput = chunks.length * chunkOutput + Math.max(0, mergeRequests - 1) * mergeOutput;
|
|
2911
|
+
const requestCount = chunks.length + mergeRequests;
|
|
2912
|
+
return {
|
|
2913
|
+
inputTokens: Math.max(1, chunkInput + intermediateOutput * 8 + requestCount * 1e3),
|
|
2914
|
+
maxOutputTokens: totalOutput
|
|
2915
|
+
};
|
|
2713
2916
|
}
|
|
2714
2917
|
function isConversationUser(message) {
|
|
2715
2918
|
if (message.role !== "user") return false;
|
|
@@ -2717,50 +2920,249 @@ function isConversationUser(message) {
|
|
|
2717
2920
|
(block) => block.type === "image" || block.type === "file" || block.type === "text" && !block.text.startsWith("[Earlier conversation summary]")
|
|
2718
2921
|
);
|
|
2719
2922
|
}
|
|
2720
|
-
function
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
2923
|
+
function chunkRenderedMessages(messages, targetChars, overlapMessages, overlapMaxChars) {
|
|
2924
|
+
const units = messages.flatMap((message) => splitLongMessage(message, targetChars));
|
|
2925
|
+
const chunks = [];
|
|
2926
|
+
let current = [];
|
|
2927
|
+
const sizeOf = (parts) => parts.reduce((total, part) => total + part.length, 0) + Math.max(0, parts.length - 1) * 2;
|
|
2928
|
+
for (const unit of units) {
|
|
2929
|
+
if (current.length > 0 && sizeOf([...current, unit]) > targetChars) {
|
|
2930
|
+
chunks.push(current.join("\n\n"));
|
|
2931
|
+
const overlap = [];
|
|
2932
|
+
let overlapChars = 0;
|
|
2933
|
+
for (let index = current.length - 1; index >= 0 && overlap.length < overlapMessages; index -= 1) {
|
|
2934
|
+
const candidate = current[index];
|
|
2935
|
+
if (overlapChars + candidate.length > overlapMaxChars) break;
|
|
2936
|
+
overlap.unshift(candidate);
|
|
2937
|
+
overlapChars += candidate.length;
|
|
2938
|
+
}
|
|
2939
|
+
current = overlap;
|
|
2940
|
+
while (current.length > 0 && sizeOf([...current, unit]) > targetChars) current.shift();
|
|
2730
2941
|
}
|
|
2731
|
-
|
|
2942
|
+
current.push(unit);
|
|
2732
2943
|
}
|
|
2733
|
-
|
|
2944
|
+
if (current.length > 0) chunks.push(current.join("\n\n"));
|
|
2945
|
+
return chunks;
|
|
2946
|
+
}
|
|
2947
|
+
function splitLongMessage(message, targetChars) {
|
|
2948
|
+
if (message.length <= targetChars) return [message];
|
|
2949
|
+
const parts = [];
|
|
2950
|
+
let remaining = message;
|
|
2951
|
+
const payloadLimit = Math.max(1, targetChars - 32);
|
|
2952
|
+
while (remaining.length > payloadLimit) {
|
|
2953
|
+
let cut = remaining.lastIndexOf("\n", payloadLimit);
|
|
2954
|
+
if (cut < payloadLimit * 0.6) cut = payloadLimit;
|
|
2955
|
+
parts.push(`${remaining.slice(0, cut)}
|
|
2956
|
+
[message continues]`);
|
|
2957
|
+
remaining = `[continued message]
|
|
2958
|
+
${remaining.slice(cut).replace(/^\n/, "")}`;
|
|
2959
|
+
}
|
|
2960
|
+
if (remaining) parts.push(remaining);
|
|
2961
|
+
return parts;
|
|
2962
|
+
}
|
|
2963
|
+
async function summarizeSourcesParallel(provider, model, maxTokens, signal, sources, systemPrompt, maxOutputTokens) {
|
|
2964
|
+
const results = new Array(sources.length);
|
|
2965
|
+
const controller = new AbortController();
|
|
2966
|
+
const abort = () => controller.abort(signal.reason);
|
|
2967
|
+
if (signal.aborted) abort();
|
|
2968
|
+
else signal.addEventListener("abort", abort, { once: true });
|
|
2969
|
+
let next = 0;
|
|
2970
|
+
const worker = async () => {
|
|
2971
|
+
for (; ; ) {
|
|
2972
|
+
const index = next++;
|
|
2973
|
+
if (index >= sources.length) return;
|
|
2974
|
+
try {
|
|
2975
|
+
results[index] = await summarizeText(
|
|
2976
|
+
provider,
|
|
2977
|
+
model,
|
|
2978
|
+
maxTokens,
|
|
2979
|
+
controller.signal,
|
|
2980
|
+
sources[index],
|
|
2981
|
+
systemPrompt,
|
|
2982
|
+
maxOutputTokens
|
|
2983
|
+
);
|
|
2984
|
+
} catch (error) {
|
|
2985
|
+
controller.abort(error);
|
|
2986
|
+
throw error;
|
|
2987
|
+
}
|
|
2988
|
+
}
|
|
2989
|
+
};
|
|
2990
|
+
try {
|
|
2991
|
+
await Promise.all(
|
|
2992
|
+
Array.from({ length: Math.min(CHUNK_CONCURRENCY, sources.length) }, () => worker())
|
|
2993
|
+
);
|
|
2994
|
+
return results;
|
|
2995
|
+
} finally {
|
|
2996
|
+
signal.removeEventListener("abort", abort);
|
|
2997
|
+
}
|
|
2998
|
+
}
|
|
2999
|
+
async function mergePartialSummaries(provider, model, maxTokens, signal, summaries, mergeMaxTokens) {
|
|
3000
|
+
let current = summaries;
|
|
3001
|
+
const usages = [];
|
|
3002
|
+
let rateLimits;
|
|
3003
|
+
while (current.length > 1) {
|
|
3004
|
+
const groups = groupSummaries(current, MERGE_TARGET_CHARS);
|
|
3005
|
+
const sources = groups.map(
|
|
3006
|
+
(group) => group.map((summary, index) => `## Part ${index + 1}
|
|
3007
|
+
${summary}`).join("\n\n")
|
|
3008
|
+
);
|
|
3009
|
+
const merged = await summarizeSourcesParallel(
|
|
3010
|
+
provider,
|
|
3011
|
+
model,
|
|
3012
|
+
maxTokens,
|
|
3013
|
+
signal,
|
|
3014
|
+
sources,
|
|
3015
|
+
SYSTEM_MERGE,
|
|
3016
|
+
mergeMaxTokens
|
|
3017
|
+
);
|
|
3018
|
+
for (const result of merged) {
|
|
3019
|
+
if (!result.text) throw new Error("The provider returned an empty partial context summary.");
|
|
3020
|
+
if (result.usage) usages.push(result.usage);
|
|
3021
|
+
if (result.rateLimits) rateLimits = result.rateLimits;
|
|
3022
|
+
}
|
|
3023
|
+
current = merged.map((result) => result.text);
|
|
3024
|
+
}
|
|
3025
|
+
return {
|
|
3026
|
+
text: current[0] ?? "",
|
|
3027
|
+
usage: usages.length > 0 ? mergeUsage(usages) : void 0,
|
|
3028
|
+
rateLimits
|
|
3029
|
+
};
|
|
3030
|
+
}
|
|
3031
|
+
function singleOutputLimit(maxTokens, totalBudget) {
|
|
3032
|
+
const requested = Math.max(1, Math.min(SINGLE_MAX_TOKENS, Math.floor(maxTokens)));
|
|
3033
|
+
if (totalBudget === void 0) return requested;
|
|
3034
|
+
const allowed = Math.floor(totalBudget);
|
|
3035
|
+
if (allowed < 1) throw new Error("The remaining turn budget is too small to compact context.");
|
|
3036
|
+
return Math.min(requested, allowed);
|
|
3037
|
+
}
|
|
3038
|
+
function chunkOutputLimits(chunkCount, maxTokens, totalBudget) {
|
|
3039
|
+
let chunk = Math.max(1, Math.min(CHUNK_MAX_TOKENS, Math.floor(maxTokens)));
|
|
3040
|
+
let merge = Math.max(1, Math.min(MERGE_MAX_TOKENS, Math.floor(maxTokens)));
|
|
3041
|
+
if (totalBudget === void 0) return { chunk, merge };
|
|
3042
|
+
const mergeRequests = Math.max(0, chunkCount - 1);
|
|
3043
|
+
const requestCount = chunkCount + mergeRequests;
|
|
3044
|
+
const allowed = Math.floor(totalBudget);
|
|
3045
|
+
if (allowed < requestCount) {
|
|
3046
|
+
throw new Error("The remaining turn budget is too small for all context-compaction requests.");
|
|
3047
|
+
}
|
|
3048
|
+
const requested = chunkCount * chunk + mergeRequests * merge;
|
|
3049
|
+
if (allowed >= requested) return { chunk, merge };
|
|
3050
|
+
const scale = allowed / requested;
|
|
3051
|
+
chunk = Math.max(1, Math.floor(chunk * scale));
|
|
3052
|
+
merge = Math.max(1, Math.floor(merge * scale));
|
|
3053
|
+
const total = () => chunkCount * chunk + mergeRequests * merge;
|
|
3054
|
+
while (total() > allowed) {
|
|
3055
|
+
if (mergeRequests > 0 && merge > 1 && merge >= chunk) merge -= 1;
|
|
3056
|
+
else if (chunk > 1) chunk -= 1;
|
|
3057
|
+
else if (merge > 1) merge -= 1;
|
|
3058
|
+
else throw new Error("Could not fit context compaction into the remaining turn budget.");
|
|
3059
|
+
}
|
|
3060
|
+
return { chunk, merge };
|
|
3061
|
+
}
|
|
3062
|
+
function groupSummaries(summaries, targetChars) {
|
|
3063
|
+
const groups = [];
|
|
3064
|
+
let current = [];
|
|
3065
|
+
let size = 0;
|
|
3066
|
+
for (const summary of summaries) {
|
|
3067
|
+
if (current.length >= 2 && size + 12 + summary.length > targetChars) {
|
|
3068
|
+
groups.push(current);
|
|
3069
|
+
current = [];
|
|
3070
|
+
size = 0;
|
|
3071
|
+
}
|
|
3072
|
+
const separator = current.length > 0 ? 12 : 0;
|
|
3073
|
+
current.push(summary);
|
|
3074
|
+
size += separator + summary.length;
|
|
3075
|
+
}
|
|
3076
|
+
if (current.length > 0) groups.push(current);
|
|
3077
|
+
return groups;
|
|
3078
|
+
}
|
|
3079
|
+
async function summarizeText(provider, model, maxTokens, signal, source, systemPrompt, maxOutputTokens) {
|
|
3080
|
+
if (signal.aborted) throw signal.reason ?? new Error("Context compaction was cancelled.");
|
|
3081
|
+
let summary = "";
|
|
3082
|
+
let finalText2 = "";
|
|
3083
|
+
let usage;
|
|
3084
|
+
let rateLimits;
|
|
3085
|
+
const stream = provider.stream({
|
|
3086
|
+
model,
|
|
3087
|
+
system: systemPrompt,
|
|
3088
|
+
messages: [{ role: "user", content: [{ type: "text", text: source }] }],
|
|
3089
|
+
tools: [],
|
|
3090
|
+
maxTokens: Math.max(1, Math.floor(Math.min(maxOutputTokens, maxTokens))),
|
|
3091
|
+
thinking: false,
|
|
3092
|
+
signal
|
|
3093
|
+
});
|
|
3094
|
+
for await (const event of stream) {
|
|
3095
|
+
if (event.type === "text_delta") summary += event.text;
|
|
3096
|
+
else if (event.type === "usage") usage = event.usage;
|
|
3097
|
+
else if (event.type === "rate_limits") rateLimits = event.limits;
|
|
3098
|
+
else if (event.type === "done") {
|
|
3099
|
+
finalText2 = event.content.filter((block) => block.type === "text").map((block) => block.type === "text" ? block.text : "").join("");
|
|
3100
|
+
}
|
|
3101
|
+
}
|
|
3102
|
+
if (signal.aborted) throw signal.reason ?? new Error("Context compaction was cancelled.");
|
|
3103
|
+
return { text: (finalText2 || summary).trim(), usage, rateLimits };
|
|
3104
|
+
}
|
|
3105
|
+
function mergeUsage(usages) {
|
|
3106
|
+
return usages.reduce(
|
|
3107
|
+
(acc, u) => ({
|
|
3108
|
+
input: acc.input + u.input,
|
|
3109
|
+
output: acc.output + u.output,
|
|
3110
|
+
cacheWrite: acc.cacheWrite + u.cacheWrite,
|
|
3111
|
+
cacheRead: acc.cacheRead + u.cacheRead
|
|
3112
|
+
}),
|
|
3113
|
+
{ input: 0, output: 0, cacheWrite: 0, cacheRead: 0 }
|
|
3114
|
+
);
|
|
2734
3115
|
}
|
|
2735
|
-
function
|
|
3116
|
+
function renderMessages(history) {
|
|
3117
|
+
return history.flatMap((message) => {
|
|
3118
|
+
const blocks = message.content.map((block) => renderBlock(block, message.role)).filter(Boolean);
|
|
3119
|
+
return blocks.length > 0 ? [`${message.role.toUpperCase()}:
|
|
3120
|
+
${blocks.join("\n")}`] : [];
|
|
3121
|
+
});
|
|
3122
|
+
}
|
|
3123
|
+
function renderBlock(block, role) {
|
|
2736
3124
|
switch (block.type) {
|
|
2737
3125
|
case "text":
|
|
2738
|
-
return truncate(
|
|
3126
|
+
return truncate(
|
|
3127
|
+
block.text,
|
|
3128
|
+
role === "user" ? MAX_USER_TEXT_CHARS : MAX_ASSISTANT_TEXT_CHARS
|
|
3129
|
+
);
|
|
2739
3130
|
case "thinking":
|
|
2740
3131
|
return "";
|
|
2741
3132
|
case "image":
|
|
2742
|
-
return `[
|
|
3133
|
+
return `[image: ${block.name}]`;
|
|
2743
3134
|
case "file":
|
|
2744
|
-
return `[
|
|
2745
|
-
${truncate(block.text,
|
|
3135
|
+
return `[file: ${block.name}]
|
|
3136
|
+
${truncate(block.text, MAX_FILE_BLOCK_CHARS)}`;
|
|
2746
3137
|
case "tool_use":
|
|
2747
|
-
return `[
|
|
2748
|
-
${safeJson(block.input)}`;
|
|
3138
|
+
return `[tool: ${block.name}(${truncate(safeJson(block.input), MAX_TOOL_INPUT_CHARS)})]`;
|
|
2749
3139
|
case "tool_result":
|
|
2750
|
-
|
|
2751
|
-
${truncate(block.content,
|
|
3140
|
+
if (block.isError) {
|
|
3141
|
+
return `[tool result error: ${truncate(block.content, MAX_TOOL_ERROR_CHARS)}]`;
|
|
3142
|
+
}
|
|
3143
|
+
if (isTrivialSuccess(block.content)) return "";
|
|
3144
|
+
return truncate(block.content, MAX_TOOL_RESULT_CHARS);
|
|
2752
3145
|
}
|
|
2753
3146
|
}
|
|
3147
|
+
function isTrivialSuccess(content) {
|
|
3148
|
+
const t = content.trim().toLowerCase();
|
|
3149
|
+
if (!t || t === "ok" || t === "done" || t === "success") return true;
|
|
3150
|
+
if (/^no matches found/.test(t)) return true;
|
|
3151
|
+
if (t === "(no output)") return true;
|
|
3152
|
+
return false;
|
|
3153
|
+
}
|
|
2754
3154
|
function safeJson(value) {
|
|
2755
3155
|
try {
|
|
2756
|
-
return
|
|
3156
|
+
return JSON.stringify(value);
|
|
2757
3157
|
} catch {
|
|
2758
|
-
return "[unserializable
|
|
3158
|
+
return "[unserializable]";
|
|
2759
3159
|
}
|
|
2760
3160
|
}
|
|
2761
3161
|
function truncate(value, max) {
|
|
2762
|
-
|
|
2763
|
-
|
|
3162
|
+
if (value.length <= max) return value;
|
|
3163
|
+
const keep = Math.floor(max * 0.4);
|
|
3164
|
+
return `${value.slice(0, max - keep)}
|
|
3165
|
+
...${value.slice(-keep)}`;
|
|
2764
3166
|
}
|
|
2765
3167
|
|
|
2766
3168
|
// src/core/prompt.ts
|
|
@@ -2798,6 +3200,8 @@ var STEP_LIMIT_NOTE = `The subagent was stopped after ${MAX_SUBAGENT_STEPS} mode
|
|
|
2798
3200
|
var SUBAGENT_SYSTEM = [
|
|
2799
3201
|
"You are a subagent launched by the main kitcode agent to carry out one task on your own.",
|
|
2800
3202
|
"",
|
|
3203
|
+
"SECURITY CRITICAL: You must NEVER execute commands that read, modify, or exfiltrate sensitive files including but not limited to: /etc/passwd, /etc/shadow, ~/.ssh/*, ~/.aws/*, ~/.env*, .git/config, credentials files, or any files outside the workspace. Never run `curl ... | bash`, `wget ... | bash`, or similar patterns. Never send data to external servers.",
|
|
3204
|
+
"",
|
|
2801
3205
|
"Nobody is reading along and nobody can answer you. Never ask a question, never stop to propose a plan, never wait for approval. If the request is ambiguous, take the most reasonable reading, do the work, and state the assumption in your answer.",
|
|
2802
3206
|
"",
|
|
2803
3207
|
'Your final message is the return value \u2014 it is the only thing the caller ever sees. End with the answer itself: the findings, the file paths, the conclusion. Never end with "I will now..." or a recap of the steps you took. Carry over every concrete detail the caller needs (paths, names, line numbers) and leave out the narration of how you found them.'
|
|
@@ -2971,10 +3375,14 @@ function printableInput(input) {
|
|
|
2971
3375
|
}
|
|
2972
3376
|
async function invoke(call, tool, input, signal) {
|
|
2973
3377
|
try {
|
|
2974
|
-
|
|
3378
|
+
if (input !== null && typeof input !== "object" || Array.isArray(input)) {
|
|
3379
|
+
return { content: `MCP tool "${tool}" received invalid input: arguments must be a JSON object.`, isError: true };
|
|
3380
|
+
}
|
|
3381
|
+
const result = await call(tool, input ?? {}, signal);
|
|
2975
3382
|
return { content: flattenResult(result), isError: result.isError };
|
|
2976
3383
|
} catch (error) {
|
|
2977
|
-
|
|
3384
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3385
|
+
return { content: redactSecrets(message), isError: true };
|
|
2978
3386
|
}
|
|
2979
3387
|
}
|
|
2980
3388
|
function flattenResult(result) {
|
|
@@ -3016,10 +3424,18 @@ function clip(value) {
|
|
|
3016
3424
|
// package.json
|
|
3017
3425
|
var package_default = {
|
|
3018
3426
|
name: "@kernelonpanic/kitcode",
|
|
3019
|
-
version: "1.0
|
|
3427
|
+
version: "1.2.0",
|
|
3020
3428
|
description: "Terminal coding agent with a config you never have to write by hand",
|
|
3021
3429
|
type: "module",
|
|
3022
3430
|
license: "MIT",
|
|
3431
|
+
homepage: "https://github.com/KernelEditor/KitCode#readme",
|
|
3432
|
+
bugs: {
|
|
3433
|
+
url: "https://github.com/KernelEditor/KitCode/issues"
|
|
3434
|
+
},
|
|
3435
|
+
repository: {
|
|
3436
|
+
type: "git",
|
|
3437
|
+
url: "git+https://github.com/KernelEditor/KitCode.git"
|
|
3438
|
+
},
|
|
3023
3439
|
bin: {
|
|
3024
3440
|
kitcode: "dist/index.js"
|
|
3025
3441
|
},
|
|
@@ -3075,8 +3491,8 @@ var package_default = {
|
|
|
3075
3491
|
|
|
3076
3492
|
// src/version.ts
|
|
3077
3493
|
var KITCODE_VERSION = package_default.version;
|
|
3078
|
-
var KITCODE_REPOSITORY = "
|
|
3079
|
-
var KITCODE_COMMIT = true ? "
|
|
3494
|
+
var KITCODE_REPOSITORY = "KernelEditor/KitCode";
|
|
3495
|
+
var KITCODE_COMMIT = true ? "c93b03ff85112af7b4c28f543aff6b9377f84ad6" : "development";
|
|
3080
3496
|
|
|
3081
3497
|
// src/mcp/client.ts
|
|
3082
3498
|
var clientInfo = { name: "kitcode", version: KITCODE_VERSION };
|
|
@@ -3098,7 +3514,41 @@ var inheritedEnvKeys = /* @__PURE__ */ new Set([
|
|
|
3098
3514
|
"COMSPEC",
|
|
3099
3515
|
"PATHEXT",
|
|
3100
3516
|
"APPDATA",
|
|
3101
|
-
"LOCALAPPDATA"
|
|
3517
|
+
"LOCALAPPDATA",
|
|
3518
|
+
"USERPROFILE",
|
|
3519
|
+
"HOMEDRIVE",
|
|
3520
|
+
"HOMEPATH",
|
|
3521
|
+
"USERNAME"
|
|
3522
|
+
]);
|
|
3523
|
+
var blockedEnvKeys = /* @__PURE__ */ new Set([
|
|
3524
|
+
"ANTHROPIC_API_KEY",
|
|
3525
|
+
"OPENAI_API_KEY",
|
|
3526
|
+
"AWS_ACCESS_KEY_ID",
|
|
3527
|
+
"AWS_SECRET_ACCESS_KEY",
|
|
3528
|
+
"AWS_SESSION_TOKEN",
|
|
3529
|
+
"GITHUB_TOKEN",
|
|
3530
|
+
"GH_TOKEN",
|
|
3531
|
+
"GIT_TOKEN",
|
|
3532
|
+
"NPM_TOKEN",
|
|
3533
|
+
"PYPI_TOKEN",
|
|
3534
|
+
"TWINE_PASSWORD",
|
|
3535
|
+
"DOCKER_PASSWORD",
|
|
3536
|
+
"DATABASE_URL",
|
|
3537
|
+
"REDIS_URL",
|
|
3538
|
+
"MONGODB_URI",
|
|
3539
|
+
"POSTGRES_PASSWORD",
|
|
3540
|
+
"MYSQL_PASSWORD",
|
|
3541
|
+
"PRIVATE_KEY",
|
|
3542
|
+
"SSH_KEY",
|
|
3543
|
+
"GPG_KEY",
|
|
3544
|
+
"SIGNING_KEY",
|
|
3545
|
+
"COOKIE",
|
|
3546
|
+
"SESSION_SECRET",
|
|
3547
|
+
"JWT_SECRET",
|
|
3548
|
+
"ENCRYPTION_KEY",
|
|
3549
|
+
"MASTER_KEY",
|
|
3550
|
+
"API_KEY",
|
|
3551
|
+
"API_SECRET"
|
|
3102
3552
|
]);
|
|
3103
3553
|
function createMcpManager(servers) {
|
|
3104
3554
|
const serverStates = /* @__PURE__ */ new Map();
|
|
@@ -3245,8 +3695,13 @@ function configuredSecrets(config) {
|
|
|
3245
3695
|
}
|
|
3246
3696
|
function inheritedEnv(source = process.env) {
|
|
3247
3697
|
const env = {};
|
|
3698
|
+
const allowed = new Set([...inheritedEnvKeys].map((key) => key.toUpperCase()));
|
|
3699
|
+
const blocked = new Set([...blockedEnvKeys].map((key) => key.toUpperCase()));
|
|
3248
3700
|
for (const [key, value] of Object.entries(source)) {
|
|
3249
|
-
|
|
3701
|
+
const normalized = key.toUpperCase();
|
|
3702
|
+
if (value !== void 0 && allowed.has(normalized) && !blocked.has(normalized)) {
|
|
3703
|
+
env[key] = value;
|
|
3704
|
+
}
|
|
3250
3705
|
}
|
|
3251
3706
|
return env;
|
|
3252
3707
|
}
|
|
@@ -3278,7 +3733,10 @@ async function fetchProviderBalance(config, apiKey2, options = {}) {
|
|
|
3278
3733
|
},
|
|
3279
3734
|
signal: controller.signal
|
|
3280
3735
|
});
|
|
3281
|
-
if (!response.ok)
|
|
3736
|
+
if (!response.ok) {
|
|
3737
|
+
await response.body?.cancel().catch(() => void 0);
|
|
3738
|
+
continue;
|
|
3739
|
+
}
|
|
3282
3740
|
const text = await limitedResponseText(response, MAX_RESPONSE_BYTES);
|
|
3283
3741
|
if (text === null) continue;
|
|
3284
3742
|
const balance = endpoint2.parse(JSON.parse(text));
|
|
@@ -3393,7 +3851,10 @@ function finiteNumber(value) {
|
|
|
3393
3851
|
}
|
|
3394
3852
|
async function limitedResponseText(response, limit) {
|
|
3395
3853
|
const declared = Number(response.headers.get("content-length"));
|
|
3396
|
-
if (Number.isFinite(declared) && declared > limit)
|
|
3854
|
+
if (Number.isFinite(declared) && declared > limit) {
|
|
3855
|
+
await response.body?.cancel().catch(() => void 0);
|
|
3856
|
+
return null;
|
|
3857
|
+
}
|
|
3397
3858
|
if (!response.body) return "";
|
|
3398
3859
|
const reader = response.body.getReader();
|
|
3399
3860
|
const decoder = new TextDecoder();
|
|
@@ -3443,11 +3904,34 @@ function cacheFile(providerId) {
|
|
|
3443
3904
|
}
|
|
3444
3905
|
async function readCache(file) {
|
|
3445
3906
|
try {
|
|
3446
|
-
|
|
3907
|
+
const parsed = JSON.parse(await readFile7(file, "utf8"));
|
|
3908
|
+
if (typeof parsed !== "object" || parsed === null) return void 0;
|
|
3909
|
+
const candidate = parsed;
|
|
3910
|
+
if (typeof candidate.fetchedAt !== "number" || !Number.isFinite(candidate.fetchedAt) || !Array.isArray(candidate.models) || candidate.models.length > 1e4 || !candidate.models.every(validModel)) {
|
|
3911
|
+
return void 0;
|
|
3912
|
+
}
|
|
3913
|
+
return candidate;
|
|
3447
3914
|
} catch {
|
|
3448
3915
|
return void 0;
|
|
3449
3916
|
}
|
|
3450
3917
|
}
|
|
3918
|
+
function validModel(value) {
|
|
3919
|
+
if (typeof value !== "object" || value === null) return false;
|
|
3920
|
+
const model = value;
|
|
3921
|
+
if (typeof model.id !== "string" || model.id === "") return false;
|
|
3922
|
+
if (model.name !== void 0 && typeof model.name !== "string") return false;
|
|
3923
|
+
if (model.contextWindow !== void 0 && (!Number.isInteger(model.contextWindow) || model.contextWindow < 1)) {
|
|
3924
|
+
return false;
|
|
3925
|
+
}
|
|
3926
|
+
if (model.pricing === void 0) return true;
|
|
3927
|
+
const prices = [
|
|
3928
|
+
model.pricing.input,
|
|
3929
|
+
model.pricing.output,
|
|
3930
|
+
model.pricing.cacheWrite,
|
|
3931
|
+
model.pricing.cacheRead
|
|
3932
|
+
].filter((price2) => price2 !== void 0);
|
|
3933
|
+
return prices.length >= 2 && prices.every((price2) => Number.isFinite(price2) && price2 >= 0);
|
|
3934
|
+
}
|
|
3451
3935
|
async function writeCache(file, models) {
|
|
3452
3936
|
const payload = { version: CACHE_VERSION, fetchedAt: Date.now(), models };
|
|
3453
3937
|
try {
|
|
@@ -4071,7 +4555,7 @@ function isFileEdit(tool) {
|
|
|
4071
4555
|
}
|
|
4072
4556
|
|
|
4073
4557
|
// src/tools/edit.ts
|
|
4074
|
-
import { readFile as readFile8, stat as stat6, writeFile as writeFile6 } from "fs/promises";
|
|
4558
|
+
import { lstat as lstat3, readFile as readFile8, stat as stat6, writeFile as writeFile6 } from "fs/promises";
|
|
4075
4559
|
var MAX_FILE_BYTES2 = 5e6;
|
|
4076
4560
|
function countOccurrences(haystack, needle) {
|
|
4077
4561
|
if (needle === "") return 0;
|
|
@@ -4127,6 +4611,10 @@ var editTool = {
|
|
|
4127
4611
|
const { path: path14, oldString, newString, replaceAll = false } = input;
|
|
4128
4612
|
const safe = resolveInside(ctx.cwd, path14);
|
|
4129
4613
|
if (!safe.ok) return { content: safe.reason, isError: true };
|
|
4614
|
+
const linkInfo = await lstat3(safe.path).catch(() => null);
|
|
4615
|
+
if (linkInfo?.isSymbolicLink()) {
|
|
4616
|
+
return { content: `Cannot edit ${path14}: the path is a symbolic link. Remove the symlink first.`, isError: true };
|
|
4617
|
+
}
|
|
4130
4618
|
const info = await stat6(safe.path).catch(() => null);
|
|
4131
4619
|
if (info && info.size > MAX_FILE_BYTES2) {
|
|
4132
4620
|
return {
|
|
@@ -4244,13 +4732,25 @@ var sensitiveNames = /* @__PURE__ */ new Set([
|
|
|
4244
4732
|
".npmrc",
|
|
4245
4733
|
".pypirc",
|
|
4246
4734
|
".git-credentials",
|
|
4735
|
+
".netrc",
|
|
4736
|
+
".pgpass",
|
|
4737
|
+
".my.cnf",
|
|
4738
|
+
".docker/config.json",
|
|
4247
4739
|
"auth.json",
|
|
4248
4740
|
"credentials",
|
|
4249
4741
|
"credentials.json",
|
|
4250
4742
|
"id_rsa",
|
|
4251
|
-
"id_ed25519"
|
|
4743
|
+
"id_ed25519",
|
|
4744
|
+
"id_dsa",
|
|
4745
|
+
"id_ecdsa",
|
|
4746
|
+
"kubeconfig",
|
|
4747
|
+
"known_hosts",
|
|
4748
|
+
"secret.json",
|
|
4749
|
+
"secrets.json",
|
|
4750
|
+
"tokens.json",
|
|
4751
|
+
"service-account.json"
|
|
4252
4752
|
]);
|
|
4253
|
-
var sensitiveExtensions = /* @__PURE__ */ new Set([".pem", ".key", ".p12", ".pfx"]);
|
|
4753
|
+
var sensitiveExtensions = /* @__PURE__ */ new Set([".pem", ".key", ".p12", ".pfx", ".jks", ".keystore", ".p8", ".der"]);
|
|
4254
4754
|
function isSensitivePath(value) {
|
|
4255
4755
|
if (typeof value !== "string") return false;
|
|
4256
4756
|
const normalized = value.replaceAll("\\", "/").toLowerCase();
|
|
@@ -4262,7 +4762,7 @@ function isSensitivePath(value) {
|
|
|
4262
4762
|
function mentionsSensitivePattern(value) {
|
|
4263
4763
|
if (typeof value !== "string") return false;
|
|
4264
4764
|
const lower = value.toLowerCase();
|
|
4265
|
-
return isSensitivePath(lower) || lower.includes(".env") || lower.includes(".npmrc") || lower.includes("auth.json") || /\.(?:pem|key|p12|pfx)(?:$|[^a-z0-9])/.test(lower);
|
|
4765
|
+
return isSensitivePath(lower) || lower.includes(".env") || lower.includes(".npmrc") || lower.includes(".netrc") || lower.includes(".pgpass") || lower.includes("auth.json") || lower.includes("credentials") || lower.includes("kubeconfig") || lower.includes("secret") || lower.includes("tokens.json") || /\.(?:pem|key|p12|pfx|jks|keystore|p8|der)(?:$|[^a-z0-9])/.test(lower);
|
|
4266
4766
|
}
|
|
4267
4767
|
|
|
4268
4768
|
// src/tools/regex.ts
|
|
@@ -4539,7 +5039,7 @@ function toToolSchema(tool) {
|
|
|
4539
5039
|
}
|
|
4540
5040
|
|
|
4541
5041
|
// src/tools/write.ts
|
|
4542
|
-
import { mkdir as
|
|
5042
|
+
import { lstat as lstat4, mkdir as mkdir4, readFile as readFile11, stat as stat9, writeFile as writeFile7 } from "fs/promises";
|
|
4543
5043
|
import { dirname as dirname2 } from "path";
|
|
4544
5044
|
var MAX_FILE_BYTES5 = 5e6;
|
|
4545
5045
|
var writeTool = {
|
|
@@ -4590,6 +5090,10 @@ var writeTool = {
|
|
|
4590
5090
|
}
|
|
4591
5091
|
const safe = resolveInside(ctx.cwd, path14);
|
|
4592
5092
|
if (!safe.ok) return { content: safe.reason, isError: true };
|
|
5093
|
+
const linkInfo = await lstat4(safe.path).catch(() => null);
|
|
5094
|
+
if (linkInfo?.isSymbolicLink()) {
|
|
5095
|
+
return { content: `Cannot write ${path14}: the path is a symbolic link. Remove the symlink first.`, isError: true };
|
|
5096
|
+
}
|
|
4593
5097
|
const beforeInfo = await stat9(safe.path).catch(() => null);
|
|
4594
5098
|
if (beforeInfo && beforeInfo.size > MAX_FILE_BYTES5) {
|
|
4595
5099
|
return {
|
|
@@ -4604,7 +5108,7 @@ var writeTool = {
|
|
|
4604
5108
|
let before = "";
|
|
4605
5109
|
if (beforeBuffer) before = beforeBuffer.toString("utf8");
|
|
4606
5110
|
try {
|
|
4607
|
-
await
|
|
5111
|
+
await mkdir4(dirname2(safe.path), { recursive: true });
|
|
4608
5112
|
await ctx.checkpoint?.capture(safe.path);
|
|
4609
5113
|
await writeFile7(safe.path, content, "utf8");
|
|
4610
5114
|
ctx.checkpoint?.markChanged(safe.path);
|
|
@@ -4821,11 +5325,22 @@ function parseFrontmatter(text) {
|
|
|
4821
5325
|
}
|
|
4822
5326
|
|
|
4823
5327
|
// src/skills/install.ts
|
|
5328
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
4824
5329
|
import { execFileSync } from "child_process";
|
|
4825
|
-
import {
|
|
4826
|
-
|
|
5330
|
+
import {
|
|
5331
|
+
existsSync as existsSync2,
|
|
5332
|
+
lstatSync,
|
|
5333
|
+
mkdirSync,
|
|
5334
|
+
readFileSync,
|
|
5335
|
+
readdirSync,
|
|
5336
|
+
realpathSync as realpathSync2,
|
|
5337
|
+
rmSync,
|
|
5338
|
+
writeFileSync
|
|
5339
|
+
} from "fs";
|
|
5340
|
+
import { chmod as chmod6, mkdir as mkdir5 } from "fs/promises";
|
|
4827
5341
|
import path12 from "path";
|
|
4828
5342
|
var TMP_DIR = path12.join(skillsDir, ".tmp");
|
|
5343
|
+
var MAX_SKILL_BYTES2 = 5e6;
|
|
4829
5344
|
async function installSkill(source) {
|
|
4830
5345
|
await ensureDir(skillsDir);
|
|
4831
5346
|
if (isGitHubUrl(source)) {
|
|
@@ -4837,30 +5352,31 @@ async function installSkill(source) {
|
|
|
4837
5352
|
return installFromLocal(source);
|
|
4838
5353
|
}
|
|
4839
5354
|
function isGitHubUrl(input) {
|
|
4840
|
-
|
|
5355
|
+
try {
|
|
5356
|
+
const url = new URL(input);
|
|
5357
|
+
return url.protocol === "https:" && (url.hostname === "github.com" || url.hostname === "www.github.com");
|
|
5358
|
+
} catch {
|
|
5359
|
+
return false;
|
|
5360
|
+
}
|
|
4841
5361
|
}
|
|
4842
5362
|
function isNpmPackage(input) {
|
|
4843
|
-
|
|
4844
|
-
return /^(@[\w.-]+\/)?[\w.-]+(@[\w.*+-]+)?$/.test(input);
|
|
5363
|
+
return npmSkillName(input) !== null;
|
|
4845
5364
|
}
|
|
4846
5365
|
async function installFromGitHub(url) {
|
|
4847
5366
|
const { owner, repo, subdir, branch } = parseGitHubUrl(url);
|
|
4848
|
-
const name = subdir
|
|
4849
|
-
const skillDir =
|
|
4850
|
-
const tmpDir =
|
|
5367
|
+
const name = safeSkillName(subdir ? path12.posix.basename(subdir) : repo);
|
|
5368
|
+
const skillDir = safeChildPath(skillsDir, name);
|
|
5369
|
+
const tmpDir = safeChildPath(TMP_DIR, `${name}-${randomUUID3()}`);
|
|
4851
5370
|
try {
|
|
4852
|
-
rmSync(skillDir, { recursive: true, force: true });
|
|
4853
5371
|
mkdirSync(tmpDir, { recursive: true });
|
|
4854
5372
|
const cloneUrl = `https://github.com/${owner}/${repo}.git`;
|
|
4855
5373
|
const gitArgs = ["clone", "--depth", "1"];
|
|
4856
5374
|
if (branch) gitArgs.push("--branch", branch);
|
|
4857
5375
|
gitArgs.push(cloneUrl, tmpDir);
|
|
4858
5376
|
execFileSync("git", gitArgs, { stdio: "ignore" });
|
|
4859
|
-
const skillFile = subdir ?
|
|
4860
|
-
|
|
4861
|
-
|
|
4862
|
-
}
|
|
4863
|
-
const body = readFileSync(skillFile, "utf8");
|
|
5377
|
+
const skillFile = subdir ? safeChildPath(tmpDir, ...subdir.split("/"), "SKILL.md") : safeChildPath(tmpDir, "SKILL.md");
|
|
5378
|
+
const body = readSkillFile(skillFile, tmpDir, subdir || repo);
|
|
5379
|
+
removeExistingSkillDir(skillDir);
|
|
4864
5380
|
await writeSkillFile(skillDir, body);
|
|
4865
5381
|
return { name, dir: skillDir, source: url };
|
|
4866
5382
|
} finally {
|
|
@@ -4868,9 +5384,11 @@ async function installFromGitHub(url) {
|
|
|
4868
5384
|
}
|
|
4869
5385
|
}
|
|
4870
5386
|
async function installFromNpm(packageName) {
|
|
4871
|
-
const
|
|
4872
|
-
|
|
4873
|
-
const
|
|
5387
|
+
const parsedName = npmSkillName(packageName);
|
|
5388
|
+
if (!parsedName) throw new Error(`Invalid npm package name: ${packageName}`);
|
|
5389
|
+
const name = safeSkillName(parsedName);
|
|
5390
|
+
const skillDir = safeChildPath(skillsDir, name);
|
|
5391
|
+
const tmpDir = safeChildPath(TMP_DIR, `${name}-${randomUUID3()}`);
|
|
4874
5392
|
try {
|
|
4875
5393
|
mkdirSync(tmpDir, { recursive: true });
|
|
4876
5394
|
execFileSync("npm", ["pack", packageName, "--prefix", tmpDir], {
|
|
@@ -4882,6 +5400,7 @@ async function installFromNpm(packageName) {
|
|
|
4882
5400
|
throw new Error(`Could not download npm package "${packageName}"`);
|
|
4883
5401
|
}
|
|
4884
5402
|
const tarballPath = path12.join(tmpDir, tarball);
|
|
5403
|
+
validateTarball(tarballPath);
|
|
4885
5404
|
const extractDir = path12.join(tmpDir, "extracted");
|
|
4886
5405
|
mkdirSync(extractDir, { recursive: true });
|
|
4887
5406
|
try {
|
|
@@ -4891,7 +5410,7 @@ async function installFromNpm(packageName) {
|
|
|
4891
5410
|
if (existsSync2(packageDir)) {
|
|
4892
5411
|
const skillFile2 = path12.join(packageDir, "SKILL.md");
|
|
4893
5412
|
if (existsSync2(skillFile2)) {
|
|
4894
|
-
const body2 =
|
|
5413
|
+
const body2 = readSkillFile(skillFile2, packageDir, packageName);
|
|
4895
5414
|
await writeSkillFile(skillDir, body2);
|
|
4896
5415
|
return { name, dir: skillDir, source: packageName };
|
|
4897
5416
|
}
|
|
@@ -4905,13 +5424,41 @@ async function installFromNpm(packageName) {
|
|
|
4905
5424
|
if (!existsSync2(skillFile)) {
|
|
4906
5425
|
throw new Error(`No SKILL.md found in npm package "${packageName}"`);
|
|
4907
5426
|
}
|
|
4908
|
-
const body =
|
|
5427
|
+
const body = readSkillFile(skillFile, extractDir, packageName);
|
|
4909
5428
|
await writeSkillFile(skillDir, body);
|
|
4910
5429
|
return { name, dir: skillDir, source: packageName };
|
|
4911
5430
|
} finally {
|
|
4912
5431
|
rmSync(tmpDir, { recursive: true, force: true });
|
|
4913
5432
|
}
|
|
4914
5433
|
}
|
|
5434
|
+
function npmSkillName(input) {
|
|
5435
|
+
if (input.includes("\\") || input.startsWith(".")) return null;
|
|
5436
|
+
const scoped = input.match(/^@[\w.-]+\/([\w.-]+)(?:@[\w.*+-]+)?$/);
|
|
5437
|
+
if (scoped) return scoped[1] ?? null;
|
|
5438
|
+
const unscoped = input.match(/^([\w.-]+)(?:@[\w.*+-]+)?$/);
|
|
5439
|
+
return unscoped?.[1] ?? null;
|
|
5440
|
+
}
|
|
5441
|
+
function validateTarball(file) {
|
|
5442
|
+
const info = lstatSync(file, { throwIfNoEntry: false });
|
|
5443
|
+
if (!info?.isFile() || info.size > 2e7) {
|
|
5444
|
+
throw new Error("The npm skill archive is missing or exceeds the 20 MB safety limit.");
|
|
5445
|
+
}
|
|
5446
|
+
const listing = execFileSync("tar", ["-tzf", file], {
|
|
5447
|
+
encoding: "utf8",
|
|
5448
|
+
maxBuffer: 2e6,
|
|
5449
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
5450
|
+
});
|
|
5451
|
+
const entries = listing.split(/\r?\n/).filter(Boolean);
|
|
5452
|
+
if (entries.length === 0 || entries.length > 1e4) {
|
|
5453
|
+
throw new Error("The npm skill archive has an invalid number of entries.");
|
|
5454
|
+
}
|
|
5455
|
+
for (const entry of entries) {
|
|
5456
|
+
const normalized = entry.replace(/^\.\//, "");
|
|
5457
|
+
if (normalized.includes("\\") || path12.posix.isAbsolute(normalized) || normalized.split("/").some((segment) => segment === "..")) {
|
|
5458
|
+
throw new Error(`Unsafe path in npm skill archive: ${entry}`);
|
|
5459
|
+
}
|
|
5460
|
+
}
|
|
5461
|
+
}
|
|
4915
5462
|
async function installFromLocal(srcPath) {
|
|
4916
5463
|
const resolved = path12.resolve(srcPath);
|
|
4917
5464
|
let skillDir;
|
|
@@ -4927,29 +5474,102 @@ async function installFromLocal(srcPath) {
|
|
|
4927
5474
|
throw new Error(`No SKILL.md found at "${srcPath}"`);
|
|
4928
5475
|
}
|
|
4929
5476
|
const name = path12.basename(skillDir);
|
|
4930
|
-
const destDir =
|
|
4931
|
-
const body =
|
|
5477
|
+
const destDir = safeChildPath(skillsDir, safeSkillName(name));
|
|
5478
|
+
const body = readSkillFile(skillFile, skillDir, srcPath);
|
|
4932
5479
|
await writeSkillFile(destDir, body);
|
|
4933
5480
|
return { name, dir: destDir, source: resolved };
|
|
4934
5481
|
}
|
|
4935
5482
|
async function writeSkillFile(dir, body) {
|
|
4936
|
-
|
|
5483
|
+
assertInside(skillsDir, dir);
|
|
5484
|
+
const existingDir = lstatSync(dir, { throwIfNoEntry: false });
|
|
5485
|
+
if (existingDir?.isSymbolicLink()) {
|
|
5486
|
+
throw new Error(`Refusing to replace symlinked skill directory: ${dir}`);
|
|
5487
|
+
}
|
|
5488
|
+
await mkdir5(dir, { recursive: true, mode: 448 });
|
|
4937
5489
|
const file = path12.join(dir, "SKILL.md");
|
|
4938
|
-
|
|
5490
|
+
const existingFile = lstatSync(file, { throwIfNoEntry: false });
|
|
5491
|
+
if (existingFile?.isSymbolicLink()) {
|
|
5492
|
+
throw new Error(`Refusing to replace symlinked skill file: ${file}`);
|
|
5493
|
+
}
|
|
5494
|
+
writeFileSync(file, body, { encoding: "utf8", mode: 384 });
|
|
4939
5495
|
await chmod6(file, 384);
|
|
4940
5496
|
}
|
|
4941
5497
|
function parseGitHubUrl(url) {
|
|
4942
|
-
|
|
4943
|
-
|
|
4944
|
-
|
|
4945
|
-
|
|
4946
|
-
const simple = url.match(/^https?:\/\/(?:www\.)?github\.com\/([^/]+)\/([^/]+)/);
|
|
4947
|
-
if (simple) {
|
|
4948
|
-
return { owner: simple[1], repo: simple[2] };
|
|
4949
|
-
}
|
|
5498
|
+
let parsed;
|
|
5499
|
+
try {
|
|
5500
|
+
parsed = new URL(url);
|
|
5501
|
+
} catch {
|
|
4950
5502
|
throw new Error(`Cannot parse GitHub URL: ${url}`);
|
|
4951
5503
|
}
|
|
4952
|
-
|
|
5504
|
+
if (parsed.protocol !== "https:" || parsed.hostname !== "github.com" && parsed.hostname !== "www.github.com" || parsed.username || parsed.password) {
|
|
5505
|
+
throw new Error("GitHub skills must use an https://github.com URL without credentials.");
|
|
5506
|
+
}
|
|
5507
|
+
let segments;
|
|
5508
|
+
try {
|
|
5509
|
+
segments = parsed.pathname.split("/").filter(Boolean).map((segment) => decodeURIComponent(segment));
|
|
5510
|
+
} catch {
|
|
5511
|
+
throw new Error(`Cannot parse GitHub URL: ${url}`);
|
|
5512
|
+
}
|
|
5513
|
+
if (segments.length < 2) throw new Error(`Cannot parse GitHub URL: ${url}`);
|
|
5514
|
+
const owner = safeGitHubSegment(segments[0], "owner");
|
|
5515
|
+
const repo = safeSkillName(segments[1].replace(/\.git$/i, ""));
|
|
5516
|
+
if (segments.length === 2) return { owner, repo };
|
|
5517
|
+
if (segments[2] !== "tree" || segments.length < 4) {
|
|
5518
|
+
throw new Error(`Unsupported GitHub skill URL: ${url}`);
|
|
5519
|
+
}
|
|
5520
|
+
const branch = safeGitHubSegment(segments[3], "branch");
|
|
5521
|
+
const subdirSegments = segments.slice(4).map((segment) => safeGitHubSegment(segment, "path"));
|
|
5522
|
+
return {
|
|
5523
|
+
owner,
|
|
5524
|
+
repo,
|
|
5525
|
+
branch,
|
|
5526
|
+
subdir: subdirSegments.length > 0 ? subdirSegments.join("/") : void 0
|
|
5527
|
+
};
|
|
5528
|
+
}
|
|
5529
|
+
function safeGitHubSegment(value, kind) {
|
|
5530
|
+
if (value === "" || value === "." || value === ".." || value.length > 255 || /[\0-\x1f\x7f/\\:*?"<>|#%]/.test(value)) {
|
|
5531
|
+
throw new Error(`Unsafe GitHub ${kind}: ${value || "(empty)"}`);
|
|
5532
|
+
}
|
|
5533
|
+
return value;
|
|
5534
|
+
}
|
|
5535
|
+
function safeSkillName(value) {
|
|
5536
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(value) || value === "." || value === "..") {
|
|
5537
|
+
throw new Error(`Unsafe skill name: ${value || "(empty)"}`);
|
|
5538
|
+
}
|
|
5539
|
+
return value;
|
|
5540
|
+
}
|
|
5541
|
+
function safeChildPath(root, ...segments) {
|
|
5542
|
+
const target = path12.resolve(root, ...segments);
|
|
5543
|
+
assertInside(root, target);
|
|
5544
|
+
return target;
|
|
5545
|
+
}
|
|
5546
|
+
function assertInside(root, target) {
|
|
5547
|
+
const relative2 = path12.relative(path12.resolve(root), path12.resolve(target));
|
|
5548
|
+
if (relative2 === "" || !path12.isAbsolute(relative2) && relative2 !== ".." && !relative2.startsWith(`..${path12.sep}`)) {
|
|
5549
|
+
return;
|
|
5550
|
+
}
|
|
5551
|
+
throw new Error(`Path escapes the skills directory: ${target}`);
|
|
5552
|
+
}
|
|
5553
|
+
function readSkillFile(file, allowedRoot, source) {
|
|
5554
|
+
const info = lstatSync(file, { throwIfNoEntry: false });
|
|
5555
|
+
if (!info?.isFile() || info.isSymbolicLink()) {
|
|
5556
|
+
throw new Error(`No regular SKILL.md found in "${source}"`);
|
|
5557
|
+
}
|
|
5558
|
+
if (info.size > MAX_SKILL_BYTES2) {
|
|
5559
|
+
throw new Error(`SKILL.md in "${source}" exceeds the ${MAX_SKILL_BYTES2 / 1e6} MB limit`);
|
|
5560
|
+
}
|
|
5561
|
+
const root = realpathSync2(allowedRoot);
|
|
5562
|
+
const resolved = realpathSync2(file);
|
|
5563
|
+
assertInside(root, resolved);
|
|
5564
|
+
return readFileSync(resolved, "utf8");
|
|
5565
|
+
}
|
|
5566
|
+
function removeExistingSkillDir(dir) {
|
|
5567
|
+
const info = lstatSync(dir, { throwIfNoEntry: false });
|
|
5568
|
+
if (!info) return;
|
|
5569
|
+
if (info.isSymbolicLink()) {
|
|
5570
|
+
throw new Error(`Refusing to remove symlinked skill directory: ${dir}`);
|
|
5571
|
+
}
|
|
5572
|
+
rmSync(dir, { recursive: true, force: true });
|
|
4953
5573
|
}
|
|
4954
5574
|
|
|
4955
5575
|
// src/tools/skill.ts
|
|
@@ -5091,6 +5711,7 @@ async function checkForUpdates(fetcher = fetch, currentCommit = KITCODE_COMMIT)
|
|
|
5091
5711
|
signal: controller.signal
|
|
5092
5712
|
});
|
|
5093
5713
|
if (!response.ok) {
|
|
5714
|
+
await response.body?.cancel().catch(() => void 0);
|
|
5094
5715
|
return { status: "unknown", reason: `GitHub returned ${response.status}` };
|
|
5095
5716
|
}
|
|
5096
5717
|
const payload = await readJsonBounded(response);
|
|
@@ -5116,7 +5737,10 @@ async function checkForUpdates(fetcher = fetch, currentCommit = KITCODE_COMMIT)
|
|
|
5116
5737
|
}
|
|
5117
5738
|
async function readJsonBounded(response) {
|
|
5118
5739
|
const declared = Number(response.headers.get("content-length"));
|
|
5119
|
-
if (Number.isFinite(declared) && declared > MAX_RESPONSE_BYTES2)
|
|
5740
|
+
if (Number.isFinite(declared) && declared > MAX_RESPONSE_BYTES2) {
|
|
5741
|
+
await response.body?.cancel().catch(() => void 0);
|
|
5742
|
+
return null;
|
|
5743
|
+
}
|
|
5120
5744
|
if (!response.body) return null;
|
|
5121
5745
|
const reader = response.body.getReader();
|
|
5122
5746
|
const chunks = [];
|
|
@@ -5201,7 +5825,7 @@ async function boot(options) {
|
|
|
5201
5825
|
session = (options.continueSession ? await latestSessionFor(options.cwd) : null) ?? createSession(options.cwd, config.model ?? "");
|
|
5202
5826
|
}
|
|
5203
5827
|
const usage = createUsageTracker(session.usage, resolvePricing);
|
|
5204
|
-
|
|
5828
|
+
let skillCatalogue = formatSkillCatalogue(skills);
|
|
5205
5829
|
const mainSystemPrompt = () => buildSystemPrompt({
|
|
5206
5830
|
cwd: options.cwd,
|
|
5207
5831
|
toolNames: tools.list().map((tool) => tool.name),
|
|
@@ -5308,26 +5932,39 @@ async function boot(options) {
|
|
|
5308
5932
|
createTaskTool(wrappedRunner, config.budget.maxSubagentsPerTurn)
|
|
5309
5933
|
]);
|
|
5310
5934
|
let persistQueue = Promise.resolve();
|
|
5311
|
-
|
|
5312
|
-
|
|
5313
|
-
|
|
5935
|
+
let configQueue = Promise.resolve();
|
|
5936
|
+
const persistConfig = (mutate) => {
|
|
5937
|
+
const save = configQueue.catch(() => void 0).then(async () => {
|
|
5938
|
+
const before = structuredClone(config);
|
|
5939
|
+
mutate(config);
|
|
5940
|
+
try {
|
|
5941
|
+
await saveConfig(config);
|
|
5942
|
+
} catch (error) {
|
|
5943
|
+
Object.assign(config, before);
|
|
5944
|
+
throw error;
|
|
5945
|
+
}
|
|
5946
|
+
});
|
|
5947
|
+
configQueue = save;
|
|
5948
|
+
return save;
|
|
5314
5949
|
};
|
|
5315
5950
|
const compactWithBudget = async (history, signal, budget) => {
|
|
5316
5951
|
if (compactCutIndex(history) <= 0) {
|
|
5317
5952
|
return { history, compacted: false, removedMessages: 0 };
|
|
5318
5953
|
}
|
|
5319
5954
|
const resolved = registry.resolve(modelRef);
|
|
5955
|
+
const estimate = estimateCompactBudget(history, config.maxTokens);
|
|
5320
5956
|
const budgetDecision = budget.beforeRequest({
|
|
5321
5957
|
modelRef,
|
|
5322
|
-
maxOutputTokens:
|
|
5323
|
-
estimatedInputTokens:
|
|
5958
|
+
maxOutputTokens: estimate.maxOutputTokens,
|
|
5959
|
+
estimatedInputTokens: estimate.inputTokens
|
|
5324
5960
|
});
|
|
5325
5961
|
if (!budgetDecision.allowed) throw new Error(budgetDecision.reason);
|
|
5326
5962
|
const result = await compactHistory({
|
|
5327
5963
|
provider: resolved.provider,
|
|
5328
5964
|
model: resolved.modelId,
|
|
5329
5965
|
history,
|
|
5330
|
-
maxTokens:
|
|
5966
|
+
maxTokens: config.maxTokens,
|
|
5967
|
+
maxTotalOutputTokens: budgetDecision.maxOutputTokens,
|
|
5331
5968
|
signal
|
|
5332
5969
|
});
|
|
5333
5970
|
if (result.usage) {
|
|
@@ -5348,12 +5985,22 @@ async function boot(options) {
|
|
|
5348
5985
|
async addProvider(url, key) {
|
|
5349
5986
|
const detected = await detectProvider(url, key);
|
|
5350
5987
|
rememberModels(detected.id, detected.models);
|
|
5988
|
+
const configBefore = structuredClone(config);
|
|
5989
|
+
const authBefore = { ...auth };
|
|
5351
5990
|
config.providers[detected.id] = detected.config;
|
|
5352
5991
|
auth[detected.id] = key;
|
|
5353
5992
|
const chosen = preferredModel(detected.models);
|
|
5354
5993
|
if (chosen) config.model = formatModelRef(detected.id, chosen);
|
|
5355
|
-
|
|
5356
|
-
|
|
5994
|
+
try {
|
|
5995
|
+
await saveConfig(config);
|
|
5996
|
+
await saveAuth(auth);
|
|
5997
|
+
} catch (error) {
|
|
5998
|
+
Object.assign(config, configBefore);
|
|
5999
|
+
for (const id of Object.keys(auth)) delete auth[id];
|
|
6000
|
+
Object.assign(auth, authBefore);
|
|
6001
|
+
await Promise.allSettled([saveConfig(config), saveAuth(auth)]);
|
|
6002
|
+
throw error;
|
|
6003
|
+
}
|
|
5357
6004
|
registry = createRegistry(config, auth);
|
|
5358
6005
|
const nextRef = config.model ?? "";
|
|
5359
6006
|
const discovered = detected.models.find((model) => model.id === chosen)?.contextWindow;
|
|
@@ -5439,6 +6086,37 @@ async function boot(options) {
|
|
|
5439
6086
|
}
|
|
5440
6087
|
return { removed: providerId, wasActive, ...nextModel ? { nextModel } : {} };
|
|
5441
6088
|
},
|
|
6089
|
+
async changeProviderKey(providerId, newKey) {
|
|
6090
|
+
const provider = config.providers[providerId];
|
|
6091
|
+
if (!provider) throw new Error(`Provider "${providerId}" is not configured.`);
|
|
6092
|
+
const previousKey = auth[providerId];
|
|
6093
|
+
const restoreKey = () => {
|
|
6094
|
+
if (previousKey === void 0) delete auth[providerId];
|
|
6095
|
+
else auth[providerId] = previousKey;
|
|
6096
|
+
};
|
|
6097
|
+
auth[providerId] = newKey;
|
|
6098
|
+
try {
|
|
6099
|
+
const testRegistry = createRegistry(config, auth);
|
|
6100
|
+
await testRegistry.get(providerId).listModels();
|
|
6101
|
+
} catch (error) {
|
|
6102
|
+
restoreKey();
|
|
6103
|
+
throw new Error(
|
|
6104
|
+
`Key verification failed for "${providerId}": ${error instanceof Error ? error.message : String(error)}`
|
|
6105
|
+
);
|
|
6106
|
+
}
|
|
6107
|
+
try {
|
|
6108
|
+
await saveAuth(auth);
|
|
6109
|
+
} catch (error) {
|
|
6110
|
+
restoreKey();
|
|
6111
|
+
throw error;
|
|
6112
|
+
}
|
|
6113
|
+
registry = createRegistry(config, auth);
|
|
6114
|
+
if (parseModelRef(modelRef)?.provider === providerId) {
|
|
6115
|
+
const models = await loadModels(registry.get(providerId));
|
|
6116
|
+
rememberModels(providerId, models);
|
|
6117
|
+
void refreshModelContextWindow();
|
|
6118
|
+
}
|
|
6119
|
+
},
|
|
5442
6120
|
sessionId: () => session.id,
|
|
5443
6121
|
async newSession() {
|
|
5444
6122
|
await persistQueue.catch(() => void 0);
|
|
@@ -5498,7 +6176,7 @@ async function boot(options) {
|
|
|
5498
6176
|
async exportSession(id, destination) {
|
|
5499
6177
|
await persistQueue.catch(() => void 0);
|
|
5500
6178
|
const target = destination ? path13.resolve(options.cwd, destination) : path13.join(options.cwd, ".kitcode-exports");
|
|
5501
|
-
if (!destination) await
|
|
6179
|
+
if (!destination) await mkdir6(target, { recursive: true, mode: 448 });
|
|
5502
6180
|
return (await exportSession(id, target)).path;
|
|
5503
6181
|
},
|
|
5504
6182
|
configPath: () => location.path,
|
|
@@ -5694,6 +6372,9 @@ ${lines.join("\n")}` : null;
|
|
|
5694
6372
|
async installSkill(source) {
|
|
5695
6373
|
const result = await installSkill(source);
|
|
5696
6374
|
skills = await discoverSkills(skillRoots);
|
|
6375
|
+
skillCatalogue = formatSkillCatalogue(skills);
|
|
6376
|
+
tools.unregister(["skill"]);
|
|
6377
|
+
if (skills.length > 0) tools.register([createSkillTool(skills)]);
|
|
5697
6378
|
return result;
|
|
5698
6379
|
},
|
|
5699
6380
|
async loadAttachment(requestedPath) {
|
|
@@ -5745,6 +6426,17 @@ ${lines.join("\n")}` : null;
|
|
|
5745
6426
|
startupUpdateCheck: () => startupUpdate,
|
|
5746
6427
|
async run(history, hooks, signal) {
|
|
5747
6428
|
syncMcpTools();
|
|
6429
|
+
const runProviderId = parseModelRef(modelRef)?.provider;
|
|
6430
|
+
if (runProviderId) {
|
|
6431
|
+
try {
|
|
6432
|
+
const models = await loadModels(registry.get(runProviderId));
|
|
6433
|
+
rememberModels(runProviderId, models);
|
|
6434
|
+
} catch (error) {
|
|
6435
|
+
warnings.push(
|
|
6436
|
+
`Could not refresh models for "${runProviderId}": ${error instanceof Error ? error.message : String(error)}`
|
|
6437
|
+
);
|
|
6438
|
+
}
|
|
6439
|
+
}
|
|
5748
6440
|
let requestHistory = history;
|
|
5749
6441
|
const budget = createTurnBudget(config.budget, resolvePricing);
|
|
5750
6442
|
const previousContext = runtime.modelContext();
|
|
@@ -5845,7 +6537,7 @@ ${lines.join("\n")}` : null;
|
|
|
5845
6537
|
warnings,
|
|
5846
6538
|
async shutdown() {
|
|
5847
6539
|
try {
|
|
5848
|
-
await persistQueue;
|
|
6540
|
+
await Promise.all([persistQueue, configQueue]);
|
|
5849
6541
|
} finally {
|
|
5850
6542
|
await mcp.close();
|
|
5851
6543
|
}
|
|
@@ -6038,8 +6730,7 @@ function validateKey(value) {
|
|
|
6038
6730
|
import { render } from "ink";
|
|
6039
6731
|
|
|
6040
6732
|
// src/ui/App.tsx
|
|
6041
|
-
import { Box as
|
|
6042
|
-
import { execFile as execFile2, execFileSync as execFileSync2 } from "child_process";
|
|
6733
|
+
import { Box as Box13, Text as Text12, useApp, useInput as useInput2, useWindowSize as useWindowSize2 } from "ink";
|
|
6043
6734
|
import { useCallback, useEffect as useEffect2, useMemo as useMemo4, useRef as useRef4, useState as useState5 } from "react";
|
|
6044
6735
|
|
|
6045
6736
|
// src/mcp/add.ts
|
|
@@ -6080,6 +6771,7 @@ var COMMANDS = [
|
|
|
6080
6771
|
{ name: "login" },
|
|
6081
6772
|
{ name: "provider" },
|
|
6082
6773
|
{ name: "logout", args: "[provider]" },
|
|
6774
|
+
{ name: "key", args: "[provider]" },
|
|
6083
6775
|
{ name: "effort" },
|
|
6084
6776
|
{ name: "thinking" },
|
|
6085
6777
|
{ name: "budget", args: "[tokens]" },
|
|
@@ -6167,6 +6859,43 @@ function editDistance(a, b) {
|
|
|
6167
6859
|
return previous[b.length] ?? 0;
|
|
6168
6860
|
}
|
|
6169
6861
|
|
|
6862
|
+
// src/ui/App.tsx
|
|
6863
|
+
import TextInput2 from "ink-text-input";
|
|
6864
|
+
|
|
6865
|
+
// src/ui/theme.ts
|
|
6866
|
+
import { createContext, useContext } from "react";
|
|
6867
|
+
var PRESETS = {
|
|
6868
|
+
purple: "#a78bfa",
|
|
6869
|
+
violet: "#8b5cf6",
|
|
6870
|
+
blue: "#60a5fa",
|
|
6871
|
+
cyan: "#22d3ee",
|
|
6872
|
+
green: "#4ade80",
|
|
6873
|
+
orange: "#fb923c",
|
|
6874
|
+
pink: "#f472b6",
|
|
6875
|
+
red: "#f87171",
|
|
6876
|
+
yellow: "#fbbf24",
|
|
6877
|
+
white: "#e5e7eb"
|
|
6878
|
+
};
|
|
6879
|
+
var DEFAULT_ACCENT = PRESETS.purple;
|
|
6880
|
+
function resolveAccent(value) {
|
|
6881
|
+
if (!value) return DEFAULT_ACCENT;
|
|
6882
|
+
const preset = PRESETS[value.toLowerCase()];
|
|
6883
|
+
if (preset) return preset;
|
|
6884
|
+
return /^#[0-9a-f]{6}$/i.test(value) ? value : DEFAULT_ACCENT;
|
|
6885
|
+
}
|
|
6886
|
+
function makeTheme(accent) {
|
|
6887
|
+
return {
|
|
6888
|
+
accent: resolveAccent(accent),
|
|
6889
|
+
ok: PRESETS.green,
|
|
6890
|
+
warn: PRESETS.yellow,
|
|
6891
|
+
error: PRESETS.red
|
|
6892
|
+
};
|
|
6893
|
+
}
|
|
6894
|
+
var ThemeContext = createContext(makeTheme(void 0));
|
|
6895
|
+
function useTheme() {
|
|
6896
|
+
return useContext(ThemeContext);
|
|
6897
|
+
}
|
|
6898
|
+
|
|
6170
6899
|
// src/ui/components/Confirm.tsx
|
|
6171
6900
|
import { Box, Text } from "ink";
|
|
6172
6901
|
|
|
@@ -6200,7 +6929,7 @@ function normalizeHotkey(char) {
|
|
|
6200
6929
|
}
|
|
6201
6930
|
|
|
6202
6931
|
// src/ui/i18n.ts
|
|
6203
|
-
import { createContext, useContext } from "react";
|
|
6932
|
+
import { createContext as createContext2, useContext as useContext2 } from "react";
|
|
6204
6933
|
var LANGS = [
|
|
6205
6934
|
{ key: "en", label: "English" },
|
|
6206
6935
|
{ key: "ru", label: "\u0420\u0443\u0441\u0441\u043A\u0438\u0439" }
|
|
@@ -6230,6 +6959,7 @@ var en = {
|
|
|
6230
6959
|
workingFor: (elapsed) => `working ${elapsed}`,
|
|
6231
6960
|
titleProvider: "Provider",
|
|
6232
6961
|
titleLogout: "Sign out of a provider",
|
|
6962
|
+
titleKeyChange: "Change API key for a provider",
|
|
6233
6963
|
titleSessions: "Resume a session",
|
|
6234
6964
|
titleSessionAction: "Session action",
|
|
6235
6965
|
resumed: (id, count) => `Resumed ${id} \u2014 ${count} messages restored`,
|
|
@@ -6317,6 +7047,9 @@ var en = {
|
|
|
6317
7047
|
logoutAskBody: "Its API key is removed from auth.json and the provider is dropped from the config.",
|
|
6318
7048
|
loggedOut: (provider) => `Signed out of "${provider}".`,
|
|
6319
7049
|
logoutNothing: "No provider is signed in.",
|
|
7050
|
+
keyChangeAsk: (provider) => `Change API key for "${provider}"?`,
|
|
7051
|
+
keyChangeAskBody: "The new key replaces the old one. Models are re-fetched automatically.",
|
|
7052
|
+
keyChanged: (provider) => `API key updated for "${provider}".`,
|
|
6320
7053
|
promptUsage: "Usage: /prompt save <name>",
|
|
6321
7054
|
promptNothing: "Nothing to save yet \u2014 send a message first.",
|
|
6322
7055
|
promptSaved: (name) => `Saved prompt "${name}"`,
|
|
@@ -6348,6 +7081,7 @@ var en = {
|
|
|
6348
7081
|
login: "add a provider (url + key)",
|
|
6349
7082
|
provider: "switch between configured providers",
|
|
6350
7083
|
logout: "choose a provider to sign out from",
|
|
7084
|
+
key: "change API key for a provider",
|
|
6351
7085
|
effort: "set reasoning depth",
|
|
6352
7086
|
thinking: "toggle reasoning",
|
|
6353
7087
|
budget: "set token budget per turn",
|
|
@@ -6408,6 +7142,7 @@ var ru = {
|
|
|
6408
7142
|
workingFor: (elapsed) => `\u0434\u0443\u043C\u0430\u0435\u0442 ${elapsed}`,
|
|
6409
7143
|
titleProvider: "\u041F\u0440\u043E\u0432\u0430\u0439\u0434\u0435\u0440",
|
|
6410
7144
|
titleLogout: "\u0412\u044B\u0431\u0440\u0430\u0442\u044C API \u0434\u043B\u044F \u0432\u044B\u0445\u043E\u0434\u0430",
|
|
7145
|
+
titleKeyChange: "\u0421\u043C\u0435\u043D\u0438\u0442\u044C \u043A\u043B\u044E\u0447 \u043F\u0440\u043E\u0432\u0430\u0439\u0434\u0435\u0440\u0430",
|
|
6411
7146
|
titleSessions: "\u0412\u043E\u0441\u0441\u0442\u0430\u043D\u043E\u0432\u0438\u0442\u044C \u0441\u0435\u0441\u0441\u0438\u044E",
|
|
6412
7147
|
titleSessionAction: "\u0414\u0435\u0439\u0441\u0442\u0432\u0438\u0435 \u0441 \u0441\u0435\u0441\u0441\u0438\u0435\u0439",
|
|
6413
7148
|
resumed: (id, count) => `\u0421\u0435\u0441\u0441\u0438\u044F ${id} \u0432\u043E\u0441\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D\u0430 \u2014 \u0441\u043E\u043E\u0431\u0449\u0435\u043D\u0438\u0439: ${count}`,
|
|
@@ -6495,6 +7230,9 @@ var ru = {
|
|
|
6495
7230
|
logoutAskBody: "\u041A\u043B\u044E\u0447 \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043B\u0451\u043D \u0438\u0437 auth.json, \u0430 \u043F\u0440\u043E\u0432\u0430\u0439\u0434\u0435\u0440 \u2014 \u0438\u0437 \u043A\u043E\u043D\u0444\u0438\u0433\u0430.",
|
|
6496
7231
|
loggedOut: (provider) => `\u0412\u044B\u043F\u043E\u043B\u043D\u0435\u043D \u0432\u044B\u0445\u043E\u0434 \u0438\u0437 \xAB${provider}\xBB.`,
|
|
6497
7232
|
logoutNothing: "\u041D\u0438 \u043E\u0434\u0438\u043D \u043F\u0440\u043E\u0432\u0430\u0439\u0434\u0435\u0440 \u043D\u0435 \u043F\u043E\u0434\u043A\u043B\u044E\u0447\u0451\u043D.",
|
|
7233
|
+
keyChangeAsk: (provider) => `\u0421\u043C\u0435\u043D\u0438\u0442\u044C \u043A\u043B\u044E\u0447 \u0434\u043B\u044F \xAB${provider}\xBB?`,
|
|
7234
|
+
keyChangeAskBody: "\u041D\u043E\u0432\u044B\u0439 \u043A\u043B\u044E\u0447 \u0437\u0430\u043C\u0435\u043D\u0438\u0442 \u0441\u0442\u0430\u0440\u044B\u0439. \u041C\u043E\u0434\u0435\u043B\u0438 \u043E\u0431\u043D\u043E\u0432\u044F\u0442\u0441\u044F \u0430\u0432\u0442\u043E\u043C\u0430\u0442\u0438\u0447\u0435\u0441\u043A\u0438.",
|
|
7235
|
+
keyChanged: (provider) => `\u041A\u043B\u044E\u0447 \u043E\u0431\u043D\u043E\u0432\u043B\u0451\u043D \u0434\u043B\u044F \xAB${provider}\xBB.`,
|
|
6498
7236
|
promptUsage: "\u0418\u0441\u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u043D\u0438\u0435: /prompt save <\u0438\u043C\u044F>",
|
|
6499
7237
|
promptNothing: "\u041F\u043E\u043A\u0430 \u043D\u0435\u0447\u0435\u0433\u043E \u0441\u043E\u0445\u0440\u0430\u043D\u044F\u0442\u044C \u2014 \u0441\u043D\u0430\u0447\u0430\u043B\u0430 \u043E\u0442\u043F\u0440\u0430\u0432\u044C \u0441\u043E\u043E\u0431\u0449\u0435\u043D\u0438\u0435.",
|
|
6500
7238
|
promptSaved: (name) => `\u041F\u0440\u043E\u043C\u0442 \xAB${name}\xBB \u0441\u043E\u0445\u0440\u0430\u043D\u0451\u043D`,
|
|
@@ -6526,6 +7264,7 @@ var ru = {
|
|
|
6526
7264
|
login: "\u0434\u043E\u0431\u0430\u0432\u0438\u0442\u044C \u043F\u0440\u043E\u0432\u0430\u0439\u0434\u0435\u0440\u0430 (url + \u043A\u043B\u044E\u0447)",
|
|
6527
7265
|
provider: "\u043F\u0435\u0440\u0435\u043A\u043B\u044E\u0447\u0438\u0442\u044C\u0441\u044F \u043C\u0435\u0436\u0434\u0443 \u043F\u0440\u043E\u0432\u0430\u0439\u0434\u0435\u0440\u0430\u043C\u0438",
|
|
6528
7266
|
logout: "\u0432\u044B\u0431\u0440\u0430\u0442\u044C \u043F\u0440\u043E\u0432\u0430\u0439\u0434\u0435\u0440\u0430 \u0438 \u0443\u0434\u0430\u043B\u0438\u0442\u044C \u0435\u0433\u043E \u043A\u043B\u044E\u0447",
|
|
7267
|
+
key: "\u0441\u043C\u0435\u043D\u0438\u0442\u044C \u043A\u043B\u044E\u0447 \u043F\u0440\u043E\u0432\u0430\u0439\u0434\u0435\u0440\u0430",
|
|
6529
7268
|
effort: "\u0433\u043B\u0443\u0431\u0438\u043D\u0430 \u0440\u0430\u0437\u043C\u044B\u0448\u043B\u0435\u043D\u0438\u0439",
|
|
6530
7269
|
thinking: "\u0432\u043A\u043B\u044E\u0447\u0438\u0442\u044C/\u0432\u044B\u043A\u043B\u044E\u0447\u0438\u0442\u044C \u0440\u0430\u0437\u043C\u044B\u0448\u043B\u0435\u043D\u0438\u044F",
|
|
6531
7270
|
budget: "\u043B\u0438\u043C\u0438\u0442 \u0442\u043E\u043A\u0435\u043D\u043E\u0432 \u0437\u0430 \u0445\u043E\u0434 (0 = \u0431\u0435\u0437\u043B\u0438\u043C\u0438\u0442)",
|
|
@@ -6563,43 +7302,9 @@ var DICTIONARIES = { en, ru };
|
|
|
6563
7302
|
function stringsFor(lang) {
|
|
6564
7303
|
return DICTIONARIES[lang ?? "en"];
|
|
6565
7304
|
}
|
|
6566
|
-
var StringsContext =
|
|
7305
|
+
var StringsContext = createContext2(en);
|
|
6567
7306
|
function useStrings() {
|
|
6568
|
-
return
|
|
6569
|
-
}
|
|
6570
|
-
|
|
6571
|
-
// src/ui/theme.ts
|
|
6572
|
-
import { createContext as createContext2, useContext as useContext2 } from "react";
|
|
6573
|
-
var PRESETS = {
|
|
6574
|
-
purple: "#a78bfa",
|
|
6575
|
-
violet: "#8b5cf6",
|
|
6576
|
-
blue: "#60a5fa",
|
|
6577
|
-
cyan: "#22d3ee",
|
|
6578
|
-
green: "#4ade80",
|
|
6579
|
-
orange: "#fb923c",
|
|
6580
|
-
pink: "#f472b6",
|
|
6581
|
-
red: "#f87171",
|
|
6582
|
-
yellow: "#fbbf24",
|
|
6583
|
-
white: "#e5e7eb"
|
|
6584
|
-
};
|
|
6585
|
-
var DEFAULT_ACCENT = PRESETS.purple;
|
|
6586
|
-
function resolveAccent(value) {
|
|
6587
|
-
if (!value) return DEFAULT_ACCENT;
|
|
6588
|
-
const preset = PRESETS[value.toLowerCase()];
|
|
6589
|
-
if (preset) return preset;
|
|
6590
|
-
return /^#[0-9a-f]{6}$/i.test(value) ? value : DEFAULT_ACCENT;
|
|
6591
|
-
}
|
|
6592
|
-
function makeTheme(accent) {
|
|
6593
|
-
return {
|
|
6594
|
-
accent: resolveAccent(accent),
|
|
6595
|
-
ok: PRESETS.green,
|
|
6596
|
-
warn: PRESETS.yellow,
|
|
6597
|
-
error: PRESETS.red
|
|
6598
|
-
};
|
|
6599
|
-
}
|
|
6600
|
-
var ThemeContext = createContext2(makeTheme(void 0));
|
|
6601
|
-
function useTheme() {
|
|
6602
|
-
return useContext2(ThemeContext);
|
|
7307
|
+
return useContext2(StringsContext);
|
|
6603
7308
|
}
|
|
6604
7309
|
|
|
6605
7310
|
// src/ui/components/Confirm.tsx
|
|
@@ -6693,7 +7398,13 @@ function Logo({ subtitle, workspace }) {
|
|
|
6693
7398
|
return /* @__PURE__ */ jsxs2(Box2, { width: "100%", marginBottom: 1, children: [
|
|
6694
7399
|
/* @__PURE__ */ jsx2(Box2, { flexDirection: "column", flexShrink: 0, children: CAT.map((line) => /* @__PURE__ */ jsx2(Text2, { color: theme.accent, children: line }, line)) }),
|
|
6695
7400
|
/* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", marginLeft: 2, flexGrow: 1, flexShrink: 1, minWidth: 0, children: [
|
|
6696
|
-
/* @__PURE__ */
|
|
7401
|
+
/* @__PURE__ */ jsxs2(Box2, { children: [
|
|
7402
|
+
/* @__PURE__ */ jsx2(Text2, { color: theme.accent, bold: true, children: "kitcode" }),
|
|
7403
|
+
/* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
|
|
7404
|
+
" v",
|
|
7405
|
+
KITCODE_VERSION
|
|
7406
|
+
] })
|
|
7407
|
+
] }),
|
|
6697
7408
|
/* @__PURE__ */ jsx2(Text2, { dimColor: true, wrap: "truncate-end", children: subtitle ?? strings.hint }),
|
|
6698
7409
|
location && /* @__PURE__ */ jsxs2(Box2, { width: "100%", children: [
|
|
6699
7410
|
/* @__PURE__ */ jsx2(Text2, { color: theme.accent, children: "\u2302 " }),
|
|
@@ -7376,7 +8087,10 @@ function StatusBar({ status }) {
|
|
|
7376
8087
|
}
|
|
7377
8088
|
)
|
|
7378
8089
|
] }),
|
|
7379
|
-
details.length > 0 && /* @__PURE__ */ jsx9(Box9, { flexWrap: "wrap", children: /* @__PURE__ */
|
|
8090
|
+
details.length > 0 && /* @__PURE__ */ jsx9(Box9, { flexWrap: "wrap", children: details.map((item, index) => /* @__PURE__ */ jsxs9(Fragment2, { children: [
|
|
8091
|
+
index > 0 && /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: " \xB7 " }),
|
|
8092
|
+
item
|
|
8093
|
+
] }, index)) })
|
|
7380
8094
|
]
|
|
7381
8095
|
}
|
|
7382
8096
|
);
|
|
@@ -7386,11 +8100,7 @@ function Segments({ items }) {
|
|
|
7386
8100
|
const elements = [];
|
|
7387
8101
|
if (index > 0) {
|
|
7388
8102
|
elements.push(
|
|
7389
|
-
/* @__PURE__ */
|
|
7390
|
-
" ",
|
|
7391
|
-
"\xB7",
|
|
7392
|
-
" "
|
|
7393
|
-
] }, `sep-${index}`)
|
|
8103
|
+
/* @__PURE__ */ jsx9(Box9, { marginTop: 1, children: /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: " \xB7 " }) }, `sep-${index}`)
|
|
7394
8104
|
);
|
|
7395
8105
|
}
|
|
7396
8106
|
elements.push(/* @__PURE__ */ jsx9(Fragment2, { children: item }, `item-${index}`));
|
|
@@ -7468,59 +8178,148 @@ function ContextMeter({
|
|
|
7468
8178
|
] });
|
|
7469
8179
|
}
|
|
7470
8180
|
|
|
8181
|
+
// src/ui/components/TerminalViewport.tsx
|
|
8182
|
+
import { Box as Box10 } from "ink";
|
|
8183
|
+
import { jsx as jsx10 } from "react/jsx-runtime";
|
|
8184
|
+
function interactiveViewportRows(rows) {
|
|
8185
|
+
if (!Number.isFinite(rows)) return 23;
|
|
8186
|
+
return Math.max(1, Math.floor(rows) - 1);
|
|
8187
|
+
}
|
|
8188
|
+
function TerminalViewport({ children, rows }) {
|
|
8189
|
+
return /* @__PURE__ */ jsx10(
|
|
8190
|
+
Box10,
|
|
8191
|
+
{
|
|
8192
|
+
flexDirection: "column",
|
|
8193
|
+
maxHeight: interactiveViewportRows(rows),
|
|
8194
|
+
overflowY: "hidden",
|
|
8195
|
+
children
|
|
8196
|
+
}
|
|
8197
|
+
);
|
|
8198
|
+
}
|
|
8199
|
+
|
|
7471
8200
|
// src/ui/components/Transcript.tsx
|
|
7472
|
-
import { Box as
|
|
8201
|
+
import { Box as Box12, Static, Text as Text11 } from "ink";
|
|
7473
8202
|
import { memo as memo2, useMemo as useMemo3, useRef as useRef3 } from "react";
|
|
7474
8203
|
import Spinner2 from "ink-spinner";
|
|
7475
8204
|
|
|
7476
8205
|
// src/ui/markdown.tsx
|
|
7477
|
-
import { Box as
|
|
8206
|
+
import { Box as Box11, Text as Text10 } from "ink";
|
|
7478
8207
|
import { useMemo as useMemo2 } from "react";
|
|
7479
|
-
import { jsx as
|
|
8208
|
+
import { Fragment as Fragment4, jsx as jsx11, jsxs as jsxs10 } from "react/jsx-runtime";
|
|
7480
8209
|
function Markdown({ children }) {
|
|
7481
8210
|
const blocks = useMemo2(() => extractBlocks(children), [children]);
|
|
7482
|
-
return /* @__PURE__ */
|
|
8211
|
+
return /* @__PURE__ */ jsx11(Box11, { flexDirection: "column", children: blocks.map((block, i) => /* @__PURE__ */ jsx11(BlockView, { block }, i)) });
|
|
7483
8212
|
}
|
|
7484
8213
|
function BlockView({ block }) {
|
|
7485
8214
|
switch (block.type) {
|
|
7486
8215
|
case "heading": {
|
|
7487
8216
|
const sizes = [22, 20, 18, 16, 14, 13];
|
|
7488
8217
|
const size = sizes[(block.level ?? 1) - 1] ?? 14;
|
|
7489
|
-
return /* @__PURE__ */
|
|
8218
|
+
return /* @__PURE__ */ jsx11(Box11, { marginTop: block.level === 1 ? 1 : 0, children: /* @__PURE__ */ jsx11(Text10, { bold: true, children: truncateBySize(block.text ?? "", size) }) });
|
|
7490
8219
|
}
|
|
7491
8220
|
case "blockquote":
|
|
7492
|
-
return /* @__PURE__ */
|
|
7493
|
-
"\u2502 ",
|
|
7494
|
-
|
|
7495
|
-
] }) });
|
|
8221
|
+
return /* @__PURE__ */ jsx11(Box11, { flexDirection: "column", marginLeft: 2, children: (block.text ?? "").split("\n").map((line, i) => /* @__PURE__ */ jsxs10(Box11, { children: [
|
|
8222
|
+
/* @__PURE__ */ jsx11(Text10, { dimColor: true, children: "\u2502 " }),
|
|
8223
|
+
/* @__PURE__ */ jsx11(Text10, { dimColor: true, children: line })
|
|
8224
|
+
] }, i)) });
|
|
7496
8225
|
case "ul":
|
|
7497
|
-
return /* @__PURE__ */
|
|
7498
|
-
/* @__PURE__ */
|
|
7499
|
-
/* @__PURE__ */
|
|
8226
|
+
return /* @__PURE__ */ jsx11(Box11, { flexDirection: "column", children: (block.items ?? []).map((item, i) => /* @__PURE__ */ jsxs10(Box11, { children: [
|
|
8227
|
+
/* @__PURE__ */ jsx11(Text10, { dimColor: true, children: "\u2022 " }),
|
|
8228
|
+
/* @__PURE__ */ jsx11(Box11, { marginLeft: 2, children: /* @__PURE__ */ jsx11(BlockView, { block: item }) })
|
|
7500
8229
|
] }, i)) });
|
|
7501
8230
|
case "ol":
|
|
7502
|
-
return /* @__PURE__ */
|
|
7503
|
-
/* @__PURE__ */
|
|
7504
|
-
/* @__PURE__ */
|
|
8231
|
+
return /* @__PURE__ */ jsx11(Box11, { flexDirection: "column", children: (block.items ?? []).map((item, i) => /* @__PURE__ */ jsxs10(Box11, { children: [
|
|
8232
|
+
/* @__PURE__ */ jsx11(Text10, { dimColor: true, children: `${i + 1}. ` }),
|
|
8233
|
+
/* @__PURE__ */ jsx11(Box11, { marginLeft: 2, children: /* @__PURE__ */ jsx11(BlockView, { block: item }) })
|
|
7505
8234
|
] }, i)) });
|
|
7506
8235
|
case "paragraph":
|
|
7507
8236
|
default:
|
|
7508
|
-
return /* @__PURE__ */
|
|
8237
|
+
return /* @__PURE__ */ jsx11(Text10, { children: inline(block.text ?? "") });
|
|
8238
|
+
case "table":
|
|
8239
|
+
return /* @__PURE__ */ jsx11(TableView, { block });
|
|
8240
|
+
case "code":
|
|
8241
|
+
return /* @__PURE__ */ jsx11(CodeBlock, { lang: block.lang, code: block.text ?? "" });
|
|
8242
|
+
case "hr":
|
|
8243
|
+
return /* @__PURE__ */ jsx11(Box11, { marginTop: 1, marginBottom: 1, children: /* @__PURE__ */ jsx11(Text10, { dimColor: true, children: "\u2500".repeat(60) }) });
|
|
7509
8244
|
}
|
|
7510
8245
|
}
|
|
8246
|
+
function CodeBlock({ lang, code }) {
|
|
8247
|
+
const theme = useTheme();
|
|
8248
|
+
const lines = code.split("\n");
|
|
8249
|
+
return /* @__PURE__ */ jsxs10(Box11, { flexDirection: "column", marginTop: 1, children: [
|
|
8250
|
+
lang && /* @__PURE__ */ jsx11(Text10, { dimColor: true, color: theme.accent, children: lang }),
|
|
8251
|
+
/* @__PURE__ */ jsx11(Box11, { borderColor: "gray", borderStyle: "round", paddingX: 1, children: /* @__PURE__ */ jsx11(Box11, { flexDirection: "column", children: lines.map((line, i) => /* @__PURE__ */ jsx11(Text10, { color: "cyan", children: line }, i)) }) })
|
|
8252
|
+
] });
|
|
8253
|
+
}
|
|
8254
|
+
function TableView({ block }) {
|
|
8255
|
+
const headers = block.headers ?? [];
|
|
8256
|
+
const rows = block.rows ?? [];
|
|
8257
|
+
if (headers.length === 0 && rows.length === 0) {
|
|
8258
|
+
return /* @__PURE__ */ jsx11(Text10, { dimColor: true, children: "(empty table)" });
|
|
8259
|
+
}
|
|
8260
|
+
const colCount = Math.max(headers.length, ...rows.map((r) => r?.length ?? 0));
|
|
8261
|
+
if (colCount === 0) {
|
|
8262
|
+
return /* @__PURE__ */ jsx11(Text10, { dimColor: true, children: "(empty table)" });
|
|
8263
|
+
}
|
|
8264
|
+
const colWidths = new Array(colCount).fill(0);
|
|
8265
|
+
const allRows = [headers, ...rows];
|
|
8266
|
+
for (const row of allRows) {
|
|
8267
|
+
for (let i = 0; i < colCount; i++) {
|
|
8268
|
+
const cell = row[i] ?? "";
|
|
8269
|
+
colWidths[i] = Math.max(colWidths[i], [...cell].length);
|
|
8270
|
+
}
|
|
8271
|
+
}
|
|
8272
|
+
const separator = colWidths.map((w) => "\u2500".repeat(w)).join("\u253C");
|
|
8273
|
+
const lines = [];
|
|
8274
|
+
allRows.forEach((row, rowIdx) => {
|
|
8275
|
+
const cells = [];
|
|
8276
|
+
for (let i = 0; i < colCount; i++) {
|
|
8277
|
+
const cell = row[i] ?? "";
|
|
8278
|
+
const align = block.colAligns?.[i] ?? "left";
|
|
8279
|
+
const visualWidth = [...cell].length;
|
|
8280
|
+
const padWidth = colWidths[i] + (cell.length - visualWidth);
|
|
8281
|
+
let padded;
|
|
8282
|
+
if (align === "right") {
|
|
8283
|
+
padded = cell.padStart(padWidth);
|
|
8284
|
+
} else if (align === "center") {
|
|
8285
|
+
const totalPad = padWidth - visualWidth;
|
|
8286
|
+
const left = Math.floor(totalPad / 2);
|
|
8287
|
+
padded = " ".repeat(left) + cell + " ".repeat(totalPad - left);
|
|
8288
|
+
} else {
|
|
8289
|
+
padded = cell.padEnd(padWidth);
|
|
8290
|
+
}
|
|
8291
|
+
cells.push(padded);
|
|
8292
|
+
}
|
|
8293
|
+
lines.push(
|
|
8294
|
+
/* @__PURE__ */ jsx11(Text10, { bold: rowIdx === 0, children: cells.join(" \u2502 ") }, `row-${rowIdx}`)
|
|
8295
|
+
);
|
|
8296
|
+
if (rowIdx === 0) {
|
|
8297
|
+
lines.push(/* @__PURE__ */ jsx11(Text10, { dimColor: true, children: separator }, `sep-${rowIdx}`));
|
|
8298
|
+
}
|
|
8299
|
+
});
|
|
8300
|
+
return /* @__PURE__ */ jsx11(Box11, { flexDirection: "column", children: lines });
|
|
8301
|
+
}
|
|
8302
|
+
var CHARS_PER_FONT_LEVEL = 3;
|
|
7511
8303
|
function truncateBySize(text, size) {
|
|
7512
|
-
|
|
8304
|
+
const maxLen = size * CHARS_PER_FONT_LEVEL;
|
|
8305
|
+
return text.length > maxLen ? text.slice(0, maxLen) + "\u2026" : text;
|
|
7513
8306
|
}
|
|
7514
8307
|
var HEADING_RE = /^(#{1,6})\s+(.*)$/;
|
|
7515
8308
|
var UL_RE = /^([-*+])\s+(.*)$/;
|
|
7516
8309
|
var OL_RE = /^(\d+)\.\s+(.*)$/;
|
|
7517
8310
|
var QUOTE_RE = /^>\s?(.*)$/;
|
|
8311
|
+
var TASK_RE = /^[-*+]\s+\[([ xX])\]\s+(.*)$/;
|
|
8312
|
+
var INDENT_RE = /^(\s*)(.*)$/;
|
|
8313
|
+
var HR_RE = /^([-*_])\1{2,}$/;
|
|
8314
|
+
var FENCE_RE = /^(`{3,}|~{3,})(\w*)\s*$/;
|
|
7518
8315
|
function extractBlocks(src) {
|
|
7519
8316
|
const lines = src.replace(/\r\n/g, "\n").split("\n");
|
|
7520
8317
|
const blocks = [];
|
|
7521
8318
|
let paragraph = [];
|
|
7522
8319
|
let list = null;
|
|
7523
8320
|
let quote = [];
|
|
8321
|
+
let code = null;
|
|
8322
|
+
let table = null;
|
|
7524
8323
|
const flushParagraph = () => {
|
|
7525
8324
|
if (paragraph.length) {
|
|
7526
8325
|
blocks.push({ type: "paragraph", text: paragraph.join(" ").trim() });
|
|
@@ -7528,7 +8327,7 @@ function extractBlocks(src) {
|
|
|
7528
8327
|
}
|
|
7529
8328
|
};
|
|
7530
8329
|
const flushList = () => {
|
|
7531
|
-
if (list) {
|
|
8330
|
+
if (list && list.items.length) {
|
|
7532
8331
|
blocks.push({ type: list.ordered ? "ol" : "ul", items: list.items });
|
|
7533
8332
|
list = null;
|
|
7534
8333
|
}
|
|
@@ -7539,105 +8338,279 @@ function extractBlocks(src) {
|
|
|
7539
8338
|
quote = [];
|
|
7540
8339
|
}
|
|
7541
8340
|
};
|
|
8341
|
+
const flushCode = () => {
|
|
8342
|
+
if (code) {
|
|
8343
|
+
blocks.push({ type: "code", lang: code.lang, text: code.lines.join("\n") });
|
|
8344
|
+
code = null;
|
|
8345
|
+
}
|
|
8346
|
+
};
|
|
7542
8347
|
const flushAll = () => {
|
|
7543
8348
|
flushParagraph();
|
|
7544
8349
|
flushList();
|
|
7545
8350
|
flushQuote();
|
|
8351
|
+
flushCode();
|
|
7546
8352
|
};
|
|
7547
|
-
|
|
8353
|
+
const flushTable = () => {
|
|
8354
|
+
if (table && table.headers.length > 0) {
|
|
8355
|
+
blocks.push({
|
|
8356
|
+
type: "table",
|
|
8357
|
+
headers: table.headers,
|
|
8358
|
+
rows: table.rows,
|
|
8359
|
+
colAligns: table.colAligns
|
|
8360
|
+
});
|
|
8361
|
+
}
|
|
8362
|
+
table = null;
|
|
8363
|
+
};
|
|
8364
|
+
const parseListItem = (text, indent) => {
|
|
8365
|
+
const task = text.match(TASK_RE);
|
|
8366
|
+
if (task) {
|
|
8367
|
+
const done = task[1].toLowerCase() === "x";
|
|
8368
|
+
return {
|
|
8369
|
+
type: "paragraph",
|
|
8370
|
+
text: `${done ? "\u2611" : "\u2610"} ${task[2]}`
|
|
8371
|
+
};
|
|
8372
|
+
}
|
|
8373
|
+
return { type: "paragraph", text };
|
|
8374
|
+
};
|
|
8375
|
+
for (let i = 0; i < lines.length; i++) {
|
|
8376
|
+
const line = lines[i];
|
|
7548
8377
|
const trimmed = line.trim();
|
|
8378
|
+
if (code) {
|
|
8379
|
+
const endMatch = trimmed.match(FENCE_RE);
|
|
8380
|
+
if (endMatch && endMatch[1][0] === code.fence[0] && endMatch[1].length >= code.fence.length) {
|
|
8381
|
+
flushCode();
|
|
8382
|
+
continue;
|
|
8383
|
+
}
|
|
8384
|
+
code.lines.push(line);
|
|
8385
|
+
continue;
|
|
8386
|
+
}
|
|
8387
|
+
const fenceMatch = trimmed.match(FENCE_RE);
|
|
8388
|
+
if (fenceMatch) {
|
|
8389
|
+
flushAll();
|
|
8390
|
+
flushTable();
|
|
8391
|
+
code = { fence: fenceMatch[1], lang: fenceMatch[2], lines: [] };
|
|
8392
|
+
continue;
|
|
8393
|
+
}
|
|
7549
8394
|
if (trimmed === "") {
|
|
8395
|
+
flushTable();
|
|
7550
8396
|
flushAll();
|
|
7551
8397
|
continue;
|
|
7552
8398
|
}
|
|
8399
|
+
const hrMatch = trimmed.match(HR_RE);
|
|
8400
|
+
if (hrMatch) {
|
|
8401
|
+
flushTable();
|
|
8402
|
+
flushAll();
|
|
8403
|
+
blocks.push({ type: "hr" });
|
|
8404
|
+
continue;
|
|
8405
|
+
}
|
|
7553
8406
|
const heading = trimmed.match(HEADING_RE);
|
|
7554
8407
|
if (heading) {
|
|
8408
|
+
flushTable();
|
|
7555
8409
|
flushAll();
|
|
7556
8410
|
blocks.push({ type: "heading", level: heading[1].length, text: heading[2].trim() });
|
|
7557
8411
|
continue;
|
|
7558
8412
|
}
|
|
7559
8413
|
const quoteMatch = trimmed.match(QUOTE_RE);
|
|
7560
8414
|
if (quoteMatch) {
|
|
8415
|
+
flushTable();
|
|
7561
8416
|
flushParagraph();
|
|
7562
8417
|
flushList();
|
|
8418
|
+
flushCode();
|
|
7563
8419
|
quote.push(quoteMatch[1]);
|
|
7564
8420
|
continue;
|
|
7565
8421
|
}
|
|
8422
|
+
const indentMatch = line.match(INDENT_RE);
|
|
8423
|
+
const indent = indentMatch?.[1].length ?? 0;
|
|
8424
|
+
const taskMatch = trimmed.match(TASK_RE);
|
|
8425
|
+
if (taskMatch) {
|
|
8426
|
+
flushTable();
|
|
8427
|
+
flushParagraph();
|
|
8428
|
+
flushQuote();
|
|
8429
|
+
flushCode();
|
|
8430
|
+
if (!list || list.ordered || indent !== list.indent) {
|
|
8431
|
+
flushList();
|
|
8432
|
+
list = { ordered: false, items: [], indent };
|
|
8433
|
+
}
|
|
8434
|
+
list.items.push(parseListItem(trimmed, indent));
|
|
8435
|
+
continue;
|
|
8436
|
+
}
|
|
7566
8437
|
const ulMatch = trimmed.match(UL_RE);
|
|
7567
8438
|
if (ulMatch) {
|
|
8439
|
+
flushTable();
|
|
7568
8440
|
flushParagraph();
|
|
7569
8441
|
flushQuote();
|
|
7570
|
-
|
|
8442
|
+
flushCode();
|
|
8443
|
+
if (!list || list.ordered || indent !== list.indent) {
|
|
7571
8444
|
flushList();
|
|
7572
|
-
list = { ordered: false, items: [] };
|
|
8445
|
+
list = { ordered: false, items: [], indent };
|
|
7573
8446
|
}
|
|
7574
|
-
list.items.push(ulMatch[2]
|
|
8447
|
+
list.items.push(parseListItem(ulMatch[2], indent));
|
|
7575
8448
|
continue;
|
|
7576
8449
|
}
|
|
7577
8450
|
const olMatch = trimmed.match(OL_RE);
|
|
7578
8451
|
if (olMatch) {
|
|
8452
|
+
flushTable();
|
|
7579
8453
|
flushParagraph();
|
|
7580
8454
|
flushQuote();
|
|
7581
|
-
|
|
8455
|
+
flushCode();
|
|
8456
|
+
if (!list || !list.ordered || indent !== list.indent) {
|
|
7582
8457
|
flushList();
|
|
7583
|
-
list = { ordered: true, items: [] };
|
|
8458
|
+
list = { ordered: true, items: [], indent };
|
|
7584
8459
|
}
|
|
7585
|
-
list.items.push(olMatch[2]
|
|
8460
|
+
list.items.push(parseListItem(olMatch[2], indent));
|
|
7586
8461
|
continue;
|
|
7587
8462
|
}
|
|
8463
|
+
const sepMatch = trimmed.match(/^\|?\s*(\s*:?-+:?\s*\|)+\s*:?-+:?\s*\|?$/);
|
|
8464
|
+
if (sepMatch) {
|
|
8465
|
+
const cells = trimmed.split("|").map((c) => c.trim()).filter((c) => c.length > 0);
|
|
8466
|
+
if (cells.length >= 2 && cells.every((c) => /^:?-+:?$/.test(c))) {
|
|
8467
|
+
if (table) {
|
|
8468
|
+
table.colAligns = cells.map((c) => {
|
|
8469
|
+
if (c.startsWith(":") && c.endsWith(":")) return "center";
|
|
8470
|
+
if (c.endsWith(":")) return "right";
|
|
8471
|
+
return "left";
|
|
8472
|
+
});
|
|
8473
|
+
continue;
|
|
8474
|
+
}
|
|
8475
|
+
}
|
|
8476
|
+
}
|
|
8477
|
+
const rowMatch = trimmed.match(/^\|?.+\|.+\|?$/);
|
|
8478
|
+
if (rowMatch) {
|
|
8479
|
+
const cells = trimmed.split("|").map((c) => c.trim()).filter((c, idx, arr) => {
|
|
8480
|
+
if (idx === 0 && c === "") return false;
|
|
8481
|
+
if (idx === arr.length - 1 && c === "") return false;
|
|
8482
|
+
return true;
|
|
8483
|
+
});
|
|
8484
|
+
const isWindowsPath = cells.length === 2 && (/^[A-Za-z]:\\/.test(cells[0]) || /^\\\\/.test(cells[0]));
|
|
8485
|
+
if (cells.length >= 2 && cells.every((c) => c.length > 0) && !isWindowsPath) {
|
|
8486
|
+
if (table) {
|
|
8487
|
+
table.rows.push(cells);
|
|
8488
|
+
} else {
|
|
8489
|
+
flushAll();
|
|
8490
|
+
table = { headers: cells, rows: [], colAligns: [] };
|
|
8491
|
+
}
|
|
8492
|
+
continue;
|
|
8493
|
+
}
|
|
8494
|
+
}
|
|
8495
|
+
const asciiSepMatch = trimmed.match(/^[\s\-─┄┈━┅]{3,}$/);
|
|
8496
|
+
if (asciiSepMatch) {
|
|
8497
|
+
if (table && table.headers.length > 0 && table.colAligns.length === 0) {
|
|
8498
|
+
table.colAligns = table.headers.map(() => "left");
|
|
8499
|
+
continue;
|
|
8500
|
+
}
|
|
8501
|
+
if (paragraph.length === 1) {
|
|
8502
|
+
const prevLine = paragraph[0].trim();
|
|
8503
|
+
const tokens3 = prevLine.split(/\s{2,}/).map((c) => c.trim()).filter((c) => c.length > 0);
|
|
8504
|
+
if (tokens3.length >= 2 && tokens3.length <= 10) {
|
|
8505
|
+
paragraph = [];
|
|
8506
|
+
flushList();
|
|
8507
|
+
flushQuote();
|
|
8508
|
+
flushCode();
|
|
8509
|
+
table = { headers: tokens3, rows: [], colAligns: tokens3.map(() => "left") };
|
|
8510
|
+
continue;
|
|
8511
|
+
}
|
|
8512
|
+
}
|
|
8513
|
+
}
|
|
8514
|
+
if (table && table.headers.length > 0 && !trimmed.includes("|")) {
|
|
8515
|
+
const tokens3 = trimmed.split(/\s{2,}/).map((c) => c.trim()).filter((c) => c.length > 0);
|
|
8516
|
+
if (tokens3.length >= 2 && tokens3.length <= table.headers.length + 2) {
|
|
8517
|
+
const looksLikeCode = tokens3.some((t) => t.startsWith("//") || t.startsWith("#") || t.startsWith("/*"));
|
|
8518
|
+
const looksLikePath = tokens3.length === 2 && (/^[A-Za-z]:\\/.test(tokens3[0]) || /^\\\\/.test(tokens3[0]));
|
|
8519
|
+
if (!looksLikeCode && !looksLikePath) {
|
|
8520
|
+
table.rows.push(tokens3);
|
|
8521
|
+
continue;
|
|
8522
|
+
}
|
|
8523
|
+
}
|
|
8524
|
+
}
|
|
8525
|
+
flushTable();
|
|
7588
8526
|
flushList();
|
|
7589
8527
|
flushQuote();
|
|
8528
|
+
flushCode();
|
|
7590
8529
|
paragraph.push(trimmed);
|
|
7591
8530
|
}
|
|
8531
|
+
flushTable();
|
|
7592
8532
|
flushAll();
|
|
7593
8533
|
return blocks;
|
|
7594
8534
|
}
|
|
7595
|
-
|
|
8535
|
+
var MAX_INLINE_DEPTH = 10;
|
|
8536
|
+
function inline(text, depth = 0) {
|
|
8537
|
+
if (depth > MAX_INLINE_DEPTH) {
|
|
8538
|
+
return text;
|
|
8539
|
+
}
|
|
7596
8540
|
const nodes = [];
|
|
7597
8541
|
let rest = text;
|
|
7598
8542
|
let key = 0;
|
|
7599
|
-
const
|
|
8543
|
+
const matchers = [
|
|
8544
|
+
{
|
|
8545
|
+
re: /(!\[)([^\]]*)\]\(([^)]+)\)/,
|
|
8546
|
+
handler: (m) => /* @__PURE__ */ jsxs10(Text10, { color: "cyan", bold: true, children: [
|
|
8547
|
+
"[img: ",
|
|
8548
|
+
m[2],
|
|
8549
|
+
"]"
|
|
8550
|
+
] }, key++)
|
|
8551
|
+
},
|
|
8552
|
+
{
|
|
8553
|
+
re: /(\[)([^\]]*)\]\(([^)]+)\)/,
|
|
8554
|
+
handler: (m) => /* @__PURE__ */ jsxs10(Fragment4, { children: [
|
|
8555
|
+
/* @__PURE__ */ jsx11(Text10, { color: "cyan", underline: true, children: inline(m[2], depth + 1) }, key++),
|
|
8556
|
+
/* @__PURE__ */ jsxs10(Text10, { dimColor: true, color: "gray", children: [
|
|
8557
|
+
"(",
|
|
8558
|
+
m[3],
|
|
8559
|
+
")"
|
|
8560
|
+
] }, key++)
|
|
8561
|
+
] })
|
|
8562
|
+
},
|
|
8563
|
+
{
|
|
8564
|
+
re: /(`+)([^`]+?)\1/,
|
|
8565
|
+
handler: (m) => /* @__PURE__ */ jsx11(Text10, { bold: true, color: "cyan", children: m[2] }, key++)
|
|
8566
|
+
},
|
|
8567
|
+
{
|
|
8568
|
+
re: /(\*\*)(.+?)\1/,
|
|
8569
|
+
handler: (m) => /* @__PURE__ */ jsx11(Text10, { bold: true, children: inline(m[2], depth + 1) }, key++)
|
|
8570
|
+
},
|
|
8571
|
+
{
|
|
8572
|
+
re: /(__)(.+?)\1/,
|
|
8573
|
+
handler: (m) => /* @__PURE__ */ jsx11(Text10, { bold: true, children: inline(m[2], depth + 1) }, key++)
|
|
8574
|
+
},
|
|
8575
|
+
{
|
|
8576
|
+
re: /(~~)(.+?)\1/,
|
|
8577
|
+
handler: (m) => /* @__PURE__ */ jsx11(Text10, { strikethrough: true, children: m[2] }, key++)
|
|
8578
|
+
},
|
|
8579
|
+
{
|
|
8580
|
+
re: /(\*)(.+?)\1/,
|
|
8581
|
+
handler: (m) => /* @__PURE__ */ jsx11(Text10, { italic: true, children: inline(m[2], depth + 1) }, key++)
|
|
8582
|
+
}
|
|
8583
|
+
];
|
|
7600
8584
|
while (rest.length > 0) {
|
|
7601
|
-
|
|
7602
|
-
|
|
8585
|
+
let best = null;
|
|
8586
|
+
for (const { re, handler } of matchers) {
|
|
8587
|
+
const m = rest.match(re);
|
|
8588
|
+
if (m && (best === null || (m.index ?? 0) < (best.match.index ?? 0))) {
|
|
8589
|
+
best = { match: m, handler };
|
|
8590
|
+
}
|
|
8591
|
+
}
|
|
8592
|
+
if (!best) {
|
|
7603
8593
|
nodes.push(rest);
|
|
7604
8594
|
break;
|
|
7605
8595
|
}
|
|
7606
|
-
const idx =
|
|
8596
|
+
const idx = best.match.index ?? 0;
|
|
7607
8597
|
if (idx > 0) nodes.push(rest.slice(0, idx));
|
|
7608
|
-
|
|
7609
|
-
|
|
7610
|
-
/* @__PURE__ */ jsx10(Text10, { bold: true, color: "cyan", children: m[2] }, key++)
|
|
7611
|
-
);
|
|
7612
|
-
} else if (m[3]) {
|
|
7613
|
-
nodes.push(
|
|
7614
|
-
/* @__PURE__ */ jsx10(Text10, { bold: true, children: inline(m[4]) }, key++)
|
|
7615
|
-
);
|
|
7616
|
-
} else if (m[5]) {
|
|
7617
|
-
nodes.push(
|
|
7618
|
-
/* @__PURE__ */ jsx10(Text10, { strikethrough: true, children: m[6] }, key++)
|
|
7619
|
-
);
|
|
7620
|
-
} else if (m[7]) {
|
|
7621
|
-
nodes.push(
|
|
7622
|
-
/* @__PURE__ */ jsx10(Text10, { italic: true, children: inline(m[8]) }, key++)
|
|
7623
|
-
);
|
|
7624
|
-
} else if (m[9]) {
|
|
7625
|
-
nodes.push(
|
|
7626
|
-
/* @__PURE__ */ jsx10(Text10, { bold: true, children: inline(m[10]) }, key++)
|
|
7627
|
-
);
|
|
7628
|
-
}
|
|
7629
|
-
rest = rest.slice(idx + m[0].length);
|
|
8598
|
+
nodes.push(best.handler(best.match));
|
|
8599
|
+
rest = rest.slice(idx + best.match[0].length);
|
|
7630
8600
|
}
|
|
7631
8601
|
return nodes.length === 1 ? nodes[0] : nodes;
|
|
7632
8602
|
}
|
|
7633
8603
|
|
|
7634
8604
|
// src/ui/components/Transcript.tsx
|
|
7635
|
-
import { Fragment as
|
|
8605
|
+
import { Fragment as Fragment5, jsx as jsx12, jsxs as jsxs11 } from "react/jsx-runtime";
|
|
7636
8606
|
var HEADER = { kind: "header" };
|
|
7637
|
-
|
|
7638
|
-
|
|
7639
|
-
(bubble) => bubble.kind === "assistant" && bubble.streaming || bubble.kind === "tool" && bubble.state === "running"
|
|
8607
|
+
function firstMutableBubbleIndex(bubbles) {
|
|
8608
|
+
return bubbles.findIndex(
|
|
8609
|
+
(bubble) => bubble.kind === "assistant" && bubble.streaming || bubble.kind === "tool" && bubble.state === "running" || bubble.kind === "subagent" && bubble.state === "running"
|
|
7640
8610
|
);
|
|
8611
|
+
}
|
|
8612
|
+
var Transcript = memo2(function Transcript2({ bubbles, workspace }) {
|
|
8613
|
+
const liveAt = firstMutableBubbleIndex(bubbles);
|
|
7641
8614
|
const stableCount = liveAt === -1 ? bubbles.length : liveAt;
|
|
7642
8615
|
const stableRef = useRef3([]);
|
|
7643
8616
|
if (stableRef.current.length < stableCount) {
|
|
@@ -7648,40 +8621,48 @@ var Transcript = memo2(function Transcript2({ bubbles, workspace }) {
|
|
|
7648
8621
|
} else if (stableRef.current.length > stableCount) {
|
|
7649
8622
|
stableRef.current = bubbles.slice(0, stableCount);
|
|
7650
8623
|
}
|
|
7651
|
-
const stable = stableRef.current;
|
|
7652
8624
|
const live = liveAt === -1 ? [] : bubbles.slice(liveAt);
|
|
7653
|
-
const staticItems = [HEADER, ...
|
|
7654
|
-
return /* @__PURE__ */ jsxs11(
|
|
7655
|
-
/* @__PURE__ */
|
|
7656
|
-
|
|
8625
|
+
const staticItems = [HEADER, ...stableRef.current];
|
|
8626
|
+
return /* @__PURE__ */ jsxs11(Box12, { flexDirection: "column", marginBottom: 1, flexShrink: 1, overflowY: "hidden", children: [
|
|
8627
|
+
/* @__PURE__ */ jsx12(Static, { items: staticItems, children: (item) => item.kind === "header" ? /* @__PURE__ */ jsx12(Logo, { workspace }, "kitcode-header") : /* @__PURE__ */ jsx12(BubbleView, { bubble: item }, item.id) }),
|
|
8628
|
+
/* @__PURE__ */ jsx12(
|
|
8629
|
+
Box12,
|
|
8630
|
+
{
|
|
8631
|
+
flexDirection: "column",
|
|
8632
|
+
flexShrink: 1,
|
|
8633
|
+
overflowY: "hidden",
|
|
8634
|
+
justifyContent: "flex-end",
|
|
8635
|
+
children: live.map((bubble) => /* @__PURE__ */ jsx12(BubbleView, { bubble }, bubble.id))
|
|
8636
|
+
}
|
|
8637
|
+
)
|
|
7657
8638
|
] });
|
|
7658
8639
|
});
|
|
7659
8640
|
var BubbleView = memo2(function BubbleView2({ bubble }) {
|
|
7660
8641
|
const theme = useTheme();
|
|
7661
8642
|
if (bubble.kind === "user") {
|
|
7662
|
-
return /* @__PURE__ */
|
|
8643
|
+
return /* @__PURE__ */ jsx12(Box12, { marginTop: 1, children: /* @__PURE__ */ jsxs11(Text11, { color: theme.accent, bold: true, children: [
|
|
7663
8644
|
"\u203A ",
|
|
7664
8645
|
bubble.text
|
|
7665
8646
|
] }) });
|
|
7666
8647
|
}
|
|
7667
8648
|
if (bubble.kind === "notice") {
|
|
7668
8649
|
const color = bubble.level === "error" ? theme.error : bubble.level === "warn" ? theme.warn : theme.accent;
|
|
7669
|
-
return /* @__PURE__ */
|
|
8650
|
+
return /* @__PURE__ */ jsx12(Box12, { marginTop: 1, children: /* @__PURE__ */ jsx12(Text11, { color, children: bubble.text }) });
|
|
7670
8651
|
}
|
|
7671
8652
|
if (bubble.kind === "assistant") {
|
|
7672
|
-
return /* @__PURE__ */ jsxs11(
|
|
7673
|
-
bubble.thinking.trim() !== "" && /* @__PURE__ */
|
|
7674
|
-
bubble.streaming ? /* @__PURE__ */
|
|
8653
|
+
return /* @__PURE__ */ jsxs11(Box12, { flexDirection: "column", marginTop: 1, children: [
|
|
8654
|
+
bubble.thinking.trim() !== "" && /* @__PURE__ */ jsx12(Text11, { dimColor: true, italic: true, children: bubble.thinking.trim() }),
|
|
8655
|
+
bubble.streaming ? /* @__PURE__ */ jsx12(Text11, { children: bubble.text }) : /* @__PURE__ */ jsx12(Markdown, { children: bubble.text }),
|
|
7675
8656
|
bubble.streaming && bubble.text === "" && /* @__PURE__ */ jsxs11(Text11, { dimColor: true, children: [
|
|
7676
|
-
/* @__PURE__ */
|
|
8657
|
+
/* @__PURE__ */ jsx12(Spinner2, { type: "dots" }),
|
|
7677
8658
|
" thinking"
|
|
7678
8659
|
] })
|
|
7679
8660
|
] });
|
|
7680
8661
|
}
|
|
7681
8662
|
if (bubble.kind === "subagent") {
|
|
7682
|
-
return /* @__PURE__ */
|
|
8663
|
+
return /* @__PURE__ */ jsx12(SubagentView, { bubble });
|
|
7683
8664
|
}
|
|
7684
|
-
return /* @__PURE__ */
|
|
8665
|
+
return /* @__PURE__ */ jsx12(ToolView, { bubble });
|
|
7685
8666
|
});
|
|
7686
8667
|
function ToolView({ bubble }) {
|
|
7687
8668
|
const theme = useTheme();
|
|
@@ -7692,19 +8673,19 @@ function ToolView({ bubble }) {
|
|
|
7692
8673
|
() => display?.kind === "diff" ? diffLines(display.before, display.after) : null,
|
|
7693
8674
|
[display]
|
|
7694
8675
|
);
|
|
7695
|
-
if (!diff) return /* @__PURE__ */
|
|
8676
|
+
if (!diff) return /* @__PURE__ */ jsx12(NonDiffToolView, { bubble, mark, color });
|
|
7696
8677
|
const hasChanges = diff.added > 0 || diff.removed > 0;
|
|
7697
|
-
return /* @__PURE__ */ jsxs11(
|
|
8678
|
+
return /* @__PURE__ */ jsxs11(Box12, { flexDirection: "column", marginTop: 1, children: [
|
|
7698
8679
|
/* @__PURE__ */ jsxs11(Text11, { color, children: [
|
|
7699
8680
|
mark,
|
|
7700
8681
|
" ",
|
|
7701
8682
|
bubble.summary,
|
|
7702
|
-
hasChanges && /* @__PURE__ */ jsxs11(
|
|
7703
|
-
diff.added > 0 && /* @__PURE__ */
|
|
7704
|
-
diff.removed > 0 && /* @__PURE__ */
|
|
8683
|
+
hasChanges && /* @__PURE__ */ jsxs11(Fragment5, { children: [
|
|
8684
|
+
diff.added > 0 && /* @__PURE__ */ jsx12(Text11, { color: theme.ok, bold: true, children: ` +${diff.added}` }),
|
|
8685
|
+
diff.removed > 0 && /* @__PURE__ */ jsx12(Text11, { color: theme.error, bold: true, children: ` -${diff.removed}` })
|
|
7705
8686
|
] })
|
|
7706
8687
|
] }),
|
|
7707
|
-
/* @__PURE__ */
|
|
8688
|
+
/* @__PURE__ */ jsx12(Box12, { marginLeft: 2, children: /* @__PURE__ */ jsx12(DiffHunk, { lines: diff.hunk, hidden: diff.hidden }) })
|
|
7708
8689
|
] });
|
|
7709
8690
|
}
|
|
7710
8691
|
function NonDiffToolView({
|
|
@@ -7712,13 +8693,13 @@ function NonDiffToolView({
|
|
|
7712
8693
|
mark,
|
|
7713
8694
|
color
|
|
7714
8695
|
}) {
|
|
7715
|
-
return /* @__PURE__ */ jsxs11(
|
|
8696
|
+
return /* @__PURE__ */ jsxs11(Box12, { flexDirection: "column", marginTop: 1, children: [
|
|
7716
8697
|
/* @__PURE__ */ jsxs11(Text11, { color, children: [
|
|
7717
8698
|
mark,
|
|
7718
8699
|
" ",
|
|
7719
8700
|
bubble.summary
|
|
7720
8701
|
] }),
|
|
7721
|
-
bubble.state !== "running" && bubble.content.trim() !== "" && /* @__PURE__ */
|
|
8702
|
+
bubble.state !== "running" && bubble.content.trim() !== "" && /* @__PURE__ */ jsx12(Box12, { flexDirection: "column", marginLeft: 2, children: previewLines(bubble.content).map((line, index) => /* @__PURE__ */ jsx12(Text11, { dimColor: true, children: truncate2(line, 110) }, index)) })
|
|
7722
8703
|
] });
|
|
7723
8704
|
}
|
|
7724
8705
|
function previewLines(content) {
|
|
@@ -7730,15 +8711,15 @@ function SubagentView({ bubble }) {
|
|
|
7730
8711
|
const theme = useTheme();
|
|
7731
8712
|
const mark = bubble.state === "running" ? "\u25CC" : "\u25CF";
|
|
7732
8713
|
const color = bubble.state === "running" ? theme.warn : theme.ok;
|
|
7733
|
-
return /* @__PURE__ */ jsxs11(
|
|
8714
|
+
return /* @__PURE__ */ jsxs11(Box12, { flexDirection: "column", marginTop: 1, children: [
|
|
7734
8715
|
/* @__PURE__ */ jsxs11(Text11, { color, bold: true, children: [
|
|
7735
8716
|
mark,
|
|
7736
8717
|
" subagent: ",
|
|
7737
8718
|
bubble.description
|
|
7738
8719
|
] }),
|
|
7739
|
-
/* @__PURE__ */ jsxs11(
|
|
7740
|
-
bubble.bubbles.map((inner, index) => /* @__PURE__ */
|
|
7741
|
-
bubble.state === "done" && bubble.result && /* @__PURE__ */
|
|
8720
|
+
/* @__PURE__ */ jsxs11(Box12, { marginLeft: 2, flexDirection: "column", children: [
|
|
8721
|
+
bubble.bubbles.map((inner, index) => /* @__PURE__ */ jsx12(BubbleView, { bubble: inner }, index)),
|
|
8722
|
+
bubble.state === "done" && bubble.result && /* @__PURE__ */ jsx12(Box12, { marginTop: 1, children: /* @__PURE__ */ jsx12(Text11, { dimColor: true, children: "\u2500\u2500 result \u2500\u2500" }) })
|
|
7742
8723
|
] })
|
|
7743
8724
|
] });
|
|
7744
8725
|
}
|
|
@@ -7980,26 +8961,17 @@ function sanitizeDisplay(display) {
|
|
|
7980
8961
|
}
|
|
7981
8962
|
|
|
7982
8963
|
// src/ui/App.tsx
|
|
7983
|
-
import { jsx as
|
|
8964
|
+
import { jsx as jsx13, jsxs as jsxs12 } from "react/jsx-runtime";
|
|
7984
8965
|
var EFFORTS = ["low", "medium", "high", "xhigh", "max"];
|
|
7985
8966
|
var STREAM_FRAME_MS = 50;
|
|
7986
8967
|
var MAX_ATTACHMENTS = 8;
|
|
7987
|
-
var NEEDS_IDLE = /* @__PURE__ */ new Set([
|
|
7988
|
-
"clear",
|
|
7989
|
-
"resume",
|
|
7990
|
-
"sessions",
|
|
7991
|
-
"logout",
|
|
7992
|
-
"bypass",
|
|
7993
|
-
"undo",
|
|
7994
|
-
"compact",
|
|
7995
|
-
"checker"
|
|
7996
|
-
]);
|
|
7997
8968
|
function App({
|
|
7998
8969
|
runtime,
|
|
7999
8970
|
initialHistory,
|
|
8000
8971
|
warnings = []
|
|
8001
8972
|
}) {
|
|
8002
8973
|
const { exit } = useApp();
|
|
8974
|
+
const { rows } = useWindowSize2();
|
|
8003
8975
|
const [transcript, setTranscript] = useState5(
|
|
8004
8976
|
() => warnings.reduce((state, text) => pushNotice(state, "warn", text), fromHistory(initialHistory))
|
|
8005
8977
|
);
|
|
@@ -8009,8 +8981,12 @@ function App({
|
|
|
8009
8981
|
() => inputHistoryFromMessages(initialHistory)
|
|
8010
8982
|
);
|
|
8011
8983
|
const [busy, setBusy] = useState5(false);
|
|
8984
|
+
const busyRef = useRef4(false);
|
|
8012
8985
|
const [pendingCount, setPendingCount] = useState5(0);
|
|
8013
8986
|
const queueRef = useRef4([]);
|
|
8987
|
+
const slashQueueRef = useRef4(Promise.resolve());
|
|
8988
|
+
const slashPendingRef = useRef4(0);
|
|
8989
|
+
const drainQueueRef = useRef4(() => void 0);
|
|
8014
8990
|
const [attachments, setAttachments] = useState5([]);
|
|
8015
8991
|
const attachmentsRef = useRef4([]);
|
|
8016
8992
|
const automaticAttachmentTask = useRef4(null);
|
|
@@ -8077,43 +9053,6 @@ function App({
|
|
|
8077
9053
|
const timer = setInterval(() => tick((n) => n + 1), 1e3);
|
|
8078
9054
|
return () => clearInterval(timer);
|
|
8079
9055
|
}, [turnStart]);
|
|
8080
|
-
useEffect2(() => {
|
|
8081
|
-
if (process.platform === "darwin") return;
|
|
8082
|
-
let last = "";
|
|
8083
|
-
let cancelling = false;
|
|
8084
|
-
let giveUp = false;
|
|
8085
|
-
let polling = false;
|
|
8086
|
-
let interval = null;
|
|
8087
|
-
const poll = () => {
|
|
8088
|
-
if (cancelling || giveUp || polling) return;
|
|
8089
|
-
polling = true;
|
|
8090
|
-
execFile2(
|
|
8091
|
-
"xclip",
|
|
8092
|
-
["-o", "-selection", "primary"],
|
|
8093
|
-
{ encoding: "utf8" },
|
|
8094
|
-
(error, stdout) => {
|
|
8095
|
-
polling = false;
|
|
8096
|
-
if (cancelling) return;
|
|
8097
|
-
if (error) {
|
|
8098
|
-
giveUp = true;
|
|
8099
|
-
if (interval) clearInterval(interval);
|
|
8100
|
-
return;
|
|
8101
|
-
}
|
|
8102
|
-
const sel = stdout.trim();
|
|
8103
|
-
if (sel && sel !== last) {
|
|
8104
|
-
last = sel;
|
|
8105
|
-
copyToClipboard(sel);
|
|
8106
|
-
}
|
|
8107
|
-
}
|
|
8108
|
-
);
|
|
8109
|
-
};
|
|
8110
|
-
poll();
|
|
8111
|
-
interval = setInterval(poll, 750);
|
|
8112
|
-
return () => {
|
|
8113
|
-
cancelling = true;
|
|
8114
|
-
if (interval) clearInterval(interval);
|
|
8115
|
-
};
|
|
8116
|
-
}, []);
|
|
8117
9056
|
const theme = useMemo4(() => makeTheme(accent), [accent]);
|
|
8118
9057
|
const strings = useMemo4(() => stringsFor(lang), [lang]);
|
|
8119
9058
|
const notice = useCallback(
|
|
@@ -8152,9 +9091,17 @@ function App({
|
|
|
8152
9091
|
}),
|
|
8153
9092
|
[]
|
|
8154
9093
|
);
|
|
9094
|
+
const promptInput = useCallback(
|
|
9095
|
+
(title, mask) => new Promise((resolve3) => {
|
|
9096
|
+
setOverlay({ kind: "textinput", title, mask, resolve: resolve3, cancel: () => resolve3(null) });
|
|
9097
|
+
}),
|
|
9098
|
+
[]
|
|
9099
|
+
);
|
|
8155
9100
|
const runAgent = useCallback(async () => {
|
|
9101
|
+
if (busyRef.current) return;
|
|
8156
9102
|
const controller = new AbortController();
|
|
8157
9103
|
abort.current = controller;
|
|
9104
|
+
busyRef.current = true;
|
|
8158
9105
|
setBusy(true);
|
|
8159
9106
|
setTurnStart(Date.now());
|
|
8160
9107
|
turns.current += 1;
|
|
@@ -8190,17 +9137,10 @@ function App({
|
|
|
8190
9137
|
} finally {
|
|
8191
9138
|
flushTranscriptEvents();
|
|
8192
9139
|
abort.current = null;
|
|
9140
|
+
busyRef.current = false;
|
|
8193
9141
|
setBusy(false);
|
|
8194
9142
|
setTurnStart(null);
|
|
8195
|
-
|
|
8196
|
-
if (queue.length > 0) {
|
|
8197
|
-
const [next, ...rest] = queue;
|
|
8198
|
-
queueRef.current = rest;
|
|
8199
|
-
setPendingCount(rest.length);
|
|
8200
|
-
queueMicrotask(() => enqueueNext(next));
|
|
8201
|
-
} else {
|
|
8202
|
-
setPendingCount(0);
|
|
8203
|
-
}
|
|
9143
|
+
drainQueueRef.current();
|
|
8204
9144
|
}
|
|
8205
9145
|
}, [flushTranscriptEvents, notice, queueTranscriptEvent, runtime]);
|
|
8206
9146
|
const applyAccent = useCallback(
|
|
@@ -8469,6 +9409,35 @@ ${strings.modelSet(result.nextModel)}` : ""}`;
|
|
|
8469
9409
|
forceRender((n) => n + 1);
|
|
8470
9410
|
return;
|
|
8471
9411
|
}
|
|
9412
|
+
case "key": {
|
|
9413
|
+
const providers = runtime.listProviderItems();
|
|
9414
|
+
if (providers.length === 0) {
|
|
9415
|
+
notice("warn", strings.logoutNothing);
|
|
9416
|
+
return;
|
|
9417
|
+
}
|
|
9418
|
+
let providerId = rest[0];
|
|
9419
|
+
if (!providerId) {
|
|
9420
|
+
providerId = providers.length === 1 ? providers[0]?.key : await pick(strings.titleKeyChange, providers) ?? void 0;
|
|
9421
|
+
}
|
|
9422
|
+
if (!providerId) return;
|
|
9423
|
+
if (!providers.some((provider) => provider.key === providerId)) {
|
|
9424
|
+
notice("warn", `Provider "${providerId}" is not configured.`);
|
|
9425
|
+
return;
|
|
9426
|
+
}
|
|
9427
|
+
if (!await ask2(strings.keyChangeAsk(providerId), strings.keyChangeAskBody, true)) return;
|
|
9428
|
+
const newKey = await promptInput(strings.apiKey, true);
|
|
9429
|
+
if (!newKey || newKey.trim() === "") {
|
|
9430
|
+
notice("warn", "Empty key \u2014 nothing changed.");
|
|
9431
|
+
return;
|
|
9432
|
+
}
|
|
9433
|
+
try {
|
|
9434
|
+
await runtime.changeProviderKey(providerId, newKey.trim());
|
|
9435
|
+
notice("info", strings.keyChanged(providerId));
|
|
9436
|
+
} catch (error) {
|
|
9437
|
+
notice("error", error instanceof Error ? error.message : String(error));
|
|
9438
|
+
}
|
|
9439
|
+
return;
|
|
9440
|
+
}
|
|
8472
9441
|
case "lang": {
|
|
8473
9442
|
const choice = await pick(
|
|
8474
9443
|
strings.titleLang,
|
|
@@ -8745,6 +9714,7 @@ ${strings.mcpAddUsage}`
|
|
|
8745
9714
|
case "compact": {
|
|
8746
9715
|
const controller = new AbortController();
|
|
8747
9716
|
abort.current = controller;
|
|
9717
|
+
busyRef.current = true;
|
|
8748
9718
|
setBusy(true);
|
|
8749
9719
|
setTurnStart(Date.now());
|
|
8750
9720
|
notice("info", strings.compactStarting);
|
|
@@ -8767,17 +9737,9 @@ ${strings.mcpAddUsage}`
|
|
|
8767
9737
|
}
|
|
8768
9738
|
} finally {
|
|
8769
9739
|
abort.current = null;
|
|
9740
|
+
busyRef.current = false;
|
|
8770
9741
|
setBusy(false);
|
|
8771
9742
|
setTurnStart(null);
|
|
8772
|
-
const queue = queueRef.current;
|
|
8773
|
-
if (queue.length > 0) {
|
|
8774
|
-
const [next, ...remaining] = queue;
|
|
8775
|
-
queueRef.current = remaining;
|
|
8776
|
-
setPendingCount(remaining.length);
|
|
8777
|
-
if (next) queueMicrotask(() => enqueueNext(next));
|
|
8778
|
-
} else {
|
|
8779
|
-
setPendingCount(0);
|
|
8780
|
-
}
|
|
8781
9743
|
}
|
|
8782
9744
|
forceRender((n) => n + 1);
|
|
8783
9745
|
return;
|
|
@@ -8980,18 +9942,46 @@ Rename the file if you want a different name.`);
|
|
|
8980
9942
|
},
|
|
8981
9943
|
[applyAccent, appendAttachment, ask2, exit, notice, pick, replaceAttachments, runtime, strings]
|
|
8982
9944
|
);
|
|
9945
|
+
const runSlash = useCallback(
|
|
9946
|
+
(line) => {
|
|
9947
|
+
slashPendingRef.current += 1;
|
|
9948
|
+
const task = slashQueueRef.current.catch(() => void 0).then(() => handleSlash(line));
|
|
9949
|
+
slashQueueRef.current = task;
|
|
9950
|
+
void task.catch(
|
|
9951
|
+
(error) => notice("error", error instanceof Error ? error.message : String(error))
|
|
9952
|
+
);
|
|
9953
|
+
const finished = () => {
|
|
9954
|
+
slashPendingRef.current = Math.max(0, slashPendingRef.current - 1);
|
|
9955
|
+
drainQueueRef.current();
|
|
9956
|
+
};
|
|
9957
|
+
void task.then(finished, finished);
|
|
9958
|
+
},
|
|
9959
|
+
[handleSlash, notice]
|
|
9960
|
+
);
|
|
8983
9961
|
const enqueueNext = useCallback(
|
|
8984
9962
|
(item) => {
|
|
8985
9963
|
if (item.kind === "command") {
|
|
8986
|
-
|
|
9964
|
+
runSlash(item.line);
|
|
8987
9965
|
return;
|
|
8988
9966
|
}
|
|
8989
9967
|
setTranscript((state) => pushUser(state, userDisplay(item.text, item.content)));
|
|
8990
9968
|
history.current = [...history.current, { role: "user", content: item.content }];
|
|
8991
9969
|
void runAgent();
|
|
8992
9970
|
},
|
|
8993
|
-
[
|
|
9971
|
+
[runAgent, runSlash]
|
|
8994
9972
|
);
|
|
9973
|
+
const drainQueue = useCallback(() => {
|
|
9974
|
+
if (busyRef.current || slashPendingRef.current > 0) return;
|
|
9975
|
+
const [next, ...remaining] = queueRef.current;
|
|
9976
|
+
if (!next) {
|
|
9977
|
+
setPendingCount(0);
|
|
9978
|
+
return;
|
|
9979
|
+
}
|
|
9980
|
+
queueRef.current = remaining;
|
|
9981
|
+
setPendingCount(remaining.length);
|
|
9982
|
+
queueMicrotask(() => enqueueNext(next));
|
|
9983
|
+
}, [enqueueNext]);
|
|
9984
|
+
drainQueueRef.current = drainQueue;
|
|
8995
9985
|
const tryQueueAutomaticAttachment = useCallback(
|
|
8996
9986
|
(requestedPath) => {
|
|
8997
9987
|
if (!looksLikeAttachmentPath(requestedPath)) return Promise.resolve(false);
|
|
@@ -9053,12 +10043,12 @@ Rename the file if you want a different name.`);
|
|
|
9053
10043
|
const text = raw.trim();
|
|
9054
10044
|
const submitSlash = () => {
|
|
9055
10045
|
if (!detachedInput) setInput("");
|
|
9056
|
-
if (
|
|
10046
|
+
if (busyRef.current || slashPendingRef.current > 0) {
|
|
9057
10047
|
queueRef.current = [...queueRef.current, { kind: "command", line: text }];
|
|
9058
10048
|
setPendingCount(queueRef.current.length);
|
|
9059
10049
|
return;
|
|
9060
10050
|
}
|
|
9061
|
-
|
|
10051
|
+
runSlash(text);
|
|
9062
10052
|
};
|
|
9063
10053
|
if (isKnownSlashCommand(text)) {
|
|
9064
10054
|
submitSlash();
|
|
@@ -9082,7 +10072,7 @@ Rename the file if you want a different name.`);
|
|
|
9082
10072
|
];
|
|
9083
10073
|
replaceAttachments([]);
|
|
9084
10074
|
const item = { kind: "message", text, content };
|
|
9085
|
-
if (
|
|
10075
|
+
if (busyRef.current || slashPendingRef.current > 0) {
|
|
9086
10076
|
queueRef.current = [...queueRef.current, item];
|
|
9087
10077
|
setPendingCount(queueRef.current.length);
|
|
9088
10078
|
return;
|
|
@@ -9091,7 +10081,7 @@ Rename the file if you want a different name.`);
|
|
|
9091
10081
|
history.current = [...history.current, { role: "user", content }];
|
|
9092
10082
|
void runAgent();
|
|
9093
10083
|
},
|
|
9094
|
-
[
|
|
10084
|
+
[replaceAttachments, runAgent, runSlash, tryQueueAutomaticAttachment]
|
|
9095
10085
|
);
|
|
9096
10086
|
useTerminalInput((_char, key) => {
|
|
9097
10087
|
if (key.tab && key.shift) {
|
|
@@ -9125,10 +10115,10 @@ Rename the file if you want a different name.`);
|
|
|
9125
10115
|
}
|
|
9126
10116
|
setInput("");
|
|
9127
10117
|
});
|
|
9128
|
-
const shell = (children) => /* @__PURE__ */
|
|
10118
|
+
const shell = (children) => /* @__PURE__ */ jsx13(ThemeContext.Provider, { value: theme, children: /* @__PURE__ */ jsx13(StringsContext.Provider, { value: strings, children }) });
|
|
9129
10119
|
if (lang === void 0) {
|
|
9130
10120
|
return shell(
|
|
9131
|
-
/* @__PURE__ */
|
|
10121
|
+
/* @__PURE__ */ jsx13(
|
|
9132
10122
|
LanguagePicker,
|
|
9133
10123
|
{
|
|
9134
10124
|
onPick: (chosen) => {
|
|
@@ -9141,7 +10131,7 @@ Rename the file if you want a different name.`);
|
|
|
9141
10131
|
}
|
|
9142
10132
|
if (setup) {
|
|
9143
10133
|
return shell(
|
|
9144
|
-
/* @__PURE__ */
|
|
10134
|
+
/* @__PURE__ */ jsx13(
|
|
9145
10135
|
Onboarding,
|
|
9146
10136
|
{
|
|
9147
10137
|
onSubmit: async (url, key) => {
|
|
@@ -9156,8 +10146,8 @@ ${strings.configAt(runtime.configPath())}`);
|
|
|
9156
10146
|
);
|
|
9157
10147
|
}
|
|
9158
10148
|
return shell(
|
|
9159
|
-
/* @__PURE__ */ jsxs12(
|
|
9160
|
-
/* @__PURE__ */
|
|
10149
|
+
/* @__PURE__ */ jsxs12(TerminalViewport, { rows, children: [
|
|
10150
|
+
/* @__PURE__ */ jsx13(
|
|
9161
10151
|
Transcript,
|
|
9162
10152
|
{
|
|
9163
10153
|
bubbles: transcript.bubbles,
|
|
@@ -9165,7 +10155,7 @@ ${strings.configAt(runtime.configPath())}`);
|
|
|
9165
10155
|
},
|
|
9166
10156
|
transcriptRevision
|
|
9167
10157
|
),
|
|
9168
|
-
overlay.kind === "permission" && /* @__PURE__ */
|
|
10158
|
+
overlay.kind === "permission" && /* @__PURE__ */ jsx13(
|
|
9169
10159
|
PermissionPrompt,
|
|
9170
10160
|
{
|
|
9171
10161
|
request: overlay.request,
|
|
@@ -9175,7 +10165,7 @@ ${strings.configAt(runtime.configPath())}`);
|
|
|
9175
10165
|
}
|
|
9176
10166
|
}
|
|
9177
10167
|
),
|
|
9178
|
-
overlay.kind === "picker" && /* @__PURE__ */
|
|
10168
|
+
overlay.kind === "picker" && /* @__PURE__ */ jsx13(
|
|
9179
10169
|
Picker,
|
|
9180
10170
|
{
|
|
9181
10171
|
title: overlay.title,
|
|
@@ -9190,7 +10180,7 @@ ${strings.configAt(runtime.configPath())}`);
|
|
|
9190
10180
|
}
|
|
9191
10181
|
}
|
|
9192
10182
|
),
|
|
9193
|
-
overlay.kind === "confirm" && /* @__PURE__ */
|
|
10183
|
+
overlay.kind === "confirm" && /* @__PURE__ */ jsx13(
|
|
9194
10184
|
Confirm,
|
|
9195
10185
|
{
|
|
9196
10186
|
title: overlay.title,
|
|
@@ -9202,7 +10192,22 @@ ${strings.configAt(runtime.configPath())}`);
|
|
|
9202
10192
|
}
|
|
9203
10193
|
}
|
|
9204
10194
|
),
|
|
9205
|
-
overlay.kind === "
|
|
10195
|
+
overlay.kind === "textinput" && /* @__PURE__ */ jsx13(
|
|
10196
|
+
TextInputOverlay,
|
|
10197
|
+
{
|
|
10198
|
+
title: overlay.title,
|
|
10199
|
+
mask: overlay.mask,
|
|
10200
|
+
onAnswer: (value) => {
|
|
10201
|
+
overlay.resolve(value);
|
|
10202
|
+
setOverlay({ kind: "none" });
|
|
10203
|
+
},
|
|
10204
|
+
onCancel: () => {
|
|
10205
|
+
overlay.cancel?.();
|
|
10206
|
+
setOverlay({ kind: "none" });
|
|
10207
|
+
}
|
|
10208
|
+
}
|
|
10209
|
+
),
|
|
10210
|
+
overlay.kind === "none" && /* @__PURE__ */ jsx13(
|
|
9206
10211
|
PromptInput,
|
|
9207
10212
|
{
|
|
9208
10213
|
value: input,
|
|
@@ -9220,7 +10225,7 @@ ${strings.configAt(runtime.configPath())}`);
|
|
|
9220
10225
|
})
|
|
9221
10226
|
}
|
|
9222
10227
|
),
|
|
9223
|
-
/* @__PURE__ */
|
|
10228
|
+
/* @__PURE__ */ jsx13(
|
|
9224
10229
|
StatusBar,
|
|
9225
10230
|
{
|
|
9226
10231
|
status: {
|
|
@@ -9242,9 +10247,33 @@ ${strings.configAt(runtime.configPath())}`);
|
|
|
9242
10247
|
] })
|
|
9243
10248
|
);
|
|
9244
10249
|
}
|
|
9245
|
-
function
|
|
9246
|
-
|
|
9247
|
-
|
|
10250
|
+
function TextInputOverlay({
|
|
10251
|
+
title,
|
|
10252
|
+
mask,
|
|
10253
|
+
onAnswer,
|
|
10254
|
+
onCancel
|
|
10255
|
+
}) {
|
|
10256
|
+
const [value, setValue] = useState5("");
|
|
10257
|
+
const theme = useTheme();
|
|
10258
|
+
const submit = useCallback(() => {
|
|
10259
|
+
onAnswer(value || null);
|
|
10260
|
+
}, [value, onAnswer]);
|
|
10261
|
+
useInput2((_input, key) => {
|
|
10262
|
+
if (key.escape) onCancel();
|
|
10263
|
+
});
|
|
10264
|
+
return /* @__PURE__ */ jsxs12(Box13, { flexDirection: "column", children: [
|
|
10265
|
+
/* @__PURE__ */ jsx13(Text12, { dimColor: true, children: title }),
|
|
10266
|
+
/* @__PURE__ */ jsx13(
|
|
10267
|
+
TextInput2,
|
|
10268
|
+
{
|
|
10269
|
+
value,
|
|
10270
|
+
onChange: setValue,
|
|
10271
|
+
onSubmit: submit,
|
|
10272
|
+
mask: mask ? "\u2022" : void 0
|
|
10273
|
+
}
|
|
10274
|
+
),
|
|
10275
|
+
/* @__PURE__ */ jsx13(Box13, { marginTop: 1, children: /* @__PURE__ */ jsx13(Text12, { dimColor: true, color: theme.accent, children: "enter submit \xB7 esc cancel" }) })
|
|
10276
|
+
] });
|
|
9248
10277
|
}
|
|
9249
10278
|
function isKnownSlashCommand(line) {
|
|
9250
10279
|
if (!line.startsWith("/")) return false;
|
|
@@ -9314,31 +10343,9 @@ function unquoteArg(value) {
|
|
|
9314
10343
|
const last = value.at(-1);
|
|
9315
10344
|
return first2 === '"' && last === '"' || first2 === "'" && last === "'" ? value.slice(1, -1) : value;
|
|
9316
10345
|
}
|
|
9317
|
-
function copyToClipboard(text) {
|
|
9318
|
-
const run = (cmd, args) => execFileSync2(cmd, args, { input: text, stdio: ["pipe", "ignore", "ignore"] });
|
|
9319
|
-
if (process.platform === "darwin") {
|
|
9320
|
-
try {
|
|
9321
|
-
run("pbcopy", []);
|
|
9322
|
-
} catch {
|
|
9323
|
-
}
|
|
9324
|
-
return;
|
|
9325
|
-
}
|
|
9326
|
-
const candidates = [
|
|
9327
|
-
["wl-copy", []],
|
|
9328
|
-
["xclip", ["-selection", "clipboard"]],
|
|
9329
|
-
["xsel", ["--clipboard", "--input"]]
|
|
9330
|
-
];
|
|
9331
|
-
for (const [cmd, args] of candidates) {
|
|
9332
|
-
try {
|
|
9333
|
-
run(cmd, args);
|
|
9334
|
-
return;
|
|
9335
|
-
} catch {
|
|
9336
|
-
}
|
|
9337
|
-
}
|
|
9338
|
-
}
|
|
9339
10346
|
|
|
9340
10347
|
// src/app/tui.tsx
|
|
9341
|
-
import { jsx as
|
|
10348
|
+
import { jsx as jsx14 } from "react/jsx-runtime";
|
|
9342
10349
|
function clearTerminal() {
|
|
9343
10350
|
if (process.stdout.isTTY) process.stdout.write("\x1B[2J\x1B[3J\x1B[H");
|
|
9344
10351
|
}
|
|
@@ -9351,7 +10358,7 @@ async function startTui(options) {
|
|
|
9351
10358
|
mode: options.mode
|
|
9352
10359
|
});
|
|
9353
10360
|
clearTerminal();
|
|
9354
|
-
const instance = render(/* @__PURE__ */
|
|
10361
|
+
const instance = render(/* @__PURE__ */ jsx14(App, { runtime, initialHistory: history, warnings }), {
|
|
9355
10362
|
incrementalRendering: true,
|
|
9356
10363
|
maxFps: 30
|
|
9357
10364
|
});
|