@kernelonpanic/kitcode 1.1.1 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -9,6 +9,14 @@ import { existsSync } from "fs";
9
9
 
10
10
  // src/config/schema.ts
11
11
  import { z } from "zod";
12
+ var reservedRecordKeys = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
13
+ var safeRecordKeySchema = z.string().min(1).max(256).refine((value) => !reservedRecordKeys.has(value) && !/[\0-\x1f\x7f]/.test(value), {
14
+ message: "contains a reserved or unsafe key"
15
+ });
16
+ var providerIdSchema = safeRecordKeySchema.regex(
17
+ /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/,
18
+ "must start with a letter or digit and contain only letters, digits, dots, underscores, or dashes"
19
+ );
12
20
  function isAllowedEndpointUrl(value) {
13
21
  try {
14
22
  const url = new URL(value);
@@ -79,11 +87,11 @@ var configSchema = z.object({
79
87
  }),
80
88
  diagnostics: diagnosticsSchema.default({ autoRun: true, commands: [] }),
81
89
  updates: updatesSchema.default({ checkOnStart: false }),
82
- providers: z.record(z.string(), providerConfigSchema).default({}),
83
- permissions: z.record(z.string(), permissionModeSchema).default({}),
84
- mcp: z.record(z.string(), mcpServerSchema).default({})
90
+ providers: z.record(providerIdSchema, providerConfigSchema).default({}),
91
+ permissions: z.record(safeRecordKeySchema, permissionModeSchema).default({}),
92
+ mcp: z.record(safeRecordKeySchema, mcpServerSchema).default({})
85
93
  });
86
- var authSchema = z.record(z.string(), z.string());
94
+ var authSchema = z.record(providerIdSchema, z.string());
87
95
  function defaultConfig() {
88
96
  return configSchema.parse({});
89
97
  }
@@ -380,8 +388,9 @@ function perMillion(value) {
380
388
  }
381
389
  function positiveInt(value) {
382
390
  const parsed = toNumber(value);
383
- if (parsed === void 0 || parsed <= 0) return void 0;
384
- return Math.trunc(parsed);
391
+ if (parsed === void 0) return void 0;
392
+ const integer = Math.trunc(parsed);
393
+ return integer >= 1 ? integer : void 0;
385
394
  }
