@kernelonpanic/kitcode 1.3.0 → 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
@@ -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
@@ -1087,6 +1116,261 @@ ${store.read()}`, display: { kind: "diff", path: "project memory", before, after
1087
1116
  };
1088
1117
  }
1089
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
+
1090
1374
  // src/core/memory.ts
1091
1375
  import { createHash } from "crypto";
1092
1376
  import { readFile as readFile3, writeFile as writeFile3, rm as rm3 } from "fs/promises";
@@ -1778,7 +2062,7 @@ async function runOneCall(cfg, call, hooks, signal) {
1778
2062
  if (!tool) {
1779
2063
  return toolError(call.id, `Unknown tool "${call.name}". Use only the tools provided.`);
1780
2064
  }
1781
- const summary = describeCall(tool, call.input);
2065
+ const summary2 = describeCall(tool, call.input);
1782
2066
  const ctx = {
1783
2067
  cwd: cfg.cwd,
1784
2068
  signal,
@@ -1802,7 +2086,7 @@ async function runOneCall(cfg, call, hooks, signal) {
1802
2086
  if (gate === "ask") {
1803
2087
  const decision = await hooks.requestPermission({
1804
2088
  toolName: tool.name,
1805
- summary,
2089
+ summary: summary2,
1806
2090
  input: call.input,
1807
2091
  display: await previewCall(tool, call.input, ctx),
1808
2092
  allowAlways: canAlwaysApprove(tool)
@@ -1815,7 +2099,7 @@ async function runOneCall(cfg, call, hooks, signal) {
1815
2099
  if (signal.aborted) {
1816
2100
  return toolError(call.id, "Interrupted by the user before this tool ran.");
1817
2101
  }
1818
- 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 });
1819
2103
  try {
1820
2104
  const result = await tool.execute(call.input, ctx);
1821
2105
  hooks.onEvent({
@@ -1987,43 +2271,6 @@ import {
1987
2271
  writeFile as writeFile5
1988
2272
  } from "fs/promises";
1989
2273
  import path6 from "path";
1990
-
1991
- // src/tools/safepath.ts
1992
- import { realpathSync } from "fs";
1993
- import { basename, dirname, isAbsolute, relative, resolve, sep } from "path";
1994
- function canonicalize(target) {
1995
- try {
1996
- return realpathSync(target);
1997
- } catch {
1998
- let current = target;
1999
- const trailing = [];
2000
- for (let i = 0; i < 64; i++) {
2001
- try {
2002
- return resolve(realpathSync(current), ...trailing);
2003
- } catch {
2004
- const parent = dirname(current);
2005
- if (parent === current) break;
2006
- trailing.unshift(basename(current));
2007
- current = parent;
2008
- }
2009
- }
2010
- return resolve(current, ...trailing);
2011
- }
2012
- }
2013
- function resolveInside(cwd, userPath) {
2014
- const root = canonicalize(resolve(cwd));
2015
- const target = canonicalize(resolve(root, userPath));
2016
- const rel = relative(root, target);
2017
- if (rel !== "" && (isAbsolute(rel) || rel.split(sep)[0] === "..")) {
2018
- return { ok: false, reason: `Path ${userPath} resolves outside the workspace root ${root}` };
2019
- }
2020
- return { ok: true, path: target, relative: rel };
2021
- }
2022
- function patternEscapes(pattern) {
2023
- return isAbsolute(pattern) || pattern.split("/").includes("..") || pattern.split("\\").includes("..");
2024
- }
2025
-
2026
- // src/core/checkpoint.ts
2027
2274
  var VERSION = 1;
2028
2275
  var MAX_FILE_BYTES = 5e6;
2029
2276
  var MAX_CHECKPOINT_BYTES = 2e7;
@@ -2335,14 +2582,14 @@ import { readFile as readFile6, stat as stat4 } from "fs/promises";
2335
2582
  import path7 from "path";
2336
2583
 
2337
2584
  // src/tools/bash.ts
2338
- import { spawn } from "child_process";
2585
+ import { spawn as spawn2 } from "child_process";
2339
2586
  var DEFAULT_TIMEOUT = 12e4;
2340
2587
  var MAX_TIMEOUT = 6e5;
2341
2588
  var MAX_OUTPUT = 1e5;
2342
2589
  var MAX_COMMAND_CHARS = 2e4;
2343
2590
  var bashTool = {
2344
2591
  name: "bash",
2345
- 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.",
2346
2593
  inputSchema: {
2347
2594
  type: "object",
2348
2595
  properties: {
@@ -2353,8 +2600,12 @@ var bashTool = {
2353
2600
  },
2354
2601
  timeoutMs: {
2355
2602
  type: "integer",
2356
- 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`,
2357
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."
2358
2609
  }
2359
2610
  },
2360
2611
  required: ["command"],
@@ -2362,13 +2613,14 @@ var bashTool = {
2362
2613
  },
2363
2614
  defaultPermission: "ask",
2364
2615
  summarize(input) {
2365
- return `bash(${brief(input.command)})`;
2616
+ const { command, background } = input;
2617
+ return background ? `bash(${brief(command)}, background)` : `bash(${brief(command)})`;
2366
2618
  },
2367
2619
  async preview(input) {
2368
2620
  return { kind: "text", text: input.command };
2369
2621
  },
