sweet-search 2.6.17 → 2.7.0
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/README.md +7 -3
- package/core/infrastructure/code-graph-repository.js +58 -0
- package/core/infrastructure/model-fetcher.js +7 -0
- package/core/search/agent-pack-completion.js +493 -0
- package/core/search/agent-span-client.js +87 -0
- package/core/search/agent-span-ledger.js +447 -0
- package/core/search/context-expander.js +29 -0
- package/core/search/grep-output-shaping.js +28 -0
- package/core/search/regex-dialect.js +328 -0
- package/core/search/search-format.js +96 -0
- package/core/search/search-pattern.js +62 -10
- package/core/search/search-read.js +57 -10
- package/core/search/search-server.js +375 -7
- package/core/search/session-daemon-prewarm.mjs +75 -0
- package/core/search/sweet-search.js +7 -1
- package/core/search/unread-symbol-ranking.js +90 -0
- package/eval/agent-read-workflows/bin/_ss-helpers.mjs +242 -43
- package/mcp/read-tool.js +34 -3
- package/mcp/server.js +55 -9
- package/mcp/tool-handlers.js +60 -7
- package/package.json +9 -9
- package/scripts/init.js +30 -5
- package/scripts/uninstall.js +21 -19
package/mcp/tool-handlers.js
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
// mcp/tool-handlers.js — Extracted tool handler functions (SOLID refactor)
|
|
2
2
|
// Each handler receives its dependencies via a `deps` parameter.
|
|
3
|
-
|
|
4
3
|
import { z } from 'zod';
|
|
5
4
|
import { existsSync } from 'node:fs';
|
|
6
5
|
import { spawn } from 'node:child_process';
|
|
7
6
|
import path from 'node:path';
|
|
8
|
-
|
|
7
|
+
import { collectAgentShownSpans, exactRereadOmissionEnabled, renderShownFullTrailer, shownSpanTrailerEnabled } from '../core/search/agent-span-ledger.js';
|
|
8
|
+
import { sendAgentSpanOperation } from '../core/search/agent-span-client.js';
|
|
9
|
+
import { renderRegexDialectHint } from '../core/search/regex-dialect.js';
|
|
9
10
|
// ---------------------------------------------------------------------------
|
|
10
11
|
// Output schemas (Zod — SDK converts to JSON Schema for clients)
|
|
11
12
|
// ---------------------------------------------------------------------------
|
|
12
|
-
|
|
13
13
|
const SearchResultSchema = z.object({
|
|
14
14
|
file: z.string(),
|
|
15
15
|
score: z.number(),
|
|
@@ -25,6 +25,22 @@ const SearchResultSchema = z.object({
|
|
|
25
25
|
presentation: z.enum(['full', 'preview', 'summary']).optional(),
|
|
26
26
|
code: z.string().nullable().optional(),
|
|
27
27
|
codeTokens: z.number().int().optional(),
|
|
28
|
+
continuation: z.object({
|
|
29
|
+
kind: z.enum(['symbol', 'trailer']),
|
|
30
|
+
file: z.string(),
|
|
31
|
+
startLine: z.number().int(),
|
|
32
|
+
endLine: z.number().int(),
|
|
33
|
+
symbol: z.string(),
|
|
34
|
+
symbolType: z.string().nullable().optional(),
|
|
35
|
+
code: z.string().nullable().optional(),
|
|
36
|
+
rendered: z.string(),
|
|
37
|
+
tokens: z.number().int(),
|
|
38
|
+
}).optional(),
|
|
39
|
+
familyManifest: z.object({
|
|
40
|
+
rendered: z.string(),
|
|
41
|
+
tokens: z.number().int(),
|
|
42
|
+
groups: z.array(z.string()),
|
|
43
|
+
}).optional(),
|
|
28
44
|
});
|
|
29
45
|
|
|
30
46
|
export const SearchOutputSchema = z.object({
|
|
@@ -102,10 +118,11 @@ let _healthDb = null;
|
|
|
102
118
|
|
|
103
119
|
/**
|
|
104
120
|
* @param {{ query: string, k: number, mode: string, structural?: boolean, regex?: string, format?: string, tokenBudget?: number }} args
|
|
105
|
-
* @param {{ getSearcher: () => Promise<object
|
|
121
|
+
* @param {{ getSearcher: () => Promise<object>, PROJECT_ROOT?: string, agentSessionId?: string }} deps
|
|
106
122
|
*/
|
|
107
|
-
export async function handleSearch({ query, k, mode, structural, regex, format, tokenBudget },
|
|
123
|
+
export async function handleSearch({ query, k, mode, structural, regex, format, tokenBudget }, deps) {
|
|
108
124
|
try {
|
|
125
|
+
const { getSearcher } = deps;
|
|
109
126
|
const searcher = await getSearcher();
|
|
110
127
|
const searchMode = structural ? 'structural' : mode;
|
|
111
128
|
const searchResult = await searcher.search(query, {
|
|
@@ -116,11 +133,25 @@ export async function handleSearch({ query, k, mode, structural, regex, format,
|
|
|
116
133
|
...(regex && { regex }),
|
|
117
134
|
...(format && { format }),
|
|
118
135
|
...(tokenBudget && { tokenBudget }),
|
|
136
|
+
_isAgentFormat: format === 'agent',
|
|
119
137
|
});
|
|
120
138
|
|
|
121
139
|
// Agent mode: return the fully packaged response directly
|
|
122
140
|
if (searchResult.format === 'agent') {
|
|
123
141
|
const agentResults = searchResult.results || [];
|
|
142
|
+
const exactReread = exactRereadOmissionEnabled() && deps.agentSessionId;
|
|
143
|
+
const shownTrailerEnabled = shownSpanTrailerEnabled();
|
|
144
|
+
const shownSpans = (exactReread || shownTrailerEnabled)
|
|
145
|
+
? collectAgentShownSpans(agentResults, { projectRoot: deps.PROJECT_ROOT }) : [];
|
|
146
|
+
if (exactReread) {
|
|
147
|
+
await sendAgentSpanOperation({
|
|
148
|
+
operation: 'observe',
|
|
149
|
+
sessionId: deps.agentSessionId,
|
|
150
|
+
spans: shownSpans,
|
|
151
|
+
query,
|
|
152
|
+
regex,
|
|
153
|
+
});
|
|
154
|
+
}
|
|
124
155
|
const lines = agentResults
|
|
125
156
|
.filter(r => r.presentation !== 'summary')
|
|
126
157
|
.map((r) => {
|
|
@@ -134,7 +165,14 @@ export async function handleSearch({ query, k, mode, structural, regex, format,
|
|
|
134
165
|
.map(n => `${n.name} (${n.type === 'function' ? 'fn' : (n.type || 'sym')} ${n.startLine}-${n.endLine} ${n.position})`)
|
|
135
166
|
.join(' · ')} — sweep: read-semantic ${r.file}`
|
|
136
167
|
: '';
|
|
137
|
-
|
|
168
|
+
const continuation = r.continuation?.rendered
|
|
169
|
+
? `\n ${r.continuation.rendered.replace(/^#\s*/, '')}`
|
|
170
|
+
+ (r.continuation.kind === 'symbol' && r.continuation.code
|
|
171
|
+
? `\n\`\`\`\n${r.continuation.code}\n\`\`\`` : '')
|
|
172
|
+
: '';
|
|
173
|
+
const family = r.familyManifest?.rendered
|
|
174
|
+
? `\n ${r.familyManifest.rendered.replace(/^#\s*/, '')}` : '';
|
|
175
|
+
return header + symbolInfo + code + sameFile + continuation + family;
|
|
138
176
|
});
|
|
139
177
|
|
|
140
178
|
const summaries = agentResults
|
|
@@ -144,9 +182,13 @@ export async function handleSearch({ query, k, mode, structural, regex, format,
|
|
|
144
182
|
const confidence = `Confidence: ${searchResult.confidence} (${searchResult.confidenceReason})`;
|
|
145
183
|
const budget = `Tokens: ${searchResult.tokensUsed}/${searchResult.tokenBudget}`;
|
|
146
184
|
|
|
147
|
-
|
|
185
|
+
let text = lines.length > 0
|
|
148
186
|
? `${confidence} | ${budget}\n\n${lines.join('\n\n')}${summaries.length ? '\n\nAlso found:\n' + summaries.join('\n') : ''}`
|
|
149
187
|
: `No results found for "${query}"`;
|
|
188
|
+
const regexDialectNote = renderRegexDialectHint(searchResult.stats?.regexDialectHint);
|
|
189
|
+
if (regexDialectNote) text += `\n\n${regexDialectNote}`;
|
|
190
|
+
const shownTrailer = shownTrailerEnabled ? renderShownFullTrailer(shownSpans) : '';
|
|
191
|
+
if (shownTrailer) text += `\n\n${shownTrailer}`;
|
|
150
192
|
|
|
151
193
|
// Shape structuredContent to conform to SearchOutputSchema
|
|
152
194
|
const structured = {
|
|
@@ -161,6 +203,14 @@ export async function handleSearch({ query, k, mode, structural, regex, format,
|
|
|
161
203
|
presentation: r.presentation,
|
|
162
204
|
code: r.code ?? null,
|
|
163
205
|
codeTokens: r.codeTokens,
|
|
206
|
+
...(r.continuation ? { continuation: r.continuation } : {}),
|
|
207
|
+
...(r.familyManifest ? {
|
|
208
|
+
familyManifest: {
|
|
209
|
+
rendered: r.familyManifest.rendered,
|
|
210
|
+
tokens: r.familyManifest.tokens,
|
|
211
|
+
groups: r.familyManifest.groups,
|
|
212
|
+
},
|
|
213
|
+
} : {}),
|
|
164
214
|
})),
|
|
165
215
|
totalFound: searchResult.totalResults,
|
|
166
216
|
mode: searchResult.mode,
|
|
@@ -189,6 +239,9 @@ export async function handleSearch({ query, k, mode, structural, regex, format,
|
|
|
189
239
|
}
|
|
190
240
|
|
|
191
241
|
// Standard benchmark mode
|
|
242
|
+
if (exactRereadOmissionEnabled() && deps.agentSessionId) {
|
|
243
|
+
await sendAgentSpanOperation({ operation: 'observe', sessionId: deps.agentSessionId, spans: [] });
|
|
244
|
+
}
|
|
192
245
|
const { results, stats } = searchResult;
|
|
193
246
|
const structured = {
|
|
194
247
|
format: 'benchmark',
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sweet-search",
|
|
3
|
-
"version": "2.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "2.7.0",
|
|
4
|
+
"description": "Local code search for AI agents: six fast, purpose-built tools that return ranked answers, not raw grep. Because maybe grep isn't all you need... 🍬",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "core/search/sweet-search.js",
|
|
7
7
|
"exports": {
|
|
@@ -167,13 +167,13 @@
|
|
|
167
167
|
"vitest": "^4.0.16"
|
|
168
168
|
},
|
|
169
169
|
"optionalDependencies": {
|
|
170
|
-
"@sweet-search/native-darwin-arm64": "2.
|
|
171
|
-
"@sweet-search/native-darwin-x64": "2.
|
|
172
|
-
"@sweet-search/native-linux-arm64-gnu": "2.
|
|
173
|
-
"@sweet-search/native-linux-arm64-gnu-cuda": "2.
|
|
174
|
-
"@sweet-search/native-linux-x64-gnu": "2.
|
|
175
|
-
"@sweet-search/native-linux-x64-gnu-cuda": "2.
|
|
176
|
-
"@sweet-search/bg-priority": "2.
|
|
170
|
+
"@sweet-search/native-darwin-arm64": "2.7.0",
|
|
171
|
+
"@sweet-search/native-darwin-x64": "2.7.0",
|
|
172
|
+
"@sweet-search/native-linux-arm64-gnu": "2.7.0",
|
|
173
|
+
"@sweet-search/native-linux-arm64-gnu-cuda": "2.7.0",
|
|
174
|
+
"@sweet-search/native-linux-x64-gnu": "2.7.0",
|
|
175
|
+
"@sweet-search/native-linux-x64-gnu-cuda": "2.7.0",
|
|
176
|
+
"@sweet-search/bg-priority": "2.7.0"
|
|
177
177
|
},
|
|
178
178
|
"engines": {
|
|
179
179
|
"node": ">=18.0.0"
|
package/scripts/init.js
CHANGED
|
@@ -994,7 +994,8 @@ export function registerPrewarmSessionStartHook({
|
|
|
994
994
|
};
|
|
995
995
|
}
|
|
996
996
|
|
|
997
|
-
const command = `node ${hookPath}`;
|
|
997
|
+
const command = `node ${hookPath} --agent-session-hook`;
|
|
998
|
+
const dropCommand = `node ${hookPath} --agent-session-drop`;
|
|
998
999
|
|
|
999
1000
|
const settingsDir = join(projectRoot, '.claude');
|
|
1000
1001
|
const settingsPath = join(settingsDir, 'settings.json');
|
|
@@ -1012,6 +1013,7 @@ export function registerPrewarmSessionStartHook({
|
|
|
1012
1013
|
const sessionStart = Array.isArray(settings.hooks.SessionStart) ? settings.hooks.SessionStart : [];
|
|
1013
1014
|
|
|
1014
1015
|
const entry = {
|
|
1016
|
+
matcher: 'startup|resume|clear|compact',
|
|
1015
1017
|
hooks: [
|
|
1016
1018
|
{
|
|
1017
1019
|
type: 'command',
|
|
@@ -1035,6 +1037,29 @@ export function registerPrewarmSessionStartHook({
|
|
|
1035
1037
|
}
|
|
1036
1038
|
settings.hooks.SessionStart = sessionStart;
|
|
1037
1039
|
|
|
1040
|
+
// Claude exposes a real SessionEnd event. Drop the bounded RAM receipt
|
|
1041
|
+
// bucket immediately instead of waiting for daemon/session LRU eviction.
|
|
1042
|
+
const sessionEnd = Array.isArray(settings.hooks.SessionEnd) ? settings.hooks.SessionEnd : [];
|
|
1043
|
+
const endEntry = {
|
|
1044
|
+
hooks: [
|
|
1045
|
+
{
|
|
1046
|
+
type: 'command',
|
|
1047
|
+
command: dropCommand,
|
|
1048
|
+
timeout: 2000,
|
|
1049
|
+
continueOnError: true,
|
|
1050
|
+
},
|
|
1051
|
+
],
|
|
1052
|
+
};
|
|
1053
|
+
const ownedEndIdx = sessionEnd.findIndex((group) =>
|
|
1054
|
+
Array.isArray(group?.hooks) &&
|
|
1055
|
+
group.hooks.some((h) => typeof h?.command === 'string'
|
|
1056
|
+
&& h.command.includes(PREWARM_HOOK_FILENAME)
|
|
1057
|
+
&& h.command.includes('--agent-session-drop'))
|
|
1058
|
+
);
|
|
1059
|
+
if (ownedEndIdx >= 0) sessionEnd[ownedEndIdx] = endEntry;
|
|
1060
|
+
else sessionEnd.push(endEntry);
|
|
1061
|
+
settings.hooks.SessionEnd = sessionEnd;
|
|
1062
|
+
|
|
1038
1063
|
try {
|
|
1039
1064
|
mkdirSync(settingsDir, { recursive: true });
|
|
1040
1065
|
const tmpPath = settingsPath + '.tmp';
|
|
@@ -1103,7 +1128,7 @@ export function registerCodexSessionStartHook({ projectRoot, packageRoot, skippe
|
|
|
1103
1128
|
// a repo), which fixes both path resolution AND the launcher's own
|
|
1104
1129
|
// `process.cwd()` project-root detection, while staying machine-portable
|
|
1105
1130
|
// (no absolute path is written into the often-committed hooks.json).
|
|
1106
|
-
const command = `cd "$(git rev-parse --show-toplevel 2>/dev/null || pwd)" && node ${JSON.stringify(hookPath)}`;
|
|
1131
|
+
const command = `cd "$(git rev-parse --show-toplevel 2>/dev/null || pwd)" && node ${JSON.stringify(hookPath)} --agent-session-hook`;
|
|
1107
1132
|
const codexDir = join(projectRoot, '.codex');
|
|
1108
1133
|
const hooksPath = join(codexDir, CODEX_HOOKS_FILENAME);
|
|
1109
1134
|
|
|
@@ -1119,12 +1144,12 @@ export function registerCodexSessionStartHook({ projectRoot, packageRoot, skippe
|
|
|
1119
1144
|
doc.hooks = doc.hooks || {};
|
|
1120
1145
|
const sessionStart = Array.isArray(doc.hooks.SessionStart) ? doc.hooks.SessionStart : [];
|
|
1121
1146
|
|
|
1122
|
-
//
|
|
1123
|
-
//
|
|
1147
|
+
// Start/resume prewarm the daemon; clear/compact also reset shown-span
|
|
1148
|
+
// receipts for the new context epoch.
|
|
1124
1149
|
// `timeout` is in seconds (per Codex's hooks docs); the launcher detaches and
|
|
1125
1150
|
// returns in well under a second, so a small budget is plenty.
|
|
1126
1151
|
const entry = {
|
|
1127
|
-
matcher: 'startup|resume',
|
|
1152
|
+
matcher: 'startup|resume|clear|compact',
|
|
1128
1153
|
hooks: [
|
|
1129
1154
|
{
|
|
1130
1155
|
type: 'command',
|
package/scripts/uninstall.js
CHANGED
|
@@ -406,7 +406,7 @@ function pruneEmptyAncestors(start, stopAt) {
|
|
|
406
406
|
}
|
|
407
407
|
|
|
408
408
|
/**
|
|
409
|
-
* Remove the sweet-search-owned SessionStart
|
|
409
|
+
* Remove the sweet-search-owned SessionStart and SessionEnd entries from `.claude/settings.json`,
|
|
410
410
|
* preserving every other hook, permission, and top-level key. Detection is
|
|
411
411
|
* filename-based (see PREWARM_HOOK_FILENAME) — only entries whose command
|
|
412
412
|
* references the sweet-search preheat script are removed.
|
|
@@ -437,31 +437,33 @@ export function removePrewarmSessionStartHook(projectRoot, { dryRun = false } =
|
|
|
437
437
|
return { status: 'error', detail: `settings.json is not valid JSON: ${err.message}` };
|
|
438
438
|
}
|
|
439
439
|
|
|
440
|
-
const
|
|
441
|
-
|
|
442
|
-
|
|
440
|
+
const changes = [];
|
|
441
|
+
for (const event of ['SessionStart', 'SessionEnd']) {
|
|
442
|
+
const groups = settings?.hooks?.[event];
|
|
443
|
+
if (!Array.isArray(groups)) continue;
|
|
444
|
+
const filtered = groups.filter((group) =>
|
|
445
|
+
!(Array.isArray(group?.hooks) &&
|
|
446
|
+
group.hooks.some((h) => typeof h?.command === 'string' && h.command.includes(PREWARM_HOOK_FILENAME)))
|
|
447
|
+
);
|
|
448
|
+
if (filtered.length !== groups.length) changes.push({ event, groups, filtered });
|
|
443
449
|
}
|
|
444
450
|
|
|
445
|
-
|
|
446
|
-
!(Array.isArray(group?.hooks) &&
|
|
447
|
-
group.hooks.some((h) => typeof h?.command === 'string' && h.command.includes(PREWARM_HOOK_FILENAME)))
|
|
448
|
-
);
|
|
449
|
-
|
|
450
|
-
if (filtered.length === sessionStart.length) {
|
|
451
|
+
if (changes.length === 0) {
|
|
451
452
|
return { status: 'not-found', detail: 'no matching entry' };
|
|
452
453
|
}
|
|
453
454
|
|
|
455
|
+
const removedCount = changes.reduce((sum, change) => sum + change.groups.length - change.filtered.length, 0);
|
|
456
|
+
|
|
454
457
|
if (dryRun) {
|
|
455
|
-
return { status: 'dry-run', detail: `would remove ${
|
|
458
|
+
return { status: 'dry-run', detail: `would remove ${removedCount} entries` };
|
|
456
459
|
}
|
|
457
460
|
|
|
458
|
-
|
|
459
|
-
delete settings.hooks
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
settings.hooks.SessionStart = filtered;
|
|
461
|
+
for (const { event, filtered } of changes) {
|
|
462
|
+
if (filtered.length === 0) delete settings.hooks[event];
|
|
463
|
+
else settings.hooks[event] = filtered;
|
|
464
|
+
}
|
|
465
|
+
if (settings.hooks && Object.keys(settings.hooks).length === 0) {
|
|
466
|
+
delete settings.hooks;
|
|
465
467
|
}
|
|
466
468
|
|
|
467
469
|
try {
|
|
@@ -472,7 +474,7 @@ export function removePrewarmSessionStartHook(projectRoot, { dryRun = false } =
|
|
|
472
474
|
return { status: 'error', detail: `write failed: ${err.message}` };
|
|
473
475
|
}
|
|
474
476
|
|
|
475
|
-
return { status: 'removed', detail: `spliced out ${
|
|
477
|
+
return { status: 'removed', detail: `spliced out ${removedCount} entries` };
|
|
476
478
|
}
|
|
477
479
|
|
|
478
480
|
/**
|