@kernelonpanic/kitcode 1.2.9 → 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) {
@@ -1867,20 +1973,20 @@ function finitePositive(value) {
1867
1973
  }
1868
1974
 
1869
1975
  // src/core/checkpoint.ts
1870
- import { createHash, randomBytes as randomBytes2 } from "crypto";
1976
+ import { createHash as createHash2, randomBytes as randomBytes2 } from "crypto";
1871
1977
  import { createReadStream } from "fs";
1872
1978
  import {
1873
1979
  chmod as chmod3,
1874
1980
  lstat,
1875
1981
  mkdir as mkdir3,
1876
- readFile as readFile4,
1982
+ readFile as readFile5,
1877
1983
  readdir as readdir2,
1878
1984
  rename as rename4,
1879
1985
  stat as stat3,
1880
1986
  unlink as unlink2,
1881
- writeFile as writeFile4
1987
+ writeFile as writeFile5
1882
1988
  } from "fs/promises";
1883
- import path5 from "path";
1989
+ import path6 from "path";
1884
1990
 
1885
1991
  // src/tools/safepath.ts
1886
1992
  import { realpathSync } from "fs";
@@ -1975,7 +2081,7 @@ function beginCheckpoint(options) {
1975
2081
  },
1976
2082
  markChanged(absolutePath) {
1977
2083
  if (finished) return;
1978
- const resolved = path5.resolve(absolutePath);
2084
+ const resolved = path6.resolve(absolutePath);
1979
2085
  let capturedEntry = [...captured.values()].find((entry) => entry.absolutePath === resolved);
1980
2086
  if (!capturedEntry) {
1981
2087
  const safe = resolveInside(root, absolutePath);
@@ -2006,12 +2112,12 @@ function beginCheckpoint(options) {
2006
2112
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
2007
2113
  entries
2008
2114
  };
2009
- const sessionDir = path5.join(storageDir, options.sessionId);
2115
+ const sessionDir = path6.join(storageDir, options.sessionId);
2010
2116
  await ensureDir(storageDir);
2011
2117
  await ensureDir(sessionDir);
2012
2118
  lastCheckpointTimestamp = Math.max(Date.now(), lastCheckpointTimestamp + 1);
2013
2119
  const id = `${String(lastCheckpointTimestamp).padStart(13, "0")}-${randomBytes2(4).toString("hex")}`;
2014
- await writeCheckpoint(path5.join(sessionDir, `${id}.json`), stored);
2120
+ await writeCheckpoint(path6.join(sessionDir, `${id}.json`), stored);
2015
2121
  pruneCheckpoints(sessionDir).catch(() => void 0);
2016
2122
  return { id, paths: entries.map((entry) => entry.path) };
2017
2123
  }
@@ -2020,7 +2126,7 @@ function beginCheckpoint(options) {
2020
2126
  async function undoLatestCheckpoint(options) {
2021
2127
  assertSessionId2(options.sessionId);
2022
2128
  const root = workspaceRoot(options.cwd);
2023
- const sessionDir = path5.join(options.storageDir ?? checkpointsDir, options.sessionId);
2129
+ const sessionDir = path6.join(options.storageDir ?? checkpointsDir, options.sessionId);
2024
2130
  const file = await newestCheckpoint(sessionDir);
2025
2131
  const empty = { found: false, restored: [], removed: [], conflicts: [], failed: [] };
2026
2132
  if (!file) return empty;
@@ -2076,7 +2182,7 @@ async function snapshotFile(file) {
2076
2182
  if (info.size > MAX_FILE_BYTES) {
2077
2183
  throw new Error(`Cannot checkpoint ${file}: it exceeds the ${MAX_FILE_BYTES / 1e6} MB limit.`);
2078
2184
  }
2079
- const data = await readFile4(file);
2185
+ const data = await readFile5(file);
2080
2186
  if (data.length > MAX_FILE_BYTES) {
2081
2187
  throw new Error(`Cannot checkpoint ${file}: it changed beyond the file-size limit while reading.`);
2082
2188
  }
@@ -2093,7 +2199,7 @@ async function fingerprint(file) {
2093
2199
  if (error.code === "ENOENT") return { existed: false };
2094
2200
  throw error;
2095
2201
  }
2096
- const hash = createHash("sha256");
2202
+ const hash = createHash2("sha256");
2097
2203
  if (!info.isFile()) {
2098
2204
  hash.update(`kitcode:${info.isDirectory() ? "directory" : "non-file"}`);
2099
2205
  return { existed: true, sha256: hash.digest("hex") };
@@ -2105,7 +2211,7 @@ function matchesSnapshot(before, after) {
2105
2211
  if (before.existed !== after.existed) return false;
2106
2212
  if (!before.existed) return true;
2107
2213
  const data = Buffer.from(before.data ?? "", "base64");
2108
- return after.sha256 === createHash("sha256").update(data).digest("hex");
2214
+ return after.sha256 === createHash2("sha256").update(data).digest("hex");
2109
2215
  }
2110
2216
  function sameFingerprint(a, b) {
2111
2217
  return a.existed === b.existed && (!a.existed || a.sha256 === b.sha256);
@@ -2114,14 +2220,14 @@ async function restoreFile(root, relativePath, snapshot, expected) {
2114
2220
  const safe = resolveInside(root, relativePath);
2115
2221
  if (!safe.ok || safe.relative !== relativePath) throw new Error("The restore path changed.");
2116
2222
  const data = decodeSnapshot(snapshot);
2117
- await mkdir3(path5.dirname(safe.path), { recursive: true });
2223
+ await mkdir3(path6.dirname(safe.path), { recursive: true });
2118
2224
  const rechecked = resolveInside(root, relativePath);
2119
2225
  if (!rechecked.ok || rechecked.path !== safe.path || rechecked.relative !== relativePath) {
2120
2226
  throw new Error("The restore path changed while preparing its parent directory.");
2121
2227
  }
2122
2228
  const temp = `${rechecked.path}.${process.pid}.${randomBytes2(4).toString("hex")}.undo`;
2123
2229
  try {
2124
- await writeFile4(temp, data, { mode: snapshot.mode ?? 384 });
2230
+ await writeFile5(temp, data, { mode: snapshot.mode ?? 384 });
2125
2231
  await chmod3(temp, snapshot.mode ?? 384);
2126
2232
  const finalPath = resolveInside(root, relativePath);
2127
2233
  if (!finalPath.ok || finalPath.path !== rechecked.path || finalPath.relative !== relativePath) {
@@ -2144,7 +2250,7 @@ async function writeCheckpoint(file, checkpoint) {
2144
2250
  }
2145
2251
  const temp = `${file}.${process.pid}.${randomBytes2(4).toString("hex")}.tmp`;
2146
2252
  try {
2147
- await writeFile4(temp, body, { encoding: "utf8", mode: 384 });
2253
+ await writeFile5(temp, body, { encoding: "utf8", mode: 384 });
2148
2254
  await chmod3(temp, 384);
2149
2255
  await rename4(temp, file);
2150
2256
  } catch (error) {
@@ -2157,7 +2263,7 @@ async function readCheckpoint(file) {
2157
2263
  if (info.size > MAX_CHECKPOINT_JSON_BYTES) throw new Error("Undo checkpoint is too large to read.");
2158
2264
  let parsed;
2159
2265
  try {
2160
- parsed = JSON.parse(await readFile4(file, "utf8"));
2266
+ parsed = JSON.parse(await readFile5(file, "utf8"));
2161
2267
  } catch (error) {
2162
2268
  throw new Error(`Undo checkpoint is not valid JSON: ${error.message}`);
2163
2269
  }
@@ -2173,7 +2279,7 @@ function isCheckpoint(value) {
2173
2279
  return item.entries.every((entry) => {
2174
2280
  if (typeof entry !== "object" || entry === null) return false;
2175
2281
  const candidate = entry;
2176
- 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") {
2177
2283
  return false;
2178
2284
  }
2179
2285
  if (candidate.before.existed) {
@@ -2205,12 +2311,12 @@ async function newestCheckpoint(sessionDir) {
2205
2311
  throw error;
2206
2312
  }
2207
2313
  const name = names.filter((candidate) => CHECKPOINT_NAME.test(candidate)).sort().at(-1);
2208
- return name ? path5.join(sessionDir, name) : null;
2314
+ return name ? path6.join(sessionDir, name) : null;
2209
2315
  }
2210
2316
  async function pruneCheckpoints(sessionDir) {
2211
2317
  const names = (await readdir2(sessionDir)).filter((candidate) => CHECKPOINT_NAME.test(candidate)).sort().reverse();
2212
2318
  await Promise.all(
2213
- 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)))
2214
2320
  );
2215
2321
  }
2216
2322
  function workspaceRoot(cwd) {
@@ -2225,19 +2331,11 @@ function assertSessionId2(id) {
2225
2331
  }
2226
2332
 
2227
2333
  // src/core/diagnostics.ts
2228
- import { readFile as readFile5, stat as stat4 } from "fs/promises";
2229
- import path6 from "path";
2334
+ import { readFile as readFile6, stat as stat4 } from "fs/promises";
2335
+ import path7 from "path";
2230
2336
 
2231
2337
  // src/tools/bash.ts
2232
2338
  import { spawn } from "child_process";
2233
-
2234
- // src/tools/summary.ts
2235
- function brief(value, max = 60) {
2236
- const text = String(value ?? "").replace(/\s+/g, " ").trim();
2237
- return text.length > max ? `${text.slice(0, max - 1)}\u2026` : text;
2238
- }
2239
-
2240
- // src/tools/bash.ts
2241
2339
  var DEFAULT_TIMEOUT = 12e4;
2242
2340
  var MAX_TIMEOUT = 6e5;
2243
2341
  var MAX_OUTPUT = 1e5;
@@ -2377,7 +2475,7 @@ async function detectDiagnosticCommands(cwd, configured = []) {
2377
2475
  const custom = configured.map((command) => command.trim()).filter(Boolean);
2378
2476
  if (custom.length > 0) return custom.slice(0, 8);
2379
2477
  const commands = [];
2380
- const packageJson = await readPackageJson(path6.join(cwd, "package.json"));
2478
+ const packageJson = await readPackageJson(path7.join(cwd, "package.json"));
2381
2479
  if (packageJson) {
2382
2480
  const manager = await packageManager(cwd, packageJson.packageManager);
2383
2481
  for (const name of ["lint", "typecheck", "check", "test"]) {
@@ -2386,11 +2484,11 @@ async function detectDiagnosticCommands(cwd, configured = []) {
2386
2484
  }
2387
2485
  }
2388
2486
  }
2389
- if (await isFile2(path6.join(cwd, "Cargo.toml"))) {
2487
+ if (await isFile2(path7.join(cwd, "Cargo.toml"))) {
2390
2488
  commands.push("cargo check", "cargo test");
2391
2489
  }
2392
- if (await isFile2(path6.join(cwd, "go.mod"))) commands.push("go test ./...");
2393
- 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"))) {
2394
2492
  commands.push("python -m pytest");
2395
2493
  }
2396
2494
  return [...new Set(commands)].slice(0, 8);
@@ -2480,7 +2578,7 @@ async function readPackageJson(file) {
2480
2578
  try {
2481
2579
  const info = await stat4(file);
2482
2580
  if (!info.isFile() || info.size > MAX_PACKAGE_BYTES) return null;
2483
- const value = JSON.parse(await readFile5(file, "utf8"));
2581
+ const value = JSON.parse(await readFile6(file, "utf8"));
2484
2582
  if (typeof value !== "object" || value === null) return null;
2485
2583
  const candidate = value;
2486
2584
  return {
@@ -2494,9 +2592,9 @@ async function readPackageJson(file) {
2494
2592
  async function packageManager(cwd, declared) {
2495
2593
  const name = declared?.split("@")[0];
2496
2594
  if (name === "pnpm" || name === "yarn" || name === "bun" || name === "npm") return name;
2497
- if (await isFile2(path6.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
2498
- if (await isFile2(path6.join(cwd, "yarn.lock"))) return "yarn";
2499
- 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"))) {
2500
2598
  return "bun";
2501
2599
  }
2502
2600
  return "npm";
@@ -2514,9 +2612,9 @@ async function isFile2(file) {
2514
2612
 
2515
2613
  // src/core/attachments.ts
2516
2614
  import { execFile } from "child_process";
2517
- 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";
2518
2616
  import os2 from "os";
2519
- import path7 from "path";
2617
+ import path8 from "path";
2520
2618
  import { fileURLToPath } from "url";
2521
2619
  var MAX_IMAGE_BYTES = 10 * 1024 * 1024;
2522
2620
  var MAX_TEXT_BYTES = 256 * 1024;
@@ -2547,7 +2645,7 @@ async function loadAttachment(cwd, requestedPath) {
2547
2645
  const resolved = resolveAttachmentPath(cwd, requestedPath);
2548
2646
  const linkInfo = await lstat2(resolved).catch(() => null);
2549
2647
  if (linkInfo?.isSymbolicLink()) {
2550
- throw new Error(`Cannot attach symbolic links: ${path7.basename(resolved)}`);
2648
+ throw new Error(`Cannot attach symbolic links: ${path8.basename(resolved)}`);
2551
2649
  }
2552
2650
  const info = await stat5(resolved).catch((error) => {
2553
2651
  if (error.code === "ENOENT") throw new Error(`Attachment not found: ${resolved}`);
@@ -2556,7 +2654,7 @@ async function loadAttachment(cwd, requestedPath) {
2556
2654
  if (info.isDirectory()) {
2557
2655
  const attachment = await loadFirstImageFromDirectory(resolved);
2558
2656
  if (attachment) return attachment;
2559
- 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)}`);
2560
2658
  }
2561
2659
  if (!info.isFile()) throw new Error(`Attachment is not a regular file: ${resolved}`);
2562
2660
  return loadResolvedAttachment(resolved, info.size);
@@ -2566,7 +2664,7 @@ async function loadAutomaticAttachment(cwd, requestedPath) {
2566
2664
  const resolved = resolveAttachmentPath(cwd, requestedPath);
2567
2665
  if (isSensitiveAutomaticPath(resolved)) {
2568
2666
  throw new Error(
2569
- `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)}`
2570
2668
  );
2571
2669
  }
2572
2670
  const linkInfo = await lstat2(resolved).catch(() => null);
@@ -2586,14 +2684,14 @@ function looksLikeAttachmentPath(value) {
2586
2684
  if (trimmed.length > MAX_PATH_CHARS) return false;
2587
2685
  const candidate = normalizeInputPath(trimmed);
2588
2686
  if (/^file:\/\//i.test(candidate)) return true;
2589
- if (path7.isAbsolute(candidate)) return true;
2687
+ if (path8.isAbsolute(candidate)) return true;
2590
2688
  if (/^(?:~|\.{1,2})[\\/]/.test(candidate)) return true;
2591
2689
  if (candidate.includes("/") || candidate.includes("\\")) {
2592
2690
  if (trimmed.length > 200) return false;
2593
2691
  return true;
2594
2692
  }
2595
- const basename3 = path7.basename(candidate).toLowerCase();
2596
- 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);
2597
2695
  }
2598
2696
  async function loadClipboardImage(platform = process.platform, runner = runClipboardCommand) {
2599
2697
  const commands = clipboardCommands(platform);
@@ -2625,12 +2723,12 @@ async function loadClipboardImage(platform = process.platform, runner = runClipb
2625
2723
  throw new Error(`No supported image found in the clipboard. ${hint}`);
2626
2724
  }
2627
2725
  async function loadResolvedAttachment(resolved, size) {
2628
- const name = safeName(path7.basename(resolved));
2726
+ const name = safeName(path8.basename(resolved));
2629
2727
  const imageLimitCandidate = size <= MAX_IMAGE_BYTES;
2630
2728
  if (!imageLimitCandidate) {
2631
2729
  throw new Error(`Attachment is larger than ${formatBytes(MAX_IMAGE_BYTES)}: ${name}`);
2632
2730
  }
2633
- const data = await readFile6(resolved);
2731
+ const data = await readFile7(resolved);
2634
2732
  const mediaType = detectImage(data);
2635
2733
  if (mediaType) {
2636
2734
  return {
@@ -2659,10 +2757,10 @@ async function loadFirstImageFromDirectory(dirPath) {
2659
2757
  const IMAGE_EXTENSIONS = /* @__PURE__ */ new Set([".png", ".jpg", ".jpeg", ".gif", ".webp", ".bmp"]);
2660
2758
  const entries = await readdir3(dirPath, { withFileTypes: true }).catch(() => null);
2661
2759
  if (!entries) return null;
2662
- 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));
2663
2761
  if (sorted.length === 0) return null;
2664
2762
  const first2 = sorted[0];
2665
- const filePath = path7.join(dirPath, first2.name);
2763
+ const filePath = path8.join(dirPath, first2.name);
2666
2764
  const info = await stat5(filePath).catch(() => null);
2667
2765
  if (!info?.isFile()) return null;
2668
2766
  return loadResolvedAttachment(filePath, info.size);
@@ -2672,17 +2770,17 @@ function resolveAttachmentPath(cwd, requestedPath) {
2672
2770
  if (!cleaned) throw new Error("Give a file path: /attach <path>");
2673
2771
  if (/^file:\/\//i.test(cleaned)) {
2674
2772
  try {
2675
- return path7.normalize(fileURLToPath(cleaned));
2773
+ return path8.normalize(fileURLToPath(cleaned));
2676
2774
  } catch {
2677
2775
  throw new Error(`Invalid local file URL: ${cleaned}`);
2678
2776
  }
2679
2777
  }
2680
- const expanded = cleaned === "~" ? os2.homedir() : cleaned.startsWith("~/") ? path7.join(os2.homedir(), cleaned.slice(2)) : cleaned;
2681
- 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);
2682
2780
  }
2683
2781
  function isSensitiveAutomaticPath(file) {
2684
- const basename3 = path7.basename(file).toLowerCase();
2685
- 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));
2686
2784
  }
2687
2785
  function attachmentLabel(block) {
2688
2786
  if (block.type === "image") return `image: ${block.name}`;
@@ -2827,7 +2925,7 @@ try {
2827
2925
  }
2828
2926
  `;
2829
2927
  function textMime(file) {
2830
- const extension = path7.extname(file).toLowerCase();
2928
+ const extension = path8.extname(file).toLowerCase();
2831
2929
  const known = {
2832
2930
  ".json": "application/json",
2833
2931
  ".md": "text/markdown",
@@ -2880,8 +2978,8 @@ var SINGLE_MAX_TOKENS = 768;
2880
2978
  var CHUNK_MAX_TOKENS = 512;
2881
2979
  var MERGE_MAX_TOKENS = 1024;
2882
2980
  var CHUNK_CONCURRENCY = 3;
2883
- 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.";
2884
- 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.";
2885
2983
  async function compactHistory(options) {
2886
2984
  const cut = compactCutIndex(options.history);
2887
2985
  if (cut <= 0) {
@@ -3283,6 +3381,12 @@ function buildSystemPrompt(args) {
3283
3381
  if (args.skills && args.skills.trim() !== "") {
3284
3382
  lines.push("", args.skills);
3285
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
+ }
3286
3390
  return lines.join("\n");
3287
3391
  }
3288
3392
 
@@ -3533,7 +3637,7 @@ function clip(value) {
3533
3637
  // package.json
3534
3638
  var package_default = {
3535
3639
  name: "@kernelonpanic/kitcode",
3536
- version: "1.2.9",
3640
+ version: "1.3.0",
3537
3641
  description: "Terminal coding agent with a config you never have to write by hand",
3538
3642
  type: "module",
3539
3643
  license: "MIT",
@@ -3601,7 +3705,7 @@ var package_default = {
3601
3705
 
3602
3706
  // src/version.ts
3603
3707
  var KITCODE_VERSION = package_default.version;
3604
- var KITCODE_COMMIT = true ? "cc499fc443e73408f49b928f204b77c48e9d2d05" : "development";
3708
+ var KITCODE_COMMIT = true ? "7cd983ca3dc89406326e5e513870686036c3ab9c" : "development";
3605
3709
 
3606
3710
  // src/mcp/client.ts
3607
3711
  var clientInfo = { name: "kitcode", version: KITCODE_VERSION };
@@ -3892,8 +3996,8 @@ function balanceEndpoints(baseUrl) {
3892
3996
  endpoint(url.origin, "/v1/balance", genericBalance)
3893
3997
  ];
3894
3998
  }
3895
- function endpoint(origin, path14, parse2) {
3896
- return { url: `${origin}${path14}`, parse: parse2 };
3999
+ function endpoint(origin, path15, parse2) {
4000
+ return { url: `${origin}${path15}`, parse: parse2 };
3897
4001
  }
3898
4002
  function deepSeekBalance(body) {
3899
4003
  const infos = record(body)?.["balance_infos"];
@@ -3986,8 +4090,8 @@ async function limitedResponseText(response, limit) {
3986
4090
  }
3987
4091
 
3988
4092
  // src/providers/catalog.ts
3989
- import { chmod as chmod4, readFile as readFile7, writeFile as writeFile5 } from "fs/promises";
3990
- import path8 from "path";
4093
+ import { chmod as chmod4, readFile as readFile8, writeFile as writeFile6 } from "fs/promises";
4094
+ import path9 from "path";
3991
4095
  var MAX_AGE_MS = 24 * 60 * 60 * 1e3;
3992
4096
  var CACHE_VERSION = 3;
3993
4097
  async function loadModels(provider, refresh = false) {
@@ -4009,11 +4113,11 @@ async function loadModels(provider, refresh = false) {
4009
4113
  return models;
4010
4114
  }
4011
4115
  function cacheFile(providerId) {
4012
- return path8.join(cacheDir, "models", `${providerId.replace(/[^\w.-]/g, "_")}.json`);
4116
+ return path9.join(cacheDir, "models", `${providerId.replace(/[^\w.-]/g, "_")}.json`);
4013
4117
  }
4014
4118
  async function readCache(file) {
4015
4119
  try {
4016
- const parsed = JSON.parse(await readFile7(file, "utf8"));
4120
+ const parsed = JSON.parse(await readFile8(file, "utf8"));
4017
4121
  if (typeof parsed !== "object" || parsed === null) return void 0;
4018
4122
  const candidate = parsed;
4019
4123
  if (typeof candidate.fetchedAt !== "number" || !Number.isFinite(candidate.fetchedAt) || !Array.isArray(candidate.models) || candidate.models.length > 1e4 || !candidate.models.every(validModel)) {
@@ -4044,8 +4148,8 @@ function validModel(value) {
4044
4148
  async function writeCache(file, models) {
4045
4149
  const payload = { version: CACHE_VERSION, fetchedAt: Date.now(), models };
4046
4150
  try {
4047
- await ensureDir(path8.dirname(file));
4048
- 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 });
4049
4153
  await chmod4(file, 384);
4050
4154
  } catch {
4051
4155
  return;
@@ -4194,7 +4298,8 @@ async function* streamTurn(client, providerId, apiKey2, req) {
4194
4298
  tools: toToolParams(req.tools)
4195
4299
  };
4196
4300
  if (req.thinking) params.thinking = { type: "adaptive", display: "summarized" };
4197
- if (req.effort) params.output_config = { effort: req.effort };
4301
+ const effort = resolveEffort(req.effort, req.messages);
4302
+ if (effort) params.output_config = { effort };
4198
4303
  let thinking = "";
4199
4304
  let text = "";
4200
4305
  let events = 0;
@@ -4382,11 +4487,13 @@ async function* streamTurn2(client, providerId, apiKey2, req) {
4382
4487
  let recognised = 0;
4383
4488
  const calls = /* @__PURE__ */ new Map();
4384
4489
  const capture = captureResponseHead();
4490
+ const effort = openAiEffort(req.model, resolveEffort(req.effort, req.messages));
4385
4491
  try {
4386
4492
  const stream = await client.withOptions({ fetch: capture.fetch }).chat.completions.create(
4387
4493
  {
4388
4494
  model: req.model,
4389
4495
  max_tokens: req.maxTokens,
4496
+ ...effort ? { reasoning_effort: effort } : {},
4390
4497
  messages: toChatMessages(req.system, req.messages),
4391
4498
  ...req.tools.length > 0 ? { tools: toChatTools(req.tools) } : {},
4392
4499
  stream: true,
@@ -4683,11 +4790,11 @@ function isFileEdit(tool) {
4683
4790
  }
4684
4791
 
4685
4792
  // src/tools/edit.ts
4686
- import { readFile as readFile8, stat as stat6 } from "fs/promises";
4793
+ import { readFile as readFile9, stat as stat6 } from "fs/promises";
4687
4794
 
4688
4795
  // src/tools/safe-write.ts
4689
4796
  import { randomUUID as randomUUID3 } from "crypto";
4690
- 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";
4691
4798
  import { basename as basename2, dirname as dirname2, join } from "path";
4692
4799
  var UnsafeFileChangeError = class extends Error {
4693
4800
  constructor(message) {
@@ -4750,7 +4857,7 @@ async function atomicWriteSafeFile(file, data, snapshot) {
4750
4857
  await rename5(temp, file);
4751
4858
  } catch (error) {
4752
4859
  await handle?.close().catch(() => void 0);
4753
- await rm3(temp, { force: true }).catch(() => void 0);
4860
+ await rm4(temp, { force: true }).catch(() => void 0);
4754
4861
  throw error;
4755
4862
  }
4756
4863
  }
@@ -4845,56 +4952,56 @@ var editTool = {
4845
4952
  return `edit(${brief(input.path)})`;
4846
4953
  },
4847
4954
  async preview(input, ctx) {
4848
- const { path: path14, oldString, newString, replaceAll = false } = input;
4849
- const safe = resolveInside(ctx.cwd, path14);
4955
+ const { path: path15, oldString, newString, replaceAll = false } = input;
4956
+ const safe = resolveInside(ctx.cwd, path15);
4850
4957
  if (!safe.ok) return { kind: "text", text: safe.reason };
4851
4958
  const info = await stat6(safe.path).catch(() => null);
4852
4959
  if (info && info.size > MAX_FILE_BYTES2) {
4853
- 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.` };
4854
4961
  }
4855
- const beforeBuffer = await readFile8(safe.path).catch(() => null);
4856
- 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}.` };
4857
4964
  if (beforeBuffer.subarray(0, 8192).includes(0)) {
4858
- return { kind: "text", text: `Cannot preview binary file ${path14}` };
4965
+ return { kind: "text", text: `Cannot preview binary file ${path15}` };
4859
4966
  }
4860
4967
  const before = beforeBuffer.toString("utf8");
4861
4968
  const after = replacement(before, oldString, newString, replaceAll);
4862
- 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 };
4863
4970
  },
4864
4971
  async execute(input, ctx) {
4865
- const { path: path14, oldString, newString, replaceAll = false } = input;
4866
- const safe = resolveInside(ctx.cwd, path14);
4972
+ const { path: path15, oldString, newString, replaceAll = false } = input;
4973
+ const safe = resolveInside(ctx.cwd, path15);
4867
4974
  if (!safe.ok) return { content: safe.reason, isError: true };
4868
4975
  let snapshot;
4869
4976
  try {
4870
4977
  snapshot = await readSafeFileSnapshot(safe.path, MAX_FILE_BYTES2);
4871
4978
  } catch (error) {
4872
- return { content: `Cannot edit ${path14}: ${error.message}.`, isError: true };
4979
+ return { content: `Cannot edit ${path15}: ${error.message}.`, isError: true };
4873
4980
  }
4874
- if (!snapshot.exists) return { content: `Cannot read ${path14}.`, isError: true };
4981
+ if (!snapshot.exists) return { content: `Cannot read ${path15}.`, isError: true };
4875
4982
  const beforeBuffer = snapshot.data;
4876
4983
  if (beforeBuffer.subarray(0, 8192).includes(0)) {
4877
- 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 };
4878
4985
  }
4879
4986
  const before = beforeBuffer.toString("utf8");
4880
4987
  const count = countOccurrences(before, oldString);
4881
4988
  if (count === 0) {
4882
4989
  return {
4883
- 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.`,
4884
4991
  isError: true
4885
4992
  };
4886
4993
  }
4887
4994
  if (count > 1 && !replaceAll) {
4888
4995
  return {
4889
- 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.`,
4890
4997
  isError: true
4891
4998
  };
4892
4999
  }
4893
5000
  const after = replacement(before, oldString, newString, replaceAll);
4894
- 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 };
4895
5002
  if (Buffer.byteLength(after, "utf8") > MAX_FILE_BYTES2) {
4896
5003
  return {
4897
- 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.`,
4898
5005
  isError: true
4899
5006
  };
4900
5007
  }
@@ -4907,11 +5014,11 @@ var editTool = {
4907
5014
  await atomicWriteSafeFile(safe.path, after, snapshot);
4908
5015
  ctx.checkpoint?.markChanged(safe.path);
4909
5016
  } catch (error) {
4910
- return { content: `Failed to write ${path14}: ${error.message}`, isError: true };
5017
+ return { content: `Failed to write ${path15}: ${error.message}`, isError: true };
4911
5018
  }
4912
5019
  return {
4913
- content: `Edited ${path14} (${count} ${count === 1 ? "replacement" : "replacements"})`,
4914
- display: { kind: "diff", path: path14, before, after }
5020
+ content: `Edited ${path15} (${count} ${count === 1 ? "replacement" : "replacements"})`,
5021
+ display: { kind: "diff", path: path15, before, after }
4915
5022
  };
4916
5023
  }
4917
5024
  };
@@ -4946,11 +5053,11 @@ var globTool = {
4946
5053
  return `glob(${brief(input.pattern)})`;
4947
5054
  },
4948
5055
  async execute(input, ctx) {
4949
- const { pattern, path: path14 = "." } = input;
5056
+ const { pattern, path: path15 = "." } = input;
4950
5057
  if (patternEscapes(pattern)) {
4951
5058
  return { content: `Pattern ${pattern} must be relative to the workspace root.`, isError: true };
4952
5059
  }
4953
- const safe = resolveInside(ctx.cwd, path14);
5060
+ const safe = resolveInside(ctx.cwd, path15);
4954
5061
  if (!safe.ok) return { content: safe.reason, isError: true };
4955
5062
  const entries = await fg(pattern, {
4956
5063
  cwd: safe.path,
@@ -4972,12 +5079,12 @@ var globTool = {
4972
5079
  };
4973
5080
 
4974
5081
  // src/tools/grep.ts
4975
- import { readFile as readFile9, stat as stat7 } from "fs/promises";
5082
+ import { readFile as readFile10, stat as stat7 } from "fs/promises";
4976
5083
  import { join as join3 } from "path";
4977
5084
  import fg2 from "fast-glob";
4978
5085
 
4979
5086
  // src/tools/sensitive.ts
4980
- import path9 from "path";
5087
+ import path10 from "path";
4981
5088
  var sensitiveNames = /* @__PURE__ */ new Set([
4982
5089
  ".npmrc",
4983
5090
  ".pypirc",
@@ -5004,9 +5111,9 @@ var sensitiveExtensions = /* @__PURE__ */ new Set([".pem", ".key", ".p12", ".pfx
5004
5111
  function isSensitivePath(value) {
5005
5112
  if (typeof value !== "string") return false;
5006
5113
  const normalized = value.replaceAll("\\", "/").toLowerCase();
5007
- const name = path9.posix.basename(normalized);
5114
+ const name = path10.posix.basename(normalized);
5008
5115
  if (name === ".env" || name.startsWith(".env.")) return true;
5009
- 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;
5010
5117
  return normalized.split("/").some((part) => part === ".ssh" || part === ".aws");
5011
5118
  }
5012
5119
  function mentionsSensitivePattern(value) {
@@ -5124,16 +5231,16 @@ var grepTool = {
5124
5231
  defaultPermission: "allow",
5125
5232
  readOnly: true,
5126
5233
  permission(input, ctx) {
5127
- const { path: path14, glob } = input ?? {};
5128
- if (isSensitivePath(path14) || mentionsSensitivePattern(glob)) return "ask";
5129
- 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;
5130
5237
  return safe?.ok && isSensitivePath(safe.relative) ? "ask" : void 0;
5131
5238
  },
5132
5239
  summarize(input) {
5133
5240
  return `grep(${brief(input.pattern)})`;
5134
5241
  },
5135
5242
  async execute(input, ctx) {
5136
- const { pattern, path: path14 = ".", glob = "**/*", maxMatches } = input;
5243
+ const { pattern, path: path15 = ".", glob = "**/*", maxMatches } = input;
5137
5244
  if (pattern.length > MAX_PATTERN_CHARS) {
5138
5245
  return {
5139
5246
  content: `Regular expression is limited to ${MAX_PATTERN_CHARS} characters.`,
@@ -5148,10 +5255,10 @@ var grepTool = {
5148
5255
  if (patternEscapes(glob)) {
5149
5256
  return { content: `Glob ${glob} must be relative to the workspace root.`, isError: true };
5150
5257
  }
5151
- const safe = resolveInside(ctx.cwd, path14);
5258
+ const safe = resolveInside(ctx.cwd, path15);
5152
5259
  if (!safe.ok) return { content: safe.reason, isError: true };
5153
5260
  const target = await stat7(safe.path).catch(() => null);
5154
- if (!target) return { content: `Path not found: ${path14}`, isError: true };
5261
+ if (!target) return { content: `Path not found: ${path15}`, isError: true };
5155
5262
  const discovered = target.isDirectory() ? (await fg2(glob, {
5156
5263
  cwd: safe.path,
5157
5264
  dot: false,
@@ -5182,7 +5289,7 @@ var grepTool = {
5182
5289
  break;
5183
5290
  }
5184
5291
  scannedBytes += file.size;
5185
- const buffer = await readFile9(file.absolute).catch(() => null);
5292
+ const buffer = await readFile10(file.absolute).catch(() => null);
5186
5293
  if (!buffer || buffer.subarray(0, 4096).includes(0)) continue;
5187
5294
  const lines = buffer.toString("utf8").split("\n");
5188
5295
  const remaining = limit - hits.length;
@@ -5212,7 +5319,7 @@ var grepTool = {
5212
5319
  };
5213
5320
 
5214
5321
  // src/tools/read.ts
5215
- import { readFile as readFile10, stat as stat8 } from "fs/promises";
5322
+ import { readFile as readFile11, stat as stat8 } from "fs/promises";
5216
5323
  var MAX_LINES = 2e3;
5217
5324
  var MAX_CHARS = 2e5;
5218
5325
  var MAX_FILE_BYTES4 = 5e6;
@@ -5241,30 +5348,30 @@ var readTool = {
5241
5348
  return `read(${brief(input.path)})`;
5242
5349
  },
5243
5350
  async execute(input, ctx) {
5244
- const { path: path14, offset = 1, limit } = input;
5245
- const safe = resolveInside(ctx.cwd, path14);
5351
+ const { path: path15, offset = 1, limit } = input;
5352
+ const safe = resolveInside(ctx.cwd, path15);
5246
5353
  if (!safe.ok) return { content: safe.reason, isError: true };
5247
5354
  const info = await stat8(safe.path).catch(() => null);
5248
- if (!info) return { content: `File not found: ${path14}`, isError: true };
5355
+ if (!info) return { content: `File not found: ${path15}`, isError: true };
5249
5356
  if (info.isDirectory()) {
5250
- 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 };
5251
5358
  }
5252
5359
  if (info.size > MAX_FILE_BYTES4) {
5253
5360
  return {
5254
- 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.`,
5255
5362
  isError: true
5256
5363
  };
5257
5364
  }
5258
- const buffer = await readFile10(safe.path);
5365
+ const buffer = await readFile11(safe.path);
5259
5366
  if (buffer.subarray(0, 4096).includes(0)) {
5260
- 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 };
5261
5368
  }
5262
5369
  const lines = buffer.toString("utf8").split("\n");
5263
5370
  if (lines.at(-1) === "") lines.pop();
5264
- if (lines.length === 0) return { content: `${path14} is empty.` };
5371
+ if (lines.length === 0) return { content: `${path15} is empty.` };
5265
5372
  const start = Math.max(1, offset) - 1;
5266
5373
  if (start >= lines.length) {
5267
- 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 };
5268
5375
  }
5269
5376
  const window = lines.slice(start, start + Math.min(limit ?? MAX_LINES, MAX_LINES));
5270
5377
  const rendered = [];
@@ -5291,7 +5398,7 @@ function toToolSchema(tool) {
5291
5398
  }
5292
5399
 
5293
5400
  // src/tools/write.ts
5294
- 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";
5295
5402
  import { dirname as dirname3 } from "path";
5296
5403
  var MAX_FILE_BYTES5 = 5e6;
5297
5404
  var writeTool = {
@@ -5311,64 +5418,64 @@ var writeTool = {
5311
5418
  return `write(${brief(input.path)})`;
5312
5419
  },
5313
5420
  async preview(input, ctx) {
5314
- const { path: path14, content } = input;
5421
+ const { path: path15, content } = input;
5315
5422
  if (Buffer.byteLength(content, "utf8") > MAX_FILE_BYTES5) {
5316
5423
  return { kind: "text", text: `Write is limited to ${MAX_FILE_BYTES5 / 1e6} MB.` };
5317
5424
  }
5318
- const safe = resolveInside(ctx.cwd, path14);
5425
+ const safe = resolveInside(ctx.cwd, path15);
5319
5426
  if (!safe.ok) return { kind: "text", text: safe.reason };
5320
5427
  const beforeInfo = await stat9(safe.path).catch(() => null);
5321
5428
  if (beforeInfo && beforeInfo.size > MAX_FILE_BYTES5) {
5322
- 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.` };
5323
5430
  }
5324
- const beforeBuffer = await readFile11(safe.path).catch(() => null);
5431
+ const beforeBuffer = await readFile12(safe.path).catch(() => null);
5325
5432
  if (beforeBuffer?.subarray(0, 8192).includes(0)) {
5326
- return { kind: "text", text: `Cannot preview binary file ${path14}` };
5433
+ return { kind: "text", text: `Cannot preview binary file ${path15}` };
5327
5434
  }
5328
5435
  return {
5329
5436
  kind: "diff",
5330
- path: path14,
5437
+ path: path15,
5331
5438
  before: beforeBuffer?.toString("utf8") ?? "",
5332
5439
  after: content
5333
5440
  };
5334
5441
  },
5335
5442
  async execute(input, ctx) {
5336
- const { path: path14, content } = input;
5443
+ const { path: path15, content } = input;
5337
5444
  if (Buffer.byteLength(content, "utf8") > MAX_FILE_BYTES5) {
5338
5445
  return {
5339
- 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.`,
5340
5447
  isError: true
5341
5448
  };
5342
5449
  }
5343
- const safe = resolveInside(ctx.cwd, path14);
5450
+ const safe = resolveInside(ctx.cwd, path15);
5344
5451
  if (!safe.ok) return { content: safe.reason, isError: true };
5345
5452
  let target = safe.path;
5346
5453
  let snapshot;
5347
5454
  try {
5348
5455
  await mkdir4(dirname3(target), { recursive: true });
5349
- const rechecked = resolveInside(ctx.cwd, path14);
5456
+ const rechecked = resolveInside(ctx.cwd, path15);
5350
5457
  if (!rechecked.ok || rechecked.path !== target) {
5351
5458
  return {
5352
- 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.`,
5353
5460
  isError: true
5354
5461
  };
5355
5462
  }
5356
5463
  target = rechecked.path;
5357
5464
  snapshot = await readSafeFileSnapshot(target, MAX_FILE_BYTES5);
5358
5465
  if (snapshot.exists && snapshot.data.subarray(0, 8192).includes(0)) {
5359
- 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 };
5360
5467
  }
5361
5468
  await ctx.checkpoint?.capture(target);
5362
5469
  await atomicWriteSafeFile(target, content, snapshot);
5363
5470
  ctx.checkpoint?.markChanged(target);
5364
5471
  } catch (error) {
5365
- return { content: `Failed to write ${path14}: ${error.message}`, isError: true };
5472
+ return { content: `Failed to write ${path15}: ${error.message}`, isError: true };
5366
5473
  }
5367
5474
  const before = snapshot.exists ? snapshot.data.toString("utf8") : "";
5368
5475
  const lines = content === "" ? 0 : content.replace(/\n$/, "").split("\n").length;
5369
5476
  return {
5370
- content: `${before === "" ? "Created" : "Updated"} ${path14} (${lines} ${lines === 1 ? "line" : "lines"})`,
5371
- 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 }
5372
5479
  };
5373
5480
  }
5374
5481
  };
@@ -5397,8 +5504,8 @@ function createToolRegistry(tools) {
5397
5504
  }
5398
5505
 
5399
5506
  // src/prompts/library.ts
5400
- import { chmod as chmod6, readFile as readFile12, readdir as readdir4, unlink as unlink3, writeFile as writeFile6 } from "fs/promises";
5401
- 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";
5402
5509
  async function savePrompt(input) {
5403
5510
  const slug = slugify(input.name);
5404
5511
  if (!slug) throw new Error(`Prompt name has no letters or digits to name a file after: ${input.name}`);
@@ -5410,8 +5517,8 @@ async function savePrompt(input) {
5410
5517
  body: input.body.trim()
5411
5518
  };
5412
5519
  await ensureDir(promptsDir);
5413
- const file = path10.join(promptsDir, `${slug}.md`);
5414
- await writeFile6(file, serialize(prompt), {
5520
+ const file = path11.join(promptsDir, `${slug}.md`);
5521
+ await writeFile7(file, serialize(prompt), {
5415
5522
  encoding: "utf8",
5416
5523
  mode: 384
5417
5524
  });
@@ -5421,7 +5528,7 @@ async function savePrompt(input) {
5421
5528
  async function getPrompt(slug) {
5422
5529
  assertSlug(slug);
5423
5530
  try {
5424
- return parse(slug, await readFile12(path10.join(promptsDir, `${slug}.md`), "utf8"));
5531
+ return parse(slug, await readFile13(path11.join(promptsDir, `${slug}.md`), "utf8"));
5425
5532
  } catch (error) {
5426
5533
  if (isMissing(error)) return null;
5427
5534
  throw error;
@@ -5437,7 +5544,7 @@ async function listPrompts() {
5437
5544
  }
5438
5545
  const prompts2 = await Promise.all(
5439
5546
  names.filter((name) => name.endsWith(".md")).map(
5440
- 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"))
5441
5548
  )
5442
5549
  );
5443
5550
  return prompts2.sort((a, b) => a.name.localeCompare(b.name));
@@ -5445,7 +5552,7 @@ async function listPrompts() {
5445
5552
  async function deletePrompt(slug) {
5446
5553
  assertSlug(slug);
5447
5554
  try {
5448
- await unlink3(path10.join(promptsDir, `${slug}.md`));
5555
+ await unlink3(path11.join(promptsDir, `${slug}.md`));
5449
5556
  return true;
5450
5557
  } catch (error) {
5451
5558
  if (isMissing(error)) return false;
@@ -5503,7 +5610,7 @@ function parse(slug, text) {
5503
5610
 
5504
5611
  // src/skills/library.ts
5505
5612
  import { lstat as lstat4, open as open2, readdir as readdir5 } from "fs/promises";
5506
- import path11 from "path";
5613
+ import path12 from "path";
5507
5614
  var SKILL_FILE = "SKILL.md";
5508
5615
  var FRONTMATTER_BYTES = 8192;
5509
5616
  var MAX_SKILL_BYTES = 5e6;
@@ -5516,7 +5623,7 @@ async function discoverSkills(dirs) {
5516
5623
  if (!rootInfo?.isDirectory() || rootInfo.isSymbolicLink()) continue;
5517
5624
  const entries = await readdir5(root, { withFileTypes: true }).catch(() => []);
5518
5625
  const found = await Promise.all(
5519
- 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)))
5520
5627
  );
5521
5628
  for (const meta of found) {
5522
5629
  if (meta && !byName.has(meta.name)) byName.set(meta.name, meta);
@@ -5556,11 +5663,11 @@ function formatSkillCatalogue(skills) {
5556
5663
  async function readMeta(root, rootInfo, dir) {
5557
5664
  const dirInfo = await lstat4(dir).catch(() => null);
5558
5665
  if (!dirInfo?.isDirectory() || dirInfo.isSymbolicLink()) return null;
5559
- const file = path11.join(dir, SKILL_FILE);
5666
+ const file = path12.join(dir, SKILL_FILE);
5560
5667
  const head = await readFrontmatterBytes(file);
5561
5668
  if (!head) return null;
5562
5669
  const { fields } = parseFrontmatter(head.text);
5563
- 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 };
5564
5671
  guards.set(meta, {
5565
5672
  root,
5566
5673
  rootIdentity: identity(rootInfo),
@@ -5656,8 +5763,8 @@ import {
5656
5763
  writeFileSync
5657
5764
  } from "fs";
5658
5765
  import { chmod as chmod7, mkdir as mkdir5 } from "fs/promises";
5659
- import path12 from "path";
5660
- var TMP_DIR = path12.join(skillsDir, ".tmp");
5766
+ import path13 from "path";
5767
+ var TMP_DIR = path13.join(skillsDir, ".tmp");
5661
5768
  var MAX_SKILL_BYTES2 = 5e6;
5662
5769
  async function installSkill(source) {
5663
5770
  await ensureDir(skillsDir);
@@ -5682,7 +5789,7 @@ function isNpmPackage(input) {
5682
5789
  }
5683
5790
  async function installFromGitHub(url) {
5684
5791
  const { owner, repo, subdir, branch } = parseGitHubUrl(url);
5685
- const name = safeSkillName(subdir ? path12.posix.basename(subdir) : repo);
5792
+ const name = safeSkillName(subdir ? path13.posix.basename(subdir) : repo);
5686
5793
  const skillDir = safeChildPath(skillsDir, name);
5687
5794
  const tmpDir = safeChildPath(TMP_DIR, `${name}-${randomUUID4()}`);
5688
5795
  try {
@@ -5717,16 +5824,16 @@ async function installFromNpm(packageName) {
5717
5824
  if (!tarball) {
5718
5825
  throw new Error(`Could not download npm package "${packageName}"`);
5719
5826
  }
5720
- const tarballPath = path12.join(tmpDir, tarball);
5827
+ const tarballPath = path13.join(tmpDir, tarball);
5721
5828
  validateTarball(tarballPath);
5722
- const extractDir = path12.join(tmpDir, "extracted");
5829
+ const extractDir = path13.join(tmpDir, "extracted");
5723
5830
  mkdirSync(extractDir, { recursive: true });
5724
5831
  try {
5725
5832
  execFileSync("tar", ["-xzf", tarballPath, "-C", extractDir], { stdio: "ignore" });
5726
5833
  } catch {
5727
- const packageDir = path12.join(tmpDir, "node_modules", packageName);
5834
+ const packageDir = path13.join(tmpDir, "node_modules", packageName);
5728
5835
  if (existsSync2(packageDir)) {
5729
- const skillFile2 = path12.join(packageDir, "SKILL.md");
5836
+ const skillFile2 = path13.join(packageDir, "SKILL.md");
5730
5837
  if (existsSync2(skillFile2)) {
5731
5838
  const body2 = readSkillFile(skillFile2, packageDir, packageName);
5732
5839
  await writeSkillFile(skillDir, body2);
@@ -5738,7 +5845,7 @@ async function installFromNpm(packageName) {
5738
5845
  const extractedFiles = readdirSync(extractDir);
5739
5846
  const packageRoot = extractedFiles.find((f) => f === "package");
5740
5847
  if (!packageRoot) throw new Error("Could not find package root in extracted files");
5741
- const skillFile = path12.join(extractDir, "package", "SKILL.md");
5848
+ const skillFile = path13.join(extractDir, "package", "SKILL.md");
5742
5849
  if (!existsSync2(skillFile)) {
5743
5850
  throw new Error(`No SKILL.md found in npm package "${packageName}"`);
5744
5851
  }
@@ -5772,26 +5879,26 @@ function validateTarball(file) {
5772
5879
  }
5773
5880
  for (const entry of entries) {
5774
5881
  const normalized = entry.replace(/^\.\//, "");
5775
- 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 === "..")) {
5776
5883
  throw new Error(`Unsafe path in npm skill archive: ${entry}`);
5777
5884
  }
5778
5885
  }
5779
5886
  }
5780
5887
  async function installFromLocal(srcPath) {
5781
- const resolved = path12.resolve(srcPath);
5888
+ const resolved = path13.resolve(srcPath);
5782
5889
  let skillDir;
5783
5890
  let skillFile;
5784
- const directSkill = path12.join(resolved, "SKILL.md");
5891
+ const directSkill = path13.join(resolved, "SKILL.md");
5785
5892
  if (existsSync2(directSkill)) {
5786
5893
  skillDir = resolved;
5787
5894
  skillFile = directSkill;
5788
5895
  } else if (resolved.endsWith("SKILL.md") && existsSync2(resolved)) {
5789
- skillDir = path12.dirname(resolved);
5896
+ skillDir = path13.dirname(resolved);
5790
5897
  skillFile = resolved;
5791
5898
  } else {
5792
5899
  throw new Error(`No SKILL.md found at "${srcPath}"`);
5793
5900
  }
5794
- const name = path12.basename(skillDir);
5901
+ const name = path13.basename(skillDir);
5795
5902
  const destDir = safeChildPath(skillsDir, safeSkillName(name));
5796
5903
  const body = readSkillFile(skillFile, skillDir, srcPath);
5797
5904
  await writeSkillFile(destDir, body);
@@ -5804,7 +5911,7 @@ async function writeSkillFile(dir, body) {
5804
5911
  throw new Error(`Refusing to replace symlinked skill directory: ${dir}`);
5805
5912
  }
5806
5913
  await mkdir5(dir, { recursive: true, mode: 448 });
5807
- const file = path12.join(dir, "SKILL.md");
5914
+ const file = path13.join(dir, "SKILL.md");
5808
5915
  const existingFile = lstatSync(file, { throwIfNoEntry: false });
5809
5916
  if (existingFile?.isSymbolicLink()) {
5810
5917
  throw new Error(`Refusing to replace symlinked skill file: ${file}`);
@@ -5857,13 +5964,13 @@ function safeSkillName(value) {
5857
5964
  return value;
5858
5965
  }
5859
5966
  function safeChildPath(root, ...segments) {
5860
- const target = path12.resolve(root, ...segments);
5967
+ const target = path13.resolve(root, ...segments);
5861
5968
  assertInside(root, target);
5862
5969
  return target;
5863
5970
  }
5864
5971
  function assertInside(root, target) {
5865
- const relative2 = path12.relative(path12.resolve(root), path12.resolve(target));
5866
- 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}`)) {
5867
5974
  return;
5868
5975
  }
5869
5976
  throw new Error(`Path escapes the skills directory: ${target}`);
@@ -6189,10 +6296,17 @@ async function boot(options) {
6189
6296
  }
6190
6297
  const usage = createUsageTracker(session.usage, resolvePricing);
6191
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 })]);
6192
6305
  const mainSystemPrompt = () => buildSystemPrompt({
6193
6306
  cwd: options.cwd,
6194
6307
  toolNames: tools.list().map((tool) => tool.name),
6195
- skills: skillCatalogue
6308
+ skills: skillCatalogue,
6309
+ memory: projectMemory
6196
6310
  });
6197
6311
  let modelRef = session.model || config.model || "";
6198
6312
  if (options.modelRef) modelRef = options.modelRef;
@@ -6538,7 +6652,7 @@ async function boot(options) {
6538
6652
  },
6539
6653
  async exportSession(id, destination) {
6540
6654
  await persistQueue.catch(() => void 0);
6541
- 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");
6542
6656
  if (!destination) await mkdir6(target, { recursive: true, mode: 448 });
6543
6657
  return (await exportSession(id, target)).path;
6544
6658
  },
@@ -6553,6 +6667,21 @@ async function boot(options) {
6553
6667
  });
6554
6668
  void refreshModelContextWindow();
6555
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
+ },
6556
6685
  getEffort: () => config.effort,
6557
6686
  async setEffort(effort) {
6558
6687
  await persistConfig((draft) => {
@@ -7195,6 +7324,7 @@ var COMMANDS = [
7195
7324
  { name: "usage" },
7196
7325
  { name: "mcp", args: "[add|list|delete|enable|disable]" },
7197
7326
  { name: "attach", args: "<path|clipboard|clear>" },
7327
+ { name: "memory", args: "[show|set <text>|clear]" },
7198
7328
  { name: "compact" },
7199
7329
  { name: "update" },
7200
7330
  { name: "checker" },
@@ -7388,7 +7518,7 @@ var en = {
7388
7518
  escHelp: "esc \u2014 cancel the running turn",
7389
7519
  cancelled: "Cancelled.",
7390
7520
  queued: (count) => `queued: ${count}`,
7391
- configAt: (path14) => `config: ${path14}`,
7521
+ configAt: (path15) => `config: ${path15}`,
7392
7522
  skillsEmpty: "No skills installed. Drop a folder with a SKILL.md into ~/.kitcode/skills or ./.kitcode/skills",
7393
7523
  skillsInstallUsage: "Usage: /skills install <github-url|npm-package|local-path>",
7394
7524
  skillsInstalling: (source) => `Installing skill from "${source}"\u2026`,
@@ -7434,7 +7564,7 @@ var en = {
7434
7564
  sessionDeleteAllFinalBody: "This cannot be undone. Press y only if you want to erase the complete session history.",
7435
7565
  sessionsDeletedAll: (count) => `All saved chats deleted \xB7 ${count} sessions removed.`,
7436
7566
  sessionsDeleteAllFailed: (deleted, failed) => `Session cleanup was incomplete \xB7 ${deleted} removed, ${failed} failed.`,
7437
- sessionExported: (path14) => `Session exported to ${path14}`,
7567
+ sessionExported: (path15) => `Session exported to ${path15}`,
7438
7568
  sessionActionResume: "resume",
7439
7569
  sessionActionRename: "rename",
7440
7570
  sessionActionDelete: "delete",
@@ -7504,6 +7634,7 @@ ${url}`,
7504
7634
  budget: "set token budget per turn",
7505
7635
  theme: "change the accent colour",
7506
7636
  lang: "change the interface language",
7637
+ memory: "view, replace or clear project memory",
7507
7638
  prompt: "insert or save a prompt",
7508
7639
  "prompt save": "save the current or recent text as a prompt",
7509
7640
  "prompt delete": "delete a saved prompt",
@@ -7576,7 +7707,7 @@ var ru = {
7576
7707
  escHelp: "esc \u2014 \u043E\u0442\u043C\u0435\u043D\u0438\u0442\u044C \u0442\u0435\u043A\u0443\u0449\u0438\u0439 \u0445\u043E\u0434",
7577
7708
  cancelled: "\u041E\u0442\u043C\u0435\u043D\u0435\u043D\u043E.",
7578
7709
  queued: (count) => `\u0432 \u043E\u0447\u0435\u0440\u0435\u0434\u0438: ${count}`,
7579
- configAt: (path14) => `\u043A\u043E\u043D\u0444\u0438\u0433: ${path14}`,
7710
+ configAt: (path15) => `\u043A\u043E\u043D\u0444\u0438\u0433: ${path15}`,
7580
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",
7581
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>",
7582
7713
  skillsInstalling: (source) => `\u0423\u0441\u0442\u0430\u043D\u0430\u0432\u043B\u0438\u0432\u0430\u044E \u0441\u043A\u0438\u043B\u043B \u0438\u0437 "${source}"\u2026`,
@@ -7622,7 +7753,7 @@ var ru = {
7622
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.",
7623
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}.`,
7624
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}.`,
7625
- 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}`,
7626
7757
  sessionActionResume: "\u043E\u0442\u043A\u0440\u044B\u0442\u044C",
7627
7758
  sessionActionRename: "\u043F\u0435\u0440\u0435\u0438\u043C\u0435\u043D\u043E\u0432\u0430\u0442\u044C",
7628
7759
  sessionActionDelete: "\u0443\u0434\u0430\u043B\u0438\u0442\u044C",
@@ -7692,6 +7823,7 @@ ${url}`,
7692
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)",
7693
7824
  theme: "\u0441\u043C\u0435\u043D\u0438\u0442\u044C \u0446\u0432\u0435\u0442 \u0430\u043A\u0446\u0435\u043D\u0442\u0430",
7694
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",
7695
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",
7696
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",
7697
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",
@@ -8545,6 +8677,7 @@ function ModelBadge({ ref: modelRef }) {
8545
8677
  return /* @__PURE__ */ jsx9(Box9, { borderColor: theme.accent, borderStyle: "round", paddingX: 1, children: /* @__PURE__ */ jsx9(Text9, { color: theme.accent, bold: true, children: name }) });
8546
8678
  }
8547
8679
  var EFFORT_STYLES = {
8680
+ auto: { label: "auto", color: "cyan" },
8548
8681
  max: { label: "max", color: "red", bold: true },
8549
8682
  xhigh: { label: "xhigh", color: "yellow", bold: true },
8550
8683
  high: { label: "high", color: "cyan" },
@@ -9624,7 +9757,7 @@ function sanitizeDisplay(display) {
9624
9757
 
9625
9758
  // src/ui/App.tsx
9626
9759
  import { jsx as jsx13, jsxs as jsxs12 } from "react/jsx-runtime";
9627
- var EFFORTS = ["low", "medium", "high", "xhigh", "max"];
9760
+ var EFFORTS = ["auto", "low", "medium", "high", "xhigh", "max"];
9628
9761
  var STREAM_FRAME_MS = 50;
9629
9762
  var MAX_ATTACHMENTS = 8;
9630
9763
  function App({
@@ -10476,14 +10609,27 @@ ${strings.mcpAddUsage}`
10476
10609
  forceRender((n) => n + 1);
10477
10610
  return;
10478
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
+ }
10479
10624
  case "effort": {
10480
- const choice = await pick(
10625
+ notice("info", runtime.effortDescription(history.current));
10626
+ const choice = EFFORTS.includes(rawRest) ? rawRest : await pick(
10481
10627
  strings.titleEffort,
10482
10628
  EFFORTS.map((effort) => ({ key: effort, label: effort }))
10483
10629
  );
10484
10630
  if (!choice) return;
10485
10631
  await runtime.setEffort(choice);
10486
- notice("info", strings.effortSet(choice));
10632
+ notice("info", `${strings.effortSet(choice)} \xB7 ${runtime.effortDescription(history.current)}`);
10487
10633
  forceRender((n) => n + 1);
10488
10634
  return;
10489
10635
  }