386
395
  function toNumber(value) {
387
396
  if (typeof value === "number") return Number.isFinite(value) ? value : void 0;
@@ -401,7 +410,12 @@ async function detectProvider(rawUrl, apiKey2, opts = {}) {
401
410
  if (!isAllowedEndpointUrl(baseUrl)) {
402
411
  throw new Error("API URL must use https; plain http is only allowed for localhost or 127.0.0.1.");
403
412
  }
404
- const id = opts.name ?? providerIdFromUrl(baseUrl);
413
+ const candidateId = opts.name ?? providerIdFromUrl(baseUrl);
414
+ const parsedId = providerIdSchema.safeParse(candidateId);
415
+ if (!parsedId.success) {
416
+ throw new Error(`Invalid provider name "${candidateId}": ${parsedId.error.issues[0]?.message ?? "invalid name"}`);
417
+ }
418
+ const id = parsedId.data;
405
419
  if (hostname(baseUrl) === anthropicHost) {
406
420
  const probed = await probe(
407
421
  fetchImpl,
@@ -453,7 +467,8 @@ function providerIdFromUrl(url) {
453
467
  }
454
468
  const labels = host.split(".");
455
469
  const label = labels.find((part) => part !== "api" && part !== "") ?? "provider";
456
- return label.replace(/[^a-z0-9-]/g, "-");
470
+ const candidate = label.replace(/[^a-z0-9-]/g, "-").replace(/^-+|-+$/g, "") || "provider";
471
+ return providerIdSchema.safeParse(candidate).success ? candidate : `provider-${candidate}`;
457
472
  }
458
473
  function isAddressLiteral(host) {
459
474
  return host === "localhost" || /^[0-9.]+$/.test(host) || host.includes(":");
@@ -651,7 +666,7 @@ async function isFile(file) {
651
666
 
652
667
  // src/config/store.ts
653
668
  import { randomUUID as randomUUID2 } from "crypto";
654
- import { chmod as chmod2, readFile as readFile2, rename as rename2, rm as rm2, writeFile as writeFile2 } from "fs/promises";
669
+ import { chmod as chmod2, mkdir as mkdir2, readFile as readFile2, rename as rename2, rm as rm2, writeFile as writeFile2 } from "fs/promises";
655
670
  import path3 from "path";
656
671
 
657
672
  // src/config/trust.ts
@@ -671,23 +686,30 @@ async function isWorkspaceTrusted(cwd) {
671
686
  }
672
687
  async function trustWorkspace(cwd) {
673
688
  const workspace = await canonicalWorkspace(cwd);
674
- const trust = await loadTrust();
675
- if (!trust.workspaces.includes(workspace)) {
676
- trust.workspaces.push(workspace);
677
- trust.workspaces.sort();
678
- await saveTrust(trust);
679
- }
689
+ await withTrustLock(async () => {
690
+ const trust = await readTrust();
691
+ if (!trust.workspaces.includes(workspace)) {
692
+ trust.workspaces.push(workspace);
693
+ trust.workspaces.sort();
694
+ await writeTrust(trust);
695
+ }
696
+ });
680
697
  return workspace;
681
698
  }
682
699
  async function revokeWorkspaceTrust(cwd) {
683
700
  const workspace = await canonicalWorkspace(cwd);
684
- const trust = await loadTrust();
685
- const workspaces = trust.workspaces.filter((entry) => entry !== workspace);
686
- if (workspaces.length !== trust.workspaces.length) await saveTrust({ version: 1, workspaces });
701
+ await withTrustLock(async () => {
702
+ const trust = await readTrust();
703
+ const workspaces = trust.workspaces.filter((entry) => entry !== workspace);
704
+ if (workspaces.length !== trust.workspaces.length) await writeTrust({ version: 1, workspaces });
705
+ });
687
706
  return workspace;
688
707
  }
689
708
  async function loadTrust() {
690
709
  await trustLock;
710
+ return readTrust();
711
+ }
712
+ async function readTrust() {
691
713
  let parsed;
692
714
  try {
693
715
  parsed = JSON.parse(await readFile(trustPath, "utf8"));
@@ -703,26 +725,29 @@ async function loadTrust() {
703
725
  workspaces: value.workspaces.filter((entry) => typeof entry === "string")
704
726
  };
705
727
  }
706
- async function saveTrust(value) {
707
- const release = trustLock;
708
- let resolveLock;
728
+ async function writeTrust(value) {
729
+ await ensureDir(path2.dirname(trustPath));
730
+ const temp = `${trustPath}.${randomUUID()}.tmp`;
731
+ try {
732
+ await writeFile(temp, `${JSON.stringify(value, null, 2)}
733
+ `, { encoding: "utf8", mode: 384 });
734
+ await rename(temp, trustPath);
735
+ } catch (error) {
736
+ await rm(temp, { force: true });
737
+ throw error;
738
+ }
739
+ }
740
+ async function withTrustLock(action) {
741
+ const previous = trustLock;
742
+ let release;
709
743
  trustLock = new Promise((resolve3) => {
710
- resolveLock = resolve3;
744
+ release = resolve3;
711
745
  });
712
- await release;
746
+ await previous;
713
747
  try {
714
- await ensureDir(path2.dirname(trustPath));
715
- const temp = `${trustPath}.${randomUUID()}.tmp`;
716
- try {
717
- await writeFile(temp, `${JSON.stringify(value, null, 2)}
718
- `, { encoding: "utf8", mode: 384 });
719
- await rename(temp, trustPath);
720
- } catch (error) {
721
- await rm(temp, { force: true });
722
- throw error;
723
- }
748
+ return await action();
724
749
  } finally {
725
- resolveLock();
750
+ release();
726
751
  }
727
752
  }
728
753
 
@@ -754,7 +779,7 @@ async function loadConfigAt(location) {
754
779
  }
755
780
  async function saveConfig(config) {
756
781
  const location = await configLocation();
757
- await writeJsonAtomic(location.path, config);
782
+ await writeJsonAtomic(location.path, config, location.scope === "project" ? void 0 : 384);
758
783
  }
759
784
  async function initProjectConfig(dir) {
760
785
  const location = { path: projectConfigPath(dir), scope: "project" };
@@ -817,17 +842,24 @@ async function readJson(file) {
817
842
  }
818
843
  }
819
844
  async function writeJsonAtomic(file, value, mode) {
820
- await ensureDir(path3.dirname(file));
845
+ const parent = path3.dirname(file);
846
+ if (isInside(homeDir, parent)) await ensureDir(parent);
847
+ else await mkdir2(parent, { recursive: true });
821
848
  const temp = `${file}.${randomUUID2()}.tmp`;
822
849
  try {
823
850
  await writeFile2(temp, `${JSON.stringify(value, null, 2)}
824
851
  `, { encoding: "utf8", mode });
825
852
  await rename2(temp, file);
853
+ if (mode !== void 0) await chmod2(file, mode);
826
854
  } catch (error) {
827
855
  await rm2(temp, { force: true });
828
856
  throw error;
829
857
  }
830
858
  }
859
+ function isInside(root, candidate) {
860
+ const relative2 = path3.relative(path3.resolve(root), path3.resolve(candidate));
861
+ return relative2 === "" || !path3.isAbsolute(relative2) && relative2.split(path3.sep)[0] !== "..";
862
+ }
831
863
  function formatIssues(file, error) {
832
864
  const lines = error.issues.map(
833
865
  (issue) => ` ${issue.path.join(".") || "(root)"}: ${issue.message}`
@@ -856,6 +888,8 @@ async function addProvider(url, key, options = {}) {
856
888
  }
857
889
  const config = options.local ? await loadProjectConfig(cwd) : (await loadRuntimeConfig(cwd)).config;
858
890
  const auth = await loadAuth();
891
+ const configBefore = structuredClone(config);
892
+ const authBefore = { ...auth };
859
893
  config.providers[detected.id] = detected.config;
860
894
  auth[detected.id] = key;
861
895
  const models = detected.models;
@@ -863,8 +897,16 @@ async function addProvider(url, key, options = {}) {
863
897
  const chosen = pickDefault(models);
864
898
  if (chosen) config.model = formatModelRef(detected.id, chosen);
865
899
  }
866
- await saveConfig(config);
867
- await saveAuth(auth);
900
+ try {
901
+ await saveConfig(config);
902
+ await saveAuth(auth);
903
+ } catch (error) {
904
+ Object.assign(config, configBefore);
905
+ for (const id of Object.keys(auth)) delete auth[id];
906
+ Object.assign(auth, authBefore);
907
+ await Promise.allSettled([saveConfig(config), saveAuth(auth)]);
908
+ throw error;
909
+ }
868
910
  const location = await configLocation();
869
911
  const protocol = detected.config.type === "anthropic" ? "Anthropic" : "OpenAI-compatible";
870
912
  const rows = [
@@ -971,7 +1013,7 @@ function skipControlString(text, from, bellTerminates) {
971
1013
 
972
1014
  // src/app/runtime.ts
973
1015
  import path13 from "path";
974
- import { mkdir as mkdir5 } from "fs/promises";
1016
+ import { mkdir as mkdir6 } from "fs/promises";
975
1017
 
976
1018
  // src/core/session.ts
977
1019
  import { randomBytes } from "crypto";
@@ -997,8 +1039,13 @@ async function saveSession(state) {
997
1039
  await ensureDir(sessionsDir);
998
1040
  const file = path4.join(sessionsDir, `${state.id}.json`);
999
1041
  const temp = `${file}.${process.pid}.${randomBytes(4).toString("hex")}.tmp`;
1000
- await writeFile3(temp, JSON.stringify(state), { encoding: "utf8", mode: 384 });
1001
- await atomicRename(temp, file);
1042
+ try {
1043
+ await writeFile3(temp, JSON.stringify(state), { encoding: "utf8", mode: 384 });
1044
+ await atomicRename(temp, file);
1045
+ } catch (error) {
1046
+ await unlink(temp).catch(() => void 0);
1047
+ throw error;
1048
+ }
1002
1049
  }
1003
1050
  async function atomicRename(from, to, retries = 5) {
1004
1051
  for (let attempt = 0; attempt < retries; attempt++) {
@@ -1172,7 +1219,9 @@ function readState(raw) {
1172
1219
  }
1173
1220
  if (typeof parsed !== "object" || parsed === null) return null;
1174
1221
  const state = parsed;
1175
- if (typeof state.id !== "string" || !Array.isArray(state.messages)) return null;
1222
+ if (typeof state.id !== "string" || !validSessionId(state.id)) return null;
1223
+ const messages = readMessages(state.messages);
1224
+ if (!messages) return null;
1176
1225
  return {
1177
1226
  id: state.id,
1178
1227
  title: typeof state.title === "string" ? state.title.slice(0, 120) : void 0,
@@ -1180,11 +1229,59 @@ function readState(raw) {
1180
1229
  createdAt: typeof state.createdAt === "string" ? state.createdAt : "",
1181
1230
  updatedAt: typeof state.updatedAt === "string" ? state.updatedAt : "",
1182
1231
  model: typeof state.model === "string" ? state.model : "",
1183
- messages: state.messages,
1184
- usage: Array.isArray(state.usage) ? state.usage : [],
1232
+ messages,
1233
+ usage: readUsageEntries(state.usage),
1185
1234
  context: readContextUsage(state.context)
1186
1235
  };
1187
1236
  }
1237
+ function readMessages(value) {
1238
+ if (!Array.isArray(value)) return null;
1239
+ const messages = [];
1240
+ for (const item of value) {
1241
+ if (typeof item !== "object" || item === null) return null;
1242
+ const candidate = item;
1243
+ if (candidate.role !== "user" && candidate.role !== "assistant") return null;
1244
+ if (!Array.isArray(candidate.content)) return null;
1245
+ const content = [];
1246
+ for (const block of candidate.content) {
1247
+ if (!validContentBlock(block)) return null;
1248
+ content.push(block);
1249
+ }
1250
+ messages.push({ role: candidate.role, content });
1251
+ }
1252
+ return messages;
1253
+ }
1254
+ function validContentBlock(value) {
1255
+ if (typeof value !== "object" || value === null) return false;
1256
+ const block = value;
1257
+ switch (block.type) {
1258
+ case "text":
1259
+ case "thinking":
1260
+ return typeof block.text === "string";
1261
+ case "tool_use":
1262
+ return typeof block.id === "string" && typeof block.name === "string";
1263
+ case "tool_result":
1264
+ return typeof block.toolUseId === "string" && typeof block.content === "string" && (block.isError === void 0 || typeof block.isError === "boolean");
1265
+ case "image":
1266
+ return typeof block.name === "string" && typeof block.data === "string" && block.data.length <= MAX_SESSION_BYTES * 2 && ["image/jpeg", "image/png", "image/gif", "image/webp"].includes(String(block.mediaType));
1267
+ case "file":
1268
+ return typeof block.name === "string" && typeof block.mediaType === "string" && typeof block.text === "string";
1269
+ default:
1270
+ return false;
1271
+ }
1272
+ }
1273
+ function readUsageEntries(value) {
1274
+ if (!Array.isArray(value)) return [];
1275
+ return value.flatMap((item) => {
1276
+ if (typeof item !== "object" || item === null) return [];
1277
+ const candidate = item;
1278
+ const usage = readUsage(candidate.usage);
1279
+ if (typeof candidate.model !== "string" || !usage || !Number.isInteger(candidate.requests) || candidate.requests < 0) {
1280
+ return [];
1281
+ }
1282
+ return [{ model: candidate.model, usage, requests: candidate.requests, costUsd: null }];
1283
+ });
1284
+ }
1188
1285
  async function resolveExportTarget(destination, state) {
1189
1286
  const resolved = path4.resolve(destination);
1190
1287
  const info = await stat2(resolved).catch(() => null);
@@ -1245,7 +1342,9 @@ function readUsage(value) {
1245
1342
  if (typeof value !== "object" || value === null) return void 0;
1246
1343
  const usage = value;
1247
1344
  const fields = ["input", "output", "cacheWrite", "cacheRead"];
1248
- if (fields.some((field2) => typeof usage[field2] !== "number" || !Number.isFinite(usage[field2]))) {
1345
+ if (fields.some(
1346
+ (field2) => typeof usage[field2] !== "number" || !Number.isFinite(usage[field2]) || usage[field2] < 0
1347
+ )) {
1249
1348
  return void 0;
1250
1349
  }
1251
1350
  return {
@@ -1276,10 +1375,13 @@ async function resolveSessionId(query) {
1276
1375
  );
1277
1376
  }
1278
1377
  function assertSessionId(id) {
1279
- if (!/^[A-Za-z0-9_-]{1,240}$/.test(id)) {
1378
+ if (!validSessionId(id)) {
1280
1379
  throw new Error(`Invalid session id: ${id}`);
1281
1380
  }
1282
1381
  }
1382
+ function validSessionId(id) {
1383
+ return /^[A-Za-z0-9_-]{1,240}$/.test(id);
1384
+ }
1283
1385
 
1284
1386
  // src/core/agent.ts
1285
1387
  var MAX_PAUSE_RESUMES = 5;
@@ -1455,11 +1557,20 @@ function retryAfterFromError(error) {
1455
1557
  }
1456
1558
  function sleep(ms, signal) {
1457
1559
  return new Promise((resolve3, reject) => {
1458
- const id = setTimeout(resolve3, ms);
1459
- signal.addEventListener("abort", () => {
1560
+ if (signal.aborted) {
1561
+ reject(signal.reason ?? new Error("Aborted"));
1562
+ return;
1563
+ }
1564
+ const finish = () => {
1565
+ signal.removeEventListener("abort", onAbort);
1566
+ resolve3();
1567
+ };
1568
+ const onAbort = () => {
1460
1569
  clearTimeout(id);
1461
1570
  reject(signal.reason ?? new Error("Aborted"));
1462
- }, { once: true });
1571
+ };
1572
+ const id = setTimeout(finish, ms);
1573
+ signal.addEventListener("abort", onAbort, { once: true });
1463
1574
  });
1464
1575
  }
1465
1576
  function estimateRequestTokens(cfg, messages) {
@@ -1478,9 +1589,7 @@ function estimateRequestTokens(cfg, messages) {
1478
1589
  async function runToolCalls(cfg, content, hooks, signal) {
1479
1590
  const calls = content.filter((block) => block.type === "tool_use");
1480
1591
  const results = [];
1481
- for (const call of calls) {
1482
- results.push(await runOneCall(cfg, call, hooks, signal));
1483
- }
1592
+ for (const call of calls) results.push(await runOneCall(cfg, call, hooks, signal));
1484
1593
  return results;
1485
1594
  }
1486
1595
  async function runOneCall(cfg, call, hooks, signal) {
@@ -1608,6 +1717,7 @@ function costOf(usage, pricing) {
1608
1717
  }
1609
1718
 
1610
1719
  // src/core/budget.ts
1720
+ var UNKNOWN_MODEL_PRICING = { input: 3, output: 15 };
1611
1721
  function createTurnBudget(limits, resolvePricing = pricingFor) {
1612
1722
  let usage = emptyUsage();
1613
1723
  let costUsd = 0;
@@ -1645,7 +1755,7 @@ function createTurnBudget(limits, resolvePricing = pricingFor) {
1645
1755
  const pricing = resolvePricing(request.modelRef);
1646
1756
  if (current.costUsd !== null) {
1647
1757
  const remainingUsd = limits.maxCostUsdPerTurn - current.costUsd;
1648
- const effectivePricing = pricing ?? { input: 3e-3, output: 0.015 };
1758
+ const effectivePricing = pricing ?? UNKNOWN_MODEL_PRICING;
1649
1759
  const inputUsd = estimatedInput * effectivePricing.input / 1e6;
1650
1760
  const affordableOutput = Math.floor(
1651
1761
  (remainingUsd - inputUsd) * 1e6 / effectivePricing.output
@@ -1661,9 +1771,10 @@ function createTurnBudget(limits, resolvePricing = pricingFor) {
1661
1771
  return { allowed: true, maxOutputTokens: Math.floor(outputLimit) };
1662
1772
  },
1663
1773
  record(modelRef, next) {
1664
- usage = addUsage(usage, normalizeUsage(next));
1665
- const priced = costOf(usage, resolvePricing(modelRef));
1666
- costUsd = priced;
1774
+ const normalized = normalizeUsage(next);
1775
+ usage = addUsage(usage, normalized);
1776
+ const priced = costOf(normalized, resolvePricing(modelRef) ?? UNKNOWN_MODEL_PRICING);
1777
+ costUsd = (costUsd ?? 0) + (priced ?? 0);
1667
1778
  },
1668
1779
  snapshot
1669
1780
  };
@@ -1686,7 +1797,7 @@ import { createReadStream } from "fs";
1686
1797
  import {
1687
1798
  chmod as chmod3,
1688
1799
  lstat,
1689
- mkdir as mkdir2,
1800
+ mkdir as mkdir3,
1690
1801
  readFile as readFile4,
1691
1802
  readdir as readdir2,
1692
1803
  rename as rename4,
@@ -1928,7 +2039,7 @@ async function restoreFile(root, relativePath, snapshot, expected) {
1928
2039
  const safe = resolveInside(root, relativePath);
1929
2040
  if (!safe.ok || safe.relative !== relativePath) throw new Error("The restore path changed.");
1930
2041
  const data = decodeSnapshot(snapshot);
1931
- await mkdir2(path5.dirname(safe.path), { recursive: true });
2042
+ await mkdir3(path5.dirname(safe.path), { recursive: true });
1932
2043
  const rechecked = resolveInside(root, relativePath);
1933
2044
  if (!rechecked.ok || rechecked.path !== safe.path || rechecked.relative !== relativePath) {
1934
2045
  throw new Error("The restore path changed while preparing its parent directory.");
@@ -2372,17 +2483,15 @@ async function loadAttachment(cwd, requestedPath) {
2372
2483
  }
2373
2484
  async function loadAutomaticAttachment(cwd, requestedPath) {
2374
2485
  if (!looksLikeAttachmentPath(requestedPath)) return null;
2375
- const resolved = resolveAttachmentPath(cwd, requestedPath);
2486
+ const requested = resolveAttachmentPath(cwd, requestedPath);
2487
+ const safe = resolveInside(cwd, requested);
2488
+ if (!safe.ok || safe.relative === "") return null;
2489
+ const resolved = safe.path;
2376
2490
  if (isSensitiveAutomaticPath(resolved)) {
2377
2491
  throw new Error(
2378
2492
  `For safety, sensitive-looking files must be attached explicitly with /attach: ${path7.basename(resolved)}`
2379
2493
  );
2380
2494
  }
2381
- if (path7.isAbsolute(resolved)) {
2382
- const normalized = resolved.toLowerCase();
2383
- const isProjectFile = normalized.includes(path7.sep + "src" + path7.sep) || normalized.includes(path7.sep + "lib" + path7.sep) || normalized.includes(path7.sep + "app" + path7.sep) || normalized.includes(path7.sep + "test" + path7.sep) || normalized.includes(path7.sep + "tests" + path7.sep) || normalized.includes(path7.sep + "public" + path7.sep) || normalized.includes(path7.sep + "assets" + path7.sep) || normalized.includes(path7.sep + "static" + path7.sep) || normalized.endsWith(".ts") || normalized.endsWith(".js") || normalized.endsWith(".tsx") || normalized.endsWith(".jsx") || normalized.endsWith(".py") || normalized.endsWith(".go") || normalized.endsWith(".rs") || normalized.endsWith(".java") || normalized.endsWith(".c") || normalized.endsWith(".cpp") || normalized.endsWith(".h") || normalized.endsWith(".hpp") || normalized.endsWith(".css") || normalized.endsWith(".html") || normalized.endsWith(".json") || normalized.endsWith(".yaml") || normalized.endsWith(".yml") || normalized.endsWith(".md") || normalized.endsWith(".txt") || normalized.endsWith(".toml") || normalized.endsWith(".ini") || normalized.endsWith(".cfg") || normalized.endsWith(".sh") || normalized.endsWith(".bat") || normalized.endsWith(".cmd") || normalized.endsWith(".ps1") || normalized.endsWith(".dockerfile") || normalized.endsWith(".makefile");
2384
- if (!isProjectFile) return null;
2385
- }
2386
2495
  const linkInfo = await lstat2(resolved).catch(() => null);
2387
2496
  if (linkInfo?.isSymbolicLink()) return null;
2388
2497
  const info = await stat5(resolved).catch((error) => {
@@ -2661,47 +2770,90 @@ function formatBytes(value) {
2661
2770
 
2662
2771
  // src/core/compact.ts
2663
2772
  var KEEP_USER_TURNS = 2;
2664
- var MAX_SOURCE_CHARS = 5e5;
2665
- var MAX_TOOL_RESULT_CHARS = 8e3;
2666
- var MAX_TEXT_BLOCK_CHARS = 5e4;
2773
+ var SINGLE_REQUEST_MAX_CHARS = 8e4;
2774
+ var CHUNK_TARGET_CHARS = 35e3;
2775
+ var CHUNK_OVERLAP_MESSAGES = 2;
2776
+ var CHUNK_OVERLAP_MAX_CHARS = 4e3;
2777
+ var MERGE_TARGET_CHARS = 6e4;
2778
+ var MAX_USER_TEXT_CHARS = 12e3;
2779
+ var MAX_ASSISTANT_TEXT_CHARS = 6e3;
2780
+ var MAX_FILE_BLOCK_CHARS = 2e3;
2781
+ var MAX_TOOL_RESULT_CHARS = 1200;
2782
+ var MAX_TOOL_ERROR_CHARS = 2e3;
2783
+ var MAX_TOOL_INPUT_CHARS = 800;
2784
+ var SINGLE_MAX_TOKENS = 768;
2785
+ var CHUNK_MAX_TOKENS = 512;
2786
+ var MERGE_MAX_TOKENS = 1024;
2787
+ var CHUNK_CONCURRENCY = 3;
2788
+ var SYSTEM_SUMMARIZE = "You are summarizing a coding conversation so another agent can continue the work. Extract and preserve ONLY: concrete requirements, user preferences, files changed, commands run and their results, errors encountered, bugs found, architectural decisions, and unfinished work. Be terse and factual. Do NOT include greetings, acknowledgments, chain-of-thought, or step-by-step narration. Return only the summary.";
2789
+ var SYSTEM_MERGE = "Merge these partial summaries of a coding conversation into one coherent summary. Remove duplicates. Preserve: requirements, files changed, commands run, errors, decisions, and unfinished work. Be terse. Return only the merged summary.";
2667
2790
  async function compactHistory(options) {
2668
2791
  const cut = compactCutIndex(options.history);
2669
2792
  if (cut <= 0) {
2670
2793
  return { history: options.history, compacted: false, removedMessages: 0 };
2671
2794
  }
2672
- const source = renderHistory(options.history.slice(0, cut));
2673
- if (!source.trim()) {
2795
+ const oldMessages = options.history.slice(0, cut);
2796
+ const renderedMessages = renderMessages(oldMessages);
2797
+ const rendered = renderedMessages.join("\n\n");
2798
+ if (!rendered.trim()) {
2674
2799
  return { history: options.history, compacted: false, removedMessages: 0 };
2675
2800
  }
2676
- let summary = "";
2677
- let finalText2 = "";
2801
+ let summary;
2678
2802
  let usage;
2679
2803
  let rateLimits;
2680
- const stream = options.provider.stream({
2681
- model: options.model,
2682
- system: "Summarize the earlier coding conversation for another agent. Preserve concrete requirements, user preferences, architectural decisions, commands and files changed, test results, bugs, errors, and unfinished work. Be concise but lossless. Do not include private chain-of-thought or invent details. Return only the durable summary.",
2683
- messages: [
2684
- {
2685
- role: "user",
2686
- content: [{ type: "text", text: `Earlier conversation to compact:
2687
-
2688
- ${source}` }]
2689
- }
2690
- ],
2691
- tools: [],
2692
- maxTokens: Math.max(512, Math.min(4096, options.maxTokens)),
2693
- thinking: false,
2694
- signal: options.signal
2695
- });
2696
- for await (const event of stream) {
2697
- if (event.type === "text_delta") summary += event.text;
2698
- else if (event.type === "usage") usage = event.usage;
2699
- else if (event.type === "rate_limits") rateLimits = event.limits;
2700
- else if (event.type === "done") {
2701
- finalText2 = event.content.filter((block) => block.type === "text").map((block) => block.type === "text" ? block.text : "").join("");
2804
+ if (rendered.length <= SINGLE_REQUEST_MAX_CHARS) {
2805
+ const requestMax = singleOutputLimit(options.maxTokens, options.maxTotalOutputTokens);
2806
+ const result = await summarizeText(
2807
+ options.provider,
2808
+ options.model,
2809
+ options.maxTokens,
2810
+ options.signal,
2811
+ rendered,
2812
+ SYSTEM_SUMMARIZE,
2813
+ requestMax
2814
+ );
2815
+ summary = result.text;
2816
+ usage = result.usage;
2817
+ rateLimits = result.rateLimits;
2818
+ } else {
2819
+ const chunks = chunkRenderedMessages(
2820
+ renderedMessages,
2821
+ CHUNK_TARGET_CHARS,
2822
+ CHUNK_OVERLAP_MESSAGES,
2823
+ CHUNK_OVERLAP_MAX_CHARS
2824
+ );
2825
+ const limits = chunkOutputLimits(
2826
+ chunks.length,
2827
+ options.maxTokens,
2828
+ options.maxTotalOutputTokens
2829
+ );
2830
+ const chunkResults = await summarizeSourcesParallel(
2831
+ options.provider,
2832
+ options.model,
2833
+ options.maxTokens,
2834
+ options.signal,
2835
+ chunks,
2836
+ SYSTEM_SUMMARIZE,
2837
+ limits.chunk
2838
+ );
2839
+ const chunkUsages = chunkResults.map((result) => result.usage).filter(Boolean);
2840
+ usage = chunkUsages.length > 0 ? mergeUsage(chunkUsages) : void 0;
2841
+ rateLimits = [...chunkResults].reverse().find((r) => r.rateLimits)?.rateLimits;
2842
+ const mergeResult = await mergePartialSummaries(
2843
+ options.provider,
2844
+ options.model,
2845
+ options.maxTokens,
2846
+ options.signal,
2847
+ chunkResults.map((result) => result.text),
2848
+ limits.merge
2849
+ );
2850
+ summary = mergeResult.text;
2851
+ if (mergeResult.usage) {
2852
+ usage = usage ? mergeUsage([usage, mergeResult.usage]) : mergeResult.usage;
2702
2853
  }
2854
+ if (mergeResult.rateLimits) rateLimits = mergeResult.rateLimits;
2703
2855
  }
2704
- summary = (finalText2 || summary).trim();
2856
+ summary = summary.trim();
2705
2857
  if (!summary) throw new Error("The provider returned an empty context summary.");
2706
2858
  const prefix = [
2707
2859
  {
@@ -2733,11 +2885,34 @@ function compactCutIndex(history) {
2733
2885
  if (userTurns.length <= KEEP_USER_TURNS) return 0;
2734
2886
  return userTurns[userTurns.length - KEEP_USER_TURNS] ?? 0;
2735
2887
  }
2736
- function estimateCompactTokens(history) {
2888
+ function estimateCompactBudget(history, maxTokens = MERGE_MAX_TOKENS) {
2737
2889
  const cut = compactCutIndex(history);
2738
- if (cut <= 0) return 1;
2739
- const rendered = renderHistory(history.slice(0, cut));
2740
- return Math.max(1, Math.min(rendered.length, MAX_SOURCE_CHARS) + 1e3);
2890
+ if (cut <= 0) return { inputTokens: 1, maxOutputTokens: 0 };
2891
+ const renderedMessages = renderMessages(history.slice(0, cut));
2892
+ const rendered = renderedMessages.join("\n\n");
2893
+ if (rendered.length <= SINGLE_REQUEST_MAX_CHARS) {
2894
+ return {
2895
+ inputTokens: Math.max(1, rendered.length + 1e3),
2896
+ maxOutputTokens: Math.max(1, Math.min(SINGLE_MAX_TOKENS, Math.floor(maxTokens)))
2897
+ };
2898
+ }
2899
+ const chunks = chunkRenderedMessages(
2900
+ renderedMessages,
2901
+ CHUNK_TARGET_CHARS,
2902
+ CHUNK_OVERLAP_MESSAGES,
2903
+ CHUNK_OVERLAP_MAX_CHARS
2904
+ );
2905
+ const chunkInput = chunks.reduce((total, chunk) => total + chunk.length, 0);
2906
+ const chunkOutput = Math.max(1, Math.min(CHUNK_MAX_TOKENS, Math.floor(maxTokens)));
2907
+ const mergeOutput = Math.max(1, Math.min(MERGE_MAX_TOKENS, Math.floor(maxTokens)));
2908
+ const mergeRequests = Math.max(0, chunks.length - 1);
2909
+ const totalOutput = chunks.length * chunkOutput + mergeRequests * mergeOutput;
2910
+ const intermediateOutput = chunks.length * chunkOutput + Math.max(0, mergeRequests - 1) * mergeOutput;
2911
+ const requestCount = chunks.length + mergeRequests;
2912
+ return {
2913
+ inputTokens: Math.max(1, chunkInput + intermediateOutput * 8 + requestCount * 1e3),
2914
+ maxOutputTokens: totalOutput
2915
+ };
2741
2916
  }
2742
2917
  function isConversationUser(message) {
2743
2918
  if (message.role !== "user") return false;
@@ -2745,50 +2920,249 @@ function isConversationUser(message) {
2745
2920
  (block) => block.type === "image" || block.type === "file" || block.type === "text" && !block.text.startsWith("[Earlier conversation summary]")
2746
2921
  );
2747
2922
  }
2748
- function renderHistory(history) {
2749
- let result = "";
2750
- for (const message of history) {
2751
- const blocks = message.content.map(renderBlock).filter(Boolean);
2752
- if (blocks.length === 0) continue;
2753
- const part = `${message.role.toUpperCase()}:
2754
- ${blocks.join("\n\n")}`;
2755
- if (result.length + part.length > MAX_SOURCE_CHARS) {
2756
- result += "\n\n[...older context truncated for compaction...]";
2757
- break;
2923
+ function chunkRenderedMessages(messages, targetChars, overlapMessages, overlapMaxChars) {
2924
+ const units = messages.flatMap((message) => splitLongMessage(message, targetChars));
2925
+ const chunks = [];
2926
+ let current = [];
2927
+ const sizeOf = (parts) => parts.reduce((total, part) => total + part.length, 0) + Math.max(0, parts.length - 1) * 2;
2928
+ for (const unit of units) {
2929
+ if (current.length > 0 && sizeOf([...current, unit]) > targetChars) {
2930
+ chunks.push(current.join("\n\n"));
2931
+ const overlap = [];
2932
+ let overlapChars = 0;
2933
+ for (let index = current.length - 1; index >= 0 && overlap.length < overlapMessages; index -= 1) {
2934
+ const candidate = current[index];
2935
+ if (overlapChars + candidate.length > overlapMaxChars) break;
2936
+ overlap.unshift(candidate);
2937
+ overlapChars += candidate.length;
2938
+ }
2939
+ current = overlap;
2940
+ while (current.length > 0 && sizeOf([...current, unit]) > targetChars) current.shift();
2758
2941
  }
2759
- result += (result ? "\n\n" : "") + part;
2942
+ current.push(unit);
2943
+ }
2944
+ if (current.length > 0) chunks.push(current.join("\n\n"));
2945
+ return chunks;
2946
+ }
2947
+ function splitLongMessage(message, targetChars) {
2948
+ if (message.length <= targetChars) return [message];
2949
+ const parts = [];
2950
+ let remaining = message;
2951
+ const payloadLimit = Math.max(1, targetChars - 32);
2952
+ while (remaining.length > payloadLimit) {
2953
+ let cut = remaining.lastIndexOf("\n", payloadLimit);
2954
+ if (cut < payloadLimit * 0.6) cut = payloadLimit;
2955
+ parts.push(`${remaining.slice(0, cut)}
2956
+ [message continues]`);
2957
+ remaining = `[continued message]
2958
+ ${remaining.slice(cut).replace(/^\n/, "")}`;
2959
+ }
2960
+ if (remaining) parts.push(remaining);
2961
+ return parts;
2962
+ }
2963
+ async function summarizeSourcesParallel(provider, model, maxTokens, signal, sources, systemPrompt, maxOutputTokens) {
2964
+ const results = new Array(sources.length);
2965
+ const controller = new AbortController();
2966
+ const abort = () => controller.abort(signal.reason);
2967
+ if (signal.aborted) abort();
2968
+ else signal.addEventListener("abort", abort, { once: true });
2969
+ let next = 0;
2970
+ const worker = async () => {
2971
+ for (; ; ) {
2972
+ const index = next++;
2973
+ if (index >= sources.length) return;
2974
+ try {
2975
+ results[index] = await summarizeText(
2976
+ provider,
2977
+ model,
2978
+ maxTokens,
2979
+ controller.signal,
2980
+ sources[index],
2981
+ systemPrompt,
2982
+ maxOutputTokens
2983
+ );
2984
+ } catch (error) {
2985
+ controller.abort(error);
2986
+ throw error;
2987
+ }
2988
+ }
2989
+ };
2990
+ try {
2991
+ await Promise.all(
2992
+ Array.from({ length: Math.min(CHUNK_CONCURRENCY, sources.length) }, () => worker())
2993
+ );
2994
+ return results;
2995
+ } finally {
2996
+ signal.removeEventListener("abort", abort);
2760
2997
  }
2761
- return result;
2762
2998
  }
2763
- function renderBlock(block) {
2999
+ async function mergePartialSummaries(provider, model, maxTokens, signal, summaries, mergeMaxTokens) {
3000
+ let current = summaries;
3001
+ const usages = [];
3002
+ let rateLimits;
3003
+ while (current.length > 1) {
3004
+ const groups = groupSummaries(current, MERGE_TARGET_CHARS);
3005
+ const sources = groups.map(
3006
+ (group) => group.map((summary, index) => `## Part ${index + 1}
3007
+ ${summary}`).join("\n\n")
3008
+ );
3009
+ const merged = await summarizeSourcesParallel(
3010
+ provider,
3011
+ model,
3012
+ maxTokens,
3013
+ signal,
3014
+ sources,
3015
+ SYSTEM_MERGE,
3016
+ mergeMaxTokens
3017
+ );
3018
+ for (const result of merged) {
3019
+ if (!result.text) throw new Error("The provider returned an empty partial context summary.");
3020
+ if (result.usage) usages.push(result.usage);
3021
+ if (result.rateLimits) rateLimits = result.rateLimits;
3022
+ }
3023
+ current = merged.map((result) => result.text);
3024
+ }
3025
+ return {
3026
+ text: current[0] ?? "",
3027
+ usage: usages.length > 0 ? mergeUsage(usages) : void 0,
3028
+ rateLimits
3029
+ };
3030
+ }
3031
+ function singleOutputLimit(maxTokens, totalBudget) {
3032
+ const requested = Math.max(1, Math.min(SINGLE_MAX_TOKENS, Math.floor(maxTokens)));
3033
+ if (totalBudget === void 0) return requested;
3034
+ const allowed = Math.floor(totalBudget);
3035
+ if (allowed < 1) throw new Error("The remaining turn budget is too small to compact context.");
3036
+ return Math.min(requested, allowed);
3037
+ }
3038
+ function chunkOutputLimits(chunkCount, maxTokens, totalBudget) {
3039
+ let chunk = Math.max(1, Math.min(CHUNK_MAX_TOKENS, Math.floor(maxTokens)));
3040
+ let merge = Math.max(1, Math.min(MERGE_MAX_TOKENS, Math.floor(maxTokens)));
3041
+ if (totalBudget === void 0) return { chunk, merge };
3042
+ const mergeRequests = Math.max(0, chunkCount - 1);
3043
+ const requestCount = chunkCount + mergeRequests;
3044
+ const allowed = Math.floor(totalBudget);
3045
+ if (allowed < requestCount) {
3046
+ throw new Error("The remaining turn budget is too small for all context-compaction requests.");
3047
+ }
3048
+ const requested = chunkCount * chunk + mergeRequests * merge;
3049
+ if (allowed >= requested) return { chunk, merge };
3050
+ const scale = allowed / requested;
3051
+ chunk = Math.max(1, Math.floor(chunk * scale));
3052
+ merge = Math.max(1, Math.floor(merge * scale));
3053
+ const total = () => chunkCount * chunk + mergeRequests * merge;
3054
+ while (total() > allowed) {
3055
+ if (mergeRequests > 0 && merge > 1 && merge >= chunk) merge -= 1;
3056
+ else if (chunk > 1) chunk -= 1;
3057
+ else if (merge > 1) merge -= 1;
3058
+ else throw new Error("Could not fit context compaction into the remaining turn budget.");
3059
+ }
3060
+ return { chunk, merge };
3061
+ }
3062
+ function groupSummaries(summaries, targetChars) {
3063
+ const groups = [];
3064
+ let current = [];
3065
+ let size = 0;
3066
+ for (const summary of summaries) {
3067
+ if (current.length >= 2 && size + 12 + summary.length > targetChars) {
3068
+ groups.push(current);
3069
+ current = [];
3070
+ size = 0;
3071
+ }
3072
+ const separator = current.length > 0 ? 12 : 0;
3073
+ current.push(summary);
3074
+ size += separator + summary.length;
3075
+ }
3076
+ if (current.length > 0) groups.push(current);
3077
+ return groups;
3078
+ }
3079
+ async function summarizeText(provider, model, maxTokens, signal, source, systemPrompt, maxOutputTokens) {
3080
+ if (signal.aborted) throw signal.reason ?? new Error("Context compaction was cancelled.");
3081
+ let summary = "";
3082
+ let finalText2 = "";
3083
+ let usage;
3084
+ let rateLimits;
3085
+ const stream = provider.stream({
3086
+ model,
3087
+ system: systemPrompt,
3088
+ messages: [{ role: "user", content: [{ type: "text", text: source }] }],
3089
+ tools: [],
3090
+ maxTokens: Math.max(1, Math.floor(Math.min(maxOutputTokens, maxTokens))),
3091
+ thinking: false,
3092
+ signal
3093
+ });
3094
+ for await (const event of stream) {
3095
+ if (event.type === "text_delta") summary += event.text;
3096
+ else if (event.type === "usage") usage = event.usage;
3097
+ else if (event.type === "rate_limits") rateLimits = event.limits;
3098
+ else if (event.type === "done") {
3099
+ finalText2 = event.content.filter((block) => block.type === "text").map((block) => block.type === "text" ? block.text : "").join("");
3100
+ }
3101
+ }
3102
+ if (signal.aborted) throw signal.reason ?? new Error("Context compaction was cancelled.");
3103
+ return { text: (finalText2 || summary).trim(), usage, rateLimits };
3104
+ }
3105
+ function mergeUsage(usages) {
3106
+ return usages.reduce(
3107
+ (acc, u) => ({
3108
+ input: acc.input + u.input,
3109
+ output: acc.output + u.output,
3110
+ cacheWrite: acc.cacheWrite + u.cacheWrite,
3111
+ cacheRead: acc.cacheRead + u.cacheRead
3112
+ }),
3113
+ { input: 0, output: 0, cacheWrite: 0, cacheRead: 0 }
3114
+ );
3115
+ }
3116
+ function renderMessages(history) {
3117
+ return history.flatMap((message) => {
3118
+ const blocks = message.content.map((block) => renderBlock(block, message.role)).filter(Boolean);
3119
+ return blocks.length > 0 ? [`${message.role.toUpperCase()}:
3120
+ ${blocks.join("\n")}`] : [];
3121
+ });
3122
+ }
3123
+ function renderBlock(block, role) {
2764
3124
  switch (block.type) {
2765
3125
  case "text":
2766
- return truncate(block.text, MAX_TEXT_BLOCK_CHARS);
3126
+ return truncate(
3127
+ block.text,
3128
+ role === "user" ? MAX_USER_TEXT_CHARS : MAX_ASSISTANT_TEXT_CHARS
3129
+ );
2767
3130
  case "thinking":
2768
3131
  return "";
2769
3132
  case "image":
2770
- return `[Image attached: ${block.name}]`;
3133
+ return `[image: ${block.name}]`;
2771
3134
  case "file":
2772
- return `[File attached: ${block.name}]
2773
- ${truncate(block.text, MAX_TOOL_RESULT_CHARS)}`;
3135
+ return `[file: ${block.name}]
3136
+ ${truncate(block.text, MAX_FILE_BLOCK_CHARS)}`;
2774
3137
  case "tool_use":
2775
- return `[Tool call: ${block.name}]
2776
- ${safeJson(block.input)}`;
3138
+ return `[tool: ${block.name}(${truncate(safeJson(block.input), MAX_TOOL_INPUT_CHARS)})]`;
2777
3139
  case "tool_result":
2778
- return `[Tool result${block.isError ? " (error)" : ""}]
2779
- ${truncate(block.content, MAX_TOOL_RESULT_CHARS)}`;
3140
+ if (block.isError) {
3141
+ return `[tool result error: ${truncate(block.content, MAX_TOOL_ERROR_CHARS)}]`;
3142
+ }
3143
+ if (isTrivialSuccess(block.content)) return "";
3144
+ return truncate(block.content, MAX_TOOL_RESULT_CHARS);
2780
3145
  }
2781
3146
  }
3147
+ function isTrivialSuccess(content) {
3148
+ const t = content.trim().toLowerCase();
3149
+ if (!t || t === "ok" || t === "done" || t === "success") return true;
3150
+ if (/^no matches found/.test(t)) return true;
3151
+ if (t === "(no output)") return true;
3152
+ return false;
3153
+ }
2782
3154
  function safeJson(value) {
2783
3155
  try {
2784
- return truncate(JSON.stringify(value, null, 2), MAX_TOOL_RESULT_CHARS);
3156
+ return JSON.stringify(value);
2785
3157
  } catch {
2786
- return "[unserializable input]";
3158
+ return "[unserializable]";
2787
3159
  }
2788
3160
  }
2789
3161
  function truncate(value, max) {
2790
- return value.length <= max ? value : `${value.slice(0, max)}
2791
- [...truncated...]`;
3162
+ if (value.length <= max) return value;
3163
+ const keep = Math.floor(max * 0.4);
3164
+ return `${value.slice(0, max - keep)}
3165
+ ...${value.slice(-keep)}`;
2792
3166
  }
2793
3167
 
2794
3168
  // src/core/prompt.ts
@@ -3050,7 +3424,7 @@ function clip(value) {
3050
3424
  // package.json
3051
3425
  var package_default = {
3052
3426
  name: "@kernelonpanic/kitcode",
3053
- version: "1.1.1",
3427
+ version: "1.2.0",
3054
3428
  description: "Terminal coding agent with a config you never have to write by hand",
3055
3429
  type: "module",
3056
3430
  license: "MIT",
@@ -3118,7 +3492,7 @@ var package_default = {
3118
3492
  // src/version.ts
3119
3493
  var KITCODE_VERSION = package_default.version;
3120
3494
  var KITCODE_REPOSITORY = "KernelEditor/KitCode";
3121
- var KITCODE_COMMIT = true ? "a57e54102a5e1ca1e60cec4ee9c629c890d27057" : "development";
3495
+ var KITCODE_COMMIT = true ? "c93b03ff85112af7b4c28f543aff6b9377f84ad6" : "development";
3122
3496
 
3123
3497
  // src/mcp/client.ts
3124
3498
  var clientInfo = { name: "kitcode", version: KITCODE_VERSION };
@@ -3140,7 +3514,11 @@ var inheritedEnvKeys = /* @__PURE__ */ new Set([
3140
3514
  "COMSPEC",
3141
3515
  "PATHEXT",
3142
3516
  "APPDATA",
3143
- "LOCALAPPDATA"
3517
+ "LOCALAPPDATA",
3518
+ "USERPROFILE",
3519
+ "HOMEDRIVE",
3520
+ "HOMEPATH",
3521
+ "USERNAME"
3144
3522
  ]);
3145
3523
  var blockedEnvKeys = /* @__PURE__ */ new Set([
3146
3524
  "ANTHROPIC_API_KEY",
@@ -3317,8 +3695,11 @@ function configuredSecrets(config) {
3317
3695
  }
3318
3696
  function inheritedEnv(source = process.env) {
3319
3697
  const env = {};
3698
+ const allowed = new Set([...inheritedEnvKeys].map((key) => key.toUpperCase()));
3699
+ const blocked = new Set([...blockedEnvKeys].map((key) => key.toUpperCase()));
3320
3700
  for (const [key, value] of Object.entries(source)) {
3321
- if (value !== void 0 && inheritedEnvKeys.has(key) && !blockedEnvKeys.has(key)) {
3701
+ const normalized = key.toUpperCase();
3702
+ if (value !== void 0 && allowed.has(normalized) && !blocked.has(normalized)) {
3322
3703
  env[key] = value;
3323
3704
  }
3324
3705
  }
@@ -3352,7 +3733,10 @@ async function fetchProviderBalance(config, apiKey2, options = {}) {
3352
3733
  },
3353
3734
  signal: controller.signal
3354
3735
  });
3355
- if (!response.ok) continue;
3736
+ if (!response.ok) {
3737
+ await response.body?.cancel().catch(() => void 0);
3738
+ continue;
3739
+ }
3356
3740
  const text = await limitedResponseText(response, MAX_RESPONSE_BYTES);
3357
3741
  if (text === null) continue;
3358
3742
  const balance = endpoint2.parse(JSON.parse(text));
@@ -3467,7 +3851,10 @@ function finiteNumber(value) {
3467
3851
  }
3468
3852
  async function limitedResponseText(response, limit) {
3469
3853
  const declared = Number(response.headers.get("content-length"));
3470
- if (Number.isFinite(declared) && declared > limit) return null;
3854
+ if (Number.isFinite(declared) && declared > limit) {
3855
+ await response.body?.cancel().catch(() => void 0);
3856
+ return null;
3857
+ }
3471
3858
  if (!response.body) return "";
3472
3859
  const reader = response.body.getReader();
3473
3860
  const decoder = new TextDecoder();
@@ -3517,11 +3904,34 @@ function cacheFile(providerId) {
3517
3904
  }
3518
3905
  async function readCache(file) {
3519
3906
  try {
3520
- return JSON.parse(await readFile7(file, "utf8"));
3907
+ const parsed = JSON.parse(await readFile7(file, "utf8"));
3908
+ if (typeof parsed !== "object" || parsed === null) return void 0;
3909
+ const candidate = parsed;
3910
+ if (typeof candidate.fetchedAt !== "number" || !Number.isFinite(candidate.fetchedAt) || !Array.isArray(candidate.models) || candidate.models.length > 1e4 || !candidate.models.every(validModel)) {
3911
+ return void 0;
3912
+ }
3913
+ return candidate;
3521
3914
  } catch {
3522
3915
  return void 0;
3523
3916
  }
3524
3917
  }
3918
+ function validModel(value) {
3919
+ if (typeof value !== "object" || value === null) return false;
3920
+ const model = value;
3921
+ if (typeof model.id !== "string" || model.id === "") return false;
3922
+ if (model.name !== void 0 && typeof model.name !== "string") return false;
3923
+ if (model.contextWindow !== void 0 && (!Number.isInteger(model.contextWindow) || model.contextWindow < 1)) {
3924
+ return false;
3925
+ }
3926
+ if (model.pricing === void 0) return true;
3927
+ const prices = [
3928
+ model.pricing.input,
3929
+ model.pricing.output,
3930
+ model.pricing.cacheWrite,
3931
+ model.pricing.cacheRead
3932
+ ].filter((price2) => price2 !== void 0);
3933
+ return prices.length >= 2 && prices.every((price2) => Number.isFinite(price2) && price2 >= 0);
3934
+ }
3525
3935
  async function writeCache(file, models) {
3526
3936
  const payload = { version: CACHE_VERSION, fetchedAt: Date.now(), models };
3527
3937
  try {
@@ -4629,7 +5039,7 @@ function toToolSchema(tool) {
4629
5039
  }
4630
5040
 
4631
5041
  // src/tools/write.ts
4632
- import { lstat as lstat4, mkdir as mkdir3, readFile as readFile11, stat as stat9, writeFile as writeFile7 } from "fs/promises";
5042
+ import { lstat as lstat4, mkdir as mkdir4, readFile as readFile11, stat as stat9, writeFile as writeFile7 } from "fs/promises";
4633
5043
  import { dirname as dirname2 } from "path";
4634
5044
  var MAX_FILE_BYTES5 = 5e6;
4635
5045
  var writeTool = {
@@ -4698,7 +5108,7 @@ var writeTool = {
4698
5108
  let before = "";
4699
5109
  if (beforeBuffer) before = beforeBuffer.toString("utf8");
4700
5110
  try {
4701
- await mkdir3(dirname2(safe.path), { recursive: true });
5111
+ await mkdir4(dirname2(safe.path), { recursive: true });
4702
5112
  await ctx.checkpoint?.capture(safe.path);
4703
5113
  await writeFile7(safe.path, content, "utf8");
4704
5114
  ctx.checkpoint?.markChanged(safe.path);
@@ -4915,11 +5325,22 @@ function parseFrontmatter(text) {
4915
5325
  }
4916
5326
 
4917
5327
  // src/skills/install.ts
5328
+ import { randomUUID as randomUUID3 } from "crypto";
4918
5329
  import { execFileSync } from "child_process";
4919
- import { existsSync as existsSync2, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from "fs";
4920
- import { chmod as chmod6, mkdir as mkdir4 } from "fs/promises";
5330
+ import {
5331
+ existsSync as existsSync2,
5332
+ lstatSync,
5333
+ mkdirSync,
5334
+ readFileSync,
5335
+ readdirSync,
5336
+ realpathSync as realpathSync2,
5337
+ rmSync,
5338
+ writeFileSync
5339
+ } from "fs";
5340
+ import { chmod as chmod6, mkdir as mkdir5 } from "fs/promises";
4921
5341
  import path12 from "path";
4922
5342
  var TMP_DIR = path12.join(skillsDir, ".tmp");
5343
+ var MAX_SKILL_BYTES2 = 5e6;
4923
5344
  async function installSkill(source) {
4924
5345
  await ensureDir(skillsDir);
4925
5346
  if (isGitHubUrl(source)) {
@@ -4931,30 +5352,31 @@ async function installSkill(source) {
4931
5352
  return installFromLocal(source);
4932
5353
  }
4933
5354
  function isGitHubUrl(input) {
4934
- return /^https?:\/\/(www\.)?github\.com\/[\w.-]+\/[\w.-]+/.test(input);
5355
+ try {
5356
+ const url = new URL(input);
5357
+ return url.protocol === "https:" && (url.hostname === "github.com" || url.hostname === "www.github.com");
5358
+ } catch {
5359
+ return false;
5360
+ }
4935
5361
  }
4936
5362
  function isNpmPackage(input) {
4937
- if (input.includes("/") || input.includes("\\") || input.startsWith(".")) return false;
4938
- return /^(@[\w.-]+\/)?[\w.-]+(@[\w.*+-]+)?$/.test(input);
5363
+ return npmSkillName(input) !== null;
4939
5364
  }
4940
5365
  async function installFromGitHub(url) {
4941
5366
  const { owner, repo, subdir, branch } = parseGitHubUrl(url);
4942
- const name = subdir || repo;
4943
- const skillDir = path12.join(skillsDir, name);
4944
- const tmpDir = path12.join(TMP_DIR, `${name}-${Date.now()}`);
5367
+ const name = safeSkillName(subdir ? path12.posix.basename(subdir) : repo);
5368
+ const skillDir = safeChildPath(skillsDir, name);
5369
+ const tmpDir = safeChildPath(TMP_DIR, `${name}-${randomUUID3()}`);
4945
5370
  try {
4946
- rmSync(skillDir, { recursive: true, force: true });
4947
5371
  mkdirSync(tmpDir, { recursive: true });
4948
5372
  const cloneUrl = `https://github.com/${owner}/${repo}.git`;
4949
5373
  const gitArgs = ["clone", "--depth", "1"];
4950
5374
  if (branch) gitArgs.push("--branch", branch);
4951
5375
  gitArgs.push(cloneUrl, tmpDir);
4952
5376
  execFileSync("git", gitArgs, { stdio: "ignore" });
4953
- const skillFile = subdir ? path12.join(tmpDir, subdir, "SKILL.md") : path12.join(tmpDir, "SKILL.md");
4954
- if (!existsSync2(skillFile)) {
4955
- throw new Error(`No SKILL.md found in "${subdir || repo}"`);
4956
- }
4957
- const body = readFileSync(skillFile, "utf8");
5377
+ const skillFile = subdir ? safeChildPath(tmpDir, ...subdir.split("/"), "SKILL.md") : safeChildPath(tmpDir, "SKILL.md");
5378
+ const body = readSkillFile(skillFile, tmpDir, subdir || repo);
5379
+ removeExistingSkillDir(skillDir);
4958
5380
  await writeSkillFile(skillDir, body);
4959
5381
  return { name, dir: skillDir, source: url };
4960
5382
  } finally {
@@ -4962,9 +5384,11 @@ async function installFromGitHub(url) {
4962
5384
  }
4963
5385
  }
4964
5386
  async function installFromNpm(packageName) {
4965
- const name = packageName.replace(/^@/, "").replace(/\/[^/]*$/, "").replace(/@[^@]*$/, "");
4966
- const skillDir = path12.join(skillsDir, name);
4967
- const tmpDir = path12.join(TMP_DIR, `${name}-${Date.now()}`);
5387
+ const parsedName = npmSkillName(packageName);
5388
+ if (!parsedName) throw new Error(`Invalid npm package name: ${packageName}`);
5389
+ const name = safeSkillName(parsedName);
5390
+ const skillDir = safeChildPath(skillsDir, name);
5391
+ const tmpDir = safeChildPath(TMP_DIR, `${name}-${randomUUID3()}`);
4968
5392
  try {
4969
5393
  mkdirSync(tmpDir, { recursive: true });
4970
5394
  execFileSync("npm", ["pack", packageName, "--prefix", tmpDir], {
@@ -4976,6 +5400,7 @@ async function installFromNpm(packageName) {
4976
5400
  throw new Error(`Could not download npm package "${packageName}"`);
4977
5401
  }
4978
5402
  const tarballPath = path12.join(tmpDir, tarball);
5403
+ validateTarball(tarballPath);
4979
5404
  const extractDir = path12.join(tmpDir, "extracted");
4980
5405
  mkdirSync(extractDir, { recursive: true });
4981
5406
  try {
@@ -4985,7 +5410,7 @@ async function installFromNpm(packageName) {
4985
5410
  if (existsSync2(packageDir)) {
4986
5411
  const skillFile2 = path12.join(packageDir, "SKILL.md");
4987
5412
  if (existsSync2(skillFile2)) {
4988
- const body2 = readFileSync(skillFile2, "utf8");
5413
+ const body2 = readSkillFile(skillFile2, packageDir, packageName);
4989
5414
  await writeSkillFile(skillDir, body2);
4990
5415
  return { name, dir: skillDir, source: packageName };
4991
5416
  }
@@ -4999,13 +5424,41 @@ async function installFromNpm(packageName) {
4999
5424
  if (!existsSync2(skillFile)) {
5000
5425
  throw new Error(`No SKILL.md found in npm package "${packageName}"`);
5001
5426
  }
5002
- const body = readFileSync(skillFile, "utf8");
5427
+ const body = readSkillFile(skillFile, extractDir, packageName);
5003
5428
  await writeSkillFile(skillDir, body);
5004
5429
  return { name, dir: skillDir, source: packageName };
5005
5430
  } finally {
5006
5431
  rmSync(tmpDir, { recursive: true, force: true });
5007
5432
  }
5008
5433
  }
5434
+ function npmSkillName(input) {
5435
+ if (input.includes("\\") || input.startsWith(".")) return null;
5436
+ const scoped = input.match(/^@[\w.-]+\/([\w.-]+)(?:@[\w.*+-]+)?$/);
5437
+ if (scoped) return scoped[1] ?? null;
5438
+ const unscoped = input.match(/^([\w.-]+)(?:@[\w.*+-]+)?$/);
5439
+ return unscoped?.[1] ?? null;
5440
+ }
5441
+ function validateTarball(file) {
5442
+ const info = lstatSync(file, { throwIfNoEntry: false });
5443
+ if (!info?.isFile() || info.size > 2e7) {
5444
+ throw new Error("The npm skill archive is missing or exceeds the 20 MB safety limit.");
5445
+ }
5446
+ const listing = execFileSync("tar", ["-tzf", file], {
5447
+ encoding: "utf8",
5448
+ maxBuffer: 2e6,
5449
+ stdio: ["ignore", "pipe", "ignore"]
5450
+ });
5451
+ const entries = listing.split(/\r?\n/).filter(Boolean);
5452
+ if (entries.length === 0 || entries.length > 1e4) {
5453
+ throw new Error("The npm skill archive has an invalid number of entries.");
5454
+ }
5455
+ for (const entry of entries) {
5456
+ const normalized = entry.replace(/^\.\//, "");
5457
+ if (normalized.includes("\\") || path12.posix.isAbsolute(normalized) || normalized.split("/").some((segment) => segment === "..")) {
5458
+ throw new Error(`Unsafe path in npm skill archive: ${entry}`);
5459
+ }
5460
+ }
5461
+ }
5009
5462
  async function installFromLocal(srcPath) {
5010
5463
  const resolved = path12.resolve(srcPath);
5011
5464
  let skillDir;
@@ -5021,29 +5474,102 @@ async function installFromLocal(srcPath) {
5021
5474
  throw new Error(`No SKILL.md found at "${srcPath}"`);
5022
5475
  }
5023
5476
  const name = path12.basename(skillDir);
5024
- const destDir = path12.join(skillsDir, name);
5025
- const body = readFileSync(skillFile, "utf8");
5477
+ const destDir = safeChildPath(skillsDir, safeSkillName(name));
5478
+ const body = readSkillFile(skillFile, skillDir, srcPath);
5026
5479
  await writeSkillFile(destDir, body);
5027
5480
  return { name, dir: destDir, source: resolved };
5028
5481
  }
5029
5482
  async function writeSkillFile(dir, body) {
5030
- await mkdir4(dir, { recursive: true, mode: 448 });
5483
+ assertInside(skillsDir, dir);
5484
+ const existingDir = lstatSync(dir, { throwIfNoEntry: false });
5485
+ if (existingDir?.isSymbolicLink()) {
5486
+ throw new Error(`Refusing to replace symlinked skill directory: ${dir}`);
5487
+ }
5488
+ await mkdir5(dir, { recursive: true, mode: 448 });
5031
5489
  const file = path12.join(dir, "SKILL.md");
5032
- writeFileSync(file, body);
5490
+ const existingFile = lstatSync(file, { throwIfNoEntry: false });
5491
+ if (existingFile?.isSymbolicLink()) {
5492
+ throw new Error(`Refusing to replace symlinked skill file: ${file}`);
5493
+ }
5494
+ writeFileSync(file, body, { encoding: "utf8", mode: 384 });
5033
5495
  await chmod6(file, 384);
5034
5496
  }
5035
5497
  function parseGitHubUrl(url) {
5036
- const match = url.match(
5037
- /^https?:\/\/(?:www\.)?github\.com\/([^/]+)\/([^/]+)(?:\/tree\/([^/]+)(?:\/(.+)))?/
5038
- );
5039
- if (!match) {
5040
- const simple = url.match(/^https?:\/\/(?:www\.)?github\.com\/([^/]+)\/([^/]+)/);
5041
- if (simple) {
5042
- return { owner: simple[1], repo: simple[2] };
5043
- }
5498
+ let parsed;
5499
+ try {
5500
+ parsed = new URL(url);
5501
+ } catch {
5044
5502
  throw new Error(`Cannot parse GitHub URL: ${url}`);
5045
5503
  }
5046
- return { owner: match[1], repo: match[2], branch: match[3], subdir: match[4] };
5504
+ if (parsed.protocol !== "https:" || parsed.hostname !== "github.com" && parsed.hostname !== "www.github.com" || parsed.username || parsed.password) {
5505
+ throw new Error("GitHub skills must use an https://github.com URL without credentials.");
5506
+ }
5507
+ let segments;
5508
+ try {
5509
+ segments = parsed.pathname.split("/").filter(Boolean).map((segment) => decodeURIComponent(segment));
5510
+ } catch {
5511
+ throw new Error(`Cannot parse GitHub URL: ${url}`);
5512
+ }
5513
+ if (segments.length < 2) throw new Error(`Cannot parse GitHub URL: ${url}`);
5514
+ const owner = safeGitHubSegment(segments[0], "owner");
5515
+ const repo = safeSkillName(segments[1].replace(/\.git$/i, ""));
5516
+ if (segments.length === 2) return { owner, repo };
5517
+ if (segments[2] !== "tree" || segments.length < 4) {
5518
+ throw new Error(`Unsupported GitHub skill URL: ${url}`);
5519
+ }
5520
+ const branch = safeGitHubSegment(segments[3], "branch");
5521
+ const subdirSegments = segments.slice(4).map((segment) => safeGitHubSegment(segment, "path"));
5522
+ return {
5523
+ owner,
5524
+ repo,
5525
+ branch,
5526
+ subdir: subdirSegments.length > 0 ? subdirSegments.join("/") : void 0
5527
+ };
5528
+ }
5529
+ function safeGitHubSegment(value, kind) {
5530
+ if (value === "" || value === "." || value === ".." || value.length > 255 || /[\0-\x1f\x7f/\\:*?"<>|#%]/.test(value)) {
5531
+ throw new Error(`Unsafe GitHub ${kind}: ${value || "(empty)"}`);
5532
+ }
5533
+ return value;
5534
+ }
5535
+ function safeSkillName(value) {
5536
+ if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(value) || value === "." || value === "..") {
5537
+ throw new Error(`Unsafe skill name: ${value || "(empty)"}`);
5538
+ }
5539
+ return value;
5540
+ }
5541
+ function safeChildPath(root, ...segments) {
5542
+ const target = path12.resolve(root, ...segments);
5543
+ assertInside(root, target);
5544
+ return target;
5545
+ }
5546
+ function assertInside(root, target) {
5547
+ const relative2 = path12.relative(path12.resolve(root), path12.resolve(target));
5548
+ if (relative2 === "" || !path12.isAbsolute(relative2) && relative2 !== ".." && !relative2.startsWith(`..${path12.sep}`)) {
5549
+ return;
5550
+ }
5551
+ throw new Error(`Path escapes the skills directory: ${target}`);
5552
+ }
5553
+ function readSkillFile(file, allowedRoot, source) {
5554
+ const info = lstatSync(file, { throwIfNoEntry: false });
5555
+ if (!info?.isFile() || info.isSymbolicLink()) {
5556
+ throw new Error(`No regular SKILL.md found in "${source}"`);
5557
+ }
5558
+ if (info.size > MAX_SKILL_BYTES2) {
5559
+ throw new Error(`SKILL.md in "${source}" exceeds the ${MAX_SKILL_BYTES2 / 1e6} MB limit`);
5560
+ }
5561
+ const root = realpathSync2(allowedRoot);
5562
+ const resolved = realpathSync2(file);
5563
+ assertInside(root, resolved);
5564
+ return readFileSync(resolved, "utf8");
5565
+ }
5566
+ function removeExistingSkillDir(dir) {
5567
+ const info = lstatSync(dir, { throwIfNoEntry: false });
5568
+ if (!info) return;
5569
+ if (info.isSymbolicLink()) {
5570
+ throw new Error(`Refusing to remove symlinked skill directory: ${dir}`);
5571
+ }
5572
+ rmSync(dir, { recursive: true, force: true });
5047
5573
  }
5048
5574
 
5049
5575
  // src/tools/skill.ts
@@ -5185,6 +5711,7 @@ async function checkForUpdates(fetcher = fetch, currentCommit = KITCODE_COMMIT)
5185
5711
  signal: controller.signal
5186
5712
  });
5187
5713
  if (!response.ok) {
5714
+ await response.body?.cancel().catch(() => void 0);
5188
5715
  return { status: "unknown", reason: `GitHub returned ${response.status}` };
5189
5716
  }
5190
5717
  const payload = await readJsonBounded(response);
@@ -5210,7 +5737,10 @@ async function checkForUpdates(fetcher = fetch, currentCommit = KITCODE_COMMIT)
5210
5737
  }
5211
5738
  async function readJsonBounded(response) {
5212
5739
  const declared = Number(response.headers.get("content-length"));
5213
- if (Number.isFinite(declared) && declared > MAX_RESPONSE_BYTES2) return null;
5740
+ if (Number.isFinite(declared) && declared > MAX_RESPONSE_BYTES2) {
5741
+ await response.body?.cancel().catch(() => void 0);
5742
+ return null;
5743
+ }
5214
5744
  if (!response.body) return null;
5215
5745
  const reader = response.body.getReader();
5216
5746
  const chunks = [];
@@ -5295,7 +5825,7 @@ async function boot(options) {
5295
5825
  session = (options.continueSession ? await latestSessionFor(options.cwd) : null) ?? createSession(options.cwd, config.model ?? "");
5296
5826
  }
5297
5827
  const usage = createUsageTracker(session.usage, resolvePricing);
5298
- const skillCatalogue = formatSkillCatalogue(skills);
5828
+ let skillCatalogue = formatSkillCatalogue(skills);
5299
5829
  const mainSystemPrompt = () => buildSystemPrompt({
5300
5830
  cwd: options.cwd,
5301
5831
  toolNames: tools.list().map((tool) => tool.name),
@@ -5402,26 +5932,39 @@ async function boot(options) {
5402
5932
  createTaskTool(wrappedRunner, config.budget.maxSubagentsPerTurn)
5403
5933
  ]);
5404
5934
  let persistQueue = Promise.resolve();
5405
- const persistConfig = async (mutate) => {
5406
- mutate(config);
5407
- await saveConfig(config);
5935
+ let configQueue = Promise.resolve();
5936
+ const persistConfig = (mutate) => {
5937
+ const save = configQueue.catch(() => void 0).then(async () => {
5938
+ const before = structuredClone(config);
5939
+ mutate(config);
5940
+ try {
5941
+ await saveConfig(config);
5942
+ } catch (error) {
5943
+ Object.assign(config, before);
5944
+ throw error;
5945
+ }
5946
+ });
5947
+ configQueue = save;
5948
+ return save;
5408
5949
  };
5409
5950
  const compactWithBudget = async (history, signal, budget) => {
5410
5951
  if (compactCutIndex(history) <= 0) {
5411
5952
  return { history, compacted: false, removedMessages: 0 };
5412
5953
  }
5413
5954
  const resolved = registry.resolve(modelRef);
5955
+ const estimate = estimateCompactBudget(history, config.maxTokens);
5414
5956
  const budgetDecision = budget.beforeRequest({
5415
5957
  modelRef,
5416
- maxOutputTokens: Math.min(4096, config.maxTokens),
5417
- estimatedInputTokens: estimateCompactTokens(history)
5958
+ maxOutputTokens: estimate.maxOutputTokens,
5959
+ estimatedInputTokens: estimate.inputTokens
5418
5960
  });
5419
5961
  if (!budgetDecision.allowed) throw new Error(budgetDecision.reason);
5420
5962
  const result = await compactHistory({
5421
5963
  provider: resolved.provider,
5422
5964
  model: resolved.modelId,
5423
5965
  history,
5424
- maxTokens: budgetDecision.maxOutputTokens,
5966
+ maxTokens: config.maxTokens,
5967
+ maxTotalOutputTokens: budgetDecision.maxOutputTokens,
5425
5968
  signal
5426
5969
  });
5427
5970
  if (result.usage) {
@@ -5442,12 +5985,22 @@ async function boot(options) {
5442
5985
  async addProvider(url, key) {
5443
5986
  const detected = await detectProvider(url, key);
5444
5987
  rememberModels(detected.id, detected.models);
5988
+ const configBefore = structuredClone(config);
5989
+ const authBefore = { ...auth };
5445
5990
  config.providers[detected.id] = detected.config;
5446
5991
  auth[detected.id] = key;
5447
5992
  const chosen = preferredModel(detected.models);
5448
5993
  if (chosen) config.model = formatModelRef(detected.id, chosen);
5449
- await saveConfig(config);
5450
- await saveAuth(auth);
5994
+ try {
5995
+ await saveConfig(config);
5996
+ await saveAuth(auth);
5997
+ } catch (error) {
5998
+ Object.assign(config, configBefore);
5999
+ for (const id of Object.keys(auth)) delete auth[id];
6000
+ Object.assign(auth, authBefore);
6001
+ await Promise.allSettled([saveConfig(config), saveAuth(auth)]);
6002
+ throw error;
6003
+ }
5451
6004
  registry = createRegistry(config, auth);
5452
6005
  const nextRef = config.model ?? "";
5453
6006
  const discovered = detected.models.find((model) => model.id === chosen)?.contextWindow;
@@ -5537,17 +6090,26 @@ async function boot(options) {
5537
6090
  const provider = config.providers[providerId];
5538
6091
  if (!provider) throw new Error(`Provider "${providerId}" is not configured.`);
5539
6092
  const previousKey = auth[providerId];
6093
+ const restoreKey = () => {
6094
+ if (previousKey === void 0) delete auth[providerId];
6095
+ else auth[providerId] = previousKey;
6096
+ };
5540
6097
  auth[providerId] = newKey;
5541
6098
  try {
5542
6099
  const testRegistry = createRegistry(config, auth);
5543
- await loadModels(testRegistry.get(providerId));
6100
+ await testRegistry.get(providerId).listModels();
5544
6101
  } catch (error) {
5545
- auth[providerId] = previousKey;
6102
+ restoreKey();
5546
6103
  throw new Error(
5547
6104
  `Key verification failed for "${providerId}": ${error instanceof Error ? error.message : String(error)}`
5548
6105
  );
5549
6106
  }
5550
- await saveAuth(auth);
6107
+ try {
6108
+ await saveAuth(auth);
6109
+ } catch (error) {
6110
+ restoreKey();
6111
+ throw error;
6112
+ }
5551
6113
  registry = createRegistry(config, auth);
5552
6114
  if (parseModelRef(modelRef)?.provider === providerId) {
5553
6115
  const models = await loadModels(registry.get(providerId));
@@ -5614,7 +6176,7 @@ async function boot(options) {
5614
6176
  async exportSession(id, destination) {
5615
6177
  await persistQueue.catch(() => void 0);
5616
6178
  const target = destination ? path13.resolve(options.cwd, destination) : path13.join(options.cwd, ".kitcode-exports");
5617
- if (!destination) await mkdir5(target, { recursive: true, mode: 448 });
6179
+ if (!destination) await mkdir6(target, { recursive: true, mode: 448 });
5618
6180
  return (await exportSession(id, target)).path;
5619
6181
  },
5620
6182
  configPath: () => location.path,
@@ -5810,6 +6372,9 @@ ${lines.join("\n")}` : null;
5810
6372
  async installSkill(source) {
5811
6373
  const result = await installSkill(source);
5812
6374
  skills = await discoverSkills(skillRoots);
6375
+ skillCatalogue = formatSkillCatalogue(skills);
6376
+ tools.unregister(["skill"]);
6377
+ if (skills.length > 0) tools.register([createSkillTool(skills)]);
5813
6378
  return result;
5814
6379
  },
5815
6380
  async loadAttachment(requestedPath) {
@@ -5972,7 +6537,7 @@ ${lines.join("\n")}` : null;
5972
6537
  warnings,
5973
6538
  async shutdown() {
5974
6539
  try {
5975
- await persistQueue;
6540
+ await Promise.all([persistQueue, configQueue]);
5976
6541
  } finally {
5977
6542
  await mcp.close();
5978
6543
  }
@@ -6165,8 +6730,7 @@ function validateKey(value) {
6165
6730
  import { render } from "ink";
6166
6731
 
6167
6732
  // src/ui/App.tsx
6168
- import { Box as Box12, Text as Text12, useApp, useInput as useInput2 } from "ink";
6169
- import { execFile as execFile2, execFileSync as execFileSync2 } from "child_process";
6733
+ import { Box as Box13, Text as Text12, useApp, useInput as useInput2, useWindowSize as useWindowSize2 } from "ink";
6170
6734
  import { useCallback, useEffect as useEffect2, useMemo as useMemo4, useRef as useRef4, useState as useState5 } from "react";
6171
6735
 
6172
6736
  // src/mcp/add.ts
@@ -6834,7 +7398,13 @@ function Logo({ subtitle, workspace }) {
6834
7398
  return /* @__PURE__ */ jsxs2(Box2, { width: "100%", marginBottom: 1, children: [
6835
7399
  /* @__PURE__ */ jsx2(Box2, { flexDirection: "column", flexShrink: 0, children: CAT.map((line) => /* @__PURE__ */ jsx2(Text2, { color: theme.accent, children: line }, line)) }),
6836
7400
  /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", marginLeft: 2, flexGrow: 1, flexShrink: 1, minWidth: 0, children: [
6837
- /* @__PURE__ */ jsx2(Text2, { color: theme.accent, bold: true, children: "kitcode" }),
7401
+ /* @__PURE__ */ jsxs2(Box2, { children: [
7402
+ /* @__PURE__ */ jsx2(Text2, { color: theme.accent, bold: true, children: "kitcode" }),
7403
+ /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
7404
+ " v",
7405
+ KITCODE_VERSION
7406
+ ] })
7407
+ ] }),
6838
7408
  /* @__PURE__ */ jsx2(Text2, { dimColor: true, wrap: "truncate-end", children: subtitle ?? strings.hint }),
6839
7409
  location && /* @__PURE__ */ jsxs2(Box2, { width: "100%", children: [
6840
7410
  /* @__PURE__ */ jsx2(Text2, { color: theme.accent, children: "\u2302 " }),
@@ -7608,69 +8178,88 @@ function ContextMeter({
7608
8178
  ] });
7609
8179
  }
7610
8180
 
8181
+ // src/ui/components/TerminalViewport.tsx
8182
+ import { Box as Box10 } from "ink";
8183
+ import { jsx as jsx10 } from "react/jsx-runtime";
8184
+ function interactiveViewportRows(rows) {
8185
+ if (!Number.isFinite(rows)) return 23;
8186
+ return Math.max(1, Math.floor(rows) - 1);
8187
+ }
8188
+ function TerminalViewport({ children, rows }) {
8189
+ return /* @__PURE__ */ jsx10(
8190
+ Box10,
8191
+ {
8192
+ flexDirection: "column",
8193
+ maxHeight: interactiveViewportRows(rows),
8194
+ overflowY: "hidden",
8195
+ children
8196
+ }
8197
+ );
8198
+ }
8199
+
7611
8200
  // src/ui/components/Transcript.tsx
7612
- import { Box as Box11, Static, Text as Text11 } from "ink";
8201
+ import { Box as Box12, Static, Text as Text11 } from "ink";
7613
8202
  import { memo as memo2, useMemo as useMemo3, useRef as useRef3 } from "react";
7614
8203
  import Spinner2 from "ink-spinner";
7615
8204
 
7616
8205
  // src/ui/markdown.tsx
7617
- import { Box as Box10, Text as Text10 } from "ink";
8206
+ import { Box as Box11, Text as Text10 } from "ink";
7618
8207
  import { useMemo as useMemo2 } from "react";
7619
- import { Fragment as Fragment4, jsx as jsx10, jsxs as jsxs10 } from "react/jsx-runtime";
8208
+ import { Fragment as Fragment4, jsx as jsx11, jsxs as jsxs10 } from "react/jsx-runtime";
7620
8209
  function Markdown({ children }) {
7621
8210
  const blocks = useMemo2(() => extractBlocks(children), [children]);
7622
- return /* @__PURE__ */ jsx10(Box10, { flexDirection: "column", children: blocks.map((block, i) => /* @__PURE__ */ jsx10(BlockView, { block }, i)) });
8211
+ return /* @__PURE__ */ jsx11(Box11, { flexDirection: "column", children: blocks.map((block, i) => /* @__PURE__ */ jsx11(BlockView, { block }, i)) });
7623
8212
  }
7624
8213
  function BlockView({ block }) {
7625
8214
  switch (block.type) {
7626
8215
  case "heading": {
7627
8216
  const sizes = [22, 20, 18, 16, 14, 13];
7628
8217
  const size = sizes[(block.level ?? 1) - 1] ?? 14;
7629
- return /* @__PURE__ */ jsx10(Box10, { marginTop: block.level === 1 ? 1 : 0, children: /* @__PURE__ */ jsx10(Text10, { bold: true, children: truncateBySize(block.text ?? "", size) }) });
8218
+ return /* @__PURE__ */ jsx11(Box11, { marginTop: block.level === 1 ? 1 : 0, children: /* @__PURE__ */ jsx11(Text10, { bold: true, children: truncateBySize(block.text ?? "", size) }) });
7630
8219
  }
7631
8220
  case "blockquote":
7632
- return /* @__PURE__ */ jsx10(Box10, { flexDirection: "column", marginLeft: 2, children: (block.text ?? "").split("\n").map((line, i) => /* @__PURE__ */ jsxs10(Box10, { children: [
7633
- /* @__PURE__ */ jsx10(Text10, { dimColor: true, children: "\u2502 " }),
7634
- /* @__PURE__ */ jsx10(Text10, { dimColor: true, children: line })
8221
+ return /* @__PURE__ */ jsx11(Box11, { flexDirection: "column", marginLeft: 2, children: (block.text ?? "").split("\n").map((line, i) => /* @__PURE__ */ jsxs10(Box11, { children: [
8222
+ /* @__PURE__ */ jsx11(Text10, { dimColor: true, children: "\u2502 " }),
8223
+ /* @__PURE__ */ jsx11(Text10, { dimColor: true, children: line })
7635
8224
  ] }, i)) });
7636
8225
  case "ul":
7637
- return /* @__PURE__ */ jsx10(Box10, { flexDirection: "column", children: (block.items ?? []).map((item, i) => /* @__PURE__ */ jsxs10(Box10, { children: [
7638
- /* @__PURE__ */ jsx10(Text10, { dimColor: true, children: "\u2022 " }),
7639
- /* @__PURE__ */ jsx10(Box10, { marginLeft: 2, children: /* @__PURE__ */ jsx10(BlockView, { block: item }) })
8226
+ return /* @__PURE__ */ jsx11(Box11, { flexDirection: "column", children: (block.items ?? []).map((item, i) => /* @__PURE__ */ jsxs10(Box11, { children: [
8227
+ /* @__PURE__ */ jsx11(Text10, { dimColor: true, children: "\u2022 " }),
8228
+ /* @__PURE__ */ jsx11(Box11, { marginLeft: 2, children: /* @__PURE__ */ jsx11(BlockView, { block: item }) })
7640
8229
  ] }, i)) });
7641
8230
  case "ol":
7642
- return /* @__PURE__ */ jsx10(Box10, { flexDirection: "column", children: (block.items ?? []).map((item, i) => /* @__PURE__ */ jsxs10(Box10, { children: [
7643
- /* @__PURE__ */ jsx10(Text10, { dimColor: true, children: `${i + 1}. ` }),
7644
- /* @__PURE__ */ jsx10(Box10, { marginLeft: 2, children: /* @__PURE__ */ jsx10(BlockView, { block: item }) })
8231
+ return /* @__PURE__ */ jsx11(Box11, { flexDirection: "column", children: (block.items ?? []).map((item, i) => /* @__PURE__ */ jsxs10(Box11, { children: [
8232
+ /* @__PURE__ */ jsx11(Text10, { dimColor: true, children: `${i + 1}. ` }),
8233
+ /* @__PURE__ */ jsx11(Box11, { marginLeft: 2, children: /* @__PURE__ */ jsx11(BlockView, { block: item }) })
7645
8234
  ] }, i)) });
7646
8235
  case "paragraph":
7647
8236
  default:
7648
- return /* @__PURE__ */ jsx10(Text10, { children: inline(block.text ?? "") });
8237
+ return /* @__PURE__ */ jsx11(Text10, { children: inline(block.text ?? "") });
7649
8238
  case "table":
7650
- return /* @__PURE__ */ jsx10(TableView, { block });
8239
+ return /* @__PURE__ */ jsx11(TableView, { block });
7651
8240
  case "code":
7652
- return /* @__PURE__ */ jsx10(CodeBlock, { lang: block.lang, code: block.text ?? "" });
8241
+ return /* @__PURE__ */ jsx11(CodeBlock, { lang: block.lang, code: block.text ?? "" });
7653
8242
  case "hr":
7654
- return /* @__PURE__ */ jsx10(Box10, { marginTop: 1, marginBottom: 1, children: /* @__PURE__ */ jsx10(Text10, { dimColor: true, children: "\u2500".repeat(60) }) });
8243
+ return /* @__PURE__ */ jsx11(Box11, { marginTop: 1, marginBottom: 1, children: /* @__PURE__ */ jsx11(Text10, { dimColor: true, children: "\u2500".repeat(60) }) });
7655
8244
  }
7656
8245
  }
7657
8246
  function CodeBlock({ lang, code }) {
7658
8247
  const theme = useTheme();
7659
8248
  const lines = code.split("\n");
7660
- return /* @__PURE__ */ jsxs10(Box10, { flexDirection: "column", marginTop: 1, children: [
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)) }) })
8249
+ return /* @__PURE__ */ jsxs10(Box11, { flexDirection: "column", marginTop: 1, children: [
8250
+ lang && /* @__PURE__ */ jsx11(Text10, { dimColor: true, color: theme.accent, children: lang }),
8251
+ /* @__PURE__ */ jsx11(Box11, { borderColor: "gray", borderStyle: "round", paddingX: 1, children: /* @__PURE__ */ jsx11(Box11, { flexDirection: "column", children: lines.map((line, i) => /* @__PURE__ */ jsx11(Text10, { color: "cyan", children: line }, i)) }) })
7663
8252
  ] });
7664
8253
  }
7665
8254
  function TableView({ block }) {
7666
8255
  const headers = block.headers ?? [];
7667
8256
  const rows = block.rows ?? [];
7668
8257
  if (headers.length === 0 && rows.length === 0) {
7669
- return /* @__PURE__ */ jsx10(Text10, { dimColor: true, children: "(empty table)" });
8258
+ return /* @__PURE__ */ jsx11(Text10, { dimColor: true, children: "(empty table)" });
7670
8259
  }
7671
8260
  const colCount = Math.max(headers.length, ...rows.map((r) => r?.length ?? 0));
7672
8261
  if (colCount === 0) {
7673
- return /* @__PURE__ */ jsx10(Text10, { dimColor: true, children: "(empty table)" });
8262
+ return /* @__PURE__ */ jsx11(Text10, { dimColor: true, children: "(empty table)" });
7674
8263
  }
7675
8264
  const colWidths = new Array(colCount).fill(0);
7676
8265
  const allRows = [headers, ...rows];
@@ -7702,13 +8291,13 @@ function TableView({ block }) {
7702
8291
  cells.push(padded);
7703
8292
  }
7704
8293
  lines.push(
7705
- /* @__PURE__ */ jsx10(Text10, { bold: rowIdx === 0, children: cells.join(" \u2502 ") }, `row-${rowIdx}`)
8294
+ /* @__PURE__ */ jsx11(Text10, { bold: rowIdx === 0, children: cells.join(" \u2502 ") }, `row-${rowIdx}`)
7706
8295
  );
7707
8296
  if (rowIdx === 0) {
7708
- lines.push(/* @__PURE__ */ jsx10(Text10, { dimColor: true, children: separator }, `sep-${rowIdx}`));
8297
+ lines.push(/* @__PURE__ */ jsx11(Text10, { dimColor: true, children: separator }, `sep-${rowIdx}`));
7709
8298
  }
7710
8299
  });
7711
- return /* @__PURE__ */ jsx10(Box10, { flexDirection: "column", children: lines });
8300
+ return /* @__PURE__ */ jsx11(Box11, { flexDirection: "column", children: lines });
7712
8301
  }
7713
8302
  var CHARS_PER_FONT_LEVEL = 3;
7714
8303
  function truncateBySize(text, size) {
@@ -7963,7 +8552,7 @@ function inline(text, depth = 0) {
7963
8552
  {
7964
8553
  re: /(\[)([^\]]*)\]\(([^)]+)\)/,
7965
8554
  handler: (m) => /* @__PURE__ */ jsxs10(Fragment4, { children: [
7966
- /* @__PURE__ */ jsx10(Text10, { color: "cyan", underline: true, children: inline(m[2], depth + 1) }, key++),
8555
+ /* @__PURE__ */ jsx11(Text10, { color: "cyan", underline: true, children: inline(m[2], depth + 1) }, key++),
7967
8556
  /* @__PURE__ */ jsxs10(Text10, { dimColor: true, color: "gray", children: [
7968
8557
  "(",
7969
8558
  m[3],
@@ -7973,23 +8562,23 @@ function inline(text, depth = 0) {
7973
8562
  },
7974
8563
  {
7975
8564
  re: /(`+)([^`]+?)\1/,
7976
- handler: (m) => /* @__PURE__ */ jsx10(Text10, { bold: true, color: "cyan", children: m[2] }, key++)
8565
+ handler: (m) => /* @__PURE__ */ jsx11(Text10, { bold: true, color: "cyan", children: m[2] }, key++)
7977
8566
  },
7978
8567
  {
7979
8568
  re: /(\*\*)(.+?)\1/,
7980
- handler: (m) => /* @__PURE__ */ jsx10(Text10, { bold: true, children: inline(m[2], depth + 1) }, key++)
8569
+ handler: (m) => /* @__PURE__ */ jsx11(Text10, { bold: true, children: inline(m[2], depth + 1) }, key++)
7981
8570
  },
7982
8571
  {
7983
8572
  re: /(__)(.+?)\1/,
7984
- handler: (m) => /* @__PURE__ */ jsx10(Text10, { bold: true, children: inline(m[2], depth + 1) }, key++)
8573
+ handler: (m) => /* @__PURE__ */ jsx11(Text10, { bold: true, children: inline(m[2], depth + 1) }, key++)
7985
8574
  },
7986
8575
  {
7987
8576
  re: /(~~)(.+?)\1/,
7988
- handler: (m) => /* @__PURE__ */ jsx10(Text10, { strikethrough: true, children: m[2] }, key++)
8577
+ handler: (m) => /* @__PURE__ */ jsx11(Text10, { strikethrough: true, children: m[2] }, key++)
7989
8578
  },
7990
8579
  {
7991
8580
  re: /(\*)(.+?)\1/,
7992
- handler: (m) => /* @__PURE__ */ jsx10(Text10, { italic: true, children: inline(m[2], depth + 1) }, key++)
8581
+ handler: (m) => /* @__PURE__ */ jsx11(Text10, { italic: true, children: inline(m[2], depth + 1) }, key++)
7993
8582
  }
7994
8583
  ];
7995
8584
  while (rest.length > 0) {
@@ -8013,12 +8602,15 @@ function inline(text, depth = 0) {
8013
8602
  }
8014
8603
 
8015
8604
  // src/ui/components/Transcript.tsx
8016
- import { Fragment as Fragment5, jsx as jsx11, jsxs as jsxs11 } from "react/jsx-runtime";
8605
+ import { Fragment as Fragment5, jsx as jsx12, jsxs as jsxs11 } from "react/jsx-runtime";
8017
8606
  var HEADER = { kind: "header" };
8018
- var Transcript = memo2(function Transcript2({ bubbles, workspace }) {
8019
- const liveAt = bubbles.findLastIndex(
8020
- (bubble) => bubble.kind === "assistant" && bubble.streaming || bubble.kind === "tool" && bubble.state === "running"
8607
+ function firstMutableBubbleIndex(bubbles) {
8608
+ return bubbles.findIndex(
8609
+ (bubble) => bubble.kind === "assistant" && bubble.streaming || bubble.kind === "tool" && bubble.state === "running" || bubble.kind === "subagent" && bubble.state === "running"
8021
8610
  );
8611
+ }
8612
+ var Transcript = memo2(function Transcript2({ bubbles, workspace }) {
8613
+ const liveAt = firstMutableBubbleIndex(bubbles);
8022
8614
  const stableCount = liveAt === -1 ? bubbles.length : liveAt;
8023
8615
  const stableRef = useRef3([]);
8024
8616
  if (stableRef.current.length < stableCount) {
@@ -8029,40 +8621,48 @@ var Transcript = memo2(function Transcript2({ bubbles, workspace }) {
8029
8621
  } else if (stableRef.current.length > stableCount) {
8030
8622
  stableRef.current = bubbles.slice(0, stableCount);
8031
8623
  }
8032
- const stable = stableRef.current;
8033
8624
  const live = liveAt === -1 ? [] : bubbles.slice(liveAt);
8034
- const staticItems = [HEADER, ...stable];
8035
- return /* @__PURE__ */ jsxs11(Box11, { flexDirection: "column", marginBottom: 1, children: [
8036
- /* @__PURE__ */ jsx11(Static, { items: staticItems, children: (item) => item.kind === "header" ? /* @__PURE__ */ jsx11(Logo, { workspace }, "kitcode-header") : /* @__PURE__ */ jsx11(BubbleView, { bubble: item }, item.id) }),
8037
- live.map((bubble) => /* @__PURE__ */ jsx11(BubbleView, { bubble }, bubble.id))
8625
+ const staticItems = [HEADER, ...stableRef.current];
8626
+ return /* @__PURE__ */ jsxs11(Box12, { flexDirection: "column", marginBottom: 1, flexShrink: 1, overflowY: "hidden", children: [
8627
+ /* @__PURE__ */ jsx12(Static, { items: staticItems, children: (item) => item.kind === "header" ? /* @__PURE__ */ jsx12(Logo, { workspace }, "kitcode-header") : /* @__PURE__ */ jsx12(BubbleView, { bubble: item }, item.id) }),
8628
+ /* @__PURE__ */ jsx12(
8629
+ Box12,
8630
+ {
8631
+ flexDirection: "column",
8632
+ flexShrink: 1,
8633
+ overflowY: "hidden",
8634
+ justifyContent: "flex-end",
8635
+ children: live.map((bubble) => /* @__PURE__ */ jsx12(BubbleView, { bubble }, bubble.id))
8636
+ }
8637
+ )
8038
8638
  ] });
8039
8639
  });
8040
8640
  var BubbleView = memo2(function BubbleView2({ bubble }) {
8041
8641
  const theme = useTheme();
8042
8642
  if (bubble.kind === "user") {
8043
- return /* @__PURE__ */ jsx11(Box11, { marginTop: 1, children: /* @__PURE__ */ jsxs11(Text11, { color: theme.accent, bold: true, children: [
8643
+ return /* @__PURE__ */ jsx12(Box12, { marginTop: 1, children: /* @__PURE__ */ jsxs11(Text11, { color: theme.accent, bold: true, children: [
8044
8644
  "\u203A ",
8045
8645
  bubble.text
8046
8646
  ] }) });
8047
8647
  }
8048
8648
  if (bubble.kind === "notice") {
8049
8649
  const color = bubble.level === "error" ? theme.error : bubble.level === "warn" ? theme.warn : theme.accent;
8050
- return /* @__PURE__ */ jsx11(Box11, { marginTop: 1, children: /* @__PURE__ */ jsx11(Text11, { color, children: bubble.text }) });
8650
+ return /* @__PURE__ */ jsx12(Box12, { marginTop: 1, children: /* @__PURE__ */ jsx12(Text11, { color, children: bubble.text }) });
8051
8651
  }
8052
8652
  if (bubble.kind === "assistant") {
8053
- return /* @__PURE__ */ jsxs11(Box11, { flexDirection: "column", marginTop: 1, children: [
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 }),
8653
+ return /* @__PURE__ */ jsxs11(Box12, { flexDirection: "column", marginTop: 1, children: [
8654
+ bubble.thinking.trim() !== "" && /* @__PURE__ */ jsx12(Text11, { dimColor: true, italic: true, children: bubble.thinking.trim() }),
8655
+ bubble.streaming ? /* @__PURE__ */ jsx12(Text11, { children: bubble.text }) : /* @__PURE__ */ jsx12(Markdown, { children: bubble.text }),
8056
8656
  bubble.streaming && bubble.text === "" && /* @__PURE__ */ jsxs11(Text11, { dimColor: true, children: [
8057
- /* @__PURE__ */ jsx11(Spinner2, { type: "dots" }),
8657
+ /* @__PURE__ */ jsx12(Spinner2, { type: "dots" }),
8058
8658
  " thinking"
8059
8659
  ] })
8060
8660
  ] });
8061
8661
  }
8062
8662
  if (bubble.kind === "subagent") {
8063
- return /* @__PURE__ */ jsx11(SubagentView, { bubble });
8663
+ return /* @__PURE__ */ jsx12(SubagentView, { bubble });
8064
8664
  }
8065
- return /* @__PURE__ */ jsx11(ToolView, { bubble });
8665
+ return /* @__PURE__ */ jsx12(ToolView, { bubble });
8066
8666
  });
8067
8667
  function ToolView({ bubble }) {
8068
8668
  const theme = useTheme();
@@ -8073,19 +8673,19 @@ function ToolView({ bubble }) {
8073
8673
  () => display?.kind === "diff" ? diffLines(display.before, display.after) : null,
8074
8674
  [display]
8075
8675
  );
8076
- if (!diff) return /* @__PURE__ */ jsx11(NonDiffToolView, { bubble, mark, color });
8676
+ if (!diff) return /* @__PURE__ */ jsx12(NonDiffToolView, { bubble, mark, color });
8077
8677
  const hasChanges = diff.added > 0 || diff.removed > 0;
8078
- return /* @__PURE__ */ jsxs11(Box11, { flexDirection: "column", marginTop: 1, children: [
8678
+ return /* @__PURE__ */ jsxs11(Box12, { flexDirection: "column", marginTop: 1, children: [
8079
8679
  /* @__PURE__ */ jsxs11(Text11, { color, children: [
8080
8680
  mark,
8081
8681
  " ",
8082
8682
  bubble.summary,
8083
8683
  hasChanges && /* @__PURE__ */ jsxs11(Fragment5, { children: [
8084
- diff.added > 0 && /* @__PURE__ */ jsx11(Text11, { color: theme.ok, bold: true, children: ` +${diff.added}` }),
8085
- diff.removed > 0 && /* @__PURE__ */ jsx11(Text11, { color: theme.error, bold: true, children: ` -${diff.removed}` })
8684
+ diff.added > 0 && /* @__PURE__ */ jsx12(Text11, { color: theme.ok, bold: true, children: ` +${diff.added}` }),
8685
+ diff.removed > 0 && /* @__PURE__ */ jsx12(Text11, { color: theme.error, bold: true, children: ` -${diff.removed}` })
8086
8686
  ] })
8087
8687
  ] }),
8088
- /* @__PURE__ */ jsx11(Box11, { marginLeft: 2, children: /* @__PURE__ */ jsx11(DiffHunk, { lines: diff.hunk, hidden: diff.hidden }) })
8688
+ /* @__PURE__ */ jsx12(Box12, { marginLeft: 2, children: /* @__PURE__ */ jsx12(DiffHunk, { lines: diff.hunk, hidden: diff.hidden }) })
8089
8689
  ] });
8090
8690
  }
8091
8691
  function NonDiffToolView({
@@ -8093,13 +8693,13 @@ function NonDiffToolView({
8093
8693
  mark,
8094
8694
  color
8095
8695
  }) {
8096
- return /* @__PURE__ */ jsxs11(Box11, { flexDirection: "column", marginTop: 1, children: [
8696
+ return /* @__PURE__ */ jsxs11(Box12, { flexDirection: "column", marginTop: 1, children: [
8097
8697
  /* @__PURE__ */ jsxs11(Text11, { color, children: [
8098
8698
  mark,
8099
8699
  " ",
8100
8700
  bubble.summary
8101
8701
  ] }),
8102
- bubble.state !== "running" && bubble.content.trim() !== "" && /* @__PURE__ */ jsx11(Box11, { flexDirection: "column", marginLeft: 2, children: previewLines(bubble.content).map((line, index) => /* @__PURE__ */ jsx11(Text11, { dimColor: true, children: truncate2(line, 110) }, index)) })
8702
+ bubble.state !== "running" && bubble.content.trim() !== "" && /* @__PURE__ */ jsx12(Box12, { flexDirection: "column", marginLeft: 2, children: previewLines(bubble.content).map((line, index) => /* @__PURE__ */ jsx12(Text11, { dimColor: true, children: truncate2(line, 110) }, index)) })
8103
8703
  ] });
8104
8704
  }
8105
8705
  function previewLines(content) {
@@ -8111,15 +8711,15 @@ function SubagentView({ bubble }) {
8111
8711
  const theme = useTheme();
8112
8712
  const mark = bubble.state === "running" ? "\u25CC" : "\u25CF";
8113
8713
  const color = bubble.state === "running" ? theme.warn : theme.ok;
8114
- return /* @__PURE__ */ jsxs11(Box11, { flexDirection: "column", marginTop: 1, children: [
8714
+ return /* @__PURE__ */ jsxs11(Box12, { flexDirection: "column", marginTop: 1, children: [
8115
8715
  /* @__PURE__ */ jsxs11(Text11, { color, bold: true, children: [
8116
8716
  mark,
8117
8717
  " subagent: ",
8118
8718
  bubble.description
8119
8719
  ] }),
8120
- /* @__PURE__ */ jsxs11(Box11, { marginLeft: 2, flexDirection: "column", children: [
8121
- bubble.bubbles.map((inner, index) => /* @__PURE__ */ jsx11(BubbleView, { bubble: inner }, index)),
8122
- bubble.state === "done" && bubble.result && /* @__PURE__ */ jsx11(Box11, { marginTop: 1, children: /* @__PURE__ */ jsx11(Text11, { dimColor: true, children: "\u2500\u2500 result \u2500\u2500" }) })
8720
+ /* @__PURE__ */ jsxs11(Box12, { marginLeft: 2, flexDirection: "column", children: [
8721
+ bubble.bubbles.map((inner, index) => /* @__PURE__ */ jsx12(BubbleView, { bubble: inner }, index)),
8722
+ bubble.state === "done" && bubble.result && /* @__PURE__ */ jsx12(Box12, { marginTop: 1, children: /* @__PURE__ */ jsx12(Text11, { dimColor: true, children: "\u2500\u2500 result \u2500\u2500" }) })
8123
8723
  ] })
8124
8724
  ] });
8125
8725
  }
@@ -8361,26 +8961,17 @@ function sanitizeDisplay(display) {
8361
8961
  }
8362
8962
 
8363
8963
  // src/ui/App.tsx
8364
- import { jsx as jsx12, jsxs as jsxs12 } from "react/jsx-runtime";
8964
+ import { jsx as jsx13, jsxs as jsxs12 } from "react/jsx-runtime";
8365
8965
  var EFFORTS = ["low", "medium", "high", "xhigh", "max"];
8366
8966
  var STREAM_FRAME_MS = 50;
8367
8967
  var MAX_ATTACHMENTS = 8;
8368
- var NEEDS_IDLE = /* @__PURE__ */ new Set([
8369
- "clear",
8370
- "resume",
8371
- "sessions",
8372
- "logout",
8373
- "bypass",
8374
- "undo",
8375
- "compact",
8376
- "checker"
8377
- ]);
8378
8968
  function App({
8379
8969
  runtime,
8380
8970
  initialHistory,
8381
8971
  warnings = []
8382
8972
  }) {
8383
8973
  const { exit } = useApp();
8974
+ const { rows } = useWindowSize2();
8384
8975
  const [transcript, setTranscript] = useState5(
8385
8976
  () => warnings.reduce((state, text) => pushNotice(state, "warn", text), fromHistory(initialHistory))
8386
8977
  );
@@ -8390,8 +8981,12 @@ function App({
8390
8981
  () => inputHistoryFromMessages(initialHistory)
8391
8982
  );
8392
8983
  const [busy, setBusy] = useState5(false);
8984
+ const busyRef = useRef4(false);
8393
8985
  const [pendingCount, setPendingCount] = useState5(0);
8394
8986
  const queueRef = useRef4([]);
8987
+ const slashQueueRef = useRef4(Promise.resolve());
8988
+ const slashPendingRef = useRef4(0);
8989
+ const drainQueueRef = useRef4(() => void 0);
8395
8990
  const [attachments, setAttachments] = useState5([]);
8396
8991
  const attachmentsRef = useRef4([]);
8397
8992
  const automaticAttachmentTask = useRef4(null);
@@ -8458,43 +9053,6 @@ function App({
8458
9053
  const timer = setInterval(() => tick((n) => n + 1), 1e3);
8459
9054
  return () => clearInterval(timer);
8460
9055
  }, [turnStart]);
8461
- useEffect2(() => {
8462
- if (process.platform === "darwin" || process.platform === "win32") return;
8463
- let last = "";
8464
- let cancelling = false;
8465
- let giveUp = false;
8466
- let polling = false;
8467
- let interval = null;
8468
- const poll = () => {
8469
- if (cancelling || giveUp || polling) return;
8470
- polling = true;
8471
- execFile2(
8472
- "xclip",
8473
- ["-o", "-selection", "primary"],
8474
- { encoding: "utf8" },
8475
- (error, stdout) => {
8476
- polling = false;
8477
- if (cancelling) return;
8478
- if (error) {
8479
- giveUp = true;
8480
- if (interval) clearInterval(interval);
8481
- return;
8482
- }
8483
- const sel = stdout.trim();
8484
- if (sel && sel !== last) {
8485
- last = sel;
8486
- copyToClipboard(sel);
8487
- }
8488
- }
8489
- );
8490
- };
8491
- poll();
8492
- interval = setInterval(poll, 750);
8493
- return () => {
8494
- cancelling = true;
8495
- if (interval) clearInterval(interval);
8496
- };
8497
- }, []);
8498
9056
  const theme = useMemo4(() => makeTheme(accent), [accent]);
8499
9057
  const strings = useMemo4(() => stringsFor(lang), [lang]);
8500
9058
  const notice = useCallback(
@@ -8540,8 +9098,10 @@ function App({
8540
9098
  []
8541
9099
  );
8542
9100
  const runAgent = useCallback(async () => {
9101
+ if (busyRef.current) return;
8543
9102
  const controller = new AbortController();
8544
9103
  abort.current = controller;
9104
+ busyRef.current = true;
8545
9105
  setBusy(true);
8546
9106
  setTurnStart(Date.now());
8547
9107
  turns.current += 1;
@@ -8577,17 +9137,10 @@ function App({
8577
9137
  } finally {
8578
9138
  flushTranscriptEvents();
8579
9139
  abort.current = null;
9140
+ busyRef.current = false;
8580
9141
  setBusy(false);
8581
9142
  setTurnStart(null);
8582
- const queue = queueRef.current;
8583
- if (queue.length > 0) {
8584
- const [next, ...rest] = queue;
8585
- queueRef.current = rest;
8586
- setPendingCount(rest.length);
8587
- queueMicrotask(() => enqueueNext(next));
8588
- } else {
8589
- setPendingCount(0);
8590
- }
9143
+ drainQueueRef.current();
8591
9144
  }
8592
9145
  }, [flushTranscriptEvents, notice, queueTranscriptEvent, runtime]);
8593
9146
  const applyAccent = useCallback(
@@ -9161,6 +9714,7 @@ ${strings.mcpAddUsage}`
9161
9714
  case "compact": {
9162
9715
  const controller = new AbortController();
9163
9716
  abort.current = controller;
9717
+ busyRef.current = true;
9164
9718
  setBusy(true);
9165
9719
  setTurnStart(Date.now());
9166
9720
  notice("info", strings.compactStarting);
@@ -9183,17 +9737,9 @@ ${strings.mcpAddUsage}`
9183
9737
  }
9184
9738
  } finally {
9185
9739
  abort.current = null;
9740
+ busyRef.current = false;
9186
9741
  setBusy(false);
9187
9742
  setTurnStart(null);
9188
- const queue = queueRef.current;
9189
- if (queue.length > 0) {
9190
- const [next, ...remaining] = queue;
9191
- queueRef.current = remaining;
9192
- setPendingCount(remaining.length);
9193
- if (next) queueMicrotask(() => enqueueNext(next));
9194
- } else {
9195
- setPendingCount(0);
9196
- }
9197
9743
  }
9198
9744
  forceRender((n) => n + 1);
9199
9745
  return;
@@ -9396,18 +9942,46 @@ Rename the file if you want a different name.`);
9396
9942
  },
9397
9943
  [applyAccent, appendAttachment, ask2, exit, notice, pick, replaceAttachments, runtime, strings]
9398
9944
  );
9945
+ const runSlash = useCallback(
9946
+ (line) => {
9947
+ slashPendingRef.current += 1;
9948
+ const task = slashQueueRef.current.catch(() => void 0).then(() => handleSlash(line));
9949
+ slashQueueRef.current = task;
9950
+ void task.catch(
9951
+ (error) => notice("error", error instanceof Error ? error.message : String(error))
9952
+ );
9953
+ const finished = () => {
9954
+ slashPendingRef.current = Math.max(0, slashPendingRef.current - 1);
9955
+ drainQueueRef.current();
9956
+ };
9957
+ void task.then(finished, finished);
9958
+ },
9959
+ [handleSlash, notice]
9960
+ );
9399
9961
  const enqueueNext = useCallback(
9400
9962
  (item) => {
9401
9963
  if (item.kind === "command") {
9402
- void handleSlash(item.line);
9964
+ runSlash(item.line);
9403
9965
  return;
9404
9966
  }
9405
9967
  setTranscript((state) => pushUser(state, userDisplay(item.text, item.content)));
9406
9968
  history.current = [...history.current, { role: "user", content: item.content }];
9407
9969
  void runAgent();
9408
9970
  },
9409
- [handleSlash, runAgent]
9971
+ [runAgent, runSlash]
9410
9972
  );
9973
+ const drainQueue = useCallback(() => {
9974
+ if (busyRef.current || slashPendingRef.current > 0) return;
9975
+ const [next, ...remaining] = queueRef.current;
9976
+ if (!next) {
9977
+ setPendingCount(0);
9978
+ return;
9979
+ }
9980
+ queueRef.current = remaining;
9981
+ setPendingCount(remaining.length);
9982
+ queueMicrotask(() => enqueueNext(next));
9983
+ }, [enqueueNext]);
9984
+ drainQueueRef.current = drainQueue;
9411
9985
  const tryQueueAutomaticAttachment = useCallback(
9412
9986
  (requestedPath) => {
9413
9987
  if (!looksLikeAttachmentPath(requestedPath)) return Promise.resolve(false);
@@ -9469,12 +10043,12 @@ Rename the file if you want a different name.`);
9469
10043
  const text = raw.trim();
9470
10044
  const submitSlash = () => {
9471
10045
  if (!detachedInput) setInput("");
9472
- if (busy && slashNeedsIdle(text)) {
10046
+ if (busyRef.current || slashPendingRef.current > 0) {
9473
10047
  queueRef.current = [...queueRef.current, { kind: "command", line: text }];
9474
10048
  setPendingCount(queueRef.current.length);
9475
10049
  return;
9476
10050
  }
9477
- void handleSlash(text);
10051
+ runSlash(text);
9478
10052
  };
9479
10053
  if (isKnownSlashCommand(text)) {
9480
10054
  submitSlash();
@@ -9498,7 +10072,7 @@ Rename the file if you want a different name.`);
9498
10072
  ];
9499
10073
  replaceAttachments([]);
9500
10074
  const item = { kind: "message", text, content };
9501
- if (busy) {
10075
+ if (busyRef.current || slashPendingRef.current > 0) {
9502
10076
  queueRef.current = [...queueRef.current, item];
9503
10077
  setPendingCount(queueRef.current.length);
9504
10078
  return;
@@ -9507,7 +10081,7 @@ Rename the file if you want a different name.`);
9507
10081
  history.current = [...history.current, { role: "user", content }];
9508
10082
  void runAgent();
9509
10083
  },
9510
- [busy, handleSlash, replaceAttachments, runAgent, tryQueueAutomaticAttachment]
10084
+ [replaceAttachments, runAgent, runSlash, tryQueueAutomaticAttachment]
9511
10085
  );
9512
10086
  useTerminalInput((_char, key) => {
9513
10087
  if (key.tab && key.shift) {
@@ -9541,10 +10115,10 @@ Rename the file if you want a different name.`);
9541
10115
  }
9542
10116
  setInput("");
9543
10117
  });
9544
- const shell = (children) => /* @__PURE__ */ jsx12(ThemeContext.Provider, { value: theme, children: /* @__PURE__ */ jsx12(StringsContext.Provider, { value: strings, children }) });
10118
+ const shell = (children) => /* @__PURE__ */ jsx13(ThemeContext.Provider, { value: theme, children: /* @__PURE__ */ jsx13(StringsContext.Provider, { value: strings, children }) });
9545
10119
  if (lang === void 0) {
9546
10120
  return shell(
9547
- /* @__PURE__ */ jsx12(
10121
+ /* @__PURE__ */ jsx13(
9548
10122
  LanguagePicker,
9549
10123
  {
9550
10124
  onPick: (chosen) => {
@@ -9557,7 +10131,7 @@ Rename the file if you want a different name.`);
9557
10131
  }
9558
10132
  if (setup) {
9559
10133
  return shell(
9560
- /* @__PURE__ */ jsx12(
10134
+ /* @__PURE__ */ jsx13(
9561
10135
  Onboarding,
9562
10136
  {
9563
10137
  onSubmit: async (url, key) => {
@@ -9572,8 +10146,8 @@ ${strings.configAt(runtime.configPath())}`);
9572
10146
  );
9573
10147
  }
9574
10148
  return shell(
9575
- /* @__PURE__ */ jsxs12(Box12, { flexDirection: "column", children: [
9576
- /* @__PURE__ */ jsx12(
10149
+ /* @__PURE__ */ jsxs12(TerminalViewport, { rows, children: [
10150
+ /* @__PURE__ */ jsx13(
9577
10151
  Transcript,
9578
10152
  {
9579
10153
  bubbles: transcript.bubbles,
@@ -9581,7 +10155,7 @@ ${strings.configAt(runtime.configPath())}`);
9581
10155
  },
9582
10156
  transcriptRevision
9583
10157
  ),
9584
- overlay.kind === "permission" && /* @__PURE__ */ jsx12(
10158
+ overlay.kind === "permission" && /* @__PURE__ */ jsx13(
9585
10159
  PermissionPrompt,
9586
10160
  {
9587
10161
  request: overlay.request,
@@ -9591,7 +10165,7 @@ ${strings.configAt(runtime.configPath())}`);
9591
10165
  }
9592
10166
  }
9593
10167
  ),
9594
- overlay.kind === "picker" && /* @__PURE__ */ jsx12(
10168
+ overlay.kind === "picker" && /* @__PURE__ */ jsx13(
9595
10169
  Picker,
9596
10170
  {
9597
10171
  title: overlay.title,
@@ -9606,7 +10180,7 @@ ${strings.configAt(runtime.configPath())}`);
9606
10180
  }
9607
10181
  }
9608
10182
  ),
9609
- overlay.kind === "confirm" && /* @__PURE__ */ jsx12(
10183
+ overlay.kind === "confirm" && /* @__PURE__ */ jsx13(
9610
10184
  Confirm,
9611
10185
  {
9612
10186
  title: overlay.title,
@@ -9618,7 +10192,7 @@ ${strings.configAt(runtime.configPath())}`);
9618
10192
  }
9619
10193
  }
9620
10194
  ),
9621
- overlay.kind === "textinput" && /* @__PURE__ */ jsx12(
10195
+ overlay.kind === "textinput" && /* @__PURE__ */ jsx13(
9622
10196
  TextInputOverlay,
9623
10197
  {
9624
10198
  title: overlay.title,
@@ -9633,7 +10207,7 @@ ${strings.configAt(runtime.configPath())}`);
9633
10207
  }
9634
10208
  }
9635
10209
  ),
9636
- overlay.kind === "none" && /* @__PURE__ */ jsx12(
10210
+ overlay.kind === "none" && /* @__PURE__ */ jsx13(
9637
10211
  PromptInput,
9638
10212
  {
9639
10213
  value: input,
@@ -9651,7 +10225,7 @@ ${strings.configAt(runtime.configPath())}`);
9651
10225
  })
9652
10226
  }
9653
10227
  ),
9654
- /* @__PURE__ */ jsx12(
10228
+ /* @__PURE__ */ jsx13(
9655
10229
  StatusBar,
9656
10230
  {
9657
10231
  status: {
@@ -9687,9 +10261,9 @@ function TextInputOverlay({
9687
10261
  useInput2((_input, key) => {
9688
10262
  if (key.escape) onCancel();
9689
10263
  });
9690
- return /* @__PURE__ */ jsxs12(Box12, { flexDirection: "column", children: [
9691
- /* @__PURE__ */ jsx12(Text12, { dimColor: true, children: title }),
9692
- /* @__PURE__ */ jsx12(
10264
+ return /* @__PURE__ */ jsxs12(Box13, { flexDirection: "column", children: [
10265
+ /* @__PURE__ */ jsx13(Text12, { dimColor: true, children: title }),
10266
+ /* @__PURE__ */ jsx13(
9693
10267
  TextInput2,
9694
10268
  {
9695
10269
  value,
@@ -9698,13 +10272,9 @@ function TextInputOverlay({
9698
10272
  mask: mask ? "\u2022" : void 0
9699
10273
  }
9700
10274
  ),
9701
- /* @__PURE__ */ jsx12(Box12, { marginTop: 1, children: /* @__PURE__ */ jsx12(Text12, { dimColor: true, color: theme.accent, children: "enter submit \xB7 esc cancel" }) })
10275
+ /* @__PURE__ */ jsx13(Box13, { marginTop: 1, children: /* @__PURE__ */ jsx13(Text12, { dimColor: true, color: theme.accent, children: "enter submit \xB7 esc cancel" }) })
9702
10276
  ] });
9703
10277
  }
9704
- function slashNeedsIdle(line) {
9705
- const [command = "", subcommand = ""] = line.slice(1).trim().toLowerCase().split(/\s+/);
9706
- return NEEDS_IDLE.has(command) || command === "mcp" && (subcommand === "add" || subcommand === "delete" || subcommand === "enable" || subcommand === "disable");
9707
- }
9708
10278
  function isKnownSlashCommand(line) {
9709
10279
  if (!line.startsWith("/")) return false;
9710
10280
  const [name = ""] = line.slice(1).trim().toLowerCase().split(/\s+/);
@@ -9773,38 +10343,9 @@ function unquoteArg(value) {
9773
10343
  const last = value.at(-1);
9774
10344
  return first2 === '"' && last === '"' || first2 === "'" && last === "'" ? value.slice(1, -1) : value;
9775
10345
  }
9776
- function copyToClipboard(text) {
9777
- const run = (cmd, args) => execFileSync2(cmd, args, { input: text, stdio: ["pipe", "ignore", "ignore"] });
9778
- if (process.platform === "darwin") {
9779
- try {
9780
- run("pbcopy", []);
9781
- } catch {
9782
- }
9783
- return;
9784
- }
9785
- if (process.platform === "win32") {
9786
- try {
9787
- run("clip", []);
9788
- } catch {
9789
- }
9790
- return;
9791
- }
9792
- const candidates = [
9793
- ["wl-copy", []],
9794
- ["xclip", ["-selection", "clipboard"]],
9795
- ["xsel", ["--clipboard", "--input"]]
9796
- ];
9797
- for (const [cmd, args] of candidates) {
9798
- try {
9799
- run(cmd, args);
9800
- return;
9801
- } catch {
9802
- }
9803
- }
9804
- }
9805
10346
 
9806
10347
  // src/app/tui.tsx
9807
- import { jsx as jsx13 } from "react/jsx-runtime";
10348
+ import { jsx as jsx14 } from "react/jsx-runtime";
9808
10349
  function clearTerminal() {
9809
10350
  if (process.stdout.isTTY) process.stdout.write("\x1B[2J\x1B[3J\x1B[H");
9810
10351
  }
@@ -9817,7 +10358,7 @@ async function startTui(options) {
9817
10358
  mode: options.mode
9818
10359
  });
9819
10360
  clearTerminal();
9820
- const instance = render(/* @__PURE__ */ jsx13(App, { runtime, initialHistory: history, warnings }), {
10361
+ const instance = render(/* @__PURE__ */ jsx14(App, { runtime, initialHistory: history, warnings }), {
9821
10362
  incrementalRendering: true,
9822
10363
  maxFps: 30
9823
10364
  });