@kernelonpanic/kitcode 1.4.1 → 1.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +43 -21
- package/README.ru.md +24 -16
- package/dist/index.js +1040 -548
- 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);
|
|
@@ -344,11 +346,11 @@ function oneLine(text) {
|
|
|
344
346
|
return text.replace(/\s+/g, " ").trim();
|
|
345
347
|
}
|
|
346
348
|
function redactSecrets(text, knownSecrets = []) {
|
|
347
|
-
let safe = text
|
|
349
|
+
let safe = text;
|
|
348
350
|
for (const secret of knownSecrets) {
|
|
349
351
|
if (secret.length >= 4) safe = safe.split(secret).join("[redacted]");
|
|
350
352
|
}
|
|
351
|
-
return safe;
|
|
353
|
+
return safe.replace(NAMED_SECRET, "$1[redacted]").replace(SECRET, "[redacted]");
|
|
352
354
|
}
|
|
353
355
|
function prop(value, key) {
|
|
354
356
|
if (value === null || typeof value !== "object") return void 0;
|
|
@@ -1182,11 +1184,11 @@ var globTool = {
|
|
|
1182
1184
|
return `glob(${brief(input.pattern)})`;
|
|
1183
1185
|
},
|
|
1184
1186
|
async execute(input, ctx) {
|
|
1185
|
-
const { pattern, path:
|
|
1187
|
+
const { pattern, path: path16 = "." } = input;
|
|
1186
1188
|
if (patternEscapes(pattern)) {
|
|
1187
1189
|
return { content: `Pattern ${pattern} must be relative to the workspace root.`, isError: true };
|
|
1188
1190
|
}
|
|
1189
|
-
const safe = resolveInside(ctx.cwd,
|
|
1191
|
+
const safe = resolveInside(ctx.cwd, path16);
|
|
1190
1192
|
if (!safe.ok) return { content: safe.reason, isError: true };
|
|
1191
1193
|
const entries = await fg(pattern, {
|
|
1192
1194
|
cwd: safe.path,
|
|
@@ -1265,8 +1267,10 @@ function isSubsequence(needle, haystack) {
|
|
|
1265
1267
|
|
|
1266
1268
|
// src/tools/jobs.ts
|
|
1267
1269
|
import { spawn } from "child_process";
|
|
1270
|
+
import { StringDecoder } from "string_decoder";
|
|
1268
1271
|
var MAX_JOBS = 8;
|
|
1269
1272
|
var MAX_JOB_OUTPUT = 1e5;
|
|
1273
|
+
var MAX_FINISHED_JOBS = 16;
|
|
1270
1274
|
var jobs = /* @__PURE__ */ new Map();
|
|
1271
1275
|
var counter = 0;
|
|
1272
1276
|
function startJob(command, cwd) {
|
|
@@ -1274,6 +1278,8 @@ function startJob(command, cwd) {
|
|
|
1274
1278
|
if (live.length >= MAX_JOBS) {
|
|
1275
1279
|
return { error: `At most ${MAX_JOBS} background commands can run at once.` };
|
|
1276
1280
|
}
|
|
1281
|
+
pruneFinishedJobs();
|
|
1282
|
+
installExitCleanup();
|
|
1277
1283
|
counter += 1;
|
|
1278
1284
|
const id = `bg-${counter}`;
|
|
1279
1285
|
const child = spawn(command, {
|
|
@@ -1295,19 +1301,24 @@ function startJob(command, cwd) {
|
|
|
1295
1301
|
truncated: false
|
|
1296
1302
|
};
|
|
1297
1303
|
jobs.set(id, job);
|
|
1298
|
-
const
|
|
1299
|
-
const
|
|
1304
|
+
const collector = () => {
|
|
1305
|
+
const decoder = new StringDecoder("utf8");
|
|
1306
|
+
return (chunk) => collect(decoder.write(chunk));
|
|
1307
|
+
};
|
|
1308
|
+
const collect = (text) => {
|
|
1300
1309
|
const room = MAX_JOB_OUTPUT - job.buffer.length;
|
|
1301
1310
|
if (text.length <= room) {
|
|
1302
1311
|
job.buffer += text;
|
|
1303
1312
|
return;
|
|
1304
1313
|
}
|
|
1305
1314
|
job.truncated = true;
|
|
1306
|
-
|
|
1307
|
-
|
|
1315
|
+
const combined = job.buffer + text;
|
|
1316
|
+
const dropped = combined.length - MAX_JOB_OUTPUT;
|
|
1317
|
+
job.buffer = combined.slice(dropped);
|
|
1318
|
+
job.reported = Math.max(0, job.reported - dropped);
|
|
1308
1319
|
};
|
|
1309
|
-
child.stdout?.on("data",
|
|
1310
|
-
child.stderr?.on("data",
|
|
1320
|
+
child.stdout?.on("data", collector());
|
|
1321
|
+
child.stderr?.on("data", collector());
|
|
1311
1322
|
child.on("error", (error) => {
|
|
1312
1323
|
job.buffer += `
|
|
1313
1324
|
Command failed to start: ${error.message}`;
|
|
@@ -1348,6 +1359,26 @@ function killAllJobs() {
|
|
|
1348
1359
|
}
|
|
1349
1360
|
}
|
|
1350
1361
|
}
|
|
1362
|
+
var exitCleanupInstalled = false;
|
|
1363
|
+
function installExitCleanup() {
|
|
1364
|
+
if (exitCleanupInstalled) return;
|
|
1365
|
+
exitCleanupInstalled = true;
|
|
1366
|
+
process.once("exit", killAllJobs);
|
|
1367
|
+
for (const signal of ["SIGTERM", "SIGHUP"]) {
|
|
1368
|
+
const onSignal = () => {
|
|
1369
|
+
killAllJobs();
|
|
1370
|
+
process.off(signal, onSignal);
|
|
1371
|
+
process.kill(process.pid, signal);
|
|
1372
|
+
};
|
|
1373
|
+
process.on(signal, onSignal);
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
function pruneFinishedJobs() {
|
|
1377
|
+
const finished = [...jobs.values()].filter((job) => job.state !== "running");
|
|
1378
|
+
for (const job of finished.slice(0, Math.max(0, finished.length - MAX_FINISHED_JOBS + 1))) {
|
|
1379
|
+
jobs.delete(job.id);
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1351
1382
|
function summary(job) {
|
|
1352
1383
|
return {
|
|
1353
1384
|
id: job.id,
|
|
@@ -1442,12 +1473,12 @@ function openAiEffort(model, effort) {
|
|
|
1442
1473
|
}
|
|
1443
1474
|
|
|
1444
1475
|
// src/app/runtime.ts
|
|
1445
|
-
import
|
|
1476
|
+
import path15 from "path";
|
|
1446
1477
|
import { mkdir as mkdir6 } from "fs/promises";
|
|
1447
1478
|
|
|
1448
1479
|
// src/core/session.ts
|
|
1449
1480
|
import { randomBytes } from "crypto";
|
|
1450
|
-
import { readFile as readFile4, readdir, rename as rename3, stat as stat2, unlink, writeFile as writeFile4 } from "fs/promises";
|
|
1481
|
+
import { readFile as readFile4, readdir, rename as rename3, rm as rm4, stat as stat2, unlink, writeFile as writeFile4 } from "fs/promises";
|
|
1451
1482
|
import path5 from "path";
|
|
1452
1483
|
var MAX_SESSION_BYTES = 5e7;
|
|
1453
1484
|
function createSession(cwd, model) {
|
|
@@ -1548,8 +1579,13 @@ async function deleteSession(query) {
|
|
|
1548
1579
|
if (error.code === "ENOENT") throw new Error(`Session no longer exists: ${id}`);
|
|
1549
1580
|
throw error;
|
|
1550
1581
|
});
|
|
1582
|
+
await removeCheckpoints(id);
|
|
1551
1583
|
return id;
|
|
1552
1584
|
}
|
|
1585
|
+
async function removeCheckpoints(id) {
|
|
1586
|
+
assertSessionId(id);
|
|
1587
|
+
await rm4(path5.join(checkpointsDir, id), { recursive: true, force: true }).catch(() => void 0);
|
|
1588
|
+
}
|
|
1553
1589
|
async function deleteAllSessions() {
|
|
1554
1590
|
let names;
|
|
1555
1591
|
try {
|
|
@@ -1577,6 +1613,7 @@ async function deleteAllSessions() {
|
|
|
1577
1613
|
const id = ids[index];
|
|
1578
1614
|
if (result.status === "fulfilled" || result.reason?.code === "ENOENT") {
|
|
1579
1615
|
deleted.push(id);
|
|
1616
|
+
await removeCheckpoints(id);
|
|
1580
1617
|
} else {
|
|
1581
1618
|
failed.push({
|
|
1582
1619
|
id,
|
|
@@ -1944,7 +1981,7 @@ async function runTurn(cfg, history, hooks, signal) {
|
|
|
1944
1981
|
});
|
|
1945
1982
|
}
|
|
1946
1983
|
hooks.onEvent({ type: "turn_end", stopReason: outcome.stopReason });
|
|
1947
|
-
return messages;
|
|
1984
|
+
return sanitizeHistory(messages);
|
|
1948
1985
|
}
|
|
1949
1986
|
}
|
|
1950
1987
|
async function consumeStream(cfg, messages, hooks, signal, maxTokens) {
|
|
@@ -2144,6 +2181,13 @@ async function runOneCall(cfg, call, hooks, signal) {
|
|
|
2144
2181
|
if (!tool) {
|
|
2145
2182
|
return toolError(call.id, `Unknown tool "${call.name}". Use only the tools provided.`);
|
|
2146
2183
|
}
|
|
2184
|
+
const invalid = invalidArguments(call.input);
|
|
2185
|
+
if (invalid !== void 0) {
|
|
2186
|
+
return toolError(
|
|
2187
|
+
call.id,
|
|
2188
|
+
`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}`
|
|
2189
|
+
);
|
|
2190
|
+
}
|
|
2147
2191
|
const summary2 = describeCall(tool, call.input);
|
|
2148
2192
|
const ctx = {
|
|
2149
2193
|
cwd: cfg.cwd,
|
|
@@ -2241,6 +2285,11 @@ function describeRefusal(refusal) {
|
|
|
2241
2285
|
if (refusal?.explanation) parts.push(refusal.explanation);
|
|
2242
2286
|
return parts.join(" ");
|
|
2243
2287
|
}
|
|
2288
|
+
function invalidArguments(input) {
|
|
2289
|
+
if (typeof input !== "object" || input === null) return void 0;
|
|
2290
|
+
const raw = input[INVALID_ARGUMENTS_KEY];
|
|
2291
|
+
return typeof raw === "string" ? raw : void 0;
|
|
2292
|
+
}
|
|
2244
2293
|
function toolError(toolUseId, content) {
|
|
2245
2294
|
return { type: "tool_result", toolUseId, content, isError: true };
|
|
2246
2295
|
}
|
|
@@ -2688,11 +2737,13 @@ function assertSessionId2(id) {
|
|
|
2688
2737
|
}
|
|
2689
2738
|
|
|
2690
2739
|
// src/core/diagnostics.ts
|
|
2740
|
+
import { createHash as createHash3 } from "crypto";
|
|
2691
2741
|
import { readFile as readFile6, stat as stat4 } from "fs/promises";
|
|
2692
2742
|
import path7 from "path";
|
|
2693
2743
|
|
|
2694
2744
|
// src/tools/bash.ts
|
|
2695
2745
|
import { spawn as spawn2 } from "child_process";
|
|
2746
|
+
import { StringDecoder as StringDecoder2 } from "string_decoder";
|
|
2696
2747
|
var DEFAULT_TIMEOUT = 12e4;
|
|
2697
2748
|
var MAX_TIMEOUT = 6e5;
|
|
2698
2749
|
var MAX_OUTPUT = 1e5;
|
|
@@ -2776,14 +2827,12 @@ var bashTool = {
|
|
|
2776
2827
|
};
|
|
2777
2828
|
let captured = 0;
|
|
2778
2829
|
let truncated = false;
|
|
2779
|
-
const
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
text = buf.toString();
|
|
2786
|
-
}
|
|
2830
|
+
const collector = () => {
|
|
2831
|
+
const decoder = new StringDecoder2("utf8");
|
|
2832
|
+
return (buf) => collect(decoder.write(buf));
|
|
2833
|
+
};
|
|
2834
|
+
const collect = (text) => {
|
|
2835
|
+
if (truncated || text === "") return;
|
|
2787
2836
|
progress(text);
|
|
2788
2837
|
if (text.length > MAX_OUTPUT - captured) {
|
|
2789
2838
|
chunks.push(text.slice(0, MAX_OUTPUT - captured));
|
|
@@ -2793,8 +2842,8 @@ var bashTool = {
|
|
|
2793
2842
|
chunks.push(text);
|
|
2794
2843
|
captured += text.length;
|
|
2795
2844
|
};
|
|
2796
|
-
child.stdout.on("data",
|
|
2797
|
-
child.stderr.on("data",
|
|
2845
|
+
child.stdout.on("data", collector());
|
|
2846
|
+
child.stderr.on("data", collector());
|
|
2798
2847
|
const killTree2 = () => {
|
|
2799
2848
|
if (child.pid == null) return;
|
|
2800
2849
|
if (process.platform === "win32") {
|
|
@@ -2857,19 +2906,19 @@ var diagnosticsTool = {
|
|
|
2857
2906
|
summarize: () => "automatic project checks",
|
|
2858
2907
|
execute: async () => ({ content: "" })
|
|
2859
2908
|
};
|
|
2860
|
-
async function
|
|
2909
|
+
async function detectChecks(cwd, configured = []) {
|
|
2861
2910
|
const custom = configured.map((command) => command.trim()).filter(Boolean);
|
|
2862
|
-
if (custom.length > 0) return custom.slice(0, 8);
|
|
2863
|
-
const
|
|
2911
|
+
if (custom.length > 0) return custom.slice(0, 8).map((command) => ({ command }));
|
|
2912
|
+
const checks = [];
|
|
2864
2913
|
const packageJson = await readPackageJson(path7.join(cwd, "package.json"));
|
|
2865
2914
|
if (packageJson) {
|
|
2866
2915
|
const manager = await packageManager(cwd, packageJson.packageManager);
|
|
2867
2916
|
for (const name of ["lint", "typecheck", "check", "test"]) {
|
|
2868
|
-
|
|
2869
|
-
|
|
2870
|
-
}
|
|
2917
|
+
const body = packageJson.scripts?.[name];
|
|
2918
|
+
if (typeof body === "string") checks.push({ command: scriptCommand(manager, name), runs: body });
|
|
2871
2919
|
}
|
|
2872
2920
|
}
|
|
2921
|
+
const commands = [];
|
|
2873
2922
|
if (await isFile2(path7.join(cwd, "Cargo.toml"))) {
|
|
2874
2923
|
commands.push("cargo check", "cargo test");
|
|
2875
2924
|
}
|
|
@@ -2877,10 +2926,13 @@ async function detectDiagnosticCommands(cwd, configured = []) {
|
|
|
2877
2926
|
if (await isFile2(path7.join(cwd, "pyproject.toml")) || await isFile2(path7.join(cwd, "pytest.ini")) || await isFile2(path7.join(cwd, "tox.ini"))) {
|
|
2878
2927
|
commands.push("python -m pytest");
|
|
2879
2928
|
}
|
|
2880
|
-
|
|
2929
|
+
for (const command of commands) checks.push({ command });
|
|
2930
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2931
|
+
return checks.filter((check) => !seen.has(check.command) && seen.add(check.command)).slice(0, 8);
|
|
2881
2932
|
}
|
|
2882
2933
|
async function runAutomaticDiagnostics(options) {
|
|
2883
|
-
const
|
|
2934
|
+
const checks = await detectChecks(options.cwd, options.configuredCommands);
|
|
2935
|
+
const commands = checks.map((check) => check.command);
|
|
2884
2936
|
const summary2 = { commands, failed: [], skipped: false };
|
|
2885
2937
|
if (options.changedFiles.length === 0 || commands.length === 0 || options.signal.aborted) {
|
|
2886
2938
|
summary2.skipped = true;
|
|
@@ -2896,7 +2948,10 @@ async function runAutomaticDiagnostics(options) {
|
|
|
2896
2948
|
});
|
|
2897
2949
|
return summary2;
|
|
2898
2950
|
}
|
|
2899
|
-
|
|
2951
|
+
const items = checks.map((check) => check.runs ? `${check.command} \u2192 ${check.runs}` : check.command);
|
|
2952
|
+
const grantName = `${diagnosticsTool.name}#${createHash3("sha256").update(items.join("\n")).digest("hex").slice(0, 16)}`;
|
|
2953
|
+
const granted = gate === "ask" && options.permissions.decide({ ...diagnosticsTool, name: grantName }) === "allow";
|
|
2954
|
+
if (gate === "ask" && !granted) {
|
|
2900
2955
|
const decision = await options.hooks.requestPermission({
|
|
2901
2956
|
toolName: diagnosticsTool.name,
|
|
2902
2957
|
summary: `run ${commands.length} automatic project ${commands.length === 1 ? "check" : "checks"}`,
|
|
@@ -2904,7 +2959,7 @@ async function runAutomaticDiagnostics(options) {
|
|
|
2904
2959
|
display: {
|
|
2905
2960
|
kind: "list",
|
|
2906
2961
|
title: `After changes to ${options.changedFiles.length} ${options.changedFiles.length === 1 ? "file" : "files"}`,
|
|
2907
|
-
items
|
|
2962
|
+
items
|
|
2908
2963
|
},
|
|
2909
2964
|
allowAlways: true
|
|
2910
2965
|
});
|
|
@@ -2921,7 +2976,7 @@ async function runAutomaticDiagnostics(options) {
|
|
|
2921
2976
|
});
|
|
2922
2977
|
return summary2;
|
|
2923
2978
|
}
|
|
2924
|
-
if (decision === "always") options.permissions.grantForSession(
|
|
2979
|
+
if (decision === "always") options.permissions.grantForSession(grantName);
|
|
2925
2980
|
}
|
|
2926
2981
|
const ctx = {
|
|
2927
2982
|
cwd: options.cwd,
|
|
@@ -2998,10 +3053,51 @@ async function isFile2(file) {
|
|
|
2998
3053
|
|
|
2999
3054
|
// src/core/attachments.ts
|
|
3000
3055
|
import { execFile } from "child_process";
|
|
3001
|
-
import { lstat as lstat2, mkdtemp, readdir as readdir3, readFile as readFile7, rm as
|
|
3056
|
+
import { lstat as lstat2, mkdtemp, readdir as readdir3, readFile as readFile7, rm as rm5, stat as stat5, writeFile as writeFile6 } from "fs/promises";
|
|
3002
3057
|
import os2 from "os";
|
|
3003
|
-
import
|
|
3058
|
+
import path9 from "path";
|
|
3004
3059
|
import { fileURLToPath } from "url";
|
|
3060
|
+
|
|
3061
|
+
// src/tools/sensitive.ts
|
|
3062
|
+
import path8 from "path";
|
|
3063
|
+
var sensitiveNames = /* @__PURE__ */ new Set([
|
|
3064
|
+
".npmrc",
|
|
3065
|
+
".pypirc",
|
|
3066
|
+
".git-credentials",
|
|
3067
|
+
".netrc",
|
|
3068
|
+
".pgpass",
|
|
3069
|
+
".my.cnf",
|
|
3070
|
+
".docker/config.json",
|
|
3071
|
+
"auth.json",
|
|
3072
|
+
"credentials",
|
|
3073
|
+
"credentials.json",
|
|
3074
|
+
"id_rsa",
|
|
3075
|
+
"id_ed25519",
|
|
3076
|
+
"id_dsa",
|
|
3077
|
+
"id_ecdsa",
|
|
3078
|
+
"kubeconfig",
|
|
3079
|
+
"known_hosts",
|
|
3080
|
+
"secret.json",
|
|
3081
|
+
"secrets.json",
|
|
3082
|
+
"tokens.json",
|
|
3083
|
+
"service-account.json"
|
|
3084
|
+
]);
|
|
3085
|
+
var sensitiveExtensions = /* @__PURE__ */ new Set([".pem", ".key", ".p12", ".pfx", ".jks", ".keystore", ".p8", ".der"]);
|
|
3086
|
+
function isSensitivePath(value) {
|
|
3087
|
+
if (typeof value !== "string") return false;
|
|
3088
|
+
const normalized = value.replaceAll("\\", "/").toLowerCase();
|
|
3089
|
+
const name = path8.posix.basename(normalized);
|
|
3090
|
+
if (name === ".env" || name.startsWith(".env.")) return true;
|
|
3091
|
+
if (sensitiveNames.has(name) || sensitiveExtensions.has(path8.posix.extname(name))) return true;
|
|
3092
|
+
return normalized.split("/").some((part) => part === ".ssh" || part === ".aws");
|
|
3093
|
+
}
|
|
3094
|
+
function mentionsSensitivePattern(value) {
|
|
3095
|
+
if (typeof value !== "string") return false;
|
|
3096
|
+
const lower = value.toLowerCase();
|
|
3097
|
+
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);
|
|
3098
|
+
}
|
|
3099
|
+
|
|
3100
|
+
// src/core/attachments.ts
|
|
3005
3101
|
var MAX_IMAGE_BYTES = 10 * 1024 * 1024;
|
|
3006
3102
|
var MAX_TEXT_BYTES = 256 * 1024;
|
|
3007
3103
|
var MAX_CLIPBOARD_OUTPUT_BYTES = MAX_IMAGE_BYTES + 64 * 1024;
|
|
@@ -3031,7 +3127,7 @@ async function loadAttachment(cwd, requestedPath) {
|
|
|
3031
3127
|
const resolved = resolveAttachmentPath(cwd, requestedPath);
|
|
3032
3128
|
const linkInfo = await lstat2(resolved).catch(() => null);
|
|
3033
3129
|
if (linkInfo?.isSymbolicLink()) {
|
|
3034
|
-
throw new Error(`Cannot attach symbolic links: ${
|
|
3130
|
+
throw new Error(`Cannot attach symbolic links: ${path9.basename(resolved)}`);
|
|
3035
3131
|
}
|
|
3036
3132
|
const info = await stat5(resolved).catch((error) => {
|
|
3037
3133
|
if (error.code === "ENOENT") throw new Error(`Attachment not found: ${resolved}`);
|
|
@@ -3040,7 +3136,7 @@ async function loadAttachment(cwd, requestedPath) {
|
|
|
3040
3136
|
if (info.isDirectory()) {
|
|
3041
3137
|
const attachment = await loadFirstImageFromDirectory(resolved);
|
|
3042
3138
|
if (attachment) return attachment;
|
|
3043
|
-
throw new Error(`No supported images found in directory: ${
|
|
3139
|
+
throw new Error(`No supported images found in directory: ${path9.basename(resolved)}`);
|
|
3044
3140
|
}
|
|
3045
3141
|
if (!info.isFile()) throw new Error(`Attachment is not a regular file: ${resolved}`);
|
|
3046
3142
|
return loadResolvedAttachment(resolved, info.size);
|
|
@@ -3050,7 +3146,7 @@ async function loadAutomaticAttachment(cwd, requestedPath) {
|
|
|
3050
3146
|
const resolved = resolveAttachmentPath(cwd, requestedPath);
|
|
3051
3147
|
if (isSensitiveAutomaticPath(resolved)) {
|
|
3052
3148
|
throw new Error(
|
|
3053
|
-
`For safety, sensitive-looking files must be attached explicitly with /attach: ${
|
|
3149
|
+
`For safety, sensitive-looking files must be attached explicitly with /attach: ${path9.basename(resolved)}`
|
|
3054
3150
|
);
|
|
3055
3151
|
}
|
|
3056
3152
|
const linkInfo = await lstat2(resolved).catch(() => null);
|
|
@@ -3063,6 +3159,33 @@ async function loadAutomaticAttachment(cwd, requestedPath) {
|
|
|
3063
3159
|
if (!info?.isFile()) return null;
|
|
3064
3160
|
return loadResolvedAttachment(resolved, info.size);
|
|
3065
3161
|
}
|
|
3162
|
+
async function loadMentionAttachment(cwd, requestedPath) {
|
|
3163
|
+
const safe = resolveInside(cwd, normalizeInputPath(requestedPath.trim()));
|
|
3164
|
+
if (!safe.ok || safe.relative === "") return null;
|
|
3165
|
+
if (isSensitivePath(safe.relative) || isSensitiveAutomaticPath(safe.path)) {
|
|
3166
|
+
throw new Error(
|
|
3167
|
+
`For safety, sensitive-looking files must be attached explicitly with /attach: ${path9.basename(safe.path)}`
|
|
3168
|
+
);
|
|
3169
|
+
}
|
|
3170
|
+
const info = await stat5(safe.path).catch(() => null);
|
|
3171
|
+
if (!info?.isFile()) return null;
|
|
3172
|
+
return loadResolvedAttachment(safe.path, info.size);
|
|
3173
|
+
}
|
|
3174
|
+
function withoutImages(messages) {
|
|
3175
|
+
let removed = 0;
|
|
3176
|
+
const cleaned = messages.map((message) => {
|
|
3177
|
+
if (!message.content.some((block) => block.type === "image")) return message;
|
|
3178
|
+
return {
|
|
3179
|
+
...message,
|
|
3180
|
+
content: message.content.map((block) => {
|
|
3181
|
+
if (block.type !== "image") return block;
|
|
3182
|
+
removed += 1;
|
|
3183
|
+
return { type: "text", text: `[image ${block.name} removed: the model could not accept it]` };
|
|
3184
|
+
})
|
|
3185
|
+
};
|
|
3186
|
+
});
|
|
3187
|
+
return { messages: cleaned, removed };
|
|
3188
|
+
}
|
|
3066
3189
|
var MAX_PATH_CHARS = 2048;
|
|
3067
3190
|
function looksLikeAttachmentPath(value) {
|
|
3068
3191
|
const trimmed = value.trim();
|
|
@@ -3070,14 +3193,14 @@ function looksLikeAttachmentPath(value) {
|
|
|
3070
3193
|
if (trimmed.length > MAX_PATH_CHARS) return false;
|
|
3071
3194
|
const candidate = normalizeInputPath(trimmed);
|
|
3072
3195
|
if (/^file:\/\//i.test(candidate)) return true;
|
|
3073
|
-
if (
|
|
3196
|
+
if (path9.isAbsolute(candidate)) return true;
|
|
3074
3197
|
if (/^(?:~|\.{1,2})[\\/]/.test(candidate)) return true;
|
|
3075
3198
|
if (candidate.includes("/") || candidate.includes("\\")) {
|
|
3076
3199
|
if (trimmed.length > 200) return false;
|
|
3077
3200
|
return true;
|
|
3078
3201
|
}
|
|
3079
|
-
const basename3 =
|
|
3080
|
-
return
|
|
3202
|
+
const basename3 = path9.basename(candidate).toLowerCase();
|
|
3203
|
+
return path9.extname(basename3) !== "" || AUTO_PATH_NAMES.has(basename3);
|
|
3081
3204
|
}
|
|
3082
3205
|
async function loadClipboardImage(platform = process.platform, runner = runClipboardCommand) {
|
|
3083
3206
|
const commands = clipboardCommands(platform);
|
|
@@ -3110,7 +3233,7 @@ async function loadClipboardImage(platform = process.platform, runner = runClipb
|
|
|
3110
3233
|
throw new Error(`No supported image found in the clipboard. ${hint}`);
|
|
3111
3234
|
}
|
|
3112
3235
|
async function loadResolvedAttachment(resolved, size) {
|
|
3113
|
-
const name = safeName(
|
|
3236
|
+
const name = safeName(path9.basename(resolved));
|
|
3114
3237
|
const imageLimitCandidate = size <= MAX_IMAGE_BYTES;
|
|
3115
3238
|
if (!imageLimitCandidate) {
|
|
3116
3239
|
throw new Error(`Attachment is larger than ${formatBytes(MAX_IMAGE_BYTES)}: ${name}`);
|
|
@@ -3128,12 +3251,11 @@ async function loadResolvedAttachment(resolved, size) {
|
|
|
3128
3251
|
if (data.length > MAX_TEXT_BYTES) {
|
|
3129
3252
|
throw new Error(`Text attachment is larger than ${formatBytes(MAX_TEXT_BYTES)}: ${name}`);
|
|
3130
3253
|
}
|
|
3131
|
-
|
|
3132
|
-
|
|
3133
|
-
|
|
3134
|
-
|
|
3135
|
-
|
|
3136
|
-
throw new Error(`Attachment is neither a supported image nor UTF-8 text: ${name}`);
|
|
3254
|
+
const text = decodeText(data);
|
|
3255
|
+
if (text === null) {
|
|
3256
|
+
throw new Error(
|
|
3257
|
+
data.includes(0) ? `Unsupported binary attachment: ${name}` : `Attachment is neither a supported image nor UTF-8 text: ${name}`
|
|
3258
|
+
);
|
|
3137
3259
|
}
|
|
3138
3260
|
return {
|
|
3139
3261
|
path: resolved,
|
|
@@ -3142,13 +3264,13 @@ async function loadResolvedAttachment(resolved, size) {
|
|
|
3142
3264
|
};
|
|
3143
3265
|
}
|
|
3144
3266
|
async function loadFirstImageFromDirectory(dirPath) {
|
|
3145
|
-
const IMAGE_EXTENSIONS = /* @__PURE__ */ new Set([".png", ".jpg", ".jpeg", ".gif", ".webp"
|
|
3267
|
+
const IMAGE_EXTENSIONS = /* @__PURE__ */ new Set([".png", ".jpg", ".jpeg", ".gif", ".webp"]);
|
|
3146
3268
|
const entries = await readdir3(dirPath, { withFileTypes: true }).catch(() => null);
|
|
3147
3269
|
if (!entries) return null;
|
|
3148
|
-
const sorted = entries.filter((entry) => entry.isFile() && IMAGE_EXTENSIONS.has(
|
|
3270
|
+
const sorted = entries.filter((entry) => entry.isFile() && IMAGE_EXTENSIONS.has(path9.extname(entry.name).toLowerCase())).sort((a, b) => a.name.localeCompare(b.name));
|
|
3149
3271
|
if (sorted.length === 0) return null;
|
|
3150
3272
|
const first2 = sorted[0];
|
|
3151
|
-
const filePath =
|
|
3273
|
+
const filePath = path9.join(dirPath, first2.name);
|
|
3152
3274
|
const info = await stat5(filePath).catch(() => null);
|
|
3153
3275
|
if (!info?.isFile()) return null;
|
|
3154
3276
|
return loadResolvedAttachment(filePath, info.size);
|
|
@@ -3158,23 +3280,78 @@ function resolveAttachmentPath(cwd, requestedPath) {
|
|
|
3158
3280
|
if (!cleaned) throw new Error("Give a file path: /attach <path>");
|
|
3159
3281
|
if (/^file:\/\//i.test(cleaned)) {
|
|
3160
3282
|
try {
|
|
3161
|
-
return
|
|
3283
|
+
return path9.normalize(fileURLToPath(cleaned));
|
|
3162
3284
|
} catch {
|
|
3163
3285
|
throw new Error(`Invalid local file URL: ${cleaned}`);
|
|
3164
3286
|
}
|
|
3165
3287
|
}
|
|
3166
|
-
const expanded = cleaned === "~" ? os2.homedir() : cleaned.startsWith("~/") ?
|
|
3167
|
-
return
|
|
3288
|
+
const expanded = cleaned === "~" ? os2.homedir() : cleaned.startsWith("~/") ? path9.join(os2.homedir(), cleaned.slice(2)) : cleaned;
|
|
3289
|
+
return path9.isAbsolute(expanded) ? path9.normalize(expanded) : path9.resolve(cwd, expanded);
|
|
3168
3290
|
}
|
|
3169
3291
|
function isSensitiveAutomaticPath(file) {
|
|
3170
|
-
const basename3 =
|
|
3171
|
-
return
|
|
3292
|
+
const basename3 = path9.basename(file).toLowerCase();
|
|
3293
|
+
return (
|
|
3294
|
+
// Also covers ~/.ssh/config, ~/.aws/credentials, .git-credentials and the like.
|
|
3295
|
+
isSensitivePath(file) || basename3.startsWith(".env.") || SENSITIVE_AUTO_NAMES.has(basename3) || SENSITIVE_AUTO_EXTENSIONS.has(path9.extname(basename3))
|
|
3296
|
+
);
|
|
3172
3297
|
}
|
|
3173
3298
|
function attachmentLabel(block) {
|
|
3174
3299
|
if (block.type === "image") return `image: ${block.name}`;
|
|
3175
3300
|
if (block.type === "file") return `file: ${block.name}`;
|
|
3176
3301
|
return null;
|
|
3177
3302
|
}
|
|
3303
|
+
function decodeText(data) {
|
|
3304
|
+
try {
|
|
3305
|
+
if (data[0] === 255 && data[1] === 254) return new TextDecoder("utf-16le", { fatal: true }).decode(data);
|
|
3306
|
+
if (data[0] === 254 && data[1] === 255) return new TextDecoder("utf-16be", { fatal: true }).decode(data);
|
|
3307
|
+
if (data.includes(0)) return null;
|
|
3308
|
+
return new TextDecoder("utf-8", { fatal: true }).decode(data);
|
|
3309
|
+
} catch {
|
|
3310
|
+
return null;
|
|
3311
|
+
}
|
|
3312
|
+
}
|
|
3313
|
+
function imageSize(data, mediaType) {
|
|
3314
|
+
try {
|
|
3315
|
+
if (mediaType === "image/png" && data.length >= 24) {
|
|
3316
|
+
return { width: data.readUInt32BE(16), height: data.readUInt32BE(20) };
|
|
3317
|
+
}
|
|
3318
|
+
if (mediaType === "image/gif" && data.length >= 10) {
|
|
3319
|
+
return { width: data.readUInt16LE(6), height: data.readUInt16LE(8) };
|
|
3320
|
+
}
|
|
3321
|
+
if (mediaType === "image/webp" && data.length >= 30) {
|
|
3322
|
+
const chunk = data.subarray(12, 16).toString("ascii");
|
|
3323
|
+
if (chunk === "VP8X") return { width: data.readUIntLE(24, 3) + 1, height: data.readUIntLE(27, 3) + 1 };
|
|
3324
|
+
if (chunk === "VP8L") {
|
|
3325
|
+
const bits = data.readUInt32LE(21);
|
|
3326
|
+
return { width: (bits & 16383) + 1, height: (bits >> 14 & 16383) + 1 };
|
|
3327
|
+
}
|
|
3328
|
+
if (chunk === "VP8 ") return { width: data.readUInt16LE(26) & 16383, height: data.readUInt16LE(28) & 16383 };
|
|
3329
|
+
return null;
|
|
3330
|
+
}
|
|
3331
|
+
if (mediaType === "image/jpeg") {
|
|
3332
|
+
let offset = 2;
|
|
3333
|
+
while (offset + 9 < data.length) {
|
|
3334
|
+
if (data[offset] !== 255) return null;
|
|
3335
|
+
const marker = data[offset + 1];
|
|
3336
|
+
if (marker === 255) {
|
|
3337
|
+
offset += 1;
|
|
3338
|
+
continue;
|
|
3339
|
+
}
|
|
3340
|
+
if (marker >= 192 && marker <= 207 && marker !== 196 && marker !== 200 && marker !== 204) {
|
|
3341
|
+
return { width: data.readUInt16BE(offset + 7), height: data.readUInt16BE(offset + 5) };
|
|
3342
|
+
}
|
|
3343
|
+
if (marker === 216 || marker === 1 || marker >= 208 && marker <= 215) {
|
|
3344
|
+
offset += 2;
|
|
3345
|
+
continue;
|
|
3346
|
+
}
|
|
3347
|
+
offset += 2 + data.readUInt16BE(offset + 2);
|
|
3348
|
+
}
|
|
3349
|
+
}
|
|
3350
|
+
} catch {
|
|
3351
|
+
return null;
|
|
3352
|
+
}
|
|
3353
|
+
return null;
|
|
3354
|
+
}
|
|
3178
3355
|
function detectImage(data) {
|
|
3179
3356
|
if (data.length >= 8 && data.subarray(0, 8).equals(Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]))) {
|
|
3180
3357
|
return "image/png";
|
|
@@ -3301,7 +3478,15 @@ var WINDOWS_CLIPBOARD_SCRIPT = String.raw`
|
|
|
3301
3478
|
Add-Type -AssemblyName System.Windows.Forms
|
|
3302
3479
|
Add-Type -AssemblyName System.Drawing
|
|
3303
3480
|
$image = [System.Windows.Forms.Clipboard]::GetImage()
|
|
3304
|
-
if ($null -eq $image) {
|
|
3481
|
+
if ($null -eq $image) {
|
|
3482
|
+
# An image file copied in Explorer arrives as a file list, not a bitmap.
|
|
3483
|
+
$files = [System.Windows.Forms.Clipboard]::GetFileDropList()
|
|
3484
|
+
$file = $files | Where-Object { $_ -match '\.(png|jpe?g|gif|webp)$' } | Select-Object -First 1
|
|
3485
|
+
if ($null -eq $file) { exit 2 }
|
|
3486
|
+
$bytes = [System.IO.File]::ReadAllBytes($file)
|
|
3487
|
+
[Console]::OpenStandardOutput().Write($bytes, 0, $bytes.Length)
|
|
3488
|
+
exit 0
|
|
3489
|
+
}
|
|
3305
3490
|
$stream = New-Object System.IO.MemoryStream
|
|
3306
3491
|
try {
|
|
3307
3492
|
$image.Save($stream, [System.Drawing.Imaging.ImageFormat]::Png)
|
|
@@ -3313,13 +3498,16 @@ try {
|
|
|
3313
3498
|
}
|
|
3314
3499
|
`;
|
|
3315
3500
|
var MAX_SENT_IMAGE_BYTES = 37e5;
|
|
3501
|
+
var MAX_IMAGE_EDGE = 8e3;
|
|
3316
3502
|
var SHRINK_LONG_EDGE = 2e3;
|
|
3317
3503
|
async function fitImage(data, mediaType, platform = process.platform, shrink = shrinkWithSystemTools) {
|
|
3318
|
-
|
|
3319
|
-
const
|
|
3504
|
+
const size = imageSize(data, mediaType);
|
|
3505
|
+
const tooTall = size !== null && Math.max(size.width, size.height) > MAX_IMAGE_EDGE;
|
|
3506
|
+
if (data.length <= MAX_SENT_IMAGE_BYTES && !tooTall) return { data, mediaType };
|
|
3507
|
+
const dir = await mkdtemp(path9.join(os2.tmpdir(), "kitcode-image-"));
|
|
3320
3508
|
try {
|
|
3321
|
-
const input =
|
|
3322
|
-
const output =
|
|
3509
|
+
const input = path9.join(dir, `in${extensionFor(mediaType)}`);
|
|
3510
|
+
const output = path9.join(dir, "out.jpg");
|
|
3323
3511
|
await writeFile6(input, data);
|
|
3324
3512
|
try {
|
|
3325
3513
|
await shrink(input, output, platform);
|
|
@@ -3334,7 +3522,7 @@ async function fitImage(data, mediaType, platform = process.platform, shrink = s
|
|
|
3334
3522
|
}
|
|
3335
3523
|
return { data: shrunk, mediaType: "image/jpeg" };
|
|
3336
3524
|
} finally {
|
|
3337
|
-
await
|
|
3525
|
+
await rm5(dir, { recursive: true, force: true }).catch(() => void 0);
|
|
3338
3526
|
}
|
|
3339
3527
|
}
|
|
3340
3528
|
async function shrinkWithSystemTools(input, output, platform) {
|
|
@@ -3350,7 +3538,7 @@ async function shrinkWithSystemTools(input, output, platform) {
|
|
|
3350
3538
|
await runQuiet("sips", ["-Z", String(SHRINK_LONG_EDGE), "-s", "format", "jpeg", "-s", "formatOptions", "85", input, "--out", output]);
|
|
3351
3539
|
return;
|
|
3352
3540
|
}
|
|
3353
|
-
const args = [input, "-resize", `${SHRINK_LONG_EDGE}x${SHRINK_LONG_EDGE}>`, "-background", "white", "-flatten", "-quality", "85", output];
|
|
3541
|
+
const args = [input, "-auto-orient", "-resize", `${SHRINK_LONG_EDGE}x${SHRINK_LONG_EDGE}>`, "-background", "white", "-flatten", "-quality", "85", output];
|
|
3354
3542
|
try {
|
|
3355
3543
|
await runQuiet("magick", args);
|
|
3356
3544
|
} catch {
|
|
@@ -3372,6 +3560,12 @@ var WINDOWS_SHRINK_SCRIPT = String.raw`
|
|
|
3372
3560
|
Add-Type -AssemblyName System.Drawing
|
|
3373
3561
|
$source = [System.Drawing.Image]::FromFile($env:KITCODE_IMAGE_IN)
|
|
3374
3562
|
try {
|
|
3563
|
+
# Apply the EXIF orientation: the re-encoded JPEG drops the flag that kept a
|
|
3564
|
+
# phone photo upright.
|
|
3565
|
+
if ($source.PropertyIdList -contains 0x0112) {
|
|
3566
|
+
$flip = @{ 2 = 'RotateNoneFlipX'; 3 = 'Rotate180FlipNone'; 4 = 'Rotate180FlipX'; 5 = 'Rotate90FlipX'; 6 = 'Rotate90FlipNone'; 7 = 'Rotate270FlipX'; 8 = 'Rotate270FlipNone' }[[int]$source.GetPropertyItem(0x0112).Value[0]]
|
|
3567
|
+
if ($flip) { $source.RotateFlip([System.Drawing.RotateFlipType]$flip) }
|
|
3568
|
+
}
|
|
3375
3569
|
$edge = [double]$env:KITCODE_IMAGE_EDGE
|
|
3376
3570
|
$scale = [Math]::Min(1.0, $edge / [Math]::Max($source.Width, $source.Height))
|
|
3377
3571
|
$width = [Math]::Max(1, [int]($source.Width * $scale))
|
|
@@ -3392,7 +3586,7 @@ try {
|
|
|
3392
3586
|
}
|
|
3393
3587
|
`;
|
|
3394
3588
|
function textMime(file) {
|
|
3395
|
-
const extension =
|
|
3589
|
+
const extension = path9.extname(file).toLowerCase();
|
|
3396
3590
|
const known = {
|
|
3397
3591
|
".json": "application/json",
|
|
3398
3592
|
".md": "text/markdown",
|
|
@@ -4120,11 +4314,14 @@ import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"
|
|
|
4120
4314
|
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
4121
4315
|
|
|
4122
4316
|
// src/mcp/bridge.ts
|
|
4317
|
+
import { createHash as createHash4 } from "crypto";
|
|
4123
4318
|
var MAX_RESULT_CHARS = 2e5;
|
|
4319
|
+
var TOOL_NAME = /^[a-zA-Z0-9_-]{1,64}$/;
|
|
4320
|
+
var MAX_TOOL_NAME = 64;
|
|
4124
4321
|
function bridgeMcpTool(server, descriptor, call) {
|
|
4125
4322
|
const tool = descriptor.name;
|
|
4126
4323
|
return {
|
|
4127
|
-
name:
|
|
4324
|
+
name: mcpToolName(server, tool),
|
|
4128
4325
|
description: descriptor.description ?? descriptor.title ?? tool,
|
|
4129
4326
|
inputSchema: inputSchemaOf(descriptor),
|
|
4130
4327
|
defaultPermission: "ask",
|
|
@@ -4133,6 +4330,13 @@ function bridgeMcpTool(server, descriptor, call) {
|
|
|
4133
4330
|
execute: (input, ctx) => invoke(call, tool, input, ctx.signal)
|
|
4134
4331
|
};
|
|
4135
4332
|
}
|
|
4333
|
+
function mcpToolName(server, tool) {
|
|
4334
|
+
const raw = `mcp__${server}__${tool}`;
|
|
4335
|
+
if (TOOL_NAME.test(raw)) return raw;
|
|
4336
|
+
const hash = createHash4("sha256").update(raw).digest("hex").slice(0, 8);
|
|
4337
|
+
const cleaned = raw.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
4338
|
+
return `${cleaned.slice(0, MAX_TOOL_NAME - hash.length - 1)}_${hash}`;
|
|
4339
|
+
}
|
|
4136
4340
|
function printableInput(input) {
|
|
4137
4341
|
try {
|
|
4138
4342
|
return JSON.stringify(input, null, 2);
|
|
@@ -4169,10 +4373,15 @@ function renderPart(part) {
|
|
|
4169
4373
|
}
|
|
4170
4374
|
function inputSchemaOf(descriptor) {
|
|
4171
4375
|
const schema = descriptor.inputSchema;
|
|
4172
|
-
if (schema === null || typeof schema !== "object") {
|
|
4376
|
+
if (schema === null || typeof schema !== "object" || Array.isArray(schema)) {
|
|
4173
4377
|
return { type: "object", properties: {}, additionalProperties: false };
|
|
4174
4378
|
}
|
|
4175
|
-
|
|
4379
|
+
const record3 = schema;
|
|
4380
|
+
return {
|
|
4381
|
+
...record3,
|
|
4382
|
+
type: "object",
|
|
4383
|
+
properties: record3.properties && typeof record3.properties === "object" ? record3.properties : {}
|
|
4384
|
+
};
|
|
4176
4385
|
}
|
|
4177
4386
|
function argumentPreview(input) {
|
|
4178
4387
|
if (input === null || typeof input !== "object") return "";
|
|
@@ -4191,7 +4400,7 @@ function clip(value) {
|
|
|
4191
4400
|
// package.json
|
|
4192
4401
|
var package_default = {
|
|
4193
4402
|
name: "@kernelonpanic/kitcode",
|
|
4194
|
-
version: "1.4.
|
|
4403
|
+
version: "1.4.2",
|
|
4195
4404
|
description: "Terminal coding agent with a config you never have to write by hand",
|
|
4196
4405
|
type: "module",
|
|
4197
4406
|
license: "MIT",
|
|
@@ -4224,7 +4433,6 @@ var package_default = {
|
|
|
4224
4433
|
dependencies: {
|
|
4225
4434
|
"@anthropic-ai/sdk": "^0.115.0",
|
|
4226
4435
|
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
4227
|
-
build: "^0.1.4",
|
|
4228
4436
|
chalk: "^6.0.0",
|
|
4229
4437
|
commander: "^15.0.0",
|
|
4230
4438
|
"fast-glob": "^3.3.3",
|
|
@@ -4260,7 +4468,7 @@ var package_default = {
|
|
|
4260
4468
|
|
|
4261
4469
|
// src/version.ts
|
|
4262
4470
|
var KITCODE_VERSION = package_default.version;
|
|
4263
|
-
var KITCODE_COMMIT = true ? "
|
|
4471
|
+
var KITCODE_COMMIT = true ? "6b1ec0a742c44892447e71f477c068d8ba514c6a" : "development";
|
|
4264
4472
|
|
|
4265
4473
|
// src/mcp/client.ts
|
|
4266
4474
|
var clientInfo = { name: "kitcode", version: KITCODE_VERSION };
|
|
@@ -4321,6 +4529,7 @@ var blockedEnvKeys = /* @__PURE__ */ new Set([
|
|
|
4321
4529
|
function createMcpManager(servers) {
|
|
4322
4530
|
const serverStates = /* @__PURE__ */ new Map();
|
|
4323
4531
|
const sessions = /* @__PURE__ */ new Map();
|
|
4532
|
+
const changeListeners = /* @__PURE__ */ new Set();
|
|
4324
4533
|
for (const [name, config] of Object.entries(servers)) {
|
|
4325
4534
|
serverStates.set(name, {
|
|
4326
4535
|
name,
|
|
@@ -4342,14 +4551,15 @@ function createMcpManager(servers) {
|
|
|
4342
4551
|
serverStates.set(name, { name, status: "connecting", toolCount: 0 });
|
|
4343
4552
|
const client = new Client(clientInfo);
|
|
4344
4553
|
client.onclose = () => {
|
|
4345
|
-
if (sessions.
|
|
4346
|
-
|
|
4347
|
-
|
|
4348
|
-
|
|
4349
|
-
|
|
4350
|
-
|
|
4351
|
-
|
|
4352
|
-
}
|
|
4554
|
+
if (sessions.get(name)?.client !== client) return;
|
|
4555
|
+
sessions.delete(name);
|
|
4556
|
+
serverStates.set(name, {
|
|
4557
|
+
name,
|
|
4558
|
+
status: "error",
|
|
4559
|
+
toolCount: 0,
|
|
4560
|
+
error: "connection closed"
|
|
4561
|
+
});
|
|
4562
|
+
for (const listener of changeListeners) listener();
|
|
4353
4563
|
};
|
|
4354
4564
|
const transport = createTransport(config);
|
|
4355
4565
|
const stderrTail = captureStderr(transport);
|
|
@@ -4415,7 +4625,12 @@ function createMcpManager(servers) {
|
|
|
4415
4625
|
tools() {
|
|
4416
4626
|
return [...sessions.values()].flatMap((session) => session.tools);
|
|
4417
4627
|
},
|
|
4418
|
-
callTool
|
|
4628
|
+
callTool,
|
|
4629
|
+
/** Called when a server drops on its own, outside connect/disconnect. */
|
|
4630
|
+
onChange(listener) {
|
|
4631
|
+
changeListeners.add(listener);
|
|
4632
|
+
return () => changeListeners.delete(listener);
|
|
4633
|
+
}
|
|
4419
4634
|
};
|
|
4420
4635
|
}
|
|
4421
4636
|
async function handshake(client, transport) {
|
|
@@ -4551,8 +4766,8 @@ function balanceEndpoints(baseUrl) {
|
|
|
4551
4766
|
endpoint(url.origin, "/v1/balance", genericBalance)
|
|
4552
4767
|
];
|
|
4553
4768
|
}
|
|
4554
|
-
function endpoint(origin,
|
|
4555
|
-
return { url: `${origin}${
|
|
4769
|
+
function endpoint(origin, path16, parse2) {
|
|
4770
|
+
return { url: `${origin}${path16}`, parse: parse2 };
|
|
4556
4771
|
}
|
|
4557
4772
|
function deepSeekBalance(body) {
|
|
4558
4773
|
const infos = record2(body)?.["balance_infos"];
|
|
@@ -4646,7 +4861,7 @@ async function limitedResponseText(response, limit) {
|
|
|
4646
4861
|
|
|
4647
4862
|
// src/providers/catalog.ts
|
|
4648
4863
|
import { chmod as chmod4, readFile as readFile8, writeFile as writeFile7 } from "fs/promises";
|
|
4649
|
-
import
|
|
4864
|
+
import path10 from "path";
|
|
4650
4865
|
var MAX_AGE_MS = 24 * 60 * 60 * 1e3;
|
|
4651
4866
|
var CACHE_VERSION = 3;
|
|
4652
4867
|
async function loadModels(provider, refresh = false) {
|
|
@@ -4671,7 +4886,7 @@ function refreshModels(provider) {
|
|
|
4671
4886
|
return loadModels(provider, true);
|
|
4672
4887
|
}
|
|
4673
4888
|
function cacheFile(providerId) {
|
|
4674
|
-
return
|
|
4889
|
+
return path10.join(cacheDir, "models", `${providerId.replace(/[^\w.-]/g, "_")}.json`);
|
|
4675
4890
|
}
|
|
4676
4891
|
async function readCache(file) {
|
|
4677
4892
|
try {
|
|
@@ -4706,7 +4921,7 @@ function validModel(value) {
|
|
|
4706
4921
|
async function writeCache(file, models) {
|
|
4707
4922
|
const payload = { version: CACHE_VERSION, fetchedAt: Date.now(), models };
|
|
4708
4923
|
try {
|
|
4709
|
-
await ensureDir(
|
|
4924
|
+
await ensureDir(path10.dirname(file));
|
|
4710
4925
|
await writeFile7(file, JSON.stringify(payload), { encoding: "utf8", mode: 384 });
|
|
4711
4926
|
await chmod4(file, 384);
|
|
4712
4927
|
} catch {
|
|
@@ -5212,7 +5427,7 @@ function toContentBlocks2(providerId, thinking, text, calls) {
|
|
|
5212
5427
|
type: "tool_use",
|
|
5213
5428
|
id: call.id || tmpId(position),
|
|
5214
5429
|
name: call.name,
|
|
5215
|
-
input: parseArguments(
|
|
5430
|
+
input: parseArguments(call.args)
|
|
5216
5431
|
});
|
|
5217
5432
|
}
|
|
5218
5433
|
return content;
|
|
@@ -5223,21 +5438,16 @@ function toTextContentBlocks(thinking, text) {
|
|
|
5223
5438
|
if (text) content.push({ type: "text", text });
|
|
5224
5439
|
return content;
|
|
5225
5440
|
}
|
|
5226
|
-
function parseArguments(
|
|
5441
|
+
function parseArguments(args) {
|
|
5442
|
+
if (args.trim() === "") return {};
|
|
5227
5443
|
let parsed;
|
|
5228
5444
|
try {
|
|
5229
5445
|
parsed = JSON.parse(args);
|
|
5230
5446
|
} catch {
|
|
5231
|
-
|
|
5232
|
-
`Tool "${toolName || "unknown"}" returned malformed JSON arguments; the tool was not run.`,
|
|
5233
|
-
providerId
|
|
5234
|
-
);
|
|
5447
|
+
parsed = void 0;
|
|
5235
5448
|
}
|
|
5236
5449
|
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
|
-
);
|
|
5450
|
+
return { [INVALID_ARGUMENTS_KEY]: args.slice(0, 300) };
|
|
5241
5451
|
}
|
|
5242
5452
|
return parsed;
|
|
5243
5453
|
}
|
|
@@ -5382,6 +5592,7 @@ function createPermissionEngine(configPermissions) {
|
|
|
5382
5592
|
const configured = resolve3(tool, requested);
|
|
5383
5593
|
if (configured === "deny") return "deny";
|
|
5384
5594
|
if (current === "plan" && tool.readOnly !== true) return "deny";
|
|
5595
|
+
if (requested === "ask") return bypassEnabled ? "allow" : "ask";
|
|
5385
5596
|
if (configured === "allow") return "allow";
|
|
5386
5597
|
if (bypassEnabled) return "allow";
|
|
5387
5598
|
if (current === "accept" && isFileEdit(tool)) return "allow";
|
|
@@ -5494,9 +5705,24 @@ function done(result) {
|
|
|
5494
5705
|
// src/tools/edit.ts
|
|
5495
5706
|
import { readFile as readFile9, stat as stat6 } from "fs/promises";
|
|
5496
5707
|
|
|
5708
|
+
// src/tools/protected.ts
|
|
5709
|
+
import path11 from "path";
|
|
5710
|
+
function protectedWritePermission(input, ctx) {
|
|
5711
|
+
const target = input?.path;
|
|
5712
|
+
if (typeof target !== "string") return void 0;
|
|
5713
|
+
const relative2 = ctx ? resolveInside(ctx.cwd, target) : void 0;
|
|
5714
|
+
const candidate = relative2?.ok ? relative2.relative : target;
|
|
5715
|
+
return isProtectedPath(candidate) ? "ask" : void 0;
|
|
5716
|
+
}
|
|
5717
|
+
function isProtectedPath(value) {
|
|
5718
|
+
const parts = path11.normalize(value).replaceAll("\\", "/").toLowerCase().split("/").filter(Boolean);
|
|
5719
|
+
const name = parts.at(-1) ?? "";
|
|
5720
|
+
return name === "kitcode.json" || name === ".envrc" || parts.includes(".kitcode") || parts.includes(".git");
|
|
5721
|
+
}
|
|
5722
|
+
|
|
5497
5723
|
// src/tools/safe-write.ts
|
|
5498
5724
|
import { randomUUID as randomUUID3 } from "crypto";
|
|
5499
|
-
import { chmod as chmod5, lstat as lstat3, open, rename as rename5, rm as
|
|
5725
|
+
import { chmod as chmod5, lstat as lstat3, open, rename as rename5, rm as rm6 } from "fs/promises";
|
|
5500
5726
|
import { basename as basename2, dirname as dirname2, join as join2 } from "path";
|
|
5501
5727
|
var UnsafeFileChangeError = class extends Error {
|
|
5502
5728
|
constructor(message) {
|
|
@@ -5559,7 +5785,7 @@ async function atomicWriteSafeFile(file, data, snapshot) {
|
|
|
5559
5785
|
await rename5(temp, file);
|
|
5560
5786
|
} catch (error) {
|
|
5561
5787
|
await handle?.close().catch(() => void 0);
|
|
5562
|
-
await
|
|
5788
|
+
await rm6(temp, { force: true }).catch(() => void 0);
|
|
5563
5789
|
throw error;
|
|
5564
5790
|
}
|
|
5565
5791
|
}
|
|
@@ -5650,60 +5876,61 @@ var editTool = {
|
|
|
5650
5876
|
additionalProperties: false
|
|
5651
5877
|
},
|
|
5652
5878
|
defaultPermission: "ask",
|
|
5879
|
+
permission: protectedWritePermission,
|
|
5653
5880
|
summarize(input) {
|
|
5654
5881
|
return `edit(${brief(input.path)})`;
|
|
5655
5882
|
},
|
|
5656
5883
|
async preview(input, ctx) {
|
|
5657
|
-
const { path:
|
|
5658
|
-
const safe = resolveInside(ctx.cwd,
|
|
5884
|
+
const { path: path16, oldString, newString, replaceAll = false } = input;
|
|
5885
|
+
const safe = resolveInside(ctx.cwd, path16);
|
|
5659
5886
|
if (!safe.ok) return { kind: "text", text: safe.reason };
|
|
5660
5887
|
const info = await stat6(safe.path).catch(() => null);
|
|
5661
5888
|
if (info && info.size > MAX_FILE_BYTES2) {
|
|
5662
|
-
return { kind: "text", text: `Cannot preview ${
|
|
5889
|
+
return { kind: "text", text: `Cannot preview ${path16}: the file is too large.` };
|
|
5663
5890
|
}
|
|
5664
5891
|
const beforeBuffer = await readFile9(safe.path).catch(() => null);
|
|
5665
|
-
if (!beforeBuffer) return { kind: "text", text: `Cannot read ${
|
|
5892
|
+
if (!beforeBuffer) return { kind: "text", text: `Cannot read ${path16}.` };
|
|
5666
5893
|
if (beforeBuffer.subarray(0, 8192).includes(0)) {
|
|
5667
|
-
return { kind: "text", text: `Cannot preview binary file ${
|
|
5894
|
+
return { kind: "text", text: `Cannot preview binary file ${path16}` };
|
|
5668
5895
|
}
|
|
5669
5896
|
const before = beforeBuffer.toString("utf8");
|
|
5670
5897
|
const after = replacement(before, oldString, newString, replaceAll);
|
|
5671
|
-
return after === null ? { kind: "text", text: `Cannot preview edit: oldString is not a valid match in ${
|
|
5898
|
+
return after === null ? { kind: "text", text: `Cannot preview edit: oldString is not a valid match in ${path16}.` } : { kind: "diff", path: path16, before, after };
|
|
5672
5899
|
},
|
|
5673
5900
|
async execute(input, ctx) {
|
|
5674
|
-
const { path:
|
|
5675
|
-
const safe = resolveInside(ctx.cwd,
|
|
5901
|
+
const { path: path16, oldString, newString, replaceAll = false } = input;
|
|
5902
|
+
const safe = resolveInside(ctx.cwd, path16);
|
|
5676
5903
|
if (!safe.ok) return { content: safe.reason, isError: true };
|
|
5677
5904
|
let snapshot;
|
|
5678
5905
|
try {
|
|
5679
5906
|
snapshot = await readSafeFileSnapshot(safe.path, MAX_FILE_BYTES2);
|
|
5680
5907
|
} catch (error) {
|
|
5681
|
-
return { content: `Cannot edit ${
|
|
5908
|
+
return { content: `Cannot edit ${path16}: ${error.message}.`, isError: true };
|
|
5682
5909
|
}
|
|
5683
|
-
if (!snapshot.exists) return { content: `Cannot read ${
|
|
5910
|
+
if (!snapshot.exists) return { content: `Cannot read ${path16}.`, isError: true };
|
|
5684
5911
|
const beforeBuffer = snapshot.data;
|
|
5685
5912
|
if (beforeBuffer.subarray(0, 8192).includes(0)) {
|
|
5686
|
-
return { content: `Cannot edit ${
|
|
5913
|
+
return { content: `Cannot edit ${path16}: it is a binary file, not text.`, isError: true };
|
|
5687
5914
|
}
|
|
5688
5915
|
const before = beforeBuffer.toString("utf8");
|
|
5689
5916
|
const count2 = countOccurrences(before, oldString);
|
|
5690
5917
|
if (count2 === 0) {
|
|
5691
5918
|
return {
|
|
5692
|
-
content: `oldString was not found in ${
|
|
5919
|
+
content: `oldString was not found in ${path16}. Read the file again and match the text exactly, including whitespace.`,
|
|
5693
5920
|
isError: true
|
|
5694
5921
|
};
|
|
5695
5922
|
}
|
|
5696
5923
|
if (count2 > 1 && !replaceAll) {
|
|
5697
5924
|
return {
|
|
5698
|
-
content: `oldString matches ${count2} places in ${
|
|
5925
|
+
content: `oldString matches ${count2} places in ${path16}. Add more surrounding context to identify a single one, or set replaceAll to true.`,
|
|
5699
5926
|
isError: true
|
|
5700
5927
|
};
|
|
5701
5928
|
}
|
|
5702
5929
|
const after = replacement(before, oldString, newString, replaceAll);
|
|
5703
|
-
if (after === null) return { content: `Could not prepare edit for ${
|
|
5930
|
+
if (after === null) return { content: `Could not prepare edit for ${path16}.`, isError: true };
|
|
5704
5931
|
if (Buffer.byteLength(after, "utf8") > MAX_FILE_BYTES2) {
|
|
5705
5932
|
return {
|
|
5706
|
-
content: `Cannot edit ${
|
|
5933
|
+
content: `Cannot edit ${path16}: the result exceeds the ${MAX_FILE_BYTES2 / 1e6} MB limit.`,
|
|
5707
5934
|
isError: true
|
|
5708
5935
|
};
|
|
5709
5936
|
}
|
|
@@ -5716,11 +5943,11 @@ var editTool = {
|
|
|
5716
5943
|
await atomicWriteSafeFile(safe.path, after, snapshot);
|
|
5717
5944
|
ctx.checkpoint?.markChanged(safe.path);
|
|
5718
5945
|
} catch (error) {
|
|
5719
|
-
return { content: `Failed to write ${
|
|
5946
|
+
return { content: `Failed to write ${path16}: ${error.message}`, isError: true };
|
|
5720
5947
|
}
|
|
5721
5948
|
return {
|
|
5722
|
-
content: `Edited ${
|
|
5723
|
-
display: { kind: "diff", path:
|
|
5949
|
+
content: `Edited ${path16} (${count2} ${count2 === 1 ? "replacement" : "replacements"})`,
|
|
5950
|
+
display: { kind: "diff", path: path16, before, after }
|
|
5724
5951
|
};
|
|
5725
5952
|
}
|
|
5726
5953
|
};
|
|
@@ -5737,45 +5964,6 @@ import { readFile as readFile10, stat as stat7 } from "fs/promises";
|
|
|
5737
5964
|
import { join as join3 } from "path";
|
|
5738
5965
|
import fg3 from "fast-glob";
|
|
5739
5966
|
|
|
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
5967
|
// src/tools/regex.ts
|
|
5780
5968
|
import { Worker } from "worker_threads";
|
|
5781
5969
|
var MATCH_TIMEOUT_MS = 300;
|
|
@@ -5885,16 +6073,16 @@ var grepTool = {
|
|
|
5885
6073
|
defaultPermission: "allow",
|
|
5886
6074
|
readOnly: true,
|
|
5887
6075
|
permission(input, ctx) {
|
|
5888
|
-
const { path:
|
|
5889
|
-
if (isSensitivePath(
|
|
5890
|
-
const safe = ctx &&
|
|
6076
|
+
const { path: path16, glob } = input ?? {};
|
|
6077
|
+
if (isSensitivePath(path16) || mentionsSensitivePattern(glob)) return "ask";
|
|
6078
|
+
const safe = ctx && path16 ? resolveInside(ctx.cwd, path16) : void 0;
|
|
5891
6079
|
return safe?.ok && isSensitivePath(safe.relative) ? "ask" : void 0;
|
|
5892
6080
|
},
|
|
5893
6081
|
summarize(input) {
|
|
5894
6082
|
return `grep(${brief(input.pattern)})`;
|
|
5895
6083
|
},
|
|
5896
6084
|
async execute(input, ctx) {
|
|
5897
|
-
const { pattern, path:
|
|
6085
|
+
const { pattern, path: path16 = ".", glob = "**/*", maxMatches } = input;
|
|
5898
6086
|
if (pattern.length > MAX_PATTERN_CHARS) {
|
|
5899
6087
|
return {
|
|
5900
6088
|
content: `Regular expression is limited to ${MAX_PATTERN_CHARS} characters.`,
|
|
@@ -5909,10 +6097,10 @@ var grepTool = {
|
|
|
5909
6097
|
if (patternEscapes(glob)) {
|
|
5910
6098
|
return { content: `Glob ${glob} must be relative to the workspace root.`, isError: true };
|
|
5911
6099
|
}
|
|
5912
|
-
const safe = resolveInside(ctx.cwd,
|
|
6100
|
+
const safe = resolveInside(ctx.cwd, path16);
|
|
5913
6101
|
if (!safe.ok) return { content: safe.reason, isError: true };
|
|
5914
6102
|
const target = await stat7(safe.path).catch(() => null);
|
|
5915
|
-
if (!target) return { content: `Path not found: ${
|
|
6103
|
+
if (!target) return { content: `Path not found: ${path16}`, isError: true };
|
|
5916
6104
|
const discovered = target.isDirectory() ? (await fg3(glob, {
|
|
5917
6105
|
cwd: safe.path,
|
|
5918
6106
|
dot: false,
|
|
@@ -5926,7 +6114,8 @@ var grepTool = {
|
|
|
5926
6114
|
relative: join3(safe.relative, entry.path),
|
|
5927
6115
|
size: entry.stats?.size ?? 0
|
|
5928
6116
|
})) : [{ absolute: safe.path, relative: safe.relative, size: target.size }];
|
|
5929
|
-
const
|
|
6117
|
+
const sensitiveRequested = isSensitivePath(path16) || isSensitivePath(safe.relative) || mentionsSensitivePattern(glob);
|
|
6118
|
+
const files = (sensitiveRequested ? discovered : discovered.filter((file) => !isSensitivePath(file.relative))).slice(0, MAX_FILES);
|
|
5930
6119
|
const filesCapped = discovered.length > MAX_FILES;
|
|
5931
6120
|
const limit = Math.min(maxMatches ?? MAX_MATCHES, MAX_MATCHES);
|
|
5932
6121
|
const hits = [];
|
|
@@ -6002,30 +6191,30 @@ var readTool = {
|
|
|
6002
6191
|
return `read(${brief(input.path)})`;
|
|
6003
6192
|
},
|
|
6004
6193
|
async execute(input, ctx) {
|
|
6005
|
-
const { path:
|
|
6006
|
-
const safe = resolveInside(ctx.cwd,
|
|
6194
|
+
const { path: path16, offset = 1, limit } = input;
|
|
6195
|
+
const safe = resolveInside(ctx.cwd, path16);
|
|
6007
6196
|
if (!safe.ok) return { content: safe.reason, isError: true };
|
|
6008
6197
|
const info = await stat8(safe.path).catch(() => null);
|
|
6009
|
-
if (!info) return { content: `File not found: ${
|
|
6198
|
+
if (!info) return { content: `File not found: ${path16}`, isError: true };
|
|
6010
6199
|
if (info.isDirectory()) {
|
|
6011
|
-
return { content: `${
|
|
6200
|
+
return { content: `${path16} is a directory, not a file. Use glob to list its contents.`, isError: true };
|
|
6012
6201
|
}
|
|
6013
6202
|
if (info.size > MAX_FILE_BYTES4) {
|
|
6014
6203
|
return {
|
|
6015
|
-
content: `${
|
|
6204
|
+
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
6205
|
isError: true
|
|
6017
6206
|
};
|
|
6018
6207
|
}
|
|
6019
6208
|
const buffer = await readFile11(safe.path);
|
|
6020
6209
|
if (buffer.subarray(0, 4096).includes(0)) {
|
|
6021
|
-
return { content: `${
|
|
6210
|
+
return { content: `${path16} looks like a binary file and cannot be read as text.`, isError: true };
|
|
6022
6211
|
}
|
|
6023
6212
|
const lines = buffer.toString("utf8").split("\n");
|
|
6024
6213
|
if (lines.at(-1) === "") lines.pop();
|
|
6025
|
-
if (lines.length === 0) return { content: `${
|
|
6214
|
+
if (lines.length === 0) return { content: `${path16} is empty.` };
|
|
6026
6215
|
const start = Math.max(1, offset) - 1;
|
|
6027
6216
|
if (start >= lines.length) {
|
|
6028
|
-
return { content: `${
|
|
6217
|
+
return { content: `${path16} has ${lines.length} lines, so offset ${offset} is past the end.`, isError: true };
|
|
6029
6218
|
}
|
|
6030
6219
|
const window = lines.slice(start, start + Math.min(limit ?? MAX_LINES, MAX_LINES));
|
|
6031
6220
|
const rendered = [];
|
|
@@ -6068,68 +6257,69 @@ var writeTool = {
|
|
|
6068
6257
|
additionalProperties: false
|
|
6069
6258
|
},
|
|
6070
6259
|
defaultPermission: "ask",
|
|
6260
|
+
permission: protectedWritePermission,
|
|
6071
6261
|
summarize(input) {
|
|
6072
6262
|
return `write(${brief(input.path)})`;
|
|
6073
6263
|
},
|
|
6074
6264
|
async preview(input, ctx) {
|
|
6075
|
-
const { path:
|
|
6265
|
+
const { path: path16, content } = input;
|
|
6076
6266
|
if (Buffer.byteLength(content, "utf8") > MAX_FILE_BYTES5) {
|
|
6077
6267
|
return { kind: "text", text: `Write is limited to ${MAX_FILE_BYTES5 / 1e6} MB.` };
|
|
6078
6268
|
}
|
|
6079
|
-
const safe = resolveInside(ctx.cwd,
|
|
6269
|
+
const safe = resolveInside(ctx.cwd, path16);
|
|
6080
6270
|
if (!safe.ok) return { kind: "text", text: safe.reason };
|
|
6081
6271
|
const beforeInfo = await stat9(safe.path).catch(() => null);
|
|
6082
6272
|
if (beforeInfo && beforeInfo.size > MAX_FILE_BYTES5) {
|
|
6083
|
-
return { kind: "text", text: `Cannot preview ${
|
|
6273
|
+
return { kind: "text", text: `Cannot preview ${path16}: the existing file is too large.` };
|
|
6084
6274
|
}
|
|
6085
6275
|
const beforeBuffer = await readFile12(safe.path).catch(() => null);
|
|
6086
6276
|
if (beforeBuffer?.subarray(0, 8192).includes(0)) {
|
|
6087
|
-
return { kind: "text", text: `Cannot preview binary file ${
|
|
6277
|
+
return { kind: "text", text: `Cannot preview binary file ${path16}` };
|
|
6088
6278
|
}
|
|
6089
6279
|
return {
|
|
6090
6280
|
kind: "diff",
|
|
6091
|
-
path:
|
|
6281
|
+
path: path16,
|
|
6092
6282
|
before: beforeBuffer?.toString("utf8") ?? "",
|
|
6093
6283
|
after: content
|
|
6094
6284
|
};
|
|
6095
6285
|
},
|
|
6096
6286
|
async execute(input, ctx) {
|
|
6097
|
-
const { path:
|
|
6287
|
+
const { path: path16, content } = input;
|
|
6098
6288
|
if (Buffer.byteLength(content, "utf8") > MAX_FILE_BYTES5) {
|
|
6099
6289
|
return {
|
|
6100
|
-
content: `Cannot write ${
|
|
6290
|
+
content: `Cannot write ${path16}: content exceeds the ${MAX_FILE_BYTES5 / 1e6} MB limit.`,
|
|
6101
6291
|
isError: true
|
|
6102
6292
|
};
|
|
6103
6293
|
}
|
|
6104
|
-
const safe = resolveInside(ctx.cwd,
|
|
6294
|
+
const safe = resolveInside(ctx.cwd, path16);
|
|
6105
6295
|
if (!safe.ok) return { content: safe.reason, isError: true };
|
|
6106
6296
|
let target = safe.path;
|
|
6107
6297
|
let snapshot;
|
|
6108
6298
|
try {
|
|
6109
6299
|
await mkdir4(dirname3(target), { recursive: true });
|
|
6110
|
-
const rechecked = resolveInside(ctx.cwd,
|
|
6300
|
+
const rechecked = resolveInside(ctx.cwd, path16);
|
|
6111
6301
|
if (!rechecked.ok || rechecked.path !== target) {
|
|
6112
6302
|
return {
|
|
6113
|
-
content: `Cannot write ${
|
|
6303
|
+
content: `Cannot write ${path16}: the path changed while its parent was created.`,
|
|
6114
6304
|
isError: true
|
|
6115
6305
|
};
|
|
6116
6306
|
}
|
|
6117
6307
|
target = rechecked.path;
|
|
6118
6308
|
snapshot = await readSafeFileSnapshot(target, MAX_FILE_BYTES5);
|
|
6119
6309
|
if (snapshot.exists && snapshot.data.subarray(0, 8192).includes(0)) {
|
|
6120
|
-
return { content: `Cannot write ${
|
|
6310
|
+
return { content: `Cannot write ${path16}: the existing file is binary, not text.`, isError: true };
|
|
6121
6311
|
}
|
|
6122
6312
|
await ctx.checkpoint?.capture(target);
|
|
6123
6313
|
await atomicWriteSafeFile(target, content, snapshot);
|
|
6124
6314
|
ctx.checkpoint?.markChanged(target);
|
|
6125
6315
|
} catch (error) {
|
|
6126
|
-
return { content: `Failed to write ${
|
|
6316
|
+
return { content: `Failed to write ${path16}: ${error.message}`, isError: true };
|
|
6127
6317
|
}
|
|
6128
6318
|
const before = snapshot.exists ? snapshot.data.toString("utf8") : "";
|
|
6129
6319
|
const lines = content === "" ? 0 : content.replace(/\n$/, "").split("\n").length;
|
|
6130
6320
|
return {
|
|
6131
|
-
content: `${before === "" ? "Created" : "Updated"} ${
|
|
6132
|
-
display: { kind: "diff", path:
|
|
6321
|
+
content: `${before === "" ? "Created" : "Updated"} ${path16} (${lines} ${lines === 1 ? "line" : "lines"})`,
|
|
6322
|
+
display: { kind: "diff", path: path16, before, after: content }
|
|
6133
6323
|
};
|
|
6134
6324
|
}
|
|
6135
6325
|
};
|
|
@@ -6159,9 +6349,9 @@ function createToolRegistry(tools) {
|
|
|
6159
6349
|
|
|
6160
6350
|
// src/prompts/library.ts
|
|
6161
6351
|
import { chmod as chmod6, readFile as readFile13, readdir as readdir4, unlink as unlink3, writeFile as writeFile8 } from "fs/promises";
|
|
6162
|
-
import
|
|
6352
|
+
import path12 from "path";
|
|
6163
6353
|
async function savePrompt(input) {
|
|
6164
|
-
const slug =
|
|
6354
|
+
const slug = promptSlug(input.name);
|
|
6165
6355
|
if (!slug) throw new Error(`Prompt name has no letters or digits to name a file after: ${input.name}`);
|
|
6166
6356
|
const prompt = {
|
|
6167
6357
|
slug,
|
|
@@ -6171,7 +6361,7 @@ async function savePrompt(input) {
|
|
|
6171
6361
|
body: input.body.trim()
|
|
6172
6362
|
};
|
|
6173
6363
|
await ensureDir(promptsDir);
|
|
6174
|
-
const file =
|
|
6364
|
+
const file = path12.join(promptsDir, `${slug}.md`);
|
|
6175
6365
|
await writeFile8(file, serialize(prompt), {
|
|
6176
6366
|
encoding: "utf8",
|
|
6177
6367
|
mode: 384
|
|
@@ -6182,7 +6372,7 @@ async function savePrompt(input) {
|
|
|
6182
6372
|
async function getPrompt(slug) {
|
|
6183
6373
|
assertSlug(slug);
|
|
6184
6374
|
try {
|
|
6185
|
-
return parse(slug, await readFile13(
|
|
6375
|
+
return parse(slug, await readFile13(path12.join(promptsDir, `${slug}.md`), "utf8"));
|
|
6186
6376
|
} catch (error) {
|
|
6187
6377
|
if (isMissing(error)) return null;
|
|
6188
6378
|
throw error;
|
|
@@ -6197,16 +6387,27 @@ async function listPrompts() {
|
|
|
6197
6387
|
return [];
|
|
6198
6388
|
}
|
|
6199
6389
|
const prompts2 = await Promise.all(
|
|
6200
|
-
names.filter((name) => name.endsWith(".md")).map(
|
|
6201
|
-
|
|
6202
|
-
|
|
6390
|
+
names.filter((name) => name.endsWith(".md") && validSlug(name.slice(0, -3))).map(async (name) => {
|
|
6391
|
+
const text = await readFile13(path12.join(promptsDir, name), "utf8").catch(() => null);
|
|
6392
|
+
return text === null ? null : parse(name.slice(0, -3), text);
|
|
6393
|
+
})
|
|
6203
6394
|
);
|
|
6204
|
-
return prompts2.sort((a, b) => a.name.localeCompare(b.name));
|
|
6395
|
+
return prompts2.filter((prompt) => prompt !== null).sort((a, b) => a.name.localeCompare(b.name));
|
|
6205
6396
|
}
|
|
6206
6397
|
async function deletePrompt(slug) {
|
|
6207
6398
|
assertSlug(slug);
|
|
6208
6399
|
try {
|
|
6209
|
-
await unlink3(
|
|
6400
|
+
await unlink3(path12.join(promptsDir, `${slug}.md`));
|
|
6401
|
+
return true;
|
|
6402
|
+
} catch (error) {
|
|
6403
|
+
if (isMissing(error)) return false;
|
|
6404
|
+
throw error;
|
|
6405
|
+
}
|
|
6406
|
+
}
|
|
6407
|
+
async function promptExists(slug) {
|
|
6408
|
+
assertSlug(slug);
|
|
6409
|
+
try {
|
|
6410
|
+
await readFile13(path12.join(promptsDir, `${slug}.md`), "utf8");
|
|
6210
6411
|
return true;
|
|
6211
6412
|
} catch (error) {
|
|
6212
6413
|
if (isMissing(error)) return false;
|
|
@@ -6216,11 +6417,16 @@ async function deletePrompt(slug) {
|
|
|
6216
6417
|
function isMissing(error) {
|
|
6217
6418
|
return error.code === "ENOENT";
|
|
6218
6419
|
}
|
|
6219
|
-
|
|
6220
|
-
|
|
6420
|
+
var RESERVED_NAMES = /^(?:con|prn|aux|nul|com[0-9]|lpt[0-9])$/;
|
|
6421
|
+
function promptSlug(name) {
|
|
6422
|
+
const slug = name.normalize("NFC").toLowerCase().replace(/[^\p{L}\p{N}]+/gu, "-").replace(/^-+|-+$/g, "").slice(0, 80).replace(/-+$/, "");
|
|
6423
|
+
return RESERVED_NAMES.test(slug) ? `${slug}-prompt` : slug;
|
|
6424
|
+
}
|
|
6425
|
+
function validSlug(slug) {
|
|
6426
|
+
return /^[\p{L}\p{N}]+(?:-[\p{L}\p{N}]+)*$/u.test(slug) && !RESERVED_NAMES.test(slug);
|
|
6221
6427
|
}
|
|
6222
6428
|
function assertSlug(slug) {
|
|
6223
|
-
if (
|
|
6429
|
+
if (!validSlug(slug)) {
|
|
6224
6430
|
throw new Error(`Invalid prompt id: ${slug}`);
|
|
6225
6431
|
}
|
|
6226
6432
|
}
|
|
@@ -6264,7 +6470,7 @@ function parse(slug, text) {
|
|
|
6264
6470
|
|
|
6265
6471
|
// src/skills/library.ts
|
|
6266
6472
|
import { lstat as lstat4, open as open2, readdir as readdir5 } from "fs/promises";
|
|
6267
|
-
import
|
|
6473
|
+
import path13 from "path";
|
|
6268
6474
|
var SKILL_FILE = "SKILL.md";
|
|
6269
6475
|
var FRONTMATTER_BYTES = 8192;
|
|
6270
6476
|
var MAX_SKILL_BYTES = 5e6;
|
|
@@ -6277,7 +6483,7 @@ async function discoverSkills(dirs) {
|
|
|
6277
6483
|
if (!rootInfo?.isDirectory() || rootInfo.isSymbolicLink()) continue;
|
|
6278
6484
|
const entries = await readdir5(root, { withFileTypes: true }).catch(() => []);
|
|
6279
6485
|
const found = await Promise.all(
|
|
6280
|
-
entries.filter((entry) => entry.isDirectory() && !entry.isSymbolicLink()).slice(0, MAX_SKILLS_PER_ROOT).map((entry) => readMeta(root, rootInfo,
|
|
6486
|
+
entries.filter((entry) => entry.isDirectory() && !entry.isSymbolicLink()).slice(0, MAX_SKILLS_PER_ROOT).map((entry) => readMeta(root, rootInfo, path13.join(root, entry.name)))
|
|
6281
6487
|
);
|
|
6282
6488
|
for (const meta of found) {
|
|
6283
6489
|
if (meta && !byName.has(meta.name)) byName.set(meta.name, meta);
|
|
@@ -6317,11 +6523,11 @@ function formatSkillCatalogue(skills) {
|
|
|
6317
6523
|
async function readMeta(root, rootInfo, dir) {
|
|
6318
6524
|
const dirInfo = await lstat4(dir).catch(() => null);
|
|
6319
6525
|
if (!dirInfo?.isDirectory() || dirInfo.isSymbolicLink()) return null;
|
|
6320
|
-
const file =
|
|
6526
|
+
const file = path13.join(dir, SKILL_FILE);
|
|
6321
6527
|
const head = await readFrontmatterBytes(file);
|
|
6322
6528
|
if (!head) return null;
|
|
6323
6529
|
const { fields } = parseFrontmatter(head.text);
|
|
6324
|
-
const meta = { name: fields.name ||
|
|
6530
|
+
const meta = { name: fields.name || path13.basename(dir), description: fields.description ?? "", dir, file };
|
|
6325
6531
|
guards.set(meta, {
|
|
6326
6532
|
root,
|
|
6327
6533
|
rootIdentity: identity(rootInfo),
|
|
@@ -6393,10 +6599,21 @@ function parseFrontmatter(text) {
|
|
|
6393
6599
|
if (lines[0]?.trim() === "---") {
|
|
6394
6600
|
let i = 1;
|
|
6395
6601
|
for (; i < lines.length && lines[i].trim() !== "---"; i++) {
|
|
6396
|
-
const
|
|
6602
|
+
const line = lines[i];
|
|
6603
|
+
if (/^\s/.test(line)) continue;
|
|
6604
|
+
const colon = line.indexOf(":");
|
|
6397
6605
|
if (colon <= 0) continue;
|
|
6398
|
-
const key =
|
|
6399
|
-
if (key
|
|
6606
|
+
const key = line.slice(0, colon).trim();
|
|
6607
|
+
if (key !== "name" && key !== "description") continue;
|
|
6608
|
+
let value = line.slice(colon + 1).trim();
|
|
6609
|
+
if (/^[>|][+-]?$/.test(value)) {
|
|
6610
|
+
const block = [];
|
|
6611
|
+
while (i + 1 < lines.length && /^\s+\S/.test(lines[i + 1]) && lines[i + 1].trim() !== "---") {
|
|
6612
|
+
block.push(lines[++i].trim());
|
|
6613
|
+
}
|
|
6614
|
+
value = block.join(value.startsWith("|") ? "\n" : " ");
|
|
6615
|
+
}
|
|
6616
|
+
fields[key] = value.replace(/^(["'])(.*)\1$/, "$2").trim();
|
|
6400
6617
|
}
|
|
6401
6618
|
bodyStart = Math.min(i + 1, lines.length);
|
|
6402
6619
|
}
|
|
@@ -6404,174 +6621,238 @@ function parseFrontmatter(text) {
|
|
|
6404
6621
|
}
|
|
6405
6622
|
|
|
6406
6623
|
// src/skills/install.ts
|
|
6624
|
+
import { execFile as execFile2 } from "child_process";
|
|
6407
6625
|
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");
|
|
6626
|
+
import { existsSync as existsSync2 } from "fs";
|
|
6627
|
+
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";
|
|
6628
|
+
import path14 from "path";
|
|
6629
|
+
var TMP_DIR = path14.join(skillsDir, ".tmp");
|
|
6630
|
+
var SKILL_FILE2 = "SKILL.md";
|
|
6422
6631
|
var MAX_SKILL_BYTES2 = 5e6;
|
|
6423
|
-
|
|
6632
|
+
var MAX_ARCHIVE_BYTES = 2e7;
|
|
6633
|
+
var MAX_SKILL_FILES = 1e3;
|
|
6634
|
+
var MAX_SKILL_TOTAL_BYTES = 5e7;
|
|
6635
|
+
var MAX_SEARCH_DEPTH = 4;
|
|
6636
|
+
var COMMAND_TIMEOUT_MS = 12e4;
|
|
6637
|
+
var SKIPPED_DIRS = /* @__PURE__ */ new Set([".git", "node_modules"]);
|
|
6638
|
+
async function installSkill(source, run = runCommand) {
|
|
6639
|
+
const trimmed = source.trim();
|
|
6640
|
+
if (!trimmed) throw new Error("Give a skill source: a GitHub URL, an npm package or a local path.");
|
|
6424
6641
|
await ensureDir(skillsDir);
|
|
6425
|
-
if (isGitHubUrl(
|
|
6426
|
-
|
|
6642
|
+
if (isGitHubUrl(trimmed)) return installFromGitHub(trimmed, run);
|
|
6643
|
+
if (npmSkillName(trimmed) && !existsSync2(path14.resolve(trimmed))) {
|
|
6644
|
+
return installFromNpm(trimmed, run);
|
|
6427
6645
|
}
|
|
6428
|
-
|
|
6429
|
-
return installFromNpm(source);
|
|
6430
|
-
}
|
|
6431
|
-
return installFromLocal(source);
|
|
6646
|
+
return installFromLocal(trimmed);
|
|
6432
6647
|
}
|
|
6433
|
-
function
|
|
6434
|
-
|
|
6435
|
-
|
|
6436
|
-
|
|
6437
|
-
|
|
6438
|
-
|
|
6648
|
+
async function uninstallSkill(name) {
|
|
6649
|
+
const dir = safeChildPath(skillsDir, safeSkillName(name));
|
|
6650
|
+
const info = await lstat5(dir).catch(() => null);
|
|
6651
|
+
if (!info) throw new Error(`No installed skill named "${name}" in ${skillsDir}`);
|
|
6652
|
+
if (info.isSymbolicLink() || !info.isDirectory()) {
|
|
6653
|
+
throw new Error(`Refusing to remove ${dir}: it is not a plain skill directory.`);
|
|
6439
6654
|
}
|
|
6655
|
+
await rm7(dir, { recursive: true, force: true });
|
|
6656
|
+
return dir;
|
|
6440
6657
|
}
|
|
6441
|
-
function
|
|
6442
|
-
return npmSkillName(input) !== null;
|
|
6443
|
-
}
|
|
6444
|
-
async function installFromGitHub(url) {
|
|
6658
|
+
async function installFromGitHub(url, run) {
|
|
6445
6659
|
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
|
-
}
|
|
6660
|
+
return withTempDir(async (tmp) => {
|
|
6661
|
+
const checkout = path14.join(tmp, "r");
|
|
6662
|
+
const args = ["-c", "core.longpaths=true", "clone", "--depth", "1", "--single-branch"];
|
|
6663
|
+
if (branch) args.push("--branch", branch);
|
|
6664
|
+
args.push(`https://github.com/${owner}/${repo}.git`, checkout);
|
|
6665
|
+
try {
|
|
6666
|
+
await run("git", args, { cwd: tmp, env: { ...process.env, GIT_TERMINAL_PROMPT: "0" } });
|
|
6667
|
+
} catch (error) {
|
|
6668
|
+
throw new Error(`Could not clone ${owner}/${repo}${branch ? ` (${branch})` : ""}: ${messageOf2(error)}`);
|
|
6669
|
+
}
|
|
6670
|
+
const start = subdir ? safeChildPath(checkout, ...subdir.split("/")) : checkout;
|
|
6671
|
+
const skillDir = await locateSkillDir(start, url);
|
|
6672
|
+
const fallback = skillDir === checkout ? repo : path14.basename(skillDir);
|
|
6673
|
+
return copyIntoSkills(skillDir, fallback, url);
|
|
6674
|
+
});
|
|
6464
6675
|
}
|
|
6465
|
-
async function installFromNpm(packageName) {
|
|
6676
|
+
async function installFromNpm(packageName, run) {
|
|
6466
6677
|
const parsedName = npmSkillName(packageName);
|
|
6467
6678
|
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 });
|
|
6679
|
+
return withTempDir(async (tmp) => {
|
|
6485
6680
|
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?");
|
|
6681
|
+
await run("npm", ["pack", packageName, "--pack-destination", ".", "--silent"], { cwd: tmp });
|
|
6682
|
+
} catch (error) {
|
|
6683
|
+
throw new Error(`Could not download npm package "${packageName}": ${messageOf2(error)}`);
|
|
6498
6684
|
}
|
|
6499
|
-
const
|
|
6500
|
-
|
|
6501
|
-
|
|
6502
|
-
const
|
|
6503
|
-
if (!
|
|
6504
|
-
throw new Error(`
|
|
6685
|
+
const tarball = (await readdir6(tmp)).find((file) => file.endsWith(".tgz"));
|
|
6686
|
+
if (!tarball) throw new Error(`npm did not produce an archive for "${packageName}"`);
|
|
6687
|
+
const archive = path14.join(tmp, tarball);
|
|
6688
|
+
const info = await lstat5(archive);
|
|
6689
|
+
if (!info.isFile() || info.size > MAX_ARCHIVE_BYTES) {
|
|
6690
|
+
throw new Error(`The npm archive exceeds the ${MAX_ARCHIVE_BYTES / 1e6} MB limit.`);
|
|
6505
6691
|
}
|
|
6506
|
-
const
|
|
6507
|
-
await
|
|
6508
|
-
|
|
6509
|
-
|
|
6510
|
-
|
|
6511
|
-
|
|
6692
|
+
const extracted = path14.join(tmp, "extracted");
|
|
6693
|
+
await mkdir5(extracted);
|
|
6694
|
+
try {
|
|
6695
|
+
await run("tar", ["-xzf", tarball, "-C", "extracted"], { cwd: tmp });
|
|
6696
|
+
} catch (error) {
|
|
6697
|
+
throw new Error(`Could not extract the npm package (is tar available?): ${messageOf2(error)}`);
|
|
6698
|
+
}
|
|
6699
|
+
const skillDir = await locateSkillDir(path14.join(extracted, "package"), packageName);
|
|
6700
|
+
return copyIntoSkills(skillDir, parsedName, packageName);
|
|
6701
|
+
});
|
|
6512
6702
|
}
|
|
6513
|
-
function
|
|
6514
|
-
|
|
6515
|
-
const
|
|
6516
|
-
if (
|
|
6517
|
-
const
|
|
6518
|
-
|
|
6703
|
+
async function installFromLocal(source) {
|
|
6704
|
+
const resolved = path14.resolve(source);
|
|
6705
|
+
const info = await lstat5(resolved).catch(() => null);
|
|
6706
|
+
if (!info) throw new Error(`No such file or directory: ${source}`);
|
|
6707
|
+
const start = info.isFile() && path14.basename(resolved) === SKILL_FILE2 ? path14.dirname(resolved) : resolved;
|
|
6708
|
+
const skillDir = await locateSkillDir(start, source);
|
|
6709
|
+
return copyIntoSkills(skillDir, path14.basename(skillDir), resolved);
|
|
6519
6710
|
}
|
|
6520
|
-
function
|
|
6521
|
-
const info =
|
|
6522
|
-
if (!info?.
|
|
6523
|
-
throw new Error("
|
|
6711
|
+
async function locateSkillDir(start, source) {
|
|
6712
|
+
const info = await lstat5(start).catch(() => null);
|
|
6713
|
+
if (!info?.isDirectory() || info.isSymbolicLink()) {
|
|
6714
|
+
throw new Error(`"${source}" does not point to a directory.`);
|
|
6524
6715
|
}
|
|
6525
|
-
|
|
6526
|
-
|
|
6527
|
-
|
|
6528
|
-
|
|
6529
|
-
|
|
6530
|
-
|
|
6531
|
-
|
|
6532
|
-
|
|
6716
|
+
if (await isPlainFile(path14.join(start, SKILL_FILE2))) return start;
|
|
6717
|
+
const found = [];
|
|
6718
|
+
const walk = async (dir, depth) => {
|
|
6719
|
+
if (depth > MAX_SEARCH_DEPTH || found.length > 50) return;
|
|
6720
|
+
const entries = await readdir6(dir, { withFileTypes: true }).catch(() => []);
|
|
6721
|
+
for (const entry of entries) {
|
|
6722
|
+
if (!entry.isDirectory() || SKIPPED_DIRS.has(entry.name)) continue;
|
|
6723
|
+
const child = path14.join(dir, entry.name);
|
|
6724
|
+
if (await isPlainFile(path14.join(child, SKILL_FILE2))) found.push(child);
|
|
6725
|
+
else await walk(child, depth + 1);
|
|
6726
|
+
}
|
|
6727
|
+
};
|
|
6728
|
+
await walk(start, 1);
|
|
6729
|
+
if (found.length === 1) return found[0];
|
|
6730
|
+
if (found.length === 0) throw new Error(`No ${SKILL_FILE2} found in "${source}".`);
|
|
6731
|
+
const listed = found.slice(0, 20).map((dir) => ` ${path14.relative(start, dir).split(path14.sep).join("/")}`).join("\n");
|
|
6732
|
+
throw new Error(
|
|
6733
|
+
`"${source}" contains ${found.length} skills; install one of them by its own path (for GitHub: .../tree/<branch>/<path>):
|
|
6734
|
+
${listed}`
|
|
6735
|
+
);
|
|
6736
|
+
}
|
|
6737
|
+
async function copyIntoSkills(sourceDir, fallbackName, source) {
|
|
6738
|
+
const skillFile = path14.join(sourceDir, SKILL_FILE2);
|
|
6739
|
+
const skillInfo = await lstat5(skillFile);
|
|
6740
|
+
if (skillInfo.size > MAX_SKILL_BYTES2) {
|
|
6741
|
+
throw new Error(`${SKILL_FILE2} exceeds the ${MAX_SKILL_BYTES2 / 1e6} MB limit.`);
|
|
6533
6742
|
}
|
|
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}"`);
|
|
6743
|
+
const name = safeSkillName(frontmatterName(await readFile14(skillFile, "utf8")) ?? fallbackName);
|
|
6744
|
+
const destination = safeChildPath(skillsDir, name);
|
|
6745
|
+
const existing = await lstat5(destination).catch(() => null);
|
|
6746
|
+
if (existing && (existing.isSymbolicLink() || !existing.isDirectory())) {
|
|
6747
|
+
throw new Error(`Refusing to replace ${destination}: it is not a plain skill directory.`);
|
|
6748
|
+
}
|
|
6749
|
+
await ensureDir(TMP_DIR);
|
|
6750
|
+
const staging = safeChildPath(TMP_DIR, `s-${randomUUID4().slice(0, 8)}`);
|
|
6751
|
+
try {
|
|
6752
|
+
await copyTree(sourceDir, staging);
|
|
6753
|
+
if (existing) await rm7(destination, { recursive: true, force: true });
|
|
6754
|
+
await rename6(staging, destination);
|
|
6755
|
+
} catch (error) {
|
|
6756
|
+
await rm7(staging, { recursive: true, force: true }).catch(() => void 0);
|
|
6757
|
+
throw error;
|
|
6554
6758
|
}
|
|
6555
|
-
|
|
6556
|
-
|
|
6557
|
-
|
|
6558
|
-
|
|
6559
|
-
|
|
6759
|
+
return { name, dir: destination, source };
|
|
6760
|
+
}
|
|
6761
|
+
async function copyTree(from, to) {
|
|
6762
|
+
let files = 0;
|
|
6763
|
+
let bytes = 0;
|
|
6764
|
+
const visit = async (dir, target) => {
|
|
6765
|
+
await mkdir5(target, { recursive: true, mode: 448 });
|
|
6766
|
+
for (const entry of await readdir6(dir, { withFileTypes: true })) {
|
|
6767
|
+
const source = path14.join(dir, entry.name);
|
|
6768
|
+
const destination = path14.join(target, entry.name);
|
|
6769
|
+
if (entry.isDirectory()) {
|
|
6770
|
+
if (!SKIPPED_DIRS.has(entry.name)) await visit(source, destination);
|
|
6771
|
+
continue;
|
|
6772
|
+
}
|
|
6773
|
+
if (!entry.isFile()) continue;
|
|
6774
|
+
const info = await lstat5(source);
|
|
6775
|
+
files += 1;
|
|
6776
|
+
bytes += info.size;
|
|
6777
|
+
if (files > MAX_SKILL_FILES || bytes > MAX_SKILL_TOTAL_BYTES) {
|
|
6778
|
+
throw new Error(
|
|
6779
|
+
`The skill is too large to install (limit ${MAX_SKILL_FILES} files, ${MAX_SKILL_TOTAL_BYTES / 1e6} MB).`
|
|
6780
|
+
);
|
|
6781
|
+
}
|
|
6782
|
+
await copyFile(source, destination);
|
|
6783
|
+
await chmod7(destination, (info.mode & 73) !== 0 ? 448 : 384);
|
|
6784
|
+
}
|
|
6785
|
+
};
|
|
6786
|
+
await visit(from, to);
|
|
6560
6787
|
}
|
|
6561
|
-
|
|
6562
|
-
|
|
6563
|
-
|
|
6564
|
-
|
|
6565
|
-
|
|
6788
|
+
function frontmatterName(text) {
|
|
6789
|
+
const lines = text.replace(/^/, "").split(/\r?\n/);
|
|
6790
|
+
if (lines[0]?.trim() !== "---") return void 0;
|
|
6791
|
+
for (const line of lines.slice(1)) {
|
|
6792
|
+
if (line.trim() === "---") return void 0;
|
|
6793
|
+
const match = /^name\s*:\s*(.+)$/.exec(line);
|
|
6794
|
+
if (match) {
|
|
6795
|
+
const value = match[1].trim().replace(/^(["'])(.*)\1$/, "$2").trim();
|
|
6796
|
+
return /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(value) ? value : void 0;
|
|
6797
|
+
}
|
|
6566
6798
|
}
|
|
6567
|
-
|
|
6568
|
-
|
|
6569
|
-
|
|
6570
|
-
|
|
6571
|
-
|
|
6799
|
+
return void 0;
|
|
6800
|
+
}
|
|
6801
|
+
async function withTempDir(work) {
|
|
6802
|
+
await ensureDir(TMP_DIR);
|
|
6803
|
+
const dir = safeChildPath(TMP_DIR, randomUUID4().slice(0, 8));
|
|
6804
|
+
await mkdir5(dir, { mode: 448 });
|
|
6805
|
+
try {
|
|
6806
|
+
return await work(dir);
|
|
6807
|
+
} finally {
|
|
6808
|
+
await rm7(dir, { recursive: true, force: true }).catch(() => void 0);
|
|
6809
|
+
}
|
|
6810
|
+
}
|
|
6811
|
+
function runCommand(command, args, options) {
|
|
6812
|
+
const viaShell = process.platform === "win32" && command === "npm";
|
|
6813
|
+
const file = viaShell ? [command, ...args.map(quoteForCmd)].join(" ") : command;
|
|
6814
|
+
return new Promise((resolve3, reject) => {
|
|
6815
|
+
execFile2(
|
|
6816
|
+
file,
|
|
6817
|
+
viaShell ? [] : args,
|
|
6818
|
+
{
|
|
6819
|
+
cwd: options.cwd,
|
|
6820
|
+
env: options.env,
|
|
6821
|
+
shell: viaShell,
|
|
6822
|
+
timeout: COMMAND_TIMEOUT_MS,
|
|
6823
|
+
windowsHide: true,
|
|
6824
|
+
maxBuffer: 4e6
|
|
6825
|
+
},
|
|
6826
|
+
(error, _stdout, stderr) => {
|
|
6827
|
+
if (!error) return resolve3();
|
|
6828
|
+
const code = error.code;
|
|
6829
|
+
if (code === "ENOENT") return reject(new Error(`${command} is not installed or not on PATH`));
|
|
6830
|
+
if (error.killed) {
|
|
6831
|
+
return reject(new Error(`${command} timed out after ${COMMAND_TIMEOUT_MS / 1e3}s`));
|
|
6832
|
+
}
|
|
6833
|
+
const detail = String(stderr ?? "").trim().split(/\r?\n/).filter((line) => line.trim() !== "" && !/^Cloning into /.test(line)).slice(-3).join(" ");
|
|
6834
|
+
reject(new Error(detail || error.message));
|
|
6835
|
+
}
|
|
6836
|
+
);
|
|
6837
|
+
});
|
|
6838
|
+
}
|
|
6839
|
+
function quoteForCmd(value) {
|
|
6840
|
+
return /^[\w@./:=+-]+$/.test(value) ? value : `"${value.replace(/"/g, '""')}"`;
|
|
6841
|
+
}
|
|
6842
|
+
function isGitHubUrl(input) {
|
|
6843
|
+
try {
|
|
6844
|
+
const url = new URL(input);
|
|
6845
|
+
return url.protocol === "https:" && (url.hostname === "github.com" || url.hostname === "www.github.com");
|
|
6846
|
+
} catch {
|
|
6847
|
+
return false;
|
|
6572
6848
|
}
|
|
6573
|
-
|
|
6574
|
-
|
|
6849
|
+
}
|
|
6850
|
+
function npmSkillName(input) {
|
|
6851
|
+
if (input.includes("\\") || input.startsWith(".")) return null;
|
|
6852
|
+
const scoped = input.match(/^@[\w][\w.-]*\/([\w][\w.-]*)(?:@[\w.*+-]+)?$/);
|
|
6853
|
+
if (scoped) return scoped[1] ?? null;
|
|
6854
|
+
const unscoped = input.match(/^([\w][\w.-]*)(?:@[\w.*+-]+)?$/);
|
|
6855
|
+
return unscoped?.[1] ?? null;
|
|
6575
6856
|
}
|
|
6576
6857
|
function parseGitHubUrl(url) {
|
|
6577
6858
|
let parsed;
|
|
@@ -6593,11 +6874,13 @@ function parseGitHubUrl(url) {
|
|
|
6593
6874
|
const owner = safeGitHubSegment(segments[0], "owner");
|
|
6594
6875
|
const repo = safeSkillName(segments[1].replace(/\.git$/i, ""));
|
|
6595
6876
|
if (segments.length === 2) return { owner, repo };
|
|
6596
|
-
if (segments[2] !== "tree" || segments.length < 4) {
|
|
6877
|
+
if (segments[2] !== "tree" && segments[2] !== "blob" || segments.length < 4) {
|
|
6597
6878
|
throw new Error(`Unsupported GitHub skill URL: ${url}`);
|
|
6598
6879
|
}
|
|
6599
6880
|
const branch = safeGitHubSegment(segments[3], "branch");
|
|
6600
|
-
const
|
|
6881
|
+
const pathSegments = segments.slice(4);
|
|
6882
|
+
if (segments[2] === "blob" && pathSegments.at(-1) === SKILL_FILE2) pathSegments.pop();
|
|
6883
|
+
const subdirSegments = pathSegments.map((segment) => safeGitHubSegment(segment, "path"));
|
|
6601
6884
|
return {
|
|
6602
6885
|
owner,
|
|
6603
6886
|
repo,
|
|
@@ -6606,7 +6889,7 @@ function parseGitHubUrl(url) {
|
|
|
6606
6889
|
};
|
|
6607
6890
|
}
|
|
6608
6891
|
function safeGitHubSegment(value, kind) {
|
|
6609
|
-
if (value === "" || value === "." || value === ".." || value.length > 255 || /[\0-\x1f\x7f/\\:*?"<>|#%]/.test(value)) {
|
|
6892
|
+
if (value === "" || value === "." || value === ".." || value.startsWith("-") || value.length > 255 || /[\0-\x1f\x7f/\\:*?"<>|#%]/.test(value)) {
|
|
6610
6893
|
throw new Error(`Unsafe GitHub ${kind}: ${value || "(empty)"}`);
|
|
6611
6894
|
}
|
|
6612
6895
|
return value;
|
|
@@ -6618,37 +6901,19 @@ function safeSkillName(value) {
|
|
|
6618
6901
|
return value;
|
|
6619
6902
|
}
|
|
6620
6903
|
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;
|
|
6904
|
+
const target = path14.resolve(root, ...segments);
|
|
6905
|
+
const relative2 = path14.relative(path14.resolve(root), target);
|
|
6906
|
+
if (relative2 !== "" && (path14.isAbsolute(relative2) || relative2 === ".." || relative2.startsWith(`..${path14.sep}`))) {
|
|
6907
|
+
throw new Error(`Path escapes the skills directory: ${target}`);
|
|
6629
6908
|
}
|
|
6630
|
-
|
|
6909
|
+
return target;
|
|
6631
6910
|
}
|
|
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");
|
|
6911
|
+
async function isPlainFile(file) {
|
|
6912
|
+
const info = await lstat5(file).catch(() => null);
|
|
6913
|
+
return Boolean(info?.isFile() && !info.isSymbolicLink());
|
|
6644
6914
|
}
|
|
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 });
|
|
6915
|
+
function messageOf2(error) {
|
|
6916
|
+
return error instanceof Error ? error.message : String(error);
|
|
6652
6917
|
}
|
|
6653
6918
|
|
|
6654
6919
|
// src/tools/skill.ts
|
|
@@ -6922,6 +7187,12 @@ async function boot(options) {
|
|
|
6922
7187
|
const skillRoots = workspaceTrusted ? [projectSkillsDir(workspaceRoot2), skillsDir] : [skillsDir];
|
|
6923
7188
|
let skills = await discoverSkills(skillRoots);
|
|
6924
7189
|
if (skills.length > 0) tools.register([createSkillTool(skills)]);
|
|
7190
|
+
const reloadSkills = async () => {
|
|
7191
|
+
skills = await discoverSkills(skillRoots);
|
|
7192
|
+
skillCatalogue = formatSkillCatalogue(skills);
|
|
7193
|
+
tools.unregister(["skill"]);
|
|
7194
|
+
if (skills.length > 0) tools.register([createSkillTool(skills)]);
|
|
7195
|
+
};
|
|
6925
7196
|
const mcp = createMcpManager(config.mcp);
|
|
6926
7197
|
await mcp.connectAll();
|
|
6927
7198
|
let registeredMcpToolNames = /* @__PURE__ */ new Set();
|
|
@@ -6934,6 +7205,7 @@ async function boot(options) {
|
|
|
6934
7205
|
);
|
|
6935
7206
|
};
|
|
6936
7207
|
syncMcpTools();
|
|
7208
|
+
mcp.onChange(syncMcpTools);
|
|
6937
7209
|
for (const state of mcp.states()) {
|
|
6938
7210
|
if (state.status === "error") warnings.push(`MCP "${state.name}": ${state.error ?? "failed"}`);
|
|
6939
7211
|
}
|
|
@@ -7326,7 +7598,7 @@ async function boot(options) {
|
|
|
7326
7598
|
},
|
|
7327
7599
|
async exportSession(id, destination) {
|
|
7328
7600
|
await persistQueue.catch(() => void 0);
|
|
7329
|
-
const target = destination ?
|
|
7601
|
+
const target = destination ? path15.resolve(options.cwd, destination) : path15.join(options.cwd, ".kitcode-exports");
|
|
7330
7602
|
if (!destination) await mkdir6(target, { recursive: true, mode: 448 });
|
|
7331
7603
|
return (await exportSession(id, target)).path;
|
|
7332
7604
|
},
|
|
@@ -7553,16 +7825,20 @@ ${lines.join("\n")}` : null;
|
|
|
7553
7825
|
hint: prompt.description || void 0
|
|
7554
7826
|
}));
|
|
7555
7827
|
},
|
|
7556
|
-
async readPrompt(
|
|
7557
|
-
const prompt = await getPrompt(
|
|
7828
|
+
async readPrompt(name) {
|
|
7829
|
+
const prompt = await getPrompt(promptSlug(name));
|
|
7558
7830
|
return prompt?.body ?? "";
|
|
7559
7831
|
},
|
|
7560
7832
|
async savePrompt(name, body) {
|
|
7561
|
-
await savePrompt({ name, body });
|
|
7833
|
+
return (await savePrompt({ name, body })).slug;
|
|
7562
7834
|
},
|
|
7563
|
-
async
|
|
7564
|
-
const
|
|
7565
|
-
|
|
7835
|
+
async promptExists(name) {
|
|
7836
|
+
const slug = promptSlug(name);
|
|
7837
|
+
return slug !== "" && await promptExists(slug);
|
|
7838
|
+
},
|
|
7839
|
+
async deletePrompt(name) {
|
|
7840
|
+
const slug = promptSlug(name);
|
|
7841
|
+
if (!slug || !await deletePrompt(slug)) throw new Error(`Prompt "${name}" not found.`);
|
|
7566
7842
|
},
|
|
7567
7843
|
async loadSkill(name) {
|
|
7568
7844
|
const meta = skills.find((s) => s.name === name);
|
|
@@ -7571,15 +7847,25 @@ ${lines.join("\n")}` : null;
|
|
|
7571
7847
|
},
|
|
7572
7848
|
async installSkill(source) {
|
|
7573
7849
|
const result = await installSkill(source);
|
|
7574
|
-
|
|
7575
|
-
skillCatalogue = formatSkillCatalogue(skills);
|
|
7576
|
-
tools.unregister(["skill"]);
|
|
7577
|
-
if (skills.length > 0) tools.register([createSkillTool(skills)]);
|
|
7850
|
+
await reloadSkills();
|
|
7578
7851
|
return result;
|
|
7579
7852
|
},
|
|
7853
|
+
async uninstallSkill(name) {
|
|
7854
|
+
const meta = skills.find((skill) => skill.name === name);
|
|
7855
|
+
if (!meta) throw new Error(`Skill "${name}" not found.`);
|
|
7856
|
+
if (path15.dirname(meta.dir) !== skillsDir) {
|
|
7857
|
+
throw new Error(`Skill "${name}" belongs to this project: delete ${meta.dir} instead.`);
|
|
7858
|
+
}
|
|
7859
|
+
await uninstallSkill(path15.basename(meta.dir));
|
|
7860
|
+
await reloadSkills();
|
|
7861
|
+
},
|
|
7580
7862
|
async loadAttachment(requestedPath) {
|
|
7581
7863
|
return (await loadAttachment(options.cwd, requestedPath)).block;
|
|
7582
7864
|
},
|
|
7865
|
+
summarizeToolCall: (name, input) => tools.get(name)?.summarize(input) ?? name,
|
|
7866
|
+
async loadMentionAttachment(requestedPath) {
|
|
7867
|
+
return (await loadMentionAttachment(options.cwd, requestedPath))?.block ?? null;
|
|
7868
|
+
},
|
|
7583
7869
|
async loadAutomaticAttachment(requestedPath) {
|
|
7584
7870
|
return (await loadAutomaticAttachment(options.cwd, requestedPath))?.block ?? null;
|
|
7585
7871
|
},
|
|
@@ -7697,6 +7983,16 @@ ${lines.join("\n")}` : null;
|
|
|
7697
7983
|
});
|
|
7698
7984
|
}
|
|
7699
7985
|
return next;
|
|
7986
|
+
} catch (error) {
|
|
7987
|
+
const recovered = signal.aborted ? null : imageRejection(error, history);
|
|
7988
|
+
if (!recovered) throw error;
|
|
7989
|
+
hooks.onEvent({
|
|
7990
|
+
type: "notice",
|
|
7991
|
+
level: "error",
|
|
7992
|
+
text: `${error instanceof Error ? error.message : String(error)}
|
|
7993
|
+
${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.`
|
|
7994
|
+
});
|
|
7995
|
+
return recovered.messages;
|
|
7700
7996
|
} finally {
|
|
7701
7997
|
try {
|
|
7702
7998
|
await checkpoint.commit();
|
|
@@ -7743,6 +8039,14 @@ ${lines.join("\n")}` : null;
|
|
|
7743
8039
|
}
|
|
7744
8040
|
};
|
|
7745
8041
|
}
|
|
8042
|
+
function imageRejection(error, history) {
|
|
8043
|
+
const status = error?.status;
|
|
8044
|
+
if (typeof status !== "number" || ![400, 413, 415, 422].includes(status)) return null;
|
|
8045
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
8046
|
+
if (status !== 413 && !/image|vision|multi-?modal/i.test(message)) return null;
|
|
8047
|
+
const result = withoutImages(history);
|
|
8048
|
+
return result.removed > 0 ? result : null;
|
|
8049
|
+
}
|
|
7746
8050
|
function preferredModel(models) {
|
|
7747
8051
|
for (const preferred of PREFERRED2) {
|
|
7748
8052
|
const match = models.find((model) => model.id.endsWith(preferred));
|
|
@@ -7998,35 +8302,89 @@ function useTerminalSize() {
|
|
|
7998
8302
|
|
|
7999
8303
|
// src/mcp/add.ts
|
|
8000
8304
|
var SERVER_NAME = /^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$/;
|
|
8305
|
+
var ENV_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
8306
|
+
var HEADER_NAME = /^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/;
|
|
8307
|
+
function parseMcpAddCommand(raw) {
|
|
8308
|
+
const args = splitCommandLine(raw);
|
|
8309
|
+
return args ? parseMcpAddArgs(args) : { ok: false, error: "usage" };
|
|
8310
|
+
}
|
|
8001
8311
|
function parseMcpAddArgs(args) {
|
|
8002
|
-
const [name,
|
|
8003
|
-
if (!name
|
|
8312
|
+
const [name, ...tail2] = args;
|
|
8313
|
+
if (!name) return { ok: false, error: "usage" };
|
|
8004
8314
|
if (!SERVER_NAME.test(name)) return { ok: false, error: "invalid-name" };
|
|
8005
|
-
|
|
8006
|
-
|
|
8007
|
-
|
|
8315
|
+
const env = {};
|
|
8316
|
+
const headers = {};
|
|
8317
|
+
let index = 0;
|
|
8318
|
+
for (; index < tail2.length; index += 1) {
|
|
8319
|
+
const option = tail2[index];
|
|
8320
|
+
if (option === "-e" || option === "--env") {
|
|
8321
|
+
const value = tail2[++index];
|
|
8322
|
+
const eq = value?.indexOf("=") ?? -1;
|
|
8323
|
+
if (!value || eq <= 0 || !ENV_NAME.test(value.slice(0, eq))) return { ok: false, error: "invalid-env" };
|
|
8324
|
+
env[value.slice(0, eq)] = value.slice(eq + 1);
|
|
8325
|
+
continue;
|
|
8326
|
+
}
|
|
8327
|
+
if (option === "-H" || option === "--header") {
|
|
8328
|
+
const value = tail2[++index];
|
|
8329
|
+
const colon = value?.indexOf(":") ?? -1;
|
|
8330
|
+
const key = value && colon > 0 ? value.slice(0, colon).trim() : "";
|
|
8331
|
+
if (!value || !HEADER_NAME.test(key)) return { ok: false, error: "invalid-header" };
|
|
8332
|
+
headers[key] = value.slice(colon + 1).trim();
|
|
8333
|
+
continue;
|
|
8334
|
+
}
|
|
8335
|
+
break;
|
|
8336
|
+
}
|
|
8337
|
+
let rest = tail2.slice(index);
|
|
8338
|
+
let kind;
|
|
8339
|
+
if (rest[0] === "http") {
|
|
8340
|
+
kind = "http";
|
|
8341
|
+
rest = rest.slice(1);
|
|
8342
|
+
} else if (rest[0] === "stdio" || rest[0] === "--") {
|
|
8343
|
+
kind = "stdio";
|
|
8344
|
+
rest = rest.slice(1);
|
|
8345
|
+
}
|
|
8346
|
+
const [target, ...extra] = rest;
|
|
8347
|
+
if (!target) return { ok: false, error: "usage" };
|
|
8348
|
+
kind ??= /^https?:\/\//i.test(target) ? "http" : "stdio";
|
|
8349
|
+
if (kind === "http") {
|
|
8008
8350
|
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);
|
|
8351
|
+
if (Object.keys(env).length > 0) return { ok: false, error: "option-mismatch" };
|
|
8352
|
+
const parsed2 = mcpServerSchema.safeParse({ type: "http", url: target, headers });
|
|
8353
|
+
return parsed2.success ? { ok: true, name, config: parsed2.data } : { ok: false, error: "invalid-url" };
|
|
8019
8354
|
}
|
|
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 });
|
|
8355
|
+
if (Object.keys(headers).length > 0) return { ok: false, error: "option-mismatch" };
|
|
8356
|
+
const parsed = mcpServerSchema.safeParse({ type: "stdio", command: target, args: extra, env });
|
|
8028
8357
|
return parsed.success ? { ok: true, name, config: parsed.data } : { ok: false, error: "usage" };
|
|
8029
8358
|
}
|
|
8359
|
+
function splitCommandLine(raw) {
|
|
8360
|
+
const words2 = [];
|
|
8361
|
+
let current = "";
|
|
8362
|
+
let inWord = false;
|
|
8363
|
+
let quote;
|
|
8364
|
+
for (const char of raw) {
|
|
8365
|
+
if (quote) {
|
|
8366
|
+
if (char === quote) quote = void 0;
|
|
8367
|
+
else current += char;
|
|
8368
|
+
continue;
|
|
8369
|
+
}
|
|
8370
|
+
if (char === '"' || char === "'") {
|
|
8371
|
+
quote = char;
|
|
8372
|
+
inWord = true;
|
|
8373
|
+
continue;
|
|
8374
|
+
}
|
|
8375
|
+
if (/\s/.test(char)) {
|
|
8376
|
+
if (inWord) words2.push(current);
|
|
8377
|
+
current = "";
|
|
8378
|
+
inWord = false;
|
|
8379
|
+
continue;
|
|
8380
|
+
}
|
|
8381
|
+
current += char;
|
|
8382
|
+
inWord = true;
|
|
8383
|
+
}
|
|
8384
|
+
if (quote) return null;
|
|
8385
|
+
if (inWord) words2.push(current);
|
|
8386
|
+
return words2;
|
|
8387
|
+
}
|
|
8030
8388
|
|
|
8031
8389
|
// src/ui/speed.ts
|
|
8032
8390
|
var CHARS_PER_TOKEN = 4;
|
|
@@ -8065,21 +8423,21 @@ function readSpeed(meter, busy, now = Date.now()) {
|
|
|
8065
8423
|
// src/ui/commands.ts
|
|
8066
8424
|
var BUSY_SAFE = { runsWhileBusy: true };
|
|
8067
8425
|
var COMMANDS = [
|
|
8068
|
-
{ name: "model", ...BUSY_SAFE },
|
|
8426
|
+
{ name: "model", args: "[model]", ...BUSY_SAFE },
|
|
8069
8427
|
{ name: "login" },
|
|
8070
8428
|
{ name: "provider" },
|
|
8071
8429
|
{ name: "logout", args: "[provider]" },
|
|
8072
8430
|
{ name: "key", args: "[provider]" },
|
|
8073
8431
|
{ name: "refresh", args: "[provider]" },
|
|
8074
8432
|
{ name: "effort", ...BUSY_SAFE },
|
|
8075
|
-
{ name: "thinking", ...BUSY_SAFE },
|
|
8433
|
+
{ name: "thinking", args: "[on|off]", ...BUSY_SAFE },
|
|
8076
8434
|
{ name: "bell", args: "[on|off]", ...BUSY_SAFE },
|
|
8077
8435
|
{ name: "context", args: "[tokens|auto]", ...BUSY_SAFE },
|
|
8078
8436
|
{ name: "budget", args: "[tokens]", ...BUSY_SAFE },
|
|
8079
8437
|
{ name: "theme", args: "[color]", ...BUSY_SAFE },
|
|
8080
|
-
{ name: "lang", ...BUSY_SAFE },
|
|
8081
|
-
{ name: "prompt", args: "[save
|
|
8082
|
-
{ name: "skills", args: "[install <source
|
|
8438
|
+
{ name: "lang", args: "[en|ru]", ...BUSY_SAFE },
|
|
8439
|
+
{ name: "prompt", args: "[name|save [name]|delete [name]]" },
|
|
8440
|
+
{ name: "skills", args: "[install <source>|remove [name]]" },
|
|
8083
8441
|
{ name: "bypass" },
|
|
8084
8442
|
{ name: "usage", ...BUSY_SAFE },
|
|
8085
8443
|
{ name: "mcp", args: "[add|list|delete|enable|disable]" },
|
|
@@ -8098,7 +8456,9 @@ var COMMANDS = [
|
|
|
8098
8456
|
{ name: "exit", ...BUSY_SAFE }
|
|
8099
8457
|
];
|
|
8100
8458
|
var SUBCOMMANDS = [
|
|
8101
|
-
{ name: "
|
|
8459
|
+
{ name: "skills install", args: "<github-url|npm-package|path>" },
|
|
8460
|
+
{ name: "skills remove", args: "[name]" },
|
|
8461
|
+
{ name: "mcp add", args: '<name> [-e K=V] [-H "N: V"] <url|-- command>' },
|
|
8102
8462
|
{ name: "mcp list" },
|
|
8103
8463
|
{ name: "mcp delete", args: "[name]" },
|
|
8104
8464
|
{ name: "mcp enable", args: "[name]" },
|
|
@@ -8188,10 +8548,10 @@ function mentionSpan(value, cursor) {
|
|
|
8188
8548
|
while (end < value.length && !/[\s@]/.test(value[end] ?? "")) end += 1;
|
|
8189
8549
|
return { start, end, query };
|
|
8190
8550
|
}
|
|
8191
|
-
function completeMention(value, span,
|
|
8551
|
+
function completeMention(value, span, path16) {
|
|
8192
8552
|
const rest = value.slice(span.end);
|
|
8193
8553
|
const spaced = rest.startsWith(" ");
|
|
8194
|
-
const inserted = `@${
|
|
8554
|
+
const inserted = `@${path16}${spaced ? "" : " "}`;
|
|
8195
8555
|
return {
|
|
8196
8556
|
value: value.slice(0, span.start) + inserted + rest,
|
|
8197
8557
|
cursor: span.start + inserted.length + (spaced ? 1 : 0)
|
|
@@ -8200,8 +8560,8 @@ function completeMention(value, span, path15) {
|
|
|
8200
8560
|
function extractMentions(text) {
|
|
8201
8561
|
const found = [];
|
|
8202
8562
|
for (const match of text.matchAll(MENTION_PATTERN)) {
|
|
8203
|
-
const
|
|
8204
|
-
if (
|
|
8563
|
+
const path16 = (match[1] ?? "").replace(TRAILING_PUNCTUATION, "");
|
|
8564
|
+
if (path16 !== "" && !found.includes(path16)) found.push(path16);
|
|
8205
8565
|
}
|
|
8206
8566
|
return found;
|
|
8207
8567
|
}
|
|
@@ -8315,7 +8675,7 @@ var en = {
|
|
|
8315
8675
|
pickerHelp: (shown, total) => `${shown} of ${total} \xB7 \u2191\u2193 move \xB7 enter pick \xB7 esc cancel`,
|
|
8316
8676
|
noModel: "no model",
|
|
8317
8677
|
busy: "working",
|
|
8318
|
-
sessionFor: (elapsed) => `
|
|
8678
|
+
sessionFor: (elapsed) => `session ${elapsed}`,
|
|
8319
8679
|
workingFor: (elapsed) => `working ${elapsed}`,
|
|
8320
8680
|
titleProvider: "Provider",
|
|
8321
8681
|
titleLogout: "Sign out of a provider",
|
|
@@ -8340,17 +8700,22 @@ var en = {
|
|
|
8340
8700
|
pasteToken: (id, lines, chars) => `[paste #${id} \xB7 ${lines > 1 ? `${lines} lines` : `${chars} chars`}]`,
|
|
8341
8701
|
inputRows: (from, to, total) => `lines ${from}\u2013${to} of ${total}`,
|
|
8342
8702
|
queueHint: "waiting for the current answer \xB7 \u2191/\u2193 pick one to edit \xB7 enter puts it back in its place",
|
|
8343
|
-
configAt: (
|
|
8703
|
+
configAt: (path16) => `config: ${path16}`,
|
|
8344
8704
|
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>",
|
|
8705
|
+
skillsInstallUsage: "Usage: /skills install <github-url|npm-package|local-path> \xB7 /skills remove [name]",
|
|
8346
8706
|
skillsInstalling: (source) => `Installing skill from "${source}"\u2026`,
|
|
8347
8707
|
skillsInstalled: (name, source) => `Skill "${name}" installed from "${source}"`,
|
|
8708
|
+
skillsRemoved: (name) => `Skill "${name}" removed`,
|
|
8709
|
+
titleSkillsRemove: "Remove skill",
|
|
8348
8710
|
mcpStatus: (connected, failed) => `MCP: ${connected} connected, ${failed} failed`,
|
|
8349
8711
|
mcpEmpty: "No MCP servers configured.",
|
|
8350
|
-
mcpAddUsage:
|
|
8712
|
+
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
8713
|
mcpAddInvalidName: "MCP name may contain only letters, numbers, _ and - (up to 64 chars).",
|
|
8352
8714
|
mcpAddInvalidUrl: "MCP URL must use HTTPS (HTTP is allowed only for localhost).",
|
|
8353
8715
|
mcpAddHttpArgs: "An HTTP MCP accepts one URL and no command arguments.",
|
|
8716
|
+
mcpAddInvalidEnv: "Environment variables look like -e KEY=value.",
|
|
8717
|
+
mcpAddInvalidHeader: 'Headers look like -H "Name: value".',
|
|
8718
|
+
mcpAddOptionMismatch: "-e is for local (command) servers, -H is for remote (URL) servers.",
|
|
8354
8719
|
mcpExists: (name) => `MCP server "${name}" already exists.`,
|
|
8355
8720
|
mcpConnecting: (name) => `Connecting MCP "${name}"\u2026`,
|
|
8356
8721
|
mcpAdded: (name, tools) => `MCP "${name}" connected and saved \xB7 ${tools} tools`,
|
|
@@ -8378,6 +8743,9 @@ var en = {
|
|
|
8378
8743
|
attachUsage: "Usage: /attach <image-or-text-file> \xB7 /attach clipboard \xB7 /attach clear",
|
|
8379
8744
|
attachmentAdded: (name) => `Attached for the next message: ${name}`,
|
|
8380
8745
|
attachmentsCleared: "Pending attachments cleared.",
|
|
8746
|
+
attachmentsLimit: (max) => `At most ${max} attachments can be queued for one message.`,
|
|
8747
|
+
attachmentsTooLarge: (mb) => `Attachments for one message are limited to about ${mb} MB in total. Send these first or use smaller files.`,
|
|
8748
|
+
mentionAttached: (name) => `Sending with this message: ${name}`,
|
|
8381
8749
|
compactNothing: "Not enough older conversation to compact yet.",
|
|
8382
8750
|
compactStarting: "Compacting context\u2026",
|
|
8383
8751
|
compactDone: (summarised, cleared, before, after) => [
|
|
@@ -8386,7 +8754,7 @@ var en = {
|
|
|
8386
8754
|
cleared > 0 ? `${cleared} old tool outputs cleared` : "",
|
|
8387
8755
|
before !== void 0 && after !== void 0 ? `~${tokens(before)} \u2192 ~${tokens(after)} tokens` : ""
|
|
8388
8756
|
].filter(Boolean).join(" \xB7 ") + ".",
|
|
8389
|
-
sessionRenameUsage: "Type the title after the command: /sessions rename
|
|
8757
|
+
sessionRenameUsage: "Type the title after the command: /sessions rename [id] <title>",
|
|
8390
8758
|
sessionRenamed: (title) => `Session renamed to "${title}".`,
|
|
8391
8759
|
sessionDeleteAsk: "The saved chat file will be deleted. This cannot be undone.",
|
|
8392
8760
|
sessionDeleted: (id) => `Session ${id} deleted.`,
|
|
@@ -8396,7 +8764,7 @@ var en = {
|
|
|
8396
8764
|
sessionDeleteAllFinalBody: "This cannot be undone. Press y only if you want to erase the complete session history.",
|
|
8397
8765
|
sessionsDeletedAll: (count2) => `All saved chats deleted \xB7 ${count2} sessions removed.`,
|
|
8398
8766
|
sessionsDeleteAllFailed: (deleted, failed) => `Session cleanup was incomplete \xB7 ${deleted} removed, ${failed} failed.`,
|
|
8399
|
-
sessionExported: (
|
|
8767
|
+
sessionExported: (path16) => `Session exported to ${path16}`,
|
|
8400
8768
|
sessionActionResume: "resume",
|
|
8401
8769
|
sessionActionRename: "rename",
|
|
8402
8770
|
sessionActionDelete: "delete",
|
|
@@ -8437,11 +8805,19 @@ ${url}`,
|
|
|
8437
8805
|
promptUsage: "Usage: /prompt save <name>",
|
|
8438
8806
|
promptNothing: "Nothing to save yet \u2014 send a message first.",
|
|
8439
8807
|
promptSaved: (name) => `Saved prompt "${name}"`,
|
|
8440
|
-
|
|
8808
|
+
promptDeleteAsk: (name) => `Delete prompt "${name}"?`,
|
|
8809
|
+
promptOverwriteAsk: (name) => `A prompt named "${name}" exists. Replace it?`,
|
|
8810
|
+
promptNameTitle: "Prompt name",
|
|
8811
|
+
sessionNoMatch: (query) => `No saved session matches "${query}".`,
|
|
8812
|
+
sessionAmbiguous: (query, count2) => `"${query}" matches ${count2} sessions; give more of the id.`,
|
|
8813
|
+
sessionsUsage: "Usage: /sessions [list|rename [id] <title>|delete [id|all]|export [id] [path]]",
|
|
8814
|
+
modelNoMatch: (query) => `No single model matches "${query}"; pick one from the list.`,
|
|
8815
|
+
memoryUsage: "/memory show \xB7 /memory set <text> \xB7 /memory clear",
|
|
8816
|
+
memoryEmpty: "Project memory is empty.",
|
|
8817
|
+
memoryClearAsk: "Clear project memory?",
|
|
8818
|
+
memoryReplaceAsk: "Replace all project memory notes with this text?",
|
|
8441
8819
|
promptDeleted: (name) => `Deleted prompt "${name}"`,
|
|
8442
8820
|
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
8821
|
promptsEmpty: "No saved prompts. Use /prompt save <name>.",
|
|
8446
8822
|
unknownCommand: (name) => `Unknown command /${name}. Try /help`,
|
|
8447
8823
|
didYouMean: (typed, guess) => `Unknown command /${typed}. Did you mean /${guess}?`,
|
|
@@ -8536,7 +8912,7 @@ var ru = {
|
|
|
8536
8912
|
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
8913
|
noModel: "\u043D\u0435\u0442 \u043C\u043E\u0434\u0435\u043B\u0438",
|
|
8538
8914
|
busy: "\u0440\u0430\u0431\u043E\u0442\u0430\u044E",
|
|
8539
|
-
sessionFor: (elapsed) => `\
|
|
8915
|
+
sessionFor: (elapsed) => `\u0441\u0435\u0441\u0441\u0438\u044F ${elapsed}`,
|
|
8540
8916
|
workingFor: (elapsed) => `\u0434\u0443\u043C\u0430\u0435\u0442 ${elapsed}`,
|
|
8541
8917
|
titleProvider: "\u041F\u0440\u043E\u0432\u0430\u0439\u0434\u0435\u0440",
|
|
8542
8918
|
titleLogout: "\u0412\u044B\u0431\u0440\u0430\u0442\u044C API \u0434\u043B\u044F \u0432\u044B\u0445\u043E\u0434\u0430",
|
|
@@ -8561,17 +8937,22 @@ var ru = {
|
|
|
8561
8937
|
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
8938
|
inputRows: (from, to, total) => `\u0441\u0442\u0440\u043E\u043A\u0438 ${from}\u2013${to} \u0438\u0437 ${total}`,
|
|
8563
8939
|
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: (
|
|
8940
|
+
configAt: (path16) => `\u043A\u043E\u043D\u0444\u0438\u0433: ${path16}`,
|
|
8565
8941
|
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>",
|
|
8942
|
+
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
8943
|
skillsInstalling: (source) => `\u0423\u0441\u0442\u0430\u043D\u0430\u0432\u043B\u0438\u0432\u0430\u044E \u0441\u043A\u0438\u043B\u043B \u0438\u0437 "${source}"\u2026`,
|
|
8568
8944
|
skillsInstalled: (name, source) => `\u0421\u043A\u0438\u043B\u043B \xAB${name}\xBB \u0443\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D \u0438\u0437 "${source}"`,
|
|
8945
|
+
skillsRemoved: (name) => `\u0421\u043A\u0438\u043B\u043B \xAB${name}\xBB \u0443\u0434\u0430\u043B\u0451\u043D`,
|
|
8946
|
+
titleSkillsRemove: "\u0423\u0434\u0430\u043B\u0438\u0442\u044C \u0441\u043A\u0438\u043B\u043B",
|
|
8569
8947
|
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
8948
|
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:
|
|
8949
|
+
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
8950
|
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
8951
|
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
8952
|
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.",
|
|
8953
|
+
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.",
|
|
8954
|
+
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".',
|
|
8955
|
+
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
8956
|
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
8957
|
mcpConnecting: (name) => `\u041F\u043E\u0434\u043A\u043B\u044E\u0447\u0430\u044E MCP \xAB${name}\xBB\u2026`,
|
|
8577
8958
|
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 +8980,9 @@ var ru = {
|
|
|
8599
8980
|
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
8981
|
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
8982
|
attachmentsCleared: "\u041E\u0436\u0438\u0434\u0430\u044E\u0449\u0438\u0435 \u0432\u043B\u043E\u0436\u0435\u043D\u0438\u044F \u0443\u0431\u0440\u0430\u043D\u044B.",
|
|
8983
|
+
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.`,
|
|
8984
|
+
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.`,
|
|
8985
|
+
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
8986
|
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
8987
|
compactStarting: "\u0421\u0436\u0430\u0442\u0438\u0435 \u043A\u043E\u043D\u0442\u0435\u043A\u0441\u0442\u0430\u2026",
|
|
8604
8988
|
compactDone: (summarised, cleared, before, after) => [
|
|
@@ -8607,7 +8991,7 @@ var ru = {
|
|
|
8607
8991
|
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
8992
|
before !== void 0 && after !== void 0 ? `~${tokens(before)} \u2192 ~${tokens(after)} \u0442\u043E\u043A\u0435\u043D\u043E\u0432` : ""
|
|
8609
8993
|
].filter(Boolean).join(" \xB7 ") + ".",
|
|
8610
|
-
sessionRenameUsage: "\u0414\u043E\u043F\u0438\u0448\u0438 \u043D\u0430\u0437\u0432\u0430\u043D\u0438\u0435: /sessions rename
|
|
8994
|
+
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
8995
|
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
8996
|
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
8997
|
sessionDeleted: (id) => `\u0421\u0435\u0441\u0441\u0438\u044F ${id} \u0443\u0434\u0430\u043B\u0435\u043D\u0430.`,
|
|
@@ -8617,7 +9001,7 @@ var ru = {
|
|
|
8617
9001
|
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
9002
|
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
9003
|
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: (
|
|
9004
|
+
sessionExported: (path16) => `\u0421\u0435\u0441\u0441\u0438\u044F \u044D\u043A\u0441\u043F\u043E\u0440\u0442\u0438\u0440\u043E\u0432\u0430\u043D\u0430: ${path16}`,
|
|
8621
9005
|
sessionActionResume: "\u043E\u0442\u043A\u0440\u044B\u0442\u044C",
|
|
8622
9006
|
sessionActionRename: "\u043F\u0435\u0440\u0435\u0438\u043C\u0435\u043D\u043E\u0432\u0430\u0442\u044C",
|
|
8623
9007
|
sessionActionDelete: "\u0443\u0434\u0430\u043B\u0438\u0442\u044C",
|
|
@@ -8658,11 +9042,19 @@ ${url}`,
|
|
|
8658
9042
|
promptUsage: "\u0418\u0441\u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u043D\u0438\u0435: /prompt save <\u0438\u043C\u044F>",
|
|
8659
9043
|
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
9044
|
promptSaved: (name) => `\u041F\u0440\u043E\u043C\u0442 \xAB${name}\xBB \u0441\u043E\u0445\u0440\u0430\u043D\u0451\u043D`,
|
|
8661
|
-
|
|
9045
|
+
promptDeleteAsk: (name) => `\u0423\u0434\u0430\u043B\u0438\u0442\u044C \u043F\u0440\u043E\u043C\u0442 \xAB${name}\xBB?`,
|
|
9046
|
+
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?`,
|
|
9047
|
+
promptNameTitle: "\u041D\u0430\u0437\u0432\u0430\u043D\u0438\u0435 \u043F\u0440\u043E\u043C\u0442\u0430",
|
|
9048
|
+
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.`,
|
|
9049
|
+
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.`,
|
|
9050
|
+
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]]",
|
|
9051
|
+
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.`,
|
|
9052
|
+
memoryUsage: "/memory show \xB7 /memory set <\u0442\u0435\u043A\u0441\u0442> \xB7 /memory clear",
|
|
9053
|
+
memoryEmpty: "\u041F\u0430\u043C\u044F\u0442\u044C \u043F\u0440\u043E\u0435\u043A\u0442\u0430 \u043F\u0443\u0441\u0442\u0430.",
|
|
9054
|
+
memoryClearAsk: "\u041E\u0447\u0438\u0441\u0442\u0438\u0442\u044C \u043F\u0430\u043C\u044F\u0442\u044C \u043F\u0440\u043E\u0435\u043A\u0442\u0430?",
|
|
9055
|
+
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
9056
|
promptDeleted: (name) => `\u041F\u0440\u043E\u043C\u0442 \xAB${name}\xBB \u0443\u0434\u0430\u043B\u0451\u043D`,
|
|
8663
9057
|
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
9058
|
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
9059
|
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
9060
|
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 +10203,10 @@ function StatusBar({ status }) {
|
|
|
9811
10203
|
flexDirection: "column",
|
|
9812
10204
|
children: [
|
|
9813
10205
|
/* @__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: [
|
|
10206
|
+
/* @__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
10207
|
index > 0 && /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: " \xB7 " }),
|
|
9816
10208
|
item
|
|
9817
|
-
] }, index)) }),
|
|
10209
|
+
] }) }, index)) }),
|
|
9818
10210
|
/* @__PURE__ */ jsx9(Box9, { flexShrink: 0, marginLeft: 2, children: /* @__PURE__ */ jsx9(ContextMeter, { context: status.context }) })
|
|
9819
10211
|
] }),
|
|
9820
10212
|
status.agents.length > 0 && /* @__PURE__ */ jsxs9(Box9, { width: "100%", flexDirection: "column", alignItems: "flex-end", children: [
|
|
@@ -9859,6 +10251,8 @@ function ContextMeter({
|
|
|
9859
10251
|
context
|
|
9860
10252
|
}) {
|
|
9861
10253
|
const theme = useTheme();
|
|
10254
|
+
const { columns } = useTerminalSize();
|
|
10255
|
+
const compact = columns < 80;
|
|
9862
10256
|
const { window: win, exact } = context;
|
|
9863
10257
|
const used = exact ? context.used : 0;
|
|
9864
10258
|
if (win === null || win <= 0) {
|
|
@@ -9871,6 +10265,12 @@ function ContextMeter({
|
|
|
9871
10265
|
const filled = Math.max(0, Math.min(BAR_WIDTH, Math.round(BAR_WIDTH * pct)));
|
|
9872
10266
|
const color = pct < 0.6 ? theme.ok : pct < 0.85 ? theme.warn : theme.error;
|
|
9873
10267
|
const percent = Math.round(pct * 100);
|
|
10268
|
+
if (compact) {
|
|
10269
|
+
return /* @__PURE__ */ jsxs9(Text9, { children: [
|
|
10270
|
+
/* @__PURE__ */ jsx9(Text9, { dimColor: true, children: "ctx " }),
|
|
10271
|
+
/* @__PURE__ */ jsx9(Text9, { color, bold: true, children: `${percent}%` })
|
|
10272
|
+
] });
|
|
10273
|
+
}
|
|
9874
10274
|
return /* @__PURE__ */ jsxs9(Text9, { children: [
|
|
9875
10275
|
/* @__PURE__ */ jsx9(Text9, { dimColor: true, children: "ctx " }),
|
|
9876
10276
|
/* @__PURE__ */ jsx9(Text9, { color, bold: true, children: `${percent}%`.padStart(4) }),
|
|
@@ -10744,8 +11144,7 @@ var BubbleView = memo2(function BubbleView2({
|
|
|
10744
11144
|
] }) });
|
|
10745
11145
|
}
|
|
10746
11146
|
if (bubble.kind === "notice") {
|
|
10747
|
-
|
|
10748
|
-
return /* @__PURE__ */ jsx11(Box11, { marginTop: 1, children: /* @__PURE__ */ jsx11(Text11, { color, children: bubble.text }) });
|
|
11147
|
+
return /* @__PURE__ */ jsx11(NoticeView, { level: bubble.level, text: bubble.text });
|
|
10749
11148
|
}
|
|
10750
11149
|
if (bubble.kind === "assistant") {
|
|
10751
11150
|
return /* @__PURE__ */ jsx11(AssistantView, { bubble, maxRows });
|
|
@@ -10819,6 +11218,24 @@ function wrapVisualLine(line, columns) {
|
|
|
10819
11218
|
if (current !== "" || rows.length === 0) rows.push(current);
|
|
10820
11219
|
return rows;
|
|
10821
11220
|
}
|
|
11221
|
+
var NOTICE_ICONS = { warn: "\u25B2", error: "\u2717" };
|
|
11222
|
+
function NoticeView({ level, text }) {
|
|
11223
|
+
const theme = useTheme();
|
|
11224
|
+
const newline = text.indexOf("\n");
|
|
11225
|
+
const headline = newline === -1 ? text : text.slice(0, newline);
|
|
11226
|
+
const details = newline === -1 ? "" : text.slice(newline + 1);
|
|
11227
|
+
if (level === "info") {
|
|
11228
|
+
return /* @__PURE__ */ jsx11(Box11, { marginTop: 1, paddingLeft: 2, children: /* @__PURE__ */ jsx11(Text11, { dimColor: details === "", children: text }) });
|
|
11229
|
+
}
|
|
11230
|
+
const color = level === "error" ? theme.error : theme.warn;
|
|
11231
|
+
return /* @__PURE__ */ jsxs11(Box11, { marginTop: 1, children: [
|
|
11232
|
+
/* @__PURE__ */ jsx11(Box11, { width: 2, flexShrink: 0, children: /* @__PURE__ */ jsx11(Text11, { color, bold: true, children: NOTICE_ICONS[level] }) }),
|
|
11233
|
+
/* @__PURE__ */ jsxs11(Box11, { flexDirection: "column", flexShrink: 1, minWidth: 0, children: [
|
|
11234
|
+
/* @__PURE__ */ jsx11(Text11, { color, bold: level === "error", children: headline }),
|
|
11235
|
+
details !== "" && /* @__PURE__ */ jsx11(Text11, { children: details })
|
|
11236
|
+
] })
|
|
11237
|
+
] });
|
|
11238
|
+
}
|
|
10822
11239
|
function ToolView({ bubble }) {
|
|
10823
11240
|
const theme = useTheme();
|
|
10824
11241
|
const mark = bubble.state === "running" ? "\u25CC" : bubble.state === "ok" ? "\u25CF" : "\u2717";
|
|
@@ -11155,7 +11572,7 @@ function closeAssistant(bubbles) {
|
|
|
11155
11572
|
(bubble) => bubble.kind === "assistant" && bubble.streaming ? { ...bubble, streaming: false } : bubble
|
|
11156
11573
|
);
|
|
11157
11574
|
}
|
|
11158
|
-
function fromHistory(messages) {
|
|
11575
|
+
function fromHistory(messages, summarize = (name) => name) {
|
|
11159
11576
|
let state = emptyTranscript();
|
|
11160
11577
|
for (const message of messages) {
|
|
11161
11578
|
if (message.role === "user") {
|
|
@@ -11186,7 +11603,7 @@ function fromHistory(messages) {
|
|
|
11186
11603
|
kind: "tool",
|
|
11187
11604
|
id: block.id,
|
|
11188
11605
|
name: sanitizeTerminalText(block.name),
|
|
11189
|
-
summary: sanitizeTerminalText(block.name),
|
|
11606
|
+
summary: sanitizeTerminalText(safeSummary(summarize, block.name, block.input)),
|
|
11190
11607
|
state: "ok",
|
|
11191
11608
|
content: ""
|
|
11192
11609
|
}
|
|
@@ -11249,6 +11666,13 @@ function sanitizeDisplay(display) {
|
|
|
11249
11666
|
after: sanitizeTerminalText(display.after)
|
|
11250
11667
|
};
|
|
11251
11668
|
}
|
|
11669
|
+
function safeSummary(summarize, name, input) {
|
|
11670
|
+
try {
|
|
11671
|
+
return summarize(name, input) || name;
|
|
11672
|
+
} catch {
|
|
11673
|
+
return name;
|
|
11674
|
+
}
|
|
11675
|
+
}
|
|
11252
11676
|
|
|
11253
11677
|
// src/ui/App.tsx
|
|
11254
11678
|
import { jsx as jsx14, jsxs as jsxs13 } from "react/jsx-runtime";
|
|
@@ -11258,6 +11682,7 @@ var CUSTOM_ACCENT = "__custom__";
|
|
|
11258
11682
|
var BELL_AFTER_MS = 15e3;
|
|
11259
11683
|
var SUBAGENT_CHAT_EXTRA_ROWS = 4;
|
|
11260
11684
|
var MAX_ATTACHMENTS = 8;
|
|
11685
|
+
var MAX_ATTACHMENT_PAYLOAD = 24e6;
|
|
11261
11686
|
var CONTEXT_PRESETS = [256e3, 512e3, 1e6];
|
|
11262
11687
|
function formatContextTokens(tokens2) {
|
|
11263
11688
|
if (tokens2 >= 1e6) return `${tokens2 / 1e6}M`;
|
|
@@ -11279,7 +11704,7 @@ function App({
|
|
|
11279
11704
|
const { stdout } = useStdout2();
|
|
11280
11705
|
const { rows } = useTerminalSize();
|
|
11281
11706
|
const [transcript, setTranscript] = useState8(
|
|
11282
|
-
() => warnings.reduce((state, text) => pushNotice(state, "warn", text), fromHistory(initialHistory))
|
|
11707
|
+
() => warnings.reduce((state, text) => pushNotice(state, "warn", text), fromHistory(initialHistory, runtime.summarizeToolCall))
|
|
11283
11708
|
);
|
|
11284
11709
|
const [transcriptRevision, setTranscriptRevision] = useState8(0);
|
|
11285
11710
|
const transcriptRef = useRef5(transcript);
|
|
@@ -11334,11 +11759,16 @@ function App({
|
|
|
11334
11759
|
}, []);
|
|
11335
11760
|
const appendAttachment = useCallback((block) => {
|
|
11336
11761
|
if (attachmentsRef.current.length >= MAX_ATTACHMENTS) return false;
|
|
11762
|
+
if (attachmentsRef.current.some((queued) => sameAttachment(queued, block))) return true;
|
|
11763
|
+
const payload = attachmentsRef.current.reduce((sum, queued) => sum + attachmentPayload(queued), 0);
|
|
11764
|
+
if (payload + attachmentPayload(block) > MAX_ATTACHMENT_PAYLOAD) {
|
|
11765
|
+
throw new Error(stringsFor(lang).attachmentsTooLarge(Math.round(MAX_ATTACHMENT_PAYLOAD / 1e6)));
|
|
11766
|
+
}
|
|
11337
11767
|
const next = [...attachmentsRef.current, block];
|
|
11338
11768
|
attachmentsRef.current = next;
|
|
11339
11769
|
setAttachments(next);
|
|
11340
11770
|
return true;
|
|
11341
|
-
}, []);
|
|
11771
|
+
}, [lang]);
|
|
11342
11772
|
const flushTranscriptEvents = useCallback(() => {
|
|
11343
11773
|
if (transcriptTimer.current !== null) {
|
|
11344
11774
|
clearTimeout(transcriptTimer.current);
|
|
@@ -11542,10 +11972,11 @@ function App({
|
|
|
11542
11972
|
try {
|
|
11543
11973
|
const restored = await runtime.resumeSession(choice);
|
|
11544
11974
|
history.current = restored;
|
|
11975
|
+
replaceAttachments([]);
|
|
11545
11976
|
setPromptHistory(inputHistoryFromMessages(restored));
|
|
11546
11977
|
setTranscript(
|
|
11547
11978
|
pushNotice(
|
|
11548
|
-
fromHistory(restored),
|
|
11979
|
+
fromHistory(restored, runtime.summarizeToolCall),
|
|
11549
11980
|
"info",
|
|
11550
11981
|
strings.resumed(choice.slice(-6), restored.length)
|
|
11551
11982
|
)
|
|
@@ -11609,12 +12040,18 @@ function App({
|
|
|
11609
12040
|
forceRender((n) => n + 1);
|
|
11610
12041
|
return;
|
|
11611
12042
|
}
|
|
12043
|
+
const matching = (query) => items.filter((item) => item.key === query || item.key.includes(query));
|
|
11612
12044
|
let id = action ? rest[1] : void 0;
|
|
12045
|
+
let argsFrom = 2;
|
|
12046
|
+
if (id && (action === "rename" || action === "export") && matching(id).length === 0) {
|
|
12047
|
+
id = void 0;
|
|
12048
|
+
argsFrom = 1;
|
|
12049
|
+
}
|
|
11613
12050
|
if (!id) id = await pick(strings.titleSessions, items) ?? void 0;
|
|
11614
12051
|
if (!id) return;
|
|
11615
|
-
const matches =
|
|
12052
|
+
const matches = matching(id);
|
|
11616
12053
|
if (matches.length !== 1) {
|
|
11617
|
-
notice("warn",
|
|
12054
|
+
notice("warn", matches.length === 0 ? strings.sessionNoMatch(id) : strings.sessionAmbiguous(id, matches.length));
|
|
11618
12055
|
return;
|
|
11619
12056
|
}
|
|
11620
12057
|
id = matches[0]?.key ?? id;
|
|
@@ -11634,13 +12071,13 @@ function App({
|
|
|
11634
12071
|
replaceAttachments([]);
|
|
11635
12072
|
setPromptHistory(inputHistoryFromMessages(restored));
|
|
11636
12073
|
setTranscript(
|
|
11637
|
-
pushNotice(fromHistory(restored), "info", strings.resumed(id.slice(-6), restored.length))
|
|
12074
|
+
pushNotice(fromHistory(restored, runtime.summarizeToolCall), "info", strings.resumed(id.slice(-6), restored.length))
|
|
11638
12075
|
);
|
|
11639
12076
|
setTranscriptRevision((revision) => revision + 1);
|
|
11640
12077
|
sessionStart.current = Date.now();
|
|
11641
12078
|
turns.current = 0;
|
|
11642
12079
|
} else if (action === "rename") {
|
|
11643
|
-
const title = rest.slice(
|
|
12080
|
+
const title = rest.slice(argsFrom).join(" ").trim();
|
|
11644
12081
|
if (!title) {
|
|
11645
12082
|
setInput(`/sessions rename ${id} `);
|
|
11646
12083
|
notice("info", strings.sessionRenameUsage);
|
|
@@ -11668,10 +12105,10 @@ function App({
|
|
|
11668
12105
|
notice("info", deletedMessage);
|
|
11669
12106
|
}
|
|
11670
12107
|
} else if (action === "export") {
|
|
11671
|
-
const destination = unquoteArg(rest.slice(
|
|
12108
|
+
const destination = unquoteArg(rest.slice(argsFrom).join(" ").trim()) || void 0;
|
|
11672
12109
|
notice("info", strings.sessionExported(await runtime.exportSession(id, destination)));
|
|
11673
12110
|
} else {
|
|
11674
|
-
notice("warn",
|
|
12111
|
+
notice("warn", strings.sessionsUsage);
|
|
11675
12112
|
return;
|
|
11676
12113
|
}
|
|
11677
12114
|
} catch (error) {
|
|
@@ -11823,7 +12260,8 @@ ${strings.modelSet(result.nextModel)}` : ""}`;
|
|
|
11823
12260
|
return;
|
|
11824
12261
|
}
|
|
11825
12262
|
case "lang": {
|
|
11826
|
-
const
|
|
12263
|
+
const typed = rawRest.toLowerCase();
|
|
12264
|
+
const choice = LANGS.some((entry) => entry.key === typed) ? typed : await pick(
|
|
11827
12265
|
strings.titleLang,
|
|
11828
12266
|
LANGS.map((entry) => ({ key: entry.key, label: entry.label }))
|
|
11829
12267
|
);
|
|
@@ -11850,6 +12288,33 @@ ${strings.modelSet(result.nextModel)}` : ""}`;
|
|
|
11850
12288
|
}
|
|
11851
12289
|
return;
|
|
11852
12290
|
}
|
|
12291
|
+
if (action === "remove" || action === "uninstall") {
|
|
12292
|
+
let name = rest[1];
|
|
12293
|
+
if (!name) {
|
|
12294
|
+
const installed = runtime.listSkills();
|
|
12295
|
+
if (installed.length === 0) {
|
|
12296
|
+
notice("info", strings.skillsEmpty);
|
|
12297
|
+
return;
|
|
12298
|
+
}
|
|
12299
|
+
name = await pick(
|
|
12300
|
+
strings.titleSkillsRemove,
|
|
12301
|
+
installed.map((skill) => ({ key: skill.name, label: skill.name, hint: skill.description || void 0 }))
|
|
12302
|
+
) ?? void 0;
|
|
12303
|
+
if (!name) return;
|
|
12304
|
+
}
|
|
12305
|
+
try {
|
|
12306
|
+
await runtime.uninstallSkill(name);
|
|
12307
|
+
notice("info", strings.skillsRemoved(name));
|
|
12308
|
+
forceRender((n) => n + 1);
|
|
12309
|
+
} catch (error) {
|
|
12310
|
+
notice("error", error instanceof Error ? error.message : String(error));
|
|
12311
|
+
}
|
|
12312
|
+
return;
|
|
12313
|
+
}
|
|
12314
|
+
if (action) {
|
|
12315
|
+
notice("warn", strings.skillsInstallUsage);
|
|
12316
|
+
return;
|
|
12317
|
+
}
|
|
11853
12318
|
const skills = runtime.listSkills();
|
|
11854
12319
|
if (skills.length === 0) {
|
|
11855
12320
|
notice("info", strings.skillsEmpty);
|
|
@@ -11915,7 +12380,7 @@ ${body2}`));
|
|
|
11915
12380
|
case "mcp": {
|
|
11916
12381
|
const action = rest[0]?.toLowerCase();
|
|
11917
12382
|
if (action === "add") {
|
|
11918
|
-
const parsed =
|
|
12383
|
+
const parsed = parseMcpAddCommand(rawRest.replace(/^\S+\s*/, ""));
|
|
11919
12384
|
if (!parsed.ok) {
|
|
11920
12385
|
const problem = mcpAddProblem(strings, parsed.error);
|
|
11921
12386
|
notice(
|
|
@@ -12058,7 +12523,7 @@ ${strings.mcpAddUsage}`
|
|
|
12058
12523
|
}
|
|
12059
12524
|
if (rawRest.toLowerCase() === "clipboard") {
|
|
12060
12525
|
if (attachmentsRef.current.length >= MAX_ATTACHMENTS) {
|
|
12061
|
-
notice("warn",
|
|
12526
|
+
notice("warn", strings.attachmentsLimit(MAX_ATTACHMENTS));
|
|
12062
12527
|
return;
|
|
12063
12528
|
}
|
|
12064
12529
|
if (clipboardPasteTask.current) return;
|
|
@@ -12088,7 +12553,7 @@ ${strings.mcpAddUsage}`
|
|
|
12088
12553
|
return;
|
|
12089
12554
|
}
|
|
12090
12555
|
if (attachmentsRef.current.length >= MAX_ATTACHMENTS) {
|
|
12091
|
-
notice("warn",
|
|
12556
|
+
notice("warn", strings.attachmentsLimit(MAX_ATTACHMENTS));
|
|
12092
12557
|
return;
|
|
12093
12558
|
}
|
|
12094
12559
|
try {
|
|
@@ -12116,7 +12581,7 @@ ${strings.mcpAddUsage}`
|
|
|
12116
12581
|
history.current = result.history;
|
|
12117
12582
|
setPromptHistory(inputHistoryFromMessages(result.history));
|
|
12118
12583
|
setTranscript(
|
|
12119
|
-
pushNotice(fromHistory(result.history), "info", strings.compactDone(result.removedMessages, result.clearedToolResults ?? 0, result.tokensBefore, result.tokensAfter))
|
|
12584
|
+
pushNotice(fromHistory(result.history, runtime.summarizeToolCall), "info", strings.compactDone(result.removedMessages, result.clearedToolResults ?? 0, result.tokensBefore, result.tokensAfter))
|
|
12120
12585
|
);
|
|
12121
12586
|
setTranscriptRevision((revision) => revision + 1);
|
|
12122
12587
|
await runtime.persist(result.history);
|
|
@@ -12185,28 +12650,47 @@ ${strings.mcpAddUsage}`
|
|
|
12185
12650
|
return;
|
|
12186
12651
|
}
|
|
12187
12652
|
case "bell": {
|
|
12188
|
-
const
|
|
12653
|
+
const setting = rawRest.toLowerCase();
|
|
12654
|
+
if (setting !== "" && setting !== "on" && setting !== "off") {
|
|
12655
|
+
notice("warn", "/bell [on|off]");
|
|
12656
|
+
return;
|
|
12657
|
+
}
|
|
12658
|
+
const next = setting === "on" ? true : setting === "off" ? false : !runtime.getBell();
|
|
12189
12659
|
await runtime.setBell(next);
|
|
12190
12660
|
notice("info", strings.bellState(next));
|
|
12191
12661
|
return;
|
|
12192
12662
|
}
|
|
12193
12663
|
case "thinking": {
|
|
12194
|
-
const
|
|
12664
|
+
const setting = rawRest.toLowerCase();
|
|
12665
|
+
if (setting !== "" && setting !== "on" && setting !== "off") {
|
|
12666
|
+
notice("warn", "/thinking [on|off]");
|
|
12667
|
+
return;
|
|
12668
|
+
}
|
|
12669
|
+
const next = setting === "" ? !runtime.getThinking() : setting === "on";
|
|
12195
12670
|
await runtime.setThinking(next);
|
|
12196
12671
|
notice("info", strings.reasoning(next));
|
|
12197
12672
|
forceRender((n) => n + 1);
|
|
12198
12673
|
return;
|
|
12199
12674
|
}
|
|
12200
12675
|
case "memory": {
|
|
12201
|
-
|
|
12202
|
-
|
|
12203
|
-
|
|
12204
|
-
await
|
|
12205
|
-
|
|
12206
|
-
|
|
12676
|
+
const sub = rawRest.split(/\s+/)[0] ?? "";
|
|
12677
|
+
const action = sub.toLowerCase();
|
|
12678
|
+
if (action === "clear") {
|
|
12679
|
+
if (!await ask2(strings.memoryClearAsk, void 0, true)) return;
|
|
12680
|
+
await runtime.clearMemory();
|
|
12681
|
+
} else if (action === "set") {
|
|
12682
|
+
const text = rawRest.slice(sub.length).trim();
|
|
12683
|
+
if (!text) {
|
|
12684
|
+
notice("warn", strings.memoryUsage);
|
|
12685
|
+
return;
|
|
12686
|
+
}
|
|
12687
|
+
if (runtime.readMemory() && !await ask2(strings.memoryReplaceAsk, void 0, true)) return;
|
|
12688
|
+
await runtime.saveMemory(text);
|
|
12689
|
+
} else if (action !== "" && action !== "show") {
|
|
12690
|
+
notice("warn", strings.memoryUsage);
|
|
12207
12691
|
return;
|
|
12208
12692
|
}
|
|
12209
|
-
notice("info", runtime.readMemory() ||
|
|
12693
|
+
notice("info", runtime.readMemory() || strings.memoryEmpty);
|
|
12210
12694
|
return;
|
|
12211
12695
|
}
|
|
12212
12696
|
case "effort": {
|
|
@@ -12224,7 +12708,7 @@ ${strings.mcpAddUsage}`
|
|
|
12224
12708
|
case "budget": {
|
|
12225
12709
|
const current = runtime.getMaxTokensPerTurn();
|
|
12226
12710
|
if (rest[0]) {
|
|
12227
|
-
const tokens2 =
|
|
12711
|
+
const tokens2 = rest[0] === "0" ? 0 : parseContextTokens(rest[0]) ?? Number.NaN;
|
|
12228
12712
|
if (tokens2 === 0) {
|
|
12229
12713
|
await runtime.setMaxTokensPerTurn(0);
|
|
12230
12714
|
notice("info", strings.budgetUnlimited);
|
|
@@ -12298,7 +12782,13 @@ ${strings.mcpAddUsage}`
|
|
|
12298
12782
|
notice("warn", strings.noModels);
|
|
12299
12783
|
return;
|
|
12300
12784
|
}
|
|
12301
|
-
const
|
|
12785
|
+
const typed = rawRest.toLowerCase();
|
|
12786
|
+
const direct = typed ? items.find((item) => item.key.toLowerCase() === typed) ?? (() => {
|
|
12787
|
+
const partial = items.filter((item) => item.key.toLowerCase().includes(typed));
|
|
12788
|
+
return partial.length === 1 ? partial[0] : void 0;
|
|
12789
|
+
})() : void 0;
|
|
12790
|
+
if (typed && !direct) notice("warn", strings.modelNoMatch(rawRest));
|
|
12791
|
+
const choice = direct?.key ?? await pick(strings.titleModel, items);
|
|
12302
12792
|
if (!choice) return;
|
|
12303
12793
|
await runtime.setModelRef(choice);
|
|
12304
12794
|
notice("info", strings.modelSet(choice));
|
|
@@ -12327,74 +12817,57 @@ ${strings.mcpAddUsage}`
|
|
|
12327
12817
|
}
|
|
12328
12818
|
case "prompt": {
|
|
12329
12819
|
const sub = rest[0]?.toLowerCase();
|
|
12820
|
+
const savePromptAs = async (name, body2) => {
|
|
12821
|
+
if (await runtime.promptExists(name) && !await ask2(strings.promptOverwriteAsk(name), void 0, true)) {
|
|
12822
|
+
return;
|
|
12823
|
+
}
|
|
12824
|
+
await runtime.savePrompt(name, body2);
|
|
12825
|
+
notice("info", strings.promptSaved(name));
|
|
12826
|
+
};
|
|
12330
12827
|
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));
|
|
12828
|
+
const recent = recentUserMessages(history.current, 20).reverse();
|
|
12829
|
+
if (recent.length === 0) {
|
|
12830
|
+
notice("warn", strings.promptNothing);
|
|
12340
12831
|
return;
|
|
12341
12832
|
}
|
|
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.`);
|
|
12833
|
+
const typedName = rest.slice(1).join(" ").trim();
|
|
12834
|
+
if (typedName) {
|
|
12835
|
+
await savePromptAs(typedName, lastUserText(history.current));
|
|
12368
12836
|
return;
|
|
12369
12837
|
}
|
|
12370
|
-
|
|
12838
|
+
const choice2 = await pick(
|
|
12839
|
+
strings.titlePrompts,
|
|
12840
|
+
recent.map((text, index) => ({
|
|
12841
|
+
key: String(index),
|
|
12842
|
+
label: text.length > 60 ? `${text.slice(0, 60)}\u2026` : text
|
|
12843
|
+
}))
|
|
12844
|
+
);
|
|
12845
|
+
if (!choice2) return;
|
|
12846
|
+
const body2 = recent[Number(choice2)];
|
|
12847
|
+
if (!body2) return;
|
|
12848
|
+
const name = (await promptInput(strings.promptNameTitle))?.trim();
|
|
12849
|
+
if (!name) return;
|
|
12850
|
+
await savePromptAs(name, body2);
|
|
12371
12851
|
return;
|
|
12372
12852
|
}
|
|
12373
12853
|
if (sub === "delete") {
|
|
12374
|
-
|
|
12375
|
-
if (name) {
|
|
12376
|
-
|
|
12377
|
-
|
|
12378
|
-
notice("info", strings.
|
|
12379
|
-
|
|
12380
|
-
notice("error", error instanceof Error ? error.message : String(error));
|
|
12854
|
+
let name = rest.slice(1).join(" ").trim() || void 0;
|
|
12855
|
+
if (!name) {
|
|
12856
|
+
const items2 = await runtime.listPromptItems();
|
|
12857
|
+
if (items2.length === 0) {
|
|
12858
|
+
notice("info", strings.promptsEmpty);
|
|
12859
|
+
return;
|
|
12381
12860
|
}
|
|
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));
|
|
12861
|
+
name = await pick(strings.titlePrompts, items2) ?? void 0;
|
|
12862
|
+
if (!name) return;
|
|
12397
12863
|
}
|
|
12864
|
+
if (!await ask2(strings.promptDeleteAsk(name), void 0, true)) return;
|
|
12865
|
+
await runtime.deletePrompt(name);
|
|
12866
|
+
notice("info", strings.promptDeleted(name));
|
|
12867
|
+
return;
|
|
12868
|
+
}
|
|
12869
|
+
if (rawRest && await runtime.promptExists(rawRest)) {
|
|
12870
|
+
setInput(await runtime.readPrompt(rawRest));
|
|
12398
12871
|
return;
|
|
12399
12872
|
}
|
|
12400
12873
|
const items = await runtime.listPromptItems();
|
|
@@ -12504,11 +12977,10 @@ Rename the file if you want a different name.`);
|
|
|
12504
12977
|
const tryQueueAutomaticAttachment = useCallback(
|
|
12505
12978
|
(requestedPath) => {
|
|
12506
12979
|
if (!looksLikeAttachmentPath(requestedPath)) return Promise.resolve(false);
|
|
12507
|
-
|
|
12508
|
-
|
|
12509
|
-
}
|
|
12980
|
+
const running = automaticAttachmentTask.current;
|
|
12981
|
+
if (running) return running.then(() => tryQueueAutomaticAttachmentRef.current(requestedPath));
|
|
12510
12982
|
if (attachmentsRef.current.length >= MAX_ATTACHMENTS) {
|
|
12511
|
-
notice("warn",
|
|
12983
|
+
notice("warn", strings.attachmentsLimit(MAX_ATTACHMENTS));
|
|
12512
12984
|
return Promise.resolve(true);
|
|
12513
12985
|
}
|
|
12514
12986
|
const paths = splitAttachmentPaths(requestedPath) ?? [requestedPath];
|
|
@@ -12521,7 +12993,7 @@ Rename the file if you want a different name.`);
|
|
|
12521
12993
|
if (automaticAttachmentTask.current !== task) return true;
|
|
12522
12994
|
if (!block) continue;
|
|
12523
12995
|
if (!appendAttachment(block)) {
|
|
12524
|
-
notice("warn",
|
|
12996
|
+
notice("warn", strings.attachmentsLimit(MAX_ATTACHMENTS));
|
|
12525
12997
|
return true;
|
|
12526
12998
|
}
|
|
12527
12999
|
added += 1;
|
|
@@ -12543,6 +13015,8 @@ Rename the file if you want a different name.`);
|
|
|
12543
13015
|
},
|
|
12544
13016
|
[appendAttachment, notice, runtime, strings]
|
|
12545
13017
|
);
|
|
13018
|
+
const tryQueueAutomaticAttachmentRef = useRef5(tryQueueAutomaticAttachment);
|
|
13019
|
+
tryQueueAutomaticAttachmentRef.current = tryQueueAutomaticAttachment;
|
|
12546
13020
|
const listWorkspaceFiles = useCallback(
|
|
12547
13021
|
(query) => runtime.listWorkspaceFiles(query).catch(() => []),
|
|
12548
13022
|
[runtime]
|
|
@@ -12550,13 +13024,13 @@ Rename the file if you want a different name.`);
|
|
|
12550
13024
|
const pasteClipboardImage = useCallback(() => {
|
|
12551
13025
|
if (clipboardPasteTask.current) return;
|
|
12552
13026
|
if (attachmentsRef.current.length >= MAX_ATTACHMENTS) {
|
|
12553
|
-
notice("warn",
|
|
13027
|
+
notice("warn", strings.attachmentsLimit(MAX_ATTACHMENTS));
|
|
12554
13028
|
return;
|
|
12555
13029
|
}
|
|
12556
13030
|
const task = runtime.loadClipboardImage().then((block) => {
|
|
12557
13031
|
if (clipboardPasteTask.current !== task) return;
|
|
12558
13032
|
if (!appendAttachment(block)) {
|
|
12559
|
-
notice("warn",
|
|
13033
|
+
notice("warn", strings.attachmentsLimit(MAX_ATTACHMENTS));
|
|
12560
13034
|
return;
|
|
12561
13035
|
}
|
|
12562
13036
|
notice("info", strings.attachmentAdded(attachmentLabel(block) ?? "clipboard image"));
|
|
@@ -12612,11 +13086,15 @@ Rename the file if you want a different name.`);
|
|
|
12612
13086
|
const queuedAttachments = attachmentsRef.current;
|
|
12613
13087
|
if (!text && queuedAttachments.length === 0) return;
|
|
12614
13088
|
const mentioned = [];
|
|
12615
|
-
for (const
|
|
13089
|
+
for (const path16 of extractMentions(text)) {
|
|
12616
13090
|
if (queuedAttachments.length + mentioned.length >= MAX_ATTACHMENTS) break;
|
|
12617
13091
|
try {
|
|
12618
|
-
|
|
12619
|
-
|
|
13092
|
+
const block = await runtime.loadMentionAttachment(path16);
|
|
13093
|
+
if (!block) continue;
|
|
13094
|
+
mentioned.push(block);
|
|
13095
|
+
notice("info", strings.mentionAttached(attachmentLabel(block) ?? path16));
|
|
13096
|
+
} catch (error) {
|
|
13097
|
+
if (error instanceof Error && !error.code) notice("warn", error.message);
|
|
12620
13098
|
}
|
|
12621
13099
|
}
|
|
12622
13100
|
if (generation !== attachmentGeneration.current) return;
|
|
@@ -12897,6 +13375,12 @@ function mcpAddProblem(strings, error) {
|
|
|
12897
13375
|
return strings.mcpAddInvalidUrl;
|
|
12898
13376
|
case "http-args":
|
|
12899
13377
|
return strings.mcpAddHttpArgs;
|
|
13378
|
+
case "invalid-env":
|
|
13379
|
+
return strings.mcpAddInvalidEnv;
|
|
13380
|
+
case "invalid-header":
|
|
13381
|
+
return strings.mcpAddInvalidHeader;
|
|
13382
|
+
case "option-mismatch":
|
|
13383
|
+
return strings.mcpAddOptionMismatch;
|
|
12900
13384
|
case "usage":
|
|
12901
13385
|
return strings.mcpAddUsage;
|
|
12902
13386
|
}
|
|
@@ -12911,6 +13395,16 @@ function mcpStateLine(strings, state) {
|
|
|
12911
13395
|
function formatBalances(balances) {
|
|
12912
13396
|
return balances.map((item) => `${item.amount} ${item.currency}`).join(" \xB7 ");
|
|
12913
13397
|
}
|
|
13398
|
+
function attachmentPayload(block) {
|
|
13399
|
+
if (block.type === "image") return block.data.length;
|
|
13400
|
+
if (block.type === "file") return Buffer.byteLength(block.text);
|
|
13401
|
+
return 0;
|
|
13402
|
+
}
|
|
13403
|
+
function sameAttachment(a, b) {
|
|
13404
|
+
if (a.type === "image" && b.type === "image") return a.name === b.name && a.data === b.data;
|
|
13405
|
+
if (a.type === "file" && b.type === "file") return a.name === b.name && a.text === b.text;
|
|
13406
|
+
return false;
|
|
13407
|
+
}
|
|
12914
13408
|
function lastUserText(messages) {
|
|
12915
13409
|
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
|
12916
13410
|
const message = messages[index];
|
|
@@ -12991,12 +13485,9 @@ program.name("kitcode").description("Terminal coding agent with a config you nev
|
|
|
12991
13485
|
if (options.mode && !MODES2.includes(options.mode)) {
|
|
12992
13486
|
throw new Error(`Unknown mode "${options.mode}". Use normal, accept or plan.`);
|
|
12993
13487
|
}
|
|
12994
|
-
|
|
12995
|
-
if (cwd) {
|
|
12996
|
-
|
|
12997
|
-
cwd = resolve2(cwd);
|
|
12998
|
-
} catch {
|
|
12999
|
-
}
|
|
13488
|
+
const cwd = options.cwd ? resolve2(options.cwd) : void 0;
|
|
13489
|
+
if (cwd && !statSync(cwd, { throwIfNoEntry: false })?.isDirectory()) {
|
|
13490
|
+
throw new Error(`--cwd is not a directory: ${cwd}`);
|
|
13000
13491
|
}
|
|
13001
13492
|
await startTui({
|
|
13002
13493
|
continueSession: options.continue,
|
|
@@ -13049,8 +13540,9 @@ prompts.command("list").description("list saved prompts").action(async () => {
|
|
|
13049
13540
|
console.log(`${prompt.slug} ${prompt.name}${prompt.description ? ` \u2014 ${prompt.description}` : ""}`);
|
|
13050
13541
|
}
|
|
13051
13542
|
});
|
|
13052
|
-
prompts.command("rm").description("delete a saved prompt").argument("<
|
|
13053
|
-
|
|
13543
|
+
prompts.command("rm").description("delete a saved prompt").argument("<name>").action(async (name) => {
|
|
13544
|
+
const slug = promptSlug(name);
|
|
13545
|
+
console.log(slug && await deletePrompt(slug) ? `deleted ${name}` : `no such prompt: ${name}`);
|
|
13054
13546
|
});
|
|
13055
13547
|
program.parseAsync(process.argv).catch((error) => {
|
|
13056
13548
|
console.error(error instanceof Error ? error.message : String(error));
|