@wrongstack/tools 0.275.1 → 0.276.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/{background-indexer-BoTUw0EM.d.ts → background-indexer-BeDBxfSh.d.ts} +6 -0
- package/dist/builtin.js +1056 -246
- package/dist/builtin.js.map +1 -1
- package/dist/codebase-index/index.d.ts +30 -2
- package/dist/codebase-index/index.js +201 -24
- package/dist/codebase-index/index.js.map +1 -1
- package/dist/codebase-index/worker.js +196 -23
- package/dist/codebase-index/worker.js.map +1 -1
- package/dist/document.js +2 -2
- package/dist/document.js.map +1 -1
- package/dist/edit.js +52 -15
- package/dist/edit.js.map +1 -1
- package/dist/fetch.js +89 -18
- package/dist/fetch.js.map +1 -1
- package/dist/glob.js +35 -1
- package/dist/glob.js.map +1 -1
- package/dist/grep.js +15 -4
- package/dist/grep.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1090 -255
- package/dist/index.js.map +1 -1
- package/dist/install.d.ts +7 -0
- package/dist/install.js +6 -0
- package/dist/install.js.map +1 -1
- package/dist/json.d.ts +26 -1
- package/dist/json.js +453 -46
- package/dist/json.js.map +1 -1
- package/dist/memory.js +26 -4
- package/dist/memory.js.map +1 -1
- package/dist/outdated.js +2 -2
- package/dist/outdated.js.map +1 -1
- package/dist/pack.js +1056 -246
- package/dist/pack.js.map +1 -1
- package/dist/read.js +36 -6
- package/dist/read.js.map +1 -1
- package/dist/replace.js +27 -9
- package/dist/replace.js.map +1 -1
- package/dist/search.d.ts +5 -1
- package/dist/search.js +179 -62
- package/dist/search.js.map +1 -1
- package/dist/tool-help.js +2 -2
- package/dist/tool-help.js.map +1 -1
- package/dist/tool-search.js +2 -2
- package/dist/tool-search.js.map +1 -1
- package/dist/write.js +13 -3
- package/dist/write.js.map +1 -1
- package/package.json +2 -2
package/dist/pack.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { spawn, execFileSync } from 'node:child_process';
|
|
2
2
|
import * as Core from '@wrongstack/core';
|
|
3
|
-
import { buildChildEnv, getDesignKitLoader, isDesignStack, loadActiveKit, applyTokenOverrides, setActiveKit, recordKitChoice, recordOverrides, setDesignOverrides, materializeTokens, runDesignVerify, detectNewlineStyle, normalizeToLf, toStyle, atomicWrite, unifiedDiff, isPrivateIPv4, isPrivateIPv6, assessCommitSafety, compileGlob, expectDefined, recordPackageAction, detectPackageEcosystem, mutatePlan, clearPlan, getPlanTemplate, addPlanItem, deriveTodosFromPlanItem, removePlanItem, setPlanItemStatus, mutateTasks, formatTaskList, formatPlan, toErrorMessage as toErrorMessage$2, computeTaskItemProgress, loadPlan, savePlan, loadTasks, saveTasks, wstackGlobalRoot, resolveWstackPaths, truncate } from '@wrongstack/core';
|
|
3
|
+
import { buildChildEnv, getDesignKitLoader, isDesignStack, loadActiveKit, applyTokenOverrides, setActiveKit, recordKitChoice, recordOverrides, setDesignOverrides, materializeTokens, runDesignVerify, ToolValidationError, detectNewlineStyle, normalizeToLf, toStyle, atomicWrite, unifiedDiff, ToolError, FetchError, isPrivateIPv4, isPrivateIPv6, assessCommitSafety, compileGlob, expectDefined, recordPackageAction, detectPackageEcosystem, deepMerge, mutatePlan, clearPlan, getPlanTemplate, addPlanItem, deriveTodosFromPlanItem, removePlanItem, setPlanItemStatus, mutateTasks, formatTaskList, formatPlan, FsError, toErrorMessage as toErrorMessage$2, computeTaskItemProgress, loadPlan, savePlan, loadTasks, saveTasks, wstackGlobalRoot, resolveWstackPaths, truncate } from '@wrongstack/core';
|
|
4
4
|
import * as fs from 'node:fs';
|
|
5
5
|
import { statSync, mkdirSync, createWriteStream } from 'node:fs';
|
|
6
6
|
import * as fs2 from 'node:fs/promises';
|
|
@@ -833,8 +833,8 @@ async function* spawnStream(opts) {
|
|
|
833
833
|
try {
|
|
834
834
|
for (; ; ) {
|
|
835
835
|
while (queue.length === 0) {
|
|
836
|
-
await new Promise((
|
|
837
|
-
waiter =
|
|
836
|
+
await new Promise((resolve7) => {
|
|
837
|
+
waiter = resolve7;
|
|
838
838
|
});
|
|
839
839
|
}
|
|
840
840
|
const chunk = queue.shift();
|
|
@@ -2133,10 +2133,10 @@ var bashTool = {
|
|
|
2133
2133
|
queue.push(c);
|
|
2134
2134
|
}
|
|
2135
2135
|
};
|
|
2136
|
-
const next = () => new Promise((
|
|
2136
|
+
const next = () => new Promise((resolve7) => {
|
|
2137
2137
|
const c = queue.shift();
|
|
2138
|
-
if (c)
|
|
2139
|
-
else resolveNext =
|
|
2138
|
+
if (c) resolve7(c);
|
|
2139
|
+
else resolveNext = resolve7;
|
|
2140
2140
|
});
|
|
2141
2141
|
let lastFlush = Date.now();
|
|
2142
2142
|
const flush = () => {
|
|
@@ -2536,8 +2536,8 @@ var Bm25Index = class {
|
|
|
2536
2536
|
df;
|
|
2537
2537
|
N;
|
|
2538
2538
|
safeAvgLen;
|
|
2539
|
-
score(
|
|
2540
|
-
const qTokens = tokenise(
|
|
2539
|
+
score(query, filter) {
|
|
2540
|
+
const qTokens = tokenise(query);
|
|
2541
2541
|
if (qTokens.length === 0) return [];
|
|
2542
2542
|
const results = [];
|
|
2543
2543
|
for (const doc of this.documents) {
|
|
@@ -2860,7 +2860,7 @@ var IndexStore = class {
|
|
|
2860
2860
|
);
|
|
2861
2861
|
}
|
|
2862
2862
|
// ─── Search ──────────────────────────────────────────────────────────────────
|
|
2863
|
-
search(
|
|
2863
|
+
search(query, filter) {
|
|
2864
2864
|
const conditions = [];
|
|
2865
2865
|
const values = [];
|
|
2866
2866
|
let effectiveKind = filter?.kind;
|
|
@@ -2884,8 +2884,8 @@ var IndexStore = class {
|
|
|
2884
2884
|
conditions.push("file LIKE ?");
|
|
2885
2885
|
values.push(`%${filter.file}%`);
|
|
2886
2886
|
}
|
|
2887
|
-
if (
|
|
2888
|
-
const tokens =
|
|
2887
|
+
if (query.trim()) {
|
|
2888
|
+
const tokens = query.toLowerCase().split(/\s+/).filter(Boolean);
|
|
2889
2889
|
const tokenConds = tokens.map(() => "text LIKE ?");
|
|
2890
2890
|
conditions.push(`(${tokenConds.join(" OR ")})`);
|
|
2891
2891
|
for (const t of tokens) values.push(`%${t}%`);
|
|
@@ -2919,10 +2919,10 @@ var IndexStore = class {
|
|
|
2919
2919
|
* `LIKE '%tok%'` recall for the common symbol-search shapes ("user" finds
|
|
2920
2920
|
* "users", camelCase-split text makes "complex" find "complexOperation").
|
|
2921
2921
|
*/
|
|
2922
|
-
searchRanked(
|
|
2923
|
-
const tokens = tokenise(
|
|
2922
|
+
searchRanked(query, filter, limit) {
|
|
2923
|
+
const tokens = tokenise(query);
|
|
2924
2924
|
if (tokens.length === 0 || !this.ftsAvailable) {
|
|
2925
|
-
return this.searchRankedFallback(
|
|
2925
|
+
return this.searchRankedFallback(query, filter, limit);
|
|
2926
2926
|
}
|
|
2927
2927
|
let effectiveKind = filter?.kind;
|
|
2928
2928
|
if (filter?.lspKind !== void 0) {
|
|
@@ -2979,19 +2979,19 @@ var IndexStore = class {
|
|
|
2979
2979
|
};
|
|
2980
2980
|
}
|
|
2981
2981
|
/** Legacy ranked path: LIKE candidates + in-process BM25 + JS snippets. */
|
|
2982
|
-
searchRankedFallback(
|
|
2983
|
-
const candidates = this.search(
|
|
2982
|
+
searchRankedFallback(query, filter, limit) {
|
|
2983
|
+
const candidates = this.search(query, filter);
|
|
2984
2984
|
if (candidates.length === 0) return { results: [], total: 0 };
|
|
2985
|
-
if (!
|
|
2985
|
+
if (!query.trim()) {
|
|
2986
2986
|
return { results: candidates.slice(0, limit), total: candidates.length };
|
|
2987
2987
|
}
|
|
2988
2988
|
const candidateById = new Map(candidates.map((c) => [c.id, c]));
|
|
2989
2989
|
const bm25 = buildBm25Index(
|
|
2990
2990
|
candidates.map((c) => ({ id: c.id, text: buildIndexableText(c.name, c.signature, c.docComment) }))
|
|
2991
2991
|
);
|
|
2992
|
-
const scored = bm25.score(
|
|
2992
|
+
const scored = bm25.score(query, (id) => candidateById.has(id));
|
|
2993
2993
|
scored.sort((a, b) => b.score - a.score);
|
|
2994
|
-
const qTokens = tokenise(
|
|
2994
|
+
const qTokens = tokenise(query);
|
|
2995
2995
|
const results = scored.slice(0, limit).map(({ id, score }) => {
|
|
2996
2996
|
const c = expectDefined(candidateById.get(id));
|
|
2997
2997
|
return { ...c, score, snippet: bm25.extractSnippet(id, qTokens) };
|
|
@@ -3102,6 +3102,104 @@ var IndexStore = class {
|
|
|
3102
3102
|
}
|
|
3103
3103
|
});
|
|
3104
3104
|
}
|
|
3105
|
+
/**
|
|
3106
|
+
* Commit a batch of file-level symbol/refs/upserts in a single transaction.
|
|
3107
|
+
*
|
|
3108
|
+
* Used by the indexer to amortize SQLite commit overhead across many files.
|
|
3109
|
+
* Before this, the indexer issued one transaction per file (BEGIN IMMEDIATE
|
|
3110
|
+
* for symbols, plus per-file deletes and an upsertFile call), so a 20-file
|
|
3111
|
+
* parallel batch cost ~5+ transactions × 20 files = 100+ commits. With
|
|
3112
|
+
* this entry point we do exactly one BEGIN/COMMIT per parallel batch.
|
|
3113
|
+
*
|
|
3114
|
+
* Each entry must already be a fully-parsed FileSymbols (symbols + refs).
|
|
3115
|
+
* The caller is responsible for the per-file prefix accounting
|
|
3116
|
+
* (refsByLine → flat list with `fromId` populated). `deleteForFiles` lets
|
|
3117
|
+
* the caller clear stale symbols/refs for any files being re-indexed before
|
|
3118
|
+
* the inserts run (required to keep refs → symbols FK invariants).
|
|
3119
|
+
*
|
|
3120
|
+
* Returns the symbols back with their assigned `id` (same shape as
|
|
3121
|
+
* {@link insertSymbols}) so callers can build final per-file results.
|
|
3122
|
+
*/
|
|
3123
|
+
commitBatch(entries, options = {}) {
|
|
3124
|
+
if (entries.length === 0 && (options.deleteForFiles?.length ?? 0) === 0) {
|
|
3125
|
+
return [];
|
|
3126
|
+
}
|
|
3127
|
+
return this.runWithRetry(() => {
|
|
3128
|
+
this.db.exec("BEGIN IMMEDIATE");
|
|
3129
|
+
try {
|
|
3130
|
+
if (options.deleteForFiles && options.deleteForFiles.length > 0) {
|
|
3131
|
+
const placeholders = options.deleteForFiles.map(() => "?").join(",");
|
|
3132
|
+
if (this.ftsAvailable) {
|
|
3133
|
+
this.db.prepare(
|
|
3134
|
+
`DELETE FROM symbols_fts WHERE rowid IN (SELECT id FROM symbols WHERE file IN (${placeholders}))`
|
|
3135
|
+
).run(...options.deleteForFiles);
|
|
3136
|
+
}
|
|
3137
|
+
this.db.prepare(
|
|
3138
|
+
`DELETE FROM refs WHERE from_id IN (SELECT id FROM symbols WHERE file IN (${placeholders}))`
|
|
3139
|
+
).run(...options.deleteForFiles);
|
|
3140
|
+
this.db.prepare(`DELETE FROM symbols WHERE file IN (${placeholders})`).run(...options.deleteForFiles);
|
|
3141
|
+
}
|
|
3142
|
+
const maxRows = this.db.prepare("SELECT MAX(id) AS m FROM symbols").all();
|
|
3143
|
+
let nextId = (maxRows[0]?.m ?? 0) + 1;
|
|
3144
|
+
const symStmt = this.db.prepare(
|
|
3145
|
+
`INSERT INTO symbols(id, lang, kind, name, file, line, col, signature, doc_comment, scope, text, file_fk)
|
|
3146
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
3147
|
+
);
|
|
3148
|
+
const ftsStmt = this.ftsAvailable ? this.db.prepare("INSERT INTO symbols_fts(rowid, text) VALUES (?, ?)") : null;
|
|
3149
|
+
const allInserted = [];
|
|
3150
|
+
const refsToInsert = [];
|
|
3151
|
+
for (const entry of entries) {
|
|
3152
|
+
for (const s of entry.symbols) {
|
|
3153
|
+
const id = nextId++;
|
|
3154
|
+
symStmt.run(
|
|
3155
|
+
id,
|
|
3156
|
+
s.lang,
|
|
3157
|
+
s.kind,
|
|
3158
|
+
s.name,
|
|
3159
|
+
s.file,
|
|
3160
|
+
s.line,
|
|
3161
|
+
s.col,
|
|
3162
|
+
s.signature,
|
|
3163
|
+
s.docComment,
|
|
3164
|
+
s.scope,
|
|
3165
|
+
s.text,
|
|
3166
|
+
s.file
|
|
3167
|
+
);
|
|
3168
|
+
ftsStmt?.run(id, buildIndexableText(s.name, s.signature, s.docComment));
|
|
3169
|
+
allInserted.push({ ...s, id });
|
|
3170
|
+
}
|
|
3171
|
+
for (const r of entry.refs) refsToInsert.push(r);
|
|
3172
|
+
}
|
|
3173
|
+
if (refsToInsert.length > 0) {
|
|
3174
|
+
const refStmt = this.db.prepare(
|
|
3175
|
+
`INSERT INTO refs(from_id, to_name, to_id, call_type, line)
|
|
3176
|
+
VALUES (?, ?, ?, ?, ?)`
|
|
3177
|
+
);
|
|
3178
|
+
for (const ref of refsToInsert) {
|
|
3179
|
+
refStmt.run(ref.fromId, ref.toName, ref.toId ?? null, ref.callType, ref.line);
|
|
3180
|
+
}
|
|
3181
|
+
}
|
|
3182
|
+
const upsertStmt = this.db.prepare(
|
|
3183
|
+
`INSERT INTO files(file, lang, mtime_ms, symbol_count, last_indexed)
|
|
3184
|
+
VALUES (?, ?, ?, ?, ?)
|
|
3185
|
+
ON CONFLICT(file) DO UPDATE SET
|
|
3186
|
+
lang = excluded.lang,
|
|
3187
|
+
mtime_ms = excluded.mtime_ms,
|
|
3188
|
+
symbol_count = excluded.symbol_count,
|
|
3189
|
+
last_indexed = excluded.last_indexed`
|
|
3190
|
+
);
|
|
3191
|
+
const now = Date.now();
|
|
3192
|
+
for (const entry of entries) {
|
|
3193
|
+
upsertStmt.run(entry.file, entry.lang, entry.mtimeMs, entry.symbolCount, now);
|
|
3194
|
+
}
|
|
3195
|
+
this.db.exec("COMMIT");
|
|
3196
|
+
return allInserted;
|
|
3197
|
+
} catch (err) {
|
|
3198
|
+
this.db.exec("ROLLBACK");
|
|
3199
|
+
throw err;
|
|
3200
|
+
}
|
|
3201
|
+
});
|
|
3202
|
+
}
|
|
3105
3203
|
/**
|
|
3106
3204
|
* Delete all refs whose source symbols are in a given file.
|
|
3107
3205
|
* Used when re-indexing a file to clear stale refs.
|
|
@@ -3618,8 +3716,8 @@ async function syncGoParse(filePath, content, lang) {
|
|
|
3618
3716
|
proc.stdin?.write(content);
|
|
3619
3717
|
proc.stdin?.end();
|
|
3620
3718
|
const { code } = await Promise.race([
|
|
3621
|
-
new Promise((
|
|
3622
|
-
proc.on("close", (c) =>
|
|
3719
|
+
new Promise((resolve7) => {
|
|
3720
|
+
proc.on("close", (c) => resolve7({ code: c }));
|
|
3623
3721
|
}),
|
|
3624
3722
|
new Promise(
|
|
3625
3723
|
(_, reject) => setTimeout(() => {
|
|
@@ -3881,8 +3979,8 @@ async function syncPyParse(filePath, content, lang) {
|
|
|
3881
3979
|
stdout += chunk.toString();
|
|
3882
3980
|
});
|
|
3883
3981
|
const { code } = await Promise.race([
|
|
3884
|
-
new Promise((
|
|
3885
|
-
proc.on("close", (c) =>
|
|
3982
|
+
new Promise((resolve7) => {
|
|
3983
|
+
proc.on("close", (c) => resolve7({ code: c }));
|
|
3886
3984
|
}),
|
|
3887
3985
|
new Promise(
|
|
3888
3986
|
(_, reject) => setTimeout(() => {
|
|
@@ -3967,8 +4065,8 @@ async function tryNativeParse(file, content) {
|
|
|
3967
4065
|
stdout += chunk.toString();
|
|
3968
4066
|
});
|
|
3969
4067
|
const { code } = await Promise.race([
|
|
3970
|
-
new Promise((
|
|
3971
|
-
proc.on("close", (c) =>
|
|
4068
|
+
new Promise((resolve7) => {
|
|
4069
|
+
proc.on("close", (c) => resolve7({ code: c }));
|
|
3972
4070
|
}),
|
|
3973
4071
|
new Promise(
|
|
3974
4072
|
(_, reject) => setTimeout(() => {
|
|
@@ -4458,7 +4556,7 @@ async function loadGitignoreMatcher(projectRoot) {
|
|
|
4458
4556
|
var YIELD_EVERY_N = 50;
|
|
4459
4557
|
var PARALLEL_BATCH = 20;
|
|
4460
4558
|
function yieldEventLoop() {
|
|
4461
|
-
return new Promise((
|
|
4559
|
+
return new Promise((resolve7) => setImmediate(resolve7));
|
|
4462
4560
|
}
|
|
4463
4561
|
function throwIfAborted(signal) {
|
|
4464
4562
|
if (!signal?.aborted) return;
|
|
@@ -4633,6 +4731,8 @@ async function runIndexerWithStore(store, opts) {
|
|
|
4633
4731
|
return { file, stat: stat11, lang, parsed, content };
|
|
4634
4732
|
})
|
|
4635
4733
|
);
|
|
4734
|
+
const batchEntries = [];
|
|
4735
|
+
const deleteForFiles = [];
|
|
4636
4736
|
for (let fi = 0; fi < statReadParse.length; fi++) {
|
|
4637
4737
|
const settled = statReadParse[fi];
|
|
4638
4738
|
const file = expectDefined(batchFiles[fi]);
|
|
@@ -4657,43 +4757,116 @@ async function runIndexerWithStore(store, opts) {
|
|
|
4657
4757
|
}
|
|
4658
4758
|
if (!lang || !parsed) {
|
|
4659
4759
|
if (lang) {
|
|
4660
|
-
store.upsertFile({
|
|
4760
|
+
store.upsertFile({
|
|
4761
|
+
file,
|
|
4762
|
+
lang,
|
|
4763
|
+
mtimeMs: Math.floor(stat11.mtimeMs),
|
|
4764
|
+
symbolCount: 0,
|
|
4765
|
+
lastIndexed: Date.now()
|
|
4766
|
+
});
|
|
4661
4767
|
filesIndexed++;
|
|
4662
4768
|
}
|
|
4663
4769
|
continue;
|
|
4664
4770
|
}
|
|
4665
|
-
store.deleteRefsForFile(file);
|
|
4666
|
-
store.deleteSymbolsForFile(file);
|
|
4667
4771
|
if (parsed.symbols.length === 0) {
|
|
4668
|
-
store.upsertFile({
|
|
4772
|
+
store.upsertFile({
|
|
4773
|
+
file,
|
|
4774
|
+
lang,
|
|
4775
|
+
mtimeMs: Math.floor(stat11.mtimeMs),
|
|
4776
|
+
symbolCount: 0,
|
|
4777
|
+
lastIndexed: Date.now()
|
|
4778
|
+
});
|
|
4669
4779
|
filesIndexed++;
|
|
4670
4780
|
continue;
|
|
4671
4781
|
}
|
|
4672
|
-
const
|
|
4673
|
-
const count = symbolsWithIds.length;
|
|
4674
|
-
symbolsIndexed += count;
|
|
4675
|
-
langStats[lang] = (langStats[lang] ?? 0) + count;
|
|
4782
|
+
const refs = [];
|
|
4676
4783
|
if (parsed.refs && parsed.refs.length > 0) {
|
|
4677
|
-
const
|
|
4678
|
-
|
|
4679
|
-
|
|
4680
|
-
|
|
4681
|
-
|
|
4682
|
-
|
|
4784
|
+
for (const r of parsed.refs) refs.push({ ...r, fromId: 0 });
|
|
4785
|
+
}
|
|
4786
|
+
batchEntries.push({
|
|
4787
|
+
file,
|
|
4788
|
+
lang,
|
|
4789
|
+
symbols: parsed.symbols,
|
|
4790
|
+
refs,
|
|
4791
|
+
mtimeMs: Math.floor(stat11.mtimeMs),
|
|
4792
|
+
symbolCount: parsed.symbols.length
|
|
4793
|
+
});
|
|
4794
|
+
deleteForFiles.push(file);
|
|
4795
|
+
}
|
|
4796
|
+
if (batchEntries.length > 0) {
|
|
4797
|
+
try {
|
|
4798
|
+
const inserted = store.commitBatch(batchEntries, { deleteForFiles });
|
|
4799
|
+
let cursor = 0;
|
|
4800
|
+
for (const entry of batchEntries) {
|
|
4801
|
+
const count = entry.symbols.length;
|
|
4802
|
+
const symbolsWithIds = inserted.slice(cursor, cursor + count);
|
|
4803
|
+
cursor += count;
|
|
4804
|
+
symbolsIndexed += count;
|
|
4805
|
+
langStats[entry.lang] = (langStats[entry.lang] ?? 0) + count;
|
|
4806
|
+
filesIndexed++;
|
|
4807
|
+
if (entry.refs.length > 0 && symbolsWithIds.length > 0) {
|
|
4808
|
+
const refsByLine = /* @__PURE__ */ new Map();
|
|
4809
|
+
for (let i = 0; i < symbolsWithIds.length; i++) {
|
|
4810
|
+
const sym = symbolsWithIds[i];
|
|
4811
|
+
let arr = refsByLine.get(sym.line);
|
|
4812
|
+
if (!arr) {
|
|
4813
|
+
arr = [];
|
|
4814
|
+
refsByLine.set(sym.line, arr);
|
|
4815
|
+
}
|
|
4816
|
+
arr.push(i);
|
|
4817
|
+
}
|
|
4818
|
+
for (const ref of entry.refs) {
|
|
4819
|
+
const indices = refsByLine.get(ref.line);
|
|
4820
|
+
if (indices && indices.length > 0) {
|
|
4821
|
+
const idx = indices.shift();
|
|
4822
|
+
ref.fromId = symbolsWithIds[idx].id;
|
|
4823
|
+
}
|
|
4824
|
+
}
|
|
4683
4825
|
}
|
|
4684
|
-
arr.push(r);
|
|
4685
4826
|
}
|
|
4686
|
-
|
|
4687
|
-
|
|
4688
|
-
|
|
4689
|
-
|
|
4690
|
-
|
|
4827
|
+
} catch (err) {
|
|
4828
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
4829
|
+
errors.push(`commitBatch failed: ${message} \u2014 falling back to per-file writes`);
|
|
4830
|
+
for (const entry of batchEntries) {
|
|
4831
|
+
try {
|
|
4832
|
+
store.deleteRefsForFile(entry.file);
|
|
4833
|
+
store.deleteSymbolsForFile(entry.file);
|
|
4834
|
+
const symbolsWithIds = store.insertSymbols(entry.symbols);
|
|
4835
|
+
symbolsIndexed += symbolsWithIds.length;
|
|
4836
|
+
langStats[entry.lang] = (langStats[entry.lang] ?? 0) + symbolsWithIds.length;
|
|
4837
|
+
filesIndexed++;
|
|
4838
|
+
if (entry.refs.length > 0 && symbolsWithIds.length > 0) {
|
|
4839
|
+
const refsByLine = /* @__PURE__ */ new Map();
|
|
4840
|
+
for (const sym of symbolsWithIds) {
|
|
4841
|
+
let arr = refsByLine.get(sym.line);
|
|
4842
|
+
if (!arr) {
|
|
4843
|
+
arr = [];
|
|
4844
|
+
refsByLine.set(sym.line, arr);
|
|
4845
|
+
}
|
|
4846
|
+
arr.push(sym);
|
|
4847
|
+
}
|
|
4848
|
+
const fallbackBatch = [];
|
|
4849
|
+
for (const ref of entry.refs) {
|
|
4850
|
+
const syms = refsByLine.get(ref.line);
|
|
4851
|
+
if (syms && syms.length > 0) {
|
|
4852
|
+
const sym = syms.shift();
|
|
4853
|
+
fallbackBatch.push({ ...ref, fromId: sym.id });
|
|
4854
|
+
}
|
|
4855
|
+
}
|
|
4856
|
+
if (fallbackBatch.length > 0) store.insertRefsBatch(fallbackBatch);
|
|
4857
|
+
}
|
|
4858
|
+
store.upsertFile({
|
|
4859
|
+
file: entry.file,
|
|
4860
|
+
lang: entry.lang,
|
|
4861
|
+
mtimeMs: entry.mtimeMs,
|
|
4862
|
+
symbolCount: entry.symbolCount,
|
|
4863
|
+
lastIndexed: Date.now()
|
|
4864
|
+
});
|
|
4865
|
+
} catch (innerErr) {
|
|
4866
|
+
errors.push(`fallback write failed: ${entry.file}: ${innerErr instanceof Error ? innerErr.message : String(innerErr)}`);
|
|
4691
4867
|
}
|
|
4692
4868
|
}
|
|
4693
|
-
if (batch.length > 0) store.insertRefsBatch(batch);
|
|
4694
4869
|
}
|
|
4695
|
-
store.upsertFile({ file, lang, mtimeMs: Math.floor(stat11.mtimeMs), symbolCount: count, lastIndexed: Date.now() });
|
|
4696
|
-
filesIndexed++;
|
|
4697
4870
|
}
|
|
4698
4871
|
}
|
|
4699
4872
|
if (discoveredFiles) {
|
|
@@ -4861,7 +5034,7 @@ function terminateWorker(reason) {
|
|
|
4861
5034
|
function callIndexOp(op, args, opts) {
|
|
4862
5035
|
const w = ensureWorker();
|
|
4863
5036
|
if (!w) return callInline(op, args, opts);
|
|
4864
|
-
return new Promise((
|
|
5037
|
+
return new Promise((resolve7, reject) => {
|
|
4865
5038
|
const id = nextRpcId++;
|
|
4866
5039
|
const timer = setTimeout(() => {
|
|
4867
5040
|
pending.delete(id);
|
|
@@ -4884,7 +5057,7 @@ function callIndexOp(op, args, opts) {
|
|
|
4884
5057
|
pending.set(id, {
|
|
4885
5058
|
resolve: (v) => {
|
|
4886
5059
|
cleanup();
|
|
4887
|
-
|
|
5060
|
+
resolve7(v);
|
|
4888
5061
|
},
|
|
4889
5062
|
reject: (e) => {
|
|
4890
5063
|
cleanup();
|
|
@@ -5076,7 +5249,7 @@ var codebaseSearchTool = {
|
|
|
5076
5249
|
name: "codebase-search",
|
|
5077
5250
|
category: "Project",
|
|
5078
5251
|
icon: "index",
|
|
5079
|
-
description: "
|
|
5252
|
+
description: "Search code symbols using a fast SQLite+BM25 index, with optional LSP fallback. Much more powerful and structured than raw `grep` for finding code by name or concept. Set `preferLsp: true` for live precision when the LSP plugin is active (supersedes codebase-lsp-search).",
|
|
5080
5253
|
usageHint: "PREFERRED FOR CODE UNDERSTANDING:\n\n- Use when you need to find where something is defined or used by name.\n- `kind` filter is very useful (e.g. only functions or only interfaces).\n- Combine with `file` filter to scope to a specific directory or module.\nThis is generally better than `grep` when you are looking for symbols rather than arbitrary text patterns.",
|
|
5081
5254
|
permission: "auto",
|
|
5082
5255
|
mutating: false,
|
|
@@ -5110,6 +5283,10 @@ var codebaseSearchTool = {
|
|
|
5110
5283
|
description: "Maximum results to return (default 20, max 100)",
|
|
5111
5284
|
minimum: 1,
|
|
5112
5285
|
maximum: 100
|
|
5286
|
+
},
|
|
5287
|
+
preferLsp: {
|
|
5288
|
+
type: "boolean",
|
|
5289
|
+
description: "Prefer live LSP results over the index. Index-only when the LSP plugin is not active. When the LSP plugin is active and this is true, results come from live workspaceSymbol queries."
|
|
5113
5290
|
}
|
|
5114
5291
|
},
|
|
5115
5292
|
required: ["query"]
|
|
@@ -5374,7 +5551,14 @@ These win over kit tokens. Run \`design {action:"materialize"}\` to write them t
|
|
|
5374
5551
|
kitId: active.kit,
|
|
5375
5552
|
outPath: input.out
|
|
5376
5553
|
});
|
|
5377
|
-
const
|
|
5554
|
+
const root = path3.resolve(ctx.projectRoot);
|
|
5555
|
+
const abs = path3.resolve(path3.join(ctx.projectRoot, result.path));
|
|
5556
|
+
const rel = path3.relative(root, abs);
|
|
5557
|
+
if (rel.startsWith("..") || path3.isAbsolute(rel)) {
|
|
5558
|
+
throw new Error(
|
|
5559
|
+
`design: materialize path "${result.path}" would escape the project root`
|
|
5560
|
+
);
|
|
5561
|
+
}
|
|
5378
5562
|
let exists = false;
|
|
5379
5563
|
try {
|
|
5380
5564
|
await fs2.access(abs);
|
|
@@ -5533,7 +5717,7 @@ function findGitDir(cwd) {
|
|
|
5533
5717
|
return null;
|
|
5534
5718
|
}
|
|
5535
5719
|
function runGit(args, cwd, signal) {
|
|
5536
|
-
return new Promise((
|
|
5720
|
+
return new Promise((resolve7) => {
|
|
5537
5721
|
let stdout = "";
|
|
5538
5722
|
let stderr = "";
|
|
5539
5723
|
const child = spawn("git", args, {
|
|
@@ -5549,8 +5733,8 @@ function runGit(args, cwd, signal) {
|
|
|
5549
5733
|
child.stderr?.on("data", (c) => {
|
|
5550
5734
|
stderr += c.toString();
|
|
5551
5735
|
});
|
|
5552
|
-
child.on("close", (code) =>
|
|
5553
|
-
child.on("error", (e) =>
|
|
5736
|
+
child.on("close", (code) => resolve7({ stdout, stderr, exitCode: code ?? 0 }));
|
|
5737
|
+
child.on("error", (e) => resolve7({ stdout: "", stderr: e.message, exitCode: 1 }));
|
|
5554
5738
|
});
|
|
5555
5739
|
}
|
|
5556
5740
|
async function fileDiff(input, ctx, _signal) {
|
|
@@ -5589,8 +5773,8 @@ ${numbered}`;
|
|
|
5589
5773
|
var documentTool = {
|
|
5590
5774
|
name: "document",
|
|
5591
5775
|
category: "Project",
|
|
5592
|
-
description: "
|
|
5593
|
-
usageHint: "
|
|
5776
|
+
description: "DEPRECATED \u2014 use the `auto_doc` tool with `dryRun: true` instead. This tool is a read-only preview stub that returns `skipped` candidates without generating real docstrings.",
|
|
5777
|
+
usageHint: "Deprecated: prefer `auto_doc` with `dryRun: true` for previewing, or `auto_doc` without dryRun for writing. This tool only lists undocumented symbols with placeholder comments \u2014 it does not generate real JSDoc/TSDoc.",
|
|
5594
5778
|
permission: "auto",
|
|
5595
5779
|
mutating: false,
|
|
5596
5780
|
timeoutMs: 3e4,
|
|
@@ -5753,28 +5937,62 @@ var editTool = {
|
|
|
5753
5937
|
required: ["path", "old_string", "new_string"]
|
|
5754
5938
|
},
|
|
5755
5939
|
async execute(input, ctx) {
|
|
5756
|
-
if (!input?.path)
|
|
5757
|
-
|
|
5758
|
-
|
|
5759
|
-
if (input.old_string ===
|
|
5940
|
+
if (!input?.path) {
|
|
5941
|
+
throw new ToolValidationError({ message: "edit: path is required", field: "path" });
|
|
5942
|
+
}
|
|
5943
|
+
if (input.old_string === void 0) {
|
|
5944
|
+
throw new ToolValidationError({
|
|
5945
|
+
message: "edit: old_string is required",
|
|
5946
|
+
field: "old_string"
|
|
5947
|
+
});
|
|
5948
|
+
}
|
|
5949
|
+
if (input.new_string === void 0) {
|
|
5950
|
+
throw new ToolValidationError({
|
|
5951
|
+
message: "edit: new_string is required",
|
|
5952
|
+
field: "new_string"
|
|
5953
|
+
});
|
|
5954
|
+
}
|
|
5955
|
+
if (input.old_string === "") {
|
|
5956
|
+
throw new ToolValidationError({
|
|
5957
|
+
message: "edit: old_string cannot be empty",
|
|
5958
|
+
field: "old_string"
|
|
5959
|
+
});
|
|
5960
|
+
}
|
|
5760
5961
|
const absPath = await safeResolveReal(input.path, ctx);
|
|
5761
5962
|
const stat11 = await fs2.stat(absPath).catch((err) => {
|
|
5762
5963
|
if (err.code === "ENOENT") {
|
|
5763
|
-
throw new
|
|
5964
|
+
throw new ToolValidationError({
|
|
5965
|
+
message: `edit: file "${input.path}" does not exist. Use \`write\` instead.`,
|
|
5966
|
+
field: "path",
|
|
5967
|
+
context: { exists: false }
|
|
5968
|
+
});
|
|
5764
5969
|
}
|
|
5765
5970
|
throw err;
|
|
5766
5971
|
});
|
|
5767
|
-
if (!stat11.isFile())
|
|
5972
|
+
if (!stat11.isFile()) {
|
|
5973
|
+
throw new ToolValidationError({
|
|
5974
|
+
message: `edit: "${input.path}" is not a regular file`,
|
|
5975
|
+
field: "path"
|
|
5976
|
+
});
|
|
5977
|
+
}
|
|
5768
5978
|
const autoRead = !ctx.hasRead(absPath);
|
|
5769
5979
|
const original = await fs2.readFile(absPath, "utf8");
|
|
5770
5980
|
const updated = await fs2.stat(absPath);
|
|
5771
5981
|
const mtimeTolerance = process.platform === "win32" ? 2e3 : 1;
|
|
5772
5982
|
const lastReadMtime = ctx.lastReadMtime(absPath);
|
|
5773
5983
|
if (lastReadMtime !== void 0 && updated.mtimeMs > lastReadMtime + mtimeTolerance) {
|
|
5774
|
-
throw new
|
|
5984
|
+
throw new ToolValidationError({
|
|
5985
|
+
message: `edit: file "${input.path}" was modified externally. Re-read it first.`,
|
|
5986
|
+
field: "path",
|
|
5987
|
+
context: { reason: "external_modification" }
|
|
5988
|
+
});
|
|
5775
5989
|
}
|
|
5776
5990
|
if (autoRead && updated.mtimeMs > stat11.mtimeMs + mtimeTolerance) {
|
|
5777
|
-
throw new
|
|
5991
|
+
throw new ToolValidationError({
|
|
5992
|
+
message: `edit: file "${input.path}" changed while being auto-read. Retry the edit.`,
|
|
5993
|
+
field: "path",
|
|
5994
|
+
context: { reason: "auto_read_race" }
|
|
5995
|
+
});
|
|
5778
5996
|
}
|
|
5779
5997
|
const autoReadNote = autoRead ? `No prior read was recorded for "${input.path}"; edit auto-read the current file and applied the replacement only after the ambiguity checks passed.` : void 0;
|
|
5780
5998
|
const style = detectNewlineStyle(original);
|
|
@@ -5800,15 +6018,18 @@ var editTool = {
|
|
|
5800
6018
|
}
|
|
5801
6019
|
if (count === 0) {
|
|
5802
6020
|
const hint = findSimilarity(fileLf, oldLf);
|
|
5803
|
-
throw new
|
|
5804
|
-
`edit: no match for old_string in "${input.path}".${hint ? ` Nearest match near line ${hint}.` : ""}
|
|
5805
|
-
|
|
6021
|
+
throw new ToolValidationError({
|
|
6022
|
+
message: `edit: no match for old_string in "${input.path}".${hint ? ` Nearest match near line ${hint}.` : ""}`,
|
|
6023
|
+
field: "old_string"
|
|
6024
|
+
});
|
|
5806
6025
|
}
|
|
5807
6026
|
if (count > 1 && !input.replace_all) {
|
|
5808
6027
|
const lines = lineNumbersFor(fileLf, matches);
|
|
5809
|
-
throw new
|
|
5810
|
-
`edit: old_string matched ${count} times in "${input.path}" (lines: ${lines.join(", ")}). Add more context to make it unique, or set replace_all: true
|
|
5811
|
-
|
|
6028
|
+
throw new ToolValidationError({
|
|
6029
|
+
message: `edit: old_string matched ${count} times in "${input.path}" (lines: ${lines.join(", ")}). Add more context to make it unique, or set replace_all: true.`,
|
|
6030
|
+
field: "old_string",
|
|
6031
|
+
context: { occurrences: count }
|
|
6032
|
+
});
|
|
5812
6033
|
}
|
|
5813
6034
|
const newFileLf = input.replace_all ? fileLf.split(oldLf).join(newLf) : fileLf.replace(oldLf, newLf);
|
|
5814
6035
|
const newFile = toStyle(newFileLf, style);
|
|
@@ -6142,7 +6363,7 @@ var execTool = {
|
|
|
6142
6363
|
}
|
|
6143
6364
|
};
|
|
6144
6365
|
function runCommand(cmd, args, cwd, timeout, signal, sessionId) {
|
|
6145
|
-
return new Promise((
|
|
6366
|
+
return new Promise((resolve7) => {
|
|
6146
6367
|
let stdout = "";
|
|
6147
6368
|
let stderr = "";
|
|
6148
6369
|
let killed = false;
|
|
@@ -6150,7 +6371,7 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId) {
|
|
|
6150
6371
|
const finish = (result) => {
|
|
6151
6372
|
if (resolvedOnce.value) return;
|
|
6152
6373
|
resolvedOnce.value = true;
|
|
6153
|
-
|
|
6374
|
+
resolve7(result);
|
|
6154
6375
|
};
|
|
6155
6376
|
const startedAt = Date.now();
|
|
6156
6377
|
const spool = createOutputSpool({ tool: `exec-${cmd}`, thresholdBytes: MAX_OUTPUT2 });
|
|
@@ -6327,10 +6548,16 @@ async function guardedFetch(url, maxRedirects, signal, headers = {
|
|
|
6327
6548
|
for (; ; ) {
|
|
6328
6549
|
const parsed = new URL(currentUrl);
|
|
6329
6550
|
if (parsed.protocol !== "https:" && parsed.protocol !== "http:") {
|
|
6330
|
-
throw new
|
|
6551
|
+
throw new ToolValidationError({
|
|
6552
|
+
message: `fetch: redirect to unsupported protocol "${parsed.protocol}"`,
|
|
6553
|
+
field: "url"
|
|
6554
|
+
});
|
|
6331
6555
|
}
|
|
6332
6556
|
if (parsed.protocol === "http:" && !ALLOW_PRIVATE) {
|
|
6333
|
-
throw new
|
|
6557
|
+
throw new ToolValidationError({
|
|
6558
|
+
message: "fetch: redirect to http:// blocked (HTTPS required by default)",
|
|
6559
|
+
field: "url"
|
|
6560
|
+
});
|
|
6334
6561
|
}
|
|
6335
6562
|
await assertNotPrivate(parsed.hostname);
|
|
6336
6563
|
const init = {
|
|
@@ -6345,11 +6572,19 @@ async function guardedFetch(url, maxRedirects, signal, headers = {
|
|
|
6345
6572
|
}
|
|
6346
6573
|
redirectCount++;
|
|
6347
6574
|
if (redirectCount > maxRedirects) {
|
|
6348
|
-
throw new
|
|
6575
|
+
throw new FetchError({
|
|
6576
|
+
message: `fetch: exceeded ${maxRedirects} redirects`,
|
|
6577
|
+
status: res.status,
|
|
6578
|
+
context: { url: currentUrl, maxRedirects, redirectCount }
|
|
6579
|
+
});
|
|
6349
6580
|
}
|
|
6350
6581
|
const location = res.headers.get("location");
|
|
6351
6582
|
if (!location) {
|
|
6352
|
-
throw new
|
|
6583
|
+
throw new FetchError({
|
|
6584
|
+
message: "fetch: redirect status with no location header",
|
|
6585
|
+
status: res.status,
|
|
6586
|
+
context: { url: currentUrl, redirectCount }
|
|
6587
|
+
});
|
|
6353
6588
|
}
|
|
6354
6589
|
currentUrl = new URL(location, currentUrl).toString();
|
|
6355
6590
|
}
|
|
@@ -6388,26 +6623,53 @@ var fetchTool = {
|
|
|
6388
6623
|
async execute(input, ctx, opts) {
|
|
6389
6624
|
let final;
|
|
6390
6625
|
const executeStream = fetchTool.executeStream;
|
|
6391
|
-
if (!executeStream)
|
|
6626
|
+
if (!executeStream) {
|
|
6627
|
+
throw new ToolError({
|
|
6628
|
+
message: "fetchTool: stream execution unavailable",
|
|
6629
|
+
code: "TOOL_EXECUTION_FAILED",
|
|
6630
|
+
toolName: "fetch"
|
|
6631
|
+
});
|
|
6632
|
+
}
|
|
6392
6633
|
for await (const ev of executeStream(input, ctx, opts)) {
|
|
6393
6634
|
if (ev.type === "final") final = ev.output;
|
|
6394
6635
|
}
|
|
6395
|
-
if (!final)
|
|
6636
|
+
if (!final) {
|
|
6637
|
+
throw new ToolError({
|
|
6638
|
+
message: "fetch: stream ended without final event",
|
|
6639
|
+
code: "TOOL_EXECUTION_FAILED",
|
|
6640
|
+
toolName: "fetch"
|
|
6641
|
+
});
|
|
6642
|
+
}
|
|
6396
6643
|
return final;
|
|
6397
6644
|
},
|
|
6398
6645
|
async *executeStream(input, ctx, opts) {
|
|
6399
|
-
if (!input?.url)
|
|
6646
|
+
if (!input?.url) {
|
|
6647
|
+
throw new ToolValidationError({
|
|
6648
|
+
message: "fetch: url is required",
|
|
6649
|
+
field: "url"
|
|
6650
|
+
});
|
|
6651
|
+
}
|
|
6400
6652
|
const u = new URL(input.url);
|
|
6401
6653
|
if (u.protocol !== "https:" && u.protocol !== "http:") {
|
|
6402
|
-
throw new
|
|
6654
|
+
throw new ToolValidationError({
|
|
6655
|
+
message: `fetch: unsupported protocol "${u.protocol}"`,
|
|
6656
|
+
field: "url"
|
|
6657
|
+
});
|
|
6403
6658
|
}
|
|
6404
6659
|
if (u.protocol === "http:" && !ALLOW_PRIVATE) {
|
|
6405
|
-
throw new
|
|
6660
|
+
throw new ToolValidationError({
|
|
6661
|
+
message: "fetch: http:// blocked (HTTPS required by default)",
|
|
6662
|
+
field: "url"
|
|
6663
|
+
});
|
|
6406
6664
|
}
|
|
6407
6665
|
await assertNotPrivate(u.hostname);
|
|
6408
6666
|
yield { type: "log", text: `GET ${input.url}` };
|
|
6409
6667
|
const ctrl = new AbortController();
|
|
6410
|
-
const timer = setTimeout(() => ctrl.abort(new
|
|
6668
|
+
const timer = setTimeout(() => ctrl.abort(new ToolError({
|
|
6669
|
+
message: "fetch timeout",
|
|
6670
|
+
code: "TOOL_TIMEOUT",
|
|
6671
|
+
toolName: "fetch"
|
|
6672
|
+
})), TIMEOUT_MS);
|
|
6411
6673
|
const combined = combineSignals([opts.signal, ctrl.signal]);
|
|
6412
6674
|
try {
|
|
6413
6675
|
let res;
|
|
@@ -6419,7 +6681,11 @@ var fetchTool = {
|
|
|
6419
6681
|
}
|
|
6420
6682
|
const ct = res.headers.get("content-type") ?? "application/octet-stream";
|
|
6421
6683
|
if (/^image\/|^audio\/|^video\/|application\/octet-stream/.test(ct)) {
|
|
6422
|
-
throw new
|
|
6684
|
+
throw new FetchError({
|
|
6685
|
+
message: `fetch: refusing to read binary content-type "${ct}"`,
|
|
6686
|
+
status: res.status,
|
|
6687
|
+
context: { url: res.url, contentType: ct }
|
|
6688
|
+
});
|
|
6423
6689
|
}
|
|
6424
6690
|
yield {
|
|
6425
6691
|
type: "log",
|
|
@@ -6484,16 +6750,25 @@ async function assertNotPrivate(hostname2) {
|
|
|
6484
6750
|
if (ALLOW_PRIVATE) return;
|
|
6485
6751
|
const host = hostname2.startsWith("[") && hostname2.endsWith("]") ? hostname2.slice(1, -1) : hostname2;
|
|
6486
6752
|
if (host === "localhost" || host.endsWith(".localhost")) {
|
|
6487
|
-
throw new
|
|
6753
|
+
throw new ToolValidationError({
|
|
6754
|
+
message: "fetch: blocked localhost target",
|
|
6755
|
+
field: "url"
|
|
6756
|
+
});
|
|
6488
6757
|
}
|
|
6489
6758
|
const ipVersion = net.isIP(host);
|
|
6490
6759
|
if (ipVersion === 4) {
|
|
6491
6760
|
if (isPrivateIPv4(host)) {
|
|
6492
|
-
throw new
|
|
6761
|
+
throw new ToolValidationError({
|
|
6762
|
+
message: `fetch: blocked private/loopback address "${host}"`,
|
|
6763
|
+
field: "url"
|
|
6764
|
+
});
|
|
6493
6765
|
}
|
|
6494
6766
|
} else if (ipVersion === 6) {
|
|
6495
6767
|
if (isPrivateIPv6(host)) {
|
|
6496
|
-
throw new
|
|
6768
|
+
throw new ToolValidationError({
|
|
6769
|
+
message: `fetch: blocked private/loopback address "${host}"`,
|
|
6770
|
+
field: "url"
|
|
6771
|
+
});
|
|
6497
6772
|
}
|
|
6498
6773
|
} else {
|
|
6499
6774
|
try {
|
|
@@ -6501,7 +6776,10 @@ async function assertNotPrivate(hostname2) {
|
|
|
6501
6776
|
for (const r of records) {
|
|
6502
6777
|
const bad = r.family === 4 ? isPrivateIPv4(r.address) : isPrivateIPv6(r.address);
|
|
6503
6778
|
if (bad) {
|
|
6504
|
-
throw new
|
|
6779
|
+
throw new ToolValidationError({
|
|
6780
|
+
message: `fetch: resolved to private address ${r.address}`,
|
|
6781
|
+
field: "url"
|
|
6782
|
+
});
|
|
6505
6783
|
}
|
|
6506
6784
|
}
|
|
6507
6785
|
} catch (err) {
|
|
@@ -6511,7 +6789,13 @@ async function assertNotPrivate(hostname2) {
|
|
|
6511
6789
|
}
|
|
6512
6790
|
function describeFetchError(err, url, timedOut) {
|
|
6513
6791
|
if (timedOut) {
|
|
6514
|
-
return new
|
|
6792
|
+
return new ToolError({
|
|
6793
|
+
message: `fetch: GET ${url} timed out after ${TIMEOUT_MS}ms`,
|
|
6794
|
+
code: "TOOL_TIMEOUT",
|
|
6795
|
+
toolName: "fetch",
|
|
6796
|
+
context: { url, timedOut: true, timeoutMs: TIMEOUT_MS },
|
|
6797
|
+
cause: err
|
|
6798
|
+
});
|
|
6515
6799
|
}
|
|
6516
6800
|
const parts = [];
|
|
6517
6801
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -6524,7 +6808,15 @@ function describeFetchError(err, url, timedOut) {
|
|
|
6524
6808
|
cur = cur.cause;
|
|
6525
6809
|
}
|
|
6526
6810
|
const detail = parts.length > 0 ? parts.join(" \u2192 ") : "fetch failed";
|
|
6527
|
-
return new
|
|
6811
|
+
return new FetchError({
|
|
6812
|
+
message: `fetch: GET ${url} failed \u2014 ${detail}`,
|
|
6813
|
+
status: 502,
|
|
6814
|
+
context: { url, timedOut: false, transportErrors: parts },
|
|
6815
|
+
// Preserve the original undici / DNS / TLS chain so callers can inspect
|
|
6816
|
+
// it via `err.cause` and structured `instanceof` checks. The flattened
|
|
6817
|
+
// text version stays in the message for human readability.
|
|
6818
|
+
cause: err
|
|
6819
|
+
});
|
|
6528
6820
|
}
|
|
6529
6821
|
function prettyJson(s) {
|
|
6530
6822
|
try {
|
|
@@ -6872,7 +7164,7 @@ function buildArgs(input) {
|
|
|
6872
7164
|
}
|
|
6873
7165
|
}
|
|
6874
7166
|
function runGit2(args, cwd, signal) {
|
|
6875
|
-
return new Promise((
|
|
7167
|
+
return new Promise((resolve7) => {
|
|
6876
7168
|
let stdout = "";
|
|
6877
7169
|
let stderr = "";
|
|
6878
7170
|
const child = spawn("git", args, {
|
|
@@ -6893,7 +7185,7 @@ function runGit2(args, cwd, signal) {
|
|
|
6893
7185
|
}
|
|
6894
7186
|
});
|
|
6895
7187
|
child.on("error", (err) => {
|
|
6896
|
-
|
|
7188
|
+
resolve7({
|
|
6897
7189
|
command: args[0],
|
|
6898
7190
|
stdout: normalizeCommandOutput(stdout),
|
|
6899
7191
|
stderr: err.message,
|
|
@@ -6902,7 +7194,7 @@ function runGit2(args, cwd, signal) {
|
|
|
6902
7194
|
});
|
|
6903
7195
|
});
|
|
6904
7196
|
child.on("close", (code) => {
|
|
6905
|
-
|
|
7197
|
+
resolve7({
|
|
6906
7198
|
command: args[0],
|
|
6907
7199
|
stdout: normalizeCommandOutput(stdout),
|
|
6908
7200
|
stderr: normalizeCommandOutput(stderr),
|
|
@@ -6964,7 +7256,7 @@ var globTool = {
|
|
|
6964
7256
|
},
|
|
6965
7257
|
async execute(input, ctx) {
|
|
6966
7258
|
if (!input?.pattern) throw new Error("glob: pattern is required");
|
|
6967
|
-
const base = input.path ?
|
|
7259
|
+
const base = input.path ? await safeResolveReal(input.path, ctx) : ctx.cwd;
|
|
6968
7260
|
const limit = Math.max(1, Math.min(input.limit ?? 1e3, 5e3));
|
|
6969
7261
|
const ignored = await readGitignore(base);
|
|
6970
7262
|
const re = compileGlob(input.pattern);
|
|
@@ -7019,8 +7311,12 @@ var globTool = {
|
|
|
7019
7311
|
try {
|
|
7020
7312
|
const st = await fs2.stat(full);
|
|
7021
7313
|
if (st.isDirectory()) {
|
|
7314
|
+
const real = await fs2.realpath(full);
|
|
7315
|
+
await assertRealInsideRoot(real, ctx);
|
|
7022
7316
|
subdirs.push({ full, rel });
|
|
7023
7317
|
} else if (st.isFile()) {
|
|
7318
|
+
const real = await fs2.realpath(full);
|
|
7319
|
+
await assertRealInsideRoot(real, ctx);
|
|
7024
7320
|
re.lastIndex = 0;
|
|
7025
7321
|
const relMatch = re.test(rel);
|
|
7026
7322
|
re.lastIndex = 0;
|
|
@@ -7158,13 +7454,21 @@ var grepTool = {
|
|
|
7158
7454
|
return final;
|
|
7159
7455
|
},
|
|
7160
7456
|
async *executeStream(input, ctx, opts) {
|
|
7161
|
-
if (!input?.pattern)
|
|
7457
|
+
if (!input?.pattern) {
|
|
7458
|
+
throw new ToolValidationError({
|
|
7459
|
+
message: "grep: pattern is required",
|
|
7460
|
+
field: "pattern"
|
|
7461
|
+
});
|
|
7462
|
+
}
|
|
7162
7463
|
const base = input.path ? safeResolve(input.path, ctx) : ctx.cwd;
|
|
7163
7464
|
const mode = input.output_mode ?? "content";
|
|
7164
7465
|
const limit = Math.max(1, Math.min(input.limit ?? 200, 2e3));
|
|
7165
7466
|
const validation = compileUserRegex(input.pattern, input.case_insensitive ? "i" : "");
|
|
7166
7467
|
if (!validation.ok) {
|
|
7167
|
-
throw new
|
|
7468
|
+
throw new ToolValidationError({
|
|
7469
|
+
message: `grep: ${validation.reason}`,
|
|
7470
|
+
field: "pattern"
|
|
7471
|
+
});
|
|
7168
7472
|
}
|
|
7169
7473
|
const rgAvailable = await detectRg(opts.signal);
|
|
7170
7474
|
if (rgAvailable) {
|
|
@@ -7180,13 +7484,13 @@ var grepTool = {
|
|
|
7180
7484
|
}
|
|
7181
7485
|
};
|
|
7182
7486
|
async function detectRg(signal) {
|
|
7183
|
-
return new Promise((
|
|
7487
|
+
return new Promise((resolve7) => {
|
|
7184
7488
|
try {
|
|
7185
7489
|
const p = spawn("rg", ["--version"], { env: buildChildEnv(), stdio: "ignore", signal, windowsHide: true });
|
|
7186
|
-
p.on("error", () =>
|
|
7187
|
-
p.on("close", (code) =>
|
|
7490
|
+
p.on("error", () => resolve7(false));
|
|
7491
|
+
p.on("close", (code) => resolve7(code === 0));
|
|
7188
7492
|
} catch {
|
|
7189
|
-
|
|
7493
|
+
resolve7(false);
|
|
7190
7494
|
}
|
|
7191
7495
|
});
|
|
7192
7496
|
}
|
|
@@ -7320,7 +7624,10 @@ async function runNative(input, base, mode, limit, signal) {
|
|
|
7320
7624
|
const flags = input.case_insensitive ? "i" : "";
|
|
7321
7625
|
const compiled = compileUserRegex(input.pattern, flags);
|
|
7322
7626
|
if (!compiled.ok) {
|
|
7323
|
-
throw new
|
|
7627
|
+
throw new ToolValidationError({
|
|
7628
|
+
message: `grep: ${compiled.reason}`,
|
|
7629
|
+
field: "pattern"
|
|
7630
|
+
});
|
|
7324
7631
|
}
|
|
7325
7632
|
const re = compiled.regex;
|
|
7326
7633
|
const globRe = input.glob ? compileGlob(input.glob) : null;
|
|
@@ -7477,6 +7784,10 @@ var installTool = {
|
|
|
7477
7784
|
global: {
|
|
7478
7785
|
type: "boolean",
|
|
7479
7786
|
description: "Whether to perform a global install (use with caution)."
|
|
7787
|
+
},
|
|
7788
|
+
lifecycleScripts: {
|
|
7789
|
+
type: "boolean",
|
|
7790
|
+
description: "Opt in to running package lifecycle scripts (preinstall / install / postinstall / prepare / \u2026). Default: false \u2014 installs pass --ignore-scripts so a malicious package cannot execute arbitrary code at install time. Set true to opt back in to the legacy npm/pnpm/yarn default."
|
|
7480
7791
|
}
|
|
7481
7792
|
}
|
|
7482
7793
|
},
|
|
@@ -7496,8 +7807,10 @@ var installTool = {
|
|
|
7496
7807
|
yield { type: "log", text: `Resolving with ${pkgManager}\u2026`, data: { phase: "resolve" } };
|
|
7497
7808
|
const save = input.save === "dev" ? "-D" : input.save === "optional" ? "-O" : "";
|
|
7498
7809
|
const globalFlag = input.global ? ["-g"] : [];
|
|
7810
|
+
const ignoreScripts = input.lifecycleScripts !== true;
|
|
7499
7811
|
const args = [];
|
|
7500
7812
|
if (input.dry_run) args.push("--dry-run");
|
|
7813
|
+
if (ignoreScripts) args.push("--ignore-scripts");
|
|
7501
7814
|
if (pkgManager === "pnpm") {
|
|
7502
7815
|
if (save) args.push(save);
|
|
7503
7816
|
args.push("add", ...globalFlag);
|
|
@@ -7592,8 +7905,8 @@ function resolveManifestPath(cwd, pkgManager) {
|
|
|
7592
7905
|
var jsonTool = {
|
|
7593
7906
|
name: "json",
|
|
7594
7907
|
category: "Data",
|
|
7595
|
-
description: "Parse, pretty-print, query,
|
|
7596
|
-
usageHint:
|
|
7908
|
+
description: "Parse, pretty-print, query, validate, transform, and merge JSON/JSON5/YAML. Use `action` to select the operation: parse (default), query, validate, transform, or merge.",
|
|
7909
|
+
usageHint: 'VERY USEFUL FOR DATA INSPECTION:\n\n- `action: "parse"` (default): read/pretty-print/convert JSON, JSON5, or YAML from `file` or `data`.\n- `action: "query"`: JMESPath-like query (`a.b[0].c`, `items[*].name`, filters, functions).\n- `action: "validate"`: validate data against a JSON Schema (`schema` param).\n- `action: "transform"`: chain multiple JMESPath transforms (`transforms` param).\n- `action: "merge"`: deep merge `base` and `patch` objects (`conflictResolution` param).\nPrefer this over raw `read` + manual parsing when dealing with configuration or data files.',
|
|
7597
7910
|
permission: "auto",
|
|
7598
7911
|
mutating: false,
|
|
7599
7912
|
timeoutMs: 5e3,
|
|
@@ -7602,69 +7915,420 @@ var jsonTool = {
|
|
|
7602
7915
|
inputSchema: {
|
|
7603
7916
|
type: "object",
|
|
7604
7917
|
properties: {
|
|
7605
|
-
|
|
7606
|
-
data: { type: "string", description: "JSON/JSON5/YAML string (alternative to file)" },
|
|
7607
|
-
query: {
|
|
7918
|
+
action: {
|
|
7608
7919
|
type: "string",
|
|
7609
|
-
|
|
7920
|
+
enum: ["parse", "query", "validate", "transform", "merge"],
|
|
7921
|
+
description: "Operation (default: parse). parse=read/pretty-print, query=JMESPath, validate=schema, transform=chained queries, merge=deep merge."
|
|
7610
7922
|
},
|
|
7923
|
+
file: { type: "string", description: "Path to JSON/JSON5/YAML file (parse/query/validate)" },
|
|
7924
|
+
data: { type: "string", description: "JSON/JSON5/YAML string (parse/query/validate, alternative to file)" },
|
|
7611
7925
|
format: {
|
|
7612
7926
|
type: "string",
|
|
7613
7927
|
enum: ["json", "json5", "yaml"],
|
|
7614
|
-
description: "Output format (default: json)"
|
|
7928
|
+
description: "Output format for parse/query/transform (default: json)"
|
|
7929
|
+
},
|
|
7930
|
+
query: {
|
|
7931
|
+
type: "string",
|
|
7932
|
+
description: "JMESPath-like query expression (query action)"
|
|
7933
|
+
},
|
|
7934
|
+
transforms: {
|
|
7935
|
+
type: "array",
|
|
7936
|
+
items: { type: "string" },
|
|
7937
|
+
description: "Ordered JMESPath query strings (transform action)"
|
|
7938
|
+
},
|
|
7939
|
+
schema: {
|
|
7940
|
+
type: "object",
|
|
7941
|
+
description: "JSON Schema to validate against (validate action)"
|
|
7942
|
+
},
|
|
7943
|
+
base: { description: "Base JSON object (merge action)" },
|
|
7944
|
+
patch: { description: "Patch JSON object to merge in (merge action)" },
|
|
7945
|
+
conflictResolution: {
|
|
7946
|
+
type: "string",
|
|
7947
|
+
enum: ["prefer-base", "prefer-patch"],
|
|
7948
|
+
description: "Merge conflict resolution (default: prefer-patch)"
|
|
7615
7949
|
},
|
|
7616
7950
|
validate: {
|
|
7617
7951
|
type: "boolean",
|
|
7618
|
-
description: "Validate syntax only, no output (default: false)"
|
|
7952
|
+
description: "Validate syntax only, no output (parse action, default: false)"
|
|
7619
7953
|
}
|
|
7620
7954
|
}
|
|
7621
7955
|
},
|
|
7622
|
-
async execute(input) {
|
|
7623
|
-
const
|
|
7624
|
-
|
|
7625
|
-
|
|
7626
|
-
|
|
7627
|
-
|
|
7628
|
-
|
|
7629
|
-
|
|
7630
|
-
return
|
|
7631
|
-
|
|
7632
|
-
|
|
7633
|
-
|
|
7634
|
-
|
|
7635
|
-
|
|
7956
|
+
async execute(input, ctx) {
|
|
7957
|
+
const action = input.action ?? "parse";
|
|
7958
|
+
switch (action) {
|
|
7959
|
+
case "query":
|
|
7960
|
+
return executeQuery(input, ctx);
|
|
7961
|
+
case "validate":
|
|
7962
|
+
return executeValidate(input, ctx);
|
|
7963
|
+
case "transform":
|
|
7964
|
+
return executeTransform(input, ctx);
|
|
7965
|
+
case "merge":
|
|
7966
|
+
return executeMerge(input);
|
|
7967
|
+
case "parse":
|
|
7968
|
+
default:
|
|
7969
|
+
return executeParse(input, ctx);
|
|
7636
7970
|
}
|
|
7971
|
+
}
|
|
7972
|
+
};
|
|
7973
|
+
async function executeParse(input, ctx) {
|
|
7974
|
+
const format = input.format ?? "json";
|
|
7975
|
+
let parsed;
|
|
7976
|
+
let raw;
|
|
7977
|
+
if (input.file) {
|
|
7637
7978
|
try {
|
|
7638
|
-
|
|
7639
|
-
} catch
|
|
7640
|
-
return {
|
|
7641
|
-
data: null,
|
|
7642
|
-
formatted: "",
|
|
7643
|
-
type: "unknown",
|
|
7644
|
-
/* v8 ignore next -- JSON.parse only throws SyntaxError (an Error); the String(e) side is defensive. */
|
|
7645
|
-
error: `Parse failed: ${e instanceof Error ? e.message : String(e)}`
|
|
7646
|
-
};
|
|
7647
|
-
}
|
|
7648
|
-
if (input.validate) {
|
|
7649
|
-
return {
|
|
7650
|
-
data: parsed,
|
|
7651
|
-
formatted: "valid",
|
|
7652
|
-
type: Array.isArray(parsed) ? "array" : typeof parsed,
|
|
7653
|
-
keys: typeof parsed === "object" && parsed !== null ? Object.keys(parsed) : void 0
|
|
7654
|
-
};
|
|
7979
|
+
raw = await fs2.readFile(await safeResolveReal(input.file, ctx), "utf8");
|
|
7980
|
+
} catch {
|
|
7981
|
+
return { data: null, formatted: "", type: "unknown", action: "parse", error: "Could not read file" };
|
|
7655
7982
|
}
|
|
7656
|
-
|
|
7657
|
-
|
|
7983
|
+
} else if (input.data) {
|
|
7984
|
+
raw = input.data;
|
|
7985
|
+
} else {
|
|
7986
|
+
return { data: null, formatted: "", type: "unknown", action: "parse", error: "Provide file or data" };
|
|
7987
|
+
}
|
|
7988
|
+
try {
|
|
7989
|
+
parsed = JSON.parse(raw);
|
|
7990
|
+
} catch (e) {
|
|
7991
|
+
return {
|
|
7992
|
+
data: null,
|
|
7993
|
+
formatted: "",
|
|
7994
|
+
type: "unknown",
|
|
7995
|
+
action: "parse",
|
|
7996
|
+
/* v8 ignore next -- JSON.parse only throws SyntaxError (an Error); the String(e) side is defensive. */
|
|
7997
|
+
error: `Parse failed: ${e instanceof Error ? e.message : String(e)}`
|
|
7998
|
+
};
|
|
7999
|
+
}
|
|
8000
|
+
if (input.validate) {
|
|
7658
8001
|
return {
|
|
7659
8002
|
data: parsed,
|
|
7660
|
-
formatted,
|
|
8003
|
+
formatted: "valid",
|
|
7661
8004
|
type: Array.isArray(parsed) ? "array" : typeof parsed,
|
|
8005
|
+
action: "parse",
|
|
8006
|
+
keys: typeof parsed === "object" && parsed !== null ? Object.keys(parsed) : void 0
|
|
8007
|
+
};
|
|
8008
|
+
}
|
|
8009
|
+
if (input.query) {
|
|
8010
|
+
const queryResult = simpleQuery(parsed, input.query);
|
|
8011
|
+
const formatted2 = formatOutput(queryResult, format);
|
|
8012
|
+
return {
|
|
8013
|
+
data: parsed,
|
|
8014
|
+
formatted: formatted2,
|
|
8015
|
+
type: Array.isArray(parsed) ? "array" : typeof parsed,
|
|
8016
|
+
action: "parse",
|
|
7662
8017
|
keys: typeof parsed === "object" && parsed !== null ? Object.keys(parsed) : void 0,
|
|
7663
8018
|
query_result: queryResult
|
|
7664
8019
|
};
|
|
7665
8020
|
}
|
|
7666
|
-
|
|
7667
|
-
|
|
8021
|
+
const formatted = formatOutput(parsed, format);
|
|
8022
|
+
return {
|
|
8023
|
+
data: parsed,
|
|
8024
|
+
formatted,
|
|
8025
|
+
type: Array.isArray(parsed) ? "array" : typeof parsed,
|
|
8026
|
+
action: "parse",
|
|
8027
|
+
keys: typeof parsed === "object" && parsed !== null ? Object.keys(parsed) : void 0
|
|
8028
|
+
};
|
|
8029
|
+
}
|
|
8030
|
+
async function executeQuery(input, ctx) {
|
|
8031
|
+
if (!input.query) {
|
|
8032
|
+
return { data: null, formatted: "", type: "unknown", action: "query", error: "query is required for action: query" };
|
|
8033
|
+
}
|
|
8034
|
+
let parsed;
|
|
8035
|
+
if (input.file) {
|
|
8036
|
+
try {
|
|
8037
|
+
const raw = await fs2.readFile(await safeResolveReal(input.file, ctx), "utf8");
|
|
8038
|
+
parsed = JSON.parse(raw);
|
|
8039
|
+
} catch {
|
|
8040
|
+
return { data: null, formatted: "", type: "unknown", action: "query", error: "Could not read/parse file" };
|
|
8041
|
+
}
|
|
8042
|
+
} else if (input.data) {
|
|
8043
|
+
try {
|
|
8044
|
+
parsed = JSON.parse(input.data);
|
|
8045
|
+
} catch {
|
|
8046
|
+
return { data: null, formatted: "", type: "unknown", action: "query", error: "Could not parse data string" };
|
|
8047
|
+
}
|
|
8048
|
+
} else {
|
|
8049
|
+
return { data: null, formatted: "", type: "unknown", action: "query", error: "Provide file or data" };
|
|
8050
|
+
}
|
|
8051
|
+
try {
|
|
8052
|
+
const result = jmespathSearch(parsed, input.query);
|
|
8053
|
+
const format = input.format ?? "json";
|
|
8054
|
+
return {
|
|
8055
|
+
data: parsed,
|
|
8056
|
+
formatted: formatOutput(result, format),
|
|
8057
|
+
type: result === null ? "null" : Array.isArray(result) ? "array" : typeof result,
|
|
8058
|
+
action: "query",
|
|
8059
|
+
query_result: result
|
|
8060
|
+
};
|
|
8061
|
+
} catch (e) {
|
|
8062
|
+
return {
|
|
8063
|
+
data: null,
|
|
8064
|
+
formatted: "",
|
|
8065
|
+
type: "unknown",
|
|
8066
|
+
action: "query",
|
|
8067
|
+
/* v8 ignore next -- defensive String(e) */
|
|
8068
|
+
error: `Query failed: ${e instanceof Error ? e.message : String(e)}`
|
|
8069
|
+
};
|
|
8070
|
+
}
|
|
8071
|
+
}
|
|
8072
|
+
async function executeValidate(input, ctx) {
|
|
8073
|
+
if (!input.schema) {
|
|
8074
|
+
return { data: null, formatted: "", type: "unknown", action: "validate", error: "schema is required for action: validate" };
|
|
8075
|
+
}
|
|
8076
|
+
let parsed;
|
|
8077
|
+
if (input.file) {
|
|
8078
|
+
try {
|
|
8079
|
+
const raw = await fs2.readFile(await safeResolveReal(input.file, ctx), "utf8");
|
|
8080
|
+
parsed = JSON.parse(raw);
|
|
8081
|
+
} catch {
|
|
8082
|
+
return { data: null, formatted: "", type: "unknown", action: "validate", error: "Could not read/parse file" };
|
|
8083
|
+
}
|
|
8084
|
+
} else if (input.data) {
|
|
8085
|
+
try {
|
|
8086
|
+
parsed = JSON.parse(input.data);
|
|
8087
|
+
} catch {
|
|
8088
|
+
return { data: null, formatted: "", type: "unknown", action: "validate", error: "Could not parse data string" };
|
|
8089
|
+
}
|
|
8090
|
+
} else {
|
|
8091
|
+
return { data: null, formatted: "", type: "unknown", action: "validate", error: "Provide file or data" };
|
|
8092
|
+
}
|
|
8093
|
+
try {
|
|
8094
|
+
const { valid, errors } = validateJsonSchema(parsed, input.schema);
|
|
8095
|
+
return {
|
|
8096
|
+
data: parsed,
|
|
8097
|
+
formatted: valid ? "valid" : "invalid",
|
|
8098
|
+
type: Array.isArray(parsed) ? "array" : typeof parsed,
|
|
8099
|
+
action: "validate",
|
|
8100
|
+
valid,
|
|
8101
|
+
errors
|
|
8102
|
+
};
|
|
8103
|
+
} catch (e) {
|
|
8104
|
+
return {
|
|
8105
|
+
data: null,
|
|
8106
|
+
formatted: "",
|
|
8107
|
+
type: "unknown",
|
|
8108
|
+
action: "validate",
|
|
8109
|
+
/* v8 ignore next -- defensive String(e) */
|
|
8110
|
+
error: `Validation failed: ${e instanceof Error ? e.message : String(e)}`
|
|
8111
|
+
};
|
|
8112
|
+
}
|
|
8113
|
+
}
|
|
8114
|
+
async function executeTransform(input, ctx) {
|
|
8115
|
+
if (!input.transforms || input.transforms.length === 0) {
|
|
8116
|
+
return { data: null, formatted: "", type: "unknown", action: "transform", error: "transforms array is required for action: transform" };
|
|
8117
|
+
}
|
|
8118
|
+
let parsed;
|
|
8119
|
+
if (input.file) {
|
|
8120
|
+
try {
|
|
8121
|
+
const raw = await fs2.readFile(await safeResolveReal(input.file, ctx), "utf8");
|
|
8122
|
+
parsed = JSON.parse(raw);
|
|
8123
|
+
} catch {
|
|
8124
|
+
return { data: null, formatted: "", type: "unknown", action: "transform", error: "Could not read/parse file" };
|
|
8125
|
+
}
|
|
8126
|
+
} else if (input.data) {
|
|
8127
|
+
try {
|
|
8128
|
+
parsed = JSON.parse(input.data);
|
|
8129
|
+
} catch {
|
|
8130
|
+
return { data: null, formatted: "", type: "unknown", action: "transform", error: "Could not parse data string" };
|
|
8131
|
+
}
|
|
8132
|
+
} else {
|
|
8133
|
+
return { data: null, formatted: "", type: "unknown", action: "transform", error: "Provide file or data" };
|
|
8134
|
+
}
|
|
8135
|
+
try {
|
|
8136
|
+
let current = parsed;
|
|
8137
|
+
const steps = [];
|
|
8138
|
+
for (const t of input.transforms) {
|
|
8139
|
+
current = jmespathSearch(current, t);
|
|
8140
|
+
steps.push({ transform: t, result: current });
|
|
8141
|
+
}
|
|
8142
|
+
const format = input.format ?? "json";
|
|
8143
|
+
return {
|
|
8144
|
+
data: parsed,
|
|
8145
|
+
formatted: formatOutput(current, format),
|
|
8146
|
+
type: current === null ? "null" : Array.isArray(current) ? "array" : typeof current,
|
|
8147
|
+
action: "transform",
|
|
8148
|
+
result: current,
|
|
8149
|
+
steps
|
|
8150
|
+
};
|
|
8151
|
+
} catch (e) {
|
|
8152
|
+
return {
|
|
8153
|
+
data: null,
|
|
8154
|
+
formatted: "",
|
|
8155
|
+
type: "unknown",
|
|
8156
|
+
action: "transform",
|
|
8157
|
+
/* v8 ignore next -- defensive String(e) */
|
|
8158
|
+
error: `Transform failed: ${e instanceof Error ? e.message : String(e)}`
|
|
8159
|
+
};
|
|
8160
|
+
}
|
|
8161
|
+
}
|
|
8162
|
+
async function executeMerge(input) {
|
|
8163
|
+
if (input.base === void 0 || input.patch === void 0) {
|
|
8164
|
+
return { data: null, formatted: "", type: "unknown", action: "merge", error: "base and patch are required for action: merge" };
|
|
8165
|
+
}
|
|
8166
|
+
const conflictResolution = input.conflictResolution ?? "prefer-patch";
|
|
8167
|
+
try {
|
|
8168
|
+
const result = deepMerge(input.base, input.patch, { conflictResolution });
|
|
8169
|
+
const format = input.format ?? "json";
|
|
8170
|
+
return {
|
|
8171
|
+
data: result,
|
|
8172
|
+
formatted: formatOutput(result, format),
|
|
8173
|
+
type: result === null ? "null" : Array.isArray(result) ? "array" : typeof result,
|
|
8174
|
+
action: "merge",
|
|
8175
|
+
result
|
|
8176
|
+
};
|
|
8177
|
+
} catch (e) {
|
|
8178
|
+
return {
|
|
8179
|
+
data: null,
|
|
8180
|
+
formatted: "",
|
|
8181
|
+
type: "unknown",
|
|
8182
|
+
action: "merge",
|
|
8183
|
+
/* v8 ignore next -- defensive String(e) */
|
|
8184
|
+
error: `Merge failed: ${e instanceof Error ? e.message : String(e)}`
|
|
8185
|
+
};
|
|
8186
|
+
}
|
|
8187
|
+
}
|
|
8188
|
+
function jmespathSearch(data, query) {
|
|
8189
|
+
if (!query || query === "@") return data;
|
|
8190
|
+
if (query === "$") return data;
|
|
8191
|
+
const dotMatch = query.match(/^([a-zA-Z_][a-zA-Z0-9_]*)(?:\.(.+))?$/);
|
|
8192
|
+
if (dotMatch) {
|
|
8193
|
+
const key = dotMatch[1];
|
|
8194
|
+
const rest = dotMatch[2];
|
|
8195
|
+
const val = data?.[key];
|
|
8196
|
+
if (rest === void 0) return val;
|
|
8197
|
+
return jmespathSearch(val, rest);
|
|
8198
|
+
}
|
|
8199
|
+
const arrMatch = query.match(/^\[(\d+)\](?:\.(.+))?$/);
|
|
8200
|
+
if (arrMatch) {
|
|
8201
|
+
const idx = Number.parseInt(arrMatch[1], 10);
|
|
8202
|
+
const rest = arrMatch[2];
|
|
8203
|
+
const arr = data;
|
|
8204
|
+
const val = arr?.[idx];
|
|
8205
|
+
if (rest === void 0) return val;
|
|
8206
|
+
return jmespathSearch(val, rest);
|
|
8207
|
+
}
|
|
8208
|
+
if (query === "[*]") {
|
|
8209
|
+
if (Array.isArray(data)) {
|
|
8210
|
+
return data;
|
|
8211
|
+
}
|
|
8212
|
+
return data;
|
|
8213
|
+
}
|
|
8214
|
+
const multiMatch = query.match(/^([a-zA-Z_][a-zA-Z0-9_]*)\[\*\](?:\.(.+))?$/);
|
|
8215
|
+
if (multiMatch) {
|
|
8216
|
+
const key = multiMatch[1];
|
|
8217
|
+
const rest = multiMatch[2];
|
|
8218
|
+
const arr = data?.[key];
|
|
8219
|
+
if (!Array.isArray(arr)) return [];
|
|
8220
|
+
if (rest === void 0) return arr;
|
|
8221
|
+
return arr.map((item) => jmespathSearch(item, rest));
|
|
8222
|
+
}
|
|
8223
|
+
const filterMatch = query.match(/^\[\\?([a-zA-Z_][a-zA-Z0-9_]*)(==|!=|<|>|<=|>=)(`[^`]+`|'[^']*')\](?:\.(.+))?$/);
|
|
8224
|
+
if (filterMatch) {
|
|
8225
|
+
const field = filterMatch[1];
|
|
8226
|
+
const op = filterMatch[2];
|
|
8227
|
+
const rawVal = filterMatch[3];
|
|
8228
|
+
const rest = filterMatch[4];
|
|
8229
|
+
const cmpVal = JSON.parse(rawVal.slice(1, -1));
|
|
8230
|
+
const arr = data;
|
|
8231
|
+
if (!Array.isArray(arr)) return [];
|
|
8232
|
+
const filtered = arr.filter((item) => {
|
|
8233
|
+
const itemVal = item[field];
|
|
8234
|
+
switch (op) {
|
|
8235
|
+
case "==":
|
|
8236
|
+
return itemVal === cmpVal;
|
|
8237
|
+
case "!=":
|
|
8238
|
+
return itemVal !== cmpVal;
|
|
8239
|
+
case ">":
|
|
8240
|
+
return Number(itemVal) > Number(cmpVal);
|
|
8241
|
+
case "<":
|
|
8242
|
+
return Number(itemVal) < Number(cmpVal);
|
|
8243
|
+
case ">=":
|
|
8244
|
+
return Number(itemVal) >= Number(cmpVal);
|
|
8245
|
+
case "<=":
|
|
8246
|
+
return Number(itemVal) <= Number(cmpVal);
|
|
8247
|
+
/* v8 ignore next -- op is constrained to the six operators by the filter regex; default is unreachable. */
|
|
8248
|
+
default:
|
|
8249
|
+
return true;
|
|
8250
|
+
}
|
|
8251
|
+
});
|
|
8252
|
+
if (rest === void 0) return filtered;
|
|
8253
|
+
return filtered.map((item) => jmespathSearch(item, rest));
|
|
8254
|
+
}
|
|
8255
|
+
const fnMatch = query.match(/^(length|keys|values|type)\(@\)$/);
|
|
8256
|
+
if (fnMatch) {
|
|
8257
|
+
const fn = fnMatch[1];
|
|
8258
|
+
switch (fn) {
|
|
8259
|
+
case "length":
|
|
8260
|
+
if (Array.isArray(data)) return data.length;
|
|
8261
|
+
if (typeof data === "string") return data.length;
|
|
8262
|
+
if (typeof data === "object" && data !== null) return Object.keys(data).length;
|
|
8263
|
+
return 0;
|
|
8264
|
+
case "keys":
|
|
8265
|
+
if (typeof data === "object" && data !== null && !Array.isArray(data)) return Object.keys(data);
|
|
8266
|
+
return [];
|
|
8267
|
+
case "values":
|
|
8268
|
+
if (typeof data === "object" && data !== null && !Array.isArray(data)) return Object.values(data);
|
|
8269
|
+
return [];
|
|
8270
|
+
case "type":
|
|
8271
|
+
if (data === null) return "null";
|
|
8272
|
+
if (Array.isArray(data)) return "array";
|
|
8273
|
+
return typeof data;
|
|
8274
|
+
/* v8 ignore next 2 -- fn is constrained to the four names by the function regex; default is unreachable. */
|
|
8275
|
+
default:
|
|
8276
|
+
return null;
|
|
8277
|
+
}
|
|
8278
|
+
}
|
|
8279
|
+
return null;
|
|
8280
|
+
}
|
|
8281
|
+
function validateJsonSchema(data, schema) {
|
|
8282
|
+
const errors = [];
|
|
8283
|
+
function check(value, s, path22) {
|
|
8284
|
+
if (s["type"]) {
|
|
8285
|
+
const expectedType = s["type"];
|
|
8286
|
+
const actualType = Array.isArray(value) ? "array" : value === null ? "null" : typeof value;
|
|
8287
|
+
if (expectedType === "integer") {
|
|
8288
|
+
if (!Number.isInteger(value)) errors.push(`${path22}: expected integer, got ${actualType}`);
|
|
8289
|
+
} else if (expectedType !== actualType) {
|
|
8290
|
+
errors.push(`${path22}: expected ${expectedType}, got ${actualType}`);
|
|
8291
|
+
}
|
|
8292
|
+
}
|
|
8293
|
+
if (typeof value === "string" && s["format"] === "uri" && value) {
|
|
8294
|
+
try {
|
|
8295
|
+
new URL(value);
|
|
8296
|
+
} catch {
|
|
8297
|
+
errors.push(`${path22}: not a valid URI`);
|
|
8298
|
+
}
|
|
8299
|
+
}
|
|
8300
|
+
if (typeof value === "string" && s["pattern"]) {
|
|
8301
|
+
const re = new RegExp(s["pattern"]);
|
|
8302
|
+
if (!re.test(value)) errors.push(`${path22}: does not match pattern ${s["pattern"]}`);
|
|
8303
|
+
}
|
|
8304
|
+
if (typeof value === "string" && s["minLength"] !== void 0 && value.length < s["minLength"]) {
|
|
8305
|
+
errors.push(`${path22}: string too short (min ${s["minLength"]})`);
|
|
8306
|
+
}
|
|
8307
|
+
if (typeof value === "string" && s["maxLength"] !== void 0 && value.length > s["maxLength"]) {
|
|
8308
|
+
errors.push(`${path22}: string too long (max ${s["maxLength"]})`);
|
|
8309
|
+
}
|
|
8310
|
+
if (typeof value === "number" && s["minimum"] !== void 0 && value < s["minimum"]) {
|
|
8311
|
+
errors.push(`${path22}: below minimum ${s["minimum"]}`);
|
|
8312
|
+
}
|
|
8313
|
+
if (typeof value === "number" && s["maximum"] !== void 0 && value > s["maximum"]) {
|
|
8314
|
+
errors.push(`${path22}: above maximum ${s["maximum"]}`);
|
|
8315
|
+
}
|
|
8316
|
+
if (Array.isArray(value) && s["items"] && Array.isArray(s["items"])) {
|
|
8317
|
+
for (let i = 0; i < value.length; i++) {
|
|
8318
|
+
check(value[i], s["items"], `${path22}[${i}]`);
|
|
8319
|
+
}
|
|
8320
|
+
}
|
|
8321
|
+
if (typeof value === "object" && value !== null && !Array.isArray(value) && s["properties"]) {
|
|
8322
|
+
const props = s["properties"];
|
|
8323
|
+
for (const [k, propSchema] of Object.entries(props)) {
|
|
8324
|
+
check(value[k], propSchema, `${path22}.${k}`);
|
|
8325
|
+
}
|
|
8326
|
+
}
|
|
8327
|
+
}
|
|
8328
|
+
check(data, schema, "$");
|
|
8329
|
+
return { valid: errors.length === 0, errors };
|
|
8330
|
+
}
|
|
8331
|
+
function simpleQuery(data, path22) {
|
|
7668
8332
|
const parts = path22.replace(/\[(\d+)\]/g, ".$1").split(".").filter(Boolean);
|
|
7669
8333
|
let current = data;
|
|
7670
8334
|
for (const part of parts) {
|
|
@@ -7891,7 +8555,7 @@ async function dockerLogs(service, lines, filterRe, cwd, signal, since) {
|
|
|
7891
8555
|
};
|
|
7892
8556
|
}
|
|
7893
8557
|
args.push("--timestamps", service);
|
|
7894
|
-
return new Promise((
|
|
8558
|
+
return new Promise((resolve7) => {
|
|
7895
8559
|
let stdout = "";
|
|
7896
8560
|
let stderr = "";
|
|
7897
8561
|
const MAX = 2e5;
|
|
@@ -7907,7 +8571,7 @@ async function dockerLogs(service, lines, filterRe, cwd, signal, since) {
|
|
|
7907
8571
|
if (settled) return;
|
|
7908
8572
|
settled = true;
|
|
7909
8573
|
clearTimeout(timer);
|
|
7910
|
-
|
|
8574
|
+
resolve7(result);
|
|
7911
8575
|
};
|
|
7912
8576
|
const child = spawn("docker", args, { cwd, signal, env: buildChildEnv(), stdio: ["ignore", "pipe", "pipe"], windowsHide: true });
|
|
7913
8577
|
const timer = setTimeout(() => {
|
|
@@ -8027,8 +8691,8 @@ var outdatedTool = {
|
|
|
8027
8691
|
// read-only, but `outdated` makes outbound HTTP calls to the
|
|
8028
8692
|
// registry. The 'confirm' permission routes the call through the
|
|
8029
8693
|
// tool.confirm_needed flow on every invocation. M-1 originally
|
|
8030
|
-
// fixed four sibling tools (mcp_control, shellcheck,
|
|
8031
|
-
//
|
|
8694
|
+
// fixed four sibling tools (mcp_control, shellcheck, shellcheck (scan mode),
|
|
8695
|
+
// search) but missed this one; applying the same contract here.
|
|
8032
8696
|
mutating: true,
|
|
8033
8697
|
// Capability is outbound network — the tool only hits the package
|
|
8034
8698
|
// registry over HTTP, never touches the filesystem or runs shell.
|
|
@@ -8069,7 +8733,7 @@ var outdatedTool = {
|
|
|
8069
8733
|
}
|
|
8070
8734
|
};
|
|
8071
8735
|
function runOutdated(manager, args, cwd, signal) {
|
|
8072
|
-
return new Promise((
|
|
8736
|
+
return new Promise((resolve7) => {
|
|
8073
8737
|
let stdout = "";
|
|
8074
8738
|
let stderr = "";
|
|
8075
8739
|
const MAX = 1e5;
|
|
@@ -8086,10 +8750,10 @@ function runOutdated(manager, args, cwd, signal) {
|
|
|
8086
8750
|
});
|
|
8087
8751
|
child.on("close", (code) => {
|
|
8088
8752
|
const result = parseOutdatedOutput(stdout, code ?? 0);
|
|
8089
|
-
|
|
8753
|
+
resolve7(result);
|
|
8090
8754
|
});
|
|
8091
8755
|
child.on("error", (e) => {
|
|
8092
|
-
|
|
8756
|
+
resolve7({
|
|
8093
8757
|
exit_code: 1,
|
|
8094
8758
|
packages: [],
|
|
8095
8759
|
total: 0,
|
|
@@ -8223,7 +8887,7 @@ function stripPathComponents(p, strip) {
|
|
|
8223
8887
|
return parts.slice(strip).join("/");
|
|
8224
8888
|
}
|
|
8225
8889
|
function runPatch(args, cwd, signal) {
|
|
8226
|
-
return new Promise((
|
|
8890
|
+
return new Promise((resolve7) => {
|
|
8227
8891
|
let stdout = "";
|
|
8228
8892
|
let stderr = "";
|
|
8229
8893
|
const env = { ...buildChildEnv(), LANG: "C", LC_ALL: "C" };
|
|
@@ -8234,8 +8898,8 @@ function runPatch(args, cwd, signal) {
|
|
|
8234
8898
|
child.stderr?.on("data", (c) => {
|
|
8235
8899
|
stderr += c.toString();
|
|
8236
8900
|
});
|
|
8237
|
-
child.on("close", (code) =>
|
|
8238
|
-
child.on("error", (e) =>
|
|
8901
|
+
child.on("close", (code) => resolve7({ exitCode: code ?? 1, stdout, stderr }));
|
|
8902
|
+
child.on("error", (e) => resolve7({ exitCode: 1, stdout: "", stderr: e.message }));
|
|
8239
8903
|
});
|
|
8240
8904
|
}
|
|
8241
8905
|
function extractPatchedFiles(output) {
|
|
@@ -8542,19 +9206,49 @@ var readTool = {
|
|
|
8542
9206
|
required: ["path"]
|
|
8543
9207
|
},
|
|
8544
9208
|
async execute(input, ctx) {
|
|
8545
|
-
if (!input?.path)
|
|
9209
|
+
if (!input?.path) {
|
|
9210
|
+
throw new ToolValidationError({
|
|
9211
|
+
message: "read: path is required",
|
|
9212
|
+
field: "path"
|
|
9213
|
+
});
|
|
9214
|
+
}
|
|
8546
9215
|
const absPath = await safeResolveReal(input.path, ctx);
|
|
8547
9216
|
let stat11;
|
|
8548
9217
|
try {
|
|
8549
9218
|
stat11 = await fs2.stat(absPath);
|
|
8550
9219
|
} catch (err) {
|
|
8551
9220
|
const code = err.code;
|
|
8552
|
-
if (code === "ENOENT")
|
|
8553
|
-
|
|
9221
|
+
if (code === "ENOENT") {
|
|
9222
|
+
throw new FsError({
|
|
9223
|
+
message: `read: file not found "${input.path}"`,
|
|
9224
|
+
code: "FS_READ_FAILED",
|
|
9225
|
+
path: absPath,
|
|
9226
|
+
context: { errno: "ENOENT" }
|
|
9227
|
+
});
|
|
9228
|
+
}
|
|
9229
|
+
throw new FsError({
|
|
9230
|
+
message: `read: failed to stat "${input.path}": ${toErrorMessage$2(err)}`,
|
|
9231
|
+
code: "FS_READ_FAILED",
|
|
9232
|
+
path: absPath,
|
|
9233
|
+
context: { errno: code },
|
|
9234
|
+
cause: err
|
|
9235
|
+
});
|
|
9236
|
+
}
|
|
9237
|
+
if (!stat11.isFile()) {
|
|
9238
|
+
throw new FsError({
|
|
9239
|
+
message: `read: "${input.path}" is not a regular file`,
|
|
9240
|
+
code: "FS_READ_FAILED",
|
|
9241
|
+
path: absPath,
|
|
9242
|
+
context: { reason: "not-a-regular-file" }
|
|
9243
|
+
});
|
|
8554
9244
|
}
|
|
8555
|
-
if (!stat11.isFile()) throw new Error(`read: "${input.path}" is not a regular file`);
|
|
8556
9245
|
if (stat11.size > MAX_BYTES2) {
|
|
8557
|
-
throw new
|
|
9246
|
+
throw new FsError({
|
|
9247
|
+
message: `read: file too large (${stat11.size} bytes, limit ${MAX_BYTES2})`,
|
|
9248
|
+
code: "FS_READ_FAILED",
|
|
9249
|
+
path: absPath,
|
|
9250
|
+
context: { size: stat11.size, limit: MAX_BYTES2, reason: "too-large" }
|
|
9251
|
+
});
|
|
8558
9252
|
}
|
|
8559
9253
|
const offset = Math.max(1, input.offset ?? 1);
|
|
8560
9254
|
const limit = Math.max(0, Math.min(input.limit ?? 2e3, 5e3));
|
|
@@ -8678,8 +9372,8 @@ var DEFAULT_IGNORE4 = ["node_modules", ".git", "dist", "build", ".next", "covera
|
|
|
8678
9372
|
var replaceTool = {
|
|
8679
9373
|
name: "replace",
|
|
8680
9374
|
category: "Transform",
|
|
8681
|
-
description: "Perform a search-and-replace across multiple files using a regex pattern. This is a powerful bulk transformation tool.
|
|
8682
|
-
usageHint: "DANGEROUS IF USED CARELESSLY \u2014 review the diff output carefully.\n\nRecommended workflow:\n1.
|
|
9375
|
+
description: "Perform a search-and-replace across multiple files using a regex pattern. This is a powerful bulk transformation tool. Dry-run is ON by default \u2014 set `dry_run: false` to apply changes.",
|
|
9376
|
+
usageHint: "DANGEROUS IF USED CARELESSLY \u2014 review the diff output carefully.\n\nRecommended workflow:\n1. Run without `dry_run: false` first to see exactly what would change (dry-run is the default).\n2. Review the diff output, then re-run with `dry_run: false` to apply.\n3. Use a specific enough `pattern` (and `glob` / `files`) to avoid accidental broad changes.\n4. `replace_all` controls whether only the first match per file or all matches are replaced.\nThis tool is excellent for large-scale refactors (renaming, import updates, etc.) but must be used with caution.",
|
|
8683
9377
|
permission: "confirm",
|
|
8684
9378
|
mutating: true,
|
|
8685
9379
|
capabilities: ["fs.write"],
|
|
@@ -8699,22 +9393,40 @@ var replaceTool = {
|
|
|
8699
9393
|
type: "boolean",
|
|
8700
9394
|
description: "Replace all occurrences in each file (default: true)"
|
|
8701
9395
|
},
|
|
8702
|
-
dry_run: { type: "boolean", description: "Preview changes without writing" }
|
|
9396
|
+
dry_run: { type: "boolean", description: "Preview changes without writing (default: true)" }
|
|
8703
9397
|
},
|
|
8704
9398
|
required: ["pattern", "replacement", "files"]
|
|
8705
9399
|
},
|
|
8706
9400
|
async execute(input, ctx) {
|
|
8707
|
-
if (!input?.pattern)
|
|
8708
|
-
|
|
8709
|
-
|
|
9401
|
+
if (!input?.pattern) {
|
|
9402
|
+
throw new ToolValidationError({
|
|
9403
|
+
message: "replace: pattern is required",
|
|
9404
|
+
field: "pattern"
|
|
9405
|
+
});
|
|
9406
|
+
}
|
|
9407
|
+
if (input.replacement === void 0) {
|
|
9408
|
+
throw new ToolValidationError({
|
|
9409
|
+
message: "replace: replacement is required",
|
|
9410
|
+
field: "replacement"
|
|
9411
|
+
});
|
|
9412
|
+
}
|
|
9413
|
+
if (!input?.files) {
|
|
9414
|
+
throw new ToolValidationError({
|
|
9415
|
+
message: "replace: files is required",
|
|
9416
|
+
field: "files"
|
|
9417
|
+
});
|
|
9418
|
+
}
|
|
8710
9419
|
const replaceAll = input.replace_all ?? true;
|
|
8711
9420
|
const compiled = compileUserRegex(input.pattern, "g");
|
|
8712
9421
|
if (!compiled.ok) {
|
|
8713
|
-
throw new
|
|
9422
|
+
throw new ToolValidationError({
|
|
9423
|
+
message: `replace: ${compiled.reason}`,
|
|
9424
|
+
field: "pattern"
|
|
9425
|
+
});
|
|
8714
9426
|
}
|
|
8715
9427
|
const re = compiled.regex;
|
|
8716
9428
|
const globRe = input.glob ? compileGlob(input.glob) : null;
|
|
8717
|
-
const dryRun = input.dry_run ??
|
|
9429
|
+
const dryRun = input.dry_run ?? true;
|
|
8718
9430
|
const filesInput = Array.isArray(input.files) ? input.files.join(",") : input.files;
|
|
8719
9431
|
const fileList = await resolveFiles2(filesInput, ctx, globRe);
|
|
8720
9432
|
const realRoot = await fs2.realpath(ctx.projectRoot).catch(() => ctx.projectRoot);
|
|
@@ -8810,13 +9522,13 @@ async function globFiles(pattern, base, extraGlob) {
|
|
|
8810
9522
|
return await globNative(pattern, base, extraGlob);
|
|
8811
9523
|
}
|
|
8812
9524
|
function checkRg() {
|
|
8813
|
-
return new Promise((
|
|
9525
|
+
return new Promise((resolve7) => {
|
|
8814
9526
|
try {
|
|
8815
9527
|
const p = spawn("rg", ["--version"], { env: buildChildEnv(), stdio: "ignore", windowsHide: true });
|
|
8816
|
-
p.on("error", () =>
|
|
8817
|
-
p.on("close", (code) =>
|
|
9528
|
+
p.on("error", () => resolve7(false));
|
|
9529
|
+
p.on("close", (code) => resolve7(code === 0));
|
|
8818
9530
|
} catch {
|
|
8819
|
-
|
|
9531
|
+
resolve7(false);
|
|
8820
9532
|
}
|
|
8821
9533
|
});
|
|
8822
9534
|
}
|
|
@@ -8833,10 +9545,10 @@ function spawnRgFind(pattern, base) {
|
|
|
8833
9545
|
buf += chunk.toString();
|
|
8834
9546
|
});
|
|
8835
9547
|
return {
|
|
8836
|
-
promise: new Promise((
|
|
9548
|
+
promise: new Promise((resolve7, reject) => {
|
|
8837
9549
|
child.on("error", reject);
|
|
8838
9550
|
child.on("close", () => {
|
|
8839
|
-
|
|
9551
|
+
resolve7(buf.split("\n").filter(Boolean));
|
|
8840
9552
|
});
|
|
8841
9553
|
})
|
|
8842
9554
|
};
|
|
@@ -9063,11 +9775,13 @@ function substituteVars(content, name, vars) {
|
|
|
9063
9775
|
var DEFAULT_NUM = 10;
|
|
9064
9776
|
var MAX_RESULTS = 50;
|
|
9065
9777
|
var TIMEOUT_MS3 = 15e3;
|
|
9778
|
+
var CACHE_TTL_MS = 3e5;
|
|
9779
|
+
var cache = /* @__PURE__ */ new Map();
|
|
9066
9780
|
var searchTool = {
|
|
9067
9781
|
name: "search",
|
|
9068
9782
|
category: "Search",
|
|
9069
|
-
description: "Perform a web search and return results with title, URL, and snippet. Use this when you need up-to-date external information that is not in the local codebase.",
|
|
9070
|
-
usageHint: "Good for: API documentation, error messages, library usage examples, current best practices.\n\n- Prefer specific queries over very broad ones.\n- Results go through the guarded fetch system (same protections as the `fetch` tool).\n- This is often better than the model trying to recall outdated knowledge.",
|
|
9783
|
+
description: "Perform a web search and return results with title, URL, and snippet. Use this when you need up-to-date external information that is not in the local codebase. Results are cached (5 min TTL) and deduplicated by URL.",
|
|
9784
|
+
usageHint: "Good for: API documentation, error messages, library usage examples, current best practices.\n\n- Prefer specific queries over very broad ones.\n- Results go through the guarded fetch system (same protections as the `fetch` tool).\n- Supports duckduckgo (default), google, and bing sources.\n- Set `skip_cache: true` to force a fresh search.\n- This is often better than the model trying to recall outdated knowledge.",
|
|
9071
9785
|
permission: "confirm",
|
|
9072
9786
|
mutating: false,
|
|
9073
9787
|
capabilities: ["net.outbound"],
|
|
@@ -9087,6 +9801,10 @@ var searchTool = {
|
|
|
9087
9801
|
type: "string",
|
|
9088
9802
|
enum: ["duckduckgo", "google", "bing"],
|
|
9089
9803
|
description: "Search engine to use (default: duckduckgo)"
|
|
9804
|
+
},
|
|
9805
|
+
skip_cache: {
|
|
9806
|
+
type: "boolean",
|
|
9807
|
+
description: "Skip the in-memory cache and force a fresh search (default: false)"
|
|
9090
9808
|
}
|
|
9091
9809
|
},
|
|
9092
9810
|
required: ["query"]
|
|
@@ -9102,57 +9820,135 @@ var searchTool = {
|
|
|
9102
9820
|
return final;
|
|
9103
9821
|
},
|
|
9104
9822
|
async *executeStream(input, _ctx, opts) {
|
|
9105
|
-
if (!input?.query
|
|
9823
|
+
if (!input?.query || input.query.trim() === "") {
|
|
9824
|
+
throw new ToolValidationError({
|
|
9825
|
+
message: "search: query is required and must be a non-empty string",
|
|
9826
|
+
field: "query"
|
|
9827
|
+
});
|
|
9828
|
+
}
|
|
9106
9829
|
const num = Math.max(1, Math.min(input.num_results ?? DEFAULT_NUM, MAX_RESULTS));
|
|
9107
9830
|
const source = input.source ?? "duckduckgo";
|
|
9831
|
+
const skipCache = input.skip_cache ?? false;
|
|
9832
|
+
const cacheKey = `${source}:${input.query}`;
|
|
9833
|
+
if (!skipCache) {
|
|
9834
|
+
const entry = cache.get(cacheKey);
|
|
9835
|
+
if (entry && Date.now() - entry.timestamp < CACHE_TTL_MS) {
|
|
9836
|
+
const results = entry.results.map((r) => ({
|
|
9837
|
+
title: r.title,
|
|
9838
|
+
url: r.url,
|
|
9839
|
+
snippet: r.snippet
|
|
9840
|
+
}));
|
|
9841
|
+
yield {
|
|
9842
|
+
type: "log",
|
|
9843
|
+
text: `Cache hit for "${input.query}" (${source})`,
|
|
9844
|
+
data: { source, query: input.query, cached: true }
|
|
9845
|
+
};
|
|
9846
|
+
yield {
|
|
9847
|
+
type: "partial_output",
|
|
9848
|
+
text: `${results.length} cached results from ${source}`,
|
|
9849
|
+
data: { count: results.length, cached: true }
|
|
9850
|
+
};
|
|
9851
|
+
yield {
|
|
9852
|
+
type: "final",
|
|
9853
|
+
output: {
|
|
9854
|
+
query: input.query,
|
|
9855
|
+
results: results.slice(0, num),
|
|
9856
|
+
source,
|
|
9857
|
+
truncated: results.length >= num,
|
|
9858
|
+
cached: true
|
|
9859
|
+
}
|
|
9860
|
+
};
|
|
9861
|
+
return;
|
|
9862
|
+
}
|
|
9863
|
+
}
|
|
9108
9864
|
yield {
|
|
9109
9865
|
type: "log",
|
|
9110
9866
|
text: `Querying ${source} for "${input.query}"\u2026`,
|
|
9111
|
-
data: { source, query: input.query }
|
|
9867
|
+
data: { source, query: input.query, cached: false }
|
|
9112
9868
|
};
|
|
9113
|
-
let
|
|
9869
|
+
let rawResults;
|
|
9114
9870
|
switch (source) {
|
|
9115
9871
|
case "duckduckgo":
|
|
9116
|
-
|
|
9872
|
+
rawResults = await duckduckgoSearch(input.query, num, opts.signal);
|
|
9117
9873
|
break;
|
|
9118
9874
|
case "google":
|
|
9119
|
-
|
|
9875
|
+
rawResults = await googleSearch(input.query, num, opts.signal);
|
|
9120
9876
|
break;
|
|
9121
9877
|
case "bing":
|
|
9122
|
-
|
|
9878
|
+
rawResults = await bingSearch(input.query, num, opts.signal);
|
|
9123
9879
|
break;
|
|
9124
9880
|
default:
|
|
9125
|
-
throw new
|
|
9881
|
+
throw new ToolValidationError({
|
|
9882
|
+
message: `search: unknown source "${source}"`,
|
|
9883
|
+
field: "source"
|
|
9884
|
+
});
|
|
9126
9885
|
}
|
|
9886
|
+
const seenUrls = /* @__PURE__ */ new Set();
|
|
9887
|
+
const deduped = [];
|
|
9888
|
+
for (const r of rawResults) {
|
|
9889
|
+
const noQuery = r.url.split("?")[0] ?? r.url;
|
|
9890
|
+
const normalized = noQuery.split("#")[0] ?? r.url;
|
|
9891
|
+
if (!seenUrls.has(normalized) && r.url.startsWith("http")) {
|
|
9892
|
+
seenUrls.add(normalized);
|
|
9893
|
+
deduped.push(r);
|
|
9894
|
+
}
|
|
9895
|
+
}
|
|
9896
|
+
const ranked = scoreResults(deduped, input.query);
|
|
9897
|
+
const finalResults = ranked.slice(0, num);
|
|
9898
|
+
cache.set(cacheKey, { results: ranked, timestamp: Date.now() });
|
|
9899
|
+
pruneStaleCacheEntries();
|
|
9127
9900
|
yield {
|
|
9128
9901
|
type: "partial_output",
|
|
9129
|
-
text: `${
|
|
9130
|
-
data: { count:
|
|
9902
|
+
text: `${finalResults.length} results from ${source}`,
|
|
9903
|
+
data: { count: finalResults.length, cached: false }
|
|
9904
|
+
};
|
|
9905
|
+
yield {
|
|
9906
|
+
type: "final",
|
|
9907
|
+
output: {
|
|
9908
|
+
query: input.query,
|
|
9909
|
+
results: finalResults.map((r) => ({
|
|
9910
|
+
title: r.title,
|
|
9911
|
+
url: r.url,
|
|
9912
|
+
snippet: r.snippet
|
|
9913
|
+
})),
|
|
9914
|
+
source,
|
|
9915
|
+
truncated: finalResults.length >= num,
|
|
9916
|
+
cached: false
|
|
9917
|
+
}
|
|
9131
9918
|
};
|
|
9132
|
-
yield { type: "final", output };
|
|
9133
9919
|
}
|
|
9134
9920
|
};
|
|
9135
|
-
|
|
9136
|
-
const
|
|
9921
|
+
function pruneStaleCacheEntries() {
|
|
9922
|
+
const cutoff = Date.now() - CACHE_TTL_MS * 2;
|
|
9923
|
+
for (const [key, entry] of cache.entries()) {
|
|
9924
|
+
if (entry.timestamp < cutoff) cache.delete(key);
|
|
9925
|
+
}
|
|
9926
|
+
}
|
|
9927
|
+
function scoreResults(results, query) {
|
|
9928
|
+
const terms = query.toLowerCase().split(/\s+/).filter((t) => t.length > 0);
|
|
9929
|
+
return results.map((r) => {
|
|
9930
|
+
const titleLower = r.title.toLowerCase();
|
|
9931
|
+
const snippetLower = r.snippet.toLowerCase();
|
|
9932
|
+
let score = r.score;
|
|
9933
|
+
for (const term of terms) {
|
|
9934
|
+
if (titleLower.includes(term)) score += 2;
|
|
9935
|
+
if (snippetLower.includes(term)) score += 1;
|
|
9936
|
+
}
|
|
9937
|
+
return { ...r, score };
|
|
9938
|
+
}).sort((a, b) => b.score - a.score);
|
|
9939
|
+
}
|
|
9940
|
+
async function duckduckgoSearch(query, num, signal) {
|
|
9941
|
+
const encoded = encodeURIComponent(query);
|
|
9137
9942
|
const url = `https://lite.duckduckgo.com/lite/?q=${encoded}&kd=-1&kl=wt-wt`;
|
|
9138
9943
|
try {
|
|
9139
9944
|
const response = await fetchWithTimeout(url, signal, TIMEOUT_MS3);
|
|
9140
9945
|
const html = await response.text();
|
|
9141
|
-
|
|
9142
|
-
return {
|
|
9143
|
-
query: query2,
|
|
9144
|
-
results,
|
|
9145
|
-
source: "duckduckgo",
|
|
9146
|
-
truncated: results.length >= num
|
|
9147
|
-
};
|
|
9946
|
+
return parseDuckDuckGo(html, num);
|
|
9148
9947
|
} catch (err) {
|
|
9149
|
-
console.log(
|
|
9150
|
-
|
|
9151
|
-
|
|
9152
|
-
|
|
9153
|
-
source: "duckduckgo",
|
|
9154
|
-
truncated: false
|
|
9155
|
-
};
|
|
9948
|
+
console.log(
|
|
9949
|
+
JSON.stringify({ level: "debug", event: "search_failed", query, error: toErrorMessage$3(err) })
|
|
9950
|
+
);
|
|
9951
|
+
return [{ title: "Search unavailable", url: "https://duckduckgo.com/unavailable", snippet: "Could not reach DuckDuckGo", score: 0 }];
|
|
9156
9952
|
}
|
|
9157
9953
|
}
|
|
9158
9954
|
function takeFrom(iter, max) {
|
|
@@ -9177,25 +9973,22 @@ function parseDuckDuckGo(html, num) {
|
|
|
9177
9973
|
);
|
|
9178
9974
|
for (let i = 0; i < linkMatches.length && i < num; i++) {
|
|
9179
9975
|
const entry = linkMatches[i];
|
|
9180
|
-
|
|
9181
|
-
|
|
9182
|
-
|
|
9183
|
-
|
|
9184
|
-
|
|
9976
|
+
if (entry) {
|
|
9977
|
+
results.push({
|
|
9978
|
+
title: entry.title ?? "",
|
|
9979
|
+
url: entry.url ?? "",
|
|
9980
|
+
snippet: snippetMatches[i] ?? "",
|
|
9981
|
+
score: 1
|
|
9982
|
+
});
|
|
9983
|
+
}
|
|
9185
9984
|
}
|
|
9186
9985
|
return results;
|
|
9187
9986
|
}
|
|
9188
|
-
async function googleSearch(
|
|
9189
|
-
const encoded = encodeURIComponent(
|
|
9987
|
+
async function googleSearch(query, num, signal) {
|
|
9988
|
+
const encoded = encodeURIComponent(query);
|
|
9190
9989
|
const url = `https://www.google.com/search?q=${encoded}&hl=en`;
|
|
9191
9990
|
const html = await fetchWithTimeout(url, signal, TIMEOUT_MS3).then((r) => r.text()).catch(() => "");
|
|
9192
|
-
|
|
9193
|
-
return {
|
|
9194
|
-
query: query2,
|
|
9195
|
-
results,
|
|
9196
|
-
source: "google",
|
|
9197
|
-
truncated: results.length >= num
|
|
9198
|
-
};
|
|
9991
|
+
return parseGoogleResults(html, num);
|
|
9199
9992
|
}
|
|
9200
9993
|
function parseGoogleResults(html, num) {
|
|
9201
9994
|
const results = [];
|
|
@@ -9218,22 +10011,17 @@ function parseGoogleResults(html, num) {
|
|
|
9218
10011
|
results.push({
|
|
9219
10012
|
title: titles[i] ?? "",
|
|
9220
10013
|
url: urls[i] ?? "",
|
|
9221
|
-
snippet: snippets[i] ?? ""
|
|
10014
|
+
snippet: snippets[i] ?? "",
|
|
10015
|
+
score: 1
|
|
9222
10016
|
});
|
|
9223
10017
|
}
|
|
9224
10018
|
return results;
|
|
9225
10019
|
}
|
|
9226
|
-
async function bingSearch(
|
|
9227
|
-
const encoded = encodeURIComponent(
|
|
10020
|
+
async function bingSearch(query, num, signal) {
|
|
10021
|
+
const encoded = encodeURIComponent(query);
|
|
9228
10022
|
const url = `https://www.bing.com/search?q=${encoded}`;
|
|
9229
10023
|
const html = await fetchWithTimeout(url, signal, TIMEOUT_MS3).then((r) => r.text()).catch(() => "");
|
|
9230
|
-
|
|
9231
|
-
return {
|
|
9232
|
-
query: query2,
|
|
9233
|
-
results,
|
|
9234
|
-
source: "bing",
|
|
9235
|
-
truncated: results.length >= num
|
|
9236
|
-
};
|
|
10024
|
+
return parseBingResults(html, num);
|
|
9237
10025
|
}
|
|
9238
10026
|
function parseBingResults(html, num) {
|
|
9239
10027
|
const results = [];
|
|
@@ -9248,11 +10036,15 @@ function parseBingResults(html, num) {
|
|
|
9248
10036
|
num
|
|
9249
10037
|
);
|
|
9250
10038
|
for (let i = 0; i < entries.length; i++) {
|
|
9251
|
-
|
|
9252
|
-
|
|
9253
|
-
|
|
9254
|
-
|
|
9255
|
-
|
|
10039
|
+
const entry = entries[i];
|
|
10040
|
+
if (entry) {
|
|
10041
|
+
results.push({
|
|
10042
|
+
title: entry.title ?? "",
|
|
10043
|
+
url: entry.url ?? "",
|
|
10044
|
+
snippet: snippets[i] ?? "",
|
|
10045
|
+
score: 1
|
|
10046
|
+
});
|
|
10047
|
+
}
|
|
9256
10048
|
}
|
|
9257
10049
|
return results;
|
|
9258
10050
|
}
|
|
@@ -9268,7 +10060,15 @@ async function fetchWithTimeout(url, signal, timeoutMs) {
|
|
|
9268
10060
|
return res;
|
|
9269
10061
|
} catch (e) {
|
|
9270
10062
|
clearTimeout(timer);
|
|
9271
|
-
|
|
10063
|
+
if (e instanceof FetchError) {
|
|
10064
|
+
throw e;
|
|
10065
|
+
}
|
|
10066
|
+
throw new FetchError({
|
|
10067
|
+
message: `search: failed to fetch ${url}`,
|
|
10068
|
+
status: 0,
|
|
10069
|
+
context: { url },
|
|
10070
|
+
cause: e
|
|
10071
|
+
});
|
|
9272
10072
|
}
|
|
9273
10073
|
}
|
|
9274
10074
|
function anySignal(...signals) {
|
|
@@ -9332,15 +10132,15 @@ var setWorkingDirTool = {
|
|
|
9332
10132
|
};
|
|
9333
10133
|
}
|
|
9334
10134
|
};
|
|
9335
|
-
function findTaskIndex(tasks,
|
|
9336
|
-
const asNum = Number.parseInt(
|
|
10135
|
+
function findTaskIndex(tasks, query) {
|
|
10136
|
+
const asNum = Number.parseInt(query, 10);
|
|
9337
10137
|
if (!Number.isNaN(asNum)) {
|
|
9338
10138
|
const idx = asNum - 1;
|
|
9339
10139
|
if (tasks[idx]) return idx;
|
|
9340
10140
|
}
|
|
9341
|
-
const byId = tasks.findIndex((t) => t.id ===
|
|
10141
|
+
const byId = tasks.findIndex((t) => t.id === query);
|
|
9342
10142
|
if (byId >= 0) return byId;
|
|
9343
|
-
const lower =
|
|
10143
|
+
const lower = query.toLowerCase();
|
|
9344
10144
|
return tasks.findIndex((t) => t.title.toLowerCase().includes(lower));
|
|
9345
10145
|
}
|
|
9346
10146
|
var taskTool = {
|
|
@@ -9964,8 +10764,8 @@ var todoTool = {
|
|
|
9964
10764
|
var toolHelpTool = {
|
|
9965
10765
|
name: "tool_help",
|
|
9966
10766
|
category: "Meta",
|
|
9967
|
-
description: "Get detailed help for
|
|
9968
|
-
usageHint: "USE WHEN YOU NEED PRECISE TOOL INFORMATION:\n\n- Call with a specific `tool` name when you want the full schema and current usageHint.\n- Omit `tool`
|
|
10767
|
+
description: "Get detailed help for a specific tool, including its full input schema and usage guidance. If you do not know which tool to use, search with `tool_search` first, then call this with the tool name.",
|
|
10768
|
+
usageHint: "USE WHEN YOU NEED PRECISE TOOL INFORMATION:\n\n- Call with a specific `tool` name when you want the full schema and current usageHint.\n- Omit `tool` to get an overview of all available tools.\n- Different `format` options give you different levels of detail.\n- Tip: use `tool_search` to find the right tool name, then `tool_help` for the full schema.\nThis tool is extremely valuable for self-correction when you are unsure about a tool's interface.",
|
|
9969
10769
|
permission: "auto",
|
|
9970
10770
|
mutating: false,
|
|
9971
10771
|
timeoutMs: 5e3,
|
|
@@ -10088,8 +10888,8 @@ function formatAllToolsMarkdown(tools) {
|
|
|
10088
10888
|
var toolSearchTool = {
|
|
10089
10889
|
name: "tool_search",
|
|
10090
10890
|
category: "Meta",
|
|
10091
|
-
description: "Search the catalog of available tools
|
|
10092
|
-
usageHint: "SELF-DISCOVERY TOOL:\n\n- Use when you need to find the right tool for a job.\n- `query` searches names and descriptions.\n- You can filter by `tags` (category), `permission`, or `mutating`.\nCall this before guessing tool names. It helps you discover the best tool for the current situation.",
|
|
10891
|
+
description: "Search the catalog of available tools by name or description. Use this to discover which tool to use for a task. For the full schema and usage details of a specific tool, use `tool_help` instead.",
|
|
10892
|
+
usageHint: "SELF-DISCOVERY TOOL:\n\n- Use when you need to find the right tool for a job.\n- `query` searches names and descriptions.\n- You can filter by `tags` (category), `permission`, or `mutating`.\n- Once you find the right tool name, use `tool_help` with that name for full schema details.\nCall this before guessing tool names. It helps you discover the best tool for the current situation.",
|
|
10093
10893
|
permission: "auto",
|
|
10094
10894
|
mutating: false,
|
|
10095
10895
|
timeoutMs: 1e3,
|
|
@@ -10127,9 +10927,9 @@ var toolSearchTool = {
|
|
|
10127
10927
|
async execute(input, ctx) {
|
|
10128
10928
|
const limit = Math.min(input.limit ?? 20, 100);
|
|
10129
10929
|
const tools = ctx.tools;
|
|
10130
|
-
const
|
|
10930
|
+
const query = input.query?.toLowerCase() ?? "";
|
|
10131
10931
|
const filtered = tools.filter((t) => {
|
|
10132
|
-
if (
|
|
10932
|
+
if (query && !t.name.toLowerCase().includes(query) && !t.description.toLowerCase().includes(query)) {
|
|
10133
10933
|
return false;
|
|
10134
10934
|
}
|
|
10135
10935
|
if (input.tags && input.tags.length > 0) {
|
|
@@ -10153,7 +10953,7 @@ var toolSearchTool = {
|
|
|
10153
10953
|
mutating: t.mutating
|
|
10154
10954
|
}));
|
|
10155
10955
|
const totalAvailable = tools.length;
|
|
10156
|
-
const hint = results.length === 0 &&
|
|
10956
|
+
const hint = results.length === 0 && query ? `No tools matched "${input.query}". Use tool-help (without arguments) to see all ${totalAvailable} available tools.` : void 0;
|
|
10157
10957
|
return {
|
|
10158
10958
|
tools: results,
|
|
10159
10959
|
total: filtered.length,
|
|
@@ -10526,8 +11326,18 @@ var writeTool = {
|
|
|
10526
11326
|
required: ["path", "content"]
|
|
10527
11327
|
},
|
|
10528
11328
|
async execute(input, ctx) {
|
|
10529
|
-
if (!input?.path)
|
|
10530
|
-
|
|
11329
|
+
if (!input?.path) {
|
|
11330
|
+
throw new ToolValidationError({
|
|
11331
|
+
message: "write: path is required",
|
|
11332
|
+
field: "path"
|
|
11333
|
+
});
|
|
11334
|
+
}
|
|
11335
|
+
if (input.content === void 0) {
|
|
11336
|
+
throw new ToolValidationError({
|
|
11337
|
+
message: "write: content is required",
|
|
11338
|
+
field: "content"
|
|
11339
|
+
});
|
|
11340
|
+
}
|
|
10531
11341
|
const absPath = await safeResolveReal(input.path, ctx);
|
|
10532
11342
|
let existed = false;
|
|
10533
11343
|
let prev = "";
|