1
- import { Command } from '@oclif/core'
1
+ import { Command , Flags } from '@oclif/core'
2
2
import * as fs from 'fs'
3
3
import * as path from 'path'
4
4
import { spawn , ChildProcessWithoutNullStreams } from 'child_process'
@@ -18,29 +18,45 @@ export default class Run extends Command {
18
18
]
19
19
static strict = false // Allow variable arguments
20
20
21
+ static flags = {
22
+ tools : Flags . string ( {
23
+ char : 't' ,
24
+ description : 'Comma separated list of approved tools' ,
25
+ default : ''
26
+ } )
27
+ } ;
28
+
21
29
async run ( ) : Promise < void > {
22
- const { argv } = await this . parse ( Run )
30
+ const { argv, flags } = await this . parse ( Run )
23
31
if ( argv . length === 0 ) {
24
32
this . error ( 'Please specify a package to run' )
25
33
}
26
34
// Assert that argv is a string array.
27
35
const stringArgs = argv as string [ ]
28
36
37
+ const approvedTools = flags . tools . split ( ',' ) . map ( tool => tool . trim ( ) ) ;
38
+
29
39
// Determine home directory for cross-platform compatibility.
30
40
const homeDir = process . env . HOME || process . env . USERPROFILE ;
31
41
if ( ! homeDir ) {
32
42
throw new Error ( 'Unable to determine home directory.' ) ;
33
43
}
34
44
35
- // Set the static log directory path: $HOME/god /logs
45
+ // Set the static log directory path: $HOME/mcpgod /logs
36
46
const logsDir = path . join ( homeDir , 'mcpgod' , 'logs' ) ;
37
47
if ( ! fs . existsSync ( logsDir ) ) {
38
48
fs . mkdirSync ( logsDir , { recursive : true } ) ; // Ensure nested directories are created.
39
49
}
40
50
51
+ function sanitizeFilename ( str : string ) : string {
52
+ // Replace invalid characters with an underscore
53
+ return str . replace ( / [ \/ \\ ? % * : | " < > ] / g, '_' ) ;
54
+ }
55
+
41
56
// Create log file with timestamp.
42
57
const timestamp = new Date ( ) . toISOString ( ) . replace ( / [: .] / g, '-' )
43
- const logFile = path . join ( logsDir , `npx-run-${ timestamp } .log` )
58
+ const filename = sanitizeFilename ( `run--${ stringArgs . join ( ' ' ) } --${ timestamp } .log` ) ;
59
+ const logFile = path . join ( logsDir , filename )
44
60
45
61
// Setup Winston logger.
46
62
const logger = winston . createLogger ( {
@@ -54,7 +70,8 @@ export default class Run extends Command {
54
70
transports : [ new winston . transports . File ( { filename : logFile } ) ]
55
71
} )
56
72
57
- logger . info ( `Command: npx -y ${ stringArgs . join ( ' ' ) } ` )
73
+ logger . info ( '' )
74
+ logger . info ( `Command: ${ stringArgs . join ( ' ' ) } ` )
58
75
logger . info ( `Started at: ${ new Date ( ) . toISOString ( ) } ` )
59
76
logger . info ( '' )
60
77
@@ -66,41 +83,24 @@ export default class Run extends Command {
66
83
filteredEnv . TERM = filteredEnv . TERM || 'xterm-color'
67
84
68
85
function handleOutput ( data : string , stream : NodeJS . WritableStream ) {
69
- const approvedTools = [ "echo" , "add" ] ;
70
-
71
- //const trimmedData = data.trim();
72
-
73
- // Only attempt JSON parsing if the data appears to be a JSON object
74
- //if (trimmedData.startsWith("{") && trimmedData.endsWith("}")) {
75
- try {
76
- //const parsed = JSON.parse(trimmedData);
77
- const parsed = JSON . parse ( data ) ;
78
-
79
- // Check if parsed JSON has the expected result.tools structure before filtering
80
- if ( parsed && parsed . result && Array . isArray ( parsed . result . tools ) ) {
81
- parsed . result . tools = parsed . result . tools . filter (
82
- ( tool : { name : string } ) => approvedTools . includes ( tool . name )
83
- ) ;
84
-
85
- const jsonString = JSON . stringify ( parsed ) ;
86
-
87
- // apparently claude desktop expects a newline at the end.
88
- data = jsonString + "\n" ;
89
- }
90
- } catch ( error ) {
91
- // If JSON parsing fails, fall back to treating the data as plain text.
86
+ try {
87
+ const parsed = JSON . parse ( data ) ;
88
+ // Check if parsed JSON has the expected result.tools structure before filtering
89
+ if ( parsed && parsed . result && Array . isArray ( parsed . result . tools ) ) {
90
+ parsed . result . tools = parsed . result . tools . filter (
91
+ ( tool : { name : string } ) => approvedTools . includes ( tool . name )
92
+ ) ;
93
+ const jsonString = JSON . stringify ( parsed ) ;
94
+ // Apparently claude desktop expects a newline at the end.
95
+ data = jsonString + "\n" ;
92
96
}
93
- //}
94
-
97
+ } catch ( error ) {
98
+ // If JSON parsing fails, fall back to treating the data as plain text.
99
+ }
95
100
stream . write ( data ) ;
96
101
const cleaned = removeControlChars ( stripAnsi ( data ) ) ;
97
102
logger . info ( cleaned ) ;
98
103
}
99
-
100
-
101
-
102
-
103
-
104
104
105
105
// Helper to forward stdin to the child process.
106
106
// writeFn should be a function accepting a string.
@@ -121,12 +121,17 @@ export default class Run extends Command {
121
121
childCommand = 'npx'
122
122
}
123
123
const childArgs = [ '-y' , ...stringArgs ]
124
+ const shell = true ; //process.stdout.isTTY ? true : false;
125
+
126
+ logger . info ( `Spawn: ${ childCommand } ${ childArgs . join ( ' ' ) } ` )
127
+ logger . info ( `Shell: ${ shell } ` )
128
+ logger . info ( '' )
124
129
125
130
const child = spawn ( childCommand , childArgs , {
126
131
cwd : process . cwd ( ) ,
127
132
env : filteredEnv ,
128
133
stdio : [ 'pipe' , 'pipe' , 'pipe' ] ,
129
- shell : process . stdout . isTTY
134
+ shell : shell
130
135
} ) as ChildProcessWithoutNullStreams
131
136
132
137
child . stdout . on ( 'data' , ( data : Buffer ) => {
0 commit comments