preflight-mcp 0.4.4 → 0.5.1

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 CHANGED
@@ -51,7 +51,8 @@ Preflight: 🔗 Trace links:
51
51
  - 📖 **Auto-generated guides** — `START_HERE.md`, `AGENTS.md`, `OVERVIEW.md`
52
52
  - ☁️ **Cloud sync** — Multi-path mirror backup for redundancy
53
53
  - 🧠 **EDDA (Evidence-Driven Deep Analysis)** — Auto-generate auditable claims with evidence
54
- - ⚡ **18 MCP tools + 5 prompts** — Complete toolkit for code exploration
54
+ - ⚡ **22 MCP tools + 5 prompts** — Complete toolkit for code exploration
55
+ - 📄 **Cursor pagination** — Handle large result sets efficiently (RFC v2)
55
56
 
56
57
  <details>
57
58
  <summary><b>All Features (click to expand)</b></summary>
@@ -75,7 +76,7 @@ Preflight: 🔗 Trace links:
75
76
  - [Demo](#demo)
76
77
  - [Core Features](#core-features)
77
78
  - [Quick Start](#quick-start)
78
- - [Tools](#tools-15-total)
79
+ - [Tools](#tools-22-total)
79
80
  - [Prompts](#prompts-5-total)
80
81
  - [Environment Variables](#environment-variables)
81
82
  - [Contributing](#contributing)
@@ -160,10 +161,11 @@ Run end-to-end smoke test:
160
161
  npm run smoke
161
162
  ```
162
163
 
163
- ## Tools (18 total)
164
+ ## Tools (22 total)
164
165
 
165
166
  ### `preflight_list_bundles`
166
167
  List bundle IDs in storage.
168
+ - **Cursor pagination** (v0.5.0): Use `cursor` parameter for large bundle lists
167
169
  - Triggers: "show bundles", "查看bundle", "有哪些bundle"
168
170
 
169
171
  ### `preflight_create_bundle`
@@ -236,10 +238,15 @@ Important: **this tool is strictly read-only**.
236
238
  - `fileTypeFilters`: Filter by extension (e.g., `[".py", ".ts"]`)
237
239
  - `includeScore`: Include BM25 relevance score in results
238
240
 
241
+ **Cursor pagination** (v0.5.0):
242
+ - `cursor`: Pagination cursor from previous call for fetching next page
243
+ - Response includes `truncation.nextCursor` when more results available
244
+
239
245
  **Deprecated parameters**: `ensureFresh`, `autoRepairIndex`, `maxAgeHours` are deprecated and will return warnings instead of errors.
240
246
 
241
247
  ### `preflight_search_by_tags`
242
248
  Search across multiple bundles filtered by tags (line-based SQLite FTS5).
249
+ - **Cursor pagination** (v0.5.0): Use `cursor` parameter for large result sets
243
250
  - Triggers: "search in MCP bundles", "在MCP项目中搜索", "搜索所有agent"
244
251
 
245
252
  Notes:
@@ -250,6 +257,7 @@ Optional parameters:
250
257
  - `tags`: Filter bundles by tags (e.g., `["mcp", "agents"]`)
251
258
  - `scope`: Search scope (`docs`, `code`, or `all`)
252
259
  - `limit`: Max total hits across all bundles
260
+ - `cursor`: Pagination cursor for fetching next page
253
261
 
254
262
  ### `preflight_evidence_dependency_graph`
255
263
  Generate an evidence-based dependency graph. Two modes:
@@ -280,6 +288,7 @@ Create or update traceability links (code↔test, code↔doc, file↔requirement
280
288
  ### `preflight_trace_query`
281
289
  Query traceability links (code↔test, code↔doc, commit↔ticket).
282
290
  - **Proactive use**: LLM automatically queries trace links when analyzing specific files
291
+ - **Cursor pagination** (v0.5.0): Use `cursor` parameter for large result sets
283
292
  - Returns `reason` and `nextSteps` when no edges found (helps LLM decide next action)
284
293
  - Fast when `bundleId` is provided; can scan across bundles when omitted.
285
294
 
@@ -301,20 +310,39 @@ Parameters:
301
310
  - `min_confidence`: 0-1 (default: 0.85)
302
311
  - `limit`: Max suggestions (default: 50)
303
312
 
304
- ### `preflight_deep_analyze_bundle` *(NEW v0.4.0)*
305
- One-call deep analysis aggregating tree, search, deps, and traces.
313
+ ### `preflight_deep_analyze_bundle` *(Enhanced v0.5.1)*
314
+ One-call deep analysis aggregating tree, search, deps, traces, **overview content**, and **test detection**.
306
315
  - Returns unified evidence pack with LLM-friendly summary
316
+ - **Now includes OVERVIEW.md, START_HERE.md, AGENTS.md, README content** (v0.5.1)
317
+ - **Auto-detects test frameworks** (jest, vitest, pytest, go, mocha) (v0.5.1)
318
+ - **Generates copyable `nextCommands`** for follow-up actions (v0.5.1)
307
319
  - Auto-generates **claims** with evidence references
308
320
  - Tracks analysis progress via **checklistStatus**
309
321
  - Reports unanswered questions as **openQuestions**
310
- - Triggers: "deep analyze", "comprehensive analysis", "深度分析"
322
+ - Triggers: "deep analyze", "comprehensive analysis", "深度分析", "快速了解项目"
323
+
324
+ **New in v0.5.1 - Aggregated content (reduces round-trips):**
325
+ - `includeOverview` (default: true): Include OVERVIEW.md, START_HERE.md, AGENTS.md
326
+ - `includeReadme` (default: true): Include repo README.md
327
+ - `includeTests` (default: true): Detect test directories and frameworks
311
328
 
312
329
  Output includes:
330
+ - `overviewContent`: `{overview, startHere, agents, readme}` - bundle documentation content
331
+ - `testInfo`: `{detected, framework, testDirs, testFileCount, configFiles, hint}` - test detection result
332
+ - `nextCommands[]`: Copyable tool calls for next steps (can be directly used as arguments)
313
333
  - `claims[]`: Auto-generated findings with evidence
314
334
  - `checklistStatus`: Analysis progress (repo_tree, deps, entrypoints, etc.)
315
335
  - `openQuestions[]`: Questions with `nextEvidenceToFetch` hints
316
336
  - `summary`: Markdown summary with checklist and key findings
317
337
 
338
+ **Example `nextCommands` output:**
339
+ ```json
340
+ [
341
+ { "tool": "preflight_search_bundle", "description": "Search for specific code", "args": { "bundleId": "...", "query": "<填入关键词>" } },
342
+ { "tool": "preflight_read_file", "description": "Read core module", "args": { "bundleId": "...", "file": "src/server.ts" } }
343
+ ]
344
+ ```
345
+
318
346
  ### `preflight_validate_report` *(NEW v0.4.0)*
319
347
  Validate claims and evidence chains for auditability.
320
348
  - Checks: missing evidence, invalid file references, broken snippet hashes
@@ -327,6 +355,37 @@ Parameters:
327
355
  - `verifyFileExists`: Check evidence files exist (default: true)
328
356
  - `strictMode`: Treat warnings as errors (default: false)
329
357
 
358
+ ### `preflight_read_files` *(NEW v0.5.0)*
359
+ Batch read multiple files from a bundle in a single call.
360
+ - Reduces round-trips for evidence gathering
361
+ - **RFC v2 unified envelope**: Returns `ok`, `meta`, `data`, `evidence[]`
362
+ - Triggers: "read these files", "get content of", "批量读取"
363
+
364
+ Parameters:
365
+ - `bundleId`: Bundle ID
366
+ - `files[]`: Array of `{path, ranges?, withLineNumbers?}`
367
+ - `format`: `"json"` (default) or `"text"`
368
+
369
+ ### `preflight_search_and_read` *(NEW v0.5.0)*
370
+ Search + excerpt in one call - finds relevant code and returns context.
371
+ - Combines search with automatic context extraction
372
+ - **RFC v2 unified envelope**: Returns `ok`, `meta`, `data`, `evidence[]`
373
+ - Triggers: "search and show code", "find and read", "搜索并读取"
374
+
375
+ Parameters:
376
+ - `bundleId`: Bundle ID
377
+ - `query`: Search query
378
+ - `contextLines`: Lines of context around matches (default: 5)
379
+ - `maxFiles`: Max files to read (default: 5)
380
+ - `format`: `"json"` (default) or `"text"`
381
+
382
+ ### `preflight_get_dependency_graph` *(Simplified wrapper)*
383
+ Simplified dependency graph query.
384
+ - `scope: "global"` (default): Project-wide graph
385
+ - `scope: "target"` with `targetFile`: Single file dependencies
386
+ - `format: "summary"` (default): Aggregated view
387
+ - `format: "full"`: Raw graph data
388
+
330
389
  ### `preflight_cleanup_orphans`
331
390
  Remove incomplete or corrupted bundles (bundles without valid manifest.json).
332
391
  - Triggers: "clean up broken bundles", "remove orphans", "清理孤儿bundle"
@@ -393,6 +452,11 @@ Common kinds:
393
452
  - `invalid_path` (unsafe path traversal attempt)
394
453
  - `permission_denied`
395
454
  - `index_missing_or_corrupt`
455
+ - `cursor_invalid` *(v0.5.0)*
456
+ - `cursor_expired` *(v0.5.0)*
457
+ - `rate_limited` *(v0.5.0)*
458
+ - `timeout` *(v0.5.0)*
459
+ - `pagination_required` *(v0.5.0)*
396
460
  - `unknown`
397
461
 
398
462
  This is designed so UIs/agents can reliably decide whether to:
@@ -8,7 +8,7 @@ import { createEmptyCoverageReport, isCoverageSufficient, } from '../types/evide
8
8
  * This is called by the server after gathering data from each source.
9
9
  */
10
10
  export function buildDeepAnalysis(bundleId, components) {
11
- const { tree, search, deps, traces, focusPath, focusQuery, errors = [] } = components;
11
+ const { tree, search, deps, traces, overviewContent, testInfo, focusPath, focusQuery, errors = [] } = components;
12
12
  // Build coverage report
13
13
  const coverageReport = createEmptyCoverageReport();
14
14
  if (tree) {
@@ -75,9 +75,28 @@ export function buildDeepAnalysis(bundleId, components) {
75
75
  }
76
76
  summaryParts.push('');
77
77
  }
78
+ // Test detection summary
79
+ if (testInfo) {
80
+ summaryParts.push(`## Test Detection`);
81
+ if (testInfo.detected) {
82
+ summaryParts.push(`- Framework: ${testInfo.framework ?? 'unknown'}`);
83
+ summaryParts.push(`- Test files: ${testInfo.testFileCount}`);
84
+ if (testInfo.testDirs.length > 0) {
85
+ summaryParts.push(`- Test directories: ${testInfo.testDirs.slice(0, 3).join(', ')}`);
86
+ }
87
+ if (testInfo.configFiles.length > 0) {
88
+ summaryParts.push(`- Config files: ${testInfo.configFiles.join(', ')}`);
89
+ }
90
+ }
91
+ else {
92
+ summaryParts.push(`- No tests detected`);
93
+ }
94
+ summaryParts.push(`- 💡 ${testInfo.hint}`);
95
+ summaryParts.push('');
96
+ }
78
97
  // Build checklist status
79
98
  const checklistStatus = {
80
- read_overview: false, // Would need OVERVIEW.md read - caller should set
99
+ read_overview: !!(overviewContent?.overview || overviewContent?.startHere),
81
100
  repo_tree: !!tree && tree.totalFiles > 0,
82
101
  search_focus: !!search && search.totalHits > 0,
83
102
  dependency_graph_global: !!deps && deps.totalNodes > 0,
@@ -271,6 +290,51 @@ export function buildDeepAnalysis(bundleId, components) {
271
290
  if (isCoverageSufficient(coverageReport) && openQuestions.length === 0 && claims.length > 0) {
272
291
  nextSteps.push('🎉 Analysis complete - all key areas covered. Ready for detailed review.');
273
292
  }
293
+ // Build nextCommands (copyable JSON for LLM)
294
+ const nextCommands = [];
295
+ // Always suggest search as a useful next step
296
+ nextCommands.push({
297
+ tool: 'preflight_search_bundle',
298
+ description: 'Search for specific code or concepts',
299
+ args: { bundleId, query: '<填入关键词>', scope: 'all', limit: 30 },
300
+ });
301
+ // Suggest reading a specific entry point if identified
302
+ if (deps && deps.topImported.length > 0) {
303
+ const coreFile = deps.topImported[0].file;
304
+ nextCommands.push({
305
+ tool: 'preflight_read_file',
306
+ description: `Read core module: ${coreFile}`,
307
+ args: { bundleId, file: coreFile, withLineNumbers: true },
308
+ });
309
+ }
310
+ // Suggest dependency analysis for a specific file if entry point identified
311
+ if (deps && deps.topImporters.length > 0) {
312
+ const entryFile = deps.topImporters[0].file;
313
+ nextCommands.push({
314
+ tool: 'preflight_evidence_dependency_graph',
315
+ description: `Analyze dependencies of entry point: ${entryFile}`,
316
+ args: { bundleId, target: { file: entryFile } },
317
+ });
318
+ }
319
+ // Suggest trace discovery if no traces exist
320
+ if (!traces || traces.totalLinks === 0) {
321
+ nextCommands.push({
322
+ tool: 'preflight_suggest_traces',
323
+ description: 'Auto-discover test↔code relationships',
324
+ args: { bundleId, edge_type: 'tested_by', scope: 'repo' },
325
+ });
326
+ }
327
+ // Suggest focused tree if large directory detected
328
+ if (tree) {
329
+ const largeDir = tree.topDirs.find(d => d.fileCount > 50);
330
+ if (largeDir) {
331
+ nextCommands.push({
332
+ tool: 'preflight_repo_tree',
333
+ description: `Explore large directory: ${largeDir.path}`,
334
+ args: { bundleId, focusDir: largeDir.path, depth: 6 },
335
+ });
336
+ }
337
+ }
274
338
  // Add checklist and claims to summary
275
339
  summaryParts.push(`## Analysis Checklist`);
276
340
  const checklistItems = [
@@ -307,11 +371,104 @@ export function buildDeepAnalysis(bundleId, components) {
307
371
  search,
308
372
  deps,
309
373
  traces,
374
+ overviewContent,
375
+ testInfo,
310
376
  claims,
311
377
  checklistStatus,
312
378
  openQuestions,
313
379
  coverageReport,
314
380
  summary: summaryParts.join('\n'),
315
381
  nextSteps,
382
+ nextCommands,
383
+ };
384
+ }
385
+ /**
386
+ * Detect test setup from file tree statistics.
387
+ * Scans for test directories, test files, and framework config files.
388
+ */
389
+ export function detectTestInfo(stats, filesFound) {
390
+ const testDirs = [];
391
+ let testFileCount = 0;
392
+ const configFiles = [];
393
+ let framework = null;
394
+ // Common test directory patterns
395
+ const testDirPatterns = ['tests', 'test', '__tests__', 'spec', 'specs', 'e2e', 'integration'];
396
+ // Check byTopDir for test directories
397
+ if (stats.byTopDir) {
398
+ for (const [dir, count] of Object.entries(stats.byTopDir)) {
399
+ const dirLower = dir.toLowerCase();
400
+ if (testDirPatterns.some(p => dirLower === p || dirLower.endsWith('/' + p))) {
401
+ testDirs.push(dir);
402
+ testFileCount += count;
403
+ }
404
+ }
405
+ }
406
+ // Framework detection from config files (if filesFound provided)
407
+ const frameworkConfigs = [
408
+ { pattern: /^jest\.config\.(js|ts|mjs|cjs|json)$/i, framework: 'jest' },
409
+ { pattern: /^vitest\.config\.(js|ts|mjs|cjs)$/i, framework: 'vitest' },
410
+ { pattern: /^pytest\.ini$/i, framework: 'pytest' },
411
+ { pattern: /^pyproject\.toml$/i, framework: 'pytest' }, // May contain pytest config
412
+ { pattern: /^setup\.cfg$/i, framework: 'pytest' },
413
+ { pattern: /^\.mocharc\.(js|json|yml|yaml)$/i, framework: 'mocha' },
414
+ { pattern: /^mocha\.opts$/i, framework: 'mocha' },
415
+ ];
416
+ if (filesFound) {
417
+ for (const file of filesFound) {
418
+ for (const cfg of frameworkConfigs) {
419
+ if (cfg.pattern.test(file.name)) {
420
+ configFiles.push(file.path);
421
+ if (!framework) {
422
+ framework = cfg.framework;
423
+ }
424
+ }
425
+ }
426
+ }
427
+ }
428
+ // Infer framework from file extensions if not detected from config
429
+ if (!framework && stats.byExtension) {
430
+ // Check for test file patterns in extensions
431
+ const hasTs = (stats.byExtension['.ts'] ?? 0) > 0 || (stats.byExtension['.tsx'] ?? 0) > 0;
432
+ const hasPy = (stats.byExtension['.py'] ?? 0) > 0;
433
+ const hasGo = (stats.byExtension['.go'] ?? 0) > 0;
434
+ if (testDirs.length > 0 || testFileCount > 0) {
435
+ if (hasGo)
436
+ framework = 'go';
437
+ else if (hasPy)
438
+ framework = 'pytest';
439
+ else if (hasTs)
440
+ framework = 'unknown'; // Could be jest/vitest/mocha
441
+ }
442
+ }
443
+ // Count test files by pattern (approximate from extensions)
444
+ // This is a heuristic - actual test files may vary
445
+ if (testFileCount === 0 && stats.byExtension) {
446
+ // If no test directories found, estimate based on common patterns
447
+ // This is imprecise but gives a hint
448
+ }
449
+ const detected = testDirs.length > 0 || testFileCount > 0 || configFiles.length > 0;
450
+ // Generate hint based on detection results
451
+ let hint;
452
+ if (detected) {
453
+ if (testFileCount > 0) {
454
+ hint = `Found ${testFileCount} test files. Run preflight_suggest_traces to map code↔test relationships.`;
455
+ }
456
+ else if (configFiles.length > 0) {
457
+ hint = `Test config found (${configFiles[0]}). Run preflight_suggest_traces to discover test files.`;
458
+ }
459
+ else {
460
+ hint = `Test directories found. Run preflight_suggest_traces to map code↔test relationships.`;
461
+ }
462
+ }
463
+ else {
464
+ hint = 'No tests detected. Consider adding tests or check if test files use non-standard naming.';
465
+ }
466
+ return {
467
+ detected,
468
+ framework,
469
+ testDirs,
470
+ testFileCount,
471
+ configFiles,
472
+ hint,
316
473
  };
317
474
  }
@@ -0,0 +1,145 @@
1
+ /**
2
+ * RFC v2: Cursor encoding/decoding for stable pagination.
3
+ *
4
+ * Cursors are opaque, base64-encoded JSON objects that contain:
5
+ * - offset: The position in the result set
6
+ * - sortKey: The last seen sort key for stable ordering
7
+ * - tool: The tool that generated the cursor (for validation)
8
+ * - timestamp: When the cursor was created (for expiration)
9
+ *
10
+ * This enables LLMs to reliably paginate through large result sets.
11
+ */
12
+ /**
13
+ * Maximum cursor age in milliseconds (24 hours).
14
+ * Cursors older than this are considered expired.
15
+ */
16
+ const MAX_CURSOR_AGE_MS = 24 * 60 * 60 * 1000;
17
+ /**
18
+ * Encode cursor state to an opaque string.
19
+ */
20
+ export function encodeCursor(state) {
21
+ const json = JSON.stringify(state);
22
+ // Use base64url encoding (URL-safe, no padding)
23
+ return Buffer.from(json, 'utf8').toString('base64url');
24
+ }
25
+ /**
26
+ * Decode cursor string to cursor state.
27
+ * Returns null if the cursor is invalid.
28
+ */
29
+ export function decodeCursor(cursor) {
30
+ try {
31
+ const json = Buffer.from(cursor, 'base64url').toString('utf8');
32
+ const state = JSON.parse(json);
33
+ // Validate structure
34
+ if (typeof state !== 'object' ||
35
+ state === null ||
36
+ typeof state.offset !== 'number' ||
37
+ typeof state.tool !== 'string' ||
38
+ typeof state.timestamp !== 'number') {
39
+ return null;
40
+ }
41
+ return state;
42
+ }
43
+ catch {
44
+ return null;
45
+ }
46
+ }
47
+ /**
48
+ * Validate a cursor for a specific tool.
49
+ * Checks structure, tool match, and expiration.
50
+ */
51
+ export function validateCursor(cursor, expectedTool, options) {
52
+ const state = decodeCursor(cursor);
53
+ if (!state) {
54
+ return {
55
+ valid: false,
56
+ error: 'Invalid cursor format',
57
+ };
58
+ }
59
+ // Check tool match
60
+ if (!options?.allowToolMismatch && state.tool !== expectedTool) {
61
+ return {
62
+ valid: false,
63
+ state,
64
+ error: `Cursor was created by ${state.tool}, not ${expectedTool}`,
65
+ };
66
+ }
67
+ // Check expiration
68
+ const maxAge = options?.maxAgeMs ?? MAX_CURSOR_AGE_MS;
69
+ const age = Date.now() - state.timestamp;
70
+ if (age > maxAge) {
71
+ return {
72
+ valid: false,
73
+ state,
74
+ error: `Cursor expired (age: ${Math.round(age / 1000)}s, max: ${Math.round(maxAge / 1000)}s)`,
75
+ };
76
+ }
77
+ return {
78
+ valid: true,
79
+ state,
80
+ };
81
+ }
82
+ /**
83
+ * Create a cursor for the next page of results.
84
+ *
85
+ * @param tool - The tool creating the cursor
86
+ * @param offset - Current offset (will be incremented by pageSize)
87
+ * @param pageSize - Number of items per page
88
+ * @param sortKey - Optional sort key for keyset pagination
89
+ * @param extra - Optional additional data
90
+ */
91
+ export function createNextCursor(tool, offset, pageSize, sortKey, extra) {
92
+ const state = {
93
+ offset: offset + pageSize,
94
+ tool,
95
+ timestamp: Date.now(),
96
+ };
97
+ if (sortKey !== undefined)
98
+ state.sortKey = sortKey;
99
+ if (extra !== undefined)
100
+ state.extra = extra;
101
+ return encodeCursor(state);
102
+ }
103
+ /**
104
+ * Parse cursor or return default offset.
105
+ * Convenience function for tool handlers.
106
+ *
107
+ * @param cursor - Optional cursor string
108
+ * @param tool - Expected tool name
109
+ * @param defaultOffset - Default offset if no cursor (default: 0)
110
+ * @returns Offset to use and any error message
111
+ */
112
+ export function parseCursorOrDefault(cursor, tool, defaultOffset = 0) {
113
+ if (!cursor) {
114
+ return { offset: defaultOffset };
115
+ }
116
+ const validation = validateCursor(cursor, tool);
117
+ if (!validation.valid) {
118
+ return { offset: defaultOffset, error: validation.error };
119
+ }
120
+ return {
121
+ offset: validation.state.offset,
122
+ sortKey: validation.state.sortKey,
123
+ extra: validation.state.extra,
124
+ };
125
+ }
126
+ /**
127
+ * Helper to determine if pagination should continue.
128
+ *
129
+ * @param returnedCount - Number of items returned in this page
130
+ * @param limit - Requested limit
131
+ * @param totalCount - Optional total count (if known)
132
+ * @param currentOffset - Current offset in result set
133
+ */
134
+ export function shouldPaginate(returnedCount, limit, totalCount, currentOffset = 0) {
135
+ // If we got fewer items than requested, we're at the end
136
+ if (returnedCount < limit) {
137
+ return false;
138
+ }
139
+ // If we know the total and have fetched everything, no more pages
140
+ if (totalCount !== undefined && currentOffset + returnedCount >= totalCount) {
141
+ return false;
142
+ }
143
+ // Otherwise, assume there might be more
144
+ return true;
145
+ }
@@ -0,0 +1,61 @@
1
+ /**
2
+ * RFC v2: Unified Response Envelope for all Preflight MCP tools.
3
+ *
4
+ * This module defines the standardized response structure that enables:
5
+ * - LLM-friendly JSON output with stable field names
6
+ * - Evidence-first design with traceable citations
7
+ * - Pagination/truncation support with cursor-based continuation
8
+ * - Structured error handling with recovery hints
9
+ */
10
+ /**
11
+ * Schema version for response envelope.
12
+ * Increment when making breaking changes to envelope structure.
13
+ */
14
+ export const SCHEMA_VERSION = '2.0';
15
+ /**
16
+ * Type guard to check if response is successful.
17
+ */
18
+ export function isSuccessResponse(response) {
19
+ return response.ok === true && response.data !== undefined;
20
+ }
21
+ /**
22
+ * Type guard to check if response is an error.
23
+ */
24
+ export function isErrorResponse(response) {
25
+ return response.ok === false && response.error !== undefined;
26
+ }
27
+ /**
28
+ * Helper to create a source range from line numbers.
29
+ */
30
+ export function createRange(startLine, endLine, startCol, endCol) {
31
+ const range = { startLine, endLine };
32
+ if (startCol !== undefined)
33
+ range.startCol = startCol;
34
+ if (endCol !== undefined)
35
+ range.endCol = endCol;
36
+ return range;
37
+ }
38
+ /**
39
+ * Helper to create an evidence pointer.
40
+ */
41
+ export function createEvidencePointer(path, range, options) {
42
+ const pointer = { path, range };
43
+ if (options?.uri)
44
+ pointer.uri = options.uri;
45
+ if (options?.snippet)
46
+ pointer.snippet = options.snippet;
47
+ if (options?.snippetSha256)
48
+ pointer.snippetSha256 = options.snippetSha256;
49
+ return pointer;
50
+ }
51
+ /**
52
+ * Format evidence pointer as citation string.
53
+ * Format: "path:startLine-endLine" or "path:line" for single line
54
+ */
55
+ export function formatEvidenceCitation(pointer) {
56
+ const { path, range } = pointer;
57
+ if (range.startLine === range.endLine) {
58
+ return `${path}:${range.startLine}`;
59
+ }
60
+ return `${path}:${range.startLine}-${range.endLine}`;
61
+ }
@@ -72,6 +72,41 @@ const LLM_RECOVERY_HINTS = {
72
72
  This parameter is deprecated. The tool is now strictly read-only.
73
73
  - For updates: use preflight_update_bundle first, then retry
74
74
  - For repairs: use preflight_repair_bundle first, then retry`,
75
+ // RFC v2 additions
76
+ cursor_invalid: `💡 Recovery steps:
77
+ 1. The cursor format is invalid or corrupted
78
+ 2. Start fresh without a cursor to get the first page
79
+ 3. Use the nextCursor from the previous response exactly as provided`,
80
+ cursor_expired: `💡 Recovery steps:
81
+ 1. Cursors expire after 24 hours
82
+ 2. Start fresh without a cursor to get the first page
83
+ 3. Complete pagination within a reasonable time window`,
84
+ cursor_tool_mismatch: `💡 Recovery steps:
85
+ 1. The cursor was created by a different tool
86
+ 2. Use the cursor only with the same tool that created it
87
+ 3. Start fresh without a cursor for this tool`,
88
+ rate_limited: `💡 Recovery steps:
89
+ 1. You are making requests too quickly
90
+ 2. Wait a few seconds before retrying
91
+ 3. Consider batching multiple operations into single calls`,
92
+ timeout: `💡 Recovery steps:
93
+ 1. The operation took too long to complete
94
+ 2. Try with a smaller scope or limit
95
+ 3. Use cursor pagination to process in smaller batches`,
96
+ pagination_required: `💡 Note:
97
+ The result set is large. Use cursor pagination:
98
+ 1. Check the 'truncation' field in the response
99
+ 2. Pass the 'nextCursor' value in subsequent calls
100
+ 3. Continue until truncated=false`,
101
+ validation_error: `💡 Recovery steps:
102
+ 1. Check that all required parameters are provided
103
+ 2. Verify parameter types match the schema
104
+ 3. Review the error message for specific field issues`,
105
+ partial_success: `💡 Note:
106
+ Some operations succeeded but others failed:
107
+ 1. Check the 'warnings' array for details on failed items
108
+ 2. Address individual issues and retry failed items
109
+ 3. Successfully processed items are already applied`,
75
110
  unknown: `💡 If this error persists:
76
111
  1. Check the error message for specific details
77
112
  2. Verify your input parameters match the tool's schema