sweet-search 2.5.2 → 2.5.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/core/cli.js +24 -3
- package/core/graph/graph-expansion.js +215 -36
- package/core/graph/graph-extractor.js +196 -11
- package/core/graph/graph-search.js +395 -92
- package/core/graph/hcgs-generator.js +2 -1
- package/core/graph/index.js +2 -0
- package/core/graph/repo-map.js +28 -6
- package/core/graph/structural-answer-cues.js +168 -0
- package/core/graph/structural-callsite-hints.js +40 -0
- package/core/graph/structural-context-format.js +40 -0
- package/core/graph/structural-context.js +450 -0
- package/core/graph/structural-forward-push.js +156 -0
- package/core/graph/structural-header-context.js +19 -0
- package/core/graph/structural-importance.js +148 -0
- package/core/graph/structural-pagerank.js +197 -0
- package/core/graph/summary-manager.js +13 -9
- package/core/incremental-indexing/application/dirty-scan.mjs +236 -0
- package/core/incremental-indexing/application/file-watcher.mjs +197 -0
- package/core/incremental-indexing/application/maintenance-handlers.mjs +519 -0
- package/core/incremental-indexing/application/maintenance-worker.mjs +380 -0
- package/core/incremental-indexing/application/operator-cli.mjs +554 -0
- package/core/incremental-indexing/application/production-li-delta.mjs +192 -0
- package/core/incremental-indexing/application/production-reconciler-helpers.mjs +107 -0
- package/core/incremental-indexing/application/production-reconciler.mjs +583 -0
- package/core/incremental-indexing/application/reconciler.mjs +477 -0
- package/core/incremental-indexing/application/tombstone-injector.mjs +148 -0
- package/core/incremental-indexing/domain/chunk-identity.mjs +260 -0
- package/core/incremental-indexing/domain/encoder-deps.mjs +193 -0
- package/core/incremental-indexing/domain/encoder-input.mjs +225 -0
- package/core/incremental-indexing/domain/interval-autotune.mjs +255 -0
- package/core/incremental-indexing/domain/reconcile-counters.mjs +149 -0
- package/core/incremental-indexing/domain/watermark-scheduler.mjs +239 -0
- package/core/incremental-indexing/infrastructure/artifact-temp-sweep.mjs +163 -0
- package/core/incremental-indexing/infrastructure/baseline-readiness.mjs +121 -0
- package/core/incremental-indexing/infrastructure/dirty-set.mjs +233 -0
- package/core/incremental-indexing/infrastructure/graph-gc.mjs +314 -0
- package/core/incremental-indexing/infrastructure/hashing.mjs +298 -0
- package/core/incremental-indexing/infrastructure/hcgs-invalidation.mjs +182 -0
- package/core/incremental-indexing/infrastructure/li-segment-merge.mjs +278 -0
- package/core/incremental-indexing/infrastructure/li-segment-state.mjs +173 -0
- package/core/incremental-indexing/infrastructure/lockfile.mjs +119 -0
- package/core/incremental-indexing/infrastructure/maintenance-state-reader.mjs +283 -0
- package/core/incremental-indexing/infrastructure/manifest.mjs +194 -0
- package/core/incremental-indexing/infrastructure/path-filter.mjs +190 -0
- package/core/incremental-indexing/infrastructure/reader-heartbeat.mjs +201 -0
- package/core/incremental-indexing/infrastructure/schema-migrations.mjs +257 -0
- package/core/incremental-indexing/infrastructure/sparse-gram-delta.mjs +335 -0
- package/core/incremental-indexing/infrastructure/sqlite-fts5.mjs +176 -0
- package/core/incremental-indexing/infrastructure/staleness-display.mjs +105 -0
- package/core/incremental-indexing/infrastructure/tombstone-bitmap.mjs +234 -0
- package/core/incremental-indexing/infrastructure/vector-delta-writer.mjs +359 -0
- package/core/incremental-indexing/infrastructure/vector-gc.mjs +133 -0
- package/core/incremental-indexing/infrastructure/worktree-stamp.mjs +155 -0
- package/core/incremental-indexing/infrastructure/wsl2-detect.mjs +115 -0
- package/core/indexing/admission-policy.js +139 -0
- package/core/indexing/artifact-builder.js +29 -12
- package/core/indexing/ast-chunker.js +107 -30
- package/core/indexing/dedup/exemplar-selector.js +19 -1
- package/core/indexing/gitignore-filter.js +223 -0
- package/core/indexing/incremental-tracker.js +99 -30
- package/core/indexing/index-codebase-v21.js +6 -5
- package/core/indexing/index-maintainer.mjs +698 -6
- package/core/indexing/indexer-ann.js +99 -15
- package/core/indexing/indexer-build.js +158 -45
- package/core/indexing/indexer-empty-baseline.js +80 -0
- package/core/indexing/indexer-manifest.js +66 -0
- package/core/indexing/indexer-phases.js +56 -23
- package/core/indexing/indexer-sparse-gram.js +54 -13
- package/core/indexing/indexer-utils.js +26 -208
- package/core/indexing/indexing-file-policy.js +32 -7
- package/core/indexing/maintainer-launcher.mjs +137 -0
- package/core/indexing/merkle-tracker.js +251 -244
- package/core/indexing/model-pool.js +46 -5
- package/core/infrastructure/code-graph-repository.js +758 -6
- package/core/infrastructure/code-graph-visibility.js +157 -0
- package/core/infrastructure/codebase-repository.js +100 -13
- package/core/infrastructure/config/search.js +1 -1
- package/core/infrastructure/db-utils.js +118 -0
- package/core/infrastructure/dedup-hashing.js +10 -13
- package/core/infrastructure/hardware-capability.js +17 -7
- package/core/infrastructure/index.js +8 -2
- package/core/infrastructure/language-patterns/maps.js +4 -1
- package/core/infrastructure/language-patterns/registry-core.js +56 -17
- package/core/infrastructure/language-patterns/registry-object-oriented.js +12 -5
- package/core/infrastructure/language-patterns.js +69 -0
- package/core/infrastructure/model-registry.js +20 -0
- package/core/infrastructure/native-inference.js +7 -12
- package/core/infrastructure/native-resolver.js +52 -37
- package/core/infrastructure/native-sparse-gram.js +261 -20
- package/core/infrastructure/native-tokenizer.js +6 -15
- package/core/infrastructure/simd-distance.js +10 -16
- package/core/infrastructure/sparse-gram-delta-reader.js +76 -0
- package/core/infrastructure/structural-alias-resolver.js +122 -0
- package/core/infrastructure/structural-candidate-ranker.js +34 -0
- package/core/infrastructure/structural-context-repository.js +472 -0
- package/core/infrastructure/structural-context-utils.js +51 -0
- package/core/infrastructure/structural-graph-signals.js +121 -0
- package/core/infrastructure/structural-qualified-resolution.js +15 -0
- package/core/infrastructure/structural-source-definitions.js +100 -0
- package/core/infrastructure/tombstone-bitmap-reader.js +139 -0
- package/core/infrastructure/tree-sitter-provider.js +811 -37
- package/core/prompt-optimization/data/p7-final/sweet-search-system-prompt.md +50 -0
- package/core/query/query-router.js +55 -5
- package/core/ranking/file-kind-ranking.js +2192 -15
- package/core/ranking/late-interaction-index.js +87 -12
- package/core/search/cli-decoration.js +290 -0
- package/core/search/context-expander.js +988 -78
- package/core/search/index.js +1 -0
- package/core/search/output-policy.js +275 -0
- package/core/search/search-anchor.js +499 -0
- package/core/search/search-boost.js +93 -1
- package/core/search/search-cli.js +61 -204
- package/core/search/search-hybrid.js +250 -10
- package/core/search/search-pattern-chunks.js +57 -8
- package/core/search/search-pattern-planner.js +68 -9
- package/core/search/search-pattern-prefilter.js +30 -10
- package/core/search/search-pattern-ripgrep.js +40 -4
- package/core/search/search-pattern-sparse-overlay.js +256 -0
- package/core/search/search-pattern.js +117 -29
- package/core/search/search-postprocess.js +479 -5
- package/core/search/search-read-semantic.js +260 -23
- package/core/search/search-read.js +82 -64
- package/core/search/search-reader-pin.js +71 -0
- package/core/search/search-rrf.js +279 -0
- package/core/search/search-semantic.js +110 -5
- package/core/search/search-server.js +130 -57
- package/core/search/search-trace.js +107 -0
- package/core/search/server-identity.js +93 -0
- package/core/search/session-daemon-prewarm.mjs +33 -10
- package/core/search/sweet-search.js +399 -7
- package/core/skills/sweet-index/SKILL.md +8 -6
- package/core/vector-store/binary-hnsw-index.js +194 -30
- package/core/vector-store/float-vector-store.js +96 -6
- package/core/vector-store/hnsw-index.js +220 -49
- package/eval/agent-read-workflows/bin/_ss-helpers.mjs +471 -0
- package/eval/agent-read-workflows/bin/ss-find +15 -0
- package/eval/agent-read-workflows/bin/ss-grep +12 -0
- package/eval/agent-read-workflows/bin/ss-read +14 -0
- package/eval/agent-read-workflows/bin/ss-search +18 -0
- package/eval/agent-read-workflows/bin/ss-semantic +12 -0
- package/eval/agent-read-workflows/bin/ss-trace +11 -0
- package/mcp/read-tool.js +109 -0
- package/mcp/server.js +55 -15
- package/mcp/tool-handlers.js +14 -124
- package/mcp/trace-tool.js +81 -0
- package/package.json +25 -10
- package/scripts/hooks/intercept-read.mjs +55 -0
- package/scripts/hooks/remind-tools.mjs +40 -0
- package/scripts/init.js +698 -54
- package/scripts/inject-agent-instructions.js +431 -0
- package/scripts/install-prompt-reminders.js +188 -0
- package/scripts/install-tool-enforcement.js +220 -0
- package/scripts/smoke-test.js +12 -9
- package/scripts/uninstall.js +276 -18
- package/scripts/write-claude-rules.js +110 -0
|
@@ -0,0 +1,471 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Bench-local agent wrappers for Sweet Search. Each subcommand is a thin,
|
|
3
|
+
// agent-friendly skin over the JS API:
|
|
4
|
+
// grep → SweetSearch.bareGrep (indexed lexical grep, gram-prefiltered)
|
|
5
|
+
// find → SweetSearch.patternSearch (ColGrep — regex candidates, MaxSim re-rank)
|
|
6
|
+
// read → search-read.readFile (filesystem-grounded read with optional line range)
|
|
7
|
+
// semantic → search-read-semantic.readSemantic (query-specific spans within one file)
|
|
8
|
+
//
|
|
9
|
+
// Output is compact, deterministic, agent-readable (one match per line for
|
|
10
|
+
// discovery; fenced code for reads). No colour codes. No JSON unless asked.
|
|
11
|
+
|
|
12
|
+
import path from 'node:path';
|
|
13
|
+
import { createHash } from 'node:crypto';
|
|
14
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
15
|
+
import { fileURLToPath } from 'node:url';
|
|
16
|
+
|
|
17
|
+
// 8-char SHA1 prefix is enough for grouping identical queries across
|
|
18
|
+
// benchmark runs without bloating artifacts.
|
|
19
|
+
function shortQueryHash(q) {
|
|
20
|
+
try { return createHash('sha1').update(String(q)).digest('hex').slice(0, 16); }
|
|
21
|
+
catch { return null; }
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
25
|
+
const REPO_ROOT = path.resolve(__dirname, '../../..');
|
|
26
|
+
|
|
27
|
+
// The agent's cwd is the target repo. SWEET_SEARCH_PROJECT_ROOT must point
|
|
28
|
+
// at the repo so DB_PATHS resolves to the repo's own .sweet-search/.
|
|
29
|
+
const PROJECT_ROOT = process.env.SWEET_SEARCH_PROJECT_ROOT || process.cwd();
|
|
30
|
+
|
|
31
|
+
if (!existsSync(path.join(PROJECT_ROOT, '.sweet-search', 'codebase.db'))) {
|
|
32
|
+
process.stderr.write(
|
|
33
|
+
`[ss-*] no Sweet Search index at ${PROJECT_ROOT}/.sweet-search/codebase.db\n` +
|
|
34
|
+
`Run: SWEET_SEARCH_PROJECT_ROOT=${PROJECT_ROOT} node ${REPO_ROOT}/core/indexing/index-codebase-v21.js --full --sqlite-fast\n`
|
|
35
|
+
);
|
|
36
|
+
process.exit(2);
|
|
37
|
+
}
|
|
38
|
+
process.env.SWEET_SEARCH_PROJECT_ROOT = PROJECT_ROOT;
|
|
39
|
+
|
|
40
|
+
const subcommand = process.argv[2];
|
|
41
|
+
const rest = process.argv.slice(3);
|
|
42
|
+
|
|
43
|
+
function parseFlag(args, name, fallback) {
|
|
44
|
+
const i = args.indexOf(name);
|
|
45
|
+
if (i === -1) return fallback;
|
|
46
|
+
const v = args[i + 1];
|
|
47
|
+
args.splice(i, 2);
|
|
48
|
+
return v;
|
|
49
|
+
}
|
|
50
|
+
function parseShortFlag(args, names, fallback) {
|
|
51
|
+
for (const n of names) {
|
|
52
|
+
const i = args.indexOf(n);
|
|
53
|
+
if (i !== -1) { const v = args[i + 1]; args.splice(i, 2); return v; }
|
|
54
|
+
}
|
|
55
|
+
return fallback;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function getSweetSearch() {
|
|
59
|
+
const { SweetSearch } = await import(path.join(REPO_ROOT, 'core/search/sweet-search.js'));
|
|
60
|
+
const s = new SweetSearch({ projectRoot: PROJECT_ROOT });
|
|
61
|
+
await s.init();
|
|
62
|
+
return s;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function ensureWarmServerReady({ timeoutMs = 60000, intervalMs = 500 } = {}) {
|
|
66
|
+
const { isServerRunning, autoSpawnServer } = await import(path.join(REPO_ROOT, 'core/search/search-server.js'));
|
|
67
|
+
if (await isServerRunning()) return true;
|
|
68
|
+
|
|
69
|
+
// autoSpawnServer has a short built-in timeout. It may return false while the
|
|
70
|
+
// detached server is still finishing model/index load, so poll afterwards.
|
|
71
|
+
await autoSpawnServer();
|
|
72
|
+
const deadline = Date.now() + timeoutMs;
|
|
73
|
+
while (Date.now() < deadline) {
|
|
74
|
+
if (await isServerRunning()) return true;
|
|
75
|
+
await new Promise(resolve => setTimeout(resolve, intervalMs));
|
|
76
|
+
}
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// --- subcommands ----------------------------------------------------------
|
|
81
|
+
|
|
82
|
+
async function cmdGrep(args) {
|
|
83
|
+
const k = +parseShortFlag(args, ['-k', '--top'], 20);
|
|
84
|
+
const regex = args[0];
|
|
85
|
+
if (!regex) {
|
|
86
|
+
process.stderr.write('Usage: ss-grep <regex> [-k N]\n');
|
|
87
|
+
process.exit(2);
|
|
88
|
+
}
|
|
89
|
+
const s = await getSweetSearch();
|
|
90
|
+
const result = await s.bareGrep(regex, null, { regex, maxMatches: k * 5, contextLines: 0 });
|
|
91
|
+
// Group by file, take first k matches across all files (ordered as bareGrep returns).
|
|
92
|
+
const grouped = new Map();
|
|
93
|
+
for (const r of result.results.slice(0, k * 5)) {
|
|
94
|
+
if (!grouped.has(r.file)) grouped.set(r.file, []);
|
|
95
|
+
grouped.get(r.file).push(r);
|
|
96
|
+
}
|
|
97
|
+
let printed = 0;
|
|
98
|
+
process.stdout.write(`# ss-grep: ${result.results.length} total match(es) for /${regex}/\n`);
|
|
99
|
+
for (const [file, lines] of grouped) {
|
|
100
|
+
for (const r of lines) {
|
|
101
|
+
const text = (r.matchText || '').replace(/\s+/g, ' ').trim().slice(0, 140);
|
|
102
|
+
process.stdout.write(`${file}:${r.line}: ${text}\n`);
|
|
103
|
+
printed++;
|
|
104
|
+
if (printed >= k) break;
|
|
105
|
+
}
|
|
106
|
+
if (printed >= k) break;
|
|
107
|
+
}
|
|
108
|
+
if (printed === 0) process.stdout.write('(no matches)\n');
|
|
109
|
+
process.exit(0);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function cmdFind(args) {
|
|
113
|
+
// ColGrep pattern search with token-budgeted agent packaging — returns the
|
|
114
|
+
// FULL useful answer (ranked code blocks + confidence + sufficiency), the same
|
|
115
|
+
// agent packaging ss-search emits. ss-grep is the short/locator counterpart, so
|
|
116
|
+
// ss-find defaults to the full answer: it saves the follow-up read entirely.
|
|
117
|
+
// (Mirrors the agent-in-the-loop H2H adapter eval/agent-eval/tools/
|
|
118
|
+
// pattern-agent-tools.js, which calls search(...,{format:'agent'}).)
|
|
119
|
+
let format = 'agent';
|
|
120
|
+
if (args.includes('--full')) { format = 'agent_full'; args.splice(args.indexOf('--full'), 1); }
|
|
121
|
+
if (args.includes('--xl')) { format = 'agent_full_xl'; args.splice(args.indexOf('--xl'), 1); }
|
|
122
|
+
const k = +parseShortFlag(args, ['-k', '--top'], 6);
|
|
123
|
+
const regex = parseFlag(args, '--regex', '');
|
|
124
|
+
const query = args[0];
|
|
125
|
+
if (!query) {
|
|
126
|
+
process.stderr.write('Usage: ss-find "<query>" --regex "<regex>" [--full|--xl] [-k N]\n');
|
|
127
|
+
process.exit(2);
|
|
128
|
+
}
|
|
129
|
+
const effectiveRegex = regex || '';
|
|
130
|
+
const s = await getSweetSearch();
|
|
131
|
+
if (!s.hasLateInteractionIndex) {
|
|
132
|
+
process.stderr.write(`[ss-find] no late-interaction index — falling back to ss-grep\n`);
|
|
133
|
+
return cmdGrep([effectiveRegex || query, '-k', String(k)]);
|
|
134
|
+
}
|
|
135
|
+
const response = await s.patternSearch(query, null, {
|
|
136
|
+
regex: effectiveRegex || `\\b\\w+\\b`,
|
|
137
|
+
k,
|
|
138
|
+
format,
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// Header (visible to agent)
|
|
142
|
+
process.stdout.write(`# ss-find: ColGrep ${response.results?.length || 0} for "${query}" /${effectiveRegex || '*'}/` +
|
|
143
|
+
` budget=${response.tokenBudget} used=${response.tokensUsed} subMode=${response.subMode ?? format}\n`);
|
|
144
|
+
if (response.confidence) {
|
|
145
|
+
process.stdout.write(`# confidence=${response.confidence}${response.confidenceReason ? ' (' + response.confidenceReason + ')' : ''}` +
|
|
146
|
+
`${response.sufficient ? ' sufficient=YES' : ' sufficient=no'}\n`);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Per-result blocks — identical shape to ss-search's agent packaging.
|
|
150
|
+
for (const r of response.results || []) {
|
|
151
|
+
const sym = r.symbol ? ` [${r.symbolType || 'code'}: ${r.symbol}]` : '';
|
|
152
|
+
const kind = r.expansionKind ? ` kind=${r.expansionKind}` : '';
|
|
153
|
+
const stale = r.stale ? ' STALE' : '';
|
|
154
|
+
process.stdout.write(`\n## #${r.rank} ${r.file}:${r.startLine}-${r.endLine}${sym} (${r.presentation}${kind}${stale}) score=${(r.score || 0).toFixed(3)}\n`);
|
|
155
|
+
if (r.headerContext) {
|
|
156
|
+
process.stdout.write(`### imports\n\`\`\`\n${r.headerContext}\n\`\`\`\n`);
|
|
157
|
+
}
|
|
158
|
+
if (r.code) {
|
|
159
|
+
process.stdout.write(`\`\`\`\n${r.code}\n\`\`\`\n`);
|
|
160
|
+
} else if (r.summary) {
|
|
161
|
+
process.stdout.write(`${r.summary}\n`);
|
|
162
|
+
}
|
|
163
|
+
if (r.neighbors && r.neighbors.rendered) {
|
|
164
|
+
process.stdout.write(`### related (1-hop graph, ~${r.neighbors.tokens} tok)\n${r.neighbors.rendered}\n`);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
if (!response.results || response.results.length === 0) process.stdout.write('(no matches)\n');
|
|
168
|
+
process.exit(0);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async function cmdRead(args) {
|
|
172
|
+
const file = args[0];
|
|
173
|
+
if (!file) {
|
|
174
|
+
process.stderr.write('Usage: ss-read <file> # whole file\n');
|
|
175
|
+
process.stderr.write(' ss-read <file> <start> # ONE line\n');
|
|
176
|
+
process.stderr.write(' ss-read <file> <start> <end>\n');
|
|
177
|
+
process.exit(2);
|
|
178
|
+
}
|
|
179
|
+
// If start is provided and end is omitted, read EXACTLY that one line —
|
|
180
|
+
// no open-ended start-to-EOF (which a previous version did and which
|
|
181
|
+
// caused accidental over-reading on large files).
|
|
182
|
+
let start = null, end = null;
|
|
183
|
+
if (args[1] != null) {
|
|
184
|
+
start = +args[1];
|
|
185
|
+
if (!Number.isFinite(start) || start < 1) {
|
|
186
|
+
process.stderr.write(`[ss-read] invalid start line: "${args[1]}"\n`);
|
|
187
|
+
process.exit(2);
|
|
188
|
+
}
|
|
189
|
+
if (args[2] != null) {
|
|
190
|
+
end = +args[2];
|
|
191
|
+
if (!Number.isFinite(end) || end < start) {
|
|
192
|
+
process.stderr.write(`[ss-read] invalid end line: "${args[2]}" (must be ≥ start ${start})\n`);
|
|
193
|
+
process.exit(2);
|
|
194
|
+
}
|
|
195
|
+
} else {
|
|
196
|
+
end = start; // single-line read
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
const { readFile } = await import(path.join(REPO_ROOT, 'core/search/search-read.js'));
|
|
200
|
+
const r = await readFile({ path: file, projectRoot: PROJECT_ROOT, startLine: start ?? undefined, endLine: end ?? undefined });
|
|
201
|
+
if (!r.ok) {
|
|
202
|
+
process.stderr.write(`[ss-read] error: ${r.error}\n`);
|
|
203
|
+
process.exit(1);
|
|
204
|
+
}
|
|
205
|
+
const range = r.range ? ` (lines ${r.range.startLine}-${r.range.endLine} of ${r.totalLines})` : ` (${r.totalLines} lines)`;
|
|
206
|
+
const fence = r.language ? '```' + r.language : '```';
|
|
207
|
+
process.stdout.write(`# ss-read ${r.file}${range}\n${fence}\n${r.text}\n\`\`\`\n`);
|
|
208
|
+
process.exit(0);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
async function cmdAgentSearch(args) {
|
|
212
|
+
// Main sweet-search auto/CatBoost search with token-budgeted agent packaging.
|
|
213
|
+
//
|
|
214
|
+
// Usage:
|
|
215
|
+
// ss-search "<query>" → format=agent (auto-pick 4k/8k/12k)
|
|
216
|
+
// ss-search "<query>" --full → force 8k (rarely needed; default auto-picks)
|
|
217
|
+
// ss-search "<query>" --xl → force 12k (rarely needed; default auto-picks)
|
|
218
|
+
// ss-search "<query>" -k 5 → top-K results
|
|
219
|
+
// ss-search "<query>" --mode hybrid → force a mode (default: auto/CatBoost)
|
|
220
|
+
//
|
|
221
|
+
// Output is agent-readable: a meta header with routed mode + budget,
|
|
222
|
+
// followed by per-result blocks with file/line + fenced code. A trailing
|
|
223
|
+
// structured marker line `<<SS_ROUTE_META>>{...json...}` lets the bench
|
|
224
|
+
// post-process parse routing/budget telemetry without affecting the agent.
|
|
225
|
+
let format = 'agent';
|
|
226
|
+
if (args.includes('--full')) { format = 'agent_full'; args.splice(args.indexOf('--full'), 1); }
|
|
227
|
+
if (args.includes('--xl')) { format = 'agent_full_xl'; args.splice(args.indexOf('--xl'), 1); }
|
|
228
|
+
const k = +parseShortFlag(args, ['-k', '--top'], 5);
|
|
229
|
+
const mode = parseFlag(args, '--mode', 'auto');
|
|
230
|
+
const query = args[0];
|
|
231
|
+
if (!query) {
|
|
232
|
+
process.stderr.write('Usage: ss-search "<query>" [--full|--xl] [-k N] [--mode auto|lexical|semantic|hybrid]\n');
|
|
233
|
+
process.exit(2);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const { queryServer } = await import(path.join(REPO_ROOT, 'core/search/search-server.js'));
|
|
237
|
+
const serverUsed = await ensureWarmServerReady();
|
|
238
|
+
if (!serverUsed) {
|
|
239
|
+
process.stderr.write('[ss-search] warm server is not ready; refusing cold direct search in benchmark wrapper\n');
|
|
240
|
+
process.exit(1);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const response = await queryServer(query, { topK: k, mode, format });
|
|
244
|
+
if (response?.error) {
|
|
245
|
+
process.stderr.write(`[ss-search] server error: ${response.error}\n`);
|
|
246
|
+
process.exit(1);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// REPO ISOLATION: refuse to return results from a daemon serving a different
|
|
250
|
+
// repo. The bench harness uses /tmp/sweet-search.sock, which is a global socket;
|
|
251
|
+
// a multi-repo bench fan-out previously reused a stale daemon and silently
|
|
252
|
+
// returned cross-repo matches. Fail closed instead.
|
|
253
|
+
const requestedProjectRoot = path.resolve(PROJECT_ROOT);
|
|
254
|
+
const serverProjectRoot = response?.serverProjectRoot
|
|
255
|
+
? path.resolve(response.serverProjectRoot) : null;
|
|
256
|
+
const repoMatches = serverProjectRoot != null && serverProjectRoot === requestedProjectRoot;
|
|
257
|
+
if (!repoMatches) {
|
|
258
|
+
process.stderr.write(
|
|
259
|
+
`[ss-search] repo isolation violation: requested projectRoot=${requestedProjectRoot} ` +
|
|
260
|
+
`but server reports serverProjectRoot=${serverProjectRoot ?? '<null>'}. ` +
|
|
261
|
+
`Refusing to surface cross-repo results.\n`
|
|
262
|
+
);
|
|
263
|
+
// Emit a structured trailer anyway so the bench can capture the failure.
|
|
264
|
+
const failMeta = {
|
|
265
|
+
query,
|
|
266
|
+
queryHash: shortQueryHash(query),
|
|
267
|
+
queryLen: query.length,
|
|
268
|
+
routedMode: response?.stats?.routing?.mode || null,
|
|
269
|
+
routeConfidence: typeof response?.stats?.routing?.confidence === 'number'
|
|
270
|
+
? response.stats.routing.confidence : null,
|
|
271
|
+
routeMethod: response?.stats?.routing?.method || null,
|
|
272
|
+
routerLatency_us: typeof response?.stats?.routing?.latency_us === 'number'
|
|
273
|
+
? response.stats.routing.latency_us : null,
|
|
274
|
+
serverUsed: true,
|
|
275
|
+
serverProjectRoot,
|
|
276
|
+
requestedProjectRoot,
|
|
277
|
+
repoMatches: false,
|
|
278
|
+
error: 'repo-isolation-mismatch',
|
|
279
|
+
};
|
|
280
|
+
process.stdout.write(`\n<<SS_ROUTE_META>>${JSON.stringify(failMeta)}\n`);
|
|
281
|
+
process.exit(3);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// The packaged response shape comes from packageForAgent (or pattern's own
|
|
285
|
+
// packager when CatBoost routes to pattern). Both include:
|
|
286
|
+
// .results[] with {rank, file, startLine, endLine, symbol, symbolType,
|
|
287
|
+
// presentation, code, codeTokens, expansionKind, ...}
|
|
288
|
+
// .tokenBudget, .tokensUsed, .subMode, .confidence, .sufficient
|
|
289
|
+
// .stats.routing (when produced by the main pipeline)
|
|
290
|
+
const routing = response.stats?.routing || {};
|
|
291
|
+
const routedMode = routing.mode || 'pattern';
|
|
292
|
+
const routeConfidence = typeof routing.confidence === 'number' ? routing.confidence : null;
|
|
293
|
+
// Route attribution: where did the decision come from? Values produced by
|
|
294
|
+
// core/query/query-router.js: 'file_pattern', 'wasm_catboost', 'wasm_rejected',
|
|
295
|
+
// 'fallback_error', 'invalid_input', 'query_too_long', 'empty_query'. When
|
|
296
|
+
// the user forced a mode, routing.method is undefined and routing.forced
|
|
297
|
+
// is true.
|
|
298
|
+
const routeMethod = routing.method || (routing.forced ? 'forced' : null);
|
|
299
|
+
const routerLatency_us = typeof routing.latency_us === 'number' ? routing.latency_us : null;
|
|
300
|
+
const tierCounts = (response.results || []).reduce((acc, r) => {
|
|
301
|
+
acc[r.presentation] = (acc[r.presentation] || 0) + 1;
|
|
302
|
+
return acc;
|
|
303
|
+
}, {});
|
|
304
|
+
const sandwichCount = (response.results || []).filter(r => r.expansionKind === 'sandwich').length;
|
|
305
|
+
const neighborCount = (response.results || []).reduce((acc, r) => acc + (r.neighbors?.count || 0), 0);
|
|
306
|
+
const headerCount = (response.results || []).filter(r => r.headerContext).length;
|
|
307
|
+
|
|
308
|
+
// Header (visible to agent)
|
|
309
|
+
const conf = routeConfidence != null ? ` conf=${routeConfidence.toFixed(2)}` : '';
|
|
310
|
+
process.stdout.write(`# ss-search: routed=${routedMode}${conf} budget=${response.tokenBudget} used=${response.tokensUsed}` +
|
|
311
|
+
` results=${response.results.length} subMode=${response.subMode}\n`);
|
|
312
|
+
if (response.confidence) {
|
|
313
|
+
process.stdout.write(`# confidence=${response.confidence}${response.confidenceReason ? ' (' + response.confidenceReason + ')' : ''}` +
|
|
314
|
+
`${response.sufficient ? ' sufficient=YES' : ' sufficient=no'}\n`);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Per-result blocks
|
|
318
|
+
for (const r of response.results || []) {
|
|
319
|
+
const sym = r.symbol ? ` [${r.symbolType || 'code'}: ${r.symbol}]` : '';
|
|
320
|
+
const kind = r.expansionKind ? ` kind=${r.expansionKind}` : '';
|
|
321
|
+
const stale = r.stale ? ' STALE' : '';
|
|
322
|
+
process.stdout.write(`\n## #${r.rank} ${r.file}:${r.startLine}-${r.endLine}${sym} (${r.presentation}${kind}${stale}) score=${(r.score || 0).toFixed(3)}\n`);
|
|
323
|
+
if (r.headerContext) {
|
|
324
|
+
process.stdout.write(`### imports\n\`\`\`\n${r.headerContext}\n\`\`\`\n`);
|
|
325
|
+
}
|
|
326
|
+
if (r.code) {
|
|
327
|
+
process.stdout.write(`\`\`\`\n${r.code}\n\`\`\`\n`);
|
|
328
|
+
} else if (r.summary) {
|
|
329
|
+
process.stdout.write(`${r.summary}\n`);
|
|
330
|
+
}
|
|
331
|
+
// Render the 1-hop graph-neighbour tier directly under top-1's code block.
|
|
332
|
+
// The package surfaces `r.neighbors` only on the rank that earned the
|
|
333
|
+
// reservation (typically top-1). Each line carries `file:line` so the
|
|
334
|
+
// agent can cite the neighbour without an extra search.
|
|
335
|
+
if (r.neighbors && r.neighbors.rendered) {
|
|
336
|
+
process.stdout.write(`### related (1-hop graph, ~${r.neighbors.tokens} tok)\n${r.neighbors.rendered}\n`);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
if (!response.results || response.results.length === 0) {
|
|
341
|
+
process.stdout.write('(no matches)\n');
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Structured trailer for bench post-processing (audit/summariseRun can parse).
|
|
345
|
+
// Route attribution fields (queryHash, routeMethod, routerLatency_us, query)
|
|
346
|
+
// let downstream analysis link a routing decision to its query and
|
|
347
|
+
// attribute failures to fast-path vs WASM vs fallback.
|
|
348
|
+
const meta = {
|
|
349
|
+
query, // exact query text (already bounded by SEARCH_SERVER_MAX_QUERY_LENGTH)
|
|
350
|
+
queryHash: shortQueryHash(query),
|
|
351
|
+
queryLen: query.length,
|
|
352
|
+
routedMode,
|
|
353
|
+
routeConfidence,
|
|
354
|
+
routeMethod,
|
|
355
|
+
routerLatency_us,
|
|
356
|
+
serverUsed,
|
|
357
|
+
serverProjectRoot,
|
|
358
|
+
requestedProjectRoot,
|
|
359
|
+
repoMatches,
|
|
360
|
+
serverPid: response.serverPid ?? null,
|
|
361
|
+
tokenBudget: response.tokenBudget,
|
|
362
|
+
tokensUsed: response.tokensUsed,
|
|
363
|
+
subMode: response.subMode,
|
|
364
|
+
resultCount: response.results?.length || 0,
|
|
365
|
+
tierCounts,
|
|
366
|
+
sandwichCount,
|
|
367
|
+
neighborCount,
|
|
368
|
+
headerCount,
|
|
369
|
+
confidence: response.confidence || null,
|
|
370
|
+
sufficient: response.sufficient ?? null,
|
|
371
|
+
sufficiencyReasons: Array.isArray(response.sufficiencyReasons) ? response.sufficiencyReasons : null,
|
|
372
|
+
unresolvedExternalCount: typeof response.unresolvedExternalCount === 'number'
|
|
373
|
+
? response.unresolvedExternalCount : null,
|
|
374
|
+
};
|
|
375
|
+
process.stdout.write(`\n<<SS_ROUTE_META>>${JSON.stringify(meta)}\n`);
|
|
376
|
+
process.exit(0);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
async function cmdSemantic(args) {
|
|
380
|
+
const file = args[0];
|
|
381
|
+
const query = args[1];
|
|
382
|
+
if (!file || !query) {
|
|
383
|
+
process.stderr.write('Usage: ss-semantic <file> "<question>" [--max-tokens N]\n');
|
|
384
|
+
process.exit(2);
|
|
385
|
+
}
|
|
386
|
+
const maxTokens = +parseFlag(args.slice(2), '--max-tokens', 800);
|
|
387
|
+
const { readSemantic } = await import(path.join(REPO_ROOT, 'core/search/search-read-semantic.js'));
|
|
388
|
+
const r = await readSemantic({
|
|
389
|
+
path: file, query, projectRoot: PROJECT_ROOT,
|
|
390
|
+
maxChars: maxTokens * 4, verbose: false,
|
|
391
|
+
});
|
|
392
|
+
if (!r.ok) {
|
|
393
|
+
process.stderr.write(`[ss-semantic] error: ${r.reason || 'unknown'}\n`);
|
|
394
|
+
process.exit(1);
|
|
395
|
+
}
|
|
396
|
+
process.stdout.write(`# ss-semantic ${r.file} | "${query}" | spans=${r.spans?.length ?? 0} | ~tokens=${r.approxTokensReturned}${r.fellBack ? ' [FALLBACK]' : ''}\n`);
|
|
397
|
+
for (const span of r.spans || []) {
|
|
398
|
+
const fence = r.language ? '```' + r.language : '```';
|
|
399
|
+
const sym = span.symbols?.length ? ` [${span.symbols.join(', ')}]` : '';
|
|
400
|
+
process.stdout.write(`### ${r.file}:${span.startLine}-${span.endLine}${sym}\n${fence}\n${span.text}\n\`\`\`\n`);
|
|
401
|
+
}
|
|
402
|
+
process.exit(0);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
async function cmdTrace(args) {
|
|
406
|
+
let json = false;
|
|
407
|
+
if (args.includes('--json')) {
|
|
408
|
+
json = true;
|
|
409
|
+
args.splice(args.indexOf('--json'), 1);
|
|
410
|
+
}
|
|
411
|
+
const symbol = args[0];
|
|
412
|
+
if (!symbol) {
|
|
413
|
+
process.stderr.write('Usage: ss-trace <symbol> [--in <file>] [--query <hint>] [--depth N] [--budget N]\n');
|
|
414
|
+
process.exit(2);
|
|
415
|
+
}
|
|
416
|
+
const { traceSymbol, formatStructuralContext } = await import(path.join(REPO_ROOT, 'core/search/search-trace.js'));
|
|
417
|
+
|
|
418
|
+
const opts = { projectRoot: PROJECT_ROOT };
|
|
419
|
+
const file = parseFlag(args, '--in', null) || parseFlag(args, '--file', null);
|
|
420
|
+
const queryHint = parseFlag(args, '--query', '') || parseFlag(args, '--hint', '');
|
|
421
|
+
const depth = parseFlag(args, '--depth', null);
|
|
422
|
+
const budget = parseFlag(args, '--budget', null);
|
|
423
|
+
if (file) opts.filePath = file;
|
|
424
|
+
if (queryHint) opts.queryHint = queryHint;
|
|
425
|
+
if (depth != null) opts.maxDepth = +depth;
|
|
426
|
+
if (budget != null) opts.tokenBudget = +budget;
|
|
427
|
+
|
|
428
|
+
const response = traceSymbol(symbol, opts);
|
|
429
|
+
if (json) process.stdout.write(JSON.stringify(response, null, 2) + '\n');
|
|
430
|
+
else process.stdout.write(formatStructuralContext(response) + '\n');
|
|
431
|
+
|
|
432
|
+
const meta = {
|
|
433
|
+
symbol,
|
|
434
|
+
queryHash: shortQueryHash(`${symbol}:${queryHint || ''}`),
|
|
435
|
+
target: response.target ? {
|
|
436
|
+
name: response.target.name,
|
|
437
|
+
type: response.target.type,
|
|
438
|
+
file: response.target.filePath,
|
|
439
|
+
startLine: response.target.startLine,
|
|
440
|
+
} : null,
|
|
441
|
+
tokenBudget: response.tokenBudget,
|
|
442
|
+
tokensUsed: response.tokensUsed,
|
|
443
|
+
budgetTier: response.budgetTier,
|
|
444
|
+
budgetReason: response.budgetReason,
|
|
445
|
+
callers: response.sections?.callers?.total || 0,
|
|
446
|
+
callees: response.sections?.callees?.total || 0,
|
|
447
|
+
impactPaths: response.sections?.impact?.total || 0,
|
|
448
|
+
latencyMs: response.stats?.latencyMs ?? null,
|
|
449
|
+
sufficient: !!response.target,
|
|
450
|
+
};
|
|
451
|
+
process.stdout.write(`\n<<SS_TRACE_META>>${JSON.stringify(meta)}\n`);
|
|
452
|
+
process.exit(response.target ? 0 : 1);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
(async () => {
|
|
456
|
+
try {
|
|
457
|
+
if (subcommand === 'grep') await cmdGrep(rest);
|
|
458
|
+
else if (subcommand === 'find') await cmdFind(rest);
|
|
459
|
+
else if (subcommand === 'read') await cmdRead(rest);
|
|
460
|
+
else if (subcommand === 'semantic') await cmdSemantic(rest);
|
|
461
|
+
else if (subcommand === 'trace') await cmdTrace(rest);
|
|
462
|
+
else if (subcommand === 'agent-search') await cmdAgentSearch(rest);
|
|
463
|
+
else { process.stderr.write(`unknown subcommand: ${subcommand}\n`); process.exit(2); }
|
|
464
|
+
} catch (err) {
|
|
465
|
+
process.stderr.write(`[ss-*] crash: ${err.stack || err.message || err}\n`);
|
|
466
|
+
process.exit(1);
|
|
467
|
+
}
|
|
468
|
+
})();
|
|
469
|
+
|
|
470
|
+
// Mark unused for lint:
|
|
471
|
+
void readFileSync;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# ss-find: ColGrep / patternSearch — regex candidates re-ranked by MaxSim
|
|
3
|
+
# against your natural-language query, returned as the FULL agent answer
|
|
4
|
+
# (ranked code blocks + confidence/sufficiency), so no follow-up read is needed.
|
|
5
|
+
# Use for behavioural / semantic questions where lexical alone won't pinpoint the
|
|
6
|
+
# chunk. (ss-grep is the short file:line locator.)
|
|
7
|
+
#
|
|
8
|
+
# Usage: ss-find "<query>" --regex "<regex>" [--full|--xl] [-k N]
|
|
9
|
+
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
|
10
|
+
TMPERR=$(mktemp)
|
|
11
|
+
node "$DIR/_ss-helpers.mjs" find "$@" 2>"$TMPERR"
|
|
12
|
+
RC=$?
|
|
13
|
+
[ $RC -ne 0 ] && cat "$TMPERR" >&2
|
|
14
|
+
rm -f "$TMPERR"
|
|
15
|
+
exit $RC
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# ss-grep: indexed bare grep (gram-prefiltered) over the cwd's Sweet Search index.
|
|
3
|
+
# Compact agent-friendly output: file:line matchText
|
|
4
|
+
#
|
|
5
|
+
# Usage: ss-grep <regex> [-k N]
|
|
6
|
+
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
|
7
|
+
TMPERR=$(mktemp)
|
|
8
|
+
node "$DIR/_ss-helpers.mjs" grep "$@" 2>"$TMPERR"
|
|
9
|
+
RC=$?
|
|
10
|
+
[ $RC -ne 0 ] && cat "$TMPERR" >&2
|
|
11
|
+
rm -f "$TMPERR"
|
|
12
|
+
exit $RC
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# ss-read: filesystem-grounded read of one file.
|
|
3
|
+
# ss-read <file> # whole file
|
|
4
|
+
# ss-read <file> <start> # ONE line (NOT start-to-EOF)
|
|
5
|
+
# ss-read <file> <start> <end> # explicit range
|
|
6
|
+
# Open-ended start-to-EOF is intentionally not supported in the bench wrapper
|
|
7
|
+
# to prevent accidental over-reading. To pull a span, give an explicit end.
|
|
8
|
+
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
|
9
|
+
TMPERR=$(mktemp)
|
|
10
|
+
node "$DIR/_ss-helpers.mjs" read "$@" 2>"$TMPERR"
|
|
11
|
+
RC=$?
|
|
12
|
+
[ $RC -ne 0 ] && cat "$TMPERR" >&2
|
|
13
|
+
rm -f "$TMPERR"
|
|
14
|
+
exit $RC
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# ss-search: Sweet Search auto/CatBoost-routed search with token-budgeted
|
|
3
|
+
# agent packaging. Exercises the main sweet-search pipeline (lexical /
|
|
4
|
+
# semantic / hybrid / structural) with auto-tier budget by default.
|
|
5
|
+
#
|
|
6
|
+
# Usage:
|
|
7
|
+
# ss-search "<query>" # auto-picks 4k / 8k / 12k from signals
|
|
8
|
+
# ss-search "<query>" --full # force 8k (rarely needed; default auto-picks)
|
|
9
|
+
# ss-search "<query>" --xl # force 12k (rarely needed; default auto-picks)
|
|
10
|
+
# ss-search "<query>" -k N # top-K (default 5)
|
|
11
|
+
# ss-search "<query>" --mode hybrid # force a mode (default: auto)
|
|
12
|
+
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
|
13
|
+
TMPERR=$(mktemp)
|
|
14
|
+
node "$DIR/_ss-helpers.mjs" agent-search "$@" 2>"$TMPERR"
|
|
15
|
+
RC=$?
|
|
16
|
+
[ $RC -ne 0 ] && cat "$TMPERR" >&2
|
|
17
|
+
rm -f "$TMPERR"
|
|
18
|
+
exit $RC
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# ss-semantic: query-specific spans inside ONE file using read-semantic.
|
|
3
|
+
# Default --max-tokens cap of 800 keeps spans focused.
|
|
4
|
+
#
|
|
5
|
+
# Usage: ss-semantic <file> "<question>" [--max-tokens N]
|
|
6
|
+
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
|
7
|
+
TMPERR=$(mktemp)
|
|
8
|
+
node "$DIR/_ss-helpers.mjs" semantic "$@" 2>"$TMPERR"
|
|
9
|
+
RC=$?
|
|
10
|
+
[ $RC -ne 0 ] && cat "$TMPERR" >&2
|
|
11
|
+
rm -f "$TMPERR"
|
|
12
|
+
exit $RC
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# ss-trace: unified structural context for one symbol (callers, callees, impact).
|
|
3
|
+
#
|
|
4
|
+
# Usage: ss-trace <symbol> [--in <file>] [--query <hint>] [--depth N] [--budget N]
|
|
5
|
+
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
|
6
|
+
TMPERR=$(mktemp)
|
|
7
|
+
node "$DIR/_ss-helpers.mjs" trace "$@" 2>"$TMPERR"
|
|
8
|
+
RC=$?
|
|
9
|
+
[ $RC -ne 0 ] && cat "$TMPERR" >&2
|
|
10
|
+
rm -f "$TMPERR"
|
|
11
|
+
exit $RC
|
package/mcp/read-tool.js
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
const ReadFileResultSchema = z.object({
|
|
4
|
+
file: z.string(),
|
|
5
|
+
absolutePath: z.string().optional(),
|
|
6
|
+
ok: z.boolean(),
|
|
7
|
+
exact: z.boolean().optional(),
|
|
8
|
+
indexed: z.boolean().optional(),
|
|
9
|
+
language: z.string().nullable().optional(),
|
|
10
|
+
totalLines: z.number().int().optional(),
|
|
11
|
+
bytes: z.number().int().optional(),
|
|
12
|
+
mtimeMs: z.number().optional(),
|
|
13
|
+
range: z.object({
|
|
14
|
+
startLine: z.number().int(),
|
|
15
|
+
endLine: z.number().int(),
|
|
16
|
+
}).nullable().optional(),
|
|
17
|
+
text: z.string().optional(),
|
|
18
|
+
chunks: z.array(z.object({
|
|
19
|
+
id: z.string(),
|
|
20
|
+
symbol: z.string().nullable().optional(),
|
|
21
|
+
type: z.string().nullable().optional(),
|
|
22
|
+
startLine: z.number().int().nullable().optional(),
|
|
23
|
+
endLine: z.number().int().nullable().optional(),
|
|
24
|
+
signature: z.string().nullable().optional(),
|
|
25
|
+
})).optional(),
|
|
26
|
+
error: z.string().optional(),
|
|
27
|
+
timings: z.object({ totalMs: z.number() }).optional(),
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
export const ReadOutputSchema = z.object({
|
|
31
|
+
files: z.array(ReadFileResultSchema),
|
|
32
|
+
totalMs: z.number(),
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const ReadSemanticSpanSchema = z.object({
|
|
36
|
+
startLine: z.number().int(),
|
|
37
|
+
endLine: z.number().int(),
|
|
38
|
+
score: z.number(),
|
|
39
|
+
symbols: z.array(z.string()).optional(),
|
|
40
|
+
types: z.array(z.string()).optional(),
|
|
41
|
+
chunkIds: z.array(z.string()).optional(),
|
|
42
|
+
text: z.string(),
|
|
43
|
+
truncated: z.boolean().optional(),
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
export const ReadSemanticOutputSchema = z.object({
|
|
47
|
+
file: z.string(),
|
|
48
|
+
query: z.string(),
|
|
49
|
+
ok: z.boolean(),
|
|
50
|
+
indexed: z.boolean(),
|
|
51
|
+
fellBack: z.boolean(),
|
|
52
|
+
reason: z.string().optional(),
|
|
53
|
+
language: z.string().nullable().optional(),
|
|
54
|
+
totalLines: z.number().int().optional(),
|
|
55
|
+
spans: z.array(ReadSemanticSpanSchema),
|
|
56
|
+
charsReturned: z.number().int().optional(),
|
|
57
|
+
approxTokensReturned: z.number().int().optional(),
|
|
58
|
+
signals: z.record(z.string(), z.any()).optional(),
|
|
59
|
+
timings: z.record(z.string(), z.number()).optional(),
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* @param {{ files: Array<{path: string, startLine?: number, endLine?: number}>, includeMetadata?: boolean }} args
|
|
64
|
+
* @param {{ PROJECT_ROOT: string }} deps
|
|
65
|
+
*/
|
|
66
|
+
export async function handleRead(args, deps) {
|
|
67
|
+
try {
|
|
68
|
+
const { readFiles, formatReadResults } = await import('../core/search/index.js');
|
|
69
|
+
const result = await readFiles(args.files || [], {
|
|
70
|
+
projectRoot: deps.PROJECT_ROOT,
|
|
71
|
+
includeMetadata: args.includeMetadata !== false,
|
|
72
|
+
});
|
|
73
|
+
return {
|
|
74
|
+
content: [{ type: 'text', text: formatReadResults(result, 'agent') }],
|
|
75
|
+
structuredContent: result,
|
|
76
|
+
};
|
|
77
|
+
} catch (err) {
|
|
78
|
+
const msg = (err.message || 'read failed').split('\n')[0];
|
|
79
|
+
return { content: [{ type: 'text', text: `read error: ${msg}` }], isError: true };
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* @param {{ file: string, query: string, topK?: number, threshold?: number, contextLines?: number, maxChars?: number, maxTokens?: number, verbose?: boolean }} args
|
|
85
|
+
* @param {{ PROJECT_ROOT: string }} deps
|
|
86
|
+
*/
|
|
87
|
+
export async function handleReadSemantic(args, deps) {
|
|
88
|
+
try {
|
|
89
|
+
const { readSemantic, formatReadSemanticResult } = await import('../core/search/index.js');
|
|
90
|
+
const result = await readSemantic({
|
|
91
|
+
path: args.file,
|
|
92
|
+
query: args.query,
|
|
93
|
+
topK: args.topK,
|
|
94
|
+
threshold: args.threshold,
|
|
95
|
+
contextLines: args.contextLines,
|
|
96
|
+
maxChars: args.maxChars,
|
|
97
|
+
maxTokens: args.maxTokens,
|
|
98
|
+
projectRoot: deps.PROJECT_ROOT,
|
|
99
|
+
verbose: args.verbose,
|
|
100
|
+
});
|
|
101
|
+
return {
|
|
102
|
+
content: [{ type: 'text', text: formatReadSemanticResult(result, 'agent') }],
|
|
103
|
+
structuredContent: result,
|
|
104
|
+
};
|
|
105
|
+
} catch (err) {
|
|
106
|
+
const msg = (err.message || 'read-semantic failed').split('\n')[0];
|
|
107
|
+
return { content: [{ type: 'text', text: `read-semantic error: ${msg}` }], isError: true };
|
|
108
|
+
}
|
|
109
|
+
}
|