11#!/usr/bin/env node
22
33import { spawn } from 'child_process' ;
4- import { readFile } from 'fs/promises' ;
4+ import { readFile , writeFile , mkdir , access } from 'fs/promises' ;
55import { homedir } from 'os' ;
66import { join } from 'path' ;
7+ import { constants } from 'fs' ;
78
89const CONFIG_FILE = join ( homedir ( ) , '.copilot-auto-config.json' ) ;
10+ const TOOLS_DIR = 'tools' ;
11+ const MANIFEST_FILE = join ( TOOLS_DIR , 'manifest.json' ) ;
912
1013// Default configuration
1114const DEFAULT_CONFIG = {
@@ -17,16 +20,153 @@ const DEFAULT_CONFIG = {
1720 autoApprove : true ,
1821 allowedTools : [ ] ,
1922 deniedTools : [ ] ,
20- model : 'claude-sonnet-4.5'
23+ model : 'claude-sonnet-4.5' ,
24+ enableLocalToolSynthesis : false ,
25+ noMcp : false
2126} ;
2227
28+ // Tool Synthesis Module
29+ class ToolSynthesizer {
30+ constructor ( ) {
31+ this . manifestPath = MANIFEST_FILE ;
32+ }
33+
34+ // Validate and sanitize tool name
35+ validateToolName ( name ) {
36+ const sanitized = name . toLowerCase ( ) . replace ( / [ ^ a - z 0 - 9 - ] / g, '-' ) ;
37+ if ( ! / ^ [ a - z ] [ a - z 0 - 9 - ] * $ / . test ( sanitized ) ) {
38+ throw new Error ( `Invalid tool name: ${ name } ` ) ;
39+ }
40+ return sanitized ;
41+ }
42+
43+ async ensureToolsDirectory ( ) {
44+ try {
45+ await mkdir ( TOOLS_DIR , { recursive : true } ) ;
46+ } catch ( error ) {
47+ if ( error . code !== 'EEXIST' ) throw error ;
48+ }
49+ }
50+
51+ async loadManifest ( ) {
52+ try {
53+ await access ( this . manifestPath , constants . F_OK ) ;
54+ const data = await readFile ( this . manifestPath , 'utf8' ) ;
55+ return JSON . parse ( data ) ;
56+ } catch ( error ) {
57+ return {
58+ version : '1.0.0' ,
59+ tools : { }
60+ } ;
61+ }
62+ }
63+
64+ async saveManifest ( manifest ) {
65+ await writeFile ( this . manifestPath , JSON . stringify ( manifest , null , 2 ) , 'utf8' ) ;
66+ }
67+
68+ async toolExists ( toolName ) {
69+ const manifest = await this . loadManifest ( ) ;
70+ return toolName in manifest . tools ;
71+ }
72+
73+ async scaffoldTool ( toolName , description = 'Auto-generated tool scaffold' ) {
74+ await this . ensureToolsDirectory ( ) ;
75+
76+ const sanitizedName = this . validateToolName ( toolName ) ;
77+
78+ // Check if tool already exists
79+ if ( await this . toolExists ( sanitizedName ) ) {
80+ console . log ( `ℹ Tool '${ sanitizedName } ' already exists. Skipping scaffold.` ) ;
81+ return sanitizedName ;
82+ }
83+
84+ const toolDir = join ( TOOLS_DIR , sanitizedName ) ;
85+ const toolSrcDir = join ( toolDir , 'src' ) ;
86+
87+ // Create directories
88+ await mkdir ( toolDir , { recursive : true } ) ;
89+ await mkdir ( toolSrcDir , { recursive : true } ) ;
90+
91+ // Create tool.md specification
92+ const toolMdContent = `# Tool: ${ sanitizedName }
93+
94+ ## Status
95+ **Scaffold** - Implementation needed
96+
97+ ## Purpose
98+ ${ description }
99+
100+ ## Parameters
101+ [Expected input parameters and their types]
102+
103+ ## Implementation Notes
104+ This is a scaffolded tool. To complete implementation:
105+ 1. Edit \`src/index.js\` with actual functionality
106+ 2. Add tests in \`tests/\` directory
107+ 3. Update status in \`tools/manifest.json\` to "implemented"
108+ 4. Test thoroughly before use
109+
110+ ## Usage Example
111+ [How to invoke this tool once implemented]
112+
113+ Created: ${ new Date ( ) . toISOString ( ) }
114+ ` ;
115+ await writeFile ( join ( toolDir , 'tool.md' ) , toolMdContent , 'utf8' ) ;
116+
117+ // Create stub implementation
118+ const stubContent = `#!/usr/bin/env node
119+
120+ /**
121+ * Auto-generated tool scaffold
122+ * Tool: ${ sanitizedName }
123+ * Created: ${ new Date ( ) . toISOString ( ) }
124+ *
125+ * TODO: Implement actual functionality
126+ */
127+
128+ console.error('Tool scaffold created locally. Implement me.');
129+ console.error('Edit: tools/${ sanitizedName } /src/index.js');
130+ process.exit(1);
131+ ` ;
132+ await writeFile ( join ( toolSrcDir , 'index.js' ) , stubContent , 'utf8' ) ;
133+
134+ // Update manifest
135+ const manifest = await this . loadManifest ( ) ;
136+ manifest . tools [ sanitizedName ] = {
137+ name : sanitizedName ,
138+ description : description ,
139+ command : 'node' ,
140+ args : [ `tools/${ sanitizedName } /src/index.js` ] ,
141+ schema : {
142+ parameters : {
143+ type : 'object' ,
144+ properties : { } ,
145+ required : [ ]
146+ }
147+ } ,
148+ createdAt : new Date ( ) . toISOString ( ) ,
149+ status : 'scaffold'
150+ } ;
151+ await this . saveManifest ( manifest ) ;
152+
153+ console . log ( `\n✓ Tool scaffold created: ${ sanitizedName } ` ) ;
154+ console . log ( ` 📄 Spec: ${ join ( toolDir , 'tool.md' ) } ` ) ;
155+ console . log ( ` 💻 Code: ${ join ( toolSrcDir , 'index.js' ) } ` ) ;
156+ console . log ( ` 📋 Registry: ${ this . manifestPath } \n` ) ;
157+
158+ return sanitizedName ;
159+ }
160+ }
161+
23162class CopilotAutoWrapper {
24163 constructor ( config = { } ) {
25164 this . config = { ...DEFAULT_CONFIG , ...config } ;
26165 this . iterations = 0 ;
27166 this . startTime = Date . now ( ) ;
28167 this . tokenCount = 0 ;
29168 this . sessionActive = false ;
169+ this . toolSynthesizer = new ToolSynthesizer ( ) ;
30170 }
31171
32172 async loadConfig ( ) {
@@ -38,6 +178,17 @@ class CopilotAutoWrapper {
38178 } catch ( error ) {
39179 console . log ( 'ℹ Using default configuration (no config file found)' ) ;
40180 }
181+
182+ // Apply environment variable overrides
183+ if ( process . env . COPILOT_LOCAL_TOOL_SYNTHESIS ) {
184+ const val = process . env . COPILOT_LOCAL_TOOL_SYNTHESIS . toLowerCase ( ) ;
185+ this . config . enableLocalToolSynthesis = [ '1' , 'true' , 'yes' , 'on' ] . includes ( val ) ;
186+ }
187+
188+ if ( process . env . COPILOT_NO_MCP ) {
189+ const val = process . env . COPILOT_NO_MCP . toLowerCase ( ) ;
190+ this . config . noMcp = [ '1' , 'true' , 'yes' , 'on' ] . includes ( val ) ;
191+ }
41192 }
42193
43194 checkLimits ( ) {
@@ -98,6 +249,12 @@ class CopilotAutoWrapper {
98249 console . log ( `📊 Limits: ${ this . config . maxIterations } iterations, ${ this . config . maxDuration / 1000 } s duration` ) ;
99250 console . log ( `🔧 Allow all tools: ${ this . config . allowAllTools } ` ) ;
100251 console . log ( `🔧 Allow all paths: ${ this . config . allowAllPaths } ` ) ;
252+ if ( this . config . enableLocalToolSynthesis ) {
253+ console . log ( `🔬 Local tool synthesis: enabled` ) ;
254+ }
255+ if ( this . config . noMcp ) {
256+ console . log ( `🚫 MCP servers: disabled` ) ;
257+ }
101258 console . log ( '' ) ;
102259
103260 const args = this . buildCopilotCommand ( initialPrompt ) ;
@@ -208,16 +365,22 @@ GitHub Copilot CLI Auto-Approval Wrapper
208365Usage: copilot-auto [options] [prompt]
209366
210367Options:
211- -h, --help Show this help message
212- -v, --version Show version
213- -c, --config <file> Use custom config file
214- --max-iterations <n> Maximum number of iterations (default: 50)
215- --max-duration <ms> Maximum duration in milliseconds (default: 1800000)
216- --model <model> AI model to use (default: claude-sonnet-4.5)
217- --allow-all-paths Allow access to all file paths
218- --deny-tool <tool> Deny specific tool (can be used multiple times)
219- --interactive Start in interactive mode (default if no prompt given)
220- --direct Run prompt and exit (default if prompt given)
368+ -h, --help Show this help message
369+ -v, --version Show version
370+ -c, --config <file> Use custom config file
371+ --max-iterations <n> Maximum number of iterations (default: 50)
372+ --max-duration <ms> Maximum duration in milliseconds (default: 1800000)
373+ --model <model> AI model to use (default: claude-sonnet-4.5)
374+ --allow-all-paths Allow access to all file paths
375+ --deny-tool <tool> Deny specific tool (can be used multiple times)
376+ --interactive Start in interactive mode (default if no prompt given)
377+ --direct Run prompt and exit (default if prompt given)
378+ --enable-local-tool-synthesis Enable local tool synthesis (default: false)
379+ --no-mcp Prevent all MCP server usage (default: false)
380+
381+ Environment Variables:
382+ COPILOT_LOCAL_TOOL_SYNTHESIS=1 Enable local tool synthesis
383+ COPILOT_NO_MCP=1 Prevent MCP server usage
221384
222385Examples:
223386 # Interactive mode with auto-approval
@@ -234,6 +397,9 @@ Examples:
234397
235398 # Deny dangerous operations
236399 copilot-auto --deny-tool "shell(rm *)" --deny-tool "shell(git push)"
400+
401+ # Enable local tool synthesis with MCP prevention (experimental)
402+ copilot-auto --enable-local-tool-synthesis --no-mcp "Create custom tools"
237403
238404Configuration File:
239405 Create ~/.copilot-auto-config.json with:
@@ -243,7 +409,9 @@ Configuration File:
243409 "allowAllTools": true,
244410 "allowAllPaths": false,
245411 "deniedTools": [],
246- "model": "claude-sonnet-4.5"
412+ "model": "claude-sonnet-4.5",
413+ "enableLocalToolSynthesis": false,
414+ "noMcp": false
247415 }
248416` ) ;
249417}
@@ -293,6 +461,12 @@ async function main() {
293461 case '--direct' :
294462 mode = 'direct' ;
295463 break ;
464+ case '--enable-local-tool-synthesis' :
465+ config . enableLocalToolSynthesis = true ;
466+ break ;
467+ case '--no-mcp' :
468+ config . noMcp = true ;
469+ break ;
296470 default :
297471 if ( ! arg . startsWith ( '-' ) ) {
298472 prompt = arg ;
0 commit comments