2370
2622
  execute(input, ctx) {
2371
- const { command, timeoutMs } = input;
2623
+ const { command, timeoutMs, background } = input;
2372
2624
  if (command.length > MAX_COMMAND_CHARS) {
2373
2625
  return Promise.resolve({
2374
2626
  content: `Command exceeds the ${MAX_COMMAND_CHARS} character limit.`,
@@ -2380,8 +2632,17 @@ var bashTool = {
2380
2632
  if (ctx.signal.aborted) {
2381
2633
  return Promise.resolve({ content: "[cancelled before the command started]", isError: true });
2382
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
+ }
2383
2644
  return new Promise((settle) => {
2384
- const child = spawn(command, {
2645
+ const child = spawn2(command, {
2385
2646
  shell: true,
2386
2647
  cwd: ctx.cwd,
2387
2648
  detached: process.platform !== "win32",
@@ -2409,11 +2670,11 @@ var bashTool = {
2409
2670
  };
2410
2671
  child.stdout.on("data", collect);
2411
2672
  child.stderr.on("data", collect);
2412
- const killTree = () => {
2673
+ const killTree2 = () => {
2413
2674
  if (child.pid == null) return;
2414
2675
  if (process.platform === "win32") {
2415
2676
  try {
2416
- spawn("taskkill", ["/pid", String(child.pid), "/t", "/f"], { stdio: "ignore" });
2677
+ spawn2("taskkill", ["/pid", String(child.pid), "/t", "/f"], { stdio: "ignore" });
2417
2678
  } catch {
2418
2679
  child.kill("SIGKILL");
2419
2680
  }
@@ -2429,11 +2690,11 @@ var bashTool = {
2429
2690
  let aborted = false;
2430
2691
  const timer = setTimeout(() => {
2431
2692
  timedOut = true;
2432
- killTree();
2693
+ killTree2();
2433
2694
  }, timeout);
2434
2695
  const onAbort = () => {
2435
2696
  aborted = true;
2436
- killTree();
2697
+ killTree2();
2437
2698
  };
2438
2699
  ctx.signal.addEventListener("abort", onAbort, { once: true });
2439
2700
  const finish = (result) => {
@@ -2495,20 +2756,20 @@ async function detectDiagnosticCommands(cwd, configured = []) {
2495
2756
  }
2496
2757
  async function runAutomaticDiagnostics(options) {
2497
2758
  const commands = await detectDiagnosticCommands(options.cwd, options.configuredCommands);
2498
- const summary = { commands, failed: [], skipped: false };
2759
+ const summary2 = { commands, failed: [], skipped: false };
2499
2760
  if (options.changedFiles.length === 0 || commands.length === 0 || options.signal.aborted) {
2500
- summary.skipped = true;
2501
- return summary;
2761
+ summary2.skipped = true;
2762
+ return summary2;
2502
2763
  }
2503
2764
  const gate = options.permissions.decide(diagnosticsTool);
2504
2765
  if (gate === "deny") {
2505
- summary.skipped = true;
2766
+ summary2.skipped = true;
2506
2767
  options.hooks.onEvent({
2507
2768
  type: "notice",
2508
2769
  level: "warn",
2509
2770
  text: "Automatic project checks were skipped by the current permission settings."
2510
2771
  });
2511
- return summary;
2772
+ return summary2;
2512
2773
  }
2513
2774
  if (gate === "ask") {
2514
2775
  const decision = await options.hooks.requestPermission({
@@ -2523,17 +2784,17 @@ async function runAutomaticDiagnostics(options) {
2523
2784
  allowAlways: true
2524
2785
  });
2525
2786
  if (options.signal.aborted) {
2526
- summary.skipped = true;
2527
- return summary;
2787
+ summary2.skipped = true;
2788
+ return summary2;
2528
2789
  }
2529
2790
  if (decision === "deny") {
2530
- summary.skipped = true;
2791
+ summary2.skipped = true;
2531
2792
  options.hooks.onEvent({
2532
2793
  type: "notice",
2533
2794
  level: "warn",
2534
2795
  text: "Automatic project checks were skipped."
2535
2796
  });
2536
- return summary;
2797
+ return summary2;
2537
2798
  }
2538
2799
  if (decision === "always") options.permissions.grantForSession(diagnosticsTool.name);
2539
2800
  }
@@ -2546,7 +2807,7 @@ async function runAutomaticDiagnostics(options) {
2546
2807
  const execute = options.execute ?? ((command, context) => bashTool.execute({ command, timeoutMs: DIAGNOSTIC_TIMEOUT_MS }, context));
2547
2808
  for (const [index, command] of commands.entries()) {
2548
2809
  if (options.signal.aborted) {
2549
- summary.skipped = true;
2810
+ summary2.skipped = true;
2550
2811
  break;
2551
2812
  }
2552
2813
  const id = `diagnostics-${Date.now()}-${index}`;
@@ -2554,7 +2815,7 @@ async function runAutomaticDiagnostics(options) {
2554
2815
  options.hooks.onEvent({ type: "tool_start", id, name: diagnosticsTool.name, summary: callSummary });
2555
2816
  try {
2556
2817
  const result = await execute(command, ctx);
2557
- if (result.isError) summary.failed.push(command);
2818
+ if (result.isError) summary2.failed.push(command);
2558
2819
  options.hooks.onEvent({
2559
2820
  type: "tool_end",
2560
2821
  id,
@@ -2563,7 +2824,7 @@ async function runAutomaticDiagnostics(options) {
2563
2824
  display: result.display
2564
2825
  });
2565
2826
  } catch (error) {
2566
- summary.failed.push(command);
2827
+ summary2.failed.push(command);
2567
2828
  options.hooks.onEvent({
2568
2829
  type: "tool_end",
2569
2830
  id,
@@ -2572,7 +2833,7 @@ async function runAutomaticDiagnostics(options) {
2572
2833
  });
2573
2834
  }
2574
2835
  }
2575
- return summary;
2836
+ return summary2;
2576
2837
  }
2577
2838
  async function readPackageJson(file) {
2578
2839
  try {
@@ -2991,7 +3252,7 @@ async function compactHistory(options) {
2991
3252
  if (!rendered.trim()) {
2992
3253
  return { history: options.history, compacted: false, removedMessages: 0 };
2993
3254
  }
2994
- let summary;
3255
+ let summary2;
2995
3256
  let usage;
2996
3257
  let rateLimits;
2997
3258
  if (rendered.length <= SINGLE_REQUEST_MAX_CHARS) {
@@ -3005,7 +3266,7 @@ async function compactHistory(options) {
3005
3266
  SYSTEM_SUMMARIZE,
3006
3267
  requestMax
3007
3268
  );
3008
- summary = result.text;
3269
+ summary2 = result.text;
3009
3270
  usage = result.usage;
3010
3271
  rateLimits = result.rateLimits;
3011
3272
  } else {
@@ -3040,19 +3301,19 @@ async function compactHistory(options) {
3040
3301
  chunkResults.map((result) => result.text),
3041
3302
  limits.merge
3042
3303
  );
3043
- summary = mergeResult.text;
3304
+ summary2 = mergeResult.text;
3044
3305
  if (mergeResult.usage) {
3045
3306
  usage = usage ? mergeUsage([usage, mergeResult.usage]) : mergeResult.usage;
3046
3307
  }
3047
3308
  if (mergeResult.rateLimits) rateLimits = mergeResult.rateLimits;
3048
3309
  }
3049
- summary = summary.trim();
3050
- 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.");
3051
3312
  const prefix = [
3052
3313
  {
3053
3314
  role: "user",
3054
3315
  content: [{ type: "text", text: `[Earlier conversation summary]
3055
- ${summary}` }]
3316
+ ${summary2}` }]
3056
3317
  },
3057
3318
  {
3058
3319
  role: "assistant",
@@ -3196,8 +3457,8 @@ async function mergePartialSummaries(provider, model, maxTokens, signal, summari
3196
3457
  while (current.length > 1) {
3197
3458
  const groups = groupSummaries(current, MERGE_TARGET_CHARS);
3198
3459
  const sources = groups.map(
3199
- (group) => group.map((summary, index) => `## Part ${index + 1}
3200
- ${summary}`).join("\n\n")
3460
+ (group) => group.map((summary2, index) => `## Part ${index + 1}
3461
+ ${summary2}`).join("\n\n")
3201
3462
  );
3202
3463
  const merged = await summarizeSourcesParallel(
3203
3464
  provider,
@@ -3256,22 +3517,22 @@ function groupSummaries(summaries, targetChars) {
3256
3517
  const groups = [];
3257
3518
  let current = [];
3258
3519
  let size = 0;
3259
- for (const summary of summaries) {
3260
- 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) {
3261
3522
  groups.push(current);
3262
3523
  current = [];
3263
3524
  size = 0;
3264
3525
  }
3265
3526
  const separator = current.length > 0 ? 12 : 0;
3266
- current.push(summary);
3267
- size += separator + summary.length;
3527
+ current.push(summary2);
3528
+ size += separator + summary2.length;
3268
3529
  }
3269
3530
  if (current.length > 0) groups.push(current);
3270
3531
  return groups;
3271
3532
  }
3272
3533
  async function summarizeText(provider, model, maxTokens, signal, source, systemPrompt, maxOutputTokens) {
3273
3534
  if (signal.aborted) throw signal.reason ?? new Error("Context compaction was cancelled.");
3274
- let summary = "";
3535
+ let summary2 = "";
3275
3536
  let finalText2 = "";
3276
3537
  let usage;
3277
3538
  let rateLimits;
@@ -3285,7 +3546,7 @@ async function summarizeText(provider, model, maxTokens, signal, source, systemP
3285
3546
  signal
3286
3547
  });
3287
3548
  for await (const event of stream) {
3288
- if (event.type === "text_delta") summary += event.text;
3549
+ if (event.type === "text_delta") summary2 += event.text;
3289
3550
  else if (event.type === "usage") usage = event.usage;
3290
3551
  else if (event.type === "rate_limits") rateLimits = event.limits;
3291
3552
  else if (event.type === "done") {
@@ -3293,7 +3554,7 @@ async function summarizeText(provider, model, maxTokens, signal, source, systemP
3293
3554
  }
3294
3555
  }
3295
3556
  if (signal.aborted) throw signal.reason ?? new Error("Context compaction was cancelled.");
3296
- return { text: (finalText2 || summary).trim(), usage, rateLimits };
3557
+ return { text: (finalText2 || summary2).trim(), usage, rateLimits };
3297
3558
  }
3298
3559
  function mergeUsage(usages) {
3299
3560
  return usages.reduce(
@@ -3637,7 +3898,7 @@ function clip(value) {
3637
3898
  // package.json
3638
3899
  var package_default = {
3639
3900
  name: "@kernelonpanic/kitcode",
3640
- version: "1.3.0",
3901
+ version: "1.3.1",
3641
3902
  description: "Terminal coding agent with a config you never have to write by hand",
3642
3903
  type: "module",
3643
3904
  license: "MIT",
@@ -3705,7 +3966,7 @@ var package_default = {
3705
3966
 
3706
3967
  // src/version.ts
3707
3968
  var KITCODE_VERSION = package_default.version;
3708
- var KITCODE_COMMIT = true ? "7cd983ca3dc89406326e5e513870686036c3ab9c" : "development";
3969
+ var KITCODE_COMMIT = true ? "4320a2bd340f59e42397dd4afbfde0051ddfeb27" : "development";
3709
3970
 
3710
3971
  // src/mcp/client.ts
3711
3972
  var clientInfo = { name: "kitcode", version: KITCODE_VERSION };
@@ -3903,8 +4164,8 @@ function describeFailure2(error, stderr, secrets) {
3903
4164
  return redactSecrets(stderr ? `${message} \u2014 ${stderr}` : message, secrets);
3904
4165
  }
3905
4166
  function configuredSecrets(config) {
3906
- const record2 = config.type === "stdio" ? config.env : config.headers;
3907
- 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);
3908
4169
  }
3909
4170
  function inheritedEnv(source = process.env) {
3910
4171
  const env = {};
@@ -4000,10 +4261,10 @@ function endpoint(origin, path15, parse2) {
4000
4261
  return { url: `${origin}${path15}`, parse: parse2 };
4001
4262
  }
4002
4263
  function deepSeekBalance(body) {
4003
- const infos = record(body)?.["balance_infos"];
4264
+ const infos = record2(body)?.["balance_infos"];
4004
4265
  if (!Array.isArray(infos)) return null;
4005
4266
  const balances = infos.flatMap((item) => {
4006
- const entry = record(item);
4267
+ const entry = record2(item);
4007
4268
  const currency = currencyCode(entry?.["currency"]);
4008
4269
  const amount = decimal(entry?.["total_balance"]);
4009
4270
  return currency && amount ? [{ kind: "balance", currency, amount }] : [];
@@ -4011,11 +4272,11 @@ function deepSeekBalance(body) {
4011
4272
  return balances.length > 0 ? balances : null;
4012
4273
  }
4013
4274
  function openRouterKeyBalance(body) {
4014
- const remaining = finiteNumber(record(record(body)?.["data"])?.["limit_remaining"]);
4275
+ const remaining = finiteNumber(record2(record2(body)?.["data"])?.["limit_remaining"]);
4015
4276
  return remaining === null ? null : [{ kind: "key-limit", currency: "USD", amount: String(remaining) }];
4016
4277
  }
4017
4278
  function openRouterCreditsBalance(body) {
4018
- const data = record(record(body)?.["data"]);
4279
+ const data = record2(record2(body)?.["data"]);
4019
4280
  const credits = finiteNumber(data?.["total_credits"]);
4020
4281
  const usage = finiteNumber(data?.["total_usage"]);
4021
4282
  if (credits === null || usage === null) return null;
@@ -4028,26 +4289,26 @@ function openRouterCreditsBalance(body) {
4028
4289
  ];
4029
4290
  }
4030
4291
  function siliconFlowBalance(body) {
4031
- const root = record(body);
4292
+ const root = record2(body);
4032
4293
  if (root?.["status"] !== true) return null;
4033
- const amount = decimal(record(root["data"])?.["balance"]);
4294
+ const amount = decimal(record2(root["data"])?.["balance"]);
4034
4295
  return amount ? [{ kind: "balance", currency: "credits", amount }] : null;
4035
4296
  }
4036
4297
  function apiMartBalance(body) {
4037
- const root = record(body);
4298
+ const root = record2(body);
4038
4299
  if (root?.["success"] !== true) return null;
4039
4300
  const amount = decimal(root["remain_balance"]);
4040
4301
  return amount ? [{ kind: "balance", currency: "credits", amount }] : null;
4041
4302
  }
4042
4303
  function genericBalance(body) {
4043
- const root = record(body);
4044
- const data = record(root?.["data"]);
4304
+ const root = record2(body);
4305
+ const data = record2(root?.["data"]);
4045
4306
  const amount = decimal(root?.["balance"] ?? data?.["balance"]);
4046
4307
  if (!amount) return null;
4047
4308
  const currency = currencyCode(root?.["currency"] ?? data?.["currency"]) ?? "credits";
4048
4309
  return [{ kind: "balance", currency, amount }];
4049
4310
  }
4050
- function record(value) {
4311
+ function record2(value) {
4051
4312
  return typeof value === "object" && value !== null ? value : null;
4052
4313
  }
4053
4314
  function currencyCode(value) {
@@ -4075,8 +4336,8 @@ async function limitedResponseText(response, limit) {
4075
4336
  let text = "";
4076
4337
  try {
4077
4338
  while (true) {
4078
- const { done, value } = await reader.read();
4079
- if (done) return text + decoder.decode();
4339
+ const { done: done2, value } = await reader.read();
4340
+ if (done2) return text + decoder.decode();
4080
4341
  size += value.byteLength;
4081
4342
  if (size > limit) {
4082
4343
  await reader.cancel();
@@ -4096,15 +4357,15 @@ var MAX_AGE_MS = 24 * 60 * 60 * 1e3;
4096
4357
  var CACHE_VERSION = 3;
4097
4358
  async function loadModels(provider, refresh = false) {
4098
4359
  const file = cacheFile(provider.id);
4099
- const cached = await readCache(file);
4100
- if (!refresh && cached?.version === CACHE_VERSION && Date.now() - cached.fetchedAt < MAX_AGE_MS) {
4101
- 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;
4102
4363
  }
4103
4364
  let models;
4104
4365
  try {
4105
4366
  models = await provider.listModels();
4106
4367
  } catch (error) {
4107
- if (cached) return cached.models;
4368
+ if (cached2) return cached2.models;
4108
4369
  const known = provider.knownModels();
4109
4370
  if (known.length > 0) return known;
4110
4371
  throw error;
@@ -4112,6 +4373,9 @@ async function loadModels(provider, refresh = false) {
4112
4373
  await writeCache(file, models);
4113
4374
  return models;
4114
4375
  }
4376
+ function refreshModels(provider) {
4377
+ return loadModels(provider, true);
4378
+ }
4115
4379
  function cacheFile(providerId) {
4116
4380
  return path9.join(cacheDir, "models", `${providerId.replace(/[^\w.-]/g, "_")}.json`);
4117
4381
  }
@@ -4696,8 +4960,8 @@ function normalizeOpenAiUsage(usage) {
4696
4960
  function createRegistry(config, auth) {
4697
4961
  const built = /* @__PURE__ */ new Map();
4698
4962
  function get(id) {
4699
- const cached = built.get(id);
4700
- if (cached) return cached;
4963
+ const cached2 = built.get(id);
4964
+ if (cached2) return cached2;
4701
4965
  const entry = config.providers[id];
4702
4966
  if (!entry) throw new Error(unknownProviderMessage(id, Object.keys(config.providers)));
4703
4967
  const provider = build(id, entry, apiKey(id, entry, auth));
@@ -4789,13 +5053,66 @@ function isFileEdit(tool) {
4789
5053
  return tool.name === "write" || tool.name === "edit";
4790
5054
  }
4791
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
+
4792
5109
  // src/tools/edit.ts
4793
5110
  import { readFile as readFile9, stat as stat6 } from "fs/promises";
4794
5111
 
4795
5112
  // src/tools/safe-write.ts
4796
5113
  import { randomUUID as randomUUID3 } from "crypto";
4797
5114
  import { chmod as chmod5, lstat as lstat3, open, rename as rename5, rm as rm4 } from "fs/promises";
4798
- import { basename as basename2, dirname as dirname2, join } from "path";
5115
+ import { basename as basename2, dirname as dirname2, join as join2 } from "path";
4799
5116
  var UnsafeFileChangeError = class extends Error {
4800
5117
  constructor(message) {
4801
5118
  super(message);
@@ -4840,7 +5157,7 @@ async function readSafeFileSnapshot(file, maxBytes) {
4840
5157
  async function atomicWriteSafeFile(file, data, snapshot) {
4841
5158
  const parent = dirname2(file);
4842
5159
  await assertSameParent(parent, snapshot.parent);
4843
- const temp = join(parent, `.${basename2(file)}.${process.pid}.${randomUUID3()}.tmp`);
5160
+ const temp = join2(parent, `.${basename2(file)}.${process.pid}.${randomUUID3()}.tmp`);
4844
5161
  let handle;
4845
5162
  try {
4846
5163
  handle = await open(temp, "wx", snapshot.mode);
@@ -5030,58 +5347,10 @@ function replacement(before, oldString, newString, replaceAll) {
5030
5347
  return before.slice(0, at) + newString + before.slice(at + oldString.length);
5031
5348
  }
5032
5349
 
5033
- // src/tools/glob.ts
5034
- import { join as join2 } from "path";
5035
- import fg from "fast-glob";
5036
- var MAX_RESULTS = 500;
5037
- var IGNORED_DIRECTORIES = ["**/node_modules/**", "**/.git/**", "**/dist/**"];
5038
- var globTool = {
5039
- name: "glob",
5040
- description: 'Find files by glob pattern, most recently modified first. Supports patterns like "src/**/*.ts". Hidden files, node_modules, .git and dist are skipped.',
5041
- inputSchema: {
5042
- type: "object",
5043
- properties: {
5044
- pattern: { type: "string", description: "Glob pattern, e.g. **/*.test.ts" },
5045
- path: { type: "string", description: "Directory to search in, relative to the workspace root" }
5046
- },
5047
- required: ["pattern"],
5048
- additionalProperties: false
5049
- },
5050
- defaultPermission: "allow",
5051
- readOnly: true,
5052
- summarize(input) {
5053
- return `glob(${brief(input.pattern)})`;
5054
- },
5055
- async execute(input, ctx) {
5056
- const { pattern, path: path15 = "." } = input;
5057
- if (patternEscapes(pattern)) {
5058
- return { content: `Pattern ${pattern} must be relative to the workspace root.`, isError: true };
5059
- }
5060
- const safe = resolveInside(ctx.cwd, path15);
5061
- if (!safe.ok) return { content: safe.reason, isError: true };
5062
- const entries = await fg(pattern, {
5063
- cwd: safe.path,
5064
- dot: false,
5065
- onlyFiles: true,
5066
- followSymbolicLinks: false,
5067
- suppressErrors: true,
5068
- ignore: IGNORED_DIRECTORIES,
5069
- stats: true
5070
- });
5071
- if (entries.length === 0) return { content: `No files matched ${pattern}` };
5072
- entries.sort((a, b) => (b.stats?.mtimeMs ?? 0) - (a.stats?.mtimeMs ?? 0));
5073
- const paths = entries.slice(0, MAX_RESULTS).map((entry) => join2(safe.relative, entry.path));
5074
- if (entries.length > MAX_RESULTS) {
5075
- paths.push(`... truncated: ${entries.length - MAX_RESULTS} more files matched.`);
5076
- }
5077
- return { content: paths.join("\n") };
5078
- }
5079
- };
5080
-
5081
5350
  // src/tools/grep.ts
5082
5351
  import { readFile as readFile10, stat as stat7 } from "fs/promises";
5083
5352
  import { join as join3 } from "path";
5084
- import fg2 from "fast-glob";
5353
+ import fg3 from "fast-glob";
5085
5354
 
5086
5355
  // src/tools/sensitive.ts
5087
5356
  import path10 from "path";
@@ -5259,7 +5528,7 @@ var grepTool = {
5259
5528
  if (!safe.ok) return { content: safe.reason, isError: true };
5260
5529
  const target = await stat7(safe.path).catch(() => null);
5261
5530
  if (!target) return { content: `Path not found: ${path15}`, isError: true };
5262
- const discovered = target.isDirectory() ? (await fg2(glob, {
5531
+ const discovered = target.isDirectory() ? (await fg3(glob, {
5263
5532
  cwd: safe.path,
5264
5533
  dot: false,
5265
5534
  onlyFiles: true,
@@ -5482,7 +5751,7 @@ var writeTool = {
5482
5751
 
5483
5752
  // src/tools/registry.ts
5484
5753
  function builtinTools() {
5485
- return [readTool, writeTool, editTool, bashTool, globTool, grepTool];
5754
+ return [readTool, writeTool, editTool, bashTool, bashOutputTool, globTool, grepTool];
5486
5755
  }
5487
5756
  function createToolRegistry(tools) {
5488
5757
  const byName = /* @__PURE__ */ new Map();
@@ -6217,8 +6486,8 @@ async function readJsonBounded(response) {
6217
6486
  let bytes = 0;
6218
6487
  try {
6219
6488
  while (true) {
6220
- const { done, value } = await reader.read();
6221
- if (done) break;
6489
+ const { done: done2, value } = await reader.read();
6490
+ if (done2) break;
6222
6491
  bytes += value.byteLength;
6223
6492
  if (bytes > MAX_RESPONSE_BYTES2) {
6224
6493
  await reader.cancel();
@@ -6391,9 +6660,9 @@ async function boot(options) {
6391
6660
  activeAgents.push(entry);
6392
6661
  const trackedRequest = {
6393
6662
  ...request,
6394
- onProgress: (summary) => {
6395
- entry.progress.push(summary);
6396
- request.onProgress(summary);
6663
+ onProgress: (summary2) => {
6664
+ entry.progress.push(summary2);
6665
+ request.onProgress(summary2);
6397
6666
  }
6398
6667
  };
6399
6668
  try {
@@ -6461,13 +6730,15 @@ async function boot(options) {
6461
6730
  needsSetup: () => Object.keys(config.providers).length === 0,
6462
6731
  async addProvider(url, key) {
6463
6732
  const detected = await detectProvider(url, key);
6464
- rememberModels(detected.id, detected.models);
6733
+ for (const provider of detected) rememberModels(provider.id, provider.models);
6465
6734
  const configBefore = structuredClone(config);
6466
6735
  const authBefore = { ...auth };
6467
- config.providers[detected.id] = detected.config;
6468
- auth[detected.id] = key;
6469
- const chosen = preferredModel(detected.models);
6470
- 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);
6471
6742
  try {
6472
6743
  await saveConfig(config);
6473
6744
  await saveAuth(auth);
@@ -6480,11 +6751,12 @@ async function boot(options) {
6480
6751
  }
6481
6752
  registry = createRegistry(config, auth);
6482
6753
  const nextRef = config.model ?? "";
6483
- const discovered = detected.models.find((model) => model.id === chosen)?.contextWindow;
6484
- activateModel(nextRef, void 0, discovered);
6754
+ activateModel(nextRef, void 0, chosen?.model.contextWindow);
6485
6755
  void refreshModelContextWindow();
6486
- const kind = detected.config.type === "anthropic" ? "Anthropic" : "OpenAI-compatible";
6487
- 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");
6488
6760
  },
6489
6761
  currentProviderId: () => parseModelRef(modelRef)?.provider,
6490
6762
  listProviderItems: () => Object.entries(config.providers).map(([id, entry]) => ({
@@ -6838,6 +7110,15 @@ ${lines.join("\n")}` : null;
6838
7110
  }
6839
7111
  return items;
6840
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
+ },
6841
7122
  async listPromptItems() {
6842
7123
  const prompts2 = await listPrompts();
6843
7124
  return prompts2.map((prompt) => ({
@@ -7033,6 +7314,7 @@ ${lines.join("\n")}` : null;
7033
7314
  try {
7034
7315
  await Promise.all([persistQueue, configQueue]);
7035
7316
  } finally {
7317
+ killAllJobs();
7036
7318
  await mcp.close();
7037
7319
  }
7038
7320
  }
@@ -7045,6 +7327,16 @@ function preferredModel(models) {
7045
7327
  }
7046
7328
  return models[0]?.id;
7047
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
+ }
7048
7340
 
7049
7341
  // src/app/ask.ts
7050
7342
  var RU_YES = /* @__PURE__ */ new Set(["y", "\u043D"]);
@@ -7313,6 +7605,7 @@ var COMMANDS = [
7313
7605
  { name: "provider" },
7314
7606
  { name: "logout", args: "[provider]" },
7315
7607
  { name: "key", args: "[provider]" },
7608
+ { name: "refresh", args: "[provider]" },
7316
7609
  { name: "effort" },
7317
7610
  { name: "thinking" },
7318
7611
  { name: "budget", args: "[tokens]" },
@@ -7362,22 +7655,22 @@ function matchCommands(line) {
7362
7655
  if (starts.length > 0) return starts;
7363
7656
  const contains = COMMANDS.filter((command) => command.name.includes(needle));
7364
7657
  if (contains.length > 0) return contains;
7365
- return COMMANDS.filter((command) => isSubsequence(needle, command.name));
7658
+ return COMMANDS.filter((command) => isSubsequence2(needle, command.name));
7366
7659
  }
7367
7660
  function closestCommand(name) {
7368
7661
  const needle = name.toLowerCase();
7369
7662
  let best;
7370
7663
  let bestScore = Number.POSITIVE_INFINITY;
7371
7664
  for (const command of COMMANDS) {
7372
- const score = editDistance(needle, command.name);
7373
- if (score < bestScore) {
7374
- bestScore = score;
7665
+ const score2 = editDistance(needle, command.name);
7666
+ if (score2 < bestScore) {
7667
+ bestScore = score2;
7375
7668
  best = command;
7376
7669
  }
7377
7670
  }
7378
7671
  return bestScore <= Math.max(2, Math.floor(needle.length / 2)) ? best : void 0;
7379
7672
  }
7380
- function isSubsequence(needle, haystack) {
7673
+ function isSubsequence2(needle, haystack) {
7381
7674
  let index = 0;
7382
7675
  for (const character of haystack) {
7383
7676
  if (character === needle[index]) index += 1;
@@ -7402,6 +7695,43 @@ function editDistance(a, b) {
7402
7695
  return previous[b.length] ?? 0;
7403
7696
  }
7404
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
+
7405
7735
  // src/ui/App.tsx
7406
7736
  import TextInput2 from "ink-text-input";
7407
7737
 
@@ -7503,6 +7833,7 @@ var en = {
7503
7833
  titleProvider: "Provider",
7504
7834
  titleLogout: "Sign out of a provider",
7505
7835
  titleKeyChange: "Change API key for a provider",
7836
+ titleModelRefresh: "Reload models for a provider",
7506
7837
  titleSessions: "Resume a session",
7507
7838
  titleSessionAction: "Session action",
7508
7839
  resumed: (id, count) => `Resumed ${id} \u2014 ${count} messages restored`,
@@ -7582,6 +7913,7 @@ ${url}`,
7582
7913
  budgetUnlimited: "Token budget: unlimited (0). Set /budget <tokens> to restore a limit.",
7583
7914
  budgetInvalid: "Invalid budget: must be between 1,000 and 10,000,000",
7584
7915
  modelSet: (value) => `Model: ${value}`,
7916
+ modelsRefreshed: (provider, count) => `${provider}: ${count} models reloaded from the endpoint.`,
7585
7917
  noModels: "No models available. Use /login to add a provider.",
7586
7918
  bypassOff: "Approval prompts are back on.",
7587
7919
  bypassAsk1: "Disable approval prompts for this session?",
@@ -7629,6 +7961,7 @@ ${url}`,
7629
7961
  provider: "switch between configured providers",
7630
7962
  logout: "choose a provider to sign out from",
7631
7963
  key: "change API key for a provider",
7964
+ refresh: "reload the model list for a provider",
7632
7965
  effort: "set reasoning depth",
7633
7966
  thinking: "toggle reasoning",
7634
7967
  budget: "set token budget per turn",
@@ -7692,6 +8025,7 @@ var ru = {
7692
8025
  titleProvider: "\u041F\u0440\u043E\u0432\u0430\u0439\u0434\u0435\u0440",
7693
8026
  titleLogout: "\u0412\u044B\u0431\u0440\u0430\u0442\u044C API \u0434\u043B\u044F \u0432\u044B\u0445\u043E\u0434\u0430",
7694
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",
7695
8029
  titleSessions: "\u0412\u043E\u0441\u0441\u0442\u0430\u043D\u043E\u0432\u0438\u0442\u044C \u0441\u0435\u0441\u0441\u0438\u044E",
7696
8030
  titleSessionAction: "\u0414\u0435\u0439\u0441\u0442\u0432\u0438\u0435 \u0441 \u0441\u0435\u0441\u0441\u0438\u0435\u0439",
7697
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}`,
@@ -7771,6 +8105,7 @@ ${url}`,
7771
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.",
7772
8106
  budgetInvalid: "\u041D\u0435\u0432\u0435\u0440\u043D\u044B\u0439 \u043B\u0438\u043C\u0438\u0442: \u043E\u0442 1 000 \u0434\u043E 10 000 000",
7773
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}.`,
7774
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.",
7775
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.",
7776
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?",
@@ -7818,6 +8153,7 @@ ${url}`,
7818
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",
7819
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",
7820
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",
7821
8157
  effort: "\u0433\u043B\u0443\u0431\u0438\u043D\u0430 \u0440\u0430\u0437\u043C\u044B\u0448\u043B\u0435\u043D\u0438\u0439",
7822
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",
7823
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)",
@@ -8306,7 +8642,8 @@ var PromptInput = memo(function PromptInput2({
8306
8642
  pending,
8307
8643
  hint,
8308
8644
  history,
8309
- attachments = []
8645
+ attachments = [],
8646
+ onListFiles
8310
8647
  }) {
8311
8648
  const theme = useTheme();
8312
8649
  const { columns } = useTerminalSize();
@@ -8321,9 +8658,28 @@ var PromptInput = memo(function PromptInput2({
8321
8658
  const cursorRef = useRef2(inputCursor);
8322
8659
  valueRef.current = safeValue;
8323
8660
  cursorRef.current = inputCursor;
8324
- const suggestions = matchCommands(safeValue);
8325
- const open3 = suggestions.length > 0;
8326
- 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));
8327
8683
  useEffect(() => {
8328
8684
  setSelectionCursor(0);
8329
8685
  }, [safeValue]);
@@ -8331,14 +8687,14 @@ var PromptInput = memo(function PromptInput2({
8331
8687
  const length = characters2(safeValue).length;
8332
8688
  if (pendingValue.current === safeValue) {
8333
8689
  pendingValue.current = null;
8334
- setInputCursor((cursor) => clamp(cursor, 0, length));
8690
+ setInputCursor((cursor) => clamp2(cursor, 0, length));
8335
8691
  return;
8336
8692
  }
8337
8693
  setInputCursor(length);
8338
8694
  }, [safeValue]);
8339
8695
  const change = (next, cursor = characters2(next).length) => {
8340
8696
  next = sanitizeTerminalText(next);
8341
- const nextCursor2 = clamp(cursor, 0, characters2(next).length);
8697
+ const nextCursor2 = clamp2(cursor, 0, characters2(next).length);
8342
8698
  pendingValue.current = next;
8343
8699
  valueRef.current = next;
8344
8700
  cursorRef.current = nextCursor2;
@@ -8356,7 +8712,7 @@ var PromptInput = memo(function PromptInput2({
8356
8712
  const inserted = characters2(sanitizeTerminalText(pasted));
8357
8713
  if (inserted.length === 0) return;
8358
8714
  const valueCharacters = characters2(valueRef.current);
8359
- const cursor = clamp(cursorRef.current, 0, valueCharacters.length);
8715
+ const cursor = clamp2(cursorRef.current, 0, valueCharacters.length);
8360
8716
  valueCharacters.splice(cursor, 0, ...inserted);
8361
8717
  change(valueCharacters.join(""), cursor + inserted.length);
8362
8718
  };
@@ -8380,7 +8736,15 @@ var PromptInput = memo(function PromptInput2({
8380
8736
  return;
8381
8737
  }
8382
8738
  if (open3 && key.downArrow) {
8383
- 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
+ }
8384
8748
  return;
8385
8749
  }
8386
8750
  if (open3 && key.tab && !key.shift) {
@@ -8418,7 +8782,7 @@ var PromptInput = memo(function PromptInput2({
8418
8782
  return;
8419
8783
  }
8420
8784
  const valueCharacters = characters2(safeValue);
8421
- const cursor = clamp(inputCursor, 0, valueCharacters.length);
8785
+ const cursor = clamp2(inputCursor, 0, valueCharacters.length);
8422
8786
  if (key.leftArrow || key.rightArrow || key.home || key.end) {
8423
8787
  setInputCursor(nextCursor(cursor, valueCharacters.length, key));
8424
8788
  return;
@@ -8445,6 +8809,7 @@ var PromptInput = memo(function PromptInput2({
8445
8809
  });
8446
8810
  const start = Math.max(0, Math.min(active2 - WINDOW2 + 2, suggestions.length - WINDOW2));
8447
8811
  const visible = suggestions.slice(start, start + WINDOW2);
8812
+ const fileStart = Math.max(0, Math.min(active2 - WINDOW2 + 2, files.length - WINDOW2));
8448
8813
  return /* @__PURE__ */ jsxs8(Box8, { width: columns, maxWidth: "100%", flexDirection: "column", marginTop: 1, flexShrink: 0, children: [
8449
8814
  /* @__PURE__ */ jsxs8(
8450
8815
  Box8,
@@ -8473,6 +8838,18 @@ var PromptInput = memo(function PromptInput2({
8473
8838
  " ",
8474
8839
  sanitizeTerminalText(hint)
8475
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
+ ] }),
8476
8853
  open3 && /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", marginLeft: 2, children: [
8477
8854
  visible.map((command, index) => {
8478
8855
  const selected = start + index === active2;
@@ -8498,7 +8875,7 @@ function EditableText({
8498
8875
  placeholder
8499
8876
  }) {
8500
8877
  const valueCharacters = characters2(value);
8501
- const at = clamp(cursor, 0, valueCharacters.length);
8878
+ const at = clamp2(cursor, 0, valueCharacters.length);
8502
8879
  if (valueCharacters.length === 0) {
8503
8880
  const placeholderCharacters = characters2(sanitizeTerminalText(placeholder));
8504
8881
  return /* @__PURE__ */ jsxs8(Text8, { children: [
@@ -8522,7 +8899,7 @@ function nextCursor(cursor, length, key) {
8522
8899
  function characters2(value) {
8523
8900
  return [...value];
8524
8901
  }
8525
- function clamp(value, min, max) {
8902
+ function clamp2(value, min, max) {
8526
8903
  return Math.max(min, Math.min(value, max));
8527
8904
  }
8528
8905
 
@@ -8771,13 +9148,18 @@ import Spinner2 from "ink-spinner";
8771
9148
  import stringWidth2 from "string-width";
8772
9149
 
8773
9150
  // src/ui/markdown.tsx
8774
- import { Box as Box11, Text as Text10 } from "ink";
8775
- 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";
8776
9153
  import stringWidth from "string-width";
8777
9154
  import { jsx as jsx11, jsxs as jsxs10 } from "react/jsx-runtime";
8778
- 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 }) {
8779
9161
  const blocks = useMemo3(() => extractBlocks(children), [children]);
8780
- 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)) }) });
8781
9163
  }
8782
9164
  function BlockView({ block }) {
8783
9165
  switch (block.type) {
@@ -9425,7 +9807,7 @@ function AssistantView({ bubble: source, maxRows }) {
9425
9807
  const usedThinkingRows = frameThinking === "" ? 0 : frameThinking.split("\n").length;
9426
9808
  const frameText = liveBudget === void 0 ? bubble.text : clipTextToRows(bubble.text, Math.max(1, liveBudget - usedThinkingRows), columns);
9427
9809
  return /* @__PURE__ */ jsxs11(Box12, { flexDirection: "column", marginTop: 1, children: [
9428
- 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 }) }),
9429
9811
  bubble.streaming ? /* @__PURE__ */ jsx12(Text11, { children: frameText }) : /* @__PURE__ */ jsx12(Markdown, { children: bubble.text }),
9430
9812
  bubble.streaming && bubble.text === "" && /* @__PURE__ */ jsxs11(Text11, { dimColor: true, children: [
9431
9813
  /* @__PURE__ */ jsx12(Spinner2, { type: "dots" }),
@@ -9929,6 +10311,11 @@ function App({
9929
10311
  })
9930
10312
  };
9931
10313
  try {
10314
+ try {
10315
+ await runtime.persist(history.current);
10316
+ } catch (error) {
10317
+ notice("error", error instanceof Error ? error.message : String(error));
10318
+ }
9932
10319
  history.current = await runtime.run(history.current, hooks, controller.signal);
9933
10320
  void runtime.persist(history.current).catch((error) => notice("error", error instanceof Error ? error.message : String(error)));
9934
10321
  } catch (error) {
@@ -10256,6 +10643,29 @@ ${strings.modelSet(result.nextModel)}` : ""}`;
10256
10643
  }
10257
10644
  return;
10258
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
+ }
10259
10669
  case "lang": {
10260
10670
  const choice = await pick(
10261
10671
  strings.titleLang,
@@ -10850,6 +11260,10 @@ Rename the file if you want a different name.`);
10850
11260
  },
10851
11261
  [appendAttachment, notice, runtime, strings]
10852
11262
  );
11263
+ const listWorkspaceFiles = useCallback(
11264
+ (query) => runtime.listWorkspaceFiles(query).catch(() => []),
11265
+ [runtime]
11266
+ );
10853
11267
  const pasteClipboardImage = useCallback(() => {
10854
11268
  if (clipboardPasteTask.current) return;
10855
11269
  if (attachmentsRef.current.length >= MAX_ATTACHMENTS) {
@@ -10913,11 +11327,21 @@ Rename the file if you want a different name.`);
10913
11327
  }
10914
11328
  const queuedAttachments = attachmentsRef.current;
10915
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;
10916
11339
  if (text) setPromptHistory((state) => appendInputHistory(state, text));
10917
11340
  if (!detachedInput) setInput("");
10918
11341
  const content = [
10919
11342
  ...text ? [{ type: "text", text }] : [],
10920
- ...queuedAttachments
11343
+ ...queuedAttachments,
11344
+ ...mentioned
10921
11345
  ];
10922
11346
  replaceAttachments([]);
10923
11347
  const item = { kind: "message", text, content };
@@ -10992,11 +11416,11 @@ Rename the file if you want a different name.`);
10992
11416
  Onboarding,
10993
11417
  {
10994
11418
  onSubmit: async (url, key) => {
10995
- const summary = await runtime.addProvider(url, key);
11419
+ const summary2 = await runtime.addProvider(url, key);
10996
11420
  setSetup(false);
10997
- notice("info", `${summary}
11421
+ notice("info", `${summary2}
10998
11422
  ${strings.configAt(runtime.configPath())}`);
10999
- return summary;
11423
+ return summary2;
11000
11424
  }
11001
11425
  }
11002
11426
  )
@@ -11073,6 +11497,7 @@ ${strings.configAt(runtime.configPath())}`);
11073
11497
  onSubmit: submit,
11074
11498
  onPastePath: tryQueueAutomaticAttachment,
11075
11499
  onPasteImage: pasteClipboardImage,
11500
+ onListFiles: listWorkspaceFiles,
11076
11501
  disabled: busy,
11077
11502
  pending: pendingCount,
11078
11503
  hint: busy ? strings.escCancel : void 0,