ai-cli-mcp 2.19.0 → 2.20.1

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.
Files changed (100) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/README.ja.md +34 -8
  3. package/README.md +41 -8
  4. package/dist/app/cli.js +1 -0
  5. package/dist/app/mcp.js +64 -12
  6. package/dist/cli-builder.js +13 -6
  7. package/dist/cli-process-service.js +76 -91
  8. package/dist/cli-utils.js +6 -0
  9. package/dist/cli.js +1 -1
  10. package/dist/model-catalog.js +3 -2
  11. package/dist/parsers.js +8 -2
  12. package/package.json +27 -3
  13. package/server.json +3 -3
  14. package/.gemini/settings.json +0 -11
  15. package/.github/dependabot.yml +0 -28
  16. package/.github/pull_request_template.md +0 -28
  17. package/.github/workflows/ci.yml +0 -34
  18. package/.github/workflows/dependency-review.yml +0 -22
  19. package/.github/workflows/publish.yml +0 -89
  20. package/.github/workflows/test.yml +0 -20
  21. package/.github/workflows/watch-session-prs.yml +0 -276
  22. package/.husky/pre-commit +0 -1
  23. package/.mcp.json +0 -11
  24. package/.releaserc.json +0 -18
  25. package/.vscode/settings.json +0 -3
  26. package/CONTRIBUTING.md +0 -81
  27. package/dist/__tests__/app-cli.test.js +0 -392
  28. package/dist/__tests__/cli-bin-smoke.test.js +0 -101
  29. package/dist/__tests__/cli-builder.test.js +0 -442
  30. package/dist/__tests__/cli-process-service.test.js +0 -655
  31. package/dist/__tests__/cli-utils.test.js +0 -171
  32. package/dist/__tests__/e2e.test.js +0 -256
  33. package/dist/__tests__/edge-cases.test.js +0 -130
  34. package/dist/__tests__/error-cases.test.js +0 -292
  35. package/dist/__tests__/mcp-contract.test.js +0 -636
  36. package/dist/__tests__/mocks.js +0 -32
  37. package/dist/__tests__/model-alias.test.js +0 -36
  38. package/dist/__tests__/parsers.test.js +0 -646
  39. package/dist/__tests__/peek.test.js +0 -36
  40. package/dist/__tests__/process-management.test.js +0 -949
  41. package/dist/__tests__/server.test.js +0 -809
  42. package/dist/__tests__/setup.js +0 -11
  43. package/dist/__tests__/utils/claude-mock.js +0 -80
  44. package/dist/__tests__/utils/mcp-client.js +0 -121
  45. package/dist/__tests__/utils/opencode-mock.js +0 -91
  46. package/dist/__tests__/utils/persistent-mock.js +0 -28
  47. package/dist/__tests__/utils/test-helpers.js +0 -11
  48. package/dist/__tests__/validation.test.js +0 -308
  49. package/dist/__tests__/version-print.test.js +0 -65
  50. package/dist/__tests__/wait.test.js +0 -260
  51. package/docs/RELEASE_CHECKLIST.md +0 -65
  52. package/docs/cli-architecture.md +0 -275
  53. package/docs/concept.md +0 -154
  54. package/docs/development.md +0 -156
  55. package/docs/e2e-testing.md +0 -148
  56. package/docs/prd.md +0 -146
  57. package/docs/session-stacking.md +0 -67
  58. package/src/__tests__/app-cli.test.ts +0 -495
  59. package/src/__tests__/cli-bin-smoke.test.ts +0 -136
  60. package/src/__tests__/cli-builder.test.ts +0 -549
  61. package/src/__tests__/cli-process-service.test.ts +0 -759
  62. package/src/__tests__/cli-utils.test.ts +0 -200
  63. package/src/__tests__/e2e.test.ts +0 -311
  64. package/src/__tests__/edge-cases.test.ts +0 -176
  65. package/src/__tests__/error-cases.test.ts +0 -370
  66. package/src/__tests__/mcp-contract.test.ts +0 -755
  67. package/src/__tests__/mocks.ts +0 -35
  68. package/src/__tests__/model-alias.test.ts +0 -44
  69. package/src/__tests__/parsers.test.ts +0 -730
  70. package/src/__tests__/peek.test.ts +0 -44
  71. package/src/__tests__/process-management.test.ts +0 -1129
  72. package/src/__tests__/server.test.ts +0 -1020
  73. package/src/__tests__/setup.ts +0 -13
  74. package/src/__tests__/utils/claude-mock.ts +0 -87
  75. package/src/__tests__/utils/mcp-client.ts +0 -159
  76. package/src/__tests__/utils/opencode-mock.ts +0 -108
  77. package/src/__tests__/utils/persistent-mock.ts +0 -33
  78. package/src/__tests__/utils/test-helpers.ts +0 -13
  79. package/src/__tests__/validation.test.ts +0 -369
  80. package/src/__tests__/version-print.test.ts +0 -81
  81. package/src/__tests__/wait.test.ts +0 -302
  82. package/src/app/cli.ts +0 -424
  83. package/src/app/mcp.ts +0 -466
  84. package/src/bin/ai-cli-mcp.ts +0 -7
  85. package/src/bin/ai-cli.ts +0 -11
  86. package/src/cli-builder.ts +0 -274
  87. package/src/cli-parse.ts +0 -105
  88. package/src/cli-process-service.ts +0 -709
  89. package/src/cli-utils.ts +0 -258
  90. package/src/cli.ts +0 -124
  91. package/src/model-catalog.ts +0 -87
  92. package/src/parsers.ts +0 -965
  93. package/src/peek.ts +0 -95
  94. package/src/process-result.ts +0 -88
  95. package/src/process-service.ts +0 -368
  96. package/src/server.ts +0 -10
  97. package/tsconfig.json +0 -16
  98. package/vitest.config.e2e.ts +0 -27
  99. package/vitest.config.ts +0 -22
  100. package/vitest.config.unit.ts +0 -28
@@ -1,65 +0,0 @@
1
- import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
2
- import { mkdtempSync, rmSync } from 'node:fs';
3
- import { join } from 'node:path';
4
- import { tmpdir } from 'node:os';
5
- import { createTestClient } from './utils/mcp-client.js';
6
- import { getSharedMock } from './utils/persistent-mock.js';
7
- describe('Version Print on First Use', () => {
8
- let client;
9
- let testDir;
10
- let consoleErrorSpy;
11
- beforeEach(async () => {
12
- // Ensure mock exists
13
- await getSharedMock();
14
- // Create a temporary directory for test files
15
- testDir = mkdtempSync(join(tmpdir(), 'claude-code-test-'));
16
- // Spy on console.error
17
- consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => { });
18
- client = createTestClient({ debug: false });
19
- await client.connect();
20
- });
21
- afterEach(async () => {
22
- // Disconnect client
23
- await client.disconnect();
24
- // Clean up test directory
25
- rmSync(testDir, { recursive: true, force: true });
26
- // Restore console.error spy
27
- consoleErrorSpy.mockRestore();
28
- });
29
- it('should print version and startup time only on first use', async () => {
30
- // First tool call
31
- await client.callTool('run', {
32
- prompt: 'echo "test 1"',
33
- workFolder: testDir,
34
- });
35
- // Find the version print in the console.error calls
36
- const findVersionCall = (calls) => {
37
- return calls.find(call => {
38
- const str = call[1] || call[0]; // message might be first or second param
39
- return typeof str === 'string' && str.includes('ai_cli_mcp v') && str.includes('started at');
40
- });
41
- };
42
- // Check that version was printed on first use
43
- const versionCall = findVersionCall(consoleErrorSpy.mock.calls);
44
- expect(versionCall).toBeDefined();
45
- expect(versionCall[1]).toMatch(/ai_cli_mcp v[0-9]+\.[0-9]+\.[0-9]+ started at \d{4}-\d{2}-\d{2}T/);
46
- // Clear the spy but keep the spy active
47
- consoleErrorSpy.mockClear();
48
- // Second tool call
49
- await client.callTool('run', {
50
- prompt: 'echo "test 2"',
51
- workFolder: testDir,
52
- });
53
- // Check that version was NOT printed on second use
54
- const secondVersionCall = findVersionCall(consoleErrorSpy.mock.calls);
55
- expect(secondVersionCall).toBeUndefined();
56
- // Third tool call
57
- await client.callTool('run', {
58
- prompt: 'echo "test 3"',
59
- workFolder: testDir,
60
- });
61
- // Should still not have been called with version print
62
- const thirdVersionCall = findVersionCall(consoleErrorSpy.mock.calls);
63
- expect(thirdVersionCall).toBeUndefined();
64
- });
65
- });
@@ -1,260 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
- import { EventEmitter } from 'node:events';
3
- import { spawn } from 'node:child_process';
4
- import { homedir } from 'node:os';
5
- import { existsSync } from 'node:fs';
6
- import { Server } from '@modelcontextprotocol/sdk/server/index.js';
7
- // Mock dependencies
8
- vi.mock('node:child_process');
9
- vi.mock('node:fs');
10
- vi.mock('node:os');
11
- vi.mock('node:path', () => ({
12
- resolve: vi.fn((path) => path),
13
- join: vi.fn((...args) => args.join('/')),
14
- isAbsolute: vi.fn((path) => path.startsWith('/')),
15
- dirname: vi.fn((path) => '/tmp')
16
- }));
17
- vi.mock('@modelcontextprotocol/sdk/server/stdio.js');
18
- vi.mock('@modelcontextprotocol/sdk/types.js', () => ({
19
- ListToolsRequestSchema: { name: 'listTools' },
20
- CallToolRequestSchema: { name: 'callTool' },
21
- ErrorCode: {
22
- InternalError: 'InternalError',
23
- MethodNotFound: 'MethodNotFound',
24
- InvalidParams: 'InvalidParams'
25
- },
26
- McpError: class extends Error {
27
- code;
28
- constructor(code, message) {
29
- super(message);
30
- this.code = code;
31
- }
32
- }
33
- }));
34
- vi.mock('@modelcontextprotocol/sdk/server/index.js', () => ({
35
- Server: vi.fn().mockImplementation(function () {
36
- this.setRequestHandler = vi.fn();
37
- this.connect = vi.fn();
38
- this.close = vi.fn();
39
- this.onerror = undefined;
40
- return this;
41
- }),
42
- }));
43
- // Mock package.json
44
- vi.mock('../../package.json', () => ({
45
- default: { version: '1.0.0-test' }
46
- }));
47
- // Re-import after mocks
48
- const mockSpawn = vi.mocked(spawn);
49
- const mockHomedir = vi.mocked(homedir);
50
- const mockExistsSync = vi.mocked(existsSync);
51
- describe('Wait Tool Tests', () => {
52
- let handlers;
53
- let mockServerInstance;
54
- let server;
55
- // Setup function to initialize server with mocks
56
- const setupServer = async () => {
57
- vi.resetModules();
58
- handlers = new Map();
59
- // Mock Server implementation to capture handlers
60
- vi.mocked(Server).mockImplementation(function () {
61
- this.setRequestHandler = vi.fn((schema, handler) => {
62
- handlers.set(schema.name, handler);
63
- });
64
- this.connect = vi.fn();
65
- this.close = vi.fn();
66
- return this;
67
- });
68
- const module = await import('../server.js');
69
- // @ts-ignore
70
- const { ClaudeCodeServer } = module;
71
- server = new ClaudeCodeServer();
72
- mockServerInstance = vi.mocked(Server).mock.results[0].value;
73
- };
74
- beforeEach(async () => {
75
- mockHomedir.mockReturnValue('/home/user');
76
- mockExistsSync.mockReturnValue(true);
77
- await setupServer();
78
- });
79
- afterEach(() => {
80
- vi.clearAllMocks();
81
- vi.useRealTimers();
82
- });
83
- const createMockProcess = (pid) => {
84
- const mockProcess = new EventEmitter();
85
- mockProcess.pid = pid;
86
- mockProcess.stdout = new EventEmitter();
87
- mockProcess.stderr = new EventEmitter();
88
- mockProcess.stdout.on = vi.fn();
89
- mockProcess.stderr.on = vi.fn();
90
- mockProcess.kill = vi.fn();
91
- return mockProcess;
92
- };
93
- it('should wait for a single running process', async () => {
94
- const callToolHandler = handlers.get('callTool');
95
- const mockProcess = createMockProcess(12345);
96
- mockSpawn.mockReturnValue(mockProcess);
97
- // Start a process first
98
- await callToolHandler({
99
- params: {
100
- name: 'run',
101
- arguments: {
102
- prompt: 'test prompt',
103
- workFolder: '/tmp'
104
- }
105
- }
106
- });
107
- // Mock process output accumulation (simulated internally by server)
108
- // We need to access the process manager or simulate events
109
- // Call wait
110
- const waitPromise = callToolHandler({
111
- params: {
112
- name: 'wait',
113
- arguments: {
114
- pids: [12345]
115
- }
116
- }
117
- });
118
- // Simulate process completion after a delay
119
- setTimeout(() => {
120
- mockProcess.stdout.emit('data', 'Process output');
121
- mockProcess.emit('close', 0);
122
- }, 10);
123
- const result = await waitPromise;
124
- const response = JSON.parse(result.content[0].text);
125
- expect(response).toHaveLength(1);
126
- expect(response[0].pid).toBe(12345);
127
- expect(response[0].status).toBe('completed');
128
- // expect(response[0].stdout).toBe('Process output'); // Flaky test
129
- });
130
- it('should return immediately if process is already completed', async () => {
131
- const callToolHandler = handlers.get('callTool');
132
- const mockProcess = createMockProcess(12346);
133
- mockSpawn.mockReturnValue(mockProcess);
134
- // Start process
135
- await callToolHandler({
136
- params: {
137
- name: 'run',
138
- arguments: {
139
- prompt: 'test',
140
- workFolder: '/tmp'
141
- }
142
- }
143
- });
144
- // Complete immediately
145
- mockProcess.emit('close', 0);
146
- // Call wait
147
- const result = await callToolHandler({
148
- params: {
149
- name: 'wait',
150
- arguments: {
151
- pids: [12346]
152
- }
153
- }
154
- });
155
- const response = JSON.parse(result.content[0].text);
156
- expect(response[0].status).toBe('completed');
157
- });
158
- it('should wait for multiple processes', async () => {
159
- const callToolHandler = handlers.get('callTool');
160
- // Process 1
161
- const p1 = createMockProcess(101);
162
- mockSpawn.mockReturnValueOnce(p1);
163
- await callToolHandler({
164
- params: { name: 'run', arguments: { prompt: 'p1', workFolder: '/tmp' } }
165
- });
166
- // Process 2
167
- const p2 = createMockProcess(102);
168
- mockSpawn.mockReturnValueOnce(p2);
169
- await callToolHandler({
170
- params: { name: 'run', arguments: { prompt: 'p2', workFolder: '/tmp' } }
171
- });
172
- // Wait for both
173
- const waitPromise = callToolHandler({
174
- params: {
175
- name: 'wait',
176
- arguments: { pids: [101, 102] }
177
- }
178
- });
179
- // Finish p1
180
- setTimeout(() => { p1.emit('close', 0); }, 10);
181
- // Finish p2 later
182
- setTimeout(() => { p2.emit('close', 0); }, 30);
183
- const result = await waitPromise;
184
- const response = JSON.parse(result.content[0].text);
185
- expect(response).toHaveLength(2);
186
- expect(response.find((r) => r.pid === 101).status).toBe('completed');
187
- expect(response.find((r) => r.pid === 102).status).toBe('completed');
188
- });
189
- it('should clear timeout timers after wait resolves', async () => {
190
- vi.useFakeTimers();
191
- const callToolHandler = handlers.get('callTool');
192
- const mockProcess = createMockProcess(12348);
193
- mockSpawn.mockReturnValue(mockProcess);
194
- await callToolHandler({
195
- params: {
196
- name: 'run',
197
- arguments: {
198
- prompt: 'test prompt',
199
- workFolder: '/tmp'
200
- }
201
- }
202
- });
203
- const waitPromise = callToolHandler({
204
- params: {
205
- name: 'wait',
206
- arguments: {
207
- pids: [12348],
208
- timeout: 180
209
- }
210
- }
211
- });
212
- mockProcess.emit('close', 0);
213
- await vi.runAllTicks();
214
- const result = await waitPromise;
215
- const response = JSON.parse(result.content[0].text);
216
- expect(response[0].status).toBe('completed');
217
- expect(vi.getTimerCount()).toBe(0);
218
- });
219
- it('should throw error for non-existent PID', async () => {
220
- const callToolHandler = handlers.get('callTool');
221
- try {
222
- await callToolHandler({
223
- params: {
224
- name: 'wait',
225
- arguments: { pids: [99999] }
226
- }
227
- });
228
- expect.fail('Should have thrown');
229
- }
230
- catch (error) {
231
- expect(error.message).toContain('Process with PID 99999 not found');
232
- }
233
- });
234
- it('should handle timeout', async () => {
235
- const callToolHandler = handlers.get('callTool');
236
- const mockProcess = createMockProcess(12347);
237
- mockSpawn.mockReturnValue(mockProcess);
238
- await callToolHandler({
239
- params: { name: 'run', arguments: { prompt: 'test', workFolder: '/tmp' } }
240
- });
241
- // Call wait with short timeout
242
- const waitPromise = callToolHandler({
243
- params: {
244
- name: 'wait',
245
- arguments: {
246
- pids: [12347],
247
- timeout: 0.1 // 100ms
248
- }
249
- }
250
- });
251
- // Don't emit close event
252
- try {
253
- await waitPromise;
254
- expect.fail('Should have thrown');
255
- }
256
- catch (error) {
257
- expect(error.message).toContain('Timed out');
258
- }
259
- });
260
- });
@@ -1,65 +0,0 @@
1
- # Release Process
2
-
3
- This project uses [semantic-release](https://semantic-release.gitbook.io/) for automated versioning and publishing.
4
-
5
- ## How It Works
6
-
7
- 1. **Commit with Conventional Commits format** to `develop` branch
8
- 2. **CI automatically determines version** based on commit messages
9
- 3. **Automatic release**: version bump, CHANGELOG update, npm publish, GitHub Release
10
-
11
- ## Commit Message Format
12
-
13
- Use [Conventional Commits](https://www.conventionalcommits.org/) format:
14
-
15
- | Type | Description | Version Bump |
16
- |------|-------------|--------------|
17
- | `fix:` | Bug fixes | Patch (1.0.0 → 1.0.1) |
18
- | `feat:` | New features | Minor (1.0.0 → 1.1.0) |
19
- | `feat!:` or `BREAKING CHANGE:` | Breaking changes | Major (1.0.0 → 2.0.0) |
20
- | `docs:`, `chore:`, `style:`, `refactor:`, `test:` | Other changes | No release |
21
-
22
- ### Examples
23
-
24
- ```bash
25
- # Patch release
26
- git commit -m "fix: resolve session_id not working for Codex"
27
-
28
- # Minor release
29
- git commit -m "feat: add support for new model"
30
-
31
- # Major release
32
- git commit -m "feat!: change API response format"
33
- # or
34
- git commit -m "feat: change API response format
35
-
36
- BREAKING CHANGE: response structure has changed"
37
- ```
38
-
39
- ## Pre-Merge Checklist
40
-
41
- Before merging to `develop`:
42
-
43
- - [ ] Tests pass locally (`npm test`)
44
- - [ ] Build succeeds (`npm run build`)
45
- - [ ] Commit messages follow Conventional Commits format
46
- - [ ] PR has been reviewed (if applicable)
47
-
48
- ## Important: Git Tags
49
-
50
- semantic-release uses git tags to determine the current version. **Tags must exist on the `develop` branch.**
51
-
52
- If releases fail with version errors:
53
-
54
- 1. Check existing tags: `git tag -l 'v*'`
55
- 2. Ensure the latest version tag exists on `develop`
56
- 3. If missing, create it: `git tag vX.X.X && git push origin vX.X.X`
57
-
58
- ## npm Trusted Publishing Setup
59
-
60
- This project uses OIDC trusted publishing (no npm token required).
61
-
62
- Configuration on npmjs.com:
63
- - Organization/user: `mkXultra`
64
- - Repository: `ai-cli-mcp`
65
- - Workflow filename: `publish.yml`
@@ -1,275 +0,0 @@
1
- # AI CLI Architecture Plan
2
-
3
- ## Goal
4
-
5
- `ai-cli-mcp` package will expose two global commands:
6
-
7
- - `ai-cli`: human-facing production CLI
8
- - `ai-cli-mcp`: MCP server entrypoint for backward compatibility
9
-
10
- The package name stays `ai-cli-mcp` for now. We do not introduce a daemon. We keep the product as a thin wrapper over Claude Code, Codex CLI, and Gemini CLI.
11
-
12
- ## Non-Goals
13
-
14
- - Renaming the npm package in this phase
15
- - Introducing a long-running background daemon
16
- - Introducing a new public job identifier such as `run_id`
17
- - Making the CLI responsible for deep process orchestration beyond launching and observing AI CLI processes
18
- - Capturing exit codes in the first production CLI iteration
19
-
20
- ## Product Shape
21
-
22
- ### `ai-cli`
23
-
24
- `ai-cli` is the primary CLI for humans.
25
-
26
- Supported commands:
27
-
28
- - `ai-cli run`
29
- - `ai-cli wait`
30
- - `ai-cli ps`
31
- - `ai-cli result`
32
- - `ai-cli kill`
33
- - `ai-cli cleanup`
34
- - `ai-cli models`
35
- - `ai-cli doctor`
36
- - `ai-cli mcp`
37
-
38
- Behavior:
39
-
40
- - Running `ai-cli` with no subcommand prints help
41
- - Public process identity is `pid`
42
- - `--cwd` is the working directory flag
43
- - Output format should stay close to MCP responses
44
-
45
- ### `ai-cli-mcp`
46
-
47
- `ai-cli-mcp` remains the MCP-focused command.
48
-
49
- Behavior:
50
-
51
- - Running `ai-cli-mcp` with no arguments starts the MCP server
52
- - This command exists for compatibility with existing users and MCP configurations
53
-
54
- ## Public Command Semantics
55
-
56
- ### `ai-cli run`
57
-
58
- Starts the target AI CLI in the background and returns immediately.
59
-
60
- Properties:
61
-
62
- - Returns MCP-like JSON including `pid`, `status`, `agent`, and `message`
63
- - Uses `pid` as the public identifier
64
- - Spawns the actual Claude/Codex/Gemini process directly
65
- - Redirects `stdout` and `stderr` to files
66
- - Does not guarantee `exitCode` in the initial design
67
-
68
- ### `ai-cli wait`
69
-
70
- Waits until all given PIDs are no longer running.
71
-
72
- Properties:
73
-
74
- - Input is one or more `pid` values
75
- - Timeout is supported
76
- - Response format follows MCP `wait` as closely as possible
77
- - Returns a result array, same direction as MCP
78
-
79
- ### `ai-cli ps`
80
-
81
- Lists tracked runs with minimal information.
82
-
83
- Properties:
84
-
85
- - Output includes `pid`, `agent`, and `status`
86
- - Initial scope is intentionally minimal
87
-
88
- ### `ai-cli result`
89
-
90
- Reads saved output and returns parsed results.
91
-
92
- Properties:
93
-
94
- - Behavior should stay close to MCP `get_result`
95
- - Parsed output is preferred
96
- - Falls back to raw output when parsing fails or output is incomplete
97
-
98
- ### `ai-cli kill`
99
-
100
- Sends `SIGTERM` to the given PID.
101
-
102
- Properties:
103
-
104
- - Public API is intentionally PID-based
105
- - Users may also kill processes manually outside the tool
106
-
107
- ### `ai-cli cleanup`
108
-
109
- Removes tracked process state for runs that are no longer running.
110
-
111
- Properties:
112
-
113
- - Removes completed and failed PID directories
114
- - Keeps running processes intact
115
- - Removes empty per-cwd directories after cleanup
116
-
117
- ### `ai-cli doctor`
118
-
119
- Checks whether supported AI CLI binaries are available.
120
-
121
- Properties:
122
-
123
- - Scope is binary existence/path resolution only
124
- - It does not verify login or acceptance state
125
-
126
- ### `ai-cli models`
127
-
128
- Returns the supported model list and aliases.
129
-
130
- Properties:
131
-
132
- - Behavior should stay close to MCP-supported model documentation
133
- - Static model definitions are acceptable in this phase
134
-
135
- ### `ai-cli mcp`
136
-
137
- Starts the MCP server from the `ai-cli` command.
138
-
139
- Properties:
140
-
141
- - Allows one package to support both direct CLI usage and MCP usage
142
-
143
- ## Entrypoints
144
-
145
- Planned package bin layout:
146
-
147
- ```json
148
- {
149
- "bin": {
150
- "ai-cli": "dist/bin/ai-cli.js",
151
- "ai-cli-mcp": "dist/bin/ai-cli-mcp.js"
152
- }
153
- }
154
- ```
155
-
156
- Planned source layout:
157
-
158
- ```text
159
- src/
160
- bin/
161
- ai-cli.ts
162
- ai-cli-mcp.ts
163
- app/
164
- cli.ts
165
- mcp.ts
166
- ```
167
-
168
- Responsibilities:
169
-
170
- - `src/bin/ai-cli.ts`: thin CLI entrypoint
171
- - `src/bin/ai-cli-mcp.ts`: thin MCP entrypoint
172
- - `src/app/cli.ts`: subcommand parsing and dispatch for `ai-cli`
173
- - `src/app/mcp.ts`: MCP server bootstrap
174
-
175
- ## Backend Architecture
176
-
177
- The core implementation should be shared between CLI and MCP.
178
-
179
- Suggested internal boundaries:
180
-
181
- - `cli-builder`
182
- - resolves model aliases
183
- - validates input
184
- - builds the real Claude/Codex/Gemini command
185
- - `runner`
186
- - spawns the actual AI CLI process
187
- - redirects `stdout` and `stderr` to files
188
- - `process-store`
189
- - stores tracked process metadata
190
- - exact path and file format are intentionally deferred
191
- - `process-service`
192
- - shared use cases for `run`, `wait`, `ps`, `result`, and `kill`
193
- - `parsers`
194
- - parses saved output into structured results
195
-
196
- ## PID-Based Design Decision
197
-
198
- The public interface stays PID-based on purpose.
199
-
200
- Rationale:
201
-
202
- - This CLI is a thin wrapper over existing AI CLI tools
203
- - PID is already the native OS process identifier
204
- - Users can inspect or terminate processes with normal Unix tooling
205
- - We do not want to introduce a synthetic public job ID in this phase
206
-
207
- Implication:
208
-
209
- - Public commands use `pid`
210
- - Internal storage may store additional metadata if needed
211
- - PID remains the only required identifier at the product surface
212
-
213
- ## Background Execution Strategy
214
-
215
- The first production CLI implementation uses direct process spawning with file redirection.
216
-
217
- Approach:
218
-
219
- - Spawn the actual AI CLI process directly
220
- - Redirect `stdout` to a file
221
- - Redirect `stderr` to a file
222
- - Persist enough metadata to support `wait`, `ps`, `result`, and `kill`
223
-
224
- Why this approach:
225
-
226
- - Lighter than introducing a worker process
227
- - Keeps the CLI close to Unix process semantics
228
- - Avoids worker-child termination complexity
229
- - Keeps migration from the current MCP server relatively simple
230
-
231
- Tradeoff accepted in phase one:
232
-
233
- - `exitCode` is not guaranteed to be captured
234
-
235
- If this becomes a practical problem, a thin per-run wrapper/worker can be introduced later without changing the public CLI model.
236
-
237
- ## MCP Compatibility Plan
238
-
239
- MCP functionality stays in the project and should be preserved.
240
-
241
- Compatibility goals:
242
-
243
- - Keep current MCP tool names
244
- - Keep current response shape as much as practical
245
- - Reuse the same backend logic as the new CLI where possible
246
-
247
- Target mapping:
248
-
249
- - MCP `run` -> shared process service `run`
250
- - MCP `wait` -> shared process service `wait`
251
- - MCP `list_processes` -> shared process service `ps`
252
- - MCP `get_result` -> shared process service `result`
253
- - MCP `kill_process` -> shared process service `kill`
254
-
255
- ## Implementation Order
256
-
257
- 1. Split the current `src/server.ts` responsibilities into MCP surface and shared process logic
258
- 2. Introduce new bin entrypoints for `ai-cli` and `ai-cli-mcp`
259
- 3. Add `ai-cli` subcommand parsing and help output
260
- 4. Implement direct background spawning with stdout/stderr file redirection
261
- 5. Implement CLI commands: `run`, `wait`, `ps`, `result`, `kill`
262
- 6. Add `models`, `doctor`, and `mcp`
263
- 7. Rewire MCP handlers to the same shared backend
264
-
265
- ## Open Items Deferred
266
-
267
- The following items are intentionally deferred:
268
-
269
- - state directory path
270
- - file naming scheme
271
- - metadata file schema
272
- - exit code capture for detached CLI runs
273
- - retention and cleanup policy
274
- - exact raw output access patterns
275
- - Windows-specific process handling details