@@ -12,11 +12,27 @@ const require = createRequire(import.meta.url)
1212const protoc = path . join ( require . resolve ( "grpc-tools" ) , "../bin/protoc" )
1313const tsProtoPlugin = require . resolve ( "ts-proto/protoc-gen-ts_proto" )
1414
15- // Get script directory and root directory
1615const __filename = fileURLToPath ( import . meta. url )
1716const SCRIPT_DIR = path . dirname ( __filename )
1817const ROOT_DIR = path . resolve ( SCRIPT_DIR , ".." )
1918
19+ // List of gRPC services
20+ // To add a new service, simply add it to this map and run this script
21+ // The service handler will be automatically discovered and used by grpc-handler.ts
22+ const serviceNameMap = {
23+ account : "cline.AccountService" ,
24+ browser : "cline.BrowserService" ,
25+ checkpoints : "cline.CheckpointsService" ,
26+ file : "cline.FileService" ,
27+ mcp : "cline.McpService" ,
28+ state : "cline.StateService" ,
29+ task : "cline.TaskService" ,
30+ web : "cline.WebService" ,
31+ models : "cline.ModelsService" ,
32+ // Add new services here - no other code changes needed!
33+ }
34+ const serviceDirs = Object . keys ( serviceNameMap ) . map ( ( serviceKey ) => path . join ( ROOT_DIR , "src" , "core" , "controller" , serviceKey ) )
35+
2036async function main ( ) {
2137 console . log ( chalk . bold . blue ( "Starting Protocol Buffer code generation..." ) )
2238
@@ -33,6 +49,9 @@ async function main() {
3349 await fs . unlink ( path . join ( TS_OUT_DIR , file ) )
3450 }
3551
52+ // Check for missing proto files for services in serviceNameMap
53+ await ensureProtoFilesExist ( )
54+
3655 // Process all proto files
3756 console . log ( chalk . cyan ( "Processing proto files from" ) , SCRIPT_DIR )
3857 const protoFiles = await globby ( "*.proto" , { cwd : SCRIPT_DIR } )
@@ -64,42 +83,135 @@ async function main() {
6483 console . log ( chalk . green ( "Protocol Buffer code generation completed successfully." ) )
6584 console . log ( chalk . green ( `TypeScript files generated in: ${ TS_OUT_DIR } ` ) )
6685
67- // Generate method registration files
6886 await generateMethodRegistrations ( )
87+ await generateServiceConfig ( )
88+ await generateGrpcClientConfig ( )
89+ }
90+
91+ /**
92+ * Generate a gRPC client configuration file for the webview
93+ * This eliminates the need for manual imports and client creation in grpc-client.ts
94+ */
95+ async function generateGrpcClientConfig ( ) {
96+ console . log ( chalk . cyan ( "Generating gRPC client configuration..." ) )
97+
98+ const serviceImports = [ ]
99+ const serviceClientCreations = [ ]
100+ const serviceExports = [ ]
101+
102+ // Process each service in the serviceNameMap
103+ for ( const [ dirName , fullServiceName ] of Object . entries ( serviceNameMap ) ) {
104+ const capitalizedName = dirName . charAt ( 0 ) . toUpperCase ( ) + dirName . slice ( 1 )
105+
106+ // Add import statement
107+ serviceImports . push ( `import { ${ capitalizedName } ServiceDefinition } from "@shared/proto/${ dirName } "` )
108+
109+ // Add client creation
110+ serviceClientCreations . push (
111+ `const ${ capitalizedName } ServiceClient = createGrpcClient(${ capitalizedName } ServiceDefinition)` ,
112+ )
113+
114+ // Add to exports
115+ serviceExports . push ( `${ capitalizedName } ServiceClient` )
116+ }
117+
118+ // Generate the file content
119+ const content = `// AUTO-GENERATED FILE - DO NOT MODIFY DIRECTLY
120+ // Generated by proto/build-proto.js
121+
122+ import { createGrpcClient } from "./grpc-client-base"
123+ ${ serviceImports . join ( "\n" ) }
124+
125+ ${ serviceClientCreations . join ( "\n" ) }
126+
127+ export {
128+ ${ serviceExports . join ( ",\n\t" ) }
129+ }`
130+
131+ const configPath = path . join ( ROOT_DIR , "webview-ui" , "src" , "services" , "grpc-client.ts" )
132+ await fs . writeFile ( configPath , content )
133+ console . log ( chalk . green ( `Generated gRPC client at ${ configPath } ` ) )
134+ }
69135
70- // Make the script executable
71- try {
72- await fs . chmod ( path . join ( SCRIPT_DIR , "build-proto.js" ) , 0o755 )
73- } catch ( error ) {
74- console . warn ( chalk . yellow ( "Warning: Could not make script executable:" ) , error )
136+ /**
137+ * Parse proto files to extract streaming method information
138+ * @param protoFiles Array of proto file names
139+ * @param scriptDir Directory containing proto files
140+ * @returns Map of service names to their streaming methods
141+ */
142+ async function parseProtoForStreamingMethods ( protoFiles , scriptDir ) {
143+ console . log ( chalk . cyan ( "Parsing proto files for streaming methods..." ) )
144+
145+ // Map of service name to array of streaming method names
146+ const streamingMethodsMap = new Map ( )
147+
148+ for ( const protoFile of protoFiles ) {
149+ const content = await fs . readFile ( path . join ( scriptDir , protoFile ) , "utf8" )
150+
151+ // Extract package name
152+ const packageMatch = content . match ( / p a c k a g e \s + ( [ ^ ; ] + ) ; / )
153+ const packageName = packageMatch ? packageMatch [ 1 ] . trim ( ) : "unknown"
154+
155+ // Extract service definitions
156+ const serviceMatches = Array . from ( content . matchAll ( / s e r v i c e \s + ( \w + ) \s * \{ ( [ ^ } ] + ) \} / g) )
157+ for ( const serviceMatch of serviceMatches ) {
158+ const serviceName = serviceMatch [ 1 ]
159+ const serviceBody = serviceMatch [ 2 ]
160+ const fullServiceName = `${ packageName } .${ serviceName } `
161+
162+ // Extract method definitions with streaming
163+ const methodMatches = Array . from (
164+ serviceBody . matchAll ( / r p c \s + ( \w + ) \s * \( \s * ( s t r e a m \s + ) ? ( \w + ) \s * \) \s * r e t u r n s \s * \( \s * ( s t r e a m \s + ) ? ( \w + ) \s * \) / g) ,
165+ )
166+
167+ const streamingMethods = [ ]
168+ for ( const methodMatch of methodMatches ) {
169+ const methodName = methodMatch [ 1 ]
170+ const isRequestStreaming = ! ! methodMatch [ 2 ]
171+ const requestType = methodMatch [ 3 ]
172+ const isResponseStreaming = ! ! methodMatch [ 4 ]
173+ const responseType = methodMatch [ 5 ]
174+
175+ if ( isResponseStreaming ) {
176+ streamingMethods . push ( {
177+ name : methodName ,
178+ requestType,
179+ responseType,
180+ isRequestStreaming,
181+ } )
182+ }
183+ }
184+
185+ if ( streamingMethods . length > 0 ) {
186+ streamingMethodsMap . set ( fullServiceName , streamingMethods )
187+ }
188+ }
75189 }
190+
191+ return streamingMethodsMap
76192}
77193
78194async function generateMethodRegistrations ( ) {
79195 console . log ( chalk . cyan ( "Generating method registration files..." ) )
80196
81- const serviceDirs = [
82- path . join ( ROOT_DIR , "src" , "core" , "controller" , "account" ) ,
83- path . join ( ROOT_DIR , "src" , "core" , "controller" , "browser" ) ,
84- path . join ( ROOT_DIR , "src" , "core" , "controller" , "checkpoints" ) ,
85- path . join ( ROOT_DIR , "src" , "core" , "controller" , "file" ) ,
86- path . join ( ROOT_DIR , "src" , "core" , "controller" , "mcp" ) ,
87- path . join ( ROOT_DIR , "src" , "core" , "controller" , "models" ) ,
88- path . join ( ROOT_DIR , "src" , "core" , "controller" , "task" ) ,
89- path . join ( ROOT_DIR , "src" , "core" , "controller" , "web-content" ) ,
90- // Add more service directories here as needed
91- ]
197+ // Parse proto files for streaming methods
198+ const protoFiles = await globby ( "*.proto" , { cwd : SCRIPT_DIR } )
199+ const streamingMethodsMap = await parseProtoForStreamingMethods ( protoFiles , SCRIPT_DIR )
92200
93201 for ( const serviceDir of serviceDirs ) {
94202 try {
95203 await fs . access ( serviceDir )
96204 } catch ( error ) {
97- console . log ( chalk . gray ( `Skipping ${ serviceDir } - directory does not exist `) )
98- continue
205+ console . log ( chalk . cyan ( `Creating directory ${ serviceDir } for new service `) )
206+ await fs . mkdir ( serviceDir , { recursive : true } )
99207 }
100208
101209 const serviceName = path . basename ( serviceDir )
102210 const registryFile = path . join ( serviceDir , "methods.ts" )
211+ const indexFile = path . join ( serviceDir , "index.ts" )
212+
213+ const fullServiceName = serviceNameMap [ serviceName ]
214+ const streamingMethods = streamingMethodsMap . get ( fullServiceName ) || [ ]
103215
104216 console . log ( chalk . cyan ( `Generating method registrations for ${ serviceName } ...` ) )
105217
@@ -109,8 +221,8 @@ async function generateMethodRegistrations() {
109221 // Filter out index.ts and methods.ts
110222 const implementationFiles = files . filter ( ( file ) => file !== "index.ts" && file !== "methods.ts" )
111223
112- // Create the output file with header
113- let content = `// AUTO-GENERATED FILE - DO NOT MODIFY DIRECTLY
224+ // Create the methods.ts file with header
225+ let methodsContent = `// AUTO-GENERATED FILE - DO NOT MODIFY DIRECTLY
114226// Generated by proto/build-proto.js
115227
116228// Import all method implementations
@@ -119,31 +231,177 @@ import { registerMethod } from "./index"\n`
119231 // Add imports for all implementation files
120232 for ( const file of implementationFiles ) {
121233 const baseName = path . basename ( file , ".ts" )
122- content += `import { ${ baseName } } from "./${ baseName } "\n`
234+ methodsContent += `import { ${ baseName } } from "./${ baseName } "\n`
235+ }
236+
237+ // Add streaming methods information
238+ if ( streamingMethods . length > 0 ) {
239+ methodsContent += `\n// Streaming methods for this service
240+ export const streamingMethods = ${ JSON . stringify (
241+ streamingMethods . map ( ( m ) => m . name ) ,
242+ null ,
243+ 2 ,
244+ ) } \n`
123245 }
124246
125247 // Add registration function
126- content += `\n// Register all ${ serviceName } service methods
248+ methodsContent += `\n// Register all ${ serviceName } service methods
127249export function registerAllMethods(): void {
128250\t// Register each method with the registry\n`
129251
130252 // Add registration statements
131253 for ( const file of implementationFiles ) {
132254 const baseName = path . basename ( file , ".ts" )
133- content += `\tregisterMethod("${ baseName } ", ${ baseName } )\n`
255+ const isStreaming = streamingMethods . some ( ( m ) => m . name === baseName )
256+
257+ if ( isStreaming ) {
258+ methodsContent += `\tregisterMethod("${ baseName } ", ${ baseName } , { isStreaming: true })\n`
259+ } else {
260+ methodsContent += `\tregisterMethod("${ baseName } ", ${ baseName } )\n`
261+ }
134262 }
135263
136264 // Close the function
137- content += `}`
265+ methodsContent += `}`
138266
139- // Write the file
140- await fs . writeFile ( registryFile , content )
267+ // Write the methods.ts file
268+ await fs . writeFile ( registryFile , methodsContent )
141269 console . log ( chalk . green ( `Generated ${ registryFile } ` ) )
270+
271+ // Generate index.ts file
272+ const capitalizedServiceName = serviceName . charAt ( 0 ) . toUpperCase ( ) + serviceName . slice ( 1 )
273+ const indexContent = `// AUTO-GENERATED FILE - DO NOT MODIFY DIRECTLY
274+ // Generated by proto/build-proto.js
275+
276+ import { createServiceRegistry, ServiceMethodHandler, StreamingMethodHandler } from "../grpc-service"
277+ import { StreamingResponseHandler } from "../grpc-handler"
278+ import { registerAllMethods } from "./methods"
279+
280+ // Create ${ serviceName } service registry
281+ const ${ serviceName } Service = createServiceRegistry("${ serviceName } ")
282+
283+ // Export the method handler types and registration function
284+ export type ${ capitalizedServiceName } MethodHandler = ServiceMethodHandler
285+ export type ${ capitalizedServiceName } StreamingMethodHandler = StreamingMethodHandler
286+ export const registerMethod = ${ serviceName } Service.registerMethod
287+
288+ // Export the request handlers
289+ export const handle${ capitalizedServiceName } ServiceRequest = ${ serviceName } Service.handleRequest
290+ export const handle${ capitalizedServiceName } ServiceStreamingRequest = ${ serviceName } Service.handleStreamingRequest
291+ export const isStreamingMethod = ${ serviceName } Service.isStreamingMethod
292+
293+ // Register all ${ serviceName } methods
294+ registerAllMethods()`
295+
296+ // Write the index.ts file
297+ await fs . writeFile ( indexFile , indexContent )
298+ console . log ( chalk . green ( `Generated ${ indexFile } ` ) )
142299 }
143300
144301 console . log ( chalk . green ( "Method registration files generated successfully." ) )
145302}
146303
304+ /**
305+ * Generate a service configuration file that maps service names to their handlers
306+ * This eliminates the need for manual switch/case statements in grpc-handler.ts
307+ */
308+ async function generateServiceConfig ( ) {
309+ console . log ( chalk . cyan ( "Generating service configuration file..." ) )
310+
311+ const serviceImports = [ ]
312+ const serviceConfigs = [ ]
313+
314+ // Add all services from the serviceNameMap
315+ for ( const [ dirName , fullServiceName ] of Object . entries ( serviceNameMap ) ) {
316+ const capitalizedName = dirName . charAt ( 0 ) . toUpperCase ( ) + dirName . slice ( 1 )
317+ serviceImports . push (
318+ `import { handle${ capitalizedName } ServiceRequest, handle${ capitalizedName } ServiceStreamingRequest } from "./${ dirName } /index"` ,
319+ )
320+ serviceConfigs . push ( `
321+ "${ fullServiceName } ": {
322+ requestHandler: handle${ capitalizedName } ServiceRequest,
323+ streamingHandler: handle${ capitalizedName } ServiceStreamingRequest
324+ }` )
325+ }
326+
327+ const content = `// AUTO-GENERATED FILE - DO NOT MODIFY DIRECTLY
328+ // Generated by proto/build-proto.js
329+
330+ import { Controller } from "./index"
331+ import { StreamingResponseHandler } from "./grpc-handler"
332+ ${ serviceImports . join ( "\n" ) }
333+
334+ /**
335+ * Configuration for a service handler
336+ */
337+ export interface ServiceHandlerConfig {
338+ requestHandler: (controller: Controller, method: string, message: any) => Promise<any>;
339+ streamingHandler: (controller: Controller, method: string, message: any, responseStream: StreamingResponseHandler, requestId?: string) => Promise<void>;
340+ }
341+
342+ /**
343+ * Map of service names to their handler configurations
344+ */
345+ export const serviceHandlers: Record<string, ServiceHandlerConfig> = {${ serviceConfigs . join ( "," ) }
346+ };`
347+
348+ const configPath = path . join ( ROOT_DIR , "src" , "core" , "controller" , "grpc-service-config.ts" )
349+ await fs . writeFile ( configPath , content )
350+ console . log ( chalk . green ( `Generated service configuration at ${ configPath } ` ) )
351+ }
352+
353+ /**
354+ * Ensure that a .proto file exists for each service in the serviceNameMap
355+ * If a .proto file doesn't exist, create a template file
356+ */
357+ async function ensureProtoFilesExist ( ) {
358+ console . log ( chalk . cyan ( "Checking for missing proto files..." ) )
359+
360+ // Get existing proto files
361+ const existingProtoFiles = await globby ( "*.proto" , { cwd : SCRIPT_DIR } )
362+ const existingProtoServices = existingProtoFiles . map ( ( file ) => path . basename ( file , ".proto" ) )
363+
364+ // Check each service in serviceNameMap
365+ for ( const [ serviceName , fullServiceName ] of Object . entries ( serviceNameMap ) ) {
366+ if ( ! existingProtoServices . includes ( serviceName ) ) {
367+ console . log ( chalk . yellow ( `Creating template proto file for ${ serviceName } ...` ) )
368+
369+ // Extract service class name from full name (e.g., "cline.ModelsService" -> "ModelsService")
370+ const serviceClassName = fullServiceName . split ( "." ) . pop ( )
371+
372+ // Create template proto file
373+ const protoContent = `syntax = "proto3";
374+
375+ package cline;
376+ option java_package = "bot.cline.proto";
377+ option java_multiple_files = true;
378+
379+ import "common.proto";
380+
381+ // ${ serviceClassName } provides methods for managing ${ serviceName }
382+ service ${ serviceClassName } {
383+ // Add your RPC methods here
384+ // Example (String is from common.proto, responses should be generic types):
385+ // rpc YourMethod(YourRequest) returns (String);
386+ }
387+
388+ // Add your message definitions here
389+ // Example (Requests must always start with Metadata):
390+ // message YourRequest {
391+ // Metadata metadata = 1;
392+ // string stringField = 2;
393+ // int32 int32Field = 3;
394+ // }
395+ `
396+
397+ // Write the template proto file
398+ const protoFilePath = path . join ( SCRIPT_DIR , `${ serviceName } .proto` )
399+ await fs . writeFile ( protoFilePath , protoContent )
400+ console . log ( chalk . green ( `Created template proto file at ${ protoFilePath } ` ) )
401+ }
402+ }
403+ }
404+
147405// Run the main function
148406main ( ) . catch ( ( error ) => {
149407 console . error ( chalk . red ( "Error:" ) , error )
0 commit comments