@@ -16,6 +16,9 @@ import { diffLines, createTwoFilesPatch } from 'diff';
16
16
import { minimatch } from 'minimatch' ;
17
17
import { simpleGit , SimpleGit } from 'simple-git' ;
18
18
19
+ // Import state utilities
20
+ import { getState , saveState , hasValidatedInPrompt , markValidatedInPrompt , resetValidationState } from './state-utils.js' ;
21
+
19
22
// Command line argument parsing
20
23
const args = process . argv . slice ( 2 ) ;
21
24
@@ -135,14 +138,15 @@ async function isGitClean(filePath: string): Promise<{isRepo: boolean, isClean:
135
138
// Check if the Git status allows modification
136
139
// With the One-Check-Per-Prompt approach, validation is only performed
137
140
// on the first file operation in each prompt and skipped for subsequent operations
138
- async function validateGitStatus ( filePath : string ) : Promise < void > {
141
+ async function validateGitStatus ( filePath : string , promptId ?: string ) : Promise < void > {
139
142
if ( ! gitConfig . requireCleanBranch ) {
140
143
return ; // Git validation is disabled
141
144
}
142
145
143
146
// Skip if we've already checked in this prompt
144
- if ( gitConfig . checkedThisPrompt ) {
145
- console . log ( `Skipping validation for ${ filePath } - already checked` ) ;
147
+ const hasValidated = await hasValidatedInPrompt ( promptId ) ;
148
+ if ( hasValidated ) {
149
+ console . log ( 'Skipping validation for ' + filePath + ' - already validated in this prompt' ) ;
146
150
return ;
147
151
}
148
152
@@ -151,36 +155,27 @@ async function validateGitStatus(filePath: string): Promise<void> {
151
155
// When requireCleanBranch is set, we require the file to be in a Git repository
152
156
if ( ! isRepo ) {
153
157
throw new Error (
154
- ` The file ${ filePath } is not in a Git repository. ` +
155
- ` This server is configured to require files to be in Git repositories with clean branches.`
156
- ) ;
158
+ " The file " + filePath + " is not in a Git repository. " +
159
+ " This server is configured to require files to be in Git repositories with clean branches."
160
+ ) ;
157
161
}
158
162
159
163
// And we require the repository to be clean
160
164
if ( ! isClean ) {
161
- throw new Error (
162
- ` Git repository at ${ repoPath } has uncommitted changes. ` +
163
- ` This server is configured to require a clean branch before allowing changes.`
164
- ) ;
165
+ throw new Error (
166
+ " Git repository at " + repoPath + " has uncommitted changes. " +
167
+ " This server is configured to require a clean branch before allowing changes."
168
+ ) ;
165
169
}
166
170
167
171
// Mark that we've checked in this prompt
168
- gitConfig . checkedThisPrompt = true ;
169
- console . log ( `Validation passed for ${ filePath } - marked as checked` ) ;
170
- }
171
-
172
- // Reset the validation state
173
- // This function is called after each tool request to reset the validation state
174
- // so that the next prompt will perform a fresh validation
175
- function resetValidationState ( ) : void {
176
- if ( gitConfig . checkedThisPrompt ) {
177
- gitConfig . checkedThisPrompt = false ;
178
- console . log ( "State reset for next prompt" ) ;
179
- }
172
+ await markValidatedInPrompt ( promptId ) ;
173
+ console . log ( 'Validation passed for ' + filePath + ' - marked as checked' ) ;
180
174
}
181
175
176
+ // Git validation utilities
182
177
// Security utilities
183
- async function validatePath ( requestedPath : string , skipGitCheck : boolean = false ) : Promise < string > {
178
+ async function validatePath ( requestedPath : string , skipGitCheck : boolean = false , promptId ?: string ) : Promise < string > {
184
179
const expandedPath = expandHome ( requestedPath ) ;
185
180
const absolute = path . isAbsolute ( expandedPath )
186
181
? path . resolve ( expandedPath )
@@ -205,7 +200,7 @@ async function validatePath(requestedPath: string, skipGitCheck: boolean = false
205
200
206
201
// Perform Git validation if required
207
202
if ( ! skipGitCheck && gitConfig . requireCleanBranch ) {
208
- await validateGitStatus ( realPath ) ;
203
+ await validateGitStatus ( realPath , promptId ) ;
209
204
}
210
205
211
206
return realPath ;
@@ -222,7 +217,7 @@ async function validatePath(requestedPath: string, skipGitCheck: boolean = false
222
217
223
218
// Perform Git validation on the parent directory for new files
224
219
if ( ! skipGitCheck && gitConfig . requireCleanBranch ) {
225
- await validateGitStatus ( parentDir ) ;
220
+ await validateGitStatus ( parentDir , promptId ) ;
226
221
}
227
222
228
223
return absolute ;
@@ -334,7 +329,8 @@ async function getFileStats(filePath: string): Promise<FileInfo> {
334
329
async function searchFiles (
335
330
rootPath : string ,
336
331
pattern : string ,
337
- excludePatterns : string [ ] = [ ]
332
+ excludePatterns : string [ ] = [ ] ,
333
+ promptId ?: string
338
334
) : Promise < string [ ] > {
339
335
const results : string [ ] = [ ] ;
340
336
@@ -346,7 +342,7 @@ async function searchFiles(
346
342
347
343
try {
348
344
// Validate each path before processing (skip Git check for search)
349
- await validatePath ( fullPath , true ) ;
345
+ await validatePath ( fullPath , true , promptId ) ;
350
346
351
347
// Check if path matches any exclude pattern
352
348
const relativePath = path . relative ( rootPath , fullPath ) ;
@@ -596,6 +592,10 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
596
592
server . setRequestHandler ( CallToolRequestSchema , async ( request ) => {
597
593
try {
598
594
const { name, arguments : args } = request . params ;
595
+
596
+ // Generate a unique prompt ID for this request
597
+ const promptId = 'prompt-' + Date . now ( ) + '-' + Math . floor ( Math . random ( ) * 10000 ) ;
598
+ console . log ( 'Processing request ' + name + ' with promptId: ' + promptId ) ;
599
599
600
600
// Get the response from the appropriate tool handler
601
601
let response ;
@@ -606,7 +606,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
606
606
if ( ! parsed . success ) {
607
607
throw new Error ( `Invalid arguments for read_file: ${ parsed . error } ` ) ;
608
608
}
609
- const validPath = await validatePath ( parsed . data . path , true ) ; // Skip Git check for read-only operation
609
+ const validPath = await validatePath ( parsed . data . path , true , promptId ) ; // Skip Git check for read-only operation
610
610
const content = await fs . readFile ( validPath , "utf-8" ) ;
611
611
response = {
612
612
content : [ { type : "text" , text : content } ] ,
@@ -622,7 +622,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
622
622
const results = await Promise . all (
623
623
parsed . data . paths . map ( async ( filePath : string ) => {
624
624
try {
625
- const validPath = await validatePath ( filePath , true ) ; // Skip Git check for read-only operation
625
+ const validPath = await validatePath ( filePath , true , promptId ) ; // Skip Git check for read-only operation
626
626
const content = await fs . readFile ( validPath , "utf-8" ) ;
627
627
return `${ filePath } :\n${ content } \n` ;
628
628
} catch ( error ) {
@@ -642,7 +642,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
642
642
if ( ! parsed . success ) {
643
643
throw new Error ( `Invalid arguments for write_file: ${ parsed . error } ` ) ;
644
644
}
645
- const validPath = await validatePath ( parsed . data . path ) ; // Git check is now performed in validatePath
645
+ const validPath = await validatePath ( parsed . data . path , false , promptId ) ; // Git check is now performed in validatePath
646
646
647
647
await fs . writeFile ( validPath , parsed . data . content , "utf-8" ) ;
648
648
response = {
@@ -658,7 +658,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
658
658
}
659
659
660
660
// If this is a dry run, skip Git check
661
- const validPath = await validatePath ( parsed . data . path , parsed . data . dryRun ) ;
661
+ const validPath = await validatePath ( parsed . data . path , parsed . data . dryRun , promptId ) ;
662
662
663
663
const result = await applyFileEdits ( validPath , parsed . data . edits , parsed . data . dryRun ) ;
664
664
response = {
@@ -672,7 +672,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
672
672
if ( ! parsed . success ) {
673
673
throw new Error ( `Invalid arguments for create_directory: ${ parsed . error } ` ) ;
674
674
}
675
- const validPath = await validatePath ( parsed . data . path ) ; // Git check is now performed in validatePath
675
+ const validPath = await validatePath ( parsed . data . path , false , promptId ) ; // Git check is now performed in validatePath
676
676
677
677
await fs . mkdir ( validPath , { recursive : true } ) ;
678
678
response = {
@@ -686,7 +686,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
686
686
if ( ! parsed . success ) {
687
687
throw new Error ( `Invalid arguments for list_directory: ${ parsed . error } ` ) ;
688
688
}
689
- const validPath = await validatePath ( parsed . data . path , true ) ; // Skip Git check for read-only operation
689
+ const validPath = await validatePath ( parsed . data . path , true , promptId ) ; // Skip Git check for read-only operation
690
690
const entries = await fs . readdir ( validPath , { withFileTypes : true } ) ;
691
691
const formatted = entries
692
692
. map ( ( entry ) => `${ entry . isDirectory ( ) ? "[DIR]" : "[FILE]" } ${ entry . name } ` )
@@ -710,7 +710,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
710
710
}
711
711
712
712
async function buildTree ( currentPath : string ) : Promise < TreeEntry [ ] > {
713
- const validPath = await validatePath ( currentPath , true ) ; // Skip Git check for read-only operation
713
+ const validPath = await validatePath ( currentPath , true , promptId ) ; // Skip Git check for read-only operation
714
714
const entries = await fs . readdir ( validPath , { withFileTypes : true } ) ;
715
715
const result : TreeEntry [ ] = [ ] ;
716
716
@@ -746,8 +746,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
746
746
if ( ! parsed . success ) {
747
747
throw new Error ( `Invalid arguments for move_file: ${ parsed . error } ` ) ;
748
748
}
749
- const validSourcePath = await validatePath ( parsed . data . source ) ; // Git check is now performed in validatePath
750
- const validDestPath = await validatePath ( parsed . data . destination ) ; // Git check is now performed in validatePath
749
+ const validSourcePath = await validatePath ( parsed . data . source , false , promptId ) ; // Git check is now performed in validatePath
750
+ const validDestPath = await validatePath ( parsed . data . destination , false , promptId ) ; // Git check is now performed in validatePath
751
751
752
752
await fs . rename ( validSourcePath , validDestPath ) ;
753
753
response = {
@@ -761,8 +761,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
761
761
if ( ! parsed . success ) {
762
762
throw new Error ( `Invalid arguments for search_files: ${ parsed . error } ` ) ;
763
763
}
764
- const validPath = await validatePath ( parsed . data . path , true ) ; // Skip Git check for read-only operation
765
- const results = await searchFiles ( validPath , parsed . data . pattern , parsed . data . excludePatterns ) ;
764
+ const validPath = await validatePath ( parsed . data . path , true , promptId ) ; // Skip Git check for read-only operation
765
+ const results = await searchFiles ( validPath , parsed . data . pattern , parsed . data . excludePatterns , promptId ) ;
766
766
response = {
767
767
content : [ { type : "text" , text : results . length > 0 ? results . join ( "\n" ) : "No matches found" } ] ,
768
768
} ;
@@ -774,7 +774,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
774
774
if ( ! parsed . success ) {
775
775
throw new Error ( `Invalid arguments for get_file_info: ${ parsed . error } ` ) ;
776
776
}
777
- const validPath = await validatePath ( parsed . data . path , true ) ; // Skip Git check for read-only operation
777
+ const validPath = await validatePath ( parsed . data . path , true , promptId ) ; // Skip Git check for read-only operation
778
778
const info = await getFileStats ( validPath ) ;
779
779
response = {
780
780
content : [ { type : "text" , text : Object . entries ( info )
@@ -799,7 +799,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
799
799
if ( ! parsed . success ) {
800
800
throw new Error ( `Invalid arguments for git_status: ${ parsed . error } ` ) ;
801
801
}
802
- const validPath = await validatePath ( parsed . data . path , true ) ; // Skip Git check for read-only operation
802
+ const validPath = await validatePath ( parsed . data . path , true , promptId ) ; // Skip Git check for read-only operation
803
803
804
804
// Get Git status information
805
805
const gitStatus = await isGitClean ( validPath ) ;
@@ -842,12 +842,12 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
842
842
}
843
843
844
844
// Reset validation state after successful response
845
- resetValidationState ( ) ;
845
+ await resetValidationState ( ) ;
846
846
847
847
return response ;
848
848
} catch ( error ) {
849
849
// Reset validation state even on error
850
- resetValidationState ( ) ;
850
+ await resetValidationState ( ) ;
851
851
852
852
const errorMessage = error instanceof Error ? error . message : String ( error ) ;
853
853
return {
0 commit comments