@kernelonpanic/kitcode 1.4.1 → 1.4.3

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);
@@ -168,7 +170,10 @@ function describeProviderFailure(error, providerId, knownSecrets = []) {
168
170
  }
169
171
  function statusOf(error) {
170
172
  const value = prop(error, "status") ?? prop(error, "statusCode");
171
- return typeof value === "number" && value >= 100 ? value : void 0;
173
+ if (typeof value === "number" && value >= 100) return value;
174
+ const code = prop(error, "code") ?? prop(prop(error, "error"), "code");
175
+ const numeric = typeof code === "string" && /^\d{3}$/.test(code) ? Number(code) : code;
176
+ return typeof numeric === "number" && numeric >= 400 && numeric <= 599 ? numeric : void 0;
172
177
  }
173
178
  function isUserAbort(error, signal) {
174
179
  return signal?.aborted === true || abortKind(error) === "user";
@@ -344,11 +349,11 @@ function oneLine(text) {
344
349
  return text.replace(/\s+/g, " ").trim();
345
350
  }
346
351
  function redactSecrets(text, knownSecrets = []) {
347
- let safe = text.replace(NAMED_SECRET, "$1[redacted]").replace(SECRET, "[redacted]");
352
+ let safe = text;
348
353
  for (const secret of knownSecrets) {
349
354
  if (secret.length >= 4) safe = safe.split(secret).join("[redacted]");
350
355
  }
351
- return safe;
356
+ return safe.replace(NAMED_SECRET, "$1[redacted]").replace(SECRET, "[redacted]");
352
357
  }
353
358
  function prop(value, key) {
354
359
  if (value === null || typeof value !== "object") return void 0;
@@ -1182,11 +1187,11 @@ var globTool = {
1182
1187
  return `glob(${brief(input.pattern)})`;
1183
1188
  },
1184
1189
  async execute(input, ctx) {
1185
- const { pattern, path: path15 = "." } = input;
1190
+ const { pattern, path: path16 = "." } = input;
1186
1191
  if (patternEscapes(pattern)) {
1187
1192
  return { content: `Pattern ${pattern} must be relative to the workspace root.`, isError: true };
1188
1193
  }
1189
- const safe = resolveInside(ctx.cwd, path15);
1194
+ const safe = resolveInside(ctx.cwd, path16);
1190
1195
  if (!safe.ok) return { content: safe.reason, isError: true };
1191
1196
  const entries = await fg(pattern, {
1192
1197
  cwd: safe.path,
@@ -1265,8 +1270,10 @@ function isSubsequence(needle, haystack) {
1265
1270
 
1266
1271
  // src/tools/jobs.ts
1267
1272
  import { spawn } from "child_process";
1273
+ import { StringDecoder } from "string_decoder";
1268
1274
  var MAX_JOBS = 8;
1269
1275
  var MAX_JOB_OUTPUT = 1e5;
1276
+ var MAX_FINISHED_JOBS = 16;
1270
1277
  var jobs = /* @__PURE__ */ new Map();
1271
1278
  var counter = 0;
1272
1279
  function startJob(command, cwd) {
@@ -1274,6 +1281,8 @@ function startJob(command, cwd) {
1274
1281
  if (live.length >= MAX_JOBS) {
1275
1282
  return { error: `At most ${MAX_JOBS} background commands can run at once.` };
1276
1283
  }
1284
+ pruneFinishedJobs();
1285
+ installExitCleanup();
1277
1286
  counter += 1;
1278
1287
  const id = `bg-${counter}`;
1279
1288
  const child = spawn(command, {
@@ -1295,19 +1304,24 @@ function startJob(command, cwd) {
1295
1304
  truncated: false
1296
1305
  };
1297
1306
  jobs.set(id, job);
1298
- const collect = (chunk) => {
1299
- const text = chunk.toString("utf8");
1307
+ const collector = () => {
1308
+ const decoder = new StringDecoder("utf8");
1309
+ return (chunk) => collect(decoder.write(chunk));
1310
+ };
1311
+ const collect = (text) => {
1300
1312
  const room = MAX_JOB_OUTPUT - job.buffer.length;
1301
1313
  if (text.length <= room) {
1302
1314
  job.buffer += text;
1303
1315
  return;
1304
1316
  }
1305
1317
  job.truncated = true;
1306
- job.buffer = (job.buffer + text).slice(-MAX_JOB_OUTPUT);
1307
- job.reported = Math.min(job.reported, job.buffer.length);
1318
+ const combined = job.buffer + text;
1319
+ const dropped = combined.length - MAX_JOB_OUTPUT;
1320
+ job.buffer = combined.slice(dropped);
1321
+ job.reported = Math.max(0, job.reported - dropped);
1308
1322
  };
1309
- child.stdout?.on("data", collect);
1310
- child.stderr?.on("data", collect);
1323
+ child.stdout?.on("data", collector());
1324
+ child.stderr?.on("data", collector());
1311
1325
  child.on("error", (error) => {
1312
1326
  job.buffer += `
1313
1327
  Command failed to start: ${error.message}`;
@@ -1348,6 +1362,26 @@ function killAllJobs() {
1348
1362
  }
1349
1363
  }
1350
1364
  }
1365
+ var exitCleanupInstalled = false;
1366
+ function installExitCleanup() {
1367
+ if (exitCleanupInstalled) return;
1368
+ exitCleanupInstalled = true;
1369
+ process.once("exit", killAllJobs);
1370
+ for (const signal of ["SIGTERM", "SIGHUP"]) {
1371
+ const onSignal = () => {
1372
+ killAllJobs();
1373
+ process.off(signal, onSignal);
1374
+ process.kill(process.pid, signal);
1375
+ };
1376
+ process.on(signal, onSignal);
1377
+ }
1378
+ }
1379
+ function pruneFinishedJobs() {
1380
+ const finished = [...jobs.values()].filter((job) => job.state !== "running");
1381
+ for (const job of finished.slice(0, Math.max(0, finished.length - MAX_FINISHED_JOBS + 1))) {
1382
+ jobs.delete(job.id);
1383
+ }
1384
+ }
1351
1385
  function summary(job) {
1352
1386
  return {
1353
1387
  id: job.id,
@@ -1409,13 +1443,20 @@ function resolveEffort(effort, messages) {
1409
1443
  return text.length > 500 || HARD_TASK.test(text) ? "high" : "medium";
1410
1444
  }
1411
1445
  var rejected = /* @__PURE__ */ new Set();
1446
+ var capped = /* @__PURE__ */ new Map();
1447
+ var RANK = { low: 0, medium: 1, high: 2, xhigh: 3, max: 4 };
1412
1448
  function markEffortRejected(providerId, model) {
1413
1449
  rejected.add(`${providerId}/${model}`);
1414
1450
  }
1451
+ function markEffortCapped(providerId, model, level) {
1452
+ capped.set(`${providerId}/${model}`, level);
1453
+ }
1415
1454
  function effectiveEffort(kind, model, effort, messages, providerId) {
1416
1455
  if (providerId !== void 0 && rejected.has(`${providerId}/${model}`)) return void 0;
1417
1456
  const requested = resolveEffort(effort, messages);
1418
- return kind === "openai" ? openAiEffort(model, requested) : anthropicEffort(model, requested);
1457
+ const level = kind === "openai" ? openAiEffort(model, requested) : anthropicEffort(model, requested);
1458
+ const cap = providerId === void 0 ? void 0 : capped.get(`${providerId}/${model}`);
1459
+ return level && cap && RANK[level] > RANK[cap] ? cap : level;
1419
1460
  }
1420
1461
  function anthropicEffort(model, effort) {
1421
1462
  if (!effort) return void 0;
@@ -1442,12 +1483,12 @@ function openAiEffort(model, effort) {
1442
1483
  }
1443
1484
 
1444
1485
  // src/app/runtime.ts
1445
- import path14 from "path";
1486
+ import path15 from "path";
1446
1487
  import { mkdir as mkdir6 } from "fs/promises";
1447
1488
 
1448
1489
  // src/core/session.ts
1449
1490
  import { randomBytes } from "crypto";
1450
- import { readFile as readFile4, readdir, rename as rename3, stat as stat2, unlink, writeFile as writeFile4 } from "fs/promises";
1491
+ import { readFile as readFile4, readdir, rename as rename3, rm as rm4, stat as stat2, unlink, writeFile as writeFile4 } from "fs/promises";
1451
1492
  import path5 from "path";
1452
1493
  var MAX_SESSION_BYTES = 5e7;
1453
1494
  function createSession(cwd, model) {
@@ -1548,8 +1589,13 @@ async function deleteSession(query) {
1548
1589
  if (error.code === "ENOENT") throw new Error(`Session no longer exists: ${id}`);
1549
1590
  throw error;
1550
1591
  });
1592
+ await removeCheckpoints(id);
1551
1593
  return id;
1552
1594
  }
1595
+ async function removeCheckpoints(id) {
1596
+ assertSessionId(id);
1597
+ await rm4(path5.join(checkpointsDir, id), { recursive: true, force: true }).catch(() => void 0);
1598
+ }
1553
1599
  async function deleteAllSessions() {
1554
1600
  let names;
1555
1601
  try {
@@ -1577,6 +1623,7 @@ async function deleteAllSessions() {
1577
1623
  const id = ids[index];
1578
1624
  if (result.status === "fulfilled" || result.reason?.code === "ENOENT") {
1579
1625
  deleted.push(id);
1626
+ await removeCheckpoints(id);
1580
1627
  } else {
1581
1628
  failed.push({
1582
1629
  id,
@@ -1944,7 +1991,7 @@ async function runTurn(cfg, history, hooks, signal) {
1944
1991
  });
1945
1992
  }
1946
1993
  hooks.onEvent({ type: "turn_end", stopReason: outcome.stopReason });
1947
- return messages;
1994
+ return sanitizeHistory(messages);
1948
1995
  }
1949
1996
  }
1950
1997
  async function consumeStream(cfg, messages, hooks, signal, maxTokens) {
@@ -2100,18 +2147,18 @@ function sleep(ms, signal) {
2100
2147
  signal.addEventListener("abort", onAbort, { once: true });
2101
2148
  });
2102
2149
  }
2150
+ var CHARS_PER_TOKEN = 4;
2103
2151
  function estimateRequestTokens(cfg, messages) {
2104
2152
  let characters3 = cfg.system.length;
2105
2153
  try {
2106
2154
  characters3 += JSON.stringify(
2107
2155
  messages,
2108
- (_key, value) => typeof value === "object" && value !== null && value.type === "image" ? `[image:${"#".repeat(2e3)}]` : value
2156
+ (_key, value) => typeof value === "object" && value !== null && value.type === "image" ? `[image:${"#".repeat(6e3)}]` : value
2109
2157
  ).length;
2110
2158
  characters3 += JSON.stringify(cfg.tools.schemas()).length;
2111
2159
  } catch {
2112
- return Math.max(1, characters3);
2113
2160
  }
2114
- return Math.max(1, characters3);
2161
+ return Math.max(1, Math.ceil(characters3 / CHARS_PER_TOKEN));
2115
2162
  }
2116
2163
  async function runToolCalls(cfg, content, hooks, signal) {
2117
2164
  const calls = content.filter((block) => block.type === "tool_use");
@@ -2144,6 +2191,13 @@ async function runOneCall(cfg, call, hooks, signal) {
2144
2191
  if (!tool) {
2145
2192
  return toolError(call.id, `Unknown tool "${call.name}". Use only the tools provided.`);
2146
2193
  }
2194
+ const invalid = invalidArguments(call.input);
2195
+ if (invalid !== void 0) {
2196
+ return toolError(
2197
+ call.id,
2198
+ `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}`
2199
+ );
2200
+ }
2147
2201
  const summary2 = describeCall(tool, call.input);
2148
2202
  const ctx = {
2149
2203
  cwd: cfg.cwd,
@@ -2241,6 +2295,11 @@ function describeRefusal(refusal) {
2241
2295
  if (refusal?.explanation) parts.push(refusal.explanation);
2242
2296
  return parts.join(" ");
2243
2297
  }
2298
+ function invalidArguments(input) {
2299
+ if (typeof input !== "object" || input === null) return void 0;
2300
+ const raw = input[INVALID_ARGUMENTS_KEY];
2301
+ return typeof raw === "string" ? raw : void 0;
2302
+ }
2244
2303
  function toolError(toolUseId, content) {
2245
2304
  return { type: "tool_result", toolUseId, content, isError: true };
2246
2305
  }
@@ -2688,11 +2747,13 @@ function assertSessionId2(id) {
2688
2747
  }
2689
2748
 
2690
2749
  // src/core/diagnostics.ts
2750
+ import { createHash as createHash3 } from "crypto";
2691
2751
  import { readFile as readFile6, stat as stat4 } from "fs/promises";
2692
2752
  import path7 from "path";
2693
2753
 
2694
2754
  // src/tools/bash.ts
2695
2755
  import { spawn as spawn2 } from "child_process";
2756
+ import { StringDecoder as StringDecoder2 } from "string_decoder";
2696
2757
  var DEFAULT_TIMEOUT = 12e4;
2697
2758
  var MAX_TIMEOUT = 6e5;
2698
2759
  var MAX_OUTPUT = 1e5;
@@ -2776,14 +2837,12 @@ var bashTool = {
2776
2837
  };
2777
2838
  let captured = 0;
2778
2839
  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
- }
2840
+ const collector = () => {
2841
+ const decoder = new StringDecoder2("utf8");
2842
+ return (buf) => collect(decoder.write(buf));
2843
+ };
2844
+ const collect = (text) => {
2845
+ if (truncated || text === "") return;
2787
2846
  progress(text);
2788
2847
  if (text.length > MAX_OUTPUT - captured) {
2789
2848
  chunks.push(text.slice(0, MAX_OUTPUT - captured));
@@ -2793,8 +2852,8 @@ var bashTool = {
2793
2852
  chunks.push(text);
2794
2853
  captured += text.length;
2795
2854
  };
2796
- child.stdout.on("data", collect);
2797
- child.stderr.on("data", collect);
2855
+ child.stdout.on("data", collector());
2856
+ child.stderr.on("data", collector());
2798
2857
  const killTree2 = () => {
2799
2858
  if (child.pid == null) return;
2800
2859
  if (process.platform === "win32") {
@@ -2857,19 +2916,19 @@ var diagnosticsTool = {
2857
2916
  summarize: () => "automatic project checks",
2858
2917
  execute: async () => ({ content: "" })
2859
2918
  };
2860
- async function detectDiagnosticCommands(cwd, configured = []) {
2919
+ async function detectChecks(cwd, configured = []) {
2861
2920
  const custom = configured.map((command) => command.trim()).filter(Boolean);
2862
- if (custom.length > 0) return custom.slice(0, 8);
2863
- const commands = [];
2921
+ if (custom.length > 0) return custom.slice(0, 8).map((command) => ({ command }));
2922
+ const checks = [];
2864
2923
  const packageJson = await readPackageJson(path7.join(cwd, "package.json"));
2865
2924
  if (packageJson) {
2866
2925
  const manager = await packageManager(cwd, packageJson.packageManager);
2867
2926
  for (const name of ["lint", "typecheck", "check", "test"]) {
2868
- if (typeof packageJson.scripts?.[name] === "string") {
2869
- commands.push(scriptCommand(manager, name));
2870
- }
2927
+ const body = packageJson.scripts?.[name];
2928
+ if (typeof body === "string") checks.push({ command: scriptCommand(manager, name), runs: body });
2871
2929
  }
2872
2930
  }
2931
+ const commands = [];
2873
2932
  if (await isFile2(path7.join(cwd, "Cargo.toml"))) {
2874
2933
  commands.push("cargo check", "cargo test");
2875
2934
  }
@@ -2877,10 +2936,13 @@ async function detectDiagnosticCommands(cwd, configured = []) {
2877
2936
  if (await isFile2(path7.join(cwd, "pyproject.toml")) || await isFile2(path7.join(cwd, "pytest.ini")) || await isFile2(path7.join(cwd, "tox.ini"))) {
2878
2937
  commands.push("python -m pytest");
2879
2938
  }
2880
- return [...new Set(commands)].slice(0, 8);
2939
+ for (const command of commands) checks.push({ command });
2940
+ const seen = /* @__PURE__ */ new Set();
2941
+ return checks.filter((check) => !seen.has(check.command) && seen.add(check.command)).slice(0, 8);
2881
2942
  }
2882
2943
  async function runAutomaticDiagnostics(options) {
2883
- const commands = await detectDiagnosticCommands(options.cwd, options.configuredCommands);
2944
+ const checks = await detectChecks(options.cwd, options.configuredCommands);
2945
+ const commands = checks.map((check) => check.command);
2884
2946
  const summary2 = { commands, failed: [], skipped: false };
2885
2947
  if (options.changedFiles.length === 0 || commands.length === 0 || options.signal.aborted) {
2886
2948
  summary2.skipped = true;
@@ -2896,7 +2958,10 @@ async function runAutomaticDiagnostics(options) {
2896
2958
  });
2897
2959
  return summary2;
2898
2960
  }
2899
- if (gate === "ask") {
2961
+ const items = checks.map((check) => check.runs ? `${check.command} \u2192 ${check.runs}` : check.command);
2962
+ const grantName = `${diagnosticsTool.name}#${createHash3("sha256").update(items.join("\n")).digest("hex").slice(0, 16)}`;
2963
+ const granted = gate === "ask" && options.permissions.decide({ ...diagnosticsTool, name: grantName }) === "allow";
2964
+ if (gate === "ask" && !granted) {
2900
2965
  const decision = await options.hooks.requestPermission({
2901
2966
  toolName: diagnosticsTool.name,
2902
2967
  summary: `run ${commands.length} automatic project ${commands.length === 1 ? "check" : "checks"}`,
@@ -2904,7 +2969,7 @@ async function runAutomaticDiagnostics(options) {
2904
2969
  display: {
2905
2970
  kind: "list",
2906
2971
  title: `After changes to ${options.changedFiles.length} ${options.changedFiles.length === 1 ? "file" : "files"}`,
2907
- items: commands
2972
+ items
2908
2973
  },
2909
2974
  allowAlways: true
2910
2975
  });
@@ -2921,7 +2986,7 @@ async function runAutomaticDiagnostics(options) {
2921
2986
  });
2922
2987
  return summary2;
2923
2988
  }
2924
- if (decision === "always") options.permissions.grantForSession(diagnosticsTool.name);
2989
+ if (decision === "always") options.permissions.grantForSession(grantName);
2925
2990
  }
2926
2991
  const ctx = {
2927
2992
  cwd: options.cwd,
@@ -2998,10 +3063,51 @@ async function isFile2(file) {
2998
3063
 
2999
3064
  // src/core/attachments.ts
3000
3065
  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";
3066
+ import { lstat as lstat2, mkdtemp, readdir as readdir3, readFile as readFile7, rm as rm5, stat as stat5, writeFile as writeFile6 } from "fs/promises";
3002
3067
  import os2 from "os";
3003
- import path8 from "path";
3068
+ import path9 from "path";
3004
3069
  import { fileURLToPath } from "url";
3070
+
3071
+ // src/tools/sensitive.ts
3072
+ import path8 from "path";
3073
+ var sensitiveNames = /* @__PURE__ */ new Set([
3074
+ ".npmrc",
3075
+ ".pypirc",
3076
+ ".git-credentials",
3077
+ ".netrc",
3078
+ ".pgpass",
3079
+ ".my.cnf",
3080
+ ".docker/config.json",
3081
+ "auth.json",
3082
+ "credentials",
3083
+ "credentials.json",
3084
+ "id_rsa",
3085
+ "id_ed25519",
3086
+ "id_dsa",
3087
+ "id_ecdsa",
3088
+ "kubeconfig",
3089
+ "known_hosts",
3090
+ "secret.json",
3091
+ "secrets.json",
3092
+ "tokens.json",
3093
+ "service-account.json"
3094
+ ]);
3095
+ var sensitiveExtensions = /* @__PURE__ */ new Set([".pem", ".key", ".p12", ".pfx", ".jks", ".keystore", ".p8", ".der"]);
3096
+ function isSensitivePath(value) {
3097
+ if (typeof value !== "string") return false;
3098
+ const normalized = value.replaceAll("\\", "/").toLowerCase();
3099
+ const name = path8.posix.basename(normalized);
3100
+ if (name === ".env" || name.startsWith(".env.")) return true;
3101
+ if (sensitiveNames.has(name) || sensitiveExtensions.has(path8.posix.extname(name))) return true;
3102
+ return normalized.split("/").some((part) => part === ".ssh" || part === ".aws");
3103
+ }
3104
+ function mentionsSensitivePattern(value) {
3105
+ if (typeof value !== "string") return false;
3106
+ const lower = value.toLowerCase();
3107
+ 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);
3108
+ }
3109
+
3110
+ // src/core/attachments.ts
3005
3111
  var MAX_IMAGE_BYTES = 10 * 1024 * 1024;
3006
3112
  var MAX_TEXT_BYTES = 256 * 1024;
3007
3113
  var MAX_CLIPBOARD_OUTPUT_BYTES = MAX_IMAGE_BYTES + 64 * 1024;
@@ -3031,7 +3137,7 @@ async function loadAttachment(cwd, requestedPath) {
3031
3137
  const resolved = resolveAttachmentPath(cwd, requestedPath);
3032
3138
  const linkInfo = await lstat2(resolved).catch(() => null);
3033
3139
  if (linkInfo?.isSymbolicLink()) {
3034
- throw new Error(`Cannot attach symbolic links: ${path8.basename(resolved)}`);
3140
+ throw new Error(`Cannot attach symbolic links: ${path9.basename(resolved)}`);
3035
3141
  }
3036
3142
  const info = await stat5(resolved).catch((error) => {
3037
3143
  if (error.code === "ENOENT") throw new Error(`Attachment not found: ${resolved}`);
@@ -3040,7 +3146,7 @@ async function loadAttachment(cwd, requestedPath) {
3040
3146
  if (info.isDirectory()) {
3041
3147
  const attachment = await loadFirstImageFromDirectory(resolved);
3042
3148
  if (attachment) return attachment;
3043
- throw new Error(`No supported images found in directory: ${path8.basename(resolved)}`);
3149
+ throw new Error(`No supported images found in directory: ${path9.basename(resolved)}`);
3044
3150
  }
3045
3151
  if (!info.isFile()) throw new Error(`Attachment is not a regular file: ${resolved}`);
3046
3152
  return loadResolvedAttachment(resolved, info.size);
@@ -3050,7 +3156,7 @@ async function loadAutomaticAttachment(cwd, requestedPath) {
3050
3156
  const resolved = resolveAttachmentPath(cwd, requestedPath);
3051
3157
  if (isSensitiveAutomaticPath(resolved)) {
3052
3158
  throw new Error(
3053
- `For safety, sensitive-looking files must be attached explicitly with /attach: ${path8.basename(resolved)}`
3159
+ `For safety, sensitive-looking files must be attached explicitly with /attach: ${path9.basename(resolved)}`
3054
3160
  );
3055
3161
  }
3056
3162
  const linkInfo = await lstat2(resolved).catch(() => null);
@@ -3063,6 +3169,33 @@ async function loadAutomaticAttachment(cwd, requestedPath) {
3063
3169
  if (!info?.isFile()) return null;
3064
3170
  return loadResolvedAttachment(resolved, info.size);
3065
3171
  }
3172
+ async function loadMentionAttachment(cwd, requestedPath) {
3173
+ const safe = resolveInside(cwd, normalizeInputPath(requestedPath.trim()));
3174
+ if (!safe.ok || safe.relative === "") return null;
3175
+ if (isSensitivePath(safe.relative) || isSensitiveAutomaticPath(safe.path)) {
3176
+ throw new Error(
3177
+ `For safety, sensitive-looking files must be attached explicitly with /attach: ${path9.basename(safe.path)}`
3178
+ );
3179
+ }
3180
+ const info = await stat5(safe.path).catch(() => null);
3181
+ if (!info?.isFile()) return null;
3182
+ return loadResolvedAttachment(safe.path, info.size);
3183
+ }
3184
+ function withoutImages(messages) {
3185
+ let removed = 0;
3186
+ const cleaned = messages.map((message) => {
3187
+ if (!message.content.some((block) => block.type === "image")) return message;
3188
+ return {
3189
+ ...message,
3190
+ content: message.content.map((block) => {
3191
+ if (block.type !== "image") return block;
3192
+ removed += 1;
3193
+ return { type: "text", text: `[image ${block.name} removed: the model could not accept it]` };
3194
+ })
3195
+ };
3196
+ });
3197
+ return { messages: cleaned, removed };
3198
+ }
3066
3199
  var MAX_PATH_CHARS = 2048;
3067
3200
  function looksLikeAttachmentPath(value) {
3068
3201
  const trimmed = value.trim();
@@ -3070,14 +3203,14 @@ function looksLikeAttachmentPath(value) {
3070
3203
  if (trimmed.length > MAX_PATH_CHARS) return false;
3071
3204
  const candidate = normalizeInputPath(trimmed);
3072
3205
  if (/^file:\/\//i.test(candidate)) return true;
3073
- if (path8.isAbsolute(candidate)) return true;
3206
+ if (path9.isAbsolute(candidate)) return true;
3074
3207
  if (/^(?:~|\.{1,2})[\\/]/.test(candidate)) return true;
3075
3208
  if (candidate.includes("/") || candidate.includes("\\")) {
3076
3209
  if (trimmed.length > 200) return false;
3077
3210
  return true;
3078
3211
  }
3079
- const basename3 = path8.basename(candidate).toLowerCase();
3080
- return path8.extname(basename3) !== "" || AUTO_PATH_NAMES.has(basename3);
3212
+ const basename3 = path9.basename(candidate).toLowerCase();
3213
+ return path9.extname(basename3) !== "" || AUTO_PATH_NAMES.has(basename3);
3081
3214
  }
3082
3215
  async function loadClipboardImage(platform = process.platform, runner = runClipboardCommand) {
3083
3216
  const commands = clipboardCommands(platform);
@@ -3110,7 +3243,7 @@ async function loadClipboardImage(platform = process.platform, runner = runClipb
3110
3243
  throw new Error(`No supported image found in the clipboard. ${hint}`);
3111
3244
  }
3112
3245
  async function loadResolvedAttachment(resolved, size) {
3113
- const name = safeName(path8.basename(resolved));
3246
+ const name = safeName(path9.basename(resolved));
3114
3247
  const imageLimitCandidate = size <= MAX_IMAGE_BYTES;
3115
3248
  if (!imageLimitCandidate) {
3116
3249
  throw new Error(`Attachment is larger than ${formatBytes(MAX_IMAGE_BYTES)}: ${name}`);
@@ -3128,12 +3261,11 @@ async function loadResolvedAttachment(resolved, size) {
3128
3261
  if (data.length > MAX_TEXT_BYTES) {
3129
3262
  throw new Error(`Text attachment is larger than ${formatBytes(MAX_TEXT_BYTES)}: ${name}`);
3130
3263
  }
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}`);
3264
+ const text = decodeText(data);
3265
+ if (text === null) {
3266
+ throw new Error(
3267
+ data.includes(0) ? `Unsupported binary attachment: ${name}` : `Attachment is neither a supported image nor UTF-8 text: ${name}`
3268
+ );
3137
3269
  }
3138
3270
  return {
3139
3271
  path: resolved,
@@ -3142,13 +3274,13 @@ async function loadResolvedAttachment(resolved, size) {
3142
3274
  };
3143
3275
  }
3144
3276
  async function loadFirstImageFromDirectory(dirPath) {
3145
- const IMAGE_EXTENSIONS = /* @__PURE__ */ new Set([".png", ".jpg", ".jpeg", ".gif", ".webp", ".bmp"]);
3277
+ const IMAGE_EXTENSIONS = /* @__PURE__ */ new Set([".png", ".jpg", ".jpeg", ".gif", ".webp"]);
3146
3278
  const entries = await readdir3(dirPath, { withFileTypes: true }).catch(() => null);
3147
3279
  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));
3280
+ const sorted = entries.filter((entry) => entry.isFile() && IMAGE_EXTENSIONS.has(path9.extname(entry.name).toLowerCase())).sort((a, b) => a.name.localeCompare(b.name));
3149
3281
  if (sorted.length === 0) return null;
3150
3282
  const first2 = sorted[0];
3151
- const filePath = path8.join(dirPath, first2.name);
3283
+ const filePath = path9.join(dirPath, first2.name);
3152
3284
  const info = await stat5(filePath).catch(() => null);
3153
3285
  if (!info?.isFile()) return null;
3154
3286
  return loadResolvedAttachment(filePath, info.size);
@@ -3158,23 +3290,78 @@ function resolveAttachmentPath(cwd, requestedPath) {
3158
3290
  if (!cleaned) throw new Error("Give a file path: /attach <path>");
3159
3291
  if (/^file:\/\//i.test(cleaned)) {
3160
3292
  try {
3161
- return path8.normalize(fileURLToPath(cleaned));
3293
+ return path9.normalize(fileURLToPath(cleaned));
3162
3294
  } catch {
3163
3295
  throw new Error(`Invalid local file URL: ${cleaned}`);
3164
3296
  }
3165
3297
  }
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);
3298
+ const expanded = cleaned === "~" ? os2.homedir() : cleaned.startsWith("~/") ? path9.join(os2.homedir(), cleaned.slice(2)) : cleaned;
3299
+ return path9.isAbsolute(expanded) ? path9.normalize(expanded) : path9.resolve(cwd, expanded);
3168
3300
  }
3169
3301
  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));
3302
+ const basename3 = path9.basename(file).toLowerCase();
3303
+ return (
3304
+ // Also covers ~/.ssh/config, ~/.aws/credentials, .git-credentials and the like.
3305
+ isSensitivePath(file) || basename3.startsWith(".env.") || SENSITIVE_AUTO_NAMES.has(basename3) || SENSITIVE_AUTO_EXTENSIONS.has(path9.extname(basename3))
3306
+ );
3172
3307
  }
3173
3308
  function attachmentLabel(block) {
3174
3309
  if (block.type === "image") return `image: ${block.name}`;
3175
3310
  if (block.type === "file") return `file: ${block.name}`;
3176
3311
  return null;
3177
3312
  }
3313
+ function decodeText(data) {
3314
+ try {
3315
+ if (data[0] === 255 && data[1] === 254) return new TextDecoder("utf-16le", { fatal: true }).decode(data);
3316
+ if (data[0] === 254 && data[1] === 255) return new TextDecoder("utf-16be", { fatal: true }).decode(data);
3317
+ if (data.includes(0)) return null;
3318
+ return new TextDecoder("utf-8", { fatal: true }).decode(data);
3319
+ } catch {
3320
+ return null;
3321
+ }
3322
+ }
3323
+ function imageSize(data, mediaType) {
3324
+ try {
3325
+ if (mediaType === "image/png" && data.length >= 24) {
3326
+ return { width: data.readUInt32BE(16), height: data.readUInt32BE(20) };
3327
+ }
3328
+ if (mediaType === "image/gif" && data.length >= 10) {
3329
+ return { width: data.readUInt16LE(6), height: data.readUInt16LE(8) };
3330
+ }
3331
+ if (mediaType === "image/webp" && data.length >= 30) {
3332
+ const chunk = data.subarray(12, 16).toString("ascii");
3333
+ if (chunk === "VP8X") return { width: data.readUIntLE(24, 3) + 1, height: data.readUIntLE(27, 3) + 1 };
3334
+ if (chunk === "VP8L") {
3335
+ const bits = data.readUInt32LE(21);
3336
+ return { width: (bits & 16383) + 1, height: (bits >> 14 & 16383) + 1 };
3337
+ }
3338
+ if (chunk === "VP8 ") return { width: data.readUInt16LE(26) & 16383, height: data.readUInt16LE(28) & 16383 };
3339
+ return null;
3340
+ }
3341
+ if (mediaType === "image/jpeg") {
3342
+ let offset = 2;
3343
+ while (offset + 9 < data.length) {
3344
+ if (data[offset] !== 255) return null;
3345
+ const marker = data[offset + 1];
3346
+ if (marker === 255) {
3347
+ offset += 1;
3348
+ continue;
3349
+ }
3350
+ if (marker >= 192 && marker <= 207 && marker !== 196 && marker !== 200 && marker !== 204) {
3351
+ return { width: data.readUInt16BE(offset + 7), height: data.readUInt16BE(offset + 5) };
3352
+ }
3353
+ if (marker === 216 || marker === 1 || marker >= 208 && marker <= 215) {
3354
+ offset += 2;
3355
+ continue;
3356
+ }
3357
+ offset += 2 + data.readUInt16BE(offset + 2);
3358
+ }
3359
+ }
3360
+ } catch {
3361
+ return null;
3362
+ }
3363
+ return null;
3364
+ }
3178
3365
  function detectImage(data) {
3179
3366
  if (data.length >= 8 && data.subarray(0, 8).equals(Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]))) {
3180
3367
  return "image/png";
@@ -3301,7 +3488,15 @@ var WINDOWS_CLIPBOARD_SCRIPT = String.raw`
3301
3488
  Add-Type -AssemblyName System.Windows.Forms
3302
3489
  Add-Type -AssemblyName System.Drawing
3303
3490
  $image = [System.Windows.Forms.Clipboard]::GetImage()
3304
- if ($null -eq $image) { exit 2 }
3491
+ if ($null -eq $image) {
3492
+ # An image file copied in Explorer arrives as a file list, not a bitmap.
3493
+ $files = [System.Windows.Forms.Clipboard]::GetFileDropList()
3494
+ $file = $files | Where-Object { $_ -match '\.(png|jpe?g|gif|webp)$' } | Select-Object -First 1
3495
+ if ($null -eq $file) { exit 2 }
3496
+ $bytes = [System.IO.File]::ReadAllBytes($file)
3497
+ [Console]::OpenStandardOutput().Write($bytes, 0, $bytes.Length)
3498
+ exit 0
3499
+ }
3305
3500
  $stream = New-Object System.IO.MemoryStream
3306
3501
  try {
3307
3502
  $image.Save($stream, [System.Drawing.Imaging.ImageFormat]::Png)
@@ -3313,13 +3508,16 @@ try {
3313
3508
  }
3314
3509
  `;
3315
3510
  var MAX_SENT_IMAGE_BYTES = 37e5;
3511
+ var MAX_IMAGE_EDGE = 8e3;
3316
3512
  var SHRINK_LONG_EDGE = 2e3;
3317
3513
  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-"));
3514
+ const size = imageSize(data, mediaType);
3515
+ const tooTall = size !== null && Math.max(size.width, size.height) > MAX_IMAGE_EDGE;
3516
+ if (data.length <= MAX_SENT_IMAGE_BYTES && !tooTall) return { data, mediaType };
3517
+ const dir = await mkdtemp(path9.join(os2.tmpdir(), "kitcode-image-"));
3320
3518
  try {
3321
- const input = path8.join(dir, `in${extensionFor(mediaType)}`);
3322
- const output = path8.join(dir, "out.jpg");
3519
+ const input = path9.join(dir, `in${extensionFor(mediaType)}`);
3520
+ const output = path9.join(dir, "out.jpg");
3323
3521
  await writeFile6(input, data);
3324
3522
  try {
3325
3523
  await shrink(input, output, platform);
@@ -3334,7 +3532,7 @@ async function fitImage(data, mediaType, platform = process.platform, shrink = s
3334
3532
  }
3335
3533
  return { data: shrunk, mediaType: "image/jpeg" };
3336
3534
  } finally {
3337
- await rm4(dir, { recursive: true, force: true }).catch(() => void 0);
3535
+ await rm5(dir, { recursive: true, force: true }).catch(() => void 0);
3338
3536
  }
3339
3537
  }
3340
3538
  async function shrinkWithSystemTools(input, output, platform) {
@@ -3350,7 +3548,7 @@ async function shrinkWithSystemTools(input, output, platform) {
3350
3548
  await runQuiet("sips", ["-Z", String(SHRINK_LONG_EDGE), "-s", "format", "jpeg", "-s", "formatOptions", "85", input, "--out", output]);
3351
3549
  return;
3352
3550
  }
3353
- const args = [input, "-resize", `${SHRINK_LONG_EDGE}x${SHRINK_LONG_EDGE}>`, "-background", "white", "-flatten", "-quality", "85", output];
3551
+ const args = [input, "-auto-orient", "-resize", `${SHRINK_LONG_EDGE}x${SHRINK_LONG_EDGE}>`, "-background", "white", "-flatten", "-quality", "85", output];
3354
3552
  try {
3355
3553
  await runQuiet("magick", args);
3356
3554
  } catch {
@@ -3372,6 +3570,12 @@ var WINDOWS_SHRINK_SCRIPT = String.raw`
3372
3570
  Add-Type -AssemblyName System.Drawing
3373
3571
  $source = [System.Drawing.Image]::FromFile($env:KITCODE_IMAGE_IN)
3374
3572
  try {
3573
+ # Apply the EXIF orientation: the re-encoded JPEG drops the flag that kept a
3574
+ # phone photo upright.
3575
+ if ($source.PropertyIdList -contains 0x0112) {
3576
+ $flip = @{ 2 = 'RotateNoneFlipX'; 3 = 'Rotate180FlipNone'; 4 = 'Rotate180FlipX'; 5 = 'Rotate90FlipX'; 6 = 'Rotate90FlipNone'; 7 = 'Rotate270FlipX'; 8 = 'Rotate270FlipNone' }[[int]$source.GetPropertyItem(0x0112).Value[0]]
3577
+ if ($flip) { $source.RotateFlip([System.Drawing.RotateFlipType]$flip) }
3578
+ }
3375
3579
  $edge = [double]$env:KITCODE_IMAGE_EDGE
3376
3580
  $scale = [Math]::Min(1.0, $edge / [Math]::Max($source.Width, $source.Height))
3377
3581
  $width = [Math]::Max(1, [int]($source.Width * $scale))
@@ -3392,7 +3596,7 @@ try {
3392
3596
  }
3393
3597
  `;
3394
3598
  function textMime(file) {
3395
- const extension = path8.extname(file).toLowerCase();
3599
+ const extension = path9.extname(file).toLowerCase();
3396
3600
  const known = {
3397
3601
  ".json": "application/json",
3398
3602
  ".md": "text/markdown",
@@ -3595,6 +3799,7 @@ function withoutThinkingReplay(history) {
3595
3799
  });
3596
3800
  return changed ? out : history;
3597
3801
  }
3802
+ var CHARS_PER_TOKEN2 = 4;
3598
3803
  function estimateHistoryTokens(history) {
3599
3804
  let chars = 0;
3600
3805
  for (const message of history) {
@@ -3606,7 +3811,7 @@ function estimateHistoryTokens(history) {
3606
3811
  else if (block.type === "image") chars += 6e3;
3607
3812
  }
3608
3813
  }
3609
- return Math.round(chars / 4);
3814
+ return Math.round(chars / CHARS_PER_TOKEN2);
3610
3815
  }
3611
3816
  function shouldAutoCompact(used, window) {
3612
3817
  return window !== null && window > 0 && used / window >= 0.8;
@@ -3626,7 +3831,9 @@ function estimateCompactBudget(history, maxTokens = MERGE_MAX_TOKENS) {
3626
3831
  const rendered = renderedMessages.join("\n\n");
3627
3832
  if (rendered.length <= SINGLE_REQUEST_MAX_CHARS) {
3628
3833
  return {
3629
- inputTokens: Math.max(1, rendered.length + 1e3),
3834
+ // Tokens, not characters: counting characters made a turn budget refuse
3835
+ // compaction (four times too expensive) just when the window was full.
3836
+ inputTokens: Math.max(1, Math.ceil(rendered.length / CHARS_PER_TOKEN2) + 1e3),
3630
3837
  maxOutputTokens: Math.max(1, Math.min(SINGLE_MAX_TOKENS, Math.floor(maxTokens)))
3631
3838
  };
3632
3839
  }
@@ -3644,7 +3851,7 @@ function estimateCompactBudget(history, maxTokens = MERGE_MAX_TOKENS) {
3644
3851
  const intermediateOutput = chunks.length * chunkOutput + Math.max(0, mergeRequests - 1) * mergeOutput;
3645
3852
  const requestCount = chunks.length + mergeRequests;
3646
3853
  return {
3647
- inputTokens: Math.max(1, chunkInput + intermediateOutput * 8 + requestCount * 1e3),
3854
+ inputTokens: Math.max(1, Math.ceil(chunkInput / CHARS_PER_TOKEN2) + intermediateOutput * 2 + requestCount * 1e3),
3648
3855
  maxOutputTokens: totalOutput
3649
3856
  };
3650
3857
  }
@@ -4120,11 +4327,14 @@ import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"
4120
4327
  import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
4121
4328
 
4122
4329
  // src/mcp/bridge.ts
4330
+ import { createHash as createHash4 } from "crypto";
4123
4331
  var MAX_RESULT_CHARS = 2e5;
4332
+ var TOOL_NAME = /^[a-zA-Z0-9_-]{1,64}$/;
4333
+ var MAX_TOOL_NAME = 64;
4124
4334
  function bridgeMcpTool(server, descriptor, call) {
4125
4335
  const tool = descriptor.name;
4126
4336
  return {
4127
- name: `mcp__${server}__${tool}`,
4337
+ name: mcpToolName(server, tool),
4128
4338
  description: descriptor.description ?? descriptor.title ?? tool,
4129
4339
  inputSchema: inputSchemaOf(descriptor),
4130
4340
  defaultPermission: "ask",
@@ -4133,6 +4343,13 @@ function bridgeMcpTool(server, descriptor, call) {
4133
4343
  execute: (input, ctx) => invoke(call, tool, input, ctx.signal)
4134
4344
  };
4135
4345
  }
4346
+ function mcpToolName(server, tool) {
4347
+ const raw = `mcp__${server}__${tool}`;
4348
+ if (TOOL_NAME.test(raw)) return raw;
4349
+ const hash = createHash4("sha256").update(raw).digest("hex").slice(0, 8);
4350
+ const cleaned = raw.replace(/[^a-zA-Z0-9_-]/g, "_");
4351
+ return `${cleaned.slice(0, MAX_TOOL_NAME - hash.length - 1)}_${hash}`;
4352
+ }
4136
4353
  function printableInput(input) {
4137
4354
  try {
4138
4355
  return JSON.stringify(input, null, 2);
@@ -4169,10 +4386,15 @@ function renderPart(part) {
4169
4386
  }
4170
4387
  function inputSchemaOf(descriptor) {
4171
4388
  const schema = descriptor.inputSchema;
4172
- if (schema === null || typeof schema !== "object") {
4389
+ if (schema === null || typeof schema !== "object" || Array.isArray(schema)) {
4173
4390
  return { type: "object", properties: {}, additionalProperties: false };
4174
4391
  }
4175
- return schema;
4392
+ const record3 = schema;
4393
+ return {
4394
+ ...record3,
4395
+ type: "object",
4396
+ properties: record3.properties && typeof record3.properties === "object" ? record3.properties : {}
4397
+ };
4176
4398
  }
4177
4399
  function argumentPreview(input) {
4178
4400
  if (input === null || typeof input !== "object") return "";
@@ -4191,7 +4413,7 @@ function clip(value) {
4191
4413
  // package.json
4192
4414
  var package_default = {
4193
4415
  name: "@kernelonpanic/kitcode",
4194
- version: "1.4.1",
4416
+ version: "1.4.3",
4195
4417
  description: "Terminal coding agent with a config you never have to write by hand",
4196
4418
  type: "module",
4197
4419
  license: "MIT",
@@ -4224,7 +4446,6 @@ var package_default = {
4224
4446
  dependencies: {
4225
4447
  "@anthropic-ai/sdk": "^0.115.0",
4226
4448
  "@modelcontextprotocol/sdk": "^1.30.0",
4227
- build: "^0.1.4",
4228
4449
  chalk: "^6.0.0",
4229
4450
  commander: "^15.0.0",
4230
4451
  "fast-glob": "^3.3.3",
@@ -4260,7 +4481,22 @@ var package_default = {
4260
4481
 
4261
4482
  // src/version.ts
4262
4483
  var KITCODE_VERSION = package_default.version;
4263
- var KITCODE_COMMIT = true ? "be753d255e7675a850d0ca1ee64f790f68d3fd93" : "development";
4484
+ var KITCODE_COMMIT = true ? "586eb4def64a8ef824bf2fe2f58a9f71e18090f0" : "development";
4485
+ function kitcodeUserAgent(sdkUserAgent2) {
4486
+ return `KitCode/${KITCODE_VERSION} ${sdkUserAgent2}`.trim();
4487
+ }
4488
+ function sdkUserAgent(client) {
4489
+ const read = client?.getUserAgent;
4490
+ try {
4491
+ return typeof read === "function" ? String(read.call(client)) : "";
4492
+ } catch {
4493
+ return "";
4494
+ }
4495
+ }
4496
+ function withKitcodeUserAgent(sdkUserAgent2, configured = {}) {
4497
+ const overridden = Object.keys(configured).some((name) => name.toLowerCase() === "user-agent");
4498
+ return overridden ? { ...configured } : { "User-Agent": kitcodeUserAgent(sdkUserAgent2), ...configured };
4499
+ }
4264
4500
 
4265
4501
  // src/mcp/client.ts
4266
4502
  var clientInfo = { name: "kitcode", version: KITCODE_VERSION };
@@ -4321,6 +4557,7 @@ var blockedEnvKeys = /* @__PURE__ */ new Set([
4321
4557
  function createMcpManager(servers) {
4322
4558
  const serverStates = /* @__PURE__ */ new Map();
4323
4559
  const sessions = /* @__PURE__ */ new Map();
4560
+ const changeListeners = /* @__PURE__ */ new Set();
4324
4561
  for (const [name, config] of Object.entries(servers)) {
4325
4562
  serverStates.set(name, {
4326
4563
  name,
@@ -4342,14 +4579,15 @@ function createMcpManager(servers) {
4342
4579
  serverStates.set(name, { name, status: "connecting", toolCount: 0 });
4343
4580
  const client = new Client(clientInfo);
4344
4581
  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
- }
4582
+ if (sessions.get(name)?.client !== client) return;
4583
+ sessions.delete(name);
4584
+ serverStates.set(name, {
4585
+ name,
4586
+ status: "error",
4587
+ toolCount: 0,
4588
+ error: "connection closed"
4589
+ });
4590
+ for (const listener of changeListeners) listener();
4353
4591
  };
4354
4592
  const transport = createTransport(config);
4355
4593
  const stderrTail = captureStderr(transport);
@@ -4415,7 +4653,12 @@ function createMcpManager(servers) {
4415
4653
  tools() {
4416
4654
  return [...sessions.values()].flatMap((session) => session.tools);
4417
4655
  },
4418
- callTool
4656
+ callTool,
4657
+ /** Called when a server drops on its own, outside connect/disconnect. */
4658
+ onChange(listener) {
4659
+ changeListeners.add(listener);
4660
+ return () => changeListeners.delete(listener);
4661
+ }
4419
4662
  };
4420
4663
  }
4421
4664
  async function handshake(client, transport) {
@@ -4551,8 +4794,8 @@ function balanceEndpoints(baseUrl) {
4551
4794
  endpoint(url.origin, "/v1/balance", genericBalance)
4552
4795
  ];
4553
4796
  }
4554
- function endpoint(origin, path15, parse2) {
4555
- return { url: `${origin}${path15}`, parse: parse2 };
4797
+ function endpoint(origin, path16, parse2) {
4798
+ return { url: `${origin}${path16}`, parse: parse2 };
4556
4799
  }
4557
4800
  function deepSeekBalance(body) {
4558
4801
  const infos = record2(body)?.["balance_infos"];
@@ -4646,7 +4889,7 @@ async function limitedResponseText(response, limit) {
4646
4889
 
4647
4890
  // src/providers/catalog.ts
4648
4891
  import { chmod as chmod4, readFile as readFile8, writeFile as writeFile7 } from "fs/promises";
4649
- import path9 from "path";
4892
+ import path10 from "path";
4650
4893
  var MAX_AGE_MS = 24 * 60 * 60 * 1e3;
4651
4894
  var CACHE_VERSION = 3;
4652
4895
  async function loadModels(provider, refresh = false) {
@@ -4671,7 +4914,7 @@ function refreshModels(provider) {
4671
4914
  return loadModels(provider, true);
4672
4915
  }
4673
4916
  function cacheFile(providerId) {
4674
- return path9.join(cacheDir, "models", `${providerId.replace(/[^\w.-]/g, "_")}.json`);
4917
+ return path10.join(cacheDir, "models", `${providerId.replace(/[^\w.-]/g, "_")}.json`);
4675
4918
  }
4676
4919
  async function readCache(file) {
4677
4920
  try {
@@ -4706,7 +4949,7 @@ function validModel(value) {
4706
4949
  async function writeCache(file, models) {
4707
4950
  const payload = { version: CACHE_VERSION, fetchedAt: Date.now(), models };
4708
4951
  try {
4709
- await ensureDir(path9.dirname(file));
4952
+ await ensureDir(path10.dirname(file));
4710
4953
  await writeFile7(file, JSON.stringify(payload), { encoding: "utf8", mode: 384 });
4711
4954
  await chmod4(file, 384);
4712
4955
  } catch {
@@ -4832,13 +5075,14 @@ var KNOWN_MODELS = [
4832
5075
  knownModel("claude-fable-5", "Claude Fable 5", 1e6, 128e3)
4833
5076
  ];
4834
5077
  function createAnthropicProvider(args) {
4835
- const client = new Anthropic2({
5078
+ const options = {
4836
5079
  apiKey: args.apiKey,
4837
5080
  baseURL: args.baseUrl,
4838
- defaultHeaders: args.headers,
4839
5081
  maxRetries: MAX_RETRIES,
4840
5082
  timeout: args.timeoutMs ?? REQUEST_TIMEOUT_MS
4841
- });
5083
+ };
5084
+ const userAgent = sdkUserAgent(new Anthropic2(options));
5085
+ const client = new Anthropic2({ ...options, defaultHeaders: withKitcodeUserAgent(userAgent, args.headers) });
4842
5086
  return {
4843
5087
  id: args.id,
4844
5088
  kind: "anthropic",
@@ -4847,7 +5091,35 @@ function createAnthropicProvider(args) {
4847
5091
  knownModels: () => KNOWN_MODELS
4848
5092
  };
4849
5093
  }
5094
+ var thinkingRejected = /* @__PURE__ */ new Set();
4850
5095
  async function* streamTurn(client, providerId, apiKey2, req) {
5096
+ const key = `${providerId}/${req.model}`;
5097
+ let thinking = req.thinking === true && !thinkingRejected.has(key);
5098
+ for (; ; ) {
5099
+ let streamed = false;
5100
+ try {
5101
+ for await (const event of streamOnce(client, providerId, apiKey2, req, thinking)) {
5102
+ if (event.type !== "rate_limits" && event.type !== "usage") streamed = true;
5103
+ yield event;
5104
+ }
5105
+ return;
5106
+ } catch (error) {
5107
+ if (!streamed && thinking && refusesThinking(error)) {
5108
+ thinkingRejected.add(key);
5109
+ thinking = false;
5110
+ continue;
5111
+ }
5112
+ throw error;
5113
+ }
5114
+ }
5115
+ }
5116
+ function refusesThinking(error) {
5117
+ if (!(error instanceof ProviderError)) return false;
5118
+ if (error.status !== void 0 && error.status !== 400 && error.status !== 422) return false;
5119
+ const cause = error.cause instanceof Error ? error.cause.message : "";
5120
+ return /thinking|budget_tokens|adaptive/i.test(`${error.message} ${cause}`);
5121
+ }
5122
+ async function* streamOnce(client, providerId, apiKey2, req, thinkingOn) {
4851
5123
  const params = {
4852
5124
  model: req.model,
4853
5125
  max_tokens: req.maxTokens,
@@ -4855,7 +5127,7 @@ async function* streamTurn(client, providerId, apiKey2, req) {
4855
5127
  messages: toMessageParams(req.messages),
4856
5128
  tools: toToolParams(req.tools)
4857
5129
  };
4858
- if (req.thinking) {
5130
+ if (thinkingOn) {
4859
5131
  const thinking2 = thinkingParam(req.model, req.maxTokens);
4860
5132
  if (thinking2) params.thinking = thinking2;
4861
5133
  }
@@ -5017,7 +5289,7 @@ var THINKING_BUDGET = 16e3;
5017
5289
  var MIN_THINKING_BUDGET = 1024;
5018
5290
  function thinkingParam(model, maxTokens) {
5019
5291
  const id = (model.split("/").at(-1) ?? model).replace(/^(?:[a-z]{2}\.)?anthropic\./, "");
5020
- if (!BUDGET_THINKING.test(id)) return { type: "adaptive", display: "summarized" };
5292
+ if (id.startsWith("claude-") && !BUDGET_THINKING.test(id)) return { type: "adaptive", display: "summarized" };
5021
5293
  const budget = Math.min(THINKING_BUDGET, maxTokens - 1);
5022
5294
  return budget >= MIN_THINKING_BUDGET ? { type: "enabled", budget_tokens: budget } : void 0;
5023
5295
  }
@@ -5037,13 +5309,14 @@ var FINISH_REASONS = {
5037
5309
  content_filter: "refusal"
5038
5310
  };
5039
5311
  function createOpenAiProvider(args) {
5040
- const client = new OpenAI2({
5312
+ const options = {
5041
5313
  baseURL: args.baseUrl,
5042
5314
  apiKey: args.apiKey,
5043
- defaultHeaders: args.headers,
5044
5315
  maxRetries: MAX_RETRIES,
5045
5316
  timeout: args.timeoutMs ?? REQUEST_TIMEOUT_MS
5046
- });
5317
+ };
5318
+ const userAgent = sdkUserAgent(new OpenAI2(options));
5319
+ const client = new OpenAI2({ ...options, defaultHeaders: withKitcodeUserAgent(userAgent, args.headers) });
5047
5320
  return {
5048
5321
  id: args.id,
5049
5322
  kind: "openai",
@@ -5060,7 +5333,7 @@ async function* streamTurn2(client, providerId, apiKey2, req) {
5060
5333
  for (; ; ) {
5061
5334
  let streamed = false;
5062
5335
  try {
5063
- for await (const event of streamOnce(client, providerId, apiKey2, req, effort, thinkingOff, tokenField)) {
5336
+ for await (const event of streamOnce2(client, providerId, apiKey2, req, effort, thinkingOff, tokenField)) {
5064
5337
  if (event.type !== "rate_limits" && event.type !== "usage") streamed = true;
5065
5338
  yield event;
5066
5339
  }
@@ -5073,8 +5346,14 @@ async function* streamTurn2(client, providerId, apiKey2, req) {
5073
5346
  continue;
5074
5347
  }
5075
5348
  if (effort && rejectsField(error, /reasoning|effort/i)) {
5076
- markEffortRejected(providerId, req.model);
5077
- effort = void 0;
5349
+ const lower = effort === "xhigh" || effort === "max" ? "high" : effort === "high" ? "medium" : void 0;
5350
+ if (lower) {
5351
+ markEffortCapped(providerId, req.model, lower);
5352
+ effort = lower;
5353
+ } else {
5354
+ markEffortRejected(providerId, req.model);
5355
+ effort = void 0;
5356
+ }
5078
5357
  continue;
5079
5358
  }
5080
5359
  if (thinkingOff && rejectsField(error, /thinking/i)) {
@@ -5099,11 +5378,12 @@ function thinkingOffParams(model) {
5099
5378
  return void 0;
5100
5379
  }
5101
5380
  function rejectsField(error, field2) {
5102
- if (!(error instanceof ProviderError) || error.status !== 400 && error.status !== 422) return false;
5381
+ if (!(error instanceof ProviderError)) return false;
5382
+ if (error.status !== void 0 && error.status !== 400 && error.status !== 422) return false;
5103
5383
  const cause = error.cause instanceof Error ? error.cause.message : "";
5104
5384
  return field2.test(`${error.message} ${cause}`);
5105
5385
  }
5106
- async function* streamOnce(client, providerId, apiKey2, req, effort, thinkingOff, tokenField) {
5386
+ async function* streamOnce2(client, providerId, apiKey2, req, effort, thinkingOff, tokenField) {
5107
5387
  let text = "";
5108
5388
  let thinking = "";
5109
5389
  let finishReason = null;
@@ -5212,7 +5492,7 @@ function toContentBlocks2(providerId, thinking, text, calls) {
5212
5492
  type: "tool_use",
5213
5493
  id: call.id || tmpId(position),
5214
5494
  name: call.name,
5215
- input: parseArguments(providerId, call.name, call.args)
5495
+ input: parseArguments(call.args)
5216
5496
  });
5217
5497
  }
5218
5498
  return content;
@@ -5223,21 +5503,16 @@ function toTextContentBlocks(thinking, text) {
5223
5503
  if (text) content.push({ type: "text", text });
5224
5504
  return content;
5225
5505
  }
5226
- function parseArguments(providerId, toolName, args) {
5506
+ function parseArguments(args) {
5507
+ if (args.trim() === "") return {};
5227
5508
  let parsed;
5228
5509
  try {
5229
5510
  parsed = JSON.parse(args);
5230
5511
  } catch {
5231
- throw new ProviderError(
5232
- `Tool "${toolName || "unknown"}" returned malformed JSON arguments; the tool was not run.`,
5233
- providerId
5234
- );
5512
+ parsed = void 0;
5235
5513
  }
5236
5514
  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
- );
5515
+ return { [INVALID_ARGUMENTS_KEY]: args.slice(0, 300) };
5241
5516
  }
5242
5517
  return parsed;
5243
5518
  }
@@ -5382,6 +5657,7 @@ function createPermissionEngine(configPermissions) {
5382
5657
  const configured = resolve3(tool, requested);
5383
5658
  if (configured === "deny") return "deny";
5384
5659
  if (current === "plan" && tool.readOnly !== true) return "deny";
5660
+ if (requested === "ask") return bypassEnabled ? "allow" : "ask";
5385
5661
  if (configured === "allow") return "allow";
5386
5662
  if (bypassEnabled) return "allow";
5387
5663
  if (current === "accept" && isFileEdit(tool)) return "allow";
@@ -5494,9 +5770,24 @@ function done(result) {
5494
5770
  // src/tools/edit.ts
5495
5771
  import { readFile as readFile9, stat as stat6 } from "fs/promises";
5496
5772
 
5773
+ // src/tools/protected.ts
5774
+ import path11 from "path";
5775
+ function protectedWritePermission(input, ctx) {
5776
+ const target = input?.path;
5777
+ if (typeof target !== "string") return void 0;
5778
+ const relative2 = ctx ? resolveInside(ctx.cwd, target) : void 0;
5779
+ const candidate = relative2?.ok ? relative2.relative : target;
5780
+ return isProtectedPath(candidate) ? "ask" : void 0;
5781
+ }
5782
+ function isProtectedPath(value) {
5783
+ const parts = path11.normalize(value).replaceAll("\\", "/").toLowerCase().split("/").filter(Boolean);
5784
+ const name = parts.at(-1) ?? "";
5785
+ return name === "kitcode.json" || name === ".envrc" || parts.includes(".kitcode") || parts.includes(".git");
5786
+ }
5787
+
5497
5788
  // src/tools/safe-write.ts
5498
5789
  import { randomUUID as randomUUID3 } from "crypto";
5499
- import { chmod as chmod5, lstat as lstat3, open, rename as rename5, rm as rm5 } from "fs/promises";
5790
+ import { chmod as chmod5, lstat as lstat3, open, rename as rename5, rm as rm6 } from "fs/promises";
5500
5791
  import { basename as basename2, dirname as dirname2, join as join2 } from "path";
5501
5792
  var UnsafeFileChangeError = class extends Error {
5502
5793
  constructor(message) {
@@ -5559,7 +5850,7 @@ async function atomicWriteSafeFile(file, data, snapshot) {
5559
5850
  await rename5(temp, file);
5560
5851
  } catch (error) {
5561
5852
  await handle?.close().catch(() => void 0);
5562
- await rm5(temp, { force: true }).catch(() => void 0);
5853
+ await rm6(temp, { force: true }).catch(() => void 0);
5563
5854
  throw error;
5564
5855
  }
5565
5856
  }
@@ -5650,60 +5941,61 @@ var editTool = {
5650
5941
  additionalProperties: false
5651
5942
  },
5652
5943
  defaultPermission: "ask",
5944
+ permission: protectedWritePermission,
5653
5945
  summarize(input) {
5654
5946
  return `edit(${brief(input.path)})`;
5655
5947
  },
5656
5948
  async preview(input, ctx) {
5657
- const { path: path15, oldString, newString, replaceAll = false } = input;
5658
- const safe = resolveInside(ctx.cwd, path15);
5949
+ const { path: path16, oldString, newString, replaceAll = false } = input;
5950
+ const safe = resolveInside(ctx.cwd, path16);
5659
5951
  if (!safe.ok) return { kind: "text", text: safe.reason };
5660
5952
  const info = await stat6(safe.path).catch(() => null);
5661
5953
  if (info && info.size > MAX_FILE_BYTES2) {
5662
- return { kind: "text", text: `Cannot preview ${path15}: the file is too large.` };
5954
+ return { kind: "text", text: `Cannot preview ${path16}: the file is too large.` };
5663
5955
  }
5664
5956
  const beforeBuffer = await readFile9(safe.path).catch(() => null);
5665
- if (!beforeBuffer) return { kind: "text", text: `Cannot read ${path15}.` };
5957
+ if (!beforeBuffer) return { kind: "text", text: `Cannot read ${path16}.` };
5666
5958
  if (beforeBuffer.subarray(0, 8192).includes(0)) {
5667
- return { kind: "text", text: `Cannot preview binary file ${path15}` };
5959
+ return { kind: "text", text: `Cannot preview binary file ${path16}` };
5668
5960
  }
5669
5961
  const before = beforeBuffer.toString("utf8");
5670
5962
  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 };
5963
+ return after === null ? { kind: "text", text: `Cannot preview edit: oldString is not a valid match in ${path16}.` } : { kind: "diff", path: path16, before, after };
5672
5964
  },
5673
5965
  async execute(input, ctx) {
5674
- const { path: path15, oldString, newString, replaceAll = false } = input;
5675
- const safe = resolveInside(ctx.cwd, path15);
5966
+ const { path: path16, oldString, newString, replaceAll = false } = input;
5967
+ const safe = resolveInside(ctx.cwd, path16);
5676
5968
  if (!safe.ok) return { content: safe.reason, isError: true };
5677
5969
  let snapshot;
5678
5970
  try {
5679
5971
  snapshot = await readSafeFileSnapshot(safe.path, MAX_FILE_BYTES2);
5680
5972
  } catch (error) {
5681
- return { content: `Cannot edit ${path15}: ${error.message}.`, isError: true };
5973
+ return { content: `Cannot edit ${path16}: ${error.message}.`, isError: true };
5682
5974
  }
5683
- if (!snapshot.exists) return { content: `Cannot read ${path15}.`, isError: true };
5975
+ if (!snapshot.exists) return { content: `Cannot read ${path16}.`, isError: true };
5684
5976
  const beforeBuffer = snapshot.data;
5685
5977
  if (beforeBuffer.subarray(0, 8192).includes(0)) {
5686
- return { content: `Cannot edit ${path15}: it is a binary file, not text.`, isError: true };
5978
+ return { content: `Cannot edit ${path16}: it is a binary file, not text.`, isError: true };
5687
5979
  }
5688
5980
  const before = beforeBuffer.toString("utf8");
5689
5981
  const count2 = countOccurrences(before, oldString);
5690
5982
  if (count2 === 0) {
5691
5983
  return {
5692
- content: `oldString was not found in ${path15}. Read the file again and match the text exactly, including whitespace.`,
5984
+ content: `oldString was not found in ${path16}. Read the file again and match the text exactly, including whitespace.`,
5693
5985
  isError: true
5694
5986
  };
5695
5987
  }
5696
5988
  if (count2 > 1 && !replaceAll) {
5697
5989
  return {
5698
- content: `oldString matches ${count2} places in ${path15}. Add more surrounding context to identify a single one, or set replaceAll to true.`,
5990
+ content: `oldString matches ${count2} places in ${path16}. Add more surrounding context to identify a single one, or set replaceAll to true.`,
5699
5991
  isError: true
5700
5992
  };
5701
5993
  }
5702
5994
  const after = replacement(before, oldString, newString, replaceAll);
5703
- if (after === null) return { content: `Could not prepare edit for ${path15}.`, isError: true };
5995
+ if (after === null) return { content: `Could not prepare edit for ${path16}.`, isError: true };
5704
5996
  if (Buffer.byteLength(after, "utf8") > MAX_FILE_BYTES2) {
5705
5997
  return {
5706
- content: `Cannot edit ${path15}: the result exceeds the ${MAX_FILE_BYTES2 / 1e6} MB limit.`,
5998
+ content: `Cannot edit ${path16}: the result exceeds the ${MAX_FILE_BYTES2 / 1e6} MB limit.`,
5707
5999
  isError: true
5708
6000
  };
5709
6001
  }
@@ -5716,11 +6008,11 @@ var editTool = {
5716
6008
  await atomicWriteSafeFile(safe.path, after, snapshot);
5717
6009
  ctx.checkpoint?.markChanged(safe.path);
5718
6010
  } catch (error) {
5719
- return { content: `Failed to write ${path15}: ${error.message}`, isError: true };
6011
+ return { content: `Failed to write ${path16}: ${error.message}`, isError: true };
5720
6012
  }
5721
6013
  return {
5722
- content: `Edited ${path15} (${count2} ${count2 === 1 ? "replacement" : "replacements"})`,
5723
- display: { kind: "diff", path: path15, before, after }
6014
+ content: `Edited ${path16} (${count2} ${count2 === 1 ? "replacement" : "replacements"})`,
6015
+ display: { kind: "diff", path: path16, before, after }
5724
6016
  };
5725
6017
  }
5726
6018
  };
@@ -5737,45 +6029,6 @@ import { readFile as readFile10, stat as stat7 } from "fs/promises";
5737
6029
  import { join as join3 } from "path";
5738
6030
  import fg3 from "fast-glob";
5739
6031
 
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
6032
  // src/tools/regex.ts
5780
6033
  import { Worker } from "worker_threads";
5781
6034
  var MATCH_TIMEOUT_MS = 300;
@@ -5885,16 +6138,16 @@ var grepTool = {
5885
6138
  defaultPermission: "allow",
5886
6139
  readOnly: true,
5887
6140
  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;
6141
+ const { path: path16, glob } = input ?? {};
6142
+ if (isSensitivePath(path16) || mentionsSensitivePattern(glob)) return "ask";
6143
+ const safe = ctx && path16 ? resolveInside(ctx.cwd, path16) : void 0;
5891
6144
  return safe?.ok && isSensitivePath(safe.relative) ? "ask" : void 0;
5892
6145
  },
5893
6146
  summarize(input) {
5894
6147
  return `grep(${brief(input.pattern)})`;
5895
6148
  },
5896
6149
  async execute(input, ctx) {
5897
- const { pattern, path: path15 = ".", glob = "**/*", maxMatches } = input;
6150
+ const { pattern, path: path16 = ".", glob = "**/*", maxMatches } = input;
5898
6151
  if (pattern.length > MAX_PATTERN_CHARS) {
5899
6152
  return {
5900
6153
  content: `Regular expression is limited to ${MAX_PATTERN_CHARS} characters.`,
@@ -5909,10 +6162,10 @@ var grepTool = {
5909
6162
  if (patternEscapes(glob)) {
5910
6163
  return { content: `Glob ${glob} must be relative to the workspace root.`, isError: true };
5911
6164
  }
5912
- const safe = resolveInside(ctx.cwd, path15);
6165
+ const safe = resolveInside(ctx.cwd, path16);
5913
6166
  if (!safe.ok) return { content: safe.reason, isError: true };
5914
6167
  const target = await stat7(safe.path).catch(() => null);
5915
- if (!target) return { content: `Path not found: ${path15}`, isError: true };
6168
+ if (!target) return { content: `Path not found: ${path16}`, isError: true };
5916
6169
  const discovered = target.isDirectory() ? (await fg3(glob, {
5917
6170
  cwd: safe.path,
5918
6171
  dot: false,
@@ -5926,7 +6179,8 @@ var grepTool = {
5926
6179
  relative: join3(safe.relative, entry.path),
5927
6180
  size: entry.stats?.size ?? 0
5928
6181
  })) : [{ absolute: safe.path, relative: safe.relative, size: target.size }];
5929
- const files = discovered.slice(0, MAX_FILES);
6182
+ const sensitiveRequested = isSensitivePath(path16) || isSensitivePath(safe.relative) || mentionsSensitivePattern(glob);
6183
+ const files = (sensitiveRequested ? discovered : discovered.filter((file) => !isSensitivePath(file.relative))).slice(0, MAX_FILES);
5930
6184
  const filesCapped = discovered.length > MAX_FILES;
5931
6185
  const limit = Math.min(maxMatches ?? MAX_MATCHES, MAX_MATCHES);
5932
6186
  const hits = [];
@@ -6002,30 +6256,30 @@ var readTool = {
6002
6256
  return `read(${brief(input.path)})`;
6003
6257
  },
6004
6258
  async execute(input, ctx) {
6005
- const { path: path15, offset = 1, limit } = input;
6006
- const safe = resolveInside(ctx.cwd, path15);
6259
+ const { path: path16, offset = 1, limit } = input;
6260
+ const safe = resolveInside(ctx.cwd, path16);
6007
6261
  if (!safe.ok) return { content: safe.reason, isError: true };
6008
6262
  const info = await stat8(safe.path).catch(() => null);
6009
- if (!info) return { content: `File not found: ${path15}`, isError: true };
6263
+ if (!info) return { content: `File not found: ${path16}`, isError: true };
6010
6264
  if (info.isDirectory()) {
6011
- return { content: `${path15} is a directory, not a file. Use glob to list its contents.`, isError: true };
6265
+ return { content: `${path16} is a directory, not a file. Use glob to list its contents.`, isError: true };
6012
6266
  }
6013
6267
  if (info.size > MAX_FILE_BYTES4) {
6014
6268
  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.`,
6269
+ 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
6270
  isError: true
6017
6271
  };
6018
6272
  }
6019
6273
  const buffer = await readFile11(safe.path);
6020
6274
  if (buffer.subarray(0, 4096).includes(0)) {
6021
- return { content: `${path15} looks like a binary file and cannot be read as text.`, isError: true };
6275
+ return { content: `${path16} looks like a binary file and cannot be read as text.`, isError: true };
6022
6276
  }
6023
6277
  const lines = buffer.toString("utf8").split("\n");
6024
6278
  if (lines.at(-1) === "") lines.pop();
6025
- if (lines.length === 0) return { content: `${path15} is empty.` };
6279
+ if (lines.length === 0) return { content: `${path16} is empty.` };
6026
6280
  const start = Math.max(1, offset) - 1;
6027
6281
  if (start >= lines.length) {
6028
- return { content: `${path15} has ${lines.length} lines, so offset ${offset} is past the end.`, isError: true };
6282
+ return { content: `${path16} has ${lines.length} lines, so offset ${offset} is past the end.`, isError: true };
6029
6283
  }
6030
6284
  const window = lines.slice(start, start + Math.min(limit ?? MAX_LINES, MAX_LINES));
6031
6285
  const rendered = [];
@@ -6068,68 +6322,69 @@ var writeTool = {
6068
6322
  additionalProperties: false
6069
6323
  },
6070
6324
  defaultPermission: "ask",
6325
+ permission: protectedWritePermission,
6071
6326
  summarize(input) {
6072
6327
  return `write(${brief(input.path)})`;
6073
6328
  },
6074
6329
  async preview(input, ctx) {
6075
- const { path: path15, content } = input;
6330
+ const { path: path16, content } = input;
6076
6331
  if (Buffer.byteLength(content, "utf8") > MAX_FILE_BYTES5) {
6077
6332
  return { kind: "text", text: `Write is limited to ${MAX_FILE_BYTES5 / 1e6} MB.` };
6078
6333
  }
6079
- const safe = resolveInside(ctx.cwd, path15);
6334
+ const safe = resolveInside(ctx.cwd, path16);
6080
6335
  if (!safe.ok) return { kind: "text", text: safe.reason };
6081
6336
  const beforeInfo = await stat9(safe.path).catch(() => null);
6082
6337
  if (beforeInfo && beforeInfo.size > MAX_FILE_BYTES5) {
6083
- return { kind: "text", text: `Cannot preview ${path15}: the existing file is too large.` };
6338
+ return { kind: "text", text: `Cannot preview ${path16}: the existing file is too large.` };
6084
6339
  }
6085
6340
  const beforeBuffer = await readFile12(safe.path).catch(() => null);
6086
6341
  if (beforeBuffer?.subarray(0, 8192).includes(0)) {
6087
- return { kind: "text", text: `Cannot preview binary file ${path15}` };
6342
+ return { kind: "text", text: `Cannot preview binary file ${path16}` };
6088
6343
  }
6089
6344
  return {
6090
6345
  kind: "diff",
6091
- path: path15,
6346
+ path: path16,
6092
6347
  before: beforeBuffer?.toString("utf8") ?? "",
6093
6348
  after: content
6094
6349
  };
6095
6350
  },
6096
6351
  async execute(input, ctx) {
6097
- const { path: path15, content } = input;
6352
+ const { path: path16, content } = input;
6098
6353
  if (Buffer.byteLength(content, "utf8") > MAX_FILE_BYTES5) {
6099
6354
  return {
6100
- content: `Cannot write ${path15}: content exceeds the ${MAX_FILE_BYTES5 / 1e6} MB limit.`,
6355
+ content: `Cannot write ${path16}: content exceeds the ${MAX_FILE_BYTES5 / 1e6} MB limit.`,
6101
6356
  isError: true
6102
6357
  };
6103
6358
  }
6104
- const safe = resolveInside(ctx.cwd, path15);
6359
+ const safe = resolveInside(ctx.cwd, path16);
6105
6360
  if (!safe.ok) return { content: safe.reason, isError: true };
6106
6361
  let target = safe.path;
6107
6362
  let snapshot;
6108
6363
  try {
6109
6364
  await mkdir4(dirname3(target), { recursive: true });
6110
- const rechecked = resolveInside(ctx.cwd, path15);
6365
+ const rechecked = resolveInside(ctx.cwd, path16);
6111
6366
  if (!rechecked.ok || rechecked.path !== target) {
6112
6367
  return {
6113
- content: `Cannot write ${path15}: the path changed while its parent was created.`,
6368
+ content: `Cannot write ${path16}: the path changed while its parent was created.`,
6114
6369
  isError: true
6115
6370
  };
6116
6371
  }
6117
6372
  target = rechecked.path;
6118
6373
  snapshot = await readSafeFileSnapshot(target, MAX_FILE_BYTES5);
6119
6374
  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 };
6375
+ return { content: `Cannot write ${path16}: the existing file is binary, not text.`, isError: true };
6121
6376
  }
6122
6377
  await ctx.checkpoint?.capture(target);
6123
6378
  await atomicWriteSafeFile(target, content, snapshot);
6124
6379
  ctx.checkpoint?.markChanged(target);
6125
6380
  } catch (error) {
6126
- return { content: `Failed to write ${path15}: ${error.message}`, isError: true };
6381
+ return { content: `Failed to write ${path16}: ${error.message}`, isError: true };
6127
6382
  }
6128
6383
  const before = snapshot.exists ? snapshot.data.toString("utf8") : "";
6129
6384
  const lines = content === "" ? 0 : content.replace(/\n$/, "").split("\n").length;
6130
6385
  return {
6131
- content: `${before === "" ? "Created" : "Updated"} ${path15} (${lines} ${lines === 1 ? "line" : "lines"})`,
6132
- display: { kind: "diff", path: path15, before, after: content }
6386
+ content: `${before === "" ? "Created" : "Updated"} ${path16} (${lines} ${lines === 1 ? "line" : "lines"})`,
6387
+ display: { kind: "diff", path: path16, before, after: content }
6133
6388
  };
6134
6389
  }
6135
6390
  };
@@ -6159,9 +6414,9 @@ function createToolRegistry(tools) {
6159
6414
 
6160
6415
  // src/prompts/library.ts
6161
6416
  import { chmod as chmod6, readFile as readFile13, readdir as readdir4, unlink as unlink3, writeFile as writeFile8 } from "fs/promises";
6162
- import path11 from "path";
6417
+ import path12 from "path";
6163
6418
  async function savePrompt(input) {
6164
- const slug = slugify(input.name);
6419
+ const slug = promptSlug(input.name);
6165
6420
  if (!slug) throw new Error(`Prompt name has no letters or digits to name a file after: ${input.name}`);
6166
6421
  const prompt = {
6167
6422
  slug,
@@ -6171,7 +6426,7 @@ async function savePrompt(input) {
6171
6426
  body: input.body.trim()
6172
6427
  };
6173
6428
  await ensureDir(promptsDir);
6174
- const file = path11.join(promptsDir, `${slug}.md`);
6429
+ const file = path12.join(promptsDir, `${slug}.md`);
6175
6430
  await writeFile8(file, serialize(prompt), {
6176
6431
  encoding: "utf8",
6177
6432
  mode: 384
@@ -6182,7 +6437,7 @@ async function savePrompt(input) {
6182
6437
  async function getPrompt(slug) {
6183
6438
  assertSlug(slug);
6184
6439
  try {
6185
- return parse(slug, await readFile13(path11.join(promptsDir, `${slug}.md`), "utf8"));
6440
+ return parse(slug, await readFile13(path12.join(promptsDir, `${slug}.md`), "utf8"));
6186
6441
  } catch (error) {
6187
6442
  if (isMissing(error)) return null;
6188
6443
  throw error;
@@ -6197,16 +6452,27 @@ async function listPrompts() {
6197
6452
  return [];
6198
6453
  }
6199
6454
  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
- )
6455
+ names.filter((name) => name.endsWith(".md") && validSlug(name.slice(0, -3))).map(async (name) => {
6456
+ const text = await readFile13(path12.join(promptsDir, name), "utf8").catch(() => null);
6457
+ return text === null ? null : parse(name.slice(0, -3), text);
6458
+ })
6203
6459
  );
6204
- return prompts2.sort((a, b) => a.name.localeCompare(b.name));
6460
+ return prompts2.filter((prompt) => prompt !== null).sort((a, b) => a.name.localeCompare(b.name));
6205
6461
  }
6206
6462
  async function deletePrompt(slug) {
6207
6463
  assertSlug(slug);
6208
6464
  try {
6209
- await unlink3(path11.join(promptsDir, `${slug}.md`));
6465
+ await unlink3(path12.join(promptsDir, `${slug}.md`));
6466
+ return true;
6467
+ } catch (error) {
6468
+ if (isMissing(error)) return false;
6469
+ throw error;
6470
+ }
6471
+ }
6472
+ async function promptExists(slug) {
6473
+ assertSlug(slug);
6474
+ try {
6475
+ await readFile13(path12.join(promptsDir, `${slug}.md`), "utf8");
6210
6476
  return true;
6211
6477
  } catch (error) {
6212
6478
  if (isMissing(error)) return false;
@@ -6216,11 +6482,16 @@ async function deletePrompt(slug) {
6216
6482
  function isMissing(error) {
6217
6483
  return error.code === "ENOENT";
6218
6484
  }
6219
- function slugify(name) {
6220
- return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
6485
+ var RESERVED_NAMES = /^(?:con|prn|aux|nul|com[0-9]|lpt[0-9])$/;
6486
+ function promptSlug(name) {
6487
+ const slug = name.normalize("NFC").toLowerCase().replace(/[^\p{L}\p{N}]+/gu, "-").replace(/^-+|-+$/g, "").slice(0, 80).replace(/-+$/, "");
6488
+ return RESERVED_NAMES.test(slug) ? `${slug}-prompt` : slug;
6489
+ }
6490
+ function validSlug(slug) {
6491
+ return /^[\p{L}\p{N}]+(?:-[\p{L}\p{N}]+)*$/u.test(slug) && !RESERVED_NAMES.test(slug);
6221
6492
  }
6222
6493
  function assertSlug(slug) {
6223
- if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(slug)) {
6494
+ if (!validSlug(slug)) {
6224
6495
  throw new Error(`Invalid prompt id: ${slug}`);
6225
6496
  }
6226
6497
  }
@@ -6264,7 +6535,7 @@ function parse(slug, text) {
6264
6535
 
6265
6536
  // src/skills/library.ts
6266
6537
  import { lstat as lstat4, open as open2, readdir as readdir5 } from "fs/promises";
6267
- import path12 from "path";
6538
+ import path13 from "path";
6268
6539
  var SKILL_FILE = "SKILL.md";
6269
6540
  var FRONTMATTER_BYTES = 8192;
6270
6541
  var MAX_SKILL_BYTES = 5e6;
@@ -6277,7 +6548,7 @@ async function discoverSkills(dirs) {
6277
6548
  if (!rootInfo?.isDirectory() || rootInfo.isSymbolicLink()) continue;
6278
6549
  const entries = await readdir5(root, { withFileTypes: true }).catch(() => []);
6279
6550
  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)))
6551
+ entries.filter((entry) => entry.isDirectory() && !entry.isSymbolicLink()).slice(0, MAX_SKILLS_PER_ROOT).map((entry) => readMeta(root, rootInfo, path13.join(root, entry.name)))
6281
6552
  );
6282
6553
  for (const meta of found) {
6283
6554
  if (meta && !byName.has(meta.name)) byName.set(meta.name, meta);
@@ -6317,11 +6588,11 @@ function formatSkillCatalogue(skills) {
6317
6588
  async function readMeta(root, rootInfo, dir) {
6318
6589
  const dirInfo = await lstat4(dir).catch(() => null);
6319
6590
  if (!dirInfo?.isDirectory() || dirInfo.isSymbolicLink()) return null;
6320
- const file = path12.join(dir, SKILL_FILE);
6591
+ const file = path13.join(dir, SKILL_FILE);
6321
6592
  const head = await readFrontmatterBytes(file);
6322
6593
  if (!head) return null;
6323
6594
  const { fields } = parseFrontmatter(head.text);
6324
- const meta = { name: fields.name || path12.basename(dir), description: fields.description ?? "", dir, file };
6595
+ const meta = { name: fields.name || path13.basename(dir), description: fields.description ?? "", dir, file };
6325
6596
  guards.set(meta, {
6326
6597
  root,
6327
6598
  rootIdentity: identity(rootInfo),
@@ -6393,10 +6664,21 @@ function parseFrontmatter(text) {
6393
6664
  if (lines[0]?.trim() === "---") {
6394
6665
  let i = 1;
6395
6666
  for (; i < lines.length && lines[i].trim() !== "---"; i++) {
6396
- const colon = lines[i].indexOf(":");
6667
+ const line = lines[i];
6668
+ if (/^\s/.test(line)) continue;
6669
+ const colon = line.indexOf(":");
6397
6670
  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();
6671
+ const key = line.slice(0, colon).trim();
6672
+ if (key !== "name" && key !== "description") continue;
6673
+ let value = line.slice(colon + 1).trim();
6674
+ if (/^[>|][+-]?$/.test(value)) {
6675
+ const block = [];
6676
+ while (i + 1 < lines.length && /^\s+\S/.test(lines[i + 1]) && lines[i + 1].trim() !== "---") {
6677
+ block.push(lines[++i].trim());
6678
+ }
6679
+ value = block.join(value.startsWith("|") ? "\n" : " ");
6680
+ }
6681
+ fields[key] = value.replace(/^(["'])(.*)\1$/, "$2").trim();
6400
6682
  }
6401
6683
  bodyStart = Math.min(i + 1, lines.length);
6402
6684
  }
@@ -6404,174 +6686,238 @@ function parseFrontmatter(text) {
6404
6686
  }
6405
6687
 
6406
6688
  // src/skills/install.ts
6689
+ import { execFile as execFile2 } from "child_process";
6407
6690
  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");
6691
+ import { existsSync as existsSync2 } from "fs";
6692
+ 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";
6693
+ import path14 from "path";
6694
+ var TMP_DIR = path14.join(skillsDir, ".tmp");
6695
+ var SKILL_FILE2 = "SKILL.md";
6422
6696
  var MAX_SKILL_BYTES2 = 5e6;
6423
- async function installSkill(source) {
6697
+ var MAX_ARCHIVE_BYTES = 2e7;
6698
+ var MAX_SKILL_FILES = 1e3;
6699
+ var MAX_SKILL_TOTAL_BYTES = 5e7;
6700
+ var MAX_SEARCH_DEPTH = 4;
6701
+ var COMMAND_TIMEOUT_MS = 12e4;
6702
+ var SKIPPED_DIRS = /* @__PURE__ */ new Set([".git", "node_modules"]);
6703
+ async function installSkill(source, run = runCommand) {
6704
+ const trimmed = source.trim();
6705
+ if (!trimmed) throw new Error("Give a skill source: a GitHub URL, an npm package or a local path.");
6424
6706
  await ensureDir(skillsDir);
6425
- if (isGitHubUrl(source)) {
6426
- return installFromGitHub(source);
6707
+ if (isGitHubUrl(trimmed)) return installFromGitHub(trimmed, run);
6708
+ if (npmSkillName(trimmed) && !existsSync2(path14.resolve(trimmed))) {
6709
+ return installFromNpm(trimmed, run);
6427
6710
  }
6428
- if (isNpmPackage(source)) {
6429
- return installFromNpm(source);
6430
- }
6431
- return installFromLocal(source);
6711
+ return installFromLocal(trimmed);
6432
6712
  }
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;
6713
+ async function uninstallSkill(name) {
6714
+ const dir = safeChildPath(skillsDir, safeSkillName(name));
6715
+ const info = await lstat5(dir).catch(() => null);
6716
+ if (!info) throw new Error(`No installed skill named "${name}" in ${skillsDir}`);
6717
+ if (info.isSymbolicLink() || !info.isDirectory()) {
6718
+ throw new Error(`Refusing to remove ${dir}: it is not a plain skill directory.`);
6439
6719
  }
6720
+ await rm7(dir, { recursive: true, force: true });
6721
+ return dir;
6440
6722
  }
6441
- function isNpmPackage(input) {
6442
- return npmSkillName(input) !== null;
6443
- }
6444
- async function installFromGitHub(url) {
6723
+ async function installFromGitHub(url, run) {
6445
6724
  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
- }
6725
+ return withTempDir(async (tmp) => {
6726
+ const checkout = path14.join(tmp, "r");
6727
+ const args = ["-c", "core.longpaths=true", "clone", "--depth", "1", "--single-branch"];
6728
+ if (branch) args.push("--branch", branch);
6729
+ args.push(`https://github.com/${owner}/${repo}.git`, checkout);
6730
+ try {
6731
+ await run("git", args, { cwd: tmp, env: { ...process.env, GIT_TERMINAL_PROMPT: "0" } });
6732
+ } catch (error) {
6733
+ throw new Error(`Could not clone ${owner}/${repo}${branch ? ` (${branch})` : ""}: ${messageOf2(error)}`);
6734
+ }
6735
+ const start = subdir ? safeChildPath(checkout, ...subdir.split("/")) : checkout;
6736
+ const skillDir = await locateSkillDir(start, url);
6737
+ const fallback = skillDir === checkout ? repo : path14.basename(skillDir);
6738
+ return copyIntoSkills(skillDir, fallback, url);
6739
+ });
6464
6740
  }
6465
- async function installFromNpm(packageName) {
6741
+ async function installFromNpm(packageName, run) {
6466
6742
  const parsedName = npmSkillName(packageName);
6467
6743
  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 });
6744
+ return withTempDir(async (tmp) => {
6485
6745
  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?");
6746
+ await run("npm", ["pack", packageName, "--pack-destination", ".", "--silent"], { cwd: tmp });
6747
+ } catch (error) {
6748
+ throw new Error(`Could not download npm package "${packageName}": ${messageOf2(error)}`);
6498
6749
  }
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}"`);
6750
+ const tarball = (await readdir6(tmp)).find((file) => file.endsWith(".tgz"));
6751
+ if (!tarball) throw new Error(`npm did not produce an archive for "${packageName}"`);
6752
+ const archive = path14.join(tmp, tarball);
6753
+ const info = await lstat5(archive);
6754
+ if (!info.isFile() || info.size > MAX_ARCHIVE_BYTES) {
6755
+ throw new Error(`The npm archive exceeds the ${MAX_ARCHIVE_BYTES / 1e6} MB limit.`);
6505
6756
  }
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
- }
6757
+ const extracted = path14.join(tmp, "extracted");
6758
+ await mkdir5(extracted);
6759
+ try {
6760
+ await run("tar", ["-xzf", tarball, "-C", "extracted"], { cwd: tmp });
6761
+ } catch (error) {
6762
+ throw new Error(`Could not extract the npm package (is tar available?): ${messageOf2(error)}`);
6763
+ }
6764
+ const skillDir = await locateSkillDir(path14.join(extracted, "package"), packageName);
6765
+ return copyIntoSkills(skillDir, parsedName, packageName);
6766
+ });
6512
6767
  }
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;
6768
+ async function installFromLocal(source) {
6769
+ const resolved = path14.resolve(source);
6770
+ const info = await lstat5(resolved).catch(() => null);
6771
+ if (!info) throw new Error(`No such file or directory: ${source}`);
6772
+ const start = info.isFile() && path14.basename(resolved) === SKILL_FILE2 ? path14.dirname(resolved) : resolved;
6773
+ const skillDir = await locateSkillDir(start, source);
6774
+ return copyIntoSkills(skillDir, path14.basename(skillDir), resolved);
6519
6775
  }
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.");
6776
+ async function locateSkillDir(start, source) {
6777
+ const info = await lstat5(start).catch(() => null);
6778
+ if (!info?.isDirectory() || info.isSymbolicLink()) {
6779
+ throw new Error(`"${source}" does not point to a directory.`);
6524
6780
  }
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.");
6781
+ if (await isPlainFile(path14.join(start, SKILL_FILE2))) return start;
6782
+ const found = [];
6783
+ const walk = async (dir, depth) => {
6784
+ if (depth > MAX_SEARCH_DEPTH || found.length > 50) return;
6785
+ const entries = await readdir6(dir, { withFileTypes: true }).catch(() => []);
6786
+ for (const entry of entries) {
6787
+ if (!entry.isDirectory() || SKIPPED_DIRS.has(entry.name)) continue;
6788
+ const child = path14.join(dir, entry.name);
6789
+ if (await isPlainFile(path14.join(child, SKILL_FILE2))) found.push(child);
6790
+ else await walk(child, depth + 1);
6791
+ }
6792
+ };
6793
+ await walk(start, 1);
6794
+ if (found.length === 1) return found[0];
6795
+ if (found.length === 0) throw new Error(`No ${SKILL_FILE2} found in "${source}".`);
6796
+ const listed = found.slice(0, 20).map((dir) => ` ${path14.relative(start, dir).split(path14.sep).join("/")}`).join("\n");
6797
+ throw new Error(
6798
+ `"${source}" contains ${found.length} skills; install one of them by its own path (for GitHub: .../tree/<branch>/<path>):
6799
+ ${listed}`
6800
+ );
6801
+ }
6802
+ async function copyIntoSkills(sourceDir, fallbackName, source) {
6803
+ const skillFile = path14.join(sourceDir, SKILL_FILE2);
6804
+ const skillInfo = await lstat5(skillFile);
6805
+ if (skillInfo.size > MAX_SKILL_BYTES2) {
6806
+ throw new Error(`${SKILL_FILE2} exceeds the ${MAX_SKILL_BYTES2 / 1e6} MB limit.`);
6533
6807
  }
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}"`);
6808
+ const name = safeSkillName(frontmatterName(await readFile14(skillFile, "utf8")) ?? fallbackName);
6809
+ const destination = safeChildPath(skillsDir, name);
6810
+ const existing = await lstat5(destination).catch(() => null);
6811
+ if (existing && (existing.isSymbolicLink() || !existing.isDirectory())) {
6812
+ throw new Error(`Refusing to replace ${destination}: it is not a plain skill directory.`);
6813
+ }
6814
+ await ensureDir(TMP_DIR);
6815
+ const staging = safeChildPath(TMP_DIR, `s-${randomUUID4().slice(0, 8)}`);
6816
+ try {
6817
+ await copyTree(sourceDir, staging);
6818
+ if (existing) await rm7(destination, { recursive: true, force: true });
6819
+ await rename6(staging, destination);
6820
+ } catch (error) {
6821
+ await rm7(staging, { recursive: true, force: true }).catch(() => void 0);
6822
+ throw error;
6554
6823
  }
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 };
6824
+ return { name, dir: destination, source };
6560
6825
  }
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}`);
6826
+ async function copyTree(from, to) {
6827
+ let files = 0;
6828
+ let bytes = 0;
6829
+ const visit = async (dir, target) => {
6830
+ await mkdir5(target, { recursive: true, mode: 448 });
6831
+ for (const entry of await readdir6(dir, { withFileTypes: true })) {
6832
+ const source = path14.join(dir, entry.name);
6833
+ const destination = path14.join(target, entry.name);
6834
+ if (entry.isDirectory()) {
6835
+ if (!SKIPPED_DIRS.has(entry.name)) await visit(source, destination);
6836
+ continue;
6837
+ }
6838
+ if (!entry.isFile()) continue;
6839
+ const info = await lstat5(source);
6840
+ files += 1;
6841
+ bytes += info.size;
6842
+ if (files > MAX_SKILL_FILES || bytes > MAX_SKILL_TOTAL_BYTES) {
6843
+ throw new Error(
6844
+ `The skill is too large to install (limit ${MAX_SKILL_FILES} files, ${MAX_SKILL_TOTAL_BYTES / 1e6} MB).`
6845
+ );
6846
+ }
6847
+ await copyFile(source, destination);
6848
+ await chmod7(destination, (info.mode & 73) !== 0 ? 448 : 384);
6849
+ }
6850
+ };
6851
+ await visit(from, to);
6852
+ }
6853
+ function frontmatterName(text) {
6854
+ const lines = text.replace(/^/, "").split(/\r?\n/);
6855
+ if (lines[0]?.trim() !== "---") return void 0;
6856
+ for (const line of lines.slice(1)) {
6857
+ if (line.trim() === "---") return void 0;
6858
+ const match = /^name\s*:\s*(.+)$/.exec(line);
6859
+ if (match) {
6860
+ const value = match[1].trim().replace(/^(["'])(.*)\1$/, "$2").trim();
6861
+ return /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(value) ? value : void 0;
6862
+ }
6566
6863
  }
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}`);
6864
+ return void 0;
6865
+ }
6866
+ async function withTempDir(work) {
6867
+ await ensureDir(TMP_DIR);
6868
+ const dir = safeChildPath(TMP_DIR, randomUUID4().slice(0, 8));
6869
+ await mkdir5(dir, { mode: 448 });
6870
+ try {
6871
+ return await work(dir);
6872
+ } finally {
6873
+ await rm7(dir, { recursive: true, force: true }).catch(() => void 0);
6572
6874
  }
6573
- writeFileSync(file, body, { encoding: "utf8", mode: 384 });
6574
- await chmod7(file, 384);
6875
+ }
6876
+ function runCommand(command, args, options) {
6877
+ const viaShell = process.platform === "win32" && command === "npm";
6878
+ const file = viaShell ? [command, ...args.map(quoteForCmd)].join(" ") : command;
6879
+ return new Promise((resolve3, reject) => {
6880
+ execFile2(
6881
+ file,
6882
+ viaShell ? [] : args,
6883
+ {
6884
+ cwd: options.cwd,
6885
+ env: options.env,
6886
+ shell: viaShell,
6887
+ timeout: COMMAND_TIMEOUT_MS,
6888
+ windowsHide: true,
6889
+ maxBuffer: 4e6
6890
+ },
6891
+ (error, _stdout, stderr) => {
6892
+ if (!error) return resolve3();
6893
+ const code = error.code;
6894
+ if (code === "ENOENT") return reject(new Error(`${command} is not installed or not on PATH`));
6895
+ if (error.killed) {
6896
+ return reject(new Error(`${command} timed out after ${COMMAND_TIMEOUT_MS / 1e3}s`));
6897
+ }
6898
+ const detail = String(stderr ?? "").trim().split(/\r?\n/).filter((line) => line.trim() !== "" && !/^Cloning into /.test(line)).slice(-3).join(" ");
6899
+ reject(new Error(detail || error.message));
6900
+ }
6901
+ );
6902
+ });
6903
+ }
6904
+ function quoteForCmd(value) {
6905
+ return /^[\w@./:=+-]+$/.test(value) ? value : `"${value.replace(/"/g, '""')}"`;
6906
+ }
6907
+ function isGitHubUrl(input) {
6908
+ try {
6909
+ const url = new URL(input);
6910
+ return url.protocol === "https:" && (url.hostname === "github.com" || url.hostname === "www.github.com");
6911
+ } catch {
6912
+ return false;
6913
+ }
6914
+ }
6915
+ function npmSkillName(input) {
6916
+ if (input.includes("\\") || input.startsWith(".")) return null;
6917
+ const scoped = input.match(/^@[\w][\w.-]*\/([\w][\w.-]*)(?:@[\w.*+-]+)?$/);
6918
+ if (scoped) return scoped[1] ?? null;
6919
+ const unscoped = input.match(/^([\w][\w.-]*)(?:@[\w.*+-]+)?$/);
6920
+ return unscoped?.[1] ?? null;
6575
6921
  }
6576
6922
  function parseGitHubUrl(url) {
6577
6923
  let parsed;
@@ -6593,11 +6939,13 @@ function parseGitHubUrl(url) {
6593
6939
  const owner = safeGitHubSegment(segments[0], "owner");
6594
6940
  const repo = safeSkillName(segments[1].replace(/\.git$/i, ""));
6595
6941
  if (segments.length === 2) return { owner, repo };
6596
- if (segments[2] !== "tree" || segments.length < 4) {
6942
+ if (segments[2] !== "tree" && segments[2] !== "blob" || segments.length < 4) {
6597
6943
  throw new Error(`Unsupported GitHub skill URL: ${url}`);
6598
6944
  }
6599
6945
  const branch = safeGitHubSegment(segments[3], "branch");
6600
- const subdirSegments = segments.slice(4).map((segment) => safeGitHubSegment(segment, "path"));
6946
+ const pathSegments = segments.slice(4);
6947
+ if (segments[2] === "blob" && pathSegments.at(-1) === SKILL_FILE2) pathSegments.pop();
6948
+ const subdirSegments = pathSegments.map((segment) => safeGitHubSegment(segment, "path"));
6601
6949
  return {
6602
6950
  owner,
6603
6951
  repo,
@@ -6606,7 +6954,7 @@ function parseGitHubUrl(url) {
6606
6954
  };
6607
6955
  }
6608
6956
  function safeGitHubSegment(value, kind) {
6609
- if (value === "" || value === "." || value === ".." || value.length > 255 || /[\0-\x1f\x7f/\\:*?"<>|#%]/.test(value)) {
6957
+ if (value === "" || value === "." || value === ".." || value.startsWith("-") || value.length > 255 || /[\0-\x1f\x7f/\\:*?"<>|#%]/.test(value)) {
6610
6958
  throw new Error(`Unsafe GitHub ${kind}: ${value || "(empty)"}`);
6611
6959
  }
6612
6960
  return value;
@@ -6618,37 +6966,19 @@ function safeSkillName(value) {
6618
6966
  return value;
6619
6967
  }
6620
6968
  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;
6969
+ const target = path14.resolve(root, ...segments);
6970
+ const relative2 = path14.relative(path14.resolve(root), target);
6971
+ if (relative2 !== "" && (path14.isAbsolute(relative2) || relative2 === ".." || relative2.startsWith(`..${path14.sep}`))) {
6972
+ throw new Error(`Path escapes the skills directory: ${target}`);
6629
6973
  }
6630
- throw new Error(`Path escapes the skills directory: ${target}`);
6974
+ return target;
6631
6975
  }
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");
6976
+ async function isPlainFile(file) {
6977
+ const info = await lstat5(file).catch(() => null);
6978
+ return Boolean(info?.isFile() && !info.isSymbolicLink());
6644
6979
  }
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 });
6980
+ function messageOf2(error) {
6981
+ return error instanceof Error ? error.message : String(error);
6652
6982
  }
6653
6983
 
6654
6984
  // src/tools/skill.ts
@@ -6922,6 +7252,12 @@ async function boot(options) {
6922
7252
  const skillRoots = workspaceTrusted ? [projectSkillsDir(workspaceRoot2), skillsDir] : [skillsDir];
6923
7253
  let skills = await discoverSkills(skillRoots);
6924
7254
  if (skills.length > 0) tools.register([createSkillTool(skills)]);
7255
+ const reloadSkills = async () => {
7256
+ skills = await discoverSkills(skillRoots);
7257
+ skillCatalogue = formatSkillCatalogue(skills);
7258
+ tools.unregister(["skill"]);
7259
+ if (skills.length > 0) tools.register([createSkillTool(skills)]);
7260
+ };
6925
7261
  const mcp = createMcpManager(config.mcp);
6926
7262
  await mcp.connectAll();
6927
7263
  let registeredMcpToolNames = /* @__PURE__ */ new Set();
@@ -6934,6 +7270,7 @@ async function boot(options) {
6934
7270
  );
6935
7271
  };
6936
7272
  syncMcpTools();
7273
+ mcp.onChange(syncMcpTools);
6937
7274
  for (const state of mcp.states()) {
6938
7275
  if (state.status === "error") warnings.push(`MCP "${state.name}": ${state.error ?? "failed"}`);
6939
7276
  }
@@ -7326,7 +7663,7 @@ async function boot(options) {
7326
7663
  },
7327
7664
  async exportSession(id, destination) {
7328
7665
  await persistQueue.catch(() => void 0);
7329
- const target = destination ? path14.resolve(options.cwd, destination) : path14.join(options.cwd, ".kitcode-exports");
7666
+ const target = destination ? path15.resolve(options.cwd, destination) : path15.join(options.cwd, ".kitcode-exports");
7330
7667
  if (!destination) await mkdir6(target, { recursive: true, mode: 448 });
7331
7668
  return (await exportSession(id, target)).path;
7332
7669
  },
@@ -7553,16 +7890,20 @@ ${lines.join("\n")}` : null;
7553
7890
  hint: prompt.description || void 0
7554
7891
  }));
7555
7892
  },
7556
- async readPrompt(slug) {
7557
- const prompt = await getPrompt(slug);
7893
+ async readPrompt(name) {
7894
+ const prompt = await getPrompt(promptSlug(name));
7558
7895
  return prompt?.body ?? "";
7559
7896
  },
7560
7897
  async savePrompt(name, body) {
7561
- await savePrompt({ name, body });
7898
+ return (await savePrompt({ name, body })).slug;
7899
+ },
7900
+ async promptExists(name) {
7901
+ const slug = promptSlug(name);
7902
+ return slug !== "" && await promptExists(slug);
7562
7903
  },
7563
- async deletePrompt(slug) {
7564
- const ok = await deletePrompt(slug);
7565
- if (!ok) throw new Error(`Prompt "${slug}" not found.`);
7904
+ async deletePrompt(name) {
7905
+ const slug = promptSlug(name);
7906
+ if (!slug || !await deletePrompt(slug)) throw new Error(`Prompt "${name}" not found.`);
7566
7907
  },
7567
7908
  async loadSkill(name) {
7568
7909
  const meta = skills.find((s) => s.name === name);
@@ -7571,15 +7912,25 @@ ${lines.join("\n")}` : null;
7571
7912
  },
7572
7913
  async installSkill(source) {
7573
7914
  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)]);
7915
+ await reloadSkills();
7578
7916
  return result;
7579
7917
  },
7918
+ async uninstallSkill(name) {
7919
+ const meta = skills.find((skill) => skill.name === name);
7920
+ if (!meta) throw new Error(`Skill "${name}" not found.`);
7921
+ if (path15.dirname(meta.dir) !== skillsDir) {
7922
+ throw new Error(`Skill "${name}" belongs to this project: delete ${meta.dir} instead.`);
7923
+ }
7924
+ await uninstallSkill(path15.basename(meta.dir));
7925
+ await reloadSkills();
7926
+ },
7580
7927
  async loadAttachment(requestedPath) {
7581
7928
  return (await loadAttachment(options.cwd, requestedPath)).block;
7582
7929
  },
7930
+ summarizeToolCall: (name, input) => tools.get(name)?.summarize(input) ?? name,
7931
+ async loadMentionAttachment(requestedPath) {
7932
+ return (await loadMentionAttachment(options.cwd, requestedPath))?.block ?? null;
7933
+ },
7583
7934
  async loadAutomaticAttachment(requestedPath) {
7584
7935
  return (await loadAutomaticAttachment(options.cwd, requestedPath))?.block ?? null;
7585
7936
  },
@@ -7697,6 +8048,16 @@ ${lines.join("\n")}` : null;
7697
8048
  });
7698
8049
  }
7699
8050
  return next;
8051
+ } catch (error) {
8052
+ const recovered = signal.aborted ? null : imageRejection(error, history);
8053
+ if (!recovered) throw error;
8054
+ hooks.onEvent({
8055
+ type: "notice",
8056
+ level: "error",
8057
+ text: `${error instanceof Error ? error.message : String(error)}
8058
+ ${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.`
8059
+ });
8060
+ return recovered.messages;
7700
8061
  } finally {
7701
8062
  try {
7702
8063
  await checkpoint.commit();
@@ -7743,6 +8104,14 @@ ${lines.join("\n")}` : null;
7743
8104
  }
7744
8105
  };
7745
8106
  }
8107
+ function imageRejection(error, history) {
8108
+ const status = error?.status;
8109
+ if (status !== void 0 && (typeof status !== "number" || ![400, 413, 415, 422].includes(status))) return null;
8110
+ const message = error instanceof Error ? error.message : String(error);
8111
+ if (status !== 413 && !/image|vision|multi-?modal/i.test(message)) return null;
8112
+ const result = withoutImages(history);
8113
+ return result.removed > 0 ? result : null;
8114
+ }
7746
8115
  function preferredModel(models) {
7747
8116
  for (const preferred of PREFERRED2) {
7748
8117
  const match = models.find((model) => model.id.endsWith(preferred));
@@ -7998,38 +8367,92 @@ function useTerminalSize() {
7998
8367
 
7999
8368
  // src/mcp/add.ts
8000
8369
  var SERVER_NAME = /^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$/;
8370
+ var ENV_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
8371
+ var HEADER_NAME = /^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/;
8372
+ function parseMcpAddCommand(raw) {
8373
+ const args = splitCommandLine(raw);
8374
+ return args ? parseMcpAddArgs(args) : { ok: false, error: "usage" };
8375
+ }
8001
8376
  function parseMcpAddArgs(args) {
8002
- const [name, first2, ...remaining] = args;
8003
- if (!name || !first2) return { ok: false, error: "usage" };
8377
+ const [name, ...tail2] = args;
8378
+ if (!name) return { ok: false, error: "usage" };
8004
8379
  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" };
8380
+ const env = {};
8381
+ const headers = {};
8382
+ let index = 0;
8383
+ for (; index < tail2.length; index += 1) {
8384
+ const option = tail2[index];
8385
+ if (option === "-e" || option === "--env") {
8386
+ const value = tail2[++index];
8387
+ const eq = value?.indexOf("=") ?? -1;
8388
+ if (!value || eq <= 0 || !ENV_NAME.test(value.slice(0, eq))) return { ok: false, error: "invalid-env" };
8389
+ env[value.slice(0, eq)] = value.slice(eq + 1);
8390
+ continue;
8391
+ }
8392
+ if (option === "-H" || option === "--header") {
8393
+ const value = tail2[++index];
8394
+ const colon = value?.indexOf(":") ?? -1;
8395
+ const key = value && colon > 0 ? value.slice(0, colon).trim() : "";
8396
+ if (!value || !HEADER_NAME.test(key)) return { ok: false, error: "invalid-header" };
8397
+ headers[key] = value.slice(colon + 1).trim();
8398
+ continue;
8399
+ }
8400
+ break;
8401
+ }
8402
+ let rest = tail2.slice(index);
8403
+ let kind;
8404
+ if (rest[0] === "http") {
8405
+ kind = "http";
8406
+ rest = rest.slice(1);
8407
+ } else if (rest[0] === "stdio" || rest[0] === "--") {
8408
+ kind = "stdio";
8409
+ rest = rest.slice(1);
8410
+ }
8411
+ const [target, ...extra] = rest;
8412
+ if (!target) return { ok: false, error: "usage" };
8413
+ kind ??= /^https?:\/\//i.test(target) ? "http" : "stdio";
8414
+ if (kind === "http") {
8008
8415
  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);
8416
+ if (Object.keys(env).length > 0) return { ok: false, error: "option-mismatch" };
8417
+ const parsed2 = mcpServerSchema.safeParse({ type: "http", url: target, headers });
8418
+ return parsed2.success ? { ok: true, name, config: parsed2.data } : { ok: false, error: "invalid-url" };
8019
8419
  }
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 });
8420
+ if (Object.keys(headers).length > 0) return { ok: false, error: "option-mismatch" };
8421
+ const parsed = mcpServerSchema.safeParse({ type: "stdio", command: target, args: extra, env });
8028
8422
  return parsed.success ? { ok: true, name, config: parsed.data } : { ok: false, error: "usage" };
8029
8423
  }
8424
+ function splitCommandLine(raw) {
8425
+ const words2 = [];
8426
+ let current = "";
8427
+ let inWord = false;
8428
+ let quote;
8429
+ for (const char of raw) {
8430
+ if (quote) {
8431
+ if (char === quote) quote = void 0;
8432
+ else current += char;
8433
+ continue;
8434
+ }
8435
+ if (char === '"' || char === "'") {
8436
+ quote = char;
8437
+ inWord = true;
8438
+ continue;
8439
+ }
8440
+ if (/\s/.test(char)) {
8441
+ if (inWord) words2.push(current);
8442
+ current = "";
8443
+ inWord = false;
8444
+ continue;
8445
+ }
8446
+ current += char;
8447
+ inWord = true;
8448
+ }
8449
+ if (quote) return null;
8450
+ if (inWord) words2.push(current);
8451
+ return words2;
8452
+ }
8030
8453
 
8031
8454
  // src/ui/speed.ts
8032
- var CHARS_PER_TOKEN = 4;
8455
+ var CHARS_PER_TOKEN3 = 4;
8033
8456
  var MIN_LIVE_MS = 1e3;
8034
8457
  function createSpeedMeter() {
8035
8458
  return { streamedChars: 0, firstTokenAt: null, last: null };
@@ -8056,7 +8479,7 @@ function readSpeed(meter, busy, now = Date.now()) {
8056
8479
  if (busy && meter.firstTokenAt !== null && meter.streamedChars > 0) {
8057
8480
  const ms = now - meter.firstTokenAt;
8058
8481
  if (ms >= MIN_LIVE_MS) {
8059
- return { tokensPerSecond: meter.streamedChars / CHARS_PER_TOKEN / (ms / 1e3), live: true };
8482
+ return { tokensPerSecond: meter.streamedChars / CHARS_PER_TOKEN3 / (ms / 1e3), live: true };
8060
8483
  }
8061
8484
  }
8062
8485
  return meter.last === null || !Number.isFinite(meter.last) ? null : { tokensPerSecond: meter.last, live: false };
@@ -8065,21 +8488,21 @@ function readSpeed(meter, busy, now = Date.now()) {
8065
8488
  // src/ui/commands.ts
8066
8489
  var BUSY_SAFE = { runsWhileBusy: true };
8067
8490
  var COMMANDS = [
8068
- { name: "model", ...BUSY_SAFE },
8491
+ { name: "model", args: "[model]", ...BUSY_SAFE },
8069
8492
  { name: "login" },
8070
8493
  { name: "provider" },
8071
8494
  { name: "logout", args: "[provider]" },
8072
8495
  { name: "key", args: "[provider]" },
8073
8496
  { name: "refresh", args: "[provider]" },
8074
8497
  { name: "effort", ...BUSY_SAFE },
8075
- { name: "thinking", ...BUSY_SAFE },
8498
+ { name: "thinking", args: "[on|off]", ...BUSY_SAFE },
8076
8499
  { name: "bell", args: "[on|off]", ...BUSY_SAFE },
8077
8500
  { name: "context", args: "[tokens|auto]", ...BUSY_SAFE },
8078
8501
  { name: "budget", args: "[tokens]", ...BUSY_SAFE },
8079
8502
  { 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>]" },
8503
+ { name: "lang", args: "[en|ru]", ...BUSY_SAFE },
8504
+ { name: "prompt", args: "[name|save [name]|delete [name]]" },
8505
+ { name: "skills", args: "[install <source>|remove [name]]" },
8083
8506
  { name: "bypass" },
8084
8507
  { name: "usage", ...BUSY_SAFE },
8085
8508
  { name: "mcp", args: "[add|list|delete|enable|disable]" },
@@ -8098,7 +8521,9 @@ var COMMANDS = [
8098
8521
  { name: "exit", ...BUSY_SAFE }
8099
8522
  ];
8100
8523
  var SUBCOMMANDS = [
8101
- { name: "mcp add", args: "<name> <url|-- command>" },
8524
+ { name: "skills install", args: "<github-url|npm-package|path>" },
8525
+ { name: "skills remove", args: "[name]" },
8526
+ { name: "mcp add", args: '<name> [-e K=V] [-H "N: V"] <url|-- command>' },
8102
8527
  { name: "mcp list" },
8103
8528
  { name: "mcp delete", args: "[name]" },
8104
8529
  { name: "mcp enable", args: "[name]" },
@@ -8188,10 +8613,10 @@ function mentionSpan(value, cursor) {
8188
8613
  while (end < value.length && !/[\s@]/.test(value[end] ?? "")) end += 1;
8189
8614
  return { start, end, query };
8190
8615
  }
8191
- function completeMention(value, span, path15) {
8616
+ function completeMention(value, span, path16) {
8192
8617
  const rest = value.slice(span.end);
8193
8618
  const spaced = rest.startsWith(" ");
8194
- const inserted = `@${path15}${spaced ? "" : " "}`;
8619
+ const inserted = `@${path16}${spaced ? "" : " "}`;
8195
8620
  return {
8196
8621
  value: value.slice(0, span.start) + inserted + rest,
8197
8622
  cursor: span.start + inserted.length + (spaced ? 1 : 0)
@@ -8200,8 +8625,8 @@ function completeMention(value, span, path15) {
8200
8625
  function extractMentions(text) {
8201
8626
  const found = [];
8202
8627
  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);
8628
+ const path16 = (match[1] ?? "").replace(TRAILING_PUNCTUATION, "");
8629
+ if (path16 !== "" && !found.includes(path16)) found.push(path16);
8205
8630
  }
8206
8631
  return found;
8207
8632
  }
@@ -8315,7 +8740,7 @@ var en = {
8315
8740
  pickerHelp: (shown, total) => `${shown} of ${total} \xB7 \u2191\u2193 move \xB7 enter pick \xB7 esc cancel`,
8316
8741
  noModel: "no model",
8317
8742
  busy: "working",
8318
- sessionFor: (elapsed) => `up ${elapsed}`,
8743
+ sessionFor: (elapsed) => `session ${elapsed}`,
8319
8744
  workingFor: (elapsed) => `working ${elapsed}`,
8320
8745
  titleProvider: "Provider",
8321
8746
  titleLogout: "Sign out of a provider",
@@ -8340,17 +8765,22 @@ var en = {
8340
8765
  pasteToken: (id, lines, chars) => `[paste #${id} \xB7 ${lines > 1 ? `${lines} lines` : `${chars} chars`}]`,
8341
8766
  inputRows: (from, to, total) => `lines ${from}\u2013${to} of ${total}`,
8342
8767
  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}`,
8768
+ configAt: (path16) => `config: ${path16}`,
8344
8769
  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>",
8770
+ skillsInstallUsage: "Usage: /skills install <github-url|npm-package|local-path> \xB7 /skills remove [name]",
8346
8771
  skillsInstalling: (source) => `Installing skill from "${source}"\u2026`,
8347
8772
  skillsInstalled: (name, source) => `Skill "${name}" installed from "${source}"`,
8773
+ skillsRemoved: (name) => `Skill "${name}" removed`,
8774
+ titleSkillsRemove: "Remove skill",
8348
8775
  mcpStatus: (connected, failed) => `MCP: ${connected} connected, ${failed} failed`,
8349
8776
  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]",
8777
+ 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
8778
  mcpAddInvalidName: "MCP name may contain only letters, numbers, _ and - (up to 64 chars).",
8352
8779
  mcpAddInvalidUrl: "MCP URL must use HTTPS (HTTP is allowed only for localhost).",
8353
8780
  mcpAddHttpArgs: "An HTTP MCP accepts one URL and no command arguments.",
8781
+ mcpAddInvalidEnv: "Environment variables look like -e KEY=value.",
8782
+ mcpAddInvalidHeader: 'Headers look like -H "Name: value".',
8783
+ mcpAddOptionMismatch: "-e is for local (command) servers, -H is for remote (URL) servers.",
8354
8784
  mcpExists: (name) => `MCP server "${name}" already exists.`,
8355
8785
  mcpConnecting: (name) => `Connecting MCP "${name}"\u2026`,
8356
8786
  mcpAdded: (name, tools) => `MCP "${name}" connected and saved \xB7 ${tools} tools`,
@@ -8378,6 +8808,9 @@ var en = {
8378
8808
  attachUsage: "Usage: /attach <image-or-text-file> \xB7 /attach clipboard \xB7 /attach clear",
8379
8809
  attachmentAdded: (name) => `Attached for the next message: ${name}`,
8380
8810
  attachmentsCleared: "Pending attachments cleared.",
8811
+ attachmentsLimit: (max) => `At most ${max} attachments can be queued for one message.`,
8812
+ attachmentsTooLarge: (mb) => `Attachments for one message are limited to about ${mb} MB in total. Send these first or use smaller files.`,
8813
+ mentionAttached: (name) => `Sending with this message: ${name}`,
8381
8814
  compactNothing: "Not enough older conversation to compact yet.",
8382
8815
  compactStarting: "Compacting context\u2026",
8383
8816
  compactDone: (summarised, cleared, before, after) => [
@@ -8386,7 +8819,7 @@ var en = {
8386
8819
  cleared > 0 ? `${cleared} old tool outputs cleared` : "",
8387
8820
  before !== void 0 && after !== void 0 ? `~${tokens(before)} \u2192 ~${tokens(after)} tokens` : ""
8388
8821
  ].filter(Boolean).join(" \xB7 ") + ".",
8389
- sessionRenameUsage: "Type the title after the command: /sessions rename <id> <title>",
8822
+ sessionRenameUsage: "Type the title after the command: /sessions rename [id] <title>",
8390
8823
  sessionRenamed: (title) => `Session renamed to "${title}".`,
8391
8824
  sessionDeleteAsk: "The saved chat file will be deleted. This cannot be undone.",
8392
8825
  sessionDeleted: (id) => `Session ${id} deleted.`,
@@ -8396,7 +8829,7 @@ var en = {
8396
8829
  sessionDeleteAllFinalBody: "This cannot be undone. Press y only if you want to erase the complete session history.",
8397
8830
  sessionsDeletedAll: (count2) => `All saved chats deleted \xB7 ${count2} sessions removed.`,
8398
8831
  sessionsDeleteAllFailed: (deleted, failed) => `Session cleanup was incomplete \xB7 ${deleted} removed, ${failed} failed.`,
8399
- sessionExported: (path15) => `Session exported to ${path15}`,
8832
+ sessionExported: (path16) => `Session exported to ${path16}`,
8400
8833
  sessionActionResume: "resume",
8401
8834
  sessionActionRename: "rename",
8402
8835
  sessionActionDelete: "delete",
@@ -8437,11 +8870,19 @@ ${url}`,
8437
8870
  promptUsage: "Usage: /prompt save <name>",
8438
8871
  promptNothing: "Nothing to save yet \u2014 send a message first.",
8439
8872
  promptSaved: (name) => `Saved prompt "${name}"`,
8440
- promptDeleteUsage: "Usage: /prompt delete <name>",
8873
+ promptDeleteAsk: (name) => `Delete prompt "${name}"?`,
8874
+ promptOverwriteAsk: (name) => `A prompt named "${name}" exists. Replace it?`,
8875
+ promptNameTitle: "Prompt name",
8876
+ sessionNoMatch: (query) => `No saved session matches "${query}".`,
8877
+ sessionAmbiguous: (query, count2) => `"${query}" matches ${count2} sessions; give more of the id.`,
8878
+ sessionsUsage: "Usage: /sessions [list|rename [id] <title>|delete [id|all]|export [id] [path]]",
8879
+ modelNoMatch: (query) => `No single model matches "${query}"; pick one from the list.`,
8880
+ memoryUsage: "/memory show \xB7 /memory set <text> \xB7 /memory clear",
8881
+ memoryEmpty: "Project memory is empty.",
8882
+ memoryClearAsk: "Clear project memory?",
8883
+ memoryReplaceAsk: "Replace all project memory notes with this text?",
8441
8884
  promptDeleted: (name) => `Deleted prompt "${name}"`,
8442
8885
  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
8886
  promptsEmpty: "No saved prompts. Use /prompt save <name>.",
8446
8887
  unknownCommand: (name) => `Unknown command /${name}. Try /help`,
8447
8888
  didYouMean: (typed, guess) => `Unknown command /${typed}. Did you mean /${guess}?`,
@@ -8536,7 +8977,7 @@ var ru = {
8536
8977
  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
8978
  noModel: "\u043D\u0435\u0442 \u043C\u043E\u0434\u0435\u043B\u0438",
8538
8979
  busy: "\u0440\u0430\u0431\u043E\u0442\u0430\u044E",
8539
- sessionFor: (elapsed) => `\u0432 \u0440\u0430\u0431\u043E\u0442\u0435 ${elapsed}`,
8980
+ sessionFor: (elapsed) => `\u0441\u0435\u0441\u0441\u0438\u044F ${elapsed}`,
8540
8981
  workingFor: (elapsed) => `\u0434\u0443\u043C\u0430\u0435\u0442 ${elapsed}`,
8541
8982
  titleProvider: "\u041F\u0440\u043E\u0432\u0430\u0439\u0434\u0435\u0440",
8542
8983
  titleLogout: "\u0412\u044B\u0431\u0440\u0430\u0442\u044C API \u0434\u043B\u044F \u0432\u044B\u0445\u043E\u0434\u0430",
@@ -8561,17 +9002,22 @@ var ru = {
8561
9002
  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
9003
  inputRows: (from, to, total) => `\u0441\u0442\u0440\u043E\u043A\u0438 ${from}\u2013${to} \u0438\u0437 ${total}`,
8563
9004
  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}`,
9005
+ configAt: (path16) => `\u043A\u043E\u043D\u0444\u0438\u0433: ${path16}`,
8565
9006
  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>",
9007
+ 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
9008
  skillsInstalling: (source) => `\u0423\u0441\u0442\u0430\u043D\u0430\u0432\u043B\u0438\u0432\u0430\u044E \u0441\u043A\u0438\u043B\u043B \u0438\u0437 "${source}"\u2026`,
8568
9009
  skillsInstalled: (name, source) => `\u0421\u043A\u0438\u043B\u043B \xAB${name}\xBB \u0443\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D \u0438\u0437 "${source}"`,
9010
+ skillsRemoved: (name) => `\u0421\u043A\u0438\u043B\u043B \xAB${name}\xBB \u0443\u0434\u0430\u043B\u0451\u043D`,
9011
+ titleSkillsRemove: "\u0423\u0434\u0430\u043B\u0438\u0442\u044C \u0441\u043A\u0438\u043B\u043B",
8569
9012
  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
9013
  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]",
9014
+ 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
9015
  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
9016
  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
9017
  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.",
9018
+ 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.",
9019
+ 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".',
9020
+ 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
9021
  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
9022
  mcpConnecting: (name) => `\u041F\u043E\u0434\u043A\u043B\u044E\u0447\u0430\u044E MCP \xAB${name}\xBB\u2026`,
8577
9023
  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 +9045,9 @@ var ru = {
8599
9045
  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
9046
  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
9047
  attachmentsCleared: "\u041E\u0436\u0438\u0434\u0430\u044E\u0449\u0438\u0435 \u0432\u043B\u043E\u0436\u0435\u043D\u0438\u044F \u0443\u0431\u0440\u0430\u043D\u044B.",
9048
+ 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.`,
9049
+ 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.`,
9050
+ 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
9051
  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
9052
  compactStarting: "\u0421\u0436\u0430\u0442\u0438\u0435 \u043A\u043E\u043D\u0442\u0435\u043A\u0441\u0442\u0430\u2026",
8604
9053
  compactDone: (summarised, cleared, before, after) => [
@@ -8607,7 +9056,7 @@ var ru = {
8607
9056
  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
9057
  before !== void 0 && after !== void 0 ? `~${tokens(before)} \u2192 ~${tokens(after)} \u0442\u043E\u043A\u0435\u043D\u043E\u0432` : ""
8609
9058
  ].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>",
9059
+ 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
9060
  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
9061
  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
9062
  sessionDeleted: (id) => `\u0421\u0435\u0441\u0441\u0438\u044F ${id} \u0443\u0434\u0430\u043B\u0435\u043D\u0430.`,
@@ -8617,7 +9066,7 @@ var ru = {
8617
9066
  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
9067
  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
9068
  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}`,
9069
+ sessionExported: (path16) => `\u0421\u0435\u0441\u0441\u0438\u044F \u044D\u043A\u0441\u043F\u043E\u0440\u0442\u0438\u0440\u043E\u0432\u0430\u043D\u0430: ${path16}`,
8621
9070
  sessionActionResume: "\u043E\u0442\u043A\u0440\u044B\u0442\u044C",
8622
9071
  sessionActionRename: "\u043F\u0435\u0440\u0435\u0438\u043C\u0435\u043D\u043E\u0432\u0430\u0442\u044C",
8623
9072
  sessionActionDelete: "\u0443\u0434\u0430\u043B\u0438\u0442\u044C",
@@ -8658,11 +9107,19 @@ ${url}`,
8658
9107
  promptUsage: "\u0418\u0441\u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u043D\u0438\u0435: /prompt save <\u0438\u043C\u044F>",
8659
9108
  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
9109
  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>",
9110
+ promptDeleteAsk: (name) => `\u0423\u0434\u0430\u043B\u0438\u0442\u044C \u043F\u0440\u043E\u043C\u0442 \xAB${name}\xBB?`,
9111
+ 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?`,
9112
+ promptNameTitle: "\u041D\u0430\u0437\u0432\u0430\u043D\u0438\u0435 \u043F\u0440\u043E\u043C\u0442\u0430",
9113
+ 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.`,
9114
+ 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.`,
9115
+ 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]]",
9116
+ 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.`,
9117
+ memoryUsage: "/memory show \xB7 /memory set <\u0442\u0435\u043A\u0441\u0442> \xB7 /memory clear",
9118
+ memoryEmpty: "\u041F\u0430\u043C\u044F\u0442\u044C \u043F\u0440\u043E\u0435\u043A\u0442\u0430 \u043F\u0443\u0441\u0442\u0430.",
9119
+ memoryClearAsk: "\u041E\u0447\u0438\u0441\u0442\u0438\u0442\u044C \u043F\u0430\u043C\u044F\u0442\u044C \u043F\u0440\u043E\u0435\u043A\u0442\u0430?",
9120
+ 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
9121
  promptDeleted: (name) => `\u041F\u0440\u043E\u043C\u0442 \xAB${name}\xBB \u0443\u0434\u0430\u043B\u0451\u043D`,
8663
9122
  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
9123
  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
9124
  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
9125
  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 +10268,10 @@ function StatusBar({ status }) {
9811
10268
  flexDirection: "column",
9812
10269
  children: [
9813
10270
  /* @__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: [
10271
+ /* @__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
10272
  index > 0 && /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: " \xB7 " }),
9816
10273
  item
9817
- ] }, index)) }),
10274
+ ] }) }, index)) }),
9818
10275
  /* @__PURE__ */ jsx9(Box9, { flexShrink: 0, marginLeft: 2, children: /* @__PURE__ */ jsx9(ContextMeter, { context: status.context }) })
9819
10276
  ] }),
9820
10277
  status.agents.length > 0 && /* @__PURE__ */ jsxs9(Box9, { width: "100%", flexDirection: "column", alignItems: "flex-end", children: [
@@ -9859,6 +10316,8 @@ function ContextMeter({
9859
10316
  context
9860
10317
  }) {
9861
10318
  const theme = useTheme();
10319
+ const { columns } = useTerminalSize();
10320
+ const compact = columns < 80;
9862
10321
  const { window: win, exact } = context;
9863
10322
  const used = exact ? context.used : 0;
9864
10323
  if (win === null || win <= 0) {
@@ -9871,6 +10330,12 @@ function ContextMeter({
9871
10330
  const filled = Math.max(0, Math.min(BAR_WIDTH, Math.round(BAR_WIDTH * pct)));
9872
10331
  const color = pct < 0.6 ? theme.ok : pct < 0.85 ? theme.warn : theme.error;
9873
10332
  const percent = Math.round(pct * 100);
10333
+ if (compact) {
10334
+ return /* @__PURE__ */ jsxs9(Text9, { children: [
10335
+ /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: "ctx " }),
10336
+ /* @__PURE__ */ jsx9(Text9, { color, bold: true, children: `${percent}%` })
10337
+ ] });
10338
+ }
9874
10339
  return /* @__PURE__ */ jsxs9(Text9, { children: [
9875
10340
  /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: "ctx " }),
9876
10341
  /* @__PURE__ */ jsx9(Text9, { color, bold: true, children: `${percent}%`.padStart(4) }),
@@ -10744,8 +11209,7 @@ var BubbleView = memo2(function BubbleView2({
10744
11209
  ] }) });
10745
11210
  }
10746
11211
  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 }) });
11212
+ return /* @__PURE__ */ jsx11(NoticeView, { level: bubble.level, text: bubble.text });
10749
11213
  }
10750
11214
  if (bubble.kind === "assistant") {
10751
11215
  return /* @__PURE__ */ jsx11(AssistantView, { bubble, maxRows });
@@ -10819,6 +11283,24 @@ function wrapVisualLine(line, columns) {
10819
11283
  if (current !== "" || rows.length === 0) rows.push(current);
10820
11284
  return rows;
10821
11285
  }
11286
+ var NOTICE_ICONS = { warn: "\u25B2", error: "\u2717" };
11287
+ function NoticeView({ level, text }) {
11288
+ const theme = useTheme();
11289
+ const newline = text.indexOf("\n");
11290
+ const headline = newline === -1 ? text : text.slice(0, newline);
11291
+ const details = newline === -1 ? "" : text.slice(newline + 1);
11292
+ if (level === "info") {
11293
+ return /* @__PURE__ */ jsx11(Box11, { marginTop: 1, paddingLeft: 2, children: /* @__PURE__ */ jsx11(Text11, { dimColor: details === "", children: text }) });
11294
+ }
11295
+ const color = level === "error" ? theme.error : theme.warn;
11296
+ return /* @__PURE__ */ jsxs11(Box11, { marginTop: 1, children: [
11297
+ /* @__PURE__ */ jsx11(Box11, { width: 2, flexShrink: 0, children: /* @__PURE__ */ jsx11(Text11, { color, bold: true, children: NOTICE_ICONS[level] }) }),
11298
+ /* @__PURE__ */ jsxs11(Box11, { flexDirection: "column", flexShrink: 1, minWidth: 0, children: [
11299
+ /* @__PURE__ */ jsx11(Text11, { color, bold: level === "error", children: headline }),
11300
+ details !== "" && /* @__PURE__ */ jsx11(Text11, { children: details })
11301
+ ] })
11302
+ ] });
11303
+ }
10822
11304
  function ToolView({ bubble }) {
10823
11305
  const theme = useTheme();
10824
11306
  const mark = bubble.state === "running" ? "\u25CC" : bubble.state === "ok" ? "\u25CF" : "\u2717";
@@ -11155,7 +11637,7 @@ function closeAssistant(bubbles) {
11155
11637
  (bubble) => bubble.kind === "assistant" && bubble.streaming ? { ...bubble, streaming: false } : bubble
11156
11638
  );
11157
11639
  }
11158
- function fromHistory(messages) {
11640
+ function fromHistory(messages, summarize = (name) => name) {
11159
11641
  let state = emptyTranscript();
11160
11642
  for (const message of messages) {
11161
11643
  if (message.role === "user") {
@@ -11186,7 +11668,7 @@ function fromHistory(messages) {
11186
11668
  kind: "tool",
11187
11669
  id: block.id,
11188
11670
  name: sanitizeTerminalText(block.name),
11189
- summary: sanitizeTerminalText(block.name),
11671
+ summary: sanitizeTerminalText(safeSummary(summarize, block.name, block.input)),
11190
11672
  state: "ok",
11191
11673
  content: ""
11192
11674
  }
@@ -11249,6 +11731,13 @@ function sanitizeDisplay(display) {
11249
11731
  after: sanitizeTerminalText(display.after)
11250
11732
  };
11251
11733
  }
11734
+ function safeSummary(summarize, name, input) {
11735
+ try {
11736
+ return summarize(name, input) || name;
11737
+ } catch {
11738
+ return name;
11739
+ }
11740
+ }
11252
11741
 
11253
11742
  // src/ui/App.tsx
11254
11743
  import { jsx as jsx14, jsxs as jsxs13 } from "react/jsx-runtime";
@@ -11258,6 +11747,7 @@ var CUSTOM_ACCENT = "__custom__";
11258
11747
  var BELL_AFTER_MS = 15e3;
11259
11748
  var SUBAGENT_CHAT_EXTRA_ROWS = 4;
11260
11749
  var MAX_ATTACHMENTS = 8;
11750
+ var MAX_ATTACHMENT_PAYLOAD = 24e6;
11261
11751
  var CONTEXT_PRESETS = [256e3, 512e3, 1e6];
11262
11752
  function formatContextTokens(tokens2) {
11263
11753
  if (tokens2 >= 1e6) return `${tokens2 / 1e6}M`;
@@ -11279,7 +11769,7 @@ function App({
11279
11769
  const { stdout } = useStdout2();
11280
11770
  const { rows } = useTerminalSize();
11281
11771
  const [transcript, setTranscript] = useState8(
11282
- () => warnings.reduce((state, text) => pushNotice(state, "warn", text), fromHistory(initialHistory))
11772
+ () => warnings.reduce((state, text) => pushNotice(state, "warn", text), fromHistory(initialHistory, runtime.summarizeToolCall))
11283
11773
  );
11284
11774
  const [transcriptRevision, setTranscriptRevision] = useState8(0);
11285
11775
  const transcriptRef = useRef5(transcript);
@@ -11334,11 +11824,16 @@ function App({
11334
11824
  }, []);
11335
11825
  const appendAttachment = useCallback((block) => {
11336
11826
  if (attachmentsRef.current.length >= MAX_ATTACHMENTS) return false;
11827
+ if (attachmentsRef.current.some((queued) => sameAttachment(queued, block))) return true;
11828
+ const payload = attachmentsRef.current.reduce((sum, queued) => sum + attachmentPayload(queued), 0);
11829
+ if (payload + attachmentPayload(block) > MAX_ATTACHMENT_PAYLOAD) {
11830
+ throw new Error(stringsFor(lang).attachmentsTooLarge(Math.round(MAX_ATTACHMENT_PAYLOAD / 1e6)));
11831
+ }
11337
11832
  const next = [...attachmentsRef.current, block];
11338
11833
  attachmentsRef.current = next;
11339
11834
  setAttachments(next);
11340
11835
  return true;
11341
- }, []);
11836
+ }, [lang]);
11342
11837
  const flushTranscriptEvents = useCallback(() => {
11343
11838
  if (transcriptTimer.current !== null) {
11344
11839
  clearTimeout(transcriptTimer.current);
@@ -11542,10 +12037,11 @@ function App({
11542
12037
  try {
11543
12038
  const restored = await runtime.resumeSession(choice);
11544
12039
  history.current = restored;
12040
+ replaceAttachments([]);
11545
12041
  setPromptHistory(inputHistoryFromMessages(restored));
11546
12042
  setTranscript(
11547
12043
  pushNotice(
11548
- fromHistory(restored),
12044
+ fromHistory(restored, runtime.summarizeToolCall),
11549
12045
  "info",
11550
12046
  strings.resumed(choice.slice(-6), restored.length)
11551
12047
  )
@@ -11609,12 +12105,18 @@ function App({
11609
12105
  forceRender((n) => n + 1);
11610
12106
  return;
11611
12107
  }
12108
+ const matching = (query) => items.filter((item) => item.key === query || item.key.includes(query));
11612
12109
  let id = action ? rest[1] : void 0;
12110
+ let argsFrom = 2;
12111
+ if (id && (action === "rename" || action === "export") && matching(id).length === 0) {
12112
+ id = void 0;
12113
+ argsFrom = 1;
12114
+ }
11613
12115
  if (!id) id = await pick(strings.titleSessions, items) ?? void 0;
11614
12116
  if (!id) return;
11615
- const matches = items.filter((item) => item.key === id || item.key.includes(id));
12117
+ const matches = matching(id);
11616
12118
  if (matches.length !== 1) {
11617
- notice("warn", `No saved session matches "${id}".`);
12119
+ notice("warn", matches.length === 0 ? strings.sessionNoMatch(id) : strings.sessionAmbiguous(id, matches.length));
11618
12120
  return;
11619
12121
  }
11620
12122
  id = matches[0]?.key ?? id;
@@ -11634,13 +12136,13 @@ function App({
11634
12136
  replaceAttachments([]);
11635
12137
  setPromptHistory(inputHistoryFromMessages(restored));
11636
12138
  setTranscript(
11637
- pushNotice(fromHistory(restored), "info", strings.resumed(id.slice(-6), restored.length))
12139
+ pushNotice(fromHistory(restored, runtime.summarizeToolCall), "info", strings.resumed(id.slice(-6), restored.length))
11638
12140
  );
11639
12141
  setTranscriptRevision((revision) => revision + 1);
11640
12142
  sessionStart.current = Date.now();
11641
12143
  turns.current = 0;
11642
12144
  } else if (action === "rename") {
11643
- const title = rest.slice(2).join(" ").trim();
12145
+ const title = rest.slice(argsFrom).join(" ").trim();
11644
12146
  if (!title) {
11645
12147
  setInput(`/sessions rename ${id} `);
11646
12148
  notice("info", strings.sessionRenameUsage);
@@ -11668,10 +12170,10 @@ function App({
11668
12170
  notice("info", deletedMessage);
11669
12171
  }
11670
12172
  } else if (action === "export") {
11671
- const destination = unquoteArg(rest.slice(2).join(" ").trim()) || void 0;
12173
+ const destination = unquoteArg(rest.slice(argsFrom).join(" ").trim()) || void 0;
11672
12174
  notice("info", strings.sessionExported(await runtime.exportSession(id, destination)));
11673
12175
  } else {
11674
- notice("warn", "Usage: /sessions [list|rename|delete [all]|export]");
12176
+ notice("warn", strings.sessionsUsage);
11675
12177
  return;
11676
12178
  }
11677
12179
  } catch (error) {
@@ -11823,7 +12325,8 @@ ${strings.modelSet(result.nextModel)}` : ""}`;
11823
12325
  return;
11824
12326
  }
11825
12327
  case "lang": {
11826
- const choice = await pick(
12328
+ const typed = rawRest.toLowerCase();
12329
+ const choice = LANGS.some((entry) => entry.key === typed) ? typed : await pick(
11827
12330
  strings.titleLang,
11828
12331
  LANGS.map((entry) => ({ key: entry.key, label: entry.label }))
11829
12332
  );
@@ -11850,6 +12353,33 @@ ${strings.modelSet(result.nextModel)}` : ""}`;
11850
12353
  }
11851
12354
  return;
11852
12355
  }
12356
+ if (action === "remove" || action === "uninstall") {
12357
+ let name = rest[1];
12358
+ if (!name) {
12359
+ const installed = runtime.listSkills();
12360
+ if (installed.length === 0) {
12361
+ notice("info", strings.skillsEmpty);
12362
+ return;
12363
+ }
12364
+ name = await pick(
12365
+ strings.titleSkillsRemove,
12366
+ installed.map((skill) => ({ key: skill.name, label: skill.name, hint: skill.description || void 0 }))
12367
+ ) ?? void 0;
12368
+ if (!name) return;
12369
+ }
12370
+ try {
12371
+ await runtime.uninstallSkill(name);
12372
+ notice("info", strings.skillsRemoved(name));
12373
+ forceRender((n) => n + 1);
12374
+ } catch (error) {
12375
+ notice("error", error instanceof Error ? error.message : String(error));
12376
+ }
12377
+ return;
12378
+ }
12379
+ if (action) {
12380
+ notice("warn", strings.skillsInstallUsage);
12381
+ return;
12382
+ }
11853
12383
  const skills = runtime.listSkills();
11854
12384
  if (skills.length === 0) {
11855
12385
  notice("info", strings.skillsEmpty);
@@ -11915,7 +12445,7 @@ ${body2}`));
11915
12445
  case "mcp": {
11916
12446
  const action = rest[0]?.toLowerCase();
11917
12447
  if (action === "add") {
11918
- const parsed = parseMcpAddArgs(rest.slice(1));
12448
+ const parsed = parseMcpAddCommand(rawRest.replace(/^\S+\s*/, ""));
11919
12449
  if (!parsed.ok) {
11920
12450
  const problem = mcpAddProblem(strings, parsed.error);
11921
12451
  notice(
@@ -12058,7 +12588,7 @@ ${strings.mcpAddUsage}`
12058
12588
  }
12059
12589
  if (rawRest.toLowerCase() === "clipboard") {
12060
12590
  if (attachmentsRef.current.length >= MAX_ATTACHMENTS) {
12061
- notice("warn", `At most ${MAX_ATTACHMENTS} attachments can be queued for one message.`);
12591
+ notice("warn", strings.attachmentsLimit(MAX_ATTACHMENTS));
12062
12592
  return;
12063
12593
  }
12064
12594
  if (clipboardPasteTask.current) return;
@@ -12088,7 +12618,7 @@ ${strings.mcpAddUsage}`
12088
12618
  return;
12089
12619
  }
12090
12620
  if (attachmentsRef.current.length >= MAX_ATTACHMENTS) {
12091
- notice("warn", `At most ${MAX_ATTACHMENTS} attachments can be queued for one message.`);
12621
+ notice("warn", strings.attachmentsLimit(MAX_ATTACHMENTS));
12092
12622
  return;
12093
12623
  }
12094
12624
  try {
@@ -12116,7 +12646,7 @@ ${strings.mcpAddUsage}`
12116
12646
  history.current = result.history;
12117
12647
  setPromptHistory(inputHistoryFromMessages(result.history));
12118
12648
  setTranscript(
12119
- pushNotice(fromHistory(result.history), "info", strings.compactDone(result.removedMessages, result.clearedToolResults ?? 0, result.tokensBefore, result.tokensAfter))
12649
+ pushNotice(fromHistory(result.history, runtime.summarizeToolCall), "info", strings.compactDone(result.removedMessages, result.clearedToolResults ?? 0, result.tokensBefore, result.tokensAfter))
12120
12650
  );
12121
12651
  setTranscriptRevision((revision) => revision + 1);
12122
12652
  await runtime.persist(result.history);
@@ -12185,28 +12715,47 @@ ${strings.mcpAddUsage}`
12185
12715
  return;
12186
12716
  }
12187
12717
  case "bell": {
12188
- const next = rawRest === "on" ? true : rawRest === "off" ? false : !runtime.getBell();
12718
+ const setting = rawRest.toLowerCase();
12719
+ if (setting !== "" && setting !== "on" && setting !== "off") {
12720
+ notice("warn", "/bell [on|off]");
12721
+ return;
12722
+ }
12723
+ const next = setting === "on" ? true : setting === "off" ? false : !runtime.getBell();
12189
12724
  await runtime.setBell(next);
12190
12725
  notice("info", strings.bellState(next));
12191
12726
  return;
12192
12727
  }
12193
12728
  case "thinking": {
12194
- const next = !runtime.getThinking();
12729
+ const setting = rawRest.toLowerCase();
12730
+ if (setting !== "" && setting !== "on" && setting !== "off") {
12731
+ notice("warn", "/thinking [on|off]");
12732
+ return;
12733
+ }
12734
+ const next = setting === "" ? !runtime.getThinking() : setting === "on";
12195
12735
  await runtime.setThinking(next);
12196
12736
  notice("info", strings.reasoning(next));
12197
12737
  forceRender((n) => n + 1);
12198
12738
  return;
12199
12739
  }
12200
12740
  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");
12741
+ const sub = rawRest.split(/\s+/)[0] ?? "";
12742
+ const action = sub.toLowerCase();
12743
+ if (action === "clear") {
12744
+ if (!await ask2(strings.memoryClearAsk, void 0, true)) return;
12745
+ await runtime.clearMemory();
12746
+ } else if (action === "set") {
12747
+ const text = rawRest.slice(sub.length).trim();
12748
+ if (!text) {
12749
+ notice("warn", strings.memoryUsage);
12750
+ return;
12751
+ }
12752
+ if (runtime.readMemory() && !await ask2(strings.memoryReplaceAsk, void 0, true)) return;
12753
+ await runtime.saveMemory(text);
12754
+ } else if (action !== "" && action !== "show") {
12755
+ notice("warn", strings.memoryUsage);
12207
12756
  return;
12208
12757
  }
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."));
12758
+ notice("info", runtime.readMemory() || strings.memoryEmpty);
12210
12759
  return;
12211
12760
  }
12212
12761
  case "effort": {
@@ -12224,7 +12773,7 @@ ${strings.mcpAddUsage}`
12224
12773
  case "budget": {
12225
12774
  const current = runtime.getMaxTokensPerTurn();
12226
12775
  if (rest[0]) {
12227
- const tokens2 = Number(rest[0]);
12776
+ const tokens2 = rest[0] === "0" ? 0 : parseContextTokens(rest[0]) ?? Number.NaN;
12228
12777
  if (tokens2 === 0) {
12229
12778
  await runtime.setMaxTokensPerTurn(0);
12230
12779
  notice("info", strings.budgetUnlimited);
@@ -12298,7 +12847,13 @@ ${strings.mcpAddUsage}`
12298
12847
  notice("warn", strings.noModels);
12299
12848
  return;
12300
12849
  }
12301
- const choice = await pick(strings.titleModel, items);
12850
+ const typed = rawRest.toLowerCase();
12851
+ const direct = typed ? items.find((item) => item.key.toLowerCase() === typed) ?? (() => {
12852
+ const partial = items.filter((item) => item.key.toLowerCase().includes(typed));
12853
+ return partial.length === 1 ? partial[0] : void 0;
12854
+ })() : void 0;
12855
+ if (typed && !direct) notice("warn", strings.modelNoMatch(rawRest));
12856
+ const choice = direct?.key ?? await pick(strings.titleModel, items);
12302
12857
  if (!choice) return;
12303
12858
  await runtime.setModelRef(choice);
12304
12859
  notice("info", strings.modelSet(choice));
@@ -12327,74 +12882,57 @@ ${strings.mcpAddUsage}`
12327
12882
  }
12328
12883
  case "prompt": {
12329
12884
  const sub = rest[0]?.toLowerCase();
12885
+ const savePromptAs = async (name, body2) => {
12886
+ if (await runtime.promptExists(name) && !await ask2(strings.promptOverwriteAsk(name), void 0, true)) {
12887
+ return;
12888
+ }
12889
+ await runtime.savePrompt(name, body2);
12890
+ notice("info", strings.promptSaved(name));
12891
+ };
12330
12892
  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));
12893
+ const recent = recentUserMessages(history.current, 20).reverse();
12894
+ if (recent.length === 0) {
12895
+ notice("warn", strings.promptNothing);
12340
12896
  return;
12341
12897
  }
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.`);
12898
+ const typedName = rest.slice(1).join(" ").trim();
12899
+ if (typedName) {
12900
+ await savePromptAs(typedName, lastUserText(history.current));
12368
12901
  return;
12369
12902
  }
12370
- notice("warn", strings.promptNothing);
12903
+ const choice2 = await pick(
12904
+ strings.titlePrompts,
12905
+ recent.map((text, index) => ({
12906
+ key: String(index),
12907
+ label: text.length > 60 ? `${text.slice(0, 60)}\u2026` : text
12908
+ }))
12909
+ );
12910
+ if (!choice2) return;
12911
+ const body2 = recent[Number(choice2)];
12912
+ if (!body2) return;
12913
+ const name = (await promptInput(strings.promptNameTitle))?.trim();
12914
+ if (!name) return;
12915
+ await savePromptAs(name, body2);
12371
12916
  return;
12372
12917
  }
12373
12918
  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));
12919
+ let name = rest.slice(1).join(" ").trim() || void 0;
12920
+ if (!name) {
12921
+ const items2 = await runtime.listPromptItems();
12922
+ if (items2.length === 0) {
12923
+ notice("info", strings.promptsEmpty);
12924
+ return;
12381
12925
  }
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));
12926
+ name = await pick(strings.titlePrompts, items2) ?? void 0;
12927
+ if (!name) return;
12397
12928
  }
12929
+ if (!await ask2(strings.promptDeleteAsk(name), void 0, true)) return;
12930
+ await runtime.deletePrompt(name);
12931
+ notice("info", strings.promptDeleted(name));
12932
+ return;
12933
+ }
12934
+ if (rawRest && await runtime.promptExists(rawRest)) {
12935
+ setInput(await runtime.readPrompt(rawRest));
12398
12936
  return;
12399
12937
  }
12400
12938
  const items = await runtime.listPromptItems();
@@ -12504,11 +13042,10 @@ Rename the file if you want a different name.`);
12504
13042
  const tryQueueAutomaticAttachment = useCallback(
12505
13043
  (requestedPath) => {
12506
13044
  if (!looksLikeAttachmentPath(requestedPath)) return Promise.resolve(false);
12507
- if (automaticAttachmentTask.current) {
12508
- return automaticAttachmentTask.current.then(() => false);
12509
- }
13045
+ const running = automaticAttachmentTask.current;
13046
+ if (running) return running.then(() => tryQueueAutomaticAttachmentRef.current(requestedPath));
12510
13047
  if (attachmentsRef.current.length >= MAX_ATTACHMENTS) {
12511
- notice("warn", `At most ${MAX_ATTACHMENTS} attachments can be queued for one message.`);
13048
+ notice("warn", strings.attachmentsLimit(MAX_ATTACHMENTS));
12512
13049
  return Promise.resolve(true);
12513
13050
  }
12514
13051
  const paths = splitAttachmentPaths(requestedPath) ?? [requestedPath];
@@ -12521,7 +13058,7 @@ Rename the file if you want a different name.`);
12521
13058
  if (automaticAttachmentTask.current !== task) return true;
12522
13059
  if (!block) continue;
12523
13060
  if (!appendAttachment(block)) {
12524
- notice("warn", `At most ${MAX_ATTACHMENTS} attachments can be queued for one message.`);
13061
+ notice("warn", strings.attachmentsLimit(MAX_ATTACHMENTS));
12525
13062
  return true;
12526
13063
  }
12527
13064
  added += 1;
@@ -12543,6 +13080,8 @@ Rename the file if you want a different name.`);
12543
13080
  },
12544
13081
  [appendAttachment, notice, runtime, strings]
12545
13082
  );
13083
+ const tryQueueAutomaticAttachmentRef = useRef5(tryQueueAutomaticAttachment);
13084
+ tryQueueAutomaticAttachmentRef.current = tryQueueAutomaticAttachment;
12546
13085
  const listWorkspaceFiles = useCallback(
12547
13086
  (query) => runtime.listWorkspaceFiles(query).catch(() => []),
12548
13087
  [runtime]
@@ -12550,13 +13089,13 @@ Rename the file if you want a different name.`);
12550
13089
  const pasteClipboardImage = useCallback(() => {
12551
13090
  if (clipboardPasteTask.current) return;
12552
13091
  if (attachmentsRef.current.length >= MAX_ATTACHMENTS) {
12553
- notice("warn", `At most ${MAX_ATTACHMENTS} attachments can be queued for one message.`);
13092
+ notice("warn", strings.attachmentsLimit(MAX_ATTACHMENTS));
12554
13093
  return;
12555
13094
  }
12556
13095
  const task = runtime.loadClipboardImage().then((block) => {
12557
13096
  if (clipboardPasteTask.current !== task) return;
12558
13097
  if (!appendAttachment(block)) {
12559
- notice("warn", `At most ${MAX_ATTACHMENTS} attachments can be queued for one message.`);
13098
+ notice("warn", strings.attachmentsLimit(MAX_ATTACHMENTS));
12560
13099
  return;
12561
13100
  }
12562
13101
  notice("info", strings.attachmentAdded(attachmentLabel(block) ?? "clipboard image"));
@@ -12612,11 +13151,15 @@ Rename the file if you want a different name.`);
12612
13151
  const queuedAttachments = attachmentsRef.current;
12613
13152
  if (!text && queuedAttachments.length === 0) return;
12614
13153
  const mentioned = [];
12615
- for (const path15 of extractMentions(text)) {
13154
+ for (const path16 of extractMentions(text)) {
12616
13155
  if (queuedAttachments.length + mentioned.length >= MAX_ATTACHMENTS) break;
12617
13156
  try {
12618
- mentioned.push(await runtime.loadAttachment(path15));
12619
- } catch {
13157
+ const block = await runtime.loadMentionAttachment(path16);
13158
+ if (!block) continue;
13159
+ mentioned.push(block);
13160
+ notice("info", strings.mentionAttached(attachmentLabel(block) ?? path16));
13161
+ } catch (error) {
13162
+ if (error instanceof Error && !error.code) notice("warn", error.message);
12620
13163
  }
12621
13164
  }
12622
13165
  if (generation !== attachmentGeneration.current) return;
@@ -12897,6 +13440,12 @@ function mcpAddProblem(strings, error) {
12897
13440
  return strings.mcpAddInvalidUrl;
12898
13441
  case "http-args":
12899
13442
  return strings.mcpAddHttpArgs;
13443
+ case "invalid-env":
13444
+ return strings.mcpAddInvalidEnv;
13445
+ case "invalid-header":
13446
+ return strings.mcpAddInvalidHeader;
13447
+ case "option-mismatch":
13448
+ return strings.mcpAddOptionMismatch;
12900
13449
  case "usage":
12901
13450
  return strings.mcpAddUsage;
12902
13451
  }
@@ -12911,6 +13460,16 @@ function mcpStateLine(strings, state) {
12911
13460
  function formatBalances(balances) {
12912
13461
  return balances.map((item) => `${item.amount} ${item.currency}`).join(" \xB7 ");
12913
13462
  }
13463
+ function attachmentPayload(block) {
13464
+ if (block.type === "image") return block.data.length;
13465
+ if (block.type === "file") return Buffer.byteLength(block.text);
13466
+ return 0;
13467
+ }
13468
+ function sameAttachment(a, b) {
13469
+ if (a.type === "image" && b.type === "image") return a.name === b.name && a.data === b.data;
13470
+ if (a.type === "file" && b.type === "file") return a.name === b.name && a.text === b.text;
13471
+ return false;
13472
+ }
12914
13473
  function lastUserText(messages) {
12915
13474
  for (let index = messages.length - 1; index >= 0; index -= 1) {
12916
13475
  const message = messages[index];
@@ -12991,12 +13550,9 @@ program.name("kitcode").description("Terminal coding agent with a config you nev
12991
13550
  if (options.mode && !MODES2.includes(options.mode)) {
12992
13551
  throw new Error(`Unknown mode "${options.mode}". Use normal, accept or plan.`);
12993
13552
  }
12994
- let cwd = options.cwd;
12995
- if (cwd) {
12996
- try {
12997
- cwd = resolve2(cwd);
12998
- } catch {
12999
- }
13553
+ const cwd = options.cwd ? resolve2(options.cwd) : void 0;
13554
+ if (cwd && !statSync(cwd, { throwIfNoEntry: false })?.isDirectory()) {
13555
+ throw new Error(`--cwd is not a directory: ${cwd}`);
13000
13556
  }
13001
13557
  await startTui({
13002
13558
  continueSession: options.continue,
@@ -13049,8 +13605,9 @@ prompts.command("list").description("list saved prompts").action(async () => {
13049
13605
  console.log(`${prompt.slug} ${prompt.name}${prompt.description ? ` \u2014 ${prompt.description}` : ""}`);
13050
13606
  }
13051
13607
  });
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}`);
13608
+ prompts.command("rm").description("delete a saved prompt").argument("<name>").action(async (name) => {
13609
+ const slug = promptSlug(name);
13610
+ console.log(slug && await deletePrompt(slug) ? `deleted ${name}` : `no such prompt: ${name}`);
13054
13611
  });
13055
13612
  program.parseAsync(process.argv).catch((error) => {
13056
13613
  console.error(error instanceof Error ? error.message : String(error));