claude-coder-mac-mcp 1.0.0
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/LICENSE +21 -0
- package/README.md +178 -0
- package/dist/check.d.ts +37 -0
- package/dist/check.js +173 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +72 -0
- package/dist/iterm.d.ts +24 -0
- package/dist/iterm.js +84 -0
- package/dist/server.d.ts +16 -0
- package/dist/server.js +142 -0
- package/dist/spawn.d.ts +38 -0
- package/dist/spawn.js +76 -0
- package/package.json +67 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 pm990320
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
# claude-coder-mac-mcp
|
|
2
|
+
|
|
3
|
+
An MCP server that allows Claude Desktop to spawn new Claude Code instances in iTerm2 windows.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **spawn_claude_coder**: Spawn a new Claude Code instance with a given prompt
|
|
8
|
+
- Opens a new iTerm2 window
|
|
9
|
+
- Optionally runs with `--dangerously-skip-permissions` (must be enabled on server)
|
|
10
|
+
- Passes your prompt directly to Claude
|
|
11
|
+
- The terminal becomes fully interactive
|
|
12
|
+
|
|
13
|
+
- **list_iterm_windows**: List all iTerm2 windows and sessions
|
|
14
|
+
|
|
15
|
+
## Requirements
|
|
16
|
+
|
|
17
|
+
- macOS
|
|
18
|
+
- [iTerm2](https://iterm2.com/) installed
|
|
19
|
+
- [Claude Code CLI](https://claude.ai/code) installed
|
|
20
|
+
- Automation permissions granted to the MCP host app
|
|
21
|
+
|
|
22
|
+
## Installation
|
|
23
|
+
|
|
24
|
+
### Via npm (recommended)
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
npm install -g claude-coder-mac-mcp
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### From source
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
git clone https://github.com/pm990320/claude-coder-mac-mcp.git
|
|
34
|
+
cd claude-coder-mac-mcp
|
|
35
|
+
npm install
|
|
36
|
+
npm run build
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### Verify installation
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
claude-coder-mac-mcp check
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
This runs environment checks to verify macOS, iTerm2, Claude Code CLI, and automation permissions are all configured correctly.
|
|
46
|
+
|
|
47
|
+
## CLI Usage
|
|
48
|
+
|
|
49
|
+
The CLI has multiple commands:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
# Show help
|
|
53
|
+
claude-coder-mac-mcp --help
|
|
54
|
+
|
|
55
|
+
# Start the MCP server (used by Claude Desktop)
|
|
56
|
+
# By default, spawned Claude instances run in normal mode (user must approve actions)
|
|
57
|
+
claude-coder-mac-mcp mcp
|
|
58
|
+
|
|
59
|
+
# Start the MCP server with --dangerously-skip-permissions enabled
|
|
60
|
+
# WARNING: This allows spawned Claude instances to run without user approval
|
|
61
|
+
claude-coder-mac-mcp mcp --dangerously-skip-permissions
|
|
62
|
+
|
|
63
|
+
# Test spawning a Claude Code instance directly (normal mode)
|
|
64
|
+
claude-coder-mac-mcp spawn "Help me write a REST API"
|
|
65
|
+
claude-coder-mac-mcp spawn "Fix the tests" -d ~/myproject -t "Test Fixer"
|
|
66
|
+
|
|
67
|
+
# Test spawning with --dangerously-skip-permissions
|
|
68
|
+
claude-coder-mac-mcp spawn "Fix the tests" --dangerously-skip-permissions
|
|
69
|
+
|
|
70
|
+
# List current iTerm2 windows
|
|
71
|
+
claude-coder-mac-mcp list
|
|
72
|
+
|
|
73
|
+
# Check environment is configured correctly
|
|
74
|
+
claude-coder-mac-mcp check
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Claude Desktop Configuration
|
|
78
|
+
|
|
79
|
+
Add to your Claude Desktop config (`~/Library/Application Support/Claude/claude_desktop_config.json`):
|
|
80
|
+
|
|
81
|
+
**Normal mode** (user must approve Claude Code actions):
|
|
82
|
+
```json
|
|
83
|
+
{
|
|
84
|
+
"mcpServers": {
|
|
85
|
+
"claude-coder-mac": {
|
|
86
|
+
"command": "node",
|
|
87
|
+
"args": ["/path/to/claude-coder-mac-mcp/dist/index.js", "mcp"]
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
**With --dangerously-skip-permissions** (Claude Code runs autonomously):
|
|
94
|
+
```json
|
|
95
|
+
{
|
|
96
|
+
"mcpServers": {
|
|
97
|
+
"claude-coder-mac": {
|
|
98
|
+
"command": "node",
|
|
99
|
+
"args": ["/path/to/claude-coder-mac-mcp/dist/index.js", "mcp", "--dangerously-skip-permissions"]
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Usage with Claude Desktop
|
|
106
|
+
|
|
107
|
+
Once configured, you can ask Claude Desktop to spawn new Claude Code instances:
|
|
108
|
+
|
|
109
|
+
> "Start a new Claude coder to work on my project at ~/myproject with the task: implement user authentication"
|
|
110
|
+
|
|
111
|
+
Claude will use the `spawn_claude_coder` tool to open iTerm2 with a new Claude Code session.
|
|
112
|
+
|
|
113
|
+
## Permissions
|
|
114
|
+
|
|
115
|
+
On first use, macOS will ask you to grant automation permissions. Go to:
|
|
116
|
+
|
|
117
|
+
**System Settings > Privacy & Security > Automation**
|
|
118
|
+
|
|
119
|
+
And ensure the app running the MCP server (e.g., Claude Desktop) has permission to control iTerm2.
|
|
120
|
+
|
|
121
|
+
## MCP Tool Parameters
|
|
122
|
+
|
|
123
|
+
### spawn_claude_coder
|
|
124
|
+
|
|
125
|
+
| Parameter | Required | Description |
|
|
126
|
+
|-----------|----------|-------------|
|
|
127
|
+
| `prompt` | Yes | The task/prompt to give to Claude Code |
|
|
128
|
+
| `workingDirectory` | No | Directory to run Claude Code in (defaults to ~) |
|
|
129
|
+
| `windowTitle` | No | Custom title for the iTerm2 window |
|
|
130
|
+
|
|
131
|
+
Note: Whether `--dangerously-skip-permissions` is passed to spawned Claude instances is controlled by the server startup flag, not by tool parameters.
|
|
132
|
+
|
|
133
|
+
### list_iterm_windows
|
|
134
|
+
|
|
135
|
+
No parameters. Lists all open iTerm2 windows and their sessions.
|
|
136
|
+
|
|
137
|
+
## Development
|
|
138
|
+
|
|
139
|
+
```bash
|
|
140
|
+
# Install dependencies
|
|
141
|
+
npm install
|
|
142
|
+
|
|
143
|
+
# Build
|
|
144
|
+
npm run build
|
|
145
|
+
|
|
146
|
+
# Run tests
|
|
147
|
+
npm test
|
|
148
|
+
|
|
149
|
+
# Lint
|
|
150
|
+
npm run lint
|
|
151
|
+
|
|
152
|
+
# Fix lint issues
|
|
153
|
+
npm run fix
|
|
154
|
+
|
|
155
|
+
# Type check
|
|
156
|
+
npm run typecheck
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
## Project Structure
|
|
160
|
+
|
|
161
|
+
```
|
|
162
|
+
src/
|
|
163
|
+
├── index.ts # CLI entrypoint (uses commander)
|
|
164
|
+
├── server.ts # MCP server setup and tool registration
|
|
165
|
+
├── spawn.ts # Core spawn functionality
|
|
166
|
+
├── iterm.ts # iTerm2 AppleScript utilities
|
|
167
|
+
└── check.ts # Environment verification checks
|
|
168
|
+
|
|
169
|
+
tests/
|
|
170
|
+
├── spawn.test.ts # Unit tests for spawn functions
|
|
171
|
+
└── iterm.test.ts # Unit tests for iTerm utilities
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
The modules are separated for testability - `spawn.ts` and `iterm.ts` contain pure functions that can be unit tested independently.
|
|
175
|
+
|
|
176
|
+
## License
|
|
177
|
+
|
|
178
|
+
MIT
|
package/dist/check.d.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export interface CheckResult {
|
|
2
|
+
name: string;
|
|
3
|
+
status: 'pass' | 'fail' | 'warn';
|
|
4
|
+
message: string;
|
|
5
|
+
}
|
|
6
|
+
export interface EnvironmentCheckResult {
|
|
7
|
+
allPassed: boolean;
|
|
8
|
+
checks: CheckResult[];
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Check if running on macOS
|
|
12
|
+
*/
|
|
13
|
+
export declare function checkMacOS(): CheckResult;
|
|
14
|
+
/**
|
|
15
|
+
* Check if iTerm2 is installed
|
|
16
|
+
*/
|
|
17
|
+
export declare function checkITerm2(): Promise<CheckResult>;
|
|
18
|
+
/**
|
|
19
|
+
* Check if Claude Code CLI is installed
|
|
20
|
+
*/
|
|
21
|
+
export declare function checkClaudeCode(): Promise<CheckResult>;
|
|
22
|
+
/**
|
|
23
|
+
* Check if automation permissions are granted for iTerm2
|
|
24
|
+
*/
|
|
25
|
+
export declare function checkAutomationPermissions(): Promise<CheckResult>;
|
|
26
|
+
/**
|
|
27
|
+
* Check if Node.js version meets requirements
|
|
28
|
+
*/
|
|
29
|
+
export declare function checkNodeVersion(): CheckResult;
|
|
30
|
+
/**
|
|
31
|
+
* Run all environment checks
|
|
32
|
+
*/
|
|
33
|
+
export declare function runAllChecks(): Promise<EnvironmentCheckResult>;
|
|
34
|
+
/**
|
|
35
|
+
* Format check results for display
|
|
36
|
+
*/
|
|
37
|
+
export declare function formatCheckResults(result: EnvironmentCheckResult): string;
|
package/dist/check.js
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { exec } from 'child_process';
|
|
2
|
+
import { promisify } from 'util';
|
|
3
|
+
import * as os from 'os';
|
|
4
|
+
const execAsync = promisify(exec);
|
|
5
|
+
/**
|
|
6
|
+
* Check if running on macOS
|
|
7
|
+
*/
|
|
8
|
+
export function checkMacOS() {
|
|
9
|
+
const platform = os.platform();
|
|
10
|
+
if (platform === 'darwin') {
|
|
11
|
+
const release = os.release();
|
|
12
|
+
return {
|
|
13
|
+
name: 'macOS',
|
|
14
|
+
status: 'pass',
|
|
15
|
+
message: `Running on macOS (Darwin ${release})`,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
return {
|
|
19
|
+
name: 'macOS',
|
|
20
|
+
status: 'fail',
|
|
21
|
+
message: `This tool requires macOS. Current platform: ${platform}`,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Check if iTerm2 is installed
|
|
26
|
+
*/
|
|
27
|
+
export async function checkITerm2() {
|
|
28
|
+
try {
|
|
29
|
+
// Check if iTerm2 app exists
|
|
30
|
+
await execAsync('test -d "/Applications/iTerm.app"');
|
|
31
|
+
return {
|
|
32
|
+
name: 'iTerm2',
|
|
33
|
+
status: 'pass',
|
|
34
|
+
message: 'iTerm2 is installed at /Applications/iTerm.app',
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return {
|
|
39
|
+
name: 'iTerm2',
|
|
40
|
+
status: 'fail',
|
|
41
|
+
message: 'iTerm2 is not installed. Download from https://iterm2.com/ or install via: brew install --cask iterm2',
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Check if Claude Code CLI is installed
|
|
47
|
+
*/
|
|
48
|
+
export async function checkClaudeCode() {
|
|
49
|
+
try {
|
|
50
|
+
const { stdout } = await execAsync('which claude');
|
|
51
|
+
const path = stdout.trim();
|
|
52
|
+
// Try to get version
|
|
53
|
+
try {
|
|
54
|
+
const { stdout: versionOut } = await execAsync('claude --version');
|
|
55
|
+
const version = versionOut.trim();
|
|
56
|
+
return {
|
|
57
|
+
name: 'Claude Code CLI',
|
|
58
|
+
status: 'pass',
|
|
59
|
+
message: `Claude Code CLI found at ${path} (${version})`,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return {
|
|
64
|
+
name: 'Claude Code CLI',
|
|
65
|
+
status: 'pass',
|
|
66
|
+
message: `Claude Code CLI found at ${path}`,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
return {
|
|
72
|
+
name: 'Claude Code CLI',
|
|
73
|
+
status: 'fail',
|
|
74
|
+
message: 'Claude Code CLI is not installed. Install from https://claude.ai/code',
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Check if automation permissions are granted for iTerm2
|
|
80
|
+
*/
|
|
81
|
+
export async function checkAutomationPermissions() {
|
|
82
|
+
try {
|
|
83
|
+
// Try a simple AppleScript that requires iTerm2 automation permission
|
|
84
|
+
await execAsync('osascript -e \'tell application "iTerm2" to get name\'');
|
|
85
|
+
return {
|
|
86
|
+
name: 'Automation Permissions',
|
|
87
|
+
status: 'pass',
|
|
88
|
+
message: 'Automation permissions for iTerm2 are granted',
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
93
|
+
if (errorMessage.includes('not allowed') || errorMessage.includes('1743')) {
|
|
94
|
+
return {
|
|
95
|
+
name: 'Automation Permissions',
|
|
96
|
+
status: 'fail',
|
|
97
|
+
message: 'Automation permissions not granted. Go to System Settings > Privacy & Security > Automation and enable iTerm2 for your terminal/app.',
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
// iTerm2 might not be running, which is OK
|
|
101
|
+
if (errorMessage.includes('not running') ||
|
|
102
|
+
errorMessage.includes('(-600)')) {
|
|
103
|
+
return {
|
|
104
|
+
name: 'Automation Permissions',
|
|
105
|
+
status: 'warn',
|
|
106
|
+
message: 'Could not verify automation permissions (iTerm2 is not running). Permissions will be requested on first use.',
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
return {
|
|
110
|
+
name: 'Automation Permissions',
|
|
111
|
+
status: 'warn',
|
|
112
|
+
message: `Could not verify automation permissions: ${errorMessage}`,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Check if Node.js version meets requirements
|
|
118
|
+
*/
|
|
119
|
+
export function checkNodeVersion() {
|
|
120
|
+
const nodeVersion = process.version;
|
|
121
|
+
const major = parseInt(nodeVersion.slice(1).split('.')[0], 10);
|
|
122
|
+
if (major >= 18) {
|
|
123
|
+
return {
|
|
124
|
+
name: 'Node.js',
|
|
125
|
+
status: 'pass',
|
|
126
|
+
message: `Node.js ${nodeVersion} meets minimum requirement (>=18.0.0)`,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
return {
|
|
130
|
+
name: 'Node.js',
|
|
131
|
+
status: 'fail',
|
|
132
|
+
message: `Node.js ${nodeVersion} is below minimum requirement (>=18.0.0)`,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Run all environment checks
|
|
137
|
+
*/
|
|
138
|
+
export async function runAllChecks() {
|
|
139
|
+
const checks = [];
|
|
140
|
+
// Synchronous checks
|
|
141
|
+
checks.push(checkMacOS());
|
|
142
|
+
checks.push(checkNodeVersion());
|
|
143
|
+
// Async checks
|
|
144
|
+
checks.push(await checkITerm2());
|
|
145
|
+
checks.push(await checkClaudeCode());
|
|
146
|
+
checks.push(await checkAutomationPermissions());
|
|
147
|
+
const allPassed = checks.every(c => c.status === 'pass' || c.status === 'warn');
|
|
148
|
+
return { allPassed, checks };
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Format check results for display
|
|
152
|
+
*/
|
|
153
|
+
export function formatCheckResults(result) {
|
|
154
|
+
const lines = ['Environment Check Results', '='.repeat(50), ''];
|
|
155
|
+
for (const check of result.checks) {
|
|
156
|
+
const icon = check.status === 'pass'
|
|
157
|
+
? '[OK]'
|
|
158
|
+
: check.status === 'warn'
|
|
159
|
+
? '[!!]'
|
|
160
|
+
: '[X]';
|
|
161
|
+
lines.push(`${icon} ${check.name}`);
|
|
162
|
+
lines.push(` ${check.message}`);
|
|
163
|
+
lines.push('');
|
|
164
|
+
}
|
|
165
|
+
lines.push('='.repeat(50));
|
|
166
|
+
if (result.allPassed) {
|
|
167
|
+
lines.push('All checks passed! Environment is ready.');
|
|
168
|
+
}
|
|
169
|
+
else {
|
|
170
|
+
lines.push('Some checks failed. Please fix the issues above.');
|
|
171
|
+
}
|
|
172
|
+
return lines.join('\n');
|
|
173
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
import { startServer } from './server.js';
|
|
4
|
+
import { spawnClaudeCoder } from './spawn.js';
|
|
5
|
+
import { listItermWindows, formatWindowList } from './iterm.js';
|
|
6
|
+
import { runAllChecks, formatCheckResults } from './check.js';
|
|
7
|
+
const program = new Command();
|
|
8
|
+
program
|
|
9
|
+
.name('claude-coder-mac-mcp')
|
|
10
|
+
.description('Spawn Claude Code instances from Claude Desktop via MCP')
|
|
11
|
+
.version('1.0.0');
|
|
12
|
+
program
|
|
13
|
+
.command('mcp')
|
|
14
|
+
.alias('serve')
|
|
15
|
+
.description('Start the MCP server (for Claude Desktop)')
|
|
16
|
+
.option('--dangerously-skip-permissions', 'Pass --dangerously-skip-permissions to spawned Claude instances')
|
|
17
|
+
.action(async (options) => {
|
|
18
|
+
await startServer({
|
|
19
|
+
dangerouslySkipPermissions: options.dangerouslySkipPermissions ?? false,
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
program
|
|
23
|
+
.command('spawn')
|
|
24
|
+
.description('Test spawning a Claude Code instance in iTerm2')
|
|
25
|
+
.argument('<prompt>', 'The prompt/task to give to Claude Code')
|
|
26
|
+
.option('-d, --directory <path>', 'Working directory', process.env.HOME)
|
|
27
|
+
.option('-t, --title <title>', 'Window title', 'Claude Coder')
|
|
28
|
+
.option('--dangerously-skip-permissions', 'Pass --dangerously-skip-permissions to the Claude instance')
|
|
29
|
+
.action(async (prompt, options) => {
|
|
30
|
+
const skipPerms = options.dangerouslySkipPermissions ?? false;
|
|
31
|
+
const modeLabel = skipPerms
|
|
32
|
+
? 'with --dangerously-skip-permissions'
|
|
33
|
+
: 'in normal mode';
|
|
34
|
+
console.log(`Spawning Claude Code instance (${modeLabel})...`);
|
|
35
|
+
const result = await spawnClaudeCoder({
|
|
36
|
+
prompt,
|
|
37
|
+
workingDirectory: options.directory,
|
|
38
|
+
windowTitle: options.title,
|
|
39
|
+
dangerouslySkipPermissions: skipPerms,
|
|
40
|
+
});
|
|
41
|
+
if (result.success) {
|
|
42
|
+
console.log('Success!');
|
|
43
|
+
console.log(` Window title: ${result.windowTitle}`);
|
|
44
|
+
console.log(` Working directory: ${result.workingDirectory}`);
|
|
45
|
+
console.log(` Mode: ${modeLabel}`);
|
|
46
|
+
console.log(` Prompt: ${result.prompt}`);
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
console.error(`Failed: ${result.error}`);
|
|
50
|
+
process.exit(1);
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
program
|
|
54
|
+
.command('list')
|
|
55
|
+
.alias('ls')
|
|
56
|
+
.description('List iTerm2 windows and sessions')
|
|
57
|
+
.action(async () => {
|
|
58
|
+
const windows = await listItermWindows();
|
|
59
|
+
console.log(formatWindowList(windows));
|
|
60
|
+
});
|
|
61
|
+
program
|
|
62
|
+
.command('check')
|
|
63
|
+
.alias('doctor')
|
|
64
|
+
.description('Check if the environment is configured correctly')
|
|
65
|
+
.action(async () => {
|
|
66
|
+
const result = await runAllChecks();
|
|
67
|
+
console.log(formatCheckResults(result));
|
|
68
|
+
if (!result.allPassed) {
|
|
69
|
+
process.exit(1);
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
program.parse();
|
package/dist/iterm.d.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export interface ItermWindow {
|
|
2
|
+
id: string;
|
|
3
|
+
sessions: ItermSession[];
|
|
4
|
+
}
|
|
5
|
+
export interface ItermSession {
|
|
6
|
+
name: string;
|
|
7
|
+
tty: string;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Execute an AppleScript and return the result
|
|
11
|
+
*/
|
|
12
|
+
export declare function execAppleScript(script: string): Promise<string>;
|
|
13
|
+
/**
|
|
14
|
+
* List all iTerm2 windows and their sessions
|
|
15
|
+
*/
|
|
16
|
+
export declare function listItermWindows(): Promise<ItermWindow[]>;
|
|
17
|
+
/**
|
|
18
|
+
* Parse the window list output from AppleScript
|
|
19
|
+
*/
|
|
20
|
+
export declare function parseWindowList(output: string): ItermWindow[];
|
|
21
|
+
/**
|
|
22
|
+
* Format window list for display
|
|
23
|
+
*/
|
|
24
|
+
export declare function formatWindowList(windows: ItermWindow[]): string;
|
package/dist/iterm.js
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { exec } from 'child_process';
|
|
2
|
+
import { promisify } from 'util';
|
|
3
|
+
const execAsync = promisify(exec);
|
|
4
|
+
/**
|
|
5
|
+
* Execute an AppleScript and return the result
|
|
6
|
+
*/
|
|
7
|
+
export async function execAppleScript(script) {
|
|
8
|
+
// Escape single quotes for shell
|
|
9
|
+
const escapedScript = script.replace(/'/g, "'\"'\"'");
|
|
10
|
+
const { stdout } = await execAsync(`osascript -e '${escapedScript}'`);
|
|
11
|
+
return stdout;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* List all iTerm2 windows and their sessions
|
|
15
|
+
*/
|
|
16
|
+
export async function listItermWindows() {
|
|
17
|
+
const appleScript = `
|
|
18
|
+
tell application "iTerm"
|
|
19
|
+
set output to ""
|
|
20
|
+
repeat with w in windows
|
|
21
|
+
set output to output & "WINDOW:" & (id of w) & linefeed
|
|
22
|
+
repeat with t in tabs of w
|
|
23
|
+
repeat with s in sessions of t
|
|
24
|
+
set output to output & "SESSION:" & (name of s) & "|" & (tty of s) & linefeed
|
|
25
|
+
end repeat
|
|
26
|
+
end repeat
|
|
27
|
+
end repeat
|
|
28
|
+
return output
|
|
29
|
+
end tell
|
|
30
|
+
`;
|
|
31
|
+
try {
|
|
32
|
+
const stdout = await execAppleScript(appleScript);
|
|
33
|
+
return parseWindowList(stdout);
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return [];
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Parse the window list output from AppleScript
|
|
41
|
+
*/
|
|
42
|
+
export function parseWindowList(output) {
|
|
43
|
+
const windows = [];
|
|
44
|
+
let currentWindow = null;
|
|
45
|
+
const lines = output.split('\n').filter(line => line.trim());
|
|
46
|
+
for (const line of lines) {
|
|
47
|
+
if (line.startsWith('WINDOW:')) {
|
|
48
|
+
if (currentWindow) {
|
|
49
|
+
windows.push(currentWindow);
|
|
50
|
+
}
|
|
51
|
+
currentWindow = {
|
|
52
|
+
id: line.substring(7),
|
|
53
|
+
sessions: [],
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
else if (line.startsWith('SESSION:') && currentWindow) {
|
|
57
|
+
const parts = line.substring(8).split('|');
|
|
58
|
+
currentWindow.sessions.push({
|
|
59
|
+
name: parts[0] || '',
|
|
60
|
+
tty: parts[1] || '',
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if (currentWindow) {
|
|
65
|
+
windows.push(currentWindow);
|
|
66
|
+
}
|
|
67
|
+
return windows;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Format window list for display
|
|
71
|
+
*/
|
|
72
|
+
export function formatWindowList(windows) {
|
|
73
|
+
if (windows.length === 0) {
|
|
74
|
+
return 'No iTerm2 windows found.';
|
|
75
|
+
}
|
|
76
|
+
const lines = [];
|
|
77
|
+
for (const window of windows) {
|
|
78
|
+
lines.push(`Window: ${window.id}`);
|
|
79
|
+
for (const session of window.sessions) {
|
|
80
|
+
lines.push(` Session: ${session.name} - ${session.tty}`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return lines.join('\n');
|
|
84
|
+
}
|
package/dist/server.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
export interface ServerOptions {
|
|
3
|
+
dangerouslySkipPermissions: boolean;
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Create and configure the MCP server
|
|
7
|
+
*/
|
|
8
|
+
export declare function createServer(options: ServerOptions): McpServer;
|
|
9
|
+
/**
|
|
10
|
+
* Register all MCP tools on the server
|
|
11
|
+
*/
|
|
12
|
+
export declare function registerTools(server: McpServer, options: ServerOptions): void;
|
|
13
|
+
/**
|
|
14
|
+
* Start the MCP server with stdio transport
|
|
15
|
+
*/
|
|
16
|
+
export declare function startServer(options: ServerOptions): Promise<void>;
|
package/dist/server.js
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
import { spawnClaudeCoder } from './spawn.js';
|
|
5
|
+
import { listItermWindows, formatWindowList } from './iterm.js';
|
|
6
|
+
/**
|
|
7
|
+
* Create and configure the MCP server
|
|
8
|
+
*/
|
|
9
|
+
export function createServer(options) {
|
|
10
|
+
const server = new McpServer({
|
|
11
|
+
name: 'claude-coder-mac-mcp',
|
|
12
|
+
version: '1.0.0',
|
|
13
|
+
});
|
|
14
|
+
registerTools(server, options);
|
|
15
|
+
return server;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Register all MCP tools on the server
|
|
19
|
+
*/
|
|
20
|
+
export function registerTools(server, options) {
|
|
21
|
+
const { dangerouslySkipPermissions } = options;
|
|
22
|
+
const permissionMode = dangerouslySkipPermissions
|
|
23
|
+
? 'AUTONOMOUS MODE (--dangerously-skip-permissions): Claude will act without user approval. Be specific to prevent unintended actions.'
|
|
24
|
+
: 'INTERACTIVE MODE: User must approve each action in the spawned terminal.';
|
|
25
|
+
const toolDescription = `Spawn a new Claude Code instance in an iTerm2 window to work on a task.
|
|
26
|
+
|
|
27
|
+
${permissionMode}
|
|
28
|
+
|
|
29
|
+
PROMPT BEST PRACTICES:
|
|
30
|
+
- Be specific: "Run npm test, fix any failing tests" not "fix tests"
|
|
31
|
+
- Include context Claude can't see: "This is a Next.js app using Prisma and PostgreSQL"
|
|
32
|
+
- Set success criteria: "Done when all tests pass and npm run build succeeds"
|
|
33
|
+
- Mention key files: "The API routes are in src/app/api/"
|
|
34
|
+
- One focused task works better than multiple vague ones
|
|
35
|
+
|
|
36
|
+
EXAMPLE PROMPT:
|
|
37
|
+
"In this TypeScript Express project, add a new GET /api/health endpoint that returns {status: 'ok', timestamp: Date.now()}. Follow the pattern in src/routes/users.ts. Run npm test when done."
|
|
38
|
+
|
|
39
|
+
ALWAYS specify workingDirectory so Claude starts in the correct project folder. If you do not know the correct working directory, specify as the first instruction in your prompt that claude should find the project and cd into its directory first.`;
|
|
40
|
+
server.registerTool('spawn_claude_coder', {
|
|
41
|
+
title: 'Spawn Claude Coder',
|
|
42
|
+
description: toolDescription,
|
|
43
|
+
inputSchema: {
|
|
44
|
+
prompt: z
|
|
45
|
+
.string()
|
|
46
|
+
.describe('The prompt/task to give to the new Claude Code instance'),
|
|
47
|
+
workingDirectory: z
|
|
48
|
+
.string()
|
|
49
|
+
.optional()
|
|
50
|
+
.describe('The working directory for the Claude Code instance (defaults to home directory)'),
|
|
51
|
+
windowTitle: z
|
|
52
|
+
.string()
|
|
53
|
+
.optional()
|
|
54
|
+
.describe("Custom title for the iTerm2 window (defaults to 'Claude Coder')"),
|
|
55
|
+
},
|
|
56
|
+
annotations: {
|
|
57
|
+
title: 'Spawn Claude Coder',
|
|
58
|
+
readOnlyHint: false,
|
|
59
|
+
destructiveHint: dangerouslySkipPermissions,
|
|
60
|
+
idempotentHint: false,
|
|
61
|
+
openWorldHint: true,
|
|
62
|
+
},
|
|
63
|
+
}, async ({ prompt, workingDirectory, windowTitle }) => {
|
|
64
|
+
const result = await spawnClaudeCoder({
|
|
65
|
+
prompt,
|
|
66
|
+
workingDirectory,
|
|
67
|
+
windowTitle,
|
|
68
|
+
dangerouslySkipPermissions,
|
|
69
|
+
});
|
|
70
|
+
if (result.success) {
|
|
71
|
+
const permissionStatus = result.dangerouslySkipPermissions
|
|
72
|
+
? 'with --dangerously-skip-permissions'
|
|
73
|
+
: 'in normal mode';
|
|
74
|
+
return {
|
|
75
|
+
content: [
|
|
76
|
+
{
|
|
77
|
+
type: 'text',
|
|
78
|
+
text: `Successfully spawned Claude Code in iTerm2 (${permissionStatus})!\n\nWindow title: ${result.windowTitle}\nWorking directory: ${result.workingDirectory}\nPrompt: ${result.prompt}\n\nThe Claude Code instance is now running interactively in iTerm2.`,
|
|
79
|
+
},
|
|
80
|
+
],
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
return {
|
|
85
|
+
content: [
|
|
86
|
+
{
|
|
87
|
+
type: 'text',
|
|
88
|
+
text: `Failed to spawn Claude Code: ${result.error}\n\nMake sure iTerm2 is installed and you have granted automation permissions in System Settings > Privacy & Security > Automation.`,
|
|
89
|
+
},
|
|
90
|
+
],
|
|
91
|
+
isError: true,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
server.registerTool('list_iterm_windows', {
|
|
96
|
+
title: 'List iTerm2 Windows',
|
|
97
|
+
description: 'List all iTerm2 windows and their sessions (useful to see running Claude instances)',
|
|
98
|
+
annotations: {
|
|
99
|
+
title: 'List iTerm2 Windows',
|
|
100
|
+
readOnlyHint: true,
|
|
101
|
+
destructiveHint: false,
|
|
102
|
+
idempotentHint: true,
|
|
103
|
+
openWorldHint: true,
|
|
104
|
+
},
|
|
105
|
+
}, async () => {
|
|
106
|
+
try {
|
|
107
|
+
const windows = await listItermWindows();
|
|
108
|
+
return {
|
|
109
|
+
content: [
|
|
110
|
+
{
|
|
111
|
+
type: 'text',
|
|
112
|
+
text: formatWindowList(windows),
|
|
113
|
+
},
|
|
114
|
+
],
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
catch (error) {
|
|
118
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
119
|
+
return {
|
|
120
|
+
content: [
|
|
121
|
+
{
|
|
122
|
+
type: 'text',
|
|
123
|
+
text: `Failed to list iTerm windows: ${errorMessage}`,
|
|
124
|
+
},
|
|
125
|
+
],
|
|
126
|
+
isError: true,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Start the MCP server with stdio transport
|
|
133
|
+
*/
|
|
134
|
+
export async function startServer(options) {
|
|
135
|
+
const server = createServer(options);
|
|
136
|
+
const transport = new StdioServerTransport();
|
|
137
|
+
await server.connect(transport);
|
|
138
|
+
const modeLabel = options.dangerouslySkipPermissions
|
|
139
|
+
? 'with --dangerously-skip-permissions'
|
|
140
|
+
: 'in normal mode';
|
|
141
|
+
console.error(`Claude Coder Mac MCP server running on stdio (${modeLabel})`);
|
|
142
|
+
}
|
package/dist/spawn.d.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export interface SpawnOptions {
|
|
2
|
+
prompt: string;
|
|
3
|
+
workingDirectory?: string;
|
|
4
|
+
windowTitle?: string;
|
|
5
|
+
dangerouslySkipPermissions?: boolean;
|
|
6
|
+
}
|
|
7
|
+
export interface SpawnResult {
|
|
8
|
+
success: boolean;
|
|
9
|
+
windowTitle: string;
|
|
10
|
+
workingDirectory: string;
|
|
11
|
+
prompt: string;
|
|
12
|
+
dangerouslySkipPermissions: boolean;
|
|
13
|
+
error?: string;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Escape a string for use in AppleScript
|
|
17
|
+
*/
|
|
18
|
+
export declare function escapeForAppleScript(str: string): string;
|
|
19
|
+
/**
|
|
20
|
+
* Escape a string for use in shell (inside the AppleScript command)
|
|
21
|
+
*/
|
|
22
|
+
export declare function escapeForShell(str: string): string;
|
|
23
|
+
/**
|
|
24
|
+
* Build the claude command string
|
|
25
|
+
*/
|
|
26
|
+
export declare function buildClaudeCommand(prompt: string, dangerouslySkipPermissions: boolean): string;
|
|
27
|
+
/**
|
|
28
|
+
* Build the full shell command (cd + claude)
|
|
29
|
+
*/
|
|
30
|
+
export declare function buildFullCommand(prompt: string, workingDirectory: string, dangerouslySkipPermissions: boolean): string;
|
|
31
|
+
/**
|
|
32
|
+
* Build the AppleScript to spawn an iTerm2 window with a command
|
|
33
|
+
*/
|
|
34
|
+
export declare function buildSpawnAppleScript(command: string, windowTitle: string): string;
|
|
35
|
+
/**
|
|
36
|
+
* Spawn a new Claude Code instance in iTerm2
|
|
37
|
+
*/
|
|
38
|
+
export declare function spawnClaudeCoder(options: SpawnOptions): Promise<SpawnResult>;
|
package/dist/spawn.js
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { execAppleScript } from './iterm.js';
|
|
2
|
+
/**
|
|
3
|
+
* Escape a string for use in AppleScript
|
|
4
|
+
*/
|
|
5
|
+
export function escapeForAppleScript(str) {
|
|
6
|
+
return str.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n');
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Escape a string for use in shell (inside the AppleScript command)
|
|
10
|
+
*/
|
|
11
|
+
export function escapeForShell(str) {
|
|
12
|
+
// Use single quotes and escape any single quotes in the string
|
|
13
|
+
return `'${str.replace(/'/g, "'\"'\"'")}'`;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Build the claude command string
|
|
17
|
+
*/
|
|
18
|
+
export function buildClaudeCommand(prompt, dangerouslySkipPermissions) {
|
|
19
|
+
const flags = dangerouslySkipPermissions
|
|
20
|
+
? '--dangerously-skip-permissions '
|
|
21
|
+
: '';
|
|
22
|
+
return `claude ${flags}${escapeForShell(prompt)}`;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Build the full shell command (cd + claude)
|
|
26
|
+
*/
|
|
27
|
+
export function buildFullCommand(prompt, workingDirectory, dangerouslySkipPermissions) {
|
|
28
|
+
const claudeCommand = buildClaudeCommand(prompt, dangerouslySkipPermissions);
|
|
29
|
+
return `cd ${escapeForShell(workingDirectory)} && ${claudeCommand}`;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Build the AppleScript to spawn an iTerm2 window with a command
|
|
33
|
+
*/
|
|
34
|
+
export function buildSpawnAppleScript(command, windowTitle) {
|
|
35
|
+
return `
|
|
36
|
+
tell application "iTerm"
|
|
37
|
+
activate
|
|
38
|
+
set newWindow to (create window with default profile)
|
|
39
|
+
tell current session of newWindow
|
|
40
|
+
set name to "${escapeForAppleScript(windowTitle)}"
|
|
41
|
+
write text "${escapeForAppleScript(command)}"
|
|
42
|
+
end tell
|
|
43
|
+
end tell
|
|
44
|
+
`;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Spawn a new Claude Code instance in iTerm2
|
|
48
|
+
*/
|
|
49
|
+
export async function spawnClaudeCoder(options) {
|
|
50
|
+
const windowTitle = options.windowTitle || 'Claude Coder';
|
|
51
|
+
const workingDirectory = options.workingDirectory || process.env.HOME || '~';
|
|
52
|
+
const dangerouslySkipPermissions = options.dangerouslySkipPermissions ?? false;
|
|
53
|
+
const fullCommand = buildFullCommand(options.prompt, workingDirectory, dangerouslySkipPermissions);
|
|
54
|
+
const appleScript = buildSpawnAppleScript(fullCommand, windowTitle);
|
|
55
|
+
try {
|
|
56
|
+
await execAppleScript(appleScript);
|
|
57
|
+
return {
|
|
58
|
+
success: true,
|
|
59
|
+
windowTitle,
|
|
60
|
+
workingDirectory,
|
|
61
|
+
prompt: options.prompt,
|
|
62
|
+
dangerouslySkipPermissions,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
67
|
+
return {
|
|
68
|
+
success: false,
|
|
69
|
+
windowTitle,
|
|
70
|
+
workingDirectory,
|
|
71
|
+
prompt: options.prompt,
|
|
72
|
+
dangerouslySkipPermissions,
|
|
73
|
+
error: errorMessage,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "claude-coder-mac-mcp",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "MCP server to spawn Claude Code instances in iTerm2",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"bin": {
|
|
9
|
+
"claude-coder-mac-mcp": "./dist/index.js"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"dist",
|
|
13
|
+
"README.md",
|
|
14
|
+
"LICENSE"
|
|
15
|
+
],
|
|
16
|
+
"scripts": {
|
|
17
|
+
"build": "tsc -p tsconfig.build.json",
|
|
18
|
+
"dev": "tsc -p tsconfig.build.json --watch",
|
|
19
|
+
"start": "node dist/index.js",
|
|
20
|
+
"lint": "gts lint",
|
|
21
|
+
"fix": "gts fix",
|
|
22
|
+
"clean": "gts clean",
|
|
23
|
+
"typecheck": "tsc --noEmit",
|
|
24
|
+
"test": "vitest run",
|
|
25
|
+
"test:watch": "vitest",
|
|
26
|
+
"test:coverage": "vitest run --coverage",
|
|
27
|
+
"prepare": "npm run build",
|
|
28
|
+
"pretest": "npm run build",
|
|
29
|
+
"posttest": "npm run lint",
|
|
30
|
+
"prepublishOnly": "npm run build && npm test"
|
|
31
|
+
},
|
|
32
|
+
"keywords": [
|
|
33
|
+
"mcp",
|
|
34
|
+
"model-context-protocol",
|
|
35
|
+
"claude",
|
|
36
|
+
"claude-code",
|
|
37
|
+
"iterm2",
|
|
38
|
+
"macos",
|
|
39
|
+
"terminal",
|
|
40
|
+
"automation"
|
|
41
|
+
],
|
|
42
|
+
"author": "pm990320",
|
|
43
|
+
"license": "MIT",
|
|
44
|
+
"repository": {
|
|
45
|
+
"type": "git",
|
|
46
|
+
"url": "git+https://github.com/pm990320/claude-coder-mac-mcp.git"
|
|
47
|
+
},
|
|
48
|
+
"bugs": {
|
|
49
|
+
"url": "https://github.com/pm990320/claude-coder-mac-mcp/issues"
|
|
50
|
+
},
|
|
51
|
+
"homepage": "https://github.com/pm990320/claude-coder-mac-mcp#readme",
|
|
52
|
+
"engines": {
|
|
53
|
+
"node": ">=18.0.0"
|
|
54
|
+
},
|
|
55
|
+
"dependencies": {
|
|
56
|
+
"@modelcontextprotocol/sdk": "^1.25.3",
|
|
57
|
+
"commander": "^14.0.3",
|
|
58
|
+
"zod": "^4.3.6"
|
|
59
|
+
},
|
|
60
|
+
"devDependencies": {
|
|
61
|
+
"@types/node": "^25.2.0",
|
|
62
|
+
"eslint-plugin-security": "^3.0.1",
|
|
63
|
+
"gts": "^7.0.0",
|
|
64
|
+
"typescript": "^5.9.3",
|
|
65
|
+
"vitest": "^4.0.18"
|
|
66
|
+
}
|
|
67
|
+
}
|