@kernelonpanic/kitcode 1.2.9 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -40,7 +40,7 @@ var providerConfigSchema = z.object({
40
40
  keyEnv: z.string().optional(),
41
41
  headers: z.record(z.string(), z.string()).optional()
42
42
  });
43
- var effortSchema = z.enum(["low", "medium", "high", "xhigh", "max"]);
43
+ var effortSchema = z.enum(["auto", "low", "medium", "high", "xhigh", "max"]);
44
44
  var permissionModeSchema = z.enum(["allow", "ask", "deny"]);
45
45
  var mcpStdioSchema = z.object({
46
46
  type: z.literal("stdio"),
@@ -210,8 +210,8 @@ async function readHead(stream) {
210
210
  let text = "";
211
211
  try {
212
212
  while (text.length < CAPTURE_CHARS) {
213
- const { done, value } = await reader.read();
214
- if (done) break;
213
+ const { done: done2, value } = await reader.read();
214
+ if (done2) break;
215
215
  text += decoder.decode(value, { stream: true });
216
216
  }
217
217
  } catch {
@@ -420,39 +420,52 @@ async function detectProvider(rawUrl, apiKey2, opts = {}) {
420
420
  `${anthropicBaseUrl}/v1/models?limit=1000`,
421
421
  anthropicHeaders(apiKey2)
422
422
  );
423
- return {
424
- id,
425
- config: { type: "anthropic", baseUrl: anthropicBaseUrl },
426
- models: probed.ok ? parseModelList(probed.body) : []
427
- };
423
+ return [
424
+ {
425
+ id,
426
+ config: { type: "anthropic", baseUrl: anthropicBaseUrl },
427
+ models: probed.ok ? parseModelList(probed.body) : []
428
+ }
429
+ ];
428
430
  }
431
+ const found = [];
429
432
  let last;
430
433
  for (const candidate of candidateBaseUrls(baseUrl)) {
434
+ if (found.length === schemes.length) break;
431
435
  const url = `${candidate}/models`;
432
- let attempt = await probe(fetchImpl, url, { Authorization: `Bearer ${apiKey2}` });
433
- last = attempt;
434
- if (attempt.ok && isModelListBody(attempt.body)) {
435
- const anthropic = advertisesAnthropicProtocol(attempt.body);
436
- return {
437
- id,
438
- config: anthropic ? { type: "anthropic", baseUrl: anthropicRoot(candidate) } : { type: "openai", baseUrl: candidate },
439
- models: parseModelList(attempt.body)
440
- };
441
- }
442
- if (attempt.status === 401 || attempt.status === 403) {
443
- attempt = await probe(fetchImpl, url, anthropicHeaders(apiKey2));
436
+ for (const scheme of schemes) {
437
+ const attempt = await probe(fetchImpl, url, schemeHeaders(scheme, apiKey2));
444
438
  last = attempt;
445
- if (attempt.ok) {
446
- return {
447
- id,
448
- config: { type: "anthropic", baseUrl: anthropicRoot(candidate) },
449
- models: parseModelList(attempt.body)
450
- };
451
- }
439
+ if (attempt.status === 404 || attempt.status === 0) break;
440
+ if (attempt.ok && isModelListBody(attempt.body)) record(found, candidate, scheme, attempt.body);
441
+ if (found.length === schemes.length) break;
452
442
  }
453
- if (!attempt.ok && attempt.status !== 404 && attempt.status !== 0) break;
443
+ if (found.length === 0 && !last.ok && last.status !== 404 && last.status !== 0) break;
454
444
  }
455
- throw new Error(describeFailure(last, apiKey2));
445
+ if (found.length === 0) throw new Error(describeFailure(last, apiKey2));
446
+ return found.map((entry, index) => ({
447
+ id: index === 0 ? id : `${id.slice(0, MAX_ID_STEM)}-${entry.type}`,
448
+ config: { type: entry.type, baseUrl: entry.baseUrl },
449
+ models: entry.models
450
+ }));
451
+ }
452
+ var schemes = ["bearer", "anthropic"];
453
+ var MAX_ID_STEM = 120;
454
+ function schemeHeaders(scheme, apiKey2) {
455
+ return scheme === "bearer" ? { Authorization: `Bearer ${apiKey2}` } : anthropicHeaders(apiKey2);
456
+ }
457
+ function record(found, candidate, scheme, body) {
458
+ const type = scheme === "anthropic" || advertisesAnthropicProtocol(body) ? "anthropic" : "openai";
459
+ if (found.some((entry) => entry.type === type)) return;
460
+ const models = parseModelList(body);
461
+ const fingerprint2 = models.map((model) => model.id).sort().join("\n");
462
+ if (found.some((entry) => entry.fingerprint === fingerprint2)) return;
463
+ found.push({
464
+ type,
465
+ baseUrl: type === "anthropic" ? anthropicRoot(candidate) : candidate,
466
+ models,
467
+ fingerprint: fingerprint2
468
+ });
456
469
  }
457
470
  function normaliseBaseUrl(rawUrl) {
458
471
  return rawUrl.trim().replace(/\/+$/, "");
@@ -519,8 +532,8 @@ async function boundedResponseText(response) {
519
532
  let text = "";
520
533
  try {
521
534
  for (; ; ) {
522
- const { done, value } = await reader.read();
523
- if (done) break;
535
+ const { done: done2, value } = await reader.read();
536
+ if (done2) break;
524
537
  bytes += value.byteLength;
525
538
  if (bytes > MAX_DETECT_BODY_BYTES) {
526
539
  throw new Error(`response exceeded ${MAX_DETECT_BODY_BYTES / 1e6} MB`);
@@ -566,11 +579,9 @@ function anthropicRoot(baseUrl) {
566
579
  function advertisesAnthropicProtocol(body) {
567
580
  const entries = modelEntries(body);
568
581
  if (entries.length === 0) return false;
569
- const endpointTypes = new Set(
570
- entries.flatMap((entry) => stringList(entry["supported_endpoint_types"]))
571
- );
572
- if (endpointTypes.size > 0) {
573
- return endpointTypes.has("anthropic") && !endpointTypes.has("openai");
582
+ const advertised = entries.map((entry) => stringList(entry["supported_endpoint_types"]));
583
+ if (advertised.some((types) => types.length > 0)) {
584
+ return advertised.every((types) => types.includes("anthropic"));
574
585
  }
575
586
  if (entries.some(isVendorNamespaced)) return false;
576
587
  return entries.every((entry) => entry["owned_by"] === "anthropic");
@@ -827,9 +838,9 @@ function conventionalKeyEnv(providerId) {
827
838
  function expandEnvRefs(value) {
828
839
  return value.replace(/\$\{env:([^}]+)\}/g, (_match, name) => process.env[name] ?? "");
829
840
  }
830
- function expandEnvRecord(record2) {
841
+ function expandEnvRecord(record3) {
831
842
  return Object.fromEntries(
832
- Object.entries(record2).map(([key, value]) => [key, expandEnvRefs(value)])
843
+ Object.entries(record3).map(([key, value]) => [key, expandEnvRefs(value)])
833
844
  );
834
845
  }
835
846
  async function readJson(file) {
@@ -895,12 +906,15 @@ async function addProvider(url, key, options = {}) {
895
906
  const auth = await loadAuth();
896
907
  const configBefore = structuredClone(config);
897
908
  const authBefore = { ...auth };
898
- config.providers[detected.id] = detected.config;
899
- auth[detected.id] = key;
900
- const models = detected.models;
909
+ for (const provider of detected) {
910
+ config.providers[provider.id] = provider.config;
911
+ auth[provider.id] = key;
912
+ }
913
+ const primary = detected[0];
914
+ const models = detected.flatMap((provider) => provider.models);
901
915
  if (!config.model) {
902
- const chosen = pickDefault(models);
903
- if (chosen) config.model = formatModelRef(detected.id, chosen);
916
+ const chosen = pickDefaultRef(detected);
917
+ if (chosen) config.model = chosen;
904
918
  }
905
919
  try {
906
920
  await saveConfig(config);
@@ -913,14 +927,15 @@ async function addProvider(url, key, options = {}) {
913
927
  throw error;
914
928
  }
915
929
  const location = await configLocation();
916
- const protocol = detected.config.type === "anthropic" ? "Anthropic" : "OpenAI-compatible";
917
- const rows = [
918
- ["provider", `${detected.id} \u2014 this endpoint speaks the ${protocol} protocol`],
930
+ const rows = detected.map((provider) => [
931
+ "provider",
932
+ `${provider.id} \u2014 this endpoint speaks the ${protocolLabel(provider)} protocol`
933
+ ]);
934
+ rows.push(
919
935
  ["models", models.length === 0 ? "none listed by this endpoint" : `${models.length} available`],
920
- ["default", describeDefault(config.model, detected.id, models)]
921
- ];
922
- alternatives(models, config.model, detected.id).forEach((model, index) => {
923
- const ref = formatModelRef(detected.id, model.id);
936
+ ["default", describeDefault(config.model, detected)]
937
+ );
938
+ alternatives(detected, config.model).forEach(([ref, model], index) => {
924
939
  rows.push([index === 0 ? "also try" : "", `${ref} ${price(model)}`]);
925
940
  });
926
941
  rows.push(["config", `${location.path} (${location.scope})`]);
@@ -928,11 +943,15 @@ async function addProvider(url, key, options = {}) {
928
943
  rows.push(["next", "run kitcode to start a session; /model switches models"]);
929
944
  for (const [label, value] of rows) console.log(label.padEnd(LABEL_WIDTH) + value);
930
945
  }
931
- function describeDefault(ref, providerId, models) {
946
+ function protocolLabel(provider) {
947
+ return provider.config.type === "anthropic" ? "Anthropic" : "OpenAI-compatible";
948
+ }
949
+ function describeDefault(ref, detected) {
932
950
  if (!ref) return "not set \u2014 start kitcode and pick one with /model";
933
951
  const parsed = parseModelRef(ref);
934
- if (parsed?.provider !== providerId) return `${ref} \u2014 already in your config, left alone`;
935
- return `${ref} ${price(models.find((model) => model.id === parsed.model))}`;
952
+ const owner = detected.find((provider) => provider.id === parsed?.provider);
953
+ if (!owner) return `${ref} \u2014 already in your config, left alone`;
954
+ return `${ref} ${price(owner.models.find((model) => model.id === parsed.model))}`;
936
955
  }
937
956
  function price(model) {
938
957
  const pricing = model?.pricing;
@@ -942,25 +961,35 @@ function price(model) {
942
961
  function money(value) {
943
962
  return `$${value >= 1 ? value.toFixed(2) : Number(value.toFixed(4))}`;
944
963
  }
945
- function pickDefault(models) {
964
+ function pickDefaultRef(detected) {
965
+ const refs = modelRefs(detected);
946
966
  for (const preferred of PREFERRED) {
947
- const match = models.find((model) => model.id.endsWith(preferred));
948
- if (match) return match.id;
967
+ const match = refs.find(([, model]) => model.id.endsWith(preferred));
968
+ if (match) return match[0];
949
969
  }
950
- return models[0]?.id;
970
+ return refs[0]?.[0];
951
971
  }
952
- function alternatives(models, defaultRef, providerId) {
972
+ function alternatives(detected, defaultRef) {
973
+ const refs = modelRefs(detected);
953
974
  const ranked = [
954
- ...PREFERRED.flatMap((preferred) => models.filter((model) => model.id.endsWith(preferred))),
955
- ...models.filter((model) => model.pricing),
956
- ...models
975
+ ...PREFERRED.flatMap((preferred) => refs.filter(([, model]) => model.id.endsWith(preferred))),
976
+ ...refs.filter(([, model]) => model.pricing),
977
+ ...refs
957
978
  ];
958
979
  const picked = /* @__PURE__ */ new Map();
959
- for (const model of ranked) {
980
+ for (const [ref, model] of ranked) {
960
981
  if (picked.size === ALTERNATIVES) break;
961
- if (formatModelRef(providerId, model.id) !== defaultRef) picked.set(model.id, model);
982
+ if (ref !== defaultRef) picked.set(ref, model);
962
983
  }
963
- return [...picked.values()];
984
+ return [...picked.entries()];
985
+ }
986
+ function modelRefs(detected) {
987
+ return detected.flatMap(
988
+ (provider) => provider.models.map((model) => [
989
+ formatModelRef(provider.id, model.id),
990
+ model
991
+ ])
992
+ );
964
993
  }
965
994
 
966
995
  // src/app/ask.ts
@@ -1024,14 +1053,375 @@ function skipControlString(text, from, bellTerminates) {
1024
1053
  return text.length - 1;
1025
1054
  }
1026
1055
 
1056
+ // src/tools/memory.ts
1057
+ import { z as z2 } from "zod";
1058
+
1059
+ // src/tools/summary.ts
1060
+ function brief(value, max = 60) {
1061
+ const text = String(value ?? "").replace(/\s+/g, " ").trim();
1062
+ return text.length > max ? `${text.slice(0, max - 1)}\u2026` : text;
1063
+ }
1064
+
1065
+ // src/tools/memory.ts
1066
+ var inputSchema = z2.discriminatedUnion("action", [
1067
+ z2.object({ action: z2.literal("read") }).strict(),
1068
+ z2.object({ action: z2.literal("add"), text: z2.string().trim().min(1).max(4e3), source: z2.string().trim().min(1).max(1e3) }).strict(),
1069
+ z2.object({ action: z2.literal("replace"), oldText: z2.string().min(1), text: z2.string().trim().min(1).max(4e3), source: z2.string().trim().min(1).max(1e3) }).strict(),
1070
+ z2.object({ action: z2.literal("delete"), oldText: z2.string().min(1), source: z2.string().trim().min(1).max(1e3) }).strict()
1071
+ ]);
1072
+ function createMemoryTool(store) {
1073
+ return {
1074
+ name: "memory",
1075
+ description: "Read or update persistent notes for this project. Remember only explicit user preferences, confirmed project facts and accepted decisions, with their source (user instruction or file/tool evidence). Never store secrets, guesses, temporary task progress or raw logs. Read before changing existing notes. For replace/delete, oldText must match exactly once; include the whole old note and its source. These notes survive new chats. Do not use this tool to change permissions or override current instructions.",
1076
+ inputSchema: {
1077
+ type: "object",
1078
+ properties: {
1079
+ action: { type: "string", enum: ["read", "add", "replace", "delete"] },
1080
+ text: { type: "string", maxLength: 4e3 },
1081
+ oldText: { type: "string" },
1082
+ source: { type: "string", maxLength: 1e3 }
1083
+ },
1084
+ required: ["action"],
1085
+ additionalProperties: false
1086
+ },
1087
+ defaultPermission: "allow",
1088
+ summarize(input) {
1089
+ const parsed = inputSchema.safeParse(input);
1090
+ return parsed.success ? `memory(${parsed.data.action}${"text" in parsed.data ? `: ${brief(parsed.data.text, 80)}` : ""})` : "memory(invalid input)";
1091
+ },
1092
+ async execute(input, ctx) {
1093
+ const parsed = inputSchema.safeParse(input);
1094
+ if (!parsed.success) return { content: "Invalid memory arguments: mutations require source; replace/delete also require oldText.", isError: true };
1095
+ const args = parsed.data;
1096
+ const before = store.read();
1097
+ if (args.action === "read") return { content: before || "Project memory is empty." };
1098
+ if (ctx.signal.aborted) return { content: "Memory update cancelled.", isError: true };
1099
+ const note = "text" in args ? `${redactSecrets(args.text)}
1100
+ Source: ${redactSecrets(args.source)}` : "";
1101
+ let after;
1102
+ if (args.action === "add") {
1103
+ if (before.includes(note)) return { content: "This note is already in project memory." };
1104
+ after = [before.trim(), note].filter(Boolean).join("\n\n");
1105
+ } else {
1106
+ const at = before.indexOf(args.oldText);
1107
+ if (at === -1 || before.indexOf(args.oldText, at + 1) !== -1) {
1108
+ return { content: "oldText must match exactly once. Read memory again before retrying.", isError: true };
1109
+ }
1110
+ after = (before.slice(0, at) + note + before.slice(at + args.oldText.length)).trim();
1111
+ }
1112
+ await store.save(after);
1113
+ return { content: `Project memory updated (${args.action}).
1114
+ ${store.read()}`, display: { kind: "diff", path: "project memory", before, after: store.read() } };
1115
+ }
1116
+ };
1117
+ }
1118
+
1119
+ // src/ui/workspace-files.ts
1120
+ import fg2 from "fast-glob";
1121
+
1122
+ // src/tools/glob.ts
1123
+ import { join } from "path";
1124
+ import fg from "fast-glob";
1125
+
1126
+ // src/tools/safepath.ts
1127
+ import { realpathSync } from "fs";
1128
+ import { basename, dirname, isAbsolute, relative, resolve, sep } from "path";
1129
+ function canonicalize(target) {
1130
+ try {
1131
+ return realpathSync(target);
1132
+ } catch {
1133
+ let current = target;
1134
+ const trailing = [];
1135
+ for (let i = 0; i < 64; i++) {
1136
+ try {
1137
+ return resolve(realpathSync(current), ...trailing);
1138
+ } catch {
1139
+ const parent = dirname(current);
1140
+ if (parent === current) break;
1141
+ trailing.unshift(basename(current));
1142
+ current = parent;
1143
+ }
1144
+ }
1145
+ return resolve(current, ...trailing);
1146
+ }
1147
+ }
1148
+ function resolveInside(cwd, userPath) {
1149
+ const root = canonicalize(resolve(cwd));
1150
+ const target = canonicalize(resolve(root, userPath));
1151
+ const rel = relative(root, target);
1152
+ if (rel !== "" && (isAbsolute(rel) || rel.split(sep)[0] === "..")) {
1153
+ return { ok: false, reason: `Path ${userPath} resolves outside the workspace root ${root}` };
1154
+ }
1155
+ return { ok: true, path: target, relative: rel };
1156
+ }
1157
+ function patternEscapes(pattern) {
1158
+ return isAbsolute(pattern) || pattern.split("/").includes("..") || pattern.split("\\").includes("..");
1159
+ }
1160
+
1161
+ // src/tools/glob.ts
1162
+ var MAX_RESULTS = 500;
1163
+ var IGNORED_DIRECTORIES = ["**/node_modules/**", "**/.git/**", "**/dist/**"];
1164
+ var globTool = {
1165
+ name: "glob",
1166
+ description: 'Find files by glob pattern, most recently modified first. Supports patterns like "src/**/*.ts". Hidden files, node_modules, .git and dist are skipped.',
1167
+ inputSchema: {
1168
+ type: "object",
1169
+ properties: {
1170
+ pattern: { type: "string", description: "Glob pattern, e.g. **/*.test.ts" },
1171
+ path: { type: "string", description: "Directory to search in, relative to the workspace root" }
1172
+ },
1173
+ required: ["pattern"],
1174
+ additionalProperties: false
1175
+ },
1176
+ defaultPermission: "allow",
1177
+ readOnly: true,
1178
+ summarize(input) {
1179
+ return `glob(${brief(input.pattern)})`;
1180
+ },
1181
+ async execute(input, ctx) {
1182
+ const { pattern, path: path15 = "." } = input;
1183
+ if (patternEscapes(pattern)) {
1184
+ return { content: `Pattern ${pattern} must be relative to the workspace root.`, isError: true };
1185
+ }
1186
+ const safe = resolveInside(ctx.cwd, path15);
1187
+ if (!safe.ok) return { content: safe.reason, isError: true };
1188
+ const entries = await fg(pattern, {
1189
+ cwd: safe.path,
1190
+ dot: false,
1191
+ onlyFiles: true,
1192
+ followSymbolicLinks: false,
1193
+ suppressErrors: true,
1194
+ ignore: IGNORED_DIRECTORIES,
1195
+ stats: true
1196
+ });
1197
+ if (entries.length === 0) return { content: `No files matched ${pattern}` };
1198
+ entries.sort((a, b) => (b.stats?.mtimeMs ?? 0) - (a.stats?.mtimeMs ?? 0));
1199
+ const paths = entries.slice(0, MAX_RESULTS).map((entry) => join(safe.relative, entry.path));
1200
+ if (entries.length > MAX_RESULTS) {
1201
+ paths.push(`... truncated: ${entries.length - MAX_RESULTS} more files matched.`);
1202
+ }
1203
+ return { content: paths.join("\n") };
1204
+ }
1205
+ };
1206
+
1207
+ // src/ui/workspace-files.ts
1208
+ var MAX_SCANNED = 2e4;
1209
+ var MAX_RESULTS2 = 12;
1210
+ var CACHE_TTL_MS = 1e4;
1211
+ var cached;
1212
+ var inFlight;
1213
+ async function searchWorkspaceFiles(cwd, query) {
1214
+ const files = await workspaceFiles(cwd);
1215
+ const needle = query.trim().toLowerCase();
1216
+ if (needle === "") return files.slice(0, MAX_RESULTS2);
1217
+ const exact = [];
1218
+ const loose = [];
1219
+ for (const file of files) {
1220
+ const haystack = file.toLowerCase();
1221
+ if (haystack.includes(needle)) exact.push(file);
1222
+ else if (isSubsequence(needle, haystack)) loose.push(file);
1223
+ if (exact.length >= MAX_RESULTS2) break;
1224
+ }
1225
+ exact.sort((a, b) => score(a, needle) - score(b, needle));
1226
+ return [...exact, ...loose].slice(0, MAX_RESULTS2);
1227
+ }
1228
+ function workspaceFiles(cwd) {
1229
+ const fresh = cached && cached.cwd === cwd && Date.now() - cached.at < CACHE_TTL_MS;
1230
+ if (fresh) return Promise.resolve(cached.files);
1231
+ if (inFlight) return inFlight;
1232
+ inFlight = fg2("**/*", {
1233
+ cwd,
1234
+ dot: false,
1235
+ onlyFiles: true,
1236
+ followSymbolicLinks: false,
1237
+ suppressErrors: true,
1238
+ ignore: IGNORED_DIRECTORIES
1239
+ }).then((entries) => {
1240
+ const files = entries.slice(0, MAX_SCANNED).sort((a, b) => a.length - b.length);
1241
+ cached = { cwd, files, at: Date.now() };
1242
+ return files;
1243
+ }).catch(() => []).finally(() => {
1244
+ inFlight = void 0;
1245
+ });
1246
+ return inFlight;
1247
+ }
1248
+ function score(file, needle) {
1249
+ const basename3 = file.slice(file.lastIndexOf("/") + 1).toLowerCase();
1250
+ if (basename3.startsWith(needle)) return 0;
1251
+ if (basename3.includes(needle)) return 1;
1252
+ return 2;
1253
+ }
1254
+ function isSubsequence(needle, haystack) {
1255
+ let index = 0;
1256
+ for (const character of haystack) {
1257
+ if (character === needle[index]) index += 1;
1258
+ if (index === needle.length) return true;
1259
+ }
1260
+ return needle.length === 0;
1261
+ }
1262
+
1263
+ // src/tools/jobs.ts
1264
+ import { spawn } from "child_process";
1265
+ var MAX_JOBS = 8;
1266
+ var MAX_JOB_OUTPUT = 1e5;
1267
+ var jobs = /* @__PURE__ */ new Map();
1268
+ var counter = 0;
1269
+ function startJob(command, cwd) {
1270
+ const live = [...jobs.values()].filter((job2) => job2.state === "running");
1271
+ if (live.length >= MAX_JOBS) {
1272
+ return { error: `At most ${MAX_JOBS} background commands can run at once.` };
1273
+ }
1274
+ counter += 1;
1275
+ const id = `bg-${counter}`;
1276
+ const child = spawn(command, {
1277
+ shell: true,
1278
+ cwd,
1279
+ detached: process.platform !== "win32",
1280
+ stdio: ["ignore", "pipe", "pipe"],
1281
+ windowsHide: true
1282
+ });
1283
+ const job = {
1284
+ id,
1285
+ command,
1286
+ state: "running",
1287
+ exitCode: null,
1288
+ signal: null,
1289
+ child,
1290
+ buffer: "",
1291
+ reported: 0,
1292
+ truncated: false
1293
+ };
1294
+ jobs.set(id, job);
1295
+ const collect = (chunk) => {
1296
+ const text = chunk.toString("utf8");
1297
+ const room = MAX_JOB_OUTPUT - job.buffer.length;
1298
+ if (text.length <= room) {
1299
+ job.buffer += text;
1300
+ return;
1301
+ }
1302
+ job.truncated = true;
1303
+ job.buffer = (job.buffer + text).slice(-MAX_JOB_OUTPUT);
1304
+ job.reported = Math.min(job.reported, job.buffer.length);
1305
+ };
1306
+ child.stdout?.on("data", collect);
1307
+ child.stderr?.on("data", collect);
1308
+ child.on("error", (error) => {
1309
+ job.buffer += `
1310
+ Command failed to start: ${error.message}`;
1311
+ job.state = "exited";
1312
+ job.exitCode = job.exitCode ?? 1;
1313
+ });
1314
+ child.on("close", (code, signal) => {
1315
+ if (job.state !== "killed") job.state = "exited";
1316
+ job.exitCode = code;
1317
+ job.signal = signal;
1318
+ });
1319
+ return { id };
1320
+ }
1321
+ function readJob(id) {
1322
+ const job = jobs.get(id);
1323
+ if (!job) return void 0;
1324
+ const output = job.buffer.slice(job.reported);
1325
+ job.reported = job.buffer.length;
1326
+ return { ...summary(job), output, truncated: job.truncated };
1327
+ }
1328
+ function killJob(id) {
1329
+ const job = jobs.get(id);
1330
+ if (!job) return false;
1331
+ if (job.state === "running") {
1332
+ job.state = "killed";
1333
+ killTree(job.child);
1334
+ }
1335
+ return true;
1336
+ }
1337
+ function listJobs() {
1338
+ return [...jobs.values()].map(summary);
1339
+ }
1340
+ function killAllJobs() {
1341
+ for (const job of jobs.values()) {
1342
+ if (job.state === "running") {
1343
+ job.state = "killed";
1344
+ killTree(job.child);
1345
+ }
1346
+ }
1347
+ }
1348
+ function summary(job) {
1349
+ return {
1350
+ id: job.id,
1351
+ command: job.command,
1352
+ state: job.state,
1353
+ exitCode: job.exitCode,
1354
+ signal: job.signal
1355
+ };
1356
+ }
1357
+ function killTree(child) {
1358
+ if (child.pid == null) return;
1359
+ if (process.platform === "win32") {
1360
+ try {
1361
+ spawn("taskkill", ["/pid", String(child.pid), "/t", "/f"], { stdio: "ignore" });
1362
+ } catch {
1363
+ child.kill("SIGKILL");
1364
+ }
1365
+ return;
1366
+ }
1367
+ try {
1368
+ process.kill(-child.pid, "SIGKILL");
1369
+ } catch {
1370
+ child.kill("SIGKILL");
1371
+ }
1372
+ }
1373
+
1374
+ // src/core/memory.ts
1375
+ import { createHash } from "crypto";
1376
+ import { readFile as readFile3, writeFile as writeFile3, rm as rm3 } from "fs/promises";
1377
+ import path4 from "path";
1378
+ function projectMemoryPath(workspace) {
1379
+ const key = process.platform === "win32" ? workspace.toLowerCase() : workspace;
1380
+ return path4.join(homeDir, "memory", createHash("sha256").update(key).digest("hex") + ".txt");
1381
+ }
1382
+ async function readProjectMemory(workspace) {
1383
+ try {
1384
+ return await readFile3(projectMemoryPath(workspace), "utf8");
1385
+ } catch (error) {
1386
+ if (error.code === "ENOENT") return "";
1387
+ throw error;
1388
+ }
1389
+ }
1390
+ async function saveProjectMemory(workspace, text) {
1391
+ if (text.length > 16e3) throw new Error("Project memory is limited to 16,000 characters.");
1392
+ const file = projectMemoryPath(workspace);
1393
+ await ensureDir(path4.dirname(file));
1394
+ await writeFile3(file, redactSecrets(text), { mode: 384 });
1395
+ }
1396
+ async function clearProjectMemory(workspace) {
1397
+ await rm3(projectMemoryPath(workspace), { force: true });
1398
+ }
1399
+
1400
+ // src/providers/effort.ts
1401
+ function resolveEffort(effort, messages) {
1402
+ if (effort !== "auto") return effort;
1403
+ const latest = messages.findLast((message) => message.role === "user" && message.content.some((block) => block.type === "text"));
1404
+ const text = latest?.content.filter((block) => block.type === "text").map((block) => block.text).join("\n") ?? "";
1405
+ return text.length > 500 || /bug|fix|debug|test|refactor|implement|баг|фикс|исправ|тест|рефактор|реализ/i.test(text) ? "high" : "medium";
1406
+ }
1407
+ function openAiEffort(model, effort) {
1408
+ const id = model.split("/").at(-1) ?? model;
1409
+ if (!/^(?:o[134](?:-|$)|gpt-5(?:[.-]|$))/.test(id)) return void 0;
1410
+ if (/^(?:o[134](?:-|$)|gpt-5(?:-(?:mini|nano|\d{4})|$)|gpt-5\.1(?:-|$))/.test(id)) {
1411
+ return effort === "max" || effort === "xhigh" ? "high" : effort;
1412
+ }
1413
+ if (/^gpt-5\.[234](?:-|$)/.test(id)) return effort === "max" ? "xhigh" : effort;
1414
+ return effort === "max" || effort === "xhigh" ? "high" : effort;
1415
+ }
1416
+
1027
1417
  // src/app/runtime.ts
1028
- import path13 from "path";
1418
+ import path14 from "path";
1029
1419
  import { mkdir as mkdir6 } from "fs/promises";
1030
1420
 
1031
1421
  // src/core/session.ts
1032
1422
  import { randomBytes } from "crypto";
1033
- import { readFile as readFile3, readdir, rename as rename3, stat as stat2, unlink, writeFile as writeFile3 } from "fs/promises";
1034
- import path4 from "path";
1423
+ import { readFile as readFile4, readdir, rename as rename3, stat as stat2, unlink, writeFile as writeFile4 } from "fs/promises";
1424
+ import path5 from "path";
1035
1425
  var MAX_SESSION_BYTES = 5e7;
1036
1426
  function createSession(cwd, model) {
1037
1427
  const now = (/* @__PURE__ */ new Date()).toISOString();
@@ -1050,10 +1440,10 @@ async function saveSession(state) {
1050
1440
  assertSessionId(state.id);
1051
1441
  state.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
1052
1442
  await ensureDir(sessionsDir);
1053
- const file = path4.join(sessionsDir, `${state.id}.json`);
1443
+ const file = path5.join(sessionsDir, `${state.id}.json`);
1054
1444
  const temp = `${file}.${process.pid}.${randomBytes(4).toString("hex")}.tmp`;
1055
1445
  try {
1056
- await writeFile3(temp, JSON.stringify(state), { encoding: "utf8", mode: 384 });
1446
+ await writeFile4(temp, JSON.stringify(state), { encoding: "utf8", mode: 384 });
1057
1447
  await atomicRename(temp, file);
1058
1448
  } catch (error) {
1059
1449
  await unlink(temp).catch(() => void 0);
@@ -1077,14 +1467,14 @@ async function atomicRename(from, to, retries = 5) {
1077
1467
  }
1078
1468
  async function loadSession(id) {
1079
1469
  assertSessionId(id);
1080
- const file = path4.join(sessionsDir, `${id}.json`);
1470
+ const file = path5.join(sessionsDir, `${id}.json`);
1081
1471
  let raw;
1082
1472
  try {
1083
1473
  const info = await stat2(file);
1084
1474
  if (info.size > MAX_SESSION_BYTES) {
1085
1475
  throw new Error(`Session exceeds the ${MAX_SESSION_BYTES / 1e6} MB load limit: ${file}`);
1086
1476
  }
1087
- raw = await readFile3(file, "utf8");
1477
+ raw = await readFile4(file, "utf8");
1088
1478
  } catch (error) {
1089
1479
  if (error instanceof Error && error.message.startsWith("Session exceeds")) throw error;
1090
1480
  if (error.code === "ENOENT") {
@@ -1100,7 +1490,7 @@ async function listSessions(limit = 20) {
1100
1490
  const files = await filesByRecency();
1101
1491
  const summaries = [];
1102
1492
  for (const file of files.slice(0, limit)) {
1103
- const state = readState(await readFile3(file, "utf8").catch(() => ""));
1493
+ const state = readState(await readFile4(file, "utf8").catch(() => ""));
1104
1494
  if (state) {
1105
1495
  summaries.push({
1106
1496
  id: state.id,
@@ -1126,7 +1516,7 @@ async function renameSession(query, title) {
1126
1516
  async function deleteSession(query) {
1127
1517
  const id = await resolveSessionId(query);
1128
1518
  assertSessionId(id);
1129
- await unlink(path4.join(sessionsDir, `${id}.json`)).catch((error) => {
1519
+ await unlink(path5.join(sessionsDir, `${id}.json`)).catch((error) => {
1130
1520
  if (error.code === "ENOENT") throw new Error(`Session no longer exists: ${id}`);
1131
1521
  throw error;
1132
1522
  });
@@ -1151,7 +1541,7 @@ async function deleteAllSessions() {
1151
1541
  }
1152
1542
  });
1153
1543
  const results = await Promise.allSettled(
1154
- ids.map((id) => unlink(path4.join(sessionsDir, `${id}.json`)))
1544
+ ids.map((id) => unlink(path5.join(sessionsDir, `${id}.json`)))
1155
1545
  );
1156
1546
  const deleted = [];
1157
1547
  const failed = [];
@@ -1172,7 +1562,7 @@ async function exportSession(query, destination) {
1172
1562
  const id = await resolveSessionId(query);
1173
1563
  const state = await loadSession(id);
1174
1564
  const target = await resolveExportTarget(destination, state);
1175
- await writeFile3(target, renderSessionMarkdown(state), {
1565
+ await writeFile4(target, renderSessionMarkdown(state), {
1176
1566
  encoding: "utf8",
1177
1567
  mode: 384,
1178
1568
  flag: "wx"
@@ -1186,7 +1576,7 @@ async function exportSession(query, destination) {
1186
1576
  }
1187
1577
  async function latestSessionFor(cwd) {
1188
1578
  for (const file of await filesByRecency()) {
1189
- const state = readState(await readFile3(file, "utf8").catch(() => ""));
1579
+ const state = readState(await readFile4(file, "utf8").catch(() => ""));
1190
1580
  if (state?.cwd === cwd) return state;
1191
1581
  }
1192
1582
  return null;
@@ -1216,7 +1606,7 @@ async function filesByRecency() {
1216
1606
  }
1217
1607
  const dated = await Promise.all(
1218
1608
  names.filter((name) => name.endsWith(".json")).map(async (name) => {
1219
- const file = path4.join(sessionsDir, name);
1609
+ const file = path5.join(sessionsDir, name);
1220
1610
  const info = await stat2(file).catch(() => null);
1221
1611
  return { file, at: info?.mtimeMs ?? 0, size: info?.size ?? 0 };
1222
1612
  })
@@ -1296,14 +1686,14 @@ function readUsageEntries(value) {
1296
1686
  });
1297
1687
  }
1298
1688
  async function resolveExportTarget(destination, state) {
1299
- const resolved = path4.resolve(destination);
1689
+ const resolved = path5.resolve(destination);
1300
1690
  const info = await stat2(resolved).catch(() => null);
1301
1691
  if (info?.isDirectory()) {
1302
- return path4.join(resolved, exportFileName(state));
1692
+ return path5.join(resolved, exportFileName(state));
1303
1693
  }
1304
1694
  if (info) throw new Error(`Export destination is not a directory: ${resolved}`);
1305
- const parent = await stat2(path4.dirname(resolved)).catch(() => null);
1306
- if (!parent?.isDirectory()) throw new Error(`Export directory does not exist: ${path4.dirname(resolved)}`);
1695
+ const parent = await stat2(path5.dirname(resolved)).catch(() => null);
1696
+ if (!parent?.isDirectory()) throw new Error(`Export directory does not exist: ${path5.dirname(resolved)}`);
1307
1697
  return resolved;
1308
1698
  }
1309
1699
  function exportFileName(state) {
@@ -1672,7 +2062,7 @@ async function runOneCall(cfg, call, hooks, signal) {
1672
2062
  if (!tool) {
1673
2063
  return toolError(call.id, `Unknown tool "${call.name}". Use only the tools provided.`);
1674
2064
  }
1675
- const summary = describeCall(tool, call.input);
2065
+ const summary2 = describeCall(tool, call.input);
1676
2066
  const ctx = {
1677
2067
  cwd: cfg.cwd,
1678
2068
  signal,
@@ -1696,7 +2086,7 @@ async function runOneCall(cfg, call, hooks, signal) {
1696
2086
  if (gate === "ask") {
1697
2087
  const decision = await hooks.requestPermission({
1698
2088
  toolName: tool.name,
1699
- summary,
2089
+ summary: summary2,
1700
2090
  input: call.input,
1701
2091
  display: await previewCall(tool, call.input, ctx),
1702
2092
  allowAlways: canAlwaysApprove(tool)
@@ -1709,7 +2099,7 @@ async function runOneCall(cfg, call, hooks, signal) {
1709
2099
  if (signal.aborted) {
1710
2100
  return toolError(call.id, "Interrupted by the user before this tool ran.");
1711
2101
  }
1712
- hooks.onEvent({ type: "tool_start", id: call.id, name: tool.name, summary });
2102
+ hooks.onEvent({ type: "tool_start", id: call.id, name: tool.name, summary: summary2 });
1713
2103
  try {
1714
2104
  const result = await tool.execute(call.input, ctx);
1715
2105
  hooks.onEvent({
@@ -1867,57 +2257,20 @@ function finitePositive(value) {
1867
2257
  }
1868
2258
 
1869
2259
  // src/core/checkpoint.ts
1870
- import { createHash, randomBytes as randomBytes2 } from "crypto";
2260
+ import { createHash as createHash2, randomBytes as randomBytes2 } from "crypto";
1871
2261
  import { createReadStream } from "fs";
1872
2262
  import {
1873
2263
  chmod as chmod3,
1874
2264
  lstat,
1875
2265
  mkdir as mkdir3,
1876
- readFile as readFile4,
2266
+ readFile as readFile5,
1877
2267
  readdir as readdir2,
1878
2268
  rename as rename4,
1879
2269
  stat as stat3,
1880
2270
  unlink as unlink2,
1881
- writeFile as writeFile4
2271
+ writeFile as writeFile5
1882
2272
  } from "fs/promises";
1883
- import path5 from "path";
1884
-
1885
- // src/tools/safepath.ts
1886
- import { realpathSync } from "fs";
1887
- import { basename, dirname, isAbsolute, relative, resolve, sep } from "path";
1888
- function canonicalize(target) {
1889
- try {
1890
- return realpathSync(target);
1891
- } catch {
1892
- let current = target;
1893
- const trailing = [];
1894
- for (let i = 0; i < 64; i++) {
1895
- try {
1896
- return resolve(realpathSync(current), ...trailing);
1897
- } catch {
1898
- const parent = dirname(current);
1899
- if (parent === current) break;
1900
- trailing.unshift(basename(current));
1901
- current = parent;
1902
- }
1903
- }
1904
- return resolve(current, ...trailing);
1905
- }
1906
- }
1907
- function resolveInside(cwd, userPath) {
1908
- const root = canonicalize(resolve(cwd));
1909
- const target = canonicalize(resolve(root, userPath));
1910
- const rel = relative(root, target);
1911
- if (rel !== "" && (isAbsolute(rel) || rel.split(sep)[0] === "..")) {
1912
- return { ok: false, reason: `Path ${userPath} resolves outside the workspace root ${root}` };
1913
- }
1914
- return { ok: true, path: target, relative: rel };
1915
- }
1916
- function patternEscapes(pattern) {
1917
- return isAbsolute(pattern) || pattern.split("/").includes("..") || pattern.split("\\").includes("..");
1918
- }
1919
-
1920
- // src/core/checkpoint.ts
2273
+ import path6 from "path";
1921
2274
  var VERSION = 1;
1922
2275
  var MAX_FILE_BYTES = 5e6;
1923
2276
  var MAX_CHECKPOINT_BYTES = 2e7;
@@ -1975,7 +2328,7 @@ function beginCheckpoint(options) {
1975
2328
  },
1976
2329
  markChanged(absolutePath) {
1977
2330
  if (finished) return;
1978
- const resolved = path5.resolve(absolutePath);
2331
+ const resolved = path6.resolve(absolutePath);
1979
2332
  let capturedEntry = [...captured.values()].find((entry) => entry.absolutePath === resolved);
1980
2333
  if (!capturedEntry) {
1981
2334
  const safe = resolveInside(root, absolutePath);
@@ -2006,12 +2359,12 @@ function beginCheckpoint(options) {
2006
2359
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
2007
2360
  entries
2008
2361
  };
2009
- const sessionDir = path5.join(storageDir, options.sessionId);
2362
+ const sessionDir = path6.join(storageDir, options.sessionId);
2010
2363
  await ensureDir(storageDir);
2011
2364
  await ensureDir(sessionDir);
2012
2365
  lastCheckpointTimestamp = Math.max(Date.now(), lastCheckpointTimestamp + 1);
2013
2366
  const id = `${String(lastCheckpointTimestamp).padStart(13, "0")}-${randomBytes2(4).toString("hex")}`;
2014
- await writeCheckpoint(path5.join(sessionDir, `${id}.json`), stored);
2367
+ await writeCheckpoint(path6.join(sessionDir, `${id}.json`), stored);
2015
2368
  pruneCheckpoints(sessionDir).catch(() => void 0);
2016
2369
  return { id, paths: entries.map((entry) => entry.path) };
2017
2370
  }
@@ -2020,7 +2373,7 @@ function beginCheckpoint(options) {
2020
2373
  async function undoLatestCheckpoint(options) {
2021
2374
  assertSessionId2(options.sessionId);
2022
2375
  const root = workspaceRoot(options.cwd);
2023
- const sessionDir = path5.join(options.storageDir ?? checkpointsDir, options.sessionId);
2376
+ const sessionDir = path6.join(options.storageDir ?? checkpointsDir, options.sessionId);
2024
2377
  const file = await newestCheckpoint(sessionDir);
2025
2378
  const empty = { found: false, restored: [], removed: [], conflicts: [], failed: [] };
2026
2379
  if (!file) return empty;
@@ -2076,7 +2429,7 @@ async function snapshotFile(file) {
2076
2429
  if (info.size > MAX_FILE_BYTES) {
2077
2430
  throw new Error(`Cannot checkpoint ${file}: it exceeds the ${MAX_FILE_BYTES / 1e6} MB limit.`);
2078
2431
  }
2079
- const data = await readFile4(file);
2432
+ const data = await readFile5(file);
2080
2433
  if (data.length > MAX_FILE_BYTES) {
2081
2434
  throw new Error(`Cannot checkpoint ${file}: it changed beyond the file-size limit while reading.`);
2082
2435
  }
@@ -2093,7 +2446,7 @@ async function fingerprint(file) {
2093
2446
  if (error.code === "ENOENT") return { existed: false };
2094
2447
  throw error;
2095
2448
  }
2096
- const hash = createHash("sha256");
2449
+ const hash = createHash2("sha256");
2097
2450
  if (!info.isFile()) {
2098
2451
  hash.update(`kitcode:${info.isDirectory() ? "directory" : "non-file"}`);
2099
2452
  return { existed: true, sha256: hash.digest("hex") };
@@ -2105,7 +2458,7 @@ function matchesSnapshot(before, after) {
2105
2458
  if (before.existed !== after.existed) return false;
2106
2459
  if (!before.existed) return true;
2107
2460
  const data = Buffer.from(before.data ?? "", "base64");
2108
- return after.sha256 === createHash("sha256").update(data).digest("hex");
2461
+ return after.sha256 === createHash2("sha256").update(data).digest("hex");
2109
2462
  }
2110
2463
  function sameFingerprint(a, b) {
2111
2464
  return a.existed === b.existed && (!a.existed || a.sha256 === b.sha256);
@@ -2114,14 +2467,14 @@ async function restoreFile(root, relativePath, snapshot, expected) {
2114
2467
  const safe = resolveInside(root, relativePath);
2115
2468
  if (!safe.ok || safe.relative !== relativePath) throw new Error("The restore path changed.");
2116
2469
  const data = decodeSnapshot(snapshot);
2117
- await mkdir3(path5.dirname(safe.path), { recursive: true });
2470
+ await mkdir3(path6.dirname(safe.path), { recursive: true });
2118
2471
  const rechecked = resolveInside(root, relativePath);
2119
2472
  if (!rechecked.ok || rechecked.path !== safe.path || rechecked.relative !== relativePath) {
2120
2473
  throw new Error("The restore path changed while preparing its parent directory.");
2121
2474
  }
2122
2475
  const temp = `${rechecked.path}.${process.pid}.${randomBytes2(4).toString("hex")}.undo`;
2123
2476
  try {
2124
- await writeFile4(temp, data, { mode: snapshot.mode ?? 384 });
2477
+ await writeFile5(temp, data, { mode: snapshot.mode ?? 384 });
2125
2478
  await chmod3(temp, snapshot.mode ?? 384);
2126
2479
  const finalPath = resolveInside(root, relativePath);
2127
2480
  if (!finalPath.ok || finalPath.path !== rechecked.path || finalPath.relative !== relativePath) {
@@ -2144,7 +2497,7 @@ async function writeCheckpoint(file, checkpoint) {
2144
2497
  }
2145
2498
  const temp = `${file}.${process.pid}.${randomBytes2(4).toString("hex")}.tmp`;
2146
2499
  try {
2147
- await writeFile4(temp, body, { encoding: "utf8", mode: 384 });
2500
+ await writeFile5(temp, body, { encoding: "utf8", mode: 384 });
2148
2501
  await chmod3(temp, 384);
2149
2502
  await rename4(temp, file);
2150
2503
  } catch (error) {
@@ -2157,7 +2510,7 @@ async function readCheckpoint(file) {
2157
2510
  if (info.size > MAX_CHECKPOINT_JSON_BYTES) throw new Error("Undo checkpoint is too large to read.");
2158
2511
  let parsed;
2159
2512
  try {
2160
- parsed = JSON.parse(await readFile4(file, "utf8"));
2513
+ parsed = JSON.parse(await readFile5(file, "utf8"));
2161
2514
  } catch (error) {
2162
2515
  throw new Error(`Undo checkpoint is not valid JSON: ${error.message}`);
2163
2516
  }
@@ -2173,7 +2526,7 @@ function isCheckpoint(value) {
2173
2526
  return item.entries.every((entry) => {
2174
2527
  if (typeof entry !== "object" || entry === null) return false;
2175
2528
  const candidate = entry;
2176
- if (typeof candidate.path !== "string" || candidate.path === "" || path5.isAbsolute(candidate.path) || typeof candidate.before !== "object" || candidate.before === null || typeof candidate.after !== "object" || candidate.after === null || typeof candidate.before.existed !== "boolean" || typeof candidate.after.existed !== "boolean") {
2529
+ if (typeof candidate.path !== "string" || candidate.path === "" || path6.isAbsolute(candidate.path) || typeof candidate.before !== "object" || candidate.before === null || typeof candidate.after !== "object" || candidate.after === null || typeof candidate.before.existed !== "boolean" || typeof candidate.after.existed !== "boolean") {
2177
2530
  return false;
2178
2531
  }
2179
2532
  if (candidate.before.existed) {
@@ -2205,12 +2558,12 @@ async function newestCheckpoint(sessionDir) {
2205
2558
  throw error;
2206
2559
  }
2207
2560
  const name = names.filter((candidate) => CHECKPOINT_NAME.test(candidate)).sort().at(-1);
2208
- return name ? path5.join(sessionDir, name) : null;
2561
+ return name ? path6.join(sessionDir, name) : null;
2209
2562
  }
2210
2563
  async function pruneCheckpoints(sessionDir) {
2211
2564
  const names = (await readdir2(sessionDir)).filter((candidate) => CHECKPOINT_NAME.test(candidate)).sort().reverse();
2212
2565
  await Promise.all(
2213
- names.slice(MAX_CHECKPOINTS_PER_SESSION).map((name) => unlink2(path5.join(sessionDir, name)))
2566
+ names.slice(MAX_CHECKPOINTS_PER_SESSION).map((name) => unlink2(path6.join(sessionDir, name)))
2214
2567
  );
2215
2568
  }
2216
2569
  function workspaceRoot(cwd) {
@@ -2225,26 +2578,18 @@ function assertSessionId2(id) {
2225
2578
  }
2226
2579
 
2227
2580
  // src/core/diagnostics.ts
2228
- import { readFile as readFile5, stat as stat4 } from "fs/promises";
2229
- import path6 from "path";
2230
-
2231
- // src/tools/bash.ts
2232
- import { spawn } from "child_process";
2233
-
2234
- // src/tools/summary.ts
2235
- function brief(value, max = 60) {
2236
- const text = String(value ?? "").replace(/\s+/g, " ").trim();
2237
- return text.length > max ? `${text.slice(0, max - 1)}\u2026` : text;
2238
- }
2581
+ import { readFile as readFile6, stat as stat4 } from "fs/promises";
2582
+ import path7 from "path";
2239
2583
 
2240
2584
  // src/tools/bash.ts
2585
+ import { spawn as spawn2 } from "child_process";
2241
2586
  var DEFAULT_TIMEOUT = 12e4;
2242
2587
  var MAX_TIMEOUT = 6e5;
2243
2588
  var MAX_OUTPUT = 1e5;
2244
2589
  var MAX_COMMAND_CHARS = 2e4;
2245
2590
  var bashTool = {
2246
2591
  name: "bash",
2247
- description: "Run a shell command in the workspace root. stdout and stderr are merged. Avoid commands that wait for interactive input.",
2592
+ description: "Run a shell command in the workspace root. stdout and stderr are merged. Avoid commands that wait for interactive input. Set background: true to keep a long command (dev server, test watcher, build) running while you carry on with other work, then collect its output with bash_output.",
2248
2593
  inputSchema: {
2249
2594
  type: "object",
2250
2595
  properties: {
@@ -2255,8 +2600,12 @@ var bashTool = {
2255
2600
  },
2256
2601
  timeoutMs: {
2257
2602
  type: "integer",
2258
- description: `Timeout in milliseconds (default ${DEFAULT_TIMEOUT}, maximum ${MAX_TIMEOUT})`,
2603
+ description: `Timeout in milliseconds (default ${DEFAULT_TIMEOUT}, maximum ${MAX_TIMEOUT}); ignored for background commands`,
2259
2604
  minimum: 1
2605
+ },
2606
+ background: {
2607
+ type: "boolean",
2608
+ description: "Start the command and return at once instead of waiting for it. Read its output later with bash_output. Background commands have no timeout and are killed when the session ends."
2260
2609
  }
2261
2610
  },
2262
2611
  required: ["command"],
@@ -2264,13 +2613,14 @@ var bashTool = {
2264
2613
  },
2265
2614
  defaultPermission: "ask",
2266
2615
  summarize(input) {
2267
- return `bash(${brief(input.command)})`;
2616
+ const { command, background } = input;
2617
+ return background ? `bash(${brief(command)}, background)` : `bash(${brief(command)})`;
2268
2618
  },
2269
2619
  async preview(input) {
2270
2620
  return { kind: "text", text: input.command };
2271
2621
  },
2272
2622
  execute(input, ctx) {
2273
- const { command, timeoutMs } = input;
2623
+ const { command, timeoutMs, background } = input;
2274
2624
  if (command.length > MAX_COMMAND_CHARS) {
2275
2625
  return Promise.resolve({
2276
2626
  content: `Command exceeds the ${MAX_COMMAND_CHARS} character limit.`,
@@ -2282,8 +2632,17 @@ var bashTool = {
2282
2632
  if (ctx.signal.aborted) {
2283
2633
  return Promise.resolve({ content: "[cancelled before the command started]", isError: true });
2284
2634
  }
2635
+ if (background) {
2636
+ const started = startJob(command, ctx.cwd);
2637
+ if ("error" in started) {
2638
+ return Promise.resolve({ content: started.error, isError: true });
2639
+ }
2640
+ return Promise.resolve({
2641
+ content: `[started in the background as ${started.id}] Read its output with bash_output({ id: "${started.id}" }).`
2642
+ });
2643
+ }
2285
2644
  return new Promise((settle) => {
2286
- const child = spawn(command, {
2645
+ const child = spawn2(command, {
2287
2646
  shell: true,
2288
2647
  cwd: ctx.cwd,
2289
2648
  detached: process.platform !== "win32",
@@ -2311,11 +2670,11 @@ var bashTool = {
2311
2670
  };
2312
2671
  child.stdout.on("data", collect);
2313
2672
  child.stderr.on("data", collect);
2314
- const killTree = () => {
2673
+ const killTree2 = () => {
2315
2674
  if (child.pid == null) return;
2316
2675
  if (process.platform === "win32") {
2317
2676
  try {
2318
- spawn("taskkill", ["/pid", String(child.pid), "/t", "/f"], { stdio: "ignore" });
2677
+ spawn2("taskkill", ["/pid", String(child.pid), "/t", "/f"], { stdio: "ignore" });
2319
2678
  } catch {
2320
2679
  child.kill("SIGKILL");
2321
2680
  }
@@ -2331,11 +2690,11 @@ var bashTool = {
2331
2690
  let aborted = false;
2332
2691
  const timer = setTimeout(() => {
2333
2692
  timedOut = true;
2334
- killTree();
2693
+ killTree2();
2335
2694
  }, timeout);
2336
2695
  const onAbort = () => {
2337
2696
  aborted = true;
2338
- killTree();
2697
+ killTree2();
2339
2698
  };
2340
2699
  ctx.signal.addEventListener("abort", onAbort, { once: true });
2341
2700
  const finish = (result) => {
@@ -2377,7 +2736,7 @@ async function detectDiagnosticCommands(cwd, configured = []) {
2377
2736
  const custom = configured.map((command) => command.trim()).filter(Boolean);
2378
2737
  if (custom.length > 0) return custom.slice(0, 8);
2379
2738
  const commands = [];
2380
- const packageJson = await readPackageJson(path6.join(cwd, "package.json"));
2739
+ const packageJson = await readPackageJson(path7.join(cwd, "package.json"));
2381
2740
  if (packageJson) {
2382
2741
  const manager = await packageManager(cwd, packageJson.packageManager);
2383
2742
  for (const name of ["lint", "typecheck", "check", "test"]) {
@@ -2386,31 +2745,31 @@ async function detectDiagnosticCommands(cwd, configured = []) {
2386
2745
  }
2387
2746
  }
2388
2747
  }
2389
- if (await isFile2(path6.join(cwd, "Cargo.toml"))) {
2748
+ if (await isFile2(path7.join(cwd, "Cargo.toml"))) {
2390
2749
  commands.push("cargo check", "cargo test");
2391
2750
  }
2392
- if (await isFile2(path6.join(cwd, "go.mod"))) commands.push("go test ./...");
2393
- if (await isFile2(path6.join(cwd, "pyproject.toml")) || await isFile2(path6.join(cwd, "pytest.ini")) || await isFile2(path6.join(cwd, "tox.ini"))) {
2751
+ if (await isFile2(path7.join(cwd, "go.mod"))) commands.push("go test ./...");
2752
+ if (await isFile2(path7.join(cwd, "pyproject.toml")) || await isFile2(path7.join(cwd, "pytest.ini")) || await isFile2(path7.join(cwd, "tox.ini"))) {
2394
2753
  commands.push("python -m pytest");
2395
2754
  }
2396
2755
  return [...new Set(commands)].slice(0, 8);
2397
2756
  }
2398
2757
  async function runAutomaticDiagnostics(options) {
2399
2758
  const commands = await detectDiagnosticCommands(options.cwd, options.configuredCommands);
2400
- const summary = { commands, failed: [], skipped: false };
2759
+ const summary2 = { commands, failed: [], skipped: false };
2401
2760
  if (options.changedFiles.length === 0 || commands.length === 0 || options.signal.aborted) {
2402
- summary.skipped = true;
2403
- return summary;
2761
+ summary2.skipped = true;
2762
+ return summary2;
2404
2763
  }
2405
2764
  const gate = options.permissions.decide(diagnosticsTool);
2406
2765
  if (gate === "deny") {
2407
- summary.skipped = true;
2766
+ summary2.skipped = true;
2408
2767
  options.hooks.onEvent({
2409
2768
  type: "notice",
2410
2769
  level: "warn",
2411
2770
  text: "Automatic project checks were skipped by the current permission settings."
2412
2771
  });
2413
- return summary;
2772
+ return summary2;
2414
2773
  }
2415
2774
  if (gate === "ask") {
2416
2775
  const decision = await options.hooks.requestPermission({
@@ -2425,17 +2784,17 @@ async function runAutomaticDiagnostics(options) {
2425
2784
  allowAlways: true
2426
2785
  });
2427
2786
  if (options.signal.aborted) {
2428
- summary.skipped = true;
2429
- return summary;
2787
+ summary2.skipped = true;
2788
+ return summary2;
2430
2789
  }
2431
2790
  if (decision === "deny") {
2432
- summary.skipped = true;
2791
+ summary2.skipped = true;
2433
2792
  options.hooks.onEvent({
2434
2793
  type: "notice",
2435
2794
  level: "warn",
2436
2795
  text: "Automatic project checks were skipped."
2437
2796
  });
2438
- return summary;
2797
+ return summary2;
2439
2798
  }
2440
2799
  if (decision === "always") options.permissions.grantForSession(diagnosticsTool.name);
2441
2800
  }
@@ -2448,7 +2807,7 @@ async function runAutomaticDiagnostics(options) {
2448
2807
  const execute = options.execute ?? ((command, context) => bashTool.execute({ command, timeoutMs: DIAGNOSTIC_TIMEOUT_MS }, context));
2449
2808
  for (const [index, command] of commands.entries()) {
2450
2809
  if (options.signal.aborted) {
2451
- summary.skipped = true;
2810
+ summary2.skipped = true;
2452
2811
  break;
2453
2812
  }
2454
2813
  const id = `diagnostics-${Date.now()}-${index}`;
@@ -2456,7 +2815,7 @@ async function runAutomaticDiagnostics(options) {
2456
2815
  options.hooks.onEvent({ type: "tool_start", id, name: diagnosticsTool.name, summary: callSummary });
2457
2816
  try {
2458
2817
  const result = await execute(command, ctx);
2459
- if (result.isError) summary.failed.push(command);
2818
+ if (result.isError) summary2.failed.push(command);
2460
2819
  options.hooks.onEvent({
2461
2820
  type: "tool_end",
2462
2821
  id,
@@ -2465,7 +2824,7 @@ async function runAutomaticDiagnostics(options) {
2465
2824
  display: result.display
2466
2825
  });
2467
2826
  } catch (error) {
2468
- summary.failed.push(command);
2827
+ summary2.failed.push(command);
2469
2828
  options.hooks.onEvent({
2470
2829
  type: "tool_end",
2471
2830
  id,
@@ -2474,13 +2833,13 @@ async function runAutomaticDiagnostics(options) {
2474
2833
  });
2475
2834
  }
2476
2835
  }
2477
- return summary;
2836
+ return summary2;
2478
2837
  }
2479
2838
  async function readPackageJson(file) {
2480
2839
  try {
2481
2840
  const info = await stat4(file);
2482
2841
  if (!info.isFile() || info.size > MAX_PACKAGE_BYTES) return null;
2483
- const value = JSON.parse(await readFile5(file, "utf8"));
2842
+ const value = JSON.parse(await readFile6(file, "utf8"));
2484
2843
  if (typeof value !== "object" || value === null) return null;
2485
2844
  const candidate = value;
2486
2845
  return {
@@ -2494,9 +2853,9 @@ async function readPackageJson(file) {
2494
2853
  async function packageManager(cwd, declared) {
2495
2854
  const name = declared?.split("@")[0];
2496
2855
  if (name === "pnpm" || name === "yarn" || name === "bun" || name === "npm") return name;
2497
- if (await isFile2(path6.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
2498
- if (await isFile2(path6.join(cwd, "yarn.lock"))) return "yarn";
2499
- if (await isFile2(path6.join(cwd, "bun.lock")) || await isFile2(path6.join(cwd, "bun.lockb"))) {
2856
+ if (await isFile2(path7.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
2857
+ if (await isFile2(path7.join(cwd, "yarn.lock"))) return "yarn";
2858
+ if (await isFile2(path7.join(cwd, "bun.lock")) || await isFile2(path7.join(cwd, "bun.lockb"))) {
2500
2859
  return "bun";
2501
2860
  }
2502
2861
  return "npm";
@@ -2514,9 +2873,9 @@ async function isFile2(file) {
2514
2873
 
2515
2874
  // src/core/attachments.ts
2516
2875
  import { execFile } from "child_process";
2517
- import { lstat as lstat2, readdir as readdir3, readFile as readFile6, stat as stat5 } from "fs/promises";
2876
+ import { lstat as lstat2, readdir as readdir3, readFile as readFile7, stat as stat5 } from "fs/promises";
2518
2877
  import os2 from "os";
2519
- import path7 from "path";
2878
+ import path8 from "path";
2520
2879
  import { fileURLToPath } from "url";
2521
2880
  var MAX_IMAGE_BYTES = 10 * 1024 * 1024;
2522
2881
  var MAX_TEXT_BYTES = 256 * 1024;
@@ -2547,7 +2906,7 @@ async function loadAttachment(cwd, requestedPath) {
2547
2906
  const resolved = resolveAttachmentPath(cwd, requestedPath);
2548
2907
  const linkInfo = await lstat2(resolved).catch(() => null);
2549
2908
  if (linkInfo?.isSymbolicLink()) {
2550
- throw new Error(`Cannot attach symbolic links: ${path7.basename(resolved)}`);
2909
+ throw new Error(`Cannot attach symbolic links: ${path8.basename(resolved)}`);
2551
2910
  }
2552
2911
  const info = await stat5(resolved).catch((error) => {
2553
2912
  if (error.code === "ENOENT") throw new Error(`Attachment not found: ${resolved}`);
@@ -2556,7 +2915,7 @@ async function loadAttachment(cwd, requestedPath) {
2556
2915
  if (info.isDirectory()) {
2557
2916
  const attachment = await loadFirstImageFromDirectory(resolved);
2558
2917
  if (attachment) return attachment;
2559
- throw new Error(`No supported images found in directory: ${path7.basename(resolved)}`);
2918
+ throw new Error(`No supported images found in directory: ${path8.basename(resolved)}`);
2560
2919
  }
2561
2920
  if (!info.isFile()) throw new Error(`Attachment is not a regular file: ${resolved}`);
2562
2921
  return loadResolvedAttachment(resolved, info.size);
@@ -2566,7 +2925,7 @@ async function loadAutomaticAttachment(cwd, requestedPath) {
2566
2925
  const resolved = resolveAttachmentPath(cwd, requestedPath);
2567
2926
  if (isSensitiveAutomaticPath(resolved)) {
2568
2927
  throw new Error(
2569
- `For safety, sensitive-looking files must be attached explicitly with /attach: ${path7.basename(resolved)}`
2928
+ `For safety, sensitive-looking files must be attached explicitly with /attach: ${path8.basename(resolved)}`
2570
2929
  );
2571
2930
  }
2572
2931
  const linkInfo = await lstat2(resolved).catch(() => null);
@@ -2586,14 +2945,14 @@ function looksLikeAttachmentPath(value) {
2586
2945
  if (trimmed.length > MAX_PATH_CHARS) return false;
2587
2946
  const candidate = normalizeInputPath(trimmed);
2588
2947
  if (/^file:\/\//i.test(candidate)) return true;
2589
- if (path7.isAbsolute(candidate)) return true;
2948
+ if (path8.isAbsolute(candidate)) return true;
2590
2949
  if (/^(?:~|\.{1,2})[\\/]/.test(candidate)) return true;
2591
2950
  if (candidate.includes("/") || candidate.includes("\\")) {
2592
2951
  if (trimmed.length > 200) return false;
2593
2952
  return true;
2594
2953
  }
2595
- const basename3 = path7.basename(candidate).toLowerCase();
2596
- return path7.extname(basename3) !== "" || AUTO_PATH_NAMES.has(basename3);
2954
+ const basename3 = path8.basename(candidate).toLowerCase();
2955
+ return path8.extname(basename3) !== "" || AUTO_PATH_NAMES.has(basename3);
2597
2956
  }
2598
2957
  async function loadClipboardImage(platform = process.platform, runner = runClipboardCommand) {
2599
2958
  const commands = clipboardCommands(platform);
@@ -2625,12 +2984,12 @@ async function loadClipboardImage(platform = process.platform, runner = runClipb
2625
2984
  throw new Error(`No supported image found in the clipboard. ${hint}`);
2626
2985
  }
2627
2986
  async function loadResolvedAttachment(resolved, size) {
2628
- const name = safeName(path7.basename(resolved));
2987
+ const name = safeName(path8.basename(resolved));
2629
2988
  const imageLimitCandidate = size <= MAX_IMAGE_BYTES;
2630
2989
  if (!imageLimitCandidate) {
2631
2990
  throw new Error(`Attachment is larger than ${formatBytes(MAX_IMAGE_BYTES)}: ${name}`);
2632
2991
  }
2633
- const data = await readFile6(resolved);
2992
+ const data = await readFile7(resolved);
2634
2993
  const mediaType = detectImage(data);
2635
2994
  if (mediaType) {
2636
2995
  return {
@@ -2659,10 +3018,10 @@ async function loadFirstImageFromDirectory(dirPath) {
2659
3018
  const IMAGE_EXTENSIONS = /* @__PURE__ */ new Set([".png", ".jpg", ".jpeg", ".gif", ".webp", ".bmp"]);
2660
3019
  const entries = await readdir3(dirPath, { withFileTypes: true }).catch(() => null);
2661
3020
  if (!entries) return null;
2662
- const sorted = entries.filter((entry) => entry.isFile() && IMAGE_EXTENSIONS.has(path7.extname(entry.name).toLowerCase())).sort((a, b) => a.name.localeCompare(b.name));
3021
+ const sorted = entries.filter((entry) => entry.isFile() && IMAGE_EXTENSIONS.has(path8.extname(entry.name).toLowerCase())).sort((a, b) => a.name.localeCompare(b.name));
2663
3022
  if (sorted.length === 0) return null;
2664
3023
  const first2 = sorted[0];
2665
- const filePath = path7.join(dirPath, first2.name);
3024
+ const filePath = path8.join(dirPath, first2.name);
2666
3025
  const info = await stat5(filePath).catch(() => null);
2667
3026
  if (!info?.isFile()) return null;
2668
3027
  return loadResolvedAttachment(filePath, info.size);
@@ -2672,17 +3031,17 @@ function resolveAttachmentPath(cwd, requestedPath) {
2672
3031
  if (!cleaned) throw new Error("Give a file path: /attach <path>");
2673
3032
  if (/^file:\/\//i.test(cleaned)) {
2674
3033
  try {
2675
- return path7.normalize(fileURLToPath(cleaned));
3034
+ return path8.normalize(fileURLToPath(cleaned));
2676
3035
  } catch {
2677
3036
  throw new Error(`Invalid local file URL: ${cleaned}`);
2678
3037
  }
2679
3038
  }
2680
- const expanded = cleaned === "~" ? os2.homedir() : cleaned.startsWith("~/") ? path7.join(os2.homedir(), cleaned.slice(2)) : cleaned;
2681
- return path7.isAbsolute(expanded) ? path7.normalize(expanded) : path7.resolve(cwd, expanded);
3039
+ const expanded = cleaned === "~" ? os2.homedir() : cleaned.startsWith("~/") ? path8.join(os2.homedir(), cleaned.slice(2)) : cleaned;
3040
+ return path8.isAbsolute(expanded) ? path8.normalize(expanded) : path8.resolve(cwd, expanded);
2682
3041
  }
2683
3042
  function isSensitiveAutomaticPath(file) {
2684
- const basename3 = path7.basename(file).toLowerCase();
2685
- return basename3.startsWith(".env.") || SENSITIVE_AUTO_NAMES.has(basename3) || SENSITIVE_AUTO_EXTENSIONS.has(path7.extname(basename3));
3043
+ const basename3 = path8.basename(file).toLowerCase();
3044
+ return basename3.startsWith(".env.") || SENSITIVE_AUTO_NAMES.has(basename3) || SENSITIVE_AUTO_EXTENSIONS.has(path8.extname(basename3));
2686
3045
  }
2687
3046
  function attachmentLabel(block) {
2688
3047
  if (block.type === "image") return `image: ${block.name}`;
@@ -2827,7 +3186,7 @@ try {
2827
3186
  }
2828
3187
  `;
2829
3188
  function textMime(file) {
2830
- const extension = path7.extname(file).toLowerCase();
3189
+ const extension = path8.extname(file).toLowerCase();
2831
3190
  const known = {
2832
3191
  ".json": "application/json",
2833
3192
  ".md": "text/markdown",
@@ -2880,8 +3239,8 @@ var SINGLE_MAX_TOKENS = 768;
2880
3239
  var CHUNK_MAX_TOKENS = 512;
2881
3240
  var MERGE_MAX_TOKENS = 1024;
2882
3241
  var CHUNK_CONCURRENCY = 3;
2883
- var SYSTEM_SUMMARIZE = "You are summarizing a coding conversation so another agent can continue the work. Extract and preserve ONLY: concrete requirements, user preferences, files changed, commands run and their results, errors encountered, bugs found, architectural decisions, and unfinished work. Be terse and factual. Do NOT include greetings, acknowledgments, chain-of-thought, or step-by-step narration. Return only the summary.";
2884
- var SYSTEM_MERGE = "Merge these partial summaries of a coding conversation into one coherent summary. Remove duplicates. Preserve: requirements, files changed, commands run, errors, decisions, and unfinished work. Be terse. Return only the merged summary.";
3242
+ var SYSTEM_SUMMARIZE = "You are summarizing a coding conversation so another agent can continue the work. Extract and preserve ONLY: concrete requirements, user preferences, files changed, commands run and their results, errors encountered, bugs found, architectural decisions, and unfinished work. Be terse and factual. Do NOT include greetings, acknowledgments, chain-of-thought, or step-by-step narration. Use these sections: Requirements; Verified facts (with file paths or tool evidence); Changes made; Verification (exact commands, pass/fail/not run, and unresolved error excerpts); Decisions and assumptions; Remaining work. Keep plans and assumptions separate from verified facts. Never turn an intended command into a completed check. When checks conflict, retain the latest result and mark earlier results as superseded. Preserve explicit user corrections and project constraints. Do not retain credentials or secret values. Return only the summary.";
3243
+ var SYSTEM_MERGE = "Merge these partial summaries of a coding conversation into one coherent summary. Remove duplicates. Preserve: requirements, files changed, commands run, errors, decisions, and unfinished work. Keep the structured sections and evidence from the source summaries. Separate verified facts, assumptions, and planned work; keep exact verification commands and their latest outcomes. Do not resolve conflicting evidence by guessing. Do not retain credentials or secret values. Be terse. Return only the merged summary.";
2885
3244
  async function compactHistory(options) {
2886
3245
  const cut = compactCutIndex(options.history);
2887
3246
  if (cut <= 0) {
@@ -2893,7 +3252,7 @@ async function compactHistory(options) {
2893
3252
  if (!rendered.trim()) {
2894
3253
  return { history: options.history, compacted: false, removedMessages: 0 };
2895
3254
  }
2896
- let summary;
3255
+ let summary2;
2897
3256
  let usage;
2898
3257
  let rateLimits;
2899
3258
  if (rendered.length <= SINGLE_REQUEST_MAX_CHARS) {
@@ -2907,7 +3266,7 @@ async function compactHistory(options) {
2907
3266
  SYSTEM_SUMMARIZE,
2908
3267
  requestMax
2909
3268
  );
2910
- summary = result.text;
3269
+ summary2 = result.text;
2911
3270
  usage = result.usage;
2912
3271
  rateLimits = result.rateLimits;
2913
3272
  } else {
@@ -2942,19 +3301,19 @@ async function compactHistory(options) {
2942
3301
  chunkResults.map((result) => result.text),
2943
3302
  limits.merge
2944
3303
  );
2945
- summary = mergeResult.text;
3304
+ summary2 = mergeResult.text;
2946
3305
  if (mergeResult.usage) {
2947
3306
  usage = usage ? mergeUsage([usage, mergeResult.usage]) : mergeResult.usage;
2948
3307
  }
2949
3308
  if (mergeResult.rateLimits) rateLimits = mergeResult.rateLimits;
2950
3309
  }
2951
- summary = summary.trim();
2952
- if (!summary) throw new Error("The provider returned an empty context summary.");
3310
+ summary2 = summary2.trim();
3311
+ if (!summary2) throw new Error("The provider returned an empty context summary.");
2953
3312
  const prefix = [
2954
3313
  {
2955
3314
  role: "user",
2956
3315
  content: [{ type: "text", text: `[Earlier conversation summary]
2957
- ${summary}` }]
3316
+ ${summary2}` }]
2958
3317
  },
2959
3318
  {
2960
3319
  role: "assistant",
@@ -3098,8 +3457,8 @@ async function mergePartialSummaries(provider, model, maxTokens, signal, summari
3098
3457
  while (current.length > 1) {
3099
3458
  const groups = groupSummaries(current, MERGE_TARGET_CHARS);
3100
3459
  const sources = groups.map(
3101
- (group) => group.map((summary, index) => `## Part ${index + 1}
3102
- ${summary}`).join("\n\n")
3460
+ (group) => group.map((summary2, index) => `## Part ${index + 1}
3461
+ ${summary2}`).join("\n\n")
3103
3462
  );
3104
3463
  const merged = await summarizeSourcesParallel(
3105
3464
  provider,
@@ -3158,22 +3517,22 @@ function groupSummaries(summaries, targetChars) {
3158
3517
  const groups = [];
3159
3518
  let current = [];
3160
3519
  let size = 0;
3161
- for (const summary of summaries) {
3162
- if (current.length >= 2 && size + 12 + summary.length > targetChars) {
3520
+ for (const summary2 of summaries) {
3521
+ if (current.length >= 2 && size + 12 + summary2.length > targetChars) {
3163
3522
  groups.push(current);
3164
3523
  current = [];
3165
3524
  size = 0;
3166
3525
  }
3167
3526
  const separator = current.length > 0 ? 12 : 0;
3168
- current.push(summary);
3169
- size += separator + summary.length;
3527
+ current.push(summary2);
3528
+ size += separator + summary2.length;
3170
3529
  }
3171
3530
  if (current.length > 0) groups.push(current);
3172
3531
  return groups;
3173
3532
  }
3174
3533
  async function summarizeText(provider, model, maxTokens, signal, source, systemPrompt, maxOutputTokens) {
3175
3534
  if (signal.aborted) throw signal.reason ?? new Error("Context compaction was cancelled.");
3176
- let summary = "";
3535
+ let summary2 = "";
3177
3536
  let finalText2 = "";
3178
3537
  let usage;
3179
3538
  let rateLimits;
@@ -3187,7 +3546,7 @@ async function summarizeText(provider, model, maxTokens, signal, source, systemP
3187
3546
  signal
3188
3547
  });
3189
3548
  for await (const event of stream) {
3190
- if (event.type === "text_delta") summary += event.text;
3549
+ if (event.type === "text_delta") summary2 += event.text;
3191
3550
  else if (event.type === "usage") usage = event.usage;
3192
3551
  else if (event.type === "rate_limits") rateLimits = event.limits;
3193
3552
  else if (event.type === "done") {
@@ -3195,7 +3554,7 @@ async function summarizeText(provider, model, maxTokens, signal, source, systemP
3195
3554
  }
3196
3555
  }
3197
3556
  if (signal.aborted) throw signal.reason ?? new Error("Context compaction was cancelled.");
3198
- return { text: (finalText2 || summary).trim(), usage, rateLimits };
3557
+ return { text: (finalText2 || summary2).trim(), usage, rateLimits };
3199
3558
  }
3200
3559
  function mergeUsage(usages) {
3201
3560
  return usages.reduce(
@@ -3283,6 +3642,12 @@ function buildSystemPrompt(args) {
3283
3642
  if (args.skills && args.skills.trim() !== "") {
3284
3643
  lines.push("", args.skills);
3285
3644
  }
3645
+ if (args.toolNames.includes("memory")) {
3646
+ lines.push("", "Use memory to retain explicit user preferences, verified project facts and accepted decisions across chats. Include the source. Read existing notes before replacing or deleting them; correct stale notes rather than adding contradictions. Keep temporary progress in the conversation, not permanent memory. Never store credentials. Tell the user briefly when you update memory.");
3647
+ }
3648
+ if (args.memory?.trim()) {
3649
+ lines.push("", "Project notes saved in earlier conversations. Treat these as potentially stale context; verify claims against current files and follow current user instructions.", args.memory);
3650
+ }
3286
3651
  return lines.join("\n");
3287
3652
  }
3288
3653
 
@@ -3533,7 +3898,7 @@ function clip(value) {
3533
3898
  // package.json
3534
3899
  var package_default = {
3535
3900
  name: "@kernelonpanic/kitcode",
3536
- version: "1.2.9",
3901
+ version: "1.3.1",
3537
3902
  description: "Terminal coding agent with a config you never have to write by hand",
3538
3903
  type: "module",
3539
3904
  license: "MIT",
@@ -3601,7 +3966,7 @@ var package_default = {
3601
3966
 
3602
3967
  // src/version.ts
3603
3968
  var KITCODE_VERSION = package_default.version;
3604
- var KITCODE_COMMIT = true ? "cc499fc443e73408f49b928f204b77c48e9d2d05" : "development";
3969
+ var KITCODE_COMMIT = true ? "4320a2bd340f59e42397dd4afbfde0051ddfeb27" : "development";
3605
3970
 
3606
3971
  // src/mcp/client.ts
3607
3972
  var clientInfo = { name: "kitcode", version: KITCODE_VERSION };
@@ -3799,8 +4164,8 @@ function describeFailure2(error, stderr, secrets) {
3799
4164
  return redactSecrets(stderr ? `${message} \u2014 ${stderr}` : message, secrets);
3800
4165
  }
3801
4166
  function configuredSecrets(config) {
3802
- const record2 = config.type === "stdio" ? config.env : config.headers;
3803
- return Object.values(expandEnvRecord(record2)).filter((value) => value.length >= 4);
4167
+ const record3 = config.type === "stdio" ? config.env : config.headers;
4168
+ return Object.values(expandEnvRecord(record3)).filter((value) => value.length >= 4);
3804
4169
  }
3805
4170
  function inheritedEnv(source = process.env) {
3806
4171
  const env = {};
@@ -3892,14 +4257,14 @@ function balanceEndpoints(baseUrl) {
3892
4257
  endpoint(url.origin, "/v1/balance", genericBalance)
3893
4258
  ];
3894
4259
  }
3895
- function endpoint(origin, path14, parse2) {
3896
- return { url: `${origin}${path14}`, parse: parse2 };
4260
+ function endpoint(origin, path15, parse2) {
4261
+ return { url: `${origin}${path15}`, parse: parse2 };
3897
4262
  }
3898
4263
  function deepSeekBalance(body) {
3899
- const infos = record(body)?.["balance_infos"];
4264
+ const infos = record2(body)?.["balance_infos"];
3900
4265
  if (!Array.isArray(infos)) return null;
3901
4266
  const balances = infos.flatMap((item) => {
3902
- const entry = record(item);
4267
+ const entry = record2(item);
3903
4268
  const currency = currencyCode(entry?.["currency"]);
3904
4269
  const amount = decimal(entry?.["total_balance"]);
3905
4270
  return currency && amount ? [{ kind: "balance", currency, amount }] : [];
@@ -3907,11 +4272,11 @@ function deepSeekBalance(body) {
3907
4272
  return balances.length > 0 ? balances : null;
3908
4273
  }
3909
4274
  function openRouterKeyBalance(body) {
3910
- const remaining = finiteNumber(record(record(body)?.["data"])?.["limit_remaining"]);
4275
+ const remaining = finiteNumber(record2(record2(body)?.["data"])?.["limit_remaining"]);
3911
4276
  return remaining === null ? null : [{ kind: "key-limit", currency: "USD", amount: String(remaining) }];
3912
4277
  }
3913
4278
  function openRouterCreditsBalance(body) {
3914
- const data = record(record(body)?.["data"]);
4279
+ const data = record2(record2(body)?.["data"]);
3915
4280
  const credits = finiteNumber(data?.["total_credits"]);
3916
4281
  const usage = finiteNumber(data?.["total_usage"]);
3917
4282
  if (credits === null || usage === null) return null;
@@ -3924,26 +4289,26 @@ function openRouterCreditsBalance(body) {
3924
4289
  ];
3925
4290
  }
3926
4291
  function siliconFlowBalance(body) {
3927
- const root = record(body);
4292
+ const root = record2(body);
3928
4293
  if (root?.["status"] !== true) return null;
3929
- const amount = decimal(record(root["data"])?.["balance"]);
4294
+ const amount = decimal(record2(root["data"])?.["balance"]);
3930
4295
  return amount ? [{ kind: "balance", currency: "credits", amount }] : null;
3931
4296
  }
3932
4297
  function apiMartBalance(body) {
3933
- const root = record(body);
4298
+ const root = record2(body);
3934
4299
  if (root?.["success"] !== true) return null;
3935
4300
  const amount = decimal(root["remain_balance"]);
3936
4301
  return amount ? [{ kind: "balance", currency: "credits", amount }] : null;
3937
4302
  }
3938
4303
  function genericBalance(body) {
3939
- const root = record(body);
3940
- const data = record(root?.["data"]);
4304
+ const root = record2(body);
4305
+ const data = record2(root?.["data"]);
3941
4306
  const amount = decimal(root?.["balance"] ?? data?.["balance"]);
3942
4307
  if (!amount) return null;
3943
4308
  const currency = currencyCode(root?.["currency"] ?? data?.["currency"]) ?? "credits";
3944
4309
  return [{ kind: "balance", currency, amount }];
3945
4310
  }
3946
- function record(value) {
4311
+ function record2(value) {
3947
4312
  return typeof value === "object" && value !== null ? value : null;
3948
4313
  }
3949
4314
  function currencyCode(value) {
@@ -3971,8 +4336,8 @@ async function limitedResponseText(response, limit) {
3971
4336
  let text = "";
3972
4337
  try {
3973
4338
  while (true) {
3974
- const { done, value } = await reader.read();
3975
- if (done) return text + decoder.decode();
4339
+ const { done: done2, value } = await reader.read();
4340
+ if (done2) return text + decoder.decode();
3976
4341
  size += value.byteLength;
3977
4342
  if (size > limit) {
3978
4343
  await reader.cancel();
@@ -3986,21 +4351,21 @@ async function limitedResponseText(response, limit) {
3986
4351
  }
3987
4352
 
3988
4353
  // src/providers/catalog.ts
3989
- import { chmod as chmod4, readFile as readFile7, writeFile as writeFile5 } from "fs/promises";
3990
- import path8 from "path";
4354
+ import { chmod as chmod4, readFile as readFile8, writeFile as writeFile6 } from "fs/promises";
4355
+ import path9 from "path";
3991
4356
  var MAX_AGE_MS = 24 * 60 * 60 * 1e3;
3992
4357
  var CACHE_VERSION = 3;
3993
4358
  async function loadModels(provider, refresh = false) {
3994
4359
  const file = cacheFile(provider.id);
3995
- const cached = await readCache(file);
3996
- if (!refresh && cached?.version === CACHE_VERSION && Date.now() - cached.fetchedAt < MAX_AGE_MS) {
3997
- return cached.models;
4360
+ const cached2 = await readCache(file);
4361
+ if (!refresh && cached2?.version === CACHE_VERSION && Date.now() - cached2.fetchedAt < MAX_AGE_MS) {
4362
+ return cached2.models;
3998
4363
  }
3999
4364
  let models;
4000
4365
  try {
4001
4366
  models = await provider.listModels();
4002
4367
  } catch (error) {
4003
- if (cached) return cached.models;
4368
+ if (cached2) return cached2.models;
4004
4369
  const known = provider.knownModels();
4005
4370
  if (known.length > 0) return known;
4006
4371
  throw error;
@@ -4008,12 +4373,15 @@ async function loadModels(provider, refresh = false) {
4008
4373
  await writeCache(file, models);
4009
4374
  return models;
4010
4375
  }
4376
+ function refreshModels(provider) {
4377
+ return loadModels(provider, true);
4378
+ }
4011
4379
  function cacheFile(providerId) {
4012
- return path8.join(cacheDir, "models", `${providerId.replace(/[^\w.-]/g, "_")}.json`);
4380
+ return path9.join(cacheDir, "models", `${providerId.replace(/[^\w.-]/g, "_")}.json`);
4013
4381
  }
4014
4382
  async function readCache(file) {
4015
4383
  try {
4016
- const parsed = JSON.parse(await readFile7(file, "utf8"));
4384
+ const parsed = JSON.parse(await readFile8(file, "utf8"));
4017
4385
  if (typeof parsed !== "object" || parsed === null) return void 0;
4018
4386
  const candidate = parsed;
4019
4387
  if (typeof candidate.fetchedAt !== "number" || !Number.isFinite(candidate.fetchedAt) || !Array.isArray(candidate.models) || candidate.models.length > 1e4 || !candidate.models.every(validModel)) {
@@ -4044,8 +4412,8 @@ function validModel(value) {
4044
4412
  async function writeCache(file, models) {
4045
4413
  const payload = { version: CACHE_VERSION, fetchedAt: Date.now(), models };
4046
4414
  try {
4047
- await ensureDir(path8.dirname(file));
4048
- await writeFile5(file, JSON.stringify(payload), { encoding: "utf8", mode: 384 });
4415
+ await ensureDir(path9.dirname(file));
4416
+ await writeFile6(file, JSON.stringify(payload), { encoding: "utf8", mode: 384 });
4049
4417
  await chmod4(file, 384);
4050
4418
  } catch {
4051
4419
  return;
@@ -4194,7 +4562,8 @@ async function* streamTurn(client, providerId, apiKey2, req) {
4194
4562
  tools: toToolParams(req.tools)
4195
4563
  };
4196
4564
  if (req.thinking) params.thinking = { type: "adaptive", display: "summarized" };
4197
- if (req.effort) params.output_config = { effort: req.effort };
4565
+ const effort = resolveEffort(req.effort, req.messages);
4566
+ if (effort) params.output_config = { effort };
4198
4567
  let thinking = "";
4199
4568
  let text = "";
4200
4569
  let events = 0;
@@ -4382,11 +4751,13 @@ async function* streamTurn2(client, providerId, apiKey2, req) {
4382
4751
  let recognised = 0;
4383
4752
  const calls = /* @__PURE__ */ new Map();
4384
4753
  const capture = captureResponseHead();
4754
+ const effort = openAiEffort(req.model, resolveEffort(req.effort, req.messages));
4385
4755
  try {
4386
4756
  const stream = await client.withOptions({ fetch: capture.fetch }).chat.completions.create(
4387
4757
  {
4388
4758
  model: req.model,
4389
4759
  max_tokens: req.maxTokens,
4760
+ ...effort ? { reasoning_effort: effort } : {},
4390
4761
  messages: toChatMessages(req.system, req.messages),
4391
4762
  ...req.tools.length > 0 ? { tools: toChatTools(req.tools) } : {},
4392
4763
  stream: true,
@@ -4589,8 +4960,8 @@ function normalizeOpenAiUsage(usage) {
4589
4960
  function createRegistry(config, auth) {
4590
4961
  const built = /* @__PURE__ */ new Map();
4591
4962
  function get(id) {
4592
- const cached = built.get(id);
4593
- if (cached) return cached;
4963
+ const cached2 = built.get(id);
4964
+ if (cached2) return cached2;
4594
4965
  const entry = config.providers[id];
4595
4966
  if (!entry) throw new Error(unknownProviderMessage(id, Object.keys(config.providers)));
4596
4967
  const provider = build(id, entry, apiKey(id, entry, auth));
@@ -4682,13 +5053,66 @@ function isFileEdit(tool) {
4682
5053
  return tool.name === "write" || tool.name === "edit";
4683
5054
  }
4684
5055
 
5056
+ // src/tools/bash-output.ts
5057
+ var bashOutputTool = {
5058
+ name: "bash_output",
5059
+ description: "Collect output from commands started with bash({ background: true }). Returns only the output that arrived since the last read. Omit id to list every background command. Set kill to stop one.",
5060
+ inputSchema: {
5061
+ type: "object",
5062
+ properties: {
5063
+ id: { type: "string", description: 'Background command id, e.g. "bg-1"' },
5064
+ kill: { type: "boolean", description: "Stop the command instead of reading it" }
5065
+ },
5066
+ additionalProperties: false
5067
+ },
5068
+ defaultPermission: "allow",
5069
+ readOnly: true,
5070
+ summarize(input) {
5071
+ const { id, kill } = input;
5072
+ if (!id) return "bash_output(list)";
5073
+ return kill ? `bash_output(${id}, kill)` : `bash_output(${id})`;
5074
+ },
5075
+ execute(input) {
5076
+ const { id, kill } = input;
5077
+ if (!id) {
5078
+ const jobs2 = listJobs();
5079
+ if (jobs2.length === 0) return done({ content: "No background commands have been started." });
5080
+ const lines = jobs2.map((job2) => `${job2.id} ${describeState(job2.state, job2.exitCode, job2.signal)} ${job2.command}`);
5081
+ return done({ content: lines.join("\n") });
5082
+ }
5083
+ if (kill) {
5084
+ const stopped = killJob(id);
5085
+ return done(
5086
+ stopped ? { content: `[${id} stopped]` } : { content: `No background command with id "${id}".`, isError: true }
5087
+ );
5088
+ }
5089
+ const job = readJob(id);
5090
+ if (!job) return done({ content: `No background command with id "${id}".`, isError: true });
5091
+ const notes = [`[${id} ${describeState(job.state, job.exitCode, job.signal)}]`];
5092
+ if (job.truncated) notes.push("[output buffer full; earlier output was dropped]");
5093
+ const body = job.output.trim() === "" ? "(no new output)" : job.output.replace(/\n+$/, "");
5094
+ return done({
5095
+ content: [body, ...notes].join("\n"),
5096
+ isError: job.state === "exited" && job.exitCode !== null && job.exitCode !== 0
5097
+ });
5098
+ }
5099
+ };
5100
+ function describeState(state, exitCode, signal) {
5101
+ if (state === "running") return "still running";
5102
+ if (state === "killed") return `stopped${signal ? ` by ${signal}` : ""}`;
5103
+ return exitCode === 0 ? "finished" : `exit code ${exitCode}`;
5104
+ }
5105
+ function done(result) {
5106
+ return Promise.resolve(result);
5107
+ }
5108
+
4685
5109
  // src/tools/edit.ts
4686
- import { readFile as readFile8, stat as stat6 } from "fs/promises";
5110
+ import { readFile as readFile9, stat as stat6 } from "fs/promises";
4687
5111
 
4688
5112
  // src/tools/safe-write.ts
4689
5113
  import { randomUUID as randomUUID3 } from "crypto";
4690
- import { chmod as chmod5, lstat as lstat3, open, rename as rename5, rm as rm3 } from "fs/promises";
4691
- import { basename as basename2, dirname as dirname2, join } from "path";
5114
+ import { chmod as chmod5, lstat as lstat3, open, rename as rename5, rm as rm4 } from "fs/promises";
5115
+ import { basename as basename2, dirname as dirname2, join as join2 } from "path";
4692
5116
  var UnsafeFileChangeError = class extends Error {
4693
5117
  constructor(message) {
4694
5118
  super(message);
@@ -4733,7 +5157,7 @@ async function readSafeFileSnapshot(file, maxBytes) {
4733
5157
  async function atomicWriteSafeFile(file, data, snapshot) {
4734
5158
  const parent = dirname2(file);
4735
5159
  await assertSameParent(parent, snapshot.parent);
4736
- const temp = join(parent, `.${basename2(file)}.${process.pid}.${randomUUID3()}.tmp`);
5160
+ const temp = join2(parent, `.${basename2(file)}.${process.pid}.${randomUUID3()}.tmp`);
4737
5161
  let handle;
4738
5162
  try {
4739
5163
  handle = await open(temp, "wx", snapshot.mode);
@@ -4750,7 +5174,7 @@ async function atomicWriteSafeFile(file, data, snapshot) {
4750
5174
  await rename5(temp, file);
4751
5175
  } catch (error) {
4752
5176
  await handle?.close().catch(() => void 0);
4753
- await rm3(temp, { force: true }).catch(() => void 0);
5177
+ await rm4(temp, { force: true }).catch(() => void 0);
4754
5178
  throw error;
4755
5179
  }
4756
5180
  }
@@ -4845,56 +5269,56 @@ var editTool = {
4845
5269
  return `edit(${brief(input.path)})`;
4846
5270
  },
4847
5271
  async preview(input, ctx) {
4848
- const { path: path14, oldString, newString, replaceAll = false } = input;
4849
- const safe = resolveInside(ctx.cwd, path14);
5272
+ const { path: path15, oldString, newString, replaceAll = false } = input;
5273
+ const safe = resolveInside(ctx.cwd, path15);
4850
5274
  if (!safe.ok) return { kind: "text", text: safe.reason };
4851
5275
  const info = await stat6(safe.path).catch(() => null);
4852
5276
  if (info && info.size > MAX_FILE_BYTES2) {
4853
- return { kind: "text", text: `Cannot preview ${path14}: the file is too large.` };
5277
+ return { kind: "text", text: `Cannot preview ${path15}: the file is too large.` };
4854
5278
  }
4855
- const beforeBuffer = await readFile8(safe.path).catch(() => null);
4856
- if (!beforeBuffer) return { kind: "text", text: `Cannot read ${path14}.` };
5279
+ const beforeBuffer = await readFile9(safe.path).catch(() => null);
5280
+ if (!beforeBuffer) return { kind: "text", text: `Cannot read ${path15}.` };
4857
5281
  if (beforeBuffer.subarray(0, 8192).includes(0)) {
4858
- return { kind: "text", text: `Cannot preview binary file ${path14}` };
5282
+ return { kind: "text", text: `Cannot preview binary file ${path15}` };
4859
5283
  }
4860
5284
  const before = beforeBuffer.toString("utf8");
4861
5285
  const after = replacement(before, oldString, newString, replaceAll);
4862
- return after === null ? { kind: "text", text: `Cannot preview edit: oldString is not a valid match in ${path14}.` } : { kind: "diff", path: path14, before, after };
5286
+ return after === null ? { kind: "text", text: `Cannot preview edit: oldString is not a valid match in ${path15}.` } : { kind: "diff", path: path15, before, after };
4863
5287
  },
4864
5288
  async execute(input, ctx) {
4865
- const { path: path14, oldString, newString, replaceAll = false } = input;
4866
- const safe = resolveInside(ctx.cwd, path14);
5289
+ const { path: path15, oldString, newString, replaceAll = false } = input;
5290
+ const safe = resolveInside(ctx.cwd, path15);
4867
5291
  if (!safe.ok) return { content: safe.reason, isError: true };
4868
5292
  let snapshot;
4869
5293
  try {
4870
5294
  snapshot = await readSafeFileSnapshot(safe.path, MAX_FILE_BYTES2);
4871
5295
  } catch (error) {
4872
- return { content: `Cannot edit ${path14}: ${error.message}.`, isError: true };
5296
+ return { content: `Cannot edit ${path15}: ${error.message}.`, isError: true };
4873
5297
  }
4874
- if (!snapshot.exists) return { content: `Cannot read ${path14}.`, isError: true };
5298
+ if (!snapshot.exists) return { content: `Cannot read ${path15}.`, isError: true };
4875
5299
  const beforeBuffer = snapshot.data;
4876
5300
  if (beforeBuffer.subarray(0, 8192).includes(0)) {
4877
- return { content: `Cannot edit ${path14}: it is a binary file, not text.`, isError: true };
5301
+ return { content: `Cannot edit ${path15}: it is a binary file, not text.`, isError: true };
4878
5302
  }
4879
5303
  const before = beforeBuffer.toString("utf8");
4880
5304
  const count = countOccurrences(before, oldString);
4881
5305
  if (count === 0) {
4882
5306
  return {
4883
- content: `oldString was not found in ${path14}. Read the file again and match the text exactly, including whitespace.`,
5307
+ content: `oldString was not found in ${path15}. Read the file again and match the text exactly, including whitespace.`,
4884
5308
  isError: true
4885
5309
  };
4886
5310
  }
4887
5311
  if (count > 1 && !replaceAll) {
4888
5312
  return {
4889
- content: `oldString matches ${count} places in ${path14}. Add more surrounding context to identify a single one, or set replaceAll to true.`,
5313
+ content: `oldString matches ${count} places in ${path15}. Add more surrounding context to identify a single one, or set replaceAll to true.`,
4890
5314
  isError: true
4891
5315
  };
4892
5316
  }
4893
5317
  const after = replacement(before, oldString, newString, replaceAll);
4894
- if (after === null) return { content: `Could not prepare edit for ${path14}.`, isError: true };
5318
+ if (after === null) return { content: `Could not prepare edit for ${path15}.`, isError: true };
4895
5319
  if (Buffer.byteLength(after, "utf8") > MAX_FILE_BYTES2) {
4896
5320
  return {
4897
- content: `Cannot edit ${path14}: the result exceeds the ${MAX_FILE_BYTES2 / 1e6} MB limit.`,
5321
+ content: `Cannot edit ${path15}: the result exceeds the ${MAX_FILE_BYTES2 / 1e6} MB limit.`,
4898
5322
  isError: true
4899
5323
  };
4900
5324
  }
@@ -4907,11 +5331,11 @@ var editTool = {
4907
5331
  await atomicWriteSafeFile(safe.path, after, snapshot);
4908
5332
  ctx.checkpoint?.markChanged(safe.path);
4909
5333
  } catch (error) {
4910
- return { content: `Failed to write ${path14}: ${error.message}`, isError: true };
5334
+ return { content: `Failed to write ${path15}: ${error.message}`, isError: true };
4911
5335
  }
4912
5336
  return {
4913
- content: `Edited ${path14} (${count} ${count === 1 ? "replacement" : "replacements"})`,
4914
- display: { kind: "diff", path: path14, before, after }
5337
+ content: `Edited ${path15} (${count} ${count === 1 ? "replacement" : "replacements"})`,
5338
+ display: { kind: "diff", path: path15, before, after }
4915
5339
  };
4916
5340
  }
4917
5341
  };
@@ -4923,61 +5347,13 @@ function replacement(before, oldString, newString, replaceAll) {
4923
5347
  return before.slice(0, at) + newString + before.slice(at + oldString.length);
4924
5348
  }
4925
5349
 
4926
- // src/tools/glob.ts
4927
- import { join as join2 } from "path";
4928
- import fg from "fast-glob";
4929
- var MAX_RESULTS = 500;
4930
- var IGNORED_DIRECTORIES = ["**/node_modules/**", "**/.git/**", "**/dist/**"];
4931
- var globTool = {
4932
- name: "glob",
4933
- description: 'Find files by glob pattern, most recently modified first. Supports patterns like "src/**/*.ts". Hidden files, node_modules, .git and dist are skipped.',
4934
- inputSchema: {
4935
- type: "object",
4936
- properties: {
4937
- pattern: { type: "string", description: "Glob pattern, e.g. **/*.test.ts" },
4938
- path: { type: "string", description: "Directory to search in, relative to the workspace root" }
4939
- },
4940
- required: ["pattern"],
4941
- additionalProperties: false
4942
- },
4943
- defaultPermission: "allow",
4944
- readOnly: true,
4945
- summarize(input) {
4946
- return `glob(${brief(input.pattern)})`;
4947
- },
4948
- async execute(input, ctx) {
4949
- const { pattern, path: path14 = "." } = input;
4950
- if (patternEscapes(pattern)) {
4951
- return { content: `Pattern ${pattern} must be relative to the workspace root.`, isError: true };
4952
- }
4953
- const safe = resolveInside(ctx.cwd, path14);
4954
- if (!safe.ok) return { content: safe.reason, isError: true };
4955
- const entries = await fg(pattern, {
4956
- cwd: safe.path,
4957
- dot: false,
4958
- onlyFiles: true,
4959
- followSymbolicLinks: false,
4960
- suppressErrors: true,
4961
- ignore: IGNORED_DIRECTORIES,
4962
- stats: true
4963
- });
4964
- if (entries.length === 0) return { content: `No files matched ${pattern}` };
4965
- entries.sort((a, b) => (b.stats?.mtimeMs ?? 0) - (a.stats?.mtimeMs ?? 0));
4966
- const paths = entries.slice(0, MAX_RESULTS).map((entry) => join2(safe.relative, entry.path));
4967
- if (entries.length > MAX_RESULTS) {
4968
- paths.push(`... truncated: ${entries.length - MAX_RESULTS} more files matched.`);
4969
- }
4970
- return { content: paths.join("\n") };
4971
- }
4972
- };
4973
-
4974
5350
  // src/tools/grep.ts
4975
- import { readFile as readFile9, stat as stat7 } from "fs/promises";
5351
+ import { readFile as readFile10, stat as stat7 } from "fs/promises";
4976
5352
  import { join as join3 } from "path";
4977
- import fg2 from "fast-glob";
5353
+ import fg3 from "fast-glob";
4978
5354
 
4979
5355
  // src/tools/sensitive.ts
4980
- import path9 from "path";
5356
+ import path10 from "path";
4981
5357
  var sensitiveNames = /* @__PURE__ */ new Set([
4982
5358
  ".npmrc",
4983
5359
  ".pypirc",
@@ -5004,9 +5380,9 @@ var sensitiveExtensions = /* @__PURE__ */ new Set([".pem", ".key", ".p12", ".pfx
5004
5380
  function isSensitivePath(value) {
5005
5381
  if (typeof value !== "string") return false;
5006
5382
  const normalized = value.replaceAll("\\", "/").toLowerCase();
5007
- const name = path9.posix.basename(normalized);
5383
+ const name = path10.posix.basename(normalized);
5008
5384
  if (name === ".env" || name.startsWith(".env.")) return true;
5009
- if (sensitiveNames.has(name) || sensitiveExtensions.has(path9.posix.extname(name))) return true;
5385
+ if (sensitiveNames.has(name) || sensitiveExtensions.has(path10.posix.extname(name))) return true;
5010
5386
  return normalized.split("/").some((part) => part === ".ssh" || part === ".aws");
5011
5387
  }
5012
5388
  function mentionsSensitivePattern(value) {
@@ -5124,16 +5500,16 @@ var grepTool = {
5124
5500
  defaultPermission: "allow",
5125
5501
  readOnly: true,
5126
5502
  permission(input, ctx) {
5127
- const { path: path14, glob } = input ?? {};
5128
- if (isSensitivePath(path14) || mentionsSensitivePattern(glob)) return "ask";
5129
- const safe = ctx && path14 ? resolveInside(ctx.cwd, path14) : void 0;
5503
+ const { path: path15, glob } = input ?? {};
5504
+ if (isSensitivePath(path15) || mentionsSensitivePattern(glob)) return "ask";
5505
+ const safe = ctx && path15 ? resolveInside(ctx.cwd, path15) : void 0;
5130
5506
  return safe?.ok && isSensitivePath(safe.relative) ? "ask" : void 0;
5131
5507
  },
5132
5508
  summarize(input) {
5133
5509
  return `grep(${brief(input.pattern)})`;
5134
5510
  },
5135
5511
  async execute(input, ctx) {
5136
- const { pattern, path: path14 = ".", glob = "**/*", maxMatches } = input;
5512
+ const { pattern, path: path15 = ".", glob = "**/*", maxMatches } = input;
5137
5513
  if (pattern.length > MAX_PATTERN_CHARS) {
5138
5514
  return {
5139
5515
  content: `Regular expression is limited to ${MAX_PATTERN_CHARS} characters.`,
@@ -5148,11 +5524,11 @@ var grepTool = {
5148
5524
  if (patternEscapes(glob)) {
5149
5525
  return { content: `Glob ${glob} must be relative to the workspace root.`, isError: true };
5150
5526
  }
5151
- const safe = resolveInside(ctx.cwd, path14);
5527
+ const safe = resolveInside(ctx.cwd, path15);
5152
5528
  if (!safe.ok) return { content: safe.reason, isError: true };
5153
5529
  const target = await stat7(safe.path).catch(() => null);
5154
- if (!target) return { content: `Path not found: ${path14}`, isError: true };
5155
- const discovered = target.isDirectory() ? (await fg2(glob, {
5530
+ if (!target) return { content: `Path not found: ${path15}`, isError: true };
5531
+ const discovered = target.isDirectory() ? (await fg3(glob, {
5156
5532
  cwd: safe.path,
5157
5533
  dot: false,
5158
5534
  onlyFiles: true,
@@ -5182,7 +5558,7 @@ var grepTool = {
5182
5558
  break;
5183
5559
  }
5184
5560
  scannedBytes += file.size;
5185
- const buffer = await readFile9(file.absolute).catch(() => null);
5561
+ const buffer = await readFile10(file.absolute).catch(() => null);
5186
5562
  if (!buffer || buffer.subarray(0, 4096).includes(0)) continue;
5187
5563
  const lines = buffer.toString("utf8").split("\n");
5188
5564
  const remaining = limit - hits.length;
@@ -5212,7 +5588,7 @@ var grepTool = {
5212
5588
  };
5213
5589
 
5214
5590
  // src/tools/read.ts
5215
- import { readFile as readFile10, stat as stat8 } from "fs/promises";
5591
+ import { readFile as readFile11, stat as stat8 } from "fs/promises";
5216
5592
  var MAX_LINES = 2e3;
5217
5593
  var MAX_CHARS = 2e5;
5218
5594
  var MAX_FILE_BYTES4 = 5e6;
@@ -5241,30 +5617,30 @@ var readTool = {
5241
5617
  return `read(${brief(input.path)})`;
5242
5618
  },
5243
5619
  async execute(input, ctx) {
5244
- const { path: path14, offset = 1, limit } = input;
5245
- const safe = resolveInside(ctx.cwd, path14);
5620
+ const { path: path15, offset = 1, limit } = input;
5621
+ const safe = resolveInside(ctx.cwd, path15);
5246
5622
  if (!safe.ok) return { content: safe.reason, isError: true };
5247
5623
  const info = await stat8(safe.path).catch(() => null);
5248
- if (!info) return { content: `File not found: ${path14}`, isError: true };
5624
+ if (!info) return { content: `File not found: ${path15}`, isError: true };
5249
5625
  if (info.isDirectory()) {
5250
- return { content: `${path14} is a directory, not a file. Use glob to list its contents.`, isError: true };
5626
+ return { content: `${path15} is a directory, not a file. Use glob to list its contents.`, isError: true };
5251
5627
  }
5252
5628
  if (info.size > MAX_FILE_BYTES4) {
5253
5629
  return {
5254
- content: `${path14} is ${(info.size / 1e6).toFixed(1)} MB; read is limited to ${MAX_FILE_BYTES4 / 1e6} MB per file. Use grep or another targeted tool instead.`,
5630
+ content: `${path15} is ${(info.size / 1e6).toFixed(1)} MB; read is limited to ${MAX_FILE_BYTES4 / 1e6} MB per file. Use grep or another targeted tool instead.`,
5255
5631
  isError: true
5256
5632
  };
5257
5633
  }
5258
- const buffer = await readFile10(safe.path);
5634
+ const buffer = await readFile11(safe.path);
5259
5635
  if (buffer.subarray(0, 4096).includes(0)) {
5260
- return { content: `${path14} looks like a binary file and cannot be read as text.`, isError: true };
5636
+ return { content: `${path15} looks like a binary file and cannot be read as text.`, isError: true };
5261
5637
  }
5262
5638
  const lines = buffer.toString("utf8").split("\n");
5263
5639
  if (lines.at(-1) === "") lines.pop();
5264
- if (lines.length === 0) return { content: `${path14} is empty.` };
5640
+ if (lines.length === 0) return { content: `${path15} is empty.` };
5265
5641
  const start = Math.max(1, offset) - 1;
5266
5642
  if (start >= lines.length) {
5267
- return { content: `${path14} has ${lines.length} lines, so offset ${offset} is past the end.`, isError: true };
5643
+ return { content: `${path15} has ${lines.length} lines, so offset ${offset} is past the end.`, isError: true };
5268
5644
  }
5269
5645
  const window = lines.slice(start, start + Math.min(limit ?? MAX_LINES, MAX_LINES));
5270
5646
  const rendered = [];
@@ -5291,7 +5667,7 @@ function toToolSchema(tool) {
5291
5667
  }
5292
5668
 
5293
5669
  // src/tools/write.ts
5294
- import { mkdir as mkdir4, readFile as readFile11, stat as stat9 } from "fs/promises";
5670
+ import { mkdir as mkdir4, readFile as readFile12, stat as stat9 } from "fs/promises";
5295
5671
  import { dirname as dirname3 } from "path";
5296
5672
  var MAX_FILE_BYTES5 = 5e6;
5297
5673
  var writeTool = {
@@ -5311,71 +5687,71 @@ var writeTool = {
5311
5687
  return `write(${brief(input.path)})`;
5312
5688
  },
5313
5689
  async preview(input, ctx) {
5314
- const { path: path14, content } = input;
5690
+ const { path: path15, content } = input;
5315
5691
  if (Buffer.byteLength(content, "utf8") > MAX_FILE_BYTES5) {
5316
5692
  return { kind: "text", text: `Write is limited to ${MAX_FILE_BYTES5 / 1e6} MB.` };
5317
5693
  }
5318
- const safe = resolveInside(ctx.cwd, path14);
5694
+ const safe = resolveInside(ctx.cwd, path15);
5319
5695
  if (!safe.ok) return { kind: "text", text: safe.reason };
5320
5696
  const beforeInfo = await stat9(safe.path).catch(() => null);
5321
5697
  if (beforeInfo && beforeInfo.size > MAX_FILE_BYTES5) {
5322
- return { kind: "text", text: `Cannot preview ${path14}: the existing file is too large.` };
5698
+ return { kind: "text", text: `Cannot preview ${path15}: the existing file is too large.` };
5323
5699
  }
5324
- const beforeBuffer = await readFile11(safe.path).catch(() => null);
5700
+ const beforeBuffer = await readFile12(safe.path).catch(() => null);
5325
5701
  if (beforeBuffer?.subarray(0, 8192).includes(0)) {
5326
- return { kind: "text", text: `Cannot preview binary file ${path14}` };
5702
+ return { kind: "text", text: `Cannot preview binary file ${path15}` };
5327
5703
  }
5328
5704
  return {
5329
5705
  kind: "diff",
5330
- path: path14,
5706
+ path: path15,
5331
5707
  before: beforeBuffer?.toString("utf8") ?? "",
5332
5708
  after: content
5333
5709
  };
5334
5710
  },
5335
5711
  async execute(input, ctx) {
5336
- const { path: path14, content } = input;
5712
+ const { path: path15, content } = input;
5337
5713
  if (Buffer.byteLength(content, "utf8") > MAX_FILE_BYTES5) {
5338
5714
  return {
5339
- content: `Cannot write ${path14}: content exceeds the ${MAX_FILE_BYTES5 / 1e6} MB limit.`,
5715
+ content: `Cannot write ${path15}: content exceeds the ${MAX_FILE_BYTES5 / 1e6} MB limit.`,
5340
5716
  isError: true
5341
5717
  };
5342
5718
  }
5343
- const safe = resolveInside(ctx.cwd, path14);
5719
+ const safe = resolveInside(ctx.cwd, path15);
5344
5720
  if (!safe.ok) return { content: safe.reason, isError: true };
5345
5721
  let target = safe.path;
5346
5722
  let snapshot;
5347
5723
  try {
5348
5724
  await mkdir4(dirname3(target), { recursive: true });
5349
- const rechecked = resolveInside(ctx.cwd, path14);
5725
+ const rechecked = resolveInside(ctx.cwd, path15);
5350
5726
  if (!rechecked.ok || rechecked.path !== target) {
5351
5727
  return {
5352
- content: `Cannot write ${path14}: the path changed while its parent was created.`,
5728
+ content: `Cannot write ${path15}: the path changed while its parent was created.`,
5353
5729
  isError: true
5354
5730
  };
5355
5731
  }
5356
5732
  target = rechecked.path;
5357
5733
  snapshot = await readSafeFileSnapshot(target, MAX_FILE_BYTES5);
5358
5734
  if (snapshot.exists && snapshot.data.subarray(0, 8192).includes(0)) {
5359
- return { content: `Cannot write ${path14}: the existing file is binary, not text.`, isError: true };
5735
+ return { content: `Cannot write ${path15}: the existing file is binary, not text.`, isError: true };
5360
5736
  }
5361
5737
  await ctx.checkpoint?.capture(target);
5362
5738
  await atomicWriteSafeFile(target, content, snapshot);
5363
5739
  ctx.checkpoint?.markChanged(target);
5364
5740
  } catch (error) {
5365
- return { content: `Failed to write ${path14}: ${error.message}`, isError: true };
5741
+ return { content: `Failed to write ${path15}: ${error.message}`, isError: true };
5366
5742
  }
5367
5743
  const before = snapshot.exists ? snapshot.data.toString("utf8") : "";
5368
5744
  const lines = content === "" ? 0 : content.replace(/\n$/, "").split("\n").length;
5369
5745
  return {
5370
- content: `${before === "" ? "Created" : "Updated"} ${path14} (${lines} ${lines === 1 ? "line" : "lines"})`,
5371
- display: { kind: "diff", path: path14, before, after: content }
5746
+ content: `${before === "" ? "Created" : "Updated"} ${path15} (${lines} ${lines === 1 ? "line" : "lines"})`,
5747
+ display: { kind: "diff", path: path15, before, after: content }
5372
5748
  };
5373
5749
  }
5374
5750
  };
5375
5751
 
5376
5752
  // src/tools/registry.ts
5377
5753
  function builtinTools() {
5378
- return [readTool, writeTool, editTool, bashTool, globTool, grepTool];
5754
+ return [readTool, writeTool, editTool, bashTool, bashOutputTool, globTool, grepTool];
5379
5755
  }
5380
5756
  function createToolRegistry(tools) {
5381
5757
  const byName = /* @__PURE__ */ new Map();
@@ -5397,8 +5773,8 @@ function createToolRegistry(tools) {
5397
5773
  }
5398
5774
 
5399
5775
  // src/prompts/library.ts
5400
- import { chmod as chmod6, readFile as readFile12, readdir as readdir4, unlink as unlink3, writeFile as writeFile6 } from "fs/promises";
5401
- import path10 from "path";
5776
+ import { chmod as chmod6, readFile as readFile13, readdir as readdir4, unlink as unlink3, writeFile as writeFile7 } from "fs/promises";
5777
+ import path11 from "path";
5402
5778
  async function savePrompt(input) {
5403
5779
  const slug = slugify(input.name);
5404
5780
  if (!slug) throw new Error(`Prompt name has no letters or digits to name a file after: ${input.name}`);
@@ -5410,8 +5786,8 @@ async function savePrompt(input) {
5410
5786
  body: input.body.trim()
5411
5787
  };
5412
5788
  await ensureDir(promptsDir);
5413
- const file = path10.join(promptsDir, `${slug}.md`);
5414
- await writeFile6(file, serialize(prompt), {
5789
+ const file = path11.join(promptsDir, `${slug}.md`);
5790
+ await writeFile7(file, serialize(prompt), {
5415
5791
  encoding: "utf8",
5416
5792
  mode: 384
5417
5793
  });
@@ -5421,7 +5797,7 @@ async function savePrompt(input) {
5421
5797
  async function getPrompt(slug) {
5422
5798
  assertSlug(slug);
5423
5799
  try {
5424
- return parse(slug, await readFile12(path10.join(promptsDir, `${slug}.md`), "utf8"));
5800
+ return parse(slug, await readFile13(path11.join(promptsDir, `${slug}.md`), "utf8"));
5425
5801
  } catch (error) {
5426
5802
  if (isMissing(error)) return null;
5427
5803
  throw error;
@@ -5437,7 +5813,7 @@ async function listPrompts() {
5437
5813
  }
5438
5814
  const prompts2 = await Promise.all(
5439
5815
  names.filter((name) => name.endsWith(".md")).map(
5440
- async (name) => parse(name.slice(0, -3), await readFile12(path10.join(promptsDir, name), "utf8"))
5816
+ async (name) => parse(name.slice(0, -3), await readFile13(path11.join(promptsDir, name), "utf8"))
5441
5817
  )
5442
5818
  );
5443
5819
  return prompts2.sort((a, b) => a.name.localeCompare(b.name));
@@ -5445,7 +5821,7 @@ async function listPrompts() {
5445
5821
  async function deletePrompt(slug) {
5446
5822
  assertSlug(slug);
5447
5823
  try {
5448
- await unlink3(path10.join(promptsDir, `${slug}.md`));
5824
+ await unlink3(path11.join(promptsDir, `${slug}.md`));
5449
5825
  return true;
5450
5826
  } catch (error) {
5451
5827
  if (isMissing(error)) return false;
@@ -5503,7 +5879,7 @@ function parse(slug, text) {
5503
5879
 
5504
5880
  // src/skills/library.ts
5505
5881
  import { lstat as lstat4, open as open2, readdir as readdir5 } from "fs/promises";
5506
- import path11 from "path";
5882
+ import path12 from "path";
5507
5883
  var SKILL_FILE = "SKILL.md";
5508
5884
  var FRONTMATTER_BYTES = 8192;
5509
5885
  var MAX_SKILL_BYTES = 5e6;
@@ -5516,7 +5892,7 @@ async function discoverSkills(dirs) {
5516
5892
  if (!rootInfo?.isDirectory() || rootInfo.isSymbolicLink()) continue;
5517
5893
  const entries = await readdir5(root, { withFileTypes: true }).catch(() => []);
5518
5894
  const found = await Promise.all(
5519
- entries.filter((entry) => entry.isDirectory() && !entry.isSymbolicLink()).slice(0, MAX_SKILLS_PER_ROOT).map((entry) => readMeta(root, rootInfo, path11.join(root, entry.name)))
5895
+ entries.filter((entry) => entry.isDirectory() && !entry.isSymbolicLink()).slice(0, MAX_SKILLS_PER_ROOT).map((entry) => readMeta(root, rootInfo, path12.join(root, entry.name)))
5520
5896
  );
5521
5897
  for (const meta of found) {
5522
5898
  if (meta && !byName.has(meta.name)) byName.set(meta.name, meta);
@@ -5556,11 +5932,11 @@ function formatSkillCatalogue(skills) {
5556
5932
  async function readMeta(root, rootInfo, dir) {
5557
5933
  const dirInfo = await lstat4(dir).catch(() => null);
5558
5934
  if (!dirInfo?.isDirectory() || dirInfo.isSymbolicLink()) return null;
5559
- const file = path11.join(dir, SKILL_FILE);
5935
+ const file = path12.join(dir, SKILL_FILE);
5560
5936
  const head = await readFrontmatterBytes(file);
5561
5937
  if (!head) return null;
5562
5938
  const { fields } = parseFrontmatter(head.text);
5563
- const meta = { name: fields.name || path11.basename(dir), description: fields.description ?? "", dir, file };
5939
+ const meta = { name: fields.name || path12.basename(dir), description: fields.description ?? "", dir, file };
5564
5940
  guards.set(meta, {
5565
5941
  root,
5566
5942
  rootIdentity: identity(rootInfo),
@@ -5656,8 +6032,8 @@ import {
5656
6032
  writeFileSync
5657
6033
  } from "fs";
5658
6034
  import { chmod as chmod7, mkdir as mkdir5 } from "fs/promises";
5659
- import path12 from "path";
5660
- var TMP_DIR = path12.join(skillsDir, ".tmp");
6035
+ import path13 from "path";
6036
+ var TMP_DIR = path13.join(skillsDir, ".tmp");
5661
6037
  var MAX_SKILL_BYTES2 = 5e6;
5662
6038
  async function installSkill(source) {
5663
6039
  await ensureDir(skillsDir);
@@ -5682,7 +6058,7 @@ function isNpmPackage(input) {
5682
6058
  }
5683
6059
  async function installFromGitHub(url) {
5684
6060
  const { owner, repo, subdir, branch } = parseGitHubUrl(url);
5685
- const name = safeSkillName(subdir ? path12.posix.basename(subdir) : repo);
6061
+ const name = safeSkillName(subdir ? path13.posix.basename(subdir) : repo);
5686
6062
  const skillDir = safeChildPath(skillsDir, name);
5687
6063
  const tmpDir = safeChildPath(TMP_DIR, `${name}-${randomUUID4()}`);
5688
6064
  try {
@@ -5717,16 +6093,16 @@ async function installFromNpm(packageName) {
5717
6093
  if (!tarball) {
5718
6094
  throw new Error(`Could not download npm package "${packageName}"`);
5719
6095
  }
5720
- const tarballPath = path12.join(tmpDir, tarball);
6096
+ const tarballPath = path13.join(tmpDir, tarball);
5721
6097
  validateTarball(tarballPath);
5722
- const extractDir = path12.join(tmpDir, "extracted");
6098
+ const extractDir = path13.join(tmpDir, "extracted");
5723
6099
  mkdirSync(extractDir, { recursive: true });
5724
6100
  try {
5725
6101
  execFileSync("tar", ["-xzf", tarballPath, "-C", extractDir], { stdio: "ignore" });
5726
6102
  } catch {
5727
- const packageDir = path12.join(tmpDir, "node_modules", packageName);
6103
+ const packageDir = path13.join(tmpDir, "node_modules", packageName);
5728
6104
  if (existsSync2(packageDir)) {
5729
- const skillFile2 = path12.join(packageDir, "SKILL.md");
6105
+ const skillFile2 = path13.join(packageDir, "SKILL.md");
5730
6106
  if (existsSync2(skillFile2)) {
5731
6107
  const body2 = readSkillFile(skillFile2, packageDir, packageName);
5732
6108
  await writeSkillFile(skillDir, body2);
@@ -5738,7 +6114,7 @@ async function installFromNpm(packageName) {
5738
6114
  const extractedFiles = readdirSync(extractDir);
5739
6115
  const packageRoot = extractedFiles.find((f) => f === "package");
5740
6116
  if (!packageRoot) throw new Error("Could not find package root in extracted files");
5741
- const skillFile = path12.join(extractDir, "package", "SKILL.md");
6117
+ const skillFile = path13.join(extractDir, "package", "SKILL.md");
5742
6118
  if (!existsSync2(skillFile)) {
5743
6119
  throw new Error(`No SKILL.md found in npm package "${packageName}"`);
5744
6120
  }
@@ -5772,26 +6148,26 @@ function validateTarball(file) {
5772
6148
  }
5773
6149
  for (const entry of entries) {
5774
6150
  const normalized = entry.replace(/^\.\//, "");
5775
- if (normalized.includes("\\") || path12.posix.isAbsolute(normalized) || normalized.split("/").some((segment) => segment === "..")) {
6151
+ if (normalized.includes("\\") || path13.posix.isAbsolute(normalized) || normalized.split("/").some((segment) => segment === "..")) {
5776
6152
  throw new Error(`Unsafe path in npm skill archive: ${entry}`);
5777
6153
  }
5778
6154
  }
5779
6155
  }
5780
6156
  async function installFromLocal(srcPath) {
5781
- const resolved = path12.resolve(srcPath);
6157
+ const resolved = path13.resolve(srcPath);
5782
6158
  let skillDir;
5783
6159
  let skillFile;
5784
- const directSkill = path12.join(resolved, "SKILL.md");
6160
+ const directSkill = path13.join(resolved, "SKILL.md");
5785
6161
  if (existsSync2(directSkill)) {
5786
6162
  skillDir = resolved;
5787
6163
  skillFile = directSkill;
5788
6164
  } else if (resolved.endsWith("SKILL.md") && existsSync2(resolved)) {
5789
- skillDir = path12.dirname(resolved);
6165
+ skillDir = path13.dirname(resolved);
5790
6166
  skillFile = resolved;
5791
6167
  } else {
5792
6168
  throw new Error(`No SKILL.md found at "${srcPath}"`);
5793
6169
  }
5794
- const name = path12.basename(skillDir);
6170
+ const name = path13.basename(skillDir);
5795
6171
  const destDir = safeChildPath(skillsDir, safeSkillName(name));
5796
6172
  const body = readSkillFile(skillFile, skillDir, srcPath);
5797
6173
  await writeSkillFile(destDir, body);
@@ -5804,7 +6180,7 @@ async function writeSkillFile(dir, body) {
5804
6180
  throw new Error(`Refusing to replace symlinked skill directory: ${dir}`);
5805
6181
  }
5806
6182
  await mkdir5(dir, { recursive: true, mode: 448 });
5807
- const file = path12.join(dir, "SKILL.md");
6183
+ const file = path13.join(dir, "SKILL.md");
5808
6184
  const existingFile = lstatSync(file, { throwIfNoEntry: false });
5809
6185
  if (existingFile?.isSymbolicLink()) {
5810
6186
  throw new Error(`Refusing to replace symlinked skill file: ${file}`);
@@ -5857,13 +6233,13 @@ function safeSkillName(value) {
5857
6233
  return value;
5858
6234
  }
5859
6235
  function safeChildPath(root, ...segments) {
5860
- const target = path12.resolve(root, ...segments);
6236
+ const target = path13.resolve(root, ...segments);
5861
6237
  assertInside(root, target);
5862
6238
  return target;
5863
6239
  }
5864
6240
  function assertInside(root, target) {
5865
- const relative2 = path12.relative(path12.resolve(root), path12.resolve(target));
5866
- if (relative2 === "" || !path12.isAbsolute(relative2) && relative2 !== ".." && !relative2.startsWith(`..${path12.sep}`)) {
6241
+ const relative2 = path13.relative(path13.resolve(root), path13.resolve(target));
6242
+ if (relative2 === "" || !path13.isAbsolute(relative2) && relative2 !== ".." && !relative2.startsWith(`..${path13.sep}`)) {
5867
6243
  return;
5868
6244
  }
5869
6245
  throw new Error(`Path escapes the skills directory: ${target}`);
@@ -6110,8 +6486,8 @@ async function readJsonBounded(response) {
6110
6486
  let bytes = 0;
6111
6487
  try {
6112
6488
  while (true) {
6113
- const { done, value } = await reader.read();
6114
- if (done) break;
6489
+ const { done: done2, value } = await reader.read();
6490
+ if (done2) break;
6115
6491
  bytes += value.byteLength;
6116
6492
  if (bytes > MAX_RESPONSE_BYTES2) {
6117
6493
  await reader.cancel();
@@ -6189,10 +6565,17 @@ async function boot(options) {
6189
6565
  }
6190
6566
  const usage = createUsageTracker(session.usage, resolvePricing);
6191
6567
  let skillCatalogue = formatSkillCatalogue(skills);
6568
+ let projectMemory = await readProjectMemory(workspaceRoot2);
6569
+ const saveMemory = async (text) => {
6570
+ await saveProjectMemory(workspaceRoot2, text);
6571
+ projectMemory = await readProjectMemory(workspaceRoot2);
6572
+ };
6573
+ tools.register([createMemoryTool({ read: () => projectMemory, save: saveMemory })]);
6192
6574
  const mainSystemPrompt = () => buildSystemPrompt({
6193
6575
  cwd: options.cwd,
6194
6576
  toolNames: tools.list().map((tool) => tool.name),
6195
- skills: skillCatalogue
6577
+ skills: skillCatalogue,
6578
+ memory: projectMemory
6196
6579
  });
6197
6580
  let modelRef = session.model || config.model || "";
6198
6581
  if (options.modelRef) modelRef = options.modelRef;
@@ -6277,9 +6660,9 @@ async function boot(options) {
6277
6660
  activeAgents.push(entry);
6278
6661
  const trackedRequest = {
6279
6662
  ...request,
6280
- onProgress: (summary) => {
6281
- entry.progress.push(summary);
6282
- request.onProgress(summary);
6663
+ onProgress: (summary2) => {
6664
+ entry.progress.push(summary2);
6665
+ request.onProgress(summary2);
6283
6666
  }
6284
6667
  };
6285
6668
  try {
@@ -6347,13 +6730,15 @@ async function boot(options) {
6347
6730
  needsSetup: () => Object.keys(config.providers).length === 0,
6348
6731
  async addProvider(url, key) {
6349
6732
  const detected = await detectProvider(url, key);
6350
- rememberModels(detected.id, detected.models);
6733
+ for (const provider of detected) rememberModels(provider.id, provider.models);
6351
6734
  const configBefore = structuredClone(config);
6352
6735
  const authBefore = { ...auth };
6353
- config.providers[detected.id] = detected.config;
6354
- auth[detected.id] = key;
6355
- const chosen = preferredModel(detected.models);
6356
- if (chosen) config.model = formatModelRef(detected.id, chosen);
6736
+ for (const provider of detected) {
6737
+ config.providers[provider.id] = provider.config;
6738
+ auth[provider.id] = key;
6739
+ }
6740
+ const chosen = preferredDetectedModel(detected);
6741
+ if (chosen) config.model = formatModelRef(chosen.providerId, chosen.model.id);
6357
6742
  try {
6358
6743
  await saveConfig(config);
6359
6744
  await saveAuth(auth);
@@ -6366,11 +6751,12 @@ async function boot(options) {
6366
6751
  }
6367
6752
  registry = createRegistry(config, auth);
6368
6753
  const nextRef = config.model ?? "";
6369
- const discovered = detected.models.find((model) => model.id === chosen)?.contextWindow;
6370
- activateModel(nextRef, void 0, discovered);
6754
+ activateModel(nextRef, void 0, chosen?.model.contextWindow);
6371
6755
  void refreshModelContextWindow();
6372
- const kind = detected.config.type === "anthropic" ? "Anthropic" : "OpenAI-compatible";
6373
- return `${detected.id} \xB7 ${kind} \xB7 ${detected.models.length} models`;
6756
+ return detected.map((provider) => {
6757
+ const kind = provider.config.type === "anthropic" ? "Anthropic" : "OpenAI-compatible";
6758
+ return `${provider.id} \xB7 ${kind} \xB7 ${provider.models.length} models`;
6759
+ }).join("\n");
6374
6760
  },
6375
6761
  currentProviderId: () => parseModelRef(modelRef)?.provider,
6376
6762
  listProviderItems: () => Object.entries(config.providers).map(([id, entry]) => ({
@@ -6538,7 +6924,7 @@ async function boot(options) {
6538
6924
  },
6539
6925
  async exportSession(id, destination) {
6540
6926
  await persistQueue.catch(() => void 0);
6541
- const target = destination ? path13.resolve(options.cwd, destination) : path13.join(options.cwd, ".kitcode-exports");
6927
+ const target = destination ? path14.resolve(options.cwd, destination) : path14.join(options.cwd, ".kitcode-exports");
6542
6928
  if (!destination) await mkdir6(target, { recursive: true, mode: 448 });
6543
6929
  return (await exportSession(id, target)).path;
6544
6930
  },
@@ -6553,6 +6939,21 @@ async function boot(options) {
6553
6939
  });
6554
6940
  void refreshModelContextWindow();
6555
6941
  },
6942
+ readMemory: () => projectMemory,
6943
+ async saveMemory(text) {
6944
+ await saveMemory(text);
6945
+ },
6946
+ async clearMemory() {
6947
+ await clearProjectMemory(workspaceRoot2);
6948
+ projectMemory = "";
6949
+ },
6950
+ effortDescription(history) {
6951
+ if (!modelRef) return "unavailable";
6952
+ const { provider, modelId } = registry.resolve(modelRef);
6953
+ const requested = resolveEffort(config.effort, history);
6954
+ const effective = provider.kind === "openai" ? openAiEffort(modelId, requested) : requested;
6955
+ return effective ? `${config.effort} \u2192 ${effective} (API request)` : `${config.effort} \u2192 not sent: unknown model support`;
6956
+ },
6556
6957
  getEffort: () => config.effort,
6557
6958
  async setEffort(effort) {
6558
6959
  await persistConfig((draft) => {
@@ -6709,6 +7110,15 @@ ${lines.join("\n")}` : null;
6709
7110
  }
6710
7111
  return items;
6711
7112
  },
7113
+ async refreshProviderModels(providerId) {
7114
+ const models = await refreshModels(registry.get(providerId));
7115
+ rememberModels(providerId, models);
7116
+ if (parseModelRef(modelRef)?.provider === providerId) void refreshModelContextWindow();
7117
+ return models.length;
7118
+ },
7119
+ async listWorkspaceFiles(query) {
7120
+ return searchWorkspaceFiles(options.cwd, query);
7121
+ },
6712
7122
  async listPromptItems() {
6713
7123
  const prompts2 = await listPrompts();
6714
7124
  return prompts2.map((prompt) => ({
@@ -6904,6 +7314,7 @@ ${lines.join("\n")}` : null;
6904
7314
  try {
6905
7315
  await Promise.all([persistQueue, configQueue]);
6906
7316
  } finally {
7317
+ killAllJobs();
6907
7318
  await mcp.close();
6908
7319
  }
6909
7320
  }
@@ -6916,6 +7327,16 @@ function preferredModel(models) {
6916
7327
  }
6917
7328
  return models[0]?.id;
6918
7329
  }
7330
+ function preferredDetectedModel(detected) {
7331
+ const candidates = detected.flatMap(
7332
+ (provider) => provider.models.map((model) => ({ providerId: provider.id, model }))
7333
+ );
7334
+ for (const preferred of PREFERRED2) {
7335
+ const match = candidates.find((candidate) => candidate.model.id.endsWith(preferred));
7336
+ if (match) return match;
7337
+ }
7338
+ return candidates[0];
7339
+ }
6919
7340
 
6920
7341
  // src/app/ask.ts
6921
7342
  var RU_YES = /* @__PURE__ */ new Set(["y", "\u043D"]);
@@ -7184,6 +7605,7 @@ var COMMANDS = [
7184
7605
  { name: "provider" },
7185
7606
  { name: "logout", args: "[provider]" },
7186
7607
  { name: "key", args: "[provider]" },
7608
+ { name: "refresh", args: "[provider]" },
7187
7609
  { name: "effort" },
7188
7610
  { name: "thinking" },
7189
7611
  { name: "budget", args: "[tokens]" },
@@ -7195,6 +7617,7 @@ var COMMANDS = [
7195
7617
  { name: "usage" },
7196
7618
  { name: "mcp", args: "[add|list|delete|enable|disable]" },
7197
7619
  { name: "attach", args: "<path|clipboard|clear>" },
7620
+ { name: "memory", args: "[show|set <text>|clear]" },
7198
7621
  { name: "compact" },
7199
7622
  { name: "update" },
7200
7623
  { name: "checker" },
@@ -7232,22 +7655,22 @@ function matchCommands(line) {
7232
7655
  if (starts.length > 0) return starts;
7233
7656
  const contains = COMMANDS.filter((command) => command.name.includes(needle));
7234
7657
  if (contains.length > 0) return contains;
7235
- return COMMANDS.filter((command) => isSubsequence(needle, command.name));
7658
+ return COMMANDS.filter((command) => isSubsequence2(needle, command.name));
7236
7659
  }
7237
7660
  function closestCommand(name) {
7238
7661
  const needle = name.toLowerCase();
7239
7662
  let best;
7240
7663
  let bestScore = Number.POSITIVE_INFINITY;
7241
7664
  for (const command of COMMANDS) {
7242
- const score = editDistance(needle, command.name);
7243
- if (score < bestScore) {
7244
- bestScore = score;
7665
+ const score2 = editDistance(needle, command.name);
7666
+ if (score2 < bestScore) {
7667
+ bestScore = score2;
7245
7668
  best = command;
7246
7669
  }
7247
7670
  }
7248
7671
  return bestScore <= Math.max(2, Math.floor(needle.length / 2)) ? best : void 0;
7249
7672
  }
7250
- function isSubsequence(needle, haystack) {
7673
+ function isSubsequence2(needle, haystack) {
7251
7674
  let index = 0;
7252
7675
  for (const character of haystack) {
7253
7676
  if (character === needle[index]) index += 1;
@@ -7272,6 +7695,43 @@ function editDistance(a, b) {
7272
7695
  return previous[b.length] ?? 0;
7273
7696
  }
7274
7697
 
7698
+ // src/ui/mentions.ts
7699
+ var MENTION_PATTERN = /(?:^|\s)@([^\s@]+)/g;
7700
+ var TRAILING_PUNCTUATION = /[,;:!?]+$/;
7701
+ function mentionSpan(value, cursor) {
7702
+ const at = clamp(cursor, 0, value.length);
7703
+ const before = value.slice(0, at);
7704
+ const start = before.lastIndexOf("@");
7705
+ if (start === -1) return null;
7706
+ const preceding = start === 0 ? "" : value[start - 1] ?? "";
7707
+ if (preceding !== "" && !/\s/.test(preceding)) return null;
7708
+ const query = before.slice(start + 1);
7709
+ if (/[\s@]/.test(query)) return null;
7710
+ let end = at;
7711
+ while (end < value.length && !/[\s@]/.test(value[end] ?? "")) end += 1;
7712
+ return { start, end, query };
7713
+ }
7714
+ function completeMention(value, span, path15) {
7715
+ const rest = value.slice(span.end);
7716
+ const spaced = rest.startsWith(" ");
7717
+ const inserted = `@${path15}${spaced ? "" : " "}`;
7718
+ return {
7719
+ value: value.slice(0, span.start) + inserted + rest,
7720
+ cursor: span.start + inserted.length + (spaced ? 1 : 0)
7721
+ };
7722
+ }
7723
+ function extractMentions(text) {
7724
+ const found = [];
7725
+ for (const match of text.matchAll(MENTION_PATTERN)) {
7726
+ const path15 = (match[1] ?? "").replace(TRAILING_PUNCTUATION, "");
7727
+ if (path15 !== "" && !found.includes(path15)) found.push(path15);
7728
+ }
7729
+ return found;
7730
+ }
7731
+ function clamp(value, min, max) {
7732
+ return Math.max(min, Math.min(value, max));
7733
+ }
7734
+
7275
7735
  // src/ui/App.tsx
7276
7736
  import TextInput2 from "ink-text-input";
7277
7737
 
@@ -7373,6 +7833,7 @@ var en = {
7373
7833
  titleProvider: "Provider",
7374
7834
  titleLogout: "Sign out of a provider",
7375
7835
  titleKeyChange: "Change API key for a provider",
7836
+ titleModelRefresh: "Reload models for a provider",
7376
7837
  titleSessions: "Resume a session",
7377
7838
  titleSessionAction: "Session action",
7378
7839
  resumed: (id, count) => `Resumed ${id} \u2014 ${count} messages restored`,
@@ -7388,7 +7849,7 @@ var en = {
7388
7849
  escHelp: "esc \u2014 cancel the running turn",
7389
7850
  cancelled: "Cancelled.",
7390
7851
  queued: (count) => `queued: ${count}`,
7391
- configAt: (path14) => `config: ${path14}`,
7852
+ configAt: (path15) => `config: ${path15}`,
7392
7853
  skillsEmpty: "No skills installed. Drop a folder with a SKILL.md into ~/.kitcode/skills or ./.kitcode/skills",
7393
7854
  skillsInstallUsage: "Usage: /skills install <github-url|npm-package|local-path>",
7394
7855
  skillsInstalling: (source) => `Installing skill from "${source}"\u2026`,
@@ -7434,7 +7895,7 @@ var en = {
7434
7895
  sessionDeleteAllFinalBody: "This cannot be undone. Press y only if you want to erase the complete session history.",
7435
7896
  sessionsDeletedAll: (count) => `All saved chats deleted \xB7 ${count} sessions removed.`,
7436
7897
  sessionsDeleteAllFailed: (deleted, failed) => `Session cleanup was incomplete \xB7 ${deleted} removed, ${failed} failed.`,
7437
- sessionExported: (path14) => `Session exported to ${path14}`,
7898
+ sessionExported: (path15) => `Session exported to ${path15}`,
7438
7899
  sessionActionResume: "resume",
7439
7900
  sessionActionRename: "rename",
7440
7901
  sessionActionDelete: "delete",
@@ -7452,6 +7913,7 @@ ${url}`,
7452
7913
  budgetUnlimited: "Token budget: unlimited (0). Set /budget <tokens> to restore a limit.",
7453
7914
  budgetInvalid: "Invalid budget: must be between 1,000 and 10,000,000",
7454
7915
  modelSet: (value) => `Model: ${value}`,
7916
+ modelsRefreshed: (provider, count) => `${provider}: ${count} models reloaded from the endpoint.`,
7455
7917
  noModels: "No models available. Use /login to add a provider.",
7456
7918
  bypassOff: "Approval prompts are back on.",
7457
7919
  bypassAsk1: "Disable approval prompts for this session?",
@@ -7499,11 +7961,13 @@ ${url}`,
7499
7961
  provider: "switch between configured providers",
7500
7962
  logout: "choose a provider to sign out from",
7501
7963
  key: "change API key for a provider",
7964
+ refresh: "reload the model list for a provider",
7502
7965
  effort: "set reasoning depth",
7503
7966
  thinking: "toggle reasoning",
7504
7967
  budget: "set token budget per turn",
7505
7968
  theme: "change the accent colour",
7506
7969
  lang: "change the interface language",
7970
+ memory: "view, replace or clear project memory",
7507
7971
  prompt: "insert or save a prompt",
7508
7972
  "prompt save": "save the current or recent text as a prompt",
7509
7973
  "prompt delete": "delete a saved prompt",
@@ -7561,6 +8025,7 @@ var ru = {
7561
8025
  titleProvider: "\u041F\u0440\u043E\u0432\u0430\u0439\u0434\u0435\u0440",
7562
8026
  titleLogout: "\u0412\u044B\u0431\u0440\u0430\u0442\u044C API \u0434\u043B\u044F \u0432\u044B\u0445\u043E\u0434\u0430",
7563
8027
  titleKeyChange: "\u0421\u043C\u0435\u043D\u0438\u0442\u044C \u043A\u043B\u044E\u0447 \u043F\u0440\u043E\u0432\u0430\u0439\u0434\u0435\u0440\u0430",
8028
+ titleModelRefresh: "\u041E\u0431\u043D\u043E\u0432\u0438\u0442\u044C \u043C\u043E\u0434\u0435\u043B\u0438 \u043F\u0440\u043E\u0432\u0430\u0439\u0434\u0435\u0440\u0430",
7564
8029
  titleSessions: "\u0412\u043E\u0441\u0441\u0442\u0430\u043D\u043E\u0432\u0438\u0442\u044C \u0441\u0435\u0441\u0441\u0438\u044E",
7565
8030
  titleSessionAction: "\u0414\u0435\u0439\u0441\u0442\u0432\u0438\u0435 \u0441 \u0441\u0435\u0441\u0441\u0438\u0435\u0439",
7566
8031
  resumed: (id, count) => `\u0421\u0435\u0441\u0441\u0438\u044F ${id} \u0432\u043E\u0441\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D\u0430 \u2014 \u0441\u043E\u043E\u0431\u0449\u0435\u043D\u0438\u0439: ${count}`,
@@ -7576,7 +8041,7 @@ var ru = {
7576
8041
  escHelp: "esc \u2014 \u043E\u0442\u043C\u0435\u043D\u0438\u0442\u044C \u0442\u0435\u043A\u0443\u0449\u0438\u0439 \u0445\u043E\u0434",
7577
8042
  cancelled: "\u041E\u0442\u043C\u0435\u043D\u0435\u043D\u043E.",
7578
8043
  queued: (count) => `\u0432 \u043E\u0447\u0435\u0440\u0435\u0434\u0438: ${count}`,
7579
- configAt: (path14) => `\u043A\u043E\u043D\u0444\u0438\u0433: ${path14}`,
8044
+ configAt: (path15) => `\u043A\u043E\u043D\u0444\u0438\u0433: ${path15}`,
7580
8045
  skillsEmpty: "\u0421\u043A\u0438\u043B\u043B\u043E\u0432 \u043D\u0435\u0442. \u041F\u043E\u043B\u043E\u0436\u0438 \u043F\u0430\u043F\u043A\u0443 \u0441 \u0444\u0430\u0439\u043B\u043E\u043C SKILL.md \u0432 ~/.kitcode/skills \u0438\u043B\u0438 ./.kitcode/skills",
7581
8046
  skillsInstallUsage: "\u0418\u0441\u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u043D\u0438\u0435: /skills install <github-url|npm-\u043F\u0430\u043A\u0435\u0442|\u043B\u043E\u043A\u0430\u043B\u044C\u043D\u044B\u0439-\u043F\u0443\u0442\u044C>",
7582
8047
  skillsInstalling: (source) => `\u0423\u0441\u0442\u0430\u043D\u0430\u0432\u043B\u0438\u0432\u0430\u044E \u0441\u043A\u0438\u043B\u043B \u0438\u0437 "${source}"\u2026`,
@@ -7622,7 +8087,7 @@ var ru = {
7622
8087
  sessionDeleteAllFinalBody: "\u042D\u0442\u043E \u043D\u0435\u043B\u044C\u0437\u044F \u043E\u0442\u043C\u0435\u043D\u0438\u0442\u044C. \u041D\u0430\u0436\u0438\u043C\u0430\u0439 y \u0442\u043E\u043B\u044C\u043A\u043E \u0435\u0441\u043B\u0438 \u0445\u043E\u0447\u0435\u0448\u044C \u0441\u0442\u0435\u0440\u0435\u0442\u044C \u0432\u0441\u044E \u0438\u0441\u0442\u043E\u0440\u0438\u044E \u0441\u0435\u0441\u0441\u0438\u0439.",
7623
8088
  sessionsDeletedAll: (count) => `\u0412\u0441\u0435 \u0441\u043E\u0445\u0440\u0430\u043D\u0451\u043D\u043D\u044B\u0435 \u0447\u0430\u0442\u044B \u0443\u0434\u0430\u043B\u0435\u043D\u044B \xB7 \u0441\u0435\u0441\u0441\u0438\u0439: ${count}.`,
7624
8089
  sessionsDeleteAllFailed: (deleted, failed) => `\u0423\u0434\u0430\u043B\u0435\u043D\u0438\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043D\u043E \u043D\u0435 \u043F\u043E\u043B\u043D\u043E\u0441\u0442\u044C\u044E \xB7 \u0443\u0434\u0430\u043B\u0435\u043D\u043E: ${deleted}, \u043E\u0448\u0438\u0431\u043E\u043A: ${failed}.`,
7625
- sessionExported: (path14) => `\u0421\u0435\u0441\u0441\u0438\u044F \u044D\u043A\u0441\u043F\u043E\u0440\u0442\u0438\u0440\u043E\u0432\u0430\u043D\u0430: ${path14}`,
8090
+ sessionExported: (path15) => `\u0421\u0435\u0441\u0441\u0438\u044F \u044D\u043A\u0441\u043F\u043E\u0440\u0442\u0438\u0440\u043E\u0432\u0430\u043D\u0430: ${path15}`,
7626
8091
  sessionActionResume: "\u043E\u0442\u043A\u0440\u044B\u0442\u044C",
7627
8092
  sessionActionRename: "\u043F\u0435\u0440\u0435\u0438\u043C\u0435\u043D\u043E\u0432\u0430\u0442\u044C",
7628
8093
  sessionActionDelete: "\u0443\u0434\u0430\u043B\u0438\u0442\u044C",
@@ -7640,6 +8105,7 @@ ${url}`,
7640
8105
  budgetUnlimited: "\u041B\u0438\u043C\u0438\u0442 \u0442\u043E\u043A\u0435\u043D\u043E\u0432: \u0431\u0435\u0437\u043B\u0438\u043C\u0438\u0442\u043D\u044B\u0439 (0). \u0412\u0432\u0435\u0434\u0438 /budget <\u0447\u0438\u0441\u043B\u043E> \u0447\u0442\u043E\u0431\u044B \u0432\u0435\u0440\u043D\u0443\u0442\u044C \u043B\u0438\u043C\u0438\u0442.",
7641
8106
  budgetInvalid: "\u041D\u0435\u0432\u0435\u0440\u043D\u044B\u0439 \u043B\u0438\u043C\u0438\u0442: \u043E\u0442 1 000 \u0434\u043E 10 000 000",
7642
8107
  modelSet: (value) => `\u041C\u043E\u0434\u0435\u043B\u044C: ${value}`,
8108
+ modelsRefreshed: (provider, count) => `${provider}: \u0441 \u044D\u043D\u0434\u043F\u043E\u0438\u043D\u0442\u0430 \u043F\u043E\u043B\u0443\u0447\u0435\u043D\u043E \u043C\u043E\u0434\u0435\u043B\u0435\u0439 \u2014 ${count}.`,
7643
8109
  noModels: "\u041C\u043E\u0434\u0435\u043B\u0435\u0439 \u043D\u0435\u0442. \u0414\u043E\u0431\u0430\u0432\u044C \u043F\u0440\u043E\u0432\u0430\u0439\u0434\u0435\u0440\u0430 \u0447\u0435\u0440\u0435\u0437 /login.",
7644
8110
  bypassOff: "\u041F\u043E\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043D\u0438\u044F \u0441\u043D\u043E\u0432\u0430 \u0432\u043A\u043B\u044E\u0447\u0435\u043D\u044B.",
7645
8111
  bypassAsk1: "\u041E\u0442\u043A\u043B\u044E\u0447\u0438\u0442\u044C \u043F\u043E\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043D\u0438\u044F \u043D\u0430 \u044D\u0442\u0443 \u0441\u0435\u0441\u0441\u0438\u044E?",
@@ -7687,11 +8153,13 @@ ${url}`,
7687
8153
  provider: "\u043F\u0435\u0440\u0435\u043A\u043B\u044E\u0447\u0438\u0442\u044C\u0441\u044F \u043C\u0435\u0436\u0434\u0443 \u043F\u0440\u043E\u0432\u0430\u0439\u0434\u0435\u0440\u0430\u043C\u0438",
7688
8154
  logout: "\u0432\u044B\u0431\u0440\u0430\u0442\u044C \u043F\u0440\u043E\u0432\u0430\u0439\u0434\u0435\u0440\u0430 \u0438 \u0443\u0434\u0430\u043B\u0438\u0442\u044C \u0435\u0433\u043E \u043A\u043B\u044E\u0447",
7689
8155
  key: "\u0441\u043C\u0435\u043D\u0438\u0442\u044C \u043A\u043B\u044E\u0447 \u043F\u0440\u043E\u0432\u0430\u0439\u0434\u0435\u0440\u0430",
8156
+ refresh: "\u043E\u0431\u043D\u043E\u0432\u0438\u0442\u044C \u0441\u043F\u0438\u0441\u043E\u043A \u043C\u043E\u0434\u0435\u043B\u0435\u0439 \u043F\u0440\u043E\u0432\u0430\u0439\u0434\u0435\u0440\u0430",
7690
8157
  effort: "\u0433\u043B\u0443\u0431\u0438\u043D\u0430 \u0440\u0430\u0437\u043C\u044B\u0448\u043B\u0435\u043D\u0438\u0439",
7691
8158
  thinking: "\u0432\u043A\u043B\u044E\u0447\u0438\u0442\u044C/\u0432\u044B\u043A\u043B\u044E\u0447\u0438\u0442\u044C \u0440\u0430\u0437\u043C\u044B\u0448\u043B\u0435\u043D\u0438\u044F",
7692
8159
  budget: "\u043B\u0438\u043C\u0438\u0442 \u0442\u043E\u043A\u0435\u043D\u043E\u0432 \u0437\u0430 \u0445\u043E\u0434 (0 = \u0431\u0435\u0437\u043B\u0438\u043C\u0438\u0442)",
7693
8160
  theme: "\u0441\u043C\u0435\u043D\u0438\u0442\u044C \u0446\u0432\u0435\u0442 \u0430\u043A\u0446\u0435\u043D\u0442\u0430",
7694
8161
  lang: "\u0441\u043C\u0435\u043D\u0438\u0442\u044C \u044F\u0437\u044B\u043A \u0438\u043D\u0442\u0435\u0440\u0444\u0435\u0439\u0441\u0430",
8162
+ memory: "\u043F\u043E\u043A\u0430\u0437\u0430\u0442\u044C, \u0437\u0430\u043C\u0435\u043D\u0438\u0442\u044C \u0438\u043B\u0438 \u043E\u0447\u0438\u0441\u0442\u0438\u0442\u044C \u043F\u0430\u043C\u044F\u0442\u044C \u043F\u0440\u043E\u0435\u043A\u0442\u0430",
7695
8163
  prompt: "\u0432\u0441\u0442\u0430\u0432\u0438\u0442\u044C \u0438\u043B\u0438 \u0441\u043E\u0445\u0440\u0430\u043D\u0438\u0442\u044C \u043F\u0440\u043E\u043C\u0442",
7696
8164
  skills: "\u0441\u043F\u0438\u0441\u043E\u043A \u0443\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D\u043D\u044B\u0445 \u0441\u043A\u0438\u043B\u043B\u043E\u0432",
7697
8165
  "prompt delete": "\u0443\u0434\u0430\u043B\u0438\u0442\u044C \u0441\u043E\u0445\u0440\u0430\u043D\u0451\u043D\u043D\u044B\u0439 \u043F\u0440\u043E\u043C\u0442",
@@ -8174,7 +8642,8 @@ var PromptInput = memo(function PromptInput2({
8174
8642
  pending,
8175
8643
  hint,
8176
8644
  history,
8177
- attachments = []
8645
+ attachments = [],
8646
+ onListFiles
8178
8647
  }) {
8179
8648
  const theme = useTheme();
8180
8649
  const { columns } = useTerminalSize();
@@ -8189,9 +8658,28 @@ var PromptInput = memo(function PromptInput2({
8189
8658
  const cursorRef = useRef2(inputCursor);
8190
8659
  valueRef.current = safeValue;
8191
8660
  cursorRef.current = inputCursor;
8192
- const suggestions = matchCommands(safeValue);
8193
- const open3 = suggestions.length > 0;
8194
- const active2 = Math.min(selectionCursor, Math.max(0, suggestions.length - 1));
8661
+ const commandSuggestions = matchCommands(safeValue);
8662
+ const mention = onListFiles ? mentionSpan(safeValue, inputCursor) : null;
8663
+ const [files, setFiles] = useState4([]);
8664
+ const mentionQuery = mention?.query ?? null;
8665
+ useEffect(() => {
8666
+ if (mentionQuery === null || !onListFiles) {
8667
+ setFiles([]);
8668
+ return;
8669
+ }
8670
+ let live = true;
8671
+ void onListFiles(mentionQuery).then((found) => {
8672
+ if (live) setFiles(found);
8673
+ });
8674
+ return () => {
8675
+ live = false;
8676
+ };
8677
+ }, [mentionQuery, onListFiles]);
8678
+ const showFiles = mention !== null && files.length > 0;
8679
+ const suggestions = showFiles ? [] : commandSuggestions;
8680
+ const open3 = suggestions.length > 0 || showFiles;
8681
+ const optionCount = showFiles ? files.length : suggestions.length;
8682
+ const active2 = Math.min(selectionCursor, Math.max(0, optionCount - 1));
8195
8683
  useEffect(() => {
8196
8684
  setSelectionCursor(0);
8197
8685
  }, [safeValue]);
@@ -8199,14 +8687,14 @@ var PromptInput = memo(function PromptInput2({
8199
8687
  const length = characters2(safeValue).length;
8200
8688
  if (pendingValue.current === safeValue) {
8201
8689
  pendingValue.current = null;
8202
- setInputCursor((cursor) => clamp(cursor, 0, length));
8690
+ setInputCursor((cursor) => clamp2(cursor, 0, length));
8203
8691
  return;
8204
8692
  }
8205
8693
  setInputCursor(length);
8206
8694
  }, [safeValue]);
8207
8695
  const change = (next, cursor = characters2(next).length) => {
8208
8696
  next = sanitizeTerminalText(next);
8209
- const nextCursor2 = clamp(cursor, 0, characters2(next).length);
8697
+ const nextCursor2 = clamp2(cursor, 0, characters2(next).length);
8210
8698
  pendingValue.current = next;
8211
8699
  valueRef.current = next;
8212
8700
  cursorRef.current = nextCursor2;
@@ -8224,7 +8712,7 @@ var PromptInput = memo(function PromptInput2({
8224
8712
  const inserted = characters2(sanitizeTerminalText(pasted));
8225
8713
  if (inserted.length === 0) return;
8226
8714
  const valueCharacters = characters2(valueRef.current);
8227
- const cursor = clamp(cursorRef.current, 0, valueCharacters.length);
8715
+ const cursor = clamp2(cursorRef.current, 0, valueCharacters.length);
8228
8716
  valueCharacters.splice(cursor, 0, ...inserted);
8229
8717
  change(valueCharacters.join(""), cursor + inserted.length);
8230
8718
  };
@@ -8248,7 +8736,15 @@ var PromptInput = memo(function PromptInput2({
8248
8736
  return;
8249
8737
  }
8250
8738
  if (open3 && key.downArrow) {
8251
- setSelectionCursor(Math.min(suggestions.length - 1, active2 + 1));
8739
+ setSelectionCursor(Math.min(optionCount - 1, active2 + 1));
8740
+ return;
8741
+ }
8742
+ if (showFiles && (key.tab || key.return) && !key.shift) {
8743
+ const chosen = files[active2];
8744
+ if (chosen && mention) {
8745
+ const completed = completeMention(safeValue, mention, chosen);
8746
+ change(completed.value, completed.cursor);
8747
+ }
8252
8748
  return;
8253
8749
  }
8254
8750
  if (open3 && key.tab && !key.shift) {
@@ -8286,7 +8782,7 @@ var PromptInput = memo(function PromptInput2({
8286
8782
  return;
8287
8783
  }
8288
8784
  const valueCharacters = characters2(safeValue);
8289
- const cursor = clamp(inputCursor, 0, valueCharacters.length);
8785
+ const cursor = clamp2(inputCursor, 0, valueCharacters.length);
8290
8786
  if (key.leftArrow || key.rightArrow || key.home || key.end) {
8291
8787
  setInputCursor(nextCursor(cursor, valueCharacters.length, key));
8292
8788
  return;
@@ -8313,6 +8809,7 @@ var PromptInput = memo(function PromptInput2({
8313
8809
  });
8314
8810
  const start = Math.max(0, Math.min(active2 - WINDOW2 + 2, suggestions.length - WINDOW2));
8315
8811
  const visible = suggestions.slice(start, start + WINDOW2);
8812
+ const fileStart = Math.max(0, Math.min(active2 - WINDOW2 + 2, files.length - WINDOW2));
8316
8813
  return /* @__PURE__ */ jsxs8(Box8, { width: columns, maxWidth: "100%", flexDirection: "column", marginTop: 1, flexShrink: 0, children: [
8317
8814
  /* @__PURE__ */ jsxs8(
8318
8815
  Box8,
@@ -8341,6 +8838,18 @@ var PromptInput = memo(function PromptInput2({
8341
8838
  " ",
8342
8839
  sanitizeTerminalText(hint)
8343
8840
  ] }),
8841
+ showFiles && /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", marginLeft: 2, children: [
8842
+ files.slice(fileStart, fileStart + WINDOW2).map((file, index) => {
8843
+ const selected = fileStart + index === active2;
8844
+ return /* @__PURE__ */ jsxs8(Text8, { color: selected ? theme.accent : void 0, dimColor: !selected, children: [
8845
+ selected ? "\u276F " : " ",
8846
+ "@",
8847
+ sanitizeTerminalText(file)
8848
+ ] }, file);
8849
+ }),
8850
+ files.length > WINDOW2 && /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: strings.more(files.length - WINDOW2) }),
8851
+ /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: strings.suggestHelp })
8852
+ ] }),
8344
8853
  open3 && /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", marginLeft: 2, children: [
8345
8854
  visible.map((command, index) => {
8346
8855
  const selected = start + index === active2;
@@ -8366,7 +8875,7 @@ function EditableText({
8366
8875
  placeholder
8367
8876
  }) {
8368
8877
  const valueCharacters = characters2(value);
8369
- const at = clamp(cursor, 0, valueCharacters.length);
8878
+ const at = clamp2(cursor, 0, valueCharacters.length);
8370
8879
  if (valueCharacters.length === 0) {
8371
8880
  const placeholderCharacters = characters2(sanitizeTerminalText(placeholder));
8372
8881
  return /* @__PURE__ */ jsxs8(Text8, { children: [
@@ -8390,7 +8899,7 @@ function nextCursor(cursor, length, key) {
8390
8899
  function characters2(value) {
8391
8900
  return [...value];
8392
8901
  }
8393
- function clamp(value, min, max) {
8902
+ function clamp2(value, min, max) {
8394
8903
  return Math.max(min, Math.min(value, max));
8395
8904
  }
8396
8905
 
@@ -8545,6 +9054,7 @@ function ModelBadge({ ref: modelRef }) {
8545
9054
  return /* @__PURE__ */ jsx9(Box9, { borderColor: theme.accent, borderStyle: "round", paddingX: 1, children: /* @__PURE__ */ jsx9(Text9, { color: theme.accent, bold: true, children: name }) });
8546
9055
  }
8547
9056
  var EFFORT_STYLES = {
9057
+ auto: { label: "auto", color: "cyan" },
8548
9058
  max: { label: "max", color: "red", bold: true },
8549
9059
  xhigh: { label: "xhigh", color: "yellow", bold: true },
8550
9060
  high: { label: "high", color: "cyan" },
@@ -8638,13 +9148,18 @@ import Spinner2 from "ink-spinner";
8638
9148
  import stringWidth2 from "string-width";
8639
9149
 
8640
9150
  // src/ui/markdown.tsx
8641
- import { Box as Box11, Text as Text10 } from "ink";
8642
- import { Fragment as Fragment4, useMemo as useMemo3 } from "react";
9151
+ import { Box as Box11, Text as InkText } from "ink";
9152
+ import { createContext as createContext3, Fragment as Fragment4, useContext as useContext3, useMemo as useMemo3 } from "react";
8643
9153
  import stringWidth from "string-width";
8644
9154
  import { jsx as jsx11, jsxs as jsxs10 } from "react/jsx-runtime";
8645
- function Markdown({ children }) {
9155
+ var DimContext = createContext3(false);
9156
+ function Text10({ dimColor, ...props }) {
9157
+ const dim = useContext3(DimContext);
9158
+ return /* @__PURE__ */ jsx11(InkText, { ...props, dimColor: dimColor ?? dim });
9159
+ }
9160
+ function Markdown({ children, dim = false }) {
8646
9161
  const blocks = useMemo3(() => extractBlocks(children), [children]);
8647
- return /* @__PURE__ */ jsx11(Box11, { flexDirection: "column", children: blocks.map((block, i) => /* @__PURE__ */ jsx11(BlockView, { block }, i)) });
9162
+ return /* @__PURE__ */ jsx11(DimContext.Provider, { value: dim, children: /* @__PURE__ */ jsx11(Box11, { flexDirection: "column", children: blocks.map((block, i) => /* @__PURE__ */ jsx11(BlockView, { block }, i)) }) });
8648
9163
  }
8649
9164
  function BlockView({ block }) {
8650
9165
  switch (block.type) {
@@ -9292,7 +9807,7 @@ function AssistantView({ bubble: source, maxRows }) {
9292
9807
  const usedThinkingRows = frameThinking === "" ? 0 : frameThinking.split("\n").length;
9293
9808
  const frameText = liveBudget === void 0 ? bubble.text : clipTextToRows(bubble.text, Math.max(1, liveBudget - usedThinkingRows), columns);
9294
9809
  return /* @__PURE__ */ jsxs11(Box12, { flexDirection: "column", marginTop: 1, children: [
9295
- frameThinking !== "" && /* @__PURE__ */ jsx12(Box12, { flexDirection: "column", maxHeight: thinkingBudget, overflowY: "hidden", children: /* @__PURE__ */ jsx12(Markdown, { children: frameThinking }) }),
9810
+ frameThinking !== "" && /* @__PURE__ */ jsx12(Box12, { flexDirection: "column", maxHeight: thinkingBudget, overflowY: "hidden", children: /* @__PURE__ */ jsx12(Markdown, { dim: true, children: frameThinking }) }),
9296
9811
  bubble.streaming ? /* @__PURE__ */ jsx12(Text11, { children: frameText }) : /* @__PURE__ */ jsx12(Markdown, { children: bubble.text }),
9297
9812
  bubble.streaming && bubble.text === "" && /* @__PURE__ */ jsxs11(Text11, { dimColor: true, children: [
9298
9813
  /* @__PURE__ */ jsx12(Spinner2, { type: "dots" }),
@@ -9624,7 +10139,7 @@ function sanitizeDisplay(display) {
9624
10139
 
9625
10140
  // src/ui/App.tsx
9626
10141
  import { jsx as jsx13, jsxs as jsxs12 } from "react/jsx-runtime";
9627
- var EFFORTS = ["low", "medium", "high", "xhigh", "max"];
10142
+ var EFFORTS = ["auto", "low", "medium", "high", "xhigh", "max"];
9628
10143
  var STREAM_FRAME_MS = 50;
9629
10144
  var MAX_ATTACHMENTS = 8;
9630
10145
  function App({
@@ -9796,6 +10311,11 @@ function App({
9796
10311
  })
9797
10312
  };
9798
10313
  try {
10314
+ try {
10315
+ await runtime.persist(history.current);
10316
+ } catch (error) {
10317
+ notice("error", error instanceof Error ? error.message : String(error));
10318
+ }
9799
10319
  history.current = await runtime.run(history.current, hooks, controller.signal);
9800
10320
  void runtime.persist(history.current).catch((error) => notice("error", error instanceof Error ? error.message : String(error)));
9801
10321
  } catch (error) {
@@ -10123,6 +10643,29 @@ ${strings.modelSet(result.nextModel)}` : ""}`;
10123
10643
  }
10124
10644
  return;
10125
10645
  }
10646
+ case "refresh": {
10647
+ const providers = runtime.listProviderItems();
10648
+ if (providers.length === 0) {
10649
+ notice("warn", strings.logoutNothing);
10650
+ return;
10651
+ }
10652
+ let providerId = rest[0];
10653
+ if (!providerId) {
10654
+ providerId = providers.length === 1 ? providers[0]?.key : await pick(strings.titleModelRefresh, providers) ?? void 0;
10655
+ }
10656
+ if (!providerId) return;
10657
+ if (!providers.some((provider) => provider.key === providerId)) {
10658
+ notice("warn", `Provider "${providerId}" is not configured.`);
10659
+ return;
10660
+ }
10661
+ try {
10662
+ const count = await runtime.refreshProviderModels(providerId);
10663
+ notice("info", strings.modelsRefreshed(providerId, count));
10664
+ } catch (error) {
10665
+ notice("error", error instanceof Error ? error.message : String(error));
10666
+ }
10667
+ return;
10668
+ }
10126
10669
  case "lang": {
10127
10670
  const choice = await pick(
10128
10671
  strings.titleLang,
@@ -10476,14 +11019,27 @@ ${strings.mcpAddUsage}`
10476
11019
  forceRender((n) => n + 1);
10477
11020
  return;
10478
11021
  }
11022
+ case "memory": {
11023
+ if (rawRest === "clear") {
11024
+ 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();
11025
+ } else if (rawRest.startsWith("set ")) {
11026
+ await runtime.saveMemory(rawRest.slice(4).trim());
11027
+ } else if (rawRest !== "" && rawRest !== "show") {
11028
+ notice("warn", "/memory show | /memory set <text> | /memory clear");
11029
+ return;
11030
+ }
11031
+ 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."));
11032
+ return;
11033
+ }
10479
11034
  case "effort": {
10480
- const choice = await pick(
11035
+ notice("info", runtime.effortDescription(history.current));
11036
+ const choice = EFFORTS.includes(rawRest) ? rawRest : await pick(
10481
11037
  strings.titleEffort,
10482
11038
  EFFORTS.map((effort) => ({ key: effort, label: effort }))
10483
11039
  );
10484
11040
  if (!choice) return;
10485
11041
  await runtime.setEffort(choice);
10486
- notice("info", strings.effortSet(choice));
11042
+ notice("info", `${strings.effortSet(choice)} \xB7 ${runtime.effortDescription(history.current)}`);
10487
11043
  forceRender((n) => n + 1);
10488
11044
  return;
10489
11045
  }
@@ -10704,6 +11260,10 @@ Rename the file if you want a different name.`);
10704
11260
  },
10705
11261
  [appendAttachment, notice, runtime, strings]
10706
11262
  );
11263
+ const listWorkspaceFiles = useCallback(
11264
+ (query) => runtime.listWorkspaceFiles(query).catch(() => []),
11265
+ [runtime]
11266
+ );
10707
11267
  const pasteClipboardImage = useCallback(() => {
10708
11268
  if (clipboardPasteTask.current) return;
10709
11269
  if (attachmentsRef.current.length >= MAX_ATTACHMENTS) {
@@ -10767,11 +11327,21 @@ Rename the file if you want a different name.`);
10767
11327
  }
10768
11328
  const queuedAttachments = attachmentsRef.current;
10769
11329
  if (!text && queuedAttachments.length === 0) return;
11330
+ const mentioned = [];
11331
+ for (const path15 of extractMentions(text)) {
11332
+ if (queuedAttachments.length + mentioned.length >= MAX_ATTACHMENTS) break;
11333
+ try {
11334
+ mentioned.push(await runtime.loadAttachment(path15));
11335
+ } catch {
11336
+ }
11337
+ }
11338
+ if (generation !== attachmentGeneration.current) return;
10770
11339
  if (text) setPromptHistory((state) => appendInputHistory(state, text));
10771
11340
  if (!detachedInput) setInput("");
10772
11341
  const content = [
10773
11342
  ...text ? [{ type: "text", text }] : [],
10774
- ...queuedAttachments
11343
+ ...queuedAttachments,
11344
+ ...mentioned
10775
11345
  ];
10776
11346
  replaceAttachments([]);
10777
11347
  const item = { kind: "message", text, content };
@@ -10846,11 +11416,11 @@ Rename the file if you want a different name.`);
10846
11416
  Onboarding,
10847
11417
  {
10848
11418
  onSubmit: async (url, key) => {
10849
- const summary = await runtime.addProvider(url, key);
11419
+ const summary2 = await runtime.addProvider(url, key);
10850
11420
  setSetup(false);
10851
- notice("info", `${summary}
11421
+ notice("info", `${summary2}
10852
11422
  ${strings.configAt(runtime.configPath())}`);
10853
- return summary;
11423
+ return summary2;
10854
11424
  }
10855
11425
  }
10856
11426
  )
@@ -10927,6 +11497,7 @@ ${strings.configAt(runtime.configPath())}`);
10927
11497
  onSubmit: submit,
10928
11498
  onPastePath: tryQueueAutomaticAttachment,
10929
11499
  onPasteImage: pasteClipboardImage,
11500
+ onListFiles: listWorkspaceFiles,
10930
11501
  disabled: busy,
10931
11502
  pending: pendingCount,
10932
11503
  hint: busy ? strings.escCancel : void 0,