agent-gear 1.1.2 → 1.1.5
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/AGENTS.md +23 -0
- package/dist/acp/detector.d.ts +5 -0
- package/dist/acp/detector.d.ts.map +1 -1
- package/dist/acp/detector.js +67 -2
- package/dist/acp/detector.js.map +1 -1
- package/dist/{port-forward.d.ts → gateway.d.ts} +7 -3
- package/dist/gateway.d.ts.map +1 -0
- package/dist/gateway.js +158 -0
- package/dist/gateway.js.map +1 -0
- package/dist/gateway.test.d.ts +2 -0
- package/dist/gateway.test.d.ts.map +1 -0
- package/dist/gateway.test.js +241 -0
- package/dist/gateway.test.js.map +1 -0
- package/dist/socketio-client.d.ts.map +1 -1
- package/dist/socketio-client.js +16 -12
- package/dist/socketio-client.js.map +1 -1
- package/package.json +1 -1
- package/src/acp/detector.ts +67 -2
- package/src/gateway.test.ts +301 -0
- package/src/gateway.ts +195 -0
- package/src/socketio-client.ts +18 -12
- package/tests/acp-detector.test.ts +125 -23
- package/dist/port-forward.d.ts.map +0 -1
- package/dist/port-forward.js +0 -68
- package/dist/port-forward.js.map +0 -1
- package/dist/port-forward.test.d.ts +0 -2
- package/dist/port-forward.test.d.ts.map +0 -1
- package/dist/port-forward.test.js +0 -163
- package/dist/port-forward.test.js.map +0 -1
- package/src/port-forward.test.ts +0 -202
- package/src/port-forward.ts +0 -88
package/src/gateway.ts
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import net from 'node:net';
|
|
2
|
+
import type { Socket } from 'socket.io-client';
|
|
3
|
+
|
|
4
|
+
interface Tunnel {
|
|
5
|
+
id: string;
|
|
6
|
+
port: number;
|
|
7
|
+
localSocket: net.Socket | null;
|
|
8
|
+
serverSocket: Socket;
|
|
9
|
+
pendingRequestId?: string | null;
|
|
10
|
+
responseBuffer: Buffer[];
|
|
11
|
+
responseTimer?: ReturnType<typeof setTimeout> | null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const RECONNECT_INTERVAL_MS = 3000;
|
|
15
|
+
const HTTP_RESPONSE_COLLECT_MS = 100;
|
|
16
|
+
|
|
17
|
+
export class GatewayManager {
|
|
18
|
+
private tunnels: Map<string, Tunnel> = new Map();
|
|
19
|
+
private reconnectTimers: Map<string, ReturnType<typeof setTimeout>> = new Map();
|
|
20
|
+
|
|
21
|
+
async createTunnel(tunnelId: string, port: number, serverSocket: Socket): Promise<void> {
|
|
22
|
+
this.tunnels.set(tunnelId, { id: tunnelId, port, localSocket: null, serverSocket, responseBuffer: [] });
|
|
23
|
+
await this.connect(tunnelId, true);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
private async connect(tunnelId: string, isInitial: boolean): Promise<void> {
|
|
27
|
+
const tunnel = this.tunnels.get(tunnelId);
|
|
28
|
+
if (!tunnel) return;
|
|
29
|
+
|
|
30
|
+
const localSocket = net.connect({ host: '127.0.0.1', port: tunnel.port });
|
|
31
|
+
localSocket.setKeepAlive(true, 5000);
|
|
32
|
+
|
|
33
|
+
return new Promise<void>((resolve) => {
|
|
34
|
+
const onError = (err: Error) => {
|
|
35
|
+
console.error(`Gateway tunnel error: tunnelId=${tunnelId} error=${err.message}`);
|
|
36
|
+
localSocket.destroy();
|
|
37
|
+
this.scheduleReconnect(tunnelId);
|
|
38
|
+
resolve();
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
localSocket.once('error', onError);
|
|
42
|
+
|
|
43
|
+
localSocket.once('connect', () => {
|
|
44
|
+
localSocket.removeListener('error', onError);
|
|
45
|
+
|
|
46
|
+
tunnel.localSocket = localSocket;
|
|
47
|
+
console.log(`Gateway tunnel connected: tunnelId=${tunnelId} port=${tunnel.port}`);
|
|
48
|
+
|
|
49
|
+
if (isInitial) {
|
|
50
|
+
tunnel.serverSocket.emit('port-forward:created', { tunnelId, success: true });
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
localSocket.on('data', (data: Buffer) => {
|
|
54
|
+
if (tunnel.pendingRequestId) {
|
|
55
|
+
tunnel.responseBuffer.push(data);
|
|
56
|
+
if (tunnel.responseTimer) clearTimeout(tunnel.responseTimer);
|
|
57
|
+
tunnel.responseTimer = setTimeout(() => this.flushResponse(tunnel), HTTP_RESPONSE_COLLECT_MS);
|
|
58
|
+
} else {
|
|
59
|
+
tunnel.serverSocket.emit('port-forward:data', {
|
|
60
|
+
tunnelId,
|
|
61
|
+
data: data.toString('base64'),
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
localSocket.on('close', () => {
|
|
67
|
+
console.log(`Gateway local socket closed: tunnelId=${tunnelId}`);
|
|
68
|
+
tunnel.localSocket = null;
|
|
69
|
+
this.scheduleReconnect(tunnelId);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
localSocket.on('error', (err: Error) => {
|
|
73
|
+
console.error(`Gateway local socket error: tunnelId=${tunnelId} error=${err.message}`);
|
|
74
|
+
tunnel.serverSocket.emit('port-forward:error', { tunnelId, error: err.message });
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
resolve();
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
private flushResponse(tunnel: Tunnel): void {
|
|
83
|
+
if (!tunnel.pendingRequestId) return;
|
|
84
|
+
const combined = Buffer.concat(tunnel.responseBuffer);
|
|
85
|
+
tunnel.responseBuffer = [];
|
|
86
|
+
const requestId = tunnel.pendingRequestId;
|
|
87
|
+
tunnel.pendingRequestId = null;
|
|
88
|
+
tunnel.responseTimer = null;
|
|
89
|
+
|
|
90
|
+
tunnel.serverSocket.emit('port-forward:data', {
|
|
91
|
+
tunnelId: tunnel.id,
|
|
92
|
+
data: Buffer.from(JSON.stringify({
|
|
93
|
+
type: 'http-response',
|
|
94
|
+
requestId,
|
|
95
|
+
body: combined.toString('base64'),
|
|
96
|
+
})).toString('base64'),
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
private scheduleReconnect(tunnelId: string): void {
|
|
101
|
+
if (!this.tunnels.has(tunnelId) || this.reconnectTimers.has(tunnelId)) return;
|
|
102
|
+
|
|
103
|
+
console.log(`Gateway scheduling reconnection: tunnelId=${tunnelId}`);
|
|
104
|
+
const timer = setTimeout(() => {
|
|
105
|
+
this.reconnectTimers.delete(tunnelId);
|
|
106
|
+
this.connect(tunnelId, false);
|
|
107
|
+
}, RECONNECT_INTERVAL_MS);
|
|
108
|
+
|
|
109
|
+
this.reconnectTimers.set(tunnelId, timer);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
handleTunnelData(tunnelId: string, data: string): void {
|
|
113
|
+
const tunnel = this.tunnels.get(tunnelId);
|
|
114
|
+
if (!tunnel) {
|
|
115
|
+
console.warn(`Gateway data for unknown tunnel: tunnelId=${tunnelId}`);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const buffer = Buffer.from(data, 'base64');
|
|
120
|
+
let parsed: any;
|
|
121
|
+
try {
|
|
122
|
+
parsed = JSON.parse(buffer.toString('utf8'));
|
|
123
|
+
} catch {
|
|
124
|
+
if (tunnel.localSocket) {
|
|
125
|
+
tunnel.localSocket.write(buffer);
|
|
126
|
+
}
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (parsed.type === 'http-request') {
|
|
131
|
+
const { method, path, headers, body } = parsed;
|
|
132
|
+
let rawRequest = `${method} ${path} HTTP/1.1\r\n`;
|
|
133
|
+
for (const [key, value] of Object.entries(headers || {})) {
|
|
134
|
+
rawRequest += `${key}: ${value}\r\n`;
|
|
135
|
+
}
|
|
136
|
+
rawRequest += `Host: 127.0.0.1:${tunnel.port}\r\n`;
|
|
137
|
+
rawRequest += 'Connection: close\r\n';
|
|
138
|
+
rawRequest += '\r\n';
|
|
139
|
+
if (body) rawRequest += Buffer.from(body, 'base64').toString('utf8');
|
|
140
|
+
|
|
141
|
+
tunnel.pendingRequestId = parsed.requestId;
|
|
142
|
+
tunnel.responseBuffer = [];
|
|
143
|
+
|
|
144
|
+
if (tunnel.localSocket) {
|
|
145
|
+
tunnel.localSocket.write(rawRequest);
|
|
146
|
+
}
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (tunnel.localSocket) {
|
|
151
|
+
tunnel.localSocket.write(buffer);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
closeTunnel(tunnelId: string): void {
|
|
156
|
+
const tunnel = this.tunnels.get(tunnelId);
|
|
157
|
+
if (!tunnel) return;
|
|
158
|
+
|
|
159
|
+
console.log(`Gateway closing tunnel: tunnelId=${tunnelId}`);
|
|
160
|
+
|
|
161
|
+
const timer = this.reconnectTimers.get(tunnelId);
|
|
162
|
+
if (timer) {
|
|
163
|
+
clearTimeout(timer);
|
|
164
|
+
this.reconnectTimers.delete(tunnelId);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
this.tunnels.delete(tunnelId);
|
|
168
|
+
|
|
169
|
+
if (tunnel.localSocket) {
|
|
170
|
+
tunnel.localSocket.destroy();
|
|
171
|
+
}
|
|
172
|
+
tunnel.serverSocket.emit('port-forward:close', { tunnelId });
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
closeAll(): void {
|
|
176
|
+
console.log(`Gateway closing all tunnels: count=${this.tunnels.size}`);
|
|
177
|
+
|
|
178
|
+
for (const [tunnelId, tunnel] of [...this.tunnels]) {
|
|
179
|
+
const timer = this.reconnectTimers.get(tunnelId);
|
|
180
|
+
if (timer) {
|
|
181
|
+
clearTimeout(timer);
|
|
182
|
+
this.reconnectTimers.delete(tunnelId);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
this.tunnels.delete(tunnelId);
|
|
186
|
+
|
|
187
|
+
if (tunnel.localSocket) {
|
|
188
|
+
tunnel.localSocket.destroy();
|
|
189
|
+
}
|
|
190
|
+
tunnel.serverSocket.emit('port-forward:close', { tunnelId: tunnel.id });
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export const gatewayManager = new GatewayManager();
|
package/src/socketio-client.ts
CHANGED
|
@@ -13,7 +13,7 @@ import { AcpSyncBridge } from './acp/sync-bridge.js';
|
|
|
13
13
|
import { gitHandler } from './git-handler.js';
|
|
14
14
|
import { worktreeHandler } from './worktree-handler.js';
|
|
15
15
|
import { fsHandler } from './fs-handler.js';
|
|
16
|
-
import {
|
|
16
|
+
import { gatewayManager } from './gateway.js';
|
|
17
17
|
|
|
18
18
|
const CONTROLLER_VERSION = '1.1.1';
|
|
19
19
|
const acpBridges = new Map<string, AcpSessionBridge>();
|
|
@@ -83,12 +83,14 @@ interface AcpResumePayload {
|
|
|
83
83
|
|
|
84
84
|
interface AcpListSessionsPayload {
|
|
85
85
|
requestId: string;
|
|
86
|
+
serverType: string;
|
|
86
87
|
cwd: string;
|
|
87
88
|
}
|
|
88
89
|
|
|
89
90
|
interface AcpLoadSessionPayload {
|
|
90
91
|
requestId: string;
|
|
91
92
|
sessionId: string;
|
|
93
|
+
serverType: string;
|
|
92
94
|
cwd: string;
|
|
93
95
|
}
|
|
94
96
|
|
|
@@ -343,8 +345,10 @@ export function connectToServer(
|
|
|
343
345
|
});
|
|
344
346
|
|
|
345
347
|
socket.on('acp:list-sessions', async (data: AcpListSessionsPayload) => {
|
|
346
|
-
|
|
347
|
-
|
|
348
|
+
const serverType = data.serverType ?? 'opencode';
|
|
349
|
+
console.log(`[ACP sync] acp:list-sessions received: requestId=${data.requestId} serverType=${serverType} cwd=${data.cwd}`);
|
|
350
|
+
const spawner = createSpawner(serverType, `sync_${Date.now()}`, data.cwd);
|
|
351
|
+
const bridge = new AcpSyncBridge({ cwd: data.cwd }, spawner);
|
|
348
352
|
try {
|
|
349
353
|
await bridge.connect();
|
|
350
354
|
const result = await bridge.listSessions(data.cwd);
|
|
@@ -371,8 +375,10 @@ export function connectToServer(
|
|
|
371
375
|
});
|
|
372
376
|
|
|
373
377
|
socket.on('acp:load-session', async (data: AcpLoadSessionPayload) => {
|
|
374
|
-
|
|
375
|
-
|
|
378
|
+
const serverType = data.serverType ?? 'opencode';
|
|
379
|
+
console.log(`[ACP] load-session received: requestId=${data.requestId} sessionId=${data.sessionId} serverType=${serverType} cwd=${data.cwd}`);
|
|
380
|
+
const spawner = createSpawner(serverType, `sync_${Date.now()}`, data.cwd);
|
|
381
|
+
const bridge = new AcpSyncBridge({ cwd: data.cwd }, spawner);
|
|
376
382
|
try {
|
|
377
383
|
await bridge.connect();
|
|
378
384
|
const { config, updates } = await bridge.loadSession(data.sessionId, data.cwd);
|
|
@@ -585,19 +591,19 @@ export function connectToServer(
|
|
|
585
591
|
socket.emit('git:commit:result', { requestId: data.requestId, ...result });
|
|
586
592
|
});
|
|
587
593
|
|
|
588
|
-
// Port Forward events
|
|
594
|
+
// Port Forward events (event names kept unchanged for backward compatibility)
|
|
589
595
|
socket.on('port-forward:create', async (data: { tunnelId: string; port: number }) => {
|
|
590
|
-
console.log(`
|
|
591
|
-
await
|
|
596
|
+
console.log(`Gateway tunnel create requested: tunnelId=${data.tunnelId} port=${data.port}`);
|
|
597
|
+
await gatewayManager.createTunnel(data.tunnelId, data.port, socket);
|
|
592
598
|
});
|
|
593
599
|
|
|
594
600
|
socket.on('port-forward:data', (data: { tunnelId: string; data: string }) => {
|
|
595
|
-
|
|
601
|
+
gatewayManager.handleTunnelData(data.tunnelId, data.data);
|
|
596
602
|
});
|
|
597
603
|
|
|
598
604
|
socket.on('port-forward:close', (data: { tunnelId: string }) => {
|
|
599
|
-
console.log(`
|
|
600
|
-
|
|
605
|
+
console.log(`Gateway tunnel close: tunnelId=${data.tunnelId}`);
|
|
606
|
+
gatewayManager.closeTunnel(data.tunnelId);
|
|
601
607
|
});
|
|
602
608
|
|
|
603
609
|
socket.on('acp:permission:response', (data: {
|
|
@@ -614,7 +620,7 @@ export function connectToServer(
|
|
|
614
620
|
socket.on('disconnect', (reason) => {
|
|
615
621
|
console.log(`Disconnected: ${reason}`);
|
|
616
622
|
heartbeat.stop();
|
|
617
|
-
|
|
623
|
+
gatewayManager.closeAll();
|
|
618
624
|
permissionRequestBridge.clear();
|
|
619
625
|
});
|
|
620
626
|
|
|
@@ -32,71 +32,148 @@ describe('AcpDetector', () => {
|
|
|
32
32
|
});
|
|
33
33
|
|
|
34
34
|
describe('detect', () => {
|
|
35
|
-
it('should detect
|
|
35
|
+
it('should detect all 7 servers when all available', async () => {
|
|
36
36
|
mockExecSuccessOnce('opencode v1.2.3\n');
|
|
37
37
|
mockExecSuccessOnce('copilot 1.0.0\n');
|
|
38
|
+
mockExecSuccessOnce('claude-agent-acp 0.1.0\n');
|
|
39
|
+
mockExecSuccessOnce('gemini 2.0.0\n');
|
|
40
|
+
mockExecSuccessOnce('codex-acp 0.5.0\n');
|
|
41
|
+
mockExecSuccessOnce('mimo 0.1.0\n');
|
|
42
|
+
mockExecSuccessOnce('kimi 1.0.0\n');
|
|
38
43
|
|
|
39
44
|
const result = await detector.detect();
|
|
40
45
|
|
|
41
|
-
expect(result).toHaveLength(
|
|
46
|
+
expect(result).toHaveLength(7);
|
|
42
47
|
expect(result[0]).toEqual({ type: 'opencode', available: true, version: '1.2.3' });
|
|
43
48
|
expect(result[1]).toEqual({ type: 'github_copilot', available: true, version: '1.0.0' });
|
|
49
|
+
expect(result[2]).toEqual({ type: 'claude_code', available: true, version: '0.1.0' });
|
|
50
|
+
expect(result[3]).toEqual({ type: 'gemini_cli', available: true, version: '2.0.0' });
|
|
51
|
+
expect(result[4]).toEqual({ type: 'codex', available: true, version: '0.5.0' });
|
|
52
|
+
expect(result[5]).toEqual({ type: 'mimo_code', available: true, version: '0.1.0' });
|
|
53
|
+
expect(result[6]).toEqual({ type: 'kimi_code', available: true, version: '1.0.0' });
|
|
44
54
|
});
|
|
45
55
|
|
|
46
56
|
it('should detect as unavailable when command fails', async () => {
|
|
47
57
|
mockExecFailureOnce();
|
|
48
58
|
mockExecFailureOnce();
|
|
59
|
+
mockExecFailureOnce();
|
|
60
|
+
mockExecFailureOnce();
|
|
61
|
+
mockExecFailureOnce();
|
|
62
|
+
mockExecFailureOnce();
|
|
63
|
+
mockExecFailureOnce();
|
|
49
64
|
|
|
50
65
|
const result = await detector.detect();
|
|
51
66
|
|
|
52
|
-
expect(result).toHaveLength(
|
|
67
|
+
expect(result).toHaveLength(7);
|
|
53
68
|
expect(result[0]).toEqual({ type: 'opencode', available: false });
|
|
54
69
|
expect(result[1]).toEqual({ type: 'github_copilot', available: false });
|
|
70
|
+
expect(result[2]).toEqual({ type: 'claude_code', available: false });
|
|
71
|
+
expect(result[3]).toEqual({ type: 'gemini_cli', available: false });
|
|
72
|
+
expect(result[4]).toEqual({ type: 'codex', available: false });
|
|
73
|
+
expect(result[5]).toEqual({ type: 'mimo_code', available: false });
|
|
74
|
+
expect(result[6]).toEqual({ type: 'kimi_code', available: false });
|
|
55
75
|
});
|
|
56
76
|
|
|
57
|
-
it('should
|
|
58
|
-
|
|
59
|
-
|
|
77
|
+
it('should detect claude_code only when other servers fail', async () => {
|
|
78
|
+
mockExecFailureOnce();
|
|
79
|
+
mockExecFailureOnce();
|
|
80
|
+
mockExecSuccessOnce('claude-agent-acp 0.1.0\n');
|
|
81
|
+
mockExecFailureOnce();
|
|
82
|
+
mockExecFailureOnce();
|
|
83
|
+
mockExecFailureOnce();
|
|
84
|
+
mockExecFailureOnce();
|
|
60
85
|
|
|
61
86
|
const result = await detector.detect();
|
|
62
87
|
|
|
63
|
-
expect(result
|
|
64
|
-
expect(result[
|
|
88
|
+
expect(result).toHaveLength(7);
|
|
89
|
+
expect(result[0]).toEqual({ type: 'opencode', available: false });
|
|
90
|
+
expect(result[1]).toEqual({ type: 'github_copilot', available: false });
|
|
91
|
+
expect(result[2]).toEqual({ type: 'claude_code', available: true, version: '0.1.0' });
|
|
92
|
+
expect(result[3]).toEqual({ type: 'gemini_cli', available: false });
|
|
93
|
+
expect(result[4]).toEqual({ type: 'codex', available: false });
|
|
94
|
+
expect(result[5]).toEqual({ type: 'mimo_code', available: false });
|
|
95
|
+
expect(result[6]).toEqual({ type: 'kimi_code', available: false });
|
|
65
96
|
});
|
|
66
97
|
|
|
67
|
-
it('should
|
|
68
|
-
|
|
69
|
-
|
|
98
|
+
it('should detect gemini_cli only when other servers fail', async () => {
|
|
99
|
+
mockExecFailureOnce();
|
|
100
|
+
mockExecFailureOnce();
|
|
101
|
+
mockExecFailureOnce();
|
|
102
|
+
mockExecSuccessOnce('gemini 2.0.0\n');
|
|
103
|
+
mockExecFailureOnce();
|
|
104
|
+
mockExecFailureOnce();
|
|
105
|
+
mockExecFailureOnce();
|
|
70
106
|
|
|
71
107
|
const result = await detector.detect();
|
|
72
108
|
|
|
73
|
-
expect(result
|
|
74
|
-
expect(result[
|
|
109
|
+
expect(result).toHaveLength(7);
|
|
110
|
+
expect(result[3]).toEqual({ type: 'gemini_cli', available: true, version: '2.0.0' });
|
|
75
111
|
});
|
|
76
112
|
|
|
77
|
-
it('should detect only
|
|
78
|
-
|
|
113
|
+
it('should detect codex only when other servers fail', async () => {
|
|
114
|
+
mockExecFailureOnce();
|
|
115
|
+
mockExecFailureOnce();
|
|
116
|
+
mockExecFailureOnce();
|
|
117
|
+
mockExecFailureOnce();
|
|
118
|
+
mockExecSuccessOnce('codex-acp 0.5.0\n');
|
|
119
|
+
mockExecFailureOnce();
|
|
79
120
|
mockExecFailureOnce();
|
|
80
121
|
|
|
81
122
|
const result = await detector.detect();
|
|
82
123
|
|
|
83
|
-
expect(result).toHaveLength(
|
|
84
|
-
expect(result[
|
|
85
|
-
expect(result[1]).toEqual({ type: 'github_copilot', available: false });
|
|
124
|
+
expect(result).toHaveLength(7);
|
|
125
|
+
expect(result[4]).toEqual({ type: 'codex', available: true, version: '0.5.0' });
|
|
86
126
|
});
|
|
87
127
|
|
|
88
|
-
it('should detect only
|
|
128
|
+
it('should detect mimo_code only when other servers fail', async () => {
|
|
129
|
+
mockExecFailureOnce();
|
|
130
|
+
mockExecFailureOnce();
|
|
131
|
+
mockExecFailureOnce();
|
|
132
|
+
mockExecFailureOnce();
|
|
133
|
+
mockExecFailureOnce();
|
|
134
|
+
mockExecSuccessOnce('mimo 0.1.0\n');
|
|
89
135
|
mockExecFailureOnce();
|
|
90
|
-
mockExecSuccessOnce('copilot 1.0.0\n');
|
|
91
136
|
|
|
92
137
|
const result = await detector.detect();
|
|
93
138
|
|
|
94
|
-
expect(result).toHaveLength(
|
|
95
|
-
expect(result[
|
|
96
|
-
expect(result[1]).toEqual({ type: 'github_copilot', available: true, version: '1.0.0' });
|
|
139
|
+
expect(result).toHaveLength(7);
|
|
140
|
+
expect(result[5]).toEqual({ type: 'mimo_code', available: true, version: '0.1.0' });
|
|
97
141
|
});
|
|
98
142
|
|
|
143
|
+
it('should detect kimi_code only when other servers fail', async () => {
|
|
144
|
+
mockExecFailureOnce();
|
|
145
|
+
mockExecFailureOnce();
|
|
146
|
+
mockExecFailureOnce();
|
|
147
|
+
mockExecFailureOnce();
|
|
148
|
+
mockExecFailureOnce();
|
|
149
|
+
mockExecFailureOnce();
|
|
150
|
+
mockExecSuccessOnce('kimi 2.0.0\n');
|
|
151
|
+
|
|
152
|
+
const result = await detector.detect();
|
|
153
|
+
|
|
154
|
+
expect(result).toHaveLength(7);
|
|
155
|
+
expect(result[6]).toEqual({ type: 'kimi_code', available: true, version: '2.0.0' });
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('should parse version with different formats', async () => {
|
|
159
|
+
mockExecSuccessOnce('OpenCode version 2.0.1\n');
|
|
160
|
+
mockExecSuccessOnce('copilot 3.0.0\n');
|
|
161
|
+
mockExecSuccessOnce('v0.1.0\n');
|
|
162
|
+
mockExecSuccessOnce('Gemini 2.0.0\n');
|
|
163
|
+
mockExecSuccessOnce('codex-acp version 0.5.0\n');
|
|
164
|
+
mockExecSuccessOnce('MiMo CLI v1.0.0\n');
|
|
165
|
+
mockExecSuccessOnce('kimi 2.0.0\n');
|
|
166
|
+
|
|
167
|
+
const result = await detector.detect();
|
|
99
168
|
|
|
169
|
+
expect(result[0].version).toBe('2.0.1');
|
|
170
|
+
expect(result[1].version).toBe('3.0.0');
|
|
171
|
+
expect(result[2].version).toBe('0.1.0');
|
|
172
|
+
expect(result[3].version).toBe('2.0.0');
|
|
173
|
+
expect(result[4].version).toBe('0.5.0');
|
|
174
|
+
expect(result[5].version).toBe('1.0.0');
|
|
175
|
+
expect(result[6].version).toBe('2.0.0');
|
|
176
|
+
});
|
|
100
177
|
});
|
|
101
178
|
|
|
102
179
|
describe('getSpawnerConfig', () => {
|
|
@@ -110,6 +187,31 @@ describe('AcpDetector', () => {
|
|
|
110
187
|
expect(config).toEqual({ command: 'copilot', args: ['--acp', '--stdio'] });
|
|
111
188
|
});
|
|
112
189
|
|
|
190
|
+
it('should return claude_code spawner config', () => {
|
|
191
|
+
const config = detector.getSpawnerConfig('claude_code');
|
|
192
|
+
expect(config).toEqual({ command: 'claude-agent-acp', args: [] });
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it('should return gemini_cli spawner config', () => {
|
|
196
|
+
const config = detector.getSpawnerConfig('gemini_cli');
|
|
197
|
+
expect(config).toEqual({ command: 'gemini', args: ['--acp'] });
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it('should return codex spawner config', () => {
|
|
201
|
+
const config = detector.getSpawnerConfig('codex');
|
|
202
|
+
expect(config).toEqual({ command: 'codex-acp', args: [] });
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it('should return mimo_code spawner config', () => {
|
|
206
|
+
const config = detector.getSpawnerConfig('mimo_code');
|
|
207
|
+
expect(config).toEqual({ command: 'mimo', args: ['acp'] });
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it('should return kimi_code spawner config', () => {
|
|
211
|
+
const config = detector.getSpawnerConfig('kimi_code');
|
|
212
|
+
expect(config).toEqual({ command: 'kimi', args: ['acp'] });
|
|
213
|
+
});
|
|
214
|
+
|
|
113
215
|
it('should return opencode as default for unknown serverType', () => {
|
|
114
216
|
const config = detector.getSpawnerConfig('unknown_server');
|
|
115
217
|
expect(config).toEqual({ command: 'opencode', args: ['acp'] });
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"port-forward.d.ts","sourceRoot":"","sources":["../src/port-forward.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAQ/C,qBAAa,kBAAkB;IAC7B,OAAO,CAAC,OAAO,CAAkC;IAE3C,YAAY,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAiDvF,gBAAgB,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI;IAStD,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI;IAQnC,QAAQ,IAAI,IAAI;CAOjB;AAED,eAAO,MAAM,kBAAkB,oBAA2B,CAAC"}
|
package/dist/port-forward.js
DELETED
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
import net from 'node:net';
|
|
2
|
-
export class PortForwardManager {
|
|
3
|
-
tunnels = new Map();
|
|
4
|
-
async createTunnel(tunnelId, port, serverSocket) {
|
|
5
|
-
const localSocket = net.connect({ host: '127.0.0.1', port });
|
|
6
|
-
return new Promise((resolve) => {
|
|
7
|
-
const onError = (err) => {
|
|
8
|
-
console.error(`Port forward tunnel error: tunnelId=${tunnelId} error=${err.message}`);
|
|
9
|
-
serverSocket.emit('port-forward:created', {
|
|
10
|
-
tunnelId,
|
|
11
|
-
success: false,
|
|
12
|
-
error: err.message,
|
|
13
|
-
});
|
|
14
|
-
localSocket.destroy();
|
|
15
|
-
resolve();
|
|
16
|
-
};
|
|
17
|
-
localSocket.once('error', onError);
|
|
18
|
-
localSocket.once('connect', () => {
|
|
19
|
-
localSocket.removeListener('error', onError);
|
|
20
|
-
this.tunnels.set(tunnelId, { id: tunnelId, port, localSocket });
|
|
21
|
-
console.log(`Port forward tunnel created: tunnelId=${tunnelId} port=${port}`);
|
|
22
|
-
serverSocket.emit('port-forward:created', { tunnelId, success: true });
|
|
23
|
-
localSocket.on('data', (data) => {
|
|
24
|
-
serverSocket.emit('port-forward:data', {
|
|
25
|
-
tunnelId,
|
|
26
|
-
data: data.toString('base64'),
|
|
27
|
-
});
|
|
28
|
-
});
|
|
29
|
-
localSocket.on('close', () => {
|
|
30
|
-
console.log(`Port forward local socket closed: tunnelId=${tunnelId}`);
|
|
31
|
-
this.tunnels.delete(tunnelId);
|
|
32
|
-
serverSocket.emit('port-forward:close', { tunnelId });
|
|
33
|
-
});
|
|
34
|
-
localSocket.on('error', (err) => {
|
|
35
|
-
console.error(`Port forward local socket error: tunnelId=${tunnelId} error=${err.message}`);
|
|
36
|
-
this.tunnels.delete(tunnelId);
|
|
37
|
-
serverSocket.emit('port-forward:error', { tunnelId, error: err.message });
|
|
38
|
-
});
|
|
39
|
-
resolve();
|
|
40
|
-
});
|
|
41
|
-
});
|
|
42
|
-
}
|
|
43
|
-
handleTunnelData(tunnelId, data) {
|
|
44
|
-
const tunnel = this.tunnels.get(tunnelId);
|
|
45
|
-
if (!tunnel) {
|
|
46
|
-
console.warn(`Port forward data for unknown tunnel: tunnelId=${tunnelId}`);
|
|
47
|
-
return;
|
|
48
|
-
}
|
|
49
|
-
tunnel.localSocket.write(Buffer.from(data, 'base64'));
|
|
50
|
-
}
|
|
51
|
-
closeTunnel(tunnelId) {
|
|
52
|
-
const tunnel = this.tunnels.get(tunnelId);
|
|
53
|
-
if (!tunnel)
|
|
54
|
-
return;
|
|
55
|
-
console.log(`Port forward closing tunnel: tunnelId=${tunnelId}`);
|
|
56
|
-
tunnel.localSocket.destroy();
|
|
57
|
-
this.tunnels.delete(tunnelId);
|
|
58
|
-
}
|
|
59
|
-
closeAll() {
|
|
60
|
-
console.log(`Port forward closing all tunnels: count=${this.tunnels.size}`);
|
|
61
|
-
for (const tunnel of this.tunnels.values()) {
|
|
62
|
-
tunnel.localSocket.destroy();
|
|
63
|
-
}
|
|
64
|
-
this.tunnels.clear();
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
export const portForwardManager = new PortForwardManager();
|
|
68
|
-
//# sourceMappingURL=port-forward.js.map
|
package/dist/port-forward.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"port-forward.js","sourceRoot":"","sources":["../src/port-forward.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"port-forward.test.d.ts","sourceRoot":"","sources":["../src/port-forward.test.ts"],"names":[],"mappings":""}
|