@wrongstack/tools 0.275.1 → 0.276.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{background-indexer-BoTUw0EM.d.ts → background-indexer-BeDBxfSh.d.ts} +6 -0
- package/dist/builtin.js +1007 -204
- 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 +1041 -213
- 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 +396 -44
- 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 +1007 -204
- 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';
|
|
@@ -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.
|
|
@@ -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) {
|
|
@@ -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"]
|
|
@@ -5589,8 +5766,8 @@ ${numbered}`;
|
|
|
5589
5766
|
var documentTool = {
|
|
5590
5767
|
name: "document",
|
|
5591
5768
|
category: "Project",
|
|
5592
|
-
description: "
|
|
5593
|
-
usageHint: "
|
|
5769
|
+
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.",
|
|
5770
|
+
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
5771
|
permission: "auto",
|
|
5595
5772
|
mutating: false,
|
|
5596
5773
|
timeoutMs: 3e4,
|
|
@@ -5753,28 +5930,62 @@ var editTool = {
|
|
|
5753
5930
|
required: ["path", "old_string", "new_string"]
|
|
5754
5931
|
},
|
|
5755
5932
|
async execute(input, ctx) {
|
|
5756
|
-
if (!input?.path)
|
|
5757
|
-
|
|
5758
|
-
|
|
5759
|
-
if (input.old_string ===
|
|
5933
|
+
if (!input?.path) {
|
|
5934
|
+
throw new ToolValidationError({ message: "edit: path is required", field: "path" });
|
|
5935
|
+
}
|
|
5936
|
+
if (input.old_string === void 0) {
|
|
5937
|
+
throw new ToolValidationError({
|
|
5938
|
+
message: "edit: old_string is required",
|
|
5939
|
+
field: "old_string"
|
|
5940
|
+
});
|
|
5941
|
+
}
|
|
5942
|
+
if (input.new_string === void 0) {
|
|
5943
|
+
throw new ToolValidationError({
|
|
5944
|
+
message: "edit: new_string is required",
|
|
5945
|
+
field: "new_string"
|
|
5946
|
+
});
|
|
5947
|
+
}
|
|
5948
|
+
if (input.old_string === "") {
|
|
5949
|
+
throw new ToolValidationError({
|
|
5950
|
+
message: "edit: old_string cannot be empty",
|
|
5951
|
+
field: "old_string"
|
|
5952
|
+
});
|
|
5953
|
+
}
|
|
5760
5954
|
const absPath = await safeResolveReal(input.path, ctx);
|
|
5761
5955
|
const stat11 = await fs2.stat(absPath).catch((err) => {
|
|
5762
5956
|
if (err.code === "ENOENT") {
|
|
5763
|
-
throw new
|
|
5957
|
+
throw new ToolValidationError({
|
|
5958
|
+
message: `edit: file "${input.path}" does not exist. Use \`write\` instead.`,
|
|
5959
|
+
field: "path",
|
|
5960
|
+
context: { exists: false }
|
|
5961
|
+
});
|
|
5764
5962
|
}
|
|
5765
5963
|
throw err;
|
|
5766
5964
|
});
|
|
5767
|
-
if (!stat11.isFile())
|
|
5965
|
+
if (!stat11.isFile()) {
|
|
5966
|
+
throw new ToolValidationError({
|
|
5967
|
+
message: `edit: "${input.path}" is not a regular file`,
|
|
5968
|
+
field: "path"
|
|
5969
|
+
});
|
|
5970
|
+
}
|
|
5768
5971
|
const autoRead = !ctx.hasRead(absPath);
|
|
5769
5972
|
const original = await fs2.readFile(absPath, "utf8");
|
|
5770
5973
|
const updated = await fs2.stat(absPath);
|
|
5771
5974
|
const mtimeTolerance = process.platform === "win32" ? 2e3 : 1;
|
|
5772
5975
|
const lastReadMtime = ctx.lastReadMtime(absPath);
|
|
5773
5976
|
if (lastReadMtime !== void 0 && updated.mtimeMs > lastReadMtime + mtimeTolerance) {
|
|
5774
|
-
throw new
|
|
5977
|
+
throw new ToolValidationError({
|
|
5978
|
+
message: `edit: file "${input.path}" was modified externally. Re-read it first.`,
|
|
5979
|
+
field: "path",
|
|
5980
|
+
context: { reason: "external_modification" }
|
|
5981
|
+
});
|
|
5775
5982
|
}
|
|
5776
5983
|
if (autoRead && updated.mtimeMs > stat11.mtimeMs + mtimeTolerance) {
|
|
5777
|
-
throw new
|
|
5984
|
+
throw new ToolValidationError({
|
|
5985
|
+
message: `edit: file "${input.path}" changed while being auto-read. Retry the edit.`,
|
|
5986
|
+
field: "path",
|
|
5987
|
+
context: { reason: "auto_read_race" }
|
|
5988
|
+
});
|
|
5778
5989
|
}
|
|
5779
5990
|
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
5991
|
const style = detectNewlineStyle(original);
|
|
@@ -5800,15 +6011,18 @@ var editTool = {
|
|
|
5800
6011
|
}
|
|
5801
6012
|
if (count === 0) {
|
|
5802
6013
|
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
|
-
|
|
6014
|
+
throw new ToolValidationError({
|
|
6015
|
+
message: `edit: no match for old_string in "${input.path}".${hint ? ` Nearest match near line ${hint}.` : ""}`,
|
|
6016
|
+
field: "old_string"
|
|
6017
|
+
});
|
|
5806
6018
|
}
|
|
5807
6019
|
if (count > 1 && !input.replace_all) {
|
|
5808
6020
|
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
|
-
|
|
6021
|
+
throw new ToolValidationError({
|
|
6022
|
+
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.`,
|
|
6023
|
+
field: "old_string",
|
|
6024
|
+
context: { occurrences: count }
|
|
6025
|
+
});
|
|
5812
6026
|
}
|
|
5813
6027
|
const newFileLf = input.replace_all ? fileLf.split(oldLf).join(newLf) : fileLf.replace(oldLf, newLf);
|
|
5814
6028
|
const newFile = toStyle(newFileLf, style);
|
|
@@ -6327,10 +6541,16 @@ async function guardedFetch(url, maxRedirects, signal, headers = {
|
|
|
6327
6541
|
for (; ; ) {
|
|
6328
6542
|
const parsed = new URL(currentUrl);
|
|
6329
6543
|
if (parsed.protocol !== "https:" && parsed.protocol !== "http:") {
|
|
6330
|
-
throw new
|
|
6544
|
+
throw new ToolValidationError({
|
|
6545
|
+
message: `fetch: redirect to unsupported protocol "${parsed.protocol}"`,
|
|
6546
|
+
field: "url"
|
|
6547
|
+
});
|
|
6331
6548
|
}
|
|
6332
6549
|
if (parsed.protocol === "http:" && !ALLOW_PRIVATE) {
|
|
6333
|
-
throw new
|
|
6550
|
+
throw new ToolValidationError({
|
|
6551
|
+
message: "fetch: redirect to http:// blocked (HTTPS required by default)",
|
|
6552
|
+
field: "url"
|
|
6553
|
+
});
|
|
6334
6554
|
}
|
|
6335
6555
|
await assertNotPrivate(parsed.hostname);
|
|
6336
6556
|
const init = {
|
|
@@ -6345,11 +6565,19 @@ async function guardedFetch(url, maxRedirects, signal, headers = {
|
|
|
6345
6565
|
}
|
|
6346
6566
|
redirectCount++;
|
|
6347
6567
|
if (redirectCount > maxRedirects) {
|
|
6348
|
-
throw new
|
|
6568
|
+
throw new FetchError({
|
|
6569
|
+
message: `fetch: exceeded ${maxRedirects} redirects`,
|
|
6570
|
+
status: res.status,
|
|
6571
|
+
context: { url: currentUrl, maxRedirects, redirectCount }
|
|
6572
|
+
});
|
|
6349
6573
|
}
|
|
6350
6574
|
const location = res.headers.get("location");
|
|
6351
6575
|
if (!location) {
|
|
6352
|
-
throw new
|
|
6576
|
+
throw new FetchError({
|
|
6577
|
+
message: "fetch: redirect status with no location header",
|
|
6578
|
+
status: res.status,
|
|
6579
|
+
context: { url: currentUrl, redirectCount }
|
|
6580
|
+
});
|
|
6353
6581
|
}
|
|
6354
6582
|
currentUrl = new URL(location, currentUrl).toString();
|
|
6355
6583
|
}
|
|
@@ -6388,26 +6616,53 @@ var fetchTool = {
|
|
|
6388
6616
|
async execute(input, ctx, opts) {
|
|
6389
6617
|
let final;
|
|
6390
6618
|
const executeStream = fetchTool.executeStream;
|
|
6391
|
-
if (!executeStream)
|
|
6619
|
+
if (!executeStream) {
|
|
6620
|
+
throw new ToolError({
|
|
6621
|
+
message: "fetchTool: stream execution unavailable",
|
|
6622
|
+
code: "TOOL_EXECUTION_FAILED",
|
|
6623
|
+
toolName: "fetch"
|
|
6624
|
+
});
|
|
6625
|
+
}
|
|
6392
6626
|
for await (const ev of executeStream(input, ctx, opts)) {
|
|
6393
6627
|
if (ev.type === "final") final = ev.output;
|
|
6394
6628
|
}
|
|
6395
|
-
if (!final)
|
|
6629
|
+
if (!final) {
|
|
6630
|
+
throw new ToolError({
|
|
6631
|
+
message: "fetch: stream ended without final event",
|
|
6632
|
+
code: "TOOL_EXECUTION_FAILED",
|
|
6633
|
+
toolName: "fetch"
|
|
6634
|
+
});
|
|
6635
|
+
}
|
|
6396
6636
|
return final;
|
|
6397
6637
|
},
|
|
6398
6638
|
async *executeStream(input, ctx, opts) {
|
|
6399
|
-
if (!input?.url)
|
|
6639
|
+
if (!input?.url) {
|
|
6640
|
+
throw new ToolValidationError({
|
|
6641
|
+
message: "fetch: url is required",
|
|
6642
|
+
field: "url"
|
|
6643
|
+
});
|
|
6644
|
+
}
|
|
6400
6645
|
const u = new URL(input.url);
|
|
6401
6646
|
if (u.protocol !== "https:" && u.protocol !== "http:") {
|
|
6402
|
-
throw new
|
|
6647
|
+
throw new ToolValidationError({
|
|
6648
|
+
message: `fetch: unsupported protocol "${u.protocol}"`,
|
|
6649
|
+
field: "url"
|
|
6650
|
+
});
|
|
6403
6651
|
}
|
|
6404
6652
|
if (u.protocol === "http:" && !ALLOW_PRIVATE) {
|
|
6405
|
-
throw new
|
|
6653
|
+
throw new ToolValidationError({
|
|
6654
|
+
message: "fetch: http:// blocked (HTTPS required by default)",
|
|
6655
|
+
field: "url"
|
|
6656
|
+
});
|
|
6406
6657
|
}
|
|
6407
6658
|
await assertNotPrivate(u.hostname);
|
|
6408
6659
|
yield { type: "log", text: `GET ${input.url}` };
|
|
6409
6660
|
const ctrl = new AbortController();
|
|
6410
|
-
const timer = setTimeout(() => ctrl.abort(new
|
|
6661
|
+
const timer = setTimeout(() => ctrl.abort(new ToolError({
|
|
6662
|
+
message: "fetch timeout",
|
|
6663
|
+
code: "TOOL_TIMEOUT",
|
|
6664
|
+
toolName: "fetch"
|
|
6665
|
+
})), TIMEOUT_MS);
|
|
6411
6666
|
const combined = combineSignals([opts.signal, ctrl.signal]);
|
|
6412
6667
|
try {
|
|
6413
6668
|
let res;
|
|
@@ -6419,7 +6674,11 @@ var fetchTool = {
|
|
|
6419
6674
|
}
|
|
6420
6675
|
const ct = res.headers.get("content-type") ?? "application/octet-stream";
|
|
6421
6676
|
if (/^image\/|^audio\/|^video\/|application\/octet-stream/.test(ct)) {
|
|
6422
|
-
throw new
|
|
6677
|
+
throw new FetchError({
|
|
6678
|
+
message: `fetch: refusing to read binary content-type "${ct}"`,
|
|
6679
|
+
status: res.status,
|
|
6680
|
+
context: { url: res.url, contentType: ct }
|
|
6681
|
+
});
|
|
6423
6682
|
}
|
|
6424
6683
|
yield {
|
|
6425
6684
|
type: "log",
|
|
@@ -6484,16 +6743,25 @@ async function assertNotPrivate(hostname2) {
|
|
|
6484
6743
|
if (ALLOW_PRIVATE) return;
|
|
6485
6744
|
const host = hostname2.startsWith("[") && hostname2.endsWith("]") ? hostname2.slice(1, -1) : hostname2;
|
|
6486
6745
|
if (host === "localhost" || host.endsWith(".localhost")) {
|
|
6487
|
-
throw new
|
|
6746
|
+
throw new ToolValidationError({
|
|
6747
|
+
message: "fetch: blocked localhost target",
|
|
6748
|
+
field: "url"
|
|
6749
|
+
});
|
|
6488
6750
|
}
|
|
6489
6751
|
const ipVersion = net.isIP(host);
|
|
6490
6752
|
if (ipVersion === 4) {
|
|
6491
6753
|
if (isPrivateIPv4(host)) {
|
|
6492
|
-
throw new
|
|
6754
|
+
throw new ToolValidationError({
|
|
6755
|
+
message: `fetch: blocked private/loopback address "${host}"`,
|
|
6756
|
+
field: "url"
|
|
6757
|
+
});
|
|
6493
6758
|
}
|
|
6494
6759
|
} else if (ipVersion === 6) {
|
|
6495
6760
|
if (isPrivateIPv6(host)) {
|
|
6496
|
-
throw new
|
|
6761
|
+
throw new ToolValidationError({
|
|
6762
|
+
message: `fetch: blocked private/loopback address "${host}"`,
|
|
6763
|
+
field: "url"
|
|
6764
|
+
});
|
|
6497
6765
|
}
|
|
6498
6766
|
} else {
|
|
6499
6767
|
try {
|
|
@@ -6501,7 +6769,10 @@ async function assertNotPrivate(hostname2) {
|
|
|
6501
6769
|
for (const r of records) {
|
|
6502
6770
|
const bad = r.family === 4 ? isPrivateIPv4(r.address) : isPrivateIPv6(r.address);
|
|
6503
6771
|
if (bad) {
|
|
6504
|
-
throw new
|
|
6772
|
+
throw new ToolValidationError({
|
|
6773
|
+
message: `fetch: resolved to private address ${r.address}`,
|
|
6774
|
+
field: "url"
|
|
6775
|
+
});
|
|
6505
6776
|
}
|
|
6506
6777
|
}
|
|
6507
6778
|
} catch (err) {
|
|
@@ -6511,7 +6782,13 @@ async function assertNotPrivate(hostname2) {
|
|
|
6511
6782
|
}
|
|
6512
6783
|
function describeFetchError(err, url, timedOut) {
|
|
6513
6784
|
if (timedOut) {
|
|
6514
|
-
return new
|
|
6785
|
+
return new ToolError({
|
|
6786
|
+
message: `fetch: GET ${url} timed out after ${TIMEOUT_MS}ms`,
|
|
6787
|
+
code: "TOOL_TIMEOUT",
|
|
6788
|
+
toolName: "fetch",
|
|
6789
|
+
context: { url, timedOut: true, timeoutMs: TIMEOUT_MS },
|
|
6790
|
+
cause: err
|
|
6791
|
+
});
|
|
6515
6792
|
}
|
|
6516
6793
|
const parts = [];
|
|
6517
6794
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -6524,7 +6801,15 @@ function describeFetchError(err, url, timedOut) {
|
|
|
6524
6801
|
cur = cur.cause;
|
|
6525
6802
|
}
|
|
6526
6803
|
const detail = parts.length > 0 ? parts.join(" \u2192 ") : "fetch failed";
|
|
6527
|
-
return new
|
|
6804
|
+
return new FetchError({
|
|
6805
|
+
message: `fetch: GET ${url} failed \u2014 ${detail}`,
|
|
6806
|
+
status: 502,
|
|
6807
|
+
context: { url, timedOut: false, transportErrors: parts },
|
|
6808
|
+
// Preserve the original undici / DNS / TLS chain so callers can inspect
|
|
6809
|
+
// it via `err.cause` and structured `instanceof` checks. The flattened
|
|
6810
|
+
// text version stays in the message for human readability.
|
|
6811
|
+
cause: err
|
|
6812
|
+
});
|
|
6528
6813
|
}
|
|
6529
6814
|
function prettyJson(s) {
|
|
6530
6815
|
try {
|
|
@@ -6964,7 +7249,7 @@ var globTool = {
|
|
|
6964
7249
|
},
|
|
6965
7250
|
async execute(input, ctx) {
|
|
6966
7251
|
if (!input?.pattern) throw new Error("glob: pattern is required");
|
|
6967
|
-
const base = input.path ?
|
|
7252
|
+
const base = input.path ? await safeResolveReal(input.path, ctx) : ctx.cwd;
|
|
6968
7253
|
const limit = Math.max(1, Math.min(input.limit ?? 1e3, 5e3));
|
|
6969
7254
|
const ignored = await readGitignore(base);
|
|
6970
7255
|
const re = compileGlob(input.pattern);
|
|
@@ -7019,8 +7304,12 @@ var globTool = {
|
|
|
7019
7304
|
try {
|
|
7020
7305
|
const st = await fs2.stat(full);
|
|
7021
7306
|
if (st.isDirectory()) {
|
|
7307
|
+
const real = await fs2.realpath(full);
|
|
7308
|
+
await assertRealInsideRoot(real, ctx);
|
|
7022
7309
|
subdirs.push({ full, rel });
|
|
7023
7310
|
} else if (st.isFile()) {
|
|
7311
|
+
const real = await fs2.realpath(full);
|
|
7312
|
+
await assertRealInsideRoot(real, ctx);
|
|
7024
7313
|
re.lastIndex = 0;
|
|
7025
7314
|
const relMatch = re.test(rel);
|
|
7026
7315
|
re.lastIndex = 0;
|
|
@@ -7158,13 +7447,21 @@ var grepTool = {
|
|
|
7158
7447
|
return final;
|
|
7159
7448
|
},
|
|
7160
7449
|
async *executeStream(input, ctx, opts) {
|
|
7161
|
-
if (!input?.pattern)
|
|
7450
|
+
if (!input?.pattern) {
|
|
7451
|
+
throw new ToolValidationError({
|
|
7452
|
+
message: "grep: pattern is required",
|
|
7453
|
+
field: "pattern"
|
|
7454
|
+
});
|
|
7455
|
+
}
|
|
7162
7456
|
const base = input.path ? safeResolve(input.path, ctx) : ctx.cwd;
|
|
7163
7457
|
const mode = input.output_mode ?? "content";
|
|
7164
7458
|
const limit = Math.max(1, Math.min(input.limit ?? 200, 2e3));
|
|
7165
7459
|
const validation = compileUserRegex(input.pattern, input.case_insensitive ? "i" : "");
|
|
7166
7460
|
if (!validation.ok) {
|
|
7167
|
-
throw new
|
|
7461
|
+
throw new ToolValidationError({
|
|
7462
|
+
message: `grep: ${validation.reason}`,
|
|
7463
|
+
field: "pattern"
|
|
7464
|
+
});
|
|
7168
7465
|
}
|
|
7169
7466
|
const rgAvailable = await detectRg(opts.signal);
|
|
7170
7467
|
if (rgAvailable) {
|
|
@@ -7320,7 +7617,10 @@ async function runNative(input, base, mode, limit, signal) {
|
|
|
7320
7617
|
const flags = input.case_insensitive ? "i" : "";
|
|
7321
7618
|
const compiled = compileUserRegex(input.pattern, flags);
|
|
7322
7619
|
if (!compiled.ok) {
|
|
7323
|
-
throw new
|
|
7620
|
+
throw new ToolValidationError({
|
|
7621
|
+
message: `grep: ${compiled.reason}`,
|
|
7622
|
+
field: "pattern"
|
|
7623
|
+
});
|
|
7324
7624
|
}
|
|
7325
7625
|
const re = compiled.regex;
|
|
7326
7626
|
const globRe = input.glob ? compileGlob(input.glob) : null;
|
|
@@ -7477,6 +7777,10 @@ var installTool = {
|
|
|
7477
7777
|
global: {
|
|
7478
7778
|
type: "boolean",
|
|
7479
7779
|
description: "Whether to perform a global install (use with caution)."
|
|
7780
|
+
},
|
|
7781
|
+
lifecycleScripts: {
|
|
7782
|
+
type: "boolean",
|
|
7783
|
+
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
7784
|
}
|
|
7481
7785
|
}
|
|
7482
7786
|
},
|
|
@@ -7496,8 +7800,10 @@ var installTool = {
|
|
|
7496
7800
|
yield { type: "log", text: `Resolving with ${pkgManager}\u2026`, data: { phase: "resolve" } };
|
|
7497
7801
|
const save = input.save === "dev" ? "-D" : input.save === "optional" ? "-O" : "";
|
|
7498
7802
|
const globalFlag = input.global ? ["-g"] : [];
|
|
7803
|
+
const ignoreScripts = input.lifecycleScripts !== true;
|
|
7499
7804
|
const args = [];
|
|
7500
7805
|
if (input.dry_run) args.push("--dry-run");
|
|
7806
|
+
if (ignoreScripts) args.push("--ignore-scripts");
|
|
7501
7807
|
if (pkgManager === "pnpm") {
|
|
7502
7808
|
if (save) args.push(save);
|
|
7503
7809
|
args.push("add", ...globalFlag);
|
|
@@ -7592,8 +7898,8 @@ function resolveManifestPath(cwd, pkgManager) {
|
|
|
7592
7898
|
var jsonTool = {
|
|
7593
7899
|
name: "json",
|
|
7594
7900
|
category: "Data",
|
|
7595
|
-
description: "Parse, pretty-print, query,
|
|
7596
|
-
usageHint:
|
|
7901
|
+
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.",
|
|
7902
|
+
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
7903
|
permission: "auto",
|
|
7598
7904
|
mutating: false,
|
|
7599
7905
|
timeoutMs: 5e3,
|
|
@@ -7602,69 +7908,420 @@ var jsonTool = {
|
|
|
7602
7908
|
inputSchema: {
|
|
7603
7909
|
type: "object",
|
|
7604
7910
|
properties: {
|
|
7605
|
-
|
|
7606
|
-
data: { type: "string", description: "JSON/JSON5/YAML string (alternative to file)" },
|
|
7607
|
-
query: {
|
|
7911
|
+
action: {
|
|
7608
7912
|
type: "string",
|
|
7609
|
-
|
|
7913
|
+
enum: ["parse", "query", "validate", "transform", "merge"],
|
|
7914
|
+
description: "Operation (default: parse). parse=read/pretty-print, query=JMESPath, validate=schema, transform=chained queries, merge=deep merge."
|
|
7610
7915
|
},
|
|
7916
|
+
file: { type: "string", description: "Path to JSON/JSON5/YAML file (parse/query/validate)" },
|
|
7917
|
+
data: { type: "string", description: "JSON/JSON5/YAML string (parse/query/validate, alternative to file)" },
|
|
7611
7918
|
format: {
|
|
7612
7919
|
type: "string",
|
|
7613
7920
|
enum: ["json", "json5", "yaml"],
|
|
7614
|
-
description: "Output format (default: json)"
|
|
7921
|
+
description: "Output format for parse/query/transform (default: json)"
|
|
7922
|
+
},
|
|
7923
|
+
query: {
|
|
7924
|
+
type: "string",
|
|
7925
|
+
description: "JMESPath-like query expression (query action)"
|
|
7926
|
+
},
|
|
7927
|
+
transforms: {
|
|
7928
|
+
type: "array",
|
|
7929
|
+
items: { type: "string" },
|
|
7930
|
+
description: "Ordered JMESPath query strings (transform action)"
|
|
7931
|
+
},
|
|
7932
|
+
schema: {
|
|
7933
|
+
type: "object",
|
|
7934
|
+
description: "JSON Schema to validate against (validate action)"
|
|
7935
|
+
},
|
|
7936
|
+
base: { description: "Base JSON object (merge action)" },
|
|
7937
|
+
patch: { description: "Patch JSON object to merge in (merge action)" },
|
|
7938
|
+
conflictResolution: {
|
|
7939
|
+
type: "string",
|
|
7940
|
+
enum: ["prefer-base", "prefer-patch"],
|
|
7941
|
+
description: "Merge conflict resolution (default: prefer-patch)"
|
|
7615
7942
|
},
|
|
7616
7943
|
validate: {
|
|
7617
7944
|
type: "boolean",
|
|
7618
|
-
description: "Validate syntax only, no output (default: false)"
|
|
7945
|
+
description: "Validate syntax only, no output (parse action, default: false)"
|
|
7619
7946
|
}
|
|
7620
7947
|
}
|
|
7621
7948
|
},
|
|
7622
7949
|
async execute(input) {
|
|
7623
|
-
const
|
|
7624
|
-
|
|
7625
|
-
|
|
7626
|
-
|
|
7627
|
-
|
|
7628
|
-
|
|
7629
|
-
|
|
7630
|
-
return
|
|
7631
|
-
|
|
7632
|
-
|
|
7633
|
-
|
|
7634
|
-
|
|
7635
|
-
|
|
7950
|
+
const action = input.action ?? "parse";
|
|
7951
|
+
switch (action) {
|
|
7952
|
+
case "query":
|
|
7953
|
+
return executeQuery(input);
|
|
7954
|
+
case "validate":
|
|
7955
|
+
return executeValidate(input);
|
|
7956
|
+
case "transform":
|
|
7957
|
+
return executeTransform(input);
|
|
7958
|
+
case "merge":
|
|
7959
|
+
return executeMerge(input);
|
|
7960
|
+
case "parse":
|
|
7961
|
+
default:
|
|
7962
|
+
return executeParse(input);
|
|
7636
7963
|
}
|
|
7964
|
+
}
|
|
7965
|
+
};
|
|
7966
|
+
async function executeParse(input) {
|
|
7967
|
+
const format = input.format ?? "json";
|
|
7968
|
+
let parsed;
|
|
7969
|
+
let raw;
|
|
7970
|
+
if (input.file) {
|
|
7637
7971
|
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
|
-
};
|
|
7972
|
+
raw = await fs2.readFile(input.file, "utf8");
|
|
7973
|
+
} catch {
|
|
7974
|
+
return { data: null, formatted: "", type: "unknown", action: "parse", error: "Could not read file" };
|
|
7655
7975
|
}
|
|
7656
|
-
|
|
7657
|
-
|
|
7976
|
+
} else if (input.data) {
|
|
7977
|
+
raw = input.data;
|
|
7978
|
+
} else {
|
|
7979
|
+
return { data: null, formatted: "", type: "unknown", action: "parse", error: "Provide file or data" };
|
|
7980
|
+
}
|
|
7981
|
+
try {
|
|
7982
|
+
parsed = JSON.parse(raw);
|
|
7983
|
+
} catch (e) {
|
|
7984
|
+
return {
|
|
7985
|
+
data: null,
|
|
7986
|
+
formatted: "",
|
|
7987
|
+
type: "unknown",
|
|
7988
|
+
action: "parse",
|
|
7989
|
+
/* v8 ignore next -- JSON.parse only throws SyntaxError (an Error); the String(e) side is defensive. */
|
|
7990
|
+
error: `Parse failed: ${e instanceof Error ? e.message : String(e)}`
|
|
7991
|
+
};
|
|
7992
|
+
}
|
|
7993
|
+
if (input.validate) {
|
|
7658
7994
|
return {
|
|
7659
7995
|
data: parsed,
|
|
7660
|
-
formatted,
|
|
7996
|
+
formatted: "valid",
|
|
7661
7997
|
type: Array.isArray(parsed) ? "array" : typeof parsed,
|
|
7998
|
+
action: "parse",
|
|
7999
|
+
keys: typeof parsed === "object" && parsed !== null ? Object.keys(parsed) : void 0
|
|
8000
|
+
};
|
|
8001
|
+
}
|
|
8002
|
+
if (input.query) {
|
|
8003
|
+
const queryResult = simpleQuery(parsed, input.query);
|
|
8004
|
+
const formatted2 = formatOutput(queryResult, format);
|
|
8005
|
+
return {
|
|
8006
|
+
data: parsed,
|
|
8007
|
+
formatted: formatted2,
|
|
8008
|
+
type: Array.isArray(parsed) ? "array" : typeof parsed,
|
|
8009
|
+
action: "parse",
|
|
7662
8010
|
keys: typeof parsed === "object" && parsed !== null ? Object.keys(parsed) : void 0,
|
|
7663
8011
|
query_result: queryResult
|
|
7664
8012
|
};
|
|
7665
8013
|
}
|
|
7666
|
-
|
|
7667
|
-
|
|
8014
|
+
const formatted = formatOutput(parsed, format);
|
|
8015
|
+
return {
|
|
8016
|
+
data: parsed,
|
|
8017
|
+
formatted,
|
|
8018
|
+
type: Array.isArray(parsed) ? "array" : typeof parsed,
|
|
8019
|
+
action: "parse",
|
|
8020
|
+
keys: typeof parsed === "object" && parsed !== null ? Object.keys(parsed) : void 0
|
|
8021
|
+
};
|
|
8022
|
+
}
|
|
8023
|
+
async function executeQuery(input) {
|
|
8024
|
+
if (!input.query) {
|
|
8025
|
+
return { data: null, formatted: "", type: "unknown", action: "query", error: "query is required for action: query" };
|
|
8026
|
+
}
|
|
8027
|
+
let parsed;
|
|
8028
|
+
if (input.file) {
|
|
8029
|
+
try {
|
|
8030
|
+
const raw = await fs2.readFile(input.file, "utf8");
|
|
8031
|
+
parsed = JSON.parse(raw);
|
|
8032
|
+
} catch {
|
|
8033
|
+
return { data: null, formatted: "", type: "unknown", action: "query", error: "Could not read/parse file" };
|
|
8034
|
+
}
|
|
8035
|
+
} else if (input.data) {
|
|
8036
|
+
try {
|
|
8037
|
+
parsed = JSON.parse(input.data);
|
|
8038
|
+
} catch {
|
|
8039
|
+
return { data: null, formatted: "", type: "unknown", action: "query", error: "Could not parse data string" };
|
|
8040
|
+
}
|
|
8041
|
+
} else {
|
|
8042
|
+
return { data: null, formatted: "", type: "unknown", action: "query", error: "Provide file or data" };
|
|
8043
|
+
}
|
|
8044
|
+
try {
|
|
8045
|
+
const result = jmespathSearch(parsed, input.query);
|
|
8046
|
+
const format = input.format ?? "json";
|
|
8047
|
+
return {
|
|
8048
|
+
data: parsed,
|
|
8049
|
+
formatted: formatOutput(result, format),
|
|
8050
|
+
type: result === null ? "null" : Array.isArray(result) ? "array" : typeof result,
|
|
8051
|
+
action: "query",
|
|
8052
|
+
query_result: result
|
|
8053
|
+
};
|
|
8054
|
+
} catch (e) {
|
|
8055
|
+
return {
|
|
8056
|
+
data: null,
|
|
8057
|
+
formatted: "",
|
|
8058
|
+
type: "unknown",
|
|
8059
|
+
action: "query",
|
|
8060
|
+
/* v8 ignore next -- defensive String(e) */
|
|
8061
|
+
error: `Query failed: ${e instanceof Error ? e.message : String(e)}`
|
|
8062
|
+
};
|
|
8063
|
+
}
|
|
8064
|
+
}
|
|
8065
|
+
async function executeValidate(input) {
|
|
8066
|
+
if (!input.schema) {
|
|
8067
|
+
return { data: null, formatted: "", type: "unknown", action: "validate", error: "schema is required for action: validate" };
|
|
8068
|
+
}
|
|
8069
|
+
let parsed;
|
|
8070
|
+
if (input.file) {
|
|
8071
|
+
try {
|
|
8072
|
+
const raw = await fs2.readFile(input.file, "utf8");
|
|
8073
|
+
parsed = JSON.parse(raw);
|
|
8074
|
+
} catch {
|
|
8075
|
+
return { data: null, formatted: "", type: "unknown", action: "validate", error: "Could not read/parse file" };
|
|
8076
|
+
}
|
|
8077
|
+
} else if (input.data) {
|
|
8078
|
+
try {
|
|
8079
|
+
parsed = JSON.parse(input.data);
|
|
8080
|
+
} catch {
|
|
8081
|
+
return { data: null, formatted: "", type: "unknown", action: "validate", error: "Could not parse data string" };
|
|
8082
|
+
}
|
|
8083
|
+
} else {
|
|
8084
|
+
return { data: null, formatted: "", type: "unknown", action: "validate", error: "Provide file or data" };
|
|
8085
|
+
}
|
|
8086
|
+
try {
|
|
8087
|
+
const { valid, errors } = validateJsonSchema(parsed, input.schema);
|
|
8088
|
+
return {
|
|
8089
|
+
data: parsed,
|
|
8090
|
+
formatted: valid ? "valid" : "invalid",
|
|
8091
|
+
type: Array.isArray(parsed) ? "array" : typeof parsed,
|
|
8092
|
+
action: "validate",
|
|
8093
|
+
valid,
|
|
8094
|
+
errors
|
|
8095
|
+
};
|
|
8096
|
+
} catch (e) {
|
|
8097
|
+
return {
|
|
8098
|
+
data: null,
|
|
8099
|
+
formatted: "",
|
|
8100
|
+
type: "unknown",
|
|
8101
|
+
action: "validate",
|
|
8102
|
+
/* v8 ignore next -- defensive String(e) */
|
|
8103
|
+
error: `Validation failed: ${e instanceof Error ? e.message : String(e)}`
|
|
8104
|
+
};
|
|
8105
|
+
}
|
|
8106
|
+
}
|
|
8107
|
+
async function executeTransform(input) {
|
|
8108
|
+
if (!input.transforms || input.transforms.length === 0) {
|
|
8109
|
+
return { data: null, formatted: "", type: "unknown", action: "transform", error: "transforms array is required for action: transform" };
|
|
8110
|
+
}
|
|
8111
|
+
let parsed;
|
|
8112
|
+
if (input.file) {
|
|
8113
|
+
try {
|
|
8114
|
+
const raw = await fs2.readFile(input.file, "utf8");
|
|
8115
|
+
parsed = JSON.parse(raw);
|
|
8116
|
+
} catch {
|
|
8117
|
+
return { data: null, formatted: "", type: "unknown", action: "transform", error: "Could not read/parse file" };
|
|
8118
|
+
}
|
|
8119
|
+
} else if (input.data) {
|
|
8120
|
+
try {
|
|
8121
|
+
parsed = JSON.parse(input.data);
|
|
8122
|
+
} catch {
|
|
8123
|
+
return { data: null, formatted: "", type: "unknown", action: "transform", error: "Could not parse data string" };
|
|
8124
|
+
}
|
|
8125
|
+
} else {
|
|
8126
|
+
return { data: null, formatted: "", type: "unknown", action: "transform", error: "Provide file or data" };
|
|
8127
|
+
}
|
|
8128
|
+
try {
|
|
8129
|
+
let current = parsed;
|
|
8130
|
+
const steps = [];
|
|
8131
|
+
for (const t of input.transforms) {
|
|
8132
|
+
current = jmespathSearch(current, t);
|
|
8133
|
+
steps.push({ transform: t, result: current });
|
|
8134
|
+
}
|
|
8135
|
+
const format = input.format ?? "json";
|
|
8136
|
+
return {
|
|
8137
|
+
data: parsed,
|
|
8138
|
+
formatted: formatOutput(current, format),
|
|
8139
|
+
type: current === null ? "null" : Array.isArray(current) ? "array" : typeof current,
|
|
8140
|
+
action: "transform",
|
|
8141
|
+
result: current,
|
|
8142
|
+
steps
|
|
8143
|
+
};
|
|
8144
|
+
} catch (e) {
|
|
8145
|
+
return {
|
|
8146
|
+
data: null,
|
|
8147
|
+
formatted: "",
|
|
8148
|
+
type: "unknown",
|
|
8149
|
+
action: "transform",
|
|
8150
|
+
/* v8 ignore next -- defensive String(e) */
|
|
8151
|
+
error: `Transform failed: ${e instanceof Error ? e.message : String(e)}`
|
|
8152
|
+
};
|
|
8153
|
+
}
|
|
8154
|
+
}
|
|
8155
|
+
async function executeMerge(input) {
|
|
8156
|
+
if (input.base === void 0 || input.patch === void 0) {
|
|
8157
|
+
return { data: null, formatted: "", type: "unknown", action: "merge", error: "base and patch are required for action: merge" };
|
|
8158
|
+
}
|
|
8159
|
+
const conflictResolution = input.conflictResolution ?? "prefer-patch";
|
|
8160
|
+
try {
|
|
8161
|
+
const result = deepMerge(input.base, input.patch, { conflictResolution });
|
|
8162
|
+
const format = input.format ?? "json";
|
|
8163
|
+
return {
|
|
8164
|
+
data: result,
|
|
8165
|
+
formatted: formatOutput(result, format),
|
|
8166
|
+
type: result === null ? "null" : Array.isArray(result) ? "array" : typeof result,
|
|
8167
|
+
action: "merge",
|
|
8168
|
+
result
|
|
8169
|
+
};
|
|
8170
|
+
} catch (e) {
|
|
8171
|
+
return {
|
|
8172
|
+
data: null,
|
|
8173
|
+
formatted: "",
|
|
8174
|
+
type: "unknown",
|
|
8175
|
+
action: "merge",
|
|
8176
|
+
/* v8 ignore next -- defensive String(e) */
|
|
8177
|
+
error: `Merge failed: ${e instanceof Error ? e.message : String(e)}`
|
|
8178
|
+
};
|
|
8179
|
+
}
|
|
8180
|
+
}
|
|
8181
|
+
function jmespathSearch(data, query) {
|
|
8182
|
+
if (!query || query === "@") return data;
|
|
8183
|
+
if (query === "$") return data;
|
|
8184
|
+
const dotMatch = query.match(/^([a-zA-Z_][a-zA-Z0-9_]*)(?:\.(.+))?$/);
|
|
8185
|
+
if (dotMatch) {
|
|
8186
|
+
const key = dotMatch[1];
|
|
8187
|
+
const rest = dotMatch[2];
|
|
8188
|
+
const val = data?.[key];
|
|
8189
|
+
if (rest === void 0) return val;
|
|
8190
|
+
return jmespathSearch(val, rest);
|
|
8191
|
+
}
|
|
8192
|
+
const arrMatch = query.match(/^\[(\d+)\](?:\.(.+))?$/);
|
|
8193
|
+
if (arrMatch) {
|
|
8194
|
+
const idx = Number.parseInt(arrMatch[1], 10);
|
|
8195
|
+
const rest = arrMatch[2];
|
|
8196
|
+
const arr = data;
|
|
8197
|
+
const val = arr?.[idx];
|
|
8198
|
+
if (rest === void 0) return val;
|
|
8199
|
+
return jmespathSearch(val, rest);
|
|
8200
|
+
}
|
|
8201
|
+
if (query === "[*]") {
|
|
8202
|
+
if (Array.isArray(data)) {
|
|
8203
|
+
return data;
|
|
8204
|
+
}
|
|
8205
|
+
return data;
|
|
8206
|
+
}
|
|
8207
|
+
const multiMatch = query.match(/^([a-zA-Z_][a-zA-Z0-9_]*)\[\*\](?:\.(.+))?$/);
|
|
8208
|
+
if (multiMatch) {
|
|
8209
|
+
const key = multiMatch[1];
|
|
8210
|
+
const rest = multiMatch[2];
|
|
8211
|
+
const arr = data?.[key];
|
|
8212
|
+
if (!Array.isArray(arr)) return [];
|
|
8213
|
+
if (rest === void 0) return arr;
|
|
8214
|
+
return arr.map((item) => jmespathSearch(item, rest));
|
|
8215
|
+
}
|
|
8216
|
+
const filterMatch = query.match(/^\[\\?([a-zA-Z_][a-zA-Z0-9_]*)(==|!=|<|>|<=|>=)(`[^`]+`|'[^']*')\](?:\.(.+))?$/);
|
|
8217
|
+
if (filterMatch) {
|
|
8218
|
+
const field = filterMatch[1];
|
|
8219
|
+
const op = filterMatch[2];
|
|
8220
|
+
const rawVal = filterMatch[3];
|
|
8221
|
+
const rest = filterMatch[4];
|
|
8222
|
+
const cmpVal = JSON.parse(rawVal.slice(1, -1));
|
|
8223
|
+
const arr = data;
|
|
8224
|
+
if (!Array.isArray(arr)) return [];
|
|
8225
|
+
const filtered = arr.filter((item) => {
|
|
8226
|
+
const itemVal = item[field];
|
|
8227
|
+
switch (op) {
|
|
8228
|
+
case "==":
|
|
8229
|
+
return itemVal === cmpVal;
|
|
8230
|
+
case "!=":
|
|
8231
|
+
return itemVal !== cmpVal;
|
|
8232
|
+
case ">":
|
|
8233
|
+
return Number(itemVal) > Number(cmpVal);
|
|
8234
|
+
case "<":
|
|
8235
|
+
return Number(itemVal) < Number(cmpVal);
|
|
8236
|
+
case ">=":
|
|
8237
|
+
return Number(itemVal) >= Number(cmpVal);
|
|
8238
|
+
case "<=":
|
|
8239
|
+
return Number(itemVal) <= Number(cmpVal);
|
|
8240
|
+
/* v8 ignore next -- op is constrained to the six operators by the filter regex; default is unreachable. */
|
|
8241
|
+
default:
|
|
8242
|
+
return true;
|
|
8243
|
+
}
|
|
8244
|
+
});
|
|
8245
|
+
if (rest === void 0) return filtered;
|
|
8246
|
+
return filtered.map((item) => jmespathSearch(item, rest));
|
|
8247
|
+
}
|
|
8248
|
+
const fnMatch = query.match(/^(length|keys|values|type)\(@\)$/);
|
|
8249
|
+
if (fnMatch) {
|
|
8250
|
+
const fn = fnMatch[1];
|
|
8251
|
+
switch (fn) {
|
|
8252
|
+
case "length":
|
|
8253
|
+
if (Array.isArray(data)) return data.length;
|
|
8254
|
+
if (typeof data === "string") return data.length;
|
|
8255
|
+
if (typeof data === "object" && data !== null) return Object.keys(data).length;
|
|
8256
|
+
return 0;
|
|
8257
|
+
case "keys":
|
|
8258
|
+
if (typeof data === "object" && data !== null && !Array.isArray(data)) return Object.keys(data);
|
|
8259
|
+
return [];
|
|
8260
|
+
case "values":
|
|
8261
|
+
if (typeof data === "object" && data !== null && !Array.isArray(data)) return Object.values(data);
|
|
8262
|
+
return [];
|
|
8263
|
+
case "type":
|
|
8264
|
+
if (data === null) return "null";
|
|
8265
|
+
if (Array.isArray(data)) return "array";
|
|
8266
|
+
return typeof data;
|
|
8267
|
+
/* v8 ignore next 2 -- fn is constrained to the four names by the function regex; default is unreachable. */
|
|
8268
|
+
default:
|
|
8269
|
+
return null;
|
|
8270
|
+
}
|
|
8271
|
+
}
|
|
8272
|
+
return null;
|
|
8273
|
+
}
|
|
8274
|
+
function validateJsonSchema(data, schema) {
|
|
8275
|
+
const errors = [];
|
|
8276
|
+
function check(value, s, path22) {
|
|
8277
|
+
if (s["type"]) {
|
|
8278
|
+
const expectedType = s["type"];
|
|
8279
|
+
const actualType = Array.isArray(value) ? "array" : value === null ? "null" : typeof value;
|
|
8280
|
+
if (expectedType === "integer") {
|
|
8281
|
+
if (!Number.isInteger(value)) errors.push(`${path22}: expected integer, got ${actualType}`);
|
|
8282
|
+
} else if (expectedType !== actualType) {
|
|
8283
|
+
errors.push(`${path22}: expected ${expectedType}, got ${actualType}`);
|
|
8284
|
+
}
|
|
8285
|
+
}
|
|
8286
|
+
if (typeof value === "string" && s["format"] === "uri" && value) {
|
|
8287
|
+
try {
|
|
8288
|
+
new URL(value);
|
|
8289
|
+
} catch {
|
|
8290
|
+
errors.push(`${path22}: not a valid URI`);
|
|
8291
|
+
}
|
|
8292
|
+
}
|
|
8293
|
+
if (typeof value === "string" && s["pattern"]) {
|
|
8294
|
+
const re = new RegExp(s["pattern"]);
|
|
8295
|
+
if (!re.test(value)) errors.push(`${path22}: does not match pattern ${s["pattern"]}`);
|
|
8296
|
+
}
|
|
8297
|
+
if (typeof value === "string" && s["minLength"] !== void 0 && value.length < s["minLength"]) {
|
|
8298
|
+
errors.push(`${path22}: string too short (min ${s["minLength"]})`);
|
|
8299
|
+
}
|
|
8300
|
+
if (typeof value === "string" && s["maxLength"] !== void 0 && value.length > s["maxLength"]) {
|
|
8301
|
+
errors.push(`${path22}: string too long (max ${s["maxLength"]})`);
|
|
8302
|
+
}
|
|
8303
|
+
if (typeof value === "number" && s["minimum"] !== void 0 && value < s["minimum"]) {
|
|
8304
|
+
errors.push(`${path22}: below minimum ${s["minimum"]}`);
|
|
8305
|
+
}
|
|
8306
|
+
if (typeof value === "number" && s["maximum"] !== void 0 && value > s["maximum"]) {
|
|
8307
|
+
errors.push(`${path22}: above maximum ${s["maximum"]}`);
|
|
8308
|
+
}
|
|
8309
|
+
if (Array.isArray(value) && s["items"] && Array.isArray(s["items"])) {
|
|
8310
|
+
for (let i = 0; i < value.length; i++) {
|
|
8311
|
+
check(value[i], s["items"], `${path22}[${i}]`);
|
|
8312
|
+
}
|
|
8313
|
+
}
|
|
8314
|
+
if (typeof value === "object" && value !== null && !Array.isArray(value) && s["properties"]) {
|
|
8315
|
+
const props = s["properties"];
|
|
8316
|
+
for (const [k, propSchema] of Object.entries(props)) {
|
|
8317
|
+
check(value[k], propSchema, `${path22}.${k}`);
|
|
8318
|
+
}
|
|
8319
|
+
}
|
|
8320
|
+
}
|
|
8321
|
+
check(data, schema, "$");
|
|
8322
|
+
return { valid: errors.length === 0, errors };
|
|
8323
|
+
}
|
|
8324
|
+
function simpleQuery(data, path22) {
|
|
7668
8325
|
const parts = path22.replace(/\[(\d+)\]/g, ".$1").split(".").filter(Boolean);
|
|
7669
8326
|
let current = data;
|
|
7670
8327
|
for (const part of parts) {
|
|
@@ -8027,8 +8684,8 @@ var outdatedTool = {
|
|
|
8027
8684
|
// read-only, but `outdated` makes outbound HTTP calls to the
|
|
8028
8685
|
// registry. The 'confirm' permission routes the call through the
|
|
8029
8686
|
// tool.confirm_needed flow on every invocation. M-1 originally
|
|
8030
|
-
// fixed four sibling tools (mcp_control, shellcheck,
|
|
8031
|
-
//
|
|
8687
|
+
// fixed four sibling tools (mcp_control, shellcheck, shellcheck (scan mode),
|
|
8688
|
+
// search) but missed this one; applying the same contract here.
|
|
8032
8689
|
mutating: true,
|
|
8033
8690
|
// Capability is outbound network — the tool only hits the package
|
|
8034
8691
|
// registry over HTTP, never touches the filesystem or runs shell.
|
|
@@ -8542,19 +9199,49 @@ var readTool = {
|
|
|
8542
9199
|
required: ["path"]
|
|
8543
9200
|
},
|
|
8544
9201
|
async execute(input, ctx) {
|
|
8545
|
-
if (!input?.path)
|
|
9202
|
+
if (!input?.path) {
|
|
9203
|
+
throw new ToolValidationError({
|
|
9204
|
+
message: "read: path is required",
|
|
9205
|
+
field: "path"
|
|
9206
|
+
});
|
|
9207
|
+
}
|
|
8546
9208
|
const absPath = await safeResolveReal(input.path, ctx);
|
|
8547
9209
|
let stat11;
|
|
8548
9210
|
try {
|
|
8549
9211
|
stat11 = await fs2.stat(absPath);
|
|
8550
9212
|
} catch (err) {
|
|
8551
9213
|
const code = err.code;
|
|
8552
|
-
if (code === "ENOENT")
|
|
8553
|
-
|
|
9214
|
+
if (code === "ENOENT") {
|
|
9215
|
+
throw new FsError({
|
|
9216
|
+
message: `read: file not found "${input.path}"`,
|
|
9217
|
+
code: "FS_READ_FAILED",
|
|
9218
|
+
path: absPath,
|
|
9219
|
+
context: { errno: "ENOENT" }
|
|
9220
|
+
});
|
|
9221
|
+
}
|
|
9222
|
+
throw new FsError({
|
|
9223
|
+
message: `read: failed to stat "${input.path}": ${toErrorMessage$2(err)}`,
|
|
9224
|
+
code: "FS_READ_FAILED",
|
|
9225
|
+
path: absPath,
|
|
9226
|
+
context: { errno: code },
|
|
9227
|
+
cause: err
|
|
9228
|
+
});
|
|
9229
|
+
}
|
|
9230
|
+
if (!stat11.isFile()) {
|
|
9231
|
+
throw new FsError({
|
|
9232
|
+
message: `read: "${input.path}" is not a regular file`,
|
|
9233
|
+
code: "FS_READ_FAILED",
|
|
9234
|
+
path: absPath,
|
|
9235
|
+
context: { reason: "not-a-regular-file" }
|
|
9236
|
+
});
|
|
8554
9237
|
}
|
|
8555
|
-
if (!stat11.isFile()) throw new Error(`read: "${input.path}" is not a regular file`);
|
|
8556
9238
|
if (stat11.size > MAX_BYTES2) {
|
|
8557
|
-
throw new
|
|
9239
|
+
throw new FsError({
|
|
9240
|
+
message: `read: file too large (${stat11.size} bytes, limit ${MAX_BYTES2})`,
|
|
9241
|
+
code: "FS_READ_FAILED",
|
|
9242
|
+
path: absPath,
|
|
9243
|
+
context: { size: stat11.size, limit: MAX_BYTES2, reason: "too-large" }
|
|
9244
|
+
});
|
|
8558
9245
|
}
|
|
8559
9246
|
const offset = Math.max(1, input.offset ?? 1);
|
|
8560
9247
|
const limit = Math.max(0, Math.min(input.limit ?? 2e3, 5e3));
|
|
@@ -8678,8 +9365,8 @@ var DEFAULT_IGNORE4 = ["node_modules", ".git", "dist", "build", ".next", "covera
|
|
|
8678
9365
|
var replaceTool = {
|
|
8679
9366
|
name: "replace",
|
|
8680
9367
|
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.
|
|
9368
|
+
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.",
|
|
9369
|
+
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
9370
|
permission: "confirm",
|
|
8684
9371
|
mutating: true,
|
|
8685
9372
|
capabilities: ["fs.write"],
|
|
@@ -8699,22 +9386,40 @@ var replaceTool = {
|
|
|
8699
9386
|
type: "boolean",
|
|
8700
9387
|
description: "Replace all occurrences in each file (default: true)"
|
|
8701
9388
|
},
|
|
8702
|
-
dry_run: { type: "boolean", description: "Preview changes without writing" }
|
|
9389
|
+
dry_run: { type: "boolean", description: "Preview changes without writing (default: true)" }
|
|
8703
9390
|
},
|
|
8704
9391
|
required: ["pattern", "replacement", "files"]
|
|
8705
9392
|
},
|
|
8706
9393
|
async execute(input, ctx) {
|
|
8707
|
-
if (!input?.pattern)
|
|
8708
|
-
|
|
8709
|
-
|
|
9394
|
+
if (!input?.pattern) {
|
|
9395
|
+
throw new ToolValidationError({
|
|
9396
|
+
message: "replace: pattern is required",
|
|
9397
|
+
field: "pattern"
|
|
9398
|
+
});
|
|
9399
|
+
}
|
|
9400
|
+
if (input.replacement === void 0) {
|
|
9401
|
+
throw new ToolValidationError({
|
|
9402
|
+
message: "replace: replacement is required",
|
|
9403
|
+
field: "replacement"
|
|
9404
|
+
});
|
|
9405
|
+
}
|
|
9406
|
+
if (!input?.files) {
|
|
9407
|
+
throw new ToolValidationError({
|
|
9408
|
+
message: "replace: files is required",
|
|
9409
|
+
field: "files"
|
|
9410
|
+
});
|
|
9411
|
+
}
|
|
8710
9412
|
const replaceAll = input.replace_all ?? true;
|
|
8711
9413
|
const compiled = compileUserRegex(input.pattern, "g");
|
|
8712
9414
|
if (!compiled.ok) {
|
|
8713
|
-
throw new
|
|
9415
|
+
throw new ToolValidationError({
|
|
9416
|
+
message: `replace: ${compiled.reason}`,
|
|
9417
|
+
field: "pattern"
|
|
9418
|
+
});
|
|
8714
9419
|
}
|
|
8715
9420
|
const re = compiled.regex;
|
|
8716
9421
|
const globRe = input.glob ? compileGlob(input.glob) : null;
|
|
8717
|
-
const dryRun = input.dry_run ??
|
|
9422
|
+
const dryRun = input.dry_run ?? true;
|
|
8718
9423
|
const filesInput = Array.isArray(input.files) ? input.files.join(",") : input.files;
|
|
8719
9424
|
const fileList = await resolveFiles2(filesInput, ctx, globRe);
|
|
8720
9425
|
const realRoot = await fs2.realpath(ctx.projectRoot).catch(() => ctx.projectRoot);
|
|
@@ -9063,11 +9768,13 @@ function substituteVars(content, name, vars) {
|
|
|
9063
9768
|
var DEFAULT_NUM = 10;
|
|
9064
9769
|
var MAX_RESULTS = 50;
|
|
9065
9770
|
var TIMEOUT_MS3 = 15e3;
|
|
9771
|
+
var CACHE_TTL_MS = 3e5;
|
|
9772
|
+
var cache = /* @__PURE__ */ new Map();
|
|
9066
9773
|
var searchTool = {
|
|
9067
9774
|
name: "search",
|
|
9068
9775
|
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.",
|
|
9776
|
+
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.",
|
|
9777
|
+
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
9778
|
permission: "confirm",
|
|
9072
9779
|
mutating: false,
|
|
9073
9780
|
capabilities: ["net.outbound"],
|
|
@@ -9087,6 +9794,10 @@ var searchTool = {
|
|
|
9087
9794
|
type: "string",
|
|
9088
9795
|
enum: ["duckduckgo", "google", "bing"],
|
|
9089
9796
|
description: "Search engine to use (default: duckduckgo)"
|
|
9797
|
+
},
|
|
9798
|
+
skip_cache: {
|
|
9799
|
+
type: "boolean",
|
|
9800
|
+
description: "Skip the in-memory cache and force a fresh search (default: false)"
|
|
9090
9801
|
}
|
|
9091
9802
|
},
|
|
9092
9803
|
required: ["query"]
|
|
@@ -9102,57 +9813,135 @@ var searchTool = {
|
|
|
9102
9813
|
return final;
|
|
9103
9814
|
},
|
|
9104
9815
|
async *executeStream(input, _ctx, opts) {
|
|
9105
|
-
if (!input?.query
|
|
9816
|
+
if (!input?.query || input.query.trim() === "") {
|
|
9817
|
+
throw new ToolValidationError({
|
|
9818
|
+
message: "search: query is required and must be a non-empty string",
|
|
9819
|
+
field: "query"
|
|
9820
|
+
});
|
|
9821
|
+
}
|
|
9106
9822
|
const num = Math.max(1, Math.min(input.num_results ?? DEFAULT_NUM, MAX_RESULTS));
|
|
9107
9823
|
const source = input.source ?? "duckduckgo";
|
|
9824
|
+
const skipCache = input.skip_cache ?? false;
|
|
9825
|
+
const cacheKey = `${source}:${input.query}`;
|
|
9826
|
+
if (!skipCache) {
|
|
9827
|
+
const entry = cache.get(cacheKey);
|
|
9828
|
+
if (entry && Date.now() - entry.timestamp < CACHE_TTL_MS) {
|
|
9829
|
+
const results = entry.results.map((r) => ({
|
|
9830
|
+
title: r.title,
|
|
9831
|
+
url: r.url,
|
|
9832
|
+
snippet: r.snippet
|
|
9833
|
+
}));
|
|
9834
|
+
yield {
|
|
9835
|
+
type: "log",
|
|
9836
|
+
text: `Cache hit for "${input.query}" (${source})`,
|
|
9837
|
+
data: { source, query: input.query, cached: true }
|
|
9838
|
+
};
|
|
9839
|
+
yield {
|
|
9840
|
+
type: "partial_output",
|
|
9841
|
+
text: `${results.length} cached results from ${source}`,
|
|
9842
|
+
data: { count: results.length, cached: true }
|
|
9843
|
+
};
|
|
9844
|
+
yield {
|
|
9845
|
+
type: "final",
|
|
9846
|
+
output: {
|
|
9847
|
+
query: input.query,
|
|
9848
|
+
results: results.slice(0, num),
|
|
9849
|
+
source,
|
|
9850
|
+
truncated: results.length >= num,
|
|
9851
|
+
cached: true
|
|
9852
|
+
}
|
|
9853
|
+
};
|
|
9854
|
+
return;
|
|
9855
|
+
}
|
|
9856
|
+
}
|
|
9108
9857
|
yield {
|
|
9109
9858
|
type: "log",
|
|
9110
9859
|
text: `Querying ${source} for "${input.query}"\u2026`,
|
|
9111
|
-
data: { source, query: input.query }
|
|
9860
|
+
data: { source, query: input.query, cached: false }
|
|
9112
9861
|
};
|
|
9113
|
-
let
|
|
9862
|
+
let rawResults;
|
|
9114
9863
|
switch (source) {
|
|
9115
9864
|
case "duckduckgo":
|
|
9116
|
-
|
|
9865
|
+
rawResults = await duckduckgoSearch(input.query, num, opts.signal);
|
|
9117
9866
|
break;
|
|
9118
9867
|
case "google":
|
|
9119
|
-
|
|
9868
|
+
rawResults = await googleSearch(input.query, num, opts.signal);
|
|
9120
9869
|
break;
|
|
9121
9870
|
case "bing":
|
|
9122
|
-
|
|
9871
|
+
rawResults = await bingSearch(input.query, num, opts.signal);
|
|
9123
9872
|
break;
|
|
9124
9873
|
default:
|
|
9125
|
-
throw new
|
|
9874
|
+
throw new ToolValidationError({
|
|
9875
|
+
message: `search: unknown source "${source}"`,
|
|
9876
|
+
field: "source"
|
|
9877
|
+
});
|
|
9878
|
+
}
|
|
9879
|
+
const seenUrls = /* @__PURE__ */ new Set();
|
|
9880
|
+
const deduped = [];
|
|
9881
|
+
for (const r of rawResults) {
|
|
9882
|
+
const noQuery = r.url.split("?")[0] ?? r.url;
|
|
9883
|
+
const normalized = noQuery.split("#")[0] ?? r.url;
|
|
9884
|
+
if (!seenUrls.has(normalized) && r.url.startsWith("http")) {
|
|
9885
|
+
seenUrls.add(normalized);
|
|
9886
|
+
deduped.push(r);
|
|
9887
|
+
}
|
|
9126
9888
|
}
|
|
9889
|
+
const ranked = scoreResults(deduped, input.query);
|
|
9890
|
+
const finalResults = ranked.slice(0, num);
|
|
9891
|
+
cache.set(cacheKey, { results: ranked, timestamp: Date.now() });
|
|
9892
|
+
pruneStaleCacheEntries();
|
|
9127
9893
|
yield {
|
|
9128
9894
|
type: "partial_output",
|
|
9129
|
-
text: `${
|
|
9130
|
-
data: { count:
|
|
9895
|
+
text: `${finalResults.length} results from ${source}`,
|
|
9896
|
+
data: { count: finalResults.length, cached: false }
|
|
9897
|
+
};
|
|
9898
|
+
yield {
|
|
9899
|
+
type: "final",
|
|
9900
|
+
output: {
|
|
9901
|
+
query: input.query,
|
|
9902
|
+
results: finalResults.map((r) => ({
|
|
9903
|
+
title: r.title,
|
|
9904
|
+
url: r.url,
|
|
9905
|
+
snippet: r.snippet
|
|
9906
|
+
})),
|
|
9907
|
+
source,
|
|
9908
|
+
truncated: finalResults.length >= num,
|
|
9909
|
+
cached: false
|
|
9910
|
+
}
|
|
9131
9911
|
};
|
|
9132
|
-
yield { type: "final", output };
|
|
9133
9912
|
}
|
|
9134
9913
|
};
|
|
9135
|
-
|
|
9136
|
-
const
|
|
9914
|
+
function pruneStaleCacheEntries() {
|
|
9915
|
+
const cutoff = Date.now() - CACHE_TTL_MS * 2;
|
|
9916
|
+
for (const [key, entry] of cache.entries()) {
|
|
9917
|
+
if (entry.timestamp < cutoff) cache.delete(key);
|
|
9918
|
+
}
|
|
9919
|
+
}
|
|
9920
|
+
function scoreResults(results, query) {
|
|
9921
|
+
const terms = query.toLowerCase().split(/\s+/).filter((t) => t.length > 0);
|
|
9922
|
+
return results.map((r) => {
|
|
9923
|
+
const titleLower = r.title.toLowerCase();
|
|
9924
|
+
const snippetLower = r.snippet.toLowerCase();
|
|
9925
|
+
let score = r.score;
|
|
9926
|
+
for (const term of terms) {
|
|
9927
|
+
if (titleLower.includes(term)) score += 2;
|
|
9928
|
+
if (snippetLower.includes(term)) score += 1;
|
|
9929
|
+
}
|
|
9930
|
+
return { ...r, score };
|
|
9931
|
+
}).sort((a, b) => b.score - a.score);
|
|
9932
|
+
}
|
|
9933
|
+
async function duckduckgoSearch(query, num, signal) {
|
|
9934
|
+
const encoded = encodeURIComponent(query);
|
|
9137
9935
|
const url = `https://lite.duckduckgo.com/lite/?q=${encoded}&kd=-1&kl=wt-wt`;
|
|
9138
9936
|
try {
|
|
9139
9937
|
const response = await fetchWithTimeout(url, signal, TIMEOUT_MS3);
|
|
9140
9938
|
const html = await response.text();
|
|
9141
|
-
|
|
9142
|
-
return {
|
|
9143
|
-
query: query2,
|
|
9144
|
-
results,
|
|
9145
|
-
source: "duckduckgo",
|
|
9146
|
-
truncated: results.length >= num
|
|
9147
|
-
};
|
|
9939
|
+
return parseDuckDuckGo(html, num);
|
|
9148
9940
|
} catch (err) {
|
|
9149
|
-
console.log(
|
|
9150
|
-
|
|
9151
|
-
|
|
9152
|
-
|
|
9153
|
-
source: "duckduckgo",
|
|
9154
|
-
truncated: false
|
|
9155
|
-
};
|
|
9941
|
+
console.log(
|
|
9942
|
+
JSON.stringify({ level: "debug", event: "search_failed", query, error: toErrorMessage$3(err) })
|
|
9943
|
+
);
|
|
9944
|
+
return [{ title: "Search unavailable", url: "https://duckduckgo.com/unavailable", snippet: "Could not reach DuckDuckGo", score: 0 }];
|
|
9156
9945
|
}
|
|
9157
9946
|
}
|
|
9158
9947
|
function takeFrom(iter, max) {
|
|
@@ -9177,25 +9966,22 @@ function parseDuckDuckGo(html, num) {
|
|
|
9177
9966
|
);
|
|
9178
9967
|
for (let i = 0; i < linkMatches.length && i < num; i++) {
|
|
9179
9968
|
const entry = linkMatches[i];
|
|
9180
|
-
|
|
9181
|
-
|
|
9182
|
-
|
|
9183
|
-
|
|
9184
|
-
|
|
9969
|
+
if (entry) {
|
|
9970
|
+
results.push({
|
|
9971
|
+
title: entry.title ?? "",
|
|
9972
|
+
url: entry.url ?? "",
|
|
9973
|
+
snippet: snippetMatches[i] ?? "",
|
|
9974
|
+
score: 1
|
|
9975
|
+
});
|
|
9976
|
+
}
|
|
9185
9977
|
}
|
|
9186
9978
|
return results;
|
|
9187
9979
|
}
|
|
9188
|
-
async function googleSearch(
|
|
9189
|
-
const encoded = encodeURIComponent(
|
|
9980
|
+
async function googleSearch(query, num, signal) {
|
|
9981
|
+
const encoded = encodeURIComponent(query);
|
|
9190
9982
|
const url = `https://www.google.com/search?q=${encoded}&hl=en`;
|
|
9191
9983
|
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
|
-
};
|
|
9984
|
+
return parseGoogleResults(html, num);
|
|
9199
9985
|
}
|
|
9200
9986
|
function parseGoogleResults(html, num) {
|
|
9201
9987
|
const results = [];
|
|
@@ -9218,22 +10004,17 @@ function parseGoogleResults(html, num) {
|
|
|
9218
10004
|
results.push({
|
|
9219
10005
|
title: titles[i] ?? "",
|
|
9220
10006
|
url: urls[i] ?? "",
|
|
9221
|
-
snippet: snippets[i] ?? ""
|
|
10007
|
+
snippet: snippets[i] ?? "",
|
|
10008
|
+
score: 1
|
|
9222
10009
|
});
|
|
9223
10010
|
}
|
|
9224
10011
|
return results;
|
|
9225
10012
|
}
|
|
9226
|
-
async function bingSearch(
|
|
9227
|
-
const encoded = encodeURIComponent(
|
|
10013
|
+
async function bingSearch(query, num, signal) {
|
|
10014
|
+
const encoded = encodeURIComponent(query);
|
|
9228
10015
|
const url = `https://www.bing.com/search?q=${encoded}`;
|
|
9229
10016
|
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
|
-
};
|
|
10017
|
+
return parseBingResults(html, num);
|
|
9237
10018
|
}
|
|
9238
10019
|
function parseBingResults(html, num) {
|
|
9239
10020
|
const results = [];
|
|
@@ -9248,11 +10029,15 @@ function parseBingResults(html, num) {
|
|
|
9248
10029
|
num
|
|
9249
10030
|
);
|
|
9250
10031
|
for (let i = 0; i < entries.length; i++) {
|
|
9251
|
-
|
|
9252
|
-
|
|
9253
|
-
|
|
9254
|
-
|
|
9255
|
-
|
|
10032
|
+
const entry = entries[i];
|
|
10033
|
+
if (entry) {
|
|
10034
|
+
results.push({
|
|
10035
|
+
title: entry.title ?? "",
|
|
10036
|
+
url: entry.url ?? "",
|
|
10037
|
+
snippet: snippets[i] ?? "",
|
|
10038
|
+
score: 1
|
|
10039
|
+
});
|
|
10040
|
+
}
|
|
9256
10041
|
}
|
|
9257
10042
|
return results;
|
|
9258
10043
|
}
|
|
@@ -9268,7 +10053,15 @@ async function fetchWithTimeout(url, signal, timeoutMs) {
|
|
|
9268
10053
|
return res;
|
|
9269
10054
|
} catch (e) {
|
|
9270
10055
|
clearTimeout(timer);
|
|
9271
|
-
|
|
10056
|
+
if (e instanceof FetchError) {
|
|
10057
|
+
throw e;
|
|
10058
|
+
}
|
|
10059
|
+
throw new FetchError({
|
|
10060
|
+
message: `search: failed to fetch ${url}`,
|
|
10061
|
+
status: 0,
|
|
10062
|
+
context: { url },
|
|
10063
|
+
cause: e
|
|
10064
|
+
});
|
|
9272
10065
|
}
|
|
9273
10066
|
}
|
|
9274
10067
|
function anySignal(...signals) {
|
|
@@ -9332,15 +10125,15 @@ var setWorkingDirTool = {
|
|
|
9332
10125
|
};
|
|
9333
10126
|
}
|
|
9334
10127
|
};
|
|
9335
|
-
function findTaskIndex(tasks,
|
|
9336
|
-
const asNum = Number.parseInt(
|
|
10128
|
+
function findTaskIndex(tasks, query) {
|
|
10129
|
+
const asNum = Number.parseInt(query, 10);
|
|
9337
10130
|
if (!Number.isNaN(asNum)) {
|
|
9338
10131
|
const idx = asNum - 1;
|
|
9339
10132
|
if (tasks[idx]) return idx;
|
|
9340
10133
|
}
|
|
9341
|
-
const byId = tasks.findIndex((t) => t.id ===
|
|
10134
|
+
const byId = tasks.findIndex((t) => t.id === query);
|
|
9342
10135
|
if (byId >= 0) return byId;
|
|
9343
|
-
const lower =
|
|
10136
|
+
const lower = query.toLowerCase();
|
|
9344
10137
|
return tasks.findIndex((t) => t.title.toLowerCase().includes(lower));
|
|
9345
10138
|
}
|
|
9346
10139
|
var taskTool = {
|
|
@@ -9964,8 +10757,8 @@ var todoTool = {
|
|
|
9964
10757
|
var toolHelpTool = {
|
|
9965
10758
|
name: "tool_help",
|
|
9966
10759
|
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`
|
|
10760
|
+
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.",
|
|
10761
|
+
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
10762
|
permission: "auto",
|
|
9970
10763
|
mutating: false,
|
|
9971
10764
|
timeoutMs: 5e3,
|
|
@@ -10088,8 +10881,8 @@ function formatAllToolsMarkdown(tools) {
|
|
|
10088
10881
|
var toolSearchTool = {
|
|
10089
10882
|
name: "tool_search",
|
|
10090
10883
|
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.",
|
|
10884
|
+
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.",
|
|
10885
|
+
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
10886
|
permission: "auto",
|
|
10094
10887
|
mutating: false,
|
|
10095
10888
|
timeoutMs: 1e3,
|
|
@@ -10127,9 +10920,9 @@ var toolSearchTool = {
|
|
|
10127
10920
|
async execute(input, ctx) {
|
|
10128
10921
|
const limit = Math.min(input.limit ?? 20, 100);
|
|
10129
10922
|
const tools = ctx.tools;
|
|
10130
|
-
const
|
|
10923
|
+
const query = input.query?.toLowerCase() ?? "";
|
|
10131
10924
|
const filtered = tools.filter((t) => {
|
|
10132
|
-
if (
|
|
10925
|
+
if (query && !t.name.toLowerCase().includes(query) && !t.description.toLowerCase().includes(query)) {
|
|
10133
10926
|
return false;
|
|
10134
10927
|
}
|
|
10135
10928
|
if (input.tags && input.tags.length > 0) {
|
|
@@ -10153,7 +10946,7 @@ var toolSearchTool = {
|
|
|
10153
10946
|
mutating: t.mutating
|
|
10154
10947
|
}));
|
|
10155
10948
|
const totalAvailable = tools.length;
|
|
10156
|
-
const hint = results.length === 0 &&
|
|
10949
|
+
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
10950
|
return {
|
|
10158
10951
|
tools: results,
|
|
10159
10952
|
total: filtered.length,
|
|
@@ -10526,8 +11319,18 @@ var writeTool = {
|
|
|
10526
11319
|
required: ["path", "content"]
|
|
10527
11320
|
},
|
|
10528
11321
|
async execute(input, ctx) {
|
|
10529
|
-
if (!input?.path)
|
|
10530
|
-
|
|
11322
|
+
if (!input?.path) {
|
|
11323
|
+
throw new ToolValidationError({
|
|
11324
|
+
message: "write: path is required",
|
|
11325
|
+
field: "path"
|
|
11326
|
+
});
|
|
11327
|
+
}
|
|
11328
|
+
if (input.content === void 0) {
|
|
11329
|
+
throw new ToolValidationError({
|
|
11330
|
+
message: "write: content is required",
|
|
11331
|
+
field: "content"
|
|
11332
|
+
});
|
|
11333
|
+
}
|
|
10531
11334
|
const absPath = await safeResolveReal(input.path, ctx);
|
|
10532
11335
|
let existed = false;
|
|
10533
11336
|
let prev = "";
|