@zokizuan/satori-mcp 3.0.0 → 3.1.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 +18 -3
- package/dist/core/handlers.d.ts +44 -0
- package/dist/core/handlers.js +286 -8
- package/dist/core/search-types.d.ts +30 -0
- package/dist/telemetry/search.d.ts +4 -0
- package/dist/tools/file_outline.d.ts +3 -0
- package/dist/tools/file_outline.js +28 -0
- package/dist/tools/read_file.js +108 -17
- package/dist/tools/registry.js +2 -0
- package/dist/tools/search_codebase.js +10 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -7,13 +7,14 @@ MCP server for Satori — agent-safe semantic code search and indexing.
|
|
|
7
7
|
- Capability-driven execution via `CapabilityResolver`
|
|
8
8
|
- Runtime-first `search_codebase` with explicit `scope`, `resultMode`, `groupBy`, and optional `debug` traces
|
|
9
9
|
- First-class `call_graph` tool with deterministic node/edge sorting and TS/Python support
|
|
10
|
+
- Sidecar-backed `file_outline` tool for per-file symbol navigation and direct call_graph jump handles
|
|
10
11
|
- Snapshot v3 safety with index fingerprints and strict `requires_reindex` access gates
|
|
11
12
|
- Deterministic train-in-the-error responses for incompatible or legacy index states
|
|
12
13
|
- Query-time exclusion support with `.gitignore`-style matching
|
|
13
14
|
- Structured search telemetry logs (`[TELEMETRY]` JSON to `stderr`)
|
|
14
15
|
- Zod-first tool schemas converted to MCP JSON Schema for `ListTools`
|
|
15
16
|
- Auto-generated tool docs from live tool schemas
|
|
16
|
-
- `read_file` line-range retrieval with default large-file truncation guard
|
|
17
|
+
- `read_file` line-range retrieval with default large-file truncation guard and optional `mode="annotated"` metadata envelope
|
|
17
18
|
- Optional proactive sync watcher mode (debounced filesystem events)
|
|
18
19
|
- Index-time AST scope breadcrumbs (TS/JS/Python) rendered in search output as `🧬 Scope`
|
|
19
20
|
- Fingerprint schema `dense_v3`/`hybrid_v3` with hard gate for all pre-v3 indexes
|
|
@@ -24,19 +25,20 @@ MCP server for Satori — agent-safe semantic code search and indexing.
|
|
|
24
25
|
[MCP Client]
|
|
25
26
|
-> [index.ts bootstrap + ListTools/CallTool]
|
|
26
27
|
-> [tool registry]
|
|
27
|
-
-> [manage_index | search_codebase | call_graph | read_file | list_codebases]
|
|
28
|
+
-> [manage_index | search_codebase | call_graph | file_outline | read_file | list_codebases]
|
|
28
29
|
-> [ToolContext DI]
|
|
29
30
|
-> [CapabilityResolver]
|
|
30
31
|
-> [SnapshotManager v3 + access gate]
|
|
31
32
|
-> [Context / Vector store / Embedding / Reranker adapters]
|
|
32
33
|
```
|
|
33
34
|
|
|
34
|
-
Tool surface is hard-broken to
|
|
35
|
+
Tool surface is hard-broken to 6 tools. This keeps routing explicit while exposing call-chain traversal and file-level navigation as first-class operations.
|
|
35
36
|
|
|
36
37
|
## read_file Behavior
|
|
37
38
|
|
|
38
39
|
- Supports optional `start_line` and `end_line` (1-based, inclusive)
|
|
39
40
|
- When no range is provided and file length exceeds `READ_FILE_MAX_LINES` (default `1000`), output is truncated and includes a continuation hint with `path` and next `start_line`
|
|
41
|
+
- Optional `mode="annotated"` returns content plus `outlineStatus`, `outline`, `hasMore`, and reindex hints when sidecar data is unavailable
|
|
40
42
|
|
|
41
43
|
## Proactive Sync
|
|
42
44
|
|
|
@@ -93,6 +95,18 @@ Traverse the prebuilt TS/Python call graph sidecar for callers/callees/bidirecti
|
|
|
93
95
|
| `depth` | integer | no | `1` | Traversal depth (max 3). |
|
|
94
96
|
| `limit` | integer | no | `20` | Maximum number of returned edges. |
|
|
95
97
|
|
|
98
|
+
### `file_outline`
|
|
99
|
+
|
|
100
|
+
Return a sidecar-backed symbol outline for one file, including call_graph jump handles.
|
|
101
|
+
|
|
102
|
+
| Parameter | Type | Required | Default | Description |
|
|
103
|
+
|---|---|---|---|---|
|
|
104
|
+
| `path` | string | yes | | ABSOLUTE path to the indexed codebase root. |
|
|
105
|
+
| `file` | string | yes | | Relative file path inside the codebase root. |
|
|
106
|
+
| `start_line` | integer | no | | Optional start line filter (1-based, inclusive). |
|
|
107
|
+
| `end_line` | integer | no | | Optional end line filter (1-based, inclusive). |
|
|
108
|
+
| `limitSymbols` | integer | no | `500` | Maximum number of returned symbols after line filtering. |
|
|
109
|
+
|
|
96
110
|
### `read_file`
|
|
97
111
|
|
|
98
112
|
Read file content from the local filesystem, with optional 1-based inclusive line ranges and safe truncation.
|
|
@@ -102,6 +116,7 @@ Read file content from the local filesystem, with optional 1-based inclusive lin
|
|
|
102
116
|
| `path` | string | yes | | ABSOLUTE path to the file. |
|
|
103
117
|
| `start_line` | integer | no | | Optional start line (1-based, inclusive). |
|
|
104
118
|
| `end_line` | integer | no | | Optional end line (1-based, inclusive). |
|
|
119
|
+
| `mode` | enum("plain", "annotated") | no | `"plain"` | Output mode. plain returns text only; annotated returns content plus sidecar-backed outline metadata. |
|
|
105
120
|
|
|
106
121
|
### `list_codebases`
|
|
107
122
|
|
package/dist/core/handlers.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { Context } from "@zokizuan/satori-core";
|
|
|
2
2
|
import { SnapshotManager } from "./snapshot.js";
|
|
3
3
|
import { SyncManager } from "./sync.js";
|
|
4
4
|
import { IndexFingerprint } from "../config.js";
|
|
5
|
+
import { FileOutlineInput } from "./search-types.js";
|
|
5
6
|
import { CallGraphSidecarManager } from "./call-graph.js";
|
|
6
7
|
export declare class ToolHandlers {
|
|
7
8
|
private context;
|
|
@@ -26,6 +27,7 @@ export declare class ToolHandlers {
|
|
|
26
27
|
private getBreadcrumbMergeKey;
|
|
27
28
|
private formatScopeLine;
|
|
28
29
|
private normalizeSearchPath;
|
|
30
|
+
private hasPathSegment;
|
|
29
31
|
private isTestPath;
|
|
30
32
|
private isDocPath;
|
|
31
33
|
private isGeneratedPath;
|
|
@@ -39,6 +41,11 @@ export declare class ToolHandlers {
|
|
|
39
41
|
private buildFallbackGroupId;
|
|
40
42
|
private isCallGraphLanguageSupported;
|
|
41
43
|
private buildCallGraphHint;
|
|
44
|
+
private normalizeRelativeFilePath;
|
|
45
|
+
private buildRequiresReindexFileOutlinePayload;
|
|
46
|
+
private getOutlineStatusForLanguage;
|
|
47
|
+
private sortFileOutlineSymbols;
|
|
48
|
+
private buildSearchPassWarning;
|
|
42
49
|
private mapCallGraphStatus;
|
|
43
50
|
private getContextIgnorePatterns;
|
|
44
51
|
private rebuildCallGraphForIndex;
|
|
@@ -115,9 +122,33 @@ export declare class ToolHandlers {
|
|
|
115
122
|
excludedBySubdirectory: number;
|
|
116
123
|
filterPass: "initial" | "expanded";
|
|
117
124
|
freshnessMode: string | undefined;
|
|
125
|
+
searchPassCount: number;
|
|
126
|
+
searchPassSuccessCount: number;
|
|
127
|
+
searchPassFailureCount: number;
|
|
118
128
|
};
|
|
119
129
|
};
|
|
120
130
|
isError?: undefined;
|
|
131
|
+
} | {
|
|
132
|
+
content: {
|
|
133
|
+
type: string;
|
|
134
|
+
text: string;
|
|
135
|
+
}[];
|
|
136
|
+
isError: boolean;
|
|
137
|
+
meta: {
|
|
138
|
+
searchDiagnostics: {
|
|
139
|
+
queryLength: number;
|
|
140
|
+
limitRequested: number;
|
|
141
|
+
resultsBeforeFilter: number;
|
|
142
|
+
resultsAfterFilter: number;
|
|
143
|
+
excludedByIgnore: number;
|
|
144
|
+
excludedBySubdirectory: number;
|
|
145
|
+
filterPass: "initial" | "expanded";
|
|
146
|
+
freshnessMode: string | undefined;
|
|
147
|
+
searchPassCount: number;
|
|
148
|
+
searchPassSuccessCount: number;
|
|
149
|
+
searchPassFailureCount: number;
|
|
150
|
+
};
|
|
151
|
+
};
|
|
121
152
|
} | {
|
|
122
153
|
content: {
|
|
123
154
|
type: string;
|
|
@@ -126,6 +157,19 @@ export declare class ToolHandlers {
|
|
|
126
157
|
isError?: undefined;
|
|
127
158
|
meta?: undefined;
|
|
128
159
|
}>;
|
|
160
|
+
handleFileOutline(args: FileOutlineInput): Promise<{
|
|
161
|
+
content: {
|
|
162
|
+
type: string;
|
|
163
|
+
text: string;
|
|
164
|
+
}[];
|
|
165
|
+
isError: boolean;
|
|
166
|
+
} | {
|
|
167
|
+
content: {
|
|
168
|
+
type: string;
|
|
169
|
+
text: string;
|
|
170
|
+
}[];
|
|
171
|
+
isError?: undefined;
|
|
172
|
+
}>;
|
|
129
173
|
handleCallGraph(args: any): Promise<{
|
|
130
174
|
content: {
|
|
131
175
|
type: string;
|
package/dist/core/handlers.js
CHANGED
|
@@ -328,15 +328,24 @@ export class ToolHandlers {
|
|
|
328
328
|
normalizeSearchPath(relativePath) {
|
|
329
329
|
return relativePath.replace(/\\/g, '/').replace(/^\/+/, '').toLowerCase();
|
|
330
330
|
}
|
|
331
|
+
hasPathSegment(normalizedPath, segment) {
|
|
332
|
+
return normalizedPath === segment
|
|
333
|
+
|| normalizedPath.startsWith(`${segment}/`)
|
|
334
|
+
|| normalizedPath.includes(`/${segment}/`);
|
|
335
|
+
}
|
|
331
336
|
isTestPath(normalizedPath) {
|
|
332
|
-
return
|
|
333
|
-
||
|
|
334
|
-
||
|
|
337
|
+
return this.hasPathSegment(normalizedPath, 'test')
|
|
338
|
+
|| this.hasPathSegment(normalizedPath, 'tests')
|
|
339
|
+
|| this.hasPathSegment(normalizedPath, '__tests__')
|
|
335
340
|
|| /\.test\.[^/]+$/.test(normalizedPath)
|
|
336
341
|
|| /\.spec\.[^/]+$/.test(normalizedPath);
|
|
337
342
|
}
|
|
338
343
|
isDocPath(normalizedPath) {
|
|
339
|
-
return
|
|
344
|
+
return this.hasPathSegment(normalizedPath, 'docs')
|
|
345
|
+
|| this.hasPathSegment(normalizedPath, 'doc')
|
|
346
|
+
|| this.hasPathSegment(normalizedPath, 'documentation')
|
|
347
|
+
|| this.hasPathSegment(normalizedPath, 'guide')
|
|
348
|
+
|| this.hasPathSegment(normalizedPath, 'guides')
|
|
340
349
|
|| normalizedPath.endsWith('.md')
|
|
341
350
|
|| normalizedPath.endsWith('.mdx')
|
|
342
351
|
|| normalizedPath.endsWith('.rst')
|
|
@@ -449,6 +458,46 @@ export class ToolHandlers {
|
|
|
449
458
|
}
|
|
450
459
|
};
|
|
451
460
|
}
|
|
461
|
+
normalizeRelativeFilePath(relativeFilePath) {
|
|
462
|
+
return relativeFilePath.replace(/\\/g, '/').replace(/^\.\/+/, '').trim();
|
|
463
|
+
}
|
|
464
|
+
buildRequiresReindexFileOutlinePayload(codebasePath, input, detail) {
|
|
465
|
+
const detailLine = detail ? `${detail}\n\n` : '';
|
|
466
|
+
return {
|
|
467
|
+
status: 'requires_reindex',
|
|
468
|
+
path: codebasePath,
|
|
469
|
+
file: input.file,
|
|
470
|
+
outline: null,
|
|
471
|
+
hasMore: false,
|
|
472
|
+
message: `${detailLine}Call graph sidecar is missing or incompatible. Please run manage_index with {"action":"reindex","path":"${codebasePath}"}.`,
|
|
473
|
+
hints: {
|
|
474
|
+
reindex: this.buildReindexHint(codebasePath)
|
|
475
|
+
}
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
getOutlineStatusForLanguage(relativeFilePath) {
|
|
479
|
+
if (this.isCallGraphLanguageSupported('unknown', relativeFilePath)) {
|
|
480
|
+
return 'ok';
|
|
481
|
+
}
|
|
482
|
+
return 'unsupported';
|
|
483
|
+
}
|
|
484
|
+
sortFileOutlineSymbols(symbols) {
|
|
485
|
+
return [...symbols].sort((a, b) => {
|
|
486
|
+
const startCmp = this.compareNullableNumbersAsc(a.span?.startLine, b.span?.startLine);
|
|
487
|
+
if (startCmp !== 0)
|
|
488
|
+
return startCmp;
|
|
489
|
+
const endCmp = this.compareNullableNumbersAsc(a.span?.endLine, b.span?.endLine);
|
|
490
|
+
if (endCmp !== 0)
|
|
491
|
+
return endCmp;
|
|
492
|
+
const labelCmp = this.compareNullableStringsAsc(a.symbolLabel, b.symbolLabel);
|
|
493
|
+
if (labelCmp !== 0)
|
|
494
|
+
return labelCmp;
|
|
495
|
+
return this.compareNullableStringsAsc(a.symbolId, b.symbolId);
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
buildSearchPassWarning(passId) {
|
|
499
|
+
return `SEARCH_PASS_FAILED:${passId} - ${passId} semantic search pass failed; results may be degraded.`;
|
|
500
|
+
}
|
|
452
501
|
mapCallGraphStatus(graph) {
|
|
453
502
|
if (graph.supported) {
|
|
454
503
|
return 'ok';
|
|
@@ -1228,6 +1277,9 @@ To force rebuild from scratch: call manage_index with {"action":"create","path":
|
|
|
1228
1277
|
excludedBySubdirectory: 0,
|
|
1229
1278
|
filterPass: 'expanded',
|
|
1230
1279
|
freshnessMode: undefined,
|
|
1280
|
+
searchPassCount: 0,
|
|
1281
|
+
searchPassSuccessCount: 0,
|
|
1282
|
+
searchPassFailureCount: 0,
|
|
1231
1283
|
};
|
|
1232
1284
|
try {
|
|
1233
1285
|
await this.syncIndexedCodebasesFromCloud();
|
|
@@ -1329,8 +1381,40 @@ To force rebuild from scratch: call manage_index with {"action":"create","path":
|
|
|
1329
1381
|
console.log(`[SEARCH] 🧠 Using embedding provider: ${encoderEngine.getProvider()} for search`);
|
|
1330
1382
|
const candidateLimit = Math.max(1, Math.min(SEARCH_MAX_CANDIDATES, Math.max(input.limit * 8, 32)));
|
|
1331
1383
|
const expandedQuery = `${input.query}\nimplementation runtime source entrypoint`;
|
|
1332
|
-
const
|
|
1333
|
-
|
|
1384
|
+
const passDescriptors = [
|
|
1385
|
+
{ id: 'primary', query: input.query },
|
|
1386
|
+
{ id: 'expanded', query: expandedQuery },
|
|
1387
|
+
];
|
|
1388
|
+
searchDiagnostics.searchPassCount = passDescriptors.length;
|
|
1389
|
+
const passSettled = await Promise.allSettled(passDescriptors.map((pass) => {
|
|
1390
|
+
return this.context.semanticSearch(effectiveRoot, pass.query, candidateLimit, 0.3);
|
|
1391
|
+
}));
|
|
1392
|
+
const searchWarnings = [];
|
|
1393
|
+
const successfulPasses = [];
|
|
1394
|
+
for (let idx = 0; idx < passSettled.length; idx++) {
|
|
1395
|
+
const passResult = passSettled[idx];
|
|
1396
|
+
const passDescriptor = passDescriptors[idx];
|
|
1397
|
+
if (passResult.status === 'fulfilled' && Array.isArray(passResult.value)) {
|
|
1398
|
+
successfulPasses.push({
|
|
1399
|
+
id: passDescriptor.id,
|
|
1400
|
+
results: passResult.value
|
|
1401
|
+
});
|
|
1402
|
+
continue;
|
|
1403
|
+
}
|
|
1404
|
+
searchWarnings.push(this.buildSearchPassWarning(passDescriptor.id));
|
|
1405
|
+
}
|
|
1406
|
+
searchDiagnostics.searchPassSuccessCount = successfulPasses.length;
|
|
1407
|
+
searchDiagnostics.searchPassFailureCount = searchDiagnostics.searchPassCount - successfulPasses.length;
|
|
1408
|
+
if (successfulPasses.length === 0) {
|
|
1409
|
+
return {
|
|
1410
|
+
content: [{
|
|
1411
|
+
type: "text",
|
|
1412
|
+
text: "Error searching code: all semantic search passes failed. Please retry and verify embedding/vector backends are reachable."
|
|
1413
|
+
}],
|
|
1414
|
+
isError: true,
|
|
1415
|
+
meta: { searchDiagnostics }
|
|
1416
|
+
};
|
|
1417
|
+
}
|
|
1334
1418
|
const byChunkKey = new Map();
|
|
1335
1419
|
const addPass = (results, passWeight = 1) => {
|
|
1336
1420
|
for (let i = 0; i < results.length; i++) {
|
|
@@ -1359,8 +1443,9 @@ To force rebuild from scratch: call manage_index with {"action":"create","path":
|
|
|
1359
1443
|
}
|
|
1360
1444
|
}
|
|
1361
1445
|
};
|
|
1362
|
-
|
|
1363
|
-
|
|
1446
|
+
for (const pass of successfulPasses) {
|
|
1447
|
+
addPass(pass.results, 1);
|
|
1448
|
+
}
|
|
1364
1449
|
const beforeFilter = byChunkKey.size;
|
|
1365
1450
|
const scored = [];
|
|
1366
1451
|
for (const candidate of byChunkKey.values()) {
|
|
@@ -1424,6 +1509,7 @@ To force rebuild from scratch: call manage_index with {"action":"create","path":
|
|
|
1424
1509
|
limit: input.limit,
|
|
1425
1510
|
resultMode: "raw",
|
|
1426
1511
|
freshnessDecision,
|
|
1512
|
+
...(searchWarnings.length > 0 ? { warnings: searchWarnings } : {}),
|
|
1427
1513
|
results: rawResults
|
|
1428
1514
|
};
|
|
1429
1515
|
return {
|
|
@@ -1521,6 +1607,7 @@ To force rebuild from scratch: call manage_index with {"action":"create","path":
|
|
|
1521
1607
|
limit: input.limit,
|
|
1522
1608
|
resultMode: "grouped",
|
|
1523
1609
|
freshnessDecision,
|
|
1610
|
+
...(searchWarnings.length > 0 ? { warnings: searchWarnings } : {}),
|
|
1524
1611
|
results: groupedResults.slice(0, input.limit)
|
|
1525
1612
|
};
|
|
1526
1613
|
return {
|
|
@@ -1544,6 +1631,197 @@ To force rebuild from scratch: call manage_index with {"action":"create","path":
|
|
|
1544
1631
|
};
|
|
1545
1632
|
}
|
|
1546
1633
|
}
|
|
1634
|
+
async handleFileOutline(args) {
|
|
1635
|
+
const limitSymbols = Number.isFinite(args?.limitSymbols)
|
|
1636
|
+
? Math.max(1, Number(args.limitSymbols))
|
|
1637
|
+
: 500;
|
|
1638
|
+
const requestedStartLine = Number.isFinite(args?.start_line) ? Math.max(1, Number(args.start_line)) : undefined;
|
|
1639
|
+
const requestedEndLine = Number.isFinite(args?.end_line) ? Math.max(1, Number(args.end_line)) : undefined;
|
|
1640
|
+
try {
|
|
1641
|
+
await this.syncIndexedCodebasesFromCloud();
|
|
1642
|
+
const absoluteRoot = ensureAbsolutePath(args.path);
|
|
1643
|
+
const normalizedFile = this.normalizeRelativeFilePath(args.file);
|
|
1644
|
+
const absoluteFile = path.resolve(absoluteRoot, normalizedFile);
|
|
1645
|
+
const relativeToRoot = path.relative(absoluteRoot, absoluteFile);
|
|
1646
|
+
if (!fs.existsSync(absoluteRoot)) {
|
|
1647
|
+
return {
|
|
1648
|
+
content: [{
|
|
1649
|
+
type: "text",
|
|
1650
|
+
text: `Error: Path '${absoluteRoot}' does not exist.`
|
|
1651
|
+
}],
|
|
1652
|
+
isError: true
|
|
1653
|
+
};
|
|
1654
|
+
}
|
|
1655
|
+
const rootStat = fs.statSync(absoluteRoot);
|
|
1656
|
+
if (!rootStat.isDirectory()) {
|
|
1657
|
+
return {
|
|
1658
|
+
content: [{
|
|
1659
|
+
type: "text",
|
|
1660
|
+
text: `Error: Path '${absoluteRoot}' is not a directory`
|
|
1661
|
+
}],
|
|
1662
|
+
isError: true
|
|
1663
|
+
};
|
|
1664
|
+
}
|
|
1665
|
+
if (relativeToRoot.startsWith('..') || path.isAbsolute(relativeToRoot)) {
|
|
1666
|
+
return {
|
|
1667
|
+
content: [{
|
|
1668
|
+
type: "text",
|
|
1669
|
+
text: `Error: File '${normalizedFile}' must be inside codebase root '${absoluteRoot}'.`
|
|
1670
|
+
}],
|
|
1671
|
+
isError: true
|
|
1672
|
+
};
|
|
1673
|
+
}
|
|
1674
|
+
trackCodebasePath(absoluteRoot);
|
|
1675
|
+
const blockedRoot = this.getMatchingBlockedRoot(absoluteRoot);
|
|
1676
|
+
if (blockedRoot) {
|
|
1677
|
+
const payload = this.buildRequiresReindexFileOutlinePayload(blockedRoot.path, {
|
|
1678
|
+
...args,
|
|
1679
|
+
file: normalizedFile
|
|
1680
|
+
}, blockedRoot.message);
|
|
1681
|
+
return {
|
|
1682
|
+
content: [{ type: "text", text: JSON.stringify(payload, null, 2) }]
|
|
1683
|
+
};
|
|
1684
|
+
}
|
|
1685
|
+
const gateResult = this.enforceFingerprintGate(absoluteRoot);
|
|
1686
|
+
if (gateResult.blockedResponse) {
|
|
1687
|
+
const payload = this.buildRequiresReindexFileOutlinePayload(absoluteRoot, {
|
|
1688
|
+
...args,
|
|
1689
|
+
file: normalizedFile
|
|
1690
|
+
}, gateResult.message);
|
|
1691
|
+
return {
|
|
1692
|
+
content: [{ type: "text", text: JSON.stringify(payload, null, 2) }]
|
|
1693
|
+
};
|
|
1694
|
+
}
|
|
1695
|
+
const sidecarInfo = this.snapshotManager.getCodebaseCallGraphSidecar(absoluteRoot);
|
|
1696
|
+
if (!sidecarInfo || sidecarInfo.version !== 'v3') {
|
|
1697
|
+
const payload = this.buildRequiresReindexFileOutlinePayload(absoluteRoot, {
|
|
1698
|
+
...args,
|
|
1699
|
+
file: normalizedFile
|
|
1700
|
+
});
|
|
1701
|
+
return {
|
|
1702
|
+
content: [{ type: "text", text: JSON.stringify(payload, null, 2) }]
|
|
1703
|
+
};
|
|
1704
|
+
}
|
|
1705
|
+
if (!fs.existsSync(absoluteFile)) {
|
|
1706
|
+
const payload = {
|
|
1707
|
+
status: 'not_found',
|
|
1708
|
+
path: absoluteRoot,
|
|
1709
|
+
file: normalizedFile,
|
|
1710
|
+
outline: null,
|
|
1711
|
+
hasMore: false,
|
|
1712
|
+
message: `File '${normalizedFile}' does not exist under codebase root '${absoluteRoot}'.`
|
|
1713
|
+
};
|
|
1714
|
+
return {
|
|
1715
|
+
content: [{ type: "text", text: JSON.stringify(payload, null, 2) }]
|
|
1716
|
+
};
|
|
1717
|
+
}
|
|
1718
|
+
const fileStat = fs.statSync(absoluteFile);
|
|
1719
|
+
if (!fileStat.isFile()) {
|
|
1720
|
+
const payload = {
|
|
1721
|
+
status: 'not_found',
|
|
1722
|
+
path: absoluteRoot,
|
|
1723
|
+
file: normalizedFile,
|
|
1724
|
+
outline: null,
|
|
1725
|
+
hasMore: false,
|
|
1726
|
+
message: `'${normalizedFile}' is not a file.`
|
|
1727
|
+
};
|
|
1728
|
+
return {
|
|
1729
|
+
content: [{ type: "text", text: JSON.stringify(payload, null, 2) }]
|
|
1730
|
+
};
|
|
1731
|
+
}
|
|
1732
|
+
const languageStatus = this.getOutlineStatusForLanguage(normalizedFile);
|
|
1733
|
+
if (languageStatus !== 'ok') {
|
|
1734
|
+
const payload = {
|
|
1735
|
+
status: 'unsupported',
|
|
1736
|
+
path: absoluteRoot,
|
|
1737
|
+
file: normalizedFile,
|
|
1738
|
+
outline: null,
|
|
1739
|
+
hasMore: false,
|
|
1740
|
+
message: `File '${normalizedFile}' is not supported for sidecar outline. Supported extensions: .ts, .tsx, .py.`
|
|
1741
|
+
};
|
|
1742
|
+
return {
|
|
1743
|
+
content: [{ type: "text", text: JSON.stringify(payload, null, 2) }]
|
|
1744
|
+
};
|
|
1745
|
+
}
|
|
1746
|
+
const sidecar = this.callGraphManager.loadSidecar(absoluteRoot);
|
|
1747
|
+
if (!sidecar) {
|
|
1748
|
+
const payload = this.buildRequiresReindexFileOutlinePayload(absoluteRoot, {
|
|
1749
|
+
...args,
|
|
1750
|
+
file: normalizedFile
|
|
1751
|
+
});
|
|
1752
|
+
return {
|
|
1753
|
+
content: [{ type: "text", text: JSON.stringify(payload, null, 2) }]
|
|
1754
|
+
};
|
|
1755
|
+
}
|
|
1756
|
+
const windowStart = requestedStartLine;
|
|
1757
|
+
const windowEnd = requestedEndLine && requestedStartLine
|
|
1758
|
+
? Math.max(requestedEndLine, requestedStartLine)
|
|
1759
|
+
: requestedEndLine;
|
|
1760
|
+
const byFile = sidecar.nodes.filter((node) => this.normalizeRelativeFilePath(node.file) === normalizedFile);
|
|
1761
|
+
const windowed = byFile.filter((node) => {
|
|
1762
|
+
if (!windowStart && !windowEnd) {
|
|
1763
|
+
return true;
|
|
1764
|
+
}
|
|
1765
|
+
const startsBeforeWindowEnd = windowEnd === undefined || node.span.startLine <= windowEnd;
|
|
1766
|
+
const endsAfterWindowStart = windowStart === undefined || node.span.endLine >= windowStart;
|
|
1767
|
+
return startsBeforeWindowEnd && endsAfterWindowStart;
|
|
1768
|
+
});
|
|
1769
|
+
const symbols = this.sortFileOutlineSymbols(windowed.map((node) => {
|
|
1770
|
+
const symbolLabel = (typeof node.symbolLabel === 'string' && node.symbolLabel.trim().length > 0)
|
|
1771
|
+
? node.symbolLabel
|
|
1772
|
+
: node.symbolId;
|
|
1773
|
+
return {
|
|
1774
|
+
symbolId: node.symbolId,
|
|
1775
|
+
symbolLabel,
|
|
1776
|
+
span: {
|
|
1777
|
+
startLine: node.span.startLine,
|
|
1778
|
+
endLine: node.span.endLine
|
|
1779
|
+
},
|
|
1780
|
+
callGraphHint: {
|
|
1781
|
+
supported: true,
|
|
1782
|
+
symbolRef: {
|
|
1783
|
+
file: normalizedFile,
|
|
1784
|
+
symbolId: node.symbolId,
|
|
1785
|
+
symbolLabel,
|
|
1786
|
+
span: {
|
|
1787
|
+
startLine: node.span.startLine,
|
|
1788
|
+
endLine: node.span.endLine
|
|
1789
|
+
}
|
|
1790
|
+
}
|
|
1791
|
+
}
|
|
1792
|
+
};
|
|
1793
|
+
}));
|
|
1794
|
+
const hasMore = symbols.length > limitSymbols;
|
|
1795
|
+
const missingSymbolMetadataCount = sidecar.notes.filter((note) => {
|
|
1796
|
+
return note.type === 'missing_symbol_metadata' && this.normalizeRelativeFilePath(note.file) === normalizedFile;
|
|
1797
|
+
}).length;
|
|
1798
|
+
const warnings = missingSymbolMetadataCount > 0
|
|
1799
|
+
? [`OUTLINE_MISSING_SYMBOL_METADATA:${missingSymbolMetadataCount}`]
|
|
1800
|
+
: undefined;
|
|
1801
|
+
const payload = {
|
|
1802
|
+
status: 'ok',
|
|
1803
|
+
path: absoluteRoot,
|
|
1804
|
+
file: normalizedFile,
|
|
1805
|
+
outline: {
|
|
1806
|
+
symbols: symbols.slice(0, limitSymbols)
|
|
1807
|
+
},
|
|
1808
|
+
hasMore,
|
|
1809
|
+
...(warnings ? { warnings } : {})
|
|
1810
|
+
};
|
|
1811
|
+
return {
|
|
1812
|
+
content: [{ type: "text", text: JSON.stringify(payload, null, 2) }]
|
|
1813
|
+
};
|
|
1814
|
+
}
|
|
1815
|
+
catch (error) {
|
|
1816
|
+
return {
|
|
1817
|
+
content: [{
|
|
1818
|
+
type: "text",
|
|
1819
|
+
text: `Error building file outline: ${error?.message || error}`
|
|
1820
|
+
}],
|
|
1821
|
+
isError: true
|
|
1822
|
+
};
|
|
1823
|
+
}
|
|
1824
|
+
}
|
|
1547
1825
|
async handleCallGraph(args) {
|
|
1548
1826
|
const rawDirection = args?.direction;
|
|
1549
1827
|
const direction = rawDirection === 'callers' || rawDirection === 'callees' || rawDirection === 'both'
|
|
@@ -67,6 +67,7 @@ interface SearchBaseResponseEnvelope {
|
|
|
67
67
|
freshnessDecision: FreshnessDecision | {
|
|
68
68
|
mode: "skipped_requires_reindex";
|
|
69
69
|
} | null;
|
|
70
|
+
warnings?: string[];
|
|
70
71
|
message?: string;
|
|
71
72
|
hints?: Record<string, unknown>;
|
|
72
73
|
}
|
|
@@ -88,5 +89,34 @@ export interface SearchRequestInput {
|
|
|
88
89
|
limit: number;
|
|
89
90
|
debug?: boolean;
|
|
90
91
|
}
|
|
92
|
+
export interface FileOutlineInput {
|
|
93
|
+
path: string;
|
|
94
|
+
file: string;
|
|
95
|
+
start_line?: number;
|
|
96
|
+
end_line?: number;
|
|
97
|
+
limitSymbols?: number;
|
|
98
|
+
}
|
|
99
|
+
export type FileOutlineStatus = "ok" | "not_found" | "requires_reindex" | "unsupported";
|
|
100
|
+
export interface FileOutlineSymbolResult {
|
|
101
|
+
symbolId: string;
|
|
102
|
+
symbolLabel: string;
|
|
103
|
+
span: SearchSpan;
|
|
104
|
+
callGraphHint: {
|
|
105
|
+
supported: true;
|
|
106
|
+
symbolRef: CallGraphSymbolRef;
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
export interface FileOutlineResponseEnvelope {
|
|
110
|
+
status: FileOutlineStatus;
|
|
111
|
+
path: string;
|
|
112
|
+
file: string;
|
|
113
|
+
outline: {
|
|
114
|
+
symbols: FileOutlineSymbolResult[];
|
|
115
|
+
} | null;
|
|
116
|
+
hasMore: boolean;
|
|
117
|
+
warnings?: string[];
|
|
118
|
+
message?: string;
|
|
119
|
+
hints?: Record<string, unknown>;
|
|
120
|
+
}
|
|
91
121
|
export {};
|
|
92
122
|
//# sourceMappingURL=search-types.d.ts.map
|
|
@@ -11,6 +11,10 @@ export interface SearchTelemetryEvent {
|
|
|
11
11
|
reranker_used: boolean;
|
|
12
12
|
latency_ms: number;
|
|
13
13
|
freshness_mode?: string;
|
|
14
|
+
search_pass_count?: number;
|
|
15
|
+
search_pass_success_count?: number;
|
|
16
|
+
search_pass_failure_count?: number;
|
|
17
|
+
parallel_fanout?: boolean;
|
|
14
18
|
error?: string;
|
|
15
19
|
}
|
|
16
20
|
export declare function emitSearchTelemetry(event: SearchTelemetryEvent): void;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { formatZodError } from './types.js';
|
|
3
|
+
const fileOutlineInputSchema = z.object({
|
|
4
|
+
path: z.string().min(1).describe('ABSOLUTE path to the indexed codebase root.'),
|
|
5
|
+
file: z.string().min(1).describe('Relative file path inside the codebase root.'),
|
|
6
|
+
start_line: z.number().int().positive().optional().describe('Optional start line filter (1-based, inclusive).'),
|
|
7
|
+
end_line: z.number().int().positive().optional().describe('Optional end line filter (1-based, inclusive).'),
|
|
8
|
+
limitSymbols: z.number().int().positive().default(500).optional().describe('Maximum number of returned symbols after line filtering.'),
|
|
9
|
+
});
|
|
10
|
+
export const fileOutlineTool = {
|
|
11
|
+
name: 'file_outline',
|
|
12
|
+
description: () => 'Return a sidecar-backed symbol outline for one file, including call_graph jump handles.',
|
|
13
|
+
inputSchemaZod: () => fileOutlineInputSchema,
|
|
14
|
+
execute: async (args, ctx) => {
|
|
15
|
+
const parsed = fileOutlineInputSchema.safeParse(args || {});
|
|
16
|
+
if (!parsed.success) {
|
|
17
|
+
return {
|
|
18
|
+
content: [{
|
|
19
|
+
type: 'text',
|
|
20
|
+
text: formatZodError('file_outline', parsed.error)
|
|
21
|
+
}],
|
|
22
|
+
isError: true
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
return ctx.toolHandlers.handleFileOutline(parsed.data);
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
//# sourceMappingURL=file_outline.js.map
|
package/dist/tools/read_file.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
2
3
|
import { z } from "zod";
|
|
3
4
|
import { formatZodError } from "./types.js";
|
|
4
5
|
import { ensureAbsolutePath } from "../utils.js";
|
|
5
6
|
const readFileInputSchema = z.object({
|
|
6
7
|
path: z.string().min(1).describe("ABSOLUTE path to the file."),
|
|
7
8
|
start_line: z.number().int().positive().optional().describe("Optional start line (1-based, inclusive)."),
|
|
8
|
-
end_line: z.number().int().positive().optional().describe("Optional end line (1-based, inclusive).")
|
|
9
|
+
end_line: z.number().int().positive().optional().describe("Optional end line (1-based, inclusive)."),
|
|
10
|
+
mode: z.enum(["plain", "annotated"]).default("plain").optional().describe("Output mode. plain returns text only; annotated returns content plus sidecar-backed outline metadata.")
|
|
9
11
|
});
|
|
10
12
|
function splitIntoLines(content) {
|
|
11
13
|
if (content.length === 0) {
|
|
@@ -20,6 +22,32 @@ function splitIntoLines(content) {
|
|
|
20
22
|
function clamp(value, min, max) {
|
|
21
23
|
return Math.min(max, Math.max(min, value));
|
|
22
24
|
}
|
|
25
|
+
function normalizeRelativePath(value) {
|
|
26
|
+
return value.replace(/\\/g, "/");
|
|
27
|
+
}
|
|
28
|
+
function isOutlineSupportedFile(absolutePath) {
|
|
29
|
+
const ext = path.extname(absolutePath).toLowerCase();
|
|
30
|
+
return ext === ".ts" || ext === ".tsx" || ext === ".py";
|
|
31
|
+
}
|
|
32
|
+
function resolveCodebaseRootForFile(absolutePath, ctx) {
|
|
33
|
+
const allCodebases = typeof ctx.snapshotManager?.getAllCodebases === "function"
|
|
34
|
+
? ctx.snapshotManager.getAllCodebases()
|
|
35
|
+
: [];
|
|
36
|
+
if (!Array.isArray(allCodebases)) {
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
const matches = allCodebases
|
|
40
|
+
.map((item) => (item && typeof item.path === "string") ? ensureAbsolutePath(item.path) : undefined)
|
|
41
|
+
.filter((candidate) => Boolean(candidate))
|
|
42
|
+
.filter((candidate) => {
|
|
43
|
+
return absolutePath === candidate || absolutePath.startsWith(`${candidate}${path.sep}`);
|
|
44
|
+
});
|
|
45
|
+
if (matches.length === 0) {
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
matches.sort((a, b) => b.length - a.length);
|
|
49
|
+
return matches[0];
|
|
50
|
+
}
|
|
23
51
|
export const readFileTool = {
|
|
24
52
|
name: "read_file",
|
|
25
53
|
description: () => "Read file content from the local filesystem, with optional 1-based inclusive line ranges and safe truncation.",
|
|
@@ -36,6 +64,7 @@ export const readFileTool = {
|
|
|
36
64
|
};
|
|
37
65
|
}
|
|
38
66
|
const input = parsed.data;
|
|
67
|
+
const mode = input.mode || "plain";
|
|
39
68
|
try {
|
|
40
69
|
const absolutePath = ensureAbsolutePath(input.path);
|
|
41
70
|
if (!fs.existsSync(absolutePath)) {
|
|
@@ -54,21 +83,17 @@ export const readFileTool = {
|
|
|
54
83
|
const content = fs.readFileSync(absolutePath, "utf-8");
|
|
55
84
|
const lines = splitIntoLines(content);
|
|
56
85
|
const totalLines = lines.length;
|
|
57
|
-
if (totalLines === 0) {
|
|
58
|
-
return {
|
|
59
|
-
content: [{
|
|
60
|
-
type: "text",
|
|
61
|
-
text: content
|
|
62
|
-
}]
|
|
63
|
-
};
|
|
64
|
-
}
|
|
65
86
|
const maxLines = Math.max(1, ctx.readFileMaxLines);
|
|
66
87
|
const hasStart = input.start_line !== undefined;
|
|
67
88
|
const hasEnd = input.end_line !== undefined;
|
|
68
89
|
let startLine = 1;
|
|
69
|
-
let endLine = totalLines;
|
|
90
|
+
let endLine = totalLines > 0 ? totalLines : 0;
|
|
70
91
|
let addContinuationHint = false;
|
|
71
|
-
if (
|
|
92
|
+
if (totalLines === 0) {
|
|
93
|
+
startLine = 1;
|
|
94
|
+
endLine = 0;
|
|
95
|
+
}
|
|
96
|
+
else if (!hasStart && !hasEnd) {
|
|
72
97
|
if (totalLines > maxLines) {
|
|
73
98
|
endLine = maxLines;
|
|
74
99
|
addContinuationHint = true;
|
|
@@ -86,21 +111,87 @@ export const readFileTool = {
|
|
|
86
111
|
startLine = clamp(input.start_line, 1, totalLines);
|
|
87
112
|
endLine = clamp(input.end_line, startLine, totalLines);
|
|
88
113
|
}
|
|
89
|
-
const selected = lines.slice(startLine - 1, endLine).join("\n");
|
|
90
|
-
|
|
114
|
+
const selected = totalLines === 0 ? content : lines.slice(startLine - 1, endLine).join("\n");
|
|
115
|
+
const nextStartLine = addContinuationHint ? endLine + 1 : undefined;
|
|
116
|
+
const hint = addContinuationHint
|
|
117
|
+
? `\n\n(File truncated at line ${endLine}. To read more, call read_file with path="${absolutePath}" and start_line=${nextStartLine}.)`
|
|
118
|
+
: "";
|
|
119
|
+
const contentWithHint = `${selected}${hint}`;
|
|
120
|
+
if (mode === "plain") {
|
|
91
121
|
return {
|
|
92
122
|
content: [{
|
|
93
123
|
type: "text",
|
|
94
|
-
text:
|
|
124
|
+
text: contentWithHint
|
|
95
125
|
}]
|
|
96
126
|
};
|
|
97
127
|
}
|
|
98
|
-
const
|
|
99
|
-
const
|
|
128
|
+
const supportedByExtension = isOutlineSupportedFile(absolutePath);
|
|
129
|
+
const resolvedRoot = resolveCodebaseRootForFile(absolutePath, ctx);
|
|
130
|
+
const relativeFile = resolvedRoot
|
|
131
|
+
? normalizeRelativePath(path.relative(resolvedRoot, absolutePath))
|
|
132
|
+
: undefined;
|
|
133
|
+
let outlineStatus = supportedByExtension ? "requires_reindex" : "unsupported";
|
|
134
|
+
let outline = null;
|
|
135
|
+
let hasMore = false;
|
|
136
|
+
let warnings;
|
|
137
|
+
let hints;
|
|
138
|
+
if (!supportedByExtension) {
|
|
139
|
+
outlineStatus = "unsupported";
|
|
140
|
+
}
|
|
141
|
+
else if (!resolvedRoot || !relativeFile) {
|
|
142
|
+
outlineStatus = "requires_reindex";
|
|
143
|
+
hints = {
|
|
144
|
+
reindex: {
|
|
145
|
+
tool: "manage_index",
|
|
146
|
+
args: {
|
|
147
|
+
action: "reindex",
|
|
148
|
+
path: path.dirname(absolutePath),
|
|
149
|
+
},
|
|
150
|
+
},
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
try {
|
|
155
|
+
const outlineResponse = await ctx.toolHandlers.handleFileOutline({
|
|
156
|
+
path: resolvedRoot,
|
|
157
|
+
file: relativeFile,
|
|
158
|
+
start_line: totalLines === 0 ? undefined : startLine,
|
|
159
|
+
end_line: totalLines === 0 ? undefined : endLine,
|
|
160
|
+
});
|
|
161
|
+
const parsedOutline = JSON.parse(outlineResponse.content?.[0]?.text || "{}");
|
|
162
|
+
const status = parsedOutline?.status;
|
|
163
|
+
if (status === "ok" || status === "requires_reindex" || status === "unsupported") {
|
|
164
|
+
outlineStatus = status;
|
|
165
|
+
}
|
|
166
|
+
else {
|
|
167
|
+
outlineStatus = "requires_reindex";
|
|
168
|
+
}
|
|
169
|
+
outline = outlineStatus === "ok" && parsedOutline?.outline ? parsedOutline.outline : null;
|
|
170
|
+
hasMore = parsedOutline?.hasMore === true;
|
|
171
|
+
if (Array.isArray(parsedOutline?.warnings)) {
|
|
172
|
+
warnings = parsedOutline.warnings.filter((item) => typeof item === "string");
|
|
173
|
+
}
|
|
174
|
+
if (parsedOutline?.hints && typeof parsedOutline.hints === "object") {
|
|
175
|
+
hints = parsedOutline.hints;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
catch {
|
|
179
|
+
outlineStatus = "requires_reindex";
|
|
180
|
+
}
|
|
181
|
+
}
|
|
100
182
|
return {
|
|
101
183
|
content: [{
|
|
102
184
|
type: "text",
|
|
103
|
-
text:
|
|
185
|
+
text: JSON.stringify({
|
|
186
|
+
path: absolutePath,
|
|
187
|
+
mode: "annotated",
|
|
188
|
+
content: contentWithHint,
|
|
189
|
+
outlineStatus,
|
|
190
|
+
outline,
|
|
191
|
+
hasMore,
|
|
192
|
+
...(warnings && warnings.length > 0 ? { warnings } : {}),
|
|
193
|
+
...(hints ? { hints } : {})
|
|
194
|
+
}, null, 2)
|
|
104
195
|
}]
|
|
105
196
|
};
|
|
106
197
|
}
|
package/dist/tools/registry.js
CHANGED
|
@@ -2,12 +2,14 @@ import { zodToJsonSchema } from "zod-to-json-schema";
|
|
|
2
2
|
import { manageIndexTool } from "./manage_index.js";
|
|
3
3
|
import { searchCodebaseTool } from "./search_codebase.js";
|
|
4
4
|
import { callGraphTool } from "./call_graph.js";
|
|
5
|
+
import { fileOutlineTool } from "./file_outline.js";
|
|
5
6
|
import { readFileTool } from "./read_file.js";
|
|
6
7
|
import { listCodebasesTool } from "./list_codebases.js";
|
|
7
8
|
export const toolList = [
|
|
8
9
|
manageIndexTool,
|
|
9
10
|
searchCodebaseTool,
|
|
10
11
|
callGraphTool,
|
|
12
|
+
fileOutlineTool,
|
|
11
13
|
readFileTool,
|
|
12
14
|
listCodebasesTool,
|
|
13
15
|
];
|
|
@@ -35,6 +35,9 @@ function extractDiagnostics(response) {
|
|
|
35
35
|
excludedByIgnore: safeNumber(metaDiagnostics.excludedByIgnore, 0),
|
|
36
36
|
resultsReturned: afterFilter,
|
|
37
37
|
freshnessMode: typeof metaDiagnostics.freshnessMode === "string" ? metaDiagnostics.freshnessMode : undefined,
|
|
38
|
+
searchPassCount: safeNumber(metaDiagnostics.searchPassCount, 0),
|
|
39
|
+
searchPassSuccessCount: safeNumber(metaDiagnostics.searchPassSuccessCount, 0),
|
|
40
|
+
searchPassFailureCount: safeNumber(metaDiagnostics.searchPassFailureCount, 0),
|
|
38
41
|
};
|
|
39
42
|
}
|
|
40
43
|
const text = response.content?.[0]?.text;
|
|
@@ -50,6 +53,9 @@ function extractDiagnostics(response) {
|
|
|
50
53
|
excludedByIgnore: safeNumber(parsed?.excludedByIgnore, 0),
|
|
51
54
|
resultsReturned: results,
|
|
52
55
|
freshnessMode: typeof parsed?.freshnessDecision?.mode === "string" ? parsed.freshnessDecision.mode : undefined,
|
|
56
|
+
searchPassCount: safeNumber(parsed?.searchPassCount, 0),
|
|
57
|
+
searchPassSuccessCount: safeNumber(parsed?.searchPassSuccessCount, 0),
|
|
58
|
+
searchPassFailureCount: safeNumber(parsed?.searchPassFailureCount, 0),
|
|
53
59
|
};
|
|
54
60
|
}
|
|
55
61
|
catch {
|
|
@@ -103,6 +109,10 @@ export const searchCodebaseTool = {
|
|
|
103
109
|
reranker_used: false,
|
|
104
110
|
latency_ms: Date.now() - startedAt,
|
|
105
111
|
freshness_mode: diagnostics.freshnessMode,
|
|
112
|
+
search_pass_count: diagnostics.searchPassCount,
|
|
113
|
+
search_pass_success_count: diagnostics.searchPassSuccessCount,
|
|
114
|
+
search_pass_failure_count: diagnostics.searchPassFailureCount,
|
|
115
|
+
parallel_fanout: true,
|
|
106
116
|
...(response.isError ? { error: getErrorMessage(response) } : {})
|
|
107
117
|
});
|
|
108
118
|
return response;
|