@botconnector/bccli 0.4.1 → 0.4.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{acp-3PHBAI64.js → acp-NU4HZHQO.js} +30 -6
- package/dist/acp.d.ts +1 -0
- package/dist/agent.d.ts +5 -1
- package/dist/args-NRTZWWEV.js +13 -0
- package/dist/args.d.ts +2 -0
- package/dist/checkpoints.d.ts +1 -0
- package/dist/{chunk-MFHCQQEN.js → chunk-3I5BW3ZT.js} +3 -2
- package/dist/{chunk-UZWJQF67.js → chunk-3WHI4LN3.js} +2 -1
- package/dist/{chunk-BGXNR74O.js → chunk-4W5SKQDC.js} +152 -31
- package/dist/{chunk-BC7UXFE4.js → chunk-6AUROP2E.js} +64 -3
- package/dist/{chunk-24JN75FZ.js → chunk-6M55UPES.js} +2 -2
- package/dist/{chunk-XV7ZVL4J.js → chunk-6UO5GX55.js} +26 -12
- package/dist/{chunk-75BECX4F.js → chunk-7I2ZGPUS.js} +4 -4
- package/dist/{chunk-BKGFA2PT.js → chunk-FUJGHCWN.js} +1 -1
- package/dist/{chunk-GN4W3T6E.js → chunk-GWVLPN4B.js} +4 -4
- package/dist/{chunk-JHPGOAXU.js → chunk-HQW4BROS.js} +4 -5
- package/dist/{chunk-45SDLMTV.js → chunk-JXM6REER.js} +1 -1
- package/dist/{chunk-BLOYFIAD.js → chunk-KPTML72Q.js} +1 -1
- package/dist/{chunk-WI22M2ID.js → chunk-NQOGVP3I.js} +2 -2
- package/dist/{chunk-QZKLZELZ.js → chunk-OK2USAXX.js} +28 -4
- package/dist/{chunk-PZRKU4UM.js → chunk-QNUJHKEL.js} +659 -162
- package/dist/{chunk-FLQPE4S5.js → chunk-RX4UQX43.js} +4 -1
- package/dist/{chunk-R5RE46CW.js → chunk-WDX62DFH.js} +40 -6
- package/dist/{chunk-MHIYLXJC.js → chunk-WFZKJY3R.js} +8 -4
- package/dist/{chunk-WNACAUDK.js → chunk-XTOATEI6.js} +66 -9
- package/dist/{chunk-BXQQCUAC.js → chunk-Z5NI5FIK.js} +7 -7
- package/dist/cli.js +60 -43
- package/dist/{completion-HPPFDAPB.js → completion-D2BSBTQO.js} +1 -1
- package/dist/{config-DIDLDONU.js → config-ENDRMOTP.js} +3 -3
- package/dist/{config-IU76CPK2.js → config-LAINORU5.js} +2 -2
- package/dist/config.d.ts +2 -0
- package/dist/{ghBotCli-E3VPHNZY.js → ghBotCli-GCIUUMKQ.js} +21 -20
- package/dist/{integrationCli-Q2YPDTPG.js → integrationCli-HXCSEWH2.js} +2 -2
- package/dist/{login-FP3BB44A.js → login-YXSSB2HQ.js} +4 -4
- package/dist/lsp.d.ts +26 -0
- package/dist/mcp/manager.d.ts +18 -0
- package/dist/{mcpCli-SNBDUWJN.js → mcpCli-SETTCL3K.js} +6 -6
- package/dist/{modelsCli-CZ2HNGKR.js → modelsCli-656AKYZD.js} +6 -6
- package/dist/{oauthLogin-6FWQABJ7.js → oauthLogin-XWY7OHCG.js} +1 -1
- package/dist/{pluginsCli-LPIUUDWB.js → pluginsCli-ZQNKWZHJ.js} +2 -2
- package/dist/{prCli-5IMV3VNQ.js → prCli-6PUWTMTP.js} +20 -4
- package/dist/prCli.d.ts +13 -0
- package/dist/print.d.ts +2 -0
- package/dist/provider.d.ts +5 -0
- package/dist/providerCli-V2576JXE.js +15 -0
- package/dist/sdk.d.ts +5 -1
- package/dist/sdk.js +1003 -230
- package/dist/{serve-KWOECK5W.js → serve-K5AH4MGO.js} +3 -3
- package/dist/{serve-X2ZV4S5U.js → serve-XPAGXIGS.js} +17 -17
- package/dist/session-V2VCAF7E.js +12 -0
- package/dist/session.d.ts +3 -0
- package/dist/setup-HVVHOIL4.js +24 -0
- package/dist/slash/info.d.ts +1 -0
- package/dist/{statsCli-CF3R7ATL.js → statsCli-XR6BEZQQ.js} +2 -2
- package/dist/tokenBudget.d.ts +1 -1
- package/dist/tools/diagnostics.d.ts +8 -0
- package/dist/tools/edit.d.ts +7 -2
- package/dist/tools/grep.d.ts +14 -2
- package/dist/tools/ignore.d.ts +2 -2
- package/dist/tools/paths.d.ts +2 -0
- package/dist/tools/sandbox.d.ts +3 -0
- package/dist/{transferCli-YUFPI6AH.js → transferCli-C6XS6QYH.js} +8 -6
- package/dist/ui/complete.d.ts +5 -0
- package/dist/ui/vim.d.ts +1 -1
- package/dist/{ui-IVIFNUCC.js → ui-43C6Y57K.js} +199 -54
- package/dist/{update-TZ2G6KQ3.js → update-AVZO4XID.js} +2 -2
- package/package.json +1 -1
- package/dist/args-W6BNKY7K.js +0 -13
- package/dist/providerCli-NMUFXAAS.js +0 -15
- package/dist/session-GKEXDJKZ.js +0 -12
- package/dist/setup-OV3NWQN5.js +0 -24
package/dist/sdk.js
CHANGED
|
@@ -500,7 +500,8 @@ Opsi:
|
|
|
500
500
|
"gh is not available ({error}); install GitHub CLI first": "gh tidak tersedia ({error}); pasang GitHub CLI dulu",
|
|
501
501
|
"gh pr checkout {n} failed (exit {code})": "gh pr checkout {n} gagal (exit {code})",
|
|
502
502
|
"checked out PR #{n}": "checkout PR #{n} selesai",
|
|
503
|
-
"--allow-all: the PR task runs every tool without asking": "--allow-all: tugas PR menjalankan semua alat tanpa bertanya"
|
|
503
|
+
"--allow-all: the PR task runs every tool without asking": "--allow-all: tugas PR menjalankan semua alat tanpa bertanya",
|
|
504
|
+
"view or change configuration settings": "lihat atau ubah pengaturan konfigurasi"
|
|
504
505
|
};
|
|
505
506
|
|
|
506
507
|
// src/i18n.ts
|
|
@@ -689,8 +690,11 @@ function loadConfig(cwd, env = process.env) {
|
|
|
689
690
|
const project = readJsonConfig(join(cwd, ".bccli", "config.json"));
|
|
690
691
|
const known = { ...DEFAULT_CONFIG.providers, ...global.providers };
|
|
691
692
|
const fromProject = untrustedProviders(project, known);
|
|
693
|
+
const rawFallbacks = project.fallbackModels ?? global.fallbackModels;
|
|
694
|
+
const fallbackModels = Array.isArray(rawFallbacks) ? rawFallbacks.filter((m) => typeof m === "string" && m.includes("/")) : void 0;
|
|
692
695
|
return {
|
|
693
696
|
model: project.model ?? global.model ?? DEFAULT_CONFIG.model,
|
|
697
|
+
...fallbackModels?.length ? { fallbackModels } : {},
|
|
694
698
|
permissionMode: global.permissionMode ?? DEFAULT_CONFIG.permissionMode,
|
|
695
699
|
reasoning: global.reasoning ?? DEFAULT_CONFIG.reasoning,
|
|
696
700
|
thinkingVisible: global.thinkingVisible === true,
|
|
@@ -774,7 +778,7 @@ function resolveModel(config, modelRef, env = process.env) {
|
|
|
774
778
|
}
|
|
775
779
|
|
|
776
780
|
// src/permissions.ts
|
|
777
|
-
import { isAbsolute, relative, resolve
|
|
781
|
+
import { isAbsolute, relative, resolve } from "path";
|
|
778
782
|
var MODE_ORDER = ["default", "acceptEdits", "plan", "allowAll"];
|
|
779
783
|
var ALWAYS_ASK_COMMANDS = /* @__PURE__ */ new Set(["sudo", "sh", "bash", "zsh", "su"]);
|
|
780
784
|
function isAlwaysAsk(segment) {
|
|
@@ -832,7 +836,9 @@ function commandKey(segment) {
|
|
|
832
836
|
function requiresSensitiveToolApproval(req) {
|
|
833
837
|
const target = req.target.trim();
|
|
834
838
|
if (req.kind === "read" || req.kind === "edit") {
|
|
835
|
-
return /(?:^|[/\\])(?:\.env(
|
|
839
|
+
return /(?:^|[\s/\\])(?:\.env(?:\.[^\s/\\]+)?|\.ssh|\.aws|\.config[/\\]gcloud|\.npmrc|\.netrc|\.git-credentials|(?:secrets?|credentials?)(?:\.[a-z0-9_-]+)?)(?:[\s/\\]|$)/i.test(
|
|
840
|
+
target
|
|
841
|
+
) || /(?:id_rsa|id_ed25519|\.pem|\.p12|\.key)(?:\s|$)/i.test(target);
|
|
836
842
|
}
|
|
837
843
|
if (req.kind === "bash") {
|
|
838
844
|
return /\b(?:rm\s+-[a-z]*r[a-z]*f[a-z]*\s|rm\s+-[a-z]*f[a-z]*r[a-z]*\s|mkfs(?:\.|\s)|wipefs\b|shred\s|git\s+push\s+(?:--force|-f)\b|docker\s+(?:system\s+prune|volume\s+rm)|kubectl\s+delete\b|drop\s+(?:database|table)|npm\s+publish\b)/i.test(target) || /\b(?:deploy-service\.sh|aws\s+secretsmanager|cat\s+[^;&|]*\.env\b)/i.test(target);
|
|
@@ -857,7 +863,8 @@ var Permissions = class {
|
|
|
857
863
|
editInProject(target) {
|
|
858
864
|
const rel = relative(this.cwd, resolve(this.cwd, target));
|
|
859
865
|
if (!rel || rel.startsWith("..") || isAbsolute(rel)) return false;
|
|
860
|
-
|
|
866
|
+
const parts = rel.split(/[/\\]/).map((p) => p.toLowerCase());
|
|
867
|
+
return !parts.includes(".git");
|
|
861
868
|
}
|
|
862
869
|
/** Rules that "[a] allow for this session" would add; undefined when it must always be asked. */
|
|
863
870
|
rulesFor(req) {
|
|
@@ -875,7 +882,7 @@ var Permissions = class {
|
|
|
875
882
|
if (/`|\$\(|[<>]|^\s*[({]/.test(req.target)) return void 0;
|
|
876
883
|
const segments = req.target.split(/&&|\|\||[;&|\n]/).map((s) => s.trim()).filter(Boolean);
|
|
877
884
|
if (!segments.length) return void 0;
|
|
878
|
-
if (segments.some(isAlwaysAsk)) return void 0;
|
|
885
|
+
if (segments.some((s) => /^[({]/.test(s) || isAlwaysAsk(s))) return void 0;
|
|
879
886
|
return [...new Set(segments.map((s) => `bash(${commandKey(s)})`))];
|
|
880
887
|
}
|
|
881
888
|
if (req.kind === "mcp") return [`mcp(${req.tool})`];
|
|
@@ -953,9 +960,12 @@ var LOOPBACK_BIND = /* @__PURE__ */ new Set(["127.0.0.1", "localhost", "::1"]);
|
|
|
953
960
|
|
|
954
961
|
// src/args.ts
|
|
955
962
|
function parseCliArgs(argv) {
|
|
956
|
-
const expanded = argv.map(
|
|
957
|
-
|
|
958
|
-
|
|
963
|
+
const expanded = argv.map((a, i) => {
|
|
964
|
+
const nextIsFlagOrEnd = i + 1 >= argv.length || argv[i + 1].startsWith("-");
|
|
965
|
+
if (a === "--models" && nextIsFlagOrEnd) return "--models=all";
|
|
966
|
+
if (a === "--project" && nextIsFlagOrEnd) return "--project=";
|
|
967
|
+
return a;
|
|
968
|
+
});
|
|
959
969
|
const { values, positionals } = parseArgs({
|
|
960
970
|
args: expanded,
|
|
961
971
|
allowPositionals: true,
|
|
@@ -987,7 +997,8 @@ function parseCliArgs(argv) {
|
|
|
987
997
|
tools: { type: "string" },
|
|
988
998
|
models: { type: "string" },
|
|
989
999
|
project: { type: "string" },
|
|
990
|
-
repo: { type: "string" }
|
|
1000
|
+
repo: { type: "string" },
|
|
1001
|
+
"fallback-model": { type: "string" }
|
|
991
1002
|
}
|
|
992
1003
|
});
|
|
993
1004
|
const reasoning = values.reasoning;
|
|
@@ -1061,7 +1072,8 @@ function parseCliArgs(argv) {
|
|
|
1061
1072
|
tools,
|
|
1062
1073
|
models,
|
|
1063
1074
|
project: values.project,
|
|
1064
|
-
repo: values.repo
|
|
1075
|
+
repo: values.repo,
|
|
1076
|
+
fallbackModel: values["fallback-model"]
|
|
1065
1077
|
};
|
|
1066
1078
|
}
|
|
1067
1079
|
|
|
@@ -1153,14 +1165,14 @@ function detectRepetition(text) {
|
|
|
1153
1165
|
return -1;
|
|
1154
1166
|
}
|
|
1155
1167
|
function sleep(ms, signal) {
|
|
1156
|
-
return new Promise((
|
|
1168
|
+
return new Promise((resolve5, reject) => {
|
|
1157
1169
|
const onAbort = () => {
|
|
1158
1170
|
clearTimeout(timer);
|
|
1159
1171
|
reject(signal?.reason);
|
|
1160
1172
|
};
|
|
1161
1173
|
const timer = setTimeout(() => {
|
|
1162
1174
|
signal?.removeEventListener("abort", onAbort);
|
|
1163
|
-
|
|
1175
|
+
resolve5();
|
|
1164
1176
|
}, ms);
|
|
1165
1177
|
signal?.addEventListener("abort", onAbort, { once: true });
|
|
1166
1178
|
});
|
|
@@ -1230,7 +1242,7 @@ async function readStream(res, onText, onThinking) {
|
|
|
1230
1242
|
}
|
|
1231
1243
|
current2 = target;
|
|
1232
1244
|
if (tc.id) target.id = tc.id;
|
|
1233
|
-
if (tc.function?.name &&
|
|
1245
|
+
if (tc.function?.name && target.name !== tc.function.name) target.name += tc.function.name;
|
|
1234
1246
|
if (tc.function?.arguments) target.arguments += tc.function.arguments;
|
|
1235
1247
|
}
|
|
1236
1248
|
};
|
|
@@ -1437,7 +1449,16 @@ function responsesFinishReason(items, toolCallCount, incompleteReason) {
|
|
|
1437
1449
|
function hasPendingProgrammaticReplay(messages) {
|
|
1438
1450
|
let pending = false;
|
|
1439
1451
|
for (const message of messages) {
|
|
1452
|
+
if (message.role === "user" && typeof message.content === "string") {
|
|
1453
|
+
pending = false;
|
|
1454
|
+
continue;
|
|
1455
|
+
}
|
|
1456
|
+
if (message.role === "tool" && message.content === "Cancelled by the user.") {
|
|
1457
|
+
pending = false;
|
|
1458
|
+
continue;
|
|
1459
|
+
}
|
|
1440
1460
|
if (message.role !== "assistant") continue;
|
|
1461
|
+
if (message.content) pending = false;
|
|
1441
1462
|
for (const item of message.responses_output_items ?? []) {
|
|
1442
1463
|
if (item.type === "program") pending = true;
|
|
1443
1464
|
else if (item.type === "message" && pending) pending = false;
|
|
@@ -1511,7 +1532,7 @@ async function readResponsesStream(res, onText, onThinking) {
|
|
|
1511
1532
|
calls.set(index, {
|
|
1512
1533
|
id: String(event.item.call_id || current2?.id || event.item.id || `call_${index}`),
|
|
1513
1534
|
name: String(event.item.name || current2?.name || ""),
|
|
1514
|
-
arguments: String(event.item.arguments
|
|
1535
|
+
arguments: String(event.item.arguments || current2?.arguments || ""),
|
|
1515
1536
|
...programCaller(event.item) ?? current2?.caller ? { caller: programCaller(event.item) ?? current2?.caller } : {}
|
|
1516
1537
|
});
|
|
1517
1538
|
}
|
|
@@ -1747,7 +1768,7 @@ async function readResponsesWebSocketTurn(events, ws, onText, onThinking, signal
|
|
|
1747
1768
|
calls.set(index, {
|
|
1748
1769
|
id: String(event.item.call_id || current2?.id || event.item.id || `call_${index}`),
|
|
1749
1770
|
name: String(event.item.name || current2?.name || ""),
|
|
1750
|
-
arguments: String(event.item.arguments
|
|
1771
|
+
arguments: String(event.item.arguments || current2?.arguments || ""),
|
|
1751
1772
|
...programCaller(event.item) ?? current2?.caller ? { caller: programCaller(event.item) ?? current2?.caller } : {}
|
|
1752
1773
|
});
|
|
1753
1774
|
}
|
|
@@ -1808,7 +1829,7 @@ function createProvider(options) {
|
|
|
1808
1829
|
const isBotConnectorCloud = /^https:\/\/api\.botconnector\.id\/v1\/?$/.test(baseURL) && apiKey?.startsWith("bc_live_");
|
|
1809
1830
|
if (isBotConnectorCloud) {
|
|
1810
1831
|
headers["x-botconnector-client"] = "bccli";
|
|
1811
|
-
headers["x-botconnector-client-version"] = "0.4.
|
|
1832
|
+
headers["x-botconnector-client-version"] = "0.4.3";
|
|
1812
1833
|
}
|
|
1813
1834
|
const useNativeResponses = isBotConnectorCloud && providerId === "bc-cloud" && model === "gpt-6-luna";
|
|
1814
1835
|
const useResponsesWebSocket = useNativeResponses && Boolean(apiKey) && (fetchOverride === void 0 || responsesWebSocketFactory !== void 0);
|
|
@@ -1839,7 +1860,7 @@ function createProvider(options) {
|
|
|
1839
1860
|
baseURL: baseURL.replace(/\/+$/, ""),
|
|
1840
1861
|
defaultHeaders: {
|
|
1841
1862
|
"x-botconnector-client": "bccli",
|
|
1842
|
-
"x-botconnector-client-version": "0.4.
|
|
1863
|
+
"x-botconnector-client-version": "0.4.3"
|
|
1843
1864
|
}
|
|
1844
1865
|
});
|
|
1845
1866
|
lunaWs = responsesWebSocketFactory ? responsesWebSocketFactory(client) : new ResponsesWS(client, { reconnect: null });
|
|
@@ -1922,8 +1943,13 @@ function createProvider(options) {
|
|
|
1922
1943
|
}
|
|
1923
1944
|
async function post(path, body, signal) {
|
|
1924
1945
|
let lastError;
|
|
1946
|
+
let retryAfterWaitMs;
|
|
1925
1947
|
for (let attempt = 0; attempt < 4; attempt++) {
|
|
1926
|
-
if (attempt > 0)
|
|
1948
|
+
if (attempt > 0) {
|
|
1949
|
+
const delay = retryAfterWaitMs ?? retryDelayMs * 2 ** (attempt - 1);
|
|
1950
|
+
retryAfterWaitMs = void 0;
|
|
1951
|
+
await sleep(delay, signal);
|
|
1952
|
+
}
|
|
1927
1953
|
let res;
|
|
1928
1954
|
try {
|
|
1929
1955
|
res = await doFetch(`${baseURL}${path}`, { method: "POST", headers, body: JSON.stringify(body), signal });
|
|
@@ -1933,6 +1959,11 @@ function createProvider(options) {
|
|
|
1933
1959
|
continue;
|
|
1934
1960
|
}
|
|
1935
1961
|
if (res.ok) return res;
|
|
1962
|
+
const retryHeader = res.headers.get("retry-after");
|
|
1963
|
+
if (retryHeader) {
|
|
1964
|
+
const secs = Number(retryHeader);
|
|
1965
|
+
if (Number.isFinite(secs) && secs > 0 && secs <= 30) retryAfterWaitMs = Math.ceil(secs * 1e3);
|
|
1966
|
+
}
|
|
1936
1967
|
const text = await res.text().catch(() => "");
|
|
1937
1968
|
lastError = new ProviderError(t("{status} from {url}: {message}", { status: res.status, url: baseURL, message: errorMessage(text) }), res.status);
|
|
1938
1969
|
const deterministicQuota429 = res.status === 429 && /"code"\s*:\s*"(?:luna_monthly_quota_exhausted|plan_allowance_exhausted)"/i.test(text);
|
|
@@ -1995,6 +2026,43 @@ function createProvider(options) {
|
|
|
1995
2026
|
}
|
|
1996
2027
|
};
|
|
1997
2028
|
}
|
|
2029
|
+
function withFallbackProvider(primary, fallbacks) {
|
|
2030
|
+
return {
|
|
2031
|
+
async chat(req) {
|
|
2032
|
+
let emitted = false;
|
|
2033
|
+
const wrappedReq = {
|
|
2034
|
+
...req,
|
|
2035
|
+
onText: req.onText ? (delta) => {
|
|
2036
|
+
emitted = true;
|
|
2037
|
+
req.onText?.(delta);
|
|
2038
|
+
} : () => {
|
|
2039
|
+
emitted = true;
|
|
2040
|
+
}
|
|
2041
|
+
};
|
|
2042
|
+
try {
|
|
2043
|
+
return await primary.chat(wrappedReq);
|
|
2044
|
+
} catch (primaryError) {
|
|
2045
|
+
if (emitted || req.signal?.aborted) throw primaryError;
|
|
2046
|
+
const status = primaryError instanceof ProviderError ? primaryError.status : void 0;
|
|
2047
|
+
const canFallback = status === void 0 || status === 429 || status === 408 || status >= 500;
|
|
2048
|
+
if (!canFallback) throw primaryError;
|
|
2049
|
+
const chain = fallbacks();
|
|
2050
|
+
if (!chain.length) throw primaryError;
|
|
2051
|
+
let lastError = primaryError;
|
|
2052
|
+
for (const fb of chain) {
|
|
2053
|
+
if (req.signal?.aborted) throw lastError;
|
|
2054
|
+
try {
|
|
2055
|
+
return await fb.chat(req);
|
|
2056
|
+
} catch (fbError) {
|
|
2057
|
+
lastError = fbError;
|
|
2058
|
+
}
|
|
2059
|
+
}
|
|
2060
|
+
throw lastError;
|
|
2061
|
+
}
|
|
2062
|
+
},
|
|
2063
|
+
listModels: () => primary.listModels()
|
|
2064
|
+
};
|
|
2065
|
+
}
|
|
1998
2066
|
|
|
1999
2067
|
// src/textToolCalls.ts
|
|
2000
2068
|
var PAIR = /(?:<arg_key>\s*|`)?([A-Za-z_]\w*)(?:\s*<\/arg_key>)?`?\s*(?:=\s*`|<arg_value>)([\s\S]*?)<\/arg_value>/g;
|
|
@@ -2114,7 +2182,7 @@ function runOne(command, event, payload, opts) {
|
|
|
2114
2182
|
...payload.input !== void 0 ? { BCCLI_HOOK_INPUT: JSON.stringify(payload.input) } : {},
|
|
2115
2183
|
...payload.output !== void 0 ? { BCCLI_HOOK_OUTPUT: payload.output } : {}
|
|
2116
2184
|
};
|
|
2117
|
-
return new Promise((
|
|
2185
|
+
return new Promise((resolve5) => {
|
|
2118
2186
|
let stdout = "";
|
|
2119
2187
|
let stderr = "";
|
|
2120
2188
|
let timedOut = false;
|
|
@@ -2123,7 +2191,7 @@ function runOne(command, event, payload, opts) {
|
|
|
2123
2191
|
if (settled) return;
|
|
2124
2192
|
settled = true;
|
|
2125
2193
|
clearTimeout(timer);
|
|
2126
|
-
|
|
2194
|
+
resolve5(result);
|
|
2127
2195
|
};
|
|
2128
2196
|
let child;
|
|
2129
2197
|
const shell = defaultShell();
|
|
@@ -2139,7 +2207,7 @@ function runOne(command, event, payload, opts) {
|
|
|
2139
2207
|
child = spawn(command, { shell: true, env, cwd: opts.cwd ?? process.cwd(), stdio: ["ignore", "pipe", "pipe"] });
|
|
2140
2208
|
}
|
|
2141
2209
|
} catch (error) {
|
|
2142
|
-
|
|
2210
|
+
resolve5({ failure: error.message });
|
|
2143
2211
|
return;
|
|
2144
2212
|
}
|
|
2145
2213
|
const timer = setTimeout(() => {
|
|
@@ -2194,10 +2262,17 @@ import { z as z8 } from "zod";
|
|
|
2194
2262
|
|
|
2195
2263
|
// src/tools/bash.ts
|
|
2196
2264
|
import { spawn as spawn2 } from "child_process";
|
|
2197
|
-
import { existsSync as existsSync3 } from "fs";
|
|
2265
|
+
import { existsSync as existsSync3, writeFileSync as writeFileSync2 } from "fs";
|
|
2266
|
+
import { tmpdir } from "os";
|
|
2267
|
+
import { join as join2 } from "path";
|
|
2198
2268
|
import { z } from "zod";
|
|
2199
2269
|
|
|
2200
2270
|
// src/tools/sandbox.ts
|
|
2271
|
+
function winPathToWsl(winPath) {
|
|
2272
|
+
const normalized = winPath.replace(/\\/g, "/");
|
|
2273
|
+
const m = /^([A-Za-z]):\/(.*)$/.exec(normalized);
|
|
2274
|
+
return m ? `/mnt/${m[1].toLowerCase()}/${m[2]}` : normalized;
|
|
2275
|
+
}
|
|
2201
2276
|
function darwinProfile(cwd, network) {
|
|
2202
2277
|
const writable = [cwd, "/tmp", "/private/tmp", "/dev", "/var/folders"].map((p) => `(subpath "${p}")`).join(" ");
|
|
2203
2278
|
return [
|
|
@@ -2241,6 +2316,33 @@ function sandboxArgv(command, opts) {
|
|
|
2241
2316
|
command
|
|
2242
2317
|
];
|
|
2243
2318
|
}
|
|
2319
|
+
if (opts.platform === "win32" && (opts.wslBwrap || process.env.BCCLI_WIN32_WSL_SANDBOX === "1")) {
|
|
2320
|
+
const wslCwd = winPathToWsl(opts.cwd);
|
|
2321
|
+
return [
|
|
2322
|
+
"wsl.exe",
|
|
2323
|
+
"--",
|
|
2324
|
+
"bwrap",
|
|
2325
|
+
"--ro-bind",
|
|
2326
|
+
"/",
|
|
2327
|
+
"/",
|
|
2328
|
+
"--dev-bind",
|
|
2329
|
+
"/dev",
|
|
2330
|
+
"/dev",
|
|
2331
|
+
"--proc",
|
|
2332
|
+
"/proc",
|
|
2333
|
+
"--bind",
|
|
2334
|
+
wslCwd,
|
|
2335
|
+
wslCwd,
|
|
2336
|
+
"--tmpfs",
|
|
2337
|
+
"/tmp",
|
|
2338
|
+
"--die-with-parent",
|
|
2339
|
+
...opts.network ? [] : ["--unshare-net"],
|
|
2340
|
+
"--",
|
|
2341
|
+
"/bin/bash",
|
|
2342
|
+
"-c",
|
|
2343
|
+
command
|
|
2344
|
+
];
|
|
2345
|
+
}
|
|
2244
2346
|
return void 0;
|
|
2245
2347
|
}
|
|
2246
2348
|
|
|
@@ -2383,7 +2485,7 @@ function offlineEnv(env = process.env) {
|
|
|
2383
2485
|
};
|
|
2384
2486
|
}
|
|
2385
2487
|
function runCommand(command, opts) {
|
|
2386
|
-
return new Promise((
|
|
2488
|
+
return new Promise((resolve5) => {
|
|
2387
2489
|
const child = spawnChild(command, opts.cwd, opts.env, opts.shell ?? defaultShell());
|
|
2388
2490
|
if (!exitHookInstalled) {
|
|
2389
2491
|
process.on("exit", killAllCommands);
|
|
@@ -2396,7 +2498,14 @@ function runCommand(command, opts) {
|
|
|
2396
2498
|
let dropped = 0;
|
|
2397
2499
|
let timedOut = false;
|
|
2398
2500
|
let aborted = false;
|
|
2501
|
+
const fullChunks = [];
|
|
2502
|
+
let fullBytes = 0;
|
|
2503
|
+
const MAX_SPILL_BYTES = 10 * 1024 * 1024;
|
|
2399
2504
|
const onData = (data) => {
|
|
2505
|
+
if (fullBytes < MAX_SPILL_BYTES) {
|
|
2506
|
+
fullChunks.push(data);
|
|
2507
|
+
fullBytes += data.length;
|
|
2508
|
+
}
|
|
2400
2509
|
let s = data;
|
|
2401
2510
|
if (head.length < HALF) {
|
|
2402
2511
|
const take = s.slice(0, HALF - head.length);
|
|
@@ -2435,10 +2544,19 @@ function runCommand(command, opts) {
|
|
|
2435
2544
|
active.delete(killAsAborted);
|
|
2436
2545
|
clearTimeout(timer);
|
|
2437
2546
|
opts.signal.removeEventListener("abort", onAbort);
|
|
2547
|
+
let spillNote = "";
|
|
2548
|
+
if (dropped && fullChunks.length) {
|
|
2549
|
+
try {
|
|
2550
|
+
const spillPath = join2(tmpdir(), `bccli-bash-${Date.now()}-${Math.random().toString(36).slice(2, 8)}.log`);
|
|
2551
|
+
writeFileSync2(spillPath, fullChunks.join(""), "utf8");
|
|
2552
|
+
spillNote = `; full output saved to ${spillPath}`;
|
|
2553
|
+
} catch {
|
|
2554
|
+
}
|
|
2555
|
+
}
|
|
2438
2556
|
const output = dropped ? `${head}
|
|
2439
|
-
\u2026 [${dropped} characters truncated] \u2026
|
|
2557
|
+
\u2026 [${dropped} characters truncated${spillNote}] \u2026
|
|
2440
2558
|
${tail}` : head + tail;
|
|
2441
|
-
|
|
2559
|
+
resolve5({ output: output + extra, exitCode, timedOut, aborted });
|
|
2442
2560
|
};
|
|
2443
2561
|
child.on("error", (error) => finish(null, error.message));
|
|
2444
2562
|
child.on("close", (code) => finish(code));
|
|
@@ -2560,46 +2678,139 @@ function formatDiff(lines) {
|
|
|
2560
2678
|
}
|
|
2561
2679
|
|
|
2562
2680
|
// src/tools/paths.ts
|
|
2681
|
+
import { statSync } from "fs";
|
|
2563
2682
|
import { isAbsolute as isAbsolute2, resolve as resolve2 } from "path";
|
|
2683
|
+
var readMtimes = /* @__PURE__ */ new WeakMap();
|
|
2564
2684
|
function resolvePath(cwd, path) {
|
|
2565
2685
|
return isAbsolute2(path) ? path : resolve2(cwd, path);
|
|
2566
2686
|
}
|
|
2567
2687
|
function errorCode(error) {
|
|
2568
2688
|
return error.code ?? error.message;
|
|
2569
2689
|
}
|
|
2690
|
+
function recordFileMtime(readFiles, abs) {
|
|
2691
|
+
readFiles.add(abs);
|
|
2692
|
+
try {
|
|
2693
|
+
let map = readMtimes.get(readFiles);
|
|
2694
|
+
if (!map) {
|
|
2695
|
+
map = /* @__PURE__ */ new Map();
|
|
2696
|
+
readMtimes.set(readFiles, map);
|
|
2697
|
+
}
|
|
2698
|
+
map.set(abs, statSync(abs).mtimeMs);
|
|
2699
|
+
} catch {
|
|
2700
|
+
}
|
|
2701
|
+
}
|
|
2702
|
+
function isFileModifiedSinceRead(readFiles, abs) {
|
|
2703
|
+
const recorded = readMtimes.get(readFiles)?.get(abs);
|
|
2704
|
+
if (recorded === void 0) return false;
|
|
2705
|
+
try {
|
|
2706
|
+
return statSync(abs).mtimeMs > recorded + 50;
|
|
2707
|
+
} catch {
|
|
2708
|
+
return false;
|
|
2709
|
+
}
|
|
2710
|
+
}
|
|
2570
2711
|
|
|
2571
2712
|
// src/tools/edit.ts
|
|
2572
|
-
var
|
|
2573
|
-
path: z2.string().describe("File path, relative to the project or absolute"),
|
|
2713
|
+
var editEntrySchema = z2.object({
|
|
2574
2714
|
old_string: z2.string().describe("Exact text to replace; must be unique unless replace_all"),
|
|
2575
2715
|
new_string: z2.string().describe("Replacement text"),
|
|
2576
2716
|
replace_all: z2.boolean().optional().describe("Replace every occurrence")
|
|
2577
2717
|
});
|
|
2718
|
+
var schema = z2.object({
|
|
2719
|
+
path: z2.string().describe("File path, relative to the project or absolute"),
|
|
2720
|
+
old_string: z2.string().optional().describe("Exact text to replace; must be unique unless replace_all"),
|
|
2721
|
+
new_string: z2.string().optional().describe("Replacement text"),
|
|
2722
|
+
replace_all: z2.boolean().optional().describe("Replace every occurrence"),
|
|
2723
|
+
edits: z2.array(editEntrySchema).optional().describe("Optional list of sequential edits to apply atomically to the same file")
|
|
2724
|
+
});
|
|
2578
2725
|
function countOccurrences(haystack, needle) {
|
|
2579
2726
|
let count = 0;
|
|
2580
2727
|
for (let i = haystack.indexOf(needle); i !== -1; i = haystack.indexOf(needle, i + needle.length)) count++;
|
|
2581
2728
|
return count;
|
|
2582
2729
|
}
|
|
2583
|
-
function
|
|
2730
|
+
function normalizeTypographic(s) {
|
|
2731
|
+
return s.replace(/[\u2018\u2019\u201A\u201B]/g, "'").replace(/[\u201C\u201D\u201E\u201F]/g, '"').replace(/[\u2013\u2014]/g, "-").replace(/\u00A0/g, " ");
|
|
2732
|
+
}
|
|
2733
|
+
function stripReadLinePrefixes(s) {
|
|
2734
|
+
const lines = s.split(/\r?\n/);
|
|
2735
|
+
if (lines.every((l) => !l.trim() || /^\s*\d+\t/.test(l))) {
|
|
2736
|
+
return lines.map((l) => l.replace(/^\s*\d+\t/, "")).join("\n");
|
|
2737
|
+
}
|
|
2738
|
+
return s;
|
|
2739
|
+
}
|
|
2740
|
+
function matchByNormalizedLines(content, oldText, newText, replaceAll) {
|
|
2741
|
+
const eol = content.includes("\r\n") ? "\r\n" : "\n";
|
|
2742
|
+
const fileLines = content.split(/\r?\n/);
|
|
2743
|
+
const oldLines = stripReadLinePrefixes(oldText).split(/\r?\n/);
|
|
2744
|
+
if (!oldLines.length) return { count: 0 };
|
|
2745
|
+
const normLine = (l) => normalizeTypographic(l).trimEnd();
|
|
2746
|
+
const normOld = oldLines.map(normLine);
|
|
2747
|
+
const matches = [];
|
|
2748
|
+
for (let i = 0; i + oldLines.length <= fileLines.length; i++) {
|
|
2749
|
+
let ok = true;
|
|
2750
|
+
for (let j = 0; j < oldLines.length; j++) {
|
|
2751
|
+
if (normLine(fileLines[i + j]) !== normOld[j]) {
|
|
2752
|
+
ok = false;
|
|
2753
|
+
break;
|
|
2754
|
+
}
|
|
2755
|
+
}
|
|
2756
|
+
if (ok) {
|
|
2757
|
+
matches.push(i);
|
|
2758
|
+
if (replaceAll) i += oldLines.length - 1;
|
|
2759
|
+
}
|
|
2760
|
+
}
|
|
2761
|
+
if (matches.length === 0) return { count: 0 };
|
|
2762
|
+
if (matches.length > 1 && !replaceAll) return { count: matches.length };
|
|
2763
|
+
const replacementLines = stripReadLinePrefixes(newText).split(/\r?\n/);
|
|
2764
|
+
const outLines = [...fileLines];
|
|
2765
|
+
for (let m = matches.length - 1; m >= 0; m--) {
|
|
2766
|
+
outLines.splice(matches[m], oldLines.length, ...replacementLines);
|
|
2767
|
+
}
|
|
2768
|
+
return { count: matches.length, result: outLines.join(eol) };
|
|
2769
|
+
}
|
|
2770
|
+
function applySingleEdit(content, path, input) {
|
|
2584
2771
|
if (input.old_string === input.new_string) {
|
|
2585
2772
|
return { output: "old_string and new_string are identical; nothing to change.", isError: true };
|
|
2586
2773
|
}
|
|
2587
2774
|
const eol = content.includes("\r\n") ? "\r\n" : "\n";
|
|
2588
2775
|
const toEol = (s) => s.replace(/\r?\n/g, eol);
|
|
2776
|
+
const strippedOld = stripReadLinePrefixes(input.old_string);
|
|
2777
|
+
const strippedNew = stripReadLinePrefixes(input.new_string);
|
|
2589
2778
|
const attempts = [[input.old_string, input.new_string]];
|
|
2590
2779
|
if (toEol(input.old_string) !== input.old_string) attempts.push([toEol(input.old_string), toEol(input.new_string)]);
|
|
2780
|
+
if (strippedOld !== input.old_string) {
|
|
2781
|
+
attempts.push([strippedOld, strippedNew], [toEol(strippedOld), toEol(strippedNew)]);
|
|
2782
|
+
}
|
|
2591
2783
|
for (const [oldText, newText] of attempts) {
|
|
2592
2784
|
const count = oldText ? countOccurrences(content, oldText) : 0;
|
|
2593
2785
|
if (count === 0) continue;
|
|
2594
2786
|
if (count > 1 && !input.replace_all) {
|
|
2595
2787
|
return {
|
|
2596
|
-
output: `old_string appears ${count} times in ${
|
|
2788
|
+
output: `old_string appears ${count} times in ${path}. Add more context to make it unique, or use replace_all.`,
|
|
2597
2789
|
isError: true
|
|
2598
2790
|
};
|
|
2599
2791
|
}
|
|
2600
2792
|
return input.replace_all ? content.split(oldText).join(newText) : content.replace(oldText, () => newText);
|
|
2601
2793
|
}
|
|
2602
|
-
|
|
2794
|
+
const fuzzy = matchByNormalizedLines(content, input.old_string, input.new_string, input.replace_all);
|
|
2795
|
+
if (fuzzy.count > 1 && !input.replace_all) {
|
|
2796
|
+
return {
|
|
2797
|
+
output: `old_string appears ${fuzzy.count} times in ${path}. Add more context to make it unique, or use replace_all.`,
|
|
2798
|
+
isError: true
|
|
2799
|
+
};
|
|
2800
|
+
}
|
|
2801
|
+
if (fuzzy.count > 0 && fuzzy.result !== void 0) return fuzzy.result;
|
|
2802
|
+
return { output: `old_string not found in ${path}. Re-read the file.`, isError: true };
|
|
2803
|
+
}
|
|
2804
|
+
function applyEdit(content, input) {
|
|
2805
|
+
const list2 = input.edits?.length ? input.edits : input.old_string !== void 0 && input.new_string !== void 0 ? [{ old_string: input.old_string, new_string: input.new_string, replace_all: input.replace_all }] : [];
|
|
2806
|
+
if (!list2.length) return { output: "Provide old_string and new_string (or edits array).", isError: true };
|
|
2807
|
+
let current2 = content;
|
|
2808
|
+
for (const item of list2) {
|
|
2809
|
+
const next = applySingleEdit(current2, input.path, item);
|
|
2810
|
+
if (typeof next !== "string") return next;
|
|
2811
|
+
current2 = next;
|
|
2812
|
+
}
|
|
2813
|
+
return current2;
|
|
2603
2814
|
}
|
|
2604
2815
|
var editTool = defineTool({
|
|
2605
2816
|
name: "edit",
|
|
@@ -2610,6 +2821,9 @@ var editTool = defineTool({
|
|
|
2610
2821
|
async validate(input, ctx) {
|
|
2611
2822
|
const abs = resolvePath(ctx.cwd, input.path);
|
|
2612
2823
|
if (!ctx.readFiles.has(abs)) return `Read ${input.path} with read before editing it.`;
|
|
2824
|
+
if (isFileModifiedSinceRead(ctx.readFiles, abs)) {
|
|
2825
|
+
return `${input.path} has been modified since it was last read. Read it again before editing.`;
|
|
2826
|
+
}
|
|
2613
2827
|
let content;
|
|
2614
2828
|
try {
|
|
2615
2829
|
content = await readFile(abs, "utf8");
|
|
@@ -2630,6 +2844,9 @@ var editTool = defineTool({
|
|
|
2630
2844
|
if (!ctx.readFiles.has(abs)) {
|
|
2631
2845
|
return { output: `Read ${input.path} with read before editing it.`, isError: true };
|
|
2632
2846
|
}
|
|
2847
|
+
if (isFileModifiedSinceRead(ctx.readFiles, abs)) {
|
|
2848
|
+
return { output: `${input.path} has been modified since it was last read. Read it again before editing.`, isError: true };
|
|
2849
|
+
}
|
|
2633
2850
|
let content;
|
|
2634
2851
|
try {
|
|
2635
2852
|
content = await readFile(abs, "utf8");
|
|
@@ -2640,6 +2857,7 @@ var editTool = defineTool({
|
|
|
2640
2857
|
if (typeof next !== "string") return next;
|
|
2641
2858
|
await ctx.checkpoint?.(abs);
|
|
2642
2859
|
await writeFile(abs, next);
|
|
2860
|
+
recordFileMtime(ctx.readFiles, abs);
|
|
2643
2861
|
return { output: `Edited ${input.path}.`, display: formatDiff(diffLines(content, next)) };
|
|
2644
2862
|
}
|
|
2645
2863
|
});
|
|
@@ -2911,20 +3129,22 @@ async function loadPage(inputUrl, signal, pinned, fetchPage) {
|
|
|
2911
3129
|
var fetchTool = createFetchTool();
|
|
2912
3130
|
|
|
2913
3131
|
// src/tools/glob.ts
|
|
3132
|
+
import { statSync as statSync2 } from "fs";
|
|
2914
3133
|
import { glob as nativeGlob } from "fs/promises";
|
|
3134
|
+
import { join as join4 } from "path";
|
|
2915
3135
|
import { z as z4 } from "zod";
|
|
2916
3136
|
|
|
2917
3137
|
// src/abort.ts
|
|
2918
3138
|
var ABORTED = /* @__PURE__ */ Symbol("aborted");
|
|
2919
3139
|
function raceAbort(promise, signal) {
|
|
2920
3140
|
if (signal.aborted) return Promise.resolve(ABORTED);
|
|
2921
|
-
return new Promise((
|
|
2922
|
-
const onAbort = () =>
|
|
3141
|
+
return new Promise((resolve5, reject) => {
|
|
3142
|
+
const onAbort = () => resolve5(ABORTED);
|
|
2923
3143
|
signal.addEventListener("abort", onAbort, { once: true });
|
|
2924
3144
|
promise.then(
|
|
2925
3145
|
(value) => {
|
|
2926
3146
|
signal.removeEventListener("abort", onAbort);
|
|
2927
|
-
|
|
3147
|
+
resolve5(value);
|
|
2928
3148
|
},
|
|
2929
3149
|
(error) => {
|
|
2930
3150
|
signal.removeEventListener("abort", onAbort);
|
|
@@ -2950,22 +3170,38 @@ async function collectUntilAbort(iterable, signal, onItem) {
|
|
|
2950
3170
|
|
|
2951
3171
|
// src/tools/ignore.ts
|
|
2952
3172
|
import { readFileSync as readFileSync2 } from "fs";
|
|
2953
|
-
import { join as
|
|
2954
|
-
function ignorePatterns(root) {
|
|
3173
|
+
import { dirname as dirname2, join as join3, resolve as resolve3 } from "path";
|
|
3174
|
+
function ignorePatterns(root, projectRoot) {
|
|
2955
3175
|
const patterns = ["**/node_modules/**", "**/.git/**"];
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
|
|
2959
|
-
|
|
2960
|
-
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
|
|
3176
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3177
|
+
const dirs2 = [];
|
|
3178
|
+
let cur = resolve3(root);
|
|
3179
|
+
const stop = projectRoot ? resolve3(projectRoot) : cur;
|
|
3180
|
+
while (true) {
|
|
3181
|
+
if (!seen.has(cur)) {
|
|
3182
|
+
seen.add(cur);
|
|
3183
|
+
dirs2.push(cur);
|
|
3184
|
+
}
|
|
3185
|
+
if (cur === stop || !cur.startsWith(stop)) break;
|
|
3186
|
+
const parent = dirname2(cur);
|
|
3187
|
+
if (parent === cur) break;
|
|
3188
|
+
cur = parent;
|
|
3189
|
+
}
|
|
3190
|
+
for (const dir of dirs2) {
|
|
3191
|
+
let lines = [];
|
|
3192
|
+
try {
|
|
3193
|
+
lines = readFileSync2(join3(dir, ".gitignore"), "utf8").split(/\r?\n/);
|
|
3194
|
+
} catch {
|
|
3195
|
+
continue;
|
|
3196
|
+
}
|
|
3197
|
+
for (const raw of lines) {
|
|
3198
|
+
const line = raw.trim();
|
|
3199
|
+
if (!line || line.startsWith("#") || line.startsWith("!")) continue;
|
|
3200
|
+
const p = line.replace(/^\//, "").replace(/\/$/, "");
|
|
3201
|
+
patterns.push(`**/${p}`, `**/${p}/**`);
|
|
3202
|
+
}
|
|
2967
3203
|
}
|
|
2968
|
-
return patterns;
|
|
3204
|
+
return [...new Set(patterns)];
|
|
2969
3205
|
}
|
|
2970
3206
|
|
|
2971
3207
|
// src/tools/globSafety.ts
|
|
@@ -3007,19 +3243,30 @@ var globTool = defineTool({
|
|
|
3007
3243
|
path: z4.string().optional().describe("Base directory, default project root")
|
|
3008
3244
|
}),
|
|
3009
3245
|
kind: "read",
|
|
3010
|
-
target: (input) => input.pattern,
|
|
3246
|
+
target: (input) => input.path ? `${input.pattern} ${input.path}` : input.pattern,
|
|
3011
3247
|
async run(input, ctx) {
|
|
3012
3248
|
const invalid = validateGlobPattern(input.pattern);
|
|
3013
3249
|
if (invalid) return { output: invalid, isError: true };
|
|
3014
3250
|
const root = resolvePath(ctx.cwd, input.path ?? ".");
|
|
3015
3251
|
const files = [];
|
|
3016
3252
|
const finished = await collectUntilAbort(
|
|
3017
|
-
nativeGlob(input.pattern, { cwd: root, exclude: ignorePatterns(root) }),
|
|
3253
|
+
nativeGlob(input.pattern, { cwd: root, exclude: ignorePatterns(root, ctx.cwd) }),
|
|
3018
3254
|
ctx.signal,
|
|
3019
3255
|
(file) => files.push(normalizeGlobPath(file))
|
|
3020
3256
|
);
|
|
3021
3257
|
if (!finished) return { output: "Cancelled by the user.", isError: true };
|
|
3022
|
-
|
|
3258
|
+
const mtime = (rel) => {
|
|
3259
|
+
try {
|
|
3260
|
+
return statSync2(join4(root, rel)).mtimeMs;
|
|
3261
|
+
} catch {
|
|
3262
|
+
return 0;
|
|
3263
|
+
}
|
|
3264
|
+
};
|
|
3265
|
+
const mtimes = new Map(files.map((f) => [f, mtime(f)]));
|
|
3266
|
+
files.sort((a, b) => {
|
|
3267
|
+
const diff = (mtimes.get(b) ?? 0) - (mtimes.get(a) ?? 0);
|
|
3268
|
+
return Math.abs(diff) >= 50 ? diff : a.localeCompare(b);
|
|
3269
|
+
});
|
|
3023
3270
|
if (!files.length) return { output: "No matching files." };
|
|
3024
3271
|
const first = files.slice(0, 500);
|
|
3025
3272
|
const shown = [];
|
|
@@ -3040,75 +3287,141 @@ var globTool = defineTool({
|
|
|
3040
3287
|
|
|
3041
3288
|
// src/tools/grep.ts
|
|
3042
3289
|
import { execFile } from "child_process";
|
|
3290
|
+
import { statSync as statSync3 } from "fs";
|
|
3043
3291
|
import { readFile as readFile2 } from "fs/promises";
|
|
3044
3292
|
import { glob as nativeGlob2 } from "fs/promises";
|
|
3293
|
+
import { basename, dirname as dirname3 } from "path";
|
|
3045
3294
|
import { z as z5 } from "zod";
|
|
3046
3295
|
var MAX_MATCHES = 200;
|
|
3047
3296
|
var MAX_OUTPUT_CHARS2 = 24e3;
|
|
3297
|
+
function escapeRegExp(s) {
|
|
3298
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
3299
|
+
}
|
|
3300
|
+
function isSingleFile(p) {
|
|
3301
|
+
try {
|
|
3302
|
+
return statSync3(p).isFile();
|
|
3303
|
+
} catch {
|
|
3304
|
+
return false;
|
|
3305
|
+
}
|
|
3306
|
+
}
|
|
3048
3307
|
async function grepJs(pattern, opts) {
|
|
3049
|
-
const
|
|
3308
|
+
const source = opts.literal ? escapeRegExp(pattern) : pattern;
|
|
3309
|
+
const regex = new RegExp(source, opts.ignoreCase ? "i" : "");
|
|
3310
|
+
const single = isSingleFile(opts.root);
|
|
3050
3311
|
const files = [];
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
|
|
3312
|
+
if (single) {
|
|
3313
|
+
files.push(basename(opts.root));
|
|
3314
|
+
} else {
|
|
3315
|
+
const finished = await collectUntilAbort(
|
|
3316
|
+
nativeGlob2(opts.glob ?? "**/*", { cwd: opts.root, exclude: ignorePatterns(opts.root, opts.projectRoot) }),
|
|
3317
|
+
opts.signal,
|
|
3318
|
+
(file) => files.push(normalizeGlobPath(file))
|
|
3319
|
+
);
|
|
3320
|
+
if (!finished) return [];
|
|
3321
|
+
}
|
|
3322
|
+
const baseDir = single ? dirname3(opts.root) : opts.root;
|
|
3323
|
+
const ctxLines = Math.max(0, Math.min(10, Math.floor(opts.context ?? 0)));
|
|
3324
|
+
const mode = opts.outputMode ?? "content";
|
|
3056
3325
|
const out = [];
|
|
3057
|
-
if (!finished) return out;
|
|
3058
3326
|
for (const file of files.sort()) {
|
|
3059
3327
|
if (opts.signal?.aborted) break;
|
|
3060
3328
|
let text;
|
|
3061
3329
|
try {
|
|
3062
|
-
const buffer = await readFile2(resolvePath(
|
|
3330
|
+
const buffer = await readFile2(resolvePath(baseDir, file));
|
|
3063
3331
|
if (buffer.subarray(0, 8e3).includes(0)) continue;
|
|
3064
3332
|
text = buffer.toString("utf8");
|
|
3065
3333
|
} catch {
|
|
3066
3334
|
continue;
|
|
3067
3335
|
}
|
|
3068
3336
|
const lines = text.split(/\r?\n/);
|
|
3337
|
+
let fileMatches = 0;
|
|
3069
3338
|
for (let i = 0; i < lines.length && out.length < MAX_MATCHES; i++) {
|
|
3070
|
-
if (regex.test(lines[i]))
|
|
3339
|
+
if (regex.test(lines[i])) {
|
|
3340
|
+
fileMatches++;
|
|
3341
|
+
if (mode === "files_with_matches") {
|
|
3342
|
+
out.push(file);
|
|
3343
|
+
break;
|
|
3344
|
+
}
|
|
3345
|
+
if (mode === "content") {
|
|
3346
|
+
const from = Math.max(0, i - ctxLines);
|
|
3347
|
+
const to = Math.min(lines.length - 1, i + ctxLines);
|
|
3348
|
+
for (let j = from; j <= to && out.length < MAX_MATCHES; j++) {
|
|
3349
|
+
const sep2 = j === i ? ":" : "-";
|
|
3350
|
+
out.push(`${file}${sep2}${j + 1}${sep2}${lines[j].slice(0, 300)}`);
|
|
3351
|
+
}
|
|
3352
|
+
}
|
|
3353
|
+
}
|
|
3071
3354
|
}
|
|
3355
|
+
if (mode === "count" && fileMatches > 0) out.push(`${file}:${fileMatches}`);
|
|
3072
3356
|
if (out.length >= MAX_MATCHES) break;
|
|
3073
3357
|
}
|
|
3074
3358
|
return out;
|
|
3075
3359
|
}
|
|
3076
3360
|
function grepRg(pattern, opts) {
|
|
3077
|
-
const
|
|
3361
|
+
const single = isSingleFile(opts.root);
|
|
3362
|
+
const cwd = single ? dirname3(opts.root) : opts.root;
|
|
3363
|
+
const target = single ? basename(opts.root) : ".";
|
|
3364
|
+
const mode = opts.outputMode ?? "content";
|
|
3365
|
+
const args = ["--color", "never"];
|
|
3366
|
+
if (mode === "files_with_matches") {
|
|
3367
|
+
args.push("--files-with-matches");
|
|
3368
|
+
} else if (mode === "count") {
|
|
3369
|
+
args.push("--count");
|
|
3370
|
+
} else {
|
|
3371
|
+
args.push("--line-number", "--no-heading", "--max-columns", "300");
|
|
3372
|
+
if (opts.context && opts.context > 0) args.push("-C", String(Math.min(10, Math.floor(opts.context))));
|
|
3373
|
+
}
|
|
3374
|
+
if (opts.literal) args.push("-F");
|
|
3078
3375
|
if (opts.ignoreCase) args.push("-i");
|
|
3079
|
-
if (opts.glob) args.push("-g", opts.glob);
|
|
3080
|
-
args.push("
|
|
3081
|
-
return new Promise((
|
|
3082
|
-
execFile("rg", args, { cwd
|
|
3083
|
-
if (opts.signal?.aborted) return
|
|
3376
|
+
if (opts.glob && !single) args.push("-g", opts.glob);
|
|
3377
|
+
args.push("-e", pattern, target);
|
|
3378
|
+
return new Promise((resolve5) => {
|
|
3379
|
+
execFile("rg", args, { cwd, maxBuffer: 10 * 1024 * 1024, signal: opts.signal }, (error, stdout) => {
|
|
3380
|
+
if (opts.signal?.aborted) return resolve5([]);
|
|
3084
3381
|
const code = error?.code;
|
|
3085
|
-
if (code === "ENOENT") return
|
|
3086
|
-
if (error && error.code !== 1) return
|
|
3087
|
-
|
|
3382
|
+
if (code === "ENOENT") return resolve5(void 0);
|
|
3383
|
+
if (error && error.code !== 1) return resolve5(void 0);
|
|
3384
|
+
resolve5(
|
|
3385
|
+
stdout.split(/\r?\n/).filter(Boolean).map((l) => l.replace(/^\.[\\/]/, "").replace(/\\/g, "/")).slice(0, MAX_MATCHES)
|
|
3386
|
+
);
|
|
3088
3387
|
});
|
|
3089
3388
|
});
|
|
3090
3389
|
}
|
|
3091
3390
|
var grepTool = defineTool({
|
|
3092
3391
|
name: "grep",
|
|
3093
|
-
description: "Search file contents with a regular expression. Respects .gitignore. Returns path:line:text (max 200).",
|
|
3392
|
+
description: "Search file contents with a regular expression or literal text. Respects .gitignore. Returns path:line:text (max 200).",
|
|
3094
3393
|
schema: z5.object({
|
|
3095
|
-
pattern: z5.string().describe("Regular expression"),
|
|
3096
|
-
path: z5.string().optional().describe("
|
|
3394
|
+
pattern: z5.string().describe("Regular expression or literal string"),
|
|
3395
|
+
path: z5.string().optional().describe("File or directory to search, default project root"),
|
|
3097
3396
|
glob: z5.string().optional().describe('Only files matching this glob, e.g. "**/*.ts"'),
|
|
3098
|
-
ignore_case: z5.boolean().optional()
|
|
3397
|
+
ignore_case: z5.boolean().optional(),
|
|
3398
|
+
literal: z5.boolean().optional().describe("Treat pattern as literal text instead of regex"),
|
|
3399
|
+
context: z5.number().int().min(0).max(10).optional().describe("Number of context lines before and after each match"),
|
|
3400
|
+
output_mode: z5.enum(["content", "files_with_matches", "count"]).optional().describe("Output format, default content")
|
|
3099
3401
|
}),
|
|
3100
3402
|
kind: "read",
|
|
3101
|
-
target: (input) => input.pattern,
|
|
3403
|
+
target: (input) => input.path ? `${input.pattern} ${input.path}` : input.pattern,
|
|
3102
3404
|
async run(input, ctx) {
|
|
3103
3405
|
if (input.glob) {
|
|
3104
3406
|
const invalid = validateGlobPattern(input.glob);
|
|
3105
3407
|
if (invalid) return { output: invalid, isError: true };
|
|
3106
3408
|
}
|
|
3107
|
-
const opts = {
|
|
3108
|
-
|
|
3109
|
-
|
|
3110
|
-
|
|
3111
|
-
|
|
3409
|
+
const opts = {
|
|
3410
|
+
root: resolvePath(ctx.cwd, input.path ?? "."),
|
|
3411
|
+
projectRoot: ctx.cwd,
|
|
3412
|
+
glob: input.glob,
|
|
3413
|
+
ignoreCase: input.ignore_case,
|
|
3414
|
+
literal: input.literal,
|
|
3415
|
+
context: input.context,
|
|
3416
|
+
outputMode: input.output_mode,
|
|
3417
|
+
signal: ctx.signal
|
|
3418
|
+
};
|
|
3419
|
+
if (!input.literal) {
|
|
3420
|
+
try {
|
|
3421
|
+
new RegExp(input.pattern);
|
|
3422
|
+
} catch (error) {
|
|
3423
|
+
return { output: `Invalid regex: ${error.message}`, isError: true };
|
|
3424
|
+
}
|
|
3112
3425
|
}
|
|
3113
3426
|
const matches = await grepRg(input.pattern, opts) ?? await grepJs(input.pattern, opts);
|
|
3114
3427
|
if (ctx.signal.aborted) return { output: "Cancelled by the user.", isError: true };
|
|
@@ -3130,6 +3443,7 @@ var grepTool = defineTool({
|
|
|
3130
3443
|
});
|
|
3131
3444
|
|
|
3132
3445
|
// src/tools/read.ts
|
|
3446
|
+
import { readdirSync, statSync as statSync4 } from "fs";
|
|
3133
3447
|
import { readFile as readFile3 } from "fs/promises";
|
|
3134
3448
|
import { z as z6 } from "zod";
|
|
3135
3449
|
var MAX_LINE = 2e3;
|
|
@@ -3213,6 +3527,16 @@ function createReadTool(options = {}) {
|
|
|
3213
3527
|
target: (input) => input.path,
|
|
3214
3528
|
async run(input, ctx) {
|
|
3215
3529
|
const abs = resolvePath(ctx.cwd, input.path);
|
|
3530
|
+
try {
|
|
3531
|
+
if (statSync4(abs).isDirectory()) {
|
|
3532
|
+
const entries = readdirSync(abs, { withFileTypes: true }).map((e) => e.isDirectory() ? `${e.name}/` : e.name).sort((a, b) => a.localeCompare(b));
|
|
3533
|
+
const start = (input.offset ?? 1) - 1;
|
|
3534
|
+
const limit = input.limit ?? 2e3;
|
|
3535
|
+
const slice = entries.slice(start, start + limit);
|
|
3536
|
+
return { output: slice.join("\n") || "(empty directory)", display: t("{n} lines", { n: slice.length }) };
|
|
3537
|
+
}
|
|
3538
|
+
} catch {
|
|
3539
|
+
}
|
|
3216
3540
|
let buffer;
|
|
3217
3541
|
try {
|
|
3218
3542
|
buffer = await readFile3(abs);
|
|
@@ -3221,13 +3545,13 @@ function createReadTool(options = {}) {
|
|
|
3221
3545
|
}
|
|
3222
3546
|
const mediaType = vision ? imageMediaType(buffer) : void 0;
|
|
3223
3547
|
if (mediaType) {
|
|
3224
|
-
ctx.readFiles
|
|
3548
|
+
recordFileMtime(ctx.readFiles, abs);
|
|
3225
3549
|
return readImageResult(input, buffer, mediaType);
|
|
3226
3550
|
}
|
|
3227
3551
|
if (buffer.subarray(0, 8e3).includes(0)) {
|
|
3228
3552
|
return { output: `${input.path} is a binary file; not read.`, isError: true };
|
|
3229
3553
|
}
|
|
3230
|
-
ctx.readFiles
|
|
3554
|
+
recordFileMtime(ctx.readFiles, abs);
|
|
3231
3555
|
return readTextResult(input, buffer);
|
|
3232
3556
|
}
|
|
3233
3557
|
});
|
|
@@ -3237,7 +3561,7 @@ var readTool = createReadTool({ vision: true });
|
|
|
3237
3561
|
// src/tools/write.ts
|
|
3238
3562
|
import { existsSync as existsSync4 } from "fs";
|
|
3239
3563
|
import { mkdir, readFile as readFile4, writeFile as writeFile2 } from "fs/promises";
|
|
3240
|
-
import { dirname as
|
|
3564
|
+
import { dirname as dirname4 } from "path";
|
|
3241
3565
|
import { z as z7 } from "zod";
|
|
3242
3566
|
var writeTool = defineTool({
|
|
3243
3567
|
name: "write",
|
|
@@ -3264,12 +3588,12 @@ var writeTool = defineTool({
|
|
|
3264
3588
|
}
|
|
3265
3589
|
try {
|
|
3266
3590
|
await ctx.checkpoint?.(abs);
|
|
3267
|
-
await mkdir(
|
|
3591
|
+
await mkdir(dirname4(abs), { recursive: true });
|
|
3268
3592
|
await writeFile2(abs, input.content);
|
|
3269
3593
|
} catch (error) {
|
|
3270
3594
|
return { output: `Failed to write ${input.path}: ${errorCode(error)}`, isError: true };
|
|
3271
3595
|
}
|
|
3272
|
-
ctx.readFiles
|
|
3596
|
+
recordFileMtime(ctx.readFiles, abs);
|
|
3273
3597
|
const lines = input.content.split("\n").length;
|
|
3274
3598
|
return { output: `Wrote ${input.path} (${lines} lines).`, display: t("{lines} lines written", { lines }) };
|
|
3275
3599
|
}
|
|
@@ -3379,12 +3703,36 @@ function budgetStatus(usage, cap, model) {
|
|
|
3379
3703
|
}
|
|
3380
3704
|
|
|
3381
3705
|
// src/tokenBudget.ts
|
|
3706
|
+
var IMAGE_TOKEN_COST = 1e3;
|
|
3382
3707
|
var estimateTokens = (text) => Math.ceil(text.length / 4);
|
|
3383
|
-
|
|
3708
|
+
function isImageAttachmentMessage(m) {
|
|
3709
|
+
if (m.role !== "user" || !Array.isArray(m.content)) return false;
|
|
3710
|
+
const hasImage = m.content.some((p) => p.type === "image_url");
|
|
3711
|
+
const firstText = m.content.find((p) => p.type === "text");
|
|
3712
|
+
return hasImage && !!firstText && firstText.type === "text" && firstText.text.startsWith("Image read from ");
|
|
3713
|
+
}
|
|
3714
|
+
function isUserTurnBoundary(m) {
|
|
3715
|
+
return m.role === "user" && !isImageAttachmentMessage(m);
|
|
3716
|
+
}
|
|
3717
|
+
function estimateMessages(messages) {
|
|
3718
|
+
let total = 0;
|
|
3719
|
+
for (const m of messages) {
|
|
3720
|
+
if (m.role === "user" && Array.isArray(m.content)) {
|
|
3721
|
+
for (const part of m.content) {
|
|
3722
|
+
if (part.type === "image_url") total += IMAGE_TOKEN_COST;
|
|
3723
|
+
else total += estimateTokens(part.text);
|
|
3724
|
+
}
|
|
3725
|
+
total += 8;
|
|
3726
|
+
} else {
|
|
3727
|
+
total += estimateTokens(JSON.stringify(m));
|
|
3728
|
+
}
|
|
3729
|
+
}
|
|
3730
|
+
return total;
|
|
3731
|
+
}
|
|
3384
3732
|
function trimToFit(messages, maxTokens) {
|
|
3385
3733
|
let out = messages;
|
|
3386
3734
|
while (estimateMessages(out) > maxTokens) {
|
|
3387
|
-
const boundary = out.findIndex((m, i) => i > 0 && m
|
|
3735
|
+
const boundary = out.findIndex((m, i) => i > 0 && isUserTurnBoundary(m));
|
|
3388
3736
|
if (boundary === -1) return out;
|
|
3389
3737
|
out = out.slice(boundary);
|
|
3390
3738
|
}
|
|
@@ -3394,13 +3742,13 @@ function splitKeepRecent(messages, keepTokens) {
|
|
|
3394
3742
|
if (messages.length === 0) return [[], []];
|
|
3395
3743
|
let start = messages.length;
|
|
3396
3744
|
for (let i = messages.length - 1; i >= 0; i--) {
|
|
3397
|
-
if (i > 0 && messages[i]
|
|
3745
|
+
if (i > 0 && !isUserTurnBoundary(messages[i])) continue;
|
|
3398
3746
|
if (estimateMessages(messages.slice(i)) <= keepTokens) start = i;
|
|
3399
3747
|
else break;
|
|
3400
3748
|
}
|
|
3401
3749
|
if (start === messages.length) {
|
|
3402
3750
|
for (let i = messages.length - 1; i >= 0; i--) {
|
|
3403
|
-
if (messages[i]
|
|
3751
|
+
if (isUserTurnBoundary(messages[i])) {
|
|
3404
3752
|
start = i;
|
|
3405
3753
|
break;
|
|
3406
3754
|
}
|
|
@@ -3448,6 +3796,7 @@ var Agent2 = class {
|
|
|
3448
3796
|
this.messages = [...opts.history ?? []];
|
|
3449
3797
|
this.maxSteps = typeof opts.maxSteps === "number" && Number.isFinite(opts.maxSteps) && opts.maxSteps > 0 ? Math.max(1, Math.floor(opts.maxSteps)) : void 0;
|
|
3450
3798
|
this.contextWindow = opts.contextWindow ?? 128e3;
|
|
3799
|
+
this.budgetModel = opts.budgetModel ?? "";
|
|
3451
3800
|
this.systemPrompt = opts.systemPrompt;
|
|
3452
3801
|
this.reasoning = opts.reasoning ?? "auto";
|
|
3453
3802
|
}
|
|
@@ -3471,6 +3820,7 @@ var Agent2 = class {
|
|
|
3471
3820
|
maxSteps;
|
|
3472
3821
|
// One window for every model; resolved from contextWindowFor() (catalog + env override).
|
|
3473
3822
|
contextWindow;
|
|
3823
|
+
budgetModel;
|
|
3474
3824
|
systemPrompt;
|
|
3475
3825
|
setSystemPrompt(text) {
|
|
3476
3826
|
this.systemPrompt = text;
|
|
@@ -3478,6 +3828,15 @@ var Agent2 = class {
|
|
|
3478
3828
|
setContextWindow(window) {
|
|
3479
3829
|
this.contextWindow = window;
|
|
3480
3830
|
}
|
|
3831
|
+
getContextWindow() {
|
|
3832
|
+
return this.contextWindow;
|
|
3833
|
+
}
|
|
3834
|
+
setBudgetModel(model) {
|
|
3835
|
+
this.budgetModel = model;
|
|
3836
|
+
}
|
|
3837
|
+
getBudgetModel() {
|
|
3838
|
+
return this.budgetModel;
|
|
3839
|
+
}
|
|
3481
3840
|
setTools(tools) {
|
|
3482
3841
|
this.tools = tools;
|
|
3483
3842
|
this.definitions = toolDefinitions(tools);
|
|
@@ -3678,7 +4037,7 @@ ${t("This model may not support tool calling. Try another model with /model.")}`
|
|
|
3678
4037
|
}
|
|
3679
4038
|
/** Current cap status; undefined means the budget is not exhausted. */
|
|
3680
4039
|
overBudget() {
|
|
3681
|
-
return budgetStatus(this.totalUsage, this.opts.usageCap, this.
|
|
4040
|
+
return budgetStatus(this.totalUsage, this.opts.usageCap, this.budgetModel);
|
|
3682
4041
|
}
|
|
3683
4042
|
/** Every tool call needs an answer in the history, or the next request to the model is rejected. */
|
|
3684
4043
|
cancelCalls(calls) {
|
|
@@ -3798,7 +4157,7 @@ ${t("This model may not support tool calling. Try another model with /model.")}`
|
|
|
3798
4157
|
};
|
|
3799
4158
|
|
|
3800
4159
|
// src/checkpoints.ts
|
|
3801
|
-
import { existsSync as existsSync5, statSync } from "fs";
|
|
4160
|
+
import { existsSync as existsSync5, statSync as statSync5 } from "fs";
|
|
3802
4161
|
import { readFile as readFile5, rm, writeFile as writeFile3 } from "fs/promises";
|
|
3803
4162
|
var MAX_TURNS = 20;
|
|
3804
4163
|
var MAX_BYTES2 = 2 * 1024 * 1024;
|
|
@@ -3825,8 +4184,8 @@ var CheckpointStore = class {
|
|
|
3825
4184
|
history = [];
|
|
3826
4185
|
current;
|
|
3827
4186
|
pendingRedo;
|
|
4187
|
+
offset = 0;
|
|
3828
4188
|
beginTurn() {
|
|
3829
|
-
if (this.current?.size === 0) return;
|
|
3830
4189
|
this.current = /* @__PURE__ */ new Map();
|
|
3831
4190
|
this.history.push(this.current);
|
|
3832
4191
|
this.pendingRedo = void 0;
|
|
@@ -3837,16 +4196,19 @@ var CheckpointStore = class {
|
|
|
3837
4196
|
const turn = this.current;
|
|
3838
4197
|
if (turn.has(path)) return;
|
|
3839
4198
|
if (!existsSync5(path)) turn.set(path, null);
|
|
3840
|
-
else if (
|
|
4199
|
+
else if (statSync5(path).size > MAX_BYTES2) turn.set(path, "skipped");
|
|
3841
4200
|
else turn.set(path, await readFile5(path));
|
|
3842
|
-
while (this.turns() > MAX_TURNS)
|
|
4201
|
+
while (this.turns() > MAX_TURNS) {
|
|
4202
|
+
this.history.shift();
|
|
4203
|
+
this.offset++;
|
|
4204
|
+
}
|
|
3843
4205
|
}
|
|
3844
4206
|
turns() {
|
|
3845
4207
|
return this.history.filter((t2) => t2.size).length;
|
|
3846
4208
|
}
|
|
3847
4209
|
/** Total entries including empty ones; used to align file snapshots with conversation turns. */
|
|
3848
4210
|
entries() {
|
|
3849
|
-
return this.history.length;
|
|
4211
|
+
return this.offset + this.history.length;
|
|
3850
4212
|
}
|
|
3851
4213
|
async undo() {
|
|
3852
4214
|
while (this.history.length && !this.history.at(-1)?.size) this.history.pop();
|
|
@@ -3866,7 +4228,7 @@ var CheckpointStore = class {
|
|
|
3866
4228
|
* pairs conversation turns with the entry count recorded when each turn started.
|
|
3867
4229
|
*/
|
|
3868
4230
|
async undoTo(keep) {
|
|
3869
|
-
const target = Math.max(0, Math.min(keep, this.history.length));
|
|
4231
|
+
const target = Math.max(0, Math.min(keep - this.offset, this.history.length));
|
|
3870
4232
|
const result = emptyResult();
|
|
3871
4233
|
const redo = /* @__PURE__ */ new Map();
|
|
3872
4234
|
let applied = false;
|
|
@@ -3891,8 +4253,11 @@ var CheckpointStore = class {
|
|
|
3891
4253
|
const turn = this.pendingRedo;
|
|
3892
4254
|
if (!turn) return void 0;
|
|
3893
4255
|
this.pendingRedo = void 0;
|
|
4256
|
+
const undoEntry = /* @__PURE__ */ new Map();
|
|
4257
|
+
await this.captureFor(turn, undoEntry);
|
|
3894
4258
|
const result = emptyResult();
|
|
3895
4259
|
await restoreTurn(turn, result);
|
|
4260
|
+
if (undoEntry.size) this.history.push(undoEntry);
|
|
3896
4261
|
return result;
|
|
3897
4262
|
}
|
|
3898
4263
|
/** Records the on-disk state of a turn's paths so redo() can put it back. */
|
|
@@ -3905,7 +4270,7 @@ var CheckpointStore = class {
|
|
|
3905
4270
|
}
|
|
3906
4271
|
try {
|
|
3907
4272
|
if (!existsSync5(path)) into.set(path, null);
|
|
3908
|
-
else if (
|
|
4273
|
+
else if (statSync5(path).size > MAX_BYTES2) into.set(path, "skipped");
|
|
3909
4274
|
else into.set(path, await readFile5(path));
|
|
3910
4275
|
} catch {
|
|
3911
4276
|
into.set(path, "skipped");
|
|
@@ -3916,6 +4281,7 @@ var CheckpointStore = class {
|
|
|
3916
4281
|
this.history.length = 0;
|
|
3917
4282
|
this.current = void 0;
|
|
3918
4283
|
this.pendingRedo = void 0;
|
|
4284
|
+
this.offset = 0;
|
|
3919
4285
|
}
|
|
3920
4286
|
};
|
|
3921
4287
|
|
|
@@ -3964,7 +4330,7 @@ import { z as z9 } from "zod";
|
|
|
3964
4330
|
// src/mcp/config.ts
|
|
3965
4331
|
import { createHash } from "crypto";
|
|
3966
4332
|
import { existsSync as existsSync6, mkdirSync as mkdirSync2, readFileSync as readFileSync3 } from "fs";
|
|
3967
|
-
import { dirname as
|
|
4333
|
+
import { dirname as dirname5, join as join5, resolve as resolve4 } from "path";
|
|
3968
4334
|
var ENV_REF = /\$\{([A-Za-z_][A-Za-z0-9_]*)(?::-([^}]*))?\}/g;
|
|
3969
4335
|
function expandEnvVars(config, env) {
|
|
3970
4336
|
const expand = (value) => value.replace(ENV_REF, (match, name, fallback) => {
|
|
@@ -3988,9 +4354,9 @@ function expandEnvVars(config, env) {
|
|
|
3988
4354
|
import { spawn as spawn3 } from "child_process";
|
|
3989
4355
|
import { existsSync as existsSync7, mkdirSync as mkdirSync3, readFileSync as readFileSync4 } from "fs";
|
|
3990
4356
|
import { createServer } from "http";
|
|
3991
|
-
import { dirname as
|
|
4357
|
+
import { dirname as dirname6, join as join6 } from "path";
|
|
3992
4358
|
import { auth } from "@modelcontextprotocol/sdk/client/auth.js";
|
|
3993
|
-
var oauthStorePath = (home) =>
|
|
4359
|
+
var oauthStorePath = (home) => join6(home, "mcp-oauth.json");
|
|
3994
4360
|
function readStore(home) {
|
|
3995
4361
|
const path = oauthStorePath(home);
|
|
3996
4362
|
if (!existsSync7(path)) return {};
|
|
@@ -4003,7 +4369,7 @@ function readStore(home) {
|
|
|
4003
4369
|
}
|
|
4004
4370
|
function writeStore(home, store) {
|
|
4005
4371
|
const path = oauthStorePath(home);
|
|
4006
|
-
mkdirSync3(
|
|
4372
|
+
mkdirSync3(dirname6(path), { recursive: true });
|
|
4007
4373
|
writeFileAtomic(path, `${JSON.stringify(store, null, 2)}
|
|
4008
4374
|
`, 384);
|
|
4009
4375
|
}
|
|
@@ -4097,7 +4463,28 @@ var FileOAuthProvider = class {
|
|
|
4097
4463
|
};
|
|
4098
4464
|
|
|
4099
4465
|
// src/mcp/manager.ts
|
|
4100
|
-
|
|
4466
|
+
function formatMcpContent(content) {
|
|
4467
|
+
const parts = [];
|
|
4468
|
+
const images = [];
|
|
4469
|
+
for (const c of content ?? []) {
|
|
4470
|
+
if (c.type === "text") {
|
|
4471
|
+
parts.push(c.text ?? "");
|
|
4472
|
+
} else if (c.type === "image" && c.data) {
|
|
4473
|
+
const mediaType = c.mimeType || "image/png";
|
|
4474
|
+
images.push({ mediaType, data: c.data, path: "mcp:image" });
|
|
4475
|
+
parts.push(`[image attached: ${mediaType}]`);
|
|
4476
|
+
} else if (c.type === "resource" && c.resource?.text) {
|
|
4477
|
+
parts.push(c.resource.uri ? `[${c.resource.uri}]
|
|
4478
|
+
${c.resource.text}` : c.resource.text);
|
|
4479
|
+
} else {
|
|
4480
|
+
parts.push(`[${c.type}]`);
|
|
4481
|
+
}
|
|
4482
|
+
}
|
|
4483
|
+
return {
|
|
4484
|
+
output: parts.join("\n") || "(empty)",
|
|
4485
|
+
...images.length ? { images } : {}
|
|
4486
|
+
};
|
|
4487
|
+
}
|
|
4101
4488
|
var CHILD_ENV_KEYS = [
|
|
4102
4489
|
// portable
|
|
4103
4490
|
"PATH",
|
|
@@ -4226,11 +4613,13 @@ var McpManager = class {
|
|
|
4226
4613
|
async run(input, ctx) {
|
|
4227
4614
|
try {
|
|
4228
4615
|
const result = await client.callTool({ name: tool.name, arguments: input }, void 0, { signal: ctx.signal });
|
|
4229
|
-
const
|
|
4616
|
+
const formatted = formatMcpContent(result.content);
|
|
4617
|
+
const output = formatted.output;
|
|
4230
4618
|
return {
|
|
4231
|
-
output: output.length >
|
|
4232
|
-
\u2026 [truncated at
|
|
4233
|
-
isError: !!result.isError
|
|
4619
|
+
output: output.length > 32e3 ? `${output.slice(0, 32e3)}
|
|
4620
|
+
\u2026 [truncated at 32000 characters; narrow the MCP request if possible]` : output,
|
|
4621
|
+
isError: !!result.isError,
|
|
4622
|
+
...formatted.images ? { images: formatted.images } : {}
|
|
4234
4623
|
};
|
|
4235
4624
|
} catch (error) {
|
|
4236
4625
|
return { output: `Server MCP ${server} error: ${error.message}`, isError: true };
|
|
@@ -4264,7 +4653,7 @@ import { homedir as homedir2 } from "os";
|
|
|
4264
4653
|
|
|
4265
4654
|
// src/context.ts
|
|
4266
4655
|
import { existsSync as existsSync8, readFileSync as readFileSync5 } from "fs";
|
|
4267
|
-
import { dirname as
|
|
4656
|
+
import { dirname as dirname7, join as join7 } from "path";
|
|
4268
4657
|
var FILES = ["AGENTS.md", "CLAUDE.md", "BCCLI.md"];
|
|
4269
4658
|
function readIf(path) {
|
|
4270
4659
|
try {
|
|
@@ -4275,11 +4664,11 @@ function readIf(path) {
|
|
|
4275
4664
|
}
|
|
4276
4665
|
function instructionPaths(cwd, home) {
|
|
4277
4666
|
const dirs2 = [];
|
|
4278
|
-
for (let dir = cwd; ; dir =
|
|
4667
|
+
for (let dir = cwd; ; dir = dirname7(dir)) {
|
|
4279
4668
|
dirs2.unshift(dir);
|
|
4280
|
-
if (
|
|
4669
|
+
if (dirname7(dir) === dir) break;
|
|
4281
4670
|
}
|
|
4282
|
-
return [
|
|
4671
|
+
return [join7(home, "BCCLI.md"), ...dirs2.flatMap((dir) => FILES.map((name) => join7(dir, name)))];
|
|
4283
4672
|
}
|
|
4284
4673
|
function loadInstructions(cwd, home) {
|
|
4285
4674
|
const parts = [];
|
|
@@ -4291,9 +4680,9 @@ ${text}`);
|
|
|
4291
4680
|
return parts.join("\n\n");
|
|
4292
4681
|
}
|
|
4293
4682
|
function isGitRepo(cwd) {
|
|
4294
|
-
for (let dir = cwd; ; dir =
|
|
4295
|
-
if (existsSync8(
|
|
4296
|
-
if (
|
|
4683
|
+
for (let dir = cwd; ; dir = dirname7(dir)) {
|
|
4684
|
+
if (existsSync8(join7(dir, ".git"))) return true;
|
|
4685
|
+
if (dirname7(dir) === dir) return false;
|
|
4297
4686
|
}
|
|
4298
4687
|
}
|
|
4299
4688
|
function buildSystemPrompt(opts) {
|
|
@@ -4336,16 +4725,34 @@ ${instructions}` : ""}`;
|
|
|
4336
4725
|
}
|
|
4337
4726
|
|
|
4338
4727
|
// src/extensions.ts
|
|
4339
|
-
import { existsSync as existsSync9, readdirSync, readFileSync as readFileSync6, statSync as
|
|
4340
|
-
import { basename, join as
|
|
4728
|
+
import { existsSync as existsSync9, readdirSync as readdirSync2, readFileSync as readFileSync6, statSync as statSync6 } from "fs";
|
|
4729
|
+
import { basename as basename2, join as join8 } from "path";
|
|
4341
4730
|
function parseFrontmatter(text) {
|
|
4342
4731
|
const match = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/.exec(text);
|
|
4343
4732
|
if (!match) return { data: {}, body: text };
|
|
4344
4733
|
const data = {};
|
|
4734
|
+
let currentListKey;
|
|
4345
4735
|
for (const line of match[1].split(/\r?\n/)) {
|
|
4736
|
+
const listItem = /^\s*-\s+(.+)$/.exec(line);
|
|
4737
|
+
if (listItem && currentListKey) {
|
|
4738
|
+
const item = listItem[1].trim().replace(/^["']|["']$/g, "");
|
|
4739
|
+
const prev = data[currentListKey];
|
|
4740
|
+
if (Array.isArray(prev)) prev.push(item);
|
|
4741
|
+
else data[currentListKey] = [item];
|
|
4742
|
+
continue;
|
|
4743
|
+
}
|
|
4346
4744
|
const kv = /^([A-Za-z0-9_-]+):\s*(.*)$/.exec(line);
|
|
4347
|
-
if (!kv)
|
|
4745
|
+
if (!kv) {
|
|
4746
|
+
if (line.trim()) currentListKey = void 0;
|
|
4747
|
+
continue;
|
|
4748
|
+
}
|
|
4348
4749
|
let value = kv[2].trim();
|
|
4750
|
+
if (!value) {
|
|
4751
|
+
currentListKey = kv[1];
|
|
4752
|
+
data[kv[1]] = "";
|
|
4753
|
+
continue;
|
|
4754
|
+
}
|
|
4755
|
+
currentListKey = void 0;
|
|
4349
4756
|
if (value.startsWith("[") && value.endsWith("]")) {
|
|
4350
4757
|
data[kv[1]] = value.slice(1, -1).split(",").map((s) => s.trim().replace(/^["']|["']$/g, "")).filter(Boolean);
|
|
4351
4758
|
continue;
|
|
@@ -4357,11 +4764,11 @@ function parseFrontmatter(text) {
|
|
|
4357
4764
|
}
|
|
4358
4765
|
var str = (v) => Array.isArray(v) ? v.join(", ") : v;
|
|
4359
4766
|
function dirs(r, kind) {
|
|
4360
|
-
return [
|
|
4767
|
+
return [join8(r.userHome, ".claude", kind), join8(r.home, kind), join8(r.cwd, ".claude", kind), join8(r.cwd, ".bccli", kind)];
|
|
4361
4768
|
}
|
|
4362
4769
|
function list(dir) {
|
|
4363
4770
|
try {
|
|
4364
|
-
return
|
|
4771
|
+
return readdirSync2(dir).sort();
|
|
4365
4772
|
} catch {
|
|
4366
4773
|
return [];
|
|
4367
4774
|
}
|
|
@@ -4370,11 +4777,11 @@ function loadSkills(r) {
|
|
|
4370
4777
|
const byName = /* @__PURE__ */ new Map();
|
|
4371
4778
|
for (const dir of dirs(r, "skills")) {
|
|
4372
4779
|
for (const entry of list(dir)) {
|
|
4373
|
-
const file =
|
|
4780
|
+
const file = join8(dir, entry, "SKILL.md");
|
|
4374
4781
|
if (!existsSync9(file)) continue;
|
|
4375
4782
|
const { data } = parseFrontmatter(readFileSync6(file, "utf8"));
|
|
4376
4783
|
const name = str(data.name) || entry;
|
|
4377
|
-
byName.set(name, { name, description: str(data.description) ?? "", dir:
|
|
4784
|
+
byName.set(name, { name, description: str(data.description) ?? "", dir: join8(dir, entry), file });
|
|
4378
4785
|
}
|
|
4379
4786
|
}
|
|
4380
4787
|
return [...byName.values()];
|
|
@@ -4383,10 +4790,10 @@ function loadCommands(r) {
|
|
|
4383
4790
|
const byName = /* @__PURE__ */ new Map();
|
|
4384
4791
|
for (const dir of dirs(r, "commands")) {
|
|
4385
4792
|
for (const entry of list(dir)) {
|
|
4386
|
-
const file =
|
|
4387
|
-
if (!entry.endsWith(".md") || !
|
|
4793
|
+
const file = join8(dir, entry);
|
|
4794
|
+
if (!entry.endsWith(".md") || !statSync6(file).isFile()) continue;
|
|
4388
4795
|
const { data, body } = parseFrontmatter(readFileSync6(file, "utf8"));
|
|
4389
|
-
const def = { name:
|
|
4796
|
+
const def = { name: basename2(entry, ".md"), body: body.trim() };
|
|
4390
4797
|
if (data.description) def.description = str(data.description);
|
|
4391
4798
|
if (data["argument-hint"]) def.argumentHint = str(data["argument-hint"]);
|
|
4392
4799
|
byName.set(def.name, def);
|
|
@@ -4399,8 +4806,8 @@ function loadAgentDefs(r) {
|
|
|
4399
4806
|
for (const dir of dirs(r, "agents")) {
|
|
4400
4807
|
for (const entry of list(dir)) {
|
|
4401
4808
|
if (!entry.endsWith(".md")) continue;
|
|
4402
|
-
const { data, body } = parseFrontmatter(readFileSync6(
|
|
4403
|
-
const name = str(data.name) ||
|
|
4809
|
+
const { data, body } = parseFrontmatter(readFileSync6(join8(dir, entry), "utf8"));
|
|
4810
|
+
const name = str(data.name) || basename2(entry, ".md");
|
|
4404
4811
|
const def = { name, description: str(data.description) ?? "", prompt: body.trim() };
|
|
4405
4812
|
if (data.tools)
|
|
4406
4813
|
def.tools = Array.isArray(data.tools) ? data.tools : String(data.tools).split(",").map((s) => s.trim()).filter(Boolean);
|
|
@@ -4418,8 +4825,7 @@ function loadAgentDefs(r) {
|
|
|
4418
4825
|
}
|
|
4419
4826
|
|
|
4420
4827
|
// src/providers.ts
|
|
4421
|
-
import {
|
|
4422
|
-
import { join as join7 } from "path";
|
|
4828
|
+
import { join as join9 } from "path";
|
|
4423
4829
|
function providerName(config, id) {
|
|
4424
4830
|
return PRESETS.find((p) => p.id === id)?.name ?? config.providers[id]?.name ?? id;
|
|
4425
4831
|
}
|
|
@@ -4478,11 +4884,11 @@ async function addProviderKey(providerId, key, deps) {
|
|
|
4478
4884
|
|
|
4479
4885
|
// src/session.ts
|
|
4480
4886
|
import { randomBytes as randomBytes2 } from "crypto";
|
|
4481
|
-
import { appendFileSync, existsSync as existsSync10, mkdirSync as
|
|
4482
|
-
import { join as
|
|
4887
|
+
import { appendFileSync, existsSync as existsSync10, mkdirSync as mkdirSync4, readdirSync as readdirSync3, readFileSync as readFileSync7, rmSync as rmSync2, statSync as statSync7 } from "fs";
|
|
4888
|
+
import { basename as basename3, join as join10 } from "path";
|
|
4483
4889
|
function projectDir(home, cwd) {
|
|
4484
4890
|
const slug = cwd.replace(/[^a-zA-Z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(-100) || "root";
|
|
4485
|
-
return
|
|
4891
|
+
return join10(home, "sessions", slug);
|
|
4486
4892
|
}
|
|
4487
4893
|
function repairTail(messages) {
|
|
4488
4894
|
const out = [...messages];
|
|
@@ -4500,19 +4906,22 @@ var Session = class _Session {
|
|
|
4500
4906
|
this.file = file;
|
|
4501
4907
|
}
|
|
4502
4908
|
file;
|
|
4909
|
+
get id() {
|
|
4910
|
+
return basename3(this.file, ".jsonl");
|
|
4911
|
+
}
|
|
4503
4912
|
static create(home, cwd, now = /* @__PURE__ */ new Date()) {
|
|
4504
4913
|
const dir = projectDir(home, cwd);
|
|
4505
|
-
|
|
4914
|
+
mkdirSync4(dir, { recursive: true });
|
|
4506
4915
|
const stamp = now.toISOString().replace(/[:.]/g, "-");
|
|
4507
|
-
return new _Session(
|
|
4916
|
+
return new _Session(join10(dir, `${stamp}-${randomBytes2(3).toString("hex")}.jsonl`));
|
|
4508
4917
|
}
|
|
4509
4918
|
static list(home, cwd) {
|
|
4510
4919
|
const dir = projectDir(home, cwd);
|
|
4511
4920
|
if (!existsSync10(dir)) return [];
|
|
4512
|
-
return
|
|
4513
|
-
const session = new _Session(
|
|
4514
|
-
const first = session.
|
|
4515
|
-
return { session, mtime:
|
|
4921
|
+
return readdirSync3(dir).filter((f) => f.endsWith(".jsonl")).map((f) => {
|
|
4922
|
+
const session = new _Session(join10(dir, f));
|
|
4923
|
+
const first = session.loadHistory().find((m) => m.role === "user");
|
|
4924
|
+
return { session, mtime: statSync7(session.file).mtime, preview: first ? contentText(first.content).slice(0, 80) : "" };
|
|
4516
4925
|
}).sort((a, b) => b.mtime.getTime() - a.mtime.getTime() || b.session.file.localeCompare(a.session.file));
|
|
4517
4926
|
}
|
|
4518
4927
|
static latest(home, cwd) {
|
|
@@ -4561,6 +4970,35 @@ var Session = class _Session {
|
|
|
4561
4970
|
}
|
|
4562
4971
|
return void 0;
|
|
4563
4972
|
}
|
|
4973
|
+
/** Accumulated token usage since the last non-compaction reset. */
|
|
4974
|
+
totalUsage() {
|
|
4975
|
+
const segments = [{ msgs: [], usage: { inputTokens: 0, outputTokens: 0 } }];
|
|
4976
|
+
for (const record of this.records()) {
|
|
4977
|
+
const cur = segments[segments.length - 1];
|
|
4978
|
+
if (record.t === "reset") {
|
|
4979
|
+
segments.push({ msgs: [], usage: { inputTokens: 0, outputTokens: 0 } });
|
|
4980
|
+
} else if (record.t === "msg") {
|
|
4981
|
+
cur.msgs.push(record.m);
|
|
4982
|
+
} else if (record.t === "usage") {
|
|
4983
|
+
cur.usage.inputTokens += record.inputTokens;
|
|
4984
|
+
cur.usage.outputTokens += record.outputTokens;
|
|
4985
|
+
if (record.cachedInputTokens) cur.usage.cachedInputTokens = (cur.usage.cachedInputTokens ?? 0) + record.cachedInputTokens;
|
|
4986
|
+
if (record.cacheWriteTokens) cur.usage.cacheWriteTokens = (cur.usage.cacheWriteTokens ?? 0) + record.cacheWriteTokens;
|
|
4987
|
+
}
|
|
4988
|
+
}
|
|
4989
|
+
let total = { ...segments[0].usage };
|
|
4990
|
+
for (const seg of segments.slice(1)) {
|
|
4991
|
+
if (!isCompactionSummary(seg.msgs[0])) {
|
|
4992
|
+
total = { ...seg.usage };
|
|
4993
|
+
} else {
|
|
4994
|
+
total.inputTokens += seg.usage.inputTokens;
|
|
4995
|
+
total.outputTokens += seg.usage.outputTokens;
|
|
4996
|
+
if (seg.usage.cachedInputTokens) total.cachedInputTokens = (total.cachedInputTokens ?? 0) + seg.usage.cachedInputTokens;
|
|
4997
|
+
if (seg.usage.cacheWriteTokens) total.cacheWriteTokens = (total.cacheWriteTokens ?? 0) + seg.usage.cacheWriteTokens;
|
|
4998
|
+
}
|
|
4999
|
+
}
|
|
5000
|
+
return total;
|
|
5001
|
+
}
|
|
4564
5002
|
/**
|
|
4565
5003
|
* What the user sees on resume: the whole conversation. Compaction only shrinks what the model is sent, so
|
|
4566
5004
|
* its reset does not hide the older turns here. A real reset (/clear, rewind) still starts over, the
|
|
@@ -4573,12 +5011,22 @@ var Session = class _Session {
|
|
|
4573
5011
|
else if (record.t === "msg") segments[segments.length - 1].push(record.m);
|
|
4574
5012
|
}
|
|
4575
5013
|
let shown = segments[0];
|
|
5014
|
+
let lastSummary;
|
|
4576
5015
|
for (const segment of segments.slice(1)) {
|
|
4577
5016
|
if (!isCompactionSummary(segment[0])) {
|
|
4578
5017
|
shown = segment;
|
|
5018
|
+
lastSummary = void 0;
|
|
4579
5019
|
continue;
|
|
4580
5020
|
}
|
|
4581
5021
|
const kept = segment.slice(2);
|
|
5022
|
+
if (lastSummary && sameMessage2(segment[0], lastSummary)) {
|
|
5023
|
+
const subIdx = findContiguousSlice(shown, kept);
|
|
5024
|
+
if (subIdx !== -1) {
|
|
5025
|
+
shown = shown.slice(0, subIdx + kept.length);
|
|
5026
|
+
continue;
|
|
5027
|
+
}
|
|
5028
|
+
}
|
|
5029
|
+
lastSummary = segment[0];
|
|
4582
5030
|
let at = 0;
|
|
4583
5031
|
while (at <= shown.length && !endsWithPrefixOf(shown, at, kept)) at++;
|
|
4584
5032
|
shown = [...shown, ...kept.slice(shown.length - at)];
|
|
@@ -4605,6 +5053,20 @@ function endsWithPrefixOf(shown, at, kept) {
|
|
|
4605
5053
|
for (let j = 0; j < n; j++) if (!sameMessage2(shown[at + j], kept[j])) return false;
|
|
4606
5054
|
return true;
|
|
4607
5055
|
}
|
|
5056
|
+
function findContiguousSlice(shown, slice) {
|
|
5057
|
+
if (slice.length === 0) return shown.length;
|
|
5058
|
+
for (let i = 0; i + slice.length <= shown.length; i++) {
|
|
5059
|
+
let ok = true;
|
|
5060
|
+
for (let j = 0; j < slice.length; j++) {
|
|
5061
|
+
if (!sameMessage2(shown[i + j], slice[j])) {
|
|
5062
|
+
ok = false;
|
|
5063
|
+
break;
|
|
5064
|
+
}
|
|
5065
|
+
}
|
|
5066
|
+
if (ok) return i;
|
|
5067
|
+
}
|
|
5068
|
+
return -1;
|
|
5069
|
+
}
|
|
4608
5070
|
|
|
4609
5071
|
// src/tools/plan.ts
|
|
4610
5072
|
import { z as z10 } from "zod";
|
|
@@ -4714,6 +5176,7 @@ import { z as z13 } from "zod";
|
|
|
4714
5176
|
function createTaskTool(opts) {
|
|
4715
5177
|
const byName = new Map(opts.agents.map((a) => [a.name, a]));
|
|
4716
5178
|
const list2 = opts.agents.map((a) => `- ${a.name}: ${a.description}`).join("\n");
|
|
5179
|
+
const sessions = /* @__PURE__ */ new Map();
|
|
4717
5180
|
let askChain = Promise.resolve();
|
|
4718
5181
|
const enqueueAsk = (fn) => {
|
|
4719
5182
|
const next = askChain.then(
|
|
@@ -4733,7 +5196,8 @@ ${list2}`,
|
|
|
4733
5196
|
schema: z13.object({
|
|
4734
5197
|
agent: z13.string().describe("Agent name"),
|
|
4735
5198
|
description: z13.string().describe("3-6 word summary shown to the user"),
|
|
4736
|
-
prompt: z13.string().describe("Full instructions; the subagent sees nothing else")
|
|
5199
|
+
prompt: z13.string().describe("Full instructions; the subagent sees nothing else"),
|
|
5200
|
+
session_id: z13.string().optional().describe("Optional session id to continue a previous subagent conversation")
|
|
4737
5201
|
}),
|
|
4738
5202
|
kind: "read",
|
|
4739
5203
|
target: (input) => `[${input.agent}] ${input.description}`,
|
|
@@ -4760,6 +5224,7 @@ ${list2}`,
|
|
|
4760
5224
|
} catch (error) {
|
|
4761
5225
|
return { output: `Agent "${def.name}": ${error.message}`, isError: true };
|
|
4762
5226
|
}
|
|
5227
|
+
const priorHistory = input.session_id ? sessions.get(input.session_id) : void 0;
|
|
4763
5228
|
const child = new Agent2({
|
|
4764
5229
|
provider,
|
|
4765
5230
|
tools,
|
|
@@ -4769,6 +5234,7 @@ ${list2}`,
|
|
|
4769
5234
|
# Your role
|
|
4770
5235
|
${def.prompt}`,
|
|
4771
5236
|
cwd: opts.cwd,
|
|
5237
|
+
...priorHistory ? { history: priorHistory } : {},
|
|
4772
5238
|
// Every subagent runs with a step cap so a looping child cannot burn tokens forever.
|
|
4773
5239
|
maxSteps: def.maxSteps ?? 50,
|
|
4774
5240
|
label: def.name,
|
|
@@ -4789,12 +5255,29 @@ ${def.prompt}`,
|
|
|
4789
5255
|
const ask = ctx.ask;
|
|
4790
5256
|
if (ask) child.askPermission = (req) => enqueueAsk(() => ask(req));
|
|
4791
5257
|
await child.run(input.prompt, ctx.signal);
|
|
5258
|
+
if (input.session_id) sessions.set(input.session_id, [...child.messages]);
|
|
4792
5259
|
ctx.addUsage?.(child.totalUsage);
|
|
4793
5260
|
const tokens2 = child.totalUsage.inputTokens + child.totalUsage.outputTokens;
|
|
4794
5261
|
const display = t("{steps} steps \xB7 {tokens} tokens", { steps, tokens: tokens2 >= 1e3 ? `${Math.round(tokens2 / 1e3)}k` : tokens2 });
|
|
4795
5262
|
if (outcome?.type === "error") return { output: `Subagent ${def.name} failed: ${outcome.message}`, isError: true, display };
|
|
4796
5263
|
if (outcome?.type === "aborted") return { output: `Subagent ${def.name} was cancelled.`, isError: true, display };
|
|
4797
|
-
|
|
5264
|
+
let last = [...child.messages].reverse().find((m) => m.role === "assistant" && m.content);
|
|
5265
|
+
if (!last && outcome?.type === "stepLimit" && !ctx.signal.aborted) {
|
|
5266
|
+
try {
|
|
5267
|
+
const synthesis = await provider.chat({
|
|
5268
|
+
messages: [
|
|
5269
|
+
...child.messages,
|
|
5270
|
+
{ role: "user", content: "Step limit reached. Summarize all findings gathered so far concisely." }
|
|
5271
|
+
],
|
|
5272
|
+
signal: ctx.signal,
|
|
5273
|
+
reasoning: opts.reasoning?.() ?? "auto"
|
|
5274
|
+
});
|
|
5275
|
+
if (synthesis.text.trim()) {
|
|
5276
|
+
last = { role: "assistant", content: synthesis.text.trim() };
|
|
5277
|
+
}
|
|
5278
|
+
} catch {
|
|
5279
|
+
}
|
|
5280
|
+
}
|
|
4798
5281
|
const report = last && typeof last.content === "string" ? last.content : "(no report)";
|
|
4799
5282
|
const suffix = outcome?.type === "stepLimit" ? `
|
|
4800
5283
|
|
|
@@ -4840,7 +5323,7 @@ function createTodoTool(store) {
|
|
|
4840
5323
|
|
|
4841
5324
|
// src/tools/memory.ts
|
|
4842
5325
|
import { existsSync as existsSync12, readFileSync as readFileSync10 } from "fs";
|
|
4843
|
-
import { join as
|
|
5326
|
+
import { join as join11 } from "path";
|
|
4844
5327
|
import { z as z15 } from "zod";
|
|
4845
5328
|
|
|
4846
5329
|
// src/slash/memory.ts
|
|
@@ -4863,11 +5346,11 @@ function createSaveMemoryTool(opts) {
|
|
|
4863
5346
|
kind: "edit",
|
|
4864
5347
|
// The target is what the prompt shows and what the permission model judges: the
|
|
4865
5348
|
// global store really lives outside the project, so it must not look like one.
|
|
4866
|
-
target: (input) => input.scope === "global" ?
|
|
5349
|
+
target: (input) => input.scope === "global" ? join11(opts.home, "BCCLI.md") : "AGENTS.md",
|
|
4867
5350
|
async run(input, ctx) {
|
|
4868
5351
|
const content = input.content.trim();
|
|
4869
5352
|
if (!content) return { output: "Memory content is empty.", isError: true };
|
|
4870
|
-
const file = input.scope === "global" ?
|
|
5353
|
+
const file = input.scope === "global" ? join11(opts.home, "BCCLI.md") : join11(ctx.cwd, "AGENTS.md");
|
|
4871
5354
|
if (existsSync12(file)) {
|
|
4872
5355
|
const lines = readFileSync10(file, "utf8").split("\n");
|
|
4873
5356
|
if (lines.some((line) => line.trim() === `- ${content}`)) {
|
|
@@ -4885,18 +5368,21 @@ function createSaveMemoryTool(opts) {
|
|
|
4885
5368
|
import { z as z16 } from "zod";
|
|
4886
5369
|
|
|
4887
5370
|
// src/repomap.ts
|
|
4888
|
-
import { readdirSync as
|
|
4889
|
-
import { extname, join as
|
|
5371
|
+
import { readdirSync as readdirSync4, readFileSync as readFileSync11 } from "fs";
|
|
5372
|
+
import { extname, join as join12, relative as relative2, sep } from "path";
|
|
4890
5373
|
var SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
4891
5374
|
"node_modules",
|
|
4892
5375
|
".git",
|
|
4893
5376
|
"dist",
|
|
4894
5377
|
"build",
|
|
5378
|
+
"out",
|
|
4895
5379
|
"coverage",
|
|
4896
5380
|
".next",
|
|
5381
|
+
".turbo",
|
|
4897
5382
|
"target",
|
|
4898
5383
|
"vendor",
|
|
4899
5384
|
"__pycache__",
|
|
5385
|
+
"venv",
|
|
4900
5386
|
".venv",
|
|
4901
5387
|
".cache"
|
|
4902
5388
|
]);
|
|
@@ -4922,21 +5408,22 @@ var CODE_EXTS = /* @__PURE__ */ new Set([
|
|
|
4922
5408
|
".ps1"
|
|
4923
5409
|
]);
|
|
4924
5410
|
var MAX_FILES = 400;
|
|
5411
|
+
var MAX_SCAN_FILES = 5e3;
|
|
4925
5412
|
var MAX_FILE_BYTES = 2e5;
|
|
4926
5413
|
var MAX_SYMBOLS_PER_FILE = 12;
|
|
4927
5414
|
var FAMILY = {
|
|
4928
5415
|
ts: [
|
|
4929
|
-
/^\s*(?:export\s+)?(?:default\s+)?(?:declare\s+)?(?:abstract\s+)?(?:async\s+)?(?:function|class|interface|type|enum)\s+([A-Za-z0-9_$]+)/,
|
|
5416
|
+
/^\s*(?:export\s+)?(?:default\s+)?(?:declare\s+)?(?:public\s+|private\s+|protected\s+|internal\s+)?(?:static\s+)?(?:abstract\s+|final\s+|sealed\s+)?(?:async\s+)?(?:function|class|interface|type|enum)\s+([A-Za-z0-9_$]+)/,
|
|
4930
5417
|
/^\s*export\s+(?:const|let)\s+([A-Za-z0-9_$]+)/
|
|
4931
5418
|
],
|
|
4932
|
-
py: [/^\s*(?:async\s+)?def\s+(\w+)/, /^\s*class\s+(\w+)/],
|
|
5419
|
+
py: [/^\s*(?:async\s+)?def\s+(\w+)/, /^\s*(?:module|class)\s+(\w+)/],
|
|
4933
5420
|
go: [/^func\s+(?:\([^)]*\)\s*)?(\w+)/, /^type\s+(\w+)/],
|
|
4934
5421
|
rs: [/^\s*(?:pub(?:\([^)]*\))?\s+)?(?:unsafe\s+)?(?:async\s+)?(?:fn|struct|enum|trait|type)\s+(\w+)/],
|
|
4935
5422
|
c: [/^\s*(?:static\s+|inline\s+|extern\s+)*[A-Za-z_][A-Za-z0-9_\s*]*\b(\w+)\s*\([^;]*\)\s*\{?\s*$/]
|
|
4936
5423
|
};
|
|
4937
5424
|
function familyFor(ext) {
|
|
4938
|
-
if ([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".java", ".cs", ".php"
|
|
4939
|
-
if (ext === ".py" || ext === ".sh" || ext === ".ps1") return FAMILY.py;
|
|
5425
|
+
if ([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".java", ".cs", ".php"].includes(ext)) return FAMILY.ts;
|
|
5426
|
+
if (ext === ".py" || ext === ".rb" || ext === ".sh" || ext === ".ps1") return FAMILY.py;
|
|
4940
5427
|
if (ext === ".go") return FAMILY.go;
|
|
4941
5428
|
if (ext === ".rs") return FAMILY.rs;
|
|
4942
5429
|
return FAMILY.c;
|
|
@@ -4956,16 +5443,16 @@ function extractSymbols(text, ext) {
|
|
|
4956
5443
|
return out;
|
|
4957
5444
|
}
|
|
4958
5445
|
function walk(dir, root, files) {
|
|
4959
|
-
if (files.length >=
|
|
5446
|
+
if (files.length >= MAX_SCAN_FILES) return;
|
|
4960
5447
|
let entries;
|
|
4961
5448
|
try {
|
|
4962
|
-
entries =
|
|
5449
|
+
entries = readdirSync4(dir, { withFileTypes: true });
|
|
4963
5450
|
} catch {
|
|
4964
5451
|
return;
|
|
4965
5452
|
}
|
|
4966
5453
|
for (const e of entries.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
4967
|
-
if (files.length >=
|
|
4968
|
-
const full =
|
|
5454
|
+
if (files.length >= MAX_SCAN_FILES) return;
|
|
5455
|
+
const full = join12(dir, e.name);
|
|
4969
5456
|
if (e.isDirectory()) {
|
|
4970
5457
|
if (SKIP_DIRS.has(e.name) || e.name.startsWith(".") && full !== root) continue;
|
|
4971
5458
|
walk(full, root, files);
|
|
@@ -4976,16 +5463,17 @@ function walk(dir, root, files) {
|
|
|
4976
5463
|
}
|
|
4977
5464
|
function buildRepoMapSync(cwd, opts = {}) {
|
|
4978
5465
|
const maxTokens = opts.maxTokens ?? 2048;
|
|
4979
|
-
const
|
|
4980
|
-
walk(cwd, cwd,
|
|
4981
|
-
|
|
4982
|
-
(a, b) => relative2(cwd, a).split(
|
|
5466
|
+
const scanned = [];
|
|
5467
|
+
walk(cwd, cwd, scanned);
|
|
5468
|
+
scanned.sort(
|
|
5469
|
+
(a, b) => relative2(cwd, a).split(sep).length - relative2(cwd, b).split(sep).length || a.localeCompare(b)
|
|
4983
5470
|
);
|
|
5471
|
+
const files = scanned.slice(0, MAX_FILES);
|
|
4984
5472
|
const lines = [];
|
|
4985
5473
|
let used = 0;
|
|
4986
5474
|
let truncated = false;
|
|
4987
5475
|
for (const file of files) {
|
|
4988
|
-
const rel = relative2(cwd, file).split(
|
|
5476
|
+
const rel = relative2(cwd, file).split(sep).join("/");
|
|
4989
5477
|
let symbols = [];
|
|
4990
5478
|
try {
|
|
4991
5479
|
const text = readFileSync11(file, "utf8");
|
|
@@ -5020,13 +5508,50 @@ function createRepoMapTool() {
|
|
|
5020
5508
|
}
|
|
5021
5509
|
|
|
5022
5510
|
// src/tools/diagnostics.ts
|
|
5023
|
-
import { extname as
|
|
5511
|
+
import { extname as extname3, relative as relative3 } from "path";
|
|
5024
5512
|
import { z as z17 } from "zod";
|
|
5025
5513
|
|
|
5026
5514
|
// src/lsp.ts
|
|
5027
5515
|
import { spawn as spawn4 } from "child_process";
|
|
5028
5516
|
import { readFileSync as readFileSync12 } from "fs";
|
|
5029
|
-
import {
|
|
5517
|
+
import { extname as extname2 } from "path";
|
|
5518
|
+
import { fileURLToPath, pathToFileURL } from "url";
|
|
5519
|
+
var LANGUAGE_IDS = {
|
|
5520
|
+
".ts": "typescript",
|
|
5521
|
+
".tsx": "typescriptreact",
|
|
5522
|
+
".mts": "typescript",
|
|
5523
|
+
".cts": "typescript",
|
|
5524
|
+
".js": "javascript",
|
|
5525
|
+
".jsx": "javascriptreact",
|
|
5526
|
+
".mjs": "javascript",
|
|
5527
|
+
".cjs": "javascript",
|
|
5528
|
+
".py": "python",
|
|
5529
|
+
".rs": "rust",
|
|
5530
|
+
".go": "go",
|
|
5531
|
+
".java": "java",
|
|
5532
|
+
".c": "c",
|
|
5533
|
+
".h": "c",
|
|
5534
|
+
".cpp": "cpp",
|
|
5535
|
+
".hpp": "cpp",
|
|
5536
|
+
".cs": "csharp",
|
|
5537
|
+
".rb": "ruby",
|
|
5538
|
+
".php": "php",
|
|
5539
|
+
".json": "json",
|
|
5540
|
+
".md": "markdown",
|
|
5541
|
+
".sh": "shellscript",
|
|
5542
|
+
".ps1": "powershell",
|
|
5543
|
+
".yaml": "yaml",
|
|
5544
|
+
".yml": "yaml",
|
|
5545
|
+
".toml": "toml",
|
|
5546
|
+
".html": "html",
|
|
5547
|
+
".css": "css"
|
|
5548
|
+
};
|
|
5549
|
+
function languageIdFor(file) {
|
|
5550
|
+
return LANGUAGE_IDS[extname2(file).toLowerCase()] ?? "plaintext";
|
|
5551
|
+
}
|
|
5552
|
+
function useShellForLsp(command, platform = process.platform) {
|
|
5553
|
+
return platform === "win32" && !/\.(exe|mjs|cjs|js)$/i.test(command.trim());
|
|
5554
|
+
}
|
|
5030
5555
|
function encodeLsp(msg) {
|
|
5031
5556
|
const body = Buffer.from(JSON.stringify(msg), "utf8");
|
|
5032
5557
|
return Buffer.concat([Buffer.from(`Content-Length: ${body.length}\r
|
|
@@ -5058,89 +5583,281 @@ function createLspParser(onMessage) {
|
|
|
5058
5583
|
}
|
|
5059
5584
|
};
|
|
5060
5585
|
}
|
|
5061
|
-
function
|
|
5062
|
-
|
|
5063
|
-
|
|
5064
|
-
|
|
5065
|
-
|
|
5066
|
-
|
|
5067
|
-
|
|
5068
|
-
|
|
5069
|
-
|
|
5070
|
-
|
|
5071
|
-
|
|
5072
|
-
|
|
5073
|
-
|
|
5074
|
-
|
|
5075
|
-
|
|
5076
|
-
|
|
5077
|
-
|
|
5078
|
-
|
|
5079
|
-
|
|
5080
|
-
|
|
5081
|
-
|
|
5082
|
-
|
|
5586
|
+
function extractHoverText(contents) {
|
|
5587
|
+
if (!contents) return "";
|
|
5588
|
+
if (typeof contents === "string") return contents.trim();
|
|
5589
|
+
if (Array.isArray(contents)) return contents.map(extractHoverText).filter(Boolean).join("\n\n");
|
|
5590
|
+
if (typeof contents === "object" && "value" in contents && typeof contents.value === "string") {
|
|
5591
|
+
return contents.value.trim();
|
|
5592
|
+
}
|
|
5593
|
+
return "";
|
|
5594
|
+
}
|
|
5595
|
+
function normalizeLocations(raw) {
|
|
5596
|
+
const items = Array.isArray(raw) ? raw : raw ? [raw] : [];
|
|
5597
|
+
const out = [];
|
|
5598
|
+
for (const item of items) {
|
|
5599
|
+
if (!item || typeof item !== "object") continue;
|
|
5600
|
+
const rec = item;
|
|
5601
|
+
const uri = rec.uri ?? rec.targetUri;
|
|
5602
|
+
const start = rec.range?.start ?? rec.targetSelectionRange?.start ?? rec.targetRange?.start;
|
|
5603
|
+
if (!uri) continue;
|
|
5604
|
+
let path = uri;
|
|
5605
|
+
try {
|
|
5606
|
+
if (uri.startsWith("file:")) path = fileURLToPath(uri);
|
|
5607
|
+
} catch {
|
|
5608
|
+
}
|
|
5609
|
+
out.push({
|
|
5610
|
+
uri,
|
|
5611
|
+
path,
|
|
5612
|
+
line: start?.line ?? 0,
|
|
5613
|
+
character: start?.character ?? 0
|
|
5614
|
+
});
|
|
5615
|
+
}
|
|
5616
|
+
return out;
|
|
5617
|
+
}
|
|
5618
|
+
function createWarmLspClient(opts) {
|
|
5619
|
+
let child;
|
|
5620
|
+
let initPromise;
|
|
5621
|
+
let nextId = 2;
|
|
5622
|
+
const opened = /* @__PURE__ */ new Map();
|
|
5623
|
+
const pendingRpc = /* @__PURE__ */ new Map();
|
|
5624
|
+
const diagWaiters = /* @__PURE__ */ new Map();
|
|
5625
|
+
const send = (msg) => {
|
|
5626
|
+
try {
|
|
5627
|
+
child?.stdin?.write(encodeLsp(msg));
|
|
5628
|
+
} catch {
|
|
5629
|
+
}
|
|
5630
|
+
};
|
|
5631
|
+
const resetState = () => {
|
|
5632
|
+
child = void 0;
|
|
5633
|
+
initPromise = void 0;
|
|
5634
|
+
opened.clear();
|
|
5635
|
+
for (const [, resolve5] of pendingRpc) resolve5(void 0);
|
|
5636
|
+
pendingRpc.clear();
|
|
5637
|
+
for (const [, waiters] of diagWaiters) {
|
|
5638
|
+
for (const w of waiters) w([]);
|
|
5639
|
+
}
|
|
5640
|
+
diagWaiters.clear();
|
|
5641
|
+
};
|
|
5642
|
+
const ensureStarted = () => {
|
|
5643
|
+
if (initPromise && child && !child.killed) return initPromise;
|
|
5644
|
+
opened.clear();
|
|
5645
|
+
initPromise = new Promise((resolveInit) => {
|
|
5646
|
+
const proc = spawn4(opts.server.command, opts.server.args ?? [], {
|
|
5647
|
+
cwd: opts.cwd,
|
|
5648
|
+
stdio: ["pipe", "pipe", "ignore"],
|
|
5649
|
+
shell: useShellForLsp(opts.server.command)
|
|
5650
|
+
});
|
|
5651
|
+
child = proc;
|
|
5652
|
+
let initSettled = false;
|
|
5653
|
+
const settleInit = () => {
|
|
5654
|
+
if (!initSettled) {
|
|
5655
|
+
initSettled = true;
|
|
5656
|
+
resolveInit();
|
|
5083
5657
|
}
|
|
5084
|
-
}
|
|
5085
|
-
|
|
5086
|
-
|
|
5087
|
-
|
|
5088
|
-
|
|
5089
|
-
|
|
5090
|
-
if (msg.id === 1 && msg.result) {
|
|
5091
|
-
send({ jsonrpc: "2.0", method: "initialized", params: {} });
|
|
5092
|
-
let text = "";
|
|
5093
|
-
try {
|
|
5094
|
-
text = readFileSync12(opts.file, "utf8");
|
|
5095
|
-
} catch {
|
|
5658
|
+
};
|
|
5659
|
+
const parser = createLspParser((msg) => {
|
|
5660
|
+
if (msg.id === 1 && msg.result !== void 0) {
|
|
5661
|
+
send({ jsonrpc: "2.0", method: "initialized", params: {} });
|
|
5662
|
+
settleInit();
|
|
5663
|
+
return;
|
|
5096
5664
|
}
|
|
5097
|
-
|
|
5098
|
-
|
|
5099
|
-
|
|
5100
|
-
|
|
5101
|
-
|
|
5102
|
-
|
|
5103
|
-
|
|
5104
|
-
|
|
5105
|
-
(msg.params.diagnostics ?? []).map((d) => ({
|
|
5665
|
+
if (msg.id !== void 0 && pendingRpc.has(msg.id)) {
|
|
5666
|
+
const fn = pendingRpc.get(msg.id);
|
|
5667
|
+
pendingRpc.delete(msg.id);
|
|
5668
|
+
fn?.(msg.result);
|
|
5669
|
+
return;
|
|
5670
|
+
}
|
|
5671
|
+
if (msg.method === "textDocument/publishDiagnostics" && msg.params?.uri) {
|
|
5672
|
+
const uri = msg.params.uri;
|
|
5673
|
+
const diags = (msg.params.diagnostics ?? []).map((d) => ({
|
|
5106
5674
|
line: d.range?.start?.line ?? 0,
|
|
5107
5675
|
character: d.range?.start?.character ?? 0,
|
|
5108
5676
|
severity: d.severity ?? 1,
|
|
5109
5677
|
message: String(d.message ?? "")
|
|
5110
|
-
}))
|
|
5111
|
-
|
|
5112
|
-
|
|
5678
|
+
}));
|
|
5679
|
+
const list2 = diagWaiters.get(uri);
|
|
5680
|
+
if (list2?.length) {
|
|
5681
|
+
diagWaiters.delete(uri);
|
|
5682
|
+
for (const w of list2) w(diags);
|
|
5683
|
+
}
|
|
5684
|
+
}
|
|
5685
|
+
});
|
|
5686
|
+
proc.stdout?.on("data", (c) => parser.push(c));
|
|
5687
|
+
proc.on("error", () => {
|
|
5688
|
+
resetState();
|
|
5689
|
+
settleInit();
|
|
5690
|
+
});
|
|
5691
|
+
proc.on("close", () => {
|
|
5692
|
+
resetState();
|
|
5693
|
+
settleInit();
|
|
5694
|
+
});
|
|
5695
|
+
send({
|
|
5696
|
+
jsonrpc: "2.0",
|
|
5697
|
+
id: 1,
|
|
5698
|
+
method: "initialize",
|
|
5699
|
+
params: { processId: process.pid, capabilities: {}, rootUri: pathToFileURL(opts.cwd).href }
|
|
5700
|
+
});
|
|
5113
5701
|
});
|
|
5114
|
-
|
|
5115
|
-
|
|
5116
|
-
|
|
5117
|
-
|
|
5118
|
-
|
|
5119
|
-
|
|
5120
|
-
|
|
5121
|
-
|
|
5702
|
+
return initPromise;
|
|
5703
|
+
};
|
|
5704
|
+
const syncDocument = (file) => {
|
|
5705
|
+
const uri = pathToFileURL(file).href;
|
|
5706
|
+
let text = "";
|
|
5707
|
+
try {
|
|
5708
|
+
text = readFileSync12(file, "utf8");
|
|
5709
|
+
} catch {
|
|
5710
|
+
}
|
|
5711
|
+
const prevVersion = opened.get(uri);
|
|
5712
|
+
if (prevVersion === void 0) {
|
|
5713
|
+
opened.set(uri, 1);
|
|
5714
|
+
send({
|
|
5715
|
+
jsonrpc: "2.0",
|
|
5716
|
+
method: "textDocument/didOpen",
|
|
5717
|
+
params: { textDocument: { uri, languageId: languageIdFor(file), version: 1, text } }
|
|
5718
|
+
});
|
|
5719
|
+
} else {
|
|
5720
|
+
const version = prevVersion + 1;
|
|
5721
|
+
opened.set(uri, version);
|
|
5722
|
+
send({
|
|
5723
|
+
jsonrpc: "2.0",
|
|
5724
|
+
method: "textDocument/didChange",
|
|
5725
|
+
params: {
|
|
5726
|
+
textDocument: { uri, version },
|
|
5727
|
+
contentChanges: [{ text }]
|
|
5728
|
+
}
|
|
5729
|
+
});
|
|
5730
|
+
}
|
|
5731
|
+
return uri;
|
|
5732
|
+
};
|
|
5733
|
+
const request = (method, params, timeoutMs = opts.timeoutMs ?? 8e3) => new Promise((resolve5) => {
|
|
5734
|
+
const id = nextId++;
|
|
5735
|
+
const timer = setTimeout(() => {
|
|
5736
|
+
pendingRpc.delete(id);
|
|
5737
|
+
resolve5(void 0);
|
|
5738
|
+
}, timeoutMs);
|
|
5739
|
+
pendingRpc.set(id, (res) => {
|
|
5740
|
+
clearTimeout(timer);
|
|
5741
|
+
resolve5(res);
|
|
5122
5742
|
});
|
|
5743
|
+
send({ jsonrpc: "2.0", id, method, params });
|
|
5123
5744
|
});
|
|
5745
|
+
return {
|
|
5746
|
+
async diagnostics(file, timeoutMs = opts.timeoutMs ?? 8e3) {
|
|
5747
|
+
await ensureStarted();
|
|
5748
|
+
if (!child) return [];
|
|
5749
|
+
const uri = pathToFileURL(file).href;
|
|
5750
|
+
return new Promise((resolve5) => {
|
|
5751
|
+
const timer = setTimeout(() => {
|
|
5752
|
+
const list3 = diagWaiters.get(uri) ?? [];
|
|
5753
|
+
diagWaiters.set(
|
|
5754
|
+
uri,
|
|
5755
|
+
list3.filter((w) => w !== onDiag)
|
|
5756
|
+
);
|
|
5757
|
+
resolve5([]);
|
|
5758
|
+
}, timeoutMs);
|
|
5759
|
+
const onDiag = (diags) => {
|
|
5760
|
+
clearTimeout(timer);
|
|
5761
|
+
resolve5(diags);
|
|
5762
|
+
};
|
|
5763
|
+
const list2 = diagWaiters.get(uri) ?? [];
|
|
5764
|
+
list2.push(onDiag);
|
|
5765
|
+
diagWaiters.set(uri, list2);
|
|
5766
|
+
syncDocument(file);
|
|
5767
|
+
});
|
|
5768
|
+
},
|
|
5769
|
+
async hover(file, line, character, timeoutMs) {
|
|
5770
|
+
await ensureStarted();
|
|
5771
|
+
if (!child) return "";
|
|
5772
|
+
const uri = syncDocument(file);
|
|
5773
|
+
const res = await request("textDocument/hover", { textDocument: { uri }, position: { line, character } }, timeoutMs);
|
|
5774
|
+
return extractHoverText(res?.contents);
|
|
5775
|
+
},
|
|
5776
|
+
async definition(file, line, character, timeoutMs) {
|
|
5777
|
+
await ensureStarted();
|
|
5778
|
+
if (!child) return [];
|
|
5779
|
+
const uri = syncDocument(file);
|
|
5780
|
+
const res = await request("textDocument/definition", { textDocument: { uri }, position: { line, character } }, timeoutMs);
|
|
5781
|
+
return normalizeLocations(res);
|
|
5782
|
+
},
|
|
5783
|
+
async references(file, line, character, timeoutMs) {
|
|
5784
|
+
await ensureStarted();
|
|
5785
|
+
if (!child) return [];
|
|
5786
|
+
const uri = syncDocument(file);
|
|
5787
|
+
const res = await request(
|
|
5788
|
+
"textDocument/references",
|
|
5789
|
+
{ textDocument: { uri }, position: { line, character }, context: { includeDeclaration: true } },
|
|
5790
|
+
timeoutMs
|
|
5791
|
+
);
|
|
5792
|
+
return normalizeLocations(res);
|
|
5793
|
+
},
|
|
5794
|
+
async close() {
|
|
5795
|
+
if (!child) return;
|
|
5796
|
+
const proc = child;
|
|
5797
|
+
send({ jsonrpc: "2.0", id: nextId++, method: "shutdown" });
|
|
5798
|
+
send({ jsonrpc: "2.0", method: "exit" });
|
|
5799
|
+
resetState();
|
|
5800
|
+
const killer = setTimeout(() => {
|
|
5801
|
+
try {
|
|
5802
|
+
proc.kill();
|
|
5803
|
+
} catch {
|
|
5804
|
+
}
|
|
5805
|
+
}, 200);
|
|
5806
|
+
killer.unref?.();
|
|
5807
|
+
}
|
|
5808
|
+
};
|
|
5124
5809
|
}
|
|
5125
5810
|
|
|
5126
5811
|
// src/tools/diagnostics.ts
|
|
5127
5812
|
var SEVERITY = { 1: "error", 2: "warning", 3: "info", 4: "hint" };
|
|
5128
|
-
var SCHEMA = z17.object({
|
|
5813
|
+
var SCHEMA = z17.object({
|
|
5814
|
+
path: z17.string().describe("File to check (absolute or relative to the working directory)"),
|
|
5815
|
+
action: z17.enum(["diagnostics", "hover", "definition", "references"]).optional().describe("LSP query action, default diagnostics"),
|
|
5816
|
+
line: z17.number().int().min(1).optional().describe("1-based line number for hover, definition, or references"),
|
|
5817
|
+
character: z17.number().int().min(1).optional().describe("1-based column number for hover, definition, or references")
|
|
5818
|
+
});
|
|
5129
5819
|
function createDiagnosticsTool(servers) {
|
|
5820
|
+
const warmPool = /* @__PURE__ */ new Map();
|
|
5821
|
+
const clientFor = (server, cwd) => {
|
|
5822
|
+
const key = `${cwd}::${server.command}::${(server.args ?? []).join("\0")}`;
|
|
5823
|
+
let client = warmPool.get(key);
|
|
5824
|
+
if (!client) {
|
|
5825
|
+
client = createWarmLspClient({ server, cwd, timeoutMs: 15e3 });
|
|
5826
|
+
warmPool.set(key, client);
|
|
5827
|
+
}
|
|
5828
|
+
return client;
|
|
5829
|
+
};
|
|
5130
5830
|
return defineTool({
|
|
5131
5831
|
name: "diagnostics",
|
|
5132
|
-
description: '
|
|
5832
|
+
description: 'Query the configured LSP server(s) on a file for diagnostics (errors/warnings), hover docs, definition, or references. Configure servers under "lsp.servers" in ~/.bccli/config.json, e.g. { "extensions": [".ts"], "command": "typescript-language-server", "args": ["--stdio"] }.',
|
|
5133
5833
|
schema: SCHEMA,
|
|
5134
5834
|
kind: "read",
|
|
5135
5835
|
target: (input) => input.path,
|
|
5136
5836
|
async run(input, ctx) {
|
|
5137
5837
|
const file = resolvePath(ctx.cwd, input.path);
|
|
5138
|
-
const ext =
|
|
5838
|
+
const ext = extname3(file).toLowerCase();
|
|
5139
5839
|
const server = servers.find((s) => s.extensions.includes(ext));
|
|
5140
5840
|
if (!server) {
|
|
5141
5841
|
return { output: t("No LSP server is configured for {ext} files.", { ext: ext || "(none)" }), isError: true };
|
|
5142
5842
|
}
|
|
5143
|
-
const
|
|
5843
|
+
const client = clientFor(server, ctx.cwd);
|
|
5844
|
+
const action = input.action ?? "diagnostics";
|
|
5845
|
+
const line0 = Math.max(0, (input.line ?? 1) - 1);
|
|
5846
|
+
const char0 = Math.max(0, (input.character ?? 1) - 1);
|
|
5847
|
+
if (action === "hover") {
|
|
5848
|
+
const text = await client.hover(file, line0, char0, 15e3);
|
|
5849
|
+
return { output: text || "No hover information." };
|
|
5850
|
+
}
|
|
5851
|
+
if (action === "definition" || action === "references") {
|
|
5852
|
+
const locs = action === "definition" ? await client.definition(file, line0, char0, 15e3) : await client.references(file, line0, char0, 15e3);
|
|
5853
|
+
if (!locs.length) return { output: `No ${action} found.` };
|
|
5854
|
+
const formatted = locs.map((l) => {
|
|
5855
|
+
const rel = relative3(ctx.cwd, l.path).replace(/\\/g, "/") || input.path;
|
|
5856
|
+
return `${rel}:${l.line + 1}:${l.character + 1}`;
|
|
5857
|
+
});
|
|
5858
|
+
return { output: formatted.join("\n") };
|
|
5859
|
+
}
|
|
5860
|
+
const diags = await client.diagnostics(file, 15e3);
|
|
5144
5861
|
if (!diags.length) return { output: "No diagnostics." };
|
|
5145
5862
|
const lines = diags.map((d) => `${input.path}:${d.line + 1}:${d.character + 1} ${SEVERITY[d.severity] ?? "error"} ${d.message}`);
|
|
5146
5863
|
return { output: lines.join("\n"), isError: diags.some((d) => d.severity === 1) };
|
|
@@ -5151,7 +5868,7 @@ function createDiagnosticsTool(servers) {
|
|
|
5151
5868
|
// src/plugins.ts
|
|
5152
5869
|
import { existsSync as existsSync13 } from "fs";
|
|
5153
5870
|
import { createRequire } from "module";
|
|
5154
|
-
import { isAbsolute as isAbsolute3, join as
|
|
5871
|
+
import { isAbsolute as isAbsolute3, join as join13 } from "path";
|
|
5155
5872
|
import { z as z18 } from "zod";
|
|
5156
5873
|
var require_ = createRequire(import.meta.url);
|
|
5157
5874
|
function pluginLog(message) {
|
|
@@ -5176,7 +5893,7 @@ function loadPlugins(specs, opts) {
|
|
|
5176
5893
|
for (const spec of specs) {
|
|
5177
5894
|
let file;
|
|
5178
5895
|
try {
|
|
5179
|
-
file = isAbsolute3(spec) ? spec : spec.startsWith(".") ?
|
|
5896
|
+
file = isAbsolute3(spec) ? spec : spec.startsWith(".") ? join13(opts.home, spec) : require_.resolve(spec);
|
|
5180
5897
|
} catch {
|
|
5181
5898
|
throw new ConfigError(t("Plugin not found: {spec}", { spec }));
|
|
5182
5899
|
}
|
|
@@ -5253,7 +5970,7 @@ function createRuntime(opts) {
|
|
|
5253
5970
|
let config = loadConfig(opts.cwd, env);
|
|
5254
5971
|
let modelRef = opts.args.model ?? config.model;
|
|
5255
5972
|
let reasoning = opts.args.reasoning ?? config.reasoning;
|
|
5256
|
-
const
|
|
5973
|
+
const makeSingleProvider = (ref) => {
|
|
5257
5974
|
const resolved = resolveModel(config, ref, env);
|
|
5258
5975
|
return opts.provider ?? createProvider({
|
|
5259
5976
|
baseURL: resolved.baseURL,
|
|
@@ -5265,6 +5982,25 @@ function createRuntime(opts) {
|
|
|
5265
5982
|
enableProgrammaticToolCalling: lunaPtcAutoEnabled(env)
|
|
5266
5983
|
});
|
|
5267
5984
|
};
|
|
5985
|
+
const makeProvider = (ref) => {
|
|
5986
|
+
const primary = makeSingleProvider(ref);
|
|
5987
|
+
if (opts.provider) return primary;
|
|
5988
|
+
const fallbackRefs = [
|
|
5989
|
+
...opts.args.fallbackModel ? [opts.args.fallbackModel] : [],
|
|
5990
|
+
...config.fallbackModels ?? []
|
|
5991
|
+
].filter((r) => r && r !== ref);
|
|
5992
|
+
if (!fallbackRefs.length) return primary;
|
|
5993
|
+
return withFallbackProvider(
|
|
5994
|
+
primary,
|
|
5995
|
+
() => fallbackRefs.flatMap((fbRef) => {
|
|
5996
|
+
try {
|
|
5997
|
+
return [makeSingleProvider(fbRef)];
|
|
5998
|
+
} catch {
|
|
5999
|
+
return [];
|
|
6000
|
+
}
|
|
6001
|
+
})
|
|
6002
|
+
);
|
|
6003
|
+
};
|
|
5268
6004
|
const provider = makeProvider(modelRef);
|
|
5269
6005
|
const mode = opts.args.allowAll ? "allowAll" : opts.args.permissionMode ?? config.permissionMode;
|
|
5270
6006
|
const rules = [...config.allow, ...opts.args.allowedTools.map((t2) => TOOL_RULES[t2] ?? t2)];
|
|
@@ -5286,6 +6022,10 @@ function createRuntime(opts) {
|
|
|
5286
6022
|
if (latest) {
|
|
5287
6023
|
session = latest;
|
|
5288
6024
|
history = latest.load();
|
|
6025
|
+
if (!opts.args.model) {
|
|
6026
|
+
const prevModel = latest.lastModel();
|
|
6027
|
+
if (prevModel) modelRef = prevModel;
|
|
6028
|
+
}
|
|
5289
6029
|
}
|
|
5290
6030
|
}
|
|
5291
6031
|
const repoMap = config.repoMap ? buildRepoMapSync(opts.cwd) : void 0;
|
|
@@ -5329,7 +6069,7 @@ function createRuntime(opts) {
|
|
|
5329
6069
|
// Subagents get whatever the main agent has right now (incl. MCP tools), minus task itself.
|
|
5330
6070
|
baseTools: () => agent.tools.filter((t2) => t2.name !== "task"),
|
|
5331
6071
|
permissions,
|
|
5332
|
-
provider: () => agent.provider,
|
|
6072
|
+
provider: () => opts.provider ? agent.provider : makeProvider(modelRef),
|
|
5333
6073
|
providerFor: makeProvider,
|
|
5334
6074
|
systemPrompt: (childModelRef) => systemFor(childModelRef ?? modelRef),
|
|
5335
6075
|
cwd: opts.cwd,
|
|
@@ -5415,6 +6155,20 @@ function createRuntime(opts) {
|
|
|
5415
6155
|
session = previous;
|
|
5416
6156
|
agent.load(previous.load());
|
|
5417
6157
|
agent.totalUsage = { inputTokens: 0, outputTokens: 0 };
|
|
6158
|
+
if (!opts.args.model) {
|
|
6159
|
+
const prevModel = previous.lastModel();
|
|
6160
|
+
if (prevModel) {
|
|
6161
|
+
try {
|
|
6162
|
+
const nextProvider = makeProvider(prevModel);
|
|
6163
|
+
modelRef = prevModel;
|
|
6164
|
+
agent.provider = nextProvider;
|
|
6165
|
+
agent.setBudgetModel(prevModel);
|
|
6166
|
+
agent.setContextWindow(contextWindowFor(prevModel, env));
|
|
6167
|
+
agent.setSystemPrompt(systemFor(modelRef));
|
|
6168
|
+
} catch {
|
|
6169
|
+
}
|
|
6170
|
+
}
|
|
6171
|
+
}
|
|
5418
6172
|
todos.set([]);
|
|
5419
6173
|
checkpoints.clear();
|
|
5420
6174
|
turnMarks.length = 0;
|
|
@@ -5463,6 +6217,7 @@ function createRuntime(opts) {
|
|
|
5463
6217
|
const nextProvider = makeProvider(ref);
|
|
5464
6218
|
modelRef = ref;
|
|
5465
6219
|
agent.provider = nextProvider;
|
|
6220
|
+
agent.setBudgetModel(ref);
|
|
5466
6221
|
agent.setContextWindow(contextWindowFor(ref, env));
|
|
5467
6222
|
agent.setSystemPrompt(systemFor(modelRef));
|
|
5468
6223
|
},
|
|
@@ -5488,6 +6243,7 @@ async function* streamTask(opts) {
|
|
|
5488
6243
|
let finished = false;
|
|
5489
6244
|
let text = "";
|
|
5490
6245
|
let stopReason = "done";
|
|
6246
|
+
let errorMessage2;
|
|
5491
6247
|
const usage = { inputTokens: 0, outputTokens: 0 };
|
|
5492
6248
|
const toolCalls = [];
|
|
5493
6249
|
const byId = /* @__PURE__ */ new Map();
|
|
@@ -5537,28 +6293,45 @@ async function* streamTask(opts) {
|
|
|
5537
6293
|
case "aborted":
|
|
5538
6294
|
stopReason = "aborted";
|
|
5539
6295
|
break;
|
|
6296
|
+
case "error":
|
|
6297
|
+
stopReason = "error";
|
|
6298
|
+
errorMessage2 = event.message;
|
|
6299
|
+
push({ type: "error", message: event.message });
|
|
6300
|
+
break;
|
|
5540
6301
|
default:
|
|
5541
6302
|
break;
|
|
5542
6303
|
}
|
|
5543
6304
|
};
|
|
5544
|
-
const
|
|
5545
|
-
|
|
6305
|
+
const controller = new AbortController();
|
|
6306
|
+
const onParentAbort = () => controller.abort();
|
|
6307
|
+
if (opts.signal) {
|
|
6308
|
+
if (opts.signal.aborted) controller.abort();
|
|
6309
|
+
else opts.signal.addEventListener("abort", onParentAbort, { once: true });
|
|
6310
|
+
}
|
|
6311
|
+
const running = rt.agent.run(opts.prompt, controller.signal).catch((err) => {
|
|
6312
|
+
stopReason = "error";
|
|
6313
|
+
errorMessage2 = err instanceof Error ? err.message : String(err);
|
|
5546
6314
|
}).finally(() => {
|
|
5547
6315
|
finished = true;
|
|
5548
6316
|
wake?.();
|
|
5549
6317
|
});
|
|
5550
|
-
|
|
5551
|
-
|
|
5552
|
-
|
|
5553
|
-
|
|
6318
|
+
try {
|
|
6319
|
+
while (!finished || queue.length) {
|
|
6320
|
+
if (queue.length) {
|
|
6321
|
+
yield queue.shift();
|
|
6322
|
+
continue;
|
|
6323
|
+
}
|
|
6324
|
+
await new Promise((resolve5) => {
|
|
6325
|
+
wake = resolve5;
|
|
6326
|
+
});
|
|
6327
|
+
wake = null;
|
|
5554
6328
|
}
|
|
5555
|
-
await
|
|
5556
|
-
|
|
5557
|
-
|
|
5558
|
-
|
|
6329
|
+
await running;
|
|
6330
|
+
yield { type: "result", result: { text, toolCalls, usage, stopReason, ...errorMessage2 ? { error: errorMessage2 } : {} } };
|
|
6331
|
+
} finally {
|
|
6332
|
+
opts.signal?.removeEventListener("abort", onParentAbort);
|
|
6333
|
+
if (!finished) controller.abort();
|
|
5559
6334
|
}
|
|
5560
|
-
await running;
|
|
5561
|
-
yield { type: "result", result: { text, toolCalls, usage, stopReason } };
|
|
5562
6335
|
}
|
|
5563
6336
|
async function runTask(opts) {
|
|
5564
6337
|
let out = { text: "", toolCalls: [], usage: { inputTokens: 0, outputTokens: 0 }, stopReason: "done" };
|