converse-mcp-server 2.8.5 → 2.9.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/package.json +1 -1
- package/src/providers/anthropic.js +7 -2
- package/src/providers/claude.js +26 -26
- package/src/providers/gemini-cli.js +17 -17
- package/src/providers/google.js +1 -5
- package/src/tools/chat.js +46 -36
- package/src/tools/consensus.js +12 -4
- package/src/utils/contextProcessor.js +41 -21
- package/src/utils/conversationExporter.js +5 -3
- package/src/utils/fileValidator.js +8 -3
- package/src/utils/pathParser.js +173 -0
package/package.json
CHANGED
|
@@ -624,7 +624,11 @@ export const anthropicProvider = {
|
|
|
624
624
|
}
|
|
625
625
|
|
|
626
626
|
// Add effort parameter for Opus 4.5 (uses output_config)
|
|
627
|
-
if (
|
|
627
|
+
if (
|
|
628
|
+
modelConfig.supportsEffort &&
|
|
629
|
+
reasoning_effort &&
|
|
630
|
+
reasoning_effort !== 'none'
|
|
631
|
+
) {
|
|
628
632
|
const effortValue = EFFORT_MAP[reasoning_effort];
|
|
629
633
|
if (effortValue) {
|
|
630
634
|
requestPayload.output_config = {
|
|
@@ -852,7 +856,8 @@ export const anthropicProvider = {
|
|
|
852
856
|
const streamingPayload = { ...requestPayload, stream: true };
|
|
853
857
|
|
|
854
858
|
// Create the streaming request - use beta endpoint when beta features are enabled
|
|
855
|
-
const hasBetaFeatures =
|
|
859
|
+
const hasBetaFeatures =
|
|
860
|
+
requestPayload.betas && requestPayload.betas.length > 0;
|
|
856
861
|
const stream = hasBetaFeatures
|
|
857
862
|
? await anthropic.beta.messages.create(streamingPayload)
|
|
858
863
|
: await anthropic.messages.create(streamingPayload);
|
package/src/providers/claude.js
CHANGED
|
@@ -28,8 +28,7 @@ const SUPPORTED_MODELS = {
|
|
|
28
28
|
supportsTemperature: false, // SDK manages temperature internally
|
|
29
29
|
supportsWebSearch: false, // SDK accesses files directly, not web
|
|
30
30
|
timeout: 120000, // 2 minutes
|
|
31
|
-
description:
|
|
32
|
-
'Claude via Agent SDK - requires claude login authentication',
|
|
31
|
+
description: 'Claude via Agent SDK - requires claude login authentication',
|
|
33
32
|
aliases: ['claude-sdk', 'claude-code'],
|
|
34
33
|
},
|
|
35
34
|
};
|
|
@@ -320,10 +319,10 @@ async function* createStreamingGenerator(
|
|
|
320
319
|
input_tokens: message.usage.input_tokens || 0,
|
|
321
320
|
output_tokens: message.usage.output_tokens || 0,
|
|
322
321
|
total_tokens:
|
|
323
|
-
|
|
324
|
-
|
|
322
|
+
(message.usage.input_tokens || 0) +
|
|
323
|
+
(message.usage.output_tokens || 0),
|
|
325
324
|
cached_input_tokens:
|
|
326
|
-
|
|
325
|
+
message.usage.cache_read_input_tokens || 0,
|
|
327
326
|
},
|
|
328
327
|
};
|
|
329
328
|
}
|
|
@@ -336,7 +335,7 @@ async function* createStreamingGenerator(
|
|
|
336
335
|
};
|
|
337
336
|
} else if (
|
|
338
337
|
message.subtype === 'error_max_turns' ||
|
|
339
|
-
|
|
338
|
+
message.subtype === 'error_during_execution'
|
|
340
339
|
) {
|
|
341
340
|
throw new ClaudeProviderError(
|
|
342
341
|
`Claude SDK execution failed: ${message.subtype}`,
|
|
@@ -359,11 +358,11 @@ async function* createStreamingGenerator(
|
|
|
359
358
|
*/
|
|
360
359
|
export const claudeProvider = {
|
|
361
360
|
/**
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
361
|
+
* Invoke Claude SDK with messages and options
|
|
362
|
+
* @param {Array} messages - Message array (Converse format)
|
|
363
|
+
* @param {Object} options - Invocation options
|
|
364
|
+
* @returns {Promise<Object>|AsyncGenerator} Response or stream generator
|
|
365
|
+
*/
|
|
367
366
|
async invoke(messages, options = {}) {
|
|
368
367
|
const {
|
|
369
368
|
model = 'claude',
|
|
@@ -408,7 +407,8 @@ export const claudeProvider = {
|
|
|
408
407
|
// Returns { prompt, sdkMessage, hasImages }
|
|
409
408
|
// - prompt: string for single message mode (text-only)
|
|
410
409
|
// - sdkMessage: SDK user message for streaming input mode (with images)
|
|
411
|
-
const { prompt, sdkMessage, hasImages } =
|
|
410
|
+
const { prompt, sdkMessage, hasImages } =
|
|
411
|
+
convertMessagesToSdkInput(messages);
|
|
412
412
|
|
|
413
413
|
if (hasImages) {
|
|
414
414
|
debugLog('[Claude SDK] Using streaming input mode for image support');
|
|
@@ -465,7 +465,7 @@ export const claudeProvider = {
|
|
|
465
465
|
input_tokens: usage.input_tokens || 0,
|
|
466
466
|
output_tokens: usage.output_tokens || 0,
|
|
467
467
|
total_tokens:
|
|
468
|
-
|
|
468
|
+
(usage.input_tokens || 0) + (usage.output_tokens || 0),
|
|
469
469
|
cached_input_tokens: usage.cached_input_tokens || 0,
|
|
470
470
|
}
|
|
471
471
|
: null,
|
|
@@ -479,8 +479,8 @@ export const claudeProvider = {
|
|
|
479
479
|
// Map common errors to standard error codes
|
|
480
480
|
if (
|
|
481
481
|
error.message?.includes('authentication') ||
|
|
482
|
-
|
|
483
|
-
|
|
482
|
+
error.message?.includes('login') ||
|
|
483
|
+
error.message?.includes('not authenticated')
|
|
484
484
|
) {
|
|
485
485
|
throw new ClaudeProviderError(
|
|
486
486
|
'Claude SDK authentication failed. Run: claude login',
|
|
@@ -519,10 +519,10 @@ export const claudeProvider = {
|
|
|
519
519
|
},
|
|
520
520
|
|
|
521
521
|
/**
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
522
|
+
* Validate Claude SDK configuration
|
|
523
|
+
* Claude SDK uses CLI authentication (NOT API keys)
|
|
524
|
+
* Returns true optimistically - authentication errors handled at runtime
|
|
525
|
+
*/
|
|
526
526
|
validateConfig(_config) {
|
|
527
527
|
// Claude SDK uses CLI authentication, not API keys
|
|
528
528
|
// We can't reliably check auth status, so return true optimistically
|
|
@@ -531,22 +531,22 @@ export const claudeProvider = {
|
|
|
531
531
|
},
|
|
532
532
|
|
|
533
533
|
/**
|
|
534
|
-
|
|
535
|
-
|
|
534
|
+
* Check if Claude SDK provider is available
|
|
535
|
+
*/
|
|
536
536
|
isAvailable(config) {
|
|
537
537
|
return this.validateConfig(config);
|
|
538
538
|
},
|
|
539
539
|
|
|
540
540
|
/**
|
|
541
|
-
|
|
542
|
-
|
|
541
|
+
* Get supported Claude SDK models
|
|
542
|
+
*/
|
|
543
543
|
getSupportedModels() {
|
|
544
544
|
return SUPPORTED_MODELS;
|
|
545
545
|
},
|
|
546
546
|
|
|
547
547
|
/**
|
|
548
|
-
|
|
549
|
-
|
|
548
|
+
* Get model configuration for specific model
|
|
549
|
+
*/
|
|
550
550
|
getModelConfig(modelName) {
|
|
551
551
|
const modelNameLower = modelName.toLowerCase();
|
|
552
552
|
|
|
@@ -559,7 +559,7 @@ export const claudeProvider = {
|
|
|
559
559
|
for (const [_name, config] of Object.entries(SUPPORTED_MODELS)) {
|
|
560
560
|
if (
|
|
561
561
|
config.aliases &&
|
|
562
|
-
|
|
562
|
+
config.aliases.some((alias) => alias.toLowerCase() === modelNameLower)
|
|
563
563
|
) {
|
|
564
564
|
return config;
|
|
565
565
|
}
|
|
@@ -36,7 +36,7 @@ const SUPPORTED_MODELS = {
|
|
|
36
36
|
supportsWebSearch: true,
|
|
37
37
|
timeout: 300000, // 5 minutes
|
|
38
38
|
description:
|
|
39
|
-
|
|
39
|
+
'Gemini 3.0 Pro Preview via OAuth - requires Gemini CLI authentication',
|
|
40
40
|
aliases: ['gemini-cli'],
|
|
41
41
|
// Internal SDK model name (user-facing "gemini" maps to SDK's "gemini-3-pro-preview")
|
|
42
42
|
sdkModelName: 'gemini-3-pro-preview',
|
|
@@ -272,11 +272,11 @@ function convertToModelMessages(messages) {
|
|
|
272
272
|
*/
|
|
273
273
|
export const geminiCliProvider = {
|
|
274
274
|
/**
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
275
|
+
* Invoke Gemini CLI with messages and options
|
|
276
|
+
* @param {Array} messages - Message array (Converse format)
|
|
277
|
+
* @param {Object} options - Invocation options
|
|
278
|
+
* @returns {Promise<Object>|AsyncGenerator} Response or stream generator
|
|
279
|
+
*/
|
|
280
280
|
async invoke(messages, options = {}) {
|
|
281
281
|
const {
|
|
282
282
|
model = 'gemini',
|
|
@@ -405,8 +405,8 @@ export const geminiCliProvider = {
|
|
|
405
405
|
// Map common errors to standard error codes
|
|
406
406
|
if (
|
|
407
407
|
error.message?.includes('authentication') ||
|
|
408
|
-
|
|
409
|
-
|
|
408
|
+
error.message?.includes('oauth') ||
|
|
409
|
+
error.message?.includes('credentials')
|
|
410
410
|
) {
|
|
411
411
|
throw new GeminiCliProviderError(
|
|
412
412
|
'Gemini CLI authentication failed. Run: gemini (interactive CLI) to authenticate',
|
|
@@ -441,31 +441,31 @@ export const geminiCliProvider = {
|
|
|
441
441
|
},
|
|
442
442
|
|
|
443
443
|
/**
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
444
|
+
* Validate Gemini CLI configuration
|
|
445
|
+
* Gemini CLI uses OAuth authentication (no API keys needed)
|
|
446
|
+
*/
|
|
447
447
|
validateConfig(_config) {
|
|
448
448
|
// Check if OAuth credentials file exists
|
|
449
449
|
return hasOAuthCredentials();
|
|
450
450
|
},
|
|
451
451
|
|
|
452
452
|
/**
|
|
453
|
-
|
|
454
|
-
|
|
453
|
+
* Check if Gemini CLI provider is available
|
|
454
|
+
*/
|
|
455
455
|
isAvailable(config) {
|
|
456
456
|
return this.validateConfig(config);
|
|
457
457
|
},
|
|
458
458
|
|
|
459
459
|
/**
|
|
460
|
-
|
|
461
|
-
|
|
460
|
+
* Get supported Gemini CLI models
|
|
461
|
+
*/
|
|
462
462
|
getSupportedModels() {
|
|
463
463
|
return SUPPORTED_MODELS;
|
|
464
464
|
},
|
|
465
465
|
|
|
466
466
|
/**
|
|
467
|
-
|
|
468
|
-
|
|
467
|
+
* Get model configuration for specific model
|
|
468
|
+
*/
|
|
469
469
|
getModelConfig(modelName) {
|
|
470
470
|
const modelNameLower = modelName.toLowerCase();
|
|
471
471
|
|
package/src/providers/google.js
CHANGED
|
@@ -119,11 +119,7 @@ const SUPPORTED_MODELS = {
|
|
|
119
119
|
timeout: 300000,
|
|
120
120
|
description:
|
|
121
121
|
'Deep reasoning + thinking mode (1M context) - Complex problems, architecture, deep analysis',
|
|
122
|
-
aliases: [
|
|
123
|
-
'pro 2.5',
|
|
124
|
-
'gemini pro 2.5',
|
|
125
|
-
'gemini-2.5-pro-latest',
|
|
126
|
-
],
|
|
122
|
+
aliases: ['pro 2.5', 'gemini pro 2.5', 'gemini-2.5-pro-latest'],
|
|
127
123
|
},
|
|
128
124
|
'gemini-3-pro-preview': {
|
|
129
125
|
modelName: 'gemini-3-pro-preview',
|
package/src/tools/chat.js
CHANGED
|
@@ -375,23 +375,26 @@ export async function chatTool(args, dependencies) {
|
|
|
375
375
|
|
|
376
376
|
// Export conversation if requested
|
|
377
377
|
if (shouldExport) {
|
|
378
|
-
await exportConversation(
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
378
|
+
await exportConversation(
|
|
379
|
+
{
|
|
380
|
+
messages: updatedMessages,
|
|
381
|
+
provider: providerName,
|
|
382
|
+
model,
|
|
383
|
+
lastUpdated: Date.now(),
|
|
384
|
+
codexThreadId: response.metadata?.threadId,
|
|
385
|
+
},
|
|
386
|
+
{
|
|
387
|
+
clientCwd: config.server?.client_cwd,
|
|
388
|
+
continuation_id: continuationId,
|
|
389
|
+
model,
|
|
390
|
+
temperature,
|
|
391
|
+
reasoning_effort,
|
|
392
|
+
verbosity,
|
|
393
|
+
use_websearch,
|
|
394
|
+
files,
|
|
395
|
+
images,
|
|
396
|
+
},
|
|
397
|
+
);
|
|
395
398
|
}
|
|
396
399
|
|
|
397
400
|
// Create unified status line (similar to async status display)
|
|
@@ -495,7 +498,11 @@ export function mapModelToProvider(model, providers) {
|
|
|
495
498
|
}
|
|
496
499
|
|
|
497
500
|
// Check Claude SDK (exact match only - routes to SDK provider instead of Anthropic API)
|
|
498
|
-
if (
|
|
501
|
+
if (
|
|
502
|
+
modelLower === 'claude' ||
|
|
503
|
+
modelLower === 'claude-sdk' ||
|
|
504
|
+
modelLower === 'claude-code'
|
|
505
|
+
) {
|
|
499
506
|
return 'claude';
|
|
500
507
|
}
|
|
501
508
|
|
|
@@ -963,23 +970,26 @@ async function executeChatWithStreaming(args, dependencies, context) {
|
|
|
963
970
|
|
|
964
971
|
// Export conversation if requested
|
|
965
972
|
if (shouldExport) {
|
|
966
|
-
await exportConversation(
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
973
|
+
await exportConversation(
|
|
974
|
+
{
|
|
975
|
+
messages: updatedMessages,
|
|
976
|
+
provider: providerName,
|
|
977
|
+
model,
|
|
978
|
+
lastUpdated: Date.now(),
|
|
979
|
+
codexThreadId: response.metadata?.threadId,
|
|
980
|
+
},
|
|
981
|
+
{
|
|
982
|
+
clientCwd: config.server?.client_cwd,
|
|
983
|
+
continuation_id: continuationId,
|
|
984
|
+
model,
|
|
985
|
+
temperature,
|
|
986
|
+
reasoning_effort,
|
|
987
|
+
verbosity,
|
|
988
|
+
use_websearch,
|
|
989
|
+
files,
|
|
990
|
+
images,
|
|
991
|
+
},
|
|
992
|
+
);
|
|
983
993
|
}
|
|
984
994
|
|
|
985
995
|
// Return complete result for job completion
|
|
@@ -1019,7 +1029,7 @@ chatTool.inputSchema = {
|
|
|
1019
1029
|
type: 'array',
|
|
1020
1030
|
items: { type: 'string' },
|
|
1021
1031
|
description:
|
|
1022
|
-
'File paths to include as context (absolute or relative paths). Example: ["
|
|
1032
|
+
'File paths to include as context (absolute or relative paths). Supports line ranges: file.txt{10:50}, file.txt{100:}. Example: ["./src/utils/auth.js{50:100}", "./config.json"]',
|
|
1023
1033
|
},
|
|
1024
1034
|
images: {
|
|
1025
1035
|
type: 'array',
|
package/src/tools/consensus.js
CHANGED
|
@@ -542,7 +542,9 @@ Please provide your refined response:`;
|
|
|
542
542
|
// Add summary at the end
|
|
543
543
|
formattedContent += `\n**Summary:** Consensus completed with ${initialPhase.successful.length} successful initial responses`;
|
|
544
544
|
if (refinedPhase) {
|
|
545
|
-
const successfulRefinements = refinedPhase.filter(
|
|
545
|
+
const successfulRefinements = refinedPhase.filter(
|
|
546
|
+
(r) => r.status === 'success',
|
|
547
|
+
).length;
|
|
546
548
|
formattedContent += ` and ${successfulRefinements} successful refined responses`;
|
|
547
549
|
}
|
|
548
550
|
formattedContent += '.';
|
|
@@ -752,7 +754,11 @@ function mapModelToProvider(model, providers) {
|
|
|
752
754
|
}
|
|
753
755
|
|
|
754
756
|
// Check Claude SDK (exact match only - routes to SDK provider instead of Anthropic API)
|
|
755
|
-
if (
|
|
757
|
+
if (
|
|
758
|
+
modelLower === 'claude' ||
|
|
759
|
+
modelLower === 'claude-sdk' ||
|
|
760
|
+
modelLower === 'claude-code'
|
|
761
|
+
) {
|
|
756
762
|
return 'claude';
|
|
757
763
|
}
|
|
758
764
|
|
|
@@ -1251,7 +1257,9 @@ Please provide your refined response:`;
|
|
|
1251
1257
|
// Add summary at the end
|
|
1252
1258
|
formattedContent += `\n**Summary:** Consensus completed with ${initialPhase.successful.length} successful initial responses`;
|
|
1253
1259
|
if (refinedPhase) {
|
|
1254
|
-
const successfulRefinements = refinedPhase.filter(
|
|
1260
|
+
const successfulRefinements = refinedPhase.filter(
|
|
1261
|
+
(r) => r.status === 'success',
|
|
1262
|
+
).length;
|
|
1255
1263
|
formattedContent += ` and ${successfulRefinements} successful refined responses`;
|
|
1256
1264
|
}
|
|
1257
1265
|
formattedContent += '.';
|
|
@@ -1623,7 +1631,7 @@ consensusTool.inputSchema = {
|
|
|
1623
1631
|
type: 'array',
|
|
1624
1632
|
items: { type: 'string' },
|
|
1625
1633
|
description:
|
|
1626
|
-
'File paths for additional context (absolute or relative paths). Example: ["
|
|
1634
|
+
'File paths for additional context (absolute or relative paths). Supports line ranges: file.txt{10:50}, file.txt{100:}. Example: ["./docs/architecture.md{1:100}", "./requirements.txt"]',
|
|
1627
1635
|
},
|
|
1628
1636
|
images: {
|
|
1629
1637
|
type: 'array',
|
|
@@ -11,6 +11,11 @@ import { extname, resolve, isAbsolute } from 'path';
|
|
|
11
11
|
import { constants } from 'fs';
|
|
12
12
|
import { fileURLToPath } from 'url';
|
|
13
13
|
import { dirname } from 'path';
|
|
14
|
+
import {
|
|
15
|
+
parseFilePathWithRange,
|
|
16
|
+
extractLineRange,
|
|
17
|
+
validateRange,
|
|
18
|
+
} from './pathParser.js';
|
|
14
19
|
|
|
15
20
|
// Security: Define allowed directories for file access
|
|
16
21
|
const __filename = fileURLToPath(import.meta.url);
|
|
@@ -103,8 +108,6 @@ async function validateFilePath(filePath, options = {}) {
|
|
|
103
108
|
* @param {string} filePath - Absolute or relative path to the file
|
|
104
109
|
* @param {object} options - Processing options
|
|
105
110
|
* @param {string[]} options.allowedDirectories - Allowed directories for security
|
|
106
|
-
* @param {number} options.maxTextSize - Maximum text file size in bytes
|
|
107
|
-
* @param {number} options.maxImageSize - Maximum image file size in bytes
|
|
108
111
|
* @param {boolean} options.skipSecurityCheck - Skip security validation (for testing)
|
|
109
112
|
* @returns {Promise<object>} Processed content with metadata
|
|
110
113
|
*/
|
|
@@ -137,8 +140,21 @@ export async function processFileContent(filePath, options = {}) {
|
|
|
137
140
|
};
|
|
138
141
|
}
|
|
139
142
|
|
|
140
|
-
//
|
|
141
|
-
const
|
|
143
|
+
// Parse line range specifier from file path (e.g., "file.txt{10:50}")
|
|
144
|
+
const { filePath: actualPath, range } = parseFilePathWithRange(filePath);
|
|
145
|
+
|
|
146
|
+
// Validate range early (before expensive file operations)
|
|
147
|
+
const rangeValidation = validateRange(range);
|
|
148
|
+
if (!rangeValidation.valid) {
|
|
149
|
+
throw new ContextProcessorError(
|
|
150
|
+
rangeValidation.error,
|
|
151
|
+
rangeValidation.code,
|
|
152
|
+
{ path: filePath },
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Security validation for file paths (without range specifier)
|
|
157
|
+
const validatedPath = await validateFilePath(actualPath, options);
|
|
142
158
|
|
|
143
159
|
const fileStats = await stat(validatedPath);
|
|
144
160
|
const extension = extname(validatedPath).toLowerCase();
|
|
@@ -162,18 +178,9 @@ export async function processFileContent(filePath, options = {}) {
|
|
|
162
178
|
encoding: null,
|
|
163
179
|
};
|
|
164
180
|
|
|
165
|
-
// Check file size limits
|
|
166
|
-
const maxTextSize = options.maxTextSize || 1024 * 1024; // 1MB default
|
|
167
|
-
const maxImageSize = options.maxImageSize || 10 * 1024 * 1024; // 10MB default
|
|
168
|
-
|
|
169
181
|
if (SUPPORTED_IMAGE_EXTENSIONS.includes(extension)) {
|
|
170
182
|
result.type = 'image';
|
|
171
183
|
|
|
172
|
-
if (fileStats.size > maxImageSize) {
|
|
173
|
-
result.error = `Image too large (${fileStats.size} bytes, max ${maxImageSize})`;
|
|
174
|
-
return result;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
184
|
// For images, read as base64 for AI processing
|
|
178
185
|
const buffer = await readFile(validatedPath);
|
|
179
186
|
result.content = buffer.toString('base64');
|
|
@@ -183,16 +190,24 @@ export async function processFileContent(filePath, options = {}) {
|
|
|
183
190
|
// Read everything else as text
|
|
184
191
|
result.type = 'text';
|
|
185
192
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
193
|
+
const rawContent = await readFile(validatedPath, 'utf8');
|
|
194
|
+
const allLines = rawContent.split(/\r?\n/);
|
|
195
|
+
result.totalLineCount = allLines.length;
|
|
196
|
+
|
|
197
|
+
// Apply line range extraction if specified
|
|
198
|
+
if (range) {
|
|
199
|
+
const extraction = extractLineRange(allLines, range);
|
|
200
|
+
result.content = extraction.lines.join('\n');
|
|
201
|
+
result.lineCount = extraction.lines.length;
|
|
202
|
+
result.rangeStart = extraction.actualStart;
|
|
203
|
+
result.rangeEnd = extraction.actualEnd;
|
|
204
|
+
} else {
|
|
205
|
+
result.content = rawContent;
|
|
206
|
+
result.lineCount = allLines.length;
|
|
189
207
|
}
|
|
190
208
|
|
|
191
|
-
const content = await readFile(validatedPath, 'utf8');
|
|
192
|
-
result.content = content;
|
|
193
|
-
result.lineCount = content.split(/\r?\n/).length;
|
|
194
209
|
result.encoding = 'utf8';
|
|
195
|
-
result.charCount = content.length;
|
|
210
|
+
result.charCount = result.content.length;
|
|
196
211
|
}
|
|
197
212
|
|
|
198
213
|
return result;
|
|
@@ -350,7 +365,12 @@ export function createFileContext(processedFiles, options = {}) {
|
|
|
350
365
|
if (textFiles.length > 0) {
|
|
351
366
|
contextText += '=== FILE CONTEXT ===\n\n';
|
|
352
367
|
for (const file of textFiles) {
|
|
353
|
-
|
|
368
|
+
// Show range info if this is a partial file extraction
|
|
369
|
+
let header = file.originalPath || file.path;
|
|
370
|
+
if (file.rangeStart !== undefined && file.totalLineCount !== undefined) {
|
|
371
|
+
header += ` (lines ${file.rangeStart}-${file.rangeEnd} of ${file.totalLineCount})`;
|
|
372
|
+
}
|
|
373
|
+
contextText += `--- ${header} ---\n`;
|
|
354
374
|
if (options.includeMetadata) {
|
|
355
375
|
contextText += `Size: ${file.size} bytes, Lines: ${file.lineCount || 'N/A'}\n`;
|
|
356
376
|
contextText += `Last Modified: ${file.lastModified || 'N/A'}\n`;
|
|
@@ -150,7 +150,9 @@ function generateMetadata(conversationState, totalTurns, params) {
|
|
|
150
150
|
created_at: conversationState.createdAt
|
|
151
151
|
? new Date(conversationState.createdAt).toISOString()
|
|
152
152
|
: new Date().toISOString(),
|
|
153
|
-
last_updated: new Date(
|
|
153
|
+
last_updated: new Date(
|
|
154
|
+
conversationState.lastUpdated || Date.now(),
|
|
155
|
+
).toISOString(),
|
|
154
156
|
};
|
|
155
157
|
|
|
156
158
|
// Add optional parameters if present
|
|
@@ -168,8 +170,8 @@ function generateMetadata(conversationState, totalTurns, params) {
|
|
|
168
170
|
}
|
|
169
171
|
if (params.images && params.images.length > 0) {
|
|
170
172
|
// Don't store base64 data, just file paths or indicators
|
|
171
|
-
metadata.images = params.images.map(img =>
|
|
172
|
-
img.startsWith('data:') ? '[base64 image]' : img
|
|
173
|
+
metadata.images = params.images.map((img) =>
|
|
174
|
+
img.startsWith('data:') ? '[base64 image]' : img,
|
|
173
175
|
);
|
|
174
176
|
}
|
|
175
177
|
// Consensus-specific metadata
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import { access, constants } from 'fs/promises';
|
|
9
9
|
import { resolve, isAbsolute } from 'path';
|
|
10
10
|
import { createToolError } from '../tools/index.js';
|
|
11
|
+
import { parseFilePathWithRange } from './pathParser.js';
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
14
|
* Validate that all provided file paths exist
|
|
@@ -38,11 +39,15 @@ export async function validateFilePaths(
|
|
|
38
39
|
continue;
|
|
39
40
|
}
|
|
40
41
|
|
|
42
|
+
// Parse and strip line range specifier before validation
|
|
43
|
+
// e.g., "file.txt{5:10}" -> "file.txt"
|
|
44
|
+
const { filePath: actualPath } = parseFilePathWithRange(filePath);
|
|
45
|
+
|
|
41
46
|
// Convert to absolute path if needed
|
|
42
47
|
// Use clientCwd if provided (for auto-detected client working directory), otherwise fall back to process.cwd()
|
|
43
|
-
const absolutePath = isAbsolute(
|
|
44
|
-
?
|
|
45
|
-
: resolve(options.clientCwd || process.cwd(),
|
|
48
|
+
const absolutePath = isAbsolute(actualPath)
|
|
49
|
+
? actualPath
|
|
50
|
+
: resolve(options.clientCwd || process.cwd(), actualPath);
|
|
46
51
|
|
|
47
52
|
try {
|
|
48
53
|
// Check if file exists and is readable
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Path Parser Utility
|
|
3
|
+
*
|
|
4
|
+
* Parses file paths with optional line range specifiers.
|
|
5
|
+
* Syntax: path/to/file{start:end} where start and end are 1-based line numbers.
|
|
6
|
+
*
|
|
7
|
+
* Examples:
|
|
8
|
+
* - file.txt{10:50} - Lines 10-50 inclusive
|
|
9
|
+
* - file.txt{:20} - Lines 1-20
|
|
10
|
+
* - file.txt{100:} - Lines 100 to end
|
|
11
|
+
* - file.txt - Entire file (no range)
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Regex to match line range specifier at end of path.
|
|
16
|
+
* Captures: (filePath)(startLine)(endLine)
|
|
17
|
+
* Pattern: ^(.*)\{(\d*):(\d*)\}$
|
|
18
|
+
*/
|
|
19
|
+
const RANGE_PATTERN = /^(.*)\{(\d*):(\d*)\}$/;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Parse a file path that may contain a line range specifier.
|
|
23
|
+
*
|
|
24
|
+
* @param {string} filePath - File path potentially with range specifier
|
|
25
|
+
* @returns {{filePath: string, range: {start: number|null, end: number|null, isEmpty: boolean}|null}}
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* parseFilePathWithRange('file.txt{10:50}')
|
|
29
|
+
* // => { filePath: 'file.txt', range: { start: 10, end: 50, isEmpty: false } }
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* parseFilePathWithRange('file.txt{:20}')
|
|
33
|
+
* // => { filePath: 'file.txt', range: { start: null, end: 20, isEmpty: false } }
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* parseFilePathWithRange('file.txt')
|
|
37
|
+
* // => { filePath: 'file.txt', range: null }
|
|
38
|
+
*/
|
|
39
|
+
export function parseFilePathWithRange(filePath) {
|
|
40
|
+
if (!filePath || typeof filePath !== 'string') {
|
|
41
|
+
return { filePath, range: null };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const match = filePath.match(RANGE_PATTERN);
|
|
45
|
+
|
|
46
|
+
if (!match) {
|
|
47
|
+
// No range specifier found, return original path
|
|
48
|
+
return { filePath, range: null };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const [, extractedPath, startStr, endStr] = match;
|
|
52
|
+
|
|
53
|
+
// Check for empty range {:}
|
|
54
|
+
if (startStr === '' && endStr === '') {
|
|
55
|
+
return {
|
|
56
|
+
filePath: extractedPath,
|
|
57
|
+
range: { start: null, end: null, isEmpty: true },
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Parse start line (treat 0 as 1)
|
|
62
|
+
let start = null;
|
|
63
|
+
if (startStr !== '') {
|
|
64
|
+
start = parseInt(startStr, 10);
|
|
65
|
+
if (start === 0) {
|
|
66
|
+
start = 1;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Parse end line
|
|
71
|
+
let end = null;
|
|
72
|
+
if (endStr !== '') {
|
|
73
|
+
end = parseInt(endStr, 10);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
filePath: extractedPath,
|
|
78
|
+
range: { start, end, isEmpty: false },
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Extract a range of lines from an array of lines.
|
|
84
|
+
* Both start and end are 1-indexed and inclusive.
|
|
85
|
+
*
|
|
86
|
+
* @param {string[]} lines - Array of lines
|
|
87
|
+
* @param {{start: number|null, end: number|null, isEmpty: boolean}} range - Range specification
|
|
88
|
+
* @returns {{lines: string[], actualStart: number, actualEnd: number}}
|
|
89
|
+
*
|
|
90
|
+
* @example
|
|
91
|
+
* extractLineRange(['a', 'b', 'c', 'd', 'e'], { start: 2, end: 4, isEmpty: false })
|
|
92
|
+
* // => { lines: ['b', 'c', 'd'], actualStart: 2, actualEnd: 4 }
|
|
93
|
+
*/
|
|
94
|
+
export function extractLineRange(lines, range) {
|
|
95
|
+
if (!range || !Array.isArray(lines)) {
|
|
96
|
+
return {
|
|
97
|
+
lines: lines || [],
|
|
98
|
+
actualStart: 1,
|
|
99
|
+
actualEnd: lines ? lines.length : 0,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const totalLines = lines.length;
|
|
104
|
+
|
|
105
|
+
// Determine start index (1-indexed to 0-indexed conversion)
|
|
106
|
+
// Default to 1 if not specified
|
|
107
|
+
let startLine = range.start !== null ? range.start : 1;
|
|
108
|
+
|
|
109
|
+
// Clamp start to valid range (at least 1)
|
|
110
|
+
if (startLine < 1) {
|
|
111
|
+
startLine = 1;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Determine end index (inclusive)
|
|
115
|
+
// Default to total lines if not specified
|
|
116
|
+
let endLine = range.end !== null ? range.end : totalLines;
|
|
117
|
+
|
|
118
|
+
// Clamp end to actual file bounds
|
|
119
|
+
if (endLine > totalLines) {
|
|
120
|
+
endLine = totalLines;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Handle case where start > total lines (return empty)
|
|
124
|
+
if (startLine > totalLines) {
|
|
125
|
+
return {
|
|
126
|
+
lines: [],
|
|
127
|
+
actualStart: startLine,
|
|
128
|
+
actualEnd: endLine,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Convert to 0-indexed for slice
|
|
133
|
+
// slice(start, end) is exclusive of end, so we use endLine (not endLine - 1)
|
|
134
|
+
const extractedLines = lines.slice(startLine - 1, endLine);
|
|
135
|
+
|
|
136
|
+
return {
|
|
137
|
+
lines: extractedLines,
|
|
138
|
+
actualStart: startLine,
|
|
139
|
+
actualEnd: Math.min(endLine, totalLines),
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Validate a line range.
|
|
145
|
+
*
|
|
146
|
+
* @param {{start: number|null, end: number|null, isEmpty: boolean}} range - Range to validate
|
|
147
|
+
* @returns {{valid: boolean, error: string|null, code: string|null}}
|
|
148
|
+
*/
|
|
149
|
+
export function validateRange(range) {
|
|
150
|
+
if (!range) {
|
|
151
|
+
return { valid: true, error: null, code: null };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Empty range {:} is invalid
|
|
155
|
+
if (range.isEmpty) {
|
|
156
|
+
return {
|
|
157
|
+
valid: false,
|
|
158
|
+
error: 'Empty range specifier {:} is not allowed',
|
|
159
|
+
code: 'EMPTY_RANGE',
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Check if start > end (when both are specified)
|
|
164
|
+
if (range.start !== null && range.end !== null && range.start > range.end) {
|
|
165
|
+
return {
|
|
166
|
+
valid: false,
|
|
167
|
+
error: `Invalid range: start (${range.start}) is greater than end (${range.end})`,
|
|
168
|
+
code: 'INVALID_RANGE',
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return { valid: true, error: null, code: null };
|
|
173
|
+
}
|