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