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 +7 -0
- package/CONTRIBUTING.md +10 -0
- package/dist/__tests__/cli-builder.test.js +278 -0
- package/dist/cli-builder.js +145 -0
- package/dist/cli-parse.js +85 -0
- package/dist/cli.js +111 -0
- package/dist/server.js +24 -139
- package/docs/development.md +77 -6
- package/package.json +4 -2
- package/src/__tests__/cli-builder.test.ts +345 -0
- package/src/cli-builder.ts +190 -0
- package/src/cli-parse.ts +96 -0
- package/src/cli.ts +121 -0
- package/src/server.ts +24 -186
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
|
+
});
|