agent-gear 1.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/AGENTS.md +67 -0
- package/dist/acp/bridge.d.ts +56 -0
- package/dist/acp/bridge.d.ts.map +1 -0
- package/dist/acp/bridge.js +295 -0
- package/dist/acp/bridge.js.map +1 -0
- package/dist/acp/detector.d.ts +13 -0
- package/dist/acp/detector.d.ts.map +1 -0
- package/dist/acp/detector.js +29 -0
- package/dist/acp/detector.js.map +1 -0
- package/dist/acp/spawner.d.ts +24 -0
- package/dist/acp/spawner.d.ts.map +1 -0
- package/dist/acp/spawner.js +82 -0
- package/dist/acp/spawner.js.map +1 -0
- package/dist/acp/sync-bridge.d.ts +21 -0
- package/dist/acp/sync-bridge.d.ts.map +1 -0
- package/dist/acp/sync-bridge.js +75 -0
- package/dist/acp/sync-bridge.js.map +1 -0
- package/dist/config.d.ts +22 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +63 -0
- package/dist/config.js.map +1 -0
- package/dist/daemon.d.ts +5 -0
- package/dist/daemon.d.ts.map +1 -0
- package/dist/daemon.js +32 -0
- package/dist/daemon.js.map +1 -0
- package/dist/git-handler.d.ts +17 -0
- package/dist/git-handler.d.ts.map +1 -0
- package/dist/git-handler.js +60 -0
- package/dist/git-handler.js.map +1 -0
- package/dist/git-handler.test.d.ts +2 -0
- package/dist/git-handler.test.d.ts.map +1 -0
- package/dist/git-handler.test.js +127 -0
- package/dist/git-handler.test.js.map +1 -0
- package/dist/heartbeat.d.ts +6 -0
- package/dist/heartbeat.d.ts.map +1 -0
- package/dist/heartbeat.js +16 -0
- package/dist/heartbeat.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +52 -0
- package/dist/index.js.map +1 -0
- package/dist/socketio-client.d.ts +6 -0
- package/dist/socketio-client.d.ts.map +1 -0
- package/dist/socketio-client.js +385 -0
- package/dist/socketio-client.js.map +1 -0
- package/package.json +34 -0
- package/src/acp/bridge.ts +327 -0
- package/src/acp/detector.ts +40 -0
- package/src/acp/spawner.ts +94 -0
- package/src/acp/sync-bridge.ts +111 -0
- package/src/config.ts +75 -0
- package/src/daemon.ts +33 -0
- package/src/git-handler.test.ts +153 -0
- package/src/git-handler.ts +75 -0
- package/src/heartbeat.ts +18 -0
- package/src/index.ts +60 -0
- package/src/socketio-client.ts +497 -0
- package/tests/acp-bridge.test.ts +473 -0
- package/tests/acp-detector.test.ts +70 -0
- package/tests/acp-spawner.test.ts +149 -0
- package/tests/acp-sync-bridge.test.ts +263 -0
- package/tests/config.test.ts +166 -0
- package/tests/socketio-client.test.ts +196 -0
- package/tsconfig.json +21 -0
- package/vitest.config.ts +14 -0
|
@@ -0,0 +1,497 @@
|
|
|
1
|
+
import { io as createSocketIOClient, type Socket } from 'socket.io-client';
|
|
2
|
+
import { hostname } from 'os';
|
|
3
|
+
import { readdir } from 'fs/promises';
|
|
4
|
+
import { join } from 'path';
|
|
5
|
+
import { homedir } from 'os';
|
|
6
|
+
import { Heartbeat } from './heartbeat.js';
|
|
7
|
+
import { clearConfigKey, saveKeyToConfig, saveEndpointToConfig } from './config.js';
|
|
8
|
+
import { acpDetector } from './acp/detector.js';
|
|
9
|
+
import { AcpSessionBridge } from './acp/bridge.js';
|
|
10
|
+
import { AcpSyncBridge } from './acp/sync-bridge.js';
|
|
11
|
+
import { gitHandler } from './git-handler.js';
|
|
12
|
+
|
|
13
|
+
const CONTROLLER_VERSION = '1.0.0';
|
|
14
|
+
const acpBridges = new Map<string, AcpSessionBridge>();
|
|
15
|
+
|
|
16
|
+
interface RegisteredPayload {
|
|
17
|
+
controllerId: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface ErrorPayload {
|
|
21
|
+
code: string;
|
|
22
|
+
message: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface ShutdownPayload {
|
|
26
|
+
reason: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface AcpCheckPayload {
|
|
30
|
+
requestId: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface AcpStartPayload {
|
|
34
|
+
requestId: string;
|
|
35
|
+
sessionId: string;
|
|
36
|
+
serverType: string;
|
|
37
|
+
cwd: string;
|
|
38
|
+
mcpServers?: Array<{ name: string; command: string; args?: string[]; env?: Array<{ name: string; value: string }> }>;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
interface AcpMessagePayload {
|
|
42
|
+
sessionId: string;
|
|
43
|
+
message: { type: string; content: string };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
interface AcpCancelPayload {
|
|
47
|
+
sessionId: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
interface AcpStopPayload {
|
|
51
|
+
sessionId: string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
interface AcpSetModePayload {
|
|
55
|
+
sessionId: string;
|
|
56
|
+
modeId: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
interface AcpSetModelPayload {
|
|
60
|
+
sessionId: string;
|
|
61
|
+
modelId: string;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
interface AcpResumePayload {
|
|
65
|
+
requestId: string;
|
|
66
|
+
sessionId: string;
|
|
67
|
+
externalSessionId: string;
|
|
68
|
+
cwd: string;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
interface AcpListSessionsPayload {
|
|
72
|
+
requestId: string;
|
|
73
|
+
cwd: string;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
interface AcpLoadSessionPayload {
|
|
77
|
+
requestId: string;
|
|
78
|
+
sessionId: string;
|
|
79
|
+
cwd: string;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
interface FsBrowsePayload {
|
|
83
|
+
requestId: string;
|
|
84
|
+
path: string | null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
interface FsHomePayload {
|
|
88
|
+
requestId: string;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
interface GitCheckPayload {
|
|
92
|
+
requestId: string;
|
|
93
|
+
path: string;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
interface GitInitPayload {
|
|
97
|
+
requestId: string;
|
|
98
|
+
path: string;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function connectToServer(
|
|
102
|
+
endpoint: string,
|
|
103
|
+
key: string,
|
|
104
|
+
options?: { serverName?: string },
|
|
105
|
+
): Socket {
|
|
106
|
+
const socket = createSocketIOClient(endpoint, {
|
|
107
|
+
transports: ['websocket'],
|
|
108
|
+
reconnection: true,
|
|
109
|
+
reconnectionDelay: 1000,
|
|
110
|
+
reconnectionDelayMax: 60000,
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
const heartbeat = new Heartbeat();
|
|
114
|
+
const controllerName = options?.serverName || hostname();
|
|
115
|
+
|
|
116
|
+
socket.on('connect', () => {
|
|
117
|
+
console.log(`Connected to ${endpoint}`);
|
|
118
|
+
socket.emit('register', {
|
|
119
|
+
key,
|
|
120
|
+
hostname: controllerName,
|
|
121
|
+
version: CONTROLLER_VERSION,
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
socket.on('registered', (data: RegisteredPayload) => {
|
|
126
|
+
console.log(`Controller registered: ${data.controllerId}`);
|
|
127
|
+
heartbeat.start(() => socket.emit('heartbeat'));
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
socket.on('error', (data: ErrorPayload) => {
|
|
131
|
+
console.error(`Server error: ${data.code} - ${data.message}`);
|
|
132
|
+
if (data.code === 'KEY_ALREADY_ONLINE') {
|
|
133
|
+
console.error('This key is already in use on another machine.');
|
|
134
|
+
}
|
|
135
|
+
socket.disconnect();
|
|
136
|
+
process.exit(1);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
socket.on('unregistered', () => {
|
|
140
|
+
console.log('Unregistered from server');
|
|
141
|
+
heartbeat.stop();
|
|
142
|
+
clearConfigKey();
|
|
143
|
+
socket.close();
|
|
144
|
+
process.exit(0);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
socket.on('shutdown', (data: ShutdownPayload) => {
|
|
148
|
+
console.log(`Shutdown requested: ${data.reason}`);
|
|
149
|
+
heartbeat.stop();
|
|
150
|
+
clearConfigKey();
|
|
151
|
+
socket.close();
|
|
152
|
+
process.exit(0);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
// ACP events
|
|
156
|
+
socket.on('acp:check', async (data: AcpCheckPayload) => {
|
|
157
|
+
console.log('ACP check requested');
|
|
158
|
+
const servers = await acpDetector.detect();
|
|
159
|
+
socket.emit('acp:check:result', {
|
|
160
|
+
requestId: data.requestId,
|
|
161
|
+
servers,
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
socket.on('acp:start', async (data: AcpStartPayload) => {
|
|
166
|
+
console.log(`[ACP ctrl] acp:start received: requestId=${data.requestId} sessionId=${data.sessionId} cwd=${data.cwd}`);
|
|
167
|
+
|
|
168
|
+
try {
|
|
169
|
+
const bridge = new AcpSessionBridge(socket, {
|
|
170
|
+
sessionId: data.sessionId,
|
|
171
|
+
cwd: data.cwd,
|
|
172
|
+
mcpServers: data.mcpServers,
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
acpBridges.set(data.sessionId, bridge);
|
|
176
|
+
|
|
177
|
+
const onInitialized = (payload: Record<string, unknown>) => {
|
|
178
|
+
if (payload.sessionId !== data.sessionId) return;
|
|
179
|
+
bridge.off('acp:initialized', onInitialized);
|
|
180
|
+
bridge.off('acp:stopped', onStopped);
|
|
181
|
+
console.log(`[ACP ctrl] emitting acp:start:result: requestId=${data.requestId} sessionId=${data.sessionId} success=true externalSessionId=${payload.acpSessionId}`);
|
|
182
|
+
const result: Record<string, unknown> = {
|
|
183
|
+
requestId: data.requestId,
|
|
184
|
+
sessionId: data.sessionId,
|
|
185
|
+
success: true,
|
|
186
|
+
externalSessionId: payload.acpSessionId,
|
|
187
|
+
};
|
|
188
|
+
if (payload.modes) result.modes = payload.modes;
|
|
189
|
+
if (payload.currentModeId) result.currentModeId = payload.currentModeId;
|
|
190
|
+
if (payload.currentModel) result.currentModel = payload.currentModel;
|
|
191
|
+
if (payload.availableModels) result.availableModels = payload.availableModels;
|
|
192
|
+
if (payload.currentModelId) result.currentModelId = payload.currentModelId;
|
|
193
|
+
socket.emit('acp:start:result', result);
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
const onStopped = (payload: { sessionId: string; reason: string; error?: string }) => {
|
|
197
|
+
if (payload.sessionId !== data.sessionId) return;
|
|
198
|
+
bridge.off('acp:initialized', onInitialized);
|
|
199
|
+
bridge.off('acp:stopped', onStopped);
|
|
200
|
+
acpBridges.delete(data.sessionId);
|
|
201
|
+
console.log(`[ACP ctrl] emitting acp:start:result: requestId=${data.requestId} sessionId=${data.sessionId} success=false`);
|
|
202
|
+
socket.emit('acp:start:result', {
|
|
203
|
+
requestId: data.requestId,
|
|
204
|
+
sessionId: data.sessionId,
|
|
205
|
+
success: false,
|
|
206
|
+
error: payload.error ?? `ACP process stopped during initialization: ${payload.reason}`,
|
|
207
|
+
});
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
bridge.on('acp:initialized', onInitialized);
|
|
211
|
+
bridge.on('acp:stopped', onStopped);
|
|
212
|
+
|
|
213
|
+
console.log(`[ACP ctrl] calling bridge.start() for session ${data.sessionId}`);
|
|
214
|
+
await bridge.start();
|
|
215
|
+
} catch (error) {
|
|
216
|
+
console.error(`[ACP ctrl] bridge.start() failed: ${(error as Error).message}`);
|
|
217
|
+
socket.emit('acp:start:result', {
|
|
218
|
+
requestId: data.requestId,
|
|
219
|
+
sessionId: data.sessionId,
|
|
220
|
+
success: false,
|
|
221
|
+
error: (error as Error).message,
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
socket.on('acp:resume', async (data: AcpResumePayload) => {
|
|
227
|
+
console.log(`[ACP ctrl] acp:resume received: requestId=${data.requestId} sessionId=${data.sessionId} externalSessionId=${data.externalSessionId}`);
|
|
228
|
+
|
|
229
|
+
try {
|
|
230
|
+
const bridge = new AcpSessionBridge(socket, {
|
|
231
|
+
sessionId: data.sessionId,
|
|
232
|
+
cwd: data.cwd,
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
acpBridges.set(data.sessionId, bridge);
|
|
236
|
+
|
|
237
|
+
const onInitialized = (payload: Record<string, unknown>) => {
|
|
238
|
+
if (payload.sessionId !== data.sessionId) return;
|
|
239
|
+
bridge.off('acp:initialized', onInitialized);
|
|
240
|
+
bridge.off('acp:stopped', onStopped);
|
|
241
|
+
const result: Record<string, unknown> = {
|
|
242
|
+
requestId: data.requestId,
|
|
243
|
+
sessionId: data.sessionId,
|
|
244
|
+
success: true,
|
|
245
|
+
};
|
|
246
|
+
if (payload.modes) result.modes = payload.modes;
|
|
247
|
+
if (payload.currentModeId) result.currentModeId = payload.currentModeId;
|
|
248
|
+
if (payload.currentModel) result.currentModel = payload.currentModel;
|
|
249
|
+
if (payload.availableModels) result.availableModels = payload.availableModels;
|
|
250
|
+
if (payload.currentModelId) result.currentModelId = payload.currentModelId;
|
|
251
|
+
socket.emit('acp:resume:result', result);
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
const onStopped = (payload: { sessionId: string; reason: string; error?: string }) => {
|
|
255
|
+
if (payload.sessionId !== data.sessionId) return;
|
|
256
|
+
bridge.off('acp:initialized', onInitialized);
|
|
257
|
+
bridge.off('acp:stopped', onStopped);
|
|
258
|
+
acpBridges.delete(data.sessionId);
|
|
259
|
+
socket.emit('acp:resume:result', {
|
|
260
|
+
requestId: data.requestId,
|
|
261
|
+
sessionId: data.sessionId,
|
|
262
|
+
success: false,
|
|
263
|
+
error: payload.error ?? `ACP resume failed: ${payload.reason}`,
|
|
264
|
+
});
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
bridge.on('acp:initialized', onInitialized);
|
|
268
|
+
bridge.on('acp:stopped', onStopped);
|
|
269
|
+
|
|
270
|
+
await bridge.resume(data.externalSessionId);
|
|
271
|
+
} catch (error) {
|
|
272
|
+
console.error(`[ACP ctrl] bridge.resume() failed: ${(error as Error).message}`);
|
|
273
|
+
socket.emit('acp:resume:result', {
|
|
274
|
+
requestId: data.requestId,
|
|
275
|
+
sessionId: data.sessionId,
|
|
276
|
+
success: false,
|
|
277
|
+
error: (error as Error).message,
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
socket.on('acp:list-sessions', async (data: AcpListSessionsPayload) => {
|
|
283
|
+
console.log(`[ACP sync] acp:list-sessions received: requestId=${data.requestId} cwd=${data.cwd}`);
|
|
284
|
+
const bridge = new AcpSyncBridge({ cwd: data.cwd });
|
|
285
|
+
try {
|
|
286
|
+
await bridge.connect();
|
|
287
|
+
const result = await bridge.listSessions(data.cwd);
|
|
288
|
+
await bridge.disconnect();
|
|
289
|
+
socket.emit('acp:list-sessions:result', {
|
|
290
|
+
requestId: data.requestId,
|
|
291
|
+
success: true,
|
|
292
|
+
sessions: (result.sessions ?? []).map((s) => ({
|
|
293
|
+
sessionId: s.sessionId,
|
|
294
|
+
title: s.title ?? null,
|
|
295
|
+
cwd: s.cwd,
|
|
296
|
+
updatedAt: s.updatedAt ?? null,
|
|
297
|
+
})),
|
|
298
|
+
});
|
|
299
|
+
} catch (error) {
|
|
300
|
+
await bridge.disconnect().catch(() => {});
|
|
301
|
+
console.error(`[ACP sync] list-sessions failed: ${(error as Error).message}`);
|
|
302
|
+
socket.emit('acp:list-sessions:result', {
|
|
303
|
+
requestId: data.requestId,
|
|
304
|
+
success: false,
|
|
305
|
+
error: (error as Error).message,
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
socket.on('acp:load-session', async (data: AcpLoadSessionPayload) => {
|
|
311
|
+
console.log(`[ACP] load-session received: requestId=${data.requestId} sessionId=${data.sessionId} cwd=${data.cwd}`);
|
|
312
|
+
const bridge = new AcpSyncBridge({ cwd: data.cwd });
|
|
313
|
+
try {
|
|
314
|
+
await bridge.connect();
|
|
315
|
+
const { config, updates } = await bridge.loadSession(data.sessionId, data.cwd);
|
|
316
|
+
await bridge.disconnect(data.sessionId);
|
|
317
|
+
|
|
318
|
+
let title: string | null = null;
|
|
319
|
+
const messages: Array<{ direction: 'input' | 'output'; type: string; content: string }> = [];
|
|
320
|
+
|
|
321
|
+
for (const notification of updates) {
|
|
322
|
+
const update = notification.update;
|
|
323
|
+
let direction: 'input' | 'output';
|
|
324
|
+
let type: string;
|
|
325
|
+
|
|
326
|
+
switch (update.sessionUpdate) {
|
|
327
|
+
case 'user_message_chunk':
|
|
328
|
+
direction = 'input';
|
|
329
|
+
type = 'text';
|
|
330
|
+
break;
|
|
331
|
+
case 'agent_message_chunk':
|
|
332
|
+
direction = 'output';
|
|
333
|
+
type = 'text';
|
|
334
|
+
break;
|
|
335
|
+
case 'tool_call':
|
|
336
|
+
case 'tool_call_update':
|
|
337
|
+
direction = 'output';
|
|
338
|
+
type = 'tool_call';
|
|
339
|
+
break;
|
|
340
|
+
case 'plan':
|
|
341
|
+
direction = 'output';
|
|
342
|
+
type = 'plan';
|
|
343
|
+
break;
|
|
344
|
+
case 'session_info_update':
|
|
345
|
+
direction = 'output';
|
|
346
|
+
type = 'text';
|
|
347
|
+
title = update.title ?? title;
|
|
348
|
+
break;
|
|
349
|
+
case 'usage_update':
|
|
350
|
+
case 'config_option_update':
|
|
351
|
+
case 'current_mode_update':
|
|
352
|
+
case 'available_commands_update':
|
|
353
|
+
default:
|
|
354
|
+
direction = 'output';
|
|
355
|
+
type = 'text';
|
|
356
|
+
break;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
messages.push({
|
|
360
|
+
direction,
|
|
361
|
+
type,
|
|
362
|
+
content: JSON.stringify(update),
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
socket.emit('acp:load-session:result', {
|
|
367
|
+
requestId: data.requestId,
|
|
368
|
+
success: true,
|
|
369
|
+
sessionId: data.sessionId,
|
|
370
|
+
title,
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
for (let i = 0; i < messages.length; i++) {
|
|
374
|
+
const msg = messages[i];
|
|
375
|
+
socket.emit('acp:load-session:message', {
|
|
376
|
+
requestId: data.requestId,
|
|
377
|
+
direction: msg.direction,
|
|
378
|
+
type: msg.type,
|
|
379
|
+
content: msg.content,
|
|
380
|
+
});
|
|
381
|
+
if (i % 200 === 0) {
|
|
382
|
+
await new Promise(r => setTimeout(r, 0));
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
await new Promise(r => setTimeout(r, 5));
|
|
386
|
+
socket.emit('acp:load-session:complete', { requestId: data.requestId });
|
|
387
|
+
} catch (error) {
|
|
388
|
+
await bridge.disconnect(data.sessionId).catch(() => {});
|
|
389
|
+
console.error(`[ACP] load-session FAILED: ${(error as Error).message}`);
|
|
390
|
+
console.log(`[ACP] emitting error result, socket.connected=${socket.connected}`);
|
|
391
|
+
socket.emit('acp:load-session:result', {
|
|
392
|
+
requestId: data.requestId,
|
|
393
|
+
success: false,
|
|
394
|
+
sessionId: data.sessionId,
|
|
395
|
+
error: (error as Error).message,
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
socket.on('acp:message', (data: AcpMessagePayload) => {
|
|
401
|
+
const bridge = acpBridges.get(data.sessionId);
|
|
402
|
+
if (bridge) {
|
|
403
|
+
bridge.sendInput(data.message);
|
|
404
|
+
}
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
socket.on('acp:cancel', (data: AcpCancelPayload) => {
|
|
408
|
+
const bridge = acpBridges.get(data.sessionId);
|
|
409
|
+
if (bridge) {
|
|
410
|
+
bridge.cancel();
|
|
411
|
+
}
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
socket.on('acp:stop', (data: AcpStopPayload) => {
|
|
415
|
+
console.log(`ACP stop requested for session ${data.sessionId}`);
|
|
416
|
+
const bridge = acpBridges.get(data.sessionId);
|
|
417
|
+
if (bridge) {
|
|
418
|
+
bridge.stop();
|
|
419
|
+
acpBridges.delete(data.sessionId);
|
|
420
|
+
}
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
socket.on('acp:set-mode', (data: AcpSetModePayload) => {
|
|
424
|
+
console.log(`[ACP] acp:set-mode received: sessionId=${data.sessionId} modeId=${data.modeId}`);
|
|
425
|
+
const bridge = acpBridges.get(data.sessionId);
|
|
426
|
+
if (bridge) {
|
|
427
|
+
bridge.setMode(data.modeId).catch((err) => {
|
|
428
|
+
console.error(`[ACP] setMode failed: ${(err as Error).message}`);
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
socket.on('acp:set-model', (data: AcpSetModelPayload) => {
|
|
434
|
+
console.log(`[ACP] acp:set-model received: sessionId=${data.sessionId} modelId=${data.modelId}`);
|
|
435
|
+
const bridge = acpBridges.get(data.sessionId);
|
|
436
|
+
if (bridge) {
|
|
437
|
+
bridge.setModel(data.modelId).catch((err) => {
|
|
438
|
+
console.error(`[ACP] setModel failed: ${(err as Error).message}`);
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
// File system events
|
|
444
|
+
socket.on('fs:browse', async (data: FsBrowsePayload) => {
|
|
445
|
+
console.log(`FS browse requested: requestId=${data.requestId} path=${data.path}`);
|
|
446
|
+
try {
|
|
447
|
+
const targetPath = data.path || homedir();
|
|
448
|
+
const entries = await readdir(targetPath, { withFileTypes: true });
|
|
449
|
+
const result = entries
|
|
450
|
+
.filter((entry) => !entry.name.startsWith('.'))
|
|
451
|
+
.map((entry) => ({
|
|
452
|
+
name: entry.name,
|
|
453
|
+
path: join(targetPath, entry.name),
|
|
454
|
+
isDirectory: entry.isDirectory(),
|
|
455
|
+
}))
|
|
456
|
+
.sort((a, b) => {
|
|
457
|
+
if (a.isDirectory !== b.isDirectory) return a.isDirectory ? -1 : 1;
|
|
458
|
+
return a.name.localeCompare(b.name);
|
|
459
|
+
});
|
|
460
|
+
socket.emit('fs:browse:result', { requestId: data.requestId, entries: result });
|
|
461
|
+
} catch (error) {
|
|
462
|
+
console.error(`FS browse error: ${(error as Error).message}`);
|
|
463
|
+
socket.emit('fs:browse:result', { requestId: data.requestId, entries: [] });
|
|
464
|
+
}
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
socket.on('fs:home', async (data: FsHomePayload) => {
|
|
468
|
+
console.log(`FS home requested: requestId=${data.requestId}`);
|
|
469
|
+
const homePath = homedir();
|
|
470
|
+
socket.emit('fs:home:result', { requestId: data.requestId, path: homePath });
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
socket.on('git:check', async (data: GitCheckPayload) => {
|
|
474
|
+
const result = await gitHandler.check(data.path);
|
|
475
|
+
socket.emit('git:check:result', { requestId: data.requestId, ...result });
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
socket.on('git:init', async (data: GitInitPayload) => {
|
|
479
|
+
const result = await gitHandler.init(data.path);
|
|
480
|
+
socket.emit('git:init:result', { requestId: data.requestId, ...result });
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
socket.on('disconnect', (reason) => {
|
|
484
|
+
console.log(`Disconnected: ${reason}`);
|
|
485
|
+
heartbeat.stop();
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
socket.on('connect_error', (err) => {
|
|
489
|
+
console.error(`Connection error: ${err.message}`);
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
return socket;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
export function unregister(socket: Socket): void {
|
|
496
|
+
socket.emit('unregister', { reason: 'user_detach' });
|
|
497
|
+
}
|