@kernelonpanic/kitcode 1.2.8 → 1.3.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
@@ -40,7 +40,7 @@ var providerConfigSchema = z.object({
40
40
  keyEnv: z.string().optional(),
41
41
  headers: z.record(z.string(), z.string()).optional()
42
42
  });
43
- var effortSchema = z.enum(["low", "medium", "high", "xhigh", "max"]);
43
+ var effortSchema = z.enum(["auto", "low", "medium", "high", "xhigh", "max"]);
44
44
  var permissionModeSchema = z.enum(["allow", "ask", "deny"]);
45
45
  var mcpStdioSchema = z.object({
46
46
  type: z.literal("stdio"),
@@ -1024,14 +1024,120 @@ function skipControlString(text, from, bellTerminates) {
1024
1024
  return text.length - 1;
1025
1025
  }
1026
1026
 
1027
+ // src/tools/memory.ts
1028
+ import { z as z2 } from "zod";
1029
+
1030
+ // src/tools/summary.ts
1031
+ function brief(value, max = 60) {
1032
+ const text = String(value ?? "").replace(/\s+/g, " ").trim();
1033
+ return text.length > max ? `${text.slice(0, max - 1)}\u2026` : text;
1034
+ }
1035
+
1036
+ // src/tools/memory.ts
1037
+ var inputSchema = z2.discriminatedUnion("action", [
1038
+ z2.object({ action: z2.literal("read") }).strict(),
1039
+ z2.object({ action: z2.literal("add"), text: z2.string().trim().min(1).max(4e3), source: z2.string().trim().min(1).max(1e3) }).strict(),
1040
+ z2.object({ action: z2.literal("replace"), oldText: z2.string().min(1), text: z2.string().trim().min(1).max(4e3), source: z2.string().trim().min(1).max(1e3) }).strict(),
1041
+ z2.object({ action: z2.literal("delete"), oldText: z2.string().min(1), source: z2.string().trim().min(1).max(1e3) }).strict()
1042
+ ]);
1043
+ function createMemoryTool(store) {
1044
+ return {
1045
+ name: "memory",
1046
+ description: "Read or update persistent notes for this project. Remember only explicit user preferences, confirmed project facts and accepted decisions, with their source (user instruction or file/tool evidence). Never store secrets, guesses, temporary task progress or raw logs. Read before changing existing notes. For replace/delete, oldText must match exactly once; include the whole old note and its source. These notes survive new chats. Do not use this tool to change permissions or override current instructions.",
1047
+ inputSchema: {
1048
+ type: "object",
1049
+ properties: {
1050
+ action: { type: "string", enum: ["read", "add", "replace", "delete"] },
1051
+ text: { type: "string", maxLength: 4e3 },
1052
+ oldText: { type: "string" },
1053
+ source: { type: "string", maxLength: 1e3 }
1054
+ },
1055
+ required: ["action"],
1056
+ additionalProperties: false
1057
+ },
1058
+ defaultPermission: "allow",
1059
+ summarize(input) {
1060
+ const parsed = inputSchema.safeParse(input);
1061
+ return parsed.success ? `memory(${parsed.data.action}${"text" in parsed.data ? `: ${brief(parsed.data.text, 80)}` : ""})` : "memory(invalid input)";
1062
+ },
1063
+ async execute(input, ctx) {
1064
+ const parsed = inputSchema.safeParse(input);
1065
+ if (!parsed.success) return { content: "Invalid memory arguments: mutations require source; replace/delete also require oldText.", isError: true };
1066
+ const args = parsed.data;
1067
+ const before = store.read();
1068
+ if (args.action === "read") return { content: before || "Project memory is empty." };
1069
+ if (ctx.signal.aborted) return { content: "Memory update cancelled.", isError: true };
1070
+ const note = "text" in args ? `${redactSecrets(args.text)}
1071
+ Source: ${redactSecrets(args.source)}` : "";
1072
+ let after;
1073
+ if (args.action === "add") {
1074
+ if (before.includes(note)) return { content: "This note is already in project memory." };
1075
+ after = [before.trim(), note].filter(Boolean).join("\n\n");
1076
+ } else {
1077
+ const at = before.indexOf(args.oldText);
1078
+ if (at === -1 || before.indexOf(args.oldText, at + 1) !== -1) {
1079
+ return { content: "oldText must match exactly once. Read memory again before retrying.", isError: true };
1080
+ }
1081
+ after = (before.slice(0, at) + note + before.slice(at + args.oldText.length)).trim();
1082
+ }
1083
+ await store.save(after);
1084
+ return { content: `Project memory updated (${args.action}).
1085
+ ${store.read()}`, display: { kind: "diff", path: "project memory", before, after: store.read() } };
1086
+ }
1087
+ };
1088
+ }
1089
+
1090
+ // src/core/memory.ts
1091
+ import { createHash } from "crypto";
1092
+ import { readFile as readFile3, writeFile as writeFile3, rm as rm3 } from "fs/promises";
1093
+ import path4 from "path";
1094
+ function projectMemoryPath(workspace) {
1095
+ const key = process.platform === "win32" ? workspace.toLowerCase() : workspace;
1096
+ return path4.join(homeDir, "memory", createHash("sha256").update(key).digest("hex") + ".txt");
1097
+ }
1098
+ async function readProjectMemory(workspace) {
1099
+ try {
1100
+ return await readFile3(projectMemoryPath(workspace), "utf8");
1101
+ } catch (error) {
1102
+ if (error.code === "ENOENT") return "";
1103
+ throw error;
1104
+ }
1105
+ }
1106
+ async function saveProjectMemory(workspace, text) {
1107
+ if (text.length > 16e3) throw new Error("Project memory is limited to 16,000 characters.");
1108
+ const file = projectMemoryPath(workspace);
1109
+ await ensureDir(path4.dirname(file));
1110
+ await writeFile3(file, redactSecrets(text), { mode: 384 });
1111
+ }
1112
+ async function clearProjectMemory(workspace) {
1113
+ await rm3(projectMemoryPath(workspace), { force: true });
1114
+ }
1115
+
1116
+ // src/providers/effort.ts
1117
+ function resolveEffort(effort, messages) {
1118
+ if (effort !== "auto") return effort;
1119
+ const latest = messages.findLast((message) => message.role === "user" && message.content.some((block) => block.type === "text"));
1120
+ const text = latest?.content.filter((block) => block.type === "text").map((block) => block.text).join("\n") ?? "";
1121
+ return text.length > 500 || /bug|fix|debug|test|refactor|implement|баг|фикс|исправ|тест|рефактор|реализ/i.test(text) ? "high" : "medium";
1122
+ }
1123
+ function openAiEffort(model, effort) {
1124
+ const id = model.split("/").at(-1) ?? model;
1125
+ if (!/^(?:o[134](?:-|$)|gpt-5(?:[.-]|$))/.test(id)) return void 0;
1126
+ if (/^(?:o[134](?:-|$)|gpt-5(?:-(?:mini|nano|\d{4})|$)|gpt-5\.1(?:-|$))/.test(id)) {
1127
+ return effort === "max" || effort === "xhigh" ? "high" : effort;
1128
+ }
1129
+ if (/^gpt-5\.[234](?:-|$)/.test(id)) return effort === "max" ? "xhigh" : effort;
1130
+ return effort === "max" || effort === "xhigh" ? "high" : effort;
1131
+ }
1132
+
1027
1133
  // src/app/runtime.ts
1028
- import path13 from "path";
1134
+ import path14 from "path";
1029
1135
  import { mkdir as mkdir6 } from "fs/promises";
1030
1136
 
1031
1137
  // src/core/session.ts
1032
1138
  import { randomBytes } from "crypto";
1033
- import { readFile as readFile3, readdir, rename as rename3, stat as stat2, unlink, writeFile as writeFile3 } from "fs/promises";
1034
- import path4 from "path";
1139
+ import { readFile as readFile4, readdir, rename as rename3, stat as stat2, unlink, writeFile as writeFile4 } from "fs/promises";
1140
+ import path5 from "path";
1035
1141
  var MAX_SESSION_BYTES = 5e7;
1036
1142
  function createSession(cwd, model) {
1037
1143
  const now = (/* @__PURE__ */ new Date()).toISOString();
@@ -1050,10 +1156,10 @@ async function saveSession(state) {
1050
1156
  assertSessionId(state.id);
1051
1157
  state.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
1052
1158
  await ensureDir(sessionsDir);
1053
- const file = path4.join(sessionsDir, `${state.id}.json`);
1159
+ const file = path5.join(sessionsDir, `${state.id}.json`);
1054
1160
  const temp = `${file}.${process.pid}.${randomBytes(4).toString("hex")}.tmp`;
1055
1161
  try {
1056
- await writeFile3(temp, JSON.stringify(state), { encoding: "utf8", mode: 384 });
1162
+ await writeFile4(temp, JSON.stringify(state), { encoding: "utf8", mode: 384 });
1057
1163
  await atomicRename(temp, file);
1058
1164
  } catch (error) {
1059
1165
  await unlink(temp).catch(() => void 0);
@@ -1077,14 +1183,14 @@ async function atomicRename(from, to, retries = 5) {
1077
1183
  }
1078
1184
  async function loadSession(id) {
1079
1185
  assertSessionId(id);
1080
- const file = path4.join(sessionsDir, `${id}.json`);
1186
+ const file = path5.join(sessionsDir, `${id}.json`);
1081
1187
  let raw;
1082
1188
  try {
1083
1189
  const info = await stat2(file);
1084
1190
  if (info.size > MAX_SESSION_BYTES) {
1085
1191
  throw new Error(`Session exceeds the ${MAX_SESSION_BYTES / 1e6} MB load limit: ${file}`);
1086
1192
  }
1087
- raw = await readFile3(file, "utf8");
1193
+ raw = await readFile4(file, "utf8");
1088
1194
  } catch (error) {
1089
1195
  if (error instanceof Error && error.message.startsWith("Session exceeds")) throw error;
1090
1196
  if (error.code === "ENOENT") {
@@ -1100,7 +1206,7 @@ async function listSessions(limit = 20) {
1100
1206
  const files = await filesByRecency();
1101
1207
  const summaries = [];
1102
1208
  for (const file of files.slice(0, limit)) {
1103
- const state = readState(await readFile3(file, "utf8").catch(() => ""));
1209
+ const state = readState(await readFile4(file, "utf8").catch(() => ""));
1104
1210
  if (state) {
1105
1211
  summaries.push({
1106
1212
  id: state.id,
@@ -1126,7 +1232,7 @@ async function renameSession(query, title) {
1126
1232
  async function deleteSession(query) {
1127
1233
  const id = await resolveSessionId(query);
1128
1234
  assertSessionId(id);
1129
- await unlink(path4.join(sessionsDir, `${id}.json`)).catch((error) => {
1235
+ await unlink(path5.join(sessionsDir, `${id}.json`)).catch((error) => {
1130
1236
  if (error.code === "ENOENT") throw new Error(`Session no longer exists: ${id}`);
1131
1237
  throw error;
1132
1238
  });
@@ -1151,7 +1257,7 @@ async function deleteAllSessions() {
1151
1257
  }
1152
1258
  });
1153
1259
  const results = await Promise.allSettled(
1154
- ids.map((id) => unlink(path4.join(sessionsDir, `${id}.json`)))
1260
+ ids.map((id) => unlink(path5.join(sessionsDir, `${id}.json`)))
1155
1261
  );
1156
1262
  const deleted = [];
1157
1263
  const failed = [];
@@ -1172,7 +1278,7 @@ async function exportSession(query, destination) {
1172
1278
  const id = await resolveSessionId(query);
1173
1279
  const state = await loadSession(id);
1174
1280
  const target = await resolveExportTarget(destination, state);
1175
- await writeFile3(target, renderSessionMarkdown(state), {
1281
+ await writeFile4(target, renderSessionMarkdown(state), {
1176
1282
  encoding: "utf8",
1177
1283
  mode: 384,
1178
1284
  flag: "wx"
@@ -1186,7 +1292,7 @@ async function exportSession(query, destination) {
1186
1292
  }
1187
1293
  async function latestSessionFor(cwd) {
1188
1294
  for (const file of await filesByRecency()) {
1189
- const state = readState(await readFile3(file, "utf8").catch(() => ""));
1295
+ const state = readState(await readFile4(file, "utf8").catch(() => ""));
1190
1296
  if (state?.cwd === cwd) return state;
1191
1297
  }
1192
1298
  return null;
@@ -1216,7 +1322,7 @@ async function filesByRecency() {
1216
1322
  }
1217
1323
  const dated = await Promise.all(
1218
1324
  names.filter((name) => name.endsWith(".json")).map(async (name) => {
1219
- const file = path4.join(sessionsDir, name);
1325
+ const file = path5.join(sessionsDir, name);
1220
1326
  const info = await stat2(file).catch(() => null);
1221
1327
  return { file, at: info?.mtimeMs ?? 0, size: info?.size ?? 0 };
1222
1328
  })
@@ -1296,14 +1402,14 @@ function readUsageEntries(value) {
1296
1402
  });
1297
1403
  }
1298
1404
  async function resolveExportTarget(destination, state) {
1299
- const resolved = path4.resolve(destination);
1405
+ const resolved = path5.resolve(destination);
1300
1406
  const info = await stat2(resolved).catch(() => null);
1301
1407
  if (info?.isDirectory()) {
1302
- return path4.join(resolved, exportFileName(state));
1408
+ return path5.join(resolved, exportFileName(state));
1303
1409
  }
1304
1410
  if (info) throw new Error(`Export destination is not a directory: ${resolved}`);
1305
- const parent = await stat2(path4.dirname(resolved)).catch(() => null);
1306
- if (!parent?.isDirectory()) throw new Error(`Export directory does not exist: ${path4.dirname(resolved)}`);
1411
+ const parent = await stat2(path5.dirname(resolved)).catch(() => null);
1412
+ if (!parent?.isDirectory()) throw new Error(`Export directory does not exist: ${path5.dirname(resolved)}`);
1307
1413
  return resolved;
1308
1414
  }
1309
1415
  function exportFileName(state) {
@@ -1430,6 +1536,10 @@ async function runTurn(cfg, history, hooks, signal) {
1430
1536
  let pauses = 0;
1431
1537
  let steps = 0;
1432
1538
  for (; ; ) {
1539
+ if (signal.aborted) {
1540
+ hooks.onEvent({ type: "turn_end", stopReason: "aborted" });
1541
+ return sanitizeHistory(messages);
1542
+ }
1433
1543
  if (++steps > MAX_TURN_STEPS) {
1434
1544
  hooks.onEvent({
1435
1545
  type: "notice",
@@ -1516,12 +1626,16 @@ async function consumeStream(cfg, messages, hooks, signal, maxTokens) {
1516
1626
  signal
1517
1627
  };
1518
1628
  let outcome;
1629
+ let text = "";
1630
+ let thinking = "";
1519
1631
  for await (const event of streamWithRetry(cfg.provider, request, cfg.modelRef, signal)) {
1520
1632
  switch (event.type) {
1521
1633
  case "text_delta":
1634
+ text += event.text;
1522
1635
  hooks.onEvent({ type: "text_delta", text: event.text });
1523
1636
  break;
1524
1637
  case "thinking_delta":
1638
+ thinking += event.text;
1525
1639
  hooks.onEvent({ type: "thinking_delta", text: event.text });
1526
1640
  break;
1527
1641
  case "usage":
@@ -1540,7 +1654,12 @@ async function consumeStream(cfg, messages, hooks, signal, maxTokens) {
1540
1654
  }
1541
1655
  }
1542
1656
  if (outcome) return outcome;
1543
- if (signal.aborted) return { content: [], stopReason: "aborted" };
1657
+ if (signal.aborted) {
1658
+ const content = [];
1659
+ if (thinking) content.push({ type: "thinking", text: thinking });
1660
+ if (text) content.push({ type: "text", text });
1661
+ return { content, stopReason: "aborted" };
1662
+ }
1544
1663
  throw new Error(
1545
1664
  `"${cfg.provider.id}" ended the stream without completing the turn \u2014 nothing was generated. The endpoint answered, but not with a usable ${cfg.provider.kind} response.`
1546
1665
  );
@@ -1550,8 +1669,29 @@ async function* streamWithRetry(provider, request, modelRef, signal) {
1550
1669
  for (let attempt = 0; attempt <= MAX_RETRIES2; attempt++) {
1551
1670
  if (signal.aborted) return;
1552
1671
  try {
1553
- for await (const event of provider.stream(request)) {
1554
- yield event;
1672
+ const stream = provider.stream(request)[Symbol.asyncIterator]();
1673
+ let cancel;
1674
+ let cancelTimer;
1675
+ const cancelled = new Promise((resolve3) => {
1676
+ cancel = () => {
1677
+ cancelTimer ??= setTimeout(() => resolve3({ done: true, value: void 0 }), 100);
1678
+ };
1679
+ signal.addEventListener("abort", cancel, { once: true });
1680
+ if (signal.aborted) cancel();
1681
+ });
1682
+ try {
1683
+ for (; ; ) {
1684
+ const next = await Promise.race([stream.next(), cancelled]);
1685
+ if (next.done) break;
1686
+ if (!signal.aborted || next.value.type === "usage" || next.value.type === "done") {
1687
+ yield next.value;
1688
+ }
1689
+ if (next.value.type === "done") break;
1690
+ }
1691
+ } finally {
1692
+ signal.removeEventListener("abort", cancel);
1693
+ if (cancelTimer !== void 0) clearTimeout(cancelTimer);
1694
+ void stream.return?.().catch(() => void 0);
1555
1695
  }
1556
1696
  return;
1557
1697
  } catch (error) {
@@ -1833,20 +1973,20 @@ function finitePositive(value) {
1833
1973
  }
1834
1974
 
1835
1975
  // src/core/checkpoint.ts
1836
- import { createHash, randomBytes as randomBytes2 } from "crypto";
1976
+ import { createHash as createHash2, randomBytes as randomBytes2 } from "crypto";
1837
1977
  import { createReadStream } from "fs";
1838
1978
  import {
1839
1979
  chmod as chmod3,
1840
1980
  lstat,
1841
1981
  mkdir as mkdir3,
1842
- readFile as readFile4,
1982
+ readFile as readFile5,
1843
1983
  readdir as readdir2,
1844
1984
  rename as rename4,
1845
1985
  stat as stat3,
1846
1986
  unlink as unlink2,
1847
- writeFile as writeFile4
1987
+ writeFile as writeFile5
1848
1988
  } from "fs/promises";
1849
- import path5 from "path";
1989
+ import path6 from "path";
1850
1990
 
1851
1991
  // src/tools/safepath.ts
1852
1992
  import { realpathSync } from "fs";
@@ -1941,7 +2081,7 @@ function beginCheckpoint(options) {
1941
2081
  },
1942
2082
  markChanged(absolutePath) {
1943
2083
  if (finished) return;
1944
- const resolved = path5.resolve(absolutePath);
2084
+ const resolved = path6.resolve(absolutePath);
1945
2085
  let capturedEntry = [...captured.values()].find((entry) => entry.absolutePath === resolved);
1946
2086
  if (!capturedEntry) {
1947
2087
  const safe = resolveInside(root, absolutePath);
@@ -1972,12 +2112,12 @@ function beginCheckpoint(options) {
1972
2112
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
1973
2113
  entries
1974
2114
  };
1975
- const sessionDir = path5.join(storageDir, options.sessionId);
2115
+ const sessionDir = path6.join(storageDir, options.sessionId);
1976
2116
  await ensureDir(storageDir);
1977
2117
  await ensureDir(sessionDir);
1978
2118
  lastCheckpointTimestamp = Math.max(Date.now(), lastCheckpointTimestamp + 1);
1979
2119
  const id = `${String(lastCheckpointTimestamp).padStart(13, "0")}-${randomBytes2(4).toString("hex")}`;
1980
- await writeCheckpoint(path5.join(sessionDir, `${id}.json`), stored);
2120
+ await writeCheckpoint(path6.join(sessionDir, `${id}.json`), stored);
1981
2121
  pruneCheckpoints(sessionDir).catch(() => void 0);
1982
2122
  return { id, paths: entries.map((entry) => entry.path) };
1983
2123
  }
@@ -1986,7 +2126,7 @@ function beginCheckpoint(options) {
1986
2126
  async function undoLatestCheckpoint(options) {
1987
2127
  assertSessionId2(options.sessionId);
1988
2128
  const root = workspaceRoot(options.cwd);
1989
- const sessionDir = path5.join(options.storageDir ?? checkpointsDir, options.sessionId);
2129
+ const sessionDir = path6.join(options.storageDir ?? checkpointsDir, options.sessionId);
1990
2130
  const file = await newestCheckpoint(sessionDir);
1991
2131
  const empty = { found: false, restored: [], removed: [], conflicts: [], failed: [] };
1992
2132
  if (!file) return empty;
@@ -2042,7 +2182,7 @@ async function snapshotFile(file) {
2042
2182
  if (info.size > MAX_FILE_BYTES) {
2043
2183
  throw new Error(`Cannot checkpoint ${file}: it exceeds the ${MAX_FILE_BYTES / 1e6} MB limit.`);
2044
2184
  }
2045
- const data = await readFile4(file);
2185
+ const data = await readFile5(file);
2046
2186
  if (data.length > MAX_FILE_BYTES) {
2047
2187
  throw new Error(`Cannot checkpoint ${file}: it changed beyond the file-size limit while reading.`);
2048
2188
  }
@@ -2059,7 +2199,7 @@ async function fingerprint(file) {
2059
2199
  if (error.code === "ENOENT") return { existed: false };
2060
2200
  throw error;
2061
2201
  }
2062
- const hash = createHash("sha256");
2202
+ const hash = createHash2("sha256");
2063
2203
  if (!info.isFile()) {
2064
2204
  hash.update(`kitcode:${info.isDirectory() ? "directory" : "non-file"}`);
2065
2205
  return { existed: true, sha256: hash.digest("hex") };
@@ -2071,7 +2211,7 @@ function matchesSnapshot(before, after) {
2071
2211
  if (before.existed !== after.existed) return false;
2072
2212
  if (!before.existed) return true;
2073
2213
  const data = Buffer.from(before.data ?? "", "base64");
2074
- return after.sha256 === createHash("sha256").update(data).digest("hex");
2214
+ return after.sha256 === createHash2("sha256").update(data).digest("hex");
2075
2215
  }
2076
2216
  function sameFingerprint(a, b) {
2077
2217
  return a.existed === b.existed && (!a.existed || a.sha256 === b.sha256);
@@ -2080,14 +2220,14 @@ async function restoreFile(root, relativePath, snapshot, expected) {
2080
2220
  const safe = resolveInside(root, relativePath);
2081
2221
  if (!safe.ok || safe.relative !== relativePath) throw new Error("The restore path changed.");
2082
2222
  const data = decodeSnapshot(snapshot);
2083
- await mkdir3(path5.dirname(safe.path), { recursive: true });
2223
+ await mkdir3(path6.dirname(safe.path), { recursive: true });
2084
2224
  const rechecked = resolveInside(root, relativePath);
2085
2225
  if (!rechecked.ok || rechecked.path !== safe.path || rechecked.relative !== relativePath) {
2086
2226
  throw new Error("The restore path changed while preparing its parent directory.");
2087
2227
  }
2088
2228
  const temp = `${rechecked.path}.${process.pid}.${randomBytes2(4).toString("hex")}.undo`;
2089
2229
  try {
2090
- await writeFile4(temp, data, { mode: snapshot.mode ?? 384 });
2230
+ await writeFile5(temp, data, { mode: snapshot.mode ?? 384 });
2091
2231
  await chmod3(temp, snapshot.mode ?? 384);
2092
2232
  const finalPath = resolveInside(root, relativePath);
2093
2233
  if (!finalPath.ok || finalPath.path !== rechecked.path || finalPath.relative !== relativePath) {
@@ -2110,7 +2250,7 @@ async function writeCheckpoint(file, checkpoint) {
2110
2250
  }
2111
2251
  const temp = `${file}.${process.pid}.${randomBytes2(4).toString("hex")}.tmp`;
2112
2252
  try {
2113
- await writeFile4(temp, body, { encoding: "utf8", mode: 384 });
2253
+ await writeFile5(temp, body, { encoding: "utf8", mode: 384 });
2114
2254
  await chmod3(temp, 384);
2115
2255
  await rename4(temp, file);
2116
2256
  } catch (error) {
@@ -2123,7 +2263,7 @@ async function readCheckpoint(file) {
2123
2263
  if (info.size > MAX_CHECKPOINT_JSON_BYTES) throw new Error("Undo checkpoint is too large to read.");
2124
2264
  let parsed;
2125
2265
  try {
2126
- parsed = JSON.parse(await readFile4(file, "utf8"));
2266
+ parsed = JSON.parse(await readFile5(file, "utf8"));
2127
2267
  } catch (error) {
2128
2268
  throw new Error(`Undo checkpoint is not valid JSON: ${error.message}`);
2129
2269
  }
@@ -2139,7 +2279,7 @@ function isCheckpoint(value) {
2139
2279
  return item.entries.every((entry) => {
2140
2280
  if (typeof entry !== "object" || entry === null) return false;
2141
2281
  const candidate = entry;
2142
- if (typeof candidate.path !== "string" || candidate.path === "" || path5.isAbsolute(candidate.path) || typeof candidate.before !== "object" || candidate.before === null || typeof candidate.after !== "object" || candidate.after === null || typeof candidate.before.existed !== "boolean" || typeof candidate.after.existed !== "boolean") {
2282
+ if (typeof candidate.path !== "string" || candidate.path === "" || path6.isAbsolute(candidate.path) || typeof candidate.before !== "object" || candidate.before === null || typeof candidate.after !== "object" || candidate.after === null || typeof candidate.before.existed !== "boolean" || typeof candidate.after.existed !== "boolean") {
2143
2283
  return false;
2144
2284
  }
2145
2285
  if (candidate.before.existed) {
@@ -2171,12 +2311,12 @@ async function newestCheckpoint(sessionDir) {
2171
2311
  throw error;
2172
2312
  }
2173
2313
  const name = names.filter((candidate) => CHECKPOINT_NAME.test(candidate)).sort().at(-1);
2174
- return name ? path5.join(sessionDir, name) : null;
2314
+ return name ? path6.join(sessionDir, name) : null;
2175
2315
  }
2176
2316
  async function pruneCheckpoints(sessionDir) {
2177
2317
  const names = (await readdir2(sessionDir)).filter((candidate) => CHECKPOINT_NAME.test(candidate)).sort().reverse();
2178
2318
  await Promise.all(
2179
- names.slice(MAX_CHECKPOINTS_PER_SESSION).map((name) => unlink2(path5.join(sessionDir, name)))
2319
+ names.slice(MAX_CHECKPOINTS_PER_SESSION).map((name) => unlink2(path6.join(sessionDir, name)))
2180
2320
  );
2181
2321
  }
2182
2322
  function workspaceRoot(cwd) {
@@ -2191,19 +2331,11 @@ function assertSessionId2(id) {
2191
2331
  }
2192
2332
 
2193
2333
  // src/core/diagnostics.ts
2194
- import { readFile as readFile5, stat as stat4 } from "fs/promises";
2195
- import path6 from "path";
2334
+ import { readFile as readFile6, stat as stat4 } from "fs/promises";
2335
+ import path7 from "path";
2196
2336
 
2197
2337
  // src/tools/bash.ts
2198
2338
  import { spawn } from "child_process";
2199
-
2200
- // src/tools/summary.ts
2201
- function brief(value, max = 60) {
2202
- const text = String(value ?? "").replace(/\s+/g, " ").trim();
2203
- return text.length > max ? `${text.slice(0, max - 1)}\u2026` : text;
2204
- }
2205
-
2206
- // src/tools/bash.ts
2207
2339
  var DEFAULT_TIMEOUT = 12e4;
2208
2340
  var MAX_TIMEOUT = 6e5;
2209
2341
  var MAX_OUTPUT = 1e5;
@@ -2343,7 +2475,7 @@ async function detectDiagnosticCommands(cwd, configured = []) {
2343
2475
  const custom = configured.map((command) => command.trim()).filter(Boolean);
2344
2476
  if (custom.length > 0) return custom.slice(0, 8);
2345
2477
  const commands = [];
2346
- const packageJson = await readPackageJson(path6.join(cwd, "package.json"));
2478
+ const packageJson = await readPackageJson(path7.join(cwd, "package.json"));
2347
2479
  if (packageJson) {
2348
2480
  const manager = await packageManager(cwd, packageJson.packageManager);
2349
2481
  for (const name of ["lint", "typecheck", "check", "test"]) {
@@ -2352,11 +2484,11 @@ async function detectDiagnosticCommands(cwd, configured = []) {
2352
2484
  }
2353
2485
  }
2354
2486
  }
2355
- if (await isFile2(path6.join(cwd, "Cargo.toml"))) {
2487
+ if (await isFile2(path7.join(cwd, "Cargo.toml"))) {
2356
2488
  commands.push("cargo check", "cargo test");
2357
2489
  }
2358
- if (await isFile2(path6.join(cwd, "go.mod"))) commands.push("go test ./...");
2359
- if (await isFile2(path6.join(cwd, "pyproject.toml")) || await isFile2(path6.join(cwd, "pytest.ini")) || await isFile2(path6.join(cwd, "tox.ini"))) {
2490
+ if (await isFile2(path7.join(cwd, "go.mod"))) commands.push("go test ./...");
2491
+ if (await isFile2(path7.join(cwd, "pyproject.toml")) || await isFile2(path7.join(cwd, "pytest.ini")) || await isFile2(path7.join(cwd, "tox.ini"))) {
2360
2492
  commands.push("python -m pytest");
2361
2493
  }
2362
2494
  return [...new Set(commands)].slice(0, 8);
@@ -2446,7 +2578,7 @@ async function readPackageJson(file) {
2446
2578
  try {
2447
2579
  const info = await stat4(file);
2448
2580
  if (!info.isFile() || info.size > MAX_PACKAGE_BYTES) return null;
2449
- const value = JSON.parse(await readFile5(file, "utf8"));
2581
+ const value = JSON.parse(await readFile6(file, "utf8"));
2450
2582
  if (typeof value !== "object" || value === null) return null;
2451
2583
  const candidate = value;
2452
2584
  return {
@@ -2460,9 +2592,9 @@ async function readPackageJson(file) {
2460
2592
  async function packageManager(cwd, declared) {
2461
2593
  const name = declared?.split("@")[0];
2462
2594
  if (name === "pnpm" || name === "yarn" || name === "bun" || name === "npm") return name;
2463
- if (await isFile2(path6.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
2464
- if (await isFile2(path6.join(cwd, "yarn.lock"))) return "yarn";
2465
- if (await isFile2(path6.join(cwd, "bun.lock")) || await isFile2(path6.join(cwd, "bun.lockb"))) {
2595
+ if (await isFile2(path7.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
2596
+ if (await isFile2(path7.join(cwd, "yarn.lock"))) return "yarn";
2597
+ if (await isFile2(path7.join(cwd, "bun.lock")) || await isFile2(path7.join(cwd, "bun.lockb"))) {
2466
2598
  return "bun";
2467
2599
  }
2468
2600
  return "npm";
@@ -2480,9 +2612,9 @@ async function isFile2(file) {
2480
2612
 
2481
2613
  // src/core/attachments.ts
2482
2614
  import { execFile } from "child_process";
2483
- import { lstat as lstat2, readdir as readdir3, readFile as readFile6, stat as stat5 } from "fs/promises";
2615
+ import { lstat as lstat2, readdir as readdir3, readFile as readFile7, stat as stat5 } from "fs/promises";
2484
2616
  import os2 from "os";
2485
- import path7 from "path";
2617
+ import path8 from "path";
2486
2618
  import { fileURLToPath } from "url";
2487
2619
  var MAX_IMAGE_BYTES = 10 * 1024 * 1024;
2488
2620
  var MAX_TEXT_BYTES = 256 * 1024;
@@ -2513,7 +2645,7 @@ async function loadAttachment(cwd, requestedPath) {
2513
2645
  const resolved = resolveAttachmentPath(cwd, requestedPath);
2514
2646
  const linkInfo = await lstat2(resolved).catch(() => null);
2515
2647
  if (linkInfo?.isSymbolicLink()) {
2516
- throw new Error(`Cannot attach symbolic links: ${path7.basename(resolved)}`);
2648
+ throw new Error(`Cannot attach symbolic links: ${path8.basename(resolved)}`);
2517
2649
  }
2518
2650
  const info = await stat5(resolved).catch((error) => {
2519
2651
  if (error.code === "ENOENT") throw new Error(`Attachment not found: ${resolved}`);
@@ -2522,7 +2654,7 @@ async function loadAttachment(cwd, requestedPath) {
2522
2654
  if (info.isDirectory()) {
2523
2655
  const attachment = await loadFirstImageFromDirectory(resolved);
2524
2656
  if (attachment) return attachment;
2525
- throw new Error(`No supported images found in directory: ${path7.basename(resolved)}`);
2657
+ throw new Error(`No supported images found in directory: ${path8.basename(resolved)}`);
2526
2658
  }
2527
2659
  if (!info.isFile()) throw new Error(`Attachment is not a regular file: ${resolved}`);
2528
2660
  return loadResolvedAttachment(resolved, info.size);
@@ -2532,7 +2664,7 @@ async function loadAutomaticAttachment(cwd, requestedPath) {
2532
2664
  const resolved = resolveAttachmentPath(cwd, requestedPath);
2533
2665
  if (isSensitiveAutomaticPath(resolved)) {
2534
2666
  throw new Error(
2535
- `For safety, sensitive-looking files must be attached explicitly with /attach: ${path7.basename(resolved)}`
2667
+ `For safety, sensitive-looking files must be attached explicitly with /attach: ${path8.basename(resolved)}`
2536
2668
  );
2537
2669
  }
2538
2670
  const linkInfo = await lstat2(resolved).catch(() => null);
@@ -2552,14 +2684,14 @@ function looksLikeAttachmentPath(value) {
2552
2684
  if (trimmed.length > MAX_PATH_CHARS) return false;
2553
2685
  const candidate = normalizeInputPath(trimmed);
2554
2686
  if (/^file:\/\//i.test(candidate)) return true;
2555
- if (path7.isAbsolute(candidate)) return true;
2687
+ if (path8.isAbsolute(candidate)) return true;
2556
2688
  if (/^(?:~|\.{1,2})[\\/]/.test(candidate)) return true;
2557
2689
  if (candidate.includes("/") || candidate.includes("\\")) {
2558
2690
  if (trimmed.length > 200) return false;
2559
2691
  return true;
2560
2692
  }
2561
- const basename3 = path7.basename(candidate).toLowerCase();
2562
- return path7.extname(basename3) !== "" || AUTO_PATH_NAMES.has(basename3);
2693
+ const basename3 = path8.basename(candidate).toLowerCase();
2694
+ return path8.extname(basename3) !== "" || AUTO_PATH_NAMES.has(basename3);
2563
2695
  }
2564
2696
  async function loadClipboardImage(platform = process.platform, runner = runClipboardCommand) {
2565
2697
  const commands = clipboardCommands(platform);
@@ -2591,12 +2723,12 @@ async function loadClipboardImage(platform = process.platform, runner = runClipb
2591
2723
  throw new Error(`No supported image found in the clipboard. ${hint}`);
2592
2724
  }
2593
2725
  async function loadResolvedAttachment(resolved, size) {
2594
- const name = safeName(path7.basename(resolved));
2726
+ const name = safeName(path8.basename(resolved));
2595
2727
  const imageLimitCandidate = size <= MAX_IMAGE_BYTES;
2596
2728
  if (!imageLimitCandidate) {
2597
2729
  throw new Error(`Attachment is larger than ${formatBytes(MAX_IMAGE_BYTES)}: ${name}`);
2598
2730
  }
2599
- const data = await readFile6(resolved);
2731
+ const data = await readFile7(resolved);
2600
2732
  const mediaType = detectImage(data);
2601
2733
  if (mediaType) {
2602
2734
  return {
@@ -2625,10 +2757,10 @@ async function loadFirstImageFromDirectory(dirPath) {
2625
2757
  const IMAGE_EXTENSIONS = /* @__PURE__ */ new Set([".png", ".jpg", ".jpeg", ".gif", ".webp", ".bmp"]);
2626
2758
  const entries = await readdir3(dirPath, { withFileTypes: true }).catch(() => null);
2627
2759
  if (!entries) return null;
2628
- const sorted = entries.filter((entry) => entry.isFile() && IMAGE_EXTENSIONS.has(path7.extname(entry.name).toLowerCase())).sort((a, b) => a.name.localeCompare(b.name));
2760
+ const sorted = entries.filter((entry) => entry.isFile() && IMAGE_EXTENSIONS.has(path8.extname(entry.name).toLowerCase())).sort((a, b) => a.name.localeCompare(b.name));
2629
2761
  if (sorted.length === 0) return null;
2630
2762
  const first2 = sorted[0];
2631
- const filePath = path7.join(dirPath, first2.name);
2763
+ const filePath = path8.join(dirPath, first2.name);
2632
2764
  const info = await stat5(filePath).catch(() => null);
2633
2765
  if (!info?.isFile()) return null;
2634
2766
  return loadResolvedAttachment(filePath, info.size);
@@ -2638,17 +2770,17 @@ function resolveAttachmentPath(cwd, requestedPath) {
2638
2770
  if (!cleaned) throw new Error("Give a file path: /attach <path>");
2639
2771
  if (/^file:\/\//i.test(cleaned)) {
2640
2772
  try {
2641
- return path7.normalize(fileURLToPath(cleaned));
2773
+ return path8.normalize(fileURLToPath(cleaned));
2642
2774
  } catch {
2643
2775
  throw new Error(`Invalid local file URL: ${cleaned}`);
2644
2776
  }
2645
2777
  }
2646
- const expanded = cleaned === "~" ? os2.homedir() : cleaned.startsWith("~/") ? path7.join(os2.homedir(), cleaned.slice(2)) : cleaned;
2647
- return path7.isAbsolute(expanded) ? path7.normalize(expanded) : path7.resolve(cwd, expanded);
2778
+ const expanded = cleaned === "~" ? os2.homedir() : cleaned.startsWith("~/") ? path8.join(os2.homedir(), cleaned.slice(2)) : cleaned;
2779
+ return path8.isAbsolute(expanded) ? path8.normalize(expanded) : path8.resolve(cwd, expanded);
2648
2780
  }
2649
2781
  function isSensitiveAutomaticPath(file) {
2650
- const basename3 = path7.basename(file).toLowerCase();
2651
- return basename3.startsWith(".env.") || SENSITIVE_AUTO_NAMES.has(basename3) || SENSITIVE_AUTO_EXTENSIONS.has(path7.extname(basename3));
2782
+ const basename3 = path8.basename(file).toLowerCase();
2783
+ return basename3.startsWith(".env.") || SENSITIVE_AUTO_NAMES.has(basename3) || SENSITIVE_AUTO_EXTENSIONS.has(path8.extname(basename3));
2652
2784
  }
2653
2785
  function attachmentLabel(block) {
2654
2786
  if (block.type === "image") return `image: ${block.name}`;
@@ -2793,7 +2925,7 @@ try {
2793
2925
  }
2794
2926
  `;
2795
2927
  function textMime(file) {
2796
- const extension = path7.extname(file).toLowerCase();
2928
+ const extension = path8.extname(file).toLowerCase();
2797
2929
  const known = {
2798
2930
  ".json": "application/json",
2799
2931
  ".md": "text/markdown",
@@ -2846,8 +2978,8 @@ var SINGLE_MAX_TOKENS = 768;
2846
2978
  var CHUNK_MAX_TOKENS = 512;
2847
2979
  var MERGE_MAX_TOKENS = 1024;
2848
2980
  var CHUNK_CONCURRENCY = 3;
2849
- 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.";
2850
- 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.";
2981
+ 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. Use these sections: Requirements; Verified facts (with file paths or tool evidence); Changes made; Verification (exact commands, pass/fail/not run, and unresolved error excerpts); Decisions and assumptions; Remaining work. Keep plans and assumptions separate from verified facts. Never turn an intended command into a completed check. When checks conflict, retain the latest result and mark earlier results as superseded. Preserve explicit user corrections and project constraints. Do not retain credentials or secret values. Return only the summary.";
2982
+ 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. Keep the structured sections and evidence from the source summaries. Separate verified facts, assumptions, and planned work; keep exact verification commands and their latest outcomes. Do not resolve conflicting evidence by guessing. Do not retain credentials or secret values. Be terse. Return only the merged summary.";
2851
2983
  async function compactHistory(options) {
2852
2984
  const cut = compactCutIndex(options.history);
2853
2985
  if (cut <= 0) {
@@ -3249,6 +3381,12 @@ function buildSystemPrompt(args) {
3249
3381
  if (args.skills && args.skills.trim() !== "") {
3250
3382
  lines.push("", args.skills);
3251
3383
  }
3384
+ if (args.toolNames.includes("memory")) {
3385
+ lines.push("", "Use memory to retain explicit user preferences, verified project facts and accepted decisions across chats. Include the source. Read existing notes before replacing or deleting them; correct stale notes rather than adding contradictions. Keep temporary progress in the conversation, not permanent memory. Never store credentials. Tell the user briefly when you update memory.");
3386
+ }
3387
+ if (args.memory?.trim()) {
3388
+ lines.push("", "Project notes saved in earlier conversations. Treat these as potentially stale context; verify claims against current files and follow current user instructions.", args.memory);
3389
+ }
3252
3390
  return lines.join("\n");
3253
3391
  }
3254
3392
 
@@ -3499,7 +3637,7 @@ function clip(value) {
3499
3637
  // package.json
3500
3638
  var package_default = {
3501
3639
  name: "@kernelonpanic/kitcode",
3502
- version: "1.2.8",
3640
+ version: "1.3.0",
3503
3641
  description: "Terminal coding agent with a config you never have to write by hand",
3504
3642
  type: "module",
3505
3643
  license: "MIT",
@@ -3567,7 +3705,7 @@ var package_default = {
3567
3705
 
3568
3706
  // src/version.ts
3569
3707
  var KITCODE_VERSION = package_default.version;
3570
- var KITCODE_COMMIT = true ? "422e80dd7cd81a1c20f186b1e3ab36d6e0779b89" : "development";
3708
+ var KITCODE_COMMIT = true ? "7cd983ca3dc89406326e5e513870686036c3ab9c" : "development";
3571
3709
 
3572
3710
  // src/mcp/client.ts
3573
3711
  var clientInfo = { name: "kitcode", version: KITCODE_VERSION };
@@ -3858,8 +3996,8 @@ function balanceEndpoints(baseUrl) {
3858
3996
  endpoint(url.origin, "/v1/balance", genericBalance)
3859
3997
  ];
3860
3998
  }
3861
- function endpoint(origin, path14, parse2) {
3862
- return { url: `${origin}${path14}`, parse: parse2 };
3999
+ function endpoint(origin, path15, parse2) {
4000
+ return { url: `${origin}${path15}`, parse: parse2 };
3863
4001
  }
3864
4002
  function deepSeekBalance(body) {
3865
4003
  const infos = record(body)?.["balance_infos"];
@@ -3952,8 +4090,8 @@ async function limitedResponseText(response, limit) {
3952
4090
  }
3953
4091
 
3954
4092
  // src/providers/catalog.ts
3955
- import { chmod as chmod4, readFile as readFile7, writeFile as writeFile5 } from "fs/promises";
3956
- import path8 from "path";
4093
+ import { chmod as chmod4, readFile as readFile8, writeFile as writeFile6 } from "fs/promises";
4094
+ import path9 from "path";
3957
4095
  var MAX_AGE_MS = 24 * 60 * 60 * 1e3;
3958
4096
  var CACHE_VERSION = 3;
3959
4097
  async function loadModels(provider, refresh = false) {
@@ -3975,11 +4113,11 @@ async function loadModels(provider, refresh = false) {
3975
4113
  return models;
3976
4114
  }
3977
4115
  function cacheFile(providerId) {
3978
- return path8.join(cacheDir, "models", `${providerId.replace(/[^\w.-]/g, "_")}.json`);
4116
+ return path9.join(cacheDir, "models", `${providerId.replace(/[^\w.-]/g, "_")}.json`);
3979
4117
  }
3980
4118
  async function readCache(file) {
3981
4119
  try {
3982
- const parsed = JSON.parse(await readFile7(file, "utf8"));
4120
+ const parsed = JSON.parse(await readFile8(file, "utf8"));
3983
4121
  if (typeof parsed !== "object" || parsed === null) return void 0;
3984
4122
  const candidate = parsed;
3985
4123
  if (typeof candidate.fetchedAt !== "number" || !Number.isFinite(candidate.fetchedAt) || !Array.isArray(candidate.models) || candidate.models.length > 1e4 || !candidate.models.every(validModel)) {
@@ -4010,8 +4148,8 @@ function validModel(value) {
4010
4148
  async function writeCache(file, models) {
4011
4149
  const payload = { version: CACHE_VERSION, fetchedAt: Date.now(), models };
4012
4150
  try {
4013
- await ensureDir(path8.dirname(file));
4014
- await writeFile5(file, JSON.stringify(payload), { encoding: "utf8", mode: 384 });
4151
+ await ensureDir(path9.dirname(file));
4152
+ await writeFile6(file, JSON.stringify(payload), { encoding: "utf8", mode: 384 });
4015
4153
  await chmod4(file, 384);
4016
4154
  } catch {
4017
4155
  return;
@@ -4160,7 +4298,8 @@ async function* streamTurn(client, providerId, apiKey2, req) {
4160
4298
  tools: toToolParams(req.tools)
4161
4299
  };
4162
4300
  if (req.thinking) params.thinking = { type: "adaptive", display: "summarized" };
4163
- if (req.effort) params.output_config = { effort: req.effort };
4301
+ const effort = resolveEffort(req.effort, req.messages);
4302
+ if (effort) params.output_config = { effort };
4164
4303
  let thinking = "";
4165
4304
  let text = "";
4166
4305
  let events = 0;
@@ -4348,11 +4487,13 @@ async function* streamTurn2(client, providerId, apiKey2, req) {
4348
4487
  let recognised = 0;
4349
4488
  const calls = /* @__PURE__ */ new Map();
4350
4489
  const capture = captureResponseHead();
4490
+ const effort = openAiEffort(req.model, resolveEffort(req.effort, req.messages));
4351
4491
  try {
4352
4492
  const stream = await client.withOptions({ fetch: capture.fetch }).chat.completions.create(
4353
4493
  {
4354
4494
  model: req.model,
4355
4495
  max_tokens: req.maxTokens,
4496
+ ...effort ? { reasoning_effort: effort } : {},
4356
4497
  messages: toChatMessages(req.system, req.messages),
4357
4498
  ...req.tools.length > 0 ? { tools: toChatTools(req.tools) } : {},
4358
4499
  stream: true,
@@ -4649,11 +4790,11 @@ function isFileEdit(tool) {
4649
4790
  }
4650
4791
 
4651
4792
  // src/tools/edit.ts
4652
- import { readFile as readFile8, stat as stat6 } from "fs/promises";
4793
+ import { readFile as readFile9, stat as stat6 } from "fs/promises";
4653
4794
 
4654
4795
  // src/tools/safe-write.ts
4655
4796
  import { randomUUID as randomUUID3 } from "crypto";
4656
- import { chmod as chmod5, lstat as lstat3, open, rename as rename5, rm as rm3 } from "fs/promises";
4797
+ import { chmod as chmod5, lstat as lstat3, open, rename as rename5, rm as rm4 } from "fs/promises";
4657
4798
  import { basename as basename2, dirname as dirname2, join } from "path";
4658
4799
  var UnsafeFileChangeError = class extends Error {
4659
4800
  constructor(message) {
@@ -4716,7 +4857,7 @@ async function atomicWriteSafeFile(file, data, snapshot) {
4716
4857
  await rename5(temp, file);
4717
4858
  } catch (error) {
4718
4859
  await handle?.close().catch(() => void 0);
4719
- await rm3(temp, { force: true }).catch(() => void 0);
4860
+ await rm4(temp, { force: true }).catch(() => void 0);
4720
4861
  throw error;
4721
4862
  }
4722
4863
  }
@@ -4811,56 +4952,56 @@ var editTool = {
4811
4952
  return `edit(${brief(input.path)})`;
4812
4953
  },
4813
4954
  async preview(input, ctx) {
4814
- const { path: path14, oldString, newString, replaceAll = false } = input;
4815
- const safe = resolveInside(ctx.cwd, path14);
4955
+ const { path: path15, oldString, newString, replaceAll = false } = input;
4956
+ const safe = resolveInside(ctx.cwd, path15);
4816
4957
  if (!safe.ok) return { kind: "text", text: safe.reason };
4817
4958
  const info = await stat6(safe.path).catch(() => null);
4818
4959
  if (info && info.size > MAX_FILE_BYTES2) {
4819
- return { kind: "text", text: `Cannot preview ${path14}: the file is too large.` };
4960
+ return { kind: "text", text: `Cannot preview ${path15}: the file is too large.` };
4820
4961
  }
4821
- const beforeBuffer = await readFile8(safe.path).catch(() => null);
4822
- if (!beforeBuffer) return { kind: "text", text: `Cannot read ${path14}.` };
4962
+ const beforeBuffer = await readFile9(safe.path).catch(() => null);
4963
+ if (!beforeBuffer) return { kind: "text", text: `Cannot read ${path15}.` };
4823
4964
  if (beforeBuffer.subarray(0, 8192).includes(0)) {
4824
- return { kind: "text", text: `Cannot preview binary file ${path14}` };
4965
+ return { kind: "text", text: `Cannot preview binary file ${path15}` };
4825
4966
  }
4826
4967
  const before = beforeBuffer.toString("utf8");
4827
4968
  const after = replacement(before, oldString, newString, replaceAll);
4828
- return after === null ? { kind: "text", text: `Cannot preview edit: oldString is not a valid match in ${path14}.` } : { kind: "diff", path: path14, before, after };
4969
+ return after === null ? { kind: "text", text: `Cannot preview edit: oldString is not a valid match in ${path15}.` } : { kind: "diff", path: path15, before, after };
4829
4970
  },
4830
4971
  async execute(input, ctx) {
4831
- const { path: path14, oldString, newString, replaceAll = false } = input;
4832
- const safe = resolveInside(ctx.cwd, path14);
4972
+ const { path: path15, oldString, newString, replaceAll = false } = input;
4973
+ const safe = resolveInside(ctx.cwd, path15);
4833
4974
  if (!safe.ok) return { content: safe.reason, isError: true };
4834
4975
  let snapshot;
4835
4976
  try {
4836
4977
  snapshot = await readSafeFileSnapshot(safe.path, MAX_FILE_BYTES2);
4837
4978
  } catch (error) {
4838
- return { content: `Cannot edit ${path14}: ${error.message}.`, isError: true };
4979
+ return { content: `Cannot edit ${path15}: ${error.message}.`, isError: true };
4839
4980
  }
4840
- if (!snapshot.exists) return { content: `Cannot read ${path14}.`, isError: true };
4981
+ if (!snapshot.exists) return { content: `Cannot read ${path15}.`, isError: true };
4841
4982
  const beforeBuffer = snapshot.data;
4842
4983
  if (beforeBuffer.subarray(0, 8192).includes(0)) {
4843
- return { content: `Cannot edit ${path14}: it is a binary file, not text.`, isError: true };
4984
+ return { content: `Cannot edit ${path15}: it is a binary file, not text.`, isError: true };
4844
4985
  }
4845
4986
  const before = beforeBuffer.toString("utf8");
4846
4987
  const count = countOccurrences(before, oldString);
4847
4988
  if (count === 0) {
4848
4989
  return {
4849
- content: `oldString was not found in ${path14}. Read the file again and match the text exactly, including whitespace.`,
4990
+ content: `oldString was not found in ${path15}. Read the file again and match the text exactly, including whitespace.`,
4850
4991
  isError: true
4851
4992
  };
4852
4993
  }
4853
4994
  if (count > 1 && !replaceAll) {
4854
4995
  return {
4855
- content: `oldString matches ${count} places in ${path14}. Add more surrounding context to identify a single one, or set replaceAll to true.`,
4996
+ content: `oldString matches ${count} places in ${path15}. Add more surrounding context to identify a single one, or set replaceAll to true.`,
4856
4997
  isError: true
4857
4998
  };
4858
4999
  }
4859
5000
  const after = replacement(before, oldString, newString, replaceAll);
4860
- if (after === null) return { content: `Could not prepare edit for ${path14}.`, isError: true };
5001
+ if (after === null) return { content: `Could not prepare edit for ${path15}.`, isError: true };
4861
5002
  if (Buffer.byteLength(after, "utf8") > MAX_FILE_BYTES2) {
4862
5003
  return {
4863
- content: `Cannot edit ${path14}: the result exceeds the ${MAX_FILE_BYTES2 / 1e6} MB limit.`,
5004
+ content: `Cannot edit ${path15}: the result exceeds the ${MAX_FILE_BYTES2 / 1e6} MB limit.`,
4864
5005
  isError: true
4865
5006
  };
4866
5007
  }
@@ -4873,11 +5014,11 @@ var editTool = {
4873
5014
  await atomicWriteSafeFile(safe.path, after, snapshot);
4874
5015
  ctx.checkpoint?.markChanged(safe.path);
4875
5016
  } catch (error) {
4876
- return { content: `Failed to write ${path14}: ${error.message}`, isError: true };
5017
+ return { content: `Failed to write ${path15}: ${error.message}`, isError: true };
4877
5018
  }
4878
5019
  return {
4879
- content: `Edited ${path14} (${count} ${count === 1 ? "replacement" : "replacements"})`,
4880
- display: { kind: "diff", path: path14, before, after }
5020
+ content: `Edited ${path15} (${count} ${count === 1 ? "replacement" : "replacements"})`,
5021
+ display: { kind: "diff", path: path15, before, after }
4881
5022
  };
4882
5023
  }
4883
5024
  };
@@ -4912,11 +5053,11 @@ var globTool = {
4912
5053
  return `glob(${brief(input.pattern)})`;
4913
5054
  },
4914
5055
  async execute(input, ctx) {
4915
- const { pattern, path: path14 = "." } = input;
5056
+ const { pattern, path: path15 = "." } = input;
4916
5057
  if (patternEscapes(pattern)) {
4917
5058
  return { content: `Pattern ${pattern} must be relative to the workspace root.`, isError: true };
4918
5059
  }
4919
- const safe = resolveInside(ctx.cwd, path14);
5060
+ const safe = resolveInside(ctx.cwd, path15);
4920
5061
  if (!safe.ok) return { content: safe.reason, isError: true };
4921
5062
  const entries = await fg(pattern, {
4922
5063
  cwd: safe.path,
@@ -4938,12 +5079,12 @@ var globTool = {
4938
5079
  };
4939
5080
 
4940
5081
  // src/tools/grep.ts
4941
- import { readFile as readFile9, stat as stat7 } from "fs/promises";
5082
+ import { readFile as readFile10, stat as stat7 } from "fs/promises";
4942
5083
  import { join as join3 } from "path";
4943
5084
  import fg2 from "fast-glob";
4944
5085
 
4945
5086
  // src/tools/sensitive.ts
4946
- import path9 from "path";
5087
+ import path10 from "path";
4947
5088
  var sensitiveNames = /* @__PURE__ */ new Set([
4948
5089
  ".npmrc",
4949
5090
  ".pypirc",
@@ -4970,9 +5111,9 @@ var sensitiveExtensions = /* @__PURE__ */ new Set([".pem", ".key", ".p12", ".pfx
4970
5111
  function isSensitivePath(value) {
4971
5112
  if (typeof value !== "string") return false;
4972
5113
  const normalized = value.replaceAll("\\", "/").toLowerCase();
4973
- const name = path9.posix.basename(normalized);
5114
+ const name = path10.posix.basename(normalized);
4974
5115
  if (name === ".env" || name.startsWith(".env.")) return true;
4975
- if (sensitiveNames.has(name) || sensitiveExtensions.has(path9.posix.extname(name))) return true;
5116
+ if (sensitiveNames.has(name) || sensitiveExtensions.has(path10.posix.extname(name))) return true;
4976
5117
  return normalized.split("/").some((part) => part === ".ssh" || part === ".aws");
4977
5118
  }
4978
5119
  function mentionsSensitivePattern(value) {
@@ -5090,16 +5231,16 @@ var grepTool = {
5090
5231
  defaultPermission: "allow",
5091
5232
  readOnly: true,
5092
5233
  permission(input, ctx) {
5093
- const { path: path14, glob } = input ?? {};
5094
- if (isSensitivePath(path14) || mentionsSensitivePattern(glob)) return "ask";
5095
- const safe = ctx && path14 ? resolveInside(ctx.cwd, path14) : void 0;
5234
+ const { path: path15, glob } = input ?? {};
5235
+ if (isSensitivePath(path15) || mentionsSensitivePattern(glob)) return "ask";
5236
+ const safe = ctx && path15 ? resolveInside(ctx.cwd, path15) : void 0;
5096
5237
  return safe?.ok && isSensitivePath(safe.relative) ? "ask" : void 0;
5097
5238
  },
5098
5239
  summarize(input) {
5099
5240
  return `grep(${brief(input.pattern)})`;
5100
5241
  },
5101
5242
  async execute(input, ctx) {
5102
- const { pattern, path: path14 = ".", glob = "**/*", maxMatches } = input;
5243
+ const { pattern, path: path15 = ".", glob = "**/*", maxMatches } = input;
5103
5244
  if (pattern.length > MAX_PATTERN_CHARS) {
5104
5245
  return {
5105
5246
  content: `Regular expression is limited to ${MAX_PATTERN_CHARS} characters.`,
@@ -5114,10 +5255,10 @@ var grepTool = {
5114
5255
  if (patternEscapes(glob)) {
5115
5256
  return { content: `Glob ${glob} must be relative to the workspace root.`, isError: true };
5116
5257
  }
5117
- const safe = resolveInside(ctx.cwd, path14);
5258
+ const safe = resolveInside(ctx.cwd, path15);
5118
5259
  if (!safe.ok) return { content: safe.reason, isError: true };
5119
5260
  const target = await stat7(safe.path).catch(() => null);
5120
- if (!target) return { content: `Path not found: ${path14}`, isError: true };
5261
+ if (!target) return { content: `Path not found: ${path15}`, isError: true };
5121
5262
  const discovered = target.isDirectory() ? (await fg2(glob, {
5122
5263
  cwd: safe.path,
5123
5264
  dot: false,
@@ -5148,7 +5289,7 @@ var grepTool = {
5148
5289
  break;
5149
5290
  }
5150
5291
  scannedBytes += file.size;
5151
- const buffer = await readFile9(file.absolute).catch(() => null);
5292
+ const buffer = await readFile10(file.absolute).catch(() => null);
5152
5293
  if (!buffer || buffer.subarray(0, 4096).includes(0)) continue;
5153
5294
  const lines = buffer.toString("utf8").split("\n");
5154
5295
  const remaining = limit - hits.length;
@@ -5178,7 +5319,7 @@ var grepTool = {
5178
5319
  };
5179
5320
 
5180
5321
  // src/tools/read.ts
5181
- import { readFile as readFile10, stat as stat8 } from "fs/promises";
5322
+ import { readFile as readFile11, stat as stat8 } from "fs/promises";
5182
5323
  var MAX_LINES = 2e3;
5183
5324
  var MAX_CHARS = 2e5;
5184
5325
  var MAX_FILE_BYTES4 = 5e6;
@@ -5207,30 +5348,30 @@ var readTool = {
5207
5348
  return `read(${brief(input.path)})`;
5208
5349
  },
5209
5350
  async execute(input, ctx) {
5210
- const { path: path14, offset = 1, limit } = input;
5211
- const safe = resolveInside(ctx.cwd, path14);
5351
+ const { path: path15, offset = 1, limit } = input;
5352
+ const safe = resolveInside(ctx.cwd, path15);
5212
5353
  if (!safe.ok) return { content: safe.reason, isError: true };
5213
5354
  const info = await stat8(safe.path).catch(() => null);
5214
- if (!info) return { content: `File not found: ${path14}`, isError: true };
5355
+ if (!info) return { content: `File not found: ${path15}`, isError: true };
5215
5356
  if (info.isDirectory()) {
5216
- return { content: `${path14} is a directory, not a file. Use glob to list its contents.`, isError: true };
5357
+ return { content: `${path15} is a directory, not a file. Use glob to list its contents.`, isError: true };
5217
5358
  }
5218
5359
  if (info.size > MAX_FILE_BYTES4) {
5219
5360
  return {
5220
- content: `${path14} is ${(info.size / 1e6).toFixed(1)} MB; read is limited to ${MAX_FILE_BYTES4 / 1e6} MB per file. Use grep or another targeted tool instead.`,
5361
+ content: `${path15} is ${(info.size / 1e6).toFixed(1)} MB; read is limited to ${MAX_FILE_BYTES4 / 1e6} MB per file. Use grep or another targeted tool instead.`,
5221
5362
  isError: true
5222
5363
  };
5223
5364
  }
5224
- const buffer = await readFile10(safe.path);
5365
+ const buffer = await readFile11(safe.path);
5225
5366
  if (buffer.subarray(0, 4096).includes(0)) {
5226
- return { content: `${path14} looks like a binary file and cannot be read as text.`, isError: true };
5367
+ return { content: `${path15} looks like a binary file and cannot be read as text.`, isError: true };
5227
5368
  }
5228
5369
  const lines = buffer.toString("utf8").split("\n");
5229
5370
  if (lines.at(-1) === "") lines.pop();
5230
- if (lines.length === 0) return { content: `${path14} is empty.` };
5371
+ if (lines.length === 0) return { content: `${path15} is empty.` };
5231
5372
  const start = Math.max(1, offset) - 1;
5232
5373
  if (start >= lines.length) {
5233
- return { content: `${path14} has ${lines.length} lines, so offset ${offset} is past the end.`, isError: true };
5374
+ return { content: `${path15} has ${lines.length} lines, so offset ${offset} is past the end.`, isError: true };
5234
5375
  }
5235
5376
  const window = lines.slice(start, start + Math.min(limit ?? MAX_LINES, MAX_LINES));
5236
5377
  const rendered = [];
@@ -5257,7 +5398,7 @@ function toToolSchema(tool) {
5257
5398
  }
5258
5399
 
5259
5400
  // src/tools/write.ts
5260
- import { mkdir as mkdir4, readFile as readFile11, stat as stat9 } from "fs/promises";
5401
+ import { mkdir as mkdir4, readFile as readFile12, stat as stat9 } from "fs/promises";
5261
5402
  import { dirname as dirname3 } from "path";
5262
5403
  var MAX_FILE_BYTES5 = 5e6;
5263
5404
  var writeTool = {
@@ -5277,64 +5418,64 @@ var writeTool = {
5277
5418
  return `write(${brief(input.path)})`;
5278
5419
  },
5279
5420
  async preview(input, ctx) {
5280
- const { path: path14, content } = input;
5421
+ const { path: path15, content } = input;
5281
5422
  if (Buffer.byteLength(content, "utf8") > MAX_FILE_BYTES5) {
5282
5423
  return { kind: "text", text: `Write is limited to ${MAX_FILE_BYTES5 / 1e6} MB.` };
5283
5424
  }
5284
- const safe = resolveInside(ctx.cwd, path14);
5425
+ const safe = resolveInside(ctx.cwd, path15);
5285
5426
  if (!safe.ok) return { kind: "text", text: safe.reason };
5286
5427
  const beforeInfo = await stat9(safe.path).catch(() => null);
5287
5428
  if (beforeInfo && beforeInfo.size > MAX_FILE_BYTES5) {
5288
- return { kind: "text", text: `Cannot preview ${path14}: the existing file is too large.` };
5429
+ return { kind: "text", text: `Cannot preview ${path15}: the existing file is too large.` };
5289
5430
  }
5290
- const beforeBuffer = await readFile11(safe.path).catch(() => null);
5431
+ const beforeBuffer = await readFile12(safe.path).catch(() => null);
5291
5432
  if (beforeBuffer?.subarray(0, 8192).includes(0)) {
5292
- return { kind: "text", text: `Cannot preview binary file ${path14}` };
5433
+ return { kind: "text", text: `Cannot preview binary file ${path15}` };
5293
5434
  }
5294
5435
  return {
5295
5436
  kind: "diff",
5296
- path: path14,
5437
+ path: path15,
5297
5438
  before: beforeBuffer?.toString("utf8") ?? "",
5298
5439
  after: content
5299
5440
  };
5300
5441
  },
5301
5442
  async execute(input, ctx) {
5302
- const { path: path14, content } = input;
5443
+ const { path: path15, content } = input;
5303
5444
  if (Buffer.byteLength(content, "utf8") > MAX_FILE_BYTES5) {
5304
5445
  return {
5305
- content: `Cannot write ${path14}: content exceeds the ${MAX_FILE_BYTES5 / 1e6} MB limit.`,
5446
+ content: `Cannot write ${path15}: content exceeds the ${MAX_FILE_BYTES5 / 1e6} MB limit.`,
5306
5447
  isError: true
5307
5448
  };
5308
5449
  }
5309
- const safe = resolveInside(ctx.cwd, path14);
5450
+ const safe = resolveInside(ctx.cwd, path15);
5310
5451
  if (!safe.ok) return { content: safe.reason, isError: true };
5311
5452
  let target = safe.path;
5312
5453
  let snapshot;
5313
5454
  try {
5314
5455
  await mkdir4(dirname3(target), { recursive: true });
5315
- const rechecked = resolveInside(ctx.cwd, path14);
5456
+ const rechecked = resolveInside(ctx.cwd, path15);
5316
5457
  if (!rechecked.ok || rechecked.path !== target) {
5317
5458
  return {
5318
- content: `Cannot write ${path14}: the path changed while its parent was created.`,
5459
+ content: `Cannot write ${path15}: the path changed while its parent was created.`,
5319
5460
  isError: true
5320
5461
  };
5321
5462
  }
5322
5463
  target = rechecked.path;
5323
5464
  snapshot = await readSafeFileSnapshot(target, MAX_FILE_BYTES5);
5324
5465
  if (snapshot.exists && snapshot.data.subarray(0, 8192).includes(0)) {
5325
- return { content: `Cannot write ${path14}: the existing file is binary, not text.`, isError: true };
5466
+ return { content: `Cannot write ${path15}: the existing file is binary, not text.`, isError: true };
5326
5467
  }
5327
5468
  await ctx.checkpoint?.capture(target);
5328
5469
  await atomicWriteSafeFile(target, content, snapshot);
5329
5470
  ctx.checkpoint?.markChanged(target);
5330
5471
  } catch (error) {
5331
- return { content: `Failed to write ${path14}: ${error.message}`, isError: true };
5472
+ return { content: `Failed to write ${path15}: ${error.message}`, isError: true };
5332
5473
  }
5333
5474
  const before = snapshot.exists ? snapshot.data.toString("utf8") : "";
5334
5475
  const lines = content === "" ? 0 : content.replace(/\n$/, "").split("\n").length;
5335
5476
  return {
5336
- content: `${before === "" ? "Created" : "Updated"} ${path14} (${lines} ${lines === 1 ? "line" : "lines"})`,
5337
- display: { kind: "diff", path: path14, before, after: content }
5477
+ content: `${before === "" ? "Created" : "Updated"} ${path15} (${lines} ${lines === 1 ? "line" : "lines"})`,
5478
+ display: { kind: "diff", path: path15, before, after: content }
5338
5479
  };
5339
5480
  }
5340
5481
  };
@@ -5363,8 +5504,8 @@ function createToolRegistry(tools) {
5363
5504
  }
5364
5505
 
5365
5506
  // src/prompts/library.ts
5366
- import { chmod as chmod6, readFile as readFile12, readdir as readdir4, unlink as unlink3, writeFile as writeFile6 } from "fs/promises";
5367
- import path10 from "path";
5507
+ import { chmod as chmod6, readFile as readFile13, readdir as readdir4, unlink as unlink3, writeFile as writeFile7 } from "fs/promises";
5508
+ import path11 from "path";
5368
5509
  async function savePrompt(input) {
5369
5510
  const slug = slugify(input.name);
5370
5511
  if (!slug) throw new Error(`Prompt name has no letters or digits to name a file after: ${input.name}`);
@@ -5376,8 +5517,8 @@ async function savePrompt(input) {
5376
5517
  body: input.body.trim()
5377
5518
  };
5378
5519
  await ensureDir(promptsDir);
5379
- const file = path10.join(promptsDir, `${slug}.md`);
5380
- await writeFile6(file, serialize(prompt), {
5520
+ const file = path11.join(promptsDir, `${slug}.md`);
5521
+ await writeFile7(file, serialize(prompt), {
5381
5522
  encoding: "utf8",
5382
5523
  mode: 384
5383
5524
  });
@@ -5387,7 +5528,7 @@ async function savePrompt(input) {
5387
5528
  async function getPrompt(slug) {
5388
5529
  assertSlug(slug);
5389
5530
  try {
5390
- return parse(slug, await readFile12(path10.join(promptsDir, `${slug}.md`), "utf8"));
5531
+ return parse(slug, await readFile13(path11.join(promptsDir, `${slug}.md`), "utf8"));
5391
5532
  } catch (error) {
5392
5533
  if (isMissing(error)) return null;
5393
5534
  throw error;
@@ -5403,7 +5544,7 @@ async function listPrompts() {
5403
5544
  }
5404
5545
  const prompts2 = await Promise.all(
5405
5546
  names.filter((name) => name.endsWith(".md")).map(
5406
- async (name) => parse(name.slice(0, -3), await readFile12(path10.join(promptsDir, name), "utf8"))
5547
+ async (name) => parse(name.slice(0, -3), await readFile13(path11.join(promptsDir, name), "utf8"))
5407
5548
  )
5408
5549
  );
5409
5550
  return prompts2.sort((a, b) => a.name.localeCompare(b.name));
@@ -5411,7 +5552,7 @@ async function listPrompts() {
5411
5552
  async function deletePrompt(slug) {
5412
5553
  assertSlug(slug);
5413
5554
  try {
5414
- await unlink3(path10.join(promptsDir, `${slug}.md`));
5555
+ await unlink3(path11.join(promptsDir, `${slug}.md`));
5415
5556
  return true;
5416
5557
  } catch (error) {
5417
5558
  if (isMissing(error)) return false;
@@ -5469,7 +5610,7 @@ function parse(slug, text) {
5469
5610
 
5470
5611
  // src/skills/library.ts
5471
5612
  import { lstat as lstat4, open as open2, readdir as readdir5 } from "fs/promises";
5472
- import path11 from "path";
5613
+ import path12 from "path";
5473
5614
  var SKILL_FILE = "SKILL.md";
5474
5615
  var FRONTMATTER_BYTES = 8192;
5475
5616
  var MAX_SKILL_BYTES = 5e6;
@@ -5482,7 +5623,7 @@ async function discoverSkills(dirs) {
5482
5623
  if (!rootInfo?.isDirectory() || rootInfo.isSymbolicLink()) continue;
5483
5624
  const entries = await readdir5(root, { withFileTypes: true }).catch(() => []);
5484
5625
  const found = await Promise.all(
5485
- entries.filter((entry) => entry.isDirectory() && !entry.isSymbolicLink()).slice(0, MAX_SKILLS_PER_ROOT).map((entry) => readMeta(root, rootInfo, path11.join(root, entry.name)))
5626
+ entries.filter((entry) => entry.isDirectory() && !entry.isSymbolicLink()).slice(0, MAX_SKILLS_PER_ROOT).map((entry) => readMeta(root, rootInfo, path12.join(root, entry.name)))
5486
5627
  );
5487
5628
  for (const meta of found) {
5488
5629
  if (meta && !byName.has(meta.name)) byName.set(meta.name, meta);
@@ -5522,11 +5663,11 @@ function formatSkillCatalogue(skills) {
5522
5663
  async function readMeta(root, rootInfo, dir) {
5523
5664
  const dirInfo = await lstat4(dir).catch(() => null);
5524
5665
  if (!dirInfo?.isDirectory() || dirInfo.isSymbolicLink()) return null;
5525
- const file = path11.join(dir, SKILL_FILE);
5666
+ const file = path12.join(dir, SKILL_FILE);
5526
5667
  const head = await readFrontmatterBytes(file);
5527
5668
  if (!head) return null;
5528
5669
  const { fields } = parseFrontmatter(head.text);
5529
- const meta = { name: fields.name || path11.basename(dir), description: fields.description ?? "", dir, file };
5670
+ const meta = { name: fields.name || path12.basename(dir), description: fields.description ?? "", dir, file };
5530
5671
  guards.set(meta, {
5531
5672
  root,
5532
5673
  rootIdentity: identity(rootInfo),
@@ -5622,8 +5763,8 @@ import {
5622
5763
  writeFileSync
5623
5764
  } from "fs";
5624
5765
  import { chmod as chmod7, mkdir as mkdir5 } from "fs/promises";
5625
- import path12 from "path";
5626
- var TMP_DIR = path12.join(skillsDir, ".tmp");
5766
+ import path13 from "path";
5767
+ var TMP_DIR = path13.join(skillsDir, ".tmp");
5627
5768
  var MAX_SKILL_BYTES2 = 5e6;
5628
5769
  async function installSkill(source) {
5629
5770
  await ensureDir(skillsDir);
@@ -5648,7 +5789,7 @@ function isNpmPackage(input) {
5648
5789
  }
5649
5790
  async function installFromGitHub(url) {
5650
5791
  const { owner, repo, subdir, branch } = parseGitHubUrl(url);
5651
- const name = safeSkillName(subdir ? path12.posix.basename(subdir) : repo);
5792
+ const name = safeSkillName(subdir ? path13.posix.basename(subdir) : repo);
5652
5793
  const skillDir = safeChildPath(skillsDir, name);
5653
5794
  const tmpDir = safeChildPath(TMP_DIR, `${name}-${randomUUID4()}`);
5654
5795
  try {
@@ -5683,16 +5824,16 @@ async function installFromNpm(packageName) {
5683
5824
  if (!tarball) {
5684
5825
  throw new Error(`Could not download npm package "${packageName}"`);
5685
5826
  }
5686
- const tarballPath = path12.join(tmpDir, tarball);
5827
+ const tarballPath = path13.join(tmpDir, tarball);
5687
5828
  validateTarball(tarballPath);
5688
- const extractDir = path12.join(tmpDir, "extracted");
5829
+ const extractDir = path13.join(tmpDir, "extracted");
5689
5830
  mkdirSync(extractDir, { recursive: true });
5690
5831
  try {
5691
5832
  execFileSync("tar", ["-xzf", tarballPath, "-C", extractDir], { stdio: "ignore" });
5692
5833
  } catch {
5693
- const packageDir = path12.join(tmpDir, "node_modules", packageName);
5834
+ const packageDir = path13.join(tmpDir, "node_modules", packageName);
5694
5835
  if (existsSync2(packageDir)) {
5695
- const skillFile2 = path12.join(packageDir, "SKILL.md");
5836
+ const skillFile2 = path13.join(packageDir, "SKILL.md");
5696
5837
  if (existsSync2(skillFile2)) {
5697
5838
  const body2 = readSkillFile(skillFile2, packageDir, packageName);
5698
5839
  await writeSkillFile(skillDir, body2);
@@ -5704,7 +5845,7 @@ async function installFromNpm(packageName) {
5704
5845
  const extractedFiles = readdirSync(extractDir);
5705
5846
  const packageRoot = extractedFiles.find((f) => f === "package");
5706
5847
  if (!packageRoot) throw new Error("Could not find package root in extracted files");
5707
- const skillFile = path12.join(extractDir, "package", "SKILL.md");
5848
+ const skillFile = path13.join(extractDir, "package", "SKILL.md");
5708
5849
  if (!existsSync2(skillFile)) {
5709
5850
  throw new Error(`No SKILL.md found in npm package "${packageName}"`);
5710
5851
  }
@@ -5738,26 +5879,26 @@ function validateTarball(file) {
5738
5879
  }
5739
5880
  for (const entry of entries) {
5740
5881
  const normalized = entry.replace(/^\.\//, "");
5741
- if (normalized.includes("\\") || path12.posix.isAbsolute(normalized) || normalized.split("/").some((segment) => segment === "..")) {
5882
+ if (normalized.includes("\\") || path13.posix.isAbsolute(normalized) || normalized.split("/").some((segment) => segment === "..")) {
5742
5883
  throw new Error(`Unsafe path in npm skill archive: ${entry}`);
5743
5884
  }
5744
5885
  }
5745
5886
  }
5746
5887
  async function installFromLocal(srcPath) {
5747
- const resolved = path12.resolve(srcPath);
5888
+ const resolved = path13.resolve(srcPath);
5748
5889
  let skillDir;
5749
5890
  let skillFile;
5750
- const directSkill = path12.join(resolved, "SKILL.md");
5891
+ const directSkill = path13.join(resolved, "SKILL.md");
5751
5892
  if (existsSync2(directSkill)) {
5752
5893
  skillDir = resolved;
5753
5894
  skillFile = directSkill;
5754
5895
  } else if (resolved.endsWith("SKILL.md") && existsSync2(resolved)) {
5755
- skillDir = path12.dirname(resolved);
5896
+ skillDir = path13.dirname(resolved);
5756
5897
  skillFile = resolved;
5757
5898
  } else {
5758
5899
  throw new Error(`No SKILL.md found at "${srcPath}"`);
5759
5900
  }
5760
- const name = path12.basename(skillDir);
5901
+ const name = path13.basename(skillDir);
5761
5902
  const destDir = safeChildPath(skillsDir, safeSkillName(name));
5762
5903
  const body = readSkillFile(skillFile, skillDir, srcPath);
5763
5904
  await writeSkillFile(destDir, body);
@@ -5770,7 +5911,7 @@ async function writeSkillFile(dir, body) {
5770
5911
  throw new Error(`Refusing to replace symlinked skill directory: ${dir}`);
5771
5912
  }
5772
5913
  await mkdir5(dir, { recursive: true, mode: 448 });
5773
- const file = path12.join(dir, "SKILL.md");
5914
+ const file = path13.join(dir, "SKILL.md");
5774
5915
  const existingFile = lstatSync(file, { throwIfNoEntry: false });
5775
5916
  if (existingFile?.isSymbolicLink()) {
5776
5917
  throw new Error(`Refusing to replace symlinked skill file: ${file}`);
@@ -5823,13 +5964,13 @@ function safeSkillName(value) {
5823
5964
  return value;
5824
5965
  }
5825
5966
  function safeChildPath(root, ...segments) {
5826
- const target = path12.resolve(root, ...segments);
5967
+ const target = path13.resolve(root, ...segments);
5827
5968
  assertInside(root, target);
5828
5969
  return target;
5829
5970
  }
5830
5971
  function assertInside(root, target) {
5831
- const relative2 = path12.relative(path12.resolve(root), path12.resolve(target));
5832
- if (relative2 === "" || !path12.isAbsolute(relative2) && relative2 !== ".." && !relative2.startsWith(`..${path12.sep}`)) {
5972
+ const relative2 = path13.relative(path13.resolve(root), path13.resolve(target));
5973
+ if (relative2 === "" || !path13.isAbsolute(relative2) && relative2 !== ".." && !relative2.startsWith(`..${path13.sep}`)) {
5833
5974
  return;
5834
5975
  }
5835
5976
  throw new Error(`Path escapes the skills directory: ${target}`);
@@ -6155,10 +6296,17 @@ async function boot(options) {
6155
6296
  }
6156
6297
  const usage = createUsageTracker(session.usage, resolvePricing);
6157
6298
  let skillCatalogue = formatSkillCatalogue(skills);
6299
+ let projectMemory = await readProjectMemory(workspaceRoot2);
6300
+ const saveMemory = async (text) => {
6301
+ await saveProjectMemory(workspaceRoot2, text);
6302
+ projectMemory = await readProjectMemory(workspaceRoot2);
6303
+ };
6304
+ tools.register([createMemoryTool({ read: () => projectMemory, save: saveMemory })]);
6158
6305
  const mainSystemPrompt = () => buildSystemPrompt({
6159
6306
  cwd: options.cwd,
6160
6307
  toolNames: tools.list().map((tool) => tool.name),
6161
- skills: skillCatalogue
6308
+ skills: skillCatalogue,
6309
+ memory: projectMemory
6162
6310
  });
6163
6311
  let modelRef = session.model || config.model || "";
6164
6312
  if (options.modelRef) modelRef = options.modelRef;
@@ -6504,7 +6652,7 @@ async function boot(options) {
6504
6652
  },
6505
6653
  async exportSession(id, destination) {
6506
6654
  await persistQueue.catch(() => void 0);
6507
- const target = destination ? path13.resolve(options.cwd, destination) : path13.join(options.cwd, ".kitcode-exports");
6655
+ const target = destination ? path14.resolve(options.cwd, destination) : path14.join(options.cwd, ".kitcode-exports");
6508
6656
  if (!destination) await mkdir6(target, { recursive: true, mode: 448 });
6509
6657
  return (await exportSession(id, target)).path;
6510
6658
  },
@@ -6519,6 +6667,21 @@ async function boot(options) {
6519
6667
  });
6520
6668
  void refreshModelContextWindow();
6521
6669
  },
6670
+ readMemory: () => projectMemory,
6671
+ async saveMemory(text) {
6672
+ await saveMemory(text);
6673
+ },
6674
+ async clearMemory() {
6675
+ await clearProjectMemory(workspaceRoot2);
6676
+ projectMemory = "";
6677
+ },
6678
+ effortDescription(history) {
6679
+ if (!modelRef) return "unavailable";
6680
+ const { provider, modelId } = registry.resolve(modelRef);
6681
+ const requested = resolveEffort(config.effort, history);
6682
+ const effective = provider.kind === "openai" ? openAiEffort(modelId, requested) : requested;
6683
+ return effective ? `${config.effort} \u2192 ${effective} (API request)` : `${config.effort} \u2192 not sent: unknown model support`;
6684
+ },
6522
6685
  getEffort: () => config.effort,
6523
6686
  async setEffort(effort) {
6524
6687
  await persistConfig((draft) => {
@@ -7067,6 +7230,10 @@ function validateKey(value) {
7067
7230
  // src/app/tui.tsx
7068
7231
  import { render } from "ink";
7069
7232
 
7233
+ // src/ui/App.tsx
7234
+ import { Box as Box13, Text as Text12, useApp, useInput as useInput2, useStdout as useStdout2 } from "ink";
7235
+ import { useCallback, useEffect as useEffect2, useMemo as useMemo5, useRef as useRef4, useState as useState5 } from "react";
7236
+
7070
7237
  // src/ui/terminal-size.ts
7071
7238
  import { useStdout } from "ink";
7072
7239
  import { useMemo, useSyncExternalStore } from "react";
@@ -7107,10 +7274,6 @@ function useTerminalSize() {
7107
7274
  return useSyncExternalStore(store.subscribe, store.getSnapshot, store.getSnapshot);
7108
7275
  }
7109
7276
 
7110
- // src/ui/App.tsx
7111
- import { Box as Box13, Text as Text12, useApp, useInput as useInput2 } from "ink";
7112
- import { useCallback, useEffect as useEffect2, useMemo as useMemo5, useRef as useRef4, useState as useState5 } from "react";
7113
-
7114
7277
  // src/mcp/add.ts
7115
7278
  var SERVER_NAME = /^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$/;
7116
7279
  function parseMcpAddArgs(args) {
@@ -7161,6 +7324,7 @@ var COMMANDS = [
7161
7324
  { name: "usage" },
7162
7325
  { name: "mcp", args: "[add|list|delete|enable|disable]" },
7163
7326
  { name: "attach", args: "<path|clipboard|clear>" },
7327
+ { name: "memory", args: "[show|set <text>|clear]" },
7164
7328
  { name: "compact" },
7165
7329
  { name: "update" },
7166
7330
  { name: "checker" },
@@ -7354,7 +7518,7 @@ var en = {
7354
7518
  escHelp: "esc \u2014 cancel the running turn",
7355
7519
  cancelled: "Cancelled.",
7356
7520
  queued: (count) => `queued: ${count}`,
7357
- configAt: (path14) => `config: ${path14}`,
7521
+ configAt: (path15) => `config: ${path15}`,
7358
7522
  skillsEmpty: "No skills installed. Drop a folder with a SKILL.md into ~/.kitcode/skills or ./.kitcode/skills",
7359
7523
  skillsInstallUsage: "Usage: /skills install <github-url|npm-package|local-path>",
7360
7524
  skillsInstalling: (source) => `Installing skill from "${source}"\u2026`,
@@ -7400,7 +7564,7 @@ var en = {
7400
7564
  sessionDeleteAllFinalBody: "This cannot be undone. Press y only if you want to erase the complete session history.",
7401
7565
  sessionsDeletedAll: (count) => `All saved chats deleted \xB7 ${count} sessions removed.`,
7402
7566
  sessionsDeleteAllFailed: (deleted, failed) => `Session cleanup was incomplete \xB7 ${deleted} removed, ${failed} failed.`,
7403
- sessionExported: (path14) => `Session exported to ${path14}`,
7567
+ sessionExported: (path15) => `Session exported to ${path15}`,
7404
7568
  sessionActionResume: "resume",
7405
7569
  sessionActionRename: "rename",
7406
7570
  sessionActionDelete: "delete",
@@ -7470,6 +7634,7 @@ ${url}`,
7470
7634
  budget: "set token budget per turn",
7471
7635
  theme: "change the accent colour",
7472
7636
  lang: "change the interface language",
7637
+ memory: "view, replace or clear project memory",
7473
7638
  prompt: "insert or save a prompt",
7474
7639
  "prompt save": "save the current or recent text as a prompt",
7475
7640
  "prompt delete": "delete a saved prompt",
@@ -7542,7 +7707,7 @@ var ru = {
7542
7707
  escHelp: "esc \u2014 \u043E\u0442\u043C\u0435\u043D\u0438\u0442\u044C \u0442\u0435\u043A\u0443\u0449\u0438\u0439 \u0445\u043E\u0434",
7543
7708
  cancelled: "\u041E\u0442\u043C\u0435\u043D\u0435\u043D\u043E.",
7544
7709
  queued: (count) => `\u0432 \u043E\u0447\u0435\u0440\u0435\u0434\u0438: ${count}`,
7545
- configAt: (path14) => `\u043A\u043E\u043D\u0444\u0438\u0433: ${path14}`,
7710
+ configAt: (path15) => `\u043A\u043E\u043D\u0444\u0438\u0433: ${path15}`,
7546
7711
  skillsEmpty: "\u0421\u043A\u0438\u043B\u043B\u043E\u0432 \u043D\u0435\u0442. \u041F\u043E\u043B\u043E\u0436\u0438 \u043F\u0430\u043F\u043A\u0443 \u0441 \u0444\u0430\u0439\u043B\u043E\u043C SKILL.md \u0432 ~/.kitcode/skills \u0438\u043B\u0438 ./.kitcode/skills",
7547
7712
  skillsInstallUsage: "\u0418\u0441\u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u043D\u0438\u0435: /skills install <github-url|npm-\u043F\u0430\u043A\u0435\u0442|\u043B\u043E\u043A\u0430\u043B\u044C\u043D\u044B\u0439-\u043F\u0443\u0442\u044C>",
7548
7713
  skillsInstalling: (source) => `\u0423\u0441\u0442\u0430\u043D\u0430\u0432\u043B\u0438\u0432\u0430\u044E \u0441\u043A\u0438\u043B\u043B \u0438\u0437 "${source}"\u2026`,
@@ -7588,7 +7753,7 @@ var ru = {
7588
7753
  sessionDeleteAllFinalBody: "\u042D\u0442\u043E \u043D\u0435\u043B\u044C\u0437\u044F \u043E\u0442\u043C\u0435\u043D\u0438\u0442\u044C. \u041D\u0430\u0436\u0438\u043C\u0430\u0439 y \u0442\u043E\u043B\u044C\u043A\u043E \u0435\u0441\u043B\u0438 \u0445\u043E\u0447\u0435\u0448\u044C \u0441\u0442\u0435\u0440\u0435\u0442\u044C \u0432\u0441\u044E \u0438\u0441\u0442\u043E\u0440\u0438\u044E \u0441\u0435\u0441\u0441\u0438\u0439.",
7589
7754
  sessionsDeletedAll: (count) => `\u0412\u0441\u0435 \u0441\u043E\u0445\u0440\u0430\u043D\u0451\u043D\u043D\u044B\u0435 \u0447\u0430\u0442\u044B \u0443\u0434\u0430\u043B\u0435\u043D\u044B \xB7 \u0441\u0435\u0441\u0441\u0438\u0439: ${count}.`,
7590
7755
  sessionsDeleteAllFailed: (deleted, failed) => `\u0423\u0434\u0430\u043B\u0435\u043D\u0438\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043D\u043E \u043D\u0435 \u043F\u043E\u043B\u043D\u043E\u0441\u0442\u044C\u044E \xB7 \u0443\u0434\u0430\u043B\u0435\u043D\u043E: ${deleted}, \u043E\u0448\u0438\u0431\u043E\u043A: ${failed}.`,
7591
- sessionExported: (path14) => `\u0421\u0435\u0441\u0441\u0438\u044F \u044D\u043A\u0441\u043F\u043E\u0440\u0442\u0438\u0440\u043E\u0432\u0430\u043D\u0430: ${path14}`,
7756
+ sessionExported: (path15) => `\u0421\u0435\u0441\u0441\u0438\u044F \u044D\u043A\u0441\u043F\u043E\u0440\u0442\u0438\u0440\u043E\u0432\u0430\u043D\u0430: ${path15}`,
7592
7757
  sessionActionResume: "\u043E\u0442\u043A\u0440\u044B\u0442\u044C",
7593
7758
  sessionActionRename: "\u043F\u0435\u0440\u0435\u0438\u043C\u0435\u043D\u043E\u0432\u0430\u0442\u044C",
7594
7759
  sessionActionDelete: "\u0443\u0434\u0430\u043B\u0438\u0442\u044C",
@@ -7658,6 +7823,7 @@ ${url}`,
7658
7823
  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)",
7659
7824
  theme: "\u0441\u043C\u0435\u043D\u0438\u0442\u044C \u0446\u0432\u0435\u0442 \u0430\u043A\u0446\u0435\u043D\u0442\u0430",
7660
7825
  lang: "\u0441\u043C\u0435\u043D\u0438\u0442\u044C \u044F\u0437\u044B\u043A \u0438\u043D\u0442\u0435\u0440\u0444\u0435\u0439\u0441\u0430",
7826
+ memory: "\u043F\u043E\u043A\u0430\u0437\u0430\u0442\u044C, \u0437\u0430\u043C\u0435\u043D\u0438\u0442\u044C \u0438\u043B\u0438 \u043E\u0447\u0438\u0441\u0442\u0438\u0442\u044C \u043F\u0430\u043C\u044F\u0442\u044C \u043F\u0440\u043E\u0435\u043A\u0442\u0430",
7661
7827
  prompt: "\u0432\u0441\u0442\u0430\u0432\u0438\u0442\u044C \u0438\u043B\u0438 \u0441\u043E\u0445\u0440\u0430\u043D\u0438\u0442\u044C \u043F\u0440\u043E\u043C\u0442",
7662
7828
  skills: "\u0441\u043F\u0438\u0441\u043E\u043A \u0443\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D\u043D\u044B\u0445 \u0441\u043A\u0438\u043B\u043B\u043E\u0432",
7663
7829
  "prompt delete": "\u0443\u0434\u0430\u043B\u0438\u0442\u044C \u0441\u043E\u0445\u0440\u0430\u043D\u0451\u043D\u043D\u044B\u0439 \u043F\u0440\u043E\u043C\u0442",
@@ -8511,6 +8677,7 @@ function ModelBadge({ ref: modelRef }) {
8511
8677
  return /* @__PURE__ */ jsx9(Box9, { borderColor: theme.accent, borderStyle: "round", paddingX: 1, children: /* @__PURE__ */ jsx9(Text9, { color: theme.accent, bold: true, children: name }) });
8512
8678
  }
8513
8679
  var EFFORT_STYLES = {
8680
+ auto: { label: "auto", color: "cyan" },
8514
8681
  max: { label: "max", color: "red", bold: true },
8515
8682
  xhigh: { label: "xhigh", color: "yellow", bold: true },
8516
8683
  high: { label: "high", color: "cyan" },
@@ -9590,7 +9757,7 @@ function sanitizeDisplay(display) {
9590
9757
 
9591
9758
  // src/ui/App.tsx
9592
9759
  import { jsx as jsx13, jsxs as jsxs12 } from "react/jsx-runtime";
9593
- var EFFORTS = ["low", "medium", "high", "xhigh", "max"];
9760
+ var EFFORTS = ["auto", "low", "medium", "high", "xhigh", "max"];
9594
9761
  var STREAM_FRAME_MS = 50;
9595
9762
  var MAX_ATTACHMENTS = 8;
9596
9763
  function App({
@@ -9598,7 +9765,8 @@ function App({
9598
9765
  initialHistory,
9599
9766
  warnings = []
9600
9767
  }) {
9601
- const { exit } = useApp();
9768
+ const { exit, suspendTerminal } = useApp();
9769
+ const { stdout } = useStdout2();
9602
9770
  const { rows } = useTerminalSize();
9603
9771
  const [transcript, setTranscript] = useState5(
9604
9772
  () => warnings.reduce((state, text) => pushNotice(state, "warn", text), fromHistory(initialHistory))
@@ -9634,6 +9802,9 @@ function App({
9634
9802
  const [context, setContext] = useState5(() => runtime.modelContext());
9635
9803
  const transcriptEvents = useRef4([]);
9636
9804
  const transcriptTimer = useRef4(null);
9805
+ const clearScreen = useCallback(() => {
9806
+ if (stdout.isTTY) stdout.write("\x1B[2J\x1B[3J\x1B[H");
9807
+ }, [stdout]);
9637
9808
  const replaceAttachments = useCallback((next) => {
9638
9809
  attachmentsRef.current = next;
9639
9810
  setAttachments(next);
@@ -9801,12 +9972,20 @@ function App({
9801
9972
  exit();
9802
9973
  return;
9803
9974
  case "clear":
9975
+ if (busyRef.current && abort.current) {
9976
+ abort.current.abort();
9977
+ }
9978
+ queueRef.current = [];
9979
+ setPendingCount(0);
9804
9980
  await runtime.newSession();
9805
9981
  history.current = [];
9806
9982
  replaceAttachments([]);
9807
9983
  setPromptHistory([]);
9808
- setTranscript(emptyTranscript());
9809
- setTranscriptRevision((revision) => revision + 1);
9984
+ await suspendTerminal(async () => {
9985
+ setTranscript(emptyTranscript());
9986
+ setTranscriptRevision((revision) => revision + 1);
9987
+ clearScreen();
9988
+ });
9810
9989
  sessionStart.current = Date.now();
9811
9990
  turns.current = 0;
9812
9991
  void runtime.persist([]).catch(
@@ -10430,14 +10609,27 @@ ${strings.mcpAddUsage}`
10430
10609
  forceRender((n) => n + 1);
10431
10610
  return;
10432
10611
  }
10612
+ case "memory": {
10613
+ if (rawRest === "clear") {
10614
+ if (await ask2(lang === "ru" ? "\u041E\u0447\u0438\u0441\u0442\u0438\u0442\u044C \u043F\u0430\u043C\u044F\u0442\u044C \u043F\u0440\u043E\u0435\u043A\u0442\u0430?" : "Clear project memory?")) await runtime.clearMemory();
10615
+ } else if (rawRest.startsWith("set ")) {
10616
+ await runtime.saveMemory(rawRest.slice(4).trim());
10617
+ } else if (rawRest !== "" && rawRest !== "show") {
10618
+ notice("warn", "/memory show | /memory set <text> | /memory clear");
10619
+ return;
10620
+ }
10621
+ notice("info", runtime.readMemory() || (lang === "ru" ? "\u041F\u0430\u043C\u044F\u0442\u044C \u043F\u0440\u043E\u0435\u043A\u0442\u0430 \u043F\u0443\u0441\u0442\u0430." : "Project memory is empty."));
10622
+ return;
10623
+ }
10433
10624
  case "effort": {
10434
- const choice = await pick(
10625
+ notice("info", runtime.effortDescription(history.current));
10626
+ const choice = EFFORTS.includes(rawRest) ? rawRest : await pick(
10435
10627
  strings.titleEffort,
10436
10628
  EFFORTS.map((effort) => ({ key: effort, label: effort }))
10437
10629
  );
10438
10630
  if (!choice) return;
10439
10631
  await runtime.setEffort(choice);
10440
- notice("info", strings.effortSet(choice));
10632
+ notice("info", `${strings.effortSet(choice)} \xB7 ${runtime.effortDescription(history.current)}`);
10441
10633
  forceRender((n) => n + 1);
10442
10634
  return;
10443
10635
  }