ai-cli-mcp 2.6.0 → 2.7.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/CHANGELOG.md CHANGED
@@ -1,3 +1,10 @@
1
+ # [2.7.0](https://github.com/mkXultra/claude-code-mcp/compare/v2.6.0...v2.7.0) (2026-03-01)
2
+
3
+
4
+ ### Features
5
+
6
+ * CLIラッパーとパーサーの追加(cli.run, cli.run.parse) ([330eacf](https://github.com/mkXultra/claude-code-mcp/commit/330eacf26e03484b389438522886448882f58289))
7
+
1
8
  # [2.6.0](https://github.com/mkXultra/claude-code-mcp/compare/v2.5.0...v2.6.0) (2026-02-09)
2
9
 
3
10
 
package/CONTRIBUTING.md CHANGED
@@ -38,6 +38,16 @@ npm run build
38
38
  npx @modelcontextprotocol/inspector node dist/server.js
39
39
  ```
40
40
 
41
+ ## CLI Direct Execution
42
+
43
+ Run AI CLI tools directly from the terminal (foreground, no MCP server):
44
+
45
+ ```bash
46
+ npm run -s cli.run -- --model sonnet --workFolder /tmp --prompt "hello"
47
+ ```
48
+
49
+ See [docs/development.md](docs/development.md#cli-direct-execution-clirun) for full options.
50
+
41
51
  ## Local Development with npm link
42
52
 
43
53
  ```bash
@@ -0,0 +1,278 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import { existsSync, readFileSync } from 'node:fs';
3
+ // Mock dependencies
4
+ vi.mock('node:fs');
5
+ vi.mock('node:path', () => ({
6
+ resolve: vi.fn((...args) => args[args.length - 1]),
7
+ isAbsolute: vi.fn((p) => p.startsWith('/')),
8
+ }));
9
+ const mockExistsSync = vi.mocked(existsSync);
10
+ const mockReadFileSync = vi.mocked(readFileSync);
11
+ // Import after mocks
12
+ import { buildCliCommand, resolveModelAlias, getReasoningEffort, } from '../cli-builder.js';
13
+ const DEFAULT_CLI_PATHS = {
14
+ claude: '/usr/bin/claude',
15
+ codex: '/usr/bin/codex',
16
+ gemini: '/usr/bin/gemini',
17
+ };
18
+ describe('cli-builder', () => {
19
+ beforeEach(() => {
20
+ vi.clearAllMocks();
21
+ // By default, workFolder exists
22
+ mockExistsSync.mockReturnValue(true);
23
+ });
24
+ describe('resolveModelAlias', () => {
25
+ it('should resolve claude-ultra to opus', () => {
26
+ expect(resolveModelAlias('claude-ultra')).toBe('opus');
27
+ });
28
+ it('should resolve codex-ultra to gpt-5.3-codex', () => {
29
+ expect(resolveModelAlias('codex-ultra')).toBe('gpt-5.3-codex');
30
+ });
31
+ it('should resolve gemini-ultra to gemini-3-pro-preview', () => {
32
+ expect(resolveModelAlias('gemini-ultra')).toBe('gemini-3-pro-preview');
33
+ });
34
+ it('should pass through non-alias model names', () => {
35
+ expect(resolveModelAlias('sonnet')).toBe('sonnet');
36
+ expect(resolveModelAlias('gpt-5.2-codex')).toBe('gpt-5.2-codex');
37
+ });
38
+ it('should pass through empty string', () => {
39
+ expect(resolveModelAlias('')).toBe('');
40
+ });
41
+ });
42
+ describe('getReasoningEffort', () => {
43
+ it('should return empty string for non-string input', () => {
44
+ expect(getReasoningEffort('gpt-5.2', undefined)).toBe('');
45
+ expect(getReasoningEffort('gpt-5.2', null)).toBe('');
46
+ expect(getReasoningEffort('gpt-5.2', 123)).toBe('');
47
+ });
48
+ it('should return empty string for empty/whitespace input', () => {
49
+ expect(getReasoningEffort('gpt-5.2', '')).toBe('');
50
+ expect(getReasoningEffort('gpt-5.2', ' ')).toBe('');
51
+ });
52
+ it('should normalize to lowercase', () => {
53
+ expect(getReasoningEffort('gpt-5.2', 'HIGH')).toBe('high');
54
+ expect(getReasoningEffort('gpt-5.2', 'Low')).toBe('low');
55
+ });
56
+ it('should accept valid values', () => {
57
+ expect(getReasoningEffort('gpt-5.2', 'low')).toBe('low');
58
+ expect(getReasoningEffort('gpt-5.2', 'medium')).toBe('medium');
59
+ expect(getReasoningEffort('gpt-5.2', 'high')).toBe('high');
60
+ expect(getReasoningEffort('gpt-5.2', 'xhigh')).toBe('xhigh');
61
+ });
62
+ it('should throw for invalid reasoning effort value', () => {
63
+ expect(() => getReasoningEffort('gpt-5.2', 'ultra')).toThrow('Invalid reasoning_effort: ultra. Allowed values: low, medium, high, xhigh.');
64
+ });
65
+ it('should throw for non-codex models', () => {
66
+ expect(() => getReasoningEffort('sonnet', 'high')).toThrow('reasoning_effort is only supported for Codex models (gpt-*).');
67
+ });
68
+ });
69
+ describe('buildCliCommand', () => {
70
+ describe('validation', () => {
71
+ it('should throw when workFolder is missing', () => {
72
+ expect(() => buildCliCommand({
73
+ prompt: 'hello',
74
+ workFolder: '',
75
+ cliPaths: DEFAULT_CLI_PATHS,
76
+ })).toThrow('Missing or invalid required parameter: workFolder');
77
+ });
78
+ it('should throw when neither prompt nor prompt_file is provided', () => {
79
+ expect(() => buildCliCommand({
80
+ workFolder: '/tmp',
81
+ cliPaths: DEFAULT_CLI_PATHS,
82
+ })).toThrow('Either prompt or prompt_file must be provided');
83
+ });
84
+ it('should throw when both prompt and prompt_file are provided', () => {
85
+ expect(() => buildCliCommand({
86
+ prompt: 'hello',
87
+ prompt_file: '/tmp/prompt.txt',
88
+ workFolder: '/tmp',
89
+ cliPaths: DEFAULT_CLI_PATHS,
90
+ })).toThrow('Cannot specify both prompt and prompt_file');
91
+ });
92
+ it('should throw when prompt_file does not exist', () => {
93
+ mockExistsSync.mockImplementation((p) => {
94
+ if (p === '/tmp/nonexistent.txt')
95
+ return false;
96
+ return true; // workFolder exists
97
+ });
98
+ expect(() => buildCliCommand({
99
+ prompt_file: '/tmp/nonexistent.txt',
100
+ workFolder: '/tmp',
101
+ cliPaths: DEFAULT_CLI_PATHS,
102
+ })).toThrow('Prompt file does not exist');
103
+ });
104
+ it('should throw when workFolder does not exist', () => {
105
+ mockExistsSync.mockReturnValue(false);
106
+ expect(() => buildCliCommand({
107
+ prompt: 'hello',
108
+ workFolder: '/nonexistent',
109
+ cliPaths: DEFAULT_CLI_PATHS,
110
+ })).toThrow('Working folder does not exist');
111
+ });
112
+ it('should read prompt from file', () => {
113
+ mockExistsSync.mockReturnValue(true);
114
+ mockReadFileSync.mockReturnValue('prompt from file');
115
+ const cmd = buildCliCommand({
116
+ prompt_file: '/tmp/prompt.txt',
117
+ workFolder: '/tmp',
118
+ cliPaths: DEFAULT_CLI_PATHS,
119
+ });
120
+ expect(cmd.prompt).toBe('prompt from file');
121
+ });
122
+ });
123
+ describe('claude agent', () => {
124
+ it('should build claude command with default model', () => {
125
+ const cmd = buildCliCommand({
126
+ prompt: 'hello world',
127
+ workFolder: '/tmp',
128
+ cliPaths: DEFAULT_CLI_PATHS,
129
+ });
130
+ expect(cmd.agent).toBe('claude');
131
+ expect(cmd.cliPath).toBe('/usr/bin/claude');
132
+ expect(cmd.args).toEqual([
133
+ '--dangerously-skip-permissions',
134
+ '--output-format',
135
+ 'stream-json',
136
+ '--verbose',
137
+ '-p',
138
+ 'hello world',
139
+ ]);
140
+ expect(cmd.resolvedModel).toBe('');
141
+ });
142
+ it('should build claude command with model', () => {
143
+ const cmd = buildCliCommand({
144
+ prompt: 'test',
145
+ workFolder: '/tmp',
146
+ model: 'sonnet',
147
+ cliPaths: DEFAULT_CLI_PATHS,
148
+ });
149
+ expect(cmd.agent).toBe('claude');
150
+ expect(cmd.args).toContain('--model');
151
+ expect(cmd.args).toContain('sonnet');
152
+ expect(cmd.resolvedModel).toBe('sonnet');
153
+ });
154
+ it('should build claude command with session_id', () => {
155
+ const cmd = buildCliCommand({
156
+ prompt: 'test',
157
+ workFolder: '/tmp',
158
+ session_id: 'ses-123',
159
+ cliPaths: DEFAULT_CLI_PATHS,
160
+ });
161
+ expect(cmd.args).toContain('-r');
162
+ expect(cmd.args).toContain('ses-123');
163
+ });
164
+ it('should resolve claude-ultra alias to opus', () => {
165
+ const cmd = buildCliCommand({
166
+ prompt: 'test',
167
+ workFolder: '/tmp',
168
+ model: 'claude-ultra',
169
+ cliPaths: DEFAULT_CLI_PATHS,
170
+ });
171
+ expect(cmd.agent).toBe('claude');
172
+ expect(cmd.resolvedModel).toBe('opus');
173
+ expect(cmd.args).toContain('opus');
174
+ });
175
+ });
176
+ describe('codex agent', () => {
177
+ it('should build codex command', () => {
178
+ const cmd = buildCliCommand({
179
+ prompt: 'test',
180
+ workFolder: '/tmp',
181
+ model: 'gpt-5.2-codex',
182
+ cliPaths: DEFAULT_CLI_PATHS,
183
+ });
184
+ expect(cmd.agent).toBe('codex');
185
+ expect(cmd.cliPath).toBe('/usr/bin/codex');
186
+ expect(cmd.args).toContain('exec');
187
+ expect(cmd.args).toContain('--full-auto');
188
+ expect(cmd.args).toContain('--json');
189
+ expect(cmd.args).toContain('--model');
190
+ expect(cmd.args).toContain('gpt-5.2-codex');
191
+ });
192
+ it('should build codex command with session_id using exec resume', () => {
193
+ const cmd = buildCliCommand({
194
+ prompt: 'test',
195
+ workFolder: '/tmp',
196
+ model: 'gpt-5.2',
197
+ session_id: 'codex-ses-456',
198
+ cliPaths: DEFAULT_CLI_PATHS,
199
+ });
200
+ expect(cmd.args[0]).toBe('exec');
201
+ expect(cmd.args[1]).toBe('resume');
202
+ expect(cmd.args[2]).toBe('codex-ses-456');
203
+ });
204
+ it('should build codex command with reasoning_effort', () => {
205
+ const cmd = buildCliCommand({
206
+ prompt: 'test',
207
+ workFolder: '/tmp',
208
+ model: 'gpt-5.2-codex',
209
+ reasoning_effort: 'high',
210
+ cliPaths: DEFAULT_CLI_PATHS,
211
+ });
212
+ expect(cmd.args).toContain('-c');
213
+ expect(cmd.args).toContain('model_reasoning_effort=high');
214
+ });
215
+ it('should resolve codex-ultra and default to xhigh reasoning', () => {
216
+ const cmd = buildCliCommand({
217
+ prompt: 'test',
218
+ workFolder: '/tmp',
219
+ model: 'codex-ultra',
220
+ cliPaths: DEFAULT_CLI_PATHS,
221
+ });
222
+ expect(cmd.agent).toBe('codex');
223
+ expect(cmd.resolvedModel).toBe('gpt-5.3-codex');
224
+ expect(cmd.args).toContain('-c');
225
+ expect(cmd.args).toContain('model_reasoning_effort=xhigh');
226
+ });
227
+ it('should allow overriding reasoning_effort for codex-ultra', () => {
228
+ const cmd = buildCliCommand({
229
+ prompt: 'test',
230
+ workFolder: '/tmp',
231
+ model: 'codex-ultra',
232
+ reasoning_effort: 'low',
233
+ cliPaths: DEFAULT_CLI_PATHS,
234
+ });
235
+ expect(cmd.args).toContain('model_reasoning_effort=low');
236
+ expect(cmd.args).not.toContain('model_reasoning_effort=xhigh');
237
+ });
238
+ });
239
+ describe('gemini agent', () => {
240
+ it('should build gemini command', () => {
241
+ const cmd = buildCliCommand({
242
+ prompt: 'test',
243
+ workFolder: '/tmp',
244
+ model: 'gemini-2.5-pro',
245
+ cliPaths: DEFAULT_CLI_PATHS,
246
+ });
247
+ expect(cmd.agent).toBe('gemini');
248
+ expect(cmd.cliPath).toBe('/usr/bin/gemini');
249
+ expect(cmd.args).toContain('-y');
250
+ expect(cmd.args).toContain('--output-format');
251
+ expect(cmd.args).toContain('json');
252
+ expect(cmd.args).toContain('--model');
253
+ expect(cmd.args).toContain('gemini-2.5-pro');
254
+ });
255
+ it('should build gemini command with session_id', () => {
256
+ const cmd = buildCliCommand({
257
+ prompt: 'test',
258
+ workFolder: '/tmp',
259
+ model: 'gemini-2.5-pro',
260
+ session_id: 'gem-789',
261
+ cliPaths: DEFAULT_CLI_PATHS,
262
+ });
263
+ expect(cmd.args).toContain('-r');
264
+ expect(cmd.args).toContain('gem-789');
265
+ });
266
+ it('should resolve gemini-ultra alias', () => {
267
+ const cmd = buildCliCommand({
268
+ prompt: 'test',
269
+ workFolder: '/tmp',
270
+ model: 'gemini-ultra',
271
+ cliPaths: DEFAULT_CLI_PATHS,
272
+ });
273
+ expect(cmd.agent).toBe('gemini');
274
+ expect(cmd.resolvedModel).toBe('gemini-3-pro-preview');
275
+ });
276
+ });
277
+ });
278
+ });
@@ -0,0 +1,145 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { resolve as pathResolve, isAbsolute } from 'node:path';
3
+ // Model alias mappings for user-friendly model names
4
+ export const MODEL_ALIASES = {
5
+ 'claude-ultra': 'opus',
6
+ 'codex-ultra': 'gpt-5.3-codex',
7
+ 'gemini-ultra': 'gemini-3-pro-preview'
8
+ };
9
+ export const ALLOWED_REASONING_EFFORTS = new Set(['low', 'medium', 'high', 'xhigh']);
10
+ /**
11
+ * Resolves model aliases to their full model names
12
+ * @param model - The model name or alias to resolve
13
+ * @returns The full model name, or the original value if no alias exists
14
+ */
15
+ export function resolveModelAlias(model) {
16
+ return MODEL_ALIASES[model] || model;
17
+ }
18
+ /**
19
+ * Validates and normalizes reasoning effort parameter.
20
+ * @returns normalized reasoning effort string, or '' if not applicable
21
+ * @throws Error for invalid values (plain Error, not MCP-specific)
22
+ */
23
+ export function getReasoningEffort(model, rawValue) {
24
+ if (typeof rawValue !== 'string') {
25
+ return '';
26
+ }
27
+ const trimmed = rawValue.trim();
28
+ if (!trimmed) {
29
+ return '';
30
+ }
31
+ const normalized = trimmed.toLowerCase();
32
+ if (!ALLOWED_REASONING_EFFORTS.has(normalized)) {
33
+ throw new Error(`Invalid reasoning_effort: ${rawValue}. Allowed values: low, medium, high, xhigh.`);
34
+ }
35
+ if (!model.startsWith('gpt-')) {
36
+ throw new Error('reasoning_effort is only supported for Codex models (gpt-*).');
37
+ }
38
+ return normalized;
39
+ }
40
+ /**
41
+ * Build a CLI command from the given options.
42
+ * This is a pure function (aside from filesystem reads for prompt_file / workFolder validation).
43
+ * @throws Error on validation failures
44
+ */
45
+ export function buildCliCommand(options) {
46
+ // Validate workFolder
47
+ if (!options.workFolder || typeof options.workFolder !== 'string') {
48
+ throw new Error('Missing or invalid required parameter: workFolder');
49
+ }
50
+ // Validate prompt / prompt_file
51
+ const hasPrompt = !!options.prompt && typeof options.prompt === 'string' && options.prompt.trim() !== '';
52
+ const hasPromptFile = !!options.prompt_file && typeof options.prompt_file === 'string' && options.prompt_file.trim() !== '';
53
+ if (!hasPrompt && !hasPromptFile) {
54
+ throw new Error('Either prompt or prompt_file must be provided');
55
+ }
56
+ if (hasPrompt && hasPromptFile) {
57
+ throw new Error('Cannot specify both prompt and prompt_file. Please use only one.');
58
+ }
59
+ // Determine prompt
60
+ let prompt;
61
+ if (hasPrompt) {
62
+ prompt = options.prompt;
63
+ }
64
+ else {
65
+ const promptFilePath = isAbsolute(options.prompt_file)
66
+ ? options.prompt_file
67
+ : pathResolve(options.workFolder, options.prompt_file);
68
+ if (!existsSync(promptFilePath)) {
69
+ throw new Error(`Prompt file does not exist: ${promptFilePath}`);
70
+ }
71
+ try {
72
+ prompt = readFileSync(promptFilePath, 'utf-8');
73
+ }
74
+ catch (error) {
75
+ throw new Error(`Failed to read prompt file: ${error.message}`);
76
+ }
77
+ }
78
+ // Resolve workFolder
79
+ const cwd = pathResolve(options.workFolder);
80
+ if (!existsSync(cwd)) {
81
+ throw new Error(`Working folder does not exist: ${options.workFolder}`);
82
+ }
83
+ // Resolve model
84
+ const rawModel = options.model || '';
85
+ const resolvedModel = resolveModelAlias(rawModel);
86
+ // Special handling for codex-ultra: default to high reasoning effort if not specified
87
+ let reasoningEffortArg = options.reasoning_effort;
88
+ if (rawModel === 'codex-ultra' && !reasoningEffortArg) {
89
+ reasoningEffortArg = 'xhigh';
90
+ }
91
+ const reasoningEffort = getReasoningEffort(resolvedModel, reasoningEffortArg);
92
+ // Determine agent
93
+ let agent;
94
+ if (resolvedModel.startsWith('gpt-')) {
95
+ agent = 'codex';
96
+ }
97
+ else if (resolvedModel.startsWith('gemini')) {
98
+ agent = 'gemini';
99
+ }
100
+ else {
101
+ agent = 'claude';
102
+ }
103
+ // Build CLI path and args
104
+ let cliPath;
105
+ let args;
106
+ if (agent === 'codex') {
107
+ cliPath = options.cliPaths.codex;
108
+ if (options.session_id && typeof options.session_id === 'string') {
109
+ args = ['exec', 'resume', options.session_id];
110
+ }
111
+ else {
112
+ args = ['exec'];
113
+ }
114
+ if (reasoningEffort) {
115
+ args.push('-c', `model_reasoning_effort=${reasoningEffort}`);
116
+ }
117
+ if (resolvedModel) {
118
+ args.push('--model', resolvedModel);
119
+ }
120
+ args.push('--full-auto', '--json', prompt);
121
+ }
122
+ else if (agent === 'gemini') {
123
+ cliPath = options.cliPaths.gemini;
124
+ args = ['-y', '--output-format', 'json'];
125
+ if (options.session_id && typeof options.session_id === 'string') {
126
+ args.push('-r', options.session_id);
127
+ }
128
+ if (resolvedModel) {
129
+ args.push('--model', resolvedModel);
130
+ }
131
+ args.push(prompt);
132
+ }
133
+ else {
134
+ cliPath = options.cliPaths.claude;
135
+ args = ['--dangerously-skip-permissions', '--output-format', 'stream-json', '--verbose'];
136
+ if (options.session_id && typeof options.session_id === 'string') {
137
+ args.push('-r', options.session_id);
138
+ }
139
+ args.push('-p', prompt);
140
+ if (resolvedModel) {
141
+ args.push('--model', resolvedModel);
142
+ }
143
+ }
144
+ return { cliPath, args, cwd, agent, prompt, resolvedModel };
145
+ }
@@ -0,0 +1,85 @@
1
+ #!/usr/bin/env node
2
+ import { parseClaudeOutput, parseCodexOutput, parseGeminiOutput } from './parsers.js';
3
+ const AGENTS = ['claude', 'codex', 'gemini'];
4
+ const USAGE = `Usage: npm run -s cli.run.parse -- --agent <claude|codex|gemini>
5
+
6
+ Reads raw CLI output from stdin and outputs parsed JSON to stdout.
7
+
8
+ Options:
9
+ --agent Agent type: claude, codex, or gemini (required)
10
+ --help Show this help message
11
+
12
+ Examples:
13
+ npm run -s cli.run -- --model sonnet --workFolder /tmp --prompt "hi" > raw.txt
14
+ npm run -s cli.run.parse -- --agent claude < raw.txt
15
+
16
+ # Or pipe directly
17
+ npm run -s cli.run -- --model sonnet --workFolder /tmp --prompt "hi" | npm run -s cli.run.parse -- --agent claude
18
+ `;
19
+ function parseArgs(argv) {
20
+ const result = {};
21
+ for (let i = 0; i < argv.length; i++) {
22
+ const arg = argv[i];
23
+ if (!arg.startsWith('--'))
24
+ continue;
25
+ const eqIdx = arg.indexOf('=');
26
+ if (eqIdx !== -1) {
27
+ result[arg.slice(2, eqIdx)] = arg.slice(eqIdx + 1);
28
+ }
29
+ else {
30
+ const key = arg.slice(2);
31
+ const next = argv[i + 1];
32
+ if (next !== undefined && !next.startsWith('--')) {
33
+ result[key] = next;
34
+ i++;
35
+ }
36
+ else {
37
+ result[key] = '';
38
+ }
39
+ }
40
+ }
41
+ return result;
42
+ }
43
+ function readStdin() {
44
+ return new Promise((resolve, reject) => {
45
+ const chunks = [];
46
+ process.stdin.on('data', (chunk) => chunks.push(chunk));
47
+ process.stdin.on('end', () => resolve(Buffer.concat(chunks).toString('utf-8')));
48
+ process.stdin.on('error', reject);
49
+ });
50
+ }
51
+ async function main() {
52
+ const args = parseArgs(process.argv.slice(2));
53
+ if ('help' in args) {
54
+ process.stdout.write(USAGE);
55
+ process.exit(0);
56
+ }
57
+ const agent = args.agent;
58
+ if (!agent || !AGENTS.includes(agent)) {
59
+ process.stderr.write(`Error: --agent is required (claude, codex, or gemini)\n\n`);
60
+ process.stderr.write(USAGE);
61
+ process.exit(1);
62
+ }
63
+ const input = await readStdin();
64
+ if (!input.trim()) {
65
+ process.stderr.write('Error: no input received from stdin\n');
66
+ process.exit(1);
67
+ }
68
+ let parsed = null;
69
+ switch (agent) {
70
+ case 'claude':
71
+ parsed = parseClaudeOutput(input);
72
+ break;
73
+ case 'codex':
74
+ parsed = parseCodexOutput(input);
75
+ break;
76
+ case 'gemini':
77
+ parsed = parseGeminiOutput(input);
78
+ break;
79
+ }
80
+ process.stdout.write(JSON.stringify(parsed, null, 2) + '\n');
81
+ }
82
+ main().catch((err) => {
83
+ process.stderr.write(`Fatal error: ${err.message}\n`);
84
+ process.exit(1);
85
+ });
package/dist/cli.js ADDED
@@ -0,0 +1,111 @@
1
+ #!/usr/bin/env node
2
+ import { spawn } from 'node:child_process';
3
+ import { buildCliCommand } from './cli-builder.js';
4
+ import { findClaudeCli, findCodexCli, findGeminiCli } from './server.js';
5
+ /**
6
+ * Minimal argv parser. No external dependencies.
7
+ * Supports: --key value, --key=value
8
+ */
9
+ function parseArgs(argv) {
10
+ const result = {};
11
+ for (let i = 0; i < argv.length; i++) {
12
+ const arg = argv[i];
13
+ if (!arg.startsWith('--'))
14
+ continue;
15
+ const eqIdx = arg.indexOf('=');
16
+ if (eqIdx !== -1) {
17
+ // --key=value
18
+ result[arg.slice(2, eqIdx)] = arg.slice(eqIdx + 1);
19
+ }
20
+ else {
21
+ // --key value
22
+ const key = arg.slice(2);
23
+ const next = argv[i + 1];
24
+ if (next !== undefined && !next.startsWith('--')) {
25
+ result[key] = next;
26
+ i++;
27
+ }
28
+ else {
29
+ result[key] = '';
30
+ }
31
+ }
32
+ }
33
+ return result;
34
+ }
35
+ const USAGE = `Usage: npm run -s cli.run -- --model <model> --workFolder <path> --prompt "..." [options]
36
+
37
+ Options:
38
+ --model Model name or alias (e.g. sonnet, opus, gpt-5.2-codex, gemini-2.5-pro)
39
+ --workFolder Working directory (absolute path)
40
+ --prompt Prompt string (mutually exclusive with --prompt_file)
41
+ --prompt_file Path to a file containing the prompt
42
+ --session_id Session ID to resume
43
+ --reasoning_effort Codex only: low, medium, high, xhigh
44
+ --help Show this help message
45
+
46
+ Raw CLI output goes to stdout. Use cli.run.parse to parse the output:
47
+ npm run -s cli.run -- ... > raw.txt
48
+ npm run -s cli.run.parse -- --agent claude < raw.txt
49
+ `;
50
+ async function main() {
51
+ const args = parseArgs(process.argv.slice(2));
52
+ if ('help' in args) {
53
+ process.stdout.write(USAGE);
54
+ process.exit(0);
55
+ }
56
+ if (!args.workFolder) {
57
+ process.stderr.write('Error: --workFolder is required\n\n');
58
+ process.stderr.write(USAGE);
59
+ process.exit(1);
60
+ }
61
+ if (!args.prompt && !args.prompt_file) {
62
+ process.stderr.write('Error: --prompt or --prompt_file is required\n\n');
63
+ process.stderr.write(USAGE);
64
+ process.exit(1);
65
+ }
66
+ // Resolve CLI paths
67
+ const cliPaths = {
68
+ claude: findClaudeCli(),
69
+ codex: findCodexCli(),
70
+ gemini: findGeminiCli(),
71
+ };
72
+ // Build command
73
+ let cmd;
74
+ try {
75
+ cmd = buildCliCommand({
76
+ prompt: args.prompt || undefined,
77
+ prompt_file: args.prompt_file || undefined,
78
+ workFolder: args.workFolder,
79
+ model: args.model || undefined,
80
+ session_id: args.session_id || undefined,
81
+ reasoning_effort: args.reasoning_effort || undefined,
82
+ cliPaths,
83
+ });
84
+ }
85
+ catch (error) {
86
+ process.stderr.write(`Error: ${error.message}\n`);
87
+ process.exit(1);
88
+ }
89
+ // Log agent info to stderr (does not pollute stdout)
90
+ process.stderr.write(`[cli.run] agent=${cmd.agent} model=${cmd.resolvedModel || '(default)'}\n`);
91
+ // Spawn foreground process — raw output passthrough
92
+ const child = spawn(cmd.cliPath, cmd.args, {
93
+ cwd: cmd.cwd,
94
+ stdio: 'inherit',
95
+ detached: false,
96
+ });
97
+ const exitCode = await new Promise((resolve) => {
98
+ child.on('close', (code) => {
99
+ resolve(code ?? 1);
100
+ });
101
+ child.on('error', (err) => {
102
+ process.stderr.write(`Process error: ${err.message}\n`);
103
+ resolve(1);
104
+ });
105
+ });
106
+ process.exit(exitCode);
107
+ }
108
+ main().catch((err) => {
109
+ process.stderr.write(`Fatal error: ${err.message}\n`);
110
+ process.exit(1);
111
+ });