@kernelonpanic/kitcode 1.1.1 → 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 -281
- package/README.ru.md +276 -276
- package/dist/index.js +905 -364
- package/dist/index.js.map +1 -1
- package/package.json +1 -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);
|
|
@@ -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,7 +666,7 @@ 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
|
|
@@ -671,23 +686,30 @@ async function isWorkspaceTrusted(cwd) {
|
|
|
671
686
|
}
|
|
672
687
|
async function trustWorkspace(cwd) {
|
|
673
688
|
const workspace = await canonicalWorkspace(cwd);
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
trust.workspaces.
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
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
|
+
});
|
|
680
697
|
return workspace;
|
|
681
698
|
}
|
|
682
699
|
async function revokeWorkspaceTrust(cwd) {
|
|
683
700
|
const workspace = await canonicalWorkspace(cwd);
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
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
|
+
});
|
|
687
706
|
return workspace;
|
|
688
707
|
}
|
|
689
708
|
async function loadTrust() {
|
|
690
709
|
await trustLock;
|
|
710
|
+
return readTrust();
|
|
711
|
+
}
|
|
712
|
+
async function readTrust() {
|
|
691
713
|
let parsed;
|
|
692
714
|
try {
|
|
693
715
|
parsed = JSON.parse(await readFile(trustPath, "utf8"));
|
|
@@ -703,26 +725,29 @@ async function loadTrust() {
|
|
|
703
725
|
workspaces: value.workspaces.filter((entry) => typeof entry === "string")
|
|
704
726
|
};
|
|
705
727
|
}
|
|
706
|
-
async function
|
|
707
|
-
|
|
708
|
-
|
|
728
|
+
async function writeTrust(value) {
|
|
729
|
+
await ensureDir(path2.dirname(trustPath));
|
|
730
|
+
const temp = `${trustPath}.${randomUUID()}.tmp`;
|
|
731
|
+
try {
|
|
732
|
+
await writeFile(temp, `${JSON.stringify(value, null, 2)}
|
|
733
|
+
`, { encoding: "utf8", mode: 384 });
|
|
734
|
+
await rename(temp, trustPath);
|
|
735
|
+
} catch (error) {
|
|
736
|
+
await rm(temp, { force: true });
|
|
737
|
+
throw error;
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
async function withTrustLock(action) {
|
|
741
|
+
const previous = trustLock;
|
|
742
|
+
let release;
|
|
709
743
|
trustLock = new Promise((resolve3) => {
|
|
710
|
-
|
|
744
|
+
release = resolve3;
|
|
711
745
|
});
|
|
712
|
-
await
|
|
746
|
+
await previous;
|
|
713
747
|
try {
|
|
714
|
-
await
|
|
715
|
-
const temp = `${trustPath}.${randomUUID()}.tmp`;
|
|
716
|
-
try {
|
|
717
|
-
await writeFile(temp, `${JSON.stringify(value, null, 2)}
|
|
718
|
-
`, { encoding: "utf8", mode: 384 });
|
|
719
|
-
await rename(temp, trustPath);
|
|
720
|
-
} catch (error) {
|
|
721
|
-
await rm(temp, { force: true });
|
|
722
|
-
throw error;
|
|
723
|
-
}
|
|
748
|
+
return await action();
|
|
724
749
|
} finally {
|
|
725
|
-
|
|
750
|
+
release();
|
|
726
751
|
}
|
|
727
752
|
}
|
|
728
753
|
|
|
@@ -754,7 +779,7 @@ async function loadConfigAt(location) {
|
|
|
754
779
|
}
|
|
755
780
|
async function saveConfig(config) {
|
|
756
781
|
const location = await configLocation();
|
|
757
|
-
await writeJsonAtomic(location.path, config);
|
|
782
|
+
await writeJsonAtomic(location.path, config, location.scope === "project" ? void 0 : 384);
|
|
758
783
|
}
|
|
759
784
|
async function initProjectConfig(dir) {
|
|
760
785
|
const location = { path: projectConfigPath(dir), scope: "project" };
|
|
@@ -817,17 +842,24 @@ async function readJson(file) {
|
|
|
817
842
|
}
|
|
818
843
|
}
|
|
819
844
|
async function writeJsonAtomic(file, value, mode) {
|
|
820
|
-
|
|
845
|
+
const parent = path3.dirname(file);
|
|
846
|
+
if (isInside(homeDir, parent)) await ensureDir(parent);
|
|
847
|
+
else await mkdir2(parent, { recursive: true });
|
|
821
848
|
const temp = `${file}.${randomUUID2()}.tmp`;
|
|
822
849
|
try {
|
|
823
850
|
await writeFile2(temp, `${JSON.stringify(value, null, 2)}
|
|
824
851
|
`, { encoding: "utf8", mode });
|
|
825
852
|
await rename2(temp, file);
|
|
853
|
+
if (mode !== void 0) await chmod2(file, mode);
|
|
826
854
|
} catch (error) {
|
|
827
855
|
await rm2(temp, { force: true });
|
|
828
856
|
throw error;
|
|
829
857
|
}
|
|
830
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
|
+
}
|
|
831
863
|
function formatIssues(file, error) {
|
|
832
864
|
const lines = error.issues.map(
|
|
833
865
|
(issue) => ` ${issue.path.join(".") || "(root)"}: ${issue.message}`
|
|
@@ -856,6 +888,8 @@ async function addProvider(url, key, options = {}) {
|
|
|
856
888
|
}
|
|
857
889
|
const config = options.local ? await loadProjectConfig(cwd) : (await loadRuntimeConfig(cwd)).config;
|
|
858
890
|
const auth = await loadAuth();
|
|
891
|
+
const configBefore = structuredClone(config);
|
|
892
|
+
const authBefore = { ...auth };
|
|
859
893
|
config.providers[detected.id] = detected.config;
|
|
860
894
|
auth[detected.id] = key;
|
|
861
895
|
const models = detected.models;
|
|
@@ -863,8 +897,16 @@ async function addProvider(url, key, options = {}) {
|
|
|
863
897
|
const chosen = pickDefault(models);
|
|
864
898
|
if (chosen) config.model = formatModelRef(detected.id, chosen);
|
|
865
899
|
}
|
|
866
|
-
|
|
867
|
-
|
|
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
|
+
}
|
|
868
910
|
const location = await configLocation();
|
|
869
911
|
const protocol = detected.config.type === "anthropic" ? "Anthropic" : "OpenAI-compatible";
|
|
870
912
|
const rows = [
|
|
@@ -971,7 +1013,7 @@ function skipControlString(text, from, bellTerminates) {
|
|
|
971
1013
|
|
|
972
1014
|
// src/app/runtime.ts
|
|
973
1015
|
import path13 from "path";
|
|
974
|
-
import { mkdir as
|
|
1016
|
+
import { mkdir as mkdir6 } from "fs/promises";
|
|
975
1017
|
|
|
976
1018
|
// src/core/session.ts
|
|
977
1019
|
import { randomBytes } from "crypto";
|
|
@@ -997,8 +1039,13 @@ async function saveSession(state) {
|
|
|
997
1039
|
await ensureDir(sessionsDir);
|
|
998
1040
|
const file = path4.join(sessionsDir, `${state.id}.json`);
|
|
999
1041
|
const temp = `${file}.${process.pid}.${randomBytes(4).toString("hex")}.tmp`;
|
|
1000
|
-
|
|
1001
|
-
|
|
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
|
+
}
|
|
1002
1049
|
}
|
|
1003
1050
|
async function atomicRename(from, to, retries = 5) {
|
|
1004
1051
|
for (let attempt = 0; attempt < retries; attempt++) {
|
|
@@ -1172,7 +1219,9 @@ function readState(raw) {
|
|
|
1172
1219
|
}
|
|
1173
1220
|
if (typeof parsed !== "object" || parsed === null) return null;
|
|
1174
1221
|
const state = parsed;
|
|
1175
|
-
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;
|
|
1176
1225
|
return {
|
|
1177
1226
|
id: state.id,
|
|
1178
1227
|
title: typeof state.title === "string" ? state.title.slice(0, 120) : void 0,
|
|
@@ -1180,11 +1229,59 @@ function readState(raw) {
|
|
|
1180
1229
|
createdAt: typeof state.createdAt === "string" ? state.createdAt : "",
|
|
1181
1230
|
updatedAt: typeof state.updatedAt === "string" ? state.updatedAt : "",
|
|
1182
1231
|
model: typeof state.model === "string" ? state.model : "",
|
|
1183
|
-
messages
|
|
1184
|
-
usage:
|
|
1232
|
+
messages,
|
|
1233
|
+
usage: readUsageEntries(state.usage),
|
|
1185
1234
|
context: readContextUsage(state.context)
|
|
1186
1235
|
};
|
|
1187
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
|
+
}
|
|
1188
1285
|
async function resolveExportTarget(destination, state) {
|
|
1189
1286
|
const resolved = path4.resolve(destination);
|
|
1190
1287
|
const info = await stat2(resolved).catch(() => null);
|
|
@@ -1245,7 +1342,9 @@ function readUsage(value) {
|
|
|
1245
1342
|
if (typeof value !== "object" || value === null) return void 0;
|
|
1246
1343
|
const usage = value;
|
|
1247
1344
|
const fields = ["input", "output", "cacheWrite", "cacheRead"];
|
|
1248
|
-
if (fields.some(
|
|
1345
|
+
if (fields.some(
|
|
1346
|
+
(field2) => typeof usage[field2] !== "number" || !Number.isFinite(usage[field2]) || usage[field2] < 0
|
|
1347
|
+
)) {
|
|
1249
1348
|
return void 0;
|
|
1250
1349
|
}
|
|
1251
1350
|
return {
|
|
@@ -1276,10 +1375,13 @@ async function resolveSessionId(query) {
|
|
|
1276
1375
|
);
|
|
1277
1376
|
}
|
|
1278
1377
|
function assertSessionId(id) {
|
|
1279
|
-
if (
|
|
1378
|
+
if (!validSessionId(id)) {
|
|
1280
1379
|
throw new Error(`Invalid session id: ${id}`);
|
|
1281
1380
|
}
|
|
1282
1381
|
}
|
|
1382
|
+
function validSessionId(id) {
|
|
1383
|
+
return /^[A-Za-z0-9_-]{1,240}$/.test(id);
|
|
1384
|
+
}
|
|
1283
1385
|
|
|
1284
1386
|
// src/core/agent.ts
|
|
1285
1387
|
var MAX_PAUSE_RESUMES = 5;
|
|
@@ -1455,11 +1557,20 @@ function retryAfterFromError(error) {
|
|
|
1455
1557
|
}
|
|
1456
1558
|
function sleep(ms, signal) {
|
|
1457
1559
|
return new Promise((resolve3, reject) => {
|
|
1458
|
-
|
|
1459
|
-
|
|
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 = () => {
|
|
1460
1569
|
clearTimeout(id);
|
|
1461
1570
|
reject(signal.reason ?? new Error("Aborted"));
|
|
1462
|
-
}
|
|
1571
|
+
};
|
|
1572
|
+
const id = setTimeout(finish, ms);
|
|
1573
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
1463
1574
|
});
|
|
1464
1575
|
}
|
|
1465
1576
|
function estimateRequestTokens(cfg, messages) {
|
|
@@ -1478,9 +1589,7 @@ function estimateRequestTokens(cfg, messages) {
|
|
|
1478
1589
|
async function runToolCalls(cfg, content, hooks, signal) {
|
|
1479
1590
|
const calls = content.filter((block) => block.type === "tool_use");
|
|
1480
1591
|
const results = [];
|
|
1481
|
-
for (const call of calls)
|
|
1482
|
-
results.push(await runOneCall(cfg, call, hooks, signal));
|
|
1483
|
-
}
|
|
1592
|
+
for (const call of calls) results.push(await runOneCall(cfg, call, hooks, signal));
|
|
1484
1593
|
return results;
|
|
1485
1594
|
}
|
|
1486
1595
|
async function runOneCall(cfg, call, hooks, signal) {
|
|
@@ -1608,6 +1717,7 @@ function costOf(usage, pricing) {
|
|
|
1608
1717
|
}
|
|
1609
1718
|
|
|
1610
1719
|
// src/core/budget.ts
|
|
1720
|
+
var UNKNOWN_MODEL_PRICING = { input: 3, output: 15 };
|
|
1611
1721
|
function createTurnBudget(limits, resolvePricing = pricingFor) {
|
|
1612
1722
|
let usage = emptyUsage();
|
|
1613
1723
|
let costUsd = 0;
|
|
@@ -1645,7 +1755,7 @@ function createTurnBudget(limits, resolvePricing = pricingFor) {
|
|
|
1645
1755
|
const pricing = resolvePricing(request.modelRef);
|
|
1646
1756
|
if (current.costUsd !== null) {
|
|
1647
1757
|
const remainingUsd = limits.maxCostUsdPerTurn - current.costUsd;
|
|
1648
|
-
const effectivePricing = pricing ??
|
|
1758
|
+
const effectivePricing = pricing ?? UNKNOWN_MODEL_PRICING;
|
|
1649
1759
|
const inputUsd = estimatedInput * effectivePricing.input / 1e6;
|
|
1650
1760
|
const affordableOutput = Math.floor(
|
|
1651
1761
|
(remainingUsd - inputUsd) * 1e6 / effectivePricing.output
|
|
@@ -1661,9 +1771,10 @@ function createTurnBudget(limits, resolvePricing = pricingFor) {
|
|
|
1661
1771
|
return { allowed: true, maxOutputTokens: Math.floor(outputLimit) };
|
|
1662
1772
|
},
|
|
1663
1773
|
record(modelRef, next) {
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
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);
|
|
1667
1778
|
},
|
|
1668
1779
|
snapshot
|
|
1669
1780
|
};
|
|
@@ -1686,7 +1797,7 @@ import { createReadStream } from "fs";
|
|
|
1686
1797
|
import {
|
|
1687
1798
|
chmod as chmod3,
|
|
1688
1799
|
lstat,
|
|
1689
|
-
mkdir as
|
|
1800
|
+
mkdir as mkdir3,
|
|
1690
1801
|
readFile as readFile4,
|
|
1691
1802
|
readdir as readdir2,
|
|
1692
1803
|
rename as rename4,
|
|
@@ -1928,7 +2039,7 @@ async function restoreFile(root, relativePath, snapshot, expected) {
|
|
|
1928
2039
|
const safe = resolveInside(root, relativePath);
|
|
1929
2040
|
if (!safe.ok || safe.relative !== relativePath) throw new Error("The restore path changed.");
|
|
1930
2041
|
const data = decodeSnapshot(snapshot);
|
|
1931
|
-
await
|
|
2042
|
+
await mkdir3(path5.dirname(safe.path), { recursive: true });
|
|
1932
2043
|
const rechecked = resolveInside(root, relativePath);
|
|
1933
2044
|
if (!rechecked.ok || rechecked.path !== safe.path || rechecked.relative !== relativePath) {
|
|
1934
2045
|
throw new Error("The restore path changed while preparing its parent directory.");
|
|
@@ -2372,17 +2483,15 @@ async function loadAttachment(cwd, requestedPath) {
|
|
|
2372
2483
|
}
|
|
2373
2484
|
async function loadAutomaticAttachment(cwd, requestedPath) {
|
|
2374
2485
|
if (!looksLikeAttachmentPath(requestedPath)) return null;
|
|
2375
|
-
const
|
|
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;
|
|
2376
2490
|
if (isSensitiveAutomaticPath(resolved)) {
|
|
2377
2491
|
throw new Error(
|
|
2378
2492
|
`For safety, sensitive-looking files must be attached explicitly with /attach: ${path7.basename(resolved)}`
|
|
2379
2493
|
);
|
|
2380
2494
|
}
|
|
2381
|
-
if (path7.isAbsolute(resolved)) {
|
|
2382
|
-
const normalized = resolved.toLowerCase();
|
|
2383
|
-
const isProjectFile = normalized.includes(path7.sep + "src" + path7.sep) || normalized.includes(path7.sep + "lib" + path7.sep) || normalized.includes(path7.sep + "app" + path7.sep) || normalized.includes(path7.sep + "test" + path7.sep) || normalized.includes(path7.sep + "tests" + path7.sep) || normalized.includes(path7.sep + "public" + path7.sep) || normalized.includes(path7.sep + "assets" + path7.sep) || normalized.includes(path7.sep + "static" + path7.sep) || normalized.endsWith(".ts") || normalized.endsWith(".js") || normalized.endsWith(".tsx") || normalized.endsWith(".jsx") || normalized.endsWith(".py") || normalized.endsWith(".go") || normalized.endsWith(".rs") || normalized.endsWith(".java") || normalized.endsWith(".c") || normalized.endsWith(".cpp") || normalized.endsWith(".h") || normalized.endsWith(".hpp") || normalized.endsWith(".css") || normalized.endsWith(".html") || normalized.endsWith(".json") || normalized.endsWith(".yaml") || normalized.endsWith(".yml") || normalized.endsWith(".md") || normalized.endsWith(".txt") || normalized.endsWith(".toml") || normalized.endsWith(".ini") || normalized.endsWith(".cfg") || normalized.endsWith(".sh") || normalized.endsWith(".bat") || normalized.endsWith(".cmd") || normalized.endsWith(".ps1") || normalized.endsWith(".dockerfile") || normalized.endsWith(".makefile");
|
|
2384
|
-
if (!isProjectFile) return null;
|
|
2385
|
-
}
|
|
2386
2495
|
const linkInfo = await lstat2(resolved).catch(() => null);
|
|
2387
2496
|
if (linkInfo?.isSymbolicLink()) return null;
|
|
2388
2497
|
const info = await stat5(resolved).catch((error) => {
|
|
@@ -2661,47 +2770,90 @@ function formatBytes(value) {
|
|
|
2661
2770
|
|
|
2662
2771
|
// src/core/compact.ts
|
|
2663
2772
|
var KEEP_USER_TURNS = 2;
|
|
2664
|
-
var
|
|
2665
|
-
var
|
|
2666
|
-
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.";
|
|
2667
2790
|
async function compactHistory(options) {
|
|
2668
2791
|
const cut = compactCutIndex(options.history);
|
|
2669
2792
|
if (cut <= 0) {
|
|
2670
2793
|
return { history: options.history, compacted: false, removedMessages: 0 };
|
|
2671
2794
|
}
|
|
2672
|
-
const
|
|
2673
|
-
|
|
2795
|
+
const oldMessages = options.history.slice(0, cut);
|
|
2796
|
+
const renderedMessages = renderMessages(oldMessages);
|
|
2797
|
+
const rendered = renderedMessages.join("\n\n");
|
|
2798
|
+
if (!rendered.trim()) {
|
|
2674
2799
|
return { history: options.history, compacted: false, removedMessages: 0 };
|
|
2675
2800
|
}
|
|
2676
|
-
let summary
|
|
2677
|
-
let finalText2 = "";
|
|
2801
|
+
let summary;
|
|
2678
2802
|
let usage;
|
|
2679
2803
|
let rateLimits;
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
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;
|
|
2702
2853
|
}
|
|
2854
|
+
if (mergeResult.rateLimits) rateLimits = mergeResult.rateLimits;
|
|
2703
2855
|
}
|
|
2704
|
-
summary =
|
|
2856
|
+
summary = summary.trim();
|
|
2705
2857
|
if (!summary) throw new Error("The provider returned an empty context summary.");
|
|
2706
2858
|
const prefix = [
|
|
2707
2859
|
{
|
|
@@ -2733,11 +2885,34 @@ function compactCutIndex(history) {
|
|
|
2733
2885
|
if (userTurns.length <= KEEP_USER_TURNS) return 0;
|
|
2734
2886
|
return userTurns[userTurns.length - KEEP_USER_TURNS] ?? 0;
|
|
2735
2887
|
}
|
|
2736
|
-
function
|
|
2888
|
+
function estimateCompactBudget(history, maxTokens = MERGE_MAX_TOKENS) {
|
|
2737
2889
|
const cut = compactCutIndex(history);
|
|
2738
|
-
if (cut <= 0) return 1;
|
|
2739
|
-
const
|
|
2740
|
-
|
|
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
|
+
};
|
|
2741
2916
|
}
|
|
2742
2917
|
function isConversationUser(message) {
|
|
2743
2918
|
if (message.role !== "user") return false;
|
|
@@ -2745,50 +2920,249 @@ function isConversationUser(message) {
|
|
|
2745
2920
|
(block) => block.type === "image" || block.type === "file" || block.type === "text" && !block.text.startsWith("[Earlier conversation summary]")
|
|
2746
2921
|
);
|
|
2747
2922
|
}
|
|
2748
|
-
function
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
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();
|
|
2758
2941
|
}
|
|
2759
|
-
|
|
2942
|
+
current.push(unit);
|
|
2943
|
+
}
|
|
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);
|
|
2760
2997
|
}
|
|
2761
|
-
return result;
|
|
2762
2998
|
}
|
|
2763
|
-
function
|
|
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
|
+
);
|
|
3115
|
+
}
|
|
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) {
|
|
2764
3124
|
switch (block.type) {
|
|
2765
3125
|
case "text":
|
|
2766
|
-
return truncate(
|
|
3126
|
+
return truncate(
|
|
3127
|
+
block.text,
|
|
3128
|
+
role === "user" ? MAX_USER_TEXT_CHARS : MAX_ASSISTANT_TEXT_CHARS
|
|
3129
|
+
);
|
|
2767
3130
|
case "thinking":
|
|
2768
3131
|
return "";
|
|
2769
3132
|
case "image":
|
|
2770
|
-
return `[
|
|
3133
|
+
return `[image: ${block.name}]`;
|
|
2771
3134
|
case "file":
|
|
2772
|
-
return `[
|
|
2773
|
-
${truncate(block.text,
|
|
3135
|
+
return `[file: ${block.name}]
|
|
3136
|
+
${truncate(block.text, MAX_FILE_BLOCK_CHARS)}`;
|
|
2774
3137
|
case "tool_use":
|
|
2775
|
-
return `[
|
|
2776
|
-
${safeJson(block.input)}`;
|
|
3138
|
+
return `[tool: ${block.name}(${truncate(safeJson(block.input), MAX_TOOL_INPUT_CHARS)})]`;
|
|
2777
3139
|
case "tool_result":
|
|
2778
|
-
|
|
2779
|
-
${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);
|
|
2780
3145
|
}
|
|
2781
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
|
+
}
|
|
2782
3154
|
function safeJson(value) {
|
|
2783
3155
|
try {
|
|
2784
|
-
return
|
|
3156
|
+
return JSON.stringify(value);
|
|
2785
3157
|
} catch {
|
|
2786
|
-
return "[unserializable
|
|
3158
|
+
return "[unserializable]";
|
|
2787
3159
|
}
|
|
2788
3160
|
}
|
|
2789
3161
|
function truncate(value, max) {
|
|
2790
|
-
|
|
2791
|
-
|
|
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)}`;
|
|
2792
3166
|
}
|
|
2793
3167
|
|
|
2794
3168
|
// src/core/prompt.ts
|
|
@@ -3050,7 +3424,7 @@ function clip(value) {
|
|
|
3050
3424
|
// package.json
|
|
3051
3425
|
var package_default = {
|
|
3052
3426
|
name: "@kernelonpanic/kitcode",
|
|
3053
|
-
version: "1.
|
|
3427
|
+
version: "1.2.0",
|
|
3054
3428
|
description: "Terminal coding agent with a config you never have to write by hand",
|
|
3055
3429
|
type: "module",
|
|
3056
3430
|
license: "MIT",
|
|
@@ -3118,7 +3492,7 @@ var package_default = {
|
|
|
3118
3492
|
// src/version.ts
|
|
3119
3493
|
var KITCODE_VERSION = package_default.version;
|
|
3120
3494
|
var KITCODE_REPOSITORY = "KernelEditor/KitCode";
|
|
3121
|
-
var KITCODE_COMMIT = true ? "
|
|
3495
|
+
var KITCODE_COMMIT = true ? "c93b03ff85112af7b4c28f543aff6b9377f84ad6" : "development";
|
|
3122
3496
|
|
|
3123
3497
|
// src/mcp/client.ts
|
|
3124
3498
|
var clientInfo = { name: "kitcode", version: KITCODE_VERSION };
|
|
@@ -3140,7 +3514,11 @@ var inheritedEnvKeys = /* @__PURE__ */ new Set([
|
|
|
3140
3514
|
"COMSPEC",
|
|
3141
3515
|
"PATHEXT",
|
|
3142
3516
|
"APPDATA",
|
|
3143
|
-
"LOCALAPPDATA"
|
|
3517
|
+
"LOCALAPPDATA",
|
|
3518
|
+
"USERPROFILE",
|
|
3519
|
+
"HOMEDRIVE",
|
|
3520
|
+
"HOMEPATH",
|
|
3521
|
+
"USERNAME"
|
|
3144
3522
|
]);
|
|
3145
3523
|
var blockedEnvKeys = /* @__PURE__ */ new Set([
|
|
3146
3524
|
"ANTHROPIC_API_KEY",
|
|
@@ -3317,8 +3695,11 @@ function configuredSecrets(config) {
|
|
|
3317
3695
|
}
|
|
3318
3696
|
function inheritedEnv(source = process.env) {
|
|
3319
3697
|
const env = {};
|
|
3698
|
+
const allowed = new Set([...inheritedEnvKeys].map((key) => key.toUpperCase()));
|
|
3699
|
+
const blocked = new Set([...blockedEnvKeys].map((key) => key.toUpperCase()));
|
|
3320
3700
|
for (const [key, value] of Object.entries(source)) {
|
|
3321
|
-
|
|
3701
|
+
const normalized = key.toUpperCase();
|
|
3702
|
+
if (value !== void 0 && allowed.has(normalized) && !blocked.has(normalized)) {
|
|
3322
3703
|
env[key] = value;
|
|
3323
3704
|
}
|
|
3324
3705
|
}
|
|
@@ -3352,7 +3733,10 @@ async function fetchProviderBalance(config, apiKey2, options = {}) {
|
|
|
3352
3733
|
},
|
|
3353
3734
|
signal: controller.signal
|
|
3354
3735
|
});
|
|
3355
|
-
if (!response.ok)
|
|
3736
|
+
if (!response.ok) {
|
|
3737
|
+
await response.body?.cancel().catch(() => void 0);
|
|
3738
|
+
continue;
|
|
3739
|
+
}
|
|
3356
3740
|
const text = await limitedResponseText(response, MAX_RESPONSE_BYTES);
|
|
3357
3741
|
if (text === null) continue;
|
|
3358
3742
|
const balance = endpoint2.parse(JSON.parse(text));
|
|
@@ -3467,7 +3851,10 @@ function finiteNumber(value) {
|
|
|
3467
3851
|
}
|
|
3468
3852
|
async function limitedResponseText(response, limit) {
|
|
3469
3853
|
const declared = Number(response.headers.get("content-length"));
|
|
3470
|
-
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
|
+
}
|
|
3471
3858
|
if (!response.body) return "";
|
|
3472
3859
|
const reader = response.body.getReader();
|
|
3473
3860
|
const decoder = new TextDecoder();
|
|
@@ -3517,11 +3904,34 @@ function cacheFile(providerId) {
|
|
|
3517
3904
|
}
|
|
3518
3905
|
async function readCache(file) {
|
|
3519
3906
|
try {
|
|
3520
|
-
|
|
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;
|
|
3521
3914
|
} catch {
|
|
3522
3915
|
return void 0;
|
|
3523
3916
|
}
|
|
3524
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
|
+
}
|
|
3525
3935
|
async function writeCache(file, models) {
|
|
3526
3936
|
const payload = { version: CACHE_VERSION, fetchedAt: Date.now(), models };
|
|
3527
3937
|
try {
|
|
@@ -4629,7 +5039,7 @@ function toToolSchema(tool) {
|
|
|
4629
5039
|
}
|
|
4630
5040
|
|
|
4631
5041
|
// src/tools/write.ts
|
|
4632
|
-
import { lstat as lstat4, mkdir as
|
|
5042
|
+
import { lstat as lstat4, mkdir as mkdir4, readFile as readFile11, stat as stat9, writeFile as writeFile7 } from "fs/promises";
|
|
4633
5043
|
import { dirname as dirname2 } from "path";
|
|
4634
5044
|
var MAX_FILE_BYTES5 = 5e6;
|
|
4635
5045
|
var writeTool = {
|
|
@@ -4698,7 +5108,7 @@ var writeTool = {
|
|
|
4698
5108
|
let before = "";
|
|
4699
5109
|
if (beforeBuffer) before = beforeBuffer.toString("utf8");
|
|
4700
5110
|
try {
|
|
4701
|
-
await
|
|
5111
|
+
await mkdir4(dirname2(safe.path), { recursive: true });
|
|
4702
5112
|
await ctx.checkpoint?.capture(safe.path);
|
|
4703
5113
|
await writeFile7(safe.path, content, "utf8");
|
|
4704
5114
|
ctx.checkpoint?.markChanged(safe.path);
|
|
@@ -4915,11 +5325,22 @@ function parseFrontmatter(text) {
|
|
|
4915
5325
|
}
|
|
4916
5326
|
|
|
4917
5327
|
// src/skills/install.ts
|
|
5328
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
4918
5329
|
import { execFileSync } from "child_process";
|
|
4919
|
-
import {
|
|
4920
|
-
|
|
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";
|
|
4921
5341
|
import path12 from "path";
|
|
4922
5342
|
var TMP_DIR = path12.join(skillsDir, ".tmp");
|
|
5343
|
+
var MAX_SKILL_BYTES2 = 5e6;
|
|
4923
5344
|
async function installSkill(source) {
|
|
4924
5345
|
await ensureDir(skillsDir);
|
|
4925
5346
|
if (isGitHubUrl(source)) {
|
|
@@ -4931,30 +5352,31 @@ async function installSkill(source) {
|
|
|
4931
5352
|
return installFromLocal(source);
|
|
4932
5353
|
}
|
|
4933
5354
|
function isGitHubUrl(input) {
|
|
4934
|
-
|
|
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
|
+
}
|
|
4935
5361
|
}
|
|
4936
5362
|
function isNpmPackage(input) {
|
|
4937
|
-
|
|
4938
|
-
return /^(@[\w.-]+\/)?[\w.-]+(@[\w.*+-]+)?$/.test(input);
|
|
5363
|
+
return npmSkillName(input) !== null;
|
|
4939
5364
|
}
|
|
4940
5365
|
async function installFromGitHub(url) {
|
|
4941
5366
|
const { owner, repo, subdir, branch } = parseGitHubUrl(url);
|
|
4942
|
-
const name = subdir
|
|
4943
|
-
const skillDir =
|
|
4944
|
-
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()}`);
|
|
4945
5370
|
try {
|
|
4946
|
-
rmSync(skillDir, { recursive: true, force: true });
|
|
4947
5371
|
mkdirSync(tmpDir, { recursive: true });
|
|
4948
5372
|
const cloneUrl = `https://github.com/${owner}/${repo}.git`;
|
|
4949
5373
|
const gitArgs = ["clone", "--depth", "1"];
|
|
4950
5374
|
if (branch) gitArgs.push("--branch", branch);
|
|
4951
5375
|
gitArgs.push(cloneUrl, tmpDir);
|
|
4952
5376
|
execFileSync("git", gitArgs, { stdio: "ignore" });
|
|
4953
|
-
const skillFile = subdir ?
|
|
4954
|
-
|
|
4955
|
-
|
|
4956
|
-
}
|
|
4957
|
-
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);
|
|
4958
5380
|
await writeSkillFile(skillDir, body);
|
|
4959
5381
|
return { name, dir: skillDir, source: url };
|
|
4960
5382
|
} finally {
|
|
@@ -4962,9 +5384,11 @@ async function installFromGitHub(url) {
|
|
|
4962
5384
|
}
|
|
4963
5385
|
}
|
|
4964
5386
|
async function installFromNpm(packageName) {
|
|
4965
|
-
const
|
|
4966
|
-
|
|
4967
|
-
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()}`);
|
|
4968
5392
|
try {
|
|
4969
5393
|
mkdirSync(tmpDir, { recursive: true });
|
|
4970
5394
|
execFileSync("npm", ["pack", packageName, "--prefix", tmpDir], {
|
|
@@ -4976,6 +5400,7 @@ async function installFromNpm(packageName) {
|
|
|
4976
5400
|
throw new Error(`Could not download npm package "${packageName}"`);
|
|
4977
5401
|
}
|
|
4978
5402
|
const tarballPath = path12.join(tmpDir, tarball);
|
|
5403
|
+
validateTarball(tarballPath);
|
|
4979
5404
|
const extractDir = path12.join(tmpDir, "extracted");
|
|
4980
5405
|
mkdirSync(extractDir, { recursive: true });
|
|
4981
5406
|
try {
|
|
@@ -4985,7 +5410,7 @@ async function installFromNpm(packageName) {
|
|
|
4985
5410
|
if (existsSync2(packageDir)) {
|
|
4986
5411
|
const skillFile2 = path12.join(packageDir, "SKILL.md");
|
|
4987
5412
|
if (existsSync2(skillFile2)) {
|
|
4988
|
-
const body2 =
|
|
5413
|
+
const body2 = readSkillFile(skillFile2, packageDir, packageName);
|
|
4989
5414
|
await writeSkillFile(skillDir, body2);
|
|
4990
5415
|
return { name, dir: skillDir, source: packageName };
|
|
4991
5416
|
}
|
|
@@ -4999,13 +5424,41 @@ async function installFromNpm(packageName) {
|
|
|
4999
5424
|
if (!existsSync2(skillFile)) {
|
|
5000
5425
|
throw new Error(`No SKILL.md found in npm package "${packageName}"`);
|
|
5001
5426
|
}
|
|
5002
|
-
const body =
|
|
5427
|
+
const body = readSkillFile(skillFile, extractDir, packageName);
|
|
5003
5428
|
await writeSkillFile(skillDir, body);
|
|
5004
5429
|
return { name, dir: skillDir, source: packageName };
|
|
5005
5430
|
} finally {
|
|
5006
5431
|
rmSync(tmpDir, { recursive: true, force: true });
|
|
5007
5432
|
}
|
|
5008
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
|
+
}
|
|
5009
5462
|
async function installFromLocal(srcPath) {
|
|
5010
5463
|
const resolved = path12.resolve(srcPath);
|
|
5011
5464
|
let skillDir;
|
|
@@ -5021,29 +5474,102 @@ async function installFromLocal(srcPath) {
|
|
|
5021
5474
|
throw new Error(`No SKILL.md found at "${srcPath}"`);
|
|
5022
5475
|
}
|
|
5023
5476
|
const name = path12.basename(skillDir);
|
|
5024
|
-
const destDir =
|
|
5025
|
-
const body =
|
|
5477
|
+
const destDir = safeChildPath(skillsDir, safeSkillName(name));
|
|
5478
|
+
const body = readSkillFile(skillFile, skillDir, srcPath);
|
|
5026
5479
|
await writeSkillFile(destDir, body);
|
|
5027
5480
|
return { name, dir: destDir, source: resolved };
|
|
5028
5481
|
}
|
|
5029
5482
|
async function writeSkillFile(dir, body) {
|
|
5030
|
-
|
|
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 });
|
|
5031
5489
|
const file = path12.join(dir, "SKILL.md");
|
|
5032
|
-
|
|
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 });
|
|
5033
5495
|
await chmod6(file, 384);
|
|
5034
5496
|
}
|
|
5035
5497
|
function parseGitHubUrl(url) {
|
|
5036
|
-
|
|
5037
|
-
|
|
5038
|
-
|
|
5039
|
-
|
|
5040
|
-
const simple = url.match(/^https?:\/\/(?:www\.)?github\.com\/([^/]+)\/([^/]+)/);
|
|
5041
|
-
if (simple) {
|
|
5042
|
-
return { owner: simple[1], repo: simple[2] };
|
|
5043
|
-
}
|
|
5498
|
+
let parsed;
|
|
5499
|
+
try {
|
|
5500
|
+
parsed = new URL(url);
|
|
5501
|
+
} catch {
|
|
5044
5502
|
throw new Error(`Cannot parse GitHub URL: ${url}`);
|
|
5045
5503
|
}
|
|
5046
|
-
|
|
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 });
|
|
5047
5573
|
}
|
|
5048
5574
|
|
|
5049
5575
|
// src/tools/skill.ts
|
|
@@ -5185,6 +5711,7 @@ async function checkForUpdates(fetcher = fetch, currentCommit = KITCODE_COMMIT)
|
|
|
5185
5711
|
signal: controller.signal
|
|
5186
5712
|
});
|
|
5187
5713
|
if (!response.ok) {
|
|
5714
|
+
await response.body?.cancel().catch(() => void 0);
|
|
5188
5715
|
return { status: "unknown", reason: `GitHub returned ${response.status}` };
|
|
5189
5716
|
}
|
|
5190
5717
|
const payload = await readJsonBounded(response);
|
|
@@ -5210,7 +5737,10 @@ async function checkForUpdates(fetcher = fetch, currentCommit = KITCODE_COMMIT)
|
|
|
5210
5737
|
}
|
|
5211
5738
|
async function readJsonBounded(response) {
|
|
5212
5739
|
const declared = Number(response.headers.get("content-length"));
|
|
5213
|
-
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
|
+
}
|
|
5214
5744
|
if (!response.body) return null;
|
|
5215
5745
|
const reader = response.body.getReader();
|
|
5216
5746
|
const chunks = [];
|
|
@@ -5295,7 +5825,7 @@ async function boot(options) {
|
|
|
5295
5825
|
session = (options.continueSession ? await latestSessionFor(options.cwd) : null) ?? createSession(options.cwd, config.model ?? "");
|
|
5296
5826
|
}
|
|
5297
5827
|
const usage = createUsageTracker(session.usage, resolvePricing);
|
|
5298
|
-
|
|
5828
|
+
let skillCatalogue = formatSkillCatalogue(skills);
|
|
5299
5829
|
const mainSystemPrompt = () => buildSystemPrompt({
|
|
5300
5830
|
cwd: options.cwd,
|
|
5301
5831
|
toolNames: tools.list().map((tool) => tool.name),
|
|
@@ -5402,26 +5932,39 @@ async function boot(options) {
|
|
|
5402
5932
|
createTaskTool(wrappedRunner, config.budget.maxSubagentsPerTurn)
|
|
5403
5933
|
]);
|
|
5404
5934
|
let persistQueue = Promise.resolve();
|
|
5405
|
-
|
|
5406
|
-
|
|
5407
|
-
|
|
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;
|
|
5408
5949
|
};
|
|
5409
5950
|
const compactWithBudget = async (history, signal, budget) => {
|
|
5410
5951
|
if (compactCutIndex(history) <= 0) {
|
|
5411
5952
|
return { history, compacted: false, removedMessages: 0 };
|
|
5412
5953
|
}
|
|
5413
5954
|
const resolved = registry.resolve(modelRef);
|
|
5955
|
+
const estimate = estimateCompactBudget(history, config.maxTokens);
|
|
5414
5956
|
const budgetDecision = budget.beforeRequest({
|
|
5415
5957
|
modelRef,
|
|
5416
|
-
maxOutputTokens:
|
|
5417
|
-
estimatedInputTokens:
|
|
5958
|
+
maxOutputTokens: estimate.maxOutputTokens,
|
|
5959
|
+
estimatedInputTokens: estimate.inputTokens
|
|
5418
5960
|
});
|
|
5419
5961
|
if (!budgetDecision.allowed) throw new Error(budgetDecision.reason);
|
|
5420
5962
|
const result = await compactHistory({
|
|
5421
5963
|
provider: resolved.provider,
|
|
5422
5964
|
model: resolved.modelId,
|
|
5423
5965
|
history,
|
|
5424
|
-
maxTokens:
|
|
5966
|
+
maxTokens: config.maxTokens,
|
|
5967
|
+
maxTotalOutputTokens: budgetDecision.maxOutputTokens,
|
|
5425
5968
|
signal
|
|
5426
5969
|
});
|
|
5427
5970
|
if (result.usage) {
|
|
@@ -5442,12 +5985,22 @@ async function boot(options) {
|
|
|
5442
5985
|
async addProvider(url, key) {
|
|
5443
5986
|
const detected = await detectProvider(url, key);
|
|
5444
5987
|
rememberModels(detected.id, detected.models);
|
|
5988
|
+
const configBefore = structuredClone(config);
|
|
5989
|
+
const authBefore = { ...auth };
|
|
5445
5990
|
config.providers[detected.id] = detected.config;
|
|
5446
5991
|
auth[detected.id] = key;
|
|
5447
5992
|
const chosen = preferredModel(detected.models);
|
|
5448
5993
|
if (chosen) config.model = formatModelRef(detected.id, chosen);
|
|
5449
|
-
|
|
5450
|
-
|
|
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
|
+
}
|
|
5451
6004
|
registry = createRegistry(config, auth);
|
|
5452
6005
|
const nextRef = config.model ?? "";
|
|
5453
6006
|
const discovered = detected.models.find((model) => model.id === chosen)?.contextWindow;
|
|
@@ -5537,17 +6090,26 @@ async function boot(options) {
|
|
|
5537
6090
|
const provider = config.providers[providerId];
|
|
5538
6091
|
if (!provider) throw new Error(`Provider "${providerId}" is not configured.`);
|
|
5539
6092
|
const previousKey = auth[providerId];
|
|
6093
|
+
const restoreKey = () => {
|
|
6094
|
+
if (previousKey === void 0) delete auth[providerId];
|
|
6095
|
+
else auth[providerId] = previousKey;
|
|
6096
|
+
};
|
|
5540
6097
|
auth[providerId] = newKey;
|
|
5541
6098
|
try {
|
|
5542
6099
|
const testRegistry = createRegistry(config, auth);
|
|
5543
|
-
await
|
|
6100
|
+
await testRegistry.get(providerId).listModels();
|
|
5544
6101
|
} catch (error) {
|
|
5545
|
-
|
|
6102
|
+
restoreKey();
|
|
5546
6103
|
throw new Error(
|
|
5547
6104
|
`Key verification failed for "${providerId}": ${error instanceof Error ? error.message : String(error)}`
|
|
5548
6105
|
);
|
|
5549
6106
|
}
|
|
5550
|
-
|
|
6107
|
+
try {
|
|
6108
|
+
await saveAuth(auth);
|
|
6109
|
+
} catch (error) {
|
|
6110
|
+
restoreKey();
|
|
6111
|
+
throw error;
|
|
6112
|
+
}
|
|
5551
6113
|
registry = createRegistry(config, auth);
|
|
5552
6114
|
if (parseModelRef(modelRef)?.provider === providerId) {
|
|
5553
6115
|
const models = await loadModels(registry.get(providerId));
|
|
@@ -5614,7 +6176,7 @@ async function boot(options) {
|
|
|
5614
6176
|
async exportSession(id, destination) {
|
|
5615
6177
|
await persistQueue.catch(() => void 0);
|
|
5616
6178
|
const target = destination ? path13.resolve(options.cwd, destination) : path13.join(options.cwd, ".kitcode-exports");
|
|
5617
|
-
if (!destination) await
|
|
6179
|
+
if (!destination) await mkdir6(target, { recursive: true, mode: 448 });
|
|
5618
6180
|
return (await exportSession(id, target)).path;
|
|
5619
6181
|
},
|
|
5620
6182
|
configPath: () => location.path,
|
|
@@ -5810,6 +6372,9 @@ ${lines.join("\n")}` : null;
|
|
|
5810
6372
|
async installSkill(source) {
|
|
5811
6373
|
const result = await installSkill(source);
|
|
5812
6374
|
skills = await discoverSkills(skillRoots);
|
|
6375
|
+
skillCatalogue = formatSkillCatalogue(skills);
|
|
6376
|
+
tools.unregister(["skill"]);
|
|
6377
|
+
if (skills.length > 0) tools.register([createSkillTool(skills)]);
|
|
5813
6378
|
return result;
|
|
5814
6379
|
},
|
|
5815
6380
|
async loadAttachment(requestedPath) {
|
|
@@ -5972,7 +6537,7 @@ ${lines.join("\n")}` : null;
|
|
|
5972
6537
|
warnings,
|
|
5973
6538
|
async shutdown() {
|
|
5974
6539
|
try {
|
|
5975
|
-
await persistQueue;
|
|
6540
|
+
await Promise.all([persistQueue, configQueue]);
|
|
5976
6541
|
} finally {
|
|
5977
6542
|
await mcp.close();
|
|
5978
6543
|
}
|
|
@@ -6165,8 +6730,7 @@ function validateKey(value) {
|
|
|
6165
6730
|
import { render } from "ink";
|
|
6166
6731
|
|
|
6167
6732
|
// src/ui/App.tsx
|
|
6168
|
-
import { Box as
|
|
6169
|
-
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";
|
|
6170
6734
|
import { useCallback, useEffect as useEffect2, useMemo as useMemo4, useRef as useRef4, useState as useState5 } from "react";
|
|
6171
6735
|
|
|
6172
6736
|
// src/mcp/add.ts
|
|
@@ -6834,7 +7398,13 @@ function Logo({ subtitle, workspace }) {
|
|
|
6834
7398
|
return /* @__PURE__ */ jsxs2(Box2, { width: "100%", marginBottom: 1, children: [
|
|
6835
7399
|
/* @__PURE__ */ jsx2(Box2, { flexDirection: "column", flexShrink: 0, children: CAT.map((line) => /* @__PURE__ */ jsx2(Text2, { color: theme.accent, children: line }, line)) }),
|
|
6836
7400
|
/* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", marginLeft: 2, flexGrow: 1, flexShrink: 1, minWidth: 0, children: [
|
|
6837
|
-
/* @__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
|
+
] }),
|
|
6838
7408
|
/* @__PURE__ */ jsx2(Text2, { dimColor: true, wrap: "truncate-end", children: subtitle ?? strings.hint }),
|
|
6839
7409
|
location && /* @__PURE__ */ jsxs2(Box2, { width: "100%", children: [
|
|
6840
7410
|
/* @__PURE__ */ jsx2(Text2, { color: theme.accent, children: "\u2302 " }),
|
|
@@ -7608,69 +8178,88 @@ function ContextMeter({
|
|
|
7608
8178
|
] });
|
|
7609
8179
|
}
|
|
7610
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
|
+
|
|
7611
8200
|
// src/ui/components/Transcript.tsx
|
|
7612
|
-
import { Box as
|
|
8201
|
+
import { Box as Box12, Static, Text as Text11 } from "ink";
|
|
7613
8202
|
import { memo as memo2, useMemo as useMemo3, useRef as useRef3 } from "react";
|
|
7614
8203
|
import Spinner2 from "ink-spinner";
|
|
7615
8204
|
|
|
7616
8205
|
// src/ui/markdown.tsx
|
|
7617
|
-
import { Box as
|
|
8206
|
+
import { Box as Box11, Text as Text10 } from "ink";
|
|
7618
8207
|
import { useMemo as useMemo2 } from "react";
|
|
7619
|
-
import { Fragment as Fragment4, jsx as
|
|
8208
|
+
import { Fragment as Fragment4, jsx as jsx11, jsxs as jsxs10 } from "react/jsx-runtime";
|
|
7620
8209
|
function Markdown({ children }) {
|
|
7621
8210
|
const blocks = useMemo2(() => extractBlocks(children), [children]);
|
|
7622
|
-
return /* @__PURE__ */
|
|
8211
|
+
return /* @__PURE__ */ jsx11(Box11, { flexDirection: "column", children: blocks.map((block, i) => /* @__PURE__ */ jsx11(BlockView, { block }, i)) });
|
|
7623
8212
|
}
|
|
7624
8213
|
function BlockView({ block }) {
|
|
7625
8214
|
switch (block.type) {
|
|
7626
8215
|
case "heading": {
|
|
7627
8216
|
const sizes = [22, 20, 18, 16, 14, 13];
|
|
7628
8217
|
const size = sizes[(block.level ?? 1) - 1] ?? 14;
|
|
7629
|
-
return /* @__PURE__ */
|
|
8218
|
+
return /* @__PURE__ */ jsx11(Box11, { marginTop: block.level === 1 ? 1 : 0, children: /* @__PURE__ */ jsx11(Text10, { bold: true, children: truncateBySize(block.text ?? "", size) }) });
|
|
7630
8219
|
}
|
|
7631
8220
|
case "blockquote":
|
|
7632
|
-
return /* @__PURE__ */
|
|
7633
|
-
/* @__PURE__ */
|
|
7634
|
-
/* @__PURE__ */
|
|
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 })
|
|
7635
8224
|
] }, i)) });
|
|
7636
8225
|
case "ul":
|
|
7637
|
-
return /* @__PURE__ */
|
|
7638
|
-
/* @__PURE__ */
|
|
7639
|
-
/* @__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 }) })
|
|
7640
8229
|
] }, i)) });
|
|
7641
8230
|
case "ol":
|
|
7642
|
-
return /* @__PURE__ */
|
|
7643
|
-
/* @__PURE__ */
|
|
7644
|
-
/* @__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 }) })
|
|
7645
8234
|
] }, i)) });
|
|
7646
8235
|
case "paragraph":
|
|
7647
8236
|
default:
|
|
7648
|
-
return /* @__PURE__ */
|
|
8237
|
+
return /* @__PURE__ */ jsx11(Text10, { children: inline(block.text ?? "") });
|
|
7649
8238
|
case "table":
|
|
7650
|
-
return /* @__PURE__ */
|
|
8239
|
+
return /* @__PURE__ */ jsx11(TableView, { block });
|
|
7651
8240
|
case "code":
|
|
7652
|
-
return /* @__PURE__ */
|
|
8241
|
+
return /* @__PURE__ */ jsx11(CodeBlock, { lang: block.lang, code: block.text ?? "" });
|
|
7653
8242
|
case "hr":
|
|
7654
|
-
return /* @__PURE__ */
|
|
8243
|
+
return /* @__PURE__ */ jsx11(Box11, { marginTop: 1, marginBottom: 1, children: /* @__PURE__ */ jsx11(Text10, { dimColor: true, children: "\u2500".repeat(60) }) });
|
|
7655
8244
|
}
|
|
7656
8245
|
}
|
|
7657
8246
|
function CodeBlock({ lang, code }) {
|
|
7658
8247
|
const theme = useTheme();
|
|
7659
8248
|
const lines = code.split("\n");
|
|
7660
|
-
return /* @__PURE__ */ jsxs10(
|
|
7661
|
-
lang && /* @__PURE__ */
|
|
7662
|
-
/* @__PURE__ */
|
|
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)) }) })
|
|
7663
8252
|
] });
|
|
7664
8253
|
}
|
|
7665
8254
|
function TableView({ block }) {
|
|
7666
8255
|
const headers = block.headers ?? [];
|
|
7667
8256
|
const rows = block.rows ?? [];
|
|
7668
8257
|
if (headers.length === 0 && rows.length === 0) {
|
|
7669
|
-
return /* @__PURE__ */
|
|
8258
|
+
return /* @__PURE__ */ jsx11(Text10, { dimColor: true, children: "(empty table)" });
|
|
7670
8259
|
}
|
|
7671
8260
|
const colCount = Math.max(headers.length, ...rows.map((r) => r?.length ?? 0));
|
|
7672
8261
|
if (colCount === 0) {
|
|
7673
|
-
return /* @__PURE__ */
|
|
8262
|
+
return /* @__PURE__ */ jsx11(Text10, { dimColor: true, children: "(empty table)" });
|
|
7674
8263
|
}
|
|
7675
8264
|
const colWidths = new Array(colCount).fill(0);
|
|
7676
8265
|
const allRows = [headers, ...rows];
|
|
@@ -7702,13 +8291,13 @@ function TableView({ block }) {
|
|
|
7702
8291
|
cells.push(padded);
|
|
7703
8292
|
}
|
|
7704
8293
|
lines.push(
|
|
7705
|
-
/* @__PURE__ */
|
|
8294
|
+
/* @__PURE__ */ jsx11(Text10, { bold: rowIdx === 0, children: cells.join(" \u2502 ") }, `row-${rowIdx}`)
|
|
7706
8295
|
);
|
|
7707
8296
|
if (rowIdx === 0) {
|
|
7708
|
-
lines.push(/* @__PURE__ */
|
|
8297
|
+
lines.push(/* @__PURE__ */ jsx11(Text10, { dimColor: true, children: separator }, `sep-${rowIdx}`));
|
|
7709
8298
|
}
|
|
7710
8299
|
});
|
|
7711
|
-
return /* @__PURE__ */
|
|
8300
|
+
return /* @__PURE__ */ jsx11(Box11, { flexDirection: "column", children: lines });
|
|
7712
8301
|
}
|
|
7713
8302
|
var CHARS_PER_FONT_LEVEL = 3;
|
|
7714
8303
|
function truncateBySize(text, size) {
|
|
@@ -7963,7 +8552,7 @@ function inline(text, depth = 0) {
|
|
|
7963
8552
|
{
|
|
7964
8553
|
re: /(\[)([^\]]*)\]\(([^)]+)\)/,
|
|
7965
8554
|
handler: (m) => /* @__PURE__ */ jsxs10(Fragment4, { children: [
|
|
7966
|
-
/* @__PURE__ */
|
|
8555
|
+
/* @__PURE__ */ jsx11(Text10, { color: "cyan", underline: true, children: inline(m[2], depth + 1) }, key++),
|
|
7967
8556
|
/* @__PURE__ */ jsxs10(Text10, { dimColor: true, color: "gray", children: [
|
|
7968
8557
|
"(",
|
|
7969
8558
|
m[3],
|
|
@@ -7973,23 +8562,23 @@ function inline(text, depth = 0) {
|
|
|
7973
8562
|
},
|
|
7974
8563
|
{
|
|
7975
8564
|
re: /(`+)([^`]+?)\1/,
|
|
7976
|
-
handler: (m) => /* @__PURE__ */
|
|
8565
|
+
handler: (m) => /* @__PURE__ */ jsx11(Text10, { bold: true, color: "cyan", children: m[2] }, key++)
|
|
7977
8566
|
},
|
|
7978
8567
|
{
|
|
7979
8568
|
re: /(\*\*)(.+?)\1/,
|
|
7980
|
-
handler: (m) => /* @__PURE__ */
|
|
8569
|
+
handler: (m) => /* @__PURE__ */ jsx11(Text10, { bold: true, children: inline(m[2], depth + 1) }, key++)
|
|
7981
8570
|
},
|
|
7982
8571
|
{
|
|
7983
8572
|
re: /(__)(.+?)\1/,
|
|
7984
|
-
handler: (m) => /* @__PURE__ */
|
|
8573
|
+
handler: (m) => /* @__PURE__ */ jsx11(Text10, { bold: true, children: inline(m[2], depth + 1) }, key++)
|
|
7985
8574
|
},
|
|
7986
8575
|
{
|
|
7987
8576
|
re: /(~~)(.+?)\1/,
|
|
7988
|
-
handler: (m) => /* @__PURE__ */
|
|
8577
|
+
handler: (m) => /* @__PURE__ */ jsx11(Text10, { strikethrough: true, children: m[2] }, key++)
|
|
7989
8578
|
},
|
|
7990
8579
|
{
|
|
7991
8580
|
re: /(\*)(.+?)\1/,
|
|
7992
|
-
handler: (m) => /* @__PURE__ */
|
|
8581
|
+
handler: (m) => /* @__PURE__ */ jsx11(Text10, { italic: true, children: inline(m[2], depth + 1) }, key++)
|
|
7993
8582
|
}
|
|
7994
8583
|
];
|
|
7995
8584
|
while (rest.length > 0) {
|
|
@@ -8013,12 +8602,15 @@ function inline(text, depth = 0) {
|
|
|
8013
8602
|
}
|
|
8014
8603
|
|
|
8015
8604
|
// src/ui/components/Transcript.tsx
|
|
8016
|
-
import { Fragment as Fragment5, jsx as
|
|
8605
|
+
import { Fragment as Fragment5, jsx as jsx12, jsxs as jsxs11 } from "react/jsx-runtime";
|
|
8017
8606
|
var HEADER = { kind: "header" };
|
|
8018
|
-
|
|
8019
|
-
|
|
8020
|
-
(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"
|
|
8021
8610
|
);
|
|
8611
|
+
}
|
|
8612
|
+
var Transcript = memo2(function Transcript2({ bubbles, workspace }) {
|
|
8613
|
+
const liveAt = firstMutableBubbleIndex(bubbles);
|
|
8022
8614
|
const stableCount = liveAt === -1 ? bubbles.length : liveAt;
|
|
8023
8615
|
const stableRef = useRef3([]);
|
|
8024
8616
|
if (stableRef.current.length < stableCount) {
|
|
@@ -8029,40 +8621,48 @@ var Transcript = memo2(function Transcript2({ bubbles, workspace }) {
|
|
|
8029
8621
|
} else if (stableRef.current.length > stableCount) {
|
|
8030
8622
|
stableRef.current = bubbles.slice(0, stableCount);
|
|
8031
8623
|
}
|
|
8032
|
-
const stable = stableRef.current;
|
|
8033
8624
|
const live = liveAt === -1 ? [] : bubbles.slice(liveAt);
|
|
8034
|
-
const staticItems = [HEADER, ...
|
|
8035
|
-
return /* @__PURE__ */ jsxs11(
|
|
8036
|
-
/* @__PURE__ */
|
|
8037
|
-
|
|
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
|
+
)
|
|
8038
8638
|
] });
|
|
8039
8639
|
});
|
|
8040
8640
|
var BubbleView = memo2(function BubbleView2({ bubble }) {
|
|
8041
8641
|
const theme = useTheme();
|
|
8042
8642
|
if (bubble.kind === "user") {
|
|
8043
|
-
return /* @__PURE__ */
|
|
8643
|
+
return /* @__PURE__ */ jsx12(Box12, { marginTop: 1, children: /* @__PURE__ */ jsxs11(Text11, { color: theme.accent, bold: true, children: [
|
|
8044
8644
|
"\u203A ",
|
|
8045
8645
|
bubble.text
|
|
8046
8646
|
] }) });
|
|
8047
8647
|
}
|
|
8048
8648
|
if (bubble.kind === "notice") {
|
|
8049
8649
|
const color = bubble.level === "error" ? theme.error : bubble.level === "warn" ? theme.warn : theme.accent;
|
|
8050
|
-
return /* @__PURE__ */
|
|
8650
|
+
return /* @__PURE__ */ jsx12(Box12, { marginTop: 1, children: /* @__PURE__ */ jsx12(Text11, { color, children: bubble.text }) });
|
|
8051
8651
|
}
|
|
8052
8652
|
if (bubble.kind === "assistant") {
|
|
8053
|
-
return /* @__PURE__ */ jsxs11(
|
|
8054
|
-
bubble.thinking.trim() !== "" && /* @__PURE__ */
|
|
8055
|
-
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 }),
|
|
8056
8656
|
bubble.streaming && bubble.text === "" && /* @__PURE__ */ jsxs11(Text11, { dimColor: true, children: [
|
|
8057
|
-
/* @__PURE__ */
|
|
8657
|
+
/* @__PURE__ */ jsx12(Spinner2, { type: "dots" }),
|
|
8058
8658
|
" thinking"
|
|
8059
8659
|
] })
|
|
8060
8660
|
] });
|
|
8061
8661
|
}
|
|
8062
8662
|
if (bubble.kind === "subagent") {
|
|
8063
|
-
return /* @__PURE__ */
|
|
8663
|
+
return /* @__PURE__ */ jsx12(SubagentView, { bubble });
|
|
8064
8664
|
}
|
|
8065
|
-
return /* @__PURE__ */
|
|
8665
|
+
return /* @__PURE__ */ jsx12(ToolView, { bubble });
|
|
8066
8666
|
});
|
|
8067
8667
|
function ToolView({ bubble }) {
|
|
8068
8668
|
const theme = useTheme();
|
|
@@ -8073,19 +8673,19 @@ function ToolView({ bubble }) {
|
|
|
8073
8673
|
() => display?.kind === "diff" ? diffLines(display.before, display.after) : null,
|
|
8074
8674
|
[display]
|
|
8075
8675
|
);
|
|
8076
|
-
if (!diff) return /* @__PURE__ */
|
|
8676
|
+
if (!diff) return /* @__PURE__ */ jsx12(NonDiffToolView, { bubble, mark, color });
|
|
8077
8677
|
const hasChanges = diff.added > 0 || diff.removed > 0;
|
|
8078
|
-
return /* @__PURE__ */ jsxs11(
|
|
8678
|
+
return /* @__PURE__ */ jsxs11(Box12, { flexDirection: "column", marginTop: 1, children: [
|
|
8079
8679
|
/* @__PURE__ */ jsxs11(Text11, { color, children: [
|
|
8080
8680
|
mark,
|
|
8081
8681
|
" ",
|
|
8082
8682
|
bubble.summary,
|
|
8083
8683
|
hasChanges && /* @__PURE__ */ jsxs11(Fragment5, { children: [
|
|
8084
|
-
diff.added > 0 && /* @__PURE__ */
|
|
8085
|
-
diff.removed > 0 && /* @__PURE__ */
|
|
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}` })
|
|
8086
8686
|
] })
|
|
8087
8687
|
] }),
|
|
8088
|
-
/* @__PURE__ */
|
|
8688
|
+
/* @__PURE__ */ jsx12(Box12, { marginLeft: 2, children: /* @__PURE__ */ jsx12(DiffHunk, { lines: diff.hunk, hidden: diff.hidden }) })
|
|
8089
8689
|
] });
|
|
8090
8690
|
}
|
|
8091
8691
|
function NonDiffToolView({
|
|
@@ -8093,13 +8693,13 @@ function NonDiffToolView({
|
|
|
8093
8693
|
mark,
|
|
8094
8694
|
color
|
|
8095
8695
|
}) {
|
|
8096
|
-
return /* @__PURE__ */ jsxs11(
|
|
8696
|
+
return /* @__PURE__ */ jsxs11(Box12, { flexDirection: "column", marginTop: 1, children: [
|
|
8097
8697
|
/* @__PURE__ */ jsxs11(Text11, { color, children: [
|
|
8098
8698
|
mark,
|
|
8099
8699
|
" ",
|
|
8100
8700
|
bubble.summary
|
|
8101
8701
|
] }),
|
|
8102
|
-
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)) })
|
|
8103
8703
|
] });
|
|
8104
8704
|
}
|
|
8105
8705
|
function previewLines(content) {
|
|
@@ -8111,15 +8711,15 @@ function SubagentView({ bubble }) {
|
|
|
8111
8711
|
const theme = useTheme();
|
|
8112
8712
|
const mark = bubble.state === "running" ? "\u25CC" : "\u25CF";
|
|
8113
8713
|
const color = bubble.state === "running" ? theme.warn : theme.ok;
|
|
8114
|
-
return /* @__PURE__ */ jsxs11(
|
|
8714
|
+
return /* @__PURE__ */ jsxs11(Box12, { flexDirection: "column", marginTop: 1, children: [
|
|
8115
8715
|
/* @__PURE__ */ jsxs11(Text11, { color, bold: true, children: [
|
|
8116
8716
|
mark,
|
|
8117
8717
|
" subagent: ",
|
|
8118
8718
|
bubble.description
|
|
8119
8719
|
] }),
|
|
8120
|
-
/* @__PURE__ */ jsxs11(
|
|
8121
|
-
bubble.bubbles.map((inner, index) => /* @__PURE__ */
|
|
8122
|
-
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" }) })
|
|
8123
8723
|
] })
|
|
8124
8724
|
] });
|
|
8125
8725
|
}
|
|
@@ -8361,26 +8961,17 @@ function sanitizeDisplay(display) {
|
|
|
8361
8961
|
}
|
|
8362
8962
|
|
|
8363
8963
|
// src/ui/App.tsx
|
|
8364
|
-
import { jsx as
|
|
8964
|
+
import { jsx as jsx13, jsxs as jsxs12 } from "react/jsx-runtime";
|
|
8365
8965
|
var EFFORTS = ["low", "medium", "high", "xhigh", "max"];
|
|
8366
8966
|
var STREAM_FRAME_MS = 50;
|
|
8367
8967
|
var MAX_ATTACHMENTS = 8;
|
|
8368
|
-
var NEEDS_IDLE = /* @__PURE__ */ new Set([
|
|
8369
|
-
"clear",
|
|
8370
|
-
"resume",
|
|
8371
|
-
"sessions",
|
|
8372
|
-
"logout",
|
|
8373
|
-
"bypass",
|
|
8374
|
-
"undo",
|
|
8375
|
-
"compact",
|
|
8376
|
-
"checker"
|
|
8377
|
-
]);
|
|
8378
8968
|
function App({
|
|
8379
8969
|
runtime,
|
|
8380
8970
|
initialHistory,
|
|
8381
8971
|
warnings = []
|
|
8382
8972
|
}) {
|
|
8383
8973
|
const { exit } = useApp();
|
|
8974
|
+
const { rows } = useWindowSize2();
|
|
8384
8975
|
const [transcript, setTranscript] = useState5(
|
|
8385
8976
|
() => warnings.reduce((state, text) => pushNotice(state, "warn", text), fromHistory(initialHistory))
|
|
8386
8977
|
);
|
|
@@ -8390,8 +8981,12 @@ function App({
|
|
|
8390
8981
|
() => inputHistoryFromMessages(initialHistory)
|
|
8391
8982
|
);
|
|
8392
8983
|
const [busy, setBusy] = useState5(false);
|
|
8984
|
+
const busyRef = useRef4(false);
|
|
8393
8985
|
const [pendingCount, setPendingCount] = useState5(0);
|
|
8394
8986
|
const queueRef = useRef4([]);
|
|
8987
|
+
const slashQueueRef = useRef4(Promise.resolve());
|
|
8988
|
+
const slashPendingRef = useRef4(0);
|
|
8989
|
+
const drainQueueRef = useRef4(() => void 0);
|
|
8395
8990
|
const [attachments, setAttachments] = useState5([]);
|
|
8396
8991
|
const attachmentsRef = useRef4([]);
|
|
8397
8992
|
const automaticAttachmentTask = useRef4(null);
|
|
@@ -8458,43 +9053,6 @@ function App({
|
|
|
8458
9053
|
const timer = setInterval(() => tick((n) => n + 1), 1e3);
|
|
8459
9054
|
return () => clearInterval(timer);
|
|
8460
9055
|
}, [turnStart]);
|
|
8461
|
-
useEffect2(() => {
|
|
8462
|
-
if (process.platform === "darwin" || process.platform === "win32") return;
|
|
8463
|
-
let last = "";
|
|
8464
|
-
let cancelling = false;
|
|
8465
|
-
let giveUp = false;
|
|
8466
|
-
let polling = false;
|
|
8467
|
-
let interval = null;
|
|
8468
|
-
const poll = () => {
|
|
8469
|
-
if (cancelling || giveUp || polling) return;
|
|
8470
|
-
polling = true;
|
|
8471
|
-
execFile2(
|
|
8472
|
-
"xclip",
|
|
8473
|
-
["-o", "-selection", "primary"],
|
|
8474
|
-
{ encoding: "utf8" },
|
|
8475
|
-
(error, stdout) => {
|
|
8476
|
-
polling = false;
|
|
8477
|
-
if (cancelling) return;
|
|
8478
|
-
if (error) {
|
|
8479
|
-
giveUp = true;
|
|
8480
|
-
if (interval) clearInterval(interval);
|
|
8481
|
-
return;
|
|
8482
|
-
}
|
|
8483
|
-
const sel = stdout.trim();
|
|
8484
|
-
if (sel && sel !== last) {
|
|
8485
|
-
last = sel;
|
|
8486
|
-
copyToClipboard(sel);
|
|
8487
|
-
}
|
|
8488
|
-
}
|
|
8489
|
-
);
|
|
8490
|
-
};
|
|
8491
|
-
poll();
|
|
8492
|
-
interval = setInterval(poll, 750);
|
|
8493
|
-
return () => {
|
|
8494
|
-
cancelling = true;
|
|
8495
|
-
if (interval) clearInterval(interval);
|
|
8496
|
-
};
|
|
8497
|
-
}, []);
|
|
8498
9056
|
const theme = useMemo4(() => makeTheme(accent), [accent]);
|
|
8499
9057
|
const strings = useMemo4(() => stringsFor(lang), [lang]);
|
|
8500
9058
|
const notice = useCallback(
|
|
@@ -8540,8 +9098,10 @@ function App({
|
|
|
8540
9098
|
[]
|
|
8541
9099
|
);
|
|
8542
9100
|
const runAgent = useCallback(async () => {
|
|
9101
|
+
if (busyRef.current) return;
|
|
8543
9102
|
const controller = new AbortController();
|
|
8544
9103
|
abort.current = controller;
|
|
9104
|
+
busyRef.current = true;
|
|
8545
9105
|
setBusy(true);
|
|
8546
9106
|
setTurnStart(Date.now());
|
|
8547
9107
|
turns.current += 1;
|
|
@@ -8577,17 +9137,10 @@ function App({
|
|
|
8577
9137
|
} finally {
|
|
8578
9138
|
flushTranscriptEvents();
|
|
8579
9139
|
abort.current = null;
|
|
9140
|
+
busyRef.current = false;
|
|
8580
9141
|
setBusy(false);
|
|
8581
9142
|
setTurnStart(null);
|
|
8582
|
-
|
|
8583
|
-
if (queue.length > 0) {
|
|
8584
|
-
const [next, ...rest] = queue;
|
|
8585
|
-
queueRef.current = rest;
|
|
8586
|
-
setPendingCount(rest.length);
|
|
8587
|
-
queueMicrotask(() => enqueueNext(next));
|
|
8588
|
-
} else {
|
|
8589
|
-
setPendingCount(0);
|
|
8590
|
-
}
|
|
9143
|
+
drainQueueRef.current();
|
|
8591
9144
|
}
|
|
8592
9145
|
}, [flushTranscriptEvents, notice, queueTranscriptEvent, runtime]);
|
|
8593
9146
|
const applyAccent = useCallback(
|
|
@@ -9161,6 +9714,7 @@ ${strings.mcpAddUsage}`
|
|
|
9161
9714
|
case "compact": {
|
|
9162
9715
|
const controller = new AbortController();
|
|
9163
9716
|
abort.current = controller;
|
|
9717
|
+
busyRef.current = true;
|
|
9164
9718
|
setBusy(true);
|
|
9165
9719
|
setTurnStart(Date.now());
|
|
9166
9720
|
notice("info", strings.compactStarting);
|
|
@@ -9183,17 +9737,9 @@ ${strings.mcpAddUsage}`
|
|
|
9183
9737
|
}
|
|
9184
9738
|
} finally {
|
|
9185
9739
|
abort.current = null;
|
|
9740
|
+
busyRef.current = false;
|
|
9186
9741
|
setBusy(false);
|
|
9187
9742
|
setTurnStart(null);
|
|
9188
|
-
const queue = queueRef.current;
|
|
9189
|
-
if (queue.length > 0) {
|
|
9190
|
-
const [next, ...remaining] = queue;
|
|
9191
|
-
queueRef.current = remaining;
|
|
9192
|
-
setPendingCount(remaining.length);
|
|
9193
|
-
if (next) queueMicrotask(() => enqueueNext(next));
|
|
9194
|
-
} else {
|
|
9195
|
-
setPendingCount(0);
|
|
9196
|
-
}
|
|
9197
9743
|
}
|
|
9198
9744
|
forceRender((n) => n + 1);
|
|
9199
9745
|
return;
|
|
@@ -9396,18 +9942,46 @@ Rename the file if you want a different name.`);
|
|
|
9396
9942
|
},
|
|
9397
9943
|
[applyAccent, appendAttachment, ask2, exit, notice, pick, replaceAttachments, runtime, strings]
|
|
9398
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
|
+
);
|
|
9399
9961
|
const enqueueNext = useCallback(
|
|
9400
9962
|
(item) => {
|
|
9401
9963
|
if (item.kind === "command") {
|
|
9402
|
-
|
|
9964
|
+
runSlash(item.line);
|
|
9403
9965
|
return;
|
|
9404
9966
|
}
|
|
9405
9967
|
setTranscript((state) => pushUser(state, userDisplay(item.text, item.content)));
|
|
9406
9968
|
history.current = [...history.current, { role: "user", content: item.content }];
|
|
9407
9969
|
void runAgent();
|
|
9408
9970
|
},
|
|
9409
|
-
[
|
|
9971
|
+
[runAgent, runSlash]
|
|
9410
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;
|
|
9411
9985
|
const tryQueueAutomaticAttachment = useCallback(
|
|
9412
9986
|
(requestedPath) => {
|
|
9413
9987
|
if (!looksLikeAttachmentPath(requestedPath)) return Promise.resolve(false);
|
|
@@ -9469,12 +10043,12 @@ Rename the file if you want a different name.`);
|
|
|
9469
10043
|
const text = raw.trim();
|
|
9470
10044
|
const submitSlash = () => {
|
|
9471
10045
|
if (!detachedInput) setInput("");
|
|
9472
|
-
if (
|
|
10046
|
+
if (busyRef.current || slashPendingRef.current > 0) {
|
|
9473
10047
|
queueRef.current = [...queueRef.current, { kind: "command", line: text }];
|
|
9474
10048
|
setPendingCount(queueRef.current.length);
|
|
9475
10049
|
return;
|
|
9476
10050
|
}
|
|
9477
|
-
|
|
10051
|
+
runSlash(text);
|
|
9478
10052
|
};
|
|
9479
10053
|
if (isKnownSlashCommand(text)) {
|
|
9480
10054
|
submitSlash();
|
|
@@ -9498,7 +10072,7 @@ Rename the file if you want a different name.`);
|
|
|
9498
10072
|
];
|
|
9499
10073
|
replaceAttachments([]);
|
|
9500
10074
|
const item = { kind: "message", text, content };
|
|
9501
|
-
if (
|
|
10075
|
+
if (busyRef.current || slashPendingRef.current > 0) {
|
|
9502
10076
|
queueRef.current = [...queueRef.current, item];
|
|
9503
10077
|
setPendingCount(queueRef.current.length);
|
|
9504
10078
|
return;
|
|
@@ -9507,7 +10081,7 @@ Rename the file if you want a different name.`);
|
|
|
9507
10081
|
history.current = [...history.current, { role: "user", content }];
|
|
9508
10082
|
void runAgent();
|
|
9509
10083
|
},
|
|
9510
|
-
[
|
|
10084
|
+
[replaceAttachments, runAgent, runSlash, tryQueueAutomaticAttachment]
|
|
9511
10085
|
);
|
|
9512
10086
|
useTerminalInput((_char, key) => {
|
|
9513
10087
|
if (key.tab && key.shift) {
|
|
@@ -9541,10 +10115,10 @@ Rename the file if you want a different name.`);
|
|
|
9541
10115
|
}
|
|
9542
10116
|
setInput("");
|
|
9543
10117
|
});
|
|
9544
|
-
const shell = (children) => /* @__PURE__ */
|
|
10118
|
+
const shell = (children) => /* @__PURE__ */ jsx13(ThemeContext.Provider, { value: theme, children: /* @__PURE__ */ jsx13(StringsContext.Provider, { value: strings, children }) });
|
|
9545
10119
|
if (lang === void 0) {
|
|
9546
10120
|
return shell(
|
|
9547
|
-
/* @__PURE__ */
|
|
10121
|
+
/* @__PURE__ */ jsx13(
|
|
9548
10122
|
LanguagePicker,
|
|
9549
10123
|
{
|
|
9550
10124
|
onPick: (chosen) => {
|
|
@@ -9557,7 +10131,7 @@ Rename the file if you want a different name.`);
|
|
|
9557
10131
|
}
|
|
9558
10132
|
if (setup) {
|
|
9559
10133
|
return shell(
|
|
9560
|
-
/* @__PURE__ */
|
|
10134
|
+
/* @__PURE__ */ jsx13(
|
|
9561
10135
|
Onboarding,
|
|
9562
10136
|
{
|
|
9563
10137
|
onSubmit: async (url, key) => {
|
|
@@ -9572,8 +10146,8 @@ ${strings.configAt(runtime.configPath())}`);
|
|
|
9572
10146
|
);
|
|
9573
10147
|
}
|
|
9574
10148
|
return shell(
|
|
9575
|
-
/* @__PURE__ */ jsxs12(
|
|
9576
|
-
/* @__PURE__ */
|
|
10149
|
+
/* @__PURE__ */ jsxs12(TerminalViewport, { rows, children: [
|
|
10150
|
+
/* @__PURE__ */ jsx13(
|
|
9577
10151
|
Transcript,
|
|
9578
10152
|
{
|
|
9579
10153
|
bubbles: transcript.bubbles,
|
|
@@ -9581,7 +10155,7 @@ ${strings.configAt(runtime.configPath())}`);
|
|
|
9581
10155
|
},
|
|
9582
10156
|
transcriptRevision
|
|
9583
10157
|
),
|
|
9584
|
-
overlay.kind === "permission" && /* @__PURE__ */
|
|
10158
|
+
overlay.kind === "permission" && /* @__PURE__ */ jsx13(
|
|
9585
10159
|
PermissionPrompt,
|
|
9586
10160
|
{
|
|
9587
10161
|
request: overlay.request,
|
|
@@ -9591,7 +10165,7 @@ ${strings.configAt(runtime.configPath())}`);
|
|
|
9591
10165
|
}
|
|
9592
10166
|
}
|
|
9593
10167
|
),
|
|
9594
|
-
overlay.kind === "picker" && /* @__PURE__ */
|
|
10168
|
+
overlay.kind === "picker" && /* @__PURE__ */ jsx13(
|
|
9595
10169
|
Picker,
|
|
9596
10170
|
{
|
|
9597
10171
|
title: overlay.title,
|
|
@@ -9606,7 +10180,7 @@ ${strings.configAt(runtime.configPath())}`);
|
|
|
9606
10180
|
}
|
|
9607
10181
|
}
|
|
9608
10182
|
),
|
|
9609
|
-
overlay.kind === "confirm" && /* @__PURE__ */
|
|
10183
|
+
overlay.kind === "confirm" && /* @__PURE__ */ jsx13(
|
|
9610
10184
|
Confirm,
|
|
9611
10185
|
{
|
|
9612
10186
|
title: overlay.title,
|
|
@@ -9618,7 +10192,7 @@ ${strings.configAt(runtime.configPath())}`);
|
|
|
9618
10192
|
}
|
|
9619
10193
|
}
|
|
9620
10194
|
),
|
|
9621
|
-
overlay.kind === "textinput" && /* @__PURE__ */
|
|
10195
|
+
overlay.kind === "textinput" && /* @__PURE__ */ jsx13(
|
|
9622
10196
|
TextInputOverlay,
|
|
9623
10197
|
{
|
|
9624
10198
|
title: overlay.title,
|
|
@@ -9633,7 +10207,7 @@ ${strings.configAt(runtime.configPath())}`);
|
|
|
9633
10207
|
}
|
|
9634
10208
|
}
|
|
9635
10209
|
),
|
|
9636
|
-
overlay.kind === "none" && /* @__PURE__ */
|
|
10210
|
+
overlay.kind === "none" && /* @__PURE__ */ jsx13(
|
|
9637
10211
|
PromptInput,
|
|
9638
10212
|
{
|
|
9639
10213
|
value: input,
|
|
@@ -9651,7 +10225,7 @@ ${strings.configAt(runtime.configPath())}`);
|
|
|
9651
10225
|
})
|
|
9652
10226
|
}
|
|
9653
10227
|
),
|
|
9654
|
-
/* @__PURE__ */
|
|
10228
|
+
/* @__PURE__ */ jsx13(
|
|
9655
10229
|
StatusBar,
|
|
9656
10230
|
{
|
|
9657
10231
|
status: {
|
|
@@ -9687,9 +10261,9 @@ function TextInputOverlay({
|
|
|
9687
10261
|
useInput2((_input, key) => {
|
|
9688
10262
|
if (key.escape) onCancel();
|
|
9689
10263
|
});
|
|
9690
|
-
return /* @__PURE__ */ jsxs12(
|
|
9691
|
-
/* @__PURE__ */
|
|
9692
|
-
/* @__PURE__ */
|
|
10264
|
+
return /* @__PURE__ */ jsxs12(Box13, { flexDirection: "column", children: [
|
|
10265
|
+
/* @__PURE__ */ jsx13(Text12, { dimColor: true, children: title }),
|
|
10266
|
+
/* @__PURE__ */ jsx13(
|
|
9693
10267
|
TextInput2,
|
|
9694
10268
|
{
|
|
9695
10269
|
value,
|
|
@@ -9698,13 +10272,9 @@ function TextInputOverlay({
|
|
|
9698
10272
|
mask: mask ? "\u2022" : void 0
|
|
9699
10273
|
}
|
|
9700
10274
|
),
|
|
9701
|
-
/* @__PURE__ */
|
|
10275
|
+
/* @__PURE__ */ jsx13(Box13, { marginTop: 1, children: /* @__PURE__ */ jsx13(Text12, { dimColor: true, color: theme.accent, children: "enter submit \xB7 esc cancel" }) })
|
|
9702
10276
|
] });
|
|
9703
10277
|
}
|
|
9704
|
-
function slashNeedsIdle(line) {
|
|
9705
|
-
const [command = "", subcommand = ""] = line.slice(1).trim().toLowerCase().split(/\s+/);
|
|
9706
|
-
return NEEDS_IDLE.has(command) || command === "mcp" && (subcommand === "add" || subcommand === "delete" || subcommand === "enable" || subcommand === "disable");
|
|
9707
|
-
}
|
|
9708
10278
|
function isKnownSlashCommand(line) {
|
|
9709
10279
|
if (!line.startsWith("/")) return false;
|
|
9710
10280
|
const [name = ""] = line.slice(1).trim().toLowerCase().split(/\s+/);
|
|
@@ -9773,38 +10343,9 @@ function unquoteArg(value) {
|
|
|
9773
10343
|
const last = value.at(-1);
|
|
9774
10344
|
return first2 === '"' && last === '"' || first2 === "'" && last === "'" ? value.slice(1, -1) : value;
|
|
9775
10345
|
}
|
|
9776
|
-
function copyToClipboard(text) {
|
|
9777
|
-
const run = (cmd, args) => execFileSync2(cmd, args, { input: text, stdio: ["pipe", "ignore", "ignore"] });
|
|
9778
|
-
if (process.platform === "darwin") {
|
|
9779
|
-
try {
|
|
9780
|
-
run("pbcopy", []);
|
|
9781
|
-
} catch {
|
|
9782
|
-
}
|
|
9783
|
-
return;
|
|
9784
|
-
}
|
|
9785
|
-
if (process.platform === "win32") {
|
|
9786
|
-
try {
|
|
9787
|
-
run("clip", []);
|
|
9788
|
-
} catch {
|
|
9789
|
-
}
|
|
9790
|
-
return;
|
|
9791
|
-
}
|
|
9792
|
-
const candidates = [
|
|
9793
|
-
["wl-copy", []],
|
|
9794
|
-
["xclip", ["-selection", "clipboard"]],
|
|
9795
|
-
["xsel", ["--clipboard", "--input"]]
|
|
9796
|
-
];
|
|
9797
|
-
for (const [cmd, args] of candidates) {
|
|
9798
|
-
try {
|
|
9799
|
-
run(cmd, args);
|
|
9800
|
-
return;
|
|
9801
|
-
} catch {
|
|
9802
|
-
}
|
|
9803
|
-
}
|
|
9804
|
-
}
|
|
9805
10346
|
|
|
9806
10347
|
// src/app/tui.tsx
|
|
9807
|
-
import { jsx as
|
|
10348
|
+
import { jsx as jsx14 } from "react/jsx-runtime";
|
|
9808
10349
|
function clearTerminal() {
|
|
9809
10350
|
if (process.stdout.isTTY) process.stdout.write("\x1B[2J\x1B[3J\x1B[H");
|
|
9810
10351
|
}
|
|
@@ -9817,7 +10358,7 @@ async function startTui(options) {
|
|
|
9817
10358
|
mode: options.mode
|
|
9818
10359
|
});
|
|
9819
10360
|
clearTerminal();
|
|
9820
|
-
const instance = render(/* @__PURE__ */
|
|
10361
|
+
const instance = render(/* @__PURE__ */ jsx14(App, { runtime, initialHistory: history, warnings }), {
|
|
9821
10362
|
incrementalRendering: true,
|
|
9822
10363
|
maxFps: 30
|
|
9823
10364
|
});
|