@kernelonpanic/kitcode 1.0.1-beta → 1.2.0

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