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