@kernelonpanic/kitcode 1.4.1 → 1.4.2

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
@@ -2,6 +2,7 @@
2
2
 
3
3
  // src/index.ts
4
4
  import { Command } from "commander";
5
+ import { statSync } from "fs";
5
6
  import { resolve as resolve2 } from "path";
6
7
 
7
8
  // src/app/add.ts
@@ -108,6 +109,7 @@ import Anthropic from "@anthropic-ai/sdk";
108
109
  import OpenAI from "openai";
109
110
 
110
111
  // src/providers/types.ts
112
+ var INVALID_ARGUMENTS_KEY = "__kitcode_invalid_arguments";
111
113
  var ProviderError = class extends Error {
112
114
  constructor(message, providerId, status, options) {
113
115
  super(message, options);
@@ -344,11 +346,11 @@ function oneLine(text) {
344
346
  return text.replace(/\s+/g, " ").trim();
345
347
  }
346
348
  function redactSecrets(text, knownSecrets = []) {
347
- let safe = text.replace(NAMED_SECRET, "$1[redacted]").replace(SECRET, "[redacted]");
349
+ let safe = text;
348
350
  for (const secret of knownSecrets) {
349
351
  if (secret.length >= 4) safe = safe.split(secret).join("[redacted]");
350
352
  }
351
- return safe;
353
+ return safe.replace(NAMED_SECRET, "$1[redacted]").replace(SECRET, "[redacted]");
352
354
  }
353
355
  function prop(value, key) {
354
356
  if (value === null || typeof value !== "object") return void 0;
@@ -1182,11 +1184,11 @@ var globTool = {
1182
1184
  return `glob(${brief(input.pattern)})`;
1183
1185
  },
1184
1186
  async execute(input, ctx) {
1185
- const { pattern, path: path15 = "." } = input;
1187
+ const { pattern, path: path16 = "." } = input;
1186
1188
  if (patternEscapes(pattern)) {
1187
1189
  return { content: `Pattern ${pattern} must be relative to the workspace root.`, isError: true };
1188
1190
  }
1189
- const safe = resolveInside(ctx.cwd, path15);
1191
+ const safe = resolveInside(ctx.cwd, path16);
1190
1192
  if (!safe.ok) return { content: safe.reason, isError: true };
1191
1193
  const entries = await fg(pattern, {
1192
1194
  cwd: safe.path,
@@ -1265,8 +1267,10 @@ function isSubsequence(needle, haystack) {
1265
1267
 
1266
1268
  // src/tools/jobs.ts
1267
1269
  import { spawn } from "child_process";
1270
+ import { StringDecoder } from "string_decoder";
1268
1271
  var MAX_JOBS = 8;
1269
1272
  var MAX_JOB_OUTPUT = 1e5;
1273
+ var MAX_FINISHED_JOBS = 16;
1270
1274
  var jobs = /* @__PURE__ */ new Map();
1271
1275
  var counter = 0;
1272
1276
  function startJob(command, cwd) {
@@ -1274,6 +1278,8 @@ function startJob(command, cwd) {
1274
1278
  if (live.length >= MAX_JOBS) {
1275
1279
  return { error: `At most ${MAX_JOBS} background commands can run at once.` };
1276
1280
  }
1281
+ pruneFinishedJobs();
1282
+ installExitCleanup();
1277
1283
  counter += 1;
1278
1284
  const id = `bg-${counter}`;
1279
1285
  const child = spawn(command, {
@@ -1295,19 +1301,24 @@ function startJob(command, cwd) {
1295
1301
  truncated: false
1296
1302
  };
1297
1303
  jobs.set(id, job);
1298
- const collect = (chunk) => {
1299
- const text = chunk.toString("utf8");
1304
+ const collector = () => {
1305
+ const decoder = new StringDecoder("utf8");
1306
+ return (chunk) => collect(decoder.write(chunk));
1307
+ };
1308
+ const collect = (text) => {
1300
1309
  const room = MAX_JOB_OUTPUT - job.buffer.length;
1301
1310
  if (text.length <= room) {
1302
1311
  job.buffer += text;
1303
1312
  return;
1304
1313
  }
1305
1314
  job.truncated = true;
1306
- job.buffer = (job.buffer + text).slice(-MAX_JOB_OUTPUT);
1307
- job.reported = Math.min(job.reported, job.buffer.length);
1315
+ const combined = job.buffer + text;
1316
+ const dropped = combined.length - MAX_JOB_OUTPUT;
1317
+ job.buffer = combined.slice(dropped);
1318
+ job.reported = Math.max(0, job.reported - dropped);
1308
1319
  };
1309
- child.stdout?.on("data", collect);
1310
- child.stderr?.on("data", collect);
1320
+ child.stdout?.on("data", collector());
1321
+ child.stderr?.on("data", collector());
1311
1322
  child.on("error", (error) => {
1312
1323
  job.buffer += `
1313
1324
  Command failed to start: ${error.message}`;
@@ -1348,6 +1359,26 @@ function killAllJobs() {
1348
1359
  }
1349
1360
  }
1350
1361
  }
1362
+ var exitCleanupInstalled = false;
1363
+ function installExitCleanup() {
1364
+ if (exitCleanupInstalled) return;
1365
+ exitCleanupInstalled = true;
1366
+ process.once("exit", killAllJobs);
1367
+ for (const signal of ["SIGTERM", "SIGHUP"]) {
1368
+ const onSignal = () => {
1369
+ killAllJobs();
1370
+ process.off(signal, onSignal);
1371
+ process.kill(process.pid, signal);
1372
+ };
1373
+ process.on(signal, onSignal);
1374
+ }
1375
+ }
1376
+ function pruneFinishedJobs() {
1377
+ const finished = [...jobs.values()].filter((job) => job.state !== "running");
1378
+ for (const job of finished.slice(0, Math.max(0, finished.length - MAX_FINISHED_JOBS + 1))) {
1379
+ jobs.delete(job.id);
1380
+ }
1381
+ }
1351
1382
  function summary(job) {
1352
1383
  return {
1353
1384
  id: job.id,
@@ -1442,12 +1473,12 @@ function openAiEffort(model, effort) {
1442
1473
  }
1443
1474
 
1444
1475
  // src/app/runtime.ts
1445
- import path14 from "path";
1476
+ import path15 from "path";
1446
1477
  import { mkdir as mkdir6 } from "fs/promises";
1447
1478
 
1448
1479
  // src/core/session.ts
1449
1480
  import { randomBytes } from "crypto";
1450
- import { readFile as readFile4, readdir, rename as rename3, stat as stat2, unlink, writeFile as writeFile4 } from "fs/promises";
1481
+ import { readFile as readFile4, readdir, rename as rename3, rm as rm4, stat as stat2, unlink, writeFile as writeFile4 } from "fs/promises";
1451
1482
  import path5 from "path";
1452
1483
  var MAX_SESSION_BYTES = 5e7;
1453
1484
  function createSession(cwd, model) {
@@ -1548,8 +1579,13 @@ async function deleteSession(query) {
1548
1579
  if (error.code === "ENOENT") throw new Error(`Session no longer exists: ${id}`);
1549
1580
  throw error;
1550
1581
  });
1582
+ await removeCheckpoints(id);
1551
1583
  return id;
1552
1584
  }
1585
+ async function removeCheckpoints(id) {
1586
+ assertSessionId(id);
1587
+ await rm4(path5.join(checkpointsDir, id), { recursive: true, force: true }).catch(() => void 0);
1588
+ }
1553
1589
  async function deleteAllSessions() {
1554
1590
  let names;
1555
1591
  try {
@@ -1577,6 +1613,7 @@ async function deleteAllSessions() {
1577
1613
  const id = ids[index];
1578
1614
  if (result.status === "fulfilled" || result.reason?.code === "ENOENT") {
1579
1615
  deleted.push(id);
1616
+ await removeCheckpoints(id);
1580
1617
  } else {
1581
1618
  failed.push({
1582
1619
  id,
@@ -1944,7 +1981,7 @@ async function runTurn(cfg, history, hooks, signal) {
1944
1981
  });
1945
1982
  }
1946
1983
  hooks.onEvent({ type: "turn_end", stopReason: outcome.stopReason });
1947
- return messages;
1984
+ return sanitizeHistory(messages);
1948
1985
  }
1949
1986
  }
1950
1987
  async function consumeStream(cfg, messages, hooks, signal, maxTokens) {
@@ -2144,6 +2181,13 @@ async function runOneCall(cfg, call, hooks, signal) {
2144
2181
  if (!tool) {
2145
2182
  return toolError(call.id, `Unknown tool "${call.name}". Use only the tools provided.`);
2146
2183
  }
2184
+ const invalid = invalidArguments(call.input);
2185
+ if (invalid !== void 0) {
2186
+ return toolError(
2187
+ call.id,
2188
+ `The arguments for "${call.name}" were not valid JSON, so the tool did not run. Call it again with a complete JSON object. Received: ${invalid}`
2189
+ );
2190
+ }
2147
2191
  const summary2 = describeCall(tool, call.input);
2148
2192
  const ctx = {
2149
2193
  cwd: cfg.cwd,
@@ -2241,6 +2285,11 @@ function describeRefusal(refusal) {
2241
2285
  if (refusal?.explanation) parts.push(refusal.explanation);
2242
2286
  return parts.join(" ");
2243
2287
  }
2288
+ function invalidArguments(input) {
2289
+ if (typeof input !== "object" || input === null) return void 0;
2290
+ const raw = input[INVALID_ARGUMENTS_KEY];
2291
+ return typeof raw === "string" ? raw : void 0;
2292
+ }
2244
2293
  function toolError(toolUseId, content) {
2245
2294
  return { type: "tool_result", toolUseId, content, isError: true };
2246
2295
  }
@@ -2688,11 +2737,13 @@ function assertSessionId2(id) {
2688
2737
  }
2689
2738
 
2690
2739
  // src/core/diagnostics.ts
2740
+ import { createHash as createHash3 } from "crypto";
2691
2741
  import { readFile as readFile6, stat as stat4 } from "fs/promises";
2692
2742
  import path7 from "path";
2693
2743
 
2694
2744
  // src/tools/bash.ts
2695
2745
  import { spawn as spawn2 } from "child_process";
2746
+ import { StringDecoder as StringDecoder2 } from "string_decoder";
2696
2747
  var DEFAULT_TIMEOUT = 12e4;
2697
2748
  var MAX_TIMEOUT = 6e5;
2698
2749
  var MAX_OUTPUT = 1e5;
@@ -2776,14 +2827,12 @@ var bashTool = {
2776
2827
  };
2777
2828
  let captured = 0;
2778
2829
  let truncated = false;
2779
- const collect = (buf) => {
2780
- if (truncated) return;
2781
- let text;
2782
- try {
2783
- text = buf.toString("utf8");
2784
- } catch {
2785
- text = buf.toString();
2786
- }
2830
+ const collector = () => {
2831
+ const decoder = new StringDecoder2("utf8");
2832
+ return (buf) => collect(decoder.write(buf));
2833
+ };
2834
+ const collect = (text) => {
2835
+ if (truncated || text === "") return;
2787
2836
  progress(text);
2788
2837
  if (text.length > MAX_OUTPUT - captured) {
2789
2838
  chunks.push(text.slice(0, MAX_OUTPUT - captured));
@@ -2793,8 +2842,8 @@ var bashTool = {
2793
2842
  chunks.push(text);
2794
2843
  captured += text.length;
2795
2844
  };
2796
- child.stdout.on("data", collect);
2797
- child.stderr.on("data", collect);
2845
+ child.stdout.on("data", collector());
2846
+ child.stderr.on("data", collector());
2798
2847
  const killTree2 = () => {
2799
2848
  if (child.pid == null) return;
2800
2849
  if (process.platform === "win32") {
@@ -2857,19 +2906,19 @@ var diagnosticsTool = {
2857
2906
  summarize: () => "automatic project checks",
2858
2907
  execute: async () => ({ content: "" })
2859
2908
  };
2860
- async function detectDiagnosticCommands(cwd, configured = []) {
2909
+ async function detectChecks(cwd, configured = []) {
2861
2910
  const custom = configured.map((command) => command.trim()).filter(Boolean);
2862
- if (custom.length > 0) return custom.slice(0, 8);
2863
- const commands = [];
2911
+ if (custom.length > 0) return custom.slice(0, 8).map((command) => ({ command }));
2912
+ const checks = [];
2864
2913
  const packageJson = await readPackageJson(path7.join(cwd, "package.json"));
2865
2914
  if (packageJson) {
2866
2915
  const manager = await packageManager(cwd, packageJson.packageManager);
2867
2916
  for (const name of ["lint", "typecheck", "check", "test"]) {
2868
- if (typeof packageJson.scripts?.[name] === "string") {
2869
- commands.push(scriptCommand(manager, name));
2870
- }
2917
+ const body = packageJson.scripts?.[name];
2918
+ if (typeof body === "string") checks.push({ command: scriptCommand(manager, name), runs: body });
2871
2919
  }
2872
2920
  }
2921
+ const commands = [];
2873
2922
  if (await isFile2(path7.join(cwd, "Cargo.toml"))) {
2874
2923
  commands.push("cargo check", "cargo test");
2875
2924
  }
@@ -2877,10 +2926,13 @@ async function detectDiagnosticCommands(cwd, configured = []) {
2877
2926
  if (await isFile2(path7.join(cwd, "pyproject.toml")) || await isFile2(path7.join(cwd, "pytest.ini")) || await isFile2(path7.join(cwd, "tox.ini"))) {
2878
2927
  commands.push("python -m pytest");
2879
2928
  }
2880
- return [...new Set(commands)].slice(0, 8);
2929
+ for (const command of commands) checks.push({ command });
2930
+ const seen = /* @__PURE__ */ new Set();
2931
+ return checks.filter((check) => !seen.has(check.command) && seen.add(check.command)).slice(0, 8);
2881
2932
  }
2882
2933
  async function runAutomaticDiagnostics(options) {
2883
- const commands = await detectDiagnosticCommands(options.cwd, options.configuredCommands);
2934
+ const checks = await detectChecks(options.cwd, options.configuredCommands);
2935
+ const commands = checks.map((check) => check.command);
2884
2936
  const summary2 = { commands, failed: [], skipped: false };
2885
2937
  if (options.changedFiles.length === 0 || commands.length === 0 || options.signal.aborted) {
2886
2938
  summary2.skipped = true;
@@ -2896,7 +2948,10 @@ async function runAutomaticDiagnostics(options) {
2896
2948
  });
2897
2949
  return summary2;
2898
2950
  }
2899
- if (gate === "ask") {
2951
+ const items = checks.map((check) => check.runs ? `${check.command} \u2192 ${check.runs}` : check.command);
2952
+ const grantName = `${diagnosticsTool.name}#${createHash3("sha256").update(items.join("\n")).digest("hex").slice(0, 16)}`;
2953
+ const granted = gate === "ask" && options.permissions.decide({ ...diagnosticsTool, name: grantName }) === "allow";
2954
+ if (gate === "ask" && !granted) {
2900
2955
  const decision = await options.hooks.requestPermission({
2901
2956
  toolName: diagnosticsTool.name,
2902
2957
  summary: `run ${commands.length} automatic project ${commands.length === 1 ? "check" : "checks"}`,
@@ -2904,7 +2959,7 @@ async function runAutomaticDiagnostics(options) {
2904
2959
  display: {
2905
2960
  kind: "list",
2906
2961
  title: `After changes to ${options.changedFiles.length} ${options.changedFiles.length === 1 ? "file" : "files"}`,
2907
- items: commands
2962
+ items
2908
2963
  },
2909
2964
  allowAlways: true
2910
2965
  });
@@ -2921,7 +2976,7 @@ async function runAutomaticDiagnostics(options) {
2921
2976
  });
2922
2977
  return summary2;
2923
2978
  }
2924
- if (decision === "always") options.permissions.grantForSession(diagnosticsTool.name);
2979
+ if (decision === "always") options.permissions.grantForSession(grantName);
2925
2980
  }
2926
2981
  const ctx = {
2927
2982
  cwd: options.cwd,
@@ -2998,10 +3053,51 @@ async function isFile2(file) {
2998
3053
 
2999
3054
  // src/core/attachments.ts
3000
3055
  import { execFile } from "child_process";
3001
- import { lstat as lstat2, mkdtemp, readdir as readdir3, readFile as readFile7, rm as rm4, stat as stat5, writeFile as writeFile6 } from "fs/promises";
3056
+ import { lstat as lstat2, mkdtemp, readdir as readdir3, readFile as readFile7, rm as rm5, stat as stat5, writeFile as writeFile6 } from "fs/promises";
3002
3057
  import os2 from "os";
3003
- import path8 from "path";
3058
+ import path9 from "path";
3004
3059
  import { fileURLToPath } from "url";
3060
+
3061
+ // src/tools/sensitive.ts
3062
+ import path8 from "path";
3063
+ var sensitiveNames = /* @__PURE__ */ new Set([
3064
+ ".npmrc",
3065
+ ".pypirc",
3066
+ ".git-credentials",
3067
+ ".netrc",
3068
+ ".pgpass",
3069
+ ".my.cnf",
3070
+ ".docker/config.json",
3071
+ "auth.json",
3072
+ "credentials",
3073
+ "credentials.json",
3074
+ "id_rsa",
3075
+ "id_ed25519",
3076
+ "id_dsa",
3077
+ "id_ecdsa",
3078
+ "kubeconfig",
3079
+ "known_hosts",
3080
+ "secret.json",
3081
+ "secrets.json",
3082
+ "tokens.json",
3083
+ "service-account.json"
3084
+ ]);
3085
+ var sensitiveExtensions = /* @__PURE__ */ new Set([".pem", ".key", ".p12", ".pfx", ".jks", ".keystore", ".p8", ".der"]);
3086
+ function isSensitivePath(value) {
3087
+ if (typeof value !== "string") return false;
3088
+ const normalized = value.replaceAll("\\", "/").toLowerCase();
3089
+ const name = path8.posix.basename(normalized);
3090
+ if (name === ".env" || name.startsWith(".env.")) return true;
3091
+ if (sensitiveNames.has(name) || sensitiveExtensions.has(path8.posix.extname(name))) return true;
3092
+ return normalized.split("/").some((part) => part === ".ssh" || part === ".aws");
3093
+ }
3094
+ function mentionsSensitivePattern(value) {
3095
+ if (typeof value !== "string") return false;
3096
+ const lower = value.toLowerCase();
3097
+ return isSensitivePath(lower) || lower.includes(".env") || lower.includes(".npmrc") || lower.includes(".netrc") || lower.includes(".pgpass") || lower.includes("auth.json") || lower.includes("credentials") || lower.includes("kubeconfig") || lower.includes("secret") || lower.includes("tokens.json") || /\.(?:pem|key|p12|pfx|jks|keystore|p8|der)(?:$|[^a-z0-9])/.test(lower);
3098
+ }
3099
+
3100
+ // src/core/attachments.ts
3005
3101
  var MAX_IMAGE_BYTES = 10 * 1024 * 1024;
3006
3102
  var MAX_TEXT_BYTES = 256 * 1024;
3007
3103
  var MAX_CLIPBOARD_OUTPUT_BYTES = MAX_IMAGE_BYTES + 64 * 1024;
@@ -3031,7 +3127,7 @@ async function loadAttachment(cwd, requestedPath) {
3031
3127
  const resolved = resolveAttachmentPath(cwd, requestedPath);
3032
3128
  const linkInfo = await lstat2(resolved).catch(() => null);
3033
3129
  if (linkInfo?.isSymbolicLink()) {
3034
- throw new Error(`Cannot attach symbolic links: ${path8.basename(resolved)}`);
3130
+ throw new Error(`Cannot attach symbolic links: ${path9.basename(resolved)}`);
3035
3131
  }
3036
3132
  const info = await stat5(resolved).catch((error) => {
3037
3133
  if (error.code === "ENOENT") throw new Error(`Attachment not found: ${resolved}`);
@@ -3040,7 +3136,7 @@ async function loadAttachment(cwd, requestedPath) {
3040
3136
  if (info.isDirectory()) {
3041
3137
  const attachment = await loadFirstImageFromDirectory(resolved);
3042
3138
  if (attachment) return attachment;
3043
- throw new Error(`No supported images found in directory: ${path8.basename(resolved)}`);
3139
+ throw new Error(`No supported images found in directory: ${path9.basename(resolved)}`);
3044
3140
  }
3045
3141
  if (!info.isFile()) throw new Error(`Attachment is not a regular file: ${resolved}`);
3046
3142
  return loadResolvedAttachment(resolved, info.size);
@@ -3050,7 +3146,7 @@ async function loadAutomaticAttachment(cwd, requestedPath) {
3050
3146
  const resolved = resolveAttachmentPath(cwd, requestedPath);
3051
3147
  if (isSensitiveAutomaticPath(resolved)) {
3052
3148
  throw new Error(
3053
- `For safety, sensitive-looking files must be attached explicitly with /attach: ${path8.basename(resolved)}`
3149
+ `For safety, sensitive-looking files must be attached explicitly with /attach: ${path9.basename(resolved)}`
3054
3150
  );
3055
3151
  }
3056
3152
  const linkInfo = await lstat2(resolved).catch(() => null);
@@ -3063,6 +3159,33 @@ async function loadAutomaticAttachment(cwd, requestedPath) {
3063
3159
  if (!info?.isFile()) return null;
3064
3160
  return loadResolvedAttachment(resolved, info.size);
3065
3161
  }
3162
+ async function loadMentionAttachment(cwd, requestedPath) {
3163
+ const safe = resolveInside(cwd, normalizeInputPath(requestedPath.trim()));
3164
+ if (!safe.ok || safe.relative === "") return null;
3165
+ if (isSensitivePath(safe.relative) || isSensitiveAutomaticPath(safe.path)) {
3166
+ throw new Error(
3167
+ `For safety, sensitive-looking files must be attached explicitly with /attach: ${path9.basename(safe.path)}`
3168
+ );
3169
+ }
3170
+ const info = await stat5(safe.path).catch(() => null);
3171
+ if (!info?.isFile()) return null;
3172
+ return loadResolvedAttachment(safe.path, info.size);
3173
+ }
3174
+ function withoutImages(messages) {
3175
+ let removed = 0;
3176
+ const cleaned = messages.map((message) => {
3177
+ if (!message.content.some((block) => block.type === "image")) return message;
3178
+ return {
3179
+ ...message,
3180
+ content: message.content.map((block) => {
3181
+ if (block.type !== "image") return block;
3182
+ removed += 1;
3183
+ return { type: "text", text: `[image ${block.name} removed: the model could not accept it]` };
3184
+ })
3185
+ };
3186
+ });
3187
+ return { messages: cleaned, removed };
3188
+ }
3066
3189
  var MAX_PATH_CHARS = 2048;
3067
3190
  function looksLikeAttachmentPath(value) {
3068
3191
  const trimmed = value.trim();
@@ -3070,14 +3193,14 @@ function looksLikeAttachmentPath(value) {
3070
3193
  if (trimmed.length > MAX_PATH_CHARS) return false;
3071
3194
  const candidate = normalizeInputPath(trimmed);
3072
3195
  if (/^file:\/\//i.test(candidate)) return true;
3073
- if (path8.isAbsolute(candidate)) return true;
3196
+ if (path9.isAbsolute(candidate)) return true;
3074
3197
  if (/^(?:~|\.{1,2})[\\/]/.test(candidate)) return true;
3075
3198
  if (candidate.includes("/") || candidate.includes("\\")) {
3076
3199
  if (trimmed.length > 200) return false;
3077
3200
  return true;
3078
3201
  }
3079
- const basename3 = path8.basename(candidate).toLowerCase();
3080
- return path8.extname(basename3) !== "" || AUTO_PATH_NAMES.has(basename3);
3202
+ const basename3 = path9.basename(candidate).toLowerCase();
3203
+ return path9.extname(basename3) !== "" || AUTO_PATH_NAMES.has(basename3);
3081
3204
  }
3082
3205
  async function loadClipboardImage(platform = process.platform, runner = runClipboardCommand) {
3083
3206
  const commands = clipboardCommands(platform);
@@ -3110,7 +3233,7 @@ async function loadClipboardImage(platform = process.platform, runner = runClipb
3110
3233
  throw new Error(`No supported image found in the clipboard. ${hint}`);
3111
3234
  }
3112
3235
  async function loadResolvedAttachment(resolved, size) {
3113
- const name = safeName(path8.basename(resolved));
3236
+ const name = safeName(path9.basename(resolved));
3114
3237
  const imageLimitCandidate = size <= MAX_IMAGE_BYTES;
3115
3238
  if (!imageLimitCandidate) {
3116
3239
  throw new Error(`Attachment is larger than ${formatBytes(MAX_IMAGE_BYTES)}: ${name}`);
@@ -3128,12 +3251,11 @@ async function loadResolvedAttachment(resolved, size) {
3128
3251
  if (data.length > MAX_TEXT_BYTES) {
3129
3252
  throw new Error(`Text attachment is larger than ${formatBytes(MAX_TEXT_BYTES)}: ${name}`);
3130
3253
  }
3131
- if (data.includes(0)) throw new Error(`Unsupported binary attachment: ${name}`);
3132
- let text;
3133
- try {
3134
- text = new TextDecoder("utf-8", { fatal: true }).decode(data);
3135
- } catch {
3136
- throw new Error(`Attachment is neither a supported image nor UTF-8 text: ${name}`);
3254
+ const text = decodeText(data);
3255
+ if (text === null) {
3256
+ throw new Error(
3257
+ data.includes(0) ? `Unsupported binary attachment: ${name}` : `Attachment is neither a supported image nor UTF-8 text: ${name}`
3258
+ );
3137
3259
  }
3138
3260
  return {
3139
3261
  path: resolved,
@@ -3142,13 +3264,13 @@ async function loadResolvedAttachment(resolved, size) {
3142
3264
  };
3143
3265
  }
3144
3266
  async function loadFirstImageFromDirectory(dirPath) {
3145
- const IMAGE_EXTENSIONS = /* @__PURE__ */ new Set([".png", ".jpg", ".jpeg", ".gif", ".webp", ".bmp"]);
3267
+ const IMAGE_EXTENSIONS = /* @__PURE__ */ new Set([".png", ".jpg", ".jpeg", ".gif", ".webp"]);
3146
3268
  const entries = await readdir3(dirPath, { withFileTypes: true }).catch(() => null);
3147
3269
  if (!entries) return null;
3148
- const sorted = entries.filter((entry) => entry.isFile() && IMAGE_EXTENSIONS.has(path8.extname(entry.name).toLowerCase())).sort((a, b) => a.name.localeCompare(b.name));
3270
+ const sorted = entries.filter((entry) => entry.isFile() && IMAGE_EXTENSIONS.has(path9.extname(entry.name).toLowerCase())).sort((a, b) => a.name.localeCompare(b.name));
3149
3271
  if (sorted.length === 0) return null;
3150
3272
  const first2 = sorted[0];
3151
- const filePath = path8.join(dirPath, first2.name);
3273
+ const filePath = path9.join(dirPath, first2.name);
3152
3274
  const info = await stat5(filePath).catch(() => null);
3153
3275
  if (!info?.isFile()) return null;
3154
3276
  return loadResolvedAttachment(filePath, info.size);
@@ -3158,23 +3280,78 @@ function resolveAttachmentPath(cwd, requestedPath) {
3158
3280
  if (!cleaned) throw new Error("Give a file path: /attach <path>");
3159
3281
  if (/^file:\/\//i.test(cleaned)) {
3160
3282
  try {
3161
- return path8.normalize(fileURLToPath(cleaned));
3283
+ return path9.normalize(fileURLToPath(cleaned));
3162
3284
  } catch {
3163
3285
  throw new Error(`Invalid local file URL: ${cleaned}`);
3164
3286
  }
3165
3287
  }
3166
- const expanded = cleaned === "~" ? os2.homedir() : cleaned.startsWith("~/") ? path8.join(os2.homedir(), cleaned.slice(2)) : cleaned;
3167
- return path8.isAbsolute(expanded) ? path8.normalize(expanded) : path8.resolve(cwd, expanded);
3288
+ const expanded = cleaned === "~" ? os2.homedir() : cleaned.startsWith("~/") ? path9.join(os2.homedir(), cleaned.slice(2)) : cleaned;
3289
+ return path9.isAbsolute(expanded) ? path9.normalize(expanded) : path9.resolve(cwd, expanded);
3168
3290
  }
3169
3291
  function isSensitiveAutomaticPath(file) {
3170
- const basename3 = path8.basename(file).toLowerCase();
3171
- return basename3.startsWith(".env.") || SENSITIVE_AUTO_NAMES.has(basename3) || SENSITIVE_AUTO_EXTENSIONS.has(path8.extname(basename3));
3292
+ const basename3 = path9.basename(file).toLowerCase();
3293
+ return (
3294
+ // Also covers ~/.ssh/config, ~/.aws/credentials, .git-credentials and the like.
3295
+ isSensitivePath(file) || basename3.startsWith(".env.") || SENSITIVE_AUTO_NAMES.has(basename3) || SENSITIVE_AUTO_EXTENSIONS.has(path9.extname(basename3))
3296
+ );
3172
3297
  }
3173
3298
  function attachmentLabel(block) {
3174
3299
  if (block.type === "image") return `image: ${block.name}`;
3175
3300
  if (block.type === "file") return `file: ${block.name}`;
3176
3301
  return null;
3177
3302
  }
3303
+ function decodeText(data) {
3304
+ try {
3305
+ if (data[0] === 255 && data[1] === 254) return new TextDecoder("utf-16le", { fatal: true }).decode(data);
3306
+ if (data[0] === 254 && data[1] === 255) return new TextDecoder("utf-16be", { fatal: true }).decode(data);
3307
+ if (data.includes(0)) return null;
3308
+ return new TextDecoder("utf-8", { fatal: true }).decode(data);
3309
+ } catch {
3310
+ return null;
3311
+ }
3312
+ }
3313
+ function imageSize(data, mediaType) {
3314
+ try {
3315
+ if (mediaType === "image/png" && data.length >= 24) {
3316
+ return { width: data.readUInt32BE(16), height: data.readUInt32BE(20) };
3317
+ }
3318
+ if (mediaType === "image/gif" && data.length >= 10) {
3319
+ return { width: data.readUInt16LE(6), height: data.readUInt16LE(8) };
3320
+ }
3321
+ if (mediaType === "image/webp" && data.length >= 30) {
3322
+ const chunk = data.subarray(12, 16).toString("ascii");
3323
+ if (chunk === "VP8X") return { width: data.readUIntLE(24, 3) + 1, height: data.readUIntLE(27, 3) + 1 };
3324
+ if (chunk === "VP8L") {
3325
+ const bits = data.readUInt32LE(21);
3326
+ return { width: (bits & 16383) + 1, height: (bits >> 14 & 16383) + 1 };
3327
+ }
3328
+ if (chunk === "VP8 ") return { width: data.readUInt16LE(26) & 16383, height: data.readUInt16LE(28) & 16383 };
3329
+ return null;
3330
+ }
3331
+ if (mediaType === "image/jpeg") {
3332
+ let offset = 2;
3333
+ while (offset + 9 < data.length) {
3334
+ if (data[offset] !== 255) return null;
3335
+ const marker = data[offset + 1];
3336
+ if (marker === 255) {
3337
+ offset += 1;
3338
+ continue;
3339
+ }
3340
+ if (marker >= 192 && marker <= 207 && marker !== 196 && marker !== 200 && marker !== 204) {
3341
+ return { width: data.readUInt16BE(offset + 7), height: data.readUInt16BE(offset + 5) };
3342
+ }
3343
+ if (marker === 216 || marker === 1 || marker >= 208 && marker <= 215) {
3344
+ offset += 2;
3345
+ continue;
3346
+ }
3347
+ offset += 2 + data.readUInt16BE(offset + 2);
3348
+ }
3349
+ }
3350
+ } catch {
3351
+ return null;
3352
+ }
3353
+ return null;
3354
+ }
3178
3355
  function detectImage(data) {
3179
3356
  if (data.length >= 8 && data.subarray(0, 8).equals(Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]))) {
3180
3357
  return "image/png";
@@ -3301,7 +3478,15 @@ var WINDOWS_CLIPBOARD_SCRIPT = String.raw`
3301
3478
  Add-Type -AssemblyName System.Windows.Forms
3302
3479
  Add-Type -AssemblyName System.Drawing
3303
3480
  $image = [System.Windows.Forms.Clipboard]::GetImage()
3304
- if ($null -eq $image) { exit 2 }
3481
+ if ($null -eq $image) {
3482
+ # An image file copied in Explorer arrives as a file list, not a bitmap.
3483
+ $files = [System.Windows.Forms.Clipboard]::GetFileDropList()
3484
+ $file = $files | Where-Object { $_ -match '\.(png|jpe?g|gif|webp)$' } | Select-Object -First 1
3485
+ if ($null -eq $file) { exit 2 }
3486
+ $bytes = [System.IO.File]::ReadAllBytes($file)
3487
+ [Console]::OpenStandardOutput().Write($bytes, 0, $bytes.Length)
3488
+ exit 0
3489
+ }
3305
3490
  $stream = New-Object System.IO.MemoryStream
3306
3491
  try {
3307
3492
  $image.Save($stream, [System.Drawing.Imaging.ImageFormat]::Png)
@@ -3313,13 +3498,16 @@ try {
3313
3498
  }
3314
3499
  `;
3315
3500
  var MAX_SENT_IMAGE_BYTES = 37e5;
3501
+ var MAX_IMAGE_EDGE = 8e3;
3316
3502
  var SHRINK_LONG_EDGE = 2e3;
3317
3503
  async function fitImage(data, mediaType, platform = process.platform, shrink = shrinkWithSystemTools) {
3318
- if (data.length <= MAX_SENT_IMAGE_BYTES) return { data, mediaType };
3319
- const dir = await mkdtemp(path8.join(os2.tmpdir(), "kitcode-image-"));
3504
+ const size = imageSize(data, mediaType);
3505
+ const tooTall = size !== null && Math.max(size.width, size.height) > MAX_IMAGE_EDGE;
3506
+ if (data.length <= MAX_SENT_IMAGE_BYTES && !tooTall) return { data, mediaType };
3507
+ const dir = await mkdtemp(path9.join(os2.tmpdir(), "kitcode-image-"));
3320
3508
  try {
3321
- const input = path8.join(dir, `in${extensionFor(mediaType)}`);
3322
- const output = path8.join(dir, "out.jpg");
3509
+ const input = path9.join(dir, `in${extensionFor(mediaType)}`);
3510
+ const output = path9.join(dir, "out.jpg");
3323
3511
  await writeFile6(input, data);
3324
3512
  try {
3325
3513
  await shrink(input, output, platform);
@@ -3334,7 +3522,7 @@ async function fitImage(data, mediaType, platform = process.platform, shrink = s
3334
3522
  }
3335
3523
  return { data: shrunk, mediaType: "image/jpeg" };
3336
3524
  } finally {
3337
- await rm4(dir, { recursive: true, force: true }).catch(() => void 0);
3525
+ await rm5(dir, { recursive: true, force: true }).catch(() => void 0);
3338
3526
  }
3339
3527
  }
3340
3528
  async function shrinkWithSystemTools(input, output, platform) {
@@ -3350,7 +3538,7 @@ async function shrinkWithSystemTools(input, output, platform) {
3350
3538
  await runQuiet("sips", ["-Z", String(SHRINK_LONG_EDGE), "-s", "format", "jpeg", "-s", "formatOptions", "85", input, "--out", output]);
3351
3539
  return;
3352
3540
  }
3353
- const args = [input, "-resize", `${SHRINK_LONG_EDGE}x${SHRINK_LONG_EDGE}>`, "-background", "white", "-flatten", "-quality", "85", output];
3541
+ const args = [input, "-auto-orient", "-resize", `${SHRINK_LONG_EDGE}x${SHRINK_LONG_EDGE}>`, "-background", "white", "-flatten", "-quality", "85", output];
3354
3542
  try {
3355
3543
  await runQuiet("magick", args);
3356
3544
  } catch {
@@ -3372,6 +3560,12 @@ var WINDOWS_SHRINK_SCRIPT = String.raw`
3372
3560
  Add-Type -AssemblyName System.Drawing
3373
3561
  $source = [System.Drawing.Image]::FromFile($env:KITCODE_IMAGE_IN)
3374
3562
  try {
3563
+ # Apply the EXIF orientation: the re-encoded JPEG drops the flag that kept a
3564
+ # phone photo upright.
3565
+ if ($source.PropertyIdList -contains 0x0112) {
3566
+ $flip = @{ 2 = 'RotateNoneFlipX'; 3 = 'Rotate180FlipNone'; 4 = 'Rotate180FlipX'; 5 = 'Rotate90FlipX'; 6 = 'Rotate90FlipNone'; 7 = 'Rotate270FlipX'; 8 = 'Rotate270FlipNone' }[[int]$source.GetPropertyItem(0x0112).Value[0]]
3567
+ if ($flip) { $source.RotateFlip([System.Drawing.RotateFlipType]$flip) }
3568
+ }
3375
3569
  $edge = [double]$env:KITCODE_IMAGE_EDGE
3376
3570
  $scale = [Math]::Min(1.0, $edge / [Math]::Max($source.Width, $source.Height))
3377
3571
  $width = [Math]::Max(1, [int]($source.Width * $scale))
@@ -3392,7 +3586,7 @@ try {
3392
3586
  }
3393
3587
  `;
3394
3588
  function textMime(file) {
3395
- const extension = path8.extname(file).toLowerCase();
3589
+ const extension = path9.extname(file).toLowerCase();
3396
3590
  const known = {
3397
3591
  ".json": "application/json",
3398
3592
  ".md": "text/markdown",
@@ -4120,11 +4314,14 @@ import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"
4120
4314
  import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
4121
4315
 
4122
4316
  // src/mcp/bridge.ts
4317
+ import { createHash as createHash4 } from "crypto";
4123
4318
  var MAX_RESULT_CHARS = 2e5;
4319
+ var TOOL_NAME = /^[a-zA-Z0-9_-]{1,64}$/;
4320
+ var MAX_TOOL_NAME = 64;
4124
4321
  function bridgeMcpTool(server, descriptor, call) {
4125
4322
  const tool = descriptor.name;
4126
4323
  return {
4127
- name: `mcp__${server}__${tool}`,
4324
+ name: mcpToolName(server, tool),
4128
4325
  description: descriptor.description ?? descriptor.title ?? tool,
4129
4326
  inputSchema: inputSchemaOf(descriptor),
4130
4327
  defaultPermission: "ask",
@@ -4133,6 +4330,13 @@ function bridgeMcpTool(server, descriptor, call) {
4133
4330
  execute: (input, ctx) => invoke(call, tool, input, ctx.signal)
4134
4331
  };
4135
4332
  }
4333
+ function mcpToolName(server, tool) {
4334
+ const raw = `mcp__${server}__${tool}`;
4335
+ if (TOOL_NAME.test(raw)) return raw;
4336
+ const hash = createHash4("sha256").update(raw).digest("hex").slice(0, 8);
4337
+ const cleaned = raw.replace(/[^a-zA-Z0-9_-]/g, "_");
4338
+ return `${cleaned.slice(0, MAX_TOOL_NAME - hash.length - 1)}_${hash}`;
4339
+ }
4136
4340
  function printableInput(input) {
4137
4341
  try {
4138
4342
  return JSON.stringify(input, null, 2);
@@ -4169,10 +4373,15 @@ function renderPart(part) {
4169
4373
  }
4170
4374
  function inputSchemaOf(descriptor) {
4171
4375
  const schema = descriptor.inputSchema;
4172
- if (schema === null || typeof schema !== "object") {
4376
+ if (schema === null || typeof schema !== "object" || Array.isArray(schema)) {
4173
4377
  return { type: "object", properties: {}, additionalProperties: false };
4174
4378
  }
4175
- return schema;
4379
+ const record3 = schema;
4380
+ return {
4381
+ ...record3,
4382
+ type: "object",
4383
+ properties: record3.properties && typeof record3.properties === "object" ? record3.properties : {}
4384
+ };
4176
4385
  }
4177
4386
  function argumentPreview(input) {
4178
4387
  if (input === null || typeof input !== "object") return "";
@@ -4191,7 +4400,7 @@ function clip(value) {
4191
4400
  // package.json
4192
4401
  var package_default = {
4193
4402
  name: "@kernelonpanic/kitcode",
4194
- version: "1.4.1",
4403
+ version: "1.4.2",
4195
4404
  description: "Terminal coding agent with a config you never have to write by hand",
4196
4405
  type: "module",
4197
4406
  license: "MIT",
@@ -4224,7 +4433,6 @@ var package_default = {
4224
4433
  dependencies: {
4225
4434
  "@anthropic-ai/sdk": "^0.115.0",
4226
4435
  "@modelcontextprotocol/sdk": "^1.30.0",
4227
- build: "^0.1.4",
4228
4436
  chalk: "^6.0.0",
4229
4437
  commander: "^15.0.0",
4230
4438
  "fast-glob": "^3.3.3",
@@ -4260,7 +4468,7 @@ var package_default = {
4260
4468
 
4261
4469
  // src/version.ts
4262
4470
  var KITCODE_VERSION = package_default.version;
4263
- var KITCODE_COMMIT = true ? "be753d255e7675a850d0ca1ee64f790f68d3fd93" : "development";
4471
+ var KITCODE_COMMIT = true ? "6b1ec0a742c44892447e71f477c068d8ba514c6a" : "development";
4264
4472
 
4265
4473
  // src/mcp/client.ts
4266
4474
  var clientInfo = { name: "kitcode", version: KITCODE_VERSION };
@@ -4321,6 +4529,7 @@ var blockedEnvKeys = /* @__PURE__ */ new Set([
4321
4529
  function createMcpManager(servers) {
4322
4530
  const serverStates = /* @__PURE__ */ new Map();
4323
4531
  const sessions = /* @__PURE__ */ new Map();
4532
+ const changeListeners = /* @__PURE__ */ new Set();
4324
4533
  for (const [name, config] of Object.entries(servers)) {
4325
4534
  serverStates.set(name, {
4326
4535
  name,
@@ -4342,14 +4551,15 @@ function createMcpManager(servers) {
4342
4551
  serverStates.set(name, { name, status: "connecting", toolCount: 0 });
4343
4552
  const client = new Client(clientInfo);
4344
4553
  client.onclose = () => {
4345
- if (sessions.delete(name)) {
4346
- serverStates.set(name, {
4347
- name,
4348
- status: "error",
4349
- toolCount: 0,
4350
- error: "connection closed"
4351
- });
4352
- }
4554
+ if (sessions.get(name)?.client !== client) return;
4555
+ sessions.delete(name);
4556
+ serverStates.set(name, {
4557
+ name,
4558
+ status: "error",
4559
+ toolCount: 0,
4560
+ error: "connection closed"
4561
+ });
4562
+ for (const listener of changeListeners) listener();
4353
4563
  };
4354
4564
  const transport = createTransport(config);
4355
4565
  const stderrTail = captureStderr(transport);
@@ -4415,7 +4625,12 @@ function createMcpManager(servers) {
4415
4625
  tools() {
4416
4626
  return [...sessions.values()].flatMap((session) => session.tools);
4417
4627
  },
4418
- callTool
4628
+ callTool,
4629
+ /** Called when a server drops on its own, outside connect/disconnect. */
4630
+ onChange(listener) {
4631
+ changeListeners.add(listener);
4632
+ return () => changeListeners.delete(listener);
4633
+ }
4419
4634
  };
4420
4635
  }
4421
4636
  async function handshake(client, transport) {
@@ -4551,8 +4766,8 @@ function balanceEndpoints(baseUrl) {
4551
4766
  endpoint(url.origin, "/v1/balance", genericBalance)
4552
4767
  ];
4553
4768
  }
4554
- function endpoint(origin, path15, parse2) {
4555
- return { url: `${origin}${path15}`, parse: parse2 };
4769
+ function endpoint(origin, path16, parse2) {
4770
+ return { url: `${origin}${path16}`, parse: parse2 };
4556
4771
  }
4557
4772
  function deepSeekBalance(body) {
4558
4773
  const infos = record2(body)?.["balance_infos"];
@@ -4646,7 +4861,7 @@ async function limitedResponseText(response, limit) {
4646
4861
 
4647
4862
  // src/providers/catalog.ts
4648
4863
  import { chmod as chmod4, readFile as readFile8, writeFile as writeFile7 } from "fs/promises";
4649
- import path9 from "path";
4864
+ import path10 from "path";
4650
4865
  var MAX_AGE_MS = 24 * 60 * 60 * 1e3;
4651
4866
  var CACHE_VERSION = 3;
4652
4867
  async function loadModels(provider, refresh = false) {
@@ -4671,7 +4886,7 @@ function refreshModels(provider) {
4671
4886
  return loadModels(provider, true);
4672
4887
  }
4673
4888
  function cacheFile(providerId) {
4674
- return path9.join(cacheDir, "models", `${providerId.replace(/[^\w.-]/g, "_")}.json`);
4889
+ return path10.join(cacheDir, "models", `${providerId.replace(/[^\w.-]/g, "_")}.json`);
4675
4890
  }
4676
4891
  async function readCache(file) {
4677
4892
  try {
@@ -4706,7 +4921,7 @@ function validModel(value) {
4706
4921
  async function writeCache(file, models) {
4707
4922
  const payload = { version: CACHE_VERSION, fetchedAt: Date.now(), models };
4708
4923
  try {
4709
- await ensureDir(path9.dirname(file));
4924
+ await ensureDir(path10.dirname(file));
4710
4925
  await writeFile7(file, JSON.stringify(payload), { encoding: "utf8", mode: 384 });
4711
4926
  await chmod4(file, 384);
4712
4927
  } catch {
@@ -5212,7 +5427,7 @@ function toContentBlocks2(providerId, thinking, text, calls) {
5212
5427
  type: "tool_use",
5213
5428
  id: call.id || tmpId(position),
5214
5429
  name: call.name,
5215
- input: parseArguments(providerId, call.name, call.args)
5430
+ input: parseArguments(call.args)
5216
5431
  });
5217
5432
  }
5218
5433
  return content;
@@ -5223,21 +5438,16 @@ function toTextContentBlocks(thinking, text) {
5223
5438
  if (text) content.push({ type: "text", text });
5224
5439
  return content;
5225
5440
  }
5226
- function parseArguments(providerId, toolName, args) {
5441
+ function parseArguments(args) {
5442
+ if (args.trim() === "") return {};
5227
5443
  let parsed;
5228
5444
  try {
5229
5445
  parsed = JSON.parse(args);
5230
5446
  } catch {
5231
- throw new ProviderError(
5232
- `Tool "${toolName || "unknown"}" returned malformed JSON arguments; the tool was not run.`,
5233
- providerId
5234
- );
5447
+ parsed = void 0;
5235
5448
  }
5236
5449
  if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
5237
- throw new ProviderError(
5238
- `Tool "${toolName || "unknown"}" arguments must be a JSON object; the tool was not run.`,
5239
- providerId
5240
- );
5450
+ return { [INVALID_ARGUMENTS_KEY]: args.slice(0, 300) };
5241
5451
  }
5242
5452
  return parsed;
5243
5453
  }
@@ -5382,6 +5592,7 @@ function createPermissionEngine(configPermissions) {
5382
5592
  const configured = resolve3(tool, requested);
5383
5593
  if (configured === "deny") return "deny";
5384
5594
  if (current === "plan" && tool.readOnly !== true) return "deny";
5595
+ if (requested === "ask") return bypassEnabled ? "allow" : "ask";
5385
5596
  if (configured === "allow") return "allow";
5386
5597
  if (bypassEnabled) return "allow";
5387
5598
  if (current === "accept" && isFileEdit(tool)) return "allow";
@@ -5494,9 +5705,24 @@ function done(result) {
5494
5705
  // src/tools/edit.ts
5495
5706
  import { readFile as readFile9, stat as stat6 } from "fs/promises";
5496
5707
 
5708
+ // src/tools/protected.ts
5709
+ import path11 from "path";
5710
+ function protectedWritePermission(input, ctx) {
5711
+ const target = input?.path;
5712
+ if (typeof target !== "string") return void 0;
5713
+ const relative2 = ctx ? resolveInside(ctx.cwd, target) : void 0;
5714
+ const candidate = relative2?.ok ? relative2.relative : target;
5715
+ return isProtectedPath(candidate) ? "ask" : void 0;
5716
+ }
5717
+ function isProtectedPath(value) {
5718
+ const parts = path11.normalize(value).replaceAll("\\", "/").toLowerCase().split("/").filter(Boolean);
5719
+ const name = parts.at(-1) ?? "";
5720
+ return name === "kitcode.json" || name === ".envrc" || parts.includes(".kitcode") || parts.includes(".git");
5721
+ }
5722
+
5497
5723
  // src/tools/safe-write.ts
5498
5724
  import { randomUUID as randomUUID3 } from "crypto";
5499
- import { chmod as chmod5, lstat as lstat3, open, rename as rename5, rm as rm5 } from "fs/promises";
5725
+ import { chmod as chmod5, lstat as lstat3, open, rename as rename5, rm as rm6 } from "fs/promises";
5500
5726
  import { basename as basename2, dirname as dirname2, join as join2 } from "path";
5501
5727
  var UnsafeFileChangeError = class extends Error {
5502
5728
  constructor(message) {
@@ -5559,7 +5785,7 @@ async function atomicWriteSafeFile(file, data, snapshot) {
5559
5785
  await rename5(temp, file);
5560
5786
  } catch (error) {
5561
5787
  await handle?.close().catch(() => void 0);
5562
- await rm5(temp, { force: true }).catch(() => void 0);
5788
+ await rm6(temp, { force: true }).catch(() => void 0);
5563
5789
  throw error;
5564
5790
  }
5565
5791
  }
@@ -5650,60 +5876,61 @@ var editTool = {
5650
5876
  additionalProperties: false
5651
5877
  },
5652
5878
  defaultPermission: "ask",
5879
+ permission: protectedWritePermission,
5653
5880
  summarize(input) {
5654
5881
  return `edit(${brief(input.path)})`;
5655
5882
  },
5656
5883
  async preview(input, ctx) {
5657
- const { path: path15, oldString, newString, replaceAll = false } = input;
5658
- const safe = resolveInside(ctx.cwd, path15);
5884
+ const { path: path16, oldString, newString, replaceAll = false } = input;
5885
+ const safe = resolveInside(ctx.cwd, path16);
5659
5886
  if (!safe.ok) return { kind: "text", text: safe.reason };
5660
5887
  const info = await stat6(safe.path).catch(() => null);
5661
5888
  if (info && info.size > MAX_FILE_BYTES2) {
5662
- return { kind: "text", text: `Cannot preview ${path15}: the file is too large.` };
5889
+ return { kind: "text", text: `Cannot preview ${path16}: the file is too large.` };
5663
5890
  }
5664
5891
  const beforeBuffer = await readFile9(safe.path).catch(() => null);
5665
- if (!beforeBuffer) return { kind: "text", text: `Cannot read ${path15}.` };
5892
+ if (!beforeBuffer) return { kind: "text", text: `Cannot read ${path16}.` };
5666
5893
  if (beforeBuffer.subarray(0, 8192).includes(0)) {
5667
- return { kind: "text", text: `Cannot preview binary file ${path15}` };
5894
+ return { kind: "text", text: `Cannot preview binary file ${path16}` };
5668
5895
  }
5669
5896
  const before = beforeBuffer.toString("utf8");
5670
5897
  const after = replacement(before, oldString, newString, replaceAll);
5671
- return after === null ? { kind: "text", text: `Cannot preview edit: oldString is not a valid match in ${path15}.` } : { kind: "diff", path: path15, before, after };
5898
+ return after === null ? { kind: "text", text: `Cannot preview edit: oldString is not a valid match in ${path16}.` } : { kind: "diff", path: path16, before, after };
5672
5899
  },
5673
5900
  async execute(input, ctx) {
5674
- const { path: path15, oldString, newString, replaceAll = false } = input;
5675
- const safe = resolveInside(ctx.cwd, path15);
5901
+ const { path: path16, oldString, newString, replaceAll = false } = input;
5902
+ const safe = resolveInside(ctx.cwd, path16);
5676
5903
  if (!safe.ok) return { content: safe.reason, isError: true };
5677
5904
  let snapshot;
5678
5905
  try {
5679
5906
  snapshot = await readSafeFileSnapshot(safe.path, MAX_FILE_BYTES2);
5680
5907
  } catch (error) {
5681
- return { content: `Cannot edit ${path15}: ${error.message}.`, isError: true };
5908
+ return { content: `Cannot edit ${path16}: ${error.message}.`, isError: true };
5682
5909
  }
5683
- if (!snapshot.exists) return { content: `Cannot read ${path15}.`, isError: true };
5910
+ if (!snapshot.exists) return { content: `Cannot read ${path16}.`, isError: true };
5684
5911
  const beforeBuffer = snapshot.data;
5685
5912
  if (beforeBuffer.subarray(0, 8192).includes(0)) {
5686
- return { content: `Cannot edit ${path15}: it is a binary file, not text.`, isError: true };
5913
+ return { content: `Cannot edit ${path16}: it is a binary file, not text.`, isError: true };
5687
5914
  }
5688
5915
  const before = beforeBuffer.toString("utf8");
5689
5916
  const count2 = countOccurrences(before, oldString);
5690
5917
  if (count2 === 0) {
5691
5918
  return {
5692
- content: `oldString was not found in ${path15}. Read the file again and match the text exactly, including whitespace.`,
5919
+ content: `oldString was not found in ${path16}. Read the file again and match the text exactly, including whitespace.`,
5693
5920
  isError: true
5694
5921
  };
5695
5922
  }
5696
5923
  if (count2 > 1 && !replaceAll) {
5697
5924
  return {
5698
- content: `oldString matches ${count2} places in ${path15}. Add more surrounding context to identify a single one, or set replaceAll to true.`,
5925
+ content: `oldString matches ${count2} places in ${path16}. Add more surrounding context to identify a single one, or set replaceAll to true.`,
5699
5926
  isError: true
5700
5927
  };
5701
5928
  }
5702
5929
  const after = replacement(before, oldString, newString, replaceAll);
5703
- if (after === null) return { content: `Could not prepare edit for ${path15}.`, isError: true };
5930
+ if (after === null) return { content: `Could not prepare edit for ${path16}.`, isError: true };
5704
5931
  if (Buffer.byteLength(after, "utf8") > MAX_FILE_BYTES2) {
5705
5932
  return {
5706
- content: `Cannot edit ${path15}: the result exceeds the ${MAX_FILE_BYTES2 / 1e6} MB limit.`,
5933
+ content: `Cannot edit ${path16}: the result exceeds the ${MAX_FILE_BYTES2 / 1e6} MB limit.`,
5707
5934
  isError: true
5708
5935
  };
5709
5936
  }
@@ -5716,11 +5943,11 @@ var editTool = {
5716
5943
  await atomicWriteSafeFile(safe.path, after, snapshot);
5717
5944
  ctx.checkpoint?.markChanged(safe.path);
5718
5945
  } catch (error) {
5719
- return { content: `Failed to write ${path15}: ${error.message}`, isError: true };
5946
+ return { content: `Failed to write ${path16}: ${error.message}`, isError: true };
5720
5947
  }
5721
5948
  return {
5722
- content: `Edited ${path15} (${count2} ${count2 === 1 ? "replacement" : "replacements"})`,
5723
- display: { kind: "diff", path: path15, before, after }
5949
+ content: `Edited ${path16} (${count2} ${count2 === 1 ? "replacement" : "replacements"})`,
5950
+ display: { kind: "diff", path: path16, before, after }
5724
5951
  };
5725
5952
  }
5726
5953
  };
@@ -5737,45 +5964,6 @@ import { readFile as readFile10, stat as stat7 } from "fs/promises";
5737
5964
  import { join as join3 } from "path";
5738
5965
  import fg3 from "fast-glob";
5739
5966
 
5740
- // src/tools/sensitive.ts
5741
- import path10 from "path";
5742
- var sensitiveNames = /* @__PURE__ */ new Set([
5743
- ".npmrc",
5744
- ".pypirc",
5745
- ".git-credentials",
5746
- ".netrc",
5747
- ".pgpass",
5748
- ".my.cnf",
5749
- ".docker/config.json",
5750
- "auth.json",
5751
- "credentials",
5752
- "credentials.json",
5753
- "id_rsa",
5754
- "id_ed25519",
5755
- "id_dsa",
5756
- "id_ecdsa",
5757
- "kubeconfig",
5758
- "known_hosts",
5759
- "secret.json",
5760
- "secrets.json",
5761
- "tokens.json",
5762
- "service-account.json"
5763
- ]);
5764
- var sensitiveExtensions = /* @__PURE__ */ new Set([".pem", ".key", ".p12", ".pfx", ".jks", ".keystore", ".p8", ".der"]);
5765
- function isSensitivePath(value) {
5766
- if (typeof value !== "string") return false;
5767
- const normalized = value.replaceAll("\\", "/").toLowerCase();
5768
- const name = path10.posix.basename(normalized);
5769
- if (name === ".env" || name.startsWith(".env.")) return true;
5770
- if (sensitiveNames.has(name) || sensitiveExtensions.has(path10.posix.extname(name))) return true;
5771
- return normalized.split("/").some((part) => part === ".ssh" || part === ".aws");
5772
- }
5773
- function mentionsSensitivePattern(value) {
5774
- if (typeof value !== "string") return false;
5775
- const lower = value.toLowerCase();
5776
- return isSensitivePath(lower) || lower.includes(".env") || lower.includes(".npmrc") || lower.includes(".netrc") || lower.includes(".pgpass") || lower.includes("auth.json") || lower.includes("credentials") || lower.includes("kubeconfig") || lower.includes("secret") || lower.includes("tokens.json") || /\.(?:pem|key|p12|pfx|jks|keystore|p8|der)(?:$|[^a-z0-9])/.test(lower);
5777
- }
5778
-
5779
5967
  // src/tools/regex.ts
5780
5968
  import { Worker } from "worker_threads";
5781
5969
  var MATCH_TIMEOUT_MS = 300;
@@ -5885,16 +6073,16 @@ var grepTool = {
5885
6073
  defaultPermission: "allow",
5886
6074
  readOnly: true,
5887
6075
  permission(input, ctx) {
5888
- const { path: path15, glob } = input ?? {};
5889
- if (isSensitivePath(path15) || mentionsSensitivePattern(glob)) return "ask";
5890
- const safe = ctx && path15 ? resolveInside(ctx.cwd, path15) : void 0;
6076
+ const { path: path16, glob } = input ?? {};
6077
+ if (isSensitivePath(path16) || mentionsSensitivePattern(glob)) return "ask";
6078
+ const safe = ctx && path16 ? resolveInside(ctx.cwd, path16) : void 0;
5891
6079
  return safe?.ok && isSensitivePath(safe.relative) ? "ask" : void 0;
5892
6080
  },
5893
6081
  summarize(input) {
5894
6082
  return `grep(${brief(input.pattern)})`;
5895
6083
  },
5896
6084
  async execute(input, ctx) {
5897
- const { pattern, path: path15 = ".", glob = "**/*", maxMatches } = input;
6085
+ const { pattern, path: path16 = ".", glob = "**/*", maxMatches } = input;
5898
6086
  if (pattern.length > MAX_PATTERN_CHARS) {
5899
6087
  return {
5900
6088
  content: `Regular expression is limited to ${MAX_PATTERN_CHARS} characters.`,
@@ -5909,10 +6097,10 @@ var grepTool = {
5909
6097
  if (patternEscapes(glob)) {
5910
6098
  return { content: `Glob ${glob} must be relative to the workspace root.`, isError: true };
5911
6099
  }
5912
- const safe = resolveInside(ctx.cwd, path15);
6100
+ const safe = resolveInside(ctx.cwd, path16);
5913
6101
  if (!safe.ok) return { content: safe.reason, isError: true };
5914
6102
  const target = await stat7(safe.path).catch(() => null);
5915
- if (!target) return { content: `Path not found: ${path15}`, isError: true };
6103
+ if (!target) return { content: `Path not found: ${path16}`, isError: true };
5916
6104
  const discovered = target.isDirectory() ? (await fg3(glob, {
5917
6105
  cwd: safe.path,
5918
6106
  dot: false,
@@ -5926,7 +6114,8 @@ var grepTool = {
5926
6114
  relative: join3(safe.relative, entry.path),
5927
6115
  size: entry.stats?.size ?? 0
5928
6116
  })) : [{ absolute: safe.path, relative: safe.relative, size: target.size }];
5929
- const files = discovered.slice(0, MAX_FILES);
6117
+ const sensitiveRequested = isSensitivePath(path16) || isSensitivePath(safe.relative) || mentionsSensitivePattern(glob);
6118
+ const files = (sensitiveRequested ? discovered : discovered.filter((file) => !isSensitivePath(file.relative))).slice(0, MAX_FILES);
5930
6119
  const filesCapped = discovered.length > MAX_FILES;
5931
6120
  const limit = Math.min(maxMatches ?? MAX_MATCHES, MAX_MATCHES);
5932
6121
  const hits = [];
@@ -6002,30 +6191,30 @@ var readTool = {
6002
6191
  return `read(${brief(input.path)})`;
6003
6192
  },
6004
6193
  async execute(input, ctx) {
6005
- const { path: path15, offset = 1, limit } = input;
6006
- const safe = resolveInside(ctx.cwd, path15);
6194
+ const { path: path16, offset = 1, limit } = input;
6195
+ const safe = resolveInside(ctx.cwd, path16);
6007
6196
  if (!safe.ok) return { content: safe.reason, isError: true };
6008
6197
  const info = await stat8(safe.path).catch(() => null);
6009
- if (!info) return { content: `File not found: ${path15}`, isError: true };
6198
+ if (!info) return { content: `File not found: ${path16}`, isError: true };
6010
6199
  if (info.isDirectory()) {
6011
- return { content: `${path15} is a directory, not a file. Use glob to list its contents.`, isError: true };
6200
+ return { content: `${path16} is a directory, not a file. Use glob to list its contents.`, isError: true };
6012
6201
  }
6013
6202
  if (info.size > MAX_FILE_BYTES4) {
6014
6203
  return {
6015
- 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.`,
6204
+ content: `${path16} 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.`,
6016
6205
  isError: true
6017
6206
  };
6018
6207
  }
6019
6208
  const buffer = await readFile11(safe.path);
6020
6209
  if (buffer.subarray(0, 4096).includes(0)) {
6021
- return { content: `${path15} looks like a binary file and cannot be read as text.`, isError: true };
6210
+ return { content: `${path16} looks like a binary file and cannot be read as text.`, isError: true };
6022
6211
  }
6023
6212
  const lines = buffer.toString("utf8").split("\n");
6024
6213
  if (lines.at(-1) === "") lines.pop();
6025
- if (lines.length === 0) return { content: `${path15} is empty.` };
6214
+ if (lines.length === 0) return { content: `${path16} is empty.` };
6026
6215
  const start = Math.max(1, offset) - 1;
6027
6216
  if (start >= lines.length) {
6028
- return { content: `${path15} has ${lines.length} lines, so offset ${offset} is past the end.`, isError: true };
6217
+ return { content: `${path16} has ${lines.length} lines, so offset ${offset} is past the end.`, isError: true };
6029
6218
  }
6030
6219
  const window = lines.slice(start, start + Math.min(limit ?? MAX_LINES, MAX_LINES));
6031
6220
  const rendered = [];
@@ -6068,68 +6257,69 @@ var writeTool = {
6068
6257
  additionalProperties: false
6069
6258
  },
6070
6259
  defaultPermission: "ask",
6260
+ permission: protectedWritePermission,
6071
6261
  summarize(input) {
6072
6262
  return `write(${brief(input.path)})`;
6073
6263
  },
6074
6264
  async preview(input, ctx) {
6075
- const { path: path15, content } = input;
6265
+ const { path: path16, content } = input;
6076
6266
  if (Buffer.byteLength(content, "utf8") > MAX_FILE_BYTES5) {
6077
6267
  return { kind: "text", text: `Write is limited to ${MAX_FILE_BYTES5 / 1e6} MB.` };
6078
6268
  }
6079
- const safe = resolveInside(ctx.cwd, path15);
6269
+ const safe = resolveInside(ctx.cwd, path16);
6080
6270
  if (!safe.ok) return { kind: "text", text: safe.reason };
6081
6271
  const beforeInfo = await stat9(safe.path).catch(() => null);
6082
6272
  if (beforeInfo && beforeInfo.size > MAX_FILE_BYTES5) {
6083
- return { kind: "text", text: `Cannot preview ${path15}: the existing file is too large.` };
6273
+ return { kind: "text", text: `Cannot preview ${path16}: the existing file is too large.` };
6084
6274
  }
6085
6275
  const beforeBuffer = await readFile12(safe.path).catch(() => null);
6086
6276
  if (beforeBuffer?.subarray(0, 8192).includes(0)) {
6087
- return { kind: "text", text: `Cannot preview binary file ${path15}` };
6277
+ return { kind: "text", text: `Cannot preview binary file ${path16}` };
6088
6278
  }
6089
6279
  return {
6090
6280
  kind: "diff",
6091
- path: path15,
6281
+ path: path16,
6092
6282
  before: beforeBuffer?.toString("utf8") ?? "",
6093
6283
  after: content
6094
6284
  };
6095
6285
  },
6096
6286
  async execute(input, ctx) {
6097
- const { path: path15, content } = input;
6287
+ const { path: path16, content } = input;
6098
6288
  if (Buffer.byteLength(content, "utf8") > MAX_FILE_BYTES5) {
6099
6289
  return {
6100
- content: `Cannot write ${path15}: content exceeds the ${MAX_FILE_BYTES5 / 1e6} MB limit.`,
6290
+ content: `Cannot write ${path16}: content exceeds the ${MAX_FILE_BYTES5 / 1e6} MB limit.`,
6101
6291
  isError: true
6102
6292
  };
6103
6293
  }
6104
- const safe = resolveInside(ctx.cwd, path15);
6294
+ const safe = resolveInside(ctx.cwd, path16);
6105
6295
  if (!safe.ok) return { content: safe.reason, isError: true };
6106
6296
  let target = safe.path;
6107
6297
  let snapshot;
6108
6298
  try {
6109
6299
  await mkdir4(dirname3(target), { recursive: true });
6110
- const rechecked = resolveInside(ctx.cwd, path15);
6300
+ const rechecked = resolveInside(ctx.cwd, path16);
6111
6301
  if (!rechecked.ok || rechecked.path !== target) {
6112
6302
  return {
6113
- content: `Cannot write ${path15}: the path changed while its parent was created.`,
6303
+ content: `Cannot write ${path16}: the path changed while its parent was created.`,
6114
6304
  isError: true
6115
6305
  };
6116
6306
  }
6117
6307
  target = rechecked.path;
6118
6308
  snapshot = await readSafeFileSnapshot(target, MAX_FILE_BYTES5);
6119
6309
  if (snapshot.exists && snapshot.data.subarray(0, 8192).includes(0)) {
6120
- return { content: `Cannot write ${path15}: the existing file is binary, not text.`, isError: true };
6310
+ return { content: `Cannot write ${path16}: the existing file is binary, not text.`, isError: true };
6121
6311
  }
6122
6312
  await ctx.checkpoint?.capture(target);
6123
6313
  await atomicWriteSafeFile(target, content, snapshot);
6124
6314
  ctx.checkpoint?.markChanged(target);
6125
6315
  } catch (error) {
6126
- return { content: `Failed to write ${path15}: ${error.message}`, isError: true };
6316
+ return { content: `Failed to write ${path16}: ${error.message}`, isError: true };
6127
6317
  }
6128
6318
  const before = snapshot.exists ? snapshot.data.toString("utf8") : "";
6129
6319
  const lines = content === "" ? 0 : content.replace(/\n$/, "").split("\n").length;
6130
6320
  return {
6131
- content: `${before === "" ? "Created" : "Updated"} ${path15} (${lines} ${lines === 1 ? "line" : "lines"})`,
6132
- display: { kind: "diff", path: path15, before, after: content }
6321
+ content: `${before === "" ? "Created" : "Updated"} ${path16} (${lines} ${lines === 1 ? "line" : "lines"})`,
6322
+ display: { kind: "diff", path: path16, before, after: content }
6133
6323
  };
6134
6324
  }
6135
6325
  };
@@ -6159,9 +6349,9 @@ function createToolRegistry(tools) {
6159
6349
 
6160
6350
  // src/prompts/library.ts
6161
6351
  import { chmod as chmod6, readFile as readFile13, readdir as readdir4, unlink as unlink3, writeFile as writeFile8 } from "fs/promises";
6162
- import path11 from "path";
6352
+ import path12 from "path";
6163
6353
  async function savePrompt(input) {
6164
- const slug = slugify(input.name);
6354
+ const slug = promptSlug(input.name);
6165
6355
  if (!slug) throw new Error(`Prompt name has no letters or digits to name a file after: ${input.name}`);
6166
6356
  const prompt = {
6167
6357
  slug,
@@ -6171,7 +6361,7 @@ async function savePrompt(input) {
6171
6361
  body: input.body.trim()
6172
6362
  };
6173
6363
  await ensureDir(promptsDir);
6174
- const file = path11.join(promptsDir, `${slug}.md`);
6364
+ const file = path12.join(promptsDir, `${slug}.md`);
6175
6365
  await writeFile8(file, serialize(prompt), {
6176
6366
  encoding: "utf8",
6177
6367
  mode: 384
@@ -6182,7 +6372,7 @@ async function savePrompt(input) {
6182
6372
  async function getPrompt(slug) {
6183
6373
  assertSlug(slug);
6184
6374
  try {
6185
- return parse(slug, await readFile13(path11.join(promptsDir, `${slug}.md`), "utf8"));
6375
+ return parse(slug, await readFile13(path12.join(promptsDir, `${slug}.md`), "utf8"));
6186
6376
  } catch (error) {
6187
6377
  if (isMissing(error)) return null;
6188
6378
  throw error;
@@ -6197,16 +6387,27 @@ async function listPrompts() {
6197
6387
  return [];
6198
6388
  }
6199
6389
  const prompts2 = await Promise.all(
6200
- names.filter((name) => name.endsWith(".md")).map(
6201
- async (name) => parse(name.slice(0, -3), await readFile13(path11.join(promptsDir, name), "utf8"))
6202
- )
6390
+ names.filter((name) => name.endsWith(".md") && validSlug(name.slice(0, -3))).map(async (name) => {
6391
+ const text = await readFile13(path12.join(promptsDir, name), "utf8").catch(() => null);
6392
+ return text === null ? null : parse(name.slice(0, -3), text);
6393
+ })
6203
6394
  );
6204
- return prompts2.sort((a, b) => a.name.localeCompare(b.name));
6395
+ return prompts2.filter((prompt) => prompt !== null).sort((a, b) => a.name.localeCompare(b.name));
6205
6396
  }
6206
6397
  async function deletePrompt(slug) {
6207
6398
  assertSlug(slug);
6208
6399
  try {
6209
- await unlink3(path11.join(promptsDir, `${slug}.md`));
6400
+ await unlink3(path12.join(promptsDir, `${slug}.md`));
6401
+ return true;
6402
+ } catch (error) {
6403
+ if (isMissing(error)) return false;
6404
+ throw error;
6405
+ }
6406
+ }
6407
+ async function promptExists(slug) {
6408
+ assertSlug(slug);
6409
+ try {
6410
+ await readFile13(path12.join(promptsDir, `${slug}.md`), "utf8");
6210
6411
  return true;
6211
6412
  } catch (error) {
6212
6413
  if (isMissing(error)) return false;
@@ -6216,11 +6417,16 @@ async function deletePrompt(slug) {
6216
6417
  function isMissing(error) {
6217
6418
  return error.code === "ENOENT";
6218
6419
  }
6219
- function slugify(name) {
6220
- return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
6420
+ var RESERVED_NAMES = /^(?:con|prn|aux|nul|com[0-9]|lpt[0-9])$/;
6421
+ function promptSlug(name) {
6422
+ const slug = name.normalize("NFC").toLowerCase().replace(/[^\p{L}\p{N}]+/gu, "-").replace(/^-+|-+$/g, "").slice(0, 80).replace(/-+$/, "");
6423
+ return RESERVED_NAMES.test(slug) ? `${slug}-prompt` : slug;
6424
+ }
6425
+ function validSlug(slug) {
6426
+ return /^[\p{L}\p{N}]+(?:-[\p{L}\p{N}]+)*$/u.test(slug) && !RESERVED_NAMES.test(slug);
6221
6427
  }
6222
6428
  function assertSlug(slug) {
6223
- if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(slug)) {
6429
+ if (!validSlug(slug)) {
6224
6430
  throw new Error(`Invalid prompt id: ${slug}`);
6225
6431
  }
6226
6432
  }
@@ -6264,7 +6470,7 @@ function parse(slug, text) {
6264
6470
 
6265
6471
  // src/skills/library.ts
6266
6472
  import { lstat as lstat4, open as open2, readdir as readdir5 } from "fs/promises";
6267
- import path12 from "path";
6473
+ import path13 from "path";
6268
6474
  var SKILL_FILE = "SKILL.md";
6269
6475
  var FRONTMATTER_BYTES = 8192;
6270
6476
  var MAX_SKILL_BYTES = 5e6;
@@ -6277,7 +6483,7 @@ async function discoverSkills(dirs) {
6277
6483
  if (!rootInfo?.isDirectory() || rootInfo.isSymbolicLink()) continue;
6278
6484
  const entries = await readdir5(root, { withFileTypes: true }).catch(() => []);
6279
6485
  const found = await Promise.all(
6280
- entries.filter((entry) => entry.isDirectory() && !entry.isSymbolicLink()).slice(0, MAX_SKILLS_PER_ROOT).map((entry) => readMeta(root, rootInfo, path12.join(root, entry.name)))
6486
+ entries.filter((entry) => entry.isDirectory() && !entry.isSymbolicLink()).slice(0, MAX_SKILLS_PER_ROOT).map((entry) => readMeta(root, rootInfo, path13.join(root, entry.name)))
6281
6487
  );
6282
6488
  for (const meta of found) {
6283
6489
  if (meta && !byName.has(meta.name)) byName.set(meta.name, meta);
@@ -6317,11 +6523,11 @@ function formatSkillCatalogue(skills) {
6317
6523
  async function readMeta(root, rootInfo, dir) {
6318
6524
  const dirInfo = await lstat4(dir).catch(() => null);
6319
6525
  if (!dirInfo?.isDirectory() || dirInfo.isSymbolicLink()) return null;
6320
- const file = path12.join(dir, SKILL_FILE);
6526
+ const file = path13.join(dir, SKILL_FILE);
6321
6527
  const head = await readFrontmatterBytes(file);
6322
6528
  if (!head) return null;
6323
6529
  const { fields } = parseFrontmatter(head.text);
6324
- const meta = { name: fields.name || path12.basename(dir), description: fields.description ?? "", dir, file };
6530
+ const meta = { name: fields.name || path13.basename(dir), description: fields.description ?? "", dir, file };
6325
6531
  guards.set(meta, {
6326
6532
  root,
6327
6533
  rootIdentity: identity(rootInfo),
@@ -6393,10 +6599,21 @@ function parseFrontmatter(text) {
6393
6599
  if (lines[0]?.trim() === "---") {
6394
6600
  let i = 1;
6395
6601
  for (; i < lines.length && lines[i].trim() !== "---"; i++) {
6396
- const colon = lines[i].indexOf(":");
6602
+ const line = lines[i];
6603
+ if (/^\s/.test(line)) continue;
6604
+ const colon = line.indexOf(":");
6397
6605
  if (colon <= 0) continue;
6398
- const key = lines[i].slice(0, colon).trim();
6399
- if (key === "name" || key === "description") fields[key] = lines[i].slice(colon + 1).trim();
6606
+ const key = line.slice(0, colon).trim();
6607
+ if (key !== "name" && key !== "description") continue;
6608
+ let value = line.slice(colon + 1).trim();
6609
+ if (/^[>|][+-]?$/.test(value)) {
6610
+ const block = [];
6611
+ while (i + 1 < lines.length && /^\s+\S/.test(lines[i + 1]) && lines[i + 1].trim() !== "---") {
6612
+ block.push(lines[++i].trim());
6613
+ }
6614
+ value = block.join(value.startsWith("|") ? "\n" : " ");
6615
+ }
6616
+ fields[key] = value.replace(/^(["'])(.*)\1$/, "$2").trim();
6400
6617
  }
6401
6618
  bodyStart = Math.min(i + 1, lines.length);
6402
6619
  }
@@ -6404,174 +6621,238 @@ function parseFrontmatter(text) {
6404
6621
  }
6405
6622
 
6406
6623
  // src/skills/install.ts
6624
+ import { execFile as execFile2 } from "child_process";
6407
6625
  import { randomUUID as randomUUID4 } from "crypto";
6408
- import { execFileSync } from "child_process";
6409
- import {
6410
- existsSync as existsSync2,
6411
- lstatSync,
6412
- mkdirSync,
6413
- readFileSync,
6414
- readdirSync,
6415
- realpathSync as realpathSync2,
6416
- rmSync,
6417
- writeFileSync
6418
- } from "fs";
6419
- import { chmod as chmod7, mkdir as mkdir5 } from "fs/promises";
6420
- import path13 from "path";
6421
- var TMP_DIR = path13.join(skillsDir, ".tmp");
6626
+ import { existsSync as existsSync2 } from "fs";
6627
+ import { chmod as chmod7, copyFile, lstat as lstat5, mkdir as mkdir5, readdir as readdir6, readFile as readFile14, rename as rename6, rm as rm7 } from "fs/promises";
6628
+ import path14 from "path";
6629
+ var TMP_DIR = path14.join(skillsDir, ".tmp");
6630
+ var SKILL_FILE2 = "SKILL.md";
6422
6631
  var MAX_SKILL_BYTES2 = 5e6;
6423
- async function installSkill(source) {
6632
+ var MAX_ARCHIVE_BYTES = 2e7;
6633
+ var MAX_SKILL_FILES = 1e3;
6634
+ var MAX_SKILL_TOTAL_BYTES = 5e7;
6635
+ var MAX_SEARCH_DEPTH = 4;
6636
+ var COMMAND_TIMEOUT_MS = 12e4;
6637
+ var SKIPPED_DIRS = /* @__PURE__ */ new Set([".git", "node_modules"]);
6638
+ async function installSkill(source, run = runCommand) {
6639
+ const trimmed = source.trim();
6640
+ if (!trimmed) throw new Error("Give a skill source: a GitHub URL, an npm package or a local path.");
6424
6641
  await ensureDir(skillsDir);
6425
- if (isGitHubUrl(source)) {
6426
- return installFromGitHub(source);
6642
+ if (isGitHubUrl(trimmed)) return installFromGitHub(trimmed, run);
6643
+ if (npmSkillName(trimmed) && !existsSync2(path14.resolve(trimmed))) {
6644
+ return installFromNpm(trimmed, run);
6427
6645
  }
6428
- if (isNpmPackage(source)) {
6429
- return installFromNpm(source);
6430
- }
6431
- return installFromLocal(source);
6646
+ return installFromLocal(trimmed);
6432
6647
  }
6433
- function isGitHubUrl(input) {
6434
- try {
6435
- const url = new URL(input);
6436
- return url.protocol === "https:" && (url.hostname === "github.com" || url.hostname === "www.github.com");
6437
- } catch {
6438
- return false;
6648
+ async function uninstallSkill(name) {
6649
+ const dir = safeChildPath(skillsDir, safeSkillName(name));
6650
+ const info = await lstat5(dir).catch(() => null);
6651
+ if (!info) throw new Error(`No installed skill named "${name}" in ${skillsDir}`);
6652
+ if (info.isSymbolicLink() || !info.isDirectory()) {
6653
+ throw new Error(`Refusing to remove ${dir}: it is not a plain skill directory.`);
6439
6654
  }
6655
+ await rm7(dir, { recursive: true, force: true });
6656
+ return dir;
6440
6657
  }
6441
- function isNpmPackage(input) {
6442
- return npmSkillName(input) !== null;
6443
- }
6444
- async function installFromGitHub(url) {
6658
+ async function installFromGitHub(url, run) {
6445
6659
  const { owner, repo, subdir, branch } = parseGitHubUrl(url);
6446
- const name = safeSkillName(subdir ? path13.posix.basename(subdir) : repo);
6447
- const skillDir = safeChildPath(skillsDir, name);
6448
- const tmpDir = safeChildPath(TMP_DIR, `${name}-${randomUUID4()}`);
6449
- try {
6450
- mkdirSync(tmpDir, { recursive: true });
6451
- const cloneUrl = `https://github.com/${owner}/${repo}.git`;
6452
- const gitArgs = ["clone", "--depth", "1"];
6453
- if (branch) gitArgs.push("--branch", branch);
6454
- gitArgs.push(cloneUrl, tmpDir);
6455
- execFileSync("git", gitArgs, { stdio: "ignore" });
6456
- const skillFile = subdir ? safeChildPath(tmpDir, ...subdir.split("/"), "SKILL.md") : safeChildPath(tmpDir, "SKILL.md");
6457
- const body = readSkillFile(skillFile, tmpDir, subdir || repo);
6458
- removeExistingSkillDir(skillDir);
6459
- await writeSkillFile(skillDir, body);
6460
- return { name, dir: skillDir, source: url };
6461
- } finally {
6462
- rmSync(tmpDir, { recursive: true, force: true });
6463
- }
6660
+ return withTempDir(async (tmp) => {
6661
+ const checkout = path14.join(tmp, "r");
6662
+ const args = ["-c", "core.longpaths=true", "clone", "--depth", "1", "--single-branch"];
6663
+ if (branch) args.push("--branch", branch);
6664
+ args.push(`https://github.com/${owner}/${repo}.git`, checkout);
6665
+ try {
6666
+ await run("git", args, { cwd: tmp, env: { ...process.env, GIT_TERMINAL_PROMPT: "0" } });
6667
+ } catch (error) {
6668
+ throw new Error(`Could not clone ${owner}/${repo}${branch ? ` (${branch})` : ""}: ${messageOf2(error)}`);
6669
+ }
6670
+ const start = subdir ? safeChildPath(checkout, ...subdir.split("/")) : checkout;
6671
+ const skillDir = await locateSkillDir(start, url);
6672
+ const fallback = skillDir === checkout ? repo : path14.basename(skillDir);
6673
+ return copyIntoSkills(skillDir, fallback, url);
6674
+ });
6464
6675
  }
6465
- async function installFromNpm(packageName) {
6676
+ async function installFromNpm(packageName, run) {
6466
6677
  const parsedName = npmSkillName(packageName);
6467
6678
  if (!parsedName) throw new Error(`Invalid npm package name: ${packageName}`);
6468
- const name = safeSkillName(parsedName);
6469
- const skillDir = safeChildPath(skillsDir, name);
6470
- const tmpDir = safeChildPath(TMP_DIR, `${name}-${randomUUID4()}`);
6471
- try {
6472
- mkdirSync(tmpDir, { recursive: true });
6473
- execFileSync("npm", ["pack", packageName, "--prefix", tmpDir], {
6474
- stdio: "ignore"
6475
- });
6476
- const files = readdirSync(tmpDir);
6477
- const tarball = files.find((f) => f.endsWith(".tgz"));
6478
- if (!tarball) {
6479
- throw new Error(`Could not download npm package "${packageName}"`);
6480
- }
6481
- const tarballPath = path13.join(tmpDir, tarball);
6482
- validateTarball(tarballPath);
6483
- const extractDir = path13.join(tmpDir, "extracted");
6484
- mkdirSync(extractDir, { recursive: true });
6679
+ return withTempDir(async (tmp) => {
6485
6680
  try {
6486
- execFileSync("tar", ["-xzf", tarballPath, "-C", extractDir], { stdio: "ignore" });
6487
- } catch {
6488
- const packageDir = path13.join(tmpDir, "node_modules", packageName);
6489
- if (existsSync2(packageDir)) {
6490
- const skillFile2 = path13.join(packageDir, "SKILL.md");
6491
- if (existsSync2(skillFile2)) {
6492
- const body2 = readSkillFile(skillFile2, packageDir, packageName);
6493
- await writeSkillFile(skillDir, body2);
6494
- return { name, dir: skillDir, source: packageName };
6495
- }
6496
- }
6497
- throw new Error("Could not extract the npm package. Is `tar` available?");
6681
+ await run("npm", ["pack", packageName, "--pack-destination", ".", "--silent"], { cwd: tmp });
6682
+ } catch (error) {
6683
+ throw new Error(`Could not download npm package "${packageName}": ${messageOf2(error)}`);
6498
6684
  }
6499
- const extractedFiles = readdirSync(extractDir);
6500
- const packageRoot = extractedFiles.find((f) => f === "package");
6501
- if (!packageRoot) throw new Error("Could not find package root in extracted files");
6502
- const skillFile = path13.join(extractDir, "package", "SKILL.md");
6503
- if (!existsSync2(skillFile)) {
6504
- throw new Error(`No SKILL.md found in npm package "${packageName}"`);
6685
+ const tarball = (await readdir6(tmp)).find((file) => file.endsWith(".tgz"));
6686
+ if (!tarball) throw new Error(`npm did not produce an archive for "${packageName}"`);
6687
+ const archive = path14.join(tmp, tarball);
6688
+ const info = await lstat5(archive);
6689
+ if (!info.isFile() || info.size > MAX_ARCHIVE_BYTES) {
6690
+ throw new Error(`The npm archive exceeds the ${MAX_ARCHIVE_BYTES / 1e6} MB limit.`);
6505
6691
  }
6506
- const body = readSkillFile(skillFile, extractDir, packageName);
6507
- await writeSkillFile(skillDir, body);
6508
- return { name, dir: skillDir, source: packageName };
6509
- } finally {
6510
- rmSync(tmpDir, { recursive: true, force: true });
6511
- }
6692
+ const extracted = path14.join(tmp, "extracted");
6693
+ await mkdir5(extracted);
6694
+ try {
6695
+ await run("tar", ["-xzf", tarball, "-C", "extracted"], { cwd: tmp });
6696
+ } catch (error) {
6697
+ throw new Error(`Could not extract the npm package (is tar available?): ${messageOf2(error)}`);
6698
+ }
6699
+ const skillDir = await locateSkillDir(path14.join(extracted, "package"), packageName);
6700
+ return copyIntoSkills(skillDir, parsedName, packageName);
6701
+ });
6512
6702
  }
6513
- function npmSkillName(input) {
6514
- if (input.includes("\\") || input.startsWith(".")) return null;
6515
- const scoped = input.match(/^@[\w.-]+\/([\w.-]+)(?:@[\w.*+-]+)?$/);
6516
- if (scoped) return scoped[1] ?? null;
6517
- const unscoped = input.match(/^([\w.-]+)(?:@[\w.*+-]+)?$/);
6518
- return unscoped?.[1] ?? null;
6703
+ async function installFromLocal(source) {
6704
+ const resolved = path14.resolve(source);
6705
+ const info = await lstat5(resolved).catch(() => null);
6706
+ if (!info) throw new Error(`No such file or directory: ${source}`);
6707
+ const start = info.isFile() && path14.basename(resolved) === SKILL_FILE2 ? path14.dirname(resolved) : resolved;
6708
+ const skillDir = await locateSkillDir(start, source);
6709
+ return copyIntoSkills(skillDir, path14.basename(skillDir), resolved);
6519
6710
  }
6520
- function validateTarball(file) {
6521
- const info = lstatSync(file, { throwIfNoEntry: false });
6522
- if (!info?.isFile() || info.size > 2e7) {
6523
- throw new Error("The npm skill archive is missing or exceeds the 20 MB safety limit.");
6711
+ async function locateSkillDir(start, source) {
6712
+ const info = await lstat5(start).catch(() => null);
6713
+ if (!info?.isDirectory() || info.isSymbolicLink()) {
6714
+ throw new Error(`"${source}" does not point to a directory.`);
6524
6715
  }
6525
- const listing = execFileSync("tar", ["-tzf", file], {
6526
- encoding: "utf8",
6527
- maxBuffer: 2e6,
6528
- stdio: ["ignore", "pipe", "ignore"]
6529
- });
6530
- const entries = listing.split(/\r?\n/).filter(Boolean);
6531
- if (entries.length === 0 || entries.length > 1e4) {
6532
- throw new Error("The npm skill archive has an invalid number of entries.");
6716
+ if (await isPlainFile(path14.join(start, SKILL_FILE2))) return start;
6717
+ const found = [];
6718
+ const walk = async (dir, depth) => {
6719
+ if (depth > MAX_SEARCH_DEPTH || found.length > 50) return;
6720
+ const entries = await readdir6(dir, { withFileTypes: true }).catch(() => []);
6721
+ for (const entry of entries) {
6722
+ if (!entry.isDirectory() || SKIPPED_DIRS.has(entry.name)) continue;
6723
+ const child = path14.join(dir, entry.name);
6724
+ if (await isPlainFile(path14.join(child, SKILL_FILE2))) found.push(child);
6725
+ else await walk(child, depth + 1);
6726
+ }
6727
+ };
6728
+ await walk(start, 1);
6729
+ if (found.length === 1) return found[0];
6730
+ if (found.length === 0) throw new Error(`No ${SKILL_FILE2} found in "${source}".`);
6731
+ const listed = found.slice(0, 20).map((dir) => ` ${path14.relative(start, dir).split(path14.sep).join("/")}`).join("\n");
6732
+ throw new Error(
6733
+ `"${source}" contains ${found.length} skills; install one of them by its own path (for GitHub: .../tree/<branch>/<path>):
6734
+ ${listed}`
6735
+ );
6736
+ }
6737
+ async function copyIntoSkills(sourceDir, fallbackName, source) {
6738
+ const skillFile = path14.join(sourceDir, SKILL_FILE2);
6739
+ const skillInfo = await lstat5(skillFile);
6740
+ if (skillInfo.size > MAX_SKILL_BYTES2) {
6741
+ throw new Error(`${SKILL_FILE2} exceeds the ${MAX_SKILL_BYTES2 / 1e6} MB limit.`);
6533
6742
  }
6534
- for (const entry of entries) {
6535
- const normalized = entry.replace(/^\.\//, "");
6536
- if (normalized.includes("\\") || path13.posix.isAbsolute(normalized) || normalized.split("/").some((segment) => segment === "..")) {
6537
- throw new Error(`Unsafe path in npm skill archive: ${entry}`);
6538
- }
6539
- }
6540
- }
6541
- async function installFromLocal(srcPath) {
6542
- const resolved = path13.resolve(srcPath);
6543
- let skillDir;
6544
- let skillFile;
6545
- const directSkill = path13.join(resolved, "SKILL.md");
6546
- if (existsSync2(directSkill)) {
6547
- skillDir = resolved;
6548
- skillFile = directSkill;
6549
- } else if (resolved.endsWith("SKILL.md") && existsSync2(resolved)) {
6550
- skillDir = path13.dirname(resolved);
6551
- skillFile = resolved;
6552
- } else {
6553
- throw new Error(`No SKILL.md found at "${srcPath}"`);
6743
+ const name = safeSkillName(frontmatterName(await readFile14(skillFile, "utf8")) ?? fallbackName);
6744
+ const destination = safeChildPath(skillsDir, name);
6745
+ const existing = await lstat5(destination).catch(() => null);
6746
+ if (existing && (existing.isSymbolicLink() || !existing.isDirectory())) {
6747
+ throw new Error(`Refusing to replace ${destination}: it is not a plain skill directory.`);
6748
+ }
6749
+ await ensureDir(TMP_DIR);
6750
+ const staging = safeChildPath(TMP_DIR, `s-${randomUUID4().slice(0, 8)}`);
6751
+ try {
6752
+ await copyTree(sourceDir, staging);
6753
+ if (existing) await rm7(destination, { recursive: true, force: true });
6754
+ await rename6(staging, destination);
6755
+ } catch (error) {
6756
+ await rm7(staging, { recursive: true, force: true }).catch(() => void 0);
6757
+ throw error;
6554
6758
  }
6555
- const name = path13.basename(skillDir);
6556
- const destDir = safeChildPath(skillsDir, safeSkillName(name));
6557
- const body = readSkillFile(skillFile, skillDir, srcPath);
6558
- await writeSkillFile(destDir, body);
6559
- return { name, dir: destDir, source: resolved };
6759
+ return { name, dir: destination, source };
6760
+ }
6761
+ async function copyTree(from, to) {
6762
+ let files = 0;
6763
+ let bytes = 0;
6764
+ const visit = async (dir, target) => {
6765
+ await mkdir5(target, { recursive: true, mode: 448 });
6766
+ for (const entry of await readdir6(dir, { withFileTypes: true })) {
6767
+ const source = path14.join(dir, entry.name);
6768
+ const destination = path14.join(target, entry.name);
6769
+ if (entry.isDirectory()) {
6770
+ if (!SKIPPED_DIRS.has(entry.name)) await visit(source, destination);
6771
+ continue;
6772
+ }
6773
+ if (!entry.isFile()) continue;
6774
+ const info = await lstat5(source);
6775
+ files += 1;
6776
+ bytes += info.size;
6777
+ if (files > MAX_SKILL_FILES || bytes > MAX_SKILL_TOTAL_BYTES) {
6778
+ throw new Error(
6779
+ `The skill is too large to install (limit ${MAX_SKILL_FILES} files, ${MAX_SKILL_TOTAL_BYTES / 1e6} MB).`
6780
+ );
6781
+ }
6782
+ await copyFile(source, destination);
6783
+ await chmod7(destination, (info.mode & 73) !== 0 ? 448 : 384);
6784
+ }
6785
+ };
6786
+ await visit(from, to);
6560
6787
  }
6561
- async function writeSkillFile(dir, body) {
6562
- assertInside(skillsDir, dir);
6563
- const existingDir = lstatSync(dir, { throwIfNoEntry: false });
6564
- if (existingDir?.isSymbolicLink()) {
6565
- throw new Error(`Refusing to replace symlinked skill directory: ${dir}`);
6788
+ function frontmatterName(text) {
6789
+ const lines = text.replace(/^/, "").split(/\r?\n/);
6790
+ if (lines[0]?.trim() !== "---") return void 0;
6791
+ for (const line of lines.slice(1)) {
6792
+ if (line.trim() === "---") return void 0;
6793
+ const match = /^name\s*:\s*(.+)$/.exec(line);
6794
+ if (match) {
6795
+ const value = match[1].trim().replace(/^(["'])(.*)\1$/, "$2").trim();
6796
+ return /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(value) ? value : void 0;
6797
+ }
6566
6798
  }
6567
- await mkdir5(dir, { recursive: true, mode: 448 });
6568
- const file = path13.join(dir, "SKILL.md");
6569
- const existingFile = lstatSync(file, { throwIfNoEntry: false });
6570
- if (existingFile?.isSymbolicLink()) {
6571
- throw new Error(`Refusing to replace symlinked skill file: ${file}`);
6799
+ return void 0;
6800
+ }
6801
+ async function withTempDir(work) {
6802
+ await ensureDir(TMP_DIR);
6803
+ const dir = safeChildPath(TMP_DIR, randomUUID4().slice(0, 8));
6804
+ await mkdir5(dir, { mode: 448 });
6805
+ try {
6806
+ return await work(dir);
6807
+ } finally {
6808
+ await rm7(dir, { recursive: true, force: true }).catch(() => void 0);
6809
+ }
6810
+ }
6811
+ function runCommand(command, args, options) {
6812
+ const viaShell = process.platform === "win32" && command === "npm";
6813
+ const file = viaShell ? [command, ...args.map(quoteForCmd)].join(" ") : command;
6814
+ return new Promise((resolve3, reject) => {
6815
+ execFile2(
6816
+ file,
6817
+ viaShell ? [] : args,
6818
+ {
6819
+ cwd: options.cwd,
6820
+ env: options.env,
6821
+ shell: viaShell,
6822
+ timeout: COMMAND_TIMEOUT_MS,
6823
+ windowsHide: true,
6824
+ maxBuffer: 4e6
6825
+ },
6826
+ (error, _stdout, stderr) => {
6827
+ if (!error) return resolve3();
6828
+ const code = error.code;
6829
+ if (code === "ENOENT") return reject(new Error(`${command} is not installed or not on PATH`));
6830
+ if (error.killed) {
6831
+ return reject(new Error(`${command} timed out after ${COMMAND_TIMEOUT_MS / 1e3}s`));
6832
+ }
6833
+ const detail = String(stderr ?? "").trim().split(/\r?\n/).filter((line) => line.trim() !== "" && !/^Cloning into /.test(line)).slice(-3).join(" ");
6834
+ reject(new Error(detail || error.message));
6835
+ }
6836
+ );
6837
+ });
6838
+ }
6839
+ function quoteForCmd(value) {
6840
+ return /^[\w@./:=+-]+$/.test(value) ? value : `"${value.replace(/"/g, '""')}"`;
6841
+ }
6842
+ function isGitHubUrl(input) {
6843
+ try {
6844
+ const url = new URL(input);
6845
+ return url.protocol === "https:" && (url.hostname === "github.com" || url.hostname === "www.github.com");
6846
+ } catch {
6847
+ return false;
6572
6848
  }
6573
- writeFileSync(file, body, { encoding: "utf8", mode: 384 });
6574
- await chmod7(file, 384);
6849
+ }
6850
+ function npmSkillName(input) {
6851
+ if (input.includes("\\") || input.startsWith(".")) return null;
6852
+ const scoped = input.match(/^@[\w][\w.-]*\/([\w][\w.-]*)(?:@[\w.*+-]+)?$/);
6853
+ if (scoped) return scoped[1] ?? null;
6854
+ const unscoped = input.match(/^([\w][\w.-]*)(?:@[\w.*+-]+)?$/);
6855
+ return unscoped?.[1] ?? null;
6575
6856
  }
6576
6857
  function parseGitHubUrl(url) {
6577
6858
  let parsed;
@@ -6593,11 +6874,13 @@ function parseGitHubUrl(url) {
6593
6874
  const owner = safeGitHubSegment(segments[0], "owner");
6594
6875
  const repo = safeSkillName(segments[1].replace(/\.git$/i, ""));
6595
6876
  if (segments.length === 2) return { owner, repo };
6596
- if (segments[2] !== "tree" || segments.length < 4) {
6877
+ if (segments[2] !== "tree" && segments[2] !== "blob" || segments.length < 4) {
6597
6878
  throw new Error(`Unsupported GitHub skill URL: ${url}`);
6598
6879
  }
6599
6880
  const branch = safeGitHubSegment(segments[3], "branch");
6600
- const subdirSegments = segments.slice(4).map((segment) => safeGitHubSegment(segment, "path"));
6881
+ const pathSegments = segments.slice(4);
6882
+ if (segments[2] === "blob" && pathSegments.at(-1) === SKILL_FILE2) pathSegments.pop();
6883
+ const subdirSegments = pathSegments.map((segment) => safeGitHubSegment(segment, "path"));
6601
6884
  return {
6602
6885
  owner,
6603
6886
  repo,
@@ -6606,7 +6889,7 @@ function parseGitHubUrl(url) {
6606
6889
  };
6607
6890
  }
6608
6891
  function safeGitHubSegment(value, kind) {
6609
- if (value === "" || value === "." || value === ".." || value.length > 255 || /[\0-\x1f\x7f/\\:*?"<>|#%]/.test(value)) {
6892
+ if (value === "" || value === "." || value === ".." || value.startsWith("-") || value.length > 255 || /[\0-\x1f\x7f/\\:*?"<>|#%]/.test(value)) {
6610
6893
  throw new Error(`Unsafe GitHub ${kind}: ${value || "(empty)"}`);
6611
6894
  }
6612
6895
  return value;
@@ -6618,37 +6901,19 @@ function safeSkillName(value) {
6618
6901
  return value;
6619
6902
  }
6620
6903
  function safeChildPath(root, ...segments) {
6621
- const target = path13.resolve(root, ...segments);
6622
- assertInside(root, target);
6623
- return target;
6624
- }
6625
- function assertInside(root, target) {
6626
- const relative2 = path13.relative(path13.resolve(root), path13.resolve(target));
6627
- if (relative2 === "" || !path13.isAbsolute(relative2) && relative2 !== ".." && !relative2.startsWith(`..${path13.sep}`)) {
6628
- return;
6904
+ const target = path14.resolve(root, ...segments);
6905
+ const relative2 = path14.relative(path14.resolve(root), target);
6906
+ if (relative2 !== "" && (path14.isAbsolute(relative2) || relative2 === ".." || relative2.startsWith(`..${path14.sep}`))) {
6907
+ throw new Error(`Path escapes the skills directory: ${target}`);
6629
6908
  }
6630
- throw new Error(`Path escapes the skills directory: ${target}`);
6909
+ return target;
6631
6910
  }
6632
- function readSkillFile(file, allowedRoot, source) {
6633
- const info = lstatSync(file, { throwIfNoEntry: false });
6634
- if (!info?.isFile() || info.isSymbolicLink()) {
6635
- throw new Error(`No regular SKILL.md found in "${source}"`);
6636
- }
6637
- if (info.size > MAX_SKILL_BYTES2) {
6638
- throw new Error(`SKILL.md in "${source}" exceeds the ${MAX_SKILL_BYTES2 / 1e6} MB limit`);
6639
- }
6640
- const root = realpathSync2(allowedRoot);
6641
- const resolved = realpathSync2(file);
6642
- assertInside(root, resolved);
6643
- return readFileSync(resolved, "utf8");
6911
+ async function isPlainFile(file) {
6912
+ const info = await lstat5(file).catch(() => null);
6913
+ return Boolean(info?.isFile() && !info.isSymbolicLink());
6644
6914
  }
6645
- function removeExistingSkillDir(dir) {
6646
- const info = lstatSync(dir, { throwIfNoEntry: false });
6647
- if (!info) return;
6648
- if (info.isSymbolicLink()) {
6649
- throw new Error(`Refusing to remove symlinked skill directory: ${dir}`);
6650
- }
6651
- rmSync(dir, { recursive: true, force: true });
6915
+ function messageOf2(error) {
6916
+ return error instanceof Error ? error.message : String(error);
6652
6917
  }
6653
6918
 
6654
6919
  // src/tools/skill.ts
@@ -6922,6 +7187,12 @@ async function boot(options) {
6922
7187
  const skillRoots = workspaceTrusted ? [projectSkillsDir(workspaceRoot2), skillsDir] : [skillsDir];
6923
7188
  let skills = await discoverSkills(skillRoots);
6924
7189
  if (skills.length > 0) tools.register([createSkillTool(skills)]);
7190
+ const reloadSkills = async () => {
7191
+ skills = await discoverSkills(skillRoots);
7192
+ skillCatalogue = formatSkillCatalogue(skills);
7193
+ tools.unregister(["skill"]);
7194
+ if (skills.length > 0) tools.register([createSkillTool(skills)]);
7195
+ };
6925
7196
  const mcp = createMcpManager(config.mcp);
6926
7197
  await mcp.connectAll();
6927
7198
  let registeredMcpToolNames = /* @__PURE__ */ new Set();
@@ -6934,6 +7205,7 @@ async function boot(options) {
6934
7205
  );
6935
7206
  };
6936
7207
  syncMcpTools();
7208
+ mcp.onChange(syncMcpTools);
6937
7209
  for (const state of mcp.states()) {
6938
7210
  if (state.status === "error") warnings.push(`MCP "${state.name}": ${state.error ?? "failed"}`);
6939
7211
  }
@@ -7326,7 +7598,7 @@ async function boot(options) {
7326
7598
  },
7327
7599
  async exportSession(id, destination) {
7328
7600
  await persistQueue.catch(() => void 0);
7329
- const target = destination ? path14.resolve(options.cwd, destination) : path14.join(options.cwd, ".kitcode-exports");
7601
+ const target = destination ? path15.resolve(options.cwd, destination) : path15.join(options.cwd, ".kitcode-exports");
7330
7602
  if (!destination) await mkdir6(target, { recursive: true, mode: 448 });
7331
7603
  return (await exportSession(id, target)).path;
7332
7604
  },
@@ -7553,16 +7825,20 @@ ${lines.join("\n")}` : null;
7553
7825
  hint: prompt.description || void 0
7554
7826
  }));
7555
7827
  },
7556
- async readPrompt(slug) {
7557
- const prompt = await getPrompt(slug);
7828
+ async readPrompt(name) {
7829
+ const prompt = await getPrompt(promptSlug(name));
7558
7830
  return prompt?.body ?? "";
7559
7831
  },
7560
7832
  async savePrompt(name, body) {
7561
- await savePrompt({ name, body });
7833
+ return (await savePrompt({ name, body })).slug;
7562
7834
  },
7563
- async deletePrompt(slug) {
7564
- const ok = await deletePrompt(slug);
7565
- if (!ok) throw new Error(`Prompt "${slug}" not found.`);
7835
+ async promptExists(name) {
7836
+ const slug = promptSlug(name);
7837
+ return slug !== "" && await promptExists(slug);
7838
+ },
7839
+ async deletePrompt(name) {
7840
+ const slug = promptSlug(name);
7841
+ if (!slug || !await deletePrompt(slug)) throw new Error(`Prompt "${name}" not found.`);
7566
7842
  },
7567
7843
  async loadSkill(name) {
7568
7844
  const meta = skills.find((s) => s.name === name);
@@ -7571,15 +7847,25 @@ ${lines.join("\n")}` : null;
7571
7847
  },
7572
7848
  async installSkill(source) {
7573
7849
  const result = await installSkill(source);
7574
- skills = await discoverSkills(skillRoots);
7575
- skillCatalogue = formatSkillCatalogue(skills);
7576
- tools.unregister(["skill"]);
7577
- if (skills.length > 0) tools.register([createSkillTool(skills)]);
7850
+ await reloadSkills();
7578
7851
  return result;
7579
7852
  },
7853
+ async uninstallSkill(name) {
7854
+ const meta = skills.find((skill) => skill.name === name);
7855
+ if (!meta) throw new Error(`Skill "${name}" not found.`);
7856
+ if (path15.dirname(meta.dir) !== skillsDir) {
7857
+ throw new Error(`Skill "${name}" belongs to this project: delete ${meta.dir} instead.`);
7858
+ }
7859
+ await uninstallSkill(path15.basename(meta.dir));
7860
+ await reloadSkills();
7861
+ },
7580
7862
  async loadAttachment(requestedPath) {
7581
7863
  return (await loadAttachment(options.cwd, requestedPath)).block;
7582
7864
  },
7865
+ summarizeToolCall: (name, input) => tools.get(name)?.summarize(input) ?? name,
7866
+ async loadMentionAttachment(requestedPath) {
7867
+ return (await loadMentionAttachment(options.cwd, requestedPath))?.block ?? null;
7868
+ },
7583
7869
  async loadAutomaticAttachment(requestedPath) {
7584
7870
  return (await loadAutomaticAttachment(options.cwd, requestedPath))?.block ?? null;
7585
7871
  },
@@ -7697,6 +7983,16 @@ ${lines.join("\n")}` : null;
7697
7983
  });
7698
7984
  }
7699
7985
  return next;
7986
+ } catch (error) {
7987
+ const recovered = signal.aborted ? null : imageRejection(error, history);
7988
+ if (!recovered) throw error;
7989
+ hooks.onEvent({
7990
+ type: "notice",
7991
+ level: "error",
7992
+ text: `${error instanceof Error ? error.message : String(error)}
7993
+ ${recovered.removed === 1 ? "The image was" : `${recovered.removed} images were`} removed from the conversation so the next message can go through. Send a description or a smaller image instead, or switch to a model that accepts images.`
7994
+ });
7995
+ return recovered.messages;
7700
7996
  } finally {
7701
7997
  try {
7702
7998
  await checkpoint.commit();
@@ -7743,6 +8039,14 @@ ${lines.join("\n")}` : null;
7743
8039
  }
7744
8040
  };
7745
8041
  }
8042
+ function imageRejection(error, history) {
8043
+ const status = error?.status;
8044
+ if (typeof status !== "number" || ![400, 413, 415, 422].includes(status)) return null;
8045
+ const message = error instanceof Error ? error.message : String(error);
8046
+ if (status !== 413 && !/image|vision|multi-?modal/i.test(message)) return null;
8047
+ const result = withoutImages(history);
8048
+ return result.removed > 0 ? result : null;
8049
+ }
7746
8050
  function preferredModel(models) {
7747
8051
  for (const preferred of PREFERRED2) {
7748
8052
  const match = models.find((model) => model.id.endsWith(preferred));
@@ -7998,35 +8302,89 @@ function useTerminalSize() {
7998
8302
 
7999
8303
  // src/mcp/add.ts
8000
8304
  var SERVER_NAME = /^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$/;
8305
+ var ENV_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
8306
+ var HEADER_NAME = /^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/;
8307
+ function parseMcpAddCommand(raw) {
8308
+ const args = splitCommandLine(raw);
8309
+ return args ? parseMcpAddArgs(args) : { ok: false, error: "usage" };
8310
+ }
8001
8311
  function parseMcpAddArgs(args) {
8002
- const [name, first2, ...remaining] = args;
8003
- if (!name || !first2) return { ok: false, error: "usage" };
8312
+ const [name, ...tail2] = args;
8313
+ if (!name) return { ok: false, error: "usage" };
8004
8314
  if (!SERVER_NAME.test(name)) return { ok: false, error: "invalid-name" };
8005
- if (first2 === "http") {
8006
- const [url, ...extra] = remaining;
8007
- if (!url) return { ok: false, error: "usage" };
8315
+ const env = {};
8316
+ const headers = {};
8317
+ let index = 0;
8318
+ for (; index < tail2.length; index += 1) {
8319
+ const option = tail2[index];
8320
+ if (option === "-e" || option === "--env") {
8321
+ const value = tail2[++index];
8322
+ const eq = value?.indexOf("=") ?? -1;
8323
+ if (!value || eq <= 0 || !ENV_NAME.test(value.slice(0, eq))) return { ok: false, error: "invalid-env" };
8324
+ env[value.slice(0, eq)] = value.slice(eq + 1);
8325
+ continue;
8326
+ }
8327
+ if (option === "-H" || option === "--header") {
8328
+ const value = tail2[++index];
8329
+ const colon = value?.indexOf(":") ?? -1;
8330
+ const key = value && colon > 0 ? value.slice(0, colon).trim() : "";
8331
+ if (!value || !HEADER_NAME.test(key)) return { ok: false, error: "invalid-header" };
8332
+ headers[key] = value.slice(colon + 1).trim();
8333
+ continue;
8334
+ }
8335
+ break;
8336
+ }
8337
+ let rest = tail2.slice(index);
8338
+ let kind;
8339
+ if (rest[0] === "http") {
8340
+ kind = "http";
8341
+ rest = rest.slice(1);
8342
+ } else if (rest[0] === "stdio" || rest[0] === "--") {
8343
+ kind = "stdio";
8344
+ rest = rest.slice(1);
8345
+ }
8346
+ const [target, ...extra] = rest;
8347
+ if (!target) return { ok: false, error: "usage" };
8348
+ kind ??= /^https?:\/\//i.test(target) ? "http" : "stdio";
8349
+ if (kind === "http") {
8008
8350
  if (extra.length > 0) return { ok: false, error: "http-args" };
8009
- return httpServer(name, url);
8010
- }
8011
- if (first2 === "stdio" || first2 === "--") {
8012
- const [command, ...commandArgs] = remaining;
8013
- if (!command) return { ok: false, error: "usage" };
8014
- return stdioServer(name, command, commandArgs);
8015
- }
8016
- if (/^https?:\/\//.test(first2)) {
8017
- if (remaining.length > 0) return { ok: false, error: "http-args" };
8018
- return httpServer(name, first2);
8351
+ if (Object.keys(env).length > 0) return { ok: false, error: "option-mismatch" };
8352
+ const parsed2 = mcpServerSchema.safeParse({ type: "http", url: target, headers });
8353
+ return parsed2.success ? { ok: true, name, config: parsed2.data } : { ok: false, error: "invalid-url" };
8019
8354
  }
8020
- return stdioServer(name, first2, remaining);
8021
- }
8022
- function httpServer(name, url) {
8023
- const parsed = mcpServerSchema.safeParse({ type: "http", url });
8024
- return parsed.success ? { ok: true, name, config: parsed.data } : { ok: false, error: "invalid-url" };
8025
- }
8026
- function stdioServer(name, command, args) {
8027
- const parsed = mcpServerSchema.safeParse({ type: "stdio", command, args });
8355
+ if (Object.keys(headers).length > 0) return { ok: false, error: "option-mismatch" };
8356
+ const parsed = mcpServerSchema.safeParse({ type: "stdio", command: target, args: extra, env });
8028
8357
  return parsed.success ? { ok: true, name, config: parsed.data } : { ok: false, error: "usage" };
8029
8358
  }
8359
+ function splitCommandLine(raw) {
8360
+ const words2 = [];
8361
+ let current = "";
8362
+ let inWord = false;
8363
+ let quote;
8364
+ for (const char of raw) {
8365
+ if (quote) {
8366
+ if (char === quote) quote = void 0;
8367
+ else current += char;
8368
+ continue;
8369
+ }
8370
+ if (char === '"' || char === "'") {
8371
+ quote = char;
8372
+ inWord = true;
8373
+ continue;
8374
+ }
8375
+ if (/\s/.test(char)) {
8376
+ if (inWord) words2.push(current);
8377
+ current = "";
8378
+ inWord = false;
8379
+ continue;
8380
+ }
8381
+ current += char;
8382
+ inWord = true;
8383
+ }
8384
+ if (quote) return null;
8385
+ if (inWord) words2.push(current);
8386
+ return words2;
8387
+ }
8030
8388
 
8031
8389
  // src/ui/speed.ts
8032
8390
  var CHARS_PER_TOKEN = 4;
@@ -8065,21 +8423,21 @@ function readSpeed(meter, busy, now = Date.now()) {
8065
8423
  // src/ui/commands.ts
8066
8424
  var BUSY_SAFE = { runsWhileBusy: true };
8067
8425
  var COMMANDS = [
8068
- { name: "model", ...BUSY_SAFE },
8426
+ { name: "model", args: "[model]", ...BUSY_SAFE },
8069
8427
  { name: "login" },
8070
8428
  { name: "provider" },
8071
8429
  { name: "logout", args: "[provider]" },
8072
8430
  { name: "key", args: "[provider]" },
8073
8431
  { name: "refresh", args: "[provider]" },
8074
8432
  { name: "effort", ...BUSY_SAFE },
8075
- { name: "thinking", ...BUSY_SAFE },
8433
+ { name: "thinking", args: "[on|off]", ...BUSY_SAFE },
8076
8434
  { name: "bell", args: "[on|off]", ...BUSY_SAFE },
8077
8435
  { name: "context", args: "[tokens|auto]", ...BUSY_SAFE },
8078
8436
  { name: "budget", args: "[tokens]", ...BUSY_SAFE },
8079
8437
  { name: "theme", args: "[color]", ...BUSY_SAFE },
8080
- { name: "lang", ...BUSY_SAFE },
8081
- { name: "prompt", args: "[save <name>|delete <name>]" },
8082
- { name: "skills", args: "[install <source>]" },
8438
+ { name: "lang", args: "[en|ru]", ...BUSY_SAFE },
8439
+ { name: "prompt", args: "[name|save [name]|delete [name]]" },
8440
+ { name: "skills", args: "[install <source>|remove [name]]" },
8083
8441
  { name: "bypass" },
8084
8442
  { name: "usage", ...BUSY_SAFE },
8085
8443
  { name: "mcp", args: "[add|list|delete|enable|disable]" },
@@ -8098,7 +8456,9 @@ var COMMANDS = [
8098
8456
  { name: "exit", ...BUSY_SAFE }
8099
8457
  ];
8100
8458
  var SUBCOMMANDS = [
8101
- { name: "mcp add", args: "<name> <url|-- command>" },
8459
+ { name: "skills install", args: "<github-url|npm-package|path>" },
8460
+ { name: "skills remove", args: "[name]" },
8461
+ { name: "mcp add", args: '<name> [-e K=V] [-H "N: V"] <url|-- command>' },
8102
8462
  { name: "mcp list" },
8103
8463
  { name: "mcp delete", args: "[name]" },
8104
8464
  { name: "mcp enable", args: "[name]" },
@@ -8188,10 +8548,10 @@ function mentionSpan(value, cursor) {
8188
8548
  while (end < value.length && !/[\s@]/.test(value[end] ?? "")) end += 1;
8189
8549
  return { start, end, query };
8190
8550
  }
8191
- function completeMention(value, span, path15) {
8551
+ function completeMention(value, span, path16) {
8192
8552
  const rest = value.slice(span.end);
8193
8553
  const spaced = rest.startsWith(" ");
8194
- const inserted = `@${path15}${spaced ? "" : " "}`;
8554
+ const inserted = `@${path16}${spaced ? "" : " "}`;
8195
8555
  return {
8196
8556
  value: value.slice(0, span.start) + inserted + rest,
8197
8557
  cursor: span.start + inserted.length + (spaced ? 1 : 0)
@@ -8200,8 +8560,8 @@ function completeMention(value, span, path15) {
8200
8560
  function extractMentions(text) {
8201
8561
  const found = [];
8202
8562
  for (const match of text.matchAll(MENTION_PATTERN)) {
8203
- const path15 = (match[1] ?? "").replace(TRAILING_PUNCTUATION, "");
8204
- if (path15 !== "" && !found.includes(path15)) found.push(path15);
8563
+ const path16 = (match[1] ?? "").replace(TRAILING_PUNCTUATION, "");
8564
+ if (path16 !== "" && !found.includes(path16)) found.push(path16);
8205
8565
  }
8206
8566
  return found;
8207
8567
  }
@@ -8315,7 +8675,7 @@ var en = {
8315
8675
  pickerHelp: (shown, total) => `${shown} of ${total} \xB7 \u2191\u2193 move \xB7 enter pick \xB7 esc cancel`,
8316
8676
  noModel: "no model",
8317
8677
  busy: "working",
8318
- sessionFor: (elapsed) => `up ${elapsed}`,
8678
+ sessionFor: (elapsed) => `session ${elapsed}`,
8319
8679
  workingFor: (elapsed) => `working ${elapsed}`,
8320
8680
  titleProvider: "Provider",
8321
8681
  titleLogout: "Sign out of a provider",
@@ -8340,17 +8700,22 @@ var en = {
8340
8700
  pasteToken: (id, lines, chars) => `[paste #${id} \xB7 ${lines > 1 ? `${lines} lines` : `${chars} chars`}]`,
8341
8701
  inputRows: (from, to, total) => `lines ${from}\u2013${to} of ${total}`,
8342
8702
  queueHint: "waiting for the current answer \xB7 \u2191/\u2193 pick one to edit \xB7 enter puts it back in its place",
8343
- configAt: (path15) => `config: ${path15}`,
8703
+ configAt: (path16) => `config: ${path16}`,
8344
8704
  skillsEmpty: "No skills installed. Drop a folder with a SKILL.md into ~/.kitcode/skills or ./.kitcode/skills",
8345
- skillsInstallUsage: "Usage: /skills install <github-url|npm-package|local-path>",
8705
+ skillsInstallUsage: "Usage: /skills install <github-url|npm-package|local-path> \xB7 /skills remove [name]",
8346
8706
  skillsInstalling: (source) => `Installing skill from "${source}"\u2026`,
8347
8707
  skillsInstalled: (name, source) => `Skill "${name}" installed from "${source}"`,
8708
+ skillsRemoved: (name) => `Skill "${name}" removed`,
8709
+ titleSkillsRemove: "Remove skill",
8348
8710
  mcpStatus: (connected, failed) => `MCP: ${connected} connected, ${failed} failed`,
8349
8711
  mcpEmpty: "No MCP servers configured.",
8350
- mcpAddUsage: "List: /mcp list \xB7 Enable/disable: /mcp enable|disable [name] \xB7 Delete: /mcp delete [name]\nRemote: /mcp add <name> <https://url>\nLocal: /mcp add <name> -- <command> [args]",
8712
+ mcpAddUsage: 'List: /mcp list \xB7 Enable/disable: /mcp enable|disable [name] \xB7 Delete: /mcp delete [name]\nRemote: /mcp add <name> [-H "Name: value"] <https://url>\nLocal: /mcp add <name> [-e KEY=value] -- <command> [args]\nKeep secrets out of the config with ${env:VAR}, e.g. -H "Authorization: Bearer ${env:TOKEN}"',
8351
8713
  mcpAddInvalidName: "MCP name may contain only letters, numbers, _ and - (up to 64 chars).",
8352
8714
  mcpAddInvalidUrl: "MCP URL must use HTTPS (HTTP is allowed only for localhost).",
8353
8715
  mcpAddHttpArgs: "An HTTP MCP accepts one URL and no command arguments.",
8716
+ mcpAddInvalidEnv: "Environment variables look like -e KEY=value.",
8717
+ mcpAddInvalidHeader: 'Headers look like -H "Name: value".',
8718
+ mcpAddOptionMismatch: "-e is for local (command) servers, -H is for remote (URL) servers.",
8354
8719
  mcpExists: (name) => `MCP server "${name}" already exists.`,
8355
8720
  mcpConnecting: (name) => `Connecting MCP "${name}"\u2026`,
8356
8721
  mcpAdded: (name, tools) => `MCP "${name}" connected and saved \xB7 ${tools} tools`,
@@ -8378,6 +8743,9 @@ var en = {
8378
8743
  attachUsage: "Usage: /attach <image-or-text-file> \xB7 /attach clipboard \xB7 /attach clear",
8379
8744
  attachmentAdded: (name) => `Attached for the next message: ${name}`,
8380
8745
  attachmentsCleared: "Pending attachments cleared.",
8746
+ attachmentsLimit: (max) => `At most ${max} attachments can be queued for one message.`,
8747
+ attachmentsTooLarge: (mb) => `Attachments for one message are limited to about ${mb} MB in total. Send these first or use smaller files.`,
8748
+ mentionAttached: (name) => `Sending with this message: ${name}`,
8381
8749
  compactNothing: "Not enough older conversation to compact yet.",
8382
8750
  compactStarting: "Compacting context\u2026",
8383
8751
  compactDone: (summarised, cleared, before, after) => [
@@ -8386,7 +8754,7 @@ var en = {
8386
8754
  cleared > 0 ? `${cleared} old tool outputs cleared` : "",
8387
8755
  before !== void 0 && after !== void 0 ? `~${tokens(before)} \u2192 ~${tokens(after)} tokens` : ""
8388
8756
  ].filter(Boolean).join(" \xB7 ") + ".",
8389
- sessionRenameUsage: "Type the title after the command: /sessions rename <id> <title>",
8757
+ sessionRenameUsage: "Type the title after the command: /sessions rename [id] <title>",
8390
8758
  sessionRenamed: (title) => `Session renamed to "${title}".`,
8391
8759
  sessionDeleteAsk: "The saved chat file will be deleted. This cannot be undone.",
8392
8760
  sessionDeleted: (id) => `Session ${id} deleted.`,
@@ -8396,7 +8764,7 @@ var en = {
8396
8764
  sessionDeleteAllFinalBody: "This cannot be undone. Press y only if you want to erase the complete session history.",
8397
8765
  sessionsDeletedAll: (count2) => `All saved chats deleted \xB7 ${count2} sessions removed.`,
8398
8766
  sessionsDeleteAllFailed: (deleted, failed) => `Session cleanup was incomplete \xB7 ${deleted} removed, ${failed} failed.`,
8399
- sessionExported: (path15) => `Session exported to ${path15}`,
8767
+ sessionExported: (path16) => `Session exported to ${path16}`,
8400
8768
  sessionActionResume: "resume",
8401
8769
  sessionActionRename: "rename",
8402
8770
  sessionActionDelete: "delete",
@@ -8437,11 +8805,19 @@ ${url}`,
8437
8805
  promptUsage: "Usage: /prompt save <name>",
8438
8806
  promptNothing: "Nothing to save yet \u2014 send a message first.",
8439
8807
  promptSaved: (name) => `Saved prompt "${name}"`,
8440
- promptDeleteUsage: "Usage: /prompt delete <name>",
8808
+ promptDeleteAsk: (name) => `Delete prompt "${name}"?`,
8809
+ promptOverwriteAsk: (name) => `A prompt named "${name}" exists. Replace it?`,
8810
+ promptNameTitle: "Prompt name",
8811
+ sessionNoMatch: (query) => `No saved session matches "${query}".`,
8812
+ sessionAmbiguous: (query, count2) => `"${query}" matches ${count2} sessions; give more of the id.`,
8813
+ sessionsUsage: "Usage: /sessions [list|rename [id] <title>|delete [id|all]|export [id] [path]]",
8814
+ modelNoMatch: (query) => `No single model matches "${query}"; pick one from the list.`,
8815
+ memoryUsage: "/memory show \xB7 /memory set <text> \xB7 /memory clear",
8816
+ memoryEmpty: "Project memory is empty.",
8817
+ memoryClearAsk: "Clear project memory?",
8818
+ memoryReplaceAsk: "Replace all project memory notes with this text?",
8441
8819
  promptDeleted: (name) => `Deleted prompt "${name}"`,
8442
8820
  promptNotFound: (name) => `Prompt "${name}" not found.`,
8443
- promptSaveAsk: "Save this as a prompt?",
8444
- promptSaveAskBody: "The text will be saved to a file and can be loaded later with /prompt.",
8445
8821
  promptsEmpty: "No saved prompts. Use /prompt save <name>.",
8446
8822
  unknownCommand: (name) => `Unknown command /${name}. Try /help`,
8447
8823
  didYouMean: (typed, guess) => `Unknown command /${typed}. Did you mean /${guess}?`,
@@ -8536,7 +8912,7 @@ var ru = {
8536
8912
  pickerHelp: (shown, total) => `${shown} \u0438\u0437 ${total} \xB7 \u2191\u2193 \u0432\u044B\u0431\u043E\u0440 \xB7 enter \u2014 \u043E\u043A \xB7 esc \u2014 \u043E\u0442\u043C\u0435\u043D\u0430`,
8537
8913
  noModel: "\u043D\u0435\u0442 \u043C\u043E\u0434\u0435\u043B\u0438",
8538
8914
  busy: "\u0440\u0430\u0431\u043E\u0442\u0430\u044E",
8539
- sessionFor: (elapsed) => `\u0432 \u0440\u0430\u0431\u043E\u0442\u0435 ${elapsed}`,
8915
+ sessionFor: (elapsed) => `\u0441\u0435\u0441\u0441\u0438\u044F ${elapsed}`,
8540
8916
  workingFor: (elapsed) => `\u0434\u0443\u043C\u0430\u0435\u0442 ${elapsed}`,
8541
8917
  titleProvider: "\u041F\u0440\u043E\u0432\u0430\u0439\u0434\u0435\u0440",
8542
8918
  titleLogout: "\u0412\u044B\u0431\u0440\u0430\u0442\u044C API \u0434\u043B\u044F \u0432\u044B\u0445\u043E\u0434\u0430",
@@ -8561,17 +8937,22 @@ var ru = {
8561
8937
  pasteToken: (id, lines, chars) => `[\u0432\u0441\u0442\u0430\u0432\u043A\u0430 #${id} \xB7 ${lines > 1 ? `${lines} \u0441\u0442\u0440\u043E\u043A` : `${chars} \u0441\u0438\u043C\u0432\u043E\u043B\u043E\u0432`}]`,
8562
8938
  inputRows: (from, to, total) => `\u0441\u0442\u0440\u043E\u043A\u0438 ${from}\u2013${to} \u0438\u0437 ${total}`,
8563
8939
  queueHint: "\u0436\u0434\u0443\u0442 \u0442\u0435\u043A\u0443\u0449\u0435\u0433\u043E \u043E\u0442\u0432\u0435\u0442\u0430 \xB7 \u2191/\u2193 \u2014 \u0432\u044B\u0431\u0440\u0430\u0442\u044C \u043E\u0434\u043D\u043E \u0434\u043B\u044F \u043F\u0440\u0430\u0432\u043A\u0438 \xB7 enter \u0432\u0435\u0440\u043D\u0451\u0442 \u0435\u0433\u043E \u043D\u0430 \u043C\u0435\u0441\u0442\u043E",
8564
- configAt: (path15) => `\u043A\u043E\u043D\u0444\u0438\u0433: ${path15}`,
8940
+ configAt: (path16) => `\u043A\u043E\u043D\u0444\u0438\u0433: ${path16}`,
8565
8941
  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",
8566
- 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>",
8942
+ 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> \xB7 /skills remove [\u0438\u043C\u044F]",
8567
8943
  skillsInstalling: (source) => `\u0423\u0441\u0442\u0430\u043D\u0430\u0432\u043B\u0438\u0432\u0430\u044E \u0441\u043A\u0438\u043B\u043B \u0438\u0437 "${source}"\u2026`,
8568
8944
  skillsInstalled: (name, source) => `\u0421\u043A\u0438\u043B\u043B \xAB${name}\xBB \u0443\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D \u0438\u0437 "${source}"`,
8945
+ skillsRemoved: (name) => `\u0421\u043A\u0438\u043B\u043B \xAB${name}\xBB \u0443\u0434\u0430\u043B\u0451\u043D`,
8946
+ titleSkillsRemove: "\u0423\u0434\u0430\u043B\u0438\u0442\u044C \u0441\u043A\u0438\u043B\u043B",
8569
8947
  mcpStatus: (connected, failed) => `MCP: \u043F\u043E\u0434\u043A\u043B\u044E\u0447\u0435\u043D\u043E ${connected}, \u0441 \u043E\u0448\u0438\u0431\u043A\u043E\u0439 ${failed}`,
8570
8948
  mcpEmpty: "MCP-\u0441\u0435\u0440\u0432\u0435\u0440\u044B \u043F\u043E\u043A\u0430 \u043D\u0435 \u0434\u043E\u0431\u0430\u0432\u043B\u0435\u043D\u044B.",
8571
- mcpAddUsage: "\u0421\u043F\u0438\u0441\u043E\u043A: /mcp list \xB7 \u0412\u043A\u043B/\u0432\u044B\u043A\u043B: /mcp enable|disable [\u0438\u043C\u044F] \xB7 \u0423\u0434\u0430\u043B\u0438\u0442\u044C: /mcp delete [\u0438\u043C\u044F]\n\u0423\u0434\u0430\u043B\u0451\u043D\u043D\u044B\u0439: /mcp add <\u0438\u043C\u044F> <https://url>\n\u041B\u043E\u043A\u0430\u043B\u044C\u043D\u044B\u0439: /mcp add <\u0438\u043C\u044F> -- <\u043A\u043E\u043C\u0430\u043D\u0434\u0430> [\u0430\u0440\u0433\u0443\u043C\u0435\u043D\u0442\u044B]",
8949
+ mcpAddUsage: '\u0421\u043F\u0438\u0441\u043E\u043A: /mcp list \xB7 \u0412\u043A\u043B/\u0432\u044B\u043A\u043B: /mcp enable|disable [\u0438\u043C\u044F] \xB7 \u0423\u0434\u0430\u043B\u0438\u0442\u044C: /mcp delete [\u0438\u043C\u044F]\n\u0423\u0434\u0430\u043B\u0451\u043D\u043D\u044B\u0439: /mcp add <\u0438\u043C\u044F> [-H "\u0418\u043C\u044F: \u0437\u043D\u0430\u0447\u0435\u043D\u0438\u0435"] <https://url>\n\u041B\u043E\u043A\u0430\u043B\u044C\u043D\u044B\u0439: /mcp add <\u0438\u043C\u044F> [-e \u041A\u041B\u042E\u0427=\u0437\u043D\u0430\u0447\u0435\u043D\u0438\u0435] -- <\u043A\u043E\u043C\u0430\u043D\u0434\u0430> [\u0430\u0440\u0433\u0443\u043C\u0435\u043D\u0442\u044B]\n\u0421\u0435\u043A\u0440\u0435\u0442\u044B \u043B\u0443\u0447\u0448\u0435 \u043D\u0435 \u0445\u0440\u0430\u043D\u0438\u0442\u044C \u0432 \u043A\u043E\u043D\u0444\u0438\u0433\u0435: ${env:VAR}, \u043D\u0430\u043F\u0440\u0438\u043C\u0435\u0440 -H "Authorization: Bearer ${env:TOKEN}"',
8572
8950
  mcpAddInvalidName: "\u0418\u043C\u044F MCP: \u0442\u043E\u043B\u044C\u043A\u043E \u0431\u0443\u043A\u0432\u044B, \u0446\u0438\u0444\u0440\u044B, _ \u0438 - (\u0434\u043E 64 \u0441\u0438\u043C\u0432\u043E\u043B\u043E\u0432).",
8573
8951
  mcpAddInvalidUrl: "URL MCP \u0434\u043E\u043B\u0436\u0435\u043D \u0438\u0441\u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u044C HTTPS (HTTP \u0440\u0430\u0437\u0440\u0435\u0448\u0451\u043D \u0442\u043E\u043B\u044C\u043A\u043E \u0434\u043B\u044F localhost).",
8574
8952
  mcpAddHttpArgs: "\u0414\u043B\u044F HTTP MCP \u0443\u043A\u0430\u0436\u0438 \u043E\u0434\u0438\u043D URL \u0431\u0435\u0437 \u0430\u0440\u0433\u0443\u043C\u0435\u043D\u0442\u043E\u0432 \u043A\u043E\u043C\u0430\u043D\u0434\u044B.",
8953
+ mcpAddInvalidEnv: "\u041F\u0435\u0440\u0435\u043C\u0435\u043D\u043D\u044B\u0435 \u043E\u043A\u0440\u0443\u0436\u0435\u043D\u0438\u044F \u0437\u0430\u0434\u0430\u044E\u0442\u0441\u044F \u0442\u0430\u043A: -e \u041A\u041B\u042E\u0427=\u0437\u043D\u0430\u0447\u0435\u043D\u0438\u0435.",
8954
+ mcpAddInvalidHeader: '\u0417\u0430\u0433\u043E\u043B\u043E\u0432\u043A\u0438 \u0437\u0430\u0434\u0430\u044E\u0442\u0441\u044F \u0442\u0430\u043A: -H "\u0418\u043C\u044F: \u0437\u043D\u0430\u0447\u0435\u043D\u0438\u0435".',
8955
+ mcpAddOptionMismatch: "-e \u2014 \u0434\u043B\u044F \u043B\u043E\u043A\u0430\u043B\u044C\u043D\u044B\u0445 (\u043A\u043E\u043C\u0430\u043D\u0434\u0430) \u0441\u0435\u0440\u0432\u0435\u0440\u043E\u0432, -H \u2014 \u0434\u043B\u044F \u0443\u0434\u0430\u043B\u0451\u043D\u043D\u044B\u0445 (URL).",
8575
8956
  mcpExists: (name) => `MCP-\u0441\u0435\u0440\u0432\u0435\u0440 \xAB${name}\xBB \u0443\u0436\u0435 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u0435\u0442.`,
8576
8957
  mcpConnecting: (name) => `\u041F\u043E\u0434\u043A\u043B\u044E\u0447\u0430\u044E MCP \xAB${name}\xBB\u2026`,
8577
8958
  mcpAdded: (name, tools) => `MCP \xAB${name}\xBB \u043F\u043E\u0434\u043A\u043B\u044E\u0447\u0451\u043D \u0438 \u0441\u043E\u0445\u0440\u0430\u043D\u0451\u043D \xB7 \u0438\u043D\u0441\u0442\u0440\u0443\u043C\u0435\u043D\u0442\u043E\u0432: ${tools}`,
@@ -8599,6 +8980,9 @@ var ru = {
8599
8980
  attachUsage: "\u0418\u0441\u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u043D\u0438\u0435: /attach <\u043A\u0430\u0440\u0442\u0438\u043D\u043A\u0430-\u0438\u043B\u0438-\u0442\u0435\u043A\u0441\u0442\u043E\u0432\u044B\u0439-\u0444\u0430\u0439\u043B> \xB7 /attach clipboard \xB7 /attach clear",
8600
8981
  attachmentAdded: (name) => `\u041F\u0440\u0438\u043A\u0440\u0435\u043F\u043B\u0435\u043D\u043E \u043A \u0441\u043B\u0435\u0434\u0443\u044E\u0449\u0435\u043C\u0443 \u0441\u043E\u043E\u0431\u0449\u0435\u043D\u0438\u044E: ${name}`,
8601
8982
  attachmentsCleared: "\u041E\u0436\u0438\u0434\u0430\u044E\u0449\u0438\u0435 \u0432\u043B\u043E\u0436\u0435\u043D\u0438\u044F \u0443\u0431\u0440\u0430\u043D\u044B.",
8983
+ attachmentsLimit: (max) => `\u041A \u043E\u0434\u043D\u043E\u043C\u0443 \u0441\u043E\u043E\u0431\u0449\u0435\u043D\u0438\u044E \u043C\u043E\u0436\u043D\u043E \u043F\u0440\u0438\u043A\u0440\u0435\u043F\u0438\u0442\u044C \u043D\u0435 \u0431\u043E\u043B\u044C\u0448\u0435 ${max} \u0432\u043B\u043E\u0436\u0435\u043D\u0438\u0439.`,
8984
+ attachmentsTooLarge: (mb) => `\u0412\u043B\u043E\u0436\u0435\u043D\u0438\u044F \u043E\u0434\u043D\u043E\u0433\u043E \u0441\u043E\u043E\u0431\u0449\u0435\u043D\u0438\u044F \u2014 \u043D\u0435 \u0431\u043E\u043B\u044C\u0448\u0435 ~${mb} \u041C\u0411 \u0432 \u0441\u0443\u043C\u043C\u0435. \u041E\u0442\u043F\u0440\u0430\u0432\u044C \u044D\u0442\u0438 \u0441\u043D\u0430\u0447\u0430\u043B\u0430 \u0438\u043B\u0438 \u0432\u043E\u0437\u044C\u043C\u0438 \u0444\u0430\u0439\u043B\u044B \u043F\u043E\u043C\u0435\u043D\u044C\u0448\u0435.`,
8985
+ mentionAttached: (name) => `\u041E\u0442\u043F\u0440\u0430\u0432\u043B\u044F\u044E \u0432\u043C\u0435\u0441\u0442\u0435 \u0441 \u0441\u043E\u043E\u0431\u0449\u0435\u043D\u0438\u0435\u043C: ${name}`,
8602
8986
  compactNothing: "\u041F\u043E\u043A\u0430 \u043D\u0435\u0434\u043E\u0441\u0442\u0430\u0442\u043E\u0447\u043D\u043E \u0441\u0442\u0430\u0440\u044B\u0445 \u0441\u043E\u043E\u0431\u0449\u0435\u043D\u0438\u0439 \u0434\u043B\u044F \u0441\u0436\u0430\u0442\u0438\u044F \u043A\u043E\u043D\u0442\u0435\u043A\u0441\u0442\u0430.",
8603
8987
  compactStarting: "\u0421\u0436\u0430\u0442\u0438\u0435 \u043A\u043E\u043D\u0442\u0435\u043A\u0441\u0442\u0430\u2026",
8604
8988
  compactDone: (summarised, cleared, before, after) => [
@@ -8607,7 +8991,7 @@ var ru = {
8607
8991
  cleared > 0 ? `\u043E\u0447\u0438\u0449\u0435\u043D\u043E \u0441\u0442\u0430\u0440\u044B\u0445 \u0432\u044B\u0432\u043E\u0434\u043E\u0432 \u0438\u043D\u0441\u0442\u0440\u0443\u043C\u0435\u043D\u0442\u043E\u0432: ${cleared}` : "",
8608
8992
  before !== void 0 && after !== void 0 ? `~${tokens(before)} \u2192 ~${tokens(after)} \u0442\u043E\u043A\u0435\u043D\u043E\u0432` : ""
8609
8993
  ].filter(Boolean).join(" \xB7 ") + ".",
8610
- sessionRenameUsage: "\u0414\u043E\u043F\u0438\u0448\u0438 \u043D\u0430\u0437\u0432\u0430\u043D\u0438\u0435: /sessions rename <id> <\u043D\u0430\u0437\u0432\u0430\u043D\u0438\u0435>",
8994
+ sessionRenameUsage: "\u0414\u043E\u043F\u0438\u0448\u0438 \u043D\u0430\u0437\u0432\u0430\u043D\u0438\u0435: /sessions rename [id] <\u043D\u0430\u0437\u0432\u0430\u043D\u0438\u0435>",
8611
8995
  sessionRenamed: (title) => `\u0421\u0435\u0441\u0441\u0438\u044F \u043F\u0435\u0440\u0435\u0438\u043C\u0435\u043D\u043E\u0432\u0430\u043D\u0430 \u0432 \xAB${title}\xBB.`,
8612
8996
  sessionDeleteAsk: "\u0424\u0430\u0439\u043B \u0441\u043E\u0445\u0440\u0430\u043D\u0451\u043D\u043D\u043E\u0433\u043E \u0447\u0430\u0442\u0430 \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043B\u0451\u043D \u0431\u0435\u0437 \u0432\u043E\u0437\u043C\u043E\u0436\u043D\u043E\u0441\u0442\u0438 \u0432\u043E\u0441\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D\u0438\u044F.",
8613
8997
  sessionDeleted: (id) => `\u0421\u0435\u0441\u0441\u0438\u044F ${id} \u0443\u0434\u0430\u043B\u0435\u043D\u0430.`,
@@ -8617,7 +9001,7 @@ var ru = {
8617
9001
  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.",
8618
9002
  sessionsDeletedAll: (count2) => `\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: ${count2}.`,
8619
9003
  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}.`,
8620
- sessionExported: (path15) => `\u0421\u0435\u0441\u0441\u0438\u044F \u044D\u043A\u0441\u043F\u043E\u0440\u0442\u0438\u0440\u043E\u0432\u0430\u043D\u0430: ${path15}`,
9004
+ sessionExported: (path16) => `\u0421\u0435\u0441\u0441\u0438\u044F \u044D\u043A\u0441\u043F\u043E\u0440\u0442\u0438\u0440\u043E\u0432\u0430\u043D\u0430: ${path16}`,
8621
9005
  sessionActionResume: "\u043E\u0442\u043A\u0440\u044B\u0442\u044C",
8622
9006
  sessionActionRename: "\u043F\u0435\u0440\u0435\u0438\u043C\u0435\u043D\u043E\u0432\u0430\u0442\u044C",
8623
9007
  sessionActionDelete: "\u0443\u0434\u0430\u043B\u0438\u0442\u044C",
@@ -8658,11 +9042,19 @@ ${url}`,
8658
9042
  promptUsage: "\u0418\u0441\u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u043D\u0438\u0435: /prompt save <\u0438\u043C\u044F>",
8659
9043
  promptNothing: "\u041F\u043E\u043A\u0430 \u043D\u0435\u0447\u0435\u0433\u043E \u0441\u043E\u0445\u0440\u0430\u043D\u044F\u0442\u044C \u2014 \u0441\u043D\u0430\u0447\u0430\u043B\u0430 \u043E\u0442\u043F\u0440\u0430\u0432\u044C \u0441\u043E\u043E\u0431\u0449\u0435\u043D\u0438\u0435.",
8660
9044
  promptSaved: (name) => `\u041F\u0440\u043E\u043C\u0442 \xAB${name}\xBB \u0441\u043E\u0445\u0440\u0430\u043D\u0451\u043D`,
8661
- promptDeleteUsage: "\u0418\u0441\u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u043D\u0438\u0435: /prompt delete <\u0438\u043C\u044F>",
9045
+ promptDeleteAsk: (name) => `\u0423\u0434\u0430\u043B\u0438\u0442\u044C \u043F\u0440\u043E\u043C\u0442 \xAB${name}\xBB?`,
9046
+ promptOverwriteAsk: (name) => `\u041F\u0440\u043E\u043C\u0442 \xAB${name}\xBB \u0443\u0436\u0435 \u0435\u0441\u0442\u044C. \u0417\u0430\u043C\u0435\u043D\u0438\u0442\u044C?`,
9047
+ promptNameTitle: "\u041D\u0430\u0437\u0432\u0430\u043D\u0438\u0435 \u043F\u0440\u043E\u043C\u0442\u0430",
9048
+ sessionNoMatch: (query) => `\u041D\u0435\u0442 \u0441\u043E\u0445\u0440\u0430\u043D\u0451\u043D\u043D\u043E\u0439 \u0441\u0435\u0441\u0441\u0438\u0438, \u043F\u043E\u0434\u0445\u043E\u0434\u044F\u0449\u0435\u0439 \u043F\u043E\u0434 \xAB${query}\xBB.`,
9049
+ sessionAmbiguous: (query, count2) => `\xAB${query}\xBB \u043F\u043E\u0434\u0445\u043E\u0434\u0438\u0442 \u043A ${count2} \u0441\u0435\u0441\u0441\u0438\u044F\u043C \u2014 \u0443\u0442\u043E\u0447\u043D\u0438 id.`,
9050
+ sessionsUsage: "\u0418\u0441\u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u043D\u0438\u0435: /sessions [list|rename [id] <\u043D\u0430\u0437\u0432\u0430\u043D\u0438\u0435>|delete [id|all]|export [id] [\u043F\u0443\u0442\u044C]]",
9051
+ modelNoMatch: (query) => `\u041D\u0435\u0442 \u043E\u0434\u043D\u043E\u0439 \u043C\u043E\u0434\u0435\u043B\u0438 \u043F\u043E\u0434 \xAB${query}\xBB \u2014 \u0432\u044B\u0431\u0435\u0440\u0438 \u0438\u0437 \u0441\u043F\u0438\u0441\u043A\u0430.`,
9052
+ memoryUsage: "/memory show \xB7 /memory set <\u0442\u0435\u043A\u0441\u0442> \xB7 /memory clear",
9053
+ memoryEmpty: "\u041F\u0430\u043C\u044F\u0442\u044C \u043F\u0440\u043E\u0435\u043A\u0442\u0430 \u043F\u0443\u0441\u0442\u0430.",
9054
+ memoryClearAsk: "\u041E\u0447\u0438\u0441\u0442\u0438\u0442\u044C \u043F\u0430\u043C\u044F\u0442\u044C \u043F\u0440\u043E\u0435\u043A\u0442\u0430?",
9055
+ memoryReplaceAsk: "\u0417\u0430\u043C\u0435\u043D\u0438\u0442\u044C \u0432\u0441\u0435 \u0437\u0430\u043C\u0435\u0442\u043A\u0438 \u043F\u0430\u043C\u044F\u0442\u0438 \u043F\u0440\u043E\u0435\u043A\u0442\u0430 \u044D\u0442\u0438\u043C \u0442\u0435\u043A\u0441\u0442\u043E\u043C?",
8662
9056
  promptDeleted: (name) => `\u041F\u0440\u043E\u043C\u0442 \xAB${name}\xBB \u0443\u0434\u0430\u043B\u0451\u043D`,
8663
9057
  promptNotFound: (name) => `\u041F\u0440\u043E\u043C\u0442 \xAB${name}\xBB \u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D.`,
8664
- promptSaveAsk: "\u0421\u043E\u0445\u0440\u0430\u043D\u0438\u0442\u044C \u043A\u0430\u043A \u043F\u0440\u043E\u043C\u0442?",
8665
- promptSaveAskBody: "\u0422\u0435\u043A\u0441\u0442 \u0431\u0443\u0434\u0435\u0442 \u0441\u043E\u0445\u0440\u0430\u043D\u0451\u043D \u0432 \u0444\u0430\u0439\u043B \u0438 \u0435\u0433\u043E \u043C\u043E\u0436\u043D\u043E \u0431\u0443\u0434\u0435\u0442 \u0437\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u044C \u043F\u043E\u0437\u0436\u0435 \u0447\u0435\u0440\u0435\u0437 /prompt.",
8666
9058
  promptsEmpty: "\u0421\u043E\u0445\u0440\u0430\u043D\u0451\u043D\u043D\u044B\u0445 \u043F\u0440\u043E\u043C\u0442\u043E\u0432 \u043D\u0435\u0442. \u0418\u0441\u043F\u043E\u043B\u044C\u0437\u0443\u0439 /prompt save <\u0438\u043C\u044F>.",
8667
9059
  unknownCommand: (name) => `\u041D\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043D\u0430\u044F \u043A\u043E\u043C\u0430\u043D\u0434\u0430 /${name}. \u041F\u043E\u043F\u0440\u043E\u0431\u0443\u0439 /help`,
8668
9060
  didYouMean: (typed, guess) => `\u041D\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043D\u0430\u044F \u043A\u043E\u043C\u0430\u043D\u0434\u0430 /${typed}. \u041C\u043E\u0436\u0435\u0442 \u0431\u044B\u0442\u044C /${guess}?`,
@@ -9811,10 +10203,10 @@ function StatusBar({ status }) {
9811
10203
  flexDirection: "column",
9812
10204
  children: [
9813
10205
  /* @__PURE__ */ jsxs9(Box9, { width: "100%", alignItems: "flex-start", children: [
9814
- /* @__PURE__ */ jsx9(Box9, { flexGrow: 1, flexShrink: 1, flexWrap: "wrap", children: details.map((item, index) => /* @__PURE__ */ jsxs9(Text9, { children: [
10206
+ /* @__PURE__ */ jsx9(Box9, { flexGrow: 1, flexShrink: 1, flexWrap: "wrap", children: details.map((item, index) => /* @__PURE__ */ jsx9(Box9, { flexShrink: 0, children: /* @__PURE__ */ jsxs9(Text9, { children: [
9815
10207
  index > 0 && /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: " \xB7 " }),
9816
10208
  item
9817
- ] }, index)) }),
10209
+ ] }) }, index)) }),
9818
10210
  /* @__PURE__ */ jsx9(Box9, { flexShrink: 0, marginLeft: 2, children: /* @__PURE__ */ jsx9(ContextMeter, { context: status.context }) })
9819
10211
  ] }),
9820
10212
  status.agents.length > 0 && /* @__PURE__ */ jsxs9(Box9, { width: "100%", flexDirection: "column", alignItems: "flex-end", children: [
@@ -9859,6 +10251,8 @@ function ContextMeter({
9859
10251
  context
9860
10252
  }) {
9861
10253
  const theme = useTheme();
10254
+ const { columns } = useTerminalSize();
10255
+ const compact = columns < 80;
9862
10256
  const { window: win, exact } = context;
9863
10257
  const used = exact ? context.used : 0;
9864
10258
  if (win === null || win <= 0) {
@@ -9871,6 +10265,12 @@ function ContextMeter({
9871
10265
  const filled = Math.max(0, Math.min(BAR_WIDTH, Math.round(BAR_WIDTH * pct)));
9872
10266
  const color = pct < 0.6 ? theme.ok : pct < 0.85 ? theme.warn : theme.error;
9873
10267
  const percent = Math.round(pct * 100);
10268
+ if (compact) {
10269
+ return /* @__PURE__ */ jsxs9(Text9, { children: [
10270
+ /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: "ctx " }),
10271
+ /* @__PURE__ */ jsx9(Text9, { color, bold: true, children: `${percent}%` })
10272
+ ] });
10273
+ }
9874
10274
  return /* @__PURE__ */ jsxs9(Text9, { children: [
9875
10275
  /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: "ctx " }),
9876
10276
  /* @__PURE__ */ jsx9(Text9, { color, bold: true, children: `${percent}%`.padStart(4) }),
@@ -10744,8 +11144,7 @@ var BubbleView = memo2(function BubbleView2({
10744
11144
  ] }) });
10745
11145
  }
10746
11146
  if (bubble.kind === "notice") {
10747
- const color = bubble.level === "error" ? theme.error : bubble.level === "warn" ? theme.warn : theme.accent;
10748
- return /* @__PURE__ */ jsx11(Box11, { marginTop: 1, children: /* @__PURE__ */ jsx11(Text11, { color, children: bubble.text }) });
11147
+ return /* @__PURE__ */ jsx11(NoticeView, { level: bubble.level, text: bubble.text });
10749
11148
  }
10750
11149
  if (bubble.kind === "assistant") {
10751
11150
  return /* @__PURE__ */ jsx11(AssistantView, { bubble, maxRows });
@@ -10819,6 +11218,24 @@ function wrapVisualLine(line, columns) {
10819
11218
  if (current !== "" || rows.length === 0) rows.push(current);
10820
11219
  return rows;
10821
11220
  }
11221
+ var NOTICE_ICONS = { warn: "\u25B2", error: "\u2717" };
11222
+ function NoticeView({ level, text }) {
11223
+ const theme = useTheme();
11224
+ const newline = text.indexOf("\n");
11225
+ const headline = newline === -1 ? text : text.slice(0, newline);
11226
+ const details = newline === -1 ? "" : text.slice(newline + 1);
11227
+ if (level === "info") {
11228
+ return /* @__PURE__ */ jsx11(Box11, { marginTop: 1, paddingLeft: 2, children: /* @__PURE__ */ jsx11(Text11, { dimColor: details === "", children: text }) });
11229
+ }
11230
+ const color = level === "error" ? theme.error : theme.warn;
11231
+ return /* @__PURE__ */ jsxs11(Box11, { marginTop: 1, children: [
11232
+ /* @__PURE__ */ jsx11(Box11, { width: 2, flexShrink: 0, children: /* @__PURE__ */ jsx11(Text11, { color, bold: true, children: NOTICE_ICONS[level] }) }),
11233
+ /* @__PURE__ */ jsxs11(Box11, { flexDirection: "column", flexShrink: 1, minWidth: 0, children: [
11234
+ /* @__PURE__ */ jsx11(Text11, { color, bold: level === "error", children: headline }),
11235
+ details !== "" && /* @__PURE__ */ jsx11(Text11, { children: details })
11236
+ ] })
11237
+ ] });
11238
+ }
10822
11239
  function ToolView({ bubble }) {
10823
11240
  const theme = useTheme();
10824
11241
  const mark = bubble.state === "running" ? "\u25CC" : bubble.state === "ok" ? "\u25CF" : "\u2717";
@@ -11155,7 +11572,7 @@ function closeAssistant(bubbles) {
11155
11572
  (bubble) => bubble.kind === "assistant" && bubble.streaming ? { ...bubble, streaming: false } : bubble
11156
11573
  );
11157
11574
  }
11158
- function fromHistory(messages) {
11575
+ function fromHistory(messages, summarize = (name) => name) {
11159
11576
  let state = emptyTranscript();
11160
11577
  for (const message of messages) {
11161
11578
  if (message.role === "user") {
@@ -11186,7 +11603,7 @@ function fromHistory(messages) {
11186
11603
  kind: "tool",
11187
11604
  id: block.id,
11188
11605
  name: sanitizeTerminalText(block.name),
11189
- summary: sanitizeTerminalText(block.name),
11606
+ summary: sanitizeTerminalText(safeSummary(summarize, block.name, block.input)),
11190
11607
  state: "ok",
11191
11608
  content: ""
11192
11609
  }
@@ -11249,6 +11666,13 @@ function sanitizeDisplay(display) {
11249
11666
  after: sanitizeTerminalText(display.after)
11250
11667
  };
11251
11668
  }
11669
+ function safeSummary(summarize, name, input) {
11670
+ try {
11671
+ return summarize(name, input) || name;
11672
+ } catch {
11673
+ return name;
11674
+ }
11675
+ }
11252
11676
 
11253
11677
  // src/ui/App.tsx
11254
11678
  import { jsx as jsx14, jsxs as jsxs13 } from "react/jsx-runtime";
@@ -11258,6 +11682,7 @@ var CUSTOM_ACCENT = "__custom__";
11258
11682
  var BELL_AFTER_MS = 15e3;
11259
11683
  var SUBAGENT_CHAT_EXTRA_ROWS = 4;
11260
11684
  var MAX_ATTACHMENTS = 8;
11685
+ var MAX_ATTACHMENT_PAYLOAD = 24e6;
11261
11686
  var CONTEXT_PRESETS = [256e3, 512e3, 1e6];
11262
11687
  function formatContextTokens(tokens2) {
11263
11688
  if (tokens2 >= 1e6) return `${tokens2 / 1e6}M`;
@@ -11279,7 +11704,7 @@ function App({
11279
11704
  const { stdout } = useStdout2();
11280
11705
  const { rows } = useTerminalSize();
11281
11706
  const [transcript, setTranscript] = useState8(
11282
- () => warnings.reduce((state, text) => pushNotice(state, "warn", text), fromHistory(initialHistory))
11707
+ () => warnings.reduce((state, text) => pushNotice(state, "warn", text), fromHistory(initialHistory, runtime.summarizeToolCall))
11283
11708
  );
11284
11709
  const [transcriptRevision, setTranscriptRevision] = useState8(0);
11285
11710
  const transcriptRef = useRef5(transcript);
@@ -11334,11 +11759,16 @@ function App({
11334
11759
  }, []);
11335
11760
  const appendAttachment = useCallback((block) => {
11336
11761
  if (attachmentsRef.current.length >= MAX_ATTACHMENTS) return false;
11762
+ if (attachmentsRef.current.some((queued) => sameAttachment(queued, block))) return true;
11763
+ const payload = attachmentsRef.current.reduce((sum, queued) => sum + attachmentPayload(queued), 0);
11764
+ if (payload + attachmentPayload(block) > MAX_ATTACHMENT_PAYLOAD) {
11765
+ throw new Error(stringsFor(lang).attachmentsTooLarge(Math.round(MAX_ATTACHMENT_PAYLOAD / 1e6)));
11766
+ }
11337
11767
  const next = [...attachmentsRef.current, block];
11338
11768
  attachmentsRef.current = next;
11339
11769
  setAttachments(next);
11340
11770
  return true;
11341
- }, []);
11771
+ }, [lang]);
11342
11772
  const flushTranscriptEvents = useCallback(() => {
11343
11773
  if (transcriptTimer.current !== null) {
11344
11774
  clearTimeout(transcriptTimer.current);
@@ -11542,10 +11972,11 @@ function App({
11542
11972
  try {
11543
11973
  const restored = await runtime.resumeSession(choice);
11544
11974
  history.current = restored;
11975
+ replaceAttachments([]);
11545
11976
  setPromptHistory(inputHistoryFromMessages(restored));
11546
11977
  setTranscript(
11547
11978
  pushNotice(
11548
- fromHistory(restored),
11979
+ fromHistory(restored, runtime.summarizeToolCall),
11549
11980
  "info",
11550
11981
  strings.resumed(choice.slice(-6), restored.length)
11551
11982
  )
@@ -11609,12 +12040,18 @@ function App({
11609
12040
  forceRender((n) => n + 1);
11610
12041
  return;
11611
12042
  }
12043
+ const matching = (query) => items.filter((item) => item.key === query || item.key.includes(query));
11612
12044
  let id = action ? rest[1] : void 0;
12045
+ let argsFrom = 2;
12046
+ if (id && (action === "rename" || action === "export") && matching(id).length === 0) {
12047
+ id = void 0;
12048
+ argsFrom = 1;
12049
+ }
11613
12050
  if (!id) id = await pick(strings.titleSessions, items) ?? void 0;
11614
12051
  if (!id) return;
11615
- const matches = items.filter((item) => item.key === id || item.key.includes(id));
12052
+ const matches = matching(id);
11616
12053
  if (matches.length !== 1) {
11617
- notice("warn", `No saved session matches "${id}".`);
12054
+ notice("warn", matches.length === 0 ? strings.sessionNoMatch(id) : strings.sessionAmbiguous(id, matches.length));
11618
12055
  return;
11619
12056
  }
11620
12057
  id = matches[0]?.key ?? id;
@@ -11634,13 +12071,13 @@ function App({
11634
12071
  replaceAttachments([]);
11635
12072
  setPromptHistory(inputHistoryFromMessages(restored));
11636
12073
  setTranscript(
11637
- pushNotice(fromHistory(restored), "info", strings.resumed(id.slice(-6), restored.length))
12074
+ pushNotice(fromHistory(restored, runtime.summarizeToolCall), "info", strings.resumed(id.slice(-6), restored.length))
11638
12075
  );
11639
12076
  setTranscriptRevision((revision) => revision + 1);
11640
12077
  sessionStart.current = Date.now();
11641
12078
  turns.current = 0;
11642
12079
  } else if (action === "rename") {
11643
- const title = rest.slice(2).join(" ").trim();
12080
+ const title = rest.slice(argsFrom).join(" ").trim();
11644
12081
  if (!title) {
11645
12082
  setInput(`/sessions rename ${id} `);
11646
12083
  notice("info", strings.sessionRenameUsage);
@@ -11668,10 +12105,10 @@ function App({
11668
12105
  notice("info", deletedMessage);
11669
12106
  }
11670
12107
  } else if (action === "export") {
11671
- const destination = unquoteArg(rest.slice(2).join(" ").trim()) || void 0;
12108
+ const destination = unquoteArg(rest.slice(argsFrom).join(" ").trim()) || void 0;
11672
12109
  notice("info", strings.sessionExported(await runtime.exportSession(id, destination)));
11673
12110
  } else {
11674
- notice("warn", "Usage: /sessions [list|rename|delete [all]|export]");
12111
+ notice("warn", strings.sessionsUsage);
11675
12112
  return;
11676
12113
  }
11677
12114
  } catch (error) {
@@ -11823,7 +12260,8 @@ ${strings.modelSet(result.nextModel)}` : ""}`;
11823
12260
  return;
11824
12261
  }
11825
12262
  case "lang": {
11826
- const choice = await pick(
12263
+ const typed = rawRest.toLowerCase();
12264
+ const choice = LANGS.some((entry) => entry.key === typed) ? typed : await pick(
11827
12265
  strings.titleLang,
11828
12266
  LANGS.map((entry) => ({ key: entry.key, label: entry.label }))
11829
12267
  );
@@ -11850,6 +12288,33 @@ ${strings.modelSet(result.nextModel)}` : ""}`;
11850
12288
  }
11851
12289
  return;
11852
12290
  }
12291
+ if (action === "remove" || action === "uninstall") {
12292
+ let name = rest[1];
12293
+ if (!name) {
12294
+ const installed = runtime.listSkills();
12295
+ if (installed.length === 0) {
12296
+ notice("info", strings.skillsEmpty);
12297
+ return;
12298
+ }
12299
+ name = await pick(
12300
+ strings.titleSkillsRemove,
12301
+ installed.map((skill) => ({ key: skill.name, label: skill.name, hint: skill.description || void 0 }))
12302
+ ) ?? void 0;
12303
+ if (!name) return;
12304
+ }
12305
+ try {
12306
+ await runtime.uninstallSkill(name);
12307
+ notice("info", strings.skillsRemoved(name));
12308
+ forceRender((n) => n + 1);
12309
+ } catch (error) {
12310
+ notice("error", error instanceof Error ? error.message : String(error));
12311
+ }
12312
+ return;
12313
+ }
12314
+ if (action) {
12315
+ notice("warn", strings.skillsInstallUsage);
12316
+ return;
12317
+ }
11853
12318
  const skills = runtime.listSkills();
11854
12319
  if (skills.length === 0) {
11855
12320
  notice("info", strings.skillsEmpty);
@@ -11915,7 +12380,7 @@ ${body2}`));
11915
12380
  case "mcp": {
11916
12381
  const action = rest[0]?.toLowerCase();
11917
12382
  if (action === "add") {
11918
- const parsed = parseMcpAddArgs(rest.slice(1));
12383
+ const parsed = parseMcpAddCommand(rawRest.replace(/^\S+\s*/, ""));
11919
12384
  if (!parsed.ok) {
11920
12385
  const problem = mcpAddProblem(strings, parsed.error);
11921
12386
  notice(
@@ -12058,7 +12523,7 @@ ${strings.mcpAddUsage}`
12058
12523
  }
12059
12524
  if (rawRest.toLowerCase() === "clipboard") {
12060
12525
  if (attachmentsRef.current.length >= MAX_ATTACHMENTS) {
12061
- notice("warn", `At most ${MAX_ATTACHMENTS} attachments can be queued for one message.`);
12526
+ notice("warn", strings.attachmentsLimit(MAX_ATTACHMENTS));
12062
12527
  return;
12063
12528
  }
12064
12529
  if (clipboardPasteTask.current) return;
@@ -12088,7 +12553,7 @@ ${strings.mcpAddUsage}`
12088
12553
  return;
12089
12554
  }
12090
12555
  if (attachmentsRef.current.length >= MAX_ATTACHMENTS) {
12091
- notice("warn", `At most ${MAX_ATTACHMENTS} attachments can be queued for one message.`);
12556
+ notice("warn", strings.attachmentsLimit(MAX_ATTACHMENTS));
12092
12557
  return;
12093
12558
  }
12094
12559
  try {
@@ -12116,7 +12581,7 @@ ${strings.mcpAddUsage}`
12116
12581
  history.current = result.history;
12117
12582
  setPromptHistory(inputHistoryFromMessages(result.history));
12118
12583
  setTranscript(
12119
- pushNotice(fromHistory(result.history), "info", strings.compactDone(result.removedMessages, result.clearedToolResults ?? 0, result.tokensBefore, result.tokensAfter))
12584
+ pushNotice(fromHistory(result.history, runtime.summarizeToolCall), "info", strings.compactDone(result.removedMessages, result.clearedToolResults ?? 0, result.tokensBefore, result.tokensAfter))
12120
12585
  );
12121
12586
  setTranscriptRevision((revision) => revision + 1);
12122
12587
  await runtime.persist(result.history);
@@ -12185,28 +12650,47 @@ ${strings.mcpAddUsage}`
12185
12650
  return;
12186
12651
  }
12187
12652
  case "bell": {
12188
- const next = rawRest === "on" ? true : rawRest === "off" ? false : !runtime.getBell();
12653
+ const setting = rawRest.toLowerCase();
12654
+ if (setting !== "" && setting !== "on" && setting !== "off") {
12655
+ notice("warn", "/bell [on|off]");
12656
+ return;
12657
+ }
12658
+ const next = setting === "on" ? true : setting === "off" ? false : !runtime.getBell();
12189
12659
  await runtime.setBell(next);
12190
12660
  notice("info", strings.bellState(next));
12191
12661
  return;
12192
12662
  }
12193
12663
  case "thinking": {
12194
- const next = !runtime.getThinking();
12664
+ const setting = rawRest.toLowerCase();
12665
+ if (setting !== "" && setting !== "on" && setting !== "off") {
12666
+ notice("warn", "/thinking [on|off]");
12667
+ return;
12668
+ }
12669
+ const next = setting === "" ? !runtime.getThinking() : setting === "on";
12195
12670
  await runtime.setThinking(next);
12196
12671
  notice("info", strings.reasoning(next));
12197
12672
  forceRender((n) => n + 1);
12198
12673
  return;
12199
12674
  }
12200
12675
  case "memory": {
12201
- if (rawRest === "clear") {
12202
- 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();
12203
- } else if (rawRest.startsWith("set ")) {
12204
- await runtime.saveMemory(rawRest.slice(4).trim());
12205
- } else if (rawRest !== "" && rawRest !== "show") {
12206
- notice("warn", "/memory show | /memory set <text> | /memory clear");
12676
+ const sub = rawRest.split(/\s+/)[0] ?? "";
12677
+ const action = sub.toLowerCase();
12678
+ if (action === "clear") {
12679
+ if (!await ask2(strings.memoryClearAsk, void 0, true)) return;
12680
+ await runtime.clearMemory();
12681
+ } else if (action === "set") {
12682
+ const text = rawRest.slice(sub.length).trim();
12683
+ if (!text) {
12684
+ notice("warn", strings.memoryUsage);
12685
+ return;
12686
+ }
12687
+ if (runtime.readMemory() && !await ask2(strings.memoryReplaceAsk, void 0, true)) return;
12688
+ await runtime.saveMemory(text);
12689
+ } else if (action !== "" && action !== "show") {
12690
+ notice("warn", strings.memoryUsage);
12207
12691
  return;
12208
12692
  }
12209
- 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."));
12693
+ notice("info", runtime.readMemory() || strings.memoryEmpty);
12210
12694
  return;
12211
12695
  }
12212
12696
  case "effort": {
@@ -12224,7 +12708,7 @@ ${strings.mcpAddUsage}`
12224
12708
  case "budget": {
12225
12709
  const current = runtime.getMaxTokensPerTurn();
12226
12710
  if (rest[0]) {
12227
- const tokens2 = Number(rest[0]);
12711
+ const tokens2 = rest[0] === "0" ? 0 : parseContextTokens(rest[0]) ?? Number.NaN;
12228
12712
  if (tokens2 === 0) {
12229
12713
  await runtime.setMaxTokensPerTurn(0);
12230
12714
  notice("info", strings.budgetUnlimited);
@@ -12298,7 +12782,13 @@ ${strings.mcpAddUsage}`
12298
12782
  notice("warn", strings.noModels);
12299
12783
  return;
12300
12784
  }
12301
- const choice = await pick(strings.titleModel, items);
12785
+ const typed = rawRest.toLowerCase();
12786
+ const direct = typed ? items.find((item) => item.key.toLowerCase() === typed) ?? (() => {
12787
+ const partial = items.filter((item) => item.key.toLowerCase().includes(typed));
12788
+ return partial.length === 1 ? partial[0] : void 0;
12789
+ })() : void 0;
12790
+ if (typed && !direct) notice("warn", strings.modelNoMatch(rawRest));
12791
+ const choice = direct?.key ?? await pick(strings.titleModel, items);
12302
12792
  if (!choice) return;
12303
12793
  await runtime.setModelRef(choice);
12304
12794
  notice("info", strings.modelSet(choice));
@@ -12327,74 +12817,57 @@ ${strings.mcpAddUsage}`
12327
12817
  }
12328
12818
  case "prompt": {
12329
12819
  const sub = rest[0]?.toLowerCase();
12820
+ const savePromptAs = async (name, body2) => {
12821
+ if (await runtime.promptExists(name) && !await ask2(strings.promptOverwriteAsk(name), void 0, true)) {
12822
+ return;
12823
+ }
12824
+ await runtime.savePrompt(name, body2);
12825
+ notice("info", strings.promptSaved(name));
12826
+ };
12330
12827
  if (sub === "save") {
12331
- const name = rest.slice(1).join(" ").trim();
12332
- if (name) {
12333
- const body2 = lastUserText(history.current);
12334
- if (!body2) {
12335
- notice("warn", strings.promptNothing);
12336
- return;
12337
- }
12338
- await runtime.savePrompt(name, body2);
12339
- notice("info", strings.promptSaved(name));
12828
+ const recent = recentUserMessages(history.current, 20).reverse();
12829
+ if (recent.length === 0) {
12830
+ notice("warn", strings.promptNothing);
12340
12831
  return;
12341
12832
  }
12342
- const recent = recentUserMessages(history.current, 20);
12343
- if (recent.length > 0) {
12344
- const items2 = recent.map((text, i) => ({
12345
- key: `__recent_${i}__`,
12346
- label: text.length > 60 ? `${text.slice(0, 60)}\u2026` : text,
12347
- hint: text.length > 60 ? void 0 : void 0
12348
- }));
12349
- items2.push({ key: "__custom__", label: "\u2014 type a custom prompt \u2014" });
12350
- const choice2 = await pick(strings.titlePrompts, items2);
12351
- if (!choice2) return;
12352
- if (choice2 === "__custom__") {
12353
- setInput("/prompt save ");
12354
- return;
12355
- }
12356
- const index = parseInt(choice2.replace("__recent_", "").replace("__", ""), 10);
12357
- const body2 = recent[index] ?? "";
12358
- if (!body2) {
12359
- notice("warn", strings.promptNothing);
12360
- return;
12361
- }
12362
- if (!await ask2(strings.promptSaveAsk, strings.promptSaveAskBody)) return;
12363
- const defaultName = body2.split(/\s+/).slice(0, 4).join("-").replace(/[^a-z0-9-]/gi, "").toLowerCase();
12364
- const nameInput = `${defaultName || "prompt"}`;
12365
- await runtime.savePrompt(nameInput, body2);
12366
- notice("info", `${strings.promptSaved(nameInput)}
12367
- Rename the file if you want a different name.`);
12833
+ const typedName = rest.slice(1).join(" ").trim();
12834
+ if (typedName) {
12835
+ await savePromptAs(typedName, lastUserText(history.current));
12368
12836
  return;
12369
12837
  }
12370
- notice("warn", strings.promptNothing);
12838
+ const choice2 = await pick(
12839
+ strings.titlePrompts,
12840
+ recent.map((text, index) => ({
12841
+ key: String(index),
12842
+ label: text.length > 60 ? `${text.slice(0, 60)}\u2026` : text
12843
+ }))
12844
+ );
12845
+ if (!choice2) return;
12846
+ const body2 = recent[Number(choice2)];
12847
+ if (!body2) return;
12848
+ const name = (await promptInput(strings.promptNameTitle))?.trim();
12849
+ if (!name) return;
12850
+ await savePromptAs(name, body2);
12371
12851
  return;
12372
12852
  }
12373
12853
  if (sub === "delete") {
12374
- const name = rest.slice(1).join(" ").trim();
12375
- if (name) {
12376
- try {
12377
- await runtime.deletePrompt(name);
12378
- notice("info", strings.promptDeleted(name));
12379
- } catch (error) {
12380
- notice("error", error instanceof Error ? error.message : String(error));
12854
+ let name = rest.slice(1).join(" ").trim() || void 0;
12855
+ if (!name) {
12856
+ const items2 = await runtime.listPromptItems();
12857
+ if (items2.length === 0) {
12858
+ notice("info", strings.promptsEmpty);
12859
+ return;
12381
12860
  }
12382
- return;
12383
- }
12384
- const items2 = await runtime.listPromptItems();
12385
- if (items2.length === 0) {
12386
- notice("info", strings.promptsEmpty);
12387
- return;
12388
- }
12389
- const choice2 = await pick(strings.titlePrompts, items2);
12390
- if (!choice2) return;
12391
- if (!await ask2(strings.promptDeleteUsage.replace("Usage: ", "").replace("<name>", choice2 || "") + "?", strings.sessionDeleteAsk, true)) return;
12392
- try {
12393
- await runtime.deletePrompt(choice2);
12394
- notice("info", strings.promptDeleted(choice2));
12395
- } catch (error) {
12396
- notice("error", error instanceof Error ? error.message : String(error));
12861
+ name = await pick(strings.titlePrompts, items2) ?? void 0;
12862
+ if (!name) return;
12397
12863
  }
12864
+ if (!await ask2(strings.promptDeleteAsk(name), void 0, true)) return;
12865
+ await runtime.deletePrompt(name);
12866
+ notice("info", strings.promptDeleted(name));
12867
+ return;
12868
+ }
12869
+ if (rawRest && await runtime.promptExists(rawRest)) {
12870
+ setInput(await runtime.readPrompt(rawRest));
12398
12871
  return;
12399
12872
  }
12400
12873
  const items = await runtime.listPromptItems();
@@ -12504,11 +12977,10 @@ Rename the file if you want a different name.`);
12504
12977
  const tryQueueAutomaticAttachment = useCallback(
12505
12978
  (requestedPath) => {
12506
12979
  if (!looksLikeAttachmentPath(requestedPath)) return Promise.resolve(false);
12507
- if (automaticAttachmentTask.current) {
12508
- return automaticAttachmentTask.current.then(() => false);
12509
- }
12980
+ const running = automaticAttachmentTask.current;
12981
+ if (running) return running.then(() => tryQueueAutomaticAttachmentRef.current(requestedPath));
12510
12982
  if (attachmentsRef.current.length >= MAX_ATTACHMENTS) {
12511
- notice("warn", `At most ${MAX_ATTACHMENTS} attachments can be queued for one message.`);
12983
+ notice("warn", strings.attachmentsLimit(MAX_ATTACHMENTS));
12512
12984
  return Promise.resolve(true);
12513
12985
  }
12514
12986
  const paths = splitAttachmentPaths(requestedPath) ?? [requestedPath];
@@ -12521,7 +12993,7 @@ Rename the file if you want a different name.`);
12521
12993
  if (automaticAttachmentTask.current !== task) return true;
12522
12994
  if (!block) continue;
12523
12995
  if (!appendAttachment(block)) {
12524
- notice("warn", `At most ${MAX_ATTACHMENTS} attachments can be queued for one message.`);
12996
+ notice("warn", strings.attachmentsLimit(MAX_ATTACHMENTS));
12525
12997
  return true;
12526
12998
  }
12527
12999
  added += 1;
@@ -12543,6 +13015,8 @@ Rename the file if you want a different name.`);
12543
13015
  },
12544
13016
  [appendAttachment, notice, runtime, strings]
12545
13017
  );
13018
+ const tryQueueAutomaticAttachmentRef = useRef5(tryQueueAutomaticAttachment);
13019
+ tryQueueAutomaticAttachmentRef.current = tryQueueAutomaticAttachment;
12546
13020
  const listWorkspaceFiles = useCallback(
12547
13021
  (query) => runtime.listWorkspaceFiles(query).catch(() => []),
12548
13022
  [runtime]
@@ -12550,13 +13024,13 @@ Rename the file if you want a different name.`);
12550
13024
  const pasteClipboardImage = useCallback(() => {
12551
13025
  if (clipboardPasteTask.current) return;
12552
13026
  if (attachmentsRef.current.length >= MAX_ATTACHMENTS) {
12553
- notice("warn", `At most ${MAX_ATTACHMENTS} attachments can be queued for one message.`);
13027
+ notice("warn", strings.attachmentsLimit(MAX_ATTACHMENTS));
12554
13028
  return;
12555
13029
  }
12556
13030
  const task = runtime.loadClipboardImage().then((block) => {
12557
13031
  if (clipboardPasteTask.current !== task) return;
12558
13032
  if (!appendAttachment(block)) {
12559
- notice("warn", `At most ${MAX_ATTACHMENTS} attachments can be queued for one message.`);
13033
+ notice("warn", strings.attachmentsLimit(MAX_ATTACHMENTS));
12560
13034
  return;
12561
13035
  }
12562
13036
  notice("info", strings.attachmentAdded(attachmentLabel(block) ?? "clipboard image"));
@@ -12612,11 +13086,15 @@ Rename the file if you want a different name.`);
12612
13086
  const queuedAttachments = attachmentsRef.current;
12613
13087
  if (!text && queuedAttachments.length === 0) return;
12614
13088
  const mentioned = [];
12615
- for (const path15 of extractMentions(text)) {
13089
+ for (const path16 of extractMentions(text)) {
12616
13090
  if (queuedAttachments.length + mentioned.length >= MAX_ATTACHMENTS) break;
12617
13091
  try {
12618
- mentioned.push(await runtime.loadAttachment(path15));
12619
- } catch {
13092
+ const block = await runtime.loadMentionAttachment(path16);
13093
+ if (!block) continue;
13094
+ mentioned.push(block);
13095
+ notice("info", strings.mentionAttached(attachmentLabel(block) ?? path16));
13096
+ } catch (error) {
13097
+ if (error instanceof Error && !error.code) notice("warn", error.message);
12620
13098
  }
12621
13099
  }
12622
13100
  if (generation !== attachmentGeneration.current) return;
@@ -12897,6 +13375,12 @@ function mcpAddProblem(strings, error) {
12897
13375
  return strings.mcpAddInvalidUrl;
12898
13376
  case "http-args":
12899
13377
  return strings.mcpAddHttpArgs;
13378
+ case "invalid-env":
13379
+ return strings.mcpAddInvalidEnv;
13380
+ case "invalid-header":
13381
+ return strings.mcpAddInvalidHeader;
13382
+ case "option-mismatch":
13383
+ return strings.mcpAddOptionMismatch;
12900
13384
  case "usage":
12901
13385
  return strings.mcpAddUsage;
12902
13386
  }
@@ -12911,6 +13395,16 @@ function mcpStateLine(strings, state) {
12911
13395
  function formatBalances(balances) {
12912
13396
  return balances.map((item) => `${item.amount} ${item.currency}`).join(" \xB7 ");
12913
13397
  }
13398
+ function attachmentPayload(block) {
13399
+ if (block.type === "image") return block.data.length;
13400
+ if (block.type === "file") return Buffer.byteLength(block.text);
13401
+ return 0;
13402
+ }
13403
+ function sameAttachment(a, b) {
13404
+ if (a.type === "image" && b.type === "image") return a.name === b.name && a.data === b.data;
13405
+ if (a.type === "file" && b.type === "file") return a.name === b.name && a.text === b.text;
13406
+ return false;
13407
+ }
12914
13408
  function lastUserText(messages) {
12915
13409
  for (let index = messages.length - 1; index >= 0; index -= 1) {
12916
13410
  const message = messages[index];
@@ -12991,12 +13485,9 @@ program.name("kitcode").description("Terminal coding agent with a config you nev
12991
13485
  if (options.mode && !MODES2.includes(options.mode)) {
12992
13486
  throw new Error(`Unknown mode "${options.mode}". Use normal, accept or plan.`);
12993
13487
  }
12994
- let cwd = options.cwd;
12995
- if (cwd) {
12996
- try {
12997
- cwd = resolve2(cwd);
12998
- } catch {
12999
- }
13488
+ const cwd = options.cwd ? resolve2(options.cwd) : void 0;
13489
+ if (cwd && !statSync(cwd, { throwIfNoEntry: false })?.isDirectory()) {
13490
+ throw new Error(`--cwd is not a directory: ${cwd}`);
13000
13491
  }
13001
13492
  await startTui({
13002
13493
  continueSession: options.continue,
@@ -13049,8 +13540,9 @@ prompts.command("list").description("list saved prompts").action(async () => {
13049
13540
  console.log(`${prompt.slug} ${prompt.name}${prompt.description ? ` \u2014 ${prompt.description}` : ""}`);
13050
13541
  }
13051
13542
  });
13052
- prompts.command("rm").description("delete a saved prompt").argument("<slug>").action(async (slug) => {
13053
- console.log(await deletePrompt(slug) ? `deleted ${slug}` : `no such prompt: ${slug}`);
13543
+ prompts.command("rm").description("delete a saved prompt").argument("<name>").action(async (name) => {
13544
+ const slug = promptSlug(name);
13545
+ console.log(slug && await deletePrompt(slug) ? `deleted ${name}` : `no such prompt: ${name}`);
13054
13546
  });
13055
13547
  program.parseAsync(process.argv).catch((error) => {
13056
13548
  console.error(error instanceof Error ? error.message : String(error));