@kernelonpanic/kitcode 1.1.1 → 1.2.4
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 +5 -4
- package/README.ru.md +5 -5
- package/dist/index.js +1808 -730
- package/dist/index.js.map +1 -1
- package/package.json +2 -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);
|
|
@@ -61,9 +69,6 @@ var diagnosticsSchema = z.object({
|
|
|
61
69
|
autoRun: z.boolean().default(true),
|
|
62
70
|
commands: z.array(z.string().trim().min(1).max(2e4)).max(8).default([])
|
|
63
71
|
});
|
|
64
|
-
var updatesSchema = z.object({
|
|
65
|
-
checkOnStart: z.boolean().default(true)
|
|
66
|
-
});
|
|
67
72
|
var configSchema = z.object({
|
|
68
73
|
version: z.literal(1).default(1),
|
|
69
74
|
model: z.string().optional(),
|
|
@@ -78,12 +83,11 @@ var configSchema = z.object({
|
|
|
78
83
|
maxSubagentsPerTurn: 3
|
|
79
84
|
}),
|
|
80
85
|
diagnostics: diagnosticsSchema.default({ autoRun: true, commands: [] }),
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
mcp: z.record(z.string(), mcpServerSchema).default({})
|
|
86
|
+
providers: z.record(providerIdSchema, providerConfigSchema).default({}),
|
|
87
|
+
permissions: z.record(safeRecordKeySchema, permissionModeSchema).default({}),
|
|
88
|
+
mcp: z.record(safeRecordKeySchema, mcpServerSchema).default({})
|
|
85
89
|
});
|
|
86
|
-
var authSchema = z.record(
|
|
90
|
+
var authSchema = z.record(providerIdSchema, z.string());
|
|
87
91
|
function defaultConfig() {
|
|
88
92
|
return configSchema.parse({});
|
|
89
93
|
}
|
|
@@ -145,19 +149,19 @@ var CONNECTION_REASONS = {
|
|
|
145
149
|
DEPTH_ZERO_SELF_SIGNED_CERT: "the TLS certificate is self-signed",
|
|
146
150
|
UNABLE_TO_VERIFY_LEAF_SIGNATURE: "the TLS certificate chain cannot be verified"
|
|
147
151
|
};
|
|
148
|
-
function describeProviderFailure(error, providerId) {
|
|
152
|
+
function describeProviderFailure(error, providerId, knownSecrets = []) {
|
|
149
153
|
const abort = abortKind(error);
|
|
150
154
|
if (abort === "user") return `Request to "${providerId}" was cancelled.`;
|
|
151
155
|
if (abort === "timeout") {
|
|
152
156
|
return `Request to "${providerId}" ran out of time before the provider answered \u2014 it may be overloaded, try again.`;
|
|
153
157
|
}
|
|
154
158
|
const status = statusOf(error);
|
|
155
|
-
if (status !== void 0) return describeStatus(status, providerId, error);
|
|
156
|
-
const reason = connectionReason(error);
|
|
159
|
+
if (status !== void 0) return describeStatus(status, providerId, error, knownSecrets);
|
|
160
|
+
const reason = connectionReason(error, knownSecrets);
|
|
157
161
|
if (reason) {
|
|
158
162
|
return `Cannot reach "${providerId}": ${reason}. Check the base URL and your network.`;
|
|
159
163
|
}
|
|
160
|
-
return `Request to "${providerId}" failed \u2014 ${snippet(messageOf(error))}`;
|
|
164
|
+
return `Request to "${providerId}" failed \u2014 ${snippet(messageOf(error), 160, knownSecrets)}`;
|
|
161
165
|
}
|
|
162
166
|
function statusOf(error) {
|
|
163
167
|
const value = prop(error, "status") ?? prop(error, "statusCode");
|
|
@@ -169,8 +173,8 @@ function isUserAbort(error, signal) {
|
|
|
169
173
|
function isConnectionFailure(error) {
|
|
170
174
|
return connectionReason(error) !== void 0;
|
|
171
175
|
}
|
|
172
|
-
function toProviderError(error, providerId) {
|
|
173
|
-
return new ProviderError(describeProviderFailure(error, providerId), providerId, statusOf(error), {
|
|
176
|
+
function toProviderError(error, providerId, knownSecrets = []) {
|
|
177
|
+
return new ProviderError(describeProviderFailure(error, providerId, knownSecrets), providerId, statusOf(error), {
|
|
174
178
|
cause: error
|
|
175
179
|
});
|
|
176
180
|
}
|
|
@@ -218,8 +222,8 @@ async function readHead(stream) {
|
|
|
218
222
|
}
|
|
219
223
|
return text;
|
|
220
224
|
}
|
|
221
|
-
async function invalidStreamError(providerId, capture, cause) {
|
|
222
|
-
const excerpt = snippet(await capture.head(), EXCERPT_CHARS);
|
|
225
|
+
async function invalidStreamError(providerId, capture, cause, knownSecrets = []) {
|
|
226
|
+
const excerpt = snippet(await capture.head(), EXCERPT_CHARS, knownSecrets);
|
|
223
227
|
const detail = excerpt === "" ? "the body was empty" : `the body began: ${excerpt}`;
|
|
224
228
|
return new ProviderError(
|
|
225
229
|
`"${providerId}" returned a response that is not a valid streaming chat completion \u2014 no stream events arrived and ${detail}. The endpoint may speak a different protocol than the one configured for it.`,
|
|
@@ -228,8 +232,8 @@ async function invalidStreamError(providerId, capture, cause) {
|
|
|
228
232
|
cause === void 0 ? void 0 : { cause }
|
|
229
233
|
);
|
|
230
234
|
}
|
|
231
|
-
function describeStatus(status, providerId, error) {
|
|
232
|
-
const detail = detailOf(error, status);
|
|
235
|
+
function describeStatus(status, providerId, error, knownSecrets) {
|
|
236
|
+
const detail = detailOf(error, status, knownSecrets);
|
|
233
237
|
const wait = retryAfterSeconds(error);
|
|
234
238
|
const after = wait === void 0 ? void 0 : `retry after ${wait}s`;
|
|
235
239
|
if (status === 401) {
|
|
@@ -264,7 +268,7 @@ function abortKind(error) {
|
|
|
264
268
|
if (name === "AbortError") return "user";
|
|
265
269
|
return void 0;
|
|
266
270
|
}
|
|
267
|
-
function connectionReason(error) {
|
|
271
|
+
function connectionReason(error, knownSecrets = []) {
|
|
268
272
|
const links = chain(error);
|
|
269
273
|
for (const link of links) {
|
|
270
274
|
for (const candidate of [link, ...siblings(link)]) {
|
|
@@ -274,9 +278,11 @@ function connectionReason(error) {
|
|
|
274
278
|
}
|
|
275
279
|
}
|
|
276
280
|
const deepest = links[links.length - 1];
|
|
277
|
-
if (links.length > 1 && deepest instanceof Error)
|
|
281
|
+
if (links.length > 1 && deepest instanceof Error) {
|
|
282
|
+
return snippet(deepest.message, 160, knownSecrets);
|
|
283
|
+
}
|
|
278
284
|
const message = messageOf(error);
|
|
279
|
-
return /fetch failed|connection error|socket|network/i.test(message) ? snippet(message) : void 0;
|
|
285
|
+
return /fetch failed|connection error|socket|network/i.test(message) ? snippet(message, 160, knownSecrets) : void 0;
|
|
280
286
|
}
|
|
281
287
|
function chain(error) {
|
|
282
288
|
const links = [];
|
|
@@ -309,9 +315,9 @@ function header(error, name) {
|
|
|
309
315
|
const value = headers[name];
|
|
310
316
|
return typeof value === "string" ? value : void 0;
|
|
311
317
|
}
|
|
312
|
-
function detailOf(error, status) {
|
|
318
|
+
function detailOf(error, status, knownSecrets) {
|
|
313
319
|
const raw = bodyMessage(error) ?? messageOf(error).replace(new RegExp(`^${status}\\s+`), "");
|
|
314
|
-
const text = snippet(raw);
|
|
320
|
+
const text = snippet(raw, 160, knownSecrets);
|
|
315
321
|
if (text === "" || text === String(status) || text === "status code (no body)") return "";
|
|
316
322
|
return ` \u2014 ${text}`;
|
|
317
323
|
}
|
|
@@ -328,8 +334,8 @@ function messageOf(error) {
|
|
|
328
334
|
if (error === null || error === void 0) return "unknown error";
|
|
329
335
|
return String(error);
|
|
330
336
|
}
|
|
331
|
-
function snippet(text, limit = 160) {
|
|
332
|
-
return redactSecrets(oneLine(text)).slice(0, limit);
|
|
337
|
+
function snippet(text, limit = 160, knownSecrets = []) {
|
|
338
|
+
return redactSecrets(oneLine(text), knownSecrets).slice(0, limit);
|
|
333
339
|
}
|
|
334
340
|
function oneLine(text) {
|
|
335
341
|
return text.replace(/\s+/g, " ").trim();
|
|
@@ -380,8 +386,9 @@ function perMillion(value) {
|
|
|
380
386
|
}
|
|
381
387
|
function positiveInt(value) {
|
|
382
388
|
const parsed = toNumber(value);
|
|
383
|
-
if (parsed === void 0
|
|
384
|
-
|
|
389
|
+
if (parsed === void 0) return void 0;
|
|
390
|
+
const integer = Math.trunc(parsed);
|
|
391
|
+
return integer >= 1 ? integer : void 0;
|
|
385
392
|
}
|
|
386
393
|
function toNumber(value) {
|
|
387
394
|
if (typeof value === "number") return Number.isFinite(value) ? value : void 0;
|
|
@@ -401,7 +408,12 @@ async function detectProvider(rawUrl, apiKey2, opts = {}) {
|
|
|
401
408
|
if (!isAllowedEndpointUrl(baseUrl)) {
|
|
402
409
|
throw new Error("API URL must use https; plain http is only allowed for localhost or 127.0.0.1.");
|
|
403
410
|
}
|
|
404
|
-
const
|
|
411
|
+
const candidateId = opts.name ?? providerIdFromUrl(baseUrl);
|
|
412
|
+
const parsedId = providerIdSchema.safeParse(candidateId);
|
|
413
|
+
if (!parsedId.success) {
|
|
414
|
+
throw new Error(`Invalid provider name "${candidateId}": ${parsedId.error.issues[0]?.message ?? "invalid name"}`);
|
|
415
|
+
}
|
|
416
|
+
const id = parsedId.data;
|
|
405
417
|
if (hostname(baseUrl) === anthropicHost) {
|
|
406
418
|
const probed = await probe(
|
|
407
419
|
fetchImpl,
|
|
@@ -440,7 +452,7 @@ async function detectProvider(rawUrl, apiKey2, opts = {}) {
|
|
|
440
452
|
}
|
|
441
453
|
if (!attempt.ok && attempt.status !== 404 && attempt.status !== 0) break;
|
|
442
454
|
}
|
|
443
|
-
throw new Error(describeFailure(last));
|
|
455
|
+
throw new Error(describeFailure(last, apiKey2));
|
|
444
456
|
}
|
|
445
457
|
function normaliseBaseUrl(rawUrl) {
|
|
446
458
|
return rawUrl.trim().replace(/\/+$/, "");
|
|
@@ -453,7 +465,8 @@ function providerIdFromUrl(url) {
|
|
|
453
465
|
}
|
|
454
466
|
const labels = host.split(".");
|
|
455
467
|
const label = labels.find((part) => part !== "api" && part !== "") ?? "provider";
|
|
456
|
-
|
|
468
|
+
const candidate = label.replace(/[^a-z0-9-]/g, "-").replace(/^-+|-+$/g, "") || "provider";
|
|
469
|
+
return providerIdSchema.safeParse(candidate).success ? candidate : `provider-${candidate}`;
|
|
457
470
|
}
|
|
458
471
|
function isAddressLiteral(host) {
|
|
459
472
|
return host === "localhost" || /^[0-9.]+$/.test(host) || host.includes(":");
|
|
@@ -576,9 +589,9 @@ function stringList(value) {
|
|
|
576
589
|
if (!Array.isArray(value)) return [];
|
|
577
590
|
return value.filter((item) => typeof item === "string");
|
|
578
591
|
}
|
|
579
|
-
function describeFailure(attempt) {
|
|
592
|
+
function describeFailure(attempt, apiKey2) {
|
|
580
593
|
const status = attempt.status === 0 ? "request failed" : `HTTP ${attempt.status}`;
|
|
581
|
-
const snippet2 = redactSecrets(attempt.text.trim()).slice(0, 200);
|
|
594
|
+
const snippet2 = redactSecrets(attempt.text.trim(), [apiKey2]).slice(0, 200);
|
|
582
595
|
const detail = snippet2 === "" ? "" : `: ${snippet2}`;
|
|
583
596
|
return `No OpenAI- or Anthropic-compatible API found at ${attempt.url} (${status})${detail}`;
|
|
584
597
|
}
|
|
@@ -651,7 +664,7 @@ async function isFile(file) {
|
|
|
651
664
|
|
|
652
665
|
// src/config/store.ts
|
|
653
666
|
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";
|
|
667
|
+
import { chmod as chmod2, mkdir as mkdir2, readFile as readFile2, rename as rename2, rm as rm2, writeFile as writeFile2 } from "fs/promises";
|
|
655
668
|
import path3 from "path";
|
|
656
669
|
|
|
657
670
|
// src/config/trust.ts
|
|
@@ -671,23 +684,30 @@ async function isWorkspaceTrusted(cwd) {
|
|
|
671
684
|
}
|
|
672
685
|
async function trustWorkspace(cwd) {
|
|
673
686
|
const workspace = await canonicalWorkspace(cwd);
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
trust.workspaces.
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
687
|
+
await withTrustLock(async () => {
|
|
688
|
+
const trust = await readTrust();
|
|
689
|
+
if (!trust.workspaces.includes(workspace)) {
|
|
690
|
+
trust.workspaces.push(workspace);
|
|
691
|
+
trust.workspaces.sort();
|
|
692
|
+
await writeTrust(trust);
|
|
693
|
+
}
|
|
694
|
+
});
|
|
680
695
|
return workspace;
|
|
681
696
|
}
|
|
682
697
|
async function revokeWorkspaceTrust(cwd) {
|
|
683
698
|
const workspace = await canonicalWorkspace(cwd);
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
699
|
+
await withTrustLock(async () => {
|
|
700
|
+
const trust = await readTrust();
|
|
701
|
+
const workspaces = trust.workspaces.filter((entry) => entry !== workspace);
|
|
702
|
+
if (workspaces.length !== trust.workspaces.length) await writeTrust({ version: 1, workspaces });
|
|
703
|
+
});
|
|
687
704
|
return workspace;
|
|
688
705
|
}
|
|
689
706
|
async function loadTrust() {
|
|
690
707
|
await trustLock;
|
|
708
|
+
return readTrust();
|
|
709
|
+
}
|
|
710
|
+
async function readTrust() {
|
|
691
711
|
let parsed;
|
|
692
712
|
try {
|
|
693
713
|
parsed = JSON.parse(await readFile(trustPath, "utf8"));
|
|
@@ -703,26 +723,29 @@ async function loadTrust() {
|
|
|
703
723
|
workspaces: value.workspaces.filter((entry) => typeof entry === "string")
|
|
704
724
|
};
|
|
705
725
|
}
|
|
706
|
-
async function
|
|
707
|
-
|
|
708
|
-
|
|
726
|
+
async function writeTrust(value) {
|
|
727
|
+
await ensureDir(path2.dirname(trustPath));
|
|
728
|
+
const temp = `${trustPath}.${randomUUID()}.tmp`;
|
|
729
|
+
try {
|
|
730
|
+
await writeFile(temp, `${JSON.stringify(value, null, 2)}
|
|
731
|
+
`, { encoding: "utf8", mode: 384 });
|
|
732
|
+
await rename(temp, trustPath);
|
|
733
|
+
} catch (error) {
|
|
734
|
+
await rm(temp, { force: true });
|
|
735
|
+
throw error;
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
async function withTrustLock(action) {
|
|
739
|
+
const previous = trustLock;
|
|
740
|
+
let release;
|
|
709
741
|
trustLock = new Promise((resolve3) => {
|
|
710
|
-
|
|
742
|
+
release = resolve3;
|
|
711
743
|
});
|
|
712
|
-
await
|
|
744
|
+
await previous;
|
|
713
745
|
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
|
-
}
|
|
746
|
+
return await action();
|
|
724
747
|
} finally {
|
|
725
|
-
|
|
748
|
+
release();
|
|
726
749
|
}
|
|
727
750
|
}
|
|
728
751
|
|
|
@@ -732,9 +755,16 @@ async function configLocation() {
|
|
|
732
755
|
active ??= await resolveConfigLocation();
|
|
733
756
|
return active;
|
|
734
757
|
}
|
|
758
|
+
async function loadConfig(cwd) {
|
|
759
|
+
const location = await resolveConfigLocation(cwd);
|
|
760
|
+
return loadConfigAt(location);
|
|
761
|
+
}
|
|
735
762
|
async function loadProjectConfig(dir) {
|
|
736
763
|
return loadConfigAt({ path: projectConfigPath(dir), scope: "project" });
|
|
737
764
|
}
|
|
765
|
+
async function loadGlobalConfig() {
|
|
766
|
+
return loadConfigAt({ path: configPath, scope: "global" });
|
|
767
|
+
}
|
|
738
768
|
async function loadRuntimeConfig(cwd) {
|
|
739
769
|
const location = await resolveConfigLocation(cwd);
|
|
740
770
|
if (location.scope === "project" && !await isWorkspaceTrusted(cwd)) {
|
|
@@ -754,7 +784,7 @@ async function loadConfigAt(location) {
|
|
|
754
784
|
}
|
|
755
785
|
async function saveConfig(config) {
|
|
756
786
|
const location = await configLocation();
|
|
757
|
-
await writeJsonAtomic(location.path, config);
|
|
787
|
+
await writeJsonAtomic(location.path, config, location.scope === "project" ? void 0 : 384);
|
|
758
788
|
}
|
|
759
789
|
async function initProjectConfig(dir) {
|
|
760
790
|
const location = { path: projectConfigPath(dir), scope: "project" };
|
|
@@ -817,17 +847,24 @@ async function readJson(file) {
|
|
|
817
847
|
}
|
|
818
848
|
}
|
|
819
849
|
async function writeJsonAtomic(file, value, mode) {
|
|
820
|
-
|
|
850
|
+
const parent = path3.dirname(file);
|
|
851
|
+
if (isInside(homeDir, parent)) await ensureDir(parent);
|
|
852
|
+
else await mkdir2(parent, { recursive: true });
|
|
821
853
|
const temp = `${file}.${randomUUID2()}.tmp`;
|
|
822
854
|
try {
|
|
823
855
|
await writeFile2(temp, `${JSON.stringify(value, null, 2)}
|
|
824
856
|
`, { encoding: "utf8", mode });
|
|
825
857
|
await rename2(temp, file);
|
|
858
|
+
if (mode !== void 0) await chmod2(file, mode);
|
|
826
859
|
} catch (error) {
|
|
827
860
|
await rm2(temp, { force: true });
|
|
828
861
|
throw error;
|
|
829
862
|
}
|
|
830
863
|
}
|
|
864
|
+
function isInside(root, candidate) {
|
|
865
|
+
const relative2 = path3.relative(path3.resolve(root), path3.resolve(candidate));
|
|
866
|
+
return relative2 === "" || !path3.isAbsolute(relative2) && relative2.split(path3.sep)[0] !== "..";
|
|
867
|
+
}
|
|
831
868
|
function formatIssues(file, error) {
|
|
832
869
|
const lines = error.issues.map(
|
|
833
870
|
(issue) => ` ${issue.path.join(".") || "(root)"}: ${issue.message}`
|
|
@@ -854,8 +891,10 @@ async function addProvider(url, key, options = {}) {
|
|
|
854
891
|
await initProjectConfig(cwd);
|
|
855
892
|
await trustWorkspace(cwd);
|
|
856
893
|
}
|
|
857
|
-
const config = options.local ? await loadProjectConfig(cwd) :
|
|
894
|
+
const config = options.local ? await loadProjectConfig(cwd) : process.env.KITCODE_CONFIG ? await loadConfig(cwd) : await loadGlobalConfig();
|
|
858
895
|
const auth = await loadAuth();
|
|
896
|
+
const configBefore = structuredClone(config);
|
|
897
|
+
const authBefore = { ...auth };
|
|
859
898
|
config.providers[detected.id] = detected.config;
|
|
860
899
|
auth[detected.id] = key;
|
|
861
900
|
const models = detected.models;
|
|
@@ -863,8 +902,16 @@ async function addProvider(url, key, options = {}) {
|
|
|
863
902
|
const chosen = pickDefault(models);
|
|
864
903
|
if (chosen) config.model = formatModelRef(detected.id, chosen);
|
|
865
904
|
}
|
|
866
|
-
|
|
867
|
-
|
|
905
|
+
try {
|
|
906
|
+
await saveConfig(config);
|
|
907
|
+
await saveAuth(auth);
|
|
908
|
+
} catch (error) {
|
|
909
|
+
Object.assign(config, configBefore);
|
|
910
|
+
for (const id of Object.keys(auth)) delete auth[id];
|
|
911
|
+
Object.assign(auth, authBefore);
|
|
912
|
+
await Promise.allSettled([saveConfig(config), saveAuth(auth)]);
|
|
913
|
+
throw error;
|
|
914
|
+
}
|
|
868
915
|
const location = await configLocation();
|
|
869
916
|
const protocol = detected.config.type === "anthropic" ? "Anthropic" : "OpenAI-compatible";
|
|
870
917
|
const rows = [
|
|
@@ -971,7 +1018,7 @@ function skipControlString(text, from, bellTerminates) {
|
|
|
971
1018
|
|
|
972
1019
|
// src/app/runtime.ts
|
|
973
1020
|
import path13 from "path";
|
|
974
|
-
import { mkdir as
|
|
1021
|
+
import { mkdir as mkdir6 } from "fs/promises";
|
|
975
1022
|
|
|
976
1023
|
// src/core/session.ts
|
|
977
1024
|
import { randomBytes } from "crypto";
|
|
@@ -997,8 +1044,13 @@ async function saveSession(state) {
|
|
|
997
1044
|
await ensureDir(sessionsDir);
|
|
998
1045
|
const file = path4.join(sessionsDir, `${state.id}.json`);
|
|
999
1046
|
const temp = `${file}.${process.pid}.${randomBytes(4).toString("hex")}.tmp`;
|
|
1000
|
-
|
|
1001
|
-
|
|
1047
|
+
try {
|
|
1048
|
+
await writeFile3(temp, JSON.stringify(state), { encoding: "utf8", mode: 384 });
|
|
1049
|
+
await atomicRename(temp, file);
|
|
1050
|
+
} catch (error) {
|
|
1051
|
+
await unlink(temp).catch(() => void 0);
|
|
1052
|
+
throw error;
|
|
1053
|
+
}
|
|
1002
1054
|
}
|
|
1003
1055
|
async function atomicRename(from, to, retries = 5) {
|
|
1004
1056
|
for (let attempt = 0; attempt < retries; attempt++) {
|
|
@@ -1172,7 +1224,9 @@ function readState(raw) {
|
|
|
1172
1224
|
}
|
|
1173
1225
|
if (typeof parsed !== "object" || parsed === null) return null;
|
|
1174
1226
|
const state = parsed;
|
|
1175
|
-
if (typeof state.id !== "string" || !
|
|
1227
|
+
if (typeof state.id !== "string" || !validSessionId(state.id)) return null;
|
|
1228
|
+
const messages = readMessages(state.messages);
|
|
1229
|
+
if (!messages) return null;
|
|
1176
1230
|
return {
|
|
1177
1231
|
id: state.id,
|
|
1178
1232
|
title: typeof state.title === "string" ? state.title.slice(0, 120) : void 0,
|
|
@@ -1180,11 +1234,59 @@ function readState(raw) {
|
|
|
1180
1234
|
createdAt: typeof state.createdAt === "string" ? state.createdAt : "",
|
|
1181
1235
|
updatedAt: typeof state.updatedAt === "string" ? state.updatedAt : "",
|
|
1182
1236
|
model: typeof state.model === "string" ? state.model : "",
|
|
1183
|
-
messages
|
|
1184
|
-
usage:
|
|
1237
|
+
messages,
|
|
1238
|
+
usage: readUsageEntries(state.usage),
|
|
1185
1239
|
context: readContextUsage(state.context)
|
|
1186
1240
|
};
|
|
1187
1241
|
}
|
|
1242
|
+
function readMessages(value) {
|
|
1243
|
+
if (!Array.isArray(value)) return null;
|
|
1244
|
+
const messages = [];
|
|
1245
|
+
for (const item of value) {
|
|
1246
|
+
if (typeof item !== "object" || item === null) return null;
|
|
1247
|
+
const candidate = item;
|
|
1248
|
+
if (candidate.role !== "user" && candidate.role !== "assistant") return null;
|
|
1249
|
+
if (!Array.isArray(candidate.content)) return null;
|
|
1250
|
+
const content = [];
|
|
1251
|
+
for (const block of candidate.content) {
|
|
1252
|
+
if (!validContentBlock(block)) return null;
|
|
1253
|
+
content.push(block);
|
|
1254
|
+
}
|
|
1255
|
+
messages.push({ role: candidate.role, content });
|
|
1256
|
+
}
|
|
1257
|
+
return messages;
|
|
1258
|
+
}
|
|
1259
|
+
function validContentBlock(value) {
|
|
1260
|
+
if (typeof value !== "object" || value === null) return false;
|
|
1261
|
+
const block = value;
|
|
1262
|
+
switch (block.type) {
|
|
1263
|
+
case "text":
|
|
1264
|
+
case "thinking":
|
|
1265
|
+
return typeof block.text === "string";
|
|
1266
|
+
case "tool_use":
|
|
1267
|
+
return typeof block.id === "string" && typeof block.name === "string";
|
|
1268
|
+
case "tool_result":
|
|
1269
|
+
return typeof block.toolUseId === "string" && typeof block.content === "string" && (block.isError === void 0 || typeof block.isError === "boolean");
|
|
1270
|
+
case "image":
|
|
1271
|
+
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));
|
|
1272
|
+
case "file":
|
|
1273
|
+
return typeof block.name === "string" && typeof block.mediaType === "string" && typeof block.text === "string";
|
|
1274
|
+
default:
|
|
1275
|
+
return false;
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
function readUsageEntries(value) {
|
|
1279
|
+
if (!Array.isArray(value)) return [];
|
|
1280
|
+
return value.flatMap((item) => {
|
|
1281
|
+
if (typeof item !== "object" || item === null) return [];
|
|
1282
|
+
const candidate = item;
|
|
1283
|
+
const usage = readUsage(candidate.usage);
|
|
1284
|
+
if (typeof candidate.model !== "string" || !usage || !Number.isInteger(candidate.requests) || candidate.requests < 0) {
|
|
1285
|
+
return [];
|
|
1286
|
+
}
|
|
1287
|
+
return [{ model: candidate.model, usage, requests: candidate.requests, costUsd: null }];
|
|
1288
|
+
});
|
|
1289
|
+
}
|
|
1188
1290
|
async function resolveExportTarget(destination, state) {
|
|
1189
1291
|
const resolved = path4.resolve(destination);
|
|
1190
1292
|
const info = await stat2(resolved).catch(() => null);
|
|
@@ -1202,12 +1304,12 @@ function exportFileName(state) {
|
|
|
1202
1304
|
}
|
|
1203
1305
|
function renderSessionMarkdown(state) {
|
|
1204
1306
|
const lines = [
|
|
1205
|
-
`# ${state.title || `KitCode session ${shortSessionId(state.id)}`}`,
|
|
1307
|
+
`# ${escapeMarkdownInline(state.title || `KitCode session ${shortSessionId(state.id)}`)}`,
|
|
1206
1308
|
"",
|
|
1207
|
-
`- Session: ${state.id}`,
|
|
1208
|
-
`- Workspace: ${state.cwd}`,
|
|
1209
|
-
`- Model: ${state.model || "unknown"}`,
|
|
1210
|
-
`- Updated: ${state.updatedAt}`,
|
|
1309
|
+
`- Session: ${escapeMarkdownInline(state.id)}`,
|
|
1310
|
+
`- Workspace: ${escapeMarkdownInline(state.cwd)}`,
|
|
1311
|
+
`- Model: ${escapeMarkdownInline(state.model || "unknown")}`,
|
|
1312
|
+
`- Updated: ${escapeMarkdownInline(state.updatedAt)}`,
|
|
1211
1313
|
""
|
|
1212
1314
|
];
|
|
1213
1315
|
for (const message of state.messages) {
|
|
@@ -1222,18 +1324,41 @@ function renderMessage(content) {
|
|
|
1222
1324
|
const sections = [];
|
|
1223
1325
|
for (const block of content) {
|
|
1224
1326
|
if (block.type === "text") sections.push(block.text);
|
|
1225
|
-
else if (block.type === "image")
|
|
1226
|
-
|
|
1327
|
+
else if (block.type === "image") {
|
|
1328
|
+
sections.push(`[Image attached: ${escapeMarkdownInline(block.name)}]`);
|
|
1329
|
+
} else if (block.type === "file") {
|
|
1330
|
+
sections.push(
|
|
1331
|
+
`[File attached: ${escapeMarkdownInline(block.name)}]
|
|
1227
1332
|
|
|
1228
|
-
${block.text}`
|
|
1229
|
-
|
|
1230
|
-
else if (block.type === "
|
|
1333
|
+
${markdownCodeFence(block.text)}`
|
|
1334
|
+
);
|
|
1335
|
+
} else if (block.type === "tool_use") {
|
|
1336
|
+
sections.push(`> Tool: ${escapeMarkdownInline(block.name)}`);
|
|
1337
|
+
} else if (block.type === "tool_result") {
|
|
1338
|
+
const result = block.content.replace(/\r\n?/g, "\n").split("\n").map((line) => `> ${line}`).join("\n");
|
|
1231
1339
|
sections.push(`> Tool result${block.isError ? " (error)" : ""}:
|
|
1232
|
-
|
|
1340
|
+
${result}`);
|
|
1233
1341
|
}
|
|
1234
1342
|
}
|
|
1235
1343
|
return sections.join("\n\n").trim();
|
|
1236
1344
|
}
|
|
1345
|
+
function escapeMarkdownInline(value) {
|
|
1346
|
+
return value.replace(/[\r\n\t]+/g, " ").replace(/([\\`*_[\]<>])/g, "\\$1");
|
|
1347
|
+
}
|
|
1348
|
+
function markdownCodeFence(value) {
|
|
1349
|
+
const normalized = value.replace(/\r\n?/g, "\n");
|
|
1350
|
+
const backtickLength = longestMarkerRun(normalized, /`+/g) + 1;
|
|
1351
|
+
const tildeLength = longestMarkerRun(normalized, /~+/g) + 1;
|
|
1352
|
+
const marker = backtickLength <= tildeLength ? "`" : "~";
|
|
1353
|
+
const fence = marker.repeat(Math.max(3, Math.min(backtickLength, tildeLength)));
|
|
1354
|
+
return `${fence}
|
|
1355
|
+
${normalized}${normalized.endsWith("\n") ? "" : "\n"}${fence}`;
|
|
1356
|
+
}
|
|
1357
|
+
function longestMarkerRun(value, pattern) {
|
|
1358
|
+
let longest = 0;
|
|
1359
|
+
for (const match of value.matchAll(pattern)) longest = Math.max(longest, match[0].length);
|
|
1360
|
+
return longest;
|
|
1361
|
+
}
|
|
1237
1362
|
function readContextUsage(value) {
|
|
1238
1363
|
if (typeof value !== "object" || value === null) return void 0;
|
|
1239
1364
|
const candidate = value;
|
|
@@ -1245,7 +1370,9 @@ function readUsage(value) {
|
|
|
1245
1370
|
if (typeof value !== "object" || value === null) return void 0;
|
|
1246
1371
|
const usage = value;
|
|
1247
1372
|
const fields = ["input", "output", "cacheWrite", "cacheRead"];
|
|
1248
|
-
if (fields.some(
|
|
1373
|
+
if (fields.some(
|
|
1374
|
+
(field2) => typeof usage[field2] !== "number" || !Number.isFinite(usage[field2]) || usage[field2] < 0
|
|
1375
|
+
)) {
|
|
1249
1376
|
return void 0;
|
|
1250
1377
|
}
|
|
1251
1378
|
return {
|
|
@@ -1276,10 +1403,13 @@ async function resolveSessionId(query) {
|
|
|
1276
1403
|
);
|
|
1277
1404
|
}
|
|
1278
1405
|
function assertSessionId(id) {
|
|
1279
|
-
if (
|
|
1406
|
+
if (!validSessionId(id)) {
|
|
1280
1407
|
throw new Error(`Invalid session id: ${id}`);
|
|
1281
1408
|
}
|
|
1282
1409
|
}
|
|
1410
|
+
function validSessionId(id) {
|
|
1411
|
+
return /^[A-Za-z0-9_-]{1,240}$/.test(id);
|
|
1412
|
+
}
|
|
1283
1413
|
|
|
1284
1414
|
// src/core/agent.ts
|
|
1285
1415
|
var MAX_PAUSE_RESUMES = 5;
|
|
@@ -1455,11 +1585,20 @@ function retryAfterFromError(error) {
|
|
|
1455
1585
|
}
|
|
1456
1586
|
function sleep(ms, signal) {
|
|
1457
1587
|
return new Promise((resolve3, reject) => {
|
|
1458
|
-
|
|
1459
|
-
|
|
1588
|
+
if (signal.aborted) {
|
|
1589
|
+
reject(signal.reason ?? new Error("Aborted"));
|
|
1590
|
+
return;
|
|
1591
|
+
}
|
|
1592
|
+
const finish = () => {
|
|
1593
|
+
signal.removeEventListener("abort", onAbort);
|
|
1594
|
+
resolve3();
|
|
1595
|
+
};
|
|
1596
|
+
const onAbort = () => {
|
|
1460
1597
|
clearTimeout(id);
|
|
1461
1598
|
reject(signal.reason ?? new Error("Aborted"));
|
|
1462
|
-
}
|
|
1599
|
+
};
|
|
1600
|
+
const id = setTimeout(finish, ms);
|
|
1601
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
1463
1602
|
});
|
|
1464
1603
|
}
|
|
1465
1604
|
function estimateRequestTokens(cfg, messages) {
|
|
@@ -1478,9 +1617,7 @@ function estimateRequestTokens(cfg, messages) {
|
|
|
1478
1617
|
async function runToolCalls(cfg, content, hooks, signal) {
|
|
1479
1618
|
const calls = content.filter((block) => block.type === "tool_use");
|
|
1480
1619
|
const results = [];
|
|
1481
|
-
for (const call of calls)
|
|
1482
|
-
results.push(await runOneCall(cfg, call, hooks, signal));
|
|
1483
|
-
}
|
|
1620
|
+
for (const call of calls) results.push(await runOneCall(cfg, call, hooks, signal));
|
|
1484
1621
|
return results;
|
|
1485
1622
|
}
|
|
1486
1623
|
async function runOneCall(cfg, call, hooks, signal) {
|
|
@@ -1608,6 +1745,7 @@ function costOf(usage, pricing) {
|
|
|
1608
1745
|
}
|
|
1609
1746
|
|
|
1610
1747
|
// src/core/budget.ts
|
|
1748
|
+
var UNKNOWN_MODEL_PRICING = { input: 3, output: 15 };
|
|
1611
1749
|
function createTurnBudget(limits, resolvePricing = pricingFor) {
|
|
1612
1750
|
let usage = emptyUsage();
|
|
1613
1751
|
let costUsd = 0;
|
|
@@ -1645,7 +1783,7 @@ function createTurnBudget(limits, resolvePricing = pricingFor) {
|
|
|
1645
1783
|
const pricing = resolvePricing(request.modelRef);
|
|
1646
1784
|
if (current.costUsd !== null) {
|
|
1647
1785
|
const remainingUsd = limits.maxCostUsdPerTurn - current.costUsd;
|
|
1648
|
-
const effectivePricing = pricing ??
|
|
1786
|
+
const effectivePricing = pricing ?? UNKNOWN_MODEL_PRICING;
|
|
1649
1787
|
const inputUsd = estimatedInput * effectivePricing.input / 1e6;
|
|
1650
1788
|
const affordableOutput = Math.floor(
|
|
1651
1789
|
(remainingUsd - inputUsd) * 1e6 / effectivePricing.output
|
|
@@ -1661,9 +1799,10 @@ function createTurnBudget(limits, resolvePricing = pricingFor) {
|
|
|
1661
1799
|
return { allowed: true, maxOutputTokens: Math.floor(outputLimit) };
|
|
1662
1800
|
},
|
|
1663
1801
|
record(modelRef, next) {
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1802
|
+
const normalized = normalizeUsage(next);
|
|
1803
|
+
usage = addUsage(usage, normalized);
|
|
1804
|
+
const priced = costOf(normalized, resolvePricing(modelRef) ?? UNKNOWN_MODEL_PRICING);
|
|
1805
|
+
costUsd = (costUsd ?? 0) + (priced ?? 0);
|
|
1667
1806
|
},
|
|
1668
1807
|
snapshot
|
|
1669
1808
|
};
|
|
@@ -1686,7 +1825,7 @@ import { createReadStream } from "fs";
|
|
|
1686
1825
|
import {
|
|
1687
1826
|
chmod as chmod3,
|
|
1688
1827
|
lstat,
|
|
1689
|
-
mkdir as
|
|
1828
|
+
mkdir as mkdir3,
|
|
1690
1829
|
readFile as readFile4,
|
|
1691
1830
|
readdir as readdir2,
|
|
1692
1831
|
rename as rename4,
|
|
@@ -1928,7 +2067,7 @@ async function restoreFile(root, relativePath, snapshot, expected) {
|
|
|
1928
2067
|
const safe = resolveInside(root, relativePath);
|
|
1929
2068
|
if (!safe.ok || safe.relative !== relativePath) throw new Error("The restore path changed.");
|
|
1930
2069
|
const data = decodeSnapshot(snapshot);
|
|
1931
|
-
await
|
|
2070
|
+
await mkdir3(path5.dirname(safe.path), { recursive: true });
|
|
1932
2071
|
const rechecked = resolveInside(root, relativePath);
|
|
1933
2072
|
if (!rechecked.ok || rechecked.path !== safe.path || rechecked.relative !== relativePath) {
|
|
1934
2073
|
throw new Error("The restore path changed while preparing its parent directory.");
|
|
@@ -2372,17 +2511,15 @@ async function loadAttachment(cwd, requestedPath) {
|
|
|
2372
2511
|
}
|
|
2373
2512
|
async function loadAutomaticAttachment(cwd, requestedPath) {
|
|
2374
2513
|
if (!looksLikeAttachmentPath(requestedPath)) return null;
|
|
2375
|
-
const
|
|
2514
|
+
const requested = resolveAttachmentPath(cwd, requestedPath);
|
|
2515
|
+
const safe = resolveInside(cwd, requested);
|
|
2516
|
+
if (!safe.ok || safe.relative === "") return null;
|
|
2517
|
+
const resolved = safe.path;
|
|
2376
2518
|
if (isSensitiveAutomaticPath(resolved)) {
|
|
2377
2519
|
throw new Error(
|
|
2378
2520
|
`For safety, sensitive-looking files must be attached explicitly with /attach: ${path7.basename(resolved)}`
|
|
2379
2521
|
);
|
|
2380
2522
|
}
|
|
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
2523
|
const linkInfo = await lstat2(resolved).catch(() => null);
|
|
2387
2524
|
if (linkInfo?.isSymbolicLink()) return null;
|
|
2388
2525
|
const info = await stat5(resolved).catch((error) => {
|
|
@@ -2400,8 +2537,8 @@ function looksLikeAttachmentPath(value) {
|
|
|
2400
2537
|
if (path7.isAbsolute(candidate)) return true;
|
|
2401
2538
|
if (/^(?:~|\.{1,2})[\\/]/.test(candidate)) return true;
|
|
2402
2539
|
if (candidate.includes("/") || candidate.includes("\\")) return true;
|
|
2403
|
-
const
|
|
2404
|
-
return path7.extname(
|
|
2540
|
+
const basename3 = path7.basename(candidate).toLowerCase();
|
|
2541
|
+
return path7.extname(basename3) !== "" || AUTO_PATH_NAMES.has(basename3);
|
|
2405
2542
|
}
|
|
2406
2543
|
async function loadClipboardImage(platform = process.platform, runner = runClipboardCommand) {
|
|
2407
2544
|
const commands = clipboardCommands(platform);
|
|
@@ -2477,8 +2614,8 @@ function resolveAttachmentPath(cwd, requestedPath) {
|
|
|
2477
2614
|
return path7.isAbsolute(expanded) ? path7.normalize(expanded) : path7.resolve(cwd, expanded);
|
|
2478
2615
|
}
|
|
2479
2616
|
function isSensitiveAutomaticPath(file) {
|
|
2480
|
-
const
|
|
2481
|
-
return
|
|
2617
|
+
const basename3 = path7.basename(file).toLowerCase();
|
|
2618
|
+
return basename3.startsWith(".env.") || SENSITIVE_AUTO_NAMES.has(basename3) || SENSITIVE_AUTO_EXTENSIONS.has(path7.extname(basename3));
|
|
2482
2619
|
}
|
|
2483
2620
|
function attachmentLabel(block) {
|
|
2484
2621
|
if (block.type === "image") return `image: ${block.name}`;
|
|
@@ -2661,47 +2798,90 @@ function formatBytes(value) {
|
|
|
2661
2798
|
|
|
2662
2799
|
// src/core/compact.ts
|
|
2663
2800
|
var KEEP_USER_TURNS = 2;
|
|
2664
|
-
var
|
|
2665
|
-
var
|
|
2666
|
-
var
|
|
2801
|
+
var SINGLE_REQUEST_MAX_CHARS = 8e4;
|
|
2802
|
+
var CHUNK_TARGET_CHARS = 35e3;
|
|
2803
|
+
var CHUNK_OVERLAP_MESSAGES = 2;
|
|
2804
|
+
var CHUNK_OVERLAP_MAX_CHARS = 4e3;
|
|
2805
|
+
var MERGE_TARGET_CHARS = 6e4;
|
|
2806
|
+
var MAX_USER_TEXT_CHARS = 12e3;
|
|
2807
|
+
var MAX_ASSISTANT_TEXT_CHARS = 6e3;
|
|
2808
|
+
var MAX_FILE_BLOCK_CHARS = 2e3;
|
|
2809
|
+
var MAX_TOOL_RESULT_CHARS = 1200;
|
|
2810
|
+
var MAX_TOOL_ERROR_CHARS = 2e3;
|
|
2811
|
+
var MAX_TOOL_INPUT_CHARS = 800;
|
|
2812
|
+
var SINGLE_MAX_TOKENS = 768;
|
|
2813
|
+
var CHUNK_MAX_TOKENS = 512;
|
|
2814
|
+
var MERGE_MAX_TOKENS = 1024;
|
|
2815
|
+
var CHUNK_CONCURRENCY = 3;
|
|
2816
|
+
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.";
|
|
2817
|
+
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
2818
|
async function compactHistory(options) {
|
|
2668
2819
|
const cut = compactCutIndex(options.history);
|
|
2669
2820
|
if (cut <= 0) {
|
|
2670
2821
|
return { history: options.history, compacted: false, removedMessages: 0 };
|
|
2671
2822
|
}
|
|
2672
|
-
const
|
|
2673
|
-
|
|
2823
|
+
const oldMessages = options.history.slice(0, cut);
|
|
2824
|
+
const renderedMessages = renderMessages(oldMessages);
|
|
2825
|
+
const rendered = renderedMessages.join("\n\n");
|
|
2826
|
+
if (!rendered.trim()) {
|
|
2674
2827
|
return { history: options.history, compacted: false, removedMessages: 0 };
|
|
2675
2828
|
}
|
|
2676
|
-
let summary
|
|
2677
|
-
let finalText2 = "";
|
|
2829
|
+
let summary;
|
|
2678
2830
|
let usage;
|
|
2679
2831
|
let rateLimits;
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2832
|
+
if (rendered.length <= SINGLE_REQUEST_MAX_CHARS) {
|
|
2833
|
+
const requestMax = singleOutputLimit(options.maxTokens, options.maxTotalOutputTokens);
|
|
2834
|
+
const result = await summarizeText(
|
|
2835
|
+
options.provider,
|
|
2836
|
+
options.model,
|
|
2837
|
+
options.maxTokens,
|
|
2838
|
+
options.signal,
|
|
2839
|
+
rendered,
|
|
2840
|
+
SYSTEM_SUMMARIZE,
|
|
2841
|
+
requestMax
|
|
2842
|
+
);
|
|
2843
|
+
summary = result.text;
|
|
2844
|
+
usage = result.usage;
|
|
2845
|
+
rateLimits = result.rateLimits;
|
|
2846
|
+
} else {
|
|
2847
|
+
const chunks = chunkRenderedMessages(
|
|
2848
|
+
renderedMessages,
|
|
2849
|
+
CHUNK_TARGET_CHARS,
|
|
2850
|
+
CHUNK_OVERLAP_MESSAGES,
|
|
2851
|
+
CHUNK_OVERLAP_MAX_CHARS
|
|
2852
|
+
);
|
|
2853
|
+
const limits = chunkOutputLimits(
|
|
2854
|
+
chunks.length,
|
|
2855
|
+
options.maxTokens,
|
|
2856
|
+
options.maxTotalOutputTokens
|
|
2857
|
+
);
|
|
2858
|
+
const chunkResults = await summarizeSourcesParallel(
|
|
2859
|
+
options.provider,
|
|
2860
|
+
options.model,
|
|
2861
|
+
options.maxTokens,
|
|
2862
|
+
options.signal,
|
|
2863
|
+
chunks,
|
|
2864
|
+
SYSTEM_SUMMARIZE,
|
|
2865
|
+
limits.chunk
|
|
2866
|
+
);
|
|
2867
|
+
const chunkUsages = chunkResults.map((result) => result.usage).filter(Boolean);
|
|
2868
|
+
usage = chunkUsages.length > 0 ? mergeUsage(chunkUsages) : void 0;
|
|
2869
|
+
rateLimits = [...chunkResults].reverse().find((r) => r.rateLimits)?.rateLimits;
|
|
2870
|
+
const mergeResult = await mergePartialSummaries(
|
|
2871
|
+
options.provider,
|
|
2872
|
+
options.model,
|
|
2873
|
+
options.maxTokens,
|
|
2874
|
+
options.signal,
|
|
2875
|
+
chunkResults.map((result) => result.text),
|
|
2876
|
+
limits.merge
|
|
2877
|
+
);
|
|
2878
|
+
summary = mergeResult.text;
|
|
2879
|
+
if (mergeResult.usage) {
|
|
2880
|
+
usage = usage ? mergeUsage([usage, mergeResult.usage]) : mergeResult.usage;
|
|
2702
2881
|
}
|
|
2882
|
+
if (mergeResult.rateLimits) rateLimits = mergeResult.rateLimits;
|
|
2703
2883
|
}
|
|
2704
|
-
summary =
|
|
2884
|
+
summary = summary.trim();
|
|
2705
2885
|
if (!summary) throw new Error("The provider returned an empty context summary.");
|
|
2706
2886
|
const prefix = [
|
|
2707
2887
|
{
|
|
@@ -2733,11 +2913,34 @@ function compactCutIndex(history) {
|
|
|
2733
2913
|
if (userTurns.length <= KEEP_USER_TURNS) return 0;
|
|
2734
2914
|
return userTurns[userTurns.length - KEEP_USER_TURNS] ?? 0;
|
|
2735
2915
|
}
|
|
2736
|
-
function
|
|
2916
|
+
function estimateCompactBudget(history, maxTokens = MERGE_MAX_TOKENS) {
|
|
2737
2917
|
const cut = compactCutIndex(history);
|
|
2738
|
-
if (cut <= 0) return 1;
|
|
2739
|
-
const
|
|
2740
|
-
|
|
2918
|
+
if (cut <= 0) return { inputTokens: 1, maxOutputTokens: 0 };
|
|
2919
|
+
const renderedMessages = renderMessages(history.slice(0, cut));
|
|
2920
|
+
const rendered = renderedMessages.join("\n\n");
|
|
2921
|
+
if (rendered.length <= SINGLE_REQUEST_MAX_CHARS) {
|
|
2922
|
+
return {
|
|
2923
|
+
inputTokens: Math.max(1, rendered.length + 1e3),
|
|
2924
|
+
maxOutputTokens: Math.max(1, Math.min(SINGLE_MAX_TOKENS, Math.floor(maxTokens)))
|
|
2925
|
+
};
|
|
2926
|
+
}
|
|
2927
|
+
const chunks = chunkRenderedMessages(
|
|
2928
|
+
renderedMessages,
|
|
2929
|
+
CHUNK_TARGET_CHARS,
|
|
2930
|
+
CHUNK_OVERLAP_MESSAGES,
|
|
2931
|
+
CHUNK_OVERLAP_MAX_CHARS
|
|
2932
|
+
);
|
|
2933
|
+
const chunkInput = chunks.reduce((total, chunk) => total + chunk.length, 0);
|
|
2934
|
+
const chunkOutput = Math.max(1, Math.min(CHUNK_MAX_TOKENS, Math.floor(maxTokens)));
|
|
2935
|
+
const mergeOutput = Math.max(1, Math.min(MERGE_MAX_TOKENS, Math.floor(maxTokens)));
|
|
2936
|
+
const mergeRequests = Math.max(0, chunks.length - 1);
|
|
2937
|
+
const totalOutput = chunks.length * chunkOutput + mergeRequests * mergeOutput;
|
|
2938
|
+
const intermediateOutput = chunks.length * chunkOutput + Math.max(0, mergeRequests - 1) * mergeOutput;
|
|
2939
|
+
const requestCount = chunks.length + mergeRequests;
|
|
2940
|
+
return {
|
|
2941
|
+
inputTokens: Math.max(1, chunkInput + intermediateOutput * 8 + requestCount * 1e3),
|
|
2942
|
+
maxOutputTokens: totalOutput
|
|
2943
|
+
};
|
|
2741
2944
|
}
|
|
2742
2945
|
function isConversationUser(message) {
|
|
2743
2946
|
if (message.role !== "user") return false;
|
|
@@ -2745,50 +2948,249 @@ function isConversationUser(message) {
|
|
|
2745
2948
|
(block) => block.type === "image" || block.type === "file" || block.type === "text" && !block.text.startsWith("[Earlier conversation summary]")
|
|
2746
2949
|
);
|
|
2747
2950
|
}
|
|
2748
|
-
function
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2951
|
+
function chunkRenderedMessages(messages, targetChars, overlapMessages, overlapMaxChars) {
|
|
2952
|
+
const units = messages.flatMap((message) => splitLongMessage(message, targetChars));
|
|
2953
|
+
const chunks = [];
|
|
2954
|
+
let current = [];
|
|
2955
|
+
const sizeOf = (parts) => parts.reduce((total, part) => total + part.length, 0) + Math.max(0, parts.length - 1) * 2;
|
|
2956
|
+
for (const unit of units) {
|
|
2957
|
+
if (current.length > 0 && sizeOf([...current, unit]) > targetChars) {
|
|
2958
|
+
chunks.push(current.join("\n\n"));
|
|
2959
|
+
const overlap = [];
|
|
2960
|
+
let overlapChars = 0;
|
|
2961
|
+
for (let index = current.length - 1; index >= 0 && overlap.length < overlapMessages; index -= 1) {
|
|
2962
|
+
const candidate = current[index];
|
|
2963
|
+
if (overlapChars + candidate.length > overlapMaxChars) break;
|
|
2964
|
+
overlap.unshift(candidate);
|
|
2965
|
+
overlapChars += candidate.length;
|
|
2966
|
+
}
|
|
2967
|
+
current = overlap;
|
|
2968
|
+
while (current.length > 0 && sizeOf([...current, unit]) > targetChars) current.shift();
|
|
2758
2969
|
}
|
|
2759
|
-
|
|
2970
|
+
current.push(unit);
|
|
2760
2971
|
}
|
|
2761
|
-
|
|
2972
|
+
if (current.length > 0) chunks.push(current.join("\n\n"));
|
|
2973
|
+
return chunks;
|
|
2974
|
+
}
|
|
2975
|
+
function splitLongMessage(message, targetChars) {
|
|
2976
|
+
if (message.length <= targetChars) return [message];
|
|
2977
|
+
const parts = [];
|
|
2978
|
+
let remaining = message;
|
|
2979
|
+
const payloadLimit = Math.max(1, targetChars - 32);
|
|
2980
|
+
while (remaining.length > payloadLimit) {
|
|
2981
|
+
let cut = remaining.lastIndexOf("\n", payloadLimit);
|
|
2982
|
+
if (cut < payloadLimit * 0.6) cut = payloadLimit;
|
|
2983
|
+
parts.push(`${remaining.slice(0, cut)}
|
|
2984
|
+
[message continues]`);
|
|
2985
|
+
remaining = `[continued message]
|
|
2986
|
+
${remaining.slice(cut).replace(/^\n/, "")}`;
|
|
2987
|
+
}
|
|
2988
|
+
if (remaining) parts.push(remaining);
|
|
2989
|
+
return parts;
|
|
2990
|
+
}
|
|
2991
|
+
async function summarizeSourcesParallel(provider, model, maxTokens, signal, sources, systemPrompt, maxOutputTokens) {
|
|
2992
|
+
const results = new Array(sources.length);
|
|
2993
|
+
const controller = new AbortController();
|
|
2994
|
+
const abort = () => controller.abort(signal.reason);
|
|
2995
|
+
if (signal.aborted) abort();
|
|
2996
|
+
else signal.addEventListener("abort", abort, { once: true });
|
|
2997
|
+
let next = 0;
|
|
2998
|
+
const worker = async () => {
|
|
2999
|
+
for (; ; ) {
|
|
3000
|
+
const index = next++;
|
|
3001
|
+
if (index >= sources.length) return;
|
|
3002
|
+
try {
|
|
3003
|
+
results[index] = await summarizeText(
|
|
3004
|
+
provider,
|
|
3005
|
+
model,
|
|
3006
|
+
maxTokens,
|
|
3007
|
+
controller.signal,
|
|
3008
|
+
sources[index],
|
|
3009
|
+
systemPrompt,
|
|
3010
|
+
maxOutputTokens
|
|
3011
|
+
);
|
|
3012
|
+
} catch (error) {
|
|
3013
|
+
controller.abort(error);
|
|
3014
|
+
throw error;
|
|
3015
|
+
}
|
|
3016
|
+
}
|
|
3017
|
+
};
|
|
3018
|
+
try {
|
|
3019
|
+
await Promise.all(
|
|
3020
|
+
Array.from({ length: Math.min(CHUNK_CONCURRENCY, sources.length) }, () => worker())
|
|
3021
|
+
);
|
|
3022
|
+
return results;
|
|
3023
|
+
} finally {
|
|
3024
|
+
signal.removeEventListener("abort", abort);
|
|
3025
|
+
}
|
|
3026
|
+
}
|
|
3027
|
+
async function mergePartialSummaries(provider, model, maxTokens, signal, summaries, mergeMaxTokens) {
|
|
3028
|
+
let current = summaries;
|
|
3029
|
+
const usages = [];
|
|
3030
|
+
let rateLimits;
|
|
3031
|
+
while (current.length > 1) {
|
|
3032
|
+
const groups = groupSummaries(current, MERGE_TARGET_CHARS);
|
|
3033
|
+
const sources = groups.map(
|
|
3034
|
+
(group) => group.map((summary, index) => `## Part ${index + 1}
|
|
3035
|
+
${summary}`).join("\n\n")
|
|
3036
|
+
);
|
|
3037
|
+
const merged = await summarizeSourcesParallel(
|
|
3038
|
+
provider,
|
|
3039
|
+
model,
|
|
3040
|
+
maxTokens,
|
|
3041
|
+
signal,
|
|
3042
|
+
sources,
|
|
3043
|
+
SYSTEM_MERGE,
|
|
3044
|
+
mergeMaxTokens
|
|
3045
|
+
);
|
|
3046
|
+
for (const result of merged) {
|
|
3047
|
+
if (!result.text) throw new Error("The provider returned an empty partial context summary.");
|
|
3048
|
+
if (result.usage) usages.push(result.usage);
|
|
3049
|
+
if (result.rateLimits) rateLimits = result.rateLimits;
|
|
3050
|
+
}
|
|
3051
|
+
current = merged.map((result) => result.text);
|
|
3052
|
+
}
|
|
3053
|
+
return {
|
|
3054
|
+
text: current[0] ?? "",
|
|
3055
|
+
usage: usages.length > 0 ? mergeUsage(usages) : void 0,
|
|
3056
|
+
rateLimits
|
|
3057
|
+
};
|
|
3058
|
+
}
|
|
3059
|
+
function singleOutputLimit(maxTokens, totalBudget) {
|
|
3060
|
+
const requested = Math.max(1, Math.min(SINGLE_MAX_TOKENS, Math.floor(maxTokens)));
|
|
3061
|
+
if (totalBudget === void 0) return requested;
|
|
3062
|
+
const allowed = Math.floor(totalBudget);
|
|
3063
|
+
if (allowed < 1) throw new Error("The remaining turn budget is too small to compact context.");
|
|
3064
|
+
return Math.min(requested, allowed);
|
|
3065
|
+
}
|
|
3066
|
+
function chunkOutputLimits(chunkCount, maxTokens, totalBudget) {
|
|
3067
|
+
let chunk = Math.max(1, Math.min(CHUNK_MAX_TOKENS, Math.floor(maxTokens)));
|
|
3068
|
+
let merge = Math.max(1, Math.min(MERGE_MAX_TOKENS, Math.floor(maxTokens)));
|
|
3069
|
+
if (totalBudget === void 0) return { chunk, merge };
|
|
3070
|
+
const mergeRequests = Math.max(0, chunkCount - 1);
|
|
3071
|
+
const requestCount = chunkCount + mergeRequests;
|
|
3072
|
+
const allowed = Math.floor(totalBudget);
|
|
3073
|
+
if (allowed < requestCount) {
|
|
3074
|
+
throw new Error("The remaining turn budget is too small for all context-compaction requests.");
|
|
3075
|
+
}
|
|
3076
|
+
const requested = chunkCount * chunk + mergeRequests * merge;
|
|
3077
|
+
if (allowed >= requested) return { chunk, merge };
|
|
3078
|
+
const scale = allowed / requested;
|
|
3079
|
+
chunk = Math.max(1, Math.floor(chunk * scale));
|
|
3080
|
+
merge = Math.max(1, Math.floor(merge * scale));
|
|
3081
|
+
const total = () => chunkCount * chunk + mergeRequests * merge;
|
|
3082
|
+
while (total() > allowed) {
|
|
3083
|
+
if (mergeRequests > 0 && merge > 1 && merge >= chunk) merge -= 1;
|
|
3084
|
+
else if (chunk > 1) chunk -= 1;
|
|
3085
|
+
else if (merge > 1) merge -= 1;
|
|
3086
|
+
else throw new Error("Could not fit context compaction into the remaining turn budget.");
|
|
3087
|
+
}
|
|
3088
|
+
return { chunk, merge };
|
|
3089
|
+
}
|
|
3090
|
+
function groupSummaries(summaries, targetChars) {
|
|
3091
|
+
const groups = [];
|
|
3092
|
+
let current = [];
|
|
3093
|
+
let size = 0;
|
|
3094
|
+
for (const summary of summaries) {
|
|
3095
|
+
if (current.length >= 2 && size + 12 + summary.length > targetChars) {
|
|
3096
|
+
groups.push(current);
|
|
3097
|
+
current = [];
|
|
3098
|
+
size = 0;
|
|
3099
|
+
}
|
|
3100
|
+
const separator = current.length > 0 ? 12 : 0;
|
|
3101
|
+
current.push(summary);
|
|
3102
|
+
size += separator + summary.length;
|
|
3103
|
+
}
|
|
3104
|
+
if (current.length > 0) groups.push(current);
|
|
3105
|
+
return groups;
|
|
3106
|
+
}
|
|
3107
|
+
async function summarizeText(provider, model, maxTokens, signal, source, systemPrompt, maxOutputTokens) {
|
|
3108
|
+
if (signal.aborted) throw signal.reason ?? new Error("Context compaction was cancelled.");
|
|
3109
|
+
let summary = "";
|
|
3110
|
+
let finalText2 = "";
|
|
3111
|
+
let usage;
|
|
3112
|
+
let rateLimits;
|
|
3113
|
+
const stream = provider.stream({
|
|
3114
|
+
model,
|
|
3115
|
+
system: systemPrompt,
|
|
3116
|
+
messages: [{ role: "user", content: [{ type: "text", text: source }] }],
|
|
3117
|
+
tools: [],
|
|
3118
|
+
maxTokens: Math.max(1, Math.floor(Math.min(maxOutputTokens, maxTokens))),
|
|
3119
|
+
thinking: false,
|
|
3120
|
+
signal
|
|
3121
|
+
});
|
|
3122
|
+
for await (const event of stream) {
|
|
3123
|
+
if (event.type === "text_delta") summary += event.text;
|
|
3124
|
+
else if (event.type === "usage") usage = event.usage;
|
|
3125
|
+
else if (event.type === "rate_limits") rateLimits = event.limits;
|
|
3126
|
+
else if (event.type === "done") {
|
|
3127
|
+
finalText2 = event.content.filter((block) => block.type === "text").map((block) => block.type === "text" ? block.text : "").join("");
|
|
3128
|
+
}
|
|
3129
|
+
}
|
|
3130
|
+
if (signal.aborted) throw signal.reason ?? new Error("Context compaction was cancelled.");
|
|
3131
|
+
return { text: (finalText2 || summary).trim(), usage, rateLimits };
|
|
3132
|
+
}
|
|
3133
|
+
function mergeUsage(usages) {
|
|
3134
|
+
return usages.reduce(
|
|
3135
|
+
(acc, u) => ({
|
|
3136
|
+
input: acc.input + u.input,
|
|
3137
|
+
output: acc.output + u.output,
|
|
3138
|
+
cacheWrite: acc.cacheWrite + u.cacheWrite,
|
|
3139
|
+
cacheRead: acc.cacheRead + u.cacheRead
|
|
3140
|
+
}),
|
|
3141
|
+
{ input: 0, output: 0, cacheWrite: 0, cacheRead: 0 }
|
|
3142
|
+
);
|
|
3143
|
+
}
|
|
3144
|
+
function renderMessages(history) {
|
|
3145
|
+
return history.flatMap((message) => {
|
|
3146
|
+
const blocks = message.content.map((block) => renderBlock(block, message.role)).filter(Boolean);
|
|
3147
|
+
return blocks.length > 0 ? [`${message.role.toUpperCase()}:
|
|
3148
|
+
${blocks.join("\n")}`] : [];
|
|
3149
|
+
});
|
|
2762
3150
|
}
|
|
2763
|
-
function renderBlock(block) {
|
|
3151
|
+
function renderBlock(block, role) {
|
|
2764
3152
|
switch (block.type) {
|
|
2765
3153
|
case "text":
|
|
2766
|
-
return truncate(
|
|
3154
|
+
return truncate(
|
|
3155
|
+
block.text,
|
|
3156
|
+
role === "user" ? MAX_USER_TEXT_CHARS : MAX_ASSISTANT_TEXT_CHARS
|
|
3157
|
+
);
|
|
2767
3158
|
case "thinking":
|
|
2768
3159
|
return "";
|
|
2769
3160
|
case "image":
|
|
2770
|
-
return `[
|
|
3161
|
+
return `[image: ${block.name}]`;
|
|
2771
3162
|
case "file":
|
|
2772
|
-
return `[
|
|
2773
|
-
${truncate(block.text,
|
|
3163
|
+
return `[file: ${block.name}]
|
|
3164
|
+
${truncate(block.text, MAX_FILE_BLOCK_CHARS)}`;
|
|
2774
3165
|
case "tool_use":
|
|
2775
|
-
return `[
|
|
2776
|
-
${safeJson(block.input)}`;
|
|
3166
|
+
return `[tool: ${block.name}(${truncate(safeJson(block.input), MAX_TOOL_INPUT_CHARS)})]`;
|
|
2777
3167
|
case "tool_result":
|
|
2778
|
-
|
|
2779
|
-
${truncate(block.content,
|
|
3168
|
+
if (block.isError) {
|
|
3169
|
+
return `[tool result error: ${truncate(block.content, MAX_TOOL_ERROR_CHARS)}]`;
|
|
3170
|
+
}
|
|
3171
|
+
if (isTrivialSuccess(block.content)) return "";
|
|
3172
|
+
return truncate(block.content, MAX_TOOL_RESULT_CHARS);
|
|
2780
3173
|
}
|
|
2781
3174
|
}
|
|
3175
|
+
function isTrivialSuccess(content) {
|
|
3176
|
+
const t = content.trim().toLowerCase();
|
|
3177
|
+
if (!t || t === "ok" || t === "done" || t === "success") return true;
|
|
3178
|
+
if (/^no matches found/.test(t)) return true;
|
|
3179
|
+
if (t === "(no output)") return true;
|
|
3180
|
+
return false;
|
|
3181
|
+
}
|
|
2782
3182
|
function safeJson(value) {
|
|
2783
3183
|
try {
|
|
2784
|
-
return
|
|
3184
|
+
return JSON.stringify(value);
|
|
2785
3185
|
} catch {
|
|
2786
|
-
return "[unserializable
|
|
3186
|
+
return "[unserializable]";
|
|
2787
3187
|
}
|
|
2788
3188
|
}
|
|
2789
3189
|
function truncate(value, max) {
|
|
2790
|
-
|
|
2791
|
-
|
|
3190
|
+
if (value.length <= max) return value;
|
|
3191
|
+
const keep = Math.floor(max * 0.4);
|
|
3192
|
+
return `${value.slice(0, max - keep)}
|
|
3193
|
+
...${value.slice(-keep)}`;
|
|
2792
3194
|
}
|
|
2793
3195
|
|
|
2794
3196
|
// src/core/prompt.ts
|
|
@@ -3050,7 +3452,7 @@ function clip(value) {
|
|
|
3050
3452
|
// package.json
|
|
3051
3453
|
var package_default = {
|
|
3052
3454
|
name: "@kernelonpanic/kitcode",
|
|
3053
|
-
version: "1.
|
|
3455
|
+
version: "1.2.4",
|
|
3054
3456
|
description: "Terminal coding agent with a config you never have to write by hand",
|
|
3055
3457
|
type: "module",
|
|
3056
3458
|
license: "MIT",
|
|
@@ -3091,6 +3493,7 @@ var package_default = {
|
|
|
3091
3493
|
"ink-text-input": "^6.0.0",
|
|
3092
3494
|
openai: "^7.3.0",
|
|
3093
3495
|
react: "^19.2.8",
|
|
3496
|
+
"string-width": "^8.2.2",
|
|
3094
3497
|
zod: "^4.4.3"
|
|
3095
3498
|
},
|
|
3096
3499
|
devDependencies: {
|
|
@@ -3117,8 +3520,7 @@ var package_default = {
|
|
|
3117
3520
|
|
|
3118
3521
|
// src/version.ts
|
|
3119
3522
|
var KITCODE_VERSION = package_default.version;
|
|
3120
|
-
var
|
|
3121
|
-
var KITCODE_COMMIT = true ? "a57e54102a5e1ca1e60cec4ee9c629c890d27057" : "development";
|
|
3523
|
+
var KITCODE_COMMIT = true ? "84441dfba8c67477cf72b76552e72ffedc8d41a8" : "development";
|
|
3122
3524
|
|
|
3123
3525
|
// src/mcp/client.ts
|
|
3124
3526
|
var clientInfo = { name: "kitcode", version: KITCODE_VERSION };
|
|
@@ -3140,7 +3542,11 @@ var inheritedEnvKeys = /* @__PURE__ */ new Set([
|
|
|
3140
3542
|
"COMSPEC",
|
|
3141
3543
|
"PATHEXT",
|
|
3142
3544
|
"APPDATA",
|
|
3143
|
-
"LOCALAPPDATA"
|
|
3545
|
+
"LOCALAPPDATA",
|
|
3546
|
+
"USERPROFILE",
|
|
3547
|
+
"HOMEDRIVE",
|
|
3548
|
+
"HOMEPATH",
|
|
3549
|
+
"USERNAME"
|
|
3144
3550
|
]);
|
|
3145
3551
|
var blockedEnvKeys = /* @__PURE__ */ new Set([
|
|
3146
3552
|
"ANTHROPIC_API_KEY",
|
|
@@ -3255,9 +3661,9 @@ function createMcpManager(servers) {
|
|
|
3255
3661
|
serverStates.delete(name);
|
|
3256
3662
|
},
|
|
3257
3663
|
async close() {
|
|
3258
|
-
const
|
|
3664
|
+
const open3 = [...sessions.values()];
|
|
3259
3665
|
sessions.clear();
|
|
3260
|
-
await Promise.all(
|
|
3666
|
+
await Promise.all(open3.map((session) => session.client.close().catch(() => {
|
|
3261
3667
|
})));
|
|
3262
3668
|
},
|
|
3263
3669
|
states() {
|
|
@@ -3317,8 +3723,11 @@ function configuredSecrets(config) {
|
|
|
3317
3723
|
}
|
|
3318
3724
|
function inheritedEnv(source = process.env) {
|
|
3319
3725
|
const env = {};
|
|
3726
|
+
const allowed = new Set([...inheritedEnvKeys].map((key) => key.toUpperCase()));
|
|
3727
|
+
const blocked = new Set([...blockedEnvKeys].map((key) => key.toUpperCase()));
|
|
3320
3728
|
for (const [key, value] of Object.entries(source)) {
|
|
3321
|
-
|
|
3729
|
+
const normalized = key.toUpperCase();
|
|
3730
|
+
if (value !== void 0 && allowed.has(normalized) && !blocked.has(normalized)) {
|
|
3322
3731
|
env[key] = value;
|
|
3323
3732
|
}
|
|
3324
3733
|
}
|
|
@@ -3352,7 +3761,10 @@ async function fetchProviderBalance(config, apiKey2, options = {}) {
|
|
|
3352
3761
|
},
|
|
3353
3762
|
signal: controller.signal
|
|
3354
3763
|
});
|
|
3355
|
-
if (!response.ok)
|
|
3764
|
+
if (!response.ok) {
|
|
3765
|
+
await response.body?.cancel().catch(() => void 0);
|
|
3766
|
+
continue;
|
|
3767
|
+
}
|
|
3356
3768
|
const text = await limitedResponseText(response, MAX_RESPONSE_BYTES);
|
|
3357
3769
|
if (text === null) continue;
|
|
3358
3770
|
const balance = endpoint2.parse(JSON.parse(text));
|
|
@@ -3467,7 +3879,10 @@ function finiteNumber(value) {
|
|
|
3467
3879
|
}
|
|
3468
3880
|
async function limitedResponseText(response, limit) {
|
|
3469
3881
|
const declared = Number(response.headers.get("content-length"));
|
|
3470
|
-
if (Number.isFinite(declared) && declared > limit)
|
|
3882
|
+
if (Number.isFinite(declared) && declared > limit) {
|
|
3883
|
+
await response.body?.cancel().catch(() => void 0);
|
|
3884
|
+
return null;
|
|
3885
|
+
}
|
|
3471
3886
|
if (!response.body) return "";
|
|
3472
3887
|
const reader = response.body.getReader();
|
|
3473
3888
|
const decoder = new TextDecoder();
|
|
@@ -3517,11 +3932,34 @@ function cacheFile(providerId) {
|
|
|
3517
3932
|
}
|
|
3518
3933
|
async function readCache(file) {
|
|
3519
3934
|
try {
|
|
3520
|
-
|
|
3935
|
+
const parsed = JSON.parse(await readFile7(file, "utf8"));
|
|
3936
|
+
if (typeof parsed !== "object" || parsed === null) return void 0;
|
|
3937
|
+
const candidate = parsed;
|
|
3938
|
+
if (typeof candidate.fetchedAt !== "number" || !Number.isFinite(candidate.fetchedAt) || !Array.isArray(candidate.models) || candidate.models.length > 1e4 || !candidate.models.every(validModel)) {
|
|
3939
|
+
return void 0;
|
|
3940
|
+
}
|
|
3941
|
+
return candidate;
|
|
3521
3942
|
} catch {
|
|
3522
3943
|
return void 0;
|
|
3523
3944
|
}
|
|
3524
3945
|
}
|
|
3946
|
+
function validModel(value) {
|
|
3947
|
+
if (typeof value !== "object" || value === null) return false;
|
|
3948
|
+
const model = value;
|
|
3949
|
+
if (typeof model.id !== "string" || model.id === "") return false;
|
|
3950
|
+
if (model.name !== void 0 && typeof model.name !== "string") return false;
|
|
3951
|
+
if (model.contextWindow !== void 0 && (!Number.isInteger(model.contextWindow) || model.contextWindow < 1)) {
|
|
3952
|
+
return false;
|
|
3953
|
+
}
|
|
3954
|
+
if (model.pricing === void 0) return true;
|
|
3955
|
+
const prices = [
|
|
3956
|
+
model.pricing.input,
|
|
3957
|
+
model.pricing.output,
|
|
3958
|
+
model.pricing.cacheWrite,
|
|
3959
|
+
model.pricing.cacheRead
|
|
3960
|
+
].filter((price2) => price2 !== void 0);
|
|
3961
|
+
return prices.length >= 2 && prices.every((price2) => Number.isFinite(price2) && price2 >= 0);
|
|
3962
|
+
}
|
|
3525
3963
|
async function writeCache(file, models) {
|
|
3526
3964
|
const payload = { version: CACHE_VERSION, fetchedAt: Date.now(), models };
|
|
3527
3965
|
try {
|
|
@@ -3661,12 +4099,12 @@ function createAnthropicProvider(args) {
|
|
|
3661
4099
|
return {
|
|
3662
4100
|
id: args.id,
|
|
3663
4101
|
kind: "anthropic",
|
|
3664
|
-
stream: (req) => streamTurn(client, args.id, req),
|
|
3665
|
-
listModels: () => listModels(client, args.id),
|
|
4102
|
+
stream: (req) => streamTurn(client, args.id, args.apiKey, req),
|
|
4103
|
+
listModels: () => listModels(client, args.id, args.apiKey),
|
|
3666
4104
|
knownModels: () => KNOWN_MODELS
|
|
3667
4105
|
};
|
|
3668
4106
|
}
|
|
3669
|
-
async function* streamTurn(client, providerId, req) {
|
|
4107
|
+
async function* streamTurn(client, providerId, apiKey2, req) {
|
|
3670
4108
|
const params = {
|
|
3671
4109
|
model: req.model,
|
|
3672
4110
|
max_tokens: req.maxTokens,
|
|
@@ -3707,9 +4145,9 @@ async function* streamTurn(client, providerId, req) {
|
|
|
3707
4145
|
const limits2 = parseRateLimits(capture.headers());
|
|
3708
4146
|
if (limits2) yield { type: "rate_limits", limits: limits2 };
|
|
3709
4147
|
if (events === 0 && capture.succeeded() && !isConnectionFailure(error)) {
|
|
3710
|
-
throw await invalidStreamError(providerId, capture, error);
|
|
4148
|
+
throw await invalidStreamError(providerId, capture, error, [apiKey2]);
|
|
3711
4149
|
}
|
|
3712
|
-
throw toProviderError(error, providerId);
|
|
4150
|
+
throw toProviderError(error, providerId, [apiKey2]);
|
|
3713
4151
|
}
|
|
3714
4152
|
const stopReason = STOP_REASONS[final.stop_reason ?? ""] ?? "end_turn";
|
|
3715
4153
|
const content = toContentBlocks(final.content);
|
|
@@ -3728,7 +4166,7 @@ async function* streamTurn(client, providerId, req) {
|
|
|
3728
4166
|
...stopReason === "refusal" ? { refusal: toRefusal(final.stop_details) } : {}
|
|
3729
4167
|
};
|
|
3730
4168
|
}
|
|
3731
|
-
async function listModels(client, providerId) {
|
|
4169
|
+
async function listModels(client, providerId, apiKey2) {
|
|
3732
4170
|
try {
|
|
3733
4171
|
const models = [];
|
|
3734
4172
|
for await (const model of client.models.list({ limit: 100 })) {
|
|
@@ -3742,7 +4180,7 @@ async function listModels(client, providerId) {
|
|
|
3742
4180
|
}
|
|
3743
4181
|
return models;
|
|
3744
4182
|
} catch (error) {
|
|
3745
|
-
throw toProviderError(error, providerId);
|
|
4183
|
+
throw toProviderError(error, providerId, [apiKey2]);
|
|
3746
4184
|
}
|
|
3747
4185
|
}
|
|
3748
4186
|
function toMessageParams(messages) {
|
|
@@ -3846,12 +4284,12 @@ function createOpenAiProvider(args) {
|
|
|
3846
4284
|
return {
|
|
3847
4285
|
id: args.id,
|
|
3848
4286
|
kind: "openai",
|
|
3849
|
-
stream: (req) => streamTurn2(client, args.id, req),
|
|
3850
|
-
listModels: () => listModels2(client, args.id),
|
|
4287
|
+
stream: (req) => streamTurn2(client, args.id, args.apiKey, req),
|
|
4288
|
+
listModels: () => listModels2(client, args.id, args.apiKey),
|
|
3851
4289
|
knownModels: () => []
|
|
3852
4290
|
};
|
|
3853
4291
|
}
|
|
3854
|
-
async function* streamTurn2(client, providerId, req) {
|
|
4292
|
+
async function* streamTurn2(client, providerId, apiKey2, req) {
|
|
3855
4293
|
let text = "";
|
|
3856
4294
|
let thinking = "";
|
|
3857
4295
|
let finishReason = null;
|
|
@@ -3899,19 +4337,20 @@ async function* streamTurn2(client, providerId, req) {
|
|
|
3899
4337
|
if (!isUserAbort(error, req.signal)) {
|
|
3900
4338
|
const limits2 = parseRateLimits(capture.headers());
|
|
3901
4339
|
if (limits2) yield { type: "rate_limits", limits: limits2 };
|
|
3902
|
-
throw toProviderError(error, providerId);
|
|
4340
|
+
throw toProviderError(error, providerId, [apiKey2]);
|
|
3903
4341
|
}
|
|
3904
4342
|
aborted = true;
|
|
3905
4343
|
}
|
|
3906
|
-
const content = toContentBlocks2(thinking, text, calls);
|
|
3907
4344
|
if (aborted || req.signal?.aborted) {
|
|
4345
|
+
const content2 = toTextContentBlocks(thinking, text);
|
|
3908
4346
|
if (sawUsage) yield { type: "usage", usage };
|
|
3909
4347
|
const limits2 = parseRateLimits(capture.headers());
|
|
3910
4348
|
if (limits2) yield { type: "rate_limits", limits: limits2 };
|
|
3911
|
-
yield { type: "done", stopReason: "aborted", content };
|
|
4349
|
+
yield { type: "done", stopReason: "aborted", content: content2 };
|
|
3912
4350
|
return;
|
|
3913
4351
|
}
|
|
3914
|
-
if (recognised === 0) throw await invalidStreamError(providerId, capture);
|
|
4352
|
+
if (recognised === 0) throw await invalidStreamError(providerId, capture, void 0, [apiKey2]);
|
|
4353
|
+
const content = toContentBlocks2(providerId, thinking, text, calls);
|
|
3915
4354
|
for (const block of content) {
|
|
3916
4355
|
if (block.type === "tool_use") {
|
|
3917
4356
|
yield { type: "tool_call", id: block.id, name: block.name, input: block.input };
|
|
@@ -3928,7 +4367,7 @@ function resolveStop(finishReason, hasCalls) {
|
|
|
3928
4367
|
if (hasCalls) return "tool_use";
|
|
3929
4368
|
return mapped ?? "end_turn";
|
|
3930
4369
|
}
|
|
3931
|
-
async function listModels2(client, providerId) {
|
|
4370
|
+
async function listModels2(client, providerId, apiKey2) {
|
|
3932
4371
|
try {
|
|
3933
4372
|
const models = [];
|
|
3934
4373
|
for await (const model of client.models.list()) {
|
|
@@ -3937,7 +4376,7 @@ async function listModels2(client, providerId) {
|
|
|
3937
4376
|
}
|
|
3938
4377
|
return models;
|
|
3939
4378
|
} catch (error) {
|
|
3940
|
-
throw toProviderError(error, providerId);
|
|
4379
|
+
throw toProviderError(error, providerId, [apiKey2]);
|
|
3941
4380
|
}
|
|
3942
4381
|
}
|
|
3943
4382
|
function accumulate(calls, delta) {
|
|
@@ -3947,10 +4386,8 @@ function accumulate(calls, delta) {
|
|
|
3947
4386
|
if (delta.function?.arguments) call.args += delta.function.arguments;
|
|
3948
4387
|
calls.set(delta.index, call);
|
|
3949
4388
|
}
|
|
3950
|
-
function toContentBlocks2(thinking, text, calls) {
|
|
3951
|
-
const content =
|
|
3952
|
-
if (thinking) content.push({ type: "thinking", text: thinking });
|
|
3953
|
-
if (text) content.push({ type: "text", text });
|
|
4389
|
+
function toContentBlocks2(providerId, thinking, text, calls) {
|
|
4390
|
+
const content = toTextContentBlocks(thinking, text);
|
|
3954
4391
|
const sorted = [...calls.entries()].sort((a, b) => a[0] - b[0]);
|
|
3955
4392
|
const tmpId = (position) => `call_${position}`;
|
|
3956
4393
|
for (const [position, [, call]] of sorted.entries()) {
|
|
@@ -3958,18 +4395,34 @@ function toContentBlocks2(thinking, text, calls) {
|
|
|
3958
4395
|
type: "tool_use",
|
|
3959
4396
|
id: call.id || tmpId(position),
|
|
3960
4397
|
name: call.name,
|
|
3961
|
-
input: parseArguments(call.args)
|
|
4398
|
+
input: parseArguments(providerId, call.name, call.args)
|
|
3962
4399
|
});
|
|
3963
4400
|
}
|
|
3964
4401
|
return content;
|
|
3965
4402
|
}
|
|
3966
|
-
function
|
|
3967
|
-
|
|
4403
|
+
function toTextContentBlocks(thinking, text) {
|
|
4404
|
+
const content = [];
|
|
4405
|
+
if (thinking) content.push({ type: "thinking", text: thinking });
|
|
4406
|
+
if (text) content.push({ type: "text", text });
|
|
4407
|
+
return content;
|
|
4408
|
+
}
|
|
4409
|
+
function parseArguments(providerId, toolName, args) {
|
|
4410
|
+
let parsed;
|
|
3968
4411
|
try {
|
|
3969
|
-
|
|
4412
|
+
parsed = JSON.parse(args);
|
|
3970
4413
|
} catch {
|
|
3971
|
-
|
|
4414
|
+
throw new ProviderError(
|
|
4415
|
+
`Tool "${toolName || "unknown"}" returned malformed JSON arguments; the tool was not run.`,
|
|
4416
|
+
providerId
|
|
4417
|
+
);
|
|
3972
4418
|
}
|
|
4419
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
4420
|
+
throw new ProviderError(
|
|
4421
|
+
`Tool "${toolName || "unknown"}" arguments must be a JSON object; the tool was not run.`,
|
|
4422
|
+
providerId
|
|
4423
|
+
);
|
|
4424
|
+
}
|
|
4425
|
+
return parsed;
|
|
3973
4426
|
}
|
|
3974
4427
|
function toChatMessages(system, messages) {
|
|
3975
4428
|
const out = [{ role: "system", content: system }];
|
|
@@ -4102,7 +4555,7 @@ function createPermissionEngine(configPermissions) {
|
|
|
4102
4555
|
decide(tool, requested) {
|
|
4103
4556
|
const configured = resolve3(tool, requested);
|
|
4104
4557
|
if (configured === "deny") return "deny";
|
|
4105
|
-
if (current === "plan" &&
|
|
4558
|
+
if (current === "plan" && tool.readOnly !== true) return "deny";
|
|
4106
4559
|
if (configured === "allow") return "allow";
|
|
4107
4560
|
if (bypassEnabled) return "allow";
|
|
4108
4561
|
if (current === "accept" && isFileEdit(tool)) return "allow";
|
|
@@ -4114,8 +4567,9 @@ function createPermissionEngine(configPermissions) {
|
|
|
4114
4567
|
},
|
|
4115
4568
|
denyReason(tool, requested) {
|
|
4116
4569
|
const configured = resolve3(tool, requested);
|
|
4570
|
+
if (current === "plan" && tool.readOnly !== true) return PLAN_REFUSAL;
|
|
4117
4571
|
if (configured === "deny" || configured === "allow") return void 0;
|
|
4118
|
-
return
|
|
4572
|
+
return void 0;
|
|
4119
4573
|
},
|
|
4120
4574
|
bypass: {
|
|
4121
4575
|
enable() {
|
|
@@ -4145,7 +4599,133 @@ function isFileEdit(tool) {
|
|
|
4145
4599
|
}
|
|
4146
4600
|
|
|
4147
4601
|
// src/tools/edit.ts
|
|
4148
|
-
import {
|
|
4602
|
+
import { readFile as readFile8, stat as stat6 } from "fs/promises";
|
|
4603
|
+
|
|
4604
|
+
// src/tools/safe-write.ts
|
|
4605
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
4606
|
+
import { chmod as chmod5, lstat as lstat3, open, rename as rename5, rm as rm3 } from "fs/promises";
|
|
4607
|
+
import { basename as basename2, dirname as dirname2, join } from "path";
|
|
4608
|
+
var UnsafeFileChangeError = class extends Error {
|
|
4609
|
+
constructor(message) {
|
|
4610
|
+
super(message);
|
|
4611
|
+
this.name = "UnsafeFileChangeError";
|
|
4612
|
+
}
|
|
4613
|
+
};
|
|
4614
|
+
async function readSafeFileSnapshot(file, maxBytes) {
|
|
4615
|
+
const parent = await lstat3(dirname2(file));
|
|
4616
|
+
if (parent.isSymbolicLink() || !parent.isDirectory()) {
|
|
4617
|
+
throw new UnsafeFileChangeError("the parent path is not a real directory");
|
|
4618
|
+
}
|
|
4619
|
+
const parentIdentity = directoryIdentity(parent);
|
|
4620
|
+
const pathInfo = await lstatMaybe(file);
|
|
4621
|
+
if (!pathInfo) return { exists: false, data: null, mode: 438, parent: parentIdentity };
|
|
4622
|
+
if (pathInfo.isSymbolicLink()) {
|
|
4623
|
+
throw new UnsafeFileChangeError("the path is a symbolic link");
|
|
4624
|
+
}
|
|
4625
|
+
if (!pathInfo.isFile()) throw new UnsafeFileChangeError("the path is not a regular file");
|
|
4626
|
+
if (pathInfo.size > maxBytes) throw new UnsafeFileChangeError("the file is too large");
|
|
4627
|
+
const handle = await open(file, "r");
|
|
4628
|
+
try {
|
|
4629
|
+
const opened = await handle.stat();
|
|
4630
|
+
if (!opened.isFile() || !sameObject(pathInfo, opened)) {
|
|
4631
|
+
throw new UnsafeFileChangeError("the file changed while it was being opened");
|
|
4632
|
+
}
|
|
4633
|
+
const data = await readBounded(handle, maxBytes);
|
|
4634
|
+
const afterRead = await handle.stat();
|
|
4635
|
+
if (!sameVersion(opened, afterRead)) {
|
|
4636
|
+
throw new UnsafeFileChangeError("the file changed while it was being read");
|
|
4637
|
+
}
|
|
4638
|
+
return {
|
|
4639
|
+
exists: true,
|
|
4640
|
+
data,
|
|
4641
|
+
mode: opened.mode & 511,
|
|
4642
|
+
identity: fileIdentity(opened),
|
|
4643
|
+
parent: parentIdentity
|
|
4644
|
+
};
|
|
4645
|
+
} finally {
|
|
4646
|
+
await handle.close();
|
|
4647
|
+
}
|
|
4648
|
+
}
|
|
4649
|
+
async function atomicWriteSafeFile(file, data, snapshot) {
|
|
4650
|
+
const parent = dirname2(file);
|
|
4651
|
+
await assertSameParent(parent, snapshot.parent);
|
|
4652
|
+
const temp = join(parent, `.${basename2(file)}.${process.pid}.${randomUUID3()}.tmp`);
|
|
4653
|
+
let handle;
|
|
4654
|
+
try {
|
|
4655
|
+
handle = await open(temp, "wx", snapshot.mode);
|
|
4656
|
+
await handle.writeFile(data, typeof data === "string" ? { encoding: "utf8" } : void 0);
|
|
4657
|
+
await handle.sync();
|
|
4658
|
+
await handle.close();
|
|
4659
|
+
handle = void 0;
|
|
4660
|
+
await chmod5(temp, snapshot.mode);
|
|
4661
|
+
await assertSameParent(parent, snapshot.parent);
|
|
4662
|
+
const current = await lstatMaybe(file);
|
|
4663
|
+
if (!matchesSnapshot2(current, snapshot)) {
|
|
4664
|
+
throw new UnsafeFileChangeError("the destination changed before it could be replaced");
|
|
4665
|
+
}
|
|
4666
|
+
await rename5(temp, file);
|
|
4667
|
+
} catch (error) {
|
|
4668
|
+
await handle?.close().catch(() => void 0);
|
|
4669
|
+
await rm3(temp, { force: true }).catch(() => void 0);
|
|
4670
|
+
throw error;
|
|
4671
|
+
}
|
|
4672
|
+
}
|
|
4673
|
+
async function readBounded(handle, maxBytes) {
|
|
4674
|
+
const chunks = [];
|
|
4675
|
+
let total = 0;
|
|
4676
|
+
for (; ; ) {
|
|
4677
|
+
const remaining = maxBytes + 1 - total;
|
|
4678
|
+
if (remaining <= 0) throw new UnsafeFileChangeError("the file is too large");
|
|
4679
|
+
const chunk = Buffer.allocUnsafe(Math.min(64 * 1024, remaining));
|
|
4680
|
+
const { bytesRead } = await handle.read(chunk, 0, chunk.length, null);
|
|
4681
|
+
if (bytesRead === 0) return Buffer.concat(chunks, total);
|
|
4682
|
+
chunks.push(chunk.subarray(0, bytesRead));
|
|
4683
|
+
total += bytesRead;
|
|
4684
|
+
}
|
|
4685
|
+
}
|
|
4686
|
+
async function assertSameParent(parent, expected) {
|
|
4687
|
+
const current = await lstat3(parent);
|
|
4688
|
+
if (current.isSymbolicLink() || !current.isDirectory() || current.dev !== expected.dev || current.ino !== expected.ino) {
|
|
4689
|
+
throw new UnsafeFileChangeError("the parent directory changed during the write");
|
|
4690
|
+
}
|
|
4691
|
+
}
|
|
4692
|
+
function matchesSnapshot2(current, snapshot) {
|
|
4693
|
+
if (!snapshot.exists) return current === null;
|
|
4694
|
+
return Boolean(
|
|
4695
|
+
current && !current.isSymbolicLink() && current.isFile() && sameIdentity(current, snapshot.identity)
|
|
4696
|
+
);
|
|
4697
|
+
}
|
|
4698
|
+
function sameObject(left, right) {
|
|
4699
|
+
return left.dev === right.dev && left.ino === right.ino;
|
|
4700
|
+
}
|
|
4701
|
+
function sameVersion(left, right) {
|
|
4702
|
+
return sameObject(left, right) && left.size === right.size && left.mtimeMs === right.mtimeMs && left.ctimeMs === right.ctimeMs;
|
|
4703
|
+
}
|
|
4704
|
+
function sameIdentity(info, identity2) {
|
|
4705
|
+
return info.dev === identity2.dev && info.ino === identity2.ino && info.size === identity2.size && info.mtimeMs === identity2.mtimeMs && info.ctimeMs === identity2.ctimeMs;
|
|
4706
|
+
}
|
|
4707
|
+
function fileIdentity(info) {
|
|
4708
|
+
return {
|
|
4709
|
+
dev: info.dev,
|
|
4710
|
+
ino: info.ino,
|
|
4711
|
+
size: info.size,
|
|
4712
|
+
mtimeMs: info.mtimeMs,
|
|
4713
|
+
ctimeMs: info.ctimeMs
|
|
4714
|
+
};
|
|
4715
|
+
}
|
|
4716
|
+
function directoryIdentity(info) {
|
|
4717
|
+
return { dev: info.dev, ino: info.ino };
|
|
4718
|
+
}
|
|
4719
|
+
async function lstatMaybe(file) {
|
|
4720
|
+
try {
|
|
4721
|
+
return await lstat3(file);
|
|
4722
|
+
} catch (error) {
|
|
4723
|
+
if (error.code === "ENOENT") return null;
|
|
4724
|
+
throw error;
|
|
4725
|
+
}
|
|
4726
|
+
}
|
|
4727
|
+
|
|
4728
|
+
// src/tools/edit.ts
|
|
4149
4729
|
var MAX_FILE_BYTES2 = 5e6;
|
|
4150
4730
|
function countOccurrences(haystack, needle) {
|
|
4151
4731
|
if (needle === "") return 0;
|
|
@@ -4201,19 +4781,14 @@ var editTool = {
|
|
|
4201
4781
|
const { path: path14, oldString, newString, replaceAll = false } = input;
|
|
4202
4782
|
const safe = resolveInside(ctx.cwd, path14);
|
|
4203
4783
|
if (!safe.ok) return { content: safe.reason, isError: true };
|
|
4204
|
-
|
|
4205
|
-
|
|
4206
|
-
|
|
4207
|
-
}
|
|
4208
|
-
|
|
4209
|
-
if (info && info.size > MAX_FILE_BYTES2) {
|
|
4210
|
-
return {
|
|
4211
|
-
content: `Cannot edit ${path14}: the file exceeds the ${MAX_FILE_BYTES2 / 1e6} MB limit.`,
|
|
4212
|
-
isError: true
|
|
4213
|
-
};
|
|
4784
|
+
let snapshot;
|
|
4785
|
+
try {
|
|
4786
|
+
snapshot = await readSafeFileSnapshot(safe.path, MAX_FILE_BYTES2);
|
|
4787
|
+
} catch (error) {
|
|
4788
|
+
return { content: `Cannot edit ${path14}: ${error.message}.`, isError: true };
|
|
4214
4789
|
}
|
|
4215
|
-
|
|
4216
|
-
|
|
4790
|
+
if (!snapshot.exists) return { content: `Cannot read ${path14}.`, isError: true };
|
|
4791
|
+
const beforeBuffer = snapshot.data;
|
|
4217
4792
|
if (beforeBuffer.subarray(0, 8192).includes(0)) {
|
|
4218
4793
|
return { content: `Cannot edit ${path14}: it is a binary file, not text.`, isError: true };
|
|
4219
4794
|
}
|
|
@@ -4245,7 +4820,7 @@ var editTool = {
|
|
|
4245
4820
|
return { content: `Checkpoint capture failed: ${error.message}`, isError: true };
|
|
4246
4821
|
}
|
|
4247
4822
|
try {
|
|
4248
|
-
await
|
|
4823
|
+
await atomicWriteSafeFile(safe.path, after, snapshot);
|
|
4249
4824
|
ctx.checkpoint?.markChanged(safe.path);
|
|
4250
4825
|
} catch (error) {
|
|
4251
4826
|
return { content: `Failed to write ${path14}: ${error.message}`, isError: true };
|
|
@@ -4265,7 +4840,7 @@ function replacement(before, oldString, newString, replaceAll) {
|
|
|
4265
4840
|
}
|
|
4266
4841
|
|
|
4267
4842
|
// src/tools/glob.ts
|
|
4268
|
-
import { join } from "path";
|
|
4843
|
+
import { join as join2 } from "path";
|
|
4269
4844
|
import fg from "fast-glob";
|
|
4270
4845
|
var MAX_RESULTS = 500;
|
|
4271
4846
|
var IGNORED_DIRECTORIES = ["**/node_modules/**", "**/.git/**", "**/dist/**"];
|
|
@@ -4282,6 +4857,7 @@ var globTool = {
|
|
|
4282
4857
|
additionalProperties: false
|
|
4283
4858
|
},
|
|
4284
4859
|
defaultPermission: "allow",
|
|
4860
|
+
readOnly: true,
|
|
4285
4861
|
summarize(input) {
|
|
4286
4862
|
return `glob(${brief(input.pattern)})`;
|
|
4287
4863
|
},
|
|
@@ -4303,7 +4879,7 @@ var globTool = {
|
|
|
4303
4879
|
});
|
|
4304
4880
|
if (entries.length === 0) return { content: `No files matched ${pattern}` };
|
|
4305
4881
|
entries.sort((a, b) => (b.stats?.mtimeMs ?? 0) - (a.stats?.mtimeMs ?? 0));
|
|
4306
|
-
const paths = entries.slice(0, MAX_RESULTS).map((entry) =>
|
|
4882
|
+
const paths = entries.slice(0, MAX_RESULTS).map((entry) => join2(safe.relative, entry.path));
|
|
4307
4883
|
if (entries.length > MAX_RESULTS) {
|
|
4308
4884
|
paths.push(`... truncated: ${entries.length - MAX_RESULTS} more files matched.`);
|
|
4309
4885
|
}
|
|
@@ -4313,7 +4889,7 @@ var globTool = {
|
|
|
4313
4889
|
|
|
4314
4890
|
// src/tools/grep.ts
|
|
4315
4891
|
import { readFile as readFile9, stat as stat7 } from "fs/promises";
|
|
4316
|
-
import { join as
|
|
4892
|
+
import { join as join3 } from "path";
|
|
4317
4893
|
import fg2 from "fast-glob";
|
|
4318
4894
|
|
|
4319
4895
|
// src/tools/sensitive.ts
|
|
@@ -4462,6 +5038,7 @@ var grepTool = {
|
|
|
4462
5038
|
additionalProperties: false
|
|
4463
5039
|
},
|
|
4464
5040
|
defaultPermission: "allow",
|
|
5041
|
+
readOnly: true,
|
|
4465
5042
|
permission(input, ctx) {
|
|
4466
5043
|
const { path: path14, glob } = input ?? {};
|
|
4467
5044
|
if (isSensitivePath(path14) || mentionsSensitivePattern(glob)) return "ask";
|
|
@@ -4500,8 +5077,8 @@ var grepTool = {
|
|
|
4500
5077
|
ignore: IGNORED_DIRECTORIES,
|
|
4501
5078
|
stats: true
|
|
4502
5079
|
})).sort((a, b) => (b.stats?.mtimeMs ?? 0) - (a.stats?.mtimeMs ?? 0)).map((entry) => ({
|
|
4503
|
-
absolute:
|
|
4504
|
-
relative:
|
|
5080
|
+
absolute: join3(safe.path, entry.path),
|
|
5081
|
+
relative: join3(safe.relative, entry.path),
|
|
4505
5082
|
size: entry.stats?.size ?? 0
|
|
4506
5083
|
})) : [{ absolute: safe.path, relative: safe.relative, size: target.size }];
|
|
4507
5084
|
const files = discovered.slice(0, MAX_FILES);
|
|
@@ -4569,6 +5146,7 @@ var readTool = {
|
|
|
4569
5146
|
additionalProperties: false
|
|
4570
5147
|
},
|
|
4571
5148
|
defaultPermission: "allow",
|
|
5149
|
+
readOnly: true,
|
|
4572
5150
|
permission(input, ctx) {
|
|
4573
5151
|
const target = input?.path;
|
|
4574
5152
|
if (isSensitivePath(target)) return "ask";
|
|
@@ -4629,8 +5207,8 @@ function toToolSchema(tool) {
|
|
|
4629
5207
|
}
|
|
4630
5208
|
|
|
4631
5209
|
// src/tools/write.ts
|
|
4632
|
-
import {
|
|
4633
|
-
import { dirname as
|
|
5210
|
+
import { mkdir as mkdir4, readFile as readFile11, stat as stat9 } from "fs/promises";
|
|
5211
|
+
import { dirname as dirname3 } from "path";
|
|
4634
5212
|
var MAX_FILE_BYTES5 = 5e6;
|
|
4635
5213
|
var writeTool = {
|
|
4636
5214
|
name: "write",
|
|
@@ -4680,31 +5258,29 @@ var writeTool = {
|
|
|
4680
5258
|
}
|
|
4681
5259
|
const safe = resolveInside(ctx.cwd, path14);
|
|
4682
5260
|
if (!safe.ok) return { content: safe.reason, isError: true };
|
|
4683
|
-
|
|
4684
|
-
|
|
4685
|
-
return { content: `Cannot write ${path14}: the path is a symbolic link. Remove the symlink first.`, isError: true };
|
|
4686
|
-
}
|
|
4687
|
-
const beforeInfo = await stat9(safe.path).catch(() => null);
|
|
4688
|
-
if (beforeInfo && beforeInfo.size > MAX_FILE_BYTES5) {
|
|
4689
|
-
return {
|
|
4690
|
-
content: `Cannot replace ${path14}: the existing file exceeds the ${MAX_FILE_BYTES5 / 1e6} MB limit.`,
|
|
4691
|
-
isError: true
|
|
4692
|
-
};
|
|
4693
|
-
}
|
|
4694
|
-
const beforeBuffer = await readFile11(safe.path).catch(() => null);
|
|
4695
|
-
if (beforeBuffer instanceof Buffer && beforeBuffer.subarray(0, 8192).includes(0)) {
|
|
4696
|
-
return { content: `Cannot write ${path14}: the existing file is binary, not text.`, isError: true };
|
|
4697
|
-
}
|
|
4698
|
-
let before = "";
|
|
4699
|
-
if (beforeBuffer) before = beforeBuffer.toString("utf8");
|
|
5261
|
+
let target = safe.path;
|
|
5262
|
+
let snapshot;
|
|
4700
5263
|
try {
|
|
4701
|
-
await
|
|
4702
|
-
|
|
4703
|
-
|
|
4704
|
-
|
|
5264
|
+
await mkdir4(dirname3(target), { recursive: true });
|
|
5265
|
+
const rechecked = resolveInside(ctx.cwd, path14);
|
|
5266
|
+
if (!rechecked.ok || rechecked.path !== target) {
|
|
5267
|
+
return {
|
|
5268
|
+
content: `Cannot write ${path14}: the path changed while its parent was created.`,
|
|
5269
|
+
isError: true
|
|
5270
|
+
};
|
|
5271
|
+
}
|
|
5272
|
+
target = rechecked.path;
|
|
5273
|
+
snapshot = await readSafeFileSnapshot(target, MAX_FILE_BYTES5);
|
|
5274
|
+
if (snapshot.exists && snapshot.data.subarray(0, 8192).includes(0)) {
|
|
5275
|
+
return { content: `Cannot write ${path14}: the existing file is binary, not text.`, isError: true };
|
|
5276
|
+
}
|
|
5277
|
+
await ctx.checkpoint?.capture(target);
|
|
5278
|
+
await atomicWriteSafeFile(target, content, snapshot);
|
|
5279
|
+
ctx.checkpoint?.markChanged(target);
|
|
4705
5280
|
} catch (error) {
|
|
4706
5281
|
return { content: `Failed to write ${path14}: ${error.message}`, isError: true };
|
|
4707
5282
|
}
|
|
5283
|
+
const before = snapshot.exists ? snapshot.data.toString("utf8") : "";
|
|
4708
5284
|
const lines = content === "" ? 0 : content.replace(/\n$/, "").split("\n").length;
|
|
4709
5285
|
return {
|
|
4710
5286
|
content: `${before === "" ? "Created" : "Updated"} ${path14} (${lines} ${lines === 1 ? "line" : "lines"})`,
|
|
@@ -4737,7 +5313,7 @@ function createToolRegistry(tools) {
|
|
|
4737
5313
|
}
|
|
4738
5314
|
|
|
4739
5315
|
// src/prompts/library.ts
|
|
4740
|
-
import { chmod as
|
|
5316
|
+
import { chmod as chmod6, readFile as readFile12, readdir as readdir3, unlink as unlink3, writeFile as writeFile6 } from "fs/promises";
|
|
4741
5317
|
import path10 from "path";
|
|
4742
5318
|
async function savePrompt(input) {
|
|
4743
5319
|
const slug = slugify(input.name);
|
|
@@ -4751,11 +5327,11 @@ async function savePrompt(input) {
|
|
|
4751
5327
|
};
|
|
4752
5328
|
await ensureDir(promptsDir);
|
|
4753
5329
|
const file = path10.join(promptsDir, `${slug}.md`);
|
|
4754
|
-
await
|
|
5330
|
+
await writeFile6(file, serialize(prompt), {
|
|
4755
5331
|
encoding: "utf8",
|
|
4756
5332
|
mode: 384
|
|
4757
5333
|
});
|
|
4758
|
-
await
|
|
5334
|
+
await chmod6(file, 384);
|
|
4759
5335
|
return prompt;
|
|
4760
5336
|
}
|
|
4761
5337
|
async function getPrompt(slug) {
|
|
@@ -4842,18 +5418,21 @@ function parse(slug, text) {
|
|
|
4842
5418
|
}
|
|
4843
5419
|
|
|
4844
5420
|
// src/skills/library.ts
|
|
4845
|
-
import {
|
|
5421
|
+
import { lstat as lstat4, open as open2, readdir as readdir4 } from "fs/promises";
|
|
4846
5422
|
import path11 from "path";
|
|
4847
5423
|
var SKILL_FILE = "SKILL.md";
|
|
4848
5424
|
var FRONTMATTER_BYTES = 8192;
|
|
4849
5425
|
var MAX_SKILL_BYTES = 5e6;
|
|
4850
5426
|
var MAX_SKILLS_PER_ROOT = 500;
|
|
5427
|
+
var guards = /* @__PURE__ */ new WeakMap();
|
|
4851
5428
|
async function discoverSkills(dirs) {
|
|
4852
5429
|
const byName = /* @__PURE__ */ new Map();
|
|
4853
5430
|
for (const root of dirs) {
|
|
4854
|
-
const
|
|
5431
|
+
const rootInfo = await lstat4(root).catch(() => null);
|
|
5432
|
+
if (!rootInfo?.isDirectory() || rootInfo.isSymbolicLink()) continue;
|
|
5433
|
+
const entries = await readdir4(root, { withFileTypes: true }).catch(() => []);
|
|
4855
5434
|
const found = await Promise.all(
|
|
4856
|
-
entries.slice(0, MAX_SKILLS_PER_ROOT).map((entry) => readMeta(path11.join(root, entry)))
|
|
5435
|
+
entries.filter((entry) => entry.isDirectory() && !entry.isSymbolicLink()).slice(0, MAX_SKILLS_PER_ROOT).map((entry) => readMeta(root, rootInfo, path11.join(root, entry.name)))
|
|
4857
5436
|
);
|
|
4858
5437
|
for (const meta of found) {
|
|
4859
5438
|
if (meta && !byName.has(meta.name)) byName.set(meta.name, meta);
|
|
@@ -4862,11 +5441,25 @@ async function discoverSkills(dirs) {
|
|
|
4862
5441
|
return [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
4863
5442
|
}
|
|
4864
5443
|
async function loadSkill(meta) {
|
|
4865
|
-
const
|
|
4866
|
-
if (
|
|
5444
|
+
const guard = guards.get(meta);
|
|
5445
|
+
if (!guard) throw new Error(`Skill was not discovered through a protected skill root: ${meta.file}`);
|
|
5446
|
+
await assertIdentity(guard.root, guard.rootIdentity, "skill root");
|
|
5447
|
+
await assertIdentity(meta.dir, guard.dirIdentity, "skill directory");
|
|
5448
|
+
const opened = await openVerified(meta.file, guard.fileIdentity);
|
|
5449
|
+
if (opened.info.size > MAX_SKILL_BYTES) {
|
|
5450
|
+
await opened.handle.close();
|
|
4867
5451
|
throw new Error(`Skill file exceeds the ${MAX_SKILL_BYTES / 1e6} MB limit: ${meta.file}`);
|
|
4868
5452
|
}
|
|
4869
|
-
|
|
5453
|
+
let text;
|
|
5454
|
+
try {
|
|
5455
|
+
text = (await readBounded2(opened.handle, MAX_SKILL_BYTES)).toString("utf8");
|
|
5456
|
+
} finally {
|
|
5457
|
+
await opened.handle.close();
|
|
5458
|
+
}
|
|
5459
|
+
await assertIdentity(guard.root, guard.rootIdentity, "skill root");
|
|
5460
|
+
await assertIdentity(meta.dir, guard.dirIdentity, "skill directory");
|
|
5461
|
+
await assertIdentity(meta.file, guard.fileIdentity, "skill file");
|
|
5462
|
+
const { body } = parseFrontmatter(text);
|
|
4870
5463
|
return { ...meta, body };
|
|
4871
5464
|
}
|
|
4872
5465
|
function formatSkillCatalogue(skills) {
|
|
@@ -4876,20 +5469,33 @@ function formatSkillCatalogue(skills) {
|
|
|
4876
5469
|
"\n"
|
|
4877
5470
|
);
|
|
4878
5471
|
}
|
|
4879
|
-
async function readMeta(dir) {
|
|
5472
|
+
async function readMeta(root, rootInfo, dir) {
|
|
5473
|
+
const dirInfo = await lstat4(dir).catch(() => null);
|
|
5474
|
+
if (!dirInfo?.isDirectory() || dirInfo.isSymbolicLink()) return null;
|
|
4880
5475
|
const file = path11.join(dir, SKILL_FILE);
|
|
4881
5476
|
const head = await readFrontmatterBytes(file);
|
|
4882
|
-
if (head
|
|
4883
|
-
const { fields } = parseFrontmatter(head);
|
|
4884
|
-
|
|
5477
|
+
if (!head) return null;
|
|
5478
|
+
const { fields } = parseFrontmatter(head.text);
|
|
5479
|
+
const meta = { name: fields.name || path11.basename(dir), description: fields.description ?? "", dir, file };
|
|
5480
|
+
guards.set(meta, {
|
|
5481
|
+
root,
|
|
5482
|
+
rootIdentity: identity(rootInfo),
|
|
5483
|
+
dirIdentity: identity(dirInfo),
|
|
5484
|
+
fileIdentity: head.identity
|
|
5485
|
+
});
|
|
5486
|
+
return meta;
|
|
4885
5487
|
}
|
|
4886
5488
|
async function readFrontmatterBytes(file) {
|
|
4887
|
-
const
|
|
5489
|
+
const before = await lstat4(file).catch(() => null);
|
|
5490
|
+
if (!before?.isFile() || before.isSymbolicLink()) return null;
|
|
5491
|
+
const handle = await open2(file, "r").catch(() => null);
|
|
4888
5492
|
if (!handle) return null;
|
|
4889
5493
|
try {
|
|
5494
|
+
const opened = await handle.stat();
|
|
5495
|
+
if (!opened.isFile() || !sameIdentity2(opened, identity(before))) return null;
|
|
4890
5496
|
const buffer = Buffer.alloc(FRONTMATTER_BYTES);
|
|
4891
5497
|
const { bytesRead } = await handle.read(buffer, 0, FRONTMATTER_BYTES, 0);
|
|
4892
|
-
return buffer.subarray(0, bytesRead).toString("utf8");
|
|
5498
|
+
return { text: buffer.subarray(0, bytesRead).toString("utf8"), identity: identity(opened) };
|
|
4893
5499
|
} catch {
|
|
4894
5500
|
return null;
|
|
4895
5501
|
} finally {
|
|
@@ -4897,11 +5503,49 @@ async function readFrontmatterBytes(file) {
|
|
|
4897
5503
|
});
|
|
4898
5504
|
}
|
|
4899
5505
|
}
|
|
4900
|
-
function
|
|
4901
|
-
const
|
|
4902
|
-
|
|
4903
|
-
|
|
4904
|
-
|
|
5506
|
+
async function openVerified(file, expected) {
|
|
5507
|
+
const before = await lstat4(file);
|
|
5508
|
+
if (!before.isFile() || before.isSymbolicLink() || !sameIdentity2(before, expected)) {
|
|
5509
|
+
throw new Error(`Refusing changed or symlinked skill file: ${file}`);
|
|
5510
|
+
}
|
|
5511
|
+
const handle = await open2(file, "r");
|
|
5512
|
+
const info = await handle.stat();
|
|
5513
|
+
if (!info.isFile() || !sameIdentity2(info, expected)) {
|
|
5514
|
+
await handle.close();
|
|
5515
|
+
throw new Error(`Refusing skill file changed while opening: ${file}`);
|
|
5516
|
+
}
|
|
5517
|
+
return { handle, info };
|
|
5518
|
+
}
|
|
5519
|
+
async function assertIdentity(file, expected, label) {
|
|
5520
|
+
const info = await lstat4(file);
|
|
5521
|
+
if (info.isSymbolicLink() || !sameIdentity2(info, expected)) {
|
|
5522
|
+
throw new Error(`Refusing changed or symlinked ${label}: ${file}`);
|
|
5523
|
+
}
|
|
5524
|
+
}
|
|
5525
|
+
async function readBounded2(handle, maxBytes) {
|
|
5526
|
+
const chunks = [];
|
|
5527
|
+
let total = 0;
|
|
5528
|
+
for (; ; ) {
|
|
5529
|
+
const remaining = maxBytes + 1 - total;
|
|
5530
|
+
if (remaining <= 0) throw new Error(`Skill file exceeds the ${maxBytes / 1e6} MB limit`);
|
|
5531
|
+
const chunk = Buffer.allocUnsafe(Math.min(64 * 1024, remaining));
|
|
5532
|
+
const { bytesRead } = await handle.read(chunk, 0, chunk.length, null);
|
|
5533
|
+
if (bytesRead === 0) return Buffer.concat(chunks, total);
|
|
5534
|
+
chunks.push(chunk.subarray(0, bytesRead));
|
|
5535
|
+
total += bytesRead;
|
|
5536
|
+
}
|
|
5537
|
+
}
|
|
5538
|
+
function identity(info) {
|
|
5539
|
+
return { dev: info.dev, ino: info.ino };
|
|
5540
|
+
}
|
|
5541
|
+
function sameIdentity2(info, expected) {
|
|
5542
|
+
return info.dev === expected.dev && info.ino === expected.ino;
|
|
5543
|
+
}
|
|
5544
|
+
function parseFrontmatter(text) {
|
|
5545
|
+
const lines = text.split("\n");
|
|
5546
|
+
const fields = {};
|
|
5547
|
+
let bodyStart = 0;
|
|
5548
|
+
if (lines[0]?.trim() === "---") {
|
|
4905
5549
|
let i = 1;
|
|
4906
5550
|
for (; i < lines.length && lines[i].trim() !== "---"; i++) {
|
|
4907
5551
|
const colon = lines[i].indexOf(":");
|
|
@@ -4915,11 +5559,22 @@ function parseFrontmatter(text) {
|
|
|
4915
5559
|
}
|
|
4916
5560
|
|
|
4917
5561
|
// src/skills/install.ts
|
|
5562
|
+
import { randomUUID as randomUUID4 } from "crypto";
|
|
4918
5563
|
import { execFileSync } from "child_process";
|
|
4919
|
-
import {
|
|
4920
|
-
|
|
5564
|
+
import {
|
|
5565
|
+
existsSync as existsSync2,
|
|
5566
|
+
lstatSync,
|
|
5567
|
+
mkdirSync,
|
|
5568
|
+
readFileSync,
|
|
5569
|
+
readdirSync,
|
|
5570
|
+
realpathSync as realpathSync2,
|
|
5571
|
+
rmSync,
|
|
5572
|
+
writeFileSync
|
|
5573
|
+
} from "fs";
|
|
5574
|
+
import { chmod as chmod7, mkdir as mkdir5 } from "fs/promises";
|
|
4921
5575
|
import path12 from "path";
|
|
4922
5576
|
var TMP_DIR = path12.join(skillsDir, ".tmp");
|
|
5577
|
+
var MAX_SKILL_BYTES2 = 5e6;
|
|
4923
5578
|
async function installSkill(source) {
|
|
4924
5579
|
await ensureDir(skillsDir);
|
|
4925
5580
|
if (isGitHubUrl(source)) {
|
|
@@ -4931,30 +5586,31 @@ async function installSkill(source) {
|
|
|
4931
5586
|
return installFromLocal(source);
|
|
4932
5587
|
}
|
|
4933
5588
|
function isGitHubUrl(input) {
|
|
4934
|
-
|
|
5589
|
+
try {
|
|
5590
|
+
const url = new URL(input);
|
|
5591
|
+
return url.protocol === "https:" && (url.hostname === "github.com" || url.hostname === "www.github.com");
|
|
5592
|
+
} catch {
|
|
5593
|
+
return false;
|
|
5594
|
+
}
|
|
4935
5595
|
}
|
|
4936
5596
|
function isNpmPackage(input) {
|
|
4937
|
-
|
|
4938
|
-
return /^(@[\w.-]+\/)?[\w.-]+(@[\w.*+-]+)?$/.test(input);
|
|
5597
|
+
return npmSkillName(input) !== null;
|
|
4939
5598
|
}
|
|
4940
5599
|
async function installFromGitHub(url) {
|
|
4941
5600
|
const { owner, repo, subdir, branch } = parseGitHubUrl(url);
|
|
4942
|
-
const name = subdir
|
|
4943
|
-
const skillDir =
|
|
4944
|
-
const tmpDir =
|
|
5601
|
+
const name = safeSkillName(subdir ? path12.posix.basename(subdir) : repo);
|
|
5602
|
+
const skillDir = safeChildPath(skillsDir, name);
|
|
5603
|
+
const tmpDir = safeChildPath(TMP_DIR, `${name}-${randomUUID4()}`);
|
|
4945
5604
|
try {
|
|
4946
|
-
rmSync(skillDir, { recursive: true, force: true });
|
|
4947
5605
|
mkdirSync(tmpDir, { recursive: true });
|
|
4948
5606
|
const cloneUrl = `https://github.com/${owner}/${repo}.git`;
|
|
4949
5607
|
const gitArgs = ["clone", "--depth", "1"];
|
|
4950
5608
|
if (branch) gitArgs.push("--branch", branch);
|
|
4951
5609
|
gitArgs.push(cloneUrl, tmpDir);
|
|
4952
5610
|
execFileSync("git", gitArgs, { stdio: "ignore" });
|
|
4953
|
-
const skillFile = subdir ?
|
|
4954
|
-
|
|
4955
|
-
|
|
4956
|
-
}
|
|
4957
|
-
const body = readFileSync(skillFile, "utf8");
|
|
5611
|
+
const skillFile = subdir ? safeChildPath(tmpDir, ...subdir.split("/"), "SKILL.md") : safeChildPath(tmpDir, "SKILL.md");
|
|
5612
|
+
const body = readSkillFile(skillFile, tmpDir, subdir || repo);
|
|
5613
|
+
removeExistingSkillDir(skillDir);
|
|
4958
5614
|
await writeSkillFile(skillDir, body);
|
|
4959
5615
|
return { name, dir: skillDir, source: url };
|
|
4960
5616
|
} finally {
|
|
@@ -4962,9 +5618,11 @@ async function installFromGitHub(url) {
|
|
|
4962
5618
|
}
|
|
4963
5619
|
}
|
|
4964
5620
|
async function installFromNpm(packageName) {
|
|
4965
|
-
const
|
|
4966
|
-
|
|
4967
|
-
const
|
|
5621
|
+
const parsedName = npmSkillName(packageName);
|
|
5622
|
+
if (!parsedName) throw new Error(`Invalid npm package name: ${packageName}`);
|
|
5623
|
+
const name = safeSkillName(parsedName);
|
|
5624
|
+
const skillDir = safeChildPath(skillsDir, name);
|
|
5625
|
+
const tmpDir = safeChildPath(TMP_DIR, `${name}-${randomUUID4()}`);
|
|
4968
5626
|
try {
|
|
4969
5627
|
mkdirSync(tmpDir, { recursive: true });
|
|
4970
5628
|
execFileSync("npm", ["pack", packageName, "--prefix", tmpDir], {
|
|
@@ -4976,6 +5634,7 @@ async function installFromNpm(packageName) {
|
|
|
4976
5634
|
throw new Error(`Could not download npm package "${packageName}"`);
|
|
4977
5635
|
}
|
|
4978
5636
|
const tarballPath = path12.join(tmpDir, tarball);
|
|
5637
|
+
validateTarball(tarballPath);
|
|
4979
5638
|
const extractDir = path12.join(tmpDir, "extracted");
|
|
4980
5639
|
mkdirSync(extractDir, { recursive: true });
|
|
4981
5640
|
try {
|
|
@@ -4985,7 +5644,7 @@ async function installFromNpm(packageName) {
|
|
|
4985
5644
|
if (existsSync2(packageDir)) {
|
|
4986
5645
|
const skillFile2 = path12.join(packageDir, "SKILL.md");
|
|
4987
5646
|
if (existsSync2(skillFile2)) {
|
|
4988
|
-
const body2 =
|
|
5647
|
+
const body2 = readSkillFile(skillFile2, packageDir, packageName);
|
|
4989
5648
|
await writeSkillFile(skillDir, body2);
|
|
4990
5649
|
return { name, dir: skillDir, source: packageName };
|
|
4991
5650
|
}
|
|
@@ -4999,13 +5658,41 @@ async function installFromNpm(packageName) {
|
|
|
4999
5658
|
if (!existsSync2(skillFile)) {
|
|
5000
5659
|
throw new Error(`No SKILL.md found in npm package "${packageName}"`);
|
|
5001
5660
|
}
|
|
5002
|
-
const body =
|
|
5661
|
+
const body = readSkillFile(skillFile, extractDir, packageName);
|
|
5003
5662
|
await writeSkillFile(skillDir, body);
|
|
5004
5663
|
return { name, dir: skillDir, source: packageName };
|
|
5005
5664
|
} finally {
|
|
5006
5665
|
rmSync(tmpDir, { recursive: true, force: true });
|
|
5007
5666
|
}
|
|
5008
5667
|
}
|
|
5668
|
+
function npmSkillName(input) {
|
|
5669
|
+
if (input.includes("\\") || input.startsWith(".")) return null;
|
|
5670
|
+
const scoped = input.match(/^@[\w.-]+\/([\w.-]+)(?:@[\w.*+-]+)?$/);
|
|
5671
|
+
if (scoped) return scoped[1] ?? null;
|
|
5672
|
+
const unscoped = input.match(/^([\w.-]+)(?:@[\w.*+-]+)?$/);
|
|
5673
|
+
return unscoped?.[1] ?? null;
|
|
5674
|
+
}
|
|
5675
|
+
function validateTarball(file) {
|
|
5676
|
+
const info = lstatSync(file, { throwIfNoEntry: false });
|
|
5677
|
+
if (!info?.isFile() || info.size > 2e7) {
|
|
5678
|
+
throw new Error("The npm skill archive is missing or exceeds the 20 MB safety limit.");
|
|
5679
|
+
}
|
|
5680
|
+
const listing = execFileSync("tar", ["-tzf", file], {
|
|
5681
|
+
encoding: "utf8",
|
|
5682
|
+
maxBuffer: 2e6,
|
|
5683
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
5684
|
+
});
|
|
5685
|
+
const entries = listing.split(/\r?\n/).filter(Boolean);
|
|
5686
|
+
if (entries.length === 0 || entries.length > 1e4) {
|
|
5687
|
+
throw new Error("The npm skill archive has an invalid number of entries.");
|
|
5688
|
+
}
|
|
5689
|
+
for (const entry of entries) {
|
|
5690
|
+
const normalized = entry.replace(/^\.\//, "");
|
|
5691
|
+
if (normalized.includes("\\") || path12.posix.isAbsolute(normalized) || normalized.split("/").some((segment) => segment === "..")) {
|
|
5692
|
+
throw new Error(`Unsafe path in npm skill archive: ${entry}`);
|
|
5693
|
+
}
|
|
5694
|
+
}
|
|
5695
|
+
}
|
|
5009
5696
|
async function installFromLocal(srcPath) {
|
|
5010
5697
|
const resolved = path12.resolve(srcPath);
|
|
5011
5698
|
let skillDir;
|
|
@@ -5021,29 +5708,102 @@ async function installFromLocal(srcPath) {
|
|
|
5021
5708
|
throw new Error(`No SKILL.md found at "${srcPath}"`);
|
|
5022
5709
|
}
|
|
5023
5710
|
const name = path12.basename(skillDir);
|
|
5024
|
-
const destDir =
|
|
5025
|
-
const body =
|
|
5711
|
+
const destDir = safeChildPath(skillsDir, safeSkillName(name));
|
|
5712
|
+
const body = readSkillFile(skillFile, skillDir, srcPath);
|
|
5026
5713
|
await writeSkillFile(destDir, body);
|
|
5027
5714
|
return { name, dir: destDir, source: resolved };
|
|
5028
5715
|
}
|
|
5029
5716
|
async function writeSkillFile(dir, body) {
|
|
5030
|
-
|
|
5717
|
+
assertInside(skillsDir, dir);
|
|
5718
|
+
const existingDir = lstatSync(dir, { throwIfNoEntry: false });
|
|
5719
|
+
if (existingDir?.isSymbolicLink()) {
|
|
5720
|
+
throw new Error(`Refusing to replace symlinked skill directory: ${dir}`);
|
|
5721
|
+
}
|
|
5722
|
+
await mkdir5(dir, { recursive: true, mode: 448 });
|
|
5031
5723
|
const file = path12.join(dir, "SKILL.md");
|
|
5032
|
-
|
|
5033
|
-
|
|
5724
|
+
const existingFile = lstatSync(file, { throwIfNoEntry: false });
|
|
5725
|
+
if (existingFile?.isSymbolicLink()) {
|
|
5726
|
+
throw new Error(`Refusing to replace symlinked skill file: ${file}`);
|
|
5727
|
+
}
|
|
5728
|
+
writeFileSync(file, body, { encoding: "utf8", mode: 384 });
|
|
5729
|
+
await chmod7(file, 384);
|
|
5034
5730
|
}
|
|
5035
5731
|
function parseGitHubUrl(url) {
|
|
5036
|
-
|
|
5037
|
-
|
|
5038
|
-
|
|
5039
|
-
|
|
5040
|
-
|
|
5041
|
-
|
|
5042
|
-
|
|
5043
|
-
|
|
5732
|
+
let parsed;
|
|
5733
|
+
try {
|
|
5734
|
+
parsed = new URL(url);
|
|
5735
|
+
} catch {
|
|
5736
|
+
throw new Error(`Cannot parse GitHub URL: ${url}`);
|
|
5737
|
+
}
|
|
5738
|
+
if (parsed.protocol !== "https:" || parsed.hostname !== "github.com" && parsed.hostname !== "www.github.com" || parsed.username || parsed.password) {
|
|
5739
|
+
throw new Error("GitHub skills must use an https://github.com URL without credentials.");
|
|
5740
|
+
}
|
|
5741
|
+
let segments;
|
|
5742
|
+
try {
|
|
5743
|
+
segments = parsed.pathname.split("/").filter(Boolean).map((segment) => decodeURIComponent(segment));
|
|
5744
|
+
} catch {
|
|
5044
5745
|
throw new Error(`Cannot parse GitHub URL: ${url}`);
|
|
5045
5746
|
}
|
|
5046
|
-
|
|
5747
|
+
if (segments.length < 2) throw new Error(`Cannot parse GitHub URL: ${url}`);
|
|
5748
|
+
const owner = safeGitHubSegment(segments[0], "owner");
|
|
5749
|
+
const repo = safeSkillName(segments[1].replace(/\.git$/i, ""));
|
|
5750
|
+
if (segments.length === 2) return { owner, repo };
|
|
5751
|
+
if (segments[2] !== "tree" || segments.length < 4) {
|
|
5752
|
+
throw new Error(`Unsupported GitHub skill URL: ${url}`);
|
|
5753
|
+
}
|
|
5754
|
+
const branch = safeGitHubSegment(segments[3], "branch");
|
|
5755
|
+
const subdirSegments = segments.slice(4).map((segment) => safeGitHubSegment(segment, "path"));
|
|
5756
|
+
return {
|
|
5757
|
+
owner,
|
|
5758
|
+
repo,
|
|
5759
|
+
branch,
|
|
5760
|
+
subdir: subdirSegments.length > 0 ? subdirSegments.join("/") : void 0
|
|
5761
|
+
};
|
|
5762
|
+
}
|
|
5763
|
+
function safeGitHubSegment(value, kind) {
|
|
5764
|
+
if (value === "" || value === "." || value === ".." || value.length > 255 || /[\0-\x1f\x7f/\\:*?"<>|#%]/.test(value)) {
|
|
5765
|
+
throw new Error(`Unsafe GitHub ${kind}: ${value || "(empty)"}`);
|
|
5766
|
+
}
|
|
5767
|
+
return value;
|
|
5768
|
+
}
|
|
5769
|
+
function safeSkillName(value) {
|
|
5770
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(value) || value === "." || value === "..") {
|
|
5771
|
+
throw new Error(`Unsafe skill name: ${value || "(empty)"}`);
|
|
5772
|
+
}
|
|
5773
|
+
return value;
|
|
5774
|
+
}
|
|
5775
|
+
function safeChildPath(root, ...segments) {
|
|
5776
|
+
const target = path12.resolve(root, ...segments);
|
|
5777
|
+
assertInside(root, target);
|
|
5778
|
+
return target;
|
|
5779
|
+
}
|
|
5780
|
+
function assertInside(root, target) {
|
|
5781
|
+
const relative2 = path12.relative(path12.resolve(root), path12.resolve(target));
|
|
5782
|
+
if (relative2 === "" || !path12.isAbsolute(relative2) && relative2 !== ".." && !relative2.startsWith(`..${path12.sep}`)) {
|
|
5783
|
+
return;
|
|
5784
|
+
}
|
|
5785
|
+
throw new Error(`Path escapes the skills directory: ${target}`);
|
|
5786
|
+
}
|
|
5787
|
+
function readSkillFile(file, allowedRoot, source) {
|
|
5788
|
+
const info = lstatSync(file, { throwIfNoEntry: false });
|
|
5789
|
+
if (!info?.isFile() || info.isSymbolicLink()) {
|
|
5790
|
+
throw new Error(`No regular SKILL.md found in "${source}"`);
|
|
5791
|
+
}
|
|
5792
|
+
if (info.size > MAX_SKILL_BYTES2) {
|
|
5793
|
+
throw new Error(`SKILL.md in "${source}" exceeds the ${MAX_SKILL_BYTES2 / 1e6} MB limit`);
|
|
5794
|
+
}
|
|
5795
|
+
const root = realpathSync2(allowedRoot);
|
|
5796
|
+
const resolved = realpathSync2(file);
|
|
5797
|
+
assertInside(root, resolved);
|
|
5798
|
+
return readFileSync(resolved, "utf8");
|
|
5799
|
+
}
|
|
5800
|
+
function removeExistingSkillDir(dir) {
|
|
5801
|
+
const info = lstatSync(dir, { throwIfNoEntry: false });
|
|
5802
|
+
if (!info) return;
|
|
5803
|
+
if (info.isSymbolicLink()) {
|
|
5804
|
+
throw new Error(`Refusing to remove symlinked skill directory: ${dir}`);
|
|
5805
|
+
}
|
|
5806
|
+
rmSync(dir, { recursive: true, force: true });
|
|
5047
5807
|
}
|
|
5048
5808
|
|
|
5049
5809
|
// src/tools/skill.ts
|
|
@@ -5060,6 +5820,7 @@ function createSkillTool(skills) {
|
|
|
5060
5820
|
additionalProperties: false
|
|
5061
5821
|
},
|
|
5062
5822
|
defaultPermission: "allow",
|
|
5823
|
+
readOnly: true,
|
|
5063
5824
|
summarize(input) {
|
|
5064
5825
|
return `skill(${brief(input.name)})`;
|
|
5065
5826
|
},
|
|
@@ -5165,52 +5926,100 @@ function field(input, key) {
|
|
|
5165
5926
|
}
|
|
5166
5927
|
|
|
5167
5928
|
// src/core/update.ts
|
|
5168
|
-
var UPDATE_URL =
|
|
5929
|
+
var UPDATE_URL = "https://registry.npmjs.org/%40kernelonpanic%2Fkitcode/latest";
|
|
5930
|
+
var PACKAGE_URL = "https://www.npmjs.com/package/@kernelonpanic/kitcode";
|
|
5169
5931
|
var TIMEOUT_MS = 4e3;
|
|
5170
5932
|
var MAX_RESPONSE_BYTES2 = 128e3;
|
|
5171
|
-
async function checkForUpdates(fetcher = fetch,
|
|
5172
|
-
if (
|
|
5173
|
-
return { status: "unknown", reason: "
|
|
5933
|
+
async function checkForUpdates(fetcher = fetch, currentVersion = KITCODE_VERSION) {
|
|
5934
|
+
if (!parseSemver(currentVersion)) {
|
|
5935
|
+
return { status: "unknown", reason: "installed KitCode version is not valid semver" };
|
|
5174
5936
|
}
|
|
5175
5937
|
const controller = new AbortController();
|
|
5176
5938
|
const timer = setTimeout(() => controller.abort(), TIMEOUT_MS);
|
|
5177
5939
|
try {
|
|
5178
5940
|
const response = await fetcher(UPDATE_URL, {
|
|
5179
5941
|
headers: {
|
|
5180
|
-
accept: "application/
|
|
5181
|
-
"user-agent": `KitCode/${
|
|
5182
|
-
"x-github-api-version": "2022-11-28"
|
|
5942
|
+
accept: "application/json",
|
|
5943
|
+
"user-agent": `KitCode/${currentVersion}`
|
|
5183
5944
|
},
|
|
5184
5945
|
redirect: "error",
|
|
5185
5946
|
signal: controller.signal
|
|
5186
5947
|
});
|
|
5187
5948
|
if (!response.ok) {
|
|
5188
|
-
|
|
5949
|
+
await response.body?.cancel().catch(() => void 0);
|
|
5950
|
+
return { status: "unknown", reason: `npm registry returned ${response.status}` };
|
|
5189
5951
|
}
|
|
5190
5952
|
const payload = await readJsonBounded(response);
|
|
5191
|
-
if (!payload) return { status: "unknown", reason: "
|
|
5192
|
-
if (typeof payload.
|
|
5193
|
-
return { status: "unknown", reason: "
|
|
5953
|
+
if (!payload) return { status: "unknown", reason: "npm response was too large or invalid" };
|
|
5954
|
+
if (typeof payload.version !== "string" || !parseSemver(payload.version)) {
|
|
5955
|
+
return { status: "unknown", reason: "npm response did not contain a valid version" };
|
|
5194
5956
|
}
|
|
5195
|
-
const
|
|
5196
|
-
|
|
5197
|
-
|
|
5198
|
-
return { status: "current", current, latest };
|
|
5957
|
+
const latest = payload.version;
|
|
5958
|
+
if (compareSemver(latest, currentVersion) <= 0) {
|
|
5959
|
+
return { status: "current", current: currentVersion, latest };
|
|
5199
5960
|
}
|
|
5200
|
-
|
|
5201
|
-
|
|
5961
|
+
return {
|
|
5962
|
+
status: "available",
|
|
5963
|
+
current: currentVersion,
|
|
5964
|
+
latest,
|
|
5965
|
+
url: `${PACKAGE_URL}/v/${encodeURIComponent(latest)}`
|
|
5966
|
+
};
|
|
5202
5967
|
} catch (error) {
|
|
5203
5968
|
return {
|
|
5204
5969
|
status: "unknown",
|
|
5205
|
-
reason: error instanceof Error && error.name === "AbortError" ? "
|
|
5970
|
+
reason: error instanceof Error && error.name === "AbortError" ? "npm update check timed out" : "npm update check failed"
|
|
5206
5971
|
};
|
|
5207
5972
|
} finally {
|
|
5208
5973
|
clearTimeout(timer);
|
|
5209
5974
|
}
|
|
5210
5975
|
}
|
|
5976
|
+
function compareSemver(left, right) {
|
|
5977
|
+
const a = parseSemver(left);
|
|
5978
|
+
const b = parseSemver(right);
|
|
5979
|
+
if (!a || !b) throw new Error("Cannot compare invalid semantic versions");
|
|
5980
|
+
for (let index = 0; index < 3; index += 1) {
|
|
5981
|
+
const x = a.core[index] ?? 0n;
|
|
5982
|
+
const y = b.core[index] ?? 0n;
|
|
5983
|
+
if (x !== y) return x > y ? 1 : -1;
|
|
5984
|
+
}
|
|
5985
|
+
if (a.prerelease.length === 0 || b.prerelease.length === 0) {
|
|
5986
|
+
if (a.prerelease.length === b.prerelease.length) return 0;
|
|
5987
|
+
return a.prerelease.length === 0 ? 1 : -1;
|
|
5988
|
+
}
|
|
5989
|
+
const length = Math.max(a.prerelease.length, b.prerelease.length);
|
|
5990
|
+
for (let index = 0; index < length; index += 1) {
|
|
5991
|
+
const x = a.prerelease[index];
|
|
5992
|
+
const y = b.prerelease[index];
|
|
5993
|
+
if (x === void 0 || y === void 0) return x === void 0 ? -1 : 1;
|
|
5994
|
+
if (x === y) continue;
|
|
5995
|
+
const xNumeric = /^\d+$/.test(x);
|
|
5996
|
+
const yNumeric = /^\d+$/.test(y);
|
|
5997
|
+
if (xNumeric && yNumeric) return BigInt(x) > BigInt(y) ? 1 : -1;
|
|
5998
|
+
if (xNumeric !== yNumeric) return xNumeric ? -1 : 1;
|
|
5999
|
+
return x > y ? 1 : -1;
|
|
6000
|
+
}
|
|
6001
|
+
return 0;
|
|
6002
|
+
}
|
|
6003
|
+
function parseSemver(value) {
|
|
6004
|
+
const match = value.match(
|
|
6005
|
+
/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/
|
|
6006
|
+
);
|
|
6007
|
+
if (!match) return null;
|
|
6008
|
+
const prerelease = match[4]?.split(".") ?? [];
|
|
6009
|
+
if (prerelease.some((part) => /^\d+$/.test(part) && part.length > 1 && part.startsWith("0"))) {
|
|
6010
|
+
return null;
|
|
6011
|
+
}
|
|
6012
|
+
return {
|
|
6013
|
+
core: [BigInt(match[1]), BigInt(match[2]), BigInt(match[3])],
|
|
6014
|
+
prerelease
|
|
6015
|
+
};
|
|
6016
|
+
}
|
|
5211
6017
|
async function readJsonBounded(response) {
|
|
5212
6018
|
const declared = Number(response.headers.get("content-length"));
|
|
5213
|
-
if (Number.isFinite(declared) && declared > MAX_RESPONSE_BYTES2)
|
|
6019
|
+
if (Number.isFinite(declared) && declared > MAX_RESPONSE_BYTES2) {
|
|
6020
|
+
await response.body?.cancel().catch(() => void 0);
|
|
6021
|
+
return null;
|
|
6022
|
+
}
|
|
5214
6023
|
if (!response.body) return null;
|
|
5215
6024
|
const reader = response.body.getReader();
|
|
5216
6025
|
const chunks = [];
|
|
@@ -5252,7 +6061,7 @@ async function boot(options) {
|
|
|
5252
6061
|
const warnings = [];
|
|
5253
6062
|
const workspaceRoot2 = await canonicalWorkspace(options.cwd);
|
|
5254
6063
|
const workspaceTrusted = await isWorkspaceTrusted(options.cwd);
|
|
5255
|
-
|
|
6064
|
+
let startupUpdate;
|
|
5256
6065
|
if (loadedConfig.ignoredProject) {
|
|
5257
6066
|
warnings.push(
|
|
5258
6067
|
`Project config ignored until this workspace is trusted: ${loadedConfig.ignoredProject.path}. Review it, then run: kitcode trust`
|
|
@@ -5295,7 +6104,7 @@ async function boot(options) {
|
|
|
5295
6104
|
session = (options.continueSession ? await latestSessionFor(options.cwd) : null) ?? createSession(options.cwd, config.model ?? "");
|
|
5296
6105
|
}
|
|
5297
6106
|
const usage = createUsageTracker(session.usage, resolvePricing);
|
|
5298
|
-
|
|
6107
|
+
let skillCatalogue = formatSkillCatalogue(skills);
|
|
5299
6108
|
const mainSystemPrompt = () => buildSystemPrompt({
|
|
5300
6109
|
cwd: options.cwd,
|
|
5301
6110
|
toolNames: tools.list().map((tool) => tool.name),
|
|
@@ -5402,26 +6211,39 @@ async function boot(options) {
|
|
|
5402
6211
|
createTaskTool(wrappedRunner, config.budget.maxSubagentsPerTurn)
|
|
5403
6212
|
]);
|
|
5404
6213
|
let persistQueue = Promise.resolve();
|
|
5405
|
-
|
|
5406
|
-
|
|
5407
|
-
|
|
6214
|
+
let configQueue = Promise.resolve();
|
|
6215
|
+
const persistConfig = (mutate) => {
|
|
6216
|
+
const save = configQueue.catch(() => void 0).then(async () => {
|
|
6217
|
+
const before = structuredClone(config);
|
|
6218
|
+
mutate(config);
|
|
6219
|
+
try {
|
|
6220
|
+
await saveConfig(config);
|
|
6221
|
+
} catch (error) {
|
|
6222
|
+
Object.assign(config, before);
|
|
6223
|
+
throw error;
|
|
6224
|
+
}
|
|
6225
|
+
});
|
|
6226
|
+
configQueue = save;
|
|
6227
|
+
return save;
|
|
5408
6228
|
};
|
|
5409
6229
|
const compactWithBudget = async (history, signal, budget) => {
|
|
5410
6230
|
if (compactCutIndex(history) <= 0) {
|
|
5411
6231
|
return { history, compacted: false, removedMessages: 0 };
|
|
5412
6232
|
}
|
|
5413
6233
|
const resolved = registry.resolve(modelRef);
|
|
6234
|
+
const estimate = estimateCompactBudget(history, config.maxTokens);
|
|
5414
6235
|
const budgetDecision = budget.beforeRequest({
|
|
5415
6236
|
modelRef,
|
|
5416
|
-
maxOutputTokens:
|
|
5417
|
-
estimatedInputTokens:
|
|
6237
|
+
maxOutputTokens: estimate.maxOutputTokens,
|
|
6238
|
+
estimatedInputTokens: estimate.inputTokens
|
|
5418
6239
|
});
|
|
5419
6240
|
if (!budgetDecision.allowed) throw new Error(budgetDecision.reason);
|
|
5420
6241
|
const result = await compactHistory({
|
|
5421
6242
|
provider: resolved.provider,
|
|
5422
6243
|
model: resolved.modelId,
|
|
5423
6244
|
history,
|
|
5424
|
-
maxTokens:
|
|
6245
|
+
maxTokens: config.maxTokens,
|
|
6246
|
+
maxTotalOutputTokens: budgetDecision.maxOutputTokens,
|
|
5425
6247
|
signal
|
|
5426
6248
|
});
|
|
5427
6249
|
if (result.usage) {
|
|
@@ -5442,12 +6264,22 @@ async function boot(options) {
|
|
|
5442
6264
|
async addProvider(url, key) {
|
|
5443
6265
|
const detected = await detectProvider(url, key);
|
|
5444
6266
|
rememberModels(detected.id, detected.models);
|
|
6267
|
+
const configBefore = structuredClone(config);
|
|
6268
|
+
const authBefore = { ...auth };
|
|
5445
6269
|
config.providers[detected.id] = detected.config;
|
|
5446
6270
|
auth[detected.id] = key;
|
|
5447
6271
|
const chosen = preferredModel(detected.models);
|
|
5448
6272
|
if (chosen) config.model = formatModelRef(detected.id, chosen);
|
|
5449
|
-
|
|
5450
|
-
|
|
6273
|
+
try {
|
|
6274
|
+
await saveConfig(config);
|
|
6275
|
+
await saveAuth(auth);
|
|
6276
|
+
} catch (error) {
|
|
6277
|
+
Object.assign(config, configBefore);
|
|
6278
|
+
for (const id of Object.keys(auth)) delete auth[id];
|
|
6279
|
+
Object.assign(auth, authBefore);
|
|
6280
|
+
await Promise.allSettled([saveConfig(config), saveAuth(auth)]);
|
|
6281
|
+
throw error;
|
|
6282
|
+
}
|
|
5451
6283
|
registry = createRegistry(config, auth);
|
|
5452
6284
|
const nextRef = config.model ?? "";
|
|
5453
6285
|
const discovered = detected.models.find((model) => model.id === chosen)?.contextWindow;
|
|
@@ -5537,17 +6369,26 @@ async function boot(options) {
|
|
|
5537
6369
|
const provider = config.providers[providerId];
|
|
5538
6370
|
if (!provider) throw new Error(`Provider "${providerId}" is not configured.`);
|
|
5539
6371
|
const previousKey = auth[providerId];
|
|
6372
|
+
const restoreKey = () => {
|
|
6373
|
+
if (previousKey === void 0) delete auth[providerId];
|
|
6374
|
+
else auth[providerId] = previousKey;
|
|
6375
|
+
};
|
|
5540
6376
|
auth[providerId] = newKey;
|
|
5541
6377
|
try {
|
|
5542
6378
|
const testRegistry = createRegistry(config, auth);
|
|
5543
|
-
await
|
|
6379
|
+
await testRegistry.get(providerId).listModels();
|
|
5544
6380
|
} catch (error) {
|
|
5545
|
-
|
|
6381
|
+
restoreKey();
|
|
5546
6382
|
throw new Error(
|
|
5547
6383
|
`Key verification failed for "${providerId}": ${error instanceof Error ? error.message : String(error)}`
|
|
5548
6384
|
);
|
|
5549
6385
|
}
|
|
5550
|
-
|
|
6386
|
+
try {
|
|
6387
|
+
await saveAuth(auth);
|
|
6388
|
+
} catch (error) {
|
|
6389
|
+
restoreKey();
|
|
6390
|
+
throw error;
|
|
6391
|
+
}
|
|
5551
6392
|
registry = createRegistry(config, auth);
|
|
5552
6393
|
if (parseModelRef(modelRef)?.provider === providerId) {
|
|
5553
6394
|
const models = await loadModels(registry.get(providerId));
|
|
@@ -5614,7 +6455,7 @@ async function boot(options) {
|
|
|
5614
6455
|
async exportSession(id, destination) {
|
|
5615
6456
|
await persistQueue.catch(() => void 0);
|
|
5616
6457
|
const target = destination ? path13.resolve(options.cwd, destination) : path13.join(options.cwd, ".kitcode-exports");
|
|
5617
|
-
if (!destination) await
|
|
6458
|
+
if (!destination) await mkdir6(target, { recursive: true, mode: 448 });
|
|
5618
6459
|
return (await exportSession(id, target)).path;
|
|
5619
6460
|
},
|
|
5620
6461
|
configPath: () => location.path,
|
|
@@ -5810,6 +6651,9 @@ ${lines.join("\n")}` : null;
|
|
|
5810
6651
|
async installSkill(source) {
|
|
5811
6652
|
const result = await installSkill(source);
|
|
5812
6653
|
skills = await discoverSkills(skillRoots);
|
|
6654
|
+
skillCatalogue = formatSkillCatalogue(skills);
|
|
6655
|
+
tools.unregister(["skill"]);
|
|
6656
|
+
if (skills.length > 0) tools.register([createSkillTool(skills)]);
|
|
5813
6657
|
return result;
|
|
5814
6658
|
},
|
|
5815
6659
|
async loadAttachment(requestedPath) {
|
|
@@ -5830,7 +6674,7 @@ ${lines.join("\n")}` : null;
|
|
|
5830
6674
|
const states = mcp.states();
|
|
5831
6675
|
const lines = [
|
|
5832
6676
|
`KitCode ${KITCODE_VERSION} \xB7 ${KITCODE_COMMIT.slice(0, 12)}`,
|
|
5833
|
-
|
|
6677
|
+
"updates: npm check runs on every app start",
|
|
5834
6678
|
`runtime: Node ${process.versions.node} \xB7 ${process.platform}/${process.arch}`,
|
|
5835
6679
|
`workspace: ${options.cwd}`,
|
|
5836
6680
|
`config: ${location.path}`,
|
|
@@ -5858,7 +6702,8 @@ ${lines.join("\n")}` : null;
|
|
|
5858
6702
|
);
|
|
5859
6703
|
return lines.join("\n");
|
|
5860
6704
|
},
|
|
5861
|
-
startupUpdateCheck: () => startupUpdate,
|
|
6705
|
+
startupUpdateCheck: () => startupUpdate ??= checkForUpdates(),
|
|
6706
|
+
checkForUpdates: () => checkForUpdates(),
|
|
5862
6707
|
async run(history, hooks, signal) {
|
|
5863
6708
|
syncMcpTools();
|
|
5864
6709
|
const runProviderId = parseModelRef(modelRef)?.provider;
|
|
@@ -5972,7 +6817,7 @@ ${lines.join("\n")}` : null;
|
|
|
5972
6817
|
warnings,
|
|
5973
6818
|
async shutdown() {
|
|
5974
6819
|
try {
|
|
5975
|
-
await persistQueue;
|
|
6820
|
+
await Promise.all([persistQueue, configQueue]);
|
|
5976
6821
|
} finally {
|
|
5977
6822
|
await mcp.close();
|
|
5978
6823
|
}
|
|
@@ -5996,6 +6841,13 @@ async function ask(text, options = {}) {
|
|
|
5996
6841
|
mode: options.mode
|
|
5997
6842
|
});
|
|
5998
6843
|
for (const warning of warnings) console.error(sanitizeTerminalText(warning));
|
|
6844
|
+
const update = await runtime.startupUpdateCheck();
|
|
6845
|
+
if (update.status === "available") {
|
|
6846
|
+
console.error(
|
|
6847
|
+
`A newer KitCode version is available (${update.latest}). Update with: npm install -g @kernelonpanic/kitcode@latest
|
|
6848
|
+
${update.url}`
|
|
6849
|
+
);
|
|
6850
|
+
}
|
|
5999
6851
|
const controller = new AbortController();
|
|
6000
6852
|
const onInterrupt = () => controller.abort();
|
|
6001
6853
|
process.on("SIGINT", onInterrupt);
|
|
@@ -6165,8 +7017,7 @@ function validateKey(value) {
|
|
|
6165
7017
|
import { render } from "ink";
|
|
6166
7018
|
|
|
6167
7019
|
// src/ui/App.tsx
|
|
6168
|
-
import { Box as
|
|
6169
|
-
import { execFile as execFile2, execFileSync as execFileSync2 } from "child_process";
|
|
7020
|
+
import { Box as Box13, Text as Text12, useApp, useInput as useInput2, useWindowSize as useWindowSize4 } from "ink";
|
|
6170
7021
|
import { useCallback, useEffect as useEffect2, useMemo as useMemo4, useRef as useRef4, useState as useState5 } from "react";
|
|
6171
7022
|
|
|
6172
7023
|
// src/mcp/add.ts
|
|
@@ -6220,6 +7071,7 @@ var COMMANDS = [
|
|
|
6220
7071
|
{ name: "mcp", args: "[add|list|delete|enable|disable]" },
|
|
6221
7072
|
{ name: "attach", args: "<path|clipboard|clear>" },
|
|
6222
7073
|
{ name: "compact" },
|
|
7074
|
+
{ name: "update" },
|
|
6223
7075
|
{ name: "checker" },
|
|
6224
7076
|
{ name: "sessions", args: "[list|rename|delete [all]|export]" },
|
|
6225
7077
|
{ name: "config" },
|
|
@@ -6462,7 +7314,11 @@ var en = {
|
|
|
6462
7314
|
sessionActionRename: "rename",
|
|
6463
7315
|
sessionActionDelete: "delete",
|
|
6464
7316
|
sessionActionExport: "export Markdown",
|
|
6465
|
-
updateAvailable: (version, url) => `A newer KitCode
|
|
7317
|
+
updateAvailable: (version, url) => `A newer KitCode version is available (${version}). Update with:
|
|
7318
|
+
npm install -g @kernelonpanic/kitcode@latest
|
|
7319
|
+
${url}`,
|
|
7320
|
+
updateCurrent: (version) => `KitCode ${version} is up to date.`,
|
|
7321
|
+
updateFailed: (reason) => `Could not check for updates: ${reason}.`,
|
|
6466
7322
|
accentSet: (name, hex) => `Accent: ${name} (${hex})`,
|
|
6467
7323
|
reasoning: (on) => `Reasoning ${on ? "on" : "off"}`,
|
|
6468
7324
|
effortSet: (value) => `Effort: ${value}`,
|
|
@@ -6537,6 +7393,7 @@ var en = {
|
|
|
6537
7393
|
"mcp disable": "disconnect without removing an MCP server",
|
|
6538
7394
|
attach: "attach an image or text file to the next message",
|
|
6539
7395
|
compact: "summarize older context and keep recent turns",
|
|
7396
|
+
update: "check npm for a newer KitCode version",
|
|
6540
7397
|
checker: "check local setup without spending model tokens",
|
|
6541
7398
|
sessions: "search and manage saved sessions",
|
|
6542
7399
|
"sessions list": "list saved sessions",
|
|
@@ -6645,7 +7502,11 @@ var ru = {
|
|
|
6645
7502
|
sessionActionRename: "\u043F\u0435\u0440\u0435\u0438\u043C\u0435\u043D\u043E\u0432\u0430\u0442\u044C",
|
|
6646
7503
|
sessionActionDelete: "\u0443\u0434\u0430\u043B\u0438\u0442\u044C",
|
|
6647
7504
|
sessionActionExport: "\u044D\u043A\u0441\u043F\u043E\u0440\u0442\u0438\u0440\u043E\u0432\u0430\u0442\u044C Markdown",
|
|
6648
|
-
updateAvailable: (version, url) => `\u0414\u043E\u0441\u0442\u0443\u043F\u043D\u0430 \u043D\u043E\u0432\u0430\u044F \u0432\u0435\u0440\u0441\u0438\u044F KitCode (${version}):
|
|
7505
|
+
updateAvailable: (version, url) => `\u0414\u043E\u0441\u0442\u0443\u043F\u043D\u0430 \u043D\u043E\u0432\u0430\u044F \u0432\u0435\u0440\u0441\u0438\u044F KitCode (${version}). \u041E\u0431\u043D\u043E\u0432\u0438\u0442\u044C:
|
|
7506
|
+
npm install -g @kernelonpanic/kitcode@latest
|
|
7507
|
+
${url}`,
|
|
7508
|
+
updateCurrent: (version) => `\u0423\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D\u0430 \u0430\u043A\u0442\u0443\u0430\u043B\u044C\u043D\u0430\u044F \u0432\u0435\u0440\u0441\u0438\u044F KitCode ${version}.`,
|
|
7509
|
+
updateFailed: (reason) => `\u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u043F\u0440\u043E\u0432\u0435\u0440\u0438\u0442\u044C \u043E\u0431\u043D\u043E\u0432\u043B\u0435\u043D\u0438\u044F: ${reason}.`,
|
|
6649
7510
|
accentSet: (name, hex) => `\u0426\u0432\u0435\u0442: ${name} (${hex})`,
|
|
6650
7511
|
reasoning: (on) => `\u0420\u0430\u0437\u043C\u044B\u0448\u043B\u0435\u043D\u0438\u044F ${on ? "\u0432\u043A\u043B\u044E\u0447\u0435\u043D\u044B" : "\u0432\u044B\u043A\u043B\u044E\u0447\u0435\u043D\u044B"}`,
|
|
6651
7512
|
effortSet: (value) => `\u0413\u043B\u0443\u0431\u0438\u043D\u0430: ${value}`,
|
|
@@ -6718,6 +7579,7 @@ var ru = {
|
|
|
6718
7579
|
"mcp disable": "\u043E\u0442\u043A\u043B\u044E\u0447\u0438\u0442\u044C MCP \u0431\u0435\u0437 \u0443\u0434\u0430\u043B\u0435\u043D\u0438\u044F",
|
|
6719
7580
|
attach: "\u043F\u0440\u0438\u043A\u0440\u0435\u043F\u0438\u0442\u044C \u043A\u0430\u0440\u0442\u0438\u043D\u043A\u0443 \u0438\u043B\u0438 \u0442\u0435\u043A\u0441\u0442\u043E\u0432\u044B\u0439 \u0444\u0430\u0439\u043B \u043A \u0441\u043E\u043E\u0431\u0449\u0435\u043D\u0438\u044E",
|
|
6720
7581
|
compact: "\u0441\u0436\u0430\u0442\u044C \u0441\u0442\u0430\u0440\u044B\u0439 \u043A\u043E\u043D\u0442\u0435\u043A\u0441\u0442, \u0441\u043E\u0445\u0440\u0430\u043D\u0438\u0432 \u043F\u043E\u0441\u043B\u0435\u0434\u043D\u0438\u0435 \u0445\u043E\u0434\u044B",
|
|
7582
|
+
update: "\u043F\u0440\u043E\u0432\u0435\u0440\u0438\u0442\u044C \u043D\u043E\u0432\u0443\u044E \u0432\u0435\u0440\u0441\u0438\u044E KitCode \u0432 npm",
|
|
6721
7583
|
checker: "\u043F\u0440\u043E\u0432\u0435\u0440\u0438\u0442\u044C \u043D\u0430\u0441\u0442\u0440\u043E\u0439\u043A\u0443 \u0431\u0435\u0437 \u0442\u0440\u0430\u0442\u044B \u0442\u043E\u043A\u0435\u043D\u043E\u0432 \u043C\u043E\u0434\u0435\u043B\u0438",
|
|
6722
7584
|
sessions: "\u043F\u043E\u0438\u0441\u043A \u0438 \u0443\u043F\u0440\u0430\u0432\u043B\u0435\u043D\u0438\u0435 \u0441\u043E\u0445\u0440\u0430\u043D\u0451\u043D\u043D\u044B\u043C\u0438 \u0441\u0435\u0441\u0441\u0438\u044F\u043C\u0438",
|
|
6723
7585
|
"sessions list": "\u0441\u043F\u0438\u0441\u043E\u043A \u0441\u043E\u0445\u0440\u0430\u043D\u0451\u043D\u043D\u044B\u0445 \u0441\u0435\u0441\u0441\u0438\u0439",
|
|
@@ -6834,7 +7696,13 @@ function Logo({ subtitle, workspace }) {
|
|
|
6834
7696
|
return /* @__PURE__ */ jsxs2(Box2, { width: "100%", marginBottom: 1, children: [
|
|
6835
7697
|
/* @__PURE__ */ jsx2(Box2, { flexDirection: "column", flexShrink: 0, children: CAT.map((line) => /* @__PURE__ */ jsx2(Text2, { color: theme.accent, children: line }, line)) }),
|
|
6836
7698
|
/* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", marginLeft: 2, flexGrow: 1, flexShrink: 1, minWidth: 0, children: [
|
|
6837
|
-
/* @__PURE__ */
|
|
7699
|
+
/* @__PURE__ */ jsxs2(Box2, { children: [
|
|
7700
|
+
/* @__PURE__ */ jsx2(Text2, { color: theme.accent, bold: true, children: "kitcode" }),
|
|
7701
|
+
/* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
|
|
7702
|
+
" v",
|
|
7703
|
+
KITCODE_VERSION
|
|
7704
|
+
] })
|
|
7705
|
+
] }),
|
|
6838
7706
|
/* @__PURE__ */ jsx2(Text2, { dimColor: true, wrap: "truncate-end", children: subtitle ?? strings.hint }),
|
|
6839
7707
|
location && /* @__PURE__ */ jsxs2(Box2, { width: "100%", children: [
|
|
6840
7708
|
/* @__PURE__ */ jsx2(Text2, { color: theme.accent, children: "\u2302 " }),
|
|
@@ -7194,7 +8062,7 @@ var PromptInput = memo(function PromptInput2({
|
|
|
7194
8062
|
valueRef.current = safeValue;
|
|
7195
8063
|
cursorRef.current = inputCursor;
|
|
7196
8064
|
const suggestions = matchCommands(safeValue);
|
|
7197
|
-
const
|
|
8065
|
+
const open3 = suggestions.length > 0;
|
|
7198
8066
|
const active2 = Math.min(selectionCursor, Math.max(0, suggestions.length - 1));
|
|
7199
8067
|
useEffect(() => {
|
|
7200
8068
|
setSelectionCursor(0);
|
|
@@ -7247,25 +8115,25 @@ var PromptInput = memo(function PromptInput2({
|
|
|
7247
8115
|
onPasteImage();
|
|
7248
8116
|
return;
|
|
7249
8117
|
}
|
|
7250
|
-
if (
|
|
8118
|
+
if (open3 && key.upArrow) {
|
|
7251
8119
|
setSelectionCursor(Math.max(0, active2 - 1));
|
|
7252
8120
|
return;
|
|
7253
8121
|
}
|
|
7254
|
-
if (
|
|
8122
|
+
if (open3 && key.downArrow) {
|
|
7255
8123
|
setSelectionCursor(Math.min(suggestions.length - 1, active2 + 1));
|
|
7256
8124
|
return;
|
|
7257
8125
|
}
|
|
7258
|
-
if (
|
|
8126
|
+
if (open3 && key.tab && !key.shift) {
|
|
7259
8127
|
const chosen = suggestions[active2];
|
|
7260
8128
|
if (chosen) change(`/${chosen.name} `);
|
|
7261
8129
|
return;
|
|
7262
8130
|
}
|
|
7263
|
-
if (
|
|
8131
|
+
if (open3 && key.return) {
|
|
7264
8132
|
const chosen = suggestions[active2];
|
|
7265
8133
|
if (chosen) submit(`/${chosen.name}`);
|
|
7266
8134
|
return;
|
|
7267
8135
|
}
|
|
7268
|
-
if (!
|
|
8136
|
+
if (!open3 && (key.upArrow || key.downArrow)) {
|
|
7269
8137
|
const moved = moveInputHistory(
|
|
7270
8138
|
history,
|
|
7271
8139
|
historyIndex,
|
|
@@ -7317,7 +8185,7 @@ var PromptInput = memo(function PromptInput2({
|
|
|
7317
8185
|
});
|
|
7318
8186
|
const start = Math.max(0, Math.min(active2 - WINDOW2 + 2, suggestions.length - WINDOW2));
|
|
7319
8187
|
const visible = suggestions.slice(start, start + WINDOW2);
|
|
7320
|
-
return /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", marginTop: 1, children: [
|
|
8188
|
+
return /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", marginTop: 1, flexShrink: 0, children: [
|
|
7321
8189
|
/* @__PURE__ */ jsxs8(
|
|
7322
8190
|
Box8,
|
|
7323
8191
|
{
|
|
@@ -7345,7 +8213,7 @@ var PromptInput = memo(function PromptInput2({
|
|
|
7345
8213
|
" ",
|
|
7346
8214
|
sanitizeTerminalText(hint)
|
|
7347
8215
|
] }),
|
|
7348
|
-
|
|
8216
|
+
open3 && /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", marginLeft: 2, children: [
|
|
7349
8217
|
visible.map((command, index) => {
|
|
7350
8218
|
const selected = start + index === active2;
|
|
7351
8219
|
return /* @__PURE__ */ jsxs8(Text8, { color: selected ? theme.accent : void 0, dimColor: !selected, children: [
|
|
@@ -7481,6 +8349,7 @@ function StatusBar({ status }) {
|
|
|
7481
8349
|
Box9,
|
|
7482
8350
|
{
|
|
7483
8351
|
width: "100%",
|
|
8352
|
+
flexShrink: 0,
|
|
7484
8353
|
marginTop: 1,
|
|
7485
8354
|
paddingX: 1,
|
|
7486
8355
|
borderStyle: "single",
|
|
@@ -7608,141 +8477,191 @@ function ContextMeter({
|
|
|
7608
8477
|
] });
|
|
7609
8478
|
}
|
|
7610
8479
|
|
|
8480
|
+
// src/ui/components/TerminalViewport.tsx
|
|
8481
|
+
import { Box as Box10 } from "ink";
|
|
8482
|
+
import { jsx as jsx10 } from "react/jsx-runtime";
|
|
8483
|
+
function interactiveViewportRows(rows) {
|
|
8484
|
+
if (!Number.isFinite(rows)) return 22;
|
|
8485
|
+
return Math.max(1, Math.floor(rows) - 2);
|
|
8486
|
+
}
|
|
8487
|
+
var INTERACTIVE_CHROME_ROWS = 14;
|
|
8488
|
+
function liveTranscriptRows(rows) {
|
|
8489
|
+
return Math.max(1, interactiveViewportRows(rows) - INTERACTIVE_CHROME_ROWS);
|
|
8490
|
+
}
|
|
8491
|
+
function TerminalViewport({ children, rows }) {
|
|
8492
|
+
return /* @__PURE__ */ jsx10(
|
|
8493
|
+
Box10,
|
|
8494
|
+
{
|
|
8495
|
+
flexDirection: "column",
|
|
8496
|
+
maxHeight: interactiveViewportRows(rows),
|
|
8497
|
+
overflowY: "hidden",
|
|
8498
|
+
children
|
|
8499
|
+
}
|
|
8500
|
+
);
|
|
8501
|
+
}
|
|
8502
|
+
|
|
7611
8503
|
// src/ui/components/Transcript.tsx
|
|
7612
|
-
import { Box as
|
|
8504
|
+
import { Box as Box12, Static, Text as Text11, useWindowSize as useWindowSize3 } from "ink";
|
|
7613
8505
|
import { memo as memo2, useMemo as useMemo3, useRef as useRef3 } from "react";
|
|
7614
8506
|
import Spinner2 from "ink-spinner";
|
|
8507
|
+
import stringWidth2 from "string-width";
|
|
7615
8508
|
|
|
7616
8509
|
// src/ui/markdown.tsx
|
|
7617
|
-
import { Box as
|
|
7618
|
-
import { useMemo as useMemo2 } from "react";
|
|
7619
|
-
import
|
|
8510
|
+
import { Box as Box11, Text as Text10, useWindowSize as useWindowSize2 } from "ink";
|
|
8511
|
+
import { Fragment as Fragment4, useMemo as useMemo2 } from "react";
|
|
8512
|
+
import stringWidth from "string-width";
|
|
8513
|
+
import { jsx as jsx11, jsxs as jsxs10 } from "react/jsx-runtime";
|
|
7620
8514
|
function Markdown({ children }) {
|
|
7621
8515
|
const blocks = useMemo2(() => extractBlocks(children), [children]);
|
|
7622
|
-
return /* @__PURE__ */
|
|
8516
|
+
return /* @__PURE__ */ jsx11(Box11, { flexDirection: "column", children: blocks.map((block, i) => /* @__PURE__ */ jsx11(BlockView, { block }, i)) });
|
|
7623
8517
|
}
|
|
7624
8518
|
function BlockView({ block }) {
|
|
7625
8519
|
switch (block.type) {
|
|
7626
8520
|
case "heading": {
|
|
7627
|
-
|
|
7628
|
-
const size = sizes[(block.level ?? 1) - 1] ?? 14;
|
|
7629
|
-
return /* @__PURE__ */ jsx10(Box10, { marginTop: block.level === 1 ? 1 : 0, children: /* @__PURE__ */ jsx10(Text10, { bold: true, children: truncateBySize(block.text ?? "", size) }) });
|
|
8521
|
+
return /* @__PURE__ */ jsx11(Box11, { marginTop: block.level === 1 ? 1 : 0, children: /* @__PURE__ */ jsx11(Text10, { bold: true, children: inline(block.text ?? "") }) });
|
|
7630
8522
|
}
|
|
7631
8523
|
case "blockquote":
|
|
7632
|
-
return /* @__PURE__ */
|
|
7633
|
-
/* @__PURE__ */ jsx10(Text10, { dimColor: true, children: "\u2502 " }),
|
|
7634
|
-
/* @__PURE__ */ jsx10(Text10, { dimColor: true, children: line })
|
|
7635
|
-
] }, i)) });
|
|
8524
|
+
return /* @__PURE__ */ jsx11(Box11, { flexDirection: "column", children: (block.text ?? "").split("\n").map((line, i) => /* @__PURE__ */ jsx11(Text10, { dimColor: true, italic: true, children: inline(line) }, i)) });
|
|
7636
8525
|
case "ul":
|
|
7637
|
-
return /* @__PURE__ */
|
|
7638
|
-
/* @__PURE__ */
|
|
7639
|
-
/* @__PURE__ */
|
|
8526
|
+
return /* @__PURE__ */ jsx11(Box11, { flexDirection: "column", children: (block.items ?? []).map((item, i) => /* @__PURE__ */ jsxs10(Box11, { children: [
|
|
8527
|
+
/* @__PURE__ */ jsx11(Text10, { dimColor: true, children: item.text?.match(/^[☑☐] /) ? "" : "\u2022 " }),
|
|
8528
|
+
/* @__PURE__ */ jsx11(Box11, { flexGrow: 1, flexShrink: 1, minWidth: 0, children: /* @__PURE__ */ jsx11(BlockView, { block: item }) })
|
|
7640
8529
|
] }, i)) });
|
|
7641
8530
|
case "ol":
|
|
7642
|
-
return /* @__PURE__ */
|
|
7643
|
-
/* @__PURE__ */
|
|
7644
|
-
/* @__PURE__ */
|
|
8531
|
+
return /* @__PURE__ */ jsx11(Box11, { flexDirection: "column", children: (block.items ?? []).map((item, i) => /* @__PURE__ */ jsxs10(Box11, { children: [
|
|
8532
|
+
/* @__PURE__ */ jsx11(Text10, { dimColor: true, children: `${(block.start ?? 1) + i}. ` }),
|
|
8533
|
+
/* @__PURE__ */ jsx11(Box11, { flexGrow: 1, flexShrink: 1, minWidth: 0, children: /* @__PURE__ */ jsx11(BlockView, { block: item }) })
|
|
7645
8534
|
] }, i)) });
|
|
7646
8535
|
case "paragraph":
|
|
7647
|
-
default:
|
|
7648
|
-
|
|
8536
|
+
default: {
|
|
8537
|
+
const content = /* @__PURE__ */ jsx11(Text10, { children: inline(block.text ?? "") });
|
|
8538
|
+
if (!block.children?.length) return content;
|
|
8539
|
+
return /* @__PURE__ */ jsxs10(Box11, { flexDirection: "column", children: [
|
|
8540
|
+
content,
|
|
8541
|
+
/* @__PURE__ */ jsx11(Box11, { flexDirection: "column", children: block.children.map((child, index) => /* @__PURE__ */ jsx11(BlockView, { block: child }, index)) })
|
|
8542
|
+
] });
|
|
8543
|
+
}
|
|
7649
8544
|
case "table":
|
|
7650
|
-
return /* @__PURE__ */
|
|
8545
|
+
return /* @__PURE__ */ jsx11(TableView, { block });
|
|
7651
8546
|
case "code":
|
|
7652
|
-
return /* @__PURE__ */
|
|
8547
|
+
return /* @__PURE__ */ jsx11(CodeBlock, { code: block.text ?? "" });
|
|
7653
8548
|
case "hr":
|
|
7654
|
-
return /* @__PURE__ */
|
|
8549
|
+
return /* @__PURE__ */ jsx11(Box11, { marginTop: 1, marginBottom: 1, children: /* @__PURE__ */ jsx11(Text10, { dimColor: true, wrap: "truncate-end", children: "\u2500".repeat(60) }) });
|
|
7655
8550
|
}
|
|
7656
8551
|
}
|
|
7657
|
-
function CodeBlock({
|
|
7658
|
-
const theme = useTheme();
|
|
8552
|
+
function CodeBlock({ code }) {
|
|
7659
8553
|
const lines = code.split("\n");
|
|
7660
|
-
return /* @__PURE__ */
|
|
7661
|
-
lang && /* @__PURE__ */ jsx10(Text10, { dimColor: true, color: theme.accent, children: lang }),
|
|
7662
|
-
/* @__PURE__ */ jsx10(Box10, { borderColor: "gray", borderStyle: "round", paddingX: 1, children: /* @__PURE__ */ jsx10(Box10, { flexDirection: "column", children: lines.map((line, i) => /* @__PURE__ */ jsx10(Text10, { color: "cyan", children: line }, i)) }) })
|
|
7663
|
-
] });
|
|
8554
|
+
return /* @__PURE__ */ jsx11(Box11, { flexDirection: "column", marginTop: 1, children: lines.map((line, i) => /* @__PURE__ */ jsx11(Text10, { color: "cyan", children: line }, i)) });
|
|
7664
8555
|
}
|
|
7665
8556
|
function TableView({ block }) {
|
|
8557
|
+
const { columns } = useWindowSize2();
|
|
7666
8558
|
const headers = block.headers ?? [];
|
|
7667
8559
|
const rows = block.rows ?? [];
|
|
7668
8560
|
if (headers.length === 0 && rows.length === 0) {
|
|
7669
|
-
return /* @__PURE__ */
|
|
8561
|
+
return /* @__PURE__ */ jsx11(Text10, { dimColor: true, children: "(empty table)" });
|
|
7670
8562
|
}
|
|
7671
8563
|
const colCount = Math.max(headers.length, ...rows.map((r) => r?.length ?? 0));
|
|
7672
8564
|
if (colCount === 0) {
|
|
7673
|
-
return /* @__PURE__ */
|
|
8565
|
+
return /* @__PURE__ */ jsx11(Text10, { dimColor: true, children: "(empty table)" });
|
|
7674
8566
|
}
|
|
7675
|
-
const
|
|
7676
|
-
|
|
8567
|
+
const allRows = [headers, ...rows].map(
|
|
8568
|
+
(row) => Array.from({ length: colCount }, (_, index) => inlineDisplayText(row[index] ?? ""))
|
|
8569
|
+
);
|
|
8570
|
+
const naturalWidths = new Array(colCount).fill(1);
|
|
7677
8571
|
for (const row of allRows) {
|
|
7678
8572
|
for (let i = 0; i < colCount; i++) {
|
|
7679
8573
|
const cell = row[i] ?? "";
|
|
7680
|
-
|
|
8574
|
+
naturalWidths[i] = Math.max(naturalWidths[i] ?? 1, stringWidth(cell));
|
|
7681
8575
|
}
|
|
7682
8576
|
}
|
|
7683
|
-
const
|
|
8577
|
+
const colWidths = fitColumnWidths(naturalWidths, Math.max(1, columns));
|
|
8578
|
+
const separator = colWidths.map((width, index) => {
|
|
8579
|
+
const edge = index === 0 || index === colWidths.length - 1;
|
|
8580
|
+
return "\u2500".repeat(width + (edge ? 1 : 2));
|
|
8581
|
+
}).join("\u253C");
|
|
7684
8582
|
const lines = [];
|
|
7685
8583
|
allRows.forEach((row, rowIdx) => {
|
|
7686
8584
|
const cells = [];
|
|
7687
8585
|
for (let i = 0; i < colCount; i++) {
|
|
7688
8586
|
const cell = row[i] ?? "";
|
|
7689
8587
|
const align = block.colAligns?.[i] ?? "left";
|
|
7690
|
-
const
|
|
7691
|
-
const
|
|
8588
|
+
const width = colWidths[i] ?? 1;
|
|
8589
|
+
const fitted = truncateToWidth(cell, width);
|
|
8590
|
+
const visualWidth = stringWidth(fitted);
|
|
8591
|
+
const totalPad = Math.max(0, width - visualWidth);
|
|
7692
8592
|
let padded;
|
|
7693
8593
|
if (align === "right") {
|
|
7694
|
-
padded =
|
|
8594
|
+
padded = `${" ".repeat(totalPad)}${fitted}`;
|
|
7695
8595
|
} else if (align === "center") {
|
|
7696
|
-
const totalPad = padWidth - visualWidth;
|
|
7697
8596
|
const left = Math.floor(totalPad / 2);
|
|
7698
|
-
padded = " ".repeat(left)
|
|
8597
|
+
padded = `${" ".repeat(left)}${fitted}${" ".repeat(totalPad - left)}`;
|
|
7699
8598
|
} else {
|
|
7700
|
-
padded =
|
|
8599
|
+
padded = `${fitted}${" ".repeat(totalPad)}`;
|
|
7701
8600
|
}
|
|
7702
8601
|
cells.push(padded);
|
|
7703
8602
|
}
|
|
7704
8603
|
lines.push(
|
|
7705
|
-
/* @__PURE__ */
|
|
8604
|
+
/* @__PURE__ */ jsx11(Text10, { bold: rowIdx === 0, wrap: "truncate-end", children: cells.join(" \u2502 ") }, `row-${rowIdx}`)
|
|
7706
8605
|
);
|
|
7707
8606
|
if (rowIdx === 0) {
|
|
7708
|
-
lines.push(/* @__PURE__ */
|
|
8607
|
+
lines.push(/* @__PURE__ */ jsx11(Text10, { dimColor: true, wrap: "truncate-end", children: separator }, `sep-${rowIdx}`));
|
|
7709
8608
|
}
|
|
7710
8609
|
});
|
|
7711
|
-
return /* @__PURE__ */
|
|
8610
|
+
return /* @__PURE__ */ jsx11(Box11, { flexDirection: "column", children: lines });
|
|
8611
|
+
}
|
|
8612
|
+
function fitColumnWidths(natural, maxLineWidth) {
|
|
8613
|
+
const widths = natural.map((width) => Math.max(1, width));
|
|
8614
|
+
const separatorWidth = Math.max(0, widths.length - 1) * 3;
|
|
8615
|
+
const available = Math.max(widths.length, maxLineWidth - separatorWidth);
|
|
8616
|
+
let excess = widths.reduce((sum, width) => sum + width, 0) - available;
|
|
8617
|
+
while (excess > 0) {
|
|
8618
|
+
const shrinkable = widths.map((width, index) => ({ width, index })).filter(({ width }) => width > 1);
|
|
8619
|
+
if (shrinkable.length === 0) break;
|
|
8620
|
+
const share = Math.max(1, Math.ceil(excess / shrinkable.length));
|
|
8621
|
+
for (const { index } of shrinkable) {
|
|
8622
|
+
const current = widths[index] ?? 1;
|
|
8623
|
+
const amount = Math.min(current - 1, share, excess);
|
|
8624
|
+
widths[index] = current - amount;
|
|
8625
|
+
excess -= amount;
|
|
8626
|
+
if (excess === 0) break;
|
|
8627
|
+
}
|
|
8628
|
+
}
|
|
8629
|
+
return widths;
|
|
8630
|
+
}
|
|
8631
|
+
var graphemeSegmenter = new Intl.Segmenter(void 0, { granularity: "grapheme" });
|
|
8632
|
+
function truncateToWidth(text, maxWidth) {
|
|
8633
|
+
if (stringWidth(text) <= maxWidth) return text;
|
|
8634
|
+
if (maxWidth <= 1) return "\u2026";
|
|
8635
|
+
let result = "";
|
|
8636
|
+
for (const { segment } of graphemeSegmenter.segment(text)) {
|
|
8637
|
+
if (stringWidth(result + segment) > maxWidth - 1) break;
|
|
8638
|
+
result += segment;
|
|
8639
|
+
}
|
|
8640
|
+
return `${result}\u2026`;
|
|
7712
8641
|
}
|
|
7713
|
-
|
|
7714
|
-
|
|
7715
|
-
const maxLen = size * CHARS_PER_FONT_LEVEL;
|
|
7716
|
-
return text.length > maxLen ? text.slice(0, maxLen) + "\u2026" : text;
|
|
8642
|
+
function inlineDisplayText(text) {
|
|
8643
|
+
return text.replace(/!\[([^\]]*)\]\(([^)]*)\)/g, "[$1]").replace(/\[([^\]]*)\]\(([^)]*)\)/g, "$1($2)").replace(/(`+)(.*?)\1/g, "$2").replace(/(\*\*|__|~~)(?=\S)(.*?\S)\1/g, "$2").replace(/(?<!\\)(\*|_)(?=\S)(.*?\S)\1/g, "$2").replace(/\\([\\`*_[\]{}()#+\-.!|>])/g, "$1");
|
|
7717
8644
|
}
|
|
7718
|
-
var HEADING_RE = /^(#{1,6})\
|
|
8645
|
+
var HEADING_RE = /^ {0,3}(#{1,6})(?:[ \t]+(.*)|[ \t]*)$/;
|
|
7719
8646
|
var UL_RE = /^([-*+])\s+(.*)$/;
|
|
7720
|
-
var OL_RE = /^(\d
|
|
8647
|
+
var OL_RE = /^(\d{1,9})[.)]\s+(.*)$/;
|
|
7721
8648
|
var QUOTE_RE = /^>\s?(.*)$/;
|
|
7722
|
-
var
|
|
7723
|
-
var
|
|
7724
|
-
var
|
|
7725
|
-
var
|
|
8649
|
+
var TASK_CONTENT_RE = /^\[([ xX])\]\s+(.*)$/;
|
|
8650
|
+
var SETEXT_RE = /^ {0,3}(=+|-+)[ \t]*$/;
|
|
8651
|
+
var MAX_TABLE_COLUMNS = 20;
|
|
8652
|
+
var MAX_LIST_DEPTH = 20;
|
|
7726
8653
|
function extractBlocks(src) {
|
|
7727
|
-
const lines = src.replace(/\r\n
|
|
8654
|
+
const lines = src.replace(/\r\n?/g, "\n").split("\n");
|
|
7728
8655
|
const blocks = [];
|
|
7729
8656
|
let paragraph = [];
|
|
7730
|
-
let list = null;
|
|
7731
8657
|
let quote = [];
|
|
7732
8658
|
let code = null;
|
|
7733
|
-
let table = null;
|
|
7734
8659
|
const flushParagraph = () => {
|
|
7735
8660
|
if (paragraph.length) {
|
|
7736
|
-
blocks.push({ type: "paragraph", text: paragraph
|
|
8661
|
+
blocks.push({ type: "paragraph", text: joinParagraphLines(paragraph) });
|
|
7737
8662
|
paragraph = [];
|
|
7738
8663
|
}
|
|
7739
8664
|
};
|
|
7740
|
-
const flushList = () => {
|
|
7741
|
-
if (list && list.items.length) {
|
|
7742
|
-
blocks.push({ type: list.ordered ? "ol" : "ul", items: list.items });
|
|
7743
|
-
list = null;
|
|
7744
|
-
}
|
|
7745
|
-
};
|
|
7746
8665
|
const flushQuote = () => {
|
|
7747
8666
|
if (quote.length) {
|
|
7748
8667
|
blocks.push({ type: "blockquote", text: quote.join("\n") });
|
|
@@ -7757,239 +8676,361 @@ function extractBlocks(src) {
|
|
|
7757
8676
|
};
|
|
7758
8677
|
const flushAll = () => {
|
|
7759
8678
|
flushParagraph();
|
|
7760
|
-
flushList();
|
|
7761
8679
|
flushQuote();
|
|
7762
8680
|
flushCode();
|
|
7763
8681
|
};
|
|
7764
|
-
const flushTable = () => {
|
|
7765
|
-
if (table && table.headers.length > 0) {
|
|
7766
|
-
blocks.push({
|
|
7767
|
-
type: "table",
|
|
7768
|
-
headers: table.headers,
|
|
7769
|
-
rows: table.rows,
|
|
7770
|
-
colAligns: table.colAligns
|
|
7771
|
-
});
|
|
7772
|
-
}
|
|
7773
|
-
table = null;
|
|
7774
|
-
};
|
|
7775
|
-
const parseListItem = (text, indent) => {
|
|
7776
|
-
const task = text.match(TASK_RE);
|
|
7777
|
-
if (task) {
|
|
7778
|
-
const done = task[1].toLowerCase() === "x";
|
|
7779
|
-
return {
|
|
7780
|
-
type: "paragraph",
|
|
7781
|
-
text: `${done ? "\u2611" : "\u2610"} ${task[2]}`
|
|
7782
|
-
};
|
|
7783
|
-
}
|
|
7784
|
-
return { type: "paragraph", text };
|
|
7785
|
-
};
|
|
7786
8682
|
for (let i = 0; i < lines.length; i++) {
|
|
7787
|
-
const line = lines[i];
|
|
8683
|
+
const line = lines[i] ?? "";
|
|
7788
8684
|
const trimmed = line.trim();
|
|
7789
8685
|
if (code) {
|
|
7790
|
-
|
|
7791
|
-
if (endMatch && endMatch[1][0] === code.fence[0] && endMatch[1].length >= code.fence.length) {
|
|
8686
|
+
if (isClosingFence(line, code.fence)) {
|
|
7792
8687
|
flushCode();
|
|
7793
8688
|
continue;
|
|
7794
8689
|
}
|
|
7795
8690
|
code.lines.push(line);
|
|
7796
8691
|
continue;
|
|
7797
8692
|
}
|
|
7798
|
-
const
|
|
7799
|
-
if (
|
|
8693
|
+
const fence = parseOpeningFence(line);
|
|
8694
|
+
if (fence) {
|
|
7800
8695
|
flushAll();
|
|
7801
|
-
|
|
7802
|
-
code = { fence: fenceMatch[1], lang: fenceMatch[2], lines: [] };
|
|
8696
|
+
code = { fence: fence.fence, lang: fence.lang, lines: [] };
|
|
7803
8697
|
continue;
|
|
7804
8698
|
}
|
|
7805
8699
|
if (trimmed === "") {
|
|
7806
|
-
flushTable();
|
|
7807
8700
|
flushAll();
|
|
7808
8701
|
continue;
|
|
7809
8702
|
}
|
|
7810
|
-
const
|
|
7811
|
-
if (
|
|
7812
|
-
flushTable();
|
|
8703
|
+
const heading = line.match(HEADING_RE);
|
|
8704
|
+
if (heading) {
|
|
7813
8705
|
flushAll();
|
|
7814
|
-
|
|
8706
|
+
const text = (heading[2] ?? "").replace(/[ \t]+#+[ \t]*$/, "").trim();
|
|
8707
|
+
blocks.push({ type: "heading", level: heading[1].length, text });
|
|
7815
8708
|
continue;
|
|
7816
8709
|
}
|
|
7817
|
-
const
|
|
7818
|
-
if (
|
|
7819
|
-
flushTable();
|
|
8710
|
+
const setext = lines[i + 1]?.match(SETEXT_RE);
|
|
8711
|
+
if (setext && isSetextHeadingText(line)) {
|
|
7820
8712
|
flushAll();
|
|
7821
|
-
blocks.push({ type: "heading", level:
|
|
8713
|
+
blocks.push({ type: "heading", level: setext[1][0] === "=" ? 1 : 2, text: trimmed });
|
|
8714
|
+
i += 1;
|
|
8715
|
+
continue;
|
|
8716
|
+
}
|
|
8717
|
+
if (isHorizontalRule(trimmed)) {
|
|
8718
|
+
flushAll();
|
|
8719
|
+
blocks.push({ type: "hr" });
|
|
7822
8720
|
continue;
|
|
7823
8721
|
}
|
|
7824
8722
|
const quoteMatch = trimmed.match(QUOTE_RE);
|
|
7825
8723
|
if (quoteMatch) {
|
|
7826
|
-
flushTable();
|
|
7827
8724
|
flushParagraph();
|
|
7828
|
-
flushList();
|
|
7829
8725
|
flushCode();
|
|
7830
8726
|
quote.push(quoteMatch[1]);
|
|
7831
8727
|
continue;
|
|
7832
8728
|
}
|
|
7833
|
-
const
|
|
7834
|
-
|
|
7835
|
-
const taskMatch = trimmed.match(TASK_RE);
|
|
7836
|
-
if (taskMatch) {
|
|
7837
|
-
flushTable();
|
|
8729
|
+
const parsedList = parseListBlock(lines, i);
|
|
8730
|
+
if (parsedList) {
|
|
7838
8731
|
flushParagraph();
|
|
7839
8732
|
flushQuote();
|
|
7840
8733
|
flushCode();
|
|
7841
|
-
|
|
7842
|
-
|
|
7843
|
-
list = { ordered: false, items: [], indent };
|
|
7844
|
-
}
|
|
7845
|
-
list.items.push(parseListItem(trimmed, indent));
|
|
8734
|
+
blocks.push(parsedList.block);
|
|
8735
|
+
i = parsedList.nextIndex - 1;
|
|
7846
8736
|
continue;
|
|
7847
8737
|
}
|
|
7848
|
-
const
|
|
7849
|
-
if (
|
|
7850
|
-
|
|
7851
|
-
|
|
7852
|
-
|
|
7853
|
-
flushCode();
|
|
7854
|
-
if (!list || list.ordered || indent !== list.indent) {
|
|
7855
|
-
flushList();
|
|
7856
|
-
list = { ordered: false, items: [], indent };
|
|
7857
|
-
}
|
|
7858
|
-
list.items.push(parseListItem(ulMatch[2], indent));
|
|
8738
|
+
const markdownTable = parseMarkdownTable(lines, i);
|
|
8739
|
+
if (markdownTable) {
|
|
8740
|
+
flushAll();
|
|
8741
|
+
blocks.push(markdownTable.block);
|
|
8742
|
+
i = markdownTable.nextIndex - 1;
|
|
7859
8743
|
continue;
|
|
7860
8744
|
}
|
|
7861
|
-
const
|
|
7862
|
-
if (
|
|
7863
|
-
|
|
7864
|
-
|
|
7865
|
-
|
|
7866
|
-
flushCode();
|
|
7867
|
-
if (!list || !list.ordered || indent !== list.indent) {
|
|
7868
|
-
flushList();
|
|
7869
|
-
list = { ordered: true, items: [], indent };
|
|
7870
|
-
}
|
|
7871
|
-
list.items.push(parseListItem(olMatch[2], indent));
|
|
8745
|
+
const asciiTable = parseAsciiTable(lines, i);
|
|
8746
|
+
if (asciiTable) {
|
|
8747
|
+
flushAll();
|
|
8748
|
+
blocks.push(asciiTable.block);
|
|
8749
|
+
i = asciiTable.nextIndex - 1;
|
|
7872
8750
|
continue;
|
|
7873
8751
|
}
|
|
7874
|
-
|
|
7875
|
-
|
|
7876
|
-
|
|
7877
|
-
if (cells.length >= 2 && cells.every((c) => /^:?-+:?$/.test(c))) {
|
|
7878
|
-
if (table) {
|
|
7879
|
-
table.colAligns = cells.map((c) => {
|
|
7880
|
-
if (c.startsWith(":") && c.endsWith(":")) return "center";
|
|
7881
|
-
if (c.endsWith(":")) return "right";
|
|
7882
|
-
return "left";
|
|
7883
|
-
});
|
|
7884
|
-
continue;
|
|
7885
|
-
}
|
|
7886
|
-
}
|
|
8752
|
+
if (quote.length > 0) {
|
|
8753
|
+
quote.push(trimmed);
|
|
8754
|
+
continue;
|
|
7887
8755
|
}
|
|
7888
|
-
|
|
7889
|
-
|
|
7890
|
-
|
|
7891
|
-
|
|
7892
|
-
|
|
7893
|
-
|
|
7894
|
-
|
|
7895
|
-
|
|
7896
|
-
|
|
7897
|
-
|
|
7898
|
-
|
|
7899
|
-
|
|
7900
|
-
|
|
7901
|
-
|
|
8756
|
+
flushQuote();
|
|
8757
|
+
flushCode();
|
|
8758
|
+
paragraph.push(paragraphLine(line));
|
|
8759
|
+
}
|
|
8760
|
+
flushAll();
|
|
8761
|
+
return blocks;
|
|
8762
|
+
}
|
|
8763
|
+
function paragraphLine(line) {
|
|
8764
|
+
const hardBreak = /(?: {2,}|\\)$/.test(line);
|
|
8765
|
+
const text = line.trim().replace(/\\$/, "");
|
|
8766
|
+
return hardBreak ? `${text}
|
|
8767
|
+
` : text;
|
|
8768
|
+
}
|
|
8769
|
+
function joinParagraphLines(lines) {
|
|
8770
|
+
let result = "";
|
|
8771
|
+
for (const line of lines) {
|
|
8772
|
+
if (result && !result.endsWith("\n")) result += " ";
|
|
8773
|
+
result += line;
|
|
8774
|
+
}
|
|
8775
|
+
return result.trim();
|
|
8776
|
+
}
|
|
8777
|
+
function parseListMarker(line) {
|
|
8778
|
+
const match = line.match(/^(\s*)(?:(\d{1,9})[.)]|([-+*]))\s+(.*)$/);
|
|
8779
|
+
if (!match) return null;
|
|
8780
|
+
return {
|
|
8781
|
+
indent: match[1].replace(/\t/g, " ").length,
|
|
8782
|
+
ordered: Boolean(match[2]),
|
|
8783
|
+
start: match[2] ? Number.parseInt(match[2], 10) : 1,
|
|
8784
|
+
text: match[4]
|
|
8785
|
+
};
|
|
8786
|
+
}
|
|
8787
|
+
function parseListItem(text) {
|
|
8788
|
+
const task = text.match(TASK_CONTENT_RE);
|
|
8789
|
+
return {
|
|
8790
|
+
type: "paragraph",
|
|
8791
|
+
text: task ? `${task[1].toLowerCase() === "x" ? "\u2611" : "\u2610"} ${task[2]}` : text
|
|
8792
|
+
};
|
|
8793
|
+
}
|
|
8794
|
+
function parseListBlock(lines, start, depth = 0) {
|
|
8795
|
+
const first2 = parseListMarker(lines[start] ?? "");
|
|
8796
|
+
if (!first2) return null;
|
|
8797
|
+
const items = [];
|
|
8798
|
+
let nextIndex = start;
|
|
8799
|
+
while (nextIndex < lines.length) {
|
|
8800
|
+
const marker = parseListMarker(lines[nextIndex] ?? "");
|
|
8801
|
+
if (!marker || marker.indent !== first2.indent || marker.ordered !== first2.ordered) break;
|
|
8802
|
+
const item = parseListItem(marker.text);
|
|
8803
|
+
nextIndex += 1;
|
|
8804
|
+
while (nextIndex < lines.length) {
|
|
8805
|
+
if ((lines[nextIndex] ?? "").trim() === "") {
|
|
8806
|
+
let afterBlank = nextIndex;
|
|
8807
|
+
while (afterBlank < lines.length && (lines[afterBlank] ?? "").trim() === "") {
|
|
8808
|
+
afterBlank += 1;
|
|
7902
8809
|
}
|
|
7903
|
-
|
|
7904
|
-
|
|
7905
|
-
|
|
7906
|
-
|
|
7907
|
-
if (asciiSepMatch) {
|
|
7908
|
-
if (table && table.headers.length > 0 && table.colAligns.length === 0) {
|
|
7909
|
-
table.colAligns = table.headers.map(() => "left");
|
|
7910
|
-
continue;
|
|
7911
|
-
}
|
|
7912
|
-
if (paragraph.length === 1) {
|
|
7913
|
-
const prevLine = paragraph[0].trim();
|
|
7914
|
-
const tokens3 = prevLine.split(/\s{2,}/).map((c) => c.trim()).filter((c) => c.length > 0);
|
|
7915
|
-
if (tokens3.length >= 2 && tokens3.length <= 10) {
|
|
7916
|
-
paragraph = [];
|
|
7917
|
-
flushList();
|
|
7918
|
-
flushQuote();
|
|
7919
|
-
flushCode();
|
|
7920
|
-
table = { headers: tokens3, rows: [], colAligns: tokens3.map(() => "left") };
|
|
7921
|
-
continue;
|
|
8810
|
+
const followingMarker2 = parseListMarker(lines[afterBlank] ?? "");
|
|
8811
|
+
if (!followingMarker2 || followingMarker2.indent < first2.indent) {
|
|
8812
|
+
nextIndex = afterBlank;
|
|
8813
|
+
break;
|
|
7922
8814
|
}
|
|
8815
|
+
nextIndex = afterBlank;
|
|
8816
|
+
if (followingMarker2.indent === first2.indent) break;
|
|
7923
8817
|
}
|
|
7924
|
-
|
|
7925
|
-
|
|
7926
|
-
|
|
7927
|
-
|
|
7928
|
-
|
|
7929
|
-
|
|
7930
|
-
if (!looksLikeCode && !looksLikePath) {
|
|
7931
|
-
table.rows.push(tokens3);
|
|
8818
|
+
const followingMarker = parseListMarker(lines[nextIndex] ?? "");
|
|
8819
|
+
if (followingMarker) {
|
|
8820
|
+
if (followingMarker.indent <= first2.indent) break;
|
|
8821
|
+
if (depth >= MAX_LIST_DEPTH) {
|
|
8822
|
+
item.text = `${item.text ?? ""} ${followingMarker.text}`.trim();
|
|
8823
|
+
nextIndex += 1;
|
|
7932
8824
|
continue;
|
|
7933
8825
|
}
|
|
8826
|
+
const nested = parseListBlock(lines, nextIndex, depth + 1);
|
|
8827
|
+
if (!nested) break;
|
|
8828
|
+
item.children ??= [];
|
|
8829
|
+
item.children.push(nested.block);
|
|
8830
|
+
nextIndex = nested.nextIndex;
|
|
8831
|
+
continue;
|
|
7934
8832
|
}
|
|
7935
|
-
|
|
7936
|
-
|
|
7937
|
-
|
|
7938
|
-
|
|
7939
|
-
|
|
7940
|
-
|
|
8833
|
+
if (interruptsList(lines, nextIndex)) break;
|
|
8834
|
+
const continuation = (lines[nextIndex] ?? "").trim();
|
|
8835
|
+
item.text = `${item.text ?? ""} ${continuation}`.trim();
|
|
8836
|
+
nextIndex += 1;
|
|
8837
|
+
}
|
|
8838
|
+
items.push(item);
|
|
8839
|
+
}
|
|
8840
|
+
const block = { type: first2.ordered ? "ol" : "ul", items };
|
|
8841
|
+
if (first2.ordered && first2.start !== 1) block.start = first2.start;
|
|
8842
|
+
return { block, nextIndex };
|
|
8843
|
+
}
|
|
8844
|
+
function interruptsList(lines, index) {
|
|
8845
|
+
const line = lines[index] ?? "";
|
|
8846
|
+
const trimmed = line.trim();
|
|
8847
|
+
return Boolean(
|
|
8848
|
+
line.match(HEADING_RE) || trimmed.match(QUOTE_RE) || parseOpeningFence(line) || isHorizontalRule(trimmed) || parseMarkdownTable(lines, index) || parseAsciiTable(lines, index)
|
|
8849
|
+
);
|
|
8850
|
+
}
|
|
8851
|
+
function parseOpeningFence(line) {
|
|
8852
|
+
const match = line.match(/^ {0,3}(`{3,}|~{3,})(.*)$/);
|
|
8853
|
+
if (!match) return null;
|
|
8854
|
+
const info = match[2].trim();
|
|
8855
|
+
if (match[1][0] === "`" && info.includes("`")) return null;
|
|
8856
|
+
return { fence: match[1], lang: info.split(/\s+/, 1)[0] ?? "" };
|
|
8857
|
+
}
|
|
8858
|
+
function isClosingFence(line, opening) {
|
|
8859
|
+
const match = line.match(/^ {0,3}(`+|~+)[ \t]*$/);
|
|
8860
|
+
return Boolean(
|
|
8861
|
+
match && match[1][0] === opening[0] && match[1].length >= opening.length
|
|
8862
|
+
);
|
|
8863
|
+
}
|
|
8864
|
+
function isHorizontalRule(line) {
|
|
8865
|
+
return /^(?:(?:\*[ \t]*){3,}|(?:-[ \t]*){3,}|(?:_[ \t]*){3,})$/.test(line);
|
|
8866
|
+
}
|
|
8867
|
+
function isSetextHeadingText(line) {
|
|
8868
|
+
const trimmed = line.trim();
|
|
8869
|
+
return trimmed !== "" && !HEADING_RE.test(line) && !QUOTE_RE.test(trimmed) && !UL_RE.test(trimmed) && !OL_RE.test(trimmed) && !parseOpeningFence(line);
|
|
8870
|
+
}
|
|
8871
|
+
function parseMarkdownTable(lines, start) {
|
|
8872
|
+
const headers = splitTableRow(lines[start] ?? "");
|
|
8873
|
+
const delimiter = splitTableRow(lines[start + 1] ?? "");
|
|
8874
|
+
if (!headers || !delimiter || headers.length < 2 || headers.length > MAX_TABLE_COLUMNS || delimiter.length !== headers.length || !delimiter.every((cell) => /^:?-{3,}:?$/.test(cell))) {
|
|
8875
|
+
return null;
|
|
7941
8876
|
}
|
|
7942
|
-
|
|
7943
|
-
|
|
7944
|
-
|
|
8877
|
+
const colAligns = delimiter.map((cell) => {
|
|
8878
|
+
if (cell.startsWith(":") && cell.endsWith(":")) return "center";
|
|
8879
|
+
if (cell.endsWith(":")) return "right";
|
|
8880
|
+
return "left";
|
|
8881
|
+
});
|
|
8882
|
+
const rows = [];
|
|
8883
|
+
let nextIndex = start + 2;
|
|
8884
|
+
while (nextIndex < lines.length) {
|
|
8885
|
+
const cells = splitTableRow(lines[nextIndex] ?? "");
|
|
8886
|
+
if (!cells) break;
|
|
8887
|
+
rows.push(headers.map((_, index) => cells[index] ?? ""));
|
|
8888
|
+
nextIndex += 1;
|
|
8889
|
+
}
|
|
8890
|
+
return {
|
|
8891
|
+
block: { type: "table", headers, rows, colAligns },
|
|
8892
|
+
nextIndex
|
|
8893
|
+
};
|
|
8894
|
+
}
|
|
8895
|
+
function splitTableRow(line) {
|
|
8896
|
+
const source = line.trim();
|
|
8897
|
+
if (!source.includes("|")) return null;
|
|
8898
|
+
const cells = [];
|
|
8899
|
+
let cell = "";
|
|
8900
|
+
let codeFenceLength = 0;
|
|
8901
|
+
let foundSeparator = false;
|
|
8902
|
+
for (let index = 0; index < source.length; index += 1) {
|
|
8903
|
+
const character = source[index] ?? "";
|
|
8904
|
+
if (character === "\\" && source[index + 1] === "|") {
|
|
8905
|
+
cell += "|";
|
|
8906
|
+
index += 1;
|
|
8907
|
+
continue;
|
|
8908
|
+
}
|
|
8909
|
+
if (character === "`") {
|
|
8910
|
+
let end = index + 1;
|
|
8911
|
+
while (source[end] === "`") end += 1;
|
|
8912
|
+
const runLength = end - index;
|
|
8913
|
+
if (codeFenceLength === 0) codeFenceLength = runLength;
|
|
8914
|
+
else if (codeFenceLength === runLength) codeFenceLength = 0;
|
|
8915
|
+
cell += source.slice(index, end);
|
|
8916
|
+
index = end - 1;
|
|
8917
|
+
continue;
|
|
8918
|
+
}
|
|
8919
|
+
if (character === "|" && codeFenceLength === 0) {
|
|
8920
|
+
foundSeparator = true;
|
|
8921
|
+
cells.push(cell.trim());
|
|
8922
|
+
cell = "";
|
|
8923
|
+
continue;
|
|
8924
|
+
}
|
|
8925
|
+
cell += character;
|
|
8926
|
+
}
|
|
8927
|
+
cells.push(cell.trim());
|
|
8928
|
+
if (!foundSeparator) return null;
|
|
8929
|
+
if (cells[0] === "") cells.shift();
|
|
8930
|
+
if (cells[cells.length - 1] === "") cells.pop();
|
|
8931
|
+
return cells.length >= 2 ? cells : null;
|
|
8932
|
+
}
|
|
8933
|
+
function parseAsciiTable(lines, start) {
|
|
8934
|
+
const headers = splitAsciiCells(lines[start] ?? "");
|
|
8935
|
+
if (headers.length < 2 || headers.length > MAX_TABLE_COLUMNS) return null;
|
|
8936
|
+
const separator = (lines[start + 1] ?? "").trim();
|
|
8937
|
+
const separatorCells = splitAsciiCells(separator);
|
|
8938
|
+
const validSeparator = /^[─┄┈━┅-]{3,}$/.test(separator) || separatorCells.length === headers.length && separatorCells.every((cell) => /^[─┄┈━┅-]{3,}$/.test(cell));
|
|
8939
|
+
if (!validSeparator) return null;
|
|
8940
|
+
const rows = [];
|
|
8941
|
+
let nextIndex = start + 2;
|
|
8942
|
+
while (nextIndex < lines.length) {
|
|
8943
|
+
const cells = splitAsciiCells(lines[nextIndex] ?? "");
|
|
8944
|
+
if (cells.length !== headers.length) break;
|
|
8945
|
+
rows.push(cells);
|
|
8946
|
+
nextIndex += 1;
|
|
8947
|
+
}
|
|
8948
|
+
if (rows.length === 0) return null;
|
|
8949
|
+
return {
|
|
8950
|
+
block: {
|
|
8951
|
+
type: "table",
|
|
8952
|
+
headers,
|
|
8953
|
+
rows,
|
|
8954
|
+
colAligns: headers.map(() => "left")
|
|
8955
|
+
},
|
|
8956
|
+
nextIndex
|
|
8957
|
+
};
|
|
8958
|
+
}
|
|
8959
|
+
function splitAsciiCells(line) {
|
|
8960
|
+
return line.trim().split(/\s{2,}/).map((cell) => cell.trim()).filter(Boolean);
|
|
7945
8961
|
}
|
|
7946
8962
|
var MAX_INLINE_DEPTH = 10;
|
|
8963
|
+
var INLINE_ESCAPE_BASE = 57344;
|
|
8964
|
+
var ESCAPABLE_MARKDOWN = ["\\", "`", "*", "_", "[", "]", "{", "}", "(", ")", "#", "+", "-", ".", "!", "|", ">"];
|
|
8965
|
+
function protectInlineEscapes(text) {
|
|
8966
|
+
return text.replace(/\\([\\`*_[\]{}()#+\-.!|>])/g, (_match, character) => {
|
|
8967
|
+
const index = ESCAPABLE_MARKDOWN.indexOf(character);
|
|
8968
|
+
return String.fromCodePoint(INLINE_ESCAPE_BASE + index);
|
|
8969
|
+
});
|
|
8970
|
+
}
|
|
8971
|
+
function restoreInlineEscapes(text) {
|
|
8972
|
+
return [...text].map((character) => {
|
|
8973
|
+
const index = character.codePointAt(0) - INLINE_ESCAPE_BASE;
|
|
8974
|
+
return index >= 0 && index < ESCAPABLE_MARKDOWN.length ? ESCAPABLE_MARKDOWN[index] : character;
|
|
8975
|
+
}).join("");
|
|
8976
|
+
}
|
|
7947
8977
|
function inline(text, depth = 0) {
|
|
7948
8978
|
if (depth > MAX_INLINE_DEPTH) {
|
|
7949
|
-
return text;
|
|
8979
|
+
return restoreInlineEscapes(text);
|
|
7950
8980
|
}
|
|
7951
8981
|
const nodes = [];
|
|
7952
|
-
let rest = text;
|
|
8982
|
+
let rest = protectInlineEscapes(text);
|
|
7953
8983
|
let key = 0;
|
|
7954
8984
|
const matchers = [
|
|
7955
8985
|
{
|
|
7956
|
-
re: /(!\[)([^\]]*)\]\(([^)]
|
|
8986
|
+
re: /(!\[)([^\]]*)\]\(((?:[^()\\]|\\.|\([^()]*\))+?)\)/,
|
|
7957
8987
|
handler: (m) => /* @__PURE__ */ jsxs10(Text10, { color: "cyan", bold: true, children: [
|
|
7958
8988
|
"[img: ",
|
|
7959
|
-
m[2],
|
|
8989
|
+
restoreInlineEscapes(m[2]),
|
|
7960
8990
|
"]"
|
|
7961
8991
|
] }, key++)
|
|
7962
8992
|
},
|
|
7963
8993
|
{
|
|
7964
|
-
re: /(\[)([^\]]*)\]\(([^)]
|
|
7965
|
-
handler: (m) =>
|
|
7966
|
-
|
|
7967
|
-
/* @__PURE__ */ jsxs10(
|
|
7968
|
-
|
|
7969
|
-
|
|
7970
|
-
|
|
7971
|
-
|
|
7972
|
-
|
|
8994
|
+
re: /(\[)([^\]]*)\]\(((?:[^()\\]|\\.|\([^()]*\))+?)\)/,
|
|
8995
|
+
handler: (m) => {
|
|
8996
|
+
const fragmentKey = key++;
|
|
8997
|
+
return /* @__PURE__ */ jsxs10(Fragment4, { children: [
|
|
8998
|
+
/* @__PURE__ */ jsx11(Text10, { color: "cyan", underline: true, children: inline(m[2], depth + 1) }),
|
|
8999
|
+
/* @__PURE__ */ jsxs10(Text10, { dimColor: true, color: "gray", children: [
|
|
9000
|
+
"(",
|
|
9001
|
+
restoreInlineEscapes(m[3]),
|
|
9002
|
+
")"
|
|
9003
|
+
] })
|
|
9004
|
+
] }, fragmentKey);
|
|
9005
|
+
}
|
|
7973
9006
|
},
|
|
7974
9007
|
{
|
|
7975
9008
|
re: /(`+)([^`]+?)\1/,
|
|
7976
|
-
handler: (m) => /* @__PURE__ */
|
|
9009
|
+
handler: (m) => /* @__PURE__ */ jsx11(Text10, { bold: true, color: "cyan", children: restoreInlineEscapes(m[2]) }, key++)
|
|
9010
|
+
},
|
|
9011
|
+
{
|
|
9012
|
+
re: /(\*\*\*|___)(?=\S)(.*?\S)\1/,
|
|
9013
|
+
handler: (m) => /* @__PURE__ */ jsx11(Text10, { bold: true, italic: true, children: inline(m[2], depth + 1) }, key++)
|
|
9014
|
+
},
|
|
9015
|
+
{
|
|
9016
|
+
re: /(\*\*)(?=\S)(.*?\S)\1/,
|
|
9017
|
+
handler: (m) => /* @__PURE__ */ jsx11(Text10, { bold: true, children: inline(m[2], depth + 1) }, key++)
|
|
7977
9018
|
},
|
|
7978
9019
|
{
|
|
7979
|
-
re: /(
|
|
7980
|
-
handler: (m) => /* @__PURE__ */
|
|
9020
|
+
re: /(__)(?=\S)(.*?\S)\1/,
|
|
9021
|
+
handler: (m) => /* @__PURE__ */ jsx11(Text10, { bold: true, children: inline(m[2], depth + 1) }, key++)
|
|
7981
9022
|
},
|
|
7982
9023
|
{
|
|
7983
|
-
re: /(
|
|
7984
|
-
handler: (m) => /* @__PURE__ */
|
|
9024
|
+
re: /(~~)(?=\S)(.*?\S)\1/,
|
|
9025
|
+
handler: (m) => /* @__PURE__ */ jsx11(Text10, { strikethrough: true, children: inline(m[2], depth + 1) }, key++)
|
|
7985
9026
|
},
|
|
7986
9027
|
{
|
|
7987
|
-
re: /(
|
|
7988
|
-
handler: (m) => /* @__PURE__ */
|
|
9028
|
+
re: /(\*)(?=\S)(.*?\S)\1/,
|
|
9029
|
+
handler: (m) => /* @__PURE__ */ jsx11(Text10, { italic: true, children: inline(m[2], depth + 1) }, key++)
|
|
7989
9030
|
},
|
|
7990
9031
|
{
|
|
7991
|
-
re: /(
|
|
7992
|
-
handler: (m) => /* @__PURE__ */
|
|
9032
|
+
re: /(?<!\w)(_)(?=\S)(.*?\S)\1(?!\w)/,
|
|
9033
|
+
handler: (m) => /* @__PURE__ */ jsx11(Text10, { italic: true, children: inline(m[2], depth + 1) }, key++)
|
|
7993
9034
|
}
|
|
7994
9035
|
];
|
|
7995
9036
|
while (rest.length > 0) {
|
|
@@ -8001,11 +9042,11 @@ function inline(text, depth = 0) {
|
|
|
8001
9042
|
}
|
|
8002
9043
|
}
|
|
8003
9044
|
if (!best) {
|
|
8004
|
-
nodes.push(rest);
|
|
9045
|
+
nodes.push(restoreInlineEscapes(rest));
|
|
8005
9046
|
break;
|
|
8006
9047
|
}
|
|
8007
9048
|
const idx = best.match.index ?? 0;
|
|
8008
|
-
if (idx > 0) nodes.push(rest.slice(0, idx));
|
|
9049
|
+
if (idx > 0) nodes.push(restoreInlineEscapes(rest.slice(0, idx)));
|
|
8009
9050
|
nodes.push(best.handler(best.match));
|
|
8010
9051
|
rest = rest.slice(idx + best.match[0].length);
|
|
8011
9052
|
}
|
|
@@ -8013,12 +9054,22 @@ function inline(text, depth = 0) {
|
|
|
8013
9054
|
}
|
|
8014
9055
|
|
|
8015
9056
|
// src/ui/components/Transcript.tsx
|
|
8016
|
-
import { Fragment as Fragment5, jsx as
|
|
9057
|
+
import { Fragment as Fragment5, jsx as jsx12, jsxs as jsxs11 } from "react/jsx-runtime";
|
|
8017
9058
|
var HEADER = { kind: "header" };
|
|
8018
|
-
|
|
8019
|
-
|
|
8020
|
-
|
|
9059
|
+
function assistantThinkingForFrame(bubble, frozenThinking) {
|
|
9060
|
+
return bubble.streaming && bubble.text !== "" ? frozenThinking ?? bubble.thinking : bubble.thinking;
|
|
9061
|
+
}
|
|
9062
|
+
function firstMutableBubbleIndex(bubbles) {
|
|
9063
|
+
return bubbles.findIndex(
|
|
9064
|
+
(bubble) => bubble.kind === "assistant" && bubble.streaming || bubble.kind === "tool" && bubble.state === "running" || bubble.kind === "subagent" && bubble.state === "running"
|
|
8021
9065
|
);
|
|
9066
|
+
}
|
|
9067
|
+
var Transcript = memo2(function Transcript2({
|
|
9068
|
+
bubbles,
|
|
9069
|
+
workspace,
|
|
9070
|
+
maxLiveRows = 12
|
|
9071
|
+
}) {
|
|
9072
|
+
const liveAt = firstMutableBubbleIndex(bubbles);
|
|
8022
9073
|
const stableCount = liveAt === -1 ? bubbles.length : liveAt;
|
|
8023
9074
|
const stableRef = useRef3([]);
|
|
8024
9075
|
if (stableRef.current.length < stableCount) {
|
|
@@ -8029,41 +9080,106 @@ var Transcript = memo2(function Transcript2({ bubbles, workspace }) {
|
|
|
8029
9080
|
} else if (stableRef.current.length > stableCount) {
|
|
8030
9081
|
stableRef.current = bubbles.slice(0, stableCount);
|
|
8031
9082
|
}
|
|
8032
|
-
const stable = stableRef.current;
|
|
8033
9083
|
const live = liveAt === -1 ? [] : bubbles.slice(liveAt);
|
|
8034
|
-
const staticItems = [HEADER, ...
|
|
8035
|
-
return /* @__PURE__ */ jsxs11(
|
|
8036
|
-
|
|
8037
|
-
|
|
8038
|
-
|
|
9084
|
+
const staticItems = [HEADER, ...stableRef.current];
|
|
9085
|
+
return /* @__PURE__ */ jsxs11(
|
|
9086
|
+
Box12,
|
|
9087
|
+
{
|
|
9088
|
+
flexDirection: "column",
|
|
9089
|
+
marginBottom: 1,
|
|
9090
|
+
flexShrink: 1,
|
|
9091
|
+
minHeight: 0,
|
|
9092
|
+
overflowY: "hidden",
|
|
9093
|
+
children: [
|
|
9094
|
+
/* @__PURE__ */ jsx12(Static, { items: staticItems, children: (item) => item.kind === "header" ? /* @__PURE__ */ jsx12(Logo, { workspace }, "kitcode-header") : /* @__PURE__ */ jsx12(BubbleView, { bubble: item }, item.id) }),
|
|
9095
|
+
/* @__PURE__ */ jsx12(
|
|
9096
|
+
Box12,
|
|
9097
|
+
{
|
|
9098
|
+
flexDirection: "column",
|
|
9099
|
+
flexShrink: 1,
|
|
9100
|
+
minHeight: 0,
|
|
9101
|
+
maxHeight: maxLiveRows,
|
|
9102
|
+
overflowY: "hidden",
|
|
9103
|
+
justifyContent: "flex-end",
|
|
9104
|
+
children: live.map((bubble) => /* @__PURE__ */ jsx12(BubbleView, { bubble, maxRows: maxLiveRows }, bubble.id))
|
|
9105
|
+
}
|
|
9106
|
+
)
|
|
9107
|
+
]
|
|
9108
|
+
}
|
|
9109
|
+
);
|
|
8039
9110
|
});
|
|
8040
|
-
var BubbleView = memo2(function BubbleView2({
|
|
9111
|
+
var BubbleView = memo2(function BubbleView2({
|
|
9112
|
+
bubble,
|
|
9113
|
+
maxRows
|
|
9114
|
+
}) {
|
|
8041
9115
|
const theme = useTheme();
|
|
8042
9116
|
if (bubble.kind === "user") {
|
|
8043
|
-
return /* @__PURE__ */
|
|
9117
|
+
return /* @__PURE__ */ jsx12(Box12, { marginTop: 1, children: /* @__PURE__ */ jsxs11(Text11, { color: theme.accent, bold: true, children: [
|
|
8044
9118
|
"\u203A ",
|
|
8045
9119
|
bubble.text
|
|
8046
9120
|
] }) });
|
|
8047
9121
|
}
|
|
8048
9122
|
if (bubble.kind === "notice") {
|
|
8049
9123
|
const color = bubble.level === "error" ? theme.error : bubble.level === "warn" ? theme.warn : theme.accent;
|
|
8050
|
-
return /* @__PURE__ */
|
|
9124
|
+
return /* @__PURE__ */ jsx12(Box12, { marginTop: 1, children: /* @__PURE__ */ jsx12(Text11, { color, children: bubble.text }) });
|
|
8051
9125
|
}
|
|
8052
9126
|
if (bubble.kind === "assistant") {
|
|
8053
|
-
return /* @__PURE__ */
|
|
8054
|
-
bubble.thinking.trim() !== "" && /* @__PURE__ */ jsx11(Text11, { dimColor: true, italic: true, children: bubble.thinking.trim() }),
|
|
8055
|
-
bubble.streaming ? /* @__PURE__ */ jsx11(Text11, { children: bubble.text }) : /* @__PURE__ */ jsx11(Markdown, { children: bubble.text }),
|
|
8056
|
-
bubble.streaming && bubble.text === "" && /* @__PURE__ */ jsxs11(Text11, { dimColor: true, children: [
|
|
8057
|
-
/* @__PURE__ */ jsx11(Spinner2, { type: "dots" }),
|
|
8058
|
-
" thinking"
|
|
8059
|
-
] })
|
|
8060
|
-
] });
|
|
9127
|
+
return /* @__PURE__ */ jsx12(AssistantView, { bubble, maxRows });
|
|
8061
9128
|
}
|
|
8062
9129
|
if (bubble.kind === "subagent") {
|
|
8063
|
-
return /* @__PURE__ */
|
|
9130
|
+
return /* @__PURE__ */ jsx12(SubagentView, { bubble });
|
|
8064
9131
|
}
|
|
8065
|
-
return /* @__PURE__ */
|
|
9132
|
+
return /* @__PURE__ */ jsx12(ToolView, { bubble });
|
|
8066
9133
|
});
|
|
9134
|
+
function AssistantView({ bubble, maxRows }) {
|
|
9135
|
+
const { columns } = useWindowSize3();
|
|
9136
|
+
const frozenThinking = useRef3(void 0);
|
|
9137
|
+
const answering = bubble.streaming && bubble.text !== "";
|
|
9138
|
+
if (!answering) frozenThinking.current = void 0;
|
|
9139
|
+
if (answering && frozenThinking.current === void 0) {
|
|
9140
|
+
frozenThinking.current = bubble.thinking;
|
|
9141
|
+
}
|
|
9142
|
+
const visibleThinking = assistantThinkingForFrame(bubble, frozenThinking.current);
|
|
9143
|
+
const liveBudget = bubble.streaming && maxRows !== void 0 ? Math.max(1, maxRows - 1) : void 0;
|
|
9144
|
+
const thinkingBudget = liveBudget === void 0 ? void 0 : bubble.text !== "" ? Math.min(3, Math.max(0, liveBudget - 1)) : Math.max(0, liveBudget - 1);
|
|
9145
|
+
const frameThinking = thinkingBudget === void 0 ? visibleThinking.trim() : clipTextToRows(visibleThinking.trim(), thinkingBudget, columns);
|
|
9146
|
+
const usedThinkingRows = frameThinking === "" ? 0 : frameThinking.split("\n").length;
|
|
9147
|
+
const frameText = liveBudget === void 0 ? bubble.text : clipTextToRows(bubble.text, Math.max(1, liveBudget - usedThinkingRows), columns);
|
|
9148
|
+
return /* @__PURE__ */ jsxs11(Box12, { flexDirection: "column", marginTop: 1, children: [
|
|
9149
|
+
frameThinking !== "" && /* @__PURE__ */ jsx12(Text11, { dimColor: true, italic: true, children: frameThinking }),
|
|
9150
|
+
bubble.streaming ? /* @__PURE__ */ jsx12(Text11, { children: frameText }) : /* @__PURE__ */ jsx12(Markdown, { children: bubble.text }),
|
|
9151
|
+
bubble.streaming && bubble.text === "" && /* @__PURE__ */ jsxs11(Text11, { dimColor: true, children: [
|
|
9152
|
+
/* @__PURE__ */ jsx12(Spinner2, { type: "dots" }),
|
|
9153
|
+
" thinking"
|
|
9154
|
+
] })
|
|
9155
|
+
] });
|
|
9156
|
+
}
|
|
9157
|
+
function clipTextToRows(text, maxRows, columns) {
|
|
9158
|
+
if (text === "" || maxRows <= 0) return "";
|
|
9159
|
+
const width = Number.isFinite(columns) ? Math.max(1, Math.floor(columns)) : 80;
|
|
9160
|
+
const rows = text.replace(/\r\n?/g, "\n").split("\n").flatMap((line) => wrapVisualLine(line, width));
|
|
9161
|
+
if (rows.length <= maxRows) return rows.join("\n");
|
|
9162
|
+
const tail2 = rows.slice(-Math.max(1, maxRows - 1));
|
|
9163
|
+
return maxRows === 1 ? tail2.at(-1) ?? "" : ["\u2026", ...tail2].join("\n");
|
|
9164
|
+
}
|
|
9165
|
+
function wrapVisualLine(line, columns) {
|
|
9166
|
+
if (line === "") return [""];
|
|
9167
|
+
const rows = [];
|
|
9168
|
+
let current = "";
|
|
9169
|
+
let currentWidth = 0;
|
|
9170
|
+
for (const character of line) {
|
|
9171
|
+
const width = Math.max(1, stringWidth2(character));
|
|
9172
|
+
if (current !== "" && currentWidth + width > columns) {
|
|
9173
|
+
rows.push(current);
|
|
9174
|
+
current = "";
|
|
9175
|
+
currentWidth = 0;
|
|
9176
|
+
}
|
|
9177
|
+
current += character;
|
|
9178
|
+
currentWidth += width;
|
|
9179
|
+
}
|
|
9180
|
+
if (current !== "" || rows.length === 0) rows.push(current);
|
|
9181
|
+
return rows;
|
|
9182
|
+
}
|
|
8067
9183
|
function ToolView({ bubble }) {
|
|
8068
9184
|
const theme = useTheme();
|
|
8069
9185
|
const mark = bubble.state === "running" ? "\u25CC" : bubble.state === "ok" ? "\u25CF" : "\u2717";
|
|
@@ -8073,19 +9189,19 @@ function ToolView({ bubble }) {
|
|
|
8073
9189
|
() => display?.kind === "diff" ? diffLines(display.before, display.after) : null,
|
|
8074
9190
|
[display]
|
|
8075
9191
|
);
|
|
8076
|
-
if (!diff) return /* @__PURE__ */
|
|
9192
|
+
if (!diff) return /* @__PURE__ */ jsx12(NonDiffToolView, { bubble, mark, color });
|
|
8077
9193
|
const hasChanges = diff.added > 0 || diff.removed > 0;
|
|
8078
|
-
return /* @__PURE__ */ jsxs11(
|
|
9194
|
+
return /* @__PURE__ */ jsxs11(Box12, { flexDirection: "column", marginTop: 1, children: [
|
|
8079
9195
|
/* @__PURE__ */ jsxs11(Text11, { color, children: [
|
|
8080
9196
|
mark,
|
|
8081
9197
|
" ",
|
|
8082
9198
|
bubble.summary,
|
|
8083
9199
|
hasChanges && /* @__PURE__ */ jsxs11(Fragment5, { children: [
|
|
8084
|
-
diff.added > 0 && /* @__PURE__ */
|
|
8085
|
-
diff.removed > 0 && /* @__PURE__ */
|
|
9200
|
+
diff.added > 0 && /* @__PURE__ */ jsx12(Text11, { color: theme.ok, bold: true, children: ` +${diff.added}` }),
|
|
9201
|
+
diff.removed > 0 && /* @__PURE__ */ jsx12(Text11, { color: theme.error, bold: true, children: ` -${diff.removed}` })
|
|
8086
9202
|
] })
|
|
8087
9203
|
] }),
|
|
8088
|
-
/* @__PURE__ */
|
|
9204
|
+
/* @__PURE__ */ jsx12(Box12, { marginLeft: 2, children: /* @__PURE__ */ jsx12(DiffHunk, { lines: diff.hunk, hidden: diff.hidden }) })
|
|
8089
9205
|
] });
|
|
8090
9206
|
}
|
|
8091
9207
|
function NonDiffToolView({
|
|
@@ -8093,13 +9209,13 @@ function NonDiffToolView({
|
|
|
8093
9209
|
mark,
|
|
8094
9210
|
color
|
|
8095
9211
|
}) {
|
|
8096
|
-
return /* @__PURE__ */ jsxs11(
|
|
9212
|
+
return /* @__PURE__ */ jsxs11(Box12, { flexDirection: "column", marginTop: 1, children: [
|
|
8097
9213
|
/* @__PURE__ */ jsxs11(Text11, { color, children: [
|
|
8098
9214
|
mark,
|
|
8099
9215
|
" ",
|
|
8100
9216
|
bubble.summary
|
|
8101
9217
|
] }),
|
|
8102
|
-
bubble.state !== "running" && bubble.content.trim() !== "" && /* @__PURE__ */
|
|
9218
|
+
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
9219
|
] });
|
|
8104
9220
|
}
|
|
8105
9221
|
function previewLines(content) {
|
|
@@ -8111,15 +9227,15 @@ function SubagentView({ bubble }) {
|
|
|
8111
9227
|
const theme = useTheme();
|
|
8112
9228
|
const mark = bubble.state === "running" ? "\u25CC" : "\u25CF";
|
|
8113
9229
|
const color = bubble.state === "running" ? theme.warn : theme.ok;
|
|
8114
|
-
return /* @__PURE__ */ jsxs11(
|
|
9230
|
+
return /* @__PURE__ */ jsxs11(Box12, { flexDirection: "column", marginTop: 1, children: [
|
|
8115
9231
|
/* @__PURE__ */ jsxs11(Text11, { color, bold: true, children: [
|
|
8116
9232
|
mark,
|
|
8117
9233
|
" subagent: ",
|
|
8118
9234
|
bubble.description
|
|
8119
9235
|
] }),
|
|
8120
|
-
/* @__PURE__ */ jsxs11(
|
|
8121
|
-
bubble.bubbles.map((inner, index) => /* @__PURE__ */
|
|
8122
|
-
bubble.state === "done" && bubble.result && /* @__PURE__ */
|
|
9236
|
+
/* @__PURE__ */ jsxs11(Box12, { marginLeft: 2, flexDirection: "column", children: [
|
|
9237
|
+
bubble.bubbles.map((inner, index) => /* @__PURE__ */ jsx12(BubbleView, { bubble: inner }, index)),
|
|
9238
|
+
bubble.state === "done" && bubble.result && /* @__PURE__ */ jsx12(Box12, { marginTop: 1, children: /* @__PURE__ */ jsx12(Text11, { dimColor: true, children: "\u2500\u2500 result \u2500\u2500" }) })
|
|
8123
9239
|
] })
|
|
8124
9240
|
] });
|
|
8125
9241
|
}
|
|
@@ -8361,26 +9477,17 @@ function sanitizeDisplay(display) {
|
|
|
8361
9477
|
}
|
|
8362
9478
|
|
|
8363
9479
|
// src/ui/App.tsx
|
|
8364
|
-
import { jsx as
|
|
9480
|
+
import { jsx as jsx13, jsxs as jsxs12 } from "react/jsx-runtime";
|
|
8365
9481
|
var EFFORTS = ["low", "medium", "high", "xhigh", "max"];
|
|
8366
9482
|
var STREAM_FRAME_MS = 50;
|
|
8367
9483
|
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
9484
|
function App({
|
|
8379
9485
|
runtime,
|
|
8380
9486
|
initialHistory,
|
|
8381
9487
|
warnings = []
|
|
8382
9488
|
}) {
|
|
8383
9489
|
const { exit } = useApp();
|
|
9490
|
+
const { rows } = useWindowSize4();
|
|
8384
9491
|
const [transcript, setTranscript] = useState5(
|
|
8385
9492
|
() => warnings.reduce((state, text) => pushNotice(state, "warn", text), fromHistory(initialHistory))
|
|
8386
9493
|
);
|
|
@@ -8390,8 +9497,12 @@ function App({
|
|
|
8390
9497
|
() => inputHistoryFromMessages(initialHistory)
|
|
8391
9498
|
);
|
|
8392
9499
|
const [busy, setBusy] = useState5(false);
|
|
9500
|
+
const busyRef = useRef4(false);
|
|
8393
9501
|
const [pendingCount, setPendingCount] = useState5(0);
|
|
8394
9502
|
const queueRef = useRef4([]);
|
|
9503
|
+
const slashQueueRef = useRef4(Promise.resolve());
|
|
9504
|
+
const slashPendingRef = useRef4(0);
|
|
9505
|
+
const drainQueueRef = useRef4(() => void 0);
|
|
8395
9506
|
const [attachments, setAttachments] = useState5([]);
|
|
8396
9507
|
const attachmentsRef = useRef4([]);
|
|
8397
9508
|
const automaticAttachmentTask = useRef4(null);
|
|
@@ -8458,43 +9569,6 @@ function App({
|
|
|
8458
9569
|
const timer = setInterval(() => tick((n) => n + 1), 1e3);
|
|
8459
9570
|
return () => clearInterval(timer);
|
|
8460
9571
|
}, [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
9572
|
const theme = useMemo4(() => makeTheme(accent), [accent]);
|
|
8499
9573
|
const strings = useMemo4(() => stringsFor(lang), [lang]);
|
|
8500
9574
|
const notice = useCallback(
|
|
@@ -8506,11 +9580,10 @@ function App({
|
|
|
8506
9580
|
);
|
|
8507
9581
|
useEffect2(() => {
|
|
8508
9582
|
const check = runtime.startupUpdateCheck();
|
|
8509
|
-
if (!check) return;
|
|
8510
9583
|
let active2 = true;
|
|
8511
9584
|
void check.then((result) => {
|
|
8512
9585
|
if (active2 && result.status === "available") {
|
|
8513
|
-
notice("info", strings.updateAvailable(result.latest
|
|
9586
|
+
notice("info", strings.updateAvailable(result.latest, result.url));
|
|
8514
9587
|
}
|
|
8515
9588
|
});
|
|
8516
9589
|
return () => {
|
|
@@ -8540,8 +9613,10 @@ function App({
|
|
|
8540
9613
|
[]
|
|
8541
9614
|
);
|
|
8542
9615
|
const runAgent = useCallback(async () => {
|
|
9616
|
+
if (busyRef.current) return;
|
|
8543
9617
|
const controller = new AbortController();
|
|
8544
9618
|
abort.current = controller;
|
|
9619
|
+
busyRef.current = true;
|
|
8545
9620
|
setBusy(true);
|
|
8546
9621
|
setTurnStart(Date.now());
|
|
8547
9622
|
turns.current += 1;
|
|
@@ -8577,17 +9652,10 @@ function App({
|
|
|
8577
9652
|
} finally {
|
|
8578
9653
|
flushTranscriptEvents();
|
|
8579
9654
|
abort.current = null;
|
|
9655
|
+
busyRef.current = false;
|
|
8580
9656
|
setBusy(false);
|
|
8581
9657
|
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
|
-
}
|
|
9658
|
+
drainQueueRef.current();
|
|
8591
9659
|
}
|
|
8592
9660
|
}, [flushTranscriptEvents, notice, queueTranscriptEvent, runtime]);
|
|
8593
9661
|
const applyAccent = useCallback(
|
|
@@ -8811,6 +9879,17 @@ function App({
|
|
|
8811
9879
|
case "config":
|
|
8812
9880
|
notice("info", strings.configAt(runtime.configPath()));
|
|
8813
9881
|
return;
|
|
9882
|
+
case "update": {
|
|
9883
|
+
const result = await runtime.checkForUpdates();
|
|
9884
|
+
if (result.status === "available") {
|
|
9885
|
+
notice("info", strings.updateAvailable(result.latest, result.url));
|
|
9886
|
+
} else if (result.status === "current") {
|
|
9887
|
+
notice("info", strings.updateCurrent(result.current));
|
|
9888
|
+
} else {
|
|
9889
|
+
notice("warn", strings.updateFailed(result.reason));
|
|
9890
|
+
}
|
|
9891
|
+
return;
|
|
9892
|
+
}
|
|
8814
9893
|
case "login":
|
|
8815
9894
|
setSetup(true);
|
|
8816
9895
|
return;
|
|
@@ -9161,6 +10240,7 @@ ${strings.mcpAddUsage}`
|
|
|
9161
10240
|
case "compact": {
|
|
9162
10241
|
const controller = new AbortController();
|
|
9163
10242
|
abort.current = controller;
|
|
10243
|
+
busyRef.current = true;
|
|
9164
10244
|
setBusy(true);
|
|
9165
10245
|
setTurnStart(Date.now());
|
|
9166
10246
|
notice("info", strings.compactStarting);
|
|
@@ -9183,17 +10263,9 @@ ${strings.mcpAddUsage}`
|
|
|
9183
10263
|
}
|
|
9184
10264
|
} finally {
|
|
9185
10265
|
abort.current = null;
|
|
10266
|
+
busyRef.current = false;
|
|
9186
10267
|
setBusy(false);
|
|
9187
10268
|
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
10269
|
}
|
|
9198
10270
|
forceRender((n) => n + 1);
|
|
9199
10271
|
return;
|
|
@@ -9396,18 +10468,46 @@ Rename the file if you want a different name.`);
|
|
|
9396
10468
|
},
|
|
9397
10469
|
[applyAccent, appendAttachment, ask2, exit, notice, pick, replaceAttachments, runtime, strings]
|
|
9398
10470
|
);
|
|
10471
|
+
const runSlash = useCallback(
|
|
10472
|
+
(line) => {
|
|
10473
|
+
slashPendingRef.current += 1;
|
|
10474
|
+
const task = slashQueueRef.current.catch(() => void 0).then(() => handleSlash(line));
|
|
10475
|
+
slashQueueRef.current = task;
|
|
10476
|
+
void task.catch(
|
|
10477
|
+
(error) => notice("error", error instanceof Error ? error.message : String(error))
|
|
10478
|
+
);
|
|
10479
|
+
const finished = () => {
|
|
10480
|
+
slashPendingRef.current = Math.max(0, slashPendingRef.current - 1);
|
|
10481
|
+
drainQueueRef.current();
|
|
10482
|
+
};
|
|
10483
|
+
void task.then(finished, finished);
|
|
10484
|
+
},
|
|
10485
|
+
[handleSlash, notice]
|
|
10486
|
+
);
|
|
9399
10487
|
const enqueueNext = useCallback(
|
|
9400
10488
|
(item) => {
|
|
9401
10489
|
if (item.kind === "command") {
|
|
9402
|
-
|
|
10490
|
+
runSlash(item.line);
|
|
9403
10491
|
return;
|
|
9404
10492
|
}
|
|
9405
10493
|
setTranscript((state) => pushUser(state, userDisplay(item.text, item.content)));
|
|
9406
10494
|
history.current = [...history.current, { role: "user", content: item.content }];
|
|
9407
10495
|
void runAgent();
|
|
9408
10496
|
},
|
|
9409
|
-
[
|
|
10497
|
+
[runAgent, runSlash]
|
|
9410
10498
|
);
|
|
10499
|
+
const drainQueue = useCallback(() => {
|
|
10500
|
+
if (busyRef.current || slashPendingRef.current > 0) return;
|
|
10501
|
+
const [next, ...remaining] = queueRef.current;
|
|
10502
|
+
if (!next) {
|
|
10503
|
+
setPendingCount(0);
|
|
10504
|
+
return;
|
|
10505
|
+
}
|
|
10506
|
+
queueRef.current = remaining;
|
|
10507
|
+
setPendingCount(remaining.length);
|
|
10508
|
+
queueMicrotask(() => enqueueNext(next));
|
|
10509
|
+
}, [enqueueNext]);
|
|
10510
|
+
drainQueueRef.current = drainQueue;
|
|
9411
10511
|
const tryQueueAutomaticAttachment = useCallback(
|
|
9412
10512
|
(requestedPath) => {
|
|
9413
10513
|
if (!looksLikeAttachmentPath(requestedPath)) return Promise.resolve(false);
|
|
@@ -9469,12 +10569,12 @@ Rename the file if you want a different name.`);
|
|
|
9469
10569
|
const text = raw.trim();
|
|
9470
10570
|
const submitSlash = () => {
|
|
9471
10571
|
if (!detachedInput) setInput("");
|
|
9472
|
-
if (
|
|
10572
|
+
if (busyRef.current || slashPendingRef.current > 0) {
|
|
9473
10573
|
queueRef.current = [...queueRef.current, { kind: "command", line: text }];
|
|
9474
10574
|
setPendingCount(queueRef.current.length);
|
|
9475
10575
|
return;
|
|
9476
10576
|
}
|
|
9477
|
-
|
|
10577
|
+
runSlash(text);
|
|
9478
10578
|
};
|
|
9479
10579
|
if (isKnownSlashCommand(text)) {
|
|
9480
10580
|
submitSlash();
|
|
@@ -9498,7 +10598,7 @@ Rename the file if you want a different name.`);
|
|
|
9498
10598
|
];
|
|
9499
10599
|
replaceAttachments([]);
|
|
9500
10600
|
const item = { kind: "message", text, content };
|
|
9501
|
-
if (
|
|
10601
|
+
if (busyRef.current || slashPendingRef.current > 0) {
|
|
9502
10602
|
queueRef.current = [...queueRef.current, item];
|
|
9503
10603
|
setPendingCount(queueRef.current.length);
|
|
9504
10604
|
return;
|
|
@@ -9507,7 +10607,7 @@ Rename the file if you want a different name.`);
|
|
|
9507
10607
|
history.current = [...history.current, { role: "user", content }];
|
|
9508
10608
|
void runAgent();
|
|
9509
10609
|
},
|
|
9510
|
-
[
|
|
10610
|
+
[replaceAttachments, runAgent, runSlash, tryQueueAutomaticAttachment]
|
|
9511
10611
|
);
|
|
9512
10612
|
useTerminalInput((_char, key) => {
|
|
9513
10613
|
if (key.tab && key.shift) {
|
|
@@ -9517,6 +10617,10 @@ Rename the file if you want a different name.`);
|
|
|
9517
10617
|
return;
|
|
9518
10618
|
}
|
|
9519
10619
|
if (!key.escape) return;
|
|
10620
|
+
if (input !== "") {
|
|
10621
|
+
setInput("");
|
|
10622
|
+
return;
|
|
10623
|
+
}
|
|
9520
10624
|
if (busy && abort.current) {
|
|
9521
10625
|
abort.current.abort();
|
|
9522
10626
|
queueRef.current = [];
|
|
@@ -9541,10 +10645,10 @@ Rename the file if you want a different name.`);
|
|
|
9541
10645
|
}
|
|
9542
10646
|
setInput("");
|
|
9543
10647
|
});
|
|
9544
|
-
const shell = (children) => /* @__PURE__ */
|
|
10648
|
+
const shell = (children) => /* @__PURE__ */ jsx13(ThemeContext.Provider, { value: theme, children: /* @__PURE__ */ jsx13(StringsContext.Provider, { value: strings, children }) });
|
|
9545
10649
|
if (lang === void 0) {
|
|
9546
10650
|
return shell(
|
|
9547
|
-
/* @__PURE__ */
|
|
10651
|
+
/* @__PURE__ */ jsx13(
|
|
9548
10652
|
LanguagePicker,
|
|
9549
10653
|
{
|
|
9550
10654
|
onPick: (chosen) => {
|
|
@@ -9557,7 +10661,7 @@ Rename the file if you want a different name.`);
|
|
|
9557
10661
|
}
|
|
9558
10662
|
if (setup) {
|
|
9559
10663
|
return shell(
|
|
9560
|
-
/* @__PURE__ */
|
|
10664
|
+
/* @__PURE__ */ jsx13(
|
|
9561
10665
|
Onboarding,
|
|
9562
10666
|
{
|
|
9563
10667
|
onSubmit: async (url, key) => {
|
|
@@ -9572,16 +10676,17 @@ ${strings.configAt(runtime.configPath())}`);
|
|
|
9572
10676
|
);
|
|
9573
10677
|
}
|
|
9574
10678
|
return shell(
|
|
9575
|
-
/* @__PURE__ */ jsxs12(
|
|
9576
|
-
/* @__PURE__ */
|
|
10679
|
+
/* @__PURE__ */ jsxs12(TerminalViewport, { rows, children: [
|
|
10680
|
+
/* @__PURE__ */ jsx13(
|
|
9577
10681
|
Transcript,
|
|
9578
10682
|
{
|
|
9579
10683
|
bubbles: transcript.bubbles,
|
|
9580
|
-
workspace: runtime.cwd
|
|
10684
|
+
workspace: runtime.cwd,
|
|
10685
|
+
maxLiveRows: liveTranscriptRows(rows)
|
|
9581
10686
|
},
|
|
9582
10687
|
transcriptRevision
|
|
9583
10688
|
),
|
|
9584
|
-
overlay.kind === "permission" && /* @__PURE__ */
|
|
10689
|
+
overlay.kind === "permission" && /* @__PURE__ */ jsx13(
|
|
9585
10690
|
PermissionPrompt,
|
|
9586
10691
|
{
|
|
9587
10692
|
request: overlay.request,
|
|
@@ -9591,7 +10696,7 @@ ${strings.configAt(runtime.configPath())}`);
|
|
|
9591
10696
|
}
|
|
9592
10697
|
}
|
|
9593
10698
|
),
|
|
9594
|
-
overlay.kind === "picker" && /* @__PURE__ */
|
|
10699
|
+
overlay.kind === "picker" && /* @__PURE__ */ jsx13(
|
|
9595
10700
|
Picker,
|
|
9596
10701
|
{
|
|
9597
10702
|
title: overlay.title,
|
|
@@ -9606,7 +10711,7 @@ ${strings.configAt(runtime.configPath())}`);
|
|
|
9606
10711
|
}
|
|
9607
10712
|
}
|
|
9608
10713
|
),
|
|
9609
|
-
overlay.kind === "confirm" && /* @__PURE__ */
|
|
10714
|
+
overlay.kind === "confirm" && /* @__PURE__ */ jsx13(
|
|
9610
10715
|
Confirm,
|
|
9611
10716
|
{
|
|
9612
10717
|
title: overlay.title,
|
|
@@ -9618,7 +10723,7 @@ ${strings.configAt(runtime.configPath())}`);
|
|
|
9618
10723
|
}
|
|
9619
10724
|
}
|
|
9620
10725
|
),
|
|
9621
|
-
overlay.kind === "textinput" && /* @__PURE__ */
|
|
10726
|
+
overlay.kind === "textinput" && /* @__PURE__ */ jsx13(
|
|
9622
10727
|
TextInputOverlay,
|
|
9623
10728
|
{
|
|
9624
10729
|
title: overlay.title,
|
|
@@ -9633,7 +10738,7 @@ ${strings.configAt(runtime.configPath())}`);
|
|
|
9633
10738
|
}
|
|
9634
10739
|
}
|
|
9635
10740
|
),
|
|
9636
|
-
overlay.kind === "none" && /* @__PURE__ */
|
|
10741
|
+
overlay.kind === "none" && /* @__PURE__ */ jsx13(
|
|
9637
10742
|
PromptInput,
|
|
9638
10743
|
{
|
|
9639
10744
|
value: input,
|
|
@@ -9651,7 +10756,7 @@ ${strings.configAt(runtime.configPath())}`);
|
|
|
9651
10756
|
})
|
|
9652
10757
|
}
|
|
9653
10758
|
),
|
|
9654
|
-
/* @__PURE__ */
|
|
10759
|
+
/* @__PURE__ */ jsx13(
|
|
9655
10760
|
StatusBar,
|
|
9656
10761
|
{
|
|
9657
10762
|
status: {
|
|
@@ -9687,9 +10792,9 @@ function TextInputOverlay({
|
|
|
9687
10792
|
useInput2((_input, key) => {
|
|
9688
10793
|
if (key.escape) onCancel();
|
|
9689
10794
|
});
|
|
9690
|
-
return /* @__PURE__ */ jsxs12(
|
|
9691
|
-
/* @__PURE__ */
|
|
9692
|
-
/* @__PURE__ */
|
|
10795
|
+
return /* @__PURE__ */ jsxs12(Box13, { flexDirection: "column", children: [
|
|
10796
|
+
/* @__PURE__ */ jsx13(Text12, { dimColor: true, children: title }),
|
|
10797
|
+
/* @__PURE__ */ jsx13(
|
|
9693
10798
|
TextInput2,
|
|
9694
10799
|
{
|
|
9695
10800
|
value,
|
|
@@ -9698,13 +10803,9 @@ function TextInputOverlay({
|
|
|
9698
10803
|
mask: mask ? "\u2022" : void 0
|
|
9699
10804
|
}
|
|
9700
10805
|
),
|
|
9701
|
-
/* @__PURE__ */
|
|
10806
|
+
/* @__PURE__ */ jsx13(Box13, { marginTop: 1, children: /* @__PURE__ */ jsx13(Text12, { dimColor: true, color: theme.accent, children: "enter submit \xB7 esc cancel" }) })
|
|
9702
10807
|
] });
|
|
9703
10808
|
}
|
|
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
10809
|
function isKnownSlashCommand(line) {
|
|
9709
10810
|
if (!line.startsWith("/")) return false;
|
|
9710
10811
|
const [name = ""] = line.slice(1).trim().toLowerCase().split(/\s+/);
|
|
@@ -9773,41 +10874,18 @@ function unquoteArg(value) {
|
|
|
9773
10874
|
const last = value.at(-1);
|
|
9774
10875
|
return first2 === '"' && last === '"' || first2 === "'" && last === "'" ? value.slice(1, -1) : value;
|
|
9775
10876
|
}
|
|
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
10877
|
|
|
9806
10878
|
// src/app/tui.tsx
|
|
9807
|
-
import { jsx as
|
|
10879
|
+
import { jsx as jsx14 } from "react/jsx-runtime";
|
|
9808
10880
|
function clearTerminal() {
|
|
9809
10881
|
if (process.stdout.isTTY) process.stdout.write("\x1B[2J\x1B[3J\x1B[H");
|
|
9810
10882
|
}
|
|
10883
|
+
var TUI_RENDER_OPTIONS = {
|
|
10884
|
+
// Keep unchanged streamed lines in place instead of clearing and redrawing
|
|
10885
|
+
// the whole live region on every frame.
|
|
10886
|
+
incrementalRendering: true,
|
|
10887
|
+
maxFps: 30
|
|
10888
|
+
};
|
|
9811
10889
|
async function startTui(options) {
|
|
9812
10890
|
const { runtime, history, warnings, shutdown } = await boot({
|
|
9813
10891
|
cwd: options.cwd ?? process.cwd(),
|
|
@@ -9817,10 +10895,10 @@ async function startTui(options) {
|
|
|
9817
10895
|
mode: options.mode
|
|
9818
10896
|
});
|
|
9819
10897
|
clearTerminal();
|
|
9820
|
-
const instance = render(
|
|
9821
|
-
|
|
9822
|
-
|
|
9823
|
-
|
|
10898
|
+
const instance = render(
|
|
10899
|
+
/* @__PURE__ */ jsx14(App, { runtime, initialHistory: history, warnings }),
|
|
10900
|
+
TUI_RENDER_OPTIONS
|
|
10901
|
+
);
|
|
9824
10902
|
try {
|
|
9825
10903
|
await instance.waitUntilExit();
|
|
9826
10904
|
} finally {
|