@kolisachint/hoocode-agent 0.4.142 → 0.4.143
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/CHANGELOG.md +43 -0
- package/dist/cli/args.d.ts +1 -1
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +10 -7
- package/dist/cli/args.js.map +1 -1
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +1 -1
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/embsearch/embsearch-service.d.ts +18 -0
- package/dist/core/embsearch/embsearch-service.d.ts.map +1 -1
- package/dist/core/embsearch/embsearch-service.js +24 -2
- package/dist/core/embsearch/embsearch-service.js.map +1 -1
- package/dist/core/sdk.d.ts +5 -4
- package/dist/core/sdk.d.ts.map +1 -1
- package/dist/core/sdk.js +1 -1
- package/dist/core/sdk.js.map +1 -1
- package/dist/core/search/adapter.d.ts +39 -0
- package/dist/core/search/adapter.d.ts.map +1 -0
- package/dist/core/search/adapter.js +110 -0
- package/dist/core/search/adapter.js.map +1 -0
- package/dist/core/search/context-assembler.d.ts +22 -0
- package/dist/core/search/context-assembler.d.ts.map +1 -0
- package/dist/core/search/context-assembler.js +67 -0
- package/dist/core/search/context-assembler.js.map +1 -0
- package/dist/core/search/eval.d.ts +52 -0
- package/dist/core/search/eval.d.ts.map +1 -0
- package/dist/core/search/eval.js +75 -0
- package/dist/core/search/eval.js.map +1 -0
- package/dist/core/search/hybrid-search.d.ts +61 -0
- package/dist/core/search/hybrid-search.d.ts.map +1 -0
- package/dist/core/search/hybrid-search.js +158 -0
- package/dist/core/search/hybrid-search.js.map +1 -0
- package/dist/core/search/lexical-retriever.d.ts +30 -0
- package/dist/core/search/lexical-retriever.d.ts.map +1 -0
- package/dist/core/search/lexical-retriever.js +159 -0
- package/dist/core/search/lexical-retriever.js.map +1 -0
- package/dist/core/search/mode.d.ts +25 -0
- package/dist/core/search/mode.d.ts.map +1 -0
- package/dist/core/search/mode.js +37 -0
- package/dist/core/search/mode.js.map +1 -0
- package/dist/core/search/rerank.d.ts +28 -0
- package/dist/core/search/rerank.d.ts.map +1 -0
- package/dist/core/search/rerank.js +82 -0
- package/dist/core/search/rerank.js.map +1 -0
- package/dist/core/search/rrf.d.ts +29 -0
- package/dist/core/search/rrf.d.ts.map +1 -0
- package/dist/core/search/rrf.js +66 -0
- package/dist/core/search/rrf.js.map +1 -0
- package/dist/core/search/trace.d.ts +12 -0
- package/dist/core/search/trace.d.ts.map +1 -0
- package/dist/core/search/trace.js +37 -0
- package/dist/core/search/trace.js.map +1 -0
- package/dist/core/search/types.d.ts +61 -0
- package/dist/core/search/types.d.ts.map +1 -0
- package/dist/core/search/types.js +11 -0
- package/dist/core/search/types.js.map +1 -0
- package/dist/core/settings-types.d.ts.map +1 -1
- package/dist/core/settings-types.js.map +1 -1
- package/dist/core/tools/grep.d.ts.map +1 -1
- package/dist/core/tools/grep.js +1 -1
- package/dist/core/tools/grep.js.map +1 -1
- package/dist/core/tools/index.d.ts +4 -4
- package/dist/core/tools/index.d.ts.map +1 -1
- package/dist/core/tools/index.js +3 -3
- package/dist/core/tools/index.js.map +1 -1
- package/dist/core/tools/search.d.ts +37 -0
- package/dist/core/tools/search.d.ts.map +1 -0
- package/dist/core/tools/search.js +115 -0
- package/dist/core/tools/search.js.map +1 -0
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +7 -5
- package/dist/main.js.map +1 -1
- package/dist/utils/tools-manager.d.ts.map +1 -1
- package/dist/utils/tools-manager.js +1 -1
- package/dist/utils/tools-manager.js.map +1 -1
- package/examples/extensions/custom-provider-anthropic/package.json +1 -1
- package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
- package/examples/extensions/sandbox/package.json +1 -1
- package/examples/extensions/with-deps/package.json +1 -1
- package/package.json +4 -4
- package/dist/core/tools/semantic-search.d.ts +0 -28
- package/dist/core/tools/semantic-search.d.ts.map +0 -1
- package/dist/core/tools/semantic-search.js +0 -98
- package/dist/core/tools/semantic-search.js.map +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rerank.js","sourceRoot":"","sources":["../../../src/core/search/rerank.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Deterministic reranker over the fused top-50\n * (docs/hybrid-retrieval-design.md, step 7 of the shipping order).\n *\n * The eval gate showed fused Recall@50 well above Recall@5/10 — the right\n * candidates survive fusion but sit too deep. This reranker re-orders them\n * using evidence that is only cheap to compute *after* fusion, when there\n * are ≤50 candidates instead of thousands of lines:\n *\n * - term coverage: how many distinct query terms appear in the candidate's\n * actual expanded window (read from disk);\n * - path affinity: query terms appearing in the candidate's file path —\n * this is what lets a query like `core/search/hybrid-search.ts` rank the\n * file itself first, which content grep alone cannot do;\n * - fused prior: the RRF ordering, so retriever consensus still counts.\n *\n * Purely lexical-statistical and deterministic — no model, no I/O beyond\n * reading candidate windows. A cross-encoder can later replace the scoring\n * function behind the same signature; that model work belongs to\n * `kolisachint/embeddingsearchtools`, not here.\n */\n\nimport { readFileSync } from \"fs\";\nimport path from \"path\";\nimport { buildLexicalQueryPlan } from \"./lexical-retriever.js\";\nimport type { FusedCandidate } from \"./types.js\";\n\n/** Weights of the scoring blend. The eval harness (scripts/search-eval.mjs)\n * is the instrument for changing them — don't tune blind. */\nconst WEIGHT_FUSED_PRIOR = 0.4;\nconst WEIGHT_TERM_COVERAGE = 0.35;\nconst WEIGHT_PATH_AFFINITY = 0.25;\n/** Additive bonus when the query *is* the candidate's path (or its suffix):\n * the caller named the file, so no amount of content evidence elsewhere\n * should outrank it. */\nconst EXACT_PATH_BONUS = 0.5;\n\nexport interface RerankResult {\n\tcandidates: FusedCandidate[];\n\tlatencyMs: number;\n}\n\nexport function rerankCandidates(query: string, candidates: readonly FusedCandidate[], cwd: string): RerankResult {\n\tconst startedMs = Date.now();\n\tconst plan = buildLexicalQueryPlan(query);\n\tif (!plan || candidates.length < 2) {\n\t\treturn { candidates: [...candidates], latencyMs: Date.now() - startedMs };\n\t}\n\tconst terms = plan.terms;\n\tconst queryPath = query.trim().toLowerCase();\n\n\tconst fileCache = new Map<string, string[] | undefined>();\n\tconst readLines = (rel: string): string[] | undefined => {\n\t\tif (!fileCache.has(rel)) {\n\t\t\ttry {\n\t\t\t\tconst content = readFileSync(path.resolve(cwd, rel), \"utf-8\");\n\t\t\t\tfileCache.set(rel, content.toLowerCase().split(\"\\n\"));\n\t\t\t} catch {\n\t\t\t\tfileCache.set(rel, undefined);\n\t\t\t}\n\t\t}\n\t\treturn fileCache.get(rel);\n\t};\n\n\tconst scored = candidates.map((candidate, index) => {\n\t\t// Fused prior: normalized RRF ordering, 1 for the top candidate.\n\t\tconst fusedPrior = 1 - index / candidates.length;\n\n\t\tconst lines = readLines(candidate.path);\n\t\tlet termCoverage = 0;\n\t\tif (lines && terms.length > 0) {\n\t\t\tconst window = lines\n\t\t\t\t.slice(Math.max(0, candidate.startLine - 1), Math.min(lines.length, candidate.endLine))\n\t\t\t\t.join(\"\\n\");\n\t\t\ttermCoverage = terms.filter((t) => window.includes(t)).length / terms.length;\n\t\t}\n\n\t\tconst lowerPath = candidate.path.toLowerCase();\n\t\t// A quoted phrase rarely names a file; split it into path-ish tokens so\n\t\t// `\"token budget exceeded\"` still gets partial path credit.\n\t\tconst pathTerms = terms.length === 1 ? terms[0].split(/[^a-z0-9_$]+/).filter((t) => t.length >= 3) : terms;\n\t\tconst pathAffinity =\n\t\t\tpathTerms.length > 0 ? pathTerms.filter((t) => lowerPath.includes(t)).length / pathTerms.length : 0;\n\n\t\tconst exactPath =\n\t\t\tqueryPath.length >= 3 && (lowerPath === queryPath || lowerPath.endsWith(`/${queryPath}`)) ? 1 : 0;\n\n\t\tconst score =\n\t\t\tWEIGHT_FUSED_PRIOR * fusedPrior +\n\t\t\tWEIGHT_TERM_COVERAGE * termCoverage +\n\t\t\tWEIGHT_PATH_AFFINITY * pathAffinity +\n\t\t\tEXACT_PATH_BONUS * exactPath;\n\t\treturn { candidate, index, score };\n\t});\n\n\t// Stable, deterministic: score desc, fused order as tie-break.\n\tscored.sort((a, b) => b.score - a.score || a.index - b.index);\n\treturn { candidates: scored.map((s) => s.candidate), latencyMs: Date.now() - startedMs };\n}\n"]}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reciprocal Rank Fusion — the transparent, rank-only recall layer of hybrid
|
|
3
|
+
* search (docs/hybrid-retrieval-design.md, Decision 2).
|
|
4
|
+
*
|
|
5
|
+
* Rank-only on purpose: BM25 and cosine scores are not comparable across
|
|
6
|
+
* retrievers, so raw scores are carried through as diagnostics but never
|
|
7
|
+
* enter the fused score.
|
|
8
|
+
*/
|
|
9
|
+
import type { FusedHit, RankedHit } from "./types.js";
|
|
10
|
+
/**
|
|
11
|
+
* Default RRF constant. The literature folklore default is 60; the eval gate
|
|
12
|
+
* (scripts/search-eval.mjs, 12-query gold set) measured k ∈ {0, 2} beating
|
|
13
|
+
* k = 60 on every differing query, twice, with reranking on top of either —
|
|
14
|
+
* small k keeps fusion top-heavy toward each retriever's best hits, and the
|
|
15
|
+
* reranker corrects the tail. Small sample: re-sweep when the gold set grows.
|
|
16
|
+
*/
|
|
17
|
+
export declare const DEFAULT_RRF_K = 2;
|
|
18
|
+
/**
|
|
19
|
+
* Fuse ranked lists into one deterministic ordering by summed `1/(k + rank)`.
|
|
20
|
+
*
|
|
21
|
+
* Ties break by number of agreeing retrievers, then lexicographic id, so the
|
|
22
|
+
* same inputs always produce the same context.
|
|
23
|
+
*
|
|
24
|
+
* Duplicate `source:id` pairs within one list are counted once (best rank
|
|
25
|
+
* wins). The adapter dedupes upstream, so this guard should never fire in
|
|
26
|
+
* practice — it exists so a misbehaving retriever cannot inflate its vote.
|
|
27
|
+
*/
|
|
28
|
+
export declare function rrfFuse(lists: readonly (readonly RankedHit[])[], k?: number): FusedHit[];
|
|
29
|
+
//# sourceMappingURL=rrf.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rrf.d.ts","sourceRoot":"","sources":["../../../src/core/search/rrf.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAEtD;;;;;;GAMG;AACH,eAAO,MAAM,aAAa,IAAI,CAAC;AAE/B;;;;;;;;;GASG;AACH,wBAAgB,OAAO,CAAC,KAAK,EAAE,SAAS,CAAC,SAAS,SAAS,EAAE,CAAC,EAAE,EAAE,CAAC,SAAgB,GAAG,QAAQ,EAAE,CA6C/F","sourcesContent":["/**\n * Reciprocal Rank Fusion — the transparent, rank-only recall layer of hybrid\n * search (docs/hybrid-retrieval-design.md, Decision 2).\n *\n * Rank-only on purpose: BM25 and cosine scores are not comparable across\n * retrievers, so raw scores are carried through as diagnostics but never\n * enter the fused score.\n */\n\nimport type { FusedHit, RankedHit } from \"./types.js\";\n\n/**\n * Default RRF constant. The literature folklore default is 60; the eval gate\n * (scripts/search-eval.mjs, 12-query gold set) measured k ∈ {0, 2} beating\n * k = 60 on every differing query, twice, with reranking on top of either —\n * small k keeps fusion top-heavy toward each retriever's best hits, and the\n * reranker corrects the tail. Small sample: re-sweep when the gold set grows.\n */\nexport const DEFAULT_RRF_K = 2;\n\n/**\n * Fuse ranked lists into one deterministic ordering by summed `1/(k + rank)`.\n *\n * Ties break by number of agreeing retrievers, then lexicographic id, so the\n * same inputs always produce the same context.\n *\n * Duplicate `source:id` pairs within one list are counted once (best rank\n * wins). The adapter dedupes upstream, so this guard should never fire in\n * practice — it exists so a misbehaving retriever cannot inflate its vote.\n */\nexport function rrfFuse(lists: readonly (readonly RankedHit[])[], k = DEFAULT_RRF_K): FusedHit[] {\n\tif (!Number.isFinite(k) || k < 0) {\n\t\tthrow new Error(`RRF k must be a finite non-negative number; got ${k}`);\n\t}\n\n\tconst acc = new Map<string, FusedHit>();\n\n\tfor (const list of lists) {\n\t\t// Collapse duplicates to their best rank first, so the single vote a\n\t\t// duplicated id gets is cast at the best rank regardless of emit order.\n\t\tconst collapsed = new Map<string, RankedHit>();\n\t\tfor (const hit of list) {\n\t\t\tif (!Number.isInteger(hit.rank) || hit.rank < 1) {\n\t\t\t\tthrow new Error(`RRF rank must be a positive integer; got ${hit.rank}`);\n\t\t\t}\n\t\t\tconst dedupeKey = `${hit.source}:${hit.id}`;\n\t\t\tconst existing = collapsed.get(dedupeKey);\n\t\t\tif (!existing || hit.rank < existing.rank) collapsed.set(dedupeKey, hit);\n\t\t}\n\n\t\tfor (const hit of collapsed.values()) {\n\t\t\tlet current = acc.get(hit.id);\n\t\t\tif (!current) {\n\t\t\t\tcurrent = { id: hit.id, rrfScore: 0, ranks: {}, rawScores: {} };\n\t\t\t\tacc.set(hit.id, current);\n\t\t\t}\n\n\t\t\tcurrent.rrfScore += 1 / (k + hit.rank);\n\n\t\t\tconst oldRank = current.ranks[hit.source];\n\t\t\tif (oldRank === undefined || hit.rank < oldRank) {\n\t\t\t\tcurrent.ranks[hit.source] = hit.rank;\n\t\t\t\t// rawScores follows the best rank; a best-ranked hit without a\n\t\t\t\t// score leaves any earlier score in place rather than erasing it.\n\t\t\t\tif (hit.score !== undefined) current.rawScores[hit.source] = hit.score;\n\t\t\t}\n\t\t}\n\t}\n\n\treturn [...acc.values()].sort(\n\t\t(a, b) =>\n\t\t\tb.rrfScore - a.rrfScore ||\n\t\t\tObject.keys(b.ranks).length - Object.keys(a.ranks).length ||\n\t\t\ta.id.localeCompare(b.id),\n\t);\n}\n"]}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reciprocal Rank Fusion — the transparent, rank-only recall layer of hybrid
|
|
3
|
+
* search (docs/hybrid-retrieval-design.md, Decision 2).
|
|
4
|
+
*
|
|
5
|
+
* Rank-only on purpose: BM25 and cosine scores are not comparable across
|
|
6
|
+
* retrievers, so raw scores are carried through as diagnostics but never
|
|
7
|
+
* enter the fused score.
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* Default RRF constant. The literature folklore default is 60; the eval gate
|
|
11
|
+
* (scripts/search-eval.mjs, 12-query gold set) measured k ∈ {0, 2} beating
|
|
12
|
+
* k = 60 on every differing query, twice, with reranking on top of either —
|
|
13
|
+
* small k keeps fusion top-heavy toward each retriever's best hits, and the
|
|
14
|
+
* reranker corrects the tail. Small sample: re-sweep when the gold set grows.
|
|
15
|
+
*/
|
|
16
|
+
export const DEFAULT_RRF_K = 2;
|
|
17
|
+
/**
|
|
18
|
+
* Fuse ranked lists into one deterministic ordering by summed `1/(k + rank)`.
|
|
19
|
+
*
|
|
20
|
+
* Ties break by number of agreeing retrievers, then lexicographic id, so the
|
|
21
|
+
* same inputs always produce the same context.
|
|
22
|
+
*
|
|
23
|
+
* Duplicate `source:id` pairs within one list are counted once (best rank
|
|
24
|
+
* wins). The adapter dedupes upstream, so this guard should never fire in
|
|
25
|
+
* practice — it exists so a misbehaving retriever cannot inflate its vote.
|
|
26
|
+
*/
|
|
27
|
+
export function rrfFuse(lists, k = DEFAULT_RRF_K) {
|
|
28
|
+
if (!Number.isFinite(k) || k < 0) {
|
|
29
|
+
throw new Error(`RRF k must be a finite non-negative number; got ${k}`);
|
|
30
|
+
}
|
|
31
|
+
const acc = new Map();
|
|
32
|
+
for (const list of lists) {
|
|
33
|
+
// Collapse duplicates to their best rank first, so the single vote a
|
|
34
|
+
// duplicated id gets is cast at the best rank regardless of emit order.
|
|
35
|
+
const collapsed = new Map();
|
|
36
|
+
for (const hit of list) {
|
|
37
|
+
if (!Number.isInteger(hit.rank) || hit.rank < 1) {
|
|
38
|
+
throw new Error(`RRF rank must be a positive integer; got ${hit.rank}`);
|
|
39
|
+
}
|
|
40
|
+
const dedupeKey = `${hit.source}:${hit.id}`;
|
|
41
|
+
const existing = collapsed.get(dedupeKey);
|
|
42
|
+
if (!existing || hit.rank < existing.rank)
|
|
43
|
+
collapsed.set(dedupeKey, hit);
|
|
44
|
+
}
|
|
45
|
+
for (const hit of collapsed.values()) {
|
|
46
|
+
let current = acc.get(hit.id);
|
|
47
|
+
if (!current) {
|
|
48
|
+
current = { id: hit.id, rrfScore: 0, ranks: {}, rawScores: {} };
|
|
49
|
+
acc.set(hit.id, current);
|
|
50
|
+
}
|
|
51
|
+
current.rrfScore += 1 / (k + hit.rank);
|
|
52
|
+
const oldRank = current.ranks[hit.source];
|
|
53
|
+
if (oldRank === undefined || hit.rank < oldRank) {
|
|
54
|
+
current.ranks[hit.source] = hit.rank;
|
|
55
|
+
// rawScores follows the best rank; a best-ranked hit without a
|
|
56
|
+
// score leaves any earlier score in place rather than erasing it.
|
|
57
|
+
if (hit.score !== undefined)
|
|
58
|
+
current.rawScores[hit.source] = hit.score;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return [...acc.values()].sort((a, b) => b.rrfScore - a.rrfScore ||
|
|
63
|
+
Object.keys(b.ranks).length - Object.keys(a.ranks).length ||
|
|
64
|
+
a.id.localeCompare(b.id));
|
|
65
|
+
}
|
|
66
|
+
//# sourceMappingURL=rrf.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rrf.js","sourceRoot":"","sources":["../../../src/core/search/rrf.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Reciprocal Rank Fusion — the transparent, rank-only recall layer of hybrid\n * search (docs/hybrid-retrieval-design.md, Decision 2).\n *\n * Rank-only on purpose: BM25 and cosine scores are not comparable across\n * retrievers, so raw scores are carried through as diagnostics but never\n * enter the fused score.\n */\n\nimport type { FusedHit, RankedHit } from \"./types.js\";\n\n/**\n * Default RRF constant. The literature folklore default is 60; the eval gate\n * (scripts/search-eval.mjs, 12-query gold set) measured k ∈ {0, 2} beating\n * k = 60 on every differing query, twice, with reranking on top of either —\n * small k keeps fusion top-heavy toward each retriever's best hits, and the\n * reranker corrects the tail. Small sample: re-sweep when the gold set grows.\n */\nexport const DEFAULT_RRF_K = 2;\n\n/**\n * Fuse ranked lists into one deterministic ordering by summed `1/(k + rank)`.\n *\n * Ties break by number of agreeing retrievers, then lexicographic id, so the\n * same inputs always produce the same context.\n *\n * Duplicate `source:id` pairs within one list are counted once (best rank\n * wins). The adapter dedupes upstream, so this guard should never fire in\n * practice — it exists so a misbehaving retriever cannot inflate its vote.\n */\nexport function rrfFuse(lists: readonly (readonly RankedHit[])[], k = DEFAULT_RRF_K): FusedHit[] {\n\tif (!Number.isFinite(k) || k < 0) {\n\t\tthrow new Error(`RRF k must be a finite non-negative number; got ${k}`);\n\t}\n\n\tconst acc = new Map<string, FusedHit>();\n\n\tfor (const list of lists) {\n\t\t// Collapse duplicates to their best rank first, so the single vote a\n\t\t// duplicated id gets is cast at the best rank regardless of emit order.\n\t\tconst collapsed = new Map<string, RankedHit>();\n\t\tfor (const hit of list) {\n\t\t\tif (!Number.isInteger(hit.rank) || hit.rank < 1) {\n\t\t\t\tthrow new Error(`RRF rank must be a positive integer; got ${hit.rank}`);\n\t\t\t}\n\t\t\tconst dedupeKey = `${hit.source}:${hit.id}`;\n\t\t\tconst existing = collapsed.get(dedupeKey);\n\t\t\tif (!existing || hit.rank < existing.rank) collapsed.set(dedupeKey, hit);\n\t\t}\n\n\t\tfor (const hit of collapsed.values()) {\n\t\t\tlet current = acc.get(hit.id);\n\t\t\tif (!current) {\n\t\t\t\tcurrent = { id: hit.id, rrfScore: 0, ranks: {}, rawScores: {} };\n\t\t\t\tacc.set(hit.id, current);\n\t\t\t}\n\n\t\t\tcurrent.rrfScore += 1 / (k + hit.rank);\n\n\t\t\tconst oldRank = current.ranks[hit.source];\n\t\t\tif (oldRank === undefined || hit.rank < oldRank) {\n\t\t\t\tcurrent.ranks[hit.source] = hit.rank;\n\t\t\t\t// rawScores follows the best rank; a best-ranked hit without a\n\t\t\t\t// score leaves any earlier score in place rather than erasing it.\n\t\t\t\tif (hit.score !== undefined) current.rawScores[hit.source] = hit.score;\n\t\t\t}\n\t\t}\n\t}\n\n\treturn [...acc.values()].sort(\n\t\t(a, b) =>\n\t\t\tb.rrfScore - a.rrfScore ||\n\t\t\tObject.keys(b.ranks).length - Object.keys(a.ranks).length ||\n\t\t\ta.id.localeCompare(b.id),\n\t);\n}\n"]}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Search trace sink (docs/hybrid-retrieval-design.md, Decision 5).
|
|
3
|
+
*
|
|
4
|
+
* Full per-call diagnostics — resolved mode, per-retriever latency/hits,
|
|
5
|
+
* fused ranks, raw scores — go to a jsonl sidecar in the embsearch store dir,
|
|
6
|
+
* never into model context or session files. Best-effort by design: a failed
|
|
7
|
+
* trace write must never fail a search.
|
|
8
|
+
*/
|
|
9
|
+
import type { SearchTrace } from "./types.js";
|
|
10
|
+
export declare function getSearchTracePath(cwd: string): string;
|
|
11
|
+
export declare function writeSearchTrace(cwd: string, trace: SearchTrace): void;
|
|
12
|
+
//# sourceMappingURL=trace.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"trace.d.ts","sourceRoot":"","sources":["../../../src/core/search/trace.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAKH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAO9C,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAEtD;AAED,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,WAAW,GAAG,IAAI,CActE","sourcesContent":["/**\n * Search trace sink (docs/hybrid-retrieval-design.md, Decision 5).\n *\n * Full per-call diagnostics — resolved mode, per-retriever latency/hits,\n * fused ranks, raw scores — go to a jsonl sidecar in the embsearch store dir,\n * never into model context or session files. Best-effort by design: a failed\n * trace write must never fail a search.\n */\n\nimport { appendFileSync, mkdirSync, renameSync, statSync } from \"fs\";\nimport { join } from \"path\";\nimport { getEmbsearchStoreDir } from \"../embsearch/index-meta.js\";\nimport type { SearchTrace } from \"./types.js\";\n\nconst TRACE_FILE = \"search-trace.jsonl\";\n/** One rotation (`.1` suffix) once the live file passes this size, so traces\n * stay bounded without a GC pass. */\nconst TRACE_ROTATE_BYTES = 5 * 1024 * 1024;\n\nexport function getSearchTracePath(cwd: string): string {\n\treturn join(getEmbsearchStoreDir(cwd), TRACE_FILE);\n}\n\nexport function writeSearchTrace(cwd: string, trace: SearchTrace): void {\n\ttry {\n\t\tconst dir = getEmbsearchStoreDir(cwd);\n\t\tmkdirSync(dir, { recursive: true });\n\t\tconst file = join(dir, TRACE_FILE);\n\t\ttry {\n\t\t\tif (statSync(file).size >= TRACE_ROTATE_BYTES) renameSync(file, `${file}.1`);\n\t\t} catch {\n\t\t\t// Missing file — nothing to rotate.\n\t\t}\n\t\tappendFileSync(file, `${JSON.stringify(trace)}\\n`);\n\t} catch {\n\t\t// Diagnostics only — never surface trace failures to the caller.\n\t}\n}\n"]}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Search trace sink (docs/hybrid-retrieval-design.md, Decision 5).
|
|
3
|
+
*
|
|
4
|
+
* Full per-call diagnostics — resolved mode, per-retriever latency/hits,
|
|
5
|
+
* fused ranks, raw scores — go to a jsonl sidecar in the embsearch store dir,
|
|
6
|
+
* never into model context or session files. Best-effort by design: a failed
|
|
7
|
+
* trace write must never fail a search.
|
|
8
|
+
*/
|
|
9
|
+
import { appendFileSync, mkdirSync, renameSync, statSync } from "fs";
|
|
10
|
+
import { join } from "path";
|
|
11
|
+
import { getEmbsearchStoreDir } from "../embsearch/index-meta.js";
|
|
12
|
+
const TRACE_FILE = "search-trace.jsonl";
|
|
13
|
+
/** One rotation (`.1` suffix) once the live file passes this size, so traces
|
|
14
|
+
* stay bounded without a GC pass. */
|
|
15
|
+
const TRACE_ROTATE_BYTES = 5 * 1024 * 1024;
|
|
16
|
+
export function getSearchTracePath(cwd) {
|
|
17
|
+
return join(getEmbsearchStoreDir(cwd), TRACE_FILE);
|
|
18
|
+
}
|
|
19
|
+
export function writeSearchTrace(cwd, trace) {
|
|
20
|
+
try {
|
|
21
|
+
const dir = getEmbsearchStoreDir(cwd);
|
|
22
|
+
mkdirSync(dir, { recursive: true });
|
|
23
|
+
const file = join(dir, TRACE_FILE);
|
|
24
|
+
try {
|
|
25
|
+
if (statSync(file).size >= TRACE_ROTATE_BYTES)
|
|
26
|
+
renameSync(file, `${file}.1`);
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
// Missing file — nothing to rotate.
|
|
30
|
+
}
|
|
31
|
+
appendFileSync(file, `${JSON.stringify(trace)}\n`);
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
// Diagnostics only — never surface trace failures to the caller.
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
//# sourceMappingURL=trace.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"trace.js","sourceRoot":"","sources":["../../../src/core/search/trace.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,cAAc,EAAE,SAAS,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,IAAI,CAAC;AACrE,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAC5B,OAAO,EAAE,oBAAoB,EAAE,MAAM,4BAA4B,CAAC;AAGlE,MAAM,UAAU,GAAG,oBAAoB,CAAC;AACxC;sCACsC;AACtC,MAAM,kBAAkB,GAAG,CAAC,GAAG,IAAI,GAAG,IAAI,CAAC;AAE3C,MAAM,UAAU,kBAAkB,CAAC,GAAW,EAAU;IACvD,OAAO,IAAI,CAAC,oBAAoB,CAAC,GAAG,CAAC,EAAE,UAAU,CAAC,CAAC;AAAA,CACnD;AAED,MAAM,UAAU,gBAAgB,CAAC,GAAW,EAAE,KAAkB,EAAQ;IACvE,IAAI,CAAC;QACJ,MAAM,GAAG,GAAG,oBAAoB,CAAC,GAAG,CAAC,CAAC;QACtC,SAAS,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QACpC,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,EAAE,UAAU,CAAC,CAAC;QACnC,IAAI,CAAC;YACJ,IAAI,QAAQ,CAAC,IAAI,CAAC,CAAC,IAAI,IAAI,kBAAkB;gBAAE,UAAU,CAAC,IAAI,EAAE,GAAG,IAAI,IAAI,CAAC,CAAC;QAC9E,CAAC;QAAC,MAAM,CAAC;YACR,sCAAoC;QACrC,CAAC;QACD,cAAc,CAAC,IAAI,EAAE,GAAG,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IACpD,CAAC;IAAC,MAAM,CAAC;QACR,mEAAiE;IAClE,CAAC;AAAA,CACD","sourcesContent":["/**\n * Search trace sink (docs/hybrid-retrieval-design.md, Decision 5).\n *\n * Full per-call diagnostics — resolved mode, per-retriever latency/hits,\n * fused ranks, raw scores — go to a jsonl sidecar in the embsearch store dir,\n * never into model context or session files. Best-effort by design: a failed\n * trace write must never fail a search.\n */\n\nimport { appendFileSync, mkdirSync, renameSync, statSync } from \"fs\";\nimport { join } from \"path\";\nimport { getEmbsearchStoreDir } from \"../embsearch/index-meta.js\";\nimport type { SearchTrace } from \"./types.js\";\n\nconst TRACE_FILE = \"search-trace.jsonl\";\n/** One rotation (`.1` suffix) once the live file passes this size, so traces\n * stay bounded without a GC pass. */\nconst TRACE_ROTATE_BYTES = 5 * 1024 * 1024;\n\nexport function getSearchTracePath(cwd: string): string {\n\treturn join(getEmbsearchStoreDir(cwd), TRACE_FILE);\n}\n\nexport function writeSearchTrace(cwd: string, trace: SearchTrace): void {\n\ttry {\n\t\tconst dir = getEmbsearchStoreDir(cwd);\n\t\tmkdirSync(dir, { recursive: true });\n\t\tconst file = join(dir, TRACE_FILE);\n\t\ttry {\n\t\t\tif (statSync(file).size >= TRACE_ROTATE_BYTES) renameSync(file, `${file}.1`);\n\t\t} catch {\n\t\t\t// Missing file — nothing to rotate.\n\t\t}\n\t\tappendFileSync(file, `${JSON.stringify(trace)}\\n`);\n\t} catch {\n\t\t// Diagnostics only — never surface trace failures to the caller.\n\t}\n}\n"]}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared types for hybrid code retrieval (see docs/hybrid-retrieval-design.md).
|
|
3
|
+
*
|
|
4
|
+
* Identity note: candidate ids are per-index-build chunk ids (`relpath#index`)
|
|
5
|
+
* from the embedding sidecar, or synthetic `relpath#L<line>` fallback ids for
|
|
6
|
+
* grep hits in files the index does not cover. They are stable within one
|
|
7
|
+
* query (fusion and span expansion consult the same sidecar snapshot) but not
|
|
8
|
+
* across edits or rebuilds — never persist them as durable references.
|
|
9
|
+
*/
|
|
10
|
+
export type RetrieverSource = "grep" | "embed";
|
|
11
|
+
export type SearchMode = "auto" | "lexical" | "semantic" | "hybrid";
|
|
12
|
+
export type ResolvedSearchMode = Exclude<SearchMode, "auto">;
|
|
13
|
+
export interface RankedHit {
|
|
14
|
+
id: string;
|
|
15
|
+
/** 1-indexed, gap-free rank within its retriever's list. */
|
|
16
|
+
rank: number;
|
|
17
|
+
/** Retriever-local score (BM25-ish, cosine, …). Diagnostics only — never fused. */
|
|
18
|
+
score?: number;
|
|
19
|
+
source: RetrieverSource;
|
|
20
|
+
}
|
|
21
|
+
export interface FusedHit {
|
|
22
|
+
id: string;
|
|
23
|
+
rrfScore: number;
|
|
24
|
+
/** Best rank per contributing retriever. */
|
|
25
|
+
ranks: Partial<Record<RetrieverSource, number>>;
|
|
26
|
+
/** Raw score at the best rank per retriever. Diagnostics only. */
|
|
27
|
+
rawScores: Partial<Record<RetrieverSource, number>>;
|
|
28
|
+
}
|
|
29
|
+
/** Line-span identity a candidate id resolves to, for post-fusion expansion. */
|
|
30
|
+
export interface CandidateSpan {
|
|
31
|
+
path: string;
|
|
32
|
+
/** 1-based inclusive. */
|
|
33
|
+
startLine: number;
|
|
34
|
+
/** 1-based inclusive. May exceed the file's length; readers clamp. */
|
|
35
|
+
endLine: number;
|
|
36
|
+
}
|
|
37
|
+
export interface FusedCandidate extends FusedHit, CandidateSpan {
|
|
38
|
+
}
|
|
39
|
+
/** Per-call diagnostic record, written to the store-dir trace jsonl — never
|
|
40
|
+
* into model context. */
|
|
41
|
+
export interface SearchTrace {
|
|
42
|
+
timestampMs: number;
|
|
43
|
+
query: string;
|
|
44
|
+
requestedMode: SearchMode;
|
|
45
|
+
resolvedMode: ResolvedSearchMode;
|
|
46
|
+
/** Set when the resolved mode is a degradation of the requested one. */
|
|
47
|
+
degradedReason?: string;
|
|
48
|
+
indexPhase: "ready" | "indexing" | "unavailable";
|
|
49
|
+
rrfK?: number;
|
|
50
|
+
retrievers: Partial<Record<RetrieverSource, {
|
|
51
|
+
latencyMs: number;
|
|
52
|
+
hitCount: number;
|
|
53
|
+
}>>;
|
|
54
|
+
fused: FusedHit[];
|
|
55
|
+
rerank?: {
|
|
56
|
+
applied: boolean;
|
|
57
|
+
candidateCount: number;
|
|
58
|
+
latencyMs: number;
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/core/search/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,MAAM,MAAM,eAAe,GAAG,MAAM,GAAG,OAAO,CAAC;AAE/C,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG,SAAS,GAAG,UAAU,GAAG,QAAQ,CAAC;AACpE,MAAM,MAAM,kBAAkB,GAAG,OAAO,CAAC,UAAU,EAAE,MAAM,CAAC,CAAC;AAE7D,MAAM,WAAW,SAAS;IACzB,EAAE,EAAE,MAAM,CAAC;IACX,4DAA4D;IAC5D,IAAI,EAAE,MAAM,CAAC;IACb,uFAAmF;IACnF,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,eAAe,CAAC;CACxB;AAED,MAAM,WAAW,QAAQ;IACxB,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,MAAM,CAAC;IACjB,4CAA4C;IAC5C,KAAK,EAAE,OAAO,CAAC,MAAM,CAAC,eAAe,EAAE,MAAM,CAAC,CAAC,CAAC;IAChD,kEAAkE;IAClE,SAAS,EAAE,OAAO,CAAC,MAAM,CAAC,eAAe,EAAE,MAAM,CAAC,CAAC,CAAC;CACpD;AAED,gFAAgF;AAChF,MAAM,WAAW,aAAa;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,yBAAyB;IACzB,SAAS,EAAE,MAAM,CAAC;IAClB,sEAAsE;IACtE,OAAO,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,cAAe,SAAQ,QAAQ,EAAE,aAAa;CAAG;AAElE;0BAC0B;AAC1B,MAAM,WAAW,WAAW;IAC3B,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,aAAa,EAAE,UAAU,CAAC;IAC1B,YAAY,EAAE,kBAAkB,CAAC;IACjC,wEAAwE;IACxE,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,UAAU,EAAE,OAAO,GAAG,UAAU,GAAG,aAAa,CAAC;IACjD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,OAAO,CAAC,MAAM,CAAC,eAAe,EAAE;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC,CAAC;IACtF,KAAK,EAAE,QAAQ,EAAE,CAAC;IAClB,MAAM,CAAC,EAAE;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,cAAc,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,CAAC;CACzE","sourcesContent":["/**\n * Shared types for hybrid code retrieval (see docs/hybrid-retrieval-design.md).\n *\n * Identity note: candidate ids are per-index-build chunk ids (`relpath#index`)\n * from the embedding sidecar, or synthetic `relpath#L<line>` fallback ids for\n * grep hits in files the index does not cover. They are stable within one\n * query (fusion and span expansion consult the same sidecar snapshot) but not\n * across edits or rebuilds — never persist them as durable references.\n */\n\nexport type RetrieverSource = \"grep\" | \"embed\";\n\nexport type SearchMode = \"auto\" | \"lexical\" | \"semantic\" | \"hybrid\";\nexport type ResolvedSearchMode = Exclude<SearchMode, \"auto\">;\n\nexport interface RankedHit {\n\tid: string;\n\t/** 1-indexed, gap-free rank within its retriever's list. */\n\trank: number;\n\t/** Retriever-local score (BM25-ish, cosine, …). Diagnostics only — never fused. */\n\tscore?: number;\n\tsource: RetrieverSource;\n}\n\nexport interface FusedHit {\n\tid: string;\n\trrfScore: number;\n\t/** Best rank per contributing retriever. */\n\tranks: Partial<Record<RetrieverSource, number>>;\n\t/** Raw score at the best rank per retriever. Diagnostics only. */\n\trawScores: Partial<Record<RetrieverSource, number>>;\n}\n\n/** Line-span identity a candidate id resolves to, for post-fusion expansion. */\nexport interface CandidateSpan {\n\tpath: string;\n\t/** 1-based inclusive. */\n\tstartLine: number;\n\t/** 1-based inclusive. May exceed the file's length; readers clamp. */\n\tendLine: number;\n}\n\nexport interface FusedCandidate extends FusedHit, CandidateSpan {}\n\n/** Per-call diagnostic record, written to the store-dir trace jsonl — never\n * into model context. */\nexport interface SearchTrace {\n\ttimestampMs: number;\n\tquery: string;\n\trequestedMode: SearchMode;\n\tresolvedMode: ResolvedSearchMode;\n\t/** Set when the resolved mode is a degradation of the requested one. */\n\tdegradedReason?: string;\n\tindexPhase: \"ready\" | \"indexing\" | \"unavailable\";\n\trrfK?: number;\n\tretrievers: Partial<Record<RetrieverSource, { latencyMs: number; hitCount: number }>>;\n\tfused: FusedHit[];\n\trerank?: { applied: boolean; candidateCount: number; latencyMs: number };\n}\n"]}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared types for hybrid code retrieval (see docs/hybrid-retrieval-design.md).
|
|
3
|
+
*
|
|
4
|
+
* Identity note: candidate ids are per-index-build chunk ids (`relpath#index`)
|
|
5
|
+
* from the embedding sidecar, or synthetic `relpath#L<line>` fallback ids for
|
|
6
|
+
* grep hits in files the index does not cover. They are stable within one
|
|
7
|
+
* query (fusion and span expansion consult the same sidecar snapshot) but not
|
|
8
|
+
* across edits or rebuilds — never persist them as durable references.
|
|
9
|
+
*/
|
|
10
|
+
export {};
|
|
11
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../../src/core/search/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG","sourcesContent":["/**\n * Shared types for hybrid code retrieval (see docs/hybrid-retrieval-design.md).\n *\n * Identity note: candidate ids are per-index-build chunk ids (`relpath#index`)\n * from the embedding sidecar, or synthetic `relpath#L<line>` fallback ids for\n * grep hits in files the index does not cover. They are stable within one\n * query (fusion and span expansion consult the same sidecar snapshot) but not\n * across edits or rebuilds — never persist them as durable references.\n */\n\nexport type RetrieverSource = \"grep\" | \"embed\";\n\nexport type SearchMode = \"auto\" | \"lexical\" | \"semantic\" | \"hybrid\";\nexport type ResolvedSearchMode = Exclude<SearchMode, \"auto\">;\n\nexport interface RankedHit {\n\tid: string;\n\t/** 1-indexed, gap-free rank within its retriever's list. */\n\trank: number;\n\t/** Retriever-local score (BM25-ish, cosine, …). Diagnostics only — never fused. */\n\tscore?: number;\n\tsource: RetrieverSource;\n}\n\nexport interface FusedHit {\n\tid: string;\n\trrfScore: number;\n\t/** Best rank per contributing retriever. */\n\tranks: Partial<Record<RetrieverSource, number>>;\n\t/** Raw score at the best rank per retriever. Diagnostics only. */\n\trawScores: Partial<Record<RetrieverSource, number>>;\n}\n\n/** Line-span identity a candidate id resolves to, for post-fusion expansion. */\nexport interface CandidateSpan {\n\tpath: string;\n\t/** 1-based inclusive. */\n\tstartLine: number;\n\t/** 1-based inclusive. May exceed the file's length; readers clamp. */\n\tendLine: number;\n}\n\nexport interface FusedCandidate extends FusedHit, CandidateSpan {}\n\n/** Per-call diagnostic record, written to the store-dir trace jsonl — never\n * into model context. */\nexport interface SearchTrace {\n\ttimestampMs: number;\n\tquery: string;\n\trequestedMode: SearchMode;\n\tresolvedMode: ResolvedSearchMode;\n\t/** Set when the resolved mode is a degradation of the requested one. */\n\tdegradedReason?: string;\n\tindexPhase: \"ready\" | \"indexing\" | \"unavailable\";\n\trrfK?: number;\n\tretrievers: Partial<Record<RetrieverSource, { latencyMs: number; hitCount: number }>>;\n\tfused: FusedHit[];\n\trerank?: { applied: boolean; candidateCount: number; latencyMs: number };\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"settings-types.d.ts","sourceRoot":"","sources":["../../src/core/settings-types.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Settings schema shared across the app.\n *\n * The `Settings` interface and its nested option groups describe the on-disk\n * global/project settings.json shape. Extracted from settings-manager.ts so the\n * schema can be imported without pulling in the manager implementation.\n */\n\nimport type { Transport } from \"@kolisachint/hoocode-ai\";\n\nexport interface CompactionSettings {\n\tenabled?: boolean; // default: true\n\treserveTokens?: number; // default: 16384\n\tkeepRecentTokens?: number; // default: 20000\n\tmaxContextRatio?: number; // default: 0.75 - compact once context exceeds this fraction of the window, even before the reserveTokens rule fires (bounds transcript growth on large windows)\n}\n\nexport interface ToolOutputSettings {\n\tmaxBytes?: number; // default: 16384 (16KB) - byte cap on a single read/bash tool result before truncation\n\tmaxLines?: number; // default: 800 - line cap on a single read/bash tool result before truncation\n}\n\nexport interface ContextGcSettings {\n\tenabled?: boolean; // default: true - stub out superseded read results (file later edited/re-read) from the outgoing context\n}\n\nexport interface BranchSummarySettings {\n\treserveTokens?: number; // default: 16384 (tokens reserved for prompt + LLM response)\n\tskipPrompt?: boolean; // default: false - when true, skips \"Summarize branch?\" prompt and defaults to no summary\n}\n\nexport interface ProviderRetrySettings {\n\ttimeoutMs?: number; // SDK/provider request timeout in milliseconds\n\tmaxRetries?: number; // SDK/provider retry attempts\n\tmaxRetryDelayMs?: number; // default: 60000 (max server-requested delay before failing)\n}\n\nexport interface RetrySettings {\n\tenabled?: boolean; // default: true\n\tmaxRetries?: number; // default: 3\n\tbaseDelayMs?: number; // default: 2000 (exponential backoff: 2s, 4s, 8s)\n\tprovider?: ProviderRetrySettings;\n}\n\nexport interface TerminalSettings {\n\tshowImages?: boolean; // default: true (only relevant if terminal supports images)\n\timageWidthCells?: number; // default: 60 (preferred inline image width in terminal cells)\n\tclearOnShrink?: boolean; // default: false (clear empty rows when content shrinks)\n\tshowTerminalProgress?: boolean; // default: false (OSC 9;4 terminal progress indicators)\n\tchimeOnTurnComplete?: boolean; // default: false (ring the terminal bell when a long turn finishes or the agent asks for input)\n}\n\nexport interface ImageSettings {\n\tautoResize?: boolean; // default: true (resize images to 2000x2000 max for better model compatibility)\n\tblockImages?: boolean; // default: false - when true, prevents all images from being sent to LLM providers\n}\n\nexport interface ThinkingBudgetsSettings {\n\tminimal?: number;\n\tlow?: number;\n\tmedium?: number;\n\thigh?: number;\n}\n\nexport interface MarkdownSettings {\n\tcodeBlockIndent?: string; // default: \" \"\n}\n\nexport interface WarningSettings {\n\tanthropicExtraUsage?: boolean; // default: true\n}\n\n/**\n * Model categories for subagent model selection.\n * Categories map to explicit model IDs (e.g., \"<provider>/<model-id>\").\n *\n * A field set here always wins. When a tier is left unset, it does NOT become a\n * no-op: it resolves to a default *derived* from the user's available models\n * (nothing is hardcoded, so the feature stays provider-neutral). The derivation,\n * implemented in `deriveDefaultModelCategories` (core/model-categories.ts), is:\n * - `capable` = the user's primary model — the configured default\n * (`defaultProvider`/`defaultModel`) when available, else the most capable\n * available model (highest combined input+output token price as a proxy).\n * - `fast` / `standard` = the cheapest / upper-median of every available model\n * priced at or below `capable`, ordered cheapest-first. Clamping to\n * `capable`'s price keeps tiers monotonic (`fast` <= `standard` <= `capable`).\n * Ties break on a fixed key (context window, then id), so identical inputs always\n * yield the same mapping. When no models are available, tiers stay unresolved and\n * the agent's or parent's default model is used.\n */\nexport interface ModelCategories {\n\t/** Quick, cheap models for read-only exploration (grep, find, file discovery) */\n\tfast?: string;\n\t/** Balanced models for general work (planning, moderate complexity) */\n\tstandard?: string;\n\t/** Most capable models for complex reasoning (multi-file refactors) */\n\tcapable?: string;\n}\n\nexport type TransportSetting = Transport;\n\n/**\n * Package source for npm/git packages.\n * - String form: load all resources from the package\n * - Object form: filter which resources to load\n */\nexport type PackageSource =\n\t| string\n\t| {\n\t\t\tsource: string;\n\t\t\textensions?: string[];\n\t\t\tskills?: string[];\n\t\t\tprompts?: string[];\n\t\t\tthemes?: string[];\n\t };\n\nexport interface Settings {\n\tlastChangelogVersion?: string;\n\tdefaultProvider?: string;\n\tdefaultModel?: string;\n\tdefaultThinkingLevel?: \"off\" | \"minimal\" | \"low\" | \"medium\" | \"high\" | \"xhigh\";\n\tmodelCategories?: ModelCategories; // Model categories for subagent model selection (fast, standard, capable)\n\ttransport?: TransportSetting; // default: \"auto\"\n\tsteeringMode?: \"all\" | \"one-at-a-time\";\n\tfollowUpMode?: \"all\" | \"one-at-a-time\";\n\ttheme?: string;\n\tcompaction?: CompactionSettings;\n\ttoolOutput?: ToolOutputSettings; // caps on a single read/bash result (bounds per-turn transcript growth)\n\tcontextGc?: ContextGcSettings; // garbage-collect superseded read results from the outgoing context\n\tbranchSummary?: BranchSummarySettings;\n\tretry?: RetrySettings;\n\thideThinkingBlock?: boolean;\n\tshellPath?: string; // Custom shell path (e.g., for Cygwin users on Windows)\n\tquietStartup?: boolean;\n\tshellCommandPrefix?: string; // Prefix prepended to every bash command (e.g., \"shopt -s expand_aliases\" for alias support)\n\tnpmCommand?: string[]; // Command used for npm package lookup/install operations, argv-style (e.g., [\"mise\", \"exec\", \"node@20\", \"--\", \"npm\"])\n\tcollapseChangelog?: boolean; // Show condensed changelog after update (use /changelog for full)\n\tenableInstallTelemetry?: boolean; // default: true - anonymous version/update ping after changelog-detected updates\n\tpackages?: PackageSource[]; // Array of npm/git package sources (string or object with filtering)\n\textensions?: string[]; // Array of local extension file paths or directories\n\tskills?: string[]; // Array of local skill file paths or directories\n\tprompts?: string[]; // Array of local prompt template paths or directories\n\tslashCommands?: string[]; // Array of local slash-command paths or directories\n\tthemes?: string[]; // Array of local theme file paths or directories\n\tenableSkillCommands?: boolean; // default: true - register skills as /skill:name commands\n\tenableSubagent?: boolean; // default: true - enable the subagent tool (delegate tasks to isolated agent loops); set false to disable\n\twarmSubagents?: boolean; // default: false - dispatch eligible subagents on reused warm RPC workers (experimental)\n\tmaxSubagentDepth?: number; // default: 2 - tree-wide subagent nesting cap (2 = a subagent may spawn one more level; 1 = no nesting)\n\tnestedSubagentConcurrency?: number; // default: 2 - max concurrent subagents per pool at nesting depth >= 1\n\tenableTodoWrite?: boolean; // default: true - enable the TodoWrite tool (maintain a live todo list in the task panel)\n\tenablePluginTools?: boolean; // default: false - master switch for the whole autonomous plugin system: the plugin lifecycle tools (SearchPlugins, InstallPlugin, ...) and ProposePlugin on the top-level agent AND the runtime plugin-reuse nudge. Off by default; set true to opt in.\n\tsupportPlatform?: string | string[]; // Platform layout(s) hoocode targets when writing artifacts (authored plugins, /new-* scaffolds). Tokens: claude, copilot|github|gh, agents|native. Same as the --support-platform CLI flag (which overrides this).\n\tdeferMcpSchemas?: boolean; // default: true - defer MCP tool schemas (inject names only + ResolveMcpTools on demand) instead of registering every schema up front; set false to eagerly register every schema\n\tenableWebTools?: boolean; // default: false - enable the webfetch + websearch tools (network access)\n\tenableBrowserTools?: boolean; // default: false - enable the browser_run + browser_continue tools (browsertools engine)\n\tenableBrowserLivePreview?: boolean; // default: false - default the live viewer on for browser_run runs and auto-open it\n\tenableFileTools?: boolean; // default: false - enable the document tools: DocRead/DocEdit/DocWrite + DocScan/DocGrep/DocPeek (filetools binary)\n\tenableEmbsearchTools?: boolean; // default: false - enable semantic code search: index the repo with the embsearch binary and register the semantic_search tool. grep/find are unchanged.\n\tembsearchBinaryPath?: string; // Path to the embsearch binary. Default: resolve \"embsearch\" from PATH.\n\tembsearchThresholdBytes?: number; // default: 10485760 (10 MB) - minimum indexable source bytes before a repo is embedded; smaller repos skip embedding entirely\n\tlight?: boolean; // default: false - minimal low-token preset for small/local models: read/write/edit/bash only (short schemas), terse prompt, no subagents/TodoWrite/skills/context files/mode appendix. Same as the --light CLI flag.\n\tterminal?: TerminalSettings;\n\timages?: ImageSettings;\n\tenabledModels?: string[]; // Model patterns for cycling (same format as --models CLI flag)\n\tdoubleEscapeAction?: \"fork\" | \"tree\" | \"none\"; // Action for double-escape with empty editor (default: \"tree\")\n\ttreeFilterMode?: \"default\" | \"no-tools\" | \"user-only\" | \"labeled-only\" | \"all\"; // Default filter when opening /tree\n\tthinkingBudgets?: ThinkingBudgetsSettings; // Custom token budgets for thinking levels\n\tthinkingDisplay?: \"summarized\" | \"omitted\"; // How adaptive-thinking models return thinking content. Opus 4.8 defaults to \"omitted\" (faster tool use); set \"summarized\" to surface thinking text.\n\teditorPaddingX?: number; // Horizontal padding for input editor (default: 0)\n\tautocompleteMaxVisible?: number; // Max visible items in autocomplete dropdown (default: 5)\n\tshowHardwareCursor?: boolean; // Show terminal cursor while still positioning it for IME\n\tmarkdown?: MarkdownSettings;\n\twarnings?: WarningSettings;\n\tsessionDir?: string; // Custom session storage directory (same format as --session-dir CLI flag)\n}\n"]}
|
|
1
|
+
{"version":3,"file":"settings-types.d.ts","sourceRoot":"","sources":["../../src/core/settings-types.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Settings schema shared across the app.\n *\n * The `Settings` interface and its nested option groups describe the on-disk\n * global/project settings.json shape. Extracted from settings-manager.ts so the\n * schema can be imported without pulling in the manager implementation.\n */\n\nimport type { Transport } from \"@kolisachint/hoocode-ai\";\n\nexport interface CompactionSettings {\n\tenabled?: boolean; // default: true\n\treserveTokens?: number; // default: 16384\n\tkeepRecentTokens?: number; // default: 20000\n\tmaxContextRatio?: number; // default: 0.75 - compact once context exceeds this fraction of the window, even before the reserveTokens rule fires (bounds transcript growth on large windows)\n}\n\nexport interface ToolOutputSettings {\n\tmaxBytes?: number; // default: 16384 (16KB) - byte cap on a single read/bash tool result before truncation\n\tmaxLines?: number; // default: 800 - line cap on a single read/bash tool result before truncation\n}\n\nexport interface ContextGcSettings {\n\tenabled?: boolean; // default: true - stub out superseded read results (file later edited/re-read) from the outgoing context\n}\n\nexport interface BranchSummarySettings {\n\treserveTokens?: number; // default: 16384 (tokens reserved for prompt + LLM response)\n\tskipPrompt?: boolean; // default: false - when true, skips \"Summarize branch?\" prompt and defaults to no summary\n}\n\nexport interface ProviderRetrySettings {\n\ttimeoutMs?: number; // SDK/provider request timeout in milliseconds\n\tmaxRetries?: number; // SDK/provider retry attempts\n\tmaxRetryDelayMs?: number; // default: 60000 (max server-requested delay before failing)\n}\n\nexport interface RetrySettings {\n\tenabled?: boolean; // default: true\n\tmaxRetries?: number; // default: 3\n\tbaseDelayMs?: number; // default: 2000 (exponential backoff: 2s, 4s, 8s)\n\tprovider?: ProviderRetrySettings;\n}\n\nexport interface TerminalSettings {\n\tshowImages?: boolean; // default: true (only relevant if terminal supports images)\n\timageWidthCells?: number; // default: 60 (preferred inline image width in terminal cells)\n\tclearOnShrink?: boolean; // default: false (clear empty rows when content shrinks)\n\tshowTerminalProgress?: boolean; // default: false (OSC 9;4 terminal progress indicators)\n\tchimeOnTurnComplete?: boolean; // default: false (ring the terminal bell when a long turn finishes or the agent asks for input)\n}\n\nexport interface ImageSettings {\n\tautoResize?: boolean; // default: true (resize images to 2000x2000 max for better model compatibility)\n\tblockImages?: boolean; // default: false - when true, prevents all images from being sent to LLM providers\n}\n\nexport interface ThinkingBudgetsSettings {\n\tminimal?: number;\n\tlow?: number;\n\tmedium?: number;\n\thigh?: number;\n}\n\nexport interface MarkdownSettings {\n\tcodeBlockIndent?: string; // default: \" \"\n}\n\nexport interface WarningSettings {\n\tanthropicExtraUsage?: boolean; // default: true\n}\n\n/**\n * Model categories for subagent model selection.\n * Categories map to explicit model IDs (e.g., \"<provider>/<model-id>\").\n *\n * A field set here always wins. When a tier is left unset, it does NOT become a\n * no-op: it resolves to a default *derived* from the user's available models\n * (nothing is hardcoded, so the feature stays provider-neutral). The derivation,\n * implemented in `deriveDefaultModelCategories` (core/model-categories.ts), is:\n * - `capable` = the user's primary model — the configured default\n * (`defaultProvider`/`defaultModel`) when available, else the most capable\n * available model (highest combined input+output token price as a proxy).\n * - `fast` / `standard` = the cheapest / upper-median of every available model\n * priced at or below `capable`, ordered cheapest-first. Clamping to\n * `capable`'s price keeps tiers monotonic (`fast` <= `standard` <= `capable`).\n * Ties break on a fixed key (context window, then id), so identical inputs always\n * yield the same mapping. When no models are available, tiers stay unresolved and\n * the agent's or parent's default model is used.\n */\nexport interface ModelCategories {\n\t/** Quick, cheap models for read-only exploration (grep, find, file discovery) */\n\tfast?: string;\n\t/** Balanced models for general work (planning, moderate complexity) */\n\tstandard?: string;\n\t/** Most capable models for complex reasoning (multi-file refactors) */\n\tcapable?: string;\n}\n\nexport type TransportSetting = Transport;\n\n/**\n * Package source for npm/git packages.\n * - String form: load all resources from the package\n * - Object form: filter which resources to load\n */\nexport type PackageSource =\n\t| string\n\t| {\n\t\t\tsource: string;\n\t\t\textensions?: string[];\n\t\t\tskills?: string[];\n\t\t\tprompts?: string[];\n\t\t\tthemes?: string[];\n\t };\n\nexport interface Settings {\n\tlastChangelogVersion?: string;\n\tdefaultProvider?: string;\n\tdefaultModel?: string;\n\tdefaultThinkingLevel?: \"off\" | \"minimal\" | \"low\" | \"medium\" | \"high\" | \"xhigh\";\n\tmodelCategories?: ModelCategories; // Model categories for subagent model selection (fast, standard, capable)\n\ttransport?: TransportSetting; // default: \"auto\"\n\tsteeringMode?: \"all\" | \"one-at-a-time\";\n\tfollowUpMode?: \"all\" | \"one-at-a-time\";\n\ttheme?: string;\n\tcompaction?: CompactionSettings;\n\ttoolOutput?: ToolOutputSettings; // caps on a single read/bash result (bounds per-turn transcript growth)\n\tcontextGc?: ContextGcSettings; // garbage-collect superseded read results from the outgoing context\n\tbranchSummary?: BranchSummarySettings;\n\tretry?: RetrySettings;\n\thideThinkingBlock?: boolean;\n\tshellPath?: string; // Custom shell path (e.g., for Cygwin users on Windows)\n\tquietStartup?: boolean;\n\tshellCommandPrefix?: string; // Prefix prepended to every bash command (e.g., \"shopt -s expand_aliases\" for alias support)\n\tnpmCommand?: string[]; // Command used for npm package lookup/install operations, argv-style (e.g., [\"mise\", \"exec\", \"node@20\", \"--\", \"npm\"])\n\tcollapseChangelog?: boolean; // Show condensed changelog after update (use /changelog for full)\n\tenableInstallTelemetry?: boolean; // default: true - anonymous version/update ping after changelog-detected updates\n\tpackages?: PackageSource[]; // Array of npm/git package sources (string or object with filtering)\n\textensions?: string[]; // Array of local extension file paths or directories\n\tskills?: string[]; // Array of local skill file paths or directories\n\tprompts?: string[]; // Array of local prompt template paths or directories\n\tslashCommands?: string[]; // Array of local slash-command paths or directories\n\tthemes?: string[]; // Array of local theme file paths or directories\n\tenableSkillCommands?: boolean; // default: true - register skills as /skill:name commands\n\tenableSubagent?: boolean; // default: true - enable the subagent tool (delegate tasks to isolated agent loops); set false to disable\n\twarmSubagents?: boolean; // default: false - dispatch eligible subagents on reused warm RPC workers (experimental)\n\tmaxSubagentDepth?: number; // default: 2 - tree-wide subagent nesting cap (2 = a subagent may spawn one more level; 1 = no nesting)\n\tnestedSubagentConcurrency?: number; // default: 2 - max concurrent subagents per pool at nesting depth >= 1\n\tenableTodoWrite?: boolean; // default: true - enable the TodoWrite tool (maintain a live todo list in the task panel)\n\tenablePluginTools?: boolean; // default: false - master switch for the whole autonomous plugin system: the plugin lifecycle tools (SearchPlugins, InstallPlugin, ...) and ProposePlugin on the top-level agent AND the runtime plugin-reuse nudge. Off by default; set true to opt in.\n\tsupportPlatform?: string | string[]; // Platform layout(s) hoocode targets when writing artifacts (authored plugins, /new-* scaffolds). Tokens: claude, copilot|github|gh, agents|native. Same as the --support-platform CLI flag (which overrides this).\n\tdeferMcpSchemas?: boolean; // default: true - defer MCP tool schemas (inject names only + ResolveMcpTools on demand) instead of registering every schema up front; set false to eagerly register every schema\n\tenableWebTools?: boolean; // default: false - enable the webfetch + websearch tools (network access)\n\tenableBrowserTools?: boolean; // default: false - enable the browser_run + browser_continue tools (browsertools engine)\n\tenableBrowserLivePreview?: boolean; // default: false - default the live viewer on for browser_run runs and auto-open it\n\tenableFileTools?: boolean; // default: false - enable the document tools: DocRead/DocEdit/DocWrite + DocScan/DocGrep/DocPeek (filetools binary)\n\tenableEmbsearchTools?: boolean; // default: false - enable the ranked search tool (lexical + semantic hybrid): index the repo with the embsearch binary and register the search tool. grep/find are unchanged.\n\tembsearchBinaryPath?: string; // Path to the embsearch binary. Default: resolve \"embsearch\" from PATH.\n\tembsearchThresholdBytes?: number; // default: 10485760 (10 MB) - minimum indexable source bytes before a repo is embedded; smaller repos skip embedding entirely\n\tlight?: boolean; // default: false - minimal low-token preset for small/local models: read/write/edit/bash only (short schemas), terse prompt, no subagents/TodoWrite/skills/context files/mode appendix. Same as the --light CLI flag.\n\tterminal?: TerminalSettings;\n\timages?: ImageSettings;\n\tenabledModels?: string[]; // Model patterns for cycling (same format as --models CLI flag)\n\tdoubleEscapeAction?: \"fork\" | \"tree\" | \"none\"; // Action for double-escape with empty editor (default: \"tree\")\n\ttreeFilterMode?: \"default\" | \"no-tools\" | \"user-only\" | \"labeled-only\" | \"all\"; // Default filter when opening /tree\n\tthinkingBudgets?: ThinkingBudgetsSettings; // Custom token budgets for thinking levels\n\tthinkingDisplay?: \"summarized\" | \"omitted\"; // How adaptive-thinking models return thinking content. Opus 4.8 defaults to \"omitted\" (faster tool use); set \"summarized\" to surface thinking text.\n\teditorPaddingX?: number; // Horizontal padding for input editor (default: 0)\n\tautocompleteMaxVisible?: number; // Max visible items in autocomplete dropdown (default: 5)\n\tshowHardwareCursor?: boolean; // Show terminal cursor while still positioning it for IME\n\tmarkdown?: MarkdownSettings;\n\twarnings?: WarningSettings;\n\tsessionDir?: string; // Custom session storage directory (same format as --session-dir CLI flag)\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"settings-types.js","sourceRoot":"","sources":["../../src/core/settings-types.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG","sourcesContent":["/**\n * Settings schema shared across the app.\n *\n * The `Settings` interface and its nested option groups describe the on-disk\n * global/project settings.json shape. Extracted from settings-manager.ts so the\n * schema can be imported without pulling in the manager implementation.\n */\n\nimport type { Transport } from \"@kolisachint/hoocode-ai\";\n\nexport interface CompactionSettings {\n\tenabled?: boolean; // default: true\n\treserveTokens?: number; // default: 16384\n\tkeepRecentTokens?: number; // default: 20000\n\tmaxContextRatio?: number; // default: 0.75 - compact once context exceeds this fraction of the window, even before the reserveTokens rule fires (bounds transcript growth on large windows)\n}\n\nexport interface ToolOutputSettings {\n\tmaxBytes?: number; // default: 16384 (16KB) - byte cap on a single read/bash tool result before truncation\n\tmaxLines?: number; // default: 800 - line cap on a single read/bash tool result before truncation\n}\n\nexport interface ContextGcSettings {\n\tenabled?: boolean; // default: true - stub out superseded read results (file later edited/re-read) from the outgoing context\n}\n\nexport interface BranchSummarySettings {\n\treserveTokens?: number; // default: 16384 (tokens reserved for prompt + LLM response)\n\tskipPrompt?: boolean; // default: false - when true, skips \"Summarize branch?\" prompt and defaults to no summary\n}\n\nexport interface ProviderRetrySettings {\n\ttimeoutMs?: number; // SDK/provider request timeout in milliseconds\n\tmaxRetries?: number; // SDK/provider retry attempts\n\tmaxRetryDelayMs?: number; // default: 60000 (max server-requested delay before failing)\n}\n\nexport interface RetrySettings {\n\tenabled?: boolean; // default: true\n\tmaxRetries?: number; // default: 3\n\tbaseDelayMs?: number; // default: 2000 (exponential backoff: 2s, 4s, 8s)\n\tprovider?: ProviderRetrySettings;\n}\n\nexport interface TerminalSettings {\n\tshowImages?: boolean; // default: true (only relevant if terminal supports images)\n\timageWidthCells?: number; // default: 60 (preferred inline image width in terminal cells)\n\tclearOnShrink?: boolean; // default: false (clear empty rows when content shrinks)\n\tshowTerminalProgress?: boolean; // default: false (OSC 9;4 terminal progress indicators)\n\tchimeOnTurnComplete?: boolean; // default: false (ring the terminal bell when a long turn finishes or the agent asks for input)\n}\n\nexport interface ImageSettings {\n\tautoResize?: boolean; // default: true (resize images to 2000x2000 max for better model compatibility)\n\tblockImages?: boolean; // default: false - when true, prevents all images from being sent to LLM providers\n}\n\nexport interface ThinkingBudgetsSettings {\n\tminimal?: number;\n\tlow?: number;\n\tmedium?: number;\n\thigh?: number;\n}\n\nexport interface MarkdownSettings {\n\tcodeBlockIndent?: string; // default: \" \"\n}\n\nexport interface WarningSettings {\n\tanthropicExtraUsage?: boolean; // default: true\n}\n\n/**\n * Model categories for subagent model selection.\n * Categories map to explicit model IDs (e.g., \"<provider>/<model-id>\").\n *\n * A field set here always wins. When a tier is left unset, it does NOT become a\n * no-op: it resolves to a default *derived* from the user's available models\n * (nothing is hardcoded, so the feature stays provider-neutral). The derivation,\n * implemented in `deriveDefaultModelCategories` (core/model-categories.ts), is:\n * - `capable` = the user's primary model — the configured default\n * (`defaultProvider`/`defaultModel`) when available, else the most capable\n * available model (highest combined input+output token price as a proxy).\n * - `fast` / `standard` = the cheapest / upper-median of every available model\n * priced at or below `capable`, ordered cheapest-first. Clamping to\n * `capable`'s price keeps tiers monotonic (`fast` <= `standard` <= `capable`).\n * Ties break on a fixed key (context window, then id), so identical inputs always\n * yield the same mapping. When no models are available, tiers stay unresolved and\n * the agent's or parent's default model is used.\n */\nexport interface ModelCategories {\n\t/** Quick, cheap models for read-only exploration (grep, find, file discovery) */\n\tfast?: string;\n\t/** Balanced models for general work (planning, moderate complexity) */\n\tstandard?: string;\n\t/** Most capable models for complex reasoning (multi-file refactors) */\n\tcapable?: string;\n}\n\nexport type TransportSetting = Transport;\n\n/**\n * Package source for npm/git packages.\n * - String form: load all resources from the package\n * - Object form: filter which resources to load\n */\nexport type PackageSource =\n\t| string\n\t| {\n\t\t\tsource: string;\n\t\t\textensions?: string[];\n\t\t\tskills?: string[];\n\t\t\tprompts?: string[];\n\t\t\tthemes?: string[];\n\t };\n\nexport interface Settings {\n\tlastChangelogVersion?: string;\n\tdefaultProvider?: string;\n\tdefaultModel?: string;\n\tdefaultThinkingLevel?: \"off\" | \"minimal\" | \"low\" | \"medium\" | \"high\" | \"xhigh\";\n\tmodelCategories?: ModelCategories; // Model categories for subagent model selection (fast, standard, capable)\n\ttransport?: TransportSetting; // default: \"auto\"\n\tsteeringMode?: \"all\" | \"one-at-a-time\";\n\tfollowUpMode?: \"all\" | \"one-at-a-time\";\n\ttheme?: string;\n\tcompaction?: CompactionSettings;\n\ttoolOutput?: ToolOutputSettings; // caps on a single read/bash result (bounds per-turn transcript growth)\n\tcontextGc?: ContextGcSettings; // garbage-collect superseded read results from the outgoing context\n\tbranchSummary?: BranchSummarySettings;\n\tretry?: RetrySettings;\n\thideThinkingBlock?: boolean;\n\tshellPath?: string; // Custom shell path (e.g., for Cygwin users on Windows)\n\tquietStartup?: boolean;\n\tshellCommandPrefix?: string; // Prefix prepended to every bash command (e.g., \"shopt -s expand_aliases\" for alias support)\n\tnpmCommand?: string[]; // Command used for npm package lookup/install operations, argv-style (e.g., [\"mise\", \"exec\", \"node@20\", \"--\", \"npm\"])\n\tcollapseChangelog?: boolean; // Show condensed changelog after update (use /changelog for full)\n\tenableInstallTelemetry?: boolean; // default: true - anonymous version/update ping after changelog-detected updates\n\tpackages?: PackageSource[]; // Array of npm/git package sources (string or object with filtering)\n\textensions?: string[]; // Array of local extension file paths or directories\n\tskills?: string[]; // Array of local skill file paths or directories\n\tprompts?: string[]; // Array of local prompt template paths or directories\n\tslashCommands?: string[]; // Array of local slash-command paths or directories\n\tthemes?: string[]; // Array of local theme file paths or directories\n\tenableSkillCommands?: boolean; // default: true - register skills as /skill:name commands\n\tenableSubagent?: boolean; // default: true - enable the subagent tool (delegate tasks to isolated agent loops); set false to disable\n\twarmSubagents?: boolean; // default: false - dispatch eligible subagents on reused warm RPC workers (experimental)\n\tmaxSubagentDepth?: number; // default: 2 - tree-wide subagent nesting cap (2 = a subagent may spawn one more level; 1 = no nesting)\n\tnestedSubagentConcurrency?: number; // default: 2 - max concurrent subagents per pool at nesting depth >= 1\n\tenableTodoWrite?: boolean; // default: true - enable the TodoWrite tool (maintain a live todo list in the task panel)\n\tenablePluginTools?: boolean; // default: false - master switch for the whole autonomous plugin system: the plugin lifecycle tools (SearchPlugins, InstallPlugin, ...) and ProposePlugin on the top-level agent AND the runtime plugin-reuse nudge. Off by default; set true to opt in.\n\tsupportPlatform?: string | string[]; // Platform layout(s) hoocode targets when writing artifacts (authored plugins, /new-* scaffolds). Tokens: claude, copilot|github|gh, agents|native. Same as the --support-platform CLI flag (which overrides this).\n\tdeferMcpSchemas?: boolean; // default: true - defer MCP tool schemas (inject names only + ResolveMcpTools on demand) instead of registering every schema up front; set false to eagerly register every schema\n\tenableWebTools?: boolean; // default: false - enable the webfetch + websearch tools (network access)\n\tenableBrowserTools?: boolean; // default: false - enable the browser_run + browser_continue tools (browsertools engine)\n\tenableBrowserLivePreview?: boolean; // default: false - default the live viewer on for browser_run runs and auto-open it\n\tenableFileTools?: boolean; // default: false - enable the document tools: DocRead/DocEdit/DocWrite + DocScan/DocGrep/DocPeek (filetools binary)\n\tenableEmbsearchTools?: boolean; // default: false - enable semantic code search: index the repo with the embsearch binary and register the semantic_search tool. grep/find are unchanged.\n\tembsearchBinaryPath?: string; // Path to the embsearch binary. Default: resolve \"embsearch\" from PATH.\n\tembsearchThresholdBytes?: number; // default: 10485760 (10 MB) - minimum indexable source bytes before a repo is embedded; smaller repos skip embedding entirely\n\tlight?: boolean; // default: false - minimal low-token preset for small/local models: read/write/edit/bash only (short schemas), terse prompt, no subagents/TodoWrite/skills/context files/mode appendix. Same as the --light CLI flag.\n\tterminal?: TerminalSettings;\n\timages?: ImageSettings;\n\tenabledModels?: string[]; // Model patterns for cycling (same format as --models CLI flag)\n\tdoubleEscapeAction?: \"fork\" | \"tree\" | \"none\"; // Action for double-escape with empty editor (default: \"tree\")\n\ttreeFilterMode?: \"default\" | \"no-tools\" | \"user-only\" | \"labeled-only\" | \"all\"; // Default filter when opening /tree\n\tthinkingBudgets?: ThinkingBudgetsSettings; // Custom token budgets for thinking levels\n\tthinkingDisplay?: \"summarized\" | \"omitted\"; // How adaptive-thinking models return thinking content. Opus 4.8 defaults to \"omitted\" (faster tool use); set \"summarized\" to surface thinking text.\n\teditorPaddingX?: number; // Horizontal padding for input editor (default: 0)\n\tautocompleteMaxVisible?: number; // Max visible items in autocomplete dropdown (default: 5)\n\tshowHardwareCursor?: boolean; // Show terminal cursor while still positioning it for IME\n\tmarkdown?: MarkdownSettings;\n\twarnings?: WarningSettings;\n\tsessionDir?: string; // Custom session storage directory (same format as --session-dir CLI flag)\n}\n"]}
|
|
1
|
+
{"version":3,"file":"settings-types.js","sourceRoot":"","sources":["../../src/core/settings-types.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG","sourcesContent":["/**\n * Settings schema shared across the app.\n *\n * The `Settings` interface and its nested option groups describe the on-disk\n * global/project settings.json shape. Extracted from settings-manager.ts so the\n * schema can be imported without pulling in the manager implementation.\n */\n\nimport type { Transport } from \"@kolisachint/hoocode-ai\";\n\nexport interface CompactionSettings {\n\tenabled?: boolean; // default: true\n\treserveTokens?: number; // default: 16384\n\tkeepRecentTokens?: number; // default: 20000\n\tmaxContextRatio?: number; // default: 0.75 - compact once context exceeds this fraction of the window, even before the reserveTokens rule fires (bounds transcript growth on large windows)\n}\n\nexport interface ToolOutputSettings {\n\tmaxBytes?: number; // default: 16384 (16KB) - byte cap on a single read/bash tool result before truncation\n\tmaxLines?: number; // default: 800 - line cap on a single read/bash tool result before truncation\n}\n\nexport interface ContextGcSettings {\n\tenabled?: boolean; // default: true - stub out superseded read results (file later edited/re-read) from the outgoing context\n}\n\nexport interface BranchSummarySettings {\n\treserveTokens?: number; // default: 16384 (tokens reserved for prompt + LLM response)\n\tskipPrompt?: boolean; // default: false - when true, skips \"Summarize branch?\" prompt and defaults to no summary\n}\n\nexport interface ProviderRetrySettings {\n\ttimeoutMs?: number; // SDK/provider request timeout in milliseconds\n\tmaxRetries?: number; // SDK/provider retry attempts\n\tmaxRetryDelayMs?: number; // default: 60000 (max server-requested delay before failing)\n}\n\nexport interface RetrySettings {\n\tenabled?: boolean; // default: true\n\tmaxRetries?: number; // default: 3\n\tbaseDelayMs?: number; // default: 2000 (exponential backoff: 2s, 4s, 8s)\n\tprovider?: ProviderRetrySettings;\n}\n\nexport interface TerminalSettings {\n\tshowImages?: boolean; // default: true (only relevant if terminal supports images)\n\timageWidthCells?: number; // default: 60 (preferred inline image width in terminal cells)\n\tclearOnShrink?: boolean; // default: false (clear empty rows when content shrinks)\n\tshowTerminalProgress?: boolean; // default: false (OSC 9;4 terminal progress indicators)\n\tchimeOnTurnComplete?: boolean; // default: false (ring the terminal bell when a long turn finishes or the agent asks for input)\n}\n\nexport interface ImageSettings {\n\tautoResize?: boolean; // default: true (resize images to 2000x2000 max for better model compatibility)\n\tblockImages?: boolean; // default: false - when true, prevents all images from being sent to LLM providers\n}\n\nexport interface ThinkingBudgetsSettings {\n\tminimal?: number;\n\tlow?: number;\n\tmedium?: number;\n\thigh?: number;\n}\n\nexport interface MarkdownSettings {\n\tcodeBlockIndent?: string; // default: \" \"\n}\n\nexport interface WarningSettings {\n\tanthropicExtraUsage?: boolean; // default: true\n}\n\n/**\n * Model categories for subagent model selection.\n * Categories map to explicit model IDs (e.g., \"<provider>/<model-id>\").\n *\n * A field set here always wins. When a tier is left unset, it does NOT become a\n * no-op: it resolves to a default *derived* from the user's available models\n * (nothing is hardcoded, so the feature stays provider-neutral). The derivation,\n * implemented in `deriveDefaultModelCategories` (core/model-categories.ts), is:\n * - `capable` = the user's primary model — the configured default\n * (`defaultProvider`/`defaultModel`) when available, else the most capable\n * available model (highest combined input+output token price as a proxy).\n * - `fast` / `standard` = the cheapest / upper-median of every available model\n * priced at or below `capable`, ordered cheapest-first. Clamping to\n * `capable`'s price keeps tiers monotonic (`fast` <= `standard` <= `capable`).\n * Ties break on a fixed key (context window, then id), so identical inputs always\n * yield the same mapping. When no models are available, tiers stay unresolved and\n * the agent's or parent's default model is used.\n */\nexport interface ModelCategories {\n\t/** Quick, cheap models for read-only exploration (grep, find, file discovery) */\n\tfast?: string;\n\t/** Balanced models for general work (planning, moderate complexity) */\n\tstandard?: string;\n\t/** Most capable models for complex reasoning (multi-file refactors) */\n\tcapable?: string;\n}\n\nexport type TransportSetting = Transport;\n\n/**\n * Package source for npm/git packages.\n * - String form: load all resources from the package\n * - Object form: filter which resources to load\n */\nexport type PackageSource =\n\t| string\n\t| {\n\t\t\tsource: string;\n\t\t\textensions?: string[];\n\t\t\tskills?: string[];\n\t\t\tprompts?: string[];\n\t\t\tthemes?: string[];\n\t };\n\nexport interface Settings {\n\tlastChangelogVersion?: string;\n\tdefaultProvider?: string;\n\tdefaultModel?: string;\n\tdefaultThinkingLevel?: \"off\" | \"minimal\" | \"low\" | \"medium\" | \"high\" | \"xhigh\";\n\tmodelCategories?: ModelCategories; // Model categories for subagent model selection (fast, standard, capable)\n\ttransport?: TransportSetting; // default: \"auto\"\n\tsteeringMode?: \"all\" | \"one-at-a-time\";\n\tfollowUpMode?: \"all\" | \"one-at-a-time\";\n\ttheme?: string;\n\tcompaction?: CompactionSettings;\n\ttoolOutput?: ToolOutputSettings; // caps on a single read/bash result (bounds per-turn transcript growth)\n\tcontextGc?: ContextGcSettings; // garbage-collect superseded read results from the outgoing context\n\tbranchSummary?: BranchSummarySettings;\n\tretry?: RetrySettings;\n\thideThinkingBlock?: boolean;\n\tshellPath?: string; // Custom shell path (e.g., for Cygwin users on Windows)\n\tquietStartup?: boolean;\n\tshellCommandPrefix?: string; // Prefix prepended to every bash command (e.g., \"shopt -s expand_aliases\" for alias support)\n\tnpmCommand?: string[]; // Command used for npm package lookup/install operations, argv-style (e.g., [\"mise\", \"exec\", \"node@20\", \"--\", \"npm\"])\n\tcollapseChangelog?: boolean; // Show condensed changelog after update (use /changelog for full)\n\tenableInstallTelemetry?: boolean; // default: true - anonymous version/update ping after changelog-detected updates\n\tpackages?: PackageSource[]; // Array of npm/git package sources (string or object with filtering)\n\textensions?: string[]; // Array of local extension file paths or directories\n\tskills?: string[]; // Array of local skill file paths or directories\n\tprompts?: string[]; // Array of local prompt template paths or directories\n\tslashCommands?: string[]; // Array of local slash-command paths or directories\n\tthemes?: string[]; // Array of local theme file paths or directories\n\tenableSkillCommands?: boolean; // default: true - register skills as /skill:name commands\n\tenableSubagent?: boolean; // default: true - enable the subagent tool (delegate tasks to isolated agent loops); set false to disable\n\twarmSubagents?: boolean; // default: false - dispatch eligible subagents on reused warm RPC workers (experimental)\n\tmaxSubagentDepth?: number; // default: 2 - tree-wide subagent nesting cap (2 = a subagent may spawn one more level; 1 = no nesting)\n\tnestedSubagentConcurrency?: number; // default: 2 - max concurrent subagents per pool at nesting depth >= 1\n\tenableTodoWrite?: boolean; // default: true - enable the TodoWrite tool (maintain a live todo list in the task panel)\n\tenablePluginTools?: boolean; // default: false - master switch for the whole autonomous plugin system: the plugin lifecycle tools (SearchPlugins, InstallPlugin, ...) and ProposePlugin on the top-level agent AND the runtime plugin-reuse nudge. Off by default; set true to opt in.\n\tsupportPlatform?: string | string[]; // Platform layout(s) hoocode targets when writing artifacts (authored plugins, /new-* scaffolds). Tokens: claude, copilot|github|gh, agents|native. Same as the --support-platform CLI flag (which overrides this).\n\tdeferMcpSchemas?: boolean; // default: true - defer MCP tool schemas (inject names only + ResolveMcpTools on demand) instead of registering every schema up front; set false to eagerly register every schema\n\tenableWebTools?: boolean; // default: false - enable the webfetch + websearch tools (network access)\n\tenableBrowserTools?: boolean; // default: false - enable the browser_run + browser_continue tools (browsertools engine)\n\tenableBrowserLivePreview?: boolean; // default: false - default the live viewer on for browser_run runs and auto-open it\n\tenableFileTools?: boolean; // default: false - enable the document tools: DocRead/DocEdit/DocWrite + DocScan/DocGrep/DocPeek (filetools binary)\n\tenableEmbsearchTools?: boolean; // default: false - enable the ranked search tool (lexical + semantic hybrid): index the repo with the embsearch binary and register the search tool. grep/find are unchanged.\n\tembsearchBinaryPath?: string; // Path to the embsearch binary. Default: resolve \"embsearch\" from PATH.\n\tembsearchThresholdBytes?: number; // default: 10485760 (10 MB) - minimum indexable source bytes before a repo is embedded; smaller repos skip embedding entirely\n\tlight?: boolean; // default: false - minimal low-token preset for small/local models: read/write/edit/bash only (short schemas), terse prompt, no subagents/TodoWrite/skills/context files/mode appendix. Same as the --light CLI flag.\n\tterminal?: TerminalSettings;\n\timages?: ImageSettings;\n\tenabledModels?: string[]; // Model patterns for cycling (same format as --models CLI flag)\n\tdoubleEscapeAction?: \"fork\" | \"tree\" | \"none\"; // Action for double-escape with empty editor (default: \"tree\")\n\ttreeFilterMode?: \"default\" | \"no-tools\" | \"user-only\" | \"labeled-only\" | \"all\"; // Default filter when opening /tree\n\tthinkingBudgets?: ThinkingBudgetsSettings; // Custom token budgets for thinking levels\n\tthinkingDisplay?: \"summarized\" | \"omitted\"; // How adaptive-thinking models return thinking content. Opus 4.8 defaults to \"omitted\" (faster tool use); set \"summarized\" to surface thinking text.\n\teditorPaddingX?: number; // Horizontal padding for input editor (default: 0)\n\tautocompleteMaxVisible?: number; // Max visible items in autocomplete dropdown (default: 5)\n\tshowHardwareCursor?: boolean; // Show terminal cursor while still positioning it for IME\n\tmarkdown?: MarkdownSettings;\n\twarnings?: WarningSettings;\n\tsessionDir?: string; // Custom session storage directory (same format as --session-dir CLI flag)\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"grep.d.ts","sourceRoot":"","sources":["../../../src/core/tools/grep.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iCAAiC,CAAC;AAMjE,OAAO,EAAE,KAAK,MAAM,EAAE,IAAI,EAAE,MAAM,SAAS,CAAC;AAI5C,OAAO,KAAK,EAAE,cAAc,EAA2B,MAAM,wBAAwB,CAAC;AAKtF,OAAO,EAIN,KAAK,gBAAgB,EAGrB,MAAM,eAAe,CAAC;AAEvB,QAAA,MAAM,UAAU;;;;;;;;EAYd,CAAC;AAEH,MAAM,MAAM,aAAa,GAAG,MAAM,CAAC,OAAO,UAAU,CAAC,CAAC;AAGtD,MAAM,WAAW,eAAe;IAC/B,UAAU,CAAC,EAAE,gBAAgB,CAAC;IAC9B,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,cAAc,CAAC,EAAE,OAAO,CAAC;CACzB;AAED;;;GAGG;AACH,MAAM,WAAW,cAAc;IAC9B,mEAAmE;IACnE,WAAW,EAAE,CAAC,YAAY,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,GAAG,OAAO,CAAC;IAClE,2CAA2C;IAC3C,QAAQ,EAAE,CAAC,YAAY,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,GAAG,MAAM,CAAC;CAC7D;AAOD,MAAM,WAAW,eAAe;IAC/B,yEAAyE;IACzE,UAAU,CAAC,EAAE,cAAc,CAAC;CAC5B;AAyDD,wBAAgB,wBAAwB,CACvC,GAAG,EAAE,MAAM,EACX,OAAO,CAAC,EAAE,eAAe,GACvB,cAAc,CAAC,OAAO,UAAU,EAAE,eAAe,GAAG,SAAS,CAAC,CAuUhE;AAED,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,eAAe,GAAG,SAAS,CAAC,OAAO,UAAU,CAAC,CAEnG","sourcesContent":["import { createInterface } from \"node:readline\";\nimport type { AgentTool } from \"@kolisachint/hoocode-agent-core\";\nimport { compressGrepOutput } from \"@kolisachint/hoocode-agent-core\";\nimport { Text } from \"@kolisachint/hoocode-tui\";\nimport { spawn } from \"child_process\";\nimport { readFileSync, statSync } from \"fs\";\nimport path from \"path\";\nimport { type Static, Type } from \"typebox\";\nimport { keyHint } from \"../../modes/interactive/components/keybinding-hints.js\";\nimport { ensureTool } from \"../../utils/tools-manager.js\";\nimport { getEmbsearchService } from \"../embsearch/embsearch-service.js\";\nimport type { ToolDefinition, ToolRenderResultOptions } from \"../extensions/types.js\";\nimport { isNativeSearchForced, nativeGrep } from \"./native-search.js\";\nimport { resolveToCwd } from \"./path-utils.js\";\nimport { getTextOutput, invalidArgText, shortenPath, str } from \"./render-utils.js\";\nimport { wrapToolDefinition } from \"./tool-definition-wrapper.js\";\nimport {\n\tDEFAULT_MAX_BYTES,\n\tformatSize,\n\tGREP_MAX_LINE_LENGTH,\n\ttype TruncationResult,\n\ttruncateHead,\n\ttruncateLine,\n} from \"./truncate.js\";\n\nconst grepSchema = Type.Object({\n\tpattern: Type.String({ description: \"Search pattern (regex or literal string)\" }),\n\tpath: Type.Optional(Type.String({ description: \"Directory or file to search (default: current directory)\" })),\n\tglob: Type.Optional(Type.String({ description: \"Filter files by glob pattern, e.g. '*.ts' or '**/*.spec.ts'\" })),\n\tignoreCase: Type.Optional(Type.Boolean({ description: \"Case-insensitive search (default: false)\" })),\n\tliteral: Type.Optional(\n\t\tType.Boolean({ description: \"Treat pattern as literal string instead of regex (default: false)\" }),\n\t),\n\tcontext: Type.Optional(\n\t\tType.Number({ description: \"Number of lines to show before and after each match (default: 0)\" }),\n\t),\n\tlimit: Type.Optional(Type.Number({ description: \"Maximum number of matches to return (default: 100)\" })),\n});\n\nexport type GrepToolInput = Static<typeof grepSchema>;\nconst DEFAULT_LIMIT = 100;\n\nexport interface GrepToolDetails {\n\ttruncation?: TruncationResult;\n\tmatchLimitReached?: number;\n\tlinesTruncated?: boolean;\n}\n\n/**\n * Pluggable operations for the grep tool.\n * Override these to delegate search to remote systems (for example SSH).\n */\nexport interface GrepOperations {\n\t/** Check if path is a directory. Throws if path does not exist. */\n\tisDirectory: (absolutePath: string) => Promise<boolean> | boolean;\n\t/** Read file contents for context lines */\n\treadFile: (absolutePath: string) => Promise<string> | string;\n}\n\nconst defaultGrepOperations: GrepOperations = {\n\tisDirectory: (p) => statSync(p).isDirectory(),\n\treadFile: (p) => readFileSync(p, \"utf-8\"),\n};\n\nexport interface GrepToolOptions {\n\t/** Custom operations for grep. Default: local filesystem plus ripgrep */\n\toperations?: GrepOperations;\n}\n\nfunction formatGrepCall(\n\targs: { pattern: string; path?: string; glob?: string; limit?: number } | undefined,\n\ttheme: typeof import(\"../../modes/interactive/theme/theme.js\").theme,\n): string {\n\tconst pattern = str(args?.pattern);\n\tconst rawPath = str(args?.path);\n\tconst path = rawPath !== null ? shortenPath(rawPath || \".\") : null;\n\tconst glob = str(args?.glob);\n\tconst limit = args?.limit;\n\tconst invalidArg = invalidArgText(theme);\n\tlet text =\n\t\ttheme.fg(\"toolTitle\", theme.bold(\"grep\")) +\n\t\t\" \" +\n\t\t(pattern === null ? invalidArg : theme.fg(\"accent\", `/${pattern || \"\"}/`)) +\n\t\ttheme.fg(\"toolOutput\", ` in ${path === null ? invalidArg : path}`);\n\tif (glob) text += theme.fg(\"toolOutput\", ` (${glob})`);\n\tif (limit !== undefined) text += theme.fg(\"toolOutput\", ` limit ${limit}`);\n\treturn text;\n}\n\nfunction formatGrepResult(\n\tresult: {\n\t\tcontent: Array<{ type: string; text?: string; data?: string; mimeType?: string }>;\n\t\tdetails?: GrepToolDetails;\n\t},\n\toptions: ToolRenderResultOptions,\n\ttheme: typeof import(\"../../modes/interactive/theme/theme.js\").theme,\n\tshowImages: boolean,\n): string {\n\tconst output = getTextOutput(result, showImages).trim();\n\tlet text = \"\";\n\tif (output) {\n\t\tconst lines = output.split(\"\\n\");\n\t\tconst maxLines = options.expanded ? lines.length : 15;\n\t\tconst displayLines = lines.slice(0, maxLines);\n\t\tconst remaining = lines.length - maxLines;\n\t\ttext += `\\n${displayLines.map((line) => theme.fg(\"toolOutput\", line)).join(\"\\n\")}`;\n\t\tif (remaining > 0) {\n\t\t\ttext += `${theme.fg(\"muted\", `\\n... (${remaining} more lines,`)} ${keyHint(\"app.tools.expand\", \"to expand\")})`;\n\t\t}\n\t}\n\n\tconst matchLimit = result.details?.matchLimitReached;\n\tconst truncation = result.details?.truncation;\n\tconst linesTruncated = result.details?.linesTruncated;\n\tif (matchLimit || truncation?.truncated || linesTruncated) {\n\t\tconst warnings: string[] = [];\n\t\tif (matchLimit) warnings.push(`${matchLimit} matches limit`);\n\t\tif (truncation?.truncated) warnings.push(`${formatSize(truncation.maxBytes ?? DEFAULT_MAX_BYTES)} limit`);\n\t\tif (linesTruncated) warnings.push(\"some lines truncated\");\n\t\ttext += `\\n${theme.fg(\"warning\", `[Truncated: ${warnings.join(\", \")}]`)}`;\n\t}\n\treturn text;\n}\n\nexport function createGrepToolDefinition(\n\tcwd: string,\n\toptions?: GrepToolOptions,\n): ToolDefinition<typeof grepSchema, GrepToolDetails | undefined> {\n\tconst customOps = options?.operations;\n\treturn {\n\t\tname: \"grep\",\n\t\tlabel: \"grep\",\n\t\tdescription: `Search file contents for a pattern. Returns matching lines with file paths and line numbers. Respects .gitignore. Output is truncated to ${DEFAULT_LIMIT} matches or ${DEFAULT_MAX_BYTES / 1024}KB (whichever is hit first). Long lines are truncated to ${GREP_MAX_LINE_LENGTH} chars.`,\n\t\tpromptSnippet: \"Search file contents for patterns (respects .gitignore)\",\n\t\tparameters: grepSchema,\n\t\tasync execute(\n\t\t\t_toolCallId,\n\t\t\t{\n\t\t\t\tpattern,\n\t\t\t\tpath: searchDir,\n\t\t\t\tglob,\n\t\t\t\tignoreCase,\n\t\t\t\tliteral,\n\t\t\t\tcontext,\n\t\t\t\tlimit,\n\t\t\t}: {\n\t\t\t\tpattern: string;\n\t\t\t\tpath?: string;\n\t\t\t\tglob?: string;\n\t\t\t\tignoreCase?: boolean;\n\t\t\t\tliteral?: boolean;\n\t\t\t\tcontext?: number;\n\t\t\t\tlimit?: number;\n\t\t\t},\n\t\t\tsignal?: AbortSignal,\n\t\t\t_onUpdate?,\n\t\t\t_ctx?,\n\t\t) {\n\t\t\treturn new Promise((resolve, reject) => {\n\t\t\t\tif (signal?.aborted) {\n\t\t\t\t\treject(new Error(\"Operation aborted\"));\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tlet settled = false;\n\t\t\t\tconst settle = (fn: () => void) => {\n\t\t\t\t\tif (!settled) {\n\t\t\t\t\t\tsettled = true;\n\t\t\t\t\t\tfn();\n\t\t\t\t\t}\n\t\t\t\t};\n\n\t\t\t\t(async () => {\n\t\t\t\t\ttry {\n\t\t\t\t\t\t// rg drives the fast path; when it is unavailable (restricted\n\t\t\t\t\t\t// environments) or forced off, a pure-JS scan produces the same\n\t\t\t\t\t\t// match shape so the tool degrades automatically instead of failing.\n\t\t\t\t\t\tconst rgPath = isNativeSearchForced() ? undefined : await ensureTool(\"rg\", true);\n\n\t\t\t\t\t\tconst searchPath = resolveToCwd(searchDir || \".\", cwd);\n\t\t\t\t\t\tconst ops = customOps ?? defaultGrepOperations;\n\t\t\t\t\t\tlet isDirectory: boolean;\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\tisDirectory = await ops.isDirectory(searchPath);\n\t\t\t\t\t\t} catch {\n\t\t\t\t\t\t\tsettle(() => reject(new Error(`Path not found: ${searchPath}`)));\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tconst contextValue = context && context > 0 ? context : 0;\n\t\t\t\t\t\tconst effectiveLimit = Math.max(1, limit ?? DEFAULT_LIMIT);\n\t\t\t\t\t\tconst formatPath = (filePath: string): string => {\n\t\t\t\t\t\t\tif (isDirectory) {\n\t\t\t\t\t\t\t\tconst relative = path.relative(searchPath, filePath);\n\t\t\t\t\t\t\t\tif (relative && !relative.startsWith(\"..\")) {\n\t\t\t\t\t\t\t\t\treturn relative.replace(/\\\\/g, \"/\");\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\treturn path.basename(filePath);\n\t\t\t\t\t\t};\n\n\t\t\t\t\t\tconst fileCache = new Map<string, string[]>();\n\t\t\t\t\t\tconst getFileLines = async (filePath: string): Promise<string[]> => {\n\t\t\t\t\t\t\tlet lines = fileCache.get(filePath);\n\t\t\t\t\t\t\tif (!lines) {\n\t\t\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\t\t\tconst content = await ops.readFile(filePath);\n\t\t\t\t\t\t\t\t\tlines = content.replace(/\\r\\n/g, \"\\n\").replace(/\\r/g, \"\\n\").split(\"\\n\");\n\t\t\t\t\t\t\t\t} catch {\n\t\t\t\t\t\t\t\t\tlines = [];\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tfileCache.set(filePath, lines);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\treturn lines;\n\t\t\t\t\t\t};\n\n\t\t\t\t\t\t// Match collection is shared between the rg and native paths.\n\t\t\t\t\t\tconst matches: Array<{ filePath: string; lineNumber: number; lineText?: string }> = [];\n\t\t\t\t\t\tlet matchLimitReached = false;\n\t\t\t\t\t\tlet linesTruncated = false;\n\n\t\t\t\t\t\t// Format a context block around a match line. Path is omitted (emitted once per file).\n\t\t\t\t\t\tconst formatBlock = async (filePath: string, lineNumber: number): Promise<string[]> => {\n\t\t\t\t\t\t\tconst lines = await getFileLines(filePath);\n\t\t\t\t\t\t\tif (!lines.length) return [` ${lineNumber}: (unable to read file)`];\n\t\t\t\t\t\t\tconst block: string[] = [];\n\t\t\t\t\t\t\tconst start = contextValue > 0 ? Math.max(1, lineNumber - contextValue) : lineNumber;\n\t\t\t\t\t\t\tconst end = contextValue > 0 ? Math.min(lines.length, lineNumber + contextValue) : lineNumber;\n\t\t\t\t\t\t\tfor (let current = start; current <= end; current++) {\n\t\t\t\t\t\t\t\tconst lineText = lines[current - 1] ?? \"\";\n\t\t\t\t\t\t\t\tconst sanitized = lineText.replace(/\\r/g, \"\");\n\t\t\t\t\t\t\t\tconst isMatchLine = current === lineNumber;\n\t\t\t\t\t\t\t\t// Truncate long lines so grep output stays compact.\n\t\t\t\t\t\t\t\tconst { text: truncatedText, wasTruncated } = truncateLine(sanitized);\n\t\t\t\t\t\t\t\tif (wasTruncated) linesTruncated = true;\n\t\t\t\t\t\t\t\tif (isMatchLine) block.push(`${current}: ${truncatedText}`);\n\t\t\t\t\t\t\t\telse block.push(`${current}- ${truncatedText}`);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\treturn block;\n\t\t\t\t\t\t};\n\n\t\t\t\t\t\t// Format the collected matches into the tool result. Shared by both paths.\n\t\t\t\t\t\tconst finalize = async () => {\n\t\t\t\t\t\t\tif (matches.length === 0) {\n\t\t\t\t\t\t\t\tconst service = getEmbsearchService(cwd);\n\t\t\t\t\t\t\t\tconst hint = service?.isAvailable()\n\t\t\t\t\t\t\t\t\t? \"\\n\\n[Hint: no exact matches; use semantic_search to find conceptually related code]\"\n\t\t\t\t\t\t\t\t\t: \"\";\n\t\t\t\t\t\t\t\tsettle(() =>\n\t\t\t\t\t\t\t\t\tresolve({\n\t\t\t\t\t\t\t\t\t\tcontent: [{ type: \"text\", text: `No matches found${hint}` }],\n\t\t\t\t\t\t\t\t\t\tdetails: undefined,\n\t\t\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tconst outputLines: string[] = [];\n\t\t\t\t\t\t\t// Group matches by file path, showing filename once per file.\n\t\t\t\t\t\t\tconst fileGroups = new Map<string, typeof matches>();\n\t\t\t\t\t\t\tfor (const match of matches) {\n\t\t\t\t\t\t\t\tlet group = fileGroups.get(match.filePath);\n\t\t\t\t\t\t\t\tif (!group) {\n\t\t\t\t\t\t\t\t\tgroup = [];\n\t\t\t\t\t\t\t\t\tfileGroups.set(match.filePath, group);\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tgroup.push(match);\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tfor (const [filePath, fileMatches] of fileGroups) {\n\t\t\t\t\t\t\t\tconst relativePath = formatPath(filePath);\n\t\t\t\t\t\t\t\toutputLines.push(relativePath);\n\t\t\t\t\t\t\t\tfor (const match of fileMatches) {\n\t\t\t\t\t\t\t\t\tif (contextValue === 0 && match.lineText !== undefined) {\n\t\t\t\t\t\t\t\t\t\tconst sanitized = match.lineText\n\t\t\t\t\t\t\t\t\t\t\t.replace(/\\r\\n/g, \"\\n\")\n\t\t\t\t\t\t\t\t\t\t\t.replace(/\\r/g, \"\")\n\t\t\t\t\t\t\t\t\t\t\t.replace(/\\n$/, \"\");\n\t\t\t\t\t\t\t\t\t\tconst { text: truncatedText, wasTruncated } = truncateLine(sanitized);\n\t\t\t\t\t\t\t\t\t\tif (wasTruncated) linesTruncated = true;\n\t\t\t\t\t\t\t\t\t\toutputLines.push(`${match.lineNumber}: ${truncatedText}`);\n\t\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t\tconst block = await formatBlock(match.filePath, match.lineNumber);\n\t\t\t\t\t\t\t\t\t\toutputLines.push(...block);\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tconst rawOutput = outputLines.join(\"\\n\");\n\t\t\t\t\t\t\t// Apply lossless compression then byte truncation.\n\t\t\t\t\t\t\tconst compressedOutput = compressGrepOutput(rawOutput);\n\t\t\t\t\t\t\t// Apply byte truncation. There is no line limit here because the match limit already capped rows.\n\t\t\t\t\t\t\tconst truncation = truncateHead(compressedOutput, { maxLines: Number.MAX_SAFE_INTEGER });\n\t\t\t\t\t\t\tlet output = truncation.content;\n\t\t\t\t\t\t\tconst details: GrepToolDetails = {};\n\t\t\t\t\t\t\t// Build actionable notices for truncation and match limits.\n\t\t\t\t\t\t\tconst notices: string[] = [];\n\t\t\t\t\t\t\tif (matchLimitReached) {\n\t\t\t\t\t\t\t\tnotices.push(\n\t\t\t\t\t\t\t\t\t`${effectiveLimit} matches limit reached. Use limit=${effectiveLimit * 2} for more, or refine pattern`,\n\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\tdetails.matchLimitReached = effectiveLimit;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tif (truncation.truncated) {\n\t\t\t\t\t\t\t\tnotices.push(`${formatSize(DEFAULT_MAX_BYTES)} limit reached`);\n\t\t\t\t\t\t\t\tdetails.truncation = truncation;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tif (linesTruncated) {\n\t\t\t\t\t\t\t\tnotices.push(\n\t\t\t\t\t\t\t\t\t`Some lines truncated to ${GREP_MAX_LINE_LENGTH} chars. Use read tool to see full lines`,\n\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\tdetails.linesTruncated = true;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tif (notices.length > 0) output += `\\n\\n[${notices.join(\". \")}]`;\n\t\t\t\t\t\t\tsettle(() =>\n\t\t\t\t\t\t\t\tresolve({\n\t\t\t\t\t\t\t\t\tcontent: [{ type: \"text\", text: output }],\n\t\t\t\t\t\t\t\t\tdetails: Object.keys(details).length > 0 ? details : undefined,\n\t\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t};\n\n\t\t\t\t\t\t// --- Native fallback: no rg binary available (or forced off) ---\n\t\t\t\t\t\tif (!rgPath) {\n\t\t\t\t\t\t\tlet result: Awaited<ReturnType<typeof nativeGrep>>;\n\t\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\t\tresult = await nativeGrep(searchPath, {\n\t\t\t\t\t\t\t\t\tpattern,\n\t\t\t\t\t\t\t\t\tisDirectory,\n\t\t\t\t\t\t\t\t\tignoreCase,\n\t\t\t\t\t\t\t\t\tliteral,\n\t\t\t\t\t\t\t\t\tglob,\n\t\t\t\t\t\t\t\t\tlimit: effectiveLimit,\n\t\t\t\t\t\t\t\t\tsignal,\n\t\t\t\t\t\t\t\t\treadFile: ops.readFile,\n\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t} catch (e) {\n\t\t\t\t\t\t\t\tif (signal?.aborted) {\n\t\t\t\t\t\t\t\t\tsettle(() => reject(new Error(\"Operation aborted\")));\n\t\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tconst err = e as Error & { invalidRegex?: boolean };\n\t\t\t\t\t\t\t\tlet errorMsg = err.message || \"grep failed\";\n\t\t\t\t\t\t\t\tif (err.invalidRegex && !literal) {\n\t\t\t\t\t\t\t\t\terrorMsg += `\\n\\nThe pattern is not a valid regex. To search for it as plain text, pass literal: true (or escape the regex metacharacters).`;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tsettle(() => reject(new Error(errorMsg)));\n\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tif (signal?.aborted) {\n\t\t\t\t\t\t\t\tsettle(() => reject(new Error(\"Operation aborted\")));\n\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tmatches.push(...result.matches);\n\t\t\t\t\t\t\tmatchLimitReached = result.matchLimitReached;\n\t\t\t\t\t\t\tawait finalize();\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// --- Fast path: stream rg --json ---\n\t\t\t\t\t\tconst args: string[] = [\"--json\", \"--line-number\", \"--color=never\", \"--hidden\"];\n\t\t\t\t\t\tif (ignoreCase) args.push(\"--ignore-case\");\n\t\t\t\t\t\tif (literal) args.push(\"--fixed-strings\");\n\t\t\t\t\t\tif (glob) args.push(\"--glob\", glob);\n\t\t\t\t\t\targs.push(\"--\", pattern, searchPath);\n\n\t\t\t\t\t\tconst child = spawn(rgPath, args, { stdio: [\"ignore\", \"pipe\", \"pipe\"] });\n\t\t\t\t\t\tconst rl = createInterface({ input: child.stdout });\n\t\t\t\t\t\tlet stderr = \"\";\n\t\t\t\t\t\tlet matchCount = 0;\n\t\t\t\t\t\tlet aborted = false;\n\t\t\t\t\t\tlet killedDueToLimit = false;\n\n\t\t\t\t\t\tconst cleanup = () => {\n\t\t\t\t\t\t\trl.close();\n\t\t\t\t\t\t\tsignal?.removeEventListener(\"abort\", onAbort);\n\t\t\t\t\t\t};\n\t\t\t\t\t\tconst stopChild = (dueToLimit = false) => {\n\t\t\t\t\t\t\tif (!child.killed) {\n\t\t\t\t\t\t\t\tkilledDueToLimit = dueToLimit;\n\t\t\t\t\t\t\t\tchild.kill();\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t};\n\t\t\t\t\t\tconst onAbort = () => {\n\t\t\t\t\t\t\taborted = true;\n\t\t\t\t\t\t\tstopChild();\n\t\t\t\t\t\t};\n\t\t\t\t\t\tsignal?.addEventListener(\"abort\", onAbort, { once: true });\n\t\t\t\t\t\tchild.stderr?.on(\"data\", (chunk) => {\n\t\t\t\t\t\t\tstderr += chunk.toString();\n\t\t\t\t\t\t});\n\n\t\t\t\t\t\t// Collect matches during streaming, then format them after rg exits.\n\t\t\t\t\t\trl.on(\"line\", (line) => {\n\t\t\t\t\t\t\tif (!line.trim() || matchCount >= effectiveLimit) return;\n\t\t\t\t\t\t\tlet event: any;\n\t\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\t\tevent = JSON.parse(line);\n\t\t\t\t\t\t\t} catch {\n\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tif (event.type === \"match\") {\n\t\t\t\t\t\t\t\tmatchCount++;\n\t\t\t\t\t\t\t\tconst filePath = event.data?.path?.text;\n\t\t\t\t\t\t\t\tconst lineNumber = event.data?.line_number;\n\t\t\t\t\t\t\t\tconst lineText = event.data?.lines?.text;\n\t\t\t\t\t\t\t\tif (filePath && typeof lineNumber === \"number\")\n\t\t\t\t\t\t\t\t\tmatches.push({ filePath, lineNumber, lineText });\n\t\t\t\t\t\t\t\tif (matchCount >= effectiveLimit) {\n\t\t\t\t\t\t\t\t\tmatchLimitReached = true;\n\t\t\t\t\t\t\t\t\tstopChild(true);\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t});\n\n\t\t\t\t\t\tchild.on(\"error\", (error) => {\n\t\t\t\t\t\t\tcleanup();\n\t\t\t\t\t\t\tsettle(() => reject(new Error(`Failed to run ripgrep: ${error.message}`)));\n\t\t\t\t\t\t});\n\t\t\t\t\t\tchild.on(\"close\", async (code) => {\n\t\t\t\t\t\t\tcleanup();\n\t\t\t\t\t\t\tif (aborted) {\n\t\t\t\t\t\t\t\tsettle(() => reject(new Error(\"Operation aborted\")));\n\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tif (!killedDueToLimit && code !== 0 && code !== 1) {\n\t\t\t\t\t\t\t\tconst trimmedStderr = stderr.trim();\n\t\t\t\t\t\t\t\tlet errorMsg = trimmedStderr || `ripgrep exited with code ${code}`;\n\t\t\t\t\t\t\t\t// A regex parse error means the pattern is not a valid regex. Point the\n\t\t\t\t\t\t\t\t// caller at the `literal` option so they can search the text verbatim.\n\t\t\t\t\t\t\t\tif (!literal && /regex parse error/i.test(trimmedStderr)) {\n\t\t\t\t\t\t\t\t\terrorMsg += `\\n\\nThe pattern is not a valid regex. To search for it as plain text, pass literal: true (or escape the regex metacharacters).`;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tsettle(() => reject(new Error(errorMsg)));\n\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tawait finalize();\n\t\t\t\t\t\t});\n\t\t\t\t\t} catch (err) {\n\t\t\t\t\t\tsettle(() => reject(err as Error));\n\t\t\t\t\t}\n\t\t\t\t})();\n\t\t\t});\n\t\t},\n\t\trenderCall(args, theme, context) {\n\t\t\tconst text = (context.lastComponent as Text | undefined) ?? new Text(\"\", 0, 0);\n\t\t\ttext.setText(formatGrepCall(args, theme));\n\t\t\treturn text;\n\t\t},\n\t\trenderResult(result, options, theme, context) {\n\t\t\tconst text = (context.lastComponent as Text | undefined) ?? new Text(\"\", 0, 0);\n\t\t\ttext.setText(formatGrepResult(result as any, options, theme, context.showImages));\n\t\t\treturn text;\n\t\t},\n\t};\n}\n\nexport function createGrepTool(cwd: string, options?: GrepToolOptions): AgentTool<typeof grepSchema> {\n\treturn wrapToolDefinition(createGrepToolDefinition(cwd, options));\n}\n"]}
|
|
1
|
+
{"version":3,"file":"grep.d.ts","sourceRoot":"","sources":["../../../src/core/tools/grep.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iCAAiC,CAAC;AAMjE,OAAO,EAAE,KAAK,MAAM,EAAE,IAAI,EAAE,MAAM,SAAS,CAAC;AAI5C,OAAO,KAAK,EAAE,cAAc,EAA2B,MAAM,wBAAwB,CAAC;AAKtF,OAAO,EAIN,KAAK,gBAAgB,EAGrB,MAAM,eAAe,CAAC;AAEvB,QAAA,MAAM,UAAU;;;;;;;;EAYd,CAAC;AAEH,MAAM,MAAM,aAAa,GAAG,MAAM,CAAC,OAAO,UAAU,CAAC,CAAC;AAGtD,MAAM,WAAW,eAAe;IAC/B,UAAU,CAAC,EAAE,gBAAgB,CAAC;IAC9B,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,cAAc,CAAC,EAAE,OAAO,CAAC;CACzB;AAED;;;GAGG;AACH,MAAM,WAAW,cAAc;IAC9B,mEAAmE;IACnE,WAAW,EAAE,CAAC,YAAY,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,GAAG,OAAO,CAAC;IAClE,2CAA2C;IAC3C,QAAQ,EAAE,CAAC,YAAY,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,GAAG,MAAM,CAAC;CAC7D;AAOD,MAAM,WAAW,eAAe;IAC/B,yEAAyE;IACzE,UAAU,CAAC,EAAE,cAAc,CAAC;CAC5B;AAyDD,wBAAgB,wBAAwB,CACvC,GAAG,EAAE,MAAM,EACX,OAAO,CAAC,EAAE,eAAe,GACvB,cAAc,CAAC,OAAO,UAAU,EAAE,eAAe,GAAG,SAAS,CAAC,CAuUhE;AAED,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,eAAe,GAAG,SAAS,CAAC,OAAO,UAAU,CAAC,CAEnG","sourcesContent":["import { createInterface } from \"node:readline\";\nimport type { AgentTool } from \"@kolisachint/hoocode-agent-core\";\nimport { compressGrepOutput } from \"@kolisachint/hoocode-agent-core\";\nimport { Text } from \"@kolisachint/hoocode-tui\";\nimport { spawn } from \"child_process\";\nimport { readFileSync, statSync } from \"fs\";\nimport path from \"path\";\nimport { type Static, Type } from \"typebox\";\nimport { keyHint } from \"../../modes/interactive/components/keybinding-hints.js\";\nimport { ensureTool } from \"../../utils/tools-manager.js\";\nimport { getEmbsearchService } from \"../embsearch/embsearch-service.js\";\nimport type { ToolDefinition, ToolRenderResultOptions } from \"../extensions/types.js\";\nimport { isNativeSearchForced, nativeGrep } from \"./native-search.js\";\nimport { resolveToCwd } from \"./path-utils.js\";\nimport { getTextOutput, invalidArgText, shortenPath, str } from \"./render-utils.js\";\nimport { wrapToolDefinition } from \"./tool-definition-wrapper.js\";\nimport {\n\tDEFAULT_MAX_BYTES,\n\tformatSize,\n\tGREP_MAX_LINE_LENGTH,\n\ttype TruncationResult,\n\ttruncateHead,\n\ttruncateLine,\n} from \"./truncate.js\";\n\nconst grepSchema = Type.Object({\n\tpattern: Type.String({ description: \"Search pattern (regex or literal string)\" }),\n\tpath: Type.Optional(Type.String({ description: \"Directory or file to search (default: current directory)\" })),\n\tglob: Type.Optional(Type.String({ description: \"Filter files by glob pattern, e.g. '*.ts' or '**/*.spec.ts'\" })),\n\tignoreCase: Type.Optional(Type.Boolean({ description: \"Case-insensitive search (default: false)\" })),\n\tliteral: Type.Optional(\n\t\tType.Boolean({ description: \"Treat pattern as literal string instead of regex (default: false)\" }),\n\t),\n\tcontext: Type.Optional(\n\t\tType.Number({ description: \"Number of lines to show before and after each match (default: 0)\" }),\n\t),\n\tlimit: Type.Optional(Type.Number({ description: \"Maximum number of matches to return (default: 100)\" })),\n});\n\nexport type GrepToolInput = Static<typeof grepSchema>;\nconst DEFAULT_LIMIT = 100;\n\nexport interface GrepToolDetails {\n\ttruncation?: TruncationResult;\n\tmatchLimitReached?: number;\n\tlinesTruncated?: boolean;\n}\n\n/**\n * Pluggable operations for the grep tool.\n * Override these to delegate search to remote systems (for example SSH).\n */\nexport interface GrepOperations {\n\t/** Check if path is a directory. Throws if path does not exist. */\n\tisDirectory: (absolutePath: string) => Promise<boolean> | boolean;\n\t/** Read file contents for context lines */\n\treadFile: (absolutePath: string) => Promise<string> | string;\n}\n\nconst defaultGrepOperations: GrepOperations = {\n\tisDirectory: (p) => statSync(p).isDirectory(),\n\treadFile: (p) => readFileSync(p, \"utf-8\"),\n};\n\nexport interface GrepToolOptions {\n\t/** Custom operations for grep. Default: local filesystem plus ripgrep */\n\toperations?: GrepOperations;\n}\n\nfunction formatGrepCall(\n\targs: { pattern: string; path?: string; glob?: string; limit?: number } | undefined,\n\ttheme: typeof import(\"../../modes/interactive/theme/theme.js\").theme,\n): string {\n\tconst pattern = str(args?.pattern);\n\tconst rawPath = str(args?.path);\n\tconst path = rawPath !== null ? shortenPath(rawPath || \".\") : null;\n\tconst glob = str(args?.glob);\n\tconst limit = args?.limit;\n\tconst invalidArg = invalidArgText(theme);\n\tlet text =\n\t\ttheme.fg(\"toolTitle\", theme.bold(\"grep\")) +\n\t\t\" \" +\n\t\t(pattern === null ? invalidArg : theme.fg(\"accent\", `/${pattern || \"\"}/`)) +\n\t\ttheme.fg(\"toolOutput\", ` in ${path === null ? invalidArg : path}`);\n\tif (glob) text += theme.fg(\"toolOutput\", ` (${glob})`);\n\tif (limit !== undefined) text += theme.fg(\"toolOutput\", ` limit ${limit}`);\n\treturn text;\n}\n\nfunction formatGrepResult(\n\tresult: {\n\t\tcontent: Array<{ type: string; text?: string; data?: string; mimeType?: string }>;\n\t\tdetails?: GrepToolDetails;\n\t},\n\toptions: ToolRenderResultOptions,\n\ttheme: typeof import(\"../../modes/interactive/theme/theme.js\").theme,\n\tshowImages: boolean,\n): string {\n\tconst output = getTextOutput(result, showImages).trim();\n\tlet text = \"\";\n\tif (output) {\n\t\tconst lines = output.split(\"\\n\");\n\t\tconst maxLines = options.expanded ? lines.length : 15;\n\t\tconst displayLines = lines.slice(0, maxLines);\n\t\tconst remaining = lines.length - maxLines;\n\t\ttext += `\\n${displayLines.map((line) => theme.fg(\"toolOutput\", line)).join(\"\\n\")}`;\n\t\tif (remaining > 0) {\n\t\t\ttext += `${theme.fg(\"muted\", `\\n... (${remaining} more lines,`)} ${keyHint(\"app.tools.expand\", \"to expand\")})`;\n\t\t}\n\t}\n\n\tconst matchLimit = result.details?.matchLimitReached;\n\tconst truncation = result.details?.truncation;\n\tconst linesTruncated = result.details?.linesTruncated;\n\tif (matchLimit || truncation?.truncated || linesTruncated) {\n\t\tconst warnings: string[] = [];\n\t\tif (matchLimit) warnings.push(`${matchLimit} matches limit`);\n\t\tif (truncation?.truncated) warnings.push(`${formatSize(truncation.maxBytes ?? DEFAULT_MAX_BYTES)} limit`);\n\t\tif (linesTruncated) warnings.push(\"some lines truncated\");\n\t\ttext += `\\n${theme.fg(\"warning\", `[Truncated: ${warnings.join(\", \")}]`)}`;\n\t}\n\treturn text;\n}\n\nexport function createGrepToolDefinition(\n\tcwd: string,\n\toptions?: GrepToolOptions,\n): ToolDefinition<typeof grepSchema, GrepToolDetails | undefined> {\n\tconst customOps = options?.operations;\n\treturn {\n\t\tname: \"grep\",\n\t\tlabel: \"grep\",\n\t\tdescription: `Search file contents for a pattern. Returns matching lines with file paths and line numbers. Respects .gitignore. Output is truncated to ${DEFAULT_LIMIT} matches or ${DEFAULT_MAX_BYTES / 1024}KB (whichever is hit first). Long lines are truncated to ${GREP_MAX_LINE_LENGTH} chars.`,\n\t\tpromptSnippet: \"Search file contents for patterns (respects .gitignore)\",\n\t\tparameters: grepSchema,\n\t\tasync execute(\n\t\t\t_toolCallId,\n\t\t\t{\n\t\t\t\tpattern,\n\t\t\t\tpath: searchDir,\n\t\t\t\tglob,\n\t\t\t\tignoreCase,\n\t\t\t\tliteral,\n\t\t\t\tcontext,\n\t\t\t\tlimit,\n\t\t\t}: {\n\t\t\t\tpattern: string;\n\t\t\t\tpath?: string;\n\t\t\t\tglob?: string;\n\t\t\t\tignoreCase?: boolean;\n\t\t\t\tliteral?: boolean;\n\t\t\t\tcontext?: number;\n\t\t\t\tlimit?: number;\n\t\t\t},\n\t\t\tsignal?: AbortSignal,\n\t\t\t_onUpdate?,\n\t\t\t_ctx?,\n\t\t) {\n\t\t\treturn new Promise((resolve, reject) => {\n\t\t\t\tif (signal?.aborted) {\n\t\t\t\t\treject(new Error(\"Operation aborted\"));\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tlet settled = false;\n\t\t\t\tconst settle = (fn: () => void) => {\n\t\t\t\t\tif (!settled) {\n\t\t\t\t\t\tsettled = true;\n\t\t\t\t\t\tfn();\n\t\t\t\t\t}\n\t\t\t\t};\n\n\t\t\t\t(async () => {\n\t\t\t\t\ttry {\n\t\t\t\t\t\t// rg drives the fast path; when it is unavailable (restricted\n\t\t\t\t\t\t// environments) or forced off, a pure-JS scan produces the same\n\t\t\t\t\t\t// match shape so the tool degrades automatically instead of failing.\n\t\t\t\t\t\tconst rgPath = isNativeSearchForced() ? undefined : await ensureTool(\"rg\", true);\n\n\t\t\t\t\t\tconst searchPath = resolveToCwd(searchDir || \".\", cwd);\n\t\t\t\t\t\tconst ops = customOps ?? defaultGrepOperations;\n\t\t\t\t\t\tlet isDirectory: boolean;\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\tisDirectory = await ops.isDirectory(searchPath);\n\t\t\t\t\t\t} catch {\n\t\t\t\t\t\t\tsettle(() => reject(new Error(`Path not found: ${searchPath}`)));\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tconst contextValue = context && context > 0 ? context : 0;\n\t\t\t\t\t\tconst effectiveLimit = Math.max(1, limit ?? DEFAULT_LIMIT);\n\t\t\t\t\t\tconst formatPath = (filePath: string): string => {\n\t\t\t\t\t\t\tif (isDirectory) {\n\t\t\t\t\t\t\t\tconst relative = path.relative(searchPath, filePath);\n\t\t\t\t\t\t\t\tif (relative && !relative.startsWith(\"..\")) {\n\t\t\t\t\t\t\t\t\treturn relative.replace(/\\\\/g, \"/\");\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\treturn path.basename(filePath);\n\t\t\t\t\t\t};\n\n\t\t\t\t\t\tconst fileCache = new Map<string, string[]>();\n\t\t\t\t\t\tconst getFileLines = async (filePath: string): Promise<string[]> => {\n\t\t\t\t\t\t\tlet lines = fileCache.get(filePath);\n\t\t\t\t\t\t\tif (!lines) {\n\t\t\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\t\t\tconst content = await ops.readFile(filePath);\n\t\t\t\t\t\t\t\t\tlines = content.replace(/\\r\\n/g, \"\\n\").replace(/\\r/g, \"\\n\").split(\"\\n\");\n\t\t\t\t\t\t\t\t} catch {\n\t\t\t\t\t\t\t\t\tlines = [];\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tfileCache.set(filePath, lines);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\treturn lines;\n\t\t\t\t\t\t};\n\n\t\t\t\t\t\t// Match collection is shared between the rg and native paths.\n\t\t\t\t\t\tconst matches: Array<{ filePath: string; lineNumber: number; lineText?: string }> = [];\n\t\t\t\t\t\tlet matchLimitReached = false;\n\t\t\t\t\t\tlet linesTruncated = false;\n\n\t\t\t\t\t\t// Format a context block around a match line. Path is omitted (emitted once per file).\n\t\t\t\t\t\tconst formatBlock = async (filePath: string, lineNumber: number): Promise<string[]> => {\n\t\t\t\t\t\t\tconst lines = await getFileLines(filePath);\n\t\t\t\t\t\t\tif (!lines.length) return [` ${lineNumber}: (unable to read file)`];\n\t\t\t\t\t\t\tconst block: string[] = [];\n\t\t\t\t\t\t\tconst start = contextValue > 0 ? Math.max(1, lineNumber - contextValue) : lineNumber;\n\t\t\t\t\t\t\tconst end = contextValue > 0 ? Math.min(lines.length, lineNumber + contextValue) : lineNumber;\n\t\t\t\t\t\t\tfor (let current = start; current <= end; current++) {\n\t\t\t\t\t\t\t\tconst lineText = lines[current - 1] ?? \"\";\n\t\t\t\t\t\t\t\tconst sanitized = lineText.replace(/\\r/g, \"\");\n\t\t\t\t\t\t\t\tconst isMatchLine = current === lineNumber;\n\t\t\t\t\t\t\t\t// Truncate long lines so grep output stays compact.\n\t\t\t\t\t\t\t\tconst { text: truncatedText, wasTruncated } = truncateLine(sanitized);\n\t\t\t\t\t\t\t\tif (wasTruncated) linesTruncated = true;\n\t\t\t\t\t\t\t\tif (isMatchLine) block.push(`${current}: ${truncatedText}`);\n\t\t\t\t\t\t\t\telse block.push(`${current}- ${truncatedText}`);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\treturn block;\n\t\t\t\t\t\t};\n\n\t\t\t\t\t\t// Format the collected matches into the tool result. Shared by both paths.\n\t\t\t\t\t\tconst finalize = async () => {\n\t\t\t\t\t\t\tif (matches.length === 0) {\n\t\t\t\t\t\t\t\tconst service = getEmbsearchService(cwd);\n\t\t\t\t\t\t\t\tconst hint = service?.isAvailable()\n\t\t\t\t\t\t\t\t\t? \"\\n\\n[Hint: no exact matches; use the search tool (semantic or hybrid mode) to find conceptually related code]\"\n\t\t\t\t\t\t\t\t\t: \"\";\n\t\t\t\t\t\t\t\tsettle(() =>\n\t\t\t\t\t\t\t\t\tresolve({\n\t\t\t\t\t\t\t\t\t\tcontent: [{ type: \"text\", text: `No matches found${hint}` }],\n\t\t\t\t\t\t\t\t\t\tdetails: undefined,\n\t\t\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tconst outputLines: string[] = [];\n\t\t\t\t\t\t\t// Group matches by file path, showing filename once per file.\n\t\t\t\t\t\t\tconst fileGroups = new Map<string, typeof matches>();\n\t\t\t\t\t\t\tfor (const match of matches) {\n\t\t\t\t\t\t\t\tlet group = fileGroups.get(match.filePath);\n\t\t\t\t\t\t\t\tif (!group) {\n\t\t\t\t\t\t\t\t\tgroup = [];\n\t\t\t\t\t\t\t\t\tfileGroups.set(match.filePath, group);\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tgroup.push(match);\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tfor (const [filePath, fileMatches] of fileGroups) {\n\t\t\t\t\t\t\t\tconst relativePath = formatPath(filePath);\n\t\t\t\t\t\t\t\toutputLines.push(relativePath);\n\t\t\t\t\t\t\t\tfor (const match of fileMatches) {\n\t\t\t\t\t\t\t\t\tif (contextValue === 0 && match.lineText !== undefined) {\n\t\t\t\t\t\t\t\t\t\tconst sanitized = match.lineText\n\t\t\t\t\t\t\t\t\t\t\t.replace(/\\r\\n/g, \"\\n\")\n\t\t\t\t\t\t\t\t\t\t\t.replace(/\\r/g, \"\")\n\t\t\t\t\t\t\t\t\t\t\t.replace(/\\n$/, \"\");\n\t\t\t\t\t\t\t\t\t\tconst { text: truncatedText, wasTruncated } = truncateLine(sanitized);\n\t\t\t\t\t\t\t\t\t\tif (wasTruncated) linesTruncated = true;\n\t\t\t\t\t\t\t\t\t\toutputLines.push(`${match.lineNumber}: ${truncatedText}`);\n\t\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t\tconst block = await formatBlock(match.filePath, match.lineNumber);\n\t\t\t\t\t\t\t\t\t\toutputLines.push(...block);\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tconst rawOutput = outputLines.join(\"\\n\");\n\t\t\t\t\t\t\t// Apply lossless compression then byte truncation.\n\t\t\t\t\t\t\tconst compressedOutput = compressGrepOutput(rawOutput);\n\t\t\t\t\t\t\t// Apply byte truncation. There is no line limit here because the match limit already capped rows.\n\t\t\t\t\t\t\tconst truncation = truncateHead(compressedOutput, { maxLines: Number.MAX_SAFE_INTEGER });\n\t\t\t\t\t\t\tlet output = truncation.content;\n\t\t\t\t\t\t\tconst details: GrepToolDetails = {};\n\t\t\t\t\t\t\t// Build actionable notices for truncation and match limits.\n\t\t\t\t\t\t\tconst notices: string[] = [];\n\t\t\t\t\t\t\tif (matchLimitReached) {\n\t\t\t\t\t\t\t\tnotices.push(\n\t\t\t\t\t\t\t\t\t`${effectiveLimit} matches limit reached. Use limit=${effectiveLimit * 2} for more, or refine pattern`,\n\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\tdetails.matchLimitReached = effectiveLimit;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tif (truncation.truncated) {\n\t\t\t\t\t\t\t\tnotices.push(`${formatSize(DEFAULT_MAX_BYTES)} limit reached`);\n\t\t\t\t\t\t\t\tdetails.truncation = truncation;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tif (linesTruncated) {\n\t\t\t\t\t\t\t\tnotices.push(\n\t\t\t\t\t\t\t\t\t`Some lines truncated to ${GREP_MAX_LINE_LENGTH} chars. Use read tool to see full lines`,\n\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\tdetails.linesTruncated = true;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tif (notices.length > 0) output += `\\n\\n[${notices.join(\". \")}]`;\n\t\t\t\t\t\t\tsettle(() =>\n\t\t\t\t\t\t\t\tresolve({\n\t\t\t\t\t\t\t\t\tcontent: [{ type: \"text\", text: output }],\n\t\t\t\t\t\t\t\t\tdetails: Object.keys(details).length > 0 ? details : undefined,\n\t\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t};\n\n\t\t\t\t\t\t// --- Native fallback: no rg binary available (or forced off) ---\n\t\t\t\t\t\tif (!rgPath) {\n\t\t\t\t\t\t\tlet result: Awaited<ReturnType<typeof nativeGrep>>;\n\t\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\t\tresult = await nativeGrep(searchPath, {\n\t\t\t\t\t\t\t\t\tpattern,\n\t\t\t\t\t\t\t\t\tisDirectory,\n\t\t\t\t\t\t\t\t\tignoreCase,\n\t\t\t\t\t\t\t\t\tliteral,\n\t\t\t\t\t\t\t\t\tglob,\n\t\t\t\t\t\t\t\t\tlimit: effectiveLimit,\n\t\t\t\t\t\t\t\t\tsignal,\n\t\t\t\t\t\t\t\t\treadFile: ops.readFile,\n\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t} catch (e) {\n\t\t\t\t\t\t\t\tif (signal?.aborted) {\n\t\t\t\t\t\t\t\t\tsettle(() => reject(new Error(\"Operation aborted\")));\n\t\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tconst err = e as Error & { invalidRegex?: boolean };\n\t\t\t\t\t\t\t\tlet errorMsg = err.message || \"grep failed\";\n\t\t\t\t\t\t\t\tif (err.invalidRegex && !literal) {\n\t\t\t\t\t\t\t\t\terrorMsg += `\\n\\nThe pattern is not a valid regex. To search for it as plain text, pass literal: true (or escape the regex metacharacters).`;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tsettle(() => reject(new Error(errorMsg)));\n\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tif (signal?.aborted) {\n\t\t\t\t\t\t\t\tsettle(() => reject(new Error(\"Operation aborted\")));\n\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tmatches.push(...result.matches);\n\t\t\t\t\t\t\tmatchLimitReached = result.matchLimitReached;\n\t\t\t\t\t\t\tawait finalize();\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// --- Fast path: stream rg --json ---\n\t\t\t\t\t\tconst args: string[] = [\"--json\", \"--line-number\", \"--color=never\", \"--hidden\"];\n\t\t\t\t\t\tif (ignoreCase) args.push(\"--ignore-case\");\n\t\t\t\t\t\tif (literal) args.push(\"--fixed-strings\");\n\t\t\t\t\t\tif (glob) args.push(\"--glob\", glob);\n\t\t\t\t\t\targs.push(\"--\", pattern, searchPath);\n\n\t\t\t\t\t\tconst child = spawn(rgPath, args, { stdio: [\"ignore\", \"pipe\", \"pipe\"] });\n\t\t\t\t\t\tconst rl = createInterface({ input: child.stdout });\n\t\t\t\t\t\tlet stderr = \"\";\n\t\t\t\t\t\tlet matchCount = 0;\n\t\t\t\t\t\tlet aborted = false;\n\t\t\t\t\t\tlet killedDueToLimit = false;\n\n\t\t\t\t\t\tconst cleanup = () => {\n\t\t\t\t\t\t\trl.close();\n\t\t\t\t\t\t\tsignal?.removeEventListener(\"abort\", onAbort);\n\t\t\t\t\t\t};\n\t\t\t\t\t\tconst stopChild = (dueToLimit = false) => {\n\t\t\t\t\t\t\tif (!child.killed) {\n\t\t\t\t\t\t\t\tkilledDueToLimit = dueToLimit;\n\t\t\t\t\t\t\t\tchild.kill();\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t};\n\t\t\t\t\t\tconst onAbort = () => {\n\t\t\t\t\t\t\taborted = true;\n\t\t\t\t\t\t\tstopChild();\n\t\t\t\t\t\t};\n\t\t\t\t\t\tsignal?.addEventListener(\"abort\", onAbort, { once: true });\n\t\t\t\t\t\tchild.stderr?.on(\"data\", (chunk) => {\n\t\t\t\t\t\t\tstderr += chunk.toString();\n\t\t\t\t\t\t});\n\n\t\t\t\t\t\t// Collect matches during streaming, then format them after rg exits.\n\t\t\t\t\t\trl.on(\"line\", (line) => {\n\t\t\t\t\t\t\tif (!line.trim() || matchCount >= effectiveLimit) return;\n\t\t\t\t\t\t\tlet event: any;\n\t\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\t\tevent = JSON.parse(line);\n\t\t\t\t\t\t\t} catch {\n\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tif (event.type === \"match\") {\n\t\t\t\t\t\t\t\tmatchCount++;\n\t\t\t\t\t\t\t\tconst filePath = event.data?.path?.text;\n\t\t\t\t\t\t\t\tconst lineNumber = event.data?.line_number;\n\t\t\t\t\t\t\t\tconst lineText = event.data?.lines?.text;\n\t\t\t\t\t\t\t\tif (filePath && typeof lineNumber === \"number\")\n\t\t\t\t\t\t\t\t\tmatches.push({ filePath, lineNumber, lineText });\n\t\t\t\t\t\t\t\tif (matchCount >= effectiveLimit) {\n\t\t\t\t\t\t\t\t\tmatchLimitReached = true;\n\t\t\t\t\t\t\t\t\tstopChild(true);\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t});\n\n\t\t\t\t\t\tchild.on(\"error\", (error) => {\n\t\t\t\t\t\t\tcleanup();\n\t\t\t\t\t\t\tsettle(() => reject(new Error(`Failed to run ripgrep: ${error.message}`)));\n\t\t\t\t\t\t});\n\t\t\t\t\t\tchild.on(\"close\", async (code) => {\n\t\t\t\t\t\t\tcleanup();\n\t\t\t\t\t\t\tif (aborted) {\n\t\t\t\t\t\t\t\tsettle(() => reject(new Error(\"Operation aborted\")));\n\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tif (!killedDueToLimit && code !== 0 && code !== 1) {\n\t\t\t\t\t\t\t\tconst trimmedStderr = stderr.trim();\n\t\t\t\t\t\t\t\tlet errorMsg = trimmedStderr || `ripgrep exited with code ${code}`;\n\t\t\t\t\t\t\t\t// A regex parse error means the pattern is not a valid regex. Point the\n\t\t\t\t\t\t\t\t// caller at the `literal` option so they can search the text verbatim.\n\t\t\t\t\t\t\t\tif (!literal && /regex parse error/i.test(trimmedStderr)) {\n\t\t\t\t\t\t\t\t\terrorMsg += `\\n\\nThe pattern is not a valid regex. To search for it as plain text, pass literal: true (or escape the regex metacharacters).`;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tsettle(() => reject(new Error(errorMsg)));\n\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tawait finalize();\n\t\t\t\t\t\t});\n\t\t\t\t\t} catch (err) {\n\t\t\t\t\t\tsettle(() => reject(err as Error));\n\t\t\t\t\t}\n\t\t\t\t})();\n\t\t\t});\n\t\t},\n\t\trenderCall(args, theme, context) {\n\t\t\tconst text = (context.lastComponent as Text | undefined) ?? new Text(\"\", 0, 0);\n\t\t\ttext.setText(formatGrepCall(args, theme));\n\t\t\treturn text;\n\t\t},\n\t\trenderResult(result, options, theme, context) {\n\t\t\tconst text = (context.lastComponent as Text | undefined) ?? new Text(\"\", 0, 0);\n\t\t\ttext.setText(formatGrepResult(result as any, options, theme, context.showImages));\n\t\t\treturn text;\n\t\t},\n\t};\n}\n\nexport function createGrepTool(cwd: string, options?: GrepToolOptions): AgentTool<typeof grepSchema> {\n\treturn wrapToolDefinition(createGrepToolDefinition(cwd, options));\n}\n"]}
|
package/dist/core/tools/grep.js
CHANGED
|
@@ -167,7 +167,7 @@ export function createGrepToolDefinition(cwd, options) {
|
|
|
167
167
|
if (matches.length === 0) {
|
|
168
168
|
const service = getEmbsearchService(cwd);
|
|
169
169
|
const hint = service?.isAvailable()
|
|
170
|
-
? "\n\n[Hint: no exact matches; use
|
|
170
|
+
? "\n\n[Hint: no exact matches; use the search tool (semantic or hybrid mode) to find conceptually related code]"
|
|
171
171
|
: "";
|
|
172
172
|
settle(() => resolve({
|
|
173
173
|
content: [{ type: "text", text: `No matches found${hint}` }],
|