ai-cli-mcp 2.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/.claude/settings.local.json +19 -0
- package/.github/workflows/ci.yml +31 -0
- package/.github/workflows/test.yml +43 -0
- package/.vscode/settings.json +3 -0
- package/AGENT.md +57 -0
- package/CHANGELOG.md +126 -0
- package/LICENSE +22 -0
- package/README.md +329 -0
- package/RELEASE.md +74 -0
- package/data/rooms/refactor-haiku-alias-main/messages.jsonl +5 -0
- package/data/rooms/refactor-haiku-alias-main/presence.json +20 -0
- package/data/rooms.json +10 -0
- package/dist/__tests__/e2e.test.js +238 -0
- package/dist/__tests__/edge-cases.test.js +135 -0
- package/dist/__tests__/error-cases.test.js +296 -0
- package/dist/__tests__/mocks.js +32 -0
- package/dist/__tests__/model-alias.test.js +36 -0
- package/dist/__tests__/process-management.test.js +632 -0
- package/dist/__tests__/server.test.js +665 -0
- package/dist/__tests__/setup.js +11 -0
- package/dist/__tests__/utils/claude-mock.js +80 -0
- package/dist/__tests__/utils/mcp-client.js +104 -0
- package/dist/__tests__/utils/persistent-mock.js +25 -0
- package/dist/__tests__/utils/test-helpers.js +11 -0
- package/dist/__tests__/validation.test.js +212 -0
- package/dist/__tests__/version-print.test.js +69 -0
- package/dist/parsers.js +54 -0
- package/dist/server.js +614 -0
- package/docs/RELEASE_CHECKLIST.md +26 -0
- package/docs/e2e-testing.md +148 -0
- package/docs/local_install.md +111 -0
- package/hello.txt +3 -0
- package/implementation-log.md +110 -0
- package/implementation-plan.md +189 -0
- package/investigation-report.md +135 -0
- package/package.json +53 -0
- package/print-eslint-config.js +3 -0
- package/quality-score.json +47 -0
- package/refactoring-requirements.md +25 -0
- package/review-report.md +132 -0
- package/scripts/check-version-log.sh +34 -0
- package/scripts/publish-release.sh +95 -0
- package/scripts/restore-config.sh +28 -0
- package/scripts/test-release.sh +69 -0
- package/src/__tests__/e2e.test.ts +290 -0
- package/src/__tests__/edge-cases.test.ts +181 -0
- package/src/__tests__/error-cases.test.ts +378 -0
- package/src/__tests__/mocks.ts +35 -0
- package/src/__tests__/model-alias.test.ts +44 -0
- package/src/__tests__/process-management.test.ts +772 -0
- package/src/__tests__/server.test.ts +851 -0
- package/src/__tests__/setup.ts +13 -0
- package/src/__tests__/utils/claude-mock.ts +87 -0
- package/src/__tests__/utils/mcp-client.ts +129 -0
- package/src/__tests__/utils/persistent-mock.ts +29 -0
- package/src/__tests__/utils/test-helpers.ts +13 -0
- package/src/__tests__/validation.test.ts +258 -0
- package/src/__tests__/version-print.test.ts +86 -0
- package/src/parsers.ts +55 -0
- package/src/server.ts +735 -0
- package/start.bat +9 -0
- package/start.sh +21 -0
- package/test-results.md +119 -0
- package/test-standalone.js +5877 -0
- package/tsconfig.json +16 -0
- package/vitest.config.e2e.ts +27 -0
- package/vitest.config.ts +22 -0
- package/vitest.config.unit.ts +29 -0
- package/xx.txt +1 -0
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { join, dirname } from 'node:path';
|
|
3
|
+
/**
|
|
4
|
+
* Mock Claude CLI for testing
|
|
5
|
+
* This creates a fake Claude CLI that can be used during testing
|
|
6
|
+
*/
|
|
7
|
+
export class ClaudeMock {
|
|
8
|
+
mockPath;
|
|
9
|
+
responses = new Map();
|
|
10
|
+
constructor(binaryName = 'claude') {
|
|
11
|
+
// Always use /tmp directory for mocks in tests
|
|
12
|
+
this.mockPath = join('/tmp', 'claude-code-test-mock', binaryName);
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Setup the mock Claude CLI
|
|
16
|
+
*/
|
|
17
|
+
async setup() {
|
|
18
|
+
const dir = dirname(this.mockPath);
|
|
19
|
+
if (!existsSync(dir)) {
|
|
20
|
+
mkdirSync(dir, { recursive: true });
|
|
21
|
+
}
|
|
22
|
+
// Create a simple bash script that echoes responses
|
|
23
|
+
const mockScript = `#!/bin/bash
|
|
24
|
+
# Mock Claude CLI for testing
|
|
25
|
+
|
|
26
|
+
# Extract the prompt from arguments
|
|
27
|
+
prompt=""
|
|
28
|
+
verbose=false
|
|
29
|
+
while [[ $# -gt 0 ]]; do
|
|
30
|
+
case $1 in
|
|
31
|
+
-p|--prompt)
|
|
32
|
+
prompt="$2"
|
|
33
|
+
shift 2
|
|
34
|
+
;;
|
|
35
|
+
--verbose)
|
|
36
|
+
verbose=true
|
|
37
|
+
shift
|
|
38
|
+
;;
|
|
39
|
+
--yes|-y|--dangerously-skip-permissions)
|
|
40
|
+
shift
|
|
41
|
+
;;
|
|
42
|
+
*)
|
|
43
|
+
shift
|
|
44
|
+
;;
|
|
45
|
+
esac
|
|
46
|
+
done
|
|
47
|
+
|
|
48
|
+
# Mock responses based on prompt
|
|
49
|
+
if [[ "$prompt" == *"create"* ]]; then
|
|
50
|
+
echo "Created file successfully"
|
|
51
|
+
elif [[ "$prompt" == *"Create"* ]]; then
|
|
52
|
+
echo "Created file successfully"
|
|
53
|
+
elif [[ "$prompt" == *"git"* ]] && [[ "$prompt" == *"commit"* ]]; then
|
|
54
|
+
echo "Committed changes successfully"
|
|
55
|
+
elif [[ "$prompt" == *"error"* ]]; then
|
|
56
|
+
echo "Error: Mock error response" >&2
|
|
57
|
+
exit 1
|
|
58
|
+
else
|
|
59
|
+
echo "Command executed successfully"
|
|
60
|
+
fi
|
|
61
|
+
`;
|
|
62
|
+
writeFileSync(this.mockPath, mockScript);
|
|
63
|
+
// Make executable
|
|
64
|
+
const { chmod } = await import('node:fs/promises');
|
|
65
|
+
await chmod(this.mockPath, 0o755);
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Cleanup the mock Claude CLI
|
|
69
|
+
*/
|
|
70
|
+
async cleanup() {
|
|
71
|
+
const { rm } = await import('node:fs/promises');
|
|
72
|
+
await rm(this.mockPath, { force: true });
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Add a mock response for a specific prompt pattern
|
|
76
|
+
*/
|
|
77
|
+
addResponse(pattern, response) {
|
|
78
|
+
this.responses.set(pattern, response);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { EventEmitter } from 'node:events';
|
|
3
|
+
/**
|
|
4
|
+
* Mock MCP client for testing the server
|
|
5
|
+
*/
|
|
6
|
+
export class MCPTestClient extends EventEmitter {
|
|
7
|
+
serverPath;
|
|
8
|
+
env;
|
|
9
|
+
server = null;
|
|
10
|
+
requestId = 0;
|
|
11
|
+
pendingRequests = new Map();
|
|
12
|
+
buffer = '';
|
|
13
|
+
constructor(serverPath, env = {}) {
|
|
14
|
+
super();
|
|
15
|
+
this.serverPath = serverPath;
|
|
16
|
+
this.env = env;
|
|
17
|
+
}
|
|
18
|
+
async connect() {
|
|
19
|
+
return new Promise((resolve, reject) => {
|
|
20
|
+
this.server = spawn('node', [this.serverPath], {
|
|
21
|
+
env: { ...process.env, ...this.env },
|
|
22
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
23
|
+
});
|
|
24
|
+
this.server.stdout?.on('data', (data) => {
|
|
25
|
+
this.handleData(data.toString());
|
|
26
|
+
});
|
|
27
|
+
this.server.stderr?.on('data', (data) => {
|
|
28
|
+
console.error('Server stderr:', data.toString());
|
|
29
|
+
});
|
|
30
|
+
this.server.on('error', (error) => {
|
|
31
|
+
reject(error);
|
|
32
|
+
});
|
|
33
|
+
this.server.on('spawn', () => {
|
|
34
|
+
resolve();
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
async disconnect() {
|
|
39
|
+
if (this.server) {
|
|
40
|
+
this.server.kill();
|
|
41
|
+
await new Promise((resolve) => {
|
|
42
|
+
this.server.on('exit', resolve);
|
|
43
|
+
});
|
|
44
|
+
this.server = null;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
handleData(data) {
|
|
48
|
+
this.buffer += data;
|
|
49
|
+
const lines = this.buffer.split('\n');
|
|
50
|
+
this.buffer = lines.pop() || '';
|
|
51
|
+
for (const line of lines) {
|
|
52
|
+
if (!line.trim())
|
|
53
|
+
continue;
|
|
54
|
+
try {
|
|
55
|
+
const response = JSON.parse(line);
|
|
56
|
+
if (response.id && this.pendingRequests.has(response.id)) {
|
|
57
|
+
const pending = this.pendingRequests.get(response.id);
|
|
58
|
+
this.pendingRequests.delete(response.id);
|
|
59
|
+
pending.resolve(response);
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
this.emit('notification', response);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
console.error('Failed to parse response:', line, error);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
async sendRequest(method, params) {
|
|
71
|
+
const id = ++this.requestId;
|
|
72
|
+
const request = {
|
|
73
|
+
jsonrpc: '2.0',
|
|
74
|
+
method,
|
|
75
|
+
params,
|
|
76
|
+
id,
|
|
77
|
+
};
|
|
78
|
+
return new Promise((resolve, reject) => {
|
|
79
|
+
this.pendingRequests.set(id, { resolve, reject });
|
|
80
|
+
this.server?.stdin?.write(JSON.stringify(request) + '\n');
|
|
81
|
+
// Timeout after 30 seconds
|
|
82
|
+
setTimeout(() => {
|
|
83
|
+
if (this.pendingRequests.has(id)) {
|
|
84
|
+
this.pendingRequests.delete(id);
|
|
85
|
+
reject(new Error(`Request ${id} timed out`));
|
|
86
|
+
}
|
|
87
|
+
}, 30000);
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
async callTool(name, args) {
|
|
91
|
+
const response = await this.sendRequest('tools/call', {
|
|
92
|
+
name,
|
|
93
|
+
arguments: args,
|
|
94
|
+
});
|
|
95
|
+
if (response.error) {
|
|
96
|
+
throw new Error(`Tool call failed: ${response.error.message}`);
|
|
97
|
+
}
|
|
98
|
+
return response.result?.content;
|
|
99
|
+
}
|
|
100
|
+
async listTools() {
|
|
101
|
+
const response = await this.sendRequest('tools/list');
|
|
102
|
+
return response.result?.tools || [];
|
|
103
|
+
}
|
|
104
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { ClaudeMock } from './claude-mock.js';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
let sharedMock = null;
|
|
5
|
+
export async function getSharedMock() {
|
|
6
|
+
if (!sharedMock) {
|
|
7
|
+
sharedMock = new ClaudeMock('claudeMocked');
|
|
8
|
+
}
|
|
9
|
+
// Always ensure mock exists
|
|
10
|
+
const mockPath = join('/tmp', 'claude-code-test-mock', 'claudeMocked');
|
|
11
|
+
if (!existsSync(mockPath)) {
|
|
12
|
+
console.error(`[DEBUG] Mock not found at ${mockPath}, creating it...`);
|
|
13
|
+
await sharedMock.setup();
|
|
14
|
+
}
|
|
15
|
+
else {
|
|
16
|
+
console.error(`[DEBUG] Mock already exists at ${mockPath}`);
|
|
17
|
+
}
|
|
18
|
+
return sharedMock;
|
|
19
|
+
}
|
|
20
|
+
export async function cleanupSharedMock() {
|
|
21
|
+
if (sharedMock) {
|
|
22
|
+
await sharedMock.cleanup();
|
|
23
|
+
sharedMock = null;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
export function verifyMockExists(binaryName) {
|
|
4
|
+
const mockPath = join('/tmp', 'claude-code-test-mock', binaryName);
|
|
5
|
+
return existsSync(mockPath);
|
|
6
|
+
}
|
|
7
|
+
export async function ensureMockExists(mock) {
|
|
8
|
+
if (!verifyMockExists('claudeMocked')) {
|
|
9
|
+
await mock.setup();
|
|
10
|
+
}
|
|
11
|
+
}
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { existsSync } from 'node:fs';
|
|
4
|
+
import { homedir } from 'node:os';
|
|
5
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
6
|
+
// Mock dependencies
|
|
7
|
+
vi.mock('node:child_process', () => ({
|
|
8
|
+
spawn: vi.fn()
|
|
9
|
+
}));
|
|
10
|
+
vi.mock('node:fs');
|
|
11
|
+
vi.mock('node:os');
|
|
12
|
+
vi.mock('node:path', () => ({
|
|
13
|
+
resolve: vi.fn((path) => path),
|
|
14
|
+
join: vi.fn((...args) => args.join('/')),
|
|
15
|
+
isAbsolute: vi.fn((path) => path.startsWith('/'))
|
|
16
|
+
}));
|
|
17
|
+
vi.mock('@modelcontextprotocol/sdk/server/index.js', () => ({
|
|
18
|
+
Server: vi.fn().mockImplementation(function () {
|
|
19
|
+
this.setRequestHandler = vi.fn();
|
|
20
|
+
this.connect = vi.fn();
|
|
21
|
+
this.close = vi.fn();
|
|
22
|
+
this.onerror = undefined;
|
|
23
|
+
return this;
|
|
24
|
+
}),
|
|
25
|
+
}));
|
|
26
|
+
vi.mock('@modelcontextprotocol/sdk/types.js', () => ({
|
|
27
|
+
ListToolsRequestSchema: { name: 'listTools' },
|
|
28
|
+
CallToolRequestSchema: { name: 'callTool' },
|
|
29
|
+
ErrorCode: {
|
|
30
|
+
InternalError: 'InternalError',
|
|
31
|
+
MethodNotFound: 'MethodNotFound',
|
|
32
|
+
InvalidParams: 'InvalidParams'
|
|
33
|
+
},
|
|
34
|
+
McpError: vi.fn().mockImplementation((code, message) => {
|
|
35
|
+
const error = new Error(message);
|
|
36
|
+
error.code = code;
|
|
37
|
+
return error;
|
|
38
|
+
})
|
|
39
|
+
}));
|
|
40
|
+
const mockExistsSync = vi.mocked(existsSync);
|
|
41
|
+
const mockHomedir = vi.mocked(homedir);
|
|
42
|
+
describe('Argument Validation Tests', () => {
|
|
43
|
+
let consoleErrorSpy;
|
|
44
|
+
let errorHandler = null;
|
|
45
|
+
function setupServerMock() {
|
|
46
|
+
errorHandler = null;
|
|
47
|
+
vi.mocked(Server).mockImplementation(function () {
|
|
48
|
+
this.setRequestHandler = vi.fn();
|
|
49
|
+
this.connect = vi.fn();
|
|
50
|
+
this.close = vi.fn();
|
|
51
|
+
Object.defineProperty(this, 'onerror', {
|
|
52
|
+
get() { return errorHandler; },
|
|
53
|
+
set(handler) { errorHandler = handler; },
|
|
54
|
+
enumerable: true,
|
|
55
|
+
configurable: true
|
|
56
|
+
});
|
|
57
|
+
return this;
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
beforeEach(() => {
|
|
61
|
+
vi.clearAllMocks();
|
|
62
|
+
vi.resetModules();
|
|
63
|
+
vi.unmock('../server.js');
|
|
64
|
+
consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => { });
|
|
65
|
+
// Set up process.env
|
|
66
|
+
process.env = { ...process.env };
|
|
67
|
+
});
|
|
68
|
+
describe('Tool Arguments Schema', () => {
|
|
69
|
+
it('should validate valid arguments', async () => {
|
|
70
|
+
mockHomedir.mockReturnValue('/home/user');
|
|
71
|
+
mockExistsSync.mockReturnValue(true);
|
|
72
|
+
setupServerMock();
|
|
73
|
+
const module = await import('../server.js');
|
|
74
|
+
// @ts-ignore
|
|
75
|
+
const { ClaudeCodeServer } = module;
|
|
76
|
+
const server = new ClaudeCodeServer();
|
|
77
|
+
const mockServerInstance = vi.mocked(Server).mock.results[0].value;
|
|
78
|
+
// Find tool definition
|
|
79
|
+
const listToolsCall = mockServerInstance.setRequestHandler.mock.calls.find((call) => call[0].name === 'listTools');
|
|
80
|
+
const listHandler = listToolsCall[1];
|
|
81
|
+
const tools = await listHandler();
|
|
82
|
+
const claudeCodeTool = tools.tools[0];
|
|
83
|
+
// Extract schema from tool definition
|
|
84
|
+
const schema = z.object({
|
|
85
|
+
prompt: z.string(),
|
|
86
|
+
workFolder: z.string(),
|
|
87
|
+
model: z.string().optional(),
|
|
88
|
+
session_id: z.string().optional()
|
|
89
|
+
});
|
|
90
|
+
// Test valid cases
|
|
91
|
+
expect(() => schema.parse({ prompt: 'test', workFolder: '/tmp' })).not.toThrow();
|
|
92
|
+
expect(() => schema.parse({ prompt: 'test', workFolder: '/tmp', model: 'sonnet' })).not.toThrow();
|
|
93
|
+
});
|
|
94
|
+
it('should reject invalid arguments', async () => {
|
|
95
|
+
mockHomedir.mockReturnValue('/home/user');
|
|
96
|
+
mockExistsSync.mockReturnValue(true);
|
|
97
|
+
setupServerMock();
|
|
98
|
+
const module = await import('../server.js');
|
|
99
|
+
// @ts-ignore
|
|
100
|
+
const { ClaudeCodeServer } = module;
|
|
101
|
+
const server = new ClaudeCodeServer();
|
|
102
|
+
const mockServerInstance = vi.mocked(Server).mock.results[0].value;
|
|
103
|
+
// Find tool definition
|
|
104
|
+
const listToolsCall = mockServerInstance.setRequestHandler.mock.calls.find((call) => call[0].name === 'listTools');
|
|
105
|
+
const listHandler = listToolsCall[1];
|
|
106
|
+
const tools = await listHandler();
|
|
107
|
+
const claudeCodeTool = tools.tools[0];
|
|
108
|
+
// Extract schema from tool definition
|
|
109
|
+
const schema = z.object({
|
|
110
|
+
prompt: z.string(),
|
|
111
|
+
workFolder: z.string(),
|
|
112
|
+
model: z.string().optional(),
|
|
113
|
+
session_id: z.string().optional()
|
|
114
|
+
});
|
|
115
|
+
// Test invalid cases
|
|
116
|
+
expect(() => schema.parse({})).toThrow(); // Missing prompt and workFolder
|
|
117
|
+
expect(() => schema.parse({ prompt: 'test' })).toThrow(); // Missing workFolder
|
|
118
|
+
expect(() => schema.parse({ prompt: 123, workFolder: '/tmp' })).toThrow(); // Wrong prompt type
|
|
119
|
+
expect(() => schema.parse({ prompt: 'test', workFolder: 123 })).toThrow(); // Wrong workFolder type
|
|
120
|
+
});
|
|
121
|
+
it('should handle missing required fields', async () => {
|
|
122
|
+
const schema = z.object({
|
|
123
|
+
prompt: z.string(),
|
|
124
|
+
workFolder: z.string(),
|
|
125
|
+
model: z.string().optional(),
|
|
126
|
+
session_id: z.string().optional()
|
|
127
|
+
});
|
|
128
|
+
try {
|
|
129
|
+
schema.parse({});
|
|
130
|
+
}
|
|
131
|
+
catch (error) {
|
|
132
|
+
// Both prompt and workFolder are required
|
|
133
|
+
expect(error.errors.length).toBe(2);
|
|
134
|
+
expect(error.errors.some((e) => e.path[0] === 'prompt')).toBe(true);
|
|
135
|
+
expect(error.errors.some((e) => e.path[0] === 'workFolder')).toBe(true);
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
it('should allow optional fields to be undefined', async () => {
|
|
139
|
+
const schema = z.object({
|
|
140
|
+
prompt: z.string(),
|
|
141
|
+
workFolder: z.string(),
|
|
142
|
+
model: z.string().optional(),
|
|
143
|
+
session_id: z.string().optional()
|
|
144
|
+
});
|
|
145
|
+
const result = schema.parse({ prompt: 'test', workFolder: '/tmp' });
|
|
146
|
+
expect(result.model).toBeUndefined();
|
|
147
|
+
expect(result.session_id).toBeUndefined();
|
|
148
|
+
});
|
|
149
|
+
it('should handle extra fields gracefully', async () => {
|
|
150
|
+
const schema = z.object({
|
|
151
|
+
prompt: z.string(),
|
|
152
|
+
workFolder: z.string(),
|
|
153
|
+
model: z.string().optional(),
|
|
154
|
+
session_id: z.string().optional()
|
|
155
|
+
});
|
|
156
|
+
// By default, Zod strips unknown keys
|
|
157
|
+
const result = schema.parse({
|
|
158
|
+
prompt: 'test',
|
|
159
|
+
workFolder: '/tmp',
|
|
160
|
+
extraField: 'ignored'
|
|
161
|
+
});
|
|
162
|
+
expect(result).toEqual({ prompt: 'test', workFolder: '/tmp' });
|
|
163
|
+
expect(result).not.toHaveProperty('extraField');
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
describe('Runtime Argument Validation', () => {
|
|
167
|
+
it('should validate workFolder is a string when provided', async () => {
|
|
168
|
+
mockHomedir.mockReturnValue('/home/user');
|
|
169
|
+
mockExistsSync.mockReturnValue(true);
|
|
170
|
+
setupServerMock();
|
|
171
|
+
const module = await import('../server.js');
|
|
172
|
+
// @ts-ignore
|
|
173
|
+
const { ClaudeCodeServer } = module;
|
|
174
|
+
const server = new ClaudeCodeServer();
|
|
175
|
+
const mockServerInstance = vi.mocked(Server).mock.results[0].value;
|
|
176
|
+
const callToolCall = mockServerInstance.setRequestHandler.mock.calls.find((call) => call[0].name === 'callTool');
|
|
177
|
+
const handler = callToolCall[1];
|
|
178
|
+
// Test with non-string workFolder
|
|
179
|
+
await expect(handler({
|
|
180
|
+
params: {
|
|
181
|
+
name: 'claude_code',
|
|
182
|
+
arguments: {
|
|
183
|
+
prompt: 'test',
|
|
184
|
+
workFolder: 123 // Invalid type
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
})).rejects.toThrow();
|
|
188
|
+
});
|
|
189
|
+
it('should reject empty string prompt', async () => {
|
|
190
|
+
mockHomedir.mockReturnValue('/home/user');
|
|
191
|
+
mockExistsSync.mockReturnValue(true);
|
|
192
|
+
setupServerMock();
|
|
193
|
+
const module = await import('../server.js');
|
|
194
|
+
// @ts-ignore
|
|
195
|
+
const { ClaudeCodeServer } = module;
|
|
196
|
+
const server = new ClaudeCodeServer();
|
|
197
|
+
const mockServerInstance = vi.mocked(Server).mock.results[0].value;
|
|
198
|
+
const callToolCall = mockServerInstance.setRequestHandler.mock.calls.find((call) => call[0].name === 'callTool');
|
|
199
|
+
const handler = callToolCall[1];
|
|
200
|
+
// Empty string prompt should be rejected
|
|
201
|
+
await expect(handler({
|
|
202
|
+
params: {
|
|
203
|
+
name: 'claude_code',
|
|
204
|
+
arguments: {
|
|
205
|
+
prompt: '', // Empty prompt
|
|
206
|
+
workFolder: '/tmp'
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
})).rejects.toThrow('Missing or invalid required parameter: prompt');
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
});
|
|
@@ -0,0 +1,69 @@
|
|
|
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 { MCPTestClient } 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
|
+
const serverPath = 'dist/server.js';
|
|
12
|
+
beforeEach(async () => {
|
|
13
|
+
// Ensure mock exists
|
|
14
|
+
await getSharedMock();
|
|
15
|
+
// Create a temporary directory for test files
|
|
16
|
+
testDir = mkdtempSync(join(tmpdir(), 'claude-code-test-'));
|
|
17
|
+
// Spy on console.error
|
|
18
|
+
consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => { });
|
|
19
|
+
// Initialize MCP client with custom binary name using absolute path
|
|
20
|
+
client = new MCPTestClient(serverPath, {
|
|
21
|
+
CLAUDE_CLI_NAME: '/tmp/claude-code-test-mock/claudeMocked',
|
|
22
|
+
});
|
|
23
|
+
await client.connect();
|
|
24
|
+
});
|
|
25
|
+
afterEach(async () => {
|
|
26
|
+
// Disconnect client
|
|
27
|
+
await client.disconnect();
|
|
28
|
+
// Clean up test directory
|
|
29
|
+
rmSync(testDir, { recursive: true, force: true });
|
|
30
|
+
// Restore console.error spy
|
|
31
|
+
consoleErrorSpy.mockRestore();
|
|
32
|
+
});
|
|
33
|
+
it('should print version and startup time only on first use', async () => {
|
|
34
|
+
// First tool call
|
|
35
|
+
await client.callTool('claude_code', {
|
|
36
|
+
prompt: 'echo "test 1"',
|
|
37
|
+
workFolder: testDir,
|
|
38
|
+
});
|
|
39
|
+
// Find the version print in the console.error calls
|
|
40
|
+
const findVersionCall = (calls) => {
|
|
41
|
+
return calls.find(call => {
|
|
42
|
+
const str = call[1] || call[0]; // message might be first or second param
|
|
43
|
+
return typeof str === 'string' && str.includes('claude_code v') && str.includes('started at');
|
|
44
|
+
});
|
|
45
|
+
};
|
|
46
|
+
// Check that version was printed on first use
|
|
47
|
+
const versionCall = findVersionCall(consoleErrorSpy.mock.calls);
|
|
48
|
+
expect(versionCall).toBeDefined();
|
|
49
|
+
expect(versionCall[1]).toMatch(/claude_code v[0-9]+\.[0-9]+\.[0-9]+ started at \d{4}-\d{2}-\d{2}T/);
|
|
50
|
+
// Clear the spy but keep the spy active
|
|
51
|
+
consoleErrorSpy.mockClear();
|
|
52
|
+
// Second tool call
|
|
53
|
+
await client.callTool('claude_code', {
|
|
54
|
+
prompt: 'echo "test 2"',
|
|
55
|
+
workFolder: testDir,
|
|
56
|
+
});
|
|
57
|
+
// Check that version was NOT printed on second use
|
|
58
|
+
const secondVersionCall = findVersionCall(consoleErrorSpy.mock.calls);
|
|
59
|
+
expect(secondVersionCall).toBeUndefined();
|
|
60
|
+
// Third tool call
|
|
61
|
+
await client.callTool('claude_code', {
|
|
62
|
+
prompt: 'echo "test 3"',
|
|
63
|
+
workFolder: testDir,
|
|
64
|
+
});
|
|
65
|
+
// Should still not have been called with version print
|
|
66
|
+
const thirdVersionCall = findVersionCall(consoleErrorSpy.mock.calls);
|
|
67
|
+
expect(thirdVersionCall).toBeUndefined();
|
|
68
|
+
});
|
|
69
|
+
});
|
package/dist/parsers.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { debugLog } from './server.js';
|
|
2
|
+
/**
|
|
3
|
+
* Parse Codex NDJSON output to extract the last agent message and token count
|
|
4
|
+
*/
|
|
5
|
+
export function parseCodexOutput(stdout) {
|
|
6
|
+
if (!stdout)
|
|
7
|
+
return null;
|
|
8
|
+
try {
|
|
9
|
+
const lines = stdout.trim().split('\n');
|
|
10
|
+
let lastMessage = null;
|
|
11
|
+
let tokenCount = null;
|
|
12
|
+
for (const line of lines) {
|
|
13
|
+
if (line.trim()) {
|
|
14
|
+
try {
|
|
15
|
+
const parsed = JSON.parse(line);
|
|
16
|
+
if (parsed.msg?.type === 'agent_message') {
|
|
17
|
+
lastMessage = parsed.msg.message;
|
|
18
|
+
}
|
|
19
|
+
else if (parsed.msg?.type === 'token_count') {
|
|
20
|
+
tokenCount = parsed.msg;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
catch (e) {
|
|
24
|
+
// Skip invalid JSON lines
|
|
25
|
+
debugLog(`[Debug] Skipping invalid JSON line: ${line}`);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
if (lastMessage || tokenCount) {
|
|
30
|
+
return {
|
|
31
|
+
message: lastMessage,
|
|
32
|
+
token_count: tokenCount
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
catch (e) {
|
|
37
|
+
debugLog(`[Debug] Failed to parse Codex NDJSON output: ${e}`);
|
|
38
|
+
}
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Parse Claude JSON output
|
|
43
|
+
*/
|
|
44
|
+
export function parseClaudeOutput(stdout) {
|
|
45
|
+
if (!stdout)
|
|
46
|
+
return null;
|
|
47
|
+
try {
|
|
48
|
+
return JSON.parse(stdout);
|
|
49
|
+
}
|
|
50
|
+
catch (e) {
|
|
51
|
+
debugLog(`[Debug] Failed to parse Claude JSON output: ${e}`);
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
}
|