@kernelonpanic/kitcode 1.3.0 → 1.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -2
- package/README.ru.md +3 -2
- package/dist/index.js +780 -324
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -62,8 +62,7 @@ var themeSchema = z.object({
|
|
|
62
62
|
var langSchema = z.enum(["en", "ru"]);
|
|
63
63
|
var budgetSchema = z.object({
|
|
64
64
|
maxTokensPerTurn: z.number().int().min(0).max(1e7).default(1e7),
|
|
65
|
-
maxCostUsdPerTurn: z.number().nonnegative().max(1e3).default(0)
|
|
66
|
-
maxSubagentsPerTurn: z.number().int().min(0).max(16).default(3)
|
|
65
|
+
maxCostUsdPerTurn: z.number().nonnegative().max(1e3).default(0)
|
|
67
66
|
});
|
|
68
67
|
var diagnosticsSchema = z.object({
|
|
69
68
|
autoRun: z.boolean().default(true),
|
|
@@ -79,8 +78,7 @@ var configSchema = z.object({
|
|
|
79
78
|
maxTokens: z.number().int().positive().max(2e5).default(64e3),
|
|
80
79
|
budget: budgetSchema.default({
|
|
81
80
|
maxTokensPerTurn: 1e7,
|
|
82
|
-
maxCostUsdPerTurn: 0
|
|
83
|
-
maxSubagentsPerTurn: 3
|
|
81
|
+
maxCostUsdPerTurn: 0
|
|
84
82
|
}),
|
|
85
83
|
diagnostics: diagnosticsSchema.default({ autoRun: true, commands: [] }),
|
|
86
84
|
providers: z.record(providerIdSchema, providerConfigSchema).default({}),
|
|
@@ -210,8 +208,8 @@ async function readHead(stream) {
|
|
|
210
208
|
let text = "";
|
|
211
209
|
try {
|
|
212
210
|
while (text.length < CAPTURE_CHARS) {
|
|
213
|
-
const { done, value } = await reader.read();
|
|
214
|
-
if (
|
|
211
|
+
const { done: done2, value } = await reader.read();
|
|
212
|
+
if (done2) break;
|
|
215
213
|
text += decoder.decode(value, { stream: true });
|
|
216
214
|
}
|
|
217
215
|
} catch {
|
|
@@ -420,39 +418,52 @@ async function detectProvider(rawUrl, apiKey2, opts = {}) {
|
|
|
420
418
|
`${anthropicBaseUrl}/v1/models?limit=1000`,
|
|
421
419
|
anthropicHeaders(apiKey2)
|
|
422
420
|
);
|
|
423
|
-
return
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
421
|
+
return [
|
|
422
|
+
{
|
|
423
|
+
id,
|
|
424
|
+
config: { type: "anthropic", baseUrl: anthropicBaseUrl },
|
|
425
|
+
models: probed.ok ? parseModelList(probed.body) : []
|
|
426
|
+
}
|
|
427
|
+
];
|
|
428
428
|
}
|
|
429
|
+
const found = [];
|
|
429
430
|
let last;
|
|
430
431
|
for (const candidate of candidateBaseUrls(baseUrl)) {
|
|
432
|
+
if (found.length === schemes.length) break;
|
|
431
433
|
const url = `${candidate}/models`;
|
|
432
|
-
|
|
433
|
-
|
|
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));
|
|
434
|
+
for (const scheme of schemes) {
|
|
435
|
+
const attempt = await probe(fetchImpl, url, schemeHeaders(scheme, apiKey2));
|
|
444
436
|
last = attempt;
|
|
445
|
-
if (attempt.
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
config: { type: "anthropic", baseUrl: anthropicRoot(candidate) },
|
|
449
|
-
models: parseModelList(attempt.body)
|
|
450
|
-
};
|
|
451
|
-
}
|
|
437
|
+
if (attempt.status === 404 || attempt.status === 0) break;
|
|
438
|
+
if (attempt.ok && isModelListBody(attempt.body)) record(found, candidate, scheme, attempt.body);
|
|
439
|
+
if (found.length === schemes.length) break;
|
|
452
440
|
}
|
|
453
|
-
if (!
|
|
441
|
+
if (found.length === 0 && !last.ok && last.status !== 404 && last.status !== 0) break;
|
|
454
442
|
}
|
|
455
|
-
throw new Error(describeFailure(last, apiKey2));
|
|
443
|
+
if (found.length === 0) throw new Error(describeFailure(last, apiKey2));
|
|
444
|
+
return found.map((entry, index) => ({
|
|
445
|
+
id: index === 0 ? id : `${id.slice(0, MAX_ID_STEM)}-${entry.type}`,
|
|
446
|
+
config: { type: entry.type, baseUrl: entry.baseUrl },
|
|
447
|
+
models: entry.models
|
|
448
|
+
}));
|
|
449
|
+
}
|
|
450
|
+
var schemes = ["bearer", "anthropic"];
|
|
451
|
+
var MAX_ID_STEM = 120;
|
|
452
|
+
function schemeHeaders(scheme, apiKey2) {
|
|
453
|
+
return scheme === "bearer" ? { Authorization: `Bearer ${apiKey2}` } : anthropicHeaders(apiKey2);
|
|
454
|
+
}
|
|
455
|
+
function record(found, candidate, scheme, body) {
|
|
456
|
+
const type = scheme === "anthropic" || advertisesAnthropicProtocol(body) ? "anthropic" : "openai";
|
|
457
|
+
if (found.some((entry) => entry.type === type)) return;
|
|
458
|
+
const models = parseModelList(body);
|
|
459
|
+
const fingerprint2 = models.map((model) => model.id).sort().join("\n");
|
|
460
|
+
if (found.some((entry) => entry.fingerprint === fingerprint2)) return;
|
|
461
|
+
found.push({
|
|
462
|
+
type,
|
|
463
|
+
baseUrl: type === "anthropic" ? anthropicRoot(candidate) : candidate,
|
|
464
|
+
models,
|
|
465
|
+
fingerprint: fingerprint2
|
|
466
|
+
});
|
|
456
467
|
}
|
|
457
468
|
function normaliseBaseUrl(rawUrl) {
|
|
458
469
|
return rawUrl.trim().replace(/\/+$/, "");
|
|
@@ -519,8 +530,8 @@ async function boundedResponseText(response) {
|
|
|
519
530
|
let text = "";
|
|
520
531
|
try {
|
|
521
532
|
for (; ; ) {
|
|
522
|
-
const { done, value } = await reader.read();
|
|
523
|
-
if (
|
|
533
|
+
const { done: done2, value } = await reader.read();
|
|
534
|
+
if (done2) break;
|
|
524
535
|
bytes += value.byteLength;
|
|
525
536
|
if (bytes > MAX_DETECT_BODY_BYTES) {
|
|
526
537
|
throw new Error(`response exceeded ${MAX_DETECT_BODY_BYTES / 1e6} MB`);
|
|
@@ -566,11 +577,9 @@ function anthropicRoot(baseUrl) {
|
|
|
566
577
|
function advertisesAnthropicProtocol(body) {
|
|
567
578
|
const entries = modelEntries(body);
|
|
568
579
|
if (entries.length === 0) return false;
|
|
569
|
-
const
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
if (endpointTypes.size > 0) {
|
|
573
|
-
return endpointTypes.has("anthropic") && !endpointTypes.has("openai");
|
|
580
|
+
const advertised = entries.map((entry) => stringList(entry["supported_endpoint_types"]));
|
|
581
|
+
if (advertised.some((types) => types.length > 0)) {
|
|
582
|
+
return advertised.every((types) => types.includes("anthropic"));
|
|
574
583
|
}
|
|
575
584
|
if (entries.some(isVendorNamespaced)) return false;
|
|
576
585
|
return entries.every((entry) => entry["owned_by"] === "anthropic");
|
|
@@ -827,9 +836,9 @@ function conventionalKeyEnv(providerId) {
|
|
|
827
836
|
function expandEnvRefs(value) {
|
|
828
837
|
return value.replace(/\$\{env:([^}]+)\}/g, (_match, name) => process.env[name] ?? "");
|
|
829
838
|
}
|
|
830
|
-
function expandEnvRecord(
|
|
839
|
+
function expandEnvRecord(record3) {
|
|
831
840
|
return Object.fromEntries(
|
|
832
|
-
Object.entries(
|
|
841
|
+
Object.entries(record3).map(([key, value]) => [key, expandEnvRefs(value)])
|
|
833
842
|
);
|
|
834
843
|
}
|
|
835
844
|
async function readJson(file) {
|
|
@@ -895,12 +904,15 @@ async function addProvider(url, key, options = {}) {
|
|
|
895
904
|
const auth = await loadAuth();
|
|
896
905
|
const configBefore = structuredClone(config);
|
|
897
906
|
const authBefore = { ...auth };
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
907
|
+
for (const provider of detected) {
|
|
908
|
+
config.providers[provider.id] = provider.config;
|
|
909
|
+
auth[provider.id] = key;
|
|
910
|
+
}
|
|
911
|
+
const primary = detected[0];
|
|
912
|
+
const models = detected.flatMap((provider) => provider.models);
|
|
901
913
|
if (!config.model) {
|
|
902
|
-
const chosen =
|
|
903
|
-
if (chosen) config.model =
|
|
914
|
+
const chosen = pickDefaultRef(detected);
|
|
915
|
+
if (chosen) config.model = chosen;
|
|
904
916
|
}
|
|
905
917
|
try {
|
|
906
918
|
await saveConfig(config);
|
|
@@ -913,14 +925,15 @@ async function addProvider(url, key, options = {}) {
|
|
|
913
925
|
throw error;
|
|
914
926
|
}
|
|
915
927
|
const location = await configLocation();
|
|
916
|
-
const
|
|
917
|
-
|
|
918
|
-
|
|
928
|
+
const rows = detected.map((provider) => [
|
|
929
|
+
"provider",
|
|
930
|
+
`${provider.id} \u2014 this endpoint speaks the ${protocolLabel(provider)} protocol`
|
|
931
|
+
]);
|
|
932
|
+
rows.push(
|
|
919
933
|
["models", models.length === 0 ? "none listed by this endpoint" : `${models.length} available`],
|
|
920
|
-
["default", describeDefault(config.model, detected
|
|
921
|
-
|
|
922
|
-
alternatives(
|
|
923
|
-
const ref = formatModelRef(detected.id, model.id);
|
|
934
|
+
["default", describeDefault(config.model, detected)]
|
|
935
|
+
);
|
|
936
|
+
alternatives(detected, config.model).forEach(([ref, model], index) => {
|
|
924
937
|
rows.push([index === 0 ? "also try" : "", `${ref} ${price(model)}`]);
|
|
925
938
|
});
|
|
926
939
|
rows.push(["config", `${location.path} (${location.scope})`]);
|
|
@@ -928,11 +941,15 @@ async function addProvider(url, key, options = {}) {
|
|
|
928
941
|
rows.push(["next", "run kitcode to start a session; /model switches models"]);
|
|
929
942
|
for (const [label, value] of rows) console.log(label.padEnd(LABEL_WIDTH) + value);
|
|
930
943
|
}
|
|
931
|
-
function
|
|
944
|
+
function protocolLabel(provider) {
|
|
945
|
+
return provider.config.type === "anthropic" ? "Anthropic" : "OpenAI-compatible";
|
|
946
|
+
}
|
|
947
|
+
function describeDefault(ref, detected) {
|
|
932
948
|
if (!ref) return "not set \u2014 start kitcode and pick one with /model";
|
|
933
949
|
const parsed = parseModelRef(ref);
|
|
934
|
-
|
|
935
|
-
return `${ref}
|
|
950
|
+
const owner = detected.find((provider) => provider.id === parsed?.provider);
|
|
951
|
+
if (!owner) return `${ref} \u2014 already in your config, left alone`;
|
|
952
|
+
return `${ref} ${price(owner.models.find((model) => model.id === parsed.model))}`;
|
|
936
953
|
}
|
|
937
954
|
function price(model) {
|
|
938
955
|
const pricing = model?.pricing;
|
|
@@ -942,25 +959,35 @@ function price(model) {
|
|
|
942
959
|
function money(value) {
|
|
943
960
|
return `$${value >= 1 ? value.toFixed(2) : Number(value.toFixed(4))}`;
|
|
944
961
|
}
|
|
945
|
-
function
|
|
962
|
+
function pickDefaultRef(detected) {
|
|
963
|
+
const refs = modelRefs(detected);
|
|
946
964
|
for (const preferred of PREFERRED) {
|
|
947
|
-
const match =
|
|
948
|
-
if (match) return match
|
|
965
|
+
const match = refs.find(([, model]) => model.id.endsWith(preferred));
|
|
966
|
+
if (match) return match[0];
|
|
949
967
|
}
|
|
950
|
-
return
|
|
968
|
+
return refs[0]?.[0];
|
|
951
969
|
}
|
|
952
|
-
function alternatives(
|
|
970
|
+
function alternatives(detected, defaultRef) {
|
|
971
|
+
const refs = modelRefs(detected);
|
|
953
972
|
const ranked = [
|
|
954
|
-
...PREFERRED.flatMap((preferred) =>
|
|
955
|
-
...
|
|
956
|
-
...
|
|
973
|
+
...PREFERRED.flatMap((preferred) => refs.filter(([, model]) => model.id.endsWith(preferred))),
|
|
974
|
+
...refs.filter(([, model]) => model.pricing),
|
|
975
|
+
...refs
|
|
957
976
|
];
|
|
958
977
|
const picked = /* @__PURE__ */ new Map();
|
|
959
|
-
for (const model of ranked) {
|
|
978
|
+
for (const [ref, model] of ranked) {
|
|
960
979
|
if (picked.size === ALTERNATIVES) break;
|
|
961
|
-
if (
|
|
980
|
+
if (ref !== defaultRef) picked.set(ref, model);
|
|
962
981
|
}
|
|
963
|
-
return [...picked.
|
|
982
|
+
return [...picked.entries()];
|
|
983
|
+
}
|
|
984
|
+
function modelRefs(detected) {
|
|
985
|
+
return detected.flatMap(
|
|
986
|
+
(provider) => provider.models.map((model) => [
|
|
987
|
+
formatModelRef(provider.id, model.id),
|
|
988
|
+
model
|
|
989
|
+
])
|
|
990
|
+
);
|
|
964
991
|
}
|
|
965
992
|
|
|
966
993
|
// src/app/ask.ts
|
|
@@ -1087,6 +1114,261 @@ ${store.read()}`, display: { kind: "diff", path: "project memory", before, after
|
|
|
1087
1114
|
};
|
|
1088
1115
|
}
|
|
1089
1116
|
|
|
1117
|
+
// src/ui/workspace-files.ts
|
|
1118
|
+
import fg2 from "fast-glob";
|
|
1119
|
+
|
|
1120
|
+
// src/tools/glob.ts
|
|
1121
|
+
import { join } from "path";
|
|
1122
|
+
import fg from "fast-glob";
|
|
1123
|
+
|
|
1124
|
+
// src/tools/safepath.ts
|
|
1125
|
+
import { realpathSync } from "fs";
|
|
1126
|
+
import { basename, dirname, isAbsolute, relative, resolve, sep } from "path";
|
|
1127
|
+
function canonicalize(target) {
|
|
1128
|
+
try {
|
|
1129
|
+
return realpathSync(target);
|
|
1130
|
+
} catch {
|
|
1131
|
+
let current = target;
|
|
1132
|
+
const trailing = [];
|
|
1133
|
+
for (let i = 0; i < 64; i++) {
|
|
1134
|
+
try {
|
|
1135
|
+
return resolve(realpathSync(current), ...trailing);
|
|
1136
|
+
} catch {
|
|
1137
|
+
const parent = dirname(current);
|
|
1138
|
+
if (parent === current) break;
|
|
1139
|
+
trailing.unshift(basename(current));
|
|
1140
|
+
current = parent;
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
return resolve(current, ...trailing);
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
function resolveInside(cwd, userPath) {
|
|
1147
|
+
const root = canonicalize(resolve(cwd));
|
|
1148
|
+
const target = canonicalize(resolve(root, userPath));
|
|
1149
|
+
const rel = relative(root, target);
|
|
1150
|
+
if (rel !== "" && (isAbsolute(rel) || rel.split(sep)[0] === "..")) {
|
|
1151
|
+
return { ok: false, reason: `Path ${userPath} resolves outside the workspace root ${root}` };
|
|
1152
|
+
}
|
|
1153
|
+
return { ok: true, path: target, relative: rel };
|
|
1154
|
+
}
|
|
1155
|
+
function patternEscapes(pattern) {
|
|
1156
|
+
return isAbsolute(pattern) || pattern.split("/").includes("..") || pattern.split("\\").includes("..");
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
// src/tools/glob.ts
|
|
1160
|
+
var MAX_RESULTS = 500;
|
|
1161
|
+
var IGNORED_DIRECTORIES = ["**/node_modules/**", "**/.git/**", "**/dist/**"];
|
|
1162
|
+
var globTool = {
|
|
1163
|
+
name: "glob",
|
|
1164
|
+
description: 'Find files by glob pattern, most recently modified first. Supports patterns like "src/**/*.ts". Hidden files, node_modules, .git and dist are skipped.',
|
|
1165
|
+
inputSchema: {
|
|
1166
|
+
type: "object",
|
|
1167
|
+
properties: {
|
|
1168
|
+
pattern: { type: "string", description: "Glob pattern, e.g. **/*.test.ts" },
|
|
1169
|
+
path: { type: "string", description: "Directory to search in, relative to the workspace root" }
|
|
1170
|
+
},
|
|
1171
|
+
required: ["pattern"],
|
|
1172
|
+
additionalProperties: false
|
|
1173
|
+
},
|
|
1174
|
+
defaultPermission: "allow",
|
|
1175
|
+
readOnly: true,
|
|
1176
|
+
summarize(input) {
|
|
1177
|
+
return `glob(${brief(input.pattern)})`;
|
|
1178
|
+
},
|
|
1179
|
+
async execute(input, ctx) {
|
|
1180
|
+
const { pattern, path: path15 = "." } = input;
|
|
1181
|
+
if (patternEscapes(pattern)) {
|
|
1182
|
+
return { content: `Pattern ${pattern} must be relative to the workspace root.`, isError: true };
|
|
1183
|
+
}
|
|
1184
|
+
const safe = resolveInside(ctx.cwd, path15);
|
|
1185
|
+
if (!safe.ok) return { content: safe.reason, isError: true };
|
|
1186
|
+
const entries = await fg(pattern, {
|
|
1187
|
+
cwd: safe.path,
|
|
1188
|
+
dot: false,
|
|
1189
|
+
onlyFiles: true,
|
|
1190
|
+
followSymbolicLinks: false,
|
|
1191
|
+
suppressErrors: true,
|
|
1192
|
+
ignore: IGNORED_DIRECTORIES,
|
|
1193
|
+
stats: true
|
|
1194
|
+
});
|
|
1195
|
+
if (entries.length === 0) return { content: `No files matched ${pattern}` };
|
|
1196
|
+
entries.sort((a, b) => (b.stats?.mtimeMs ?? 0) - (a.stats?.mtimeMs ?? 0));
|
|
1197
|
+
const paths = entries.slice(0, MAX_RESULTS).map((entry) => join(safe.relative, entry.path));
|
|
1198
|
+
if (entries.length > MAX_RESULTS) {
|
|
1199
|
+
paths.push(`... truncated: ${entries.length - MAX_RESULTS} more files matched.`);
|
|
1200
|
+
}
|
|
1201
|
+
return { content: paths.join("\n") };
|
|
1202
|
+
}
|
|
1203
|
+
};
|
|
1204
|
+
|
|
1205
|
+
// src/ui/workspace-files.ts
|
|
1206
|
+
var MAX_SCANNED = 2e4;
|
|
1207
|
+
var MAX_RESULTS2 = 12;
|
|
1208
|
+
var CACHE_TTL_MS = 1e4;
|
|
1209
|
+
var cached;
|
|
1210
|
+
var inFlight;
|
|
1211
|
+
async function searchWorkspaceFiles(cwd, query) {
|
|
1212
|
+
const files = await workspaceFiles(cwd);
|
|
1213
|
+
const needle = query.trim().toLowerCase();
|
|
1214
|
+
if (needle === "") return files.slice(0, MAX_RESULTS2);
|
|
1215
|
+
const exact = [];
|
|
1216
|
+
const loose = [];
|
|
1217
|
+
for (const file of files) {
|
|
1218
|
+
const haystack = file.toLowerCase();
|
|
1219
|
+
if (haystack.includes(needle)) exact.push(file);
|
|
1220
|
+
else if (isSubsequence(needle, haystack)) loose.push(file);
|
|
1221
|
+
if (exact.length >= MAX_RESULTS2) break;
|
|
1222
|
+
}
|
|
1223
|
+
exact.sort((a, b) => score(a, needle) - score(b, needle));
|
|
1224
|
+
return [...exact, ...loose].slice(0, MAX_RESULTS2);
|
|
1225
|
+
}
|
|
1226
|
+
function workspaceFiles(cwd) {
|
|
1227
|
+
const fresh = cached && cached.cwd === cwd && Date.now() - cached.at < CACHE_TTL_MS;
|
|
1228
|
+
if (fresh) return Promise.resolve(cached.files);
|
|
1229
|
+
if (inFlight) return inFlight;
|
|
1230
|
+
inFlight = fg2("**/*", {
|
|
1231
|
+
cwd,
|
|
1232
|
+
dot: false,
|
|
1233
|
+
onlyFiles: true,
|
|
1234
|
+
followSymbolicLinks: false,
|
|
1235
|
+
suppressErrors: true,
|
|
1236
|
+
ignore: IGNORED_DIRECTORIES
|
|
1237
|
+
}).then((entries) => {
|
|
1238
|
+
const files = entries.slice(0, MAX_SCANNED).sort((a, b) => a.length - b.length);
|
|
1239
|
+
cached = { cwd, files, at: Date.now() };
|
|
1240
|
+
return files;
|
|
1241
|
+
}).catch(() => []).finally(() => {
|
|
1242
|
+
inFlight = void 0;
|
|
1243
|
+
});
|
|
1244
|
+
return inFlight;
|
|
1245
|
+
}
|
|
1246
|
+
function score(file, needle) {
|
|
1247
|
+
const basename3 = file.slice(file.lastIndexOf("/") + 1).toLowerCase();
|
|
1248
|
+
if (basename3.startsWith(needle)) return 0;
|
|
1249
|
+
if (basename3.includes(needle)) return 1;
|
|
1250
|
+
return 2;
|
|
1251
|
+
}
|
|
1252
|
+
function isSubsequence(needle, haystack) {
|
|
1253
|
+
let index = 0;
|
|
1254
|
+
for (const character of haystack) {
|
|
1255
|
+
if (character === needle[index]) index += 1;
|
|
1256
|
+
if (index === needle.length) return true;
|
|
1257
|
+
}
|
|
1258
|
+
return needle.length === 0;
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
// src/tools/jobs.ts
|
|
1262
|
+
import { spawn } from "child_process";
|
|
1263
|
+
var MAX_JOBS = 8;
|
|
1264
|
+
var MAX_JOB_OUTPUT = 1e5;
|
|
1265
|
+
var jobs = /* @__PURE__ */ new Map();
|
|
1266
|
+
var counter = 0;
|
|
1267
|
+
function startJob(command, cwd) {
|
|
1268
|
+
const live = [...jobs.values()].filter((job2) => job2.state === "running");
|
|
1269
|
+
if (live.length >= MAX_JOBS) {
|
|
1270
|
+
return { error: `At most ${MAX_JOBS} background commands can run at once.` };
|
|
1271
|
+
}
|
|
1272
|
+
counter += 1;
|
|
1273
|
+
const id = `bg-${counter}`;
|
|
1274
|
+
const child = spawn(command, {
|
|
1275
|
+
shell: true,
|
|
1276
|
+
cwd,
|
|
1277
|
+
detached: process.platform !== "win32",
|
|
1278
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
1279
|
+
windowsHide: true
|
|
1280
|
+
});
|
|
1281
|
+
const job = {
|
|
1282
|
+
id,
|
|
1283
|
+
command,
|
|
1284
|
+
state: "running",
|
|
1285
|
+
exitCode: null,
|
|
1286
|
+
signal: null,
|
|
1287
|
+
child,
|
|
1288
|
+
buffer: "",
|
|
1289
|
+
reported: 0,
|
|
1290
|
+
truncated: false
|
|
1291
|
+
};
|
|
1292
|
+
jobs.set(id, job);
|
|
1293
|
+
const collect = (chunk) => {
|
|
1294
|
+
const text = chunk.toString("utf8");
|
|
1295
|
+
const room = MAX_JOB_OUTPUT - job.buffer.length;
|
|
1296
|
+
if (text.length <= room) {
|
|
1297
|
+
job.buffer += text;
|
|
1298
|
+
return;
|
|
1299
|
+
}
|
|
1300
|
+
job.truncated = true;
|
|
1301
|
+
job.buffer = (job.buffer + text).slice(-MAX_JOB_OUTPUT);
|
|
1302
|
+
job.reported = Math.min(job.reported, job.buffer.length);
|
|
1303
|
+
};
|
|
1304
|
+
child.stdout?.on("data", collect);
|
|
1305
|
+
child.stderr?.on("data", collect);
|
|
1306
|
+
child.on("error", (error) => {
|
|
1307
|
+
job.buffer += `
|
|
1308
|
+
Command failed to start: ${error.message}`;
|
|
1309
|
+
job.state = "exited";
|
|
1310
|
+
job.exitCode = job.exitCode ?? 1;
|
|
1311
|
+
});
|
|
1312
|
+
child.on("close", (code, signal) => {
|
|
1313
|
+
if (job.state !== "killed") job.state = "exited";
|
|
1314
|
+
job.exitCode = code;
|
|
1315
|
+
job.signal = signal;
|
|
1316
|
+
});
|
|
1317
|
+
return { id };
|
|
1318
|
+
}
|
|
1319
|
+
function readJob(id) {
|
|
1320
|
+
const job = jobs.get(id);
|
|
1321
|
+
if (!job) return void 0;
|
|
1322
|
+
const output = job.buffer.slice(job.reported);
|
|
1323
|
+
job.reported = job.buffer.length;
|
|
1324
|
+
return { ...summary(job), output, truncated: job.truncated };
|
|
1325
|
+
}
|
|
1326
|
+
function killJob(id) {
|
|
1327
|
+
const job = jobs.get(id);
|
|
1328
|
+
if (!job) return false;
|
|
1329
|
+
if (job.state === "running") {
|
|
1330
|
+
job.state = "killed";
|
|
1331
|
+
killTree(job.child);
|
|
1332
|
+
}
|
|
1333
|
+
return true;
|
|
1334
|
+
}
|
|
1335
|
+
function listJobs() {
|
|
1336
|
+
return [...jobs.values()].map(summary);
|
|
1337
|
+
}
|
|
1338
|
+
function killAllJobs() {
|
|
1339
|
+
for (const job of jobs.values()) {
|
|
1340
|
+
if (job.state === "running") {
|
|
1341
|
+
job.state = "killed";
|
|
1342
|
+
killTree(job.child);
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
function summary(job) {
|
|
1347
|
+
return {
|
|
1348
|
+
id: job.id,
|
|
1349
|
+
command: job.command,
|
|
1350
|
+
state: job.state,
|
|
1351
|
+
exitCode: job.exitCode,
|
|
1352
|
+
signal: job.signal
|
|
1353
|
+
};
|
|
1354
|
+
}
|
|
1355
|
+
function killTree(child) {
|
|
1356
|
+
if (child.pid == null) return;
|
|
1357
|
+
if (process.platform === "win32") {
|
|
1358
|
+
try {
|
|
1359
|
+
spawn("taskkill", ["/pid", String(child.pid), "/t", "/f"], { stdio: "ignore" });
|
|
1360
|
+
} catch {
|
|
1361
|
+
child.kill("SIGKILL");
|
|
1362
|
+
}
|
|
1363
|
+
return;
|
|
1364
|
+
}
|
|
1365
|
+
try {
|
|
1366
|
+
process.kill(-child.pid, "SIGKILL");
|
|
1367
|
+
} catch {
|
|
1368
|
+
child.kill("SIGKILL");
|
|
1369
|
+
}
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1090
1372
|
// src/core/memory.ts
|
|
1091
1373
|
import { createHash } from "crypto";
|
|
1092
1374
|
import { readFile as readFile3, writeFile as writeFile3, rm as rm3 } from "fs/promises";
|
|
@@ -1530,8 +1812,23 @@ var MAX_PAUSE_RESUMES = 5;
|
|
|
1530
1812
|
var MAX_RETRIES2 = 3;
|
|
1531
1813
|
var INITIAL_BACKOFF_MS = 1e3;
|
|
1532
1814
|
var MAX_TURN_STEPS = 64;
|
|
1533
|
-
|
|
1815
|
+
function createExecutionLane() {
|
|
1816
|
+
let tail2 = Promise.resolve();
|
|
1817
|
+
return {
|
|
1818
|
+
run(operation) {
|
|
1819
|
+
const result = tail2.then(operation);
|
|
1820
|
+
tail2 = result.then(() => void 0, () => void 0);
|
|
1821
|
+
return result;
|
|
1822
|
+
}
|
|
1823
|
+
};
|
|
1824
|
+
}
|
|
1534
1825
|
async function runTurn(cfg, history, hooks, signal) {
|
|
1826
|
+
const permissionLane = cfg.permissionLane ?? createExecutionLane();
|
|
1827
|
+
const originalHooks = hooks;
|
|
1828
|
+
hooks = {
|
|
1829
|
+
...hooks,
|
|
1830
|
+
requestPermission: (request) => permissionLane.run(async () => signal.aborted ? "deny" : originalHooks.requestPermission(request))
|
|
1831
|
+
};
|
|
1535
1832
|
let messages = [...history];
|
|
1536
1833
|
let pauses = 0;
|
|
1537
1834
|
let steps = 0;
|
|
@@ -1549,40 +1846,35 @@ async function runTurn(cfg, history, hooks, signal) {
|
|
|
1549
1846
|
hooks.onEvent({ type: "turn_end", stopReason: "max_tokens" });
|
|
1550
1847
|
return messages;
|
|
1551
1848
|
}
|
|
1552
|
-
const
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1849
|
+
const request = async () => {
|
|
1850
|
+
if (signal.aborted) return { content: [], stopReason: "aborted" };
|
|
1851
|
+
const budgetDecision = cfg.budget?.beforeRequest({
|
|
1852
|
+
modelRef: cfg.modelRef,
|
|
1853
|
+
maxOutputTokens: cfg.maxTokens,
|
|
1854
|
+
estimatedInputTokens: estimateRequestTokens(cfg, messages)
|
|
1855
|
+
});
|
|
1856
|
+
if (budgetDecision && !budgetDecision.allowed) {
|
|
1857
|
+
hooks.onEvent({ type: "notice", level: "warn", text: budgetDecision.reason });
|
|
1858
|
+
hooks.onEvent({ type: "turn_end", stopReason: "max_tokens" });
|
|
1859
|
+
return null;
|
|
1860
|
+
}
|
|
1861
|
+
hooks.onEvent({ type: "turn_start" });
|
|
1862
|
+
return consumeStream(
|
|
1863
|
+
cfg,
|
|
1864
|
+
messages,
|
|
1865
|
+
hooks,
|
|
1866
|
+
signal,
|
|
1867
|
+
budgetDecision?.maxOutputTokens ?? cfg.maxTokens
|
|
1868
|
+
);
|
|
1869
|
+
};
|
|
1870
|
+
const outcome = await (cfg.modelRequestLane?.run(request) ?? request());
|
|
1871
|
+
if (!outcome) return messages;
|
|
1570
1872
|
messages.push({ role: "assistant", content: outcome.content });
|
|
1571
1873
|
if (outcome.stopReason === "aborted") {
|
|
1572
1874
|
hooks.onEvent({ type: "turn_end", stopReason: "aborted" });
|
|
1573
1875
|
return sanitizeHistory(messages);
|
|
1574
1876
|
}
|
|
1575
1877
|
if (outcome.stopReason === "tool_use") {
|
|
1576
|
-
const callCount = outcome.content.filter((block) => block.type === "tool_use").length;
|
|
1577
|
-
if (callCount > MAX_TOOL_CALLS_PER_STEP) {
|
|
1578
|
-
hooks.onEvent({
|
|
1579
|
-
type: "notice",
|
|
1580
|
-
level: "warn",
|
|
1581
|
-
text: `The model returned ${callCount} tool calls at once; the limit is ${MAX_TOOL_CALLS_PER_STEP}. None were run. Ask it to split the work into smaller batches.`
|
|
1582
|
-
});
|
|
1583
|
-
hooks.onEvent({ type: "turn_end", stopReason: "max_tokens" });
|
|
1584
|
-
return sanitizeHistory(messages);
|
|
1585
|
-
}
|
|
1586
1878
|
const results = await runToolCalls(cfg, outcome.content, hooks, signal);
|
|
1587
1879
|
if (results.length > 0) messages.push({ role: "user", content: results });
|
|
1588
1880
|
if (signal.aborted) {
|
|
@@ -1770,15 +2062,37 @@ function estimateRequestTokens(cfg, messages) {
|
|
|
1770
2062
|
async function runToolCalls(cfg, content, hooks, signal) {
|
|
1771
2063
|
const calls = content.filter((block) => block.type === "tool_use");
|
|
1772
2064
|
const results = [];
|
|
1773
|
-
|
|
2065
|
+
const run = (call) => {
|
|
2066
|
+
const operation = () => runOneCall(cfg, call, hooks, signal);
|
|
2067
|
+
return call.name !== "task" && cfg.executionLane ? cfg.executionLane.run(operation) : operation();
|
|
2068
|
+
};
|
|
2069
|
+
for (let index = 0; index < calls.length; ) {
|
|
2070
|
+
const call = calls[index];
|
|
2071
|
+
if (call.name !== "task") {
|
|
2072
|
+
results.push(await run(call));
|
|
2073
|
+
index += 1;
|
|
2074
|
+
continue;
|
|
2075
|
+
}
|
|
2076
|
+
const batch = [];
|
|
2077
|
+
while (calls[index]?.name === "task") {
|
|
2078
|
+
batch.push(run(calls[index]));
|
|
2079
|
+
index += 1;
|
|
2080
|
+
}
|
|
2081
|
+
const settled = await Promise.allSettled(batch);
|
|
2082
|
+
for (const result of settled) {
|
|
2083
|
+
if (result.status === "rejected") throw result.reason;
|
|
2084
|
+
results.push(result.value);
|
|
2085
|
+
}
|
|
2086
|
+
}
|
|
1774
2087
|
return results;
|
|
1775
2088
|
}
|
|
1776
2089
|
async function runOneCall(cfg, call, hooks, signal) {
|
|
2090
|
+
if (signal.aborted) return toolError(call.id, "Interrupted by the user before this tool ran.");
|
|
1777
2091
|
const tool = cfg.tools.get(call.name);
|
|
1778
2092
|
if (!tool) {
|
|
1779
2093
|
return toolError(call.id, `Unknown tool "${call.name}". Use only the tools provided.`);
|
|
1780
2094
|
}
|
|
1781
|
-
const
|
|
2095
|
+
const summary2 = describeCall(tool, call.input);
|
|
1782
2096
|
const ctx = {
|
|
1783
2097
|
cwd: cfg.cwd,
|
|
1784
2098
|
signal,
|
|
@@ -1792,30 +2106,35 @@ async function runOneCall(cfg, call, hooks, signal) {
|
|
|
1792
2106
|
}) !== "deny"
|
|
1793
2107
|
};
|
|
1794
2108
|
const requestedPermission = requestedPermissionFor(tool, call.input, ctx);
|
|
1795
|
-
const
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
if (gate === "ask") {
|
|
1803
|
-
const decision = await hooks.requestPermission({
|
|
1804
|
-
toolName: tool.name,
|
|
1805
|
-
summary,
|
|
1806
|
-
input: call.input,
|
|
1807
|
-
display: await previewCall(tool, call.input, ctx),
|
|
1808
|
-
allowAlways: canAlwaysApprove(tool)
|
|
1809
|
-
});
|
|
1810
|
-
if (decision === "deny") {
|
|
1811
|
-
return toolError(call.id, "The user declined this call. Do not retry it; ask how to proceed.");
|
|
2109
|
+
const permission = async () => {
|
|
2110
|
+
const gate = cfg.permissions.decide(tool, requestedPermission);
|
|
2111
|
+
if (gate === "deny") {
|
|
2112
|
+
return toolError(
|
|
2113
|
+
call.id,
|
|
2114
|
+
cfg.permissions.denyReason?.(tool, requestedPermission) ?? `Tool "${tool.name}" is disabled by the user's configuration. Do not retry it; find another way or ask.`
|
|
2115
|
+
);
|
|
1812
2116
|
}
|
|
1813
|
-
if (
|
|
1814
|
-
|
|
2117
|
+
if (gate === "ask") {
|
|
2118
|
+
const decision = await hooks.requestPermission({
|
|
2119
|
+
toolName: tool.name,
|
|
2120
|
+
summary: summary2,
|
|
2121
|
+
input: call.input,
|
|
2122
|
+
display: await previewCall(tool, call.input, ctx),
|
|
2123
|
+
allowAlways: canAlwaysApprove(tool)
|
|
2124
|
+
});
|
|
2125
|
+
if (decision === "deny") {
|
|
2126
|
+
return toolError(call.id, "The user declined this call. Do not retry it; ask how to proceed.");
|
|
2127
|
+
}
|
|
2128
|
+
if (decision === "always") cfg.permissions.grantForSession(tool.name);
|
|
2129
|
+
}
|
|
2130
|
+
return void 0;
|
|
2131
|
+
};
|
|
2132
|
+
const permissionResult = await permission();
|
|
2133
|
+
if (permissionResult) return permissionResult;
|
|
1815
2134
|
if (signal.aborted) {
|
|
1816
2135
|
return toolError(call.id, "Interrupted by the user before this tool ran.");
|
|
1817
2136
|
}
|
|
1818
|
-
hooks.onEvent({ type: "tool_start", id: call.id, name: tool.name, summary });
|
|
2137
|
+
hooks.onEvent({ type: "tool_start", id: call.id, name: tool.name, summary: summary2 });
|
|
1819
2138
|
try {
|
|
1820
2139
|
const result = await tool.execute(call.input, ctx);
|
|
1821
2140
|
hooks.onEvent({
|
|
@@ -1907,6 +2226,7 @@ function createTurnBudget(limits, resolvePricing = pricingFor) {
|
|
|
1907
2226
|
costUsd
|
|
1908
2227
|
});
|
|
1909
2228
|
return {
|
|
2229
|
+
hasLimits: limits.maxTokensPerTurn > 0 || limits.maxCostUsdPerTurn > 0,
|
|
1910
2230
|
beforeRequest(request) {
|
|
1911
2231
|
const current = snapshot();
|
|
1912
2232
|
if (limits.maxTokensPerTurn > 0 && current.tokens >= limits.maxTokensPerTurn) {
|
|
@@ -1987,43 +2307,6 @@ import {
|
|
|
1987
2307
|
writeFile as writeFile5
|
|
1988
2308
|
} from "fs/promises";
|
|
1989
2309
|
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
2310
|
var VERSION = 1;
|
|
2028
2311
|
var MAX_FILE_BYTES = 5e6;
|
|
2029
2312
|
var MAX_CHECKPOINT_BYTES = 2e7;
|
|
@@ -2335,14 +2618,14 @@ import { readFile as readFile6, stat as stat4 } from "fs/promises";
|
|
|
2335
2618
|
import path7 from "path";
|
|
2336
2619
|
|
|
2337
2620
|
// src/tools/bash.ts
|
|
2338
|
-
import { spawn } from "child_process";
|
|
2621
|
+
import { spawn as spawn2 } from "child_process";
|
|
2339
2622
|
var DEFAULT_TIMEOUT = 12e4;
|
|
2340
2623
|
var MAX_TIMEOUT = 6e5;
|
|
2341
2624
|
var MAX_OUTPUT = 1e5;
|
|
2342
2625
|
var MAX_COMMAND_CHARS = 2e4;
|
|
2343
2626
|
var bashTool = {
|
|
2344
2627
|
name: "bash",
|
|
2345
|
-
description: "Run a shell command in the workspace root. stdout and stderr are merged. Avoid commands that wait for interactive input.",
|
|
2628
|
+
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
2629
|
inputSchema: {
|
|
2347
2630
|
type: "object",
|
|
2348
2631
|
properties: {
|
|
@@ -2353,8 +2636,12 @@ var bashTool = {
|
|
|
2353
2636
|
},
|
|
2354
2637
|
timeoutMs: {
|
|
2355
2638
|
type: "integer",
|
|
2356
|
-
description: `Timeout in milliseconds (default ${DEFAULT_TIMEOUT}, maximum ${MAX_TIMEOUT})`,
|
|
2639
|
+
description: `Timeout in milliseconds (default ${DEFAULT_TIMEOUT}, maximum ${MAX_TIMEOUT}); ignored for background commands`,
|
|
2357
2640
|
minimum: 1
|
|
2641
|
+
},
|
|
2642
|
+
background: {
|
|
2643
|
+
type: "boolean",
|
|
2644
|
+
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
2645
|
}
|
|
2359
2646
|
},
|
|
2360
2647
|
required: ["command"],
|
|
@@ -2362,13 +2649,14 @@ var bashTool = {
|
|
|
2362
2649
|
},
|
|
2363
2650
|
defaultPermission: "ask",
|
|
2364
2651
|
summarize(input) {
|
|
2365
|
-
|
|
2652
|
+
const { command, background } = input;
|
|
2653
|
+
return background ? `bash(${brief(command)}, background)` : `bash(${brief(command)})`;
|
|
2366
2654
|
},
|
|
2367
2655
|
async preview(input) {
|
|
2368
2656
|
return { kind: "text", text: input.command };
|
|
2369
2657
|
},
|
|
2370
2658
|
execute(input, ctx) {
|
|
2371
|
-
const { command, timeoutMs } = input;
|
|
2659
|
+
const { command, timeoutMs, background } = input;
|
|
2372
2660
|
if (command.length > MAX_COMMAND_CHARS) {
|
|
2373
2661
|
return Promise.resolve({
|
|
2374
2662
|
content: `Command exceeds the ${MAX_COMMAND_CHARS} character limit.`,
|
|
@@ -2380,8 +2668,17 @@ var bashTool = {
|
|
|
2380
2668
|
if (ctx.signal.aborted) {
|
|
2381
2669
|
return Promise.resolve({ content: "[cancelled before the command started]", isError: true });
|
|
2382
2670
|
}
|
|
2671
|
+
if (background) {
|
|
2672
|
+
const started = startJob(command, ctx.cwd);
|
|
2673
|
+
if ("error" in started) {
|
|
2674
|
+
return Promise.resolve({ content: started.error, isError: true });
|
|
2675
|
+
}
|
|
2676
|
+
return Promise.resolve({
|
|
2677
|
+
content: `[started in the background as ${started.id}] Read its output with bash_output({ id: "${started.id}" }).`
|
|
2678
|
+
});
|
|
2679
|
+
}
|
|
2383
2680
|
return new Promise((settle) => {
|
|
2384
|
-
const child =
|
|
2681
|
+
const child = spawn2(command, {
|
|
2385
2682
|
shell: true,
|
|
2386
2683
|
cwd: ctx.cwd,
|
|
2387
2684
|
detached: process.platform !== "win32",
|
|
@@ -2409,11 +2706,11 @@ var bashTool = {
|
|
|
2409
2706
|
};
|
|
2410
2707
|
child.stdout.on("data", collect);
|
|
2411
2708
|
child.stderr.on("data", collect);
|
|
2412
|
-
const
|
|
2709
|
+
const killTree2 = () => {
|
|
2413
2710
|
if (child.pid == null) return;
|
|
2414
2711
|
if (process.platform === "win32") {
|
|
2415
2712
|
try {
|
|
2416
|
-
|
|
2713
|
+
spawn2("taskkill", ["/pid", String(child.pid), "/t", "/f"], { stdio: "ignore" });
|
|
2417
2714
|
} catch {
|
|
2418
2715
|
child.kill("SIGKILL");
|
|
2419
2716
|
}
|
|
@@ -2429,11 +2726,11 @@ var bashTool = {
|
|
|
2429
2726
|
let aborted = false;
|
|
2430
2727
|
const timer = setTimeout(() => {
|
|
2431
2728
|
timedOut = true;
|
|
2432
|
-
|
|
2729
|
+
killTree2();
|
|
2433
2730
|
}, timeout);
|
|
2434
2731
|
const onAbort = () => {
|
|
2435
2732
|
aborted = true;
|
|
2436
|
-
|
|
2733
|
+
killTree2();
|
|
2437
2734
|
};
|
|
2438
2735
|
ctx.signal.addEventListener("abort", onAbort, { once: true });
|
|
2439
2736
|
const finish = (result) => {
|
|
@@ -2495,20 +2792,20 @@ async function detectDiagnosticCommands(cwd, configured = []) {
|
|
|
2495
2792
|
}
|
|
2496
2793
|
async function runAutomaticDiagnostics(options) {
|
|
2497
2794
|
const commands = await detectDiagnosticCommands(options.cwd, options.configuredCommands);
|
|
2498
|
-
const
|
|
2795
|
+
const summary2 = { commands, failed: [], skipped: false };
|
|
2499
2796
|
if (options.changedFiles.length === 0 || commands.length === 0 || options.signal.aborted) {
|
|
2500
|
-
|
|
2501
|
-
return
|
|
2797
|
+
summary2.skipped = true;
|
|
2798
|
+
return summary2;
|
|
2502
2799
|
}
|
|
2503
2800
|
const gate = options.permissions.decide(diagnosticsTool);
|
|
2504
2801
|
if (gate === "deny") {
|
|
2505
|
-
|
|
2802
|
+
summary2.skipped = true;
|
|
2506
2803
|
options.hooks.onEvent({
|
|
2507
2804
|
type: "notice",
|
|
2508
2805
|
level: "warn",
|
|
2509
2806
|
text: "Automatic project checks were skipped by the current permission settings."
|
|
2510
2807
|
});
|
|
2511
|
-
return
|
|
2808
|
+
return summary2;
|
|
2512
2809
|
}
|
|
2513
2810
|
if (gate === "ask") {
|
|
2514
2811
|
const decision = await options.hooks.requestPermission({
|
|
@@ -2523,17 +2820,17 @@ async function runAutomaticDiagnostics(options) {
|
|
|
2523
2820
|
allowAlways: true
|
|
2524
2821
|
});
|
|
2525
2822
|
if (options.signal.aborted) {
|
|
2526
|
-
|
|
2527
|
-
return
|
|
2823
|
+
summary2.skipped = true;
|
|
2824
|
+
return summary2;
|
|
2528
2825
|
}
|
|
2529
2826
|
if (decision === "deny") {
|
|
2530
|
-
|
|
2827
|
+
summary2.skipped = true;
|
|
2531
2828
|
options.hooks.onEvent({
|
|
2532
2829
|
type: "notice",
|
|
2533
2830
|
level: "warn",
|
|
2534
2831
|
text: "Automatic project checks were skipped."
|
|
2535
2832
|
});
|
|
2536
|
-
return
|
|
2833
|
+
return summary2;
|
|
2537
2834
|
}
|
|
2538
2835
|
if (decision === "always") options.permissions.grantForSession(diagnosticsTool.name);
|
|
2539
2836
|
}
|
|
@@ -2546,7 +2843,7 @@ async function runAutomaticDiagnostics(options) {
|
|
|
2546
2843
|
const execute = options.execute ?? ((command, context) => bashTool.execute({ command, timeoutMs: DIAGNOSTIC_TIMEOUT_MS }, context));
|
|
2547
2844
|
for (const [index, command] of commands.entries()) {
|
|
2548
2845
|
if (options.signal.aborted) {
|
|
2549
|
-
|
|
2846
|
+
summary2.skipped = true;
|
|
2550
2847
|
break;
|
|
2551
2848
|
}
|
|
2552
2849
|
const id = `diagnostics-${Date.now()}-${index}`;
|
|
@@ -2554,7 +2851,7 @@ async function runAutomaticDiagnostics(options) {
|
|
|
2554
2851
|
options.hooks.onEvent({ type: "tool_start", id, name: diagnosticsTool.name, summary: callSummary });
|
|
2555
2852
|
try {
|
|
2556
2853
|
const result = await execute(command, ctx);
|
|
2557
|
-
if (result.isError)
|
|
2854
|
+
if (result.isError) summary2.failed.push(command);
|
|
2558
2855
|
options.hooks.onEvent({
|
|
2559
2856
|
type: "tool_end",
|
|
2560
2857
|
id,
|
|
@@ -2563,7 +2860,7 @@ async function runAutomaticDiagnostics(options) {
|
|
|
2563
2860
|
display: result.display
|
|
2564
2861
|
});
|
|
2565
2862
|
} catch (error) {
|
|
2566
|
-
|
|
2863
|
+
summary2.failed.push(command);
|
|
2567
2864
|
options.hooks.onEvent({
|
|
2568
2865
|
type: "tool_end",
|
|
2569
2866
|
id,
|
|
@@ -2572,7 +2869,7 @@ async function runAutomaticDiagnostics(options) {
|
|
|
2572
2869
|
});
|
|
2573
2870
|
}
|
|
2574
2871
|
}
|
|
2575
|
-
return
|
|
2872
|
+
return summary2;
|
|
2576
2873
|
}
|
|
2577
2874
|
async function readPackageJson(file) {
|
|
2578
2875
|
try {
|
|
@@ -2991,7 +3288,7 @@ async function compactHistory(options) {
|
|
|
2991
3288
|
if (!rendered.trim()) {
|
|
2992
3289
|
return { history: options.history, compacted: false, removedMessages: 0 };
|
|
2993
3290
|
}
|
|
2994
|
-
let
|
|
3291
|
+
let summary2;
|
|
2995
3292
|
let usage;
|
|
2996
3293
|
let rateLimits;
|
|
2997
3294
|
if (rendered.length <= SINGLE_REQUEST_MAX_CHARS) {
|
|
@@ -3005,7 +3302,7 @@ async function compactHistory(options) {
|
|
|
3005
3302
|
SYSTEM_SUMMARIZE,
|
|
3006
3303
|
requestMax
|
|
3007
3304
|
);
|
|
3008
|
-
|
|
3305
|
+
summary2 = result.text;
|
|
3009
3306
|
usage = result.usage;
|
|
3010
3307
|
rateLimits = result.rateLimits;
|
|
3011
3308
|
} else {
|
|
@@ -3040,19 +3337,19 @@ async function compactHistory(options) {
|
|
|
3040
3337
|
chunkResults.map((result) => result.text),
|
|
3041
3338
|
limits.merge
|
|
3042
3339
|
);
|
|
3043
|
-
|
|
3340
|
+
summary2 = mergeResult.text;
|
|
3044
3341
|
if (mergeResult.usage) {
|
|
3045
3342
|
usage = usage ? mergeUsage([usage, mergeResult.usage]) : mergeResult.usage;
|
|
3046
3343
|
}
|
|
3047
3344
|
if (mergeResult.rateLimits) rateLimits = mergeResult.rateLimits;
|
|
3048
3345
|
}
|
|
3049
|
-
|
|
3050
|
-
if (!
|
|
3346
|
+
summary2 = summary2.trim();
|
|
3347
|
+
if (!summary2) throw new Error("The provider returned an empty context summary.");
|
|
3051
3348
|
const prefix = [
|
|
3052
3349
|
{
|
|
3053
3350
|
role: "user",
|
|
3054
3351
|
content: [{ type: "text", text: `[Earlier conversation summary]
|
|
3055
|
-
${
|
|
3352
|
+
${summary2}` }]
|
|
3056
3353
|
},
|
|
3057
3354
|
{
|
|
3058
3355
|
role: "assistant",
|
|
@@ -3196,8 +3493,8 @@ async function mergePartialSummaries(provider, model, maxTokens, signal, summari
|
|
|
3196
3493
|
while (current.length > 1) {
|
|
3197
3494
|
const groups = groupSummaries(current, MERGE_TARGET_CHARS);
|
|
3198
3495
|
const sources = groups.map(
|
|
3199
|
-
(group) => group.map((
|
|
3200
|
-
${
|
|
3496
|
+
(group) => group.map((summary2, index) => `## Part ${index + 1}
|
|
3497
|
+
${summary2}`).join("\n\n")
|
|
3201
3498
|
);
|
|
3202
3499
|
const merged = await summarizeSourcesParallel(
|
|
3203
3500
|
provider,
|
|
@@ -3256,22 +3553,22 @@ function groupSummaries(summaries, targetChars) {
|
|
|
3256
3553
|
const groups = [];
|
|
3257
3554
|
let current = [];
|
|
3258
3555
|
let size = 0;
|
|
3259
|
-
for (const
|
|
3260
|
-
if (current.length >= 2 && size + 12 +
|
|
3556
|
+
for (const summary2 of summaries) {
|
|
3557
|
+
if (current.length >= 2 && size + 12 + summary2.length > targetChars) {
|
|
3261
3558
|
groups.push(current);
|
|
3262
3559
|
current = [];
|
|
3263
3560
|
size = 0;
|
|
3264
3561
|
}
|
|
3265
3562
|
const separator = current.length > 0 ? 12 : 0;
|
|
3266
|
-
current.push(
|
|
3267
|
-
size += separator +
|
|
3563
|
+
current.push(summary2);
|
|
3564
|
+
size += separator + summary2.length;
|
|
3268
3565
|
}
|
|
3269
3566
|
if (current.length > 0) groups.push(current);
|
|
3270
3567
|
return groups;
|
|
3271
3568
|
}
|
|
3272
3569
|
async function summarizeText(provider, model, maxTokens, signal, source, systemPrompt, maxOutputTokens) {
|
|
3273
3570
|
if (signal.aborted) throw signal.reason ?? new Error("Context compaction was cancelled.");
|
|
3274
|
-
let
|
|
3571
|
+
let summary2 = "";
|
|
3275
3572
|
let finalText2 = "";
|
|
3276
3573
|
let usage;
|
|
3277
3574
|
let rateLimits;
|
|
@@ -3285,7 +3582,7 @@ async function summarizeText(provider, model, maxTokens, signal, source, systemP
|
|
|
3285
3582
|
signal
|
|
3286
3583
|
});
|
|
3287
3584
|
for await (const event of stream) {
|
|
3288
|
-
if (event.type === "text_delta")
|
|
3585
|
+
if (event.type === "text_delta") summary2 += event.text;
|
|
3289
3586
|
else if (event.type === "usage") usage = event.usage;
|
|
3290
3587
|
else if (event.type === "rate_limits") rateLimits = event.limits;
|
|
3291
3588
|
else if (event.type === "done") {
|
|
@@ -3293,7 +3590,7 @@ async function summarizeText(provider, model, maxTokens, signal, source, systemP
|
|
|
3293
3590
|
}
|
|
3294
3591
|
}
|
|
3295
3592
|
if (signal.aborted) throw signal.reason ?? new Error("Context compaction was cancelled.");
|
|
3296
|
-
return { text: (finalText2 ||
|
|
3593
|
+
return { text: (finalText2 || summary2).trim(), usage, rateLimits };
|
|
3297
3594
|
}
|
|
3298
3595
|
function mergeUsage(usages) {
|
|
3299
3596
|
return usages.reduce(
|
|
@@ -3406,6 +3703,9 @@ var SUBAGENT_SYSTEM = [
|
|
|
3406
3703
|
'Your final message is the return value \u2014 it is the only thing the caller ever sees. End with the answer itself: the findings, the file paths, the conclusion. Never end with "I will now..." or a recap of the steps you took. Carry over every concrete detail the caller needs (paths, names, line numbers) and leave out the narration of how you found them.'
|
|
3407
3704
|
].join("\n");
|
|
3408
3705
|
function createSubagentRunner(makeConfig, tools) {
|
|
3706
|
+
const permissionLane = createExecutionLane();
|
|
3707
|
+
const executionLane = createExecutionLane();
|
|
3708
|
+
const modelRequestLane = createExecutionLane();
|
|
3409
3709
|
return {
|
|
3410
3710
|
async run(request) {
|
|
3411
3711
|
if (request.signal.aborted) return SUBAGENT_CANCELLED;
|
|
@@ -3413,6 +3713,9 @@ function createSubagentRunner(makeConfig, tools) {
|
|
|
3413
3713
|
const base = makeConfig(SUBAGENT_SYSTEM, tools);
|
|
3414
3714
|
const cfg = {
|
|
3415
3715
|
...base,
|
|
3716
|
+
permissionLane,
|
|
3717
|
+
executionLane,
|
|
3718
|
+
modelRequestLane: base.budget?.hasLimits ? modelRequestLane : void 0,
|
|
3416
3719
|
provider: {
|
|
3417
3720
|
id: base.provider.id,
|
|
3418
3721
|
kind: base.provider.kind,
|
|
@@ -3637,7 +3940,7 @@ function clip(value) {
|
|
|
3637
3940
|
// package.json
|
|
3638
3941
|
var package_default = {
|
|
3639
3942
|
name: "@kernelonpanic/kitcode",
|
|
3640
|
-
version: "1.3.
|
|
3943
|
+
version: "1.3.2",
|
|
3641
3944
|
description: "Terminal coding agent with a config you never have to write by hand",
|
|
3642
3945
|
type: "module",
|
|
3643
3946
|
license: "MIT",
|
|
@@ -3705,7 +4008,7 @@ var package_default = {
|
|
|
3705
4008
|
|
|
3706
4009
|
// src/version.ts
|
|
3707
4010
|
var KITCODE_VERSION = package_default.version;
|
|
3708
|
-
var KITCODE_COMMIT = true ? "
|
|
4011
|
+
var KITCODE_COMMIT = true ? "993d55335487da4915fa49660911ba6e69aa3685" : "development";
|
|
3709
4012
|
|
|
3710
4013
|
// src/mcp/client.ts
|
|
3711
4014
|
var clientInfo = { name: "kitcode", version: KITCODE_VERSION };
|
|
@@ -3903,8 +4206,8 @@ function describeFailure2(error, stderr, secrets) {
|
|
|
3903
4206
|
return redactSecrets(stderr ? `${message} \u2014 ${stderr}` : message, secrets);
|
|
3904
4207
|
}
|
|
3905
4208
|
function configuredSecrets(config) {
|
|
3906
|
-
const
|
|
3907
|
-
return Object.values(expandEnvRecord(
|
|
4209
|
+
const record3 = config.type === "stdio" ? config.env : config.headers;
|
|
4210
|
+
return Object.values(expandEnvRecord(record3)).filter((value) => value.length >= 4);
|
|
3908
4211
|
}
|
|
3909
4212
|
function inheritedEnv(source = process.env) {
|
|
3910
4213
|
const env = {};
|
|
@@ -4000,10 +4303,10 @@ function endpoint(origin, path15, parse2) {
|
|
|
4000
4303
|
return { url: `${origin}${path15}`, parse: parse2 };
|
|
4001
4304
|
}
|
|
4002
4305
|
function deepSeekBalance(body) {
|
|
4003
|
-
const infos =
|
|
4306
|
+
const infos = record2(body)?.["balance_infos"];
|
|
4004
4307
|
if (!Array.isArray(infos)) return null;
|
|
4005
4308
|
const balances = infos.flatMap((item) => {
|
|
4006
|
-
const entry =
|
|
4309
|
+
const entry = record2(item);
|
|
4007
4310
|
const currency = currencyCode(entry?.["currency"]);
|
|
4008
4311
|
const amount = decimal(entry?.["total_balance"]);
|
|
4009
4312
|
return currency && amount ? [{ kind: "balance", currency, amount }] : [];
|
|
@@ -4011,11 +4314,11 @@ function deepSeekBalance(body) {
|
|
|
4011
4314
|
return balances.length > 0 ? balances : null;
|
|
4012
4315
|
}
|
|
4013
4316
|
function openRouterKeyBalance(body) {
|
|
4014
|
-
const remaining = finiteNumber(
|
|
4317
|
+
const remaining = finiteNumber(record2(record2(body)?.["data"])?.["limit_remaining"]);
|
|
4015
4318
|
return remaining === null ? null : [{ kind: "key-limit", currency: "USD", amount: String(remaining) }];
|
|
4016
4319
|
}
|
|
4017
4320
|
function openRouterCreditsBalance(body) {
|
|
4018
|
-
const data =
|
|
4321
|
+
const data = record2(record2(body)?.["data"]);
|
|
4019
4322
|
const credits = finiteNumber(data?.["total_credits"]);
|
|
4020
4323
|
const usage = finiteNumber(data?.["total_usage"]);
|
|
4021
4324
|
if (credits === null || usage === null) return null;
|
|
@@ -4028,26 +4331,26 @@ function openRouterCreditsBalance(body) {
|
|
|
4028
4331
|
];
|
|
4029
4332
|
}
|
|
4030
4333
|
function siliconFlowBalance(body) {
|
|
4031
|
-
const root =
|
|
4334
|
+
const root = record2(body);
|
|
4032
4335
|
if (root?.["status"] !== true) return null;
|
|
4033
|
-
const amount = decimal(
|
|
4336
|
+
const amount = decimal(record2(root["data"])?.["balance"]);
|
|
4034
4337
|
return amount ? [{ kind: "balance", currency: "credits", amount }] : null;
|
|
4035
4338
|
}
|
|
4036
4339
|
function apiMartBalance(body) {
|
|
4037
|
-
const root =
|
|
4340
|
+
const root = record2(body);
|
|
4038
4341
|
if (root?.["success"] !== true) return null;
|
|
4039
4342
|
const amount = decimal(root["remain_balance"]);
|
|
4040
4343
|
return amount ? [{ kind: "balance", currency: "credits", amount }] : null;
|
|
4041
4344
|
}
|
|
4042
4345
|
function genericBalance(body) {
|
|
4043
|
-
const root =
|
|
4044
|
-
const data =
|
|
4346
|
+
const root = record2(body);
|
|
4347
|
+
const data = record2(root?.["data"]);
|
|
4045
4348
|
const amount = decimal(root?.["balance"] ?? data?.["balance"]);
|
|
4046
4349
|
if (!amount) return null;
|
|
4047
4350
|
const currency = currencyCode(root?.["currency"] ?? data?.["currency"]) ?? "credits";
|
|
4048
4351
|
return [{ kind: "balance", currency, amount }];
|
|
4049
4352
|
}
|
|
4050
|
-
function
|
|
4353
|
+
function record2(value) {
|
|
4051
4354
|
return typeof value === "object" && value !== null ? value : null;
|
|
4052
4355
|
}
|
|
4053
4356
|
function currencyCode(value) {
|
|
@@ -4075,8 +4378,8 @@ async function limitedResponseText(response, limit) {
|
|
|
4075
4378
|
let text = "";
|
|
4076
4379
|
try {
|
|
4077
4380
|
while (true) {
|
|
4078
|
-
const { done, value } = await reader.read();
|
|
4079
|
-
if (
|
|
4381
|
+
const { done: done2, value } = await reader.read();
|
|
4382
|
+
if (done2) return text + decoder.decode();
|
|
4080
4383
|
size += value.byteLength;
|
|
4081
4384
|
if (size > limit) {
|
|
4082
4385
|
await reader.cancel();
|
|
@@ -4096,15 +4399,15 @@ var MAX_AGE_MS = 24 * 60 * 60 * 1e3;
|
|
|
4096
4399
|
var CACHE_VERSION = 3;
|
|
4097
4400
|
async function loadModels(provider, refresh = false) {
|
|
4098
4401
|
const file = cacheFile(provider.id);
|
|
4099
|
-
const
|
|
4100
|
-
if (!refresh &&
|
|
4101
|
-
return
|
|
4402
|
+
const cached2 = await readCache(file);
|
|
4403
|
+
if (!refresh && cached2?.version === CACHE_VERSION && Date.now() - cached2.fetchedAt < MAX_AGE_MS) {
|
|
4404
|
+
return cached2.models;
|
|
4102
4405
|
}
|
|
4103
4406
|
let models;
|
|
4104
4407
|
try {
|
|
4105
4408
|
models = await provider.listModels();
|
|
4106
4409
|
} catch (error) {
|
|
4107
|
-
if (
|
|
4410
|
+
if (cached2) return cached2.models;
|
|
4108
4411
|
const known = provider.knownModels();
|
|
4109
4412
|
if (known.length > 0) return known;
|
|
4110
4413
|
throw error;
|
|
@@ -4112,6 +4415,9 @@ async function loadModels(provider, refresh = false) {
|
|
|
4112
4415
|
await writeCache(file, models);
|
|
4113
4416
|
return models;
|
|
4114
4417
|
}
|
|
4418
|
+
function refreshModels(provider) {
|
|
4419
|
+
return loadModels(provider, true);
|
|
4420
|
+
}
|
|
4115
4421
|
function cacheFile(providerId) {
|
|
4116
4422
|
return path9.join(cacheDir, "models", `${providerId.replace(/[^\w.-]/g, "_")}.json`);
|
|
4117
4423
|
}
|
|
@@ -4696,8 +5002,8 @@ function normalizeOpenAiUsage(usage) {
|
|
|
4696
5002
|
function createRegistry(config, auth) {
|
|
4697
5003
|
const built = /* @__PURE__ */ new Map();
|
|
4698
5004
|
function get(id) {
|
|
4699
|
-
const
|
|
4700
|
-
if (
|
|
5005
|
+
const cached2 = built.get(id);
|
|
5006
|
+
if (cached2) return cached2;
|
|
4701
5007
|
const entry = config.providers[id];
|
|
4702
5008
|
if (!entry) throw new Error(unknownProviderMessage(id, Object.keys(config.providers)));
|
|
4703
5009
|
const provider = build(id, entry, apiKey(id, entry, auth));
|
|
@@ -4789,13 +5095,66 @@ function isFileEdit(tool) {
|
|
|
4789
5095
|
return tool.name === "write" || tool.name === "edit";
|
|
4790
5096
|
}
|
|
4791
5097
|
|
|
5098
|
+
// src/tools/bash-output.ts
|
|
5099
|
+
var bashOutputTool = {
|
|
5100
|
+
name: "bash_output",
|
|
5101
|
+
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.",
|
|
5102
|
+
inputSchema: {
|
|
5103
|
+
type: "object",
|
|
5104
|
+
properties: {
|
|
5105
|
+
id: { type: "string", description: 'Background command id, e.g. "bg-1"' },
|
|
5106
|
+
kill: { type: "boolean", description: "Stop the command instead of reading it" }
|
|
5107
|
+
},
|
|
5108
|
+
additionalProperties: false
|
|
5109
|
+
},
|
|
5110
|
+
defaultPermission: "allow",
|
|
5111
|
+
readOnly: true,
|
|
5112
|
+
summarize(input) {
|
|
5113
|
+
const { id, kill } = input;
|
|
5114
|
+
if (!id) return "bash_output(list)";
|
|
5115
|
+
return kill ? `bash_output(${id}, kill)` : `bash_output(${id})`;
|
|
5116
|
+
},
|
|
5117
|
+
execute(input) {
|
|
5118
|
+
const { id, kill } = input;
|
|
5119
|
+
if (!id) {
|
|
5120
|
+
const jobs2 = listJobs();
|
|
5121
|
+
if (jobs2.length === 0) return done({ content: "No background commands have been started." });
|
|
5122
|
+
const lines = jobs2.map((job2) => `${job2.id} ${describeState(job2.state, job2.exitCode, job2.signal)} ${job2.command}`);
|
|
5123
|
+
return done({ content: lines.join("\n") });
|
|
5124
|
+
}
|
|
5125
|
+
if (kill) {
|
|
5126
|
+
const stopped = killJob(id);
|
|
5127
|
+
return done(
|
|
5128
|
+
stopped ? { content: `[${id} stopped]` } : { content: `No background command with id "${id}".`, isError: true }
|
|
5129
|
+
);
|
|
5130
|
+
}
|
|
5131
|
+
const job = readJob(id);
|
|
5132
|
+
if (!job) return done({ content: `No background command with id "${id}".`, isError: true });
|
|
5133
|
+
const notes = [`[${id} ${describeState(job.state, job.exitCode, job.signal)}]`];
|
|
5134
|
+
if (job.truncated) notes.push("[output buffer full; earlier output was dropped]");
|
|
5135
|
+
const body = job.output.trim() === "" ? "(no new output)" : job.output.replace(/\n+$/, "");
|
|
5136
|
+
return done({
|
|
5137
|
+
content: [body, ...notes].join("\n"),
|
|
5138
|
+
isError: job.state === "exited" && job.exitCode !== null && job.exitCode !== 0
|
|
5139
|
+
});
|
|
5140
|
+
}
|
|
5141
|
+
};
|
|
5142
|
+
function describeState(state, exitCode, signal) {
|
|
5143
|
+
if (state === "running") return "still running";
|
|
5144
|
+
if (state === "killed") return `stopped${signal ? ` by ${signal}` : ""}`;
|
|
5145
|
+
return exitCode === 0 ? "finished" : `exit code ${exitCode}`;
|
|
5146
|
+
}
|
|
5147
|
+
function done(result) {
|
|
5148
|
+
return Promise.resolve(result);
|
|
5149
|
+
}
|
|
5150
|
+
|
|
4792
5151
|
// src/tools/edit.ts
|
|
4793
5152
|
import { readFile as readFile9, stat as stat6 } from "fs/promises";
|
|
4794
5153
|
|
|
4795
5154
|
// src/tools/safe-write.ts
|
|
4796
5155
|
import { randomUUID as randomUUID3 } from "crypto";
|
|
4797
5156
|
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";
|
|
5157
|
+
import { basename as basename2, dirname as dirname2, join as join2 } from "path";
|
|
4799
5158
|
var UnsafeFileChangeError = class extends Error {
|
|
4800
5159
|
constructor(message) {
|
|
4801
5160
|
super(message);
|
|
@@ -4840,7 +5199,7 @@ async function readSafeFileSnapshot(file, maxBytes) {
|
|
|
4840
5199
|
async function atomicWriteSafeFile(file, data, snapshot) {
|
|
4841
5200
|
const parent = dirname2(file);
|
|
4842
5201
|
await assertSameParent(parent, snapshot.parent);
|
|
4843
|
-
const temp =
|
|
5202
|
+
const temp = join2(parent, `.${basename2(file)}.${process.pid}.${randomUUID3()}.tmp`);
|
|
4844
5203
|
let handle;
|
|
4845
5204
|
try {
|
|
4846
5205
|
handle = await open(temp, "wx", snapshot.mode);
|
|
@@ -5030,58 +5389,10 @@ function replacement(before, oldString, newString, replaceAll) {
|
|
|
5030
5389
|
return before.slice(0, at) + newString + before.slice(at + oldString.length);
|
|
5031
5390
|
}
|
|
5032
5391
|
|
|
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
5392
|
// src/tools/grep.ts
|
|
5082
5393
|
import { readFile as readFile10, stat as stat7 } from "fs/promises";
|
|
5083
5394
|
import { join as join3 } from "path";
|
|
5084
|
-
import
|
|
5395
|
+
import fg3 from "fast-glob";
|
|
5085
5396
|
|
|
5086
5397
|
// src/tools/sensitive.ts
|
|
5087
5398
|
import path10 from "path";
|
|
@@ -5259,7 +5570,7 @@ var grepTool = {
|
|
|
5259
5570
|
if (!safe.ok) return { content: safe.reason, isError: true };
|
|
5260
5571
|
const target = await stat7(safe.path).catch(() => null);
|
|
5261
5572
|
if (!target) return { content: `Path not found: ${path15}`, isError: true };
|
|
5262
|
-
const discovered = target.isDirectory() ? (await
|
|
5573
|
+
const discovered = target.isDirectory() ? (await fg3(glob, {
|
|
5263
5574
|
cwd: safe.path,
|
|
5264
5575
|
dot: false,
|
|
5265
5576
|
onlyFiles: true,
|
|
@@ -5482,7 +5793,7 @@ var writeTool = {
|
|
|
5482
5793
|
|
|
5483
5794
|
// src/tools/registry.ts
|
|
5484
5795
|
function builtinTools() {
|
|
5485
|
-
return [readTool, writeTool, editTool, bashTool, globTool, grepTool];
|
|
5796
|
+
return [readTool, writeTool, editTool, bashTool, bashOutputTool, globTool, grepTool];
|
|
5486
5797
|
}
|
|
5487
5798
|
function createToolRegistry(tools) {
|
|
5488
5799
|
const byName = /* @__PURE__ */ new Map();
|
|
@@ -6040,16 +6351,15 @@ function unknownSkill(name, available) {
|
|
|
6040
6351
|
// src/tools/task.ts
|
|
6041
6352
|
import { randomBytes as randomBytes3 } from "crypto";
|
|
6042
6353
|
var MAX_PROGRESS_LINES = 10;
|
|
6043
|
-
var MAX_SUBAGENTS_PER_TURN = 3;
|
|
6044
6354
|
var DESCRIPTION = [
|
|
6045
6355
|
"Hand one self-contained sub-task to a subagent that works in its own fresh context and reports back a single summary.",
|
|
6046
6356
|
"Use it when the task is genuinely independent and its intermediate output would be long or noisy: a wide search across the repository, reading many files to answer one question, tracing where something is used.",
|
|
6047
6357
|
"Do not use it for work that is a couple of tool calls, and do not use it when you already have what you need \u2014 call the tools yourself instead.",
|
|
6048
6358
|
'The subagent shares this filesystem and working directory but has NO memory of this conversation and cannot ask you anything, so "prompt" must stand alone: the goal, the constraints, and exactly what to report back.',
|
|
6359
|
+
"For multiple independent tasks, issue consecutive task calls in the same response so they can run concurrently. Do not delegate overlapping file edits.",
|
|
6049
6360
|
"It runs to completion and you receive only its final message."
|
|
6050
6361
|
].join(" ");
|
|
6051
|
-
function createTaskTool(runner
|
|
6052
|
-
const spawnsPerTurn = /* @__PURE__ */ new WeakMap();
|
|
6362
|
+
function createTaskTool(runner) {
|
|
6053
6363
|
return {
|
|
6054
6364
|
name: TASK_TOOL_NAME,
|
|
6055
6365
|
description: DESCRIPTION,
|
|
@@ -6071,14 +6381,6 @@ function createTaskTool(runner, maxSubagents = MAX_SUBAGENTS_PER_TURN) {
|
|
|
6071
6381
|
if (instruction === "") {
|
|
6072
6382
|
return { content: "task needs a prompt: the full instruction for the subagent.", isError: true };
|
|
6073
6383
|
}
|
|
6074
|
-
const spawned = (spawnsPerTurn.get(ctx.signal) ?? 0) + 1;
|
|
6075
|
-
spawnsPerTurn.set(ctx.signal, spawned);
|
|
6076
|
-
if (spawned > maxSubagents) {
|
|
6077
|
-
return {
|
|
6078
|
-
content: `Already delegated ${maxSubagents} subagents while answering this message, which is the limit. Do the remaining work yourself with the tools you have.`,
|
|
6079
|
-
isError: true
|
|
6080
|
-
};
|
|
6081
|
-
}
|
|
6082
6384
|
const progress = [];
|
|
6083
6385
|
const subagentId = `sa${randomBytes3(4).toString("hex")}`;
|
|
6084
6386
|
const description = field(input, "description") || "subagent";
|
|
@@ -6217,8 +6519,8 @@ async function readJsonBounded(response) {
|
|
|
6217
6519
|
let bytes = 0;
|
|
6218
6520
|
try {
|
|
6219
6521
|
while (true) {
|
|
6220
|
-
const { done, value } = await reader.read();
|
|
6221
|
-
if (
|
|
6522
|
+
const { done: done2, value } = await reader.read();
|
|
6523
|
+
if (done2) break;
|
|
6222
6524
|
bytes += value.byteLength;
|
|
6223
6525
|
if (bytes > MAX_RESPONSE_BYTES2) {
|
|
6224
6526
|
await reader.cancel();
|
|
@@ -6391,9 +6693,9 @@ async function boot(options) {
|
|
|
6391
6693
|
activeAgents.push(entry);
|
|
6392
6694
|
const trackedRequest = {
|
|
6393
6695
|
...request,
|
|
6394
|
-
onProgress: (
|
|
6395
|
-
entry.progress.push(
|
|
6396
|
-
request.onProgress(
|
|
6696
|
+
onProgress: (summary2) => {
|
|
6697
|
+
entry.progress.push(summary2);
|
|
6698
|
+
request.onProgress(summary2);
|
|
6397
6699
|
}
|
|
6398
6700
|
};
|
|
6399
6701
|
try {
|
|
@@ -6406,7 +6708,7 @@ async function boot(options) {
|
|
|
6406
6708
|
}
|
|
6407
6709
|
};
|
|
6408
6710
|
tools.register([
|
|
6409
|
-
createTaskTool(wrappedRunner
|
|
6711
|
+
createTaskTool(wrappedRunner)
|
|
6410
6712
|
]);
|
|
6411
6713
|
let persistQueue = Promise.resolve();
|
|
6412
6714
|
let configQueue = Promise.resolve();
|
|
@@ -6461,13 +6763,15 @@ async function boot(options) {
|
|
|
6461
6763
|
needsSetup: () => Object.keys(config.providers).length === 0,
|
|
6462
6764
|
async addProvider(url, key) {
|
|
6463
6765
|
const detected = await detectProvider(url, key);
|
|
6464
|
-
rememberModels(
|
|
6766
|
+
for (const provider of detected) rememberModels(provider.id, provider.models);
|
|
6465
6767
|
const configBefore = structuredClone(config);
|
|
6466
6768
|
const authBefore = { ...auth };
|
|
6467
|
-
|
|
6468
|
-
|
|
6469
|
-
|
|
6470
|
-
|
|
6769
|
+
for (const provider of detected) {
|
|
6770
|
+
config.providers[provider.id] = provider.config;
|
|
6771
|
+
auth[provider.id] = key;
|
|
6772
|
+
}
|
|
6773
|
+
const chosen = preferredDetectedModel(detected);
|
|
6774
|
+
if (chosen) config.model = formatModelRef(chosen.providerId, chosen.model.id);
|
|
6471
6775
|
try {
|
|
6472
6776
|
await saveConfig(config);
|
|
6473
6777
|
await saveAuth(auth);
|
|
@@ -6480,11 +6784,12 @@ async function boot(options) {
|
|
|
6480
6784
|
}
|
|
6481
6785
|
registry = createRegistry(config, auth);
|
|
6482
6786
|
const nextRef = config.model ?? "";
|
|
6483
|
-
|
|
6484
|
-
activateModel(nextRef, void 0, discovered);
|
|
6787
|
+
activateModel(nextRef, void 0, chosen?.model.contextWindow);
|
|
6485
6788
|
void refreshModelContextWindow();
|
|
6486
|
-
|
|
6487
|
-
|
|
6789
|
+
return detected.map((provider) => {
|
|
6790
|
+
const kind = provider.config.type === "anthropic" ? "Anthropic" : "OpenAI-compatible";
|
|
6791
|
+
return `${provider.id} \xB7 ${kind} \xB7 ${provider.models.length} models`;
|
|
6792
|
+
}).join("\n");
|
|
6488
6793
|
},
|
|
6489
6794
|
currentProviderId: () => parseModelRef(modelRef)?.provider,
|
|
6490
6795
|
listProviderItems: () => Object.entries(config.providers).map(([id, entry]) => ({
|
|
@@ -6838,6 +7143,15 @@ ${lines.join("\n")}` : null;
|
|
|
6838
7143
|
}
|
|
6839
7144
|
return items;
|
|
6840
7145
|
},
|
|
7146
|
+
async refreshProviderModels(providerId) {
|
|
7147
|
+
const models = await refreshModels(registry.get(providerId));
|
|
7148
|
+
rememberModels(providerId, models);
|
|
7149
|
+
if (parseModelRef(modelRef)?.provider === providerId) void refreshModelContextWindow();
|
|
7150
|
+
return models.length;
|
|
7151
|
+
},
|
|
7152
|
+
async listWorkspaceFiles(query) {
|
|
7153
|
+
return searchWorkspaceFiles(options.cwd, query);
|
|
7154
|
+
},
|
|
6841
7155
|
async listPromptItems() {
|
|
6842
7156
|
const prompts2 = await listPrompts();
|
|
6843
7157
|
return prompts2.map((prompt) => ({
|
|
@@ -7033,6 +7347,7 @@ ${lines.join("\n")}` : null;
|
|
|
7033
7347
|
try {
|
|
7034
7348
|
await Promise.all([persistQueue, configQueue]);
|
|
7035
7349
|
} finally {
|
|
7350
|
+
killAllJobs();
|
|
7036
7351
|
await mcp.close();
|
|
7037
7352
|
}
|
|
7038
7353
|
}
|
|
@@ -7045,6 +7360,16 @@ function preferredModel(models) {
|
|
|
7045
7360
|
}
|
|
7046
7361
|
return models[0]?.id;
|
|
7047
7362
|
}
|
|
7363
|
+
function preferredDetectedModel(detected) {
|
|
7364
|
+
const candidates = detected.flatMap(
|
|
7365
|
+
(provider) => provider.models.map((model) => ({ providerId: provider.id, model }))
|
|
7366
|
+
);
|
|
7367
|
+
for (const preferred of PREFERRED2) {
|
|
7368
|
+
const match = candidates.find((candidate) => candidate.model.id.endsWith(preferred));
|
|
7369
|
+
if (match) return match;
|
|
7370
|
+
}
|
|
7371
|
+
return candidates[0];
|
|
7372
|
+
}
|
|
7048
7373
|
|
|
7049
7374
|
// src/app/ask.ts
|
|
7050
7375
|
var RU_YES = /* @__PURE__ */ new Set(["y", "\u043D"]);
|
|
@@ -7313,6 +7638,7 @@ var COMMANDS = [
|
|
|
7313
7638
|
{ name: "provider" },
|
|
7314
7639
|
{ name: "logout", args: "[provider]" },
|
|
7315
7640
|
{ name: "key", args: "[provider]" },
|
|
7641
|
+
{ name: "refresh", args: "[provider]" },
|
|
7316
7642
|
{ name: "effort" },
|
|
7317
7643
|
{ name: "thinking" },
|
|
7318
7644
|
{ name: "budget", args: "[tokens]" },
|
|
@@ -7362,22 +7688,22 @@ function matchCommands(line) {
|
|
|
7362
7688
|
if (starts.length > 0) return starts;
|
|
7363
7689
|
const contains = COMMANDS.filter((command) => command.name.includes(needle));
|
|
7364
7690
|
if (contains.length > 0) return contains;
|
|
7365
|
-
return COMMANDS.filter((command) =>
|
|
7691
|
+
return COMMANDS.filter((command) => isSubsequence2(needle, command.name));
|
|
7366
7692
|
}
|
|
7367
7693
|
function closestCommand(name) {
|
|
7368
7694
|
const needle = name.toLowerCase();
|
|
7369
7695
|
let best;
|
|
7370
7696
|
let bestScore = Number.POSITIVE_INFINITY;
|
|
7371
7697
|
for (const command of COMMANDS) {
|
|
7372
|
-
const
|
|
7373
|
-
if (
|
|
7374
|
-
bestScore =
|
|
7698
|
+
const score2 = editDistance(needle, command.name);
|
|
7699
|
+
if (score2 < bestScore) {
|
|
7700
|
+
bestScore = score2;
|
|
7375
7701
|
best = command;
|
|
7376
7702
|
}
|
|
7377
7703
|
}
|
|
7378
7704
|
return bestScore <= Math.max(2, Math.floor(needle.length / 2)) ? best : void 0;
|
|
7379
7705
|
}
|
|
7380
|
-
function
|
|
7706
|
+
function isSubsequence2(needle, haystack) {
|
|
7381
7707
|
let index = 0;
|
|
7382
7708
|
for (const character of haystack) {
|
|
7383
7709
|
if (character === needle[index]) index += 1;
|
|
@@ -7402,6 +7728,43 @@ function editDistance(a, b) {
|
|
|
7402
7728
|
return previous[b.length] ?? 0;
|
|
7403
7729
|
}
|
|
7404
7730
|
|
|
7731
|
+
// src/ui/mentions.ts
|
|
7732
|
+
var MENTION_PATTERN = /(?:^|\s)@([^\s@]+)/g;
|
|
7733
|
+
var TRAILING_PUNCTUATION = /[,;:!?]+$/;
|
|
7734
|
+
function mentionSpan(value, cursor) {
|
|
7735
|
+
const at = clamp(cursor, 0, value.length);
|
|
7736
|
+
const before = value.slice(0, at);
|
|
7737
|
+
const start = before.lastIndexOf("@");
|
|
7738
|
+
if (start === -1) return null;
|
|
7739
|
+
const preceding = start === 0 ? "" : value[start - 1] ?? "";
|
|
7740
|
+
if (preceding !== "" && !/\s/.test(preceding)) return null;
|
|
7741
|
+
const query = before.slice(start + 1);
|
|
7742
|
+
if (/[\s@]/.test(query)) return null;
|
|
7743
|
+
let end = at;
|
|
7744
|
+
while (end < value.length && !/[\s@]/.test(value[end] ?? "")) end += 1;
|
|
7745
|
+
return { start, end, query };
|
|
7746
|
+
}
|
|
7747
|
+
function completeMention(value, span, path15) {
|
|
7748
|
+
const rest = value.slice(span.end);
|
|
7749
|
+
const spaced = rest.startsWith(" ");
|
|
7750
|
+
const inserted = `@${path15}${spaced ? "" : " "}`;
|
|
7751
|
+
return {
|
|
7752
|
+
value: value.slice(0, span.start) + inserted + rest,
|
|
7753
|
+
cursor: span.start + inserted.length + (spaced ? 1 : 0)
|
|
7754
|
+
};
|
|
7755
|
+
}
|
|
7756
|
+
function extractMentions(text) {
|
|
7757
|
+
const found = [];
|
|
7758
|
+
for (const match of text.matchAll(MENTION_PATTERN)) {
|
|
7759
|
+
const path15 = (match[1] ?? "").replace(TRAILING_PUNCTUATION, "");
|
|
7760
|
+
if (path15 !== "" && !found.includes(path15)) found.push(path15);
|
|
7761
|
+
}
|
|
7762
|
+
return found;
|
|
7763
|
+
}
|
|
7764
|
+
function clamp(value, min, max) {
|
|
7765
|
+
return Math.max(min, Math.min(value, max));
|
|
7766
|
+
}
|
|
7767
|
+
|
|
7405
7768
|
// src/ui/App.tsx
|
|
7406
7769
|
import TextInput2 from "ink-text-input";
|
|
7407
7770
|
|
|
@@ -7503,6 +7866,7 @@ var en = {
|
|
|
7503
7866
|
titleProvider: "Provider",
|
|
7504
7867
|
titleLogout: "Sign out of a provider",
|
|
7505
7868
|
titleKeyChange: "Change API key for a provider",
|
|
7869
|
+
titleModelRefresh: "Reload models for a provider",
|
|
7506
7870
|
titleSessions: "Resume a session",
|
|
7507
7871
|
titleSessionAction: "Session action",
|
|
7508
7872
|
resumed: (id, count) => `Resumed ${id} \u2014 ${count} messages restored`,
|
|
@@ -7582,6 +7946,7 @@ ${url}`,
|
|
|
7582
7946
|
budgetUnlimited: "Token budget: unlimited (0). Set /budget <tokens> to restore a limit.",
|
|
7583
7947
|
budgetInvalid: "Invalid budget: must be between 1,000 and 10,000,000",
|
|
7584
7948
|
modelSet: (value) => `Model: ${value}`,
|
|
7949
|
+
modelsRefreshed: (provider, count) => `${provider}: ${count} models reloaded from the endpoint.`,
|
|
7585
7950
|
noModels: "No models available. Use /login to add a provider.",
|
|
7586
7951
|
bypassOff: "Approval prompts are back on.",
|
|
7587
7952
|
bypassAsk1: "Disable approval prompts for this session?",
|
|
@@ -7629,6 +7994,7 @@ ${url}`,
|
|
|
7629
7994
|
provider: "switch between configured providers",
|
|
7630
7995
|
logout: "choose a provider to sign out from",
|
|
7631
7996
|
key: "change API key for a provider",
|
|
7997
|
+
refresh: "reload the model list for a provider",
|
|
7632
7998
|
effort: "set reasoning depth",
|
|
7633
7999
|
thinking: "toggle reasoning",
|
|
7634
8000
|
budget: "set token budget per turn",
|
|
@@ -7692,6 +8058,7 @@ var ru = {
|
|
|
7692
8058
|
titleProvider: "\u041F\u0440\u043E\u0432\u0430\u0439\u0434\u0435\u0440",
|
|
7693
8059
|
titleLogout: "\u0412\u044B\u0431\u0440\u0430\u0442\u044C API \u0434\u043B\u044F \u0432\u044B\u0445\u043E\u0434\u0430",
|
|
7694
8060
|
titleKeyChange: "\u0421\u043C\u0435\u043D\u0438\u0442\u044C \u043A\u043B\u044E\u0447 \u043F\u0440\u043E\u0432\u0430\u0439\u0434\u0435\u0440\u0430",
|
|
8061
|
+
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
8062
|
titleSessions: "\u0412\u043E\u0441\u0441\u0442\u0430\u043D\u043E\u0432\u0438\u0442\u044C \u0441\u0435\u0441\u0441\u0438\u044E",
|
|
7696
8063
|
titleSessionAction: "\u0414\u0435\u0439\u0441\u0442\u0432\u0438\u0435 \u0441 \u0441\u0435\u0441\u0441\u0438\u0435\u0439",
|
|
7697
8064
|
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 +8138,7 @@ ${url}`,
|
|
|
7771
8138
|
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
8139
|
budgetInvalid: "\u041D\u0435\u0432\u0435\u0440\u043D\u044B\u0439 \u043B\u0438\u043C\u0438\u0442: \u043E\u0442 1 000 \u0434\u043E 10 000 000",
|
|
7773
8140
|
modelSet: (value) => `\u041C\u043E\u0434\u0435\u043B\u044C: ${value}`,
|
|
8141
|
+
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
8142
|
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
8143
|
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
8144
|
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 +8186,7 @@ ${url}`,
|
|
|
7818
8186
|
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
8187
|
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
8188
|
key: "\u0441\u043C\u0435\u043D\u0438\u0442\u044C \u043A\u043B\u044E\u0447 \u043F\u0440\u043E\u0432\u0430\u0439\u0434\u0435\u0440\u0430",
|
|
8189
|
+
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
8190
|
effort: "\u0433\u043B\u0443\u0431\u0438\u043D\u0430 \u0440\u0430\u0437\u043C\u044B\u0448\u043B\u0435\u043D\u0438\u0439",
|
|
7822
8191
|
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
8192
|
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 +8675,8 @@ var PromptInput = memo(function PromptInput2({
|
|
|
8306
8675
|
pending,
|
|
8307
8676
|
hint,
|
|
8308
8677
|
history,
|
|
8309
|
-
attachments = []
|
|
8678
|
+
attachments = [],
|
|
8679
|
+
onListFiles
|
|
8310
8680
|
}) {
|
|
8311
8681
|
const theme = useTheme();
|
|
8312
8682
|
const { columns } = useTerminalSize();
|
|
@@ -8321,9 +8691,28 @@ var PromptInput = memo(function PromptInput2({
|
|
|
8321
8691
|
const cursorRef = useRef2(inputCursor);
|
|
8322
8692
|
valueRef.current = safeValue;
|
|
8323
8693
|
cursorRef.current = inputCursor;
|
|
8324
|
-
const
|
|
8325
|
-
const
|
|
8326
|
-
const
|
|
8694
|
+
const commandSuggestions = matchCommands(safeValue);
|
|
8695
|
+
const mention = onListFiles ? mentionSpan(safeValue, inputCursor) : null;
|
|
8696
|
+
const [files, setFiles] = useState4([]);
|
|
8697
|
+
const mentionQuery = mention?.query ?? null;
|
|
8698
|
+
useEffect(() => {
|
|
8699
|
+
if (mentionQuery === null || !onListFiles) {
|
|
8700
|
+
setFiles([]);
|
|
8701
|
+
return;
|
|
8702
|
+
}
|
|
8703
|
+
let live = true;
|
|
8704
|
+
void onListFiles(mentionQuery).then((found) => {
|
|
8705
|
+
if (live) setFiles(found);
|
|
8706
|
+
});
|
|
8707
|
+
return () => {
|
|
8708
|
+
live = false;
|
|
8709
|
+
};
|
|
8710
|
+
}, [mentionQuery, onListFiles]);
|
|
8711
|
+
const showFiles = mention !== null && files.length > 0;
|
|
8712
|
+
const suggestions = showFiles ? [] : commandSuggestions;
|
|
8713
|
+
const open3 = suggestions.length > 0 || showFiles;
|
|
8714
|
+
const optionCount = showFiles ? files.length : suggestions.length;
|
|
8715
|
+
const active2 = Math.min(selectionCursor, Math.max(0, optionCount - 1));
|
|
8327
8716
|
useEffect(() => {
|
|
8328
8717
|
setSelectionCursor(0);
|
|
8329
8718
|
}, [safeValue]);
|
|
@@ -8331,14 +8720,14 @@ var PromptInput = memo(function PromptInput2({
|
|
|
8331
8720
|
const length = characters2(safeValue).length;
|
|
8332
8721
|
if (pendingValue.current === safeValue) {
|
|
8333
8722
|
pendingValue.current = null;
|
|
8334
|
-
setInputCursor((cursor) =>
|
|
8723
|
+
setInputCursor((cursor) => clamp2(cursor, 0, length));
|
|
8335
8724
|
return;
|
|
8336
8725
|
}
|
|
8337
8726
|
setInputCursor(length);
|
|
8338
8727
|
}, [safeValue]);
|
|
8339
8728
|
const change = (next, cursor = characters2(next).length) => {
|
|
8340
8729
|
next = sanitizeTerminalText(next);
|
|
8341
|
-
const nextCursor2 =
|
|
8730
|
+
const nextCursor2 = clamp2(cursor, 0, characters2(next).length);
|
|
8342
8731
|
pendingValue.current = next;
|
|
8343
8732
|
valueRef.current = next;
|
|
8344
8733
|
cursorRef.current = nextCursor2;
|
|
@@ -8356,7 +8745,7 @@ var PromptInput = memo(function PromptInput2({
|
|
|
8356
8745
|
const inserted = characters2(sanitizeTerminalText(pasted));
|
|
8357
8746
|
if (inserted.length === 0) return;
|
|
8358
8747
|
const valueCharacters = characters2(valueRef.current);
|
|
8359
|
-
const cursor =
|
|
8748
|
+
const cursor = clamp2(cursorRef.current, 0, valueCharacters.length);
|
|
8360
8749
|
valueCharacters.splice(cursor, 0, ...inserted);
|
|
8361
8750
|
change(valueCharacters.join(""), cursor + inserted.length);
|
|
8362
8751
|
};
|
|
@@ -8380,7 +8769,15 @@ var PromptInput = memo(function PromptInput2({
|
|
|
8380
8769
|
return;
|
|
8381
8770
|
}
|
|
8382
8771
|
if (open3 && key.downArrow) {
|
|
8383
|
-
setSelectionCursor(Math.min(
|
|
8772
|
+
setSelectionCursor(Math.min(optionCount - 1, active2 + 1));
|
|
8773
|
+
return;
|
|
8774
|
+
}
|
|
8775
|
+
if (showFiles && (key.tab || key.return) && !key.shift) {
|
|
8776
|
+
const chosen = files[active2];
|
|
8777
|
+
if (chosen && mention) {
|
|
8778
|
+
const completed = completeMention(safeValue, mention, chosen);
|
|
8779
|
+
change(completed.value, completed.cursor);
|
|
8780
|
+
}
|
|
8384
8781
|
return;
|
|
8385
8782
|
}
|
|
8386
8783
|
if (open3 && key.tab && !key.shift) {
|
|
@@ -8418,7 +8815,7 @@ var PromptInput = memo(function PromptInput2({
|
|
|
8418
8815
|
return;
|
|
8419
8816
|
}
|
|
8420
8817
|
const valueCharacters = characters2(safeValue);
|
|
8421
|
-
const cursor =
|
|
8818
|
+
const cursor = clamp2(inputCursor, 0, valueCharacters.length);
|
|
8422
8819
|
if (key.leftArrow || key.rightArrow || key.home || key.end) {
|
|
8423
8820
|
setInputCursor(nextCursor(cursor, valueCharacters.length, key));
|
|
8424
8821
|
return;
|
|
@@ -8445,14 +8842,12 @@ var PromptInput = memo(function PromptInput2({
|
|
|
8445
8842
|
});
|
|
8446
8843
|
const start = Math.max(0, Math.min(active2 - WINDOW2 + 2, suggestions.length - WINDOW2));
|
|
8447
8844
|
const visible = suggestions.slice(start, start + WINDOW2);
|
|
8845
|
+
const fileStart = Math.max(0, Math.min(active2 - WINDOW2 + 2, files.length - WINDOW2));
|
|
8448
8846
|
return /* @__PURE__ */ jsxs8(Box8, { width: columns, maxWidth: "100%", flexDirection: "column", marginTop: 1, flexShrink: 0, children: [
|
|
8449
8847
|
/* @__PURE__ */ jsxs8(
|
|
8450
8848
|
Box8,
|
|
8451
8849
|
{
|
|
8452
8850
|
width: "100%",
|
|
8453
|
-
borderStyle: "round",
|
|
8454
|
-
borderColor: disabled ? theme.warn : theme.accent,
|
|
8455
|
-
borderDimColor: disabled,
|
|
8456
8851
|
paddingX: 1,
|
|
8457
8852
|
children: [
|
|
8458
8853
|
/* @__PURE__ */ jsx8(Text8, { color: disabled ? "gray" : theme.accent, children: "\u203A " }),
|
|
@@ -8473,6 +8868,18 @@ var PromptInput = memo(function PromptInput2({
|
|
|
8473
8868
|
" ",
|
|
8474
8869
|
sanitizeTerminalText(hint)
|
|
8475
8870
|
] }),
|
|
8871
|
+
showFiles && /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", marginLeft: 2, children: [
|
|
8872
|
+
files.slice(fileStart, fileStart + WINDOW2).map((file, index) => {
|
|
8873
|
+
const selected = fileStart + index === active2;
|
|
8874
|
+
return /* @__PURE__ */ jsxs8(Text8, { color: selected ? theme.accent : void 0, dimColor: !selected, children: [
|
|
8875
|
+
selected ? "\u276F " : " ",
|
|
8876
|
+
"@",
|
|
8877
|
+
sanitizeTerminalText(file)
|
|
8878
|
+
] }, file);
|
|
8879
|
+
}),
|
|
8880
|
+
files.length > WINDOW2 && /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: strings.more(files.length - WINDOW2) }),
|
|
8881
|
+
/* @__PURE__ */ jsx8(Text8, { dimColor: true, children: strings.suggestHelp })
|
|
8882
|
+
] }),
|
|
8476
8883
|
open3 && /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", marginLeft: 2, children: [
|
|
8477
8884
|
visible.map((command, index) => {
|
|
8478
8885
|
const selected = start + index === active2;
|
|
@@ -8498,7 +8905,7 @@ function EditableText({
|
|
|
8498
8905
|
placeholder
|
|
8499
8906
|
}) {
|
|
8500
8907
|
const valueCharacters = characters2(value);
|
|
8501
|
-
const at =
|
|
8908
|
+
const at = clamp2(cursor, 0, valueCharacters.length);
|
|
8502
8909
|
if (valueCharacters.length === 0) {
|
|
8503
8910
|
const placeholderCharacters = characters2(sanitizeTerminalText(placeholder));
|
|
8504
8911
|
return /* @__PURE__ */ jsxs8(Text8, { children: [
|
|
@@ -8522,7 +8929,7 @@ function nextCursor(cursor, length, key) {
|
|
|
8522
8929
|
function characters2(value) {
|
|
8523
8930
|
return [...value];
|
|
8524
8931
|
}
|
|
8525
|
-
function
|
|
8932
|
+
function clamp2(value, min, max) {
|
|
8526
8933
|
return Math.max(min, Math.min(value, max));
|
|
8527
8934
|
}
|
|
8528
8935
|
|
|
@@ -8771,13 +9178,18 @@ import Spinner2 from "ink-spinner";
|
|
|
8771
9178
|
import stringWidth2 from "string-width";
|
|
8772
9179
|
|
|
8773
9180
|
// src/ui/markdown.tsx
|
|
8774
|
-
import { Box as Box11, Text as
|
|
8775
|
-
import { Fragment as Fragment4, useMemo as useMemo3 } from "react";
|
|
9181
|
+
import { Box as Box11, Text as InkText } from "ink";
|
|
9182
|
+
import { createContext as createContext3, Fragment as Fragment4, useContext as useContext3, useMemo as useMemo3 } from "react";
|
|
8776
9183
|
import stringWidth from "string-width";
|
|
8777
9184
|
import { jsx as jsx11, jsxs as jsxs10 } from "react/jsx-runtime";
|
|
8778
|
-
|
|
9185
|
+
var DimContext = createContext3(false);
|
|
9186
|
+
function Text10({ dimColor, color, ...props }) {
|
|
9187
|
+
const dim = useContext3(DimContext);
|
|
9188
|
+
return /* @__PURE__ */ jsx11(InkText, { ...props, color: dim ? void 0 : color, dimColor: dimColor ?? dim });
|
|
9189
|
+
}
|
|
9190
|
+
function Markdown({ children, dim = false }) {
|
|
8779
9191
|
const blocks = useMemo3(() => extractBlocks(children), [children]);
|
|
8780
|
-
return /* @__PURE__ */ jsx11(Box11, { flexDirection: "column", children: blocks.map((block, i) => /* @__PURE__ */ jsx11(BlockView, { block }, i)) });
|
|
9192
|
+
return /* @__PURE__ */ jsx11(DimContext.Provider, { value: dim, children: /* @__PURE__ */ jsx11(Box11, { flexDirection: "column", children: blocks.map((block, i) => /* @__PURE__ */ jsx11(BlockView, { block }, i)) }) });
|
|
8781
9193
|
}
|
|
8782
9194
|
function BlockView({ block }) {
|
|
8783
9195
|
switch (block.type) {
|
|
@@ -9425,7 +9837,7 @@ function AssistantView({ bubble: source, maxRows }) {
|
|
|
9425
9837
|
const usedThinkingRows = frameThinking === "" ? 0 : frameThinking.split("\n").length;
|
|
9426
9838
|
const frameText = liveBudget === void 0 ? bubble.text : clipTextToRows(bubble.text, Math.max(1, liveBudget - usedThinkingRows), columns);
|
|
9427
9839
|
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 }) }),
|
|
9840
|
+
frameThinking !== "" && /* @__PURE__ */ jsx12(Box12, { flexDirection: "column", maxHeight: thinkingBudget, overflowY: "hidden", children: /* @__PURE__ */ jsx12(Markdown, { dim: true, children: frameThinking }) }),
|
|
9429
9841
|
bubble.streaming ? /* @__PURE__ */ jsx12(Text11, { children: frameText }) : /* @__PURE__ */ jsx12(Markdown, { children: bubble.text }),
|
|
9430
9842
|
bubble.streaming && bubble.text === "" && /* @__PURE__ */ jsxs11(Text11, { dimColor: true, children: [
|
|
9431
9843
|
/* @__PURE__ */ jsx12(Spinner2, { type: "dots" }),
|
|
@@ -9929,6 +10341,11 @@ function App({
|
|
|
9929
10341
|
})
|
|
9930
10342
|
};
|
|
9931
10343
|
try {
|
|
10344
|
+
try {
|
|
10345
|
+
await runtime.persist(history.current);
|
|
10346
|
+
} catch (error) {
|
|
10347
|
+
notice("error", error instanceof Error ? error.message : String(error));
|
|
10348
|
+
}
|
|
9932
10349
|
history.current = await runtime.run(history.current, hooks, controller.signal);
|
|
9933
10350
|
void runtime.persist(history.current).catch((error) => notice("error", error instanceof Error ? error.message : String(error)));
|
|
9934
10351
|
} catch (error) {
|
|
@@ -9957,7 +10374,6 @@ function App({
|
|
|
9957
10374
|
const command = (firstSpace === -1 ? body : body.slice(0, firstSpace)).toLowerCase();
|
|
9958
10375
|
const rawRest = firstSpace === -1 ? "" : body.slice(firstSpace).trim();
|
|
9959
10376
|
const rest = rawRest ? rawRest.split(/\s+/) : [];
|
|
9960
|
-
setInput("");
|
|
9961
10377
|
switch (command) {
|
|
9962
10378
|
case "help":
|
|
9963
10379
|
notice(
|
|
@@ -10256,6 +10672,29 @@ ${strings.modelSet(result.nextModel)}` : ""}`;
|
|
|
10256
10672
|
}
|
|
10257
10673
|
return;
|
|
10258
10674
|
}
|
|
10675
|
+
case "refresh": {
|
|
10676
|
+
const providers = runtime.listProviderItems();
|
|
10677
|
+
if (providers.length === 0) {
|
|
10678
|
+
notice("warn", strings.logoutNothing);
|
|
10679
|
+
return;
|
|
10680
|
+
}
|
|
10681
|
+
let providerId = rest[0];
|
|
10682
|
+
if (!providerId) {
|
|
10683
|
+
providerId = providers.length === 1 ? providers[0]?.key : await pick(strings.titleModelRefresh, providers) ?? void 0;
|
|
10684
|
+
}
|
|
10685
|
+
if (!providerId) return;
|
|
10686
|
+
if (!providers.some((provider) => provider.key === providerId)) {
|
|
10687
|
+
notice("warn", `Provider "${providerId}" is not configured.`);
|
|
10688
|
+
return;
|
|
10689
|
+
}
|
|
10690
|
+
try {
|
|
10691
|
+
const count = await runtime.refreshProviderModels(providerId);
|
|
10692
|
+
notice("info", strings.modelsRefreshed(providerId, count));
|
|
10693
|
+
} catch (error) {
|
|
10694
|
+
notice("error", error instanceof Error ? error.message : String(error));
|
|
10695
|
+
}
|
|
10696
|
+
return;
|
|
10697
|
+
}
|
|
10259
10698
|
case "lang": {
|
|
10260
10699
|
const choice = await pick(
|
|
10261
10700
|
strings.titleLang,
|
|
@@ -10850,6 +11289,10 @@ Rename the file if you want a different name.`);
|
|
|
10850
11289
|
},
|
|
10851
11290
|
[appendAttachment, notice, runtime, strings]
|
|
10852
11291
|
);
|
|
11292
|
+
const listWorkspaceFiles = useCallback(
|
|
11293
|
+
(query) => runtime.listWorkspaceFiles(query).catch(() => []),
|
|
11294
|
+
[runtime]
|
|
11295
|
+
);
|
|
10853
11296
|
const pasteClipboardImage = useCallback(() => {
|
|
10854
11297
|
if (clipboardPasteTask.current) return;
|
|
10855
11298
|
if (attachmentsRef.current.length >= MAX_ATTACHMENTS) {
|
|
@@ -10888,7 +11331,9 @@ Rename the file if you want a different name.`);
|
|
|
10888
11331
|
const text = raw.trim();
|
|
10889
11332
|
const submitSlash = () => {
|
|
10890
11333
|
if (!detachedInput) setInput("");
|
|
10891
|
-
|
|
11334
|
+
const command = text.slice(1).trim().split(/\s+/)[0]?.toLowerCase();
|
|
11335
|
+
const liveCommand = ["help", "usage", "subagents", "config", "exit", "quit"].includes(command ?? "");
|
|
11336
|
+
if (busyRef.current && !liveCommand || slashPendingRef.current > 0) {
|
|
10892
11337
|
queueRef.current = [...queueRef.current, { kind: "command", line: text }];
|
|
10893
11338
|
setPendingCount(queueRef.current.length);
|
|
10894
11339
|
return;
|
|
@@ -10913,11 +11358,21 @@ Rename the file if you want a different name.`);
|
|
|
10913
11358
|
}
|
|
10914
11359
|
const queuedAttachments = attachmentsRef.current;
|
|
10915
11360
|
if (!text && queuedAttachments.length === 0) return;
|
|
11361
|
+
const mentioned = [];
|
|
11362
|
+
for (const path15 of extractMentions(text)) {
|
|
11363
|
+
if (queuedAttachments.length + mentioned.length >= MAX_ATTACHMENTS) break;
|
|
11364
|
+
try {
|
|
11365
|
+
mentioned.push(await runtime.loadAttachment(path15));
|
|
11366
|
+
} catch {
|
|
11367
|
+
}
|
|
11368
|
+
}
|
|
11369
|
+
if (generation !== attachmentGeneration.current) return;
|
|
10916
11370
|
if (text) setPromptHistory((state) => appendInputHistory(state, text));
|
|
10917
11371
|
if (!detachedInput) setInput("");
|
|
10918
11372
|
const content = [
|
|
10919
11373
|
...text ? [{ type: "text", text }] : [],
|
|
10920
|
-
...queuedAttachments
|
|
11374
|
+
...queuedAttachments,
|
|
11375
|
+
...mentioned
|
|
10921
11376
|
];
|
|
10922
11377
|
replaceAttachments([]);
|
|
10923
11378
|
const item = { kind: "message", text, content };
|
|
@@ -10992,11 +11447,11 @@ Rename the file if you want a different name.`);
|
|
|
10992
11447
|
Onboarding,
|
|
10993
11448
|
{
|
|
10994
11449
|
onSubmit: async (url, key) => {
|
|
10995
|
-
const
|
|
11450
|
+
const summary2 = await runtime.addProvider(url, key);
|
|
10996
11451
|
setSetup(false);
|
|
10997
|
-
notice("info", `${
|
|
11452
|
+
notice("info", `${summary2}
|
|
10998
11453
|
${strings.configAt(runtime.configPath())}`);
|
|
10999
|
-
return
|
|
11454
|
+
return summary2;
|
|
11000
11455
|
}
|
|
11001
11456
|
}
|
|
11002
11457
|
)
|
|
@@ -11073,6 +11528,7 @@ ${strings.configAt(runtime.configPath())}`);
|
|
|
11073
11528
|
onSubmit: submit,
|
|
11074
11529
|
onPastePath: tryQueueAutomaticAttachment,
|
|
11075
11530
|
onPasteImage: pasteClipboardImage,
|
|
11531
|
+
onListFiles: listWorkspaceFiles,
|
|
11076
11532
|
disabled: busy,
|
|
11077
11533
|
pending: pendingCount,
|
|
11078
11534
|
hint: busy ? strings.escCancel : void 0,
|