@agentconnect/host 0.2.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/dist/host.d.ts +36 -0
- package/dist/host.js +920 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +1 -0
- package/dist/observed.d.ts +7 -0
- package/dist/observed.js +69 -0
- package/dist/providers/claude.d.ts +12 -0
- package/dist/providers/claude.js +1188 -0
- package/dist/providers/codex.d.ts +11 -0
- package/dist/providers/codex.js +908 -0
- package/dist/providers/cursor.d.ts +11 -0
- package/dist/providers/cursor.js +866 -0
- package/dist/providers/index.d.ts +5 -0
- package/dist/providers/index.js +111 -0
- package/dist/providers/local.d.ts +9 -0
- package/dist/providers/local.js +114 -0
- package/dist/providers/utils.d.ts +33 -0
- package/dist/providers/utils.js +284 -0
- package/dist/types.d.ts +252 -0
- package/dist/types.js +1 -0
- package/package.json +43 -0
package/dist/host.js
ADDED
|
@@ -0,0 +1,920 @@
|
|
|
1
|
+
import http from 'http';
|
|
2
|
+
import { WebSocketServer } from 'ws';
|
|
3
|
+
import { spawn } from 'child_process';
|
|
4
|
+
import fs from 'fs';
|
|
5
|
+
import { promises as fsp } from 'fs';
|
|
6
|
+
import net from 'net';
|
|
7
|
+
import path from 'path';
|
|
8
|
+
import { listModels, listRecentModels, providers, resolveProviderForModel } from './providers/index.js';
|
|
9
|
+
import { debugLog } from './providers/utils.js';
|
|
10
|
+
import { createObservedTracker } from './observed.js';
|
|
11
|
+
function send(socket, payload) {
|
|
12
|
+
socket.send(JSON.stringify(payload));
|
|
13
|
+
}
|
|
14
|
+
function buildProviderList(statuses) {
|
|
15
|
+
return Object.values(providers).map((provider) => {
|
|
16
|
+
const info = statuses[provider.id] || {};
|
|
17
|
+
return {
|
|
18
|
+
id: provider.id,
|
|
19
|
+
name: provider.name,
|
|
20
|
+
installed: info.installed ?? false,
|
|
21
|
+
loggedIn: info.loggedIn ?? false,
|
|
22
|
+
version: info.version,
|
|
23
|
+
updateAvailable: info.updateAvailable,
|
|
24
|
+
latestVersion: info.latestVersion,
|
|
25
|
+
updateCheckedAt: info.updateCheckedAt,
|
|
26
|
+
updateSource: info.updateSource,
|
|
27
|
+
updateCommand: info.updateCommand,
|
|
28
|
+
updateMessage: info.updateMessage,
|
|
29
|
+
updateInProgress: info.updateInProgress,
|
|
30
|
+
};
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
function resolveLoginExperience(mode) {
|
|
34
|
+
const raw = process.env.AGENTCONNECT_LOGIN_EXPERIENCE ||
|
|
35
|
+
process.env.AGENTCONNECT_CLAUDE_LOGIN_EXPERIENCE;
|
|
36
|
+
if (raw) {
|
|
37
|
+
const normalized = raw.trim().toLowerCase();
|
|
38
|
+
if (normalized === 'terminal' || normalized === 'manual')
|
|
39
|
+
return 'terminal';
|
|
40
|
+
if (normalized === 'embedded' || normalized === 'pty')
|
|
41
|
+
return 'embedded';
|
|
42
|
+
}
|
|
43
|
+
return mode === 'dev' ? 'terminal' : 'embedded';
|
|
44
|
+
}
|
|
45
|
+
function createHostRuntime(options) {
|
|
46
|
+
const mode = options.mode ?? options.modeDefault;
|
|
47
|
+
process.env.AGENTCONNECT_HOST_MODE ||= mode;
|
|
48
|
+
const basePath = options.basePath || process.cwd();
|
|
49
|
+
const manifest = options.appManifest ?? readManifest(basePath);
|
|
50
|
+
const appId = manifest?.id || (mode === 'dev' ? 'agentconnect-dev-app' : 'agentconnect-app');
|
|
51
|
+
const requestedCapabilities = Array.isArray(manifest?.capabilities) ? manifest.capabilities : [];
|
|
52
|
+
const observedTracker = createObservedTracker({
|
|
53
|
+
basePath,
|
|
54
|
+
appId,
|
|
55
|
+
requested: requestedCapabilities,
|
|
56
|
+
});
|
|
57
|
+
const sessions = new Map();
|
|
58
|
+
const activeRuns = new Map();
|
|
59
|
+
const updatingProviders = new Map();
|
|
60
|
+
const processTable = new Map();
|
|
61
|
+
const backendState = new Map();
|
|
62
|
+
const statusCache = new Map();
|
|
63
|
+
const statusCacheTtlMs = 8000;
|
|
64
|
+
const statusInFlight = new Map();
|
|
65
|
+
const hostAddress = options.host || '127.0.0.1';
|
|
66
|
+
const hostPort = options.port || 9630;
|
|
67
|
+
const providerDefaults = options.providerConfig || {};
|
|
68
|
+
const hostId = options.hostId || (mode === 'dev' ? 'agentconnect-dev' : 'agentconnect-host');
|
|
69
|
+
const hostName = options.hostName || (mode === 'dev' ? 'AgentConnect Dev Host' : 'AgentConnect Host');
|
|
70
|
+
const hostVersion = options.hostVersion || '0.1.0';
|
|
71
|
+
function resolveAppPathInternal(input) {
|
|
72
|
+
if (!input)
|
|
73
|
+
return basePath;
|
|
74
|
+
const value = String(input);
|
|
75
|
+
return path.isAbsolute(value) ? value : path.resolve(basePath, value);
|
|
76
|
+
}
|
|
77
|
+
function mapFileType(stat) {
|
|
78
|
+
if (stat.isFile())
|
|
79
|
+
return 'file';
|
|
80
|
+
if (stat.isDirectory())
|
|
81
|
+
return 'dir';
|
|
82
|
+
if (stat.isSymbolicLink())
|
|
83
|
+
return 'link';
|
|
84
|
+
return 'other';
|
|
85
|
+
}
|
|
86
|
+
async function allocatePort() {
|
|
87
|
+
return new Promise((resolve, reject) => {
|
|
88
|
+
const socket = net.createServer();
|
|
89
|
+
socket.listen(0, hostAddress, () => {
|
|
90
|
+
const address = socket.address();
|
|
91
|
+
if (!address || typeof address === 'string') {
|
|
92
|
+
socket.close();
|
|
93
|
+
reject(new Error('Failed to allocate port.'));
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
const portValue = address.port;
|
|
97
|
+
socket.close(() => resolve(portValue));
|
|
98
|
+
});
|
|
99
|
+
socket.on('error', reject);
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
async function waitForHealthcheck(url, timeoutMs = 15000) {
|
|
103
|
+
const start = Date.now();
|
|
104
|
+
while (Date.now() - start < timeoutMs) {
|
|
105
|
+
try {
|
|
106
|
+
const res = await fetch(url, { method: 'GET' });
|
|
107
|
+
if (res.ok)
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
// ignore
|
|
112
|
+
}
|
|
113
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
114
|
+
}
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
function readManifest(root) {
|
|
118
|
+
try {
|
|
119
|
+
const raw = fs.readFileSync(path.join(root, 'agentconnect.app.json'), 'utf8');
|
|
120
|
+
return JSON.parse(raw);
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
function recordCapability(capability) {
|
|
127
|
+
observedTracker.record(capability);
|
|
128
|
+
}
|
|
129
|
+
function recordModelCapability(model) {
|
|
130
|
+
const providerId = resolveProviderForModel(model);
|
|
131
|
+
if (!providerId)
|
|
132
|
+
return;
|
|
133
|
+
recordCapability(`model.${providerId}`);
|
|
134
|
+
}
|
|
135
|
+
async function getCachedStatus(provider, options = {}) {
|
|
136
|
+
const cached = statusCache.get(provider.id);
|
|
137
|
+
const now = Date.now();
|
|
138
|
+
if (cached && now - cached.at < statusCacheTtlMs) {
|
|
139
|
+
return cached.status;
|
|
140
|
+
}
|
|
141
|
+
const existing = statusInFlight.get(provider.id);
|
|
142
|
+
if (existing)
|
|
143
|
+
return existing;
|
|
144
|
+
if (options.allowFast && provider.fastStatus) {
|
|
145
|
+
try {
|
|
146
|
+
const fast = await provider.fastStatus();
|
|
147
|
+
const startedAt = Date.now();
|
|
148
|
+
const promise = provider
|
|
149
|
+
.status()
|
|
150
|
+
.then((status) => {
|
|
151
|
+
debugLog('Providers', 'status-check', {
|
|
152
|
+
providerId: provider.id,
|
|
153
|
+
durationMs: Date.now() - startedAt,
|
|
154
|
+
completedAt: new Date().toISOString(),
|
|
155
|
+
});
|
|
156
|
+
statusCache.set(provider.id, { status, at: Date.now() });
|
|
157
|
+
return status;
|
|
158
|
+
})
|
|
159
|
+
.finally(() => {
|
|
160
|
+
statusInFlight.delete(provider.id);
|
|
161
|
+
});
|
|
162
|
+
statusInFlight.set(provider.id, promise);
|
|
163
|
+
return fast;
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
// fall through to full status
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
const startedAt = Date.now();
|
|
170
|
+
const promise = provider
|
|
171
|
+
.status()
|
|
172
|
+
.then((status) => {
|
|
173
|
+
debugLog('Providers', 'status-check', {
|
|
174
|
+
providerId: provider.id,
|
|
175
|
+
durationMs: Date.now() - startedAt,
|
|
176
|
+
completedAt: new Date().toISOString(),
|
|
177
|
+
});
|
|
178
|
+
statusCache.set(provider.id, { status, at: Date.now() });
|
|
179
|
+
return status;
|
|
180
|
+
})
|
|
181
|
+
.finally(() => {
|
|
182
|
+
statusInFlight.delete(provider.id);
|
|
183
|
+
});
|
|
184
|
+
statusInFlight.set(provider.id, promise);
|
|
185
|
+
return promise;
|
|
186
|
+
}
|
|
187
|
+
function invalidateStatus(providerId) {
|
|
188
|
+
if (!providerId)
|
|
189
|
+
return;
|
|
190
|
+
statusCache.delete(providerId);
|
|
191
|
+
}
|
|
192
|
+
function emitSessionEvent(responder, sessionId, type, data) {
|
|
193
|
+
if (process.env.AGENTCONNECT_DEBUG?.trim()) {
|
|
194
|
+
try {
|
|
195
|
+
console.log(`[AgentConnect][Session ${sessionId}] ${type} ${JSON.stringify(data)}`);
|
|
196
|
+
}
|
|
197
|
+
catch {
|
|
198
|
+
console.log(`[AgentConnect][Session ${sessionId}] ${type}`);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
responder.emit({
|
|
202
|
+
jsonrpc: '2.0',
|
|
203
|
+
method: 'acp.session.event',
|
|
204
|
+
params: { sessionId, type, data },
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
async function handleRpc(payload, responder) {
|
|
208
|
+
if (!payload || payload.jsonrpc !== '2.0' || payload.id === undefined) {
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
const id = payload.id;
|
|
212
|
+
const method = payload.method;
|
|
213
|
+
const params = (payload.params ?? {});
|
|
214
|
+
if (typeof method === 'string' && method.startsWith('acp.')) {
|
|
215
|
+
recordCapability('agent.connect');
|
|
216
|
+
}
|
|
217
|
+
if (method === 'acp.hello') {
|
|
218
|
+
const loginExperience = resolveLoginExperience(mode);
|
|
219
|
+
responder.reply(id, {
|
|
220
|
+
hostId,
|
|
221
|
+
hostName,
|
|
222
|
+
hostVersion,
|
|
223
|
+
protocolVersion: '0.1',
|
|
224
|
+
mode: 'local',
|
|
225
|
+
capabilities: [],
|
|
226
|
+
providers: Object.keys(providers),
|
|
227
|
+
loginExperience,
|
|
228
|
+
});
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
if (method === 'acp.providers.list') {
|
|
232
|
+
const statusEntries = await Promise.all(Object.values(providers).map(async (provider) => {
|
|
233
|
+
try {
|
|
234
|
+
return [provider.id, await getCachedStatus(provider, { allowFast: true })];
|
|
235
|
+
}
|
|
236
|
+
catch {
|
|
237
|
+
return [provider.id, { installed: false, loggedIn: false }];
|
|
238
|
+
}
|
|
239
|
+
}));
|
|
240
|
+
const statuses = Object.fromEntries(statusEntries);
|
|
241
|
+
const list = buildProviderList(statuses).map((entry) => ({
|
|
242
|
+
...entry,
|
|
243
|
+
updateInProgress: updatingProviders.has(entry.id),
|
|
244
|
+
}));
|
|
245
|
+
responder.reply(id, { providers: list });
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
if (method === 'acp.providers.status') {
|
|
249
|
+
const providerId = params.provider;
|
|
250
|
+
const provider = providers[providerId];
|
|
251
|
+
if (!provider) {
|
|
252
|
+
responder.error(id, 'AC_ERR_UNSUPPORTED', 'Unknown provider');
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
const status = await getCachedStatus(provider, { allowFast: true });
|
|
256
|
+
responder.reply(id, {
|
|
257
|
+
provider: {
|
|
258
|
+
id: provider.id,
|
|
259
|
+
name: provider.name,
|
|
260
|
+
installed: status.installed,
|
|
261
|
+
loggedIn: status.loggedIn,
|
|
262
|
+
version: status.version,
|
|
263
|
+
updateAvailable: status.updateAvailable,
|
|
264
|
+
latestVersion: status.latestVersion,
|
|
265
|
+
updateCheckedAt: status.updateCheckedAt,
|
|
266
|
+
updateSource: status.updateSource,
|
|
267
|
+
updateCommand: status.updateCommand,
|
|
268
|
+
updateMessage: status.updateMessage,
|
|
269
|
+
updateInProgress: updatingProviders.has(provider.id) || status.updateInProgress,
|
|
270
|
+
},
|
|
271
|
+
});
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
if (method === 'acp.providers.update') {
|
|
275
|
+
const providerId = params.provider;
|
|
276
|
+
const provider = providers[providerId];
|
|
277
|
+
if (!provider) {
|
|
278
|
+
responder.error(id, 'AC_ERR_UNSUPPORTED', 'Unknown provider');
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
debugLog('Providers', 'update-start', { providerId });
|
|
282
|
+
if (!updatingProviders.has(providerId)) {
|
|
283
|
+
const promise = provider.update().finally(() => {
|
|
284
|
+
updatingProviders.delete(providerId);
|
|
285
|
+
invalidateStatus(providerId);
|
|
286
|
+
});
|
|
287
|
+
updatingProviders.set(providerId, promise);
|
|
288
|
+
}
|
|
289
|
+
try {
|
|
290
|
+
const status = await updatingProviders.get(providerId);
|
|
291
|
+
debugLog('Providers', 'update-complete', {
|
|
292
|
+
providerId,
|
|
293
|
+
updateAvailable: status.updateAvailable,
|
|
294
|
+
latestVersion: status.latestVersion,
|
|
295
|
+
updateMessage: status.updateMessage,
|
|
296
|
+
});
|
|
297
|
+
responder.reply(id, {
|
|
298
|
+
provider: {
|
|
299
|
+
id: provider.id,
|
|
300
|
+
name: provider.name,
|
|
301
|
+
installed: status.installed,
|
|
302
|
+
loggedIn: status.loggedIn,
|
|
303
|
+
version: status.version,
|
|
304
|
+
updateAvailable: status.updateAvailable,
|
|
305
|
+
latestVersion: status.latestVersion,
|
|
306
|
+
updateCheckedAt: status.updateCheckedAt,
|
|
307
|
+
updateSource: status.updateSource,
|
|
308
|
+
updateCommand: status.updateCommand,
|
|
309
|
+
updateMessage: status.updateMessage,
|
|
310
|
+
updateInProgress: false,
|
|
311
|
+
},
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
catch (err) {
|
|
315
|
+
debugLog('Providers', 'update-error', {
|
|
316
|
+
providerId,
|
|
317
|
+
message: err instanceof Error ? err.message : String(err),
|
|
318
|
+
});
|
|
319
|
+
responder.error(id, 'AC_ERR_INTERNAL', err?.message || 'Update failed');
|
|
320
|
+
}
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
if (method === 'acp.providers.ensureInstalled') {
|
|
324
|
+
const providerId = params.provider;
|
|
325
|
+
const provider = providers[providerId];
|
|
326
|
+
if (!provider) {
|
|
327
|
+
responder.error(id, 'AC_ERR_UNSUPPORTED', 'Unknown provider');
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
const result = await provider.ensureInstalled();
|
|
331
|
+
invalidateStatus(provider.id);
|
|
332
|
+
responder.reply(id, result);
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
if (method === 'acp.providers.login') {
|
|
336
|
+
const providerId = params.provider;
|
|
337
|
+
const provider = providers[providerId];
|
|
338
|
+
if (!provider) {
|
|
339
|
+
responder.error(id, 'AC_ERR_UNSUPPORTED', 'Unknown provider');
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
try {
|
|
343
|
+
const defaults = providerDefaults[providerId];
|
|
344
|
+
const incoming = params.options;
|
|
345
|
+
const result = await provider.login({
|
|
346
|
+
...(defaults || {}),
|
|
347
|
+
...(incoming || {}),
|
|
348
|
+
});
|
|
349
|
+
invalidateStatus(provider.id);
|
|
350
|
+
responder.reply(id, result);
|
|
351
|
+
}
|
|
352
|
+
catch (err) {
|
|
353
|
+
responder.error(id, 'AC_ERR_INTERNAL', err?.message || 'Provider login failed.');
|
|
354
|
+
}
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
if (method === 'acp.providers.logout') {
|
|
358
|
+
const providerId = params.provider;
|
|
359
|
+
const provider = providers[providerId];
|
|
360
|
+
if (!provider) {
|
|
361
|
+
responder.error(id, 'AC_ERR_UNSUPPORTED', 'Unknown provider');
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
await provider.logout();
|
|
365
|
+
invalidateStatus(provider.id);
|
|
366
|
+
responder.reply(id, {});
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
if (method === 'acp.models.list') {
|
|
370
|
+
const models = await listModels();
|
|
371
|
+
const providerId = params.provider;
|
|
372
|
+
if (providerId) {
|
|
373
|
+
responder.reply(id, { models: models.filter((m) => m.provider === providerId) });
|
|
374
|
+
}
|
|
375
|
+
else {
|
|
376
|
+
responder.reply(id, { models });
|
|
377
|
+
}
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
if (method === 'acp.models.recent') {
|
|
381
|
+
const providerId = params.provider;
|
|
382
|
+
const models = await listRecentModels(providerId);
|
|
383
|
+
responder.reply(id, { models });
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
if (method === 'acp.models.info') {
|
|
387
|
+
const modelId = params.model;
|
|
388
|
+
const model = (await listModels()).find((m) => m.id === modelId);
|
|
389
|
+
if (!model) {
|
|
390
|
+
responder.error(id, 'AC_ERR_INVALID_ARGS', 'Unknown model');
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
responder.reply(id, { model });
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
if (method === 'acp.sessions.create') {
|
|
397
|
+
const sessionId = `sess_${Math.random().toString(36).slice(2, 10)}`;
|
|
398
|
+
const model = params.model || 'claude-opus';
|
|
399
|
+
const reasoningEffort = params.reasoningEffort || null;
|
|
400
|
+
const cwd = params.cwd ? resolveAppPathInternal(params.cwd) : undefined;
|
|
401
|
+
const repoRoot = params.repoRoot ? resolveAppPathInternal(params.repoRoot) : undefined;
|
|
402
|
+
const providerDetailLevel = params.providerDetailLevel || undefined;
|
|
403
|
+
const providerId = resolveProviderForModel(model);
|
|
404
|
+
recordModelCapability(model);
|
|
405
|
+
sessions.set(sessionId, {
|
|
406
|
+
id: sessionId,
|
|
407
|
+
providerId,
|
|
408
|
+
model,
|
|
409
|
+
providerSessionId: null,
|
|
410
|
+
reasoningEffort,
|
|
411
|
+
cwd,
|
|
412
|
+
repoRoot,
|
|
413
|
+
providerDetailLevel: providerDetailLevel === 'raw' || providerDetailLevel === 'minimal'
|
|
414
|
+
? providerDetailLevel
|
|
415
|
+
: undefined,
|
|
416
|
+
});
|
|
417
|
+
responder.reply(id, { sessionId });
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
if (method === 'acp.sessions.resume') {
|
|
421
|
+
const sessionId = params.sessionId;
|
|
422
|
+
const existing = sessions.get(sessionId);
|
|
423
|
+
if (!existing) {
|
|
424
|
+
const model = params.model || 'claude-opus';
|
|
425
|
+
const reasoningEffort = params.reasoningEffort || null;
|
|
426
|
+
const cwd = params.cwd ? resolveAppPathInternal(params.cwd) : undefined;
|
|
427
|
+
const repoRoot = params.repoRoot ? resolveAppPathInternal(params.repoRoot) : undefined;
|
|
428
|
+
const providerDetailLevel = params.providerDetailLevel || undefined;
|
|
429
|
+
recordModelCapability(model);
|
|
430
|
+
sessions.set(sessionId, {
|
|
431
|
+
id: sessionId,
|
|
432
|
+
providerId: resolveProviderForModel(model),
|
|
433
|
+
model,
|
|
434
|
+
providerSessionId: params.providerSessionId || null,
|
|
435
|
+
reasoningEffort,
|
|
436
|
+
cwd,
|
|
437
|
+
repoRoot,
|
|
438
|
+
providerDetailLevel: providerDetailLevel === 'raw' || providerDetailLevel === 'minimal'
|
|
439
|
+
? providerDetailLevel
|
|
440
|
+
: undefined,
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
else {
|
|
444
|
+
if (params.providerSessionId) {
|
|
445
|
+
existing.providerSessionId = String(params.providerSessionId);
|
|
446
|
+
}
|
|
447
|
+
if (params.cwd) {
|
|
448
|
+
existing.cwd = resolveAppPathInternal(params.cwd);
|
|
449
|
+
}
|
|
450
|
+
if (params.repoRoot) {
|
|
451
|
+
existing.repoRoot = resolveAppPathInternal(params.repoRoot);
|
|
452
|
+
}
|
|
453
|
+
if (params.providerDetailLevel) {
|
|
454
|
+
const level = String(params.providerDetailLevel);
|
|
455
|
+
if (level === 'raw' || level === 'minimal') {
|
|
456
|
+
existing.providerDetailLevel = level;
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
recordModelCapability(existing.model);
|
|
460
|
+
}
|
|
461
|
+
responder.reply(id, { sessionId });
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
if (method === 'acp.sessions.send') {
|
|
465
|
+
const sessionId = params.sessionId;
|
|
466
|
+
const message = params.message?.content || '';
|
|
467
|
+
const session = sessions.get(sessionId);
|
|
468
|
+
if (!session) {
|
|
469
|
+
responder.error(id, 'AC_ERR_INVALID_ARGS', 'Unknown session');
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
recordModelCapability(session.model);
|
|
473
|
+
const provider = providers[session.providerId];
|
|
474
|
+
if (!provider) {
|
|
475
|
+
responder.error(id, 'AC_ERR_UNSUPPORTED', 'Unknown provider');
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
if (updatingProviders.has(session.providerId)) {
|
|
479
|
+
responder.error(id, 'AC_ERR_BUSY', 'Provider update in progress.');
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
const status = await provider.status();
|
|
483
|
+
if (!status.installed) {
|
|
484
|
+
const installed = await provider.ensureInstalled();
|
|
485
|
+
if (!installed.installed) {
|
|
486
|
+
responder.error(id, 'AC_ERR_NOT_INSTALLED', 'Provider CLI is not installed.');
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
const controller = new AbortController();
|
|
491
|
+
const cwd = params.cwd ? resolveAppPathInternal(params.cwd) : session.cwd || basePath;
|
|
492
|
+
const repoRoot = params.repoRoot
|
|
493
|
+
? resolveAppPathInternal(params.repoRoot)
|
|
494
|
+
: session.repoRoot || basePath;
|
|
495
|
+
const providerDetailLevel = params.providerDetailLevel === 'raw' || params.providerDetailLevel === 'minimal'
|
|
496
|
+
? params.providerDetailLevel
|
|
497
|
+
: session.providerDetailLevel || 'minimal';
|
|
498
|
+
activeRuns.set(sessionId, controller);
|
|
499
|
+
let sawError = false;
|
|
500
|
+
provider
|
|
501
|
+
.runPrompt({
|
|
502
|
+
prompt: message,
|
|
503
|
+
resumeSessionId: session.providerSessionId,
|
|
504
|
+
model: session.model,
|
|
505
|
+
reasoningEffort: session.reasoningEffort,
|
|
506
|
+
repoRoot,
|
|
507
|
+
cwd,
|
|
508
|
+
providerDetailLevel,
|
|
509
|
+
signal: controller.signal,
|
|
510
|
+
onEvent: (event) => {
|
|
511
|
+
if (event.type === 'error') {
|
|
512
|
+
sawError = true;
|
|
513
|
+
}
|
|
514
|
+
if (sawError && event.type === 'final') {
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
emitSessionEvent(responder, sessionId, event.type, { ...event });
|
|
518
|
+
},
|
|
519
|
+
})
|
|
520
|
+
.then((result) => {
|
|
521
|
+
if (result?.sessionId) {
|
|
522
|
+
session.providerSessionId = result.sessionId;
|
|
523
|
+
}
|
|
524
|
+
})
|
|
525
|
+
.catch((err) => {
|
|
526
|
+
if (!sawError) {
|
|
527
|
+
emitSessionEvent(responder, sessionId, 'error', {
|
|
528
|
+
message: err?.message || 'Provider error',
|
|
529
|
+
});
|
|
530
|
+
}
|
|
531
|
+
})
|
|
532
|
+
.finally(() => {
|
|
533
|
+
activeRuns.delete(sessionId);
|
|
534
|
+
});
|
|
535
|
+
responder.reply(id, { accepted: true });
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
if (method === 'acp.sessions.cancel') {
|
|
539
|
+
const sessionId = params.sessionId;
|
|
540
|
+
const controller = activeRuns.get(sessionId);
|
|
541
|
+
if (controller) {
|
|
542
|
+
controller.abort();
|
|
543
|
+
activeRuns.delete(sessionId);
|
|
544
|
+
}
|
|
545
|
+
responder.reply(id, { cancelled: true });
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
if (method === 'acp.sessions.close') {
|
|
549
|
+
const sessionId = params.sessionId;
|
|
550
|
+
sessions.delete(sessionId);
|
|
551
|
+
responder.reply(id, { closed: true });
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
if (method === 'acp.fs.read') {
|
|
555
|
+
recordCapability('fs.read');
|
|
556
|
+
try {
|
|
557
|
+
const filePath = resolveAppPathInternal(params.path);
|
|
558
|
+
const encoding = params.encoding || 'utf8';
|
|
559
|
+
const content = await fsp.readFile(filePath, encoding);
|
|
560
|
+
responder.reply(id, { content, encoding });
|
|
561
|
+
}
|
|
562
|
+
catch (err) {
|
|
563
|
+
responder.error(id, 'AC_ERR_FS_READ', err?.message || 'Failed to read file.');
|
|
564
|
+
}
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
if (method === 'acp.fs.write') {
|
|
568
|
+
recordCapability('fs.write');
|
|
569
|
+
try {
|
|
570
|
+
const filePath = resolveAppPathInternal(params.path);
|
|
571
|
+
const encoding = params.encoding || 'utf8';
|
|
572
|
+
const content = params.content ?? '';
|
|
573
|
+
await fsp.mkdir(path.dirname(filePath), { recursive: true });
|
|
574
|
+
await fsp.writeFile(filePath, content, {
|
|
575
|
+
encoding,
|
|
576
|
+
mode: params.mode,
|
|
577
|
+
});
|
|
578
|
+
const bytes = Buffer.byteLength(String(content), encoding);
|
|
579
|
+
responder.reply(id, { bytes });
|
|
580
|
+
}
|
|
581
|
+
catch (err) {
|
|
582
|
+
responder.error(id, 'AC_ERR_FS_WRITE', err?.message || 'Failed to write file.');
|
|
583
|
+
}
|
|
584
|
+
return;
|
|
585
|
+
}
|
|
586
|
+
if (method === 'acp.fs.list') {
|
|
587
|
+
recordCapability('fs.read');
|
|
588
|
+
try {
|
|
589
|
+
const dirPath = resolveAppPathInternal(params.path);
|
|
590
|
+
const entries = await fsp.readdir(dirPath, { withFileTypes: true });
|
|
591
|
+
const results = [];
|
|
592
|
+
for (const entry of entries) {
|
|
593
|
+
const entryPath = path.join(dirPath, entry.name);
|
|
594
|
+
let size = 0;
|
|
595
|
+
let type = 'other';
|
|
596
|
+
try {
|
|
597
|
+
const stat = await fsp.lstat(entryPath);
|
|
598
|
+
type = mapFileType(stat);
|
|
599
|
+
if (type === 'file')
|
|
600
|
+
size = stat.size;
|
|
601
|
+
}
|
|
602
|
+
catch {
|
|
603
|
+
type = entry.isDirectory() ? 'dir' : entry.isFile() ? 'file' : 'other';
|
|
604
|
+
}
|
|
605
|
+
results.push({
|
|
606
|
+
name: entry.name,
|
|
607
|
+
path: entryPath,
|
|
608
|
+
type,
|
|
609
|
+
size,
|
|
610
|
+
});
|
|
611
|
+
}
|
|
612
|
+
responder.reply(id, { entries: results });
|
|
613
|
+
}
|
|
614
|
+
catch (err) {
|
|
615
|
+
responder.error(id, 'AC_ERR_FS_LIST', err?.message || 'Failed to list directory.');
|
|
616
|
+
}
|
|
617
|
+
return;
|
|
618
|
+
}
|
|
619
|
+
if (method === 'acp.fs.stat') {
|
|
620
|
+
recordCapability('fs.read');
|
|
621
|
+
try {
|
|
622
|
+
const filePath = resolveAppPathInternal(params.path);
|
|
623
|
+
const stat = await fsp.lstat(filePath);
|
|
624
|
+
responder.reply(id, {
|
|
625
|
+
type: mapFileType(stat),
|
|
626
|
+
size: stat.size,
|
|
627
|
+
mtime: stat.mtime.toISOString(),
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
catch (err) {
|
|
631
|
+
responder.error(id, 'AC_ERR_FS_STAT', err?.message || 'Failed to stat file.');
|
|
632
|
+
}
|
|
633
|
+
return;
|
|
634
|
+
}
|
|
635
|
+
if (method === 'acp.process.spawn') {
|
|
636
|
+
recordCapability('process.spawn');
|
|
637
|
+
try {
|
|
638
|
+
const command = String(params.command || '');
|
|
639
|
+
const args = Array.isArray(params.args) ? params.args.map(String) : [];
|
|
640
|
+
const cwd = params.cwd ? resolveAppPathInternal(params.cwd) : basePath;
|
|
641
|
+
const env = { ...process.env, ...(params.env || {}) };
|
|
642
|
+
const useTty = Boolean(params.tty);
|
|
643
|
+
const child = spawn(command, args, {
|
|
644
|
+
cwd,
|
|
645
|
+
env,
|
|
646
|
+
stdio: useTty ? 'inherit' : ['pipe', 'pipe', 'pipe'],
|
|
647
|
+
});
|
|
648
|
+
if (!useTty) {
|
|
649
|
+
child.stdout?.on('data', () => undefined);
|
|
650
|
+
child.stderr?.on('data', () => undefined);
|
|
651
|
+
}
|
|
652
|
+
if (typeof params.stdin === 'string' && child.stdin) {
|
|
653
|
+
child.stdin.write(params.stdin);
|
|
654
|
+
child.stdin.end();
|
|
655
|
+
}
|
|
656
|
+
if (child.pid) {
|
|
657
|
+
processTable.set(child.pid, child);
|
|
658
|
+
child.on('close', () => {
|
|
659
|
+
if (child.pid)
|
|
660
|
+
processTable.delete(child.pid);
|
|
661
|
+
});
|
|
662
|
+
}
|
|
663
|
+
responder.reply(id, { pid: child.pid });
|
|
664
|
+
}
|
|
665
|
+
catch (err) {
|
|
666
|
+
responder.error(id, 'AC_ERR_PROCESS', err?.message || 'Failed to spawn process.');
|
|
667
|
+
}
|
|
668
|
+
return;
|
|
669
|
+
}
|
|
670
|
+
if (method === 'acp.process.kill') {
|
|
671
|
+
recordCapability('process.kill');
|
|
672
|
+
const pid = Number(params.pid);
|
|
673
|
+
const signal = params.signal || 'SIGTERM';
|
|
674
|
+
const child = processTable.get(pid);
|
|
675
|
+
try {
|
|
676
|
+
const success = child ? child.kill(signal) : process.kill(pid, signal);
|
|
677
|
+
responder.reply(id, { success: Boolean(success) });
|
|
678
|
+
}
|
|
679
|
+
catch (err) {
|
|
680
|
+
responder.error(id, 'AC_ERR_PROCESS', err?.message || 'Failed to kill process.');
|
|
681
|
+
}
|
|
682
|
+
return;
|
|
683
|
+
}
|
|
684
|
+
if (method === 'acp.net.request') {
|
|
685
|
+
recordCapability('network.request');
|
|
686
|
+
try {
|
|
687
|
+
if (typeof fetch !== 'function') {
|
|
688
|
+
responder.error(id, 'AC_ERR_NET', 'Fetch is not available.');
|
|
689
|
+
return;
|
|
690
|
+
}
|
|
691
|
+
const controller = new AbortController();
|
|
692
|
+
const timeout = params.timeoutMs;
|
|
693
|
+
let timer = null;
|
|
694
|
+
if (timeout) {
|
|
695
|
+
timer = setTimeout(() => controller.abort(), Number(timeout));
|
|
696
|
+
}
|
|
697
|
+
const res = await fetch(String(params.url), {
|
|
698
|
+
method: params.method || 'GET',
|
|
699
|
+
headers: params.headers || {},
|
|
700
|
+
body: params.body,
|
|
701
|
+
signal: controller.signal,
|
|
702
|
+
});
|
|
703
|
+
if (timer)
|
|
704
|
+
clearTimeout(timer);
|
|
705
|
+
const body = await res.text();
|
|
706
|
+
const headers = {};
|
|
707
|
+
res.headers.forEach((value, key) => {
|
|
708
|
+
headers[key] = value;
|
|
709
|
+
});
|
|
710
|
+
responder.reply(id, { status: res.status, headers, body });
|
|
711
|
+
}
|
|
712
|
+
catch (err) {
|
|
713
|
+
responder.error(id, 'AC_ERR_NET', err?.message || 'Network request failed.');
|
|
714
|
+
}
|
|
715
|
+
return;
|
|
716
|
+
}
|
|
717
|
+
if (method === 'acp.backend.start') {
|
|
718
|
+
recordCapability('backend.run');
|
|
719
|
+
if (!manifest?.backend) {
|
|
720
|
+
responder.reply(id, { status: 'disabled' });
|
|
721
|
+
return;
|
|
722
|
+
}
|
|
723
|
+
const backendConfig = manifest.backend;
|
|
724
|
+
const existing = backendState.get(appId);
|
|
725
|
+
if (existing?.status === 'running') {
|
|
726
|
+
responder.reply(id, { status: 'running', url: existing.url });
|
|
727
|
+
return;
|
|
728
|
+
}
|
|
729
|
+
try {
|
|
730
|
+
let assignedPort = null;
|
|
731
|
+
const declaredPort = backendConfig.env?.PORT;
|
|
732
|
+
if (declaredPort === undefined || String(declaredPort) === '0') {
|
|
733
|
+
assignedPort = await allocatePort();
|
|
734
|
+
}
|
|
735
|
+
else if (!Number.isNaN(Number(declaredPort))) {
|
|
736
|
+
assignedPort = Number(declaredPort);
|
|
737
|
+
}
|
|
738
|
+
const env = {
|
|
739
|
+
...process.env,
|
|
740
|
+
...(backendConfig.env || {}),
|
|
741
|
+
AGENTCONNECT_HOST: `ws://${hostAddress}:${hostPort}`,
|
|
742
|
+
AGENTCONNECT_APP_ID: appId,
|
|
743
|
+
};
|
|
744
|
+
if (assignedPort) {
|
|
745
|
+
env.PORT = String(assignedPort);
|
|
746
|
+
env.AGENTCONNECT_APP_PORT = String(assignedPort);
|
|
747
|
+
}
|
|
748
|
+
const cwd = backendConfig.cwd ? resolveAppPathInternal(backendConfig.cwd) : basePath;
|
|
749
|
+
const args = backendConfig.args || [];
|
|
750
|
+
const child = spawn(backendConfig.command, args, {
|
|
751
|
+
cwd,
|
|
752
|
+
env,
|
|
753
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
754
|
+
});
|
|
755
|
+
child.stdout?.on('data', () => undefined);
|
|
756
|
+
child.stderr?.on('data', () => undefined);
|
|
757
|
+
const url = assignedPort ? `http://${hostAddress}:${assignedPort}` : undefined;
|
|
758
|
+
const record = { status: 'starting', pid: child.pid, url };
|
|
759
|
+
backendState.set(appId, record);
|
|
760
|
+
child.on('exit', () => {
|
|
761
|
+
backendState.set(appId, { status: 'stopped' });
|
|
762
|
+
});
|
|
763
|
+
if (backendConfig.healthcheck?.type === 'http' && assignedPort) {
|
|
764
|
+
const healthUrl = `http://${hostAddress}:${assignedPort}${backendConfig.healthcheck.path}`;
|
|
765
|
+
const ok = await waitForHealthcheck(healthUrl);
|
|
766
|
+
if (!ok) {
|
|
767
|
+
child.kill('SIGTERM');
|
|
768
|
+
backendState.set(appId, { status: 'error' });
|
|
769
|
+
responder.reply(id, { status: 'error' });
|
|
770
|
+
return;
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
backendState.set(appId, { status: 'running', pid: child.pid, url });
|
|
774
|
+
responder.reply(id, { status: 'running', url });
|
|
775
|
+
}
|
|
776
|
+
catch (err) {
|
|
777
|
+
responder.error(id, 'AC_ERR_BACKEND', err?.message || 'Failed to start backend.');
|
|
778
|
+
}
|
|
779
|
+
return;
|
|
780
|
+
}
|
|
781
|
+
if (method === 'acp.backend.stop') {
|
|
782
|
+
recordCapability('backend.run');
|
|
783
|
+
const current = backendState.get(appId);
|
|
784
|
+
if (!current?.pid) {
|
|
785
|
+
backendState.set(appId, { status: 'stopped' });
|
|
786
|
+
responder.reply(id, { status: 'stopped' });
|
|
787
|
+
return;
|
|
788
|
+
}
|
|
789
|
+
try {
|
|
790
|
+
process.kill(current.pid, 'SIGTERM');
|
|
791
|
+
}
|
|
792
|
+
catch {
|
|
793
|
+
// ignore
|
|
794
|
+
}
|
|
795
|
+
backendState.set(appId, { status: 'stopped' });
|
|
796
|
+
responder.reply(id, { status: 'stopped' });
|
|
797
|
+
return;
|
|
798
|
+
}
|
|
799
|
+
if (method === 'acp.backend.status') {
|
|
800
|
+
recordCapability('backend.run');
|
|
801
|
+
const current = backendState.get(appId) || { status: 'stopped' };
|
|
802
|
+
responder.reply(id, { status: current.status, url: current.url });
|
|
803
|
+
return;
|
|
804
|
+
}
|
|
805
|
+
if (method === 'acp.capabilities.observed') {
|
|
806
|
+
responder.reply(id, { ...observedTracker.snapshot() });
|
|
807
|
+
return;
|
|
808
|
+
}
|
|
809
|
+
responder.reply(id, {});
|
|
810
|
+
}
|
|
811
|
+
return {
|
|
812
|
+
handleRpc,
|
|
813
|
+
flush: () => {
|
|
814
|
+
observedTracker.flush();
|
|
815
|
+
},
|
|
816
|
+
};
|
|
817
|
+
}
|
|
818
|
+
export function startDevHost({ host = '127.0.0.1', port = 9630, appPath, uiUrl, ...options } = {}) {
|
|
819
|
+
const basePath = options.basePath || appPath || process.cwd();
|
|
820
|
+
const runtime = createHostRuntime({
|
|
821
|
+
...options,
|
|
822
|
+
basePath,
|
|
823
|
+
host,
|
|
824
|
+
port,
|
|
825
|
+
modeDefault: 'dev',
|
|
826
|
+
});
|
|
827
|
+
const server = http.createServer();
|
|
828
|
+
const wss = new WebSocketServer({ server });
|
|
829
|
+
const logger = options.log?.info || console.log;
|
|
830
|
+
wss.on('connection', (socket) => {
|
|
831
|
+
socket.on('message', async (raw) => {
|
|
832
|
+
let payload;
|
|
833
|
+
try {
|
|
834
|
+
payload = JSON.parse(String(raw));
|
|
835
|
+
}
|
|
836
|
+
catch {
|
|
837
|
+
return;
|
|
838
|
+
}
|
|
839
|
+
const responder = {
|
|
840
|
+
reply: (id, result) => {
|
|
841
|
+
send(socket, { jsonrpc: '2.0', id, result });
|
|
842
|
+
},
|
|
843
|
+
error: (id, code, message) => {
|
|
844
|
+
send(socket, {
|
|
845
|
+
jsonrpc: '2.0',
|
|
846
|
+
id,
|
|
847
|
+
error: { code, message },
|
|
848
|
+
});
|
|
849
|
+
},
|
|
850
|
+
emit: (notification) => {
|
|
851
|
+
send(socket, notification);
|
|
852
|
+
},
|
|
853
|
+
};
|
|
854
|
+
await runtime.handleRpc(payload, responder);
|
|
855
|
+
});
|
|
856
|
+
});
|
|
857
|
+
server.listen(port, host, () => {
|
|
858
|
+
logger(`AgentConnect dev host running at ws://${host}:${port}`);
|
|
859
|
+
if (appPath)
|
|
860
|
+
logger(`App path: ${appPath}`);
|
|
861
|
+
if (uiUrl)
|
|
862
|
+
logger(`UI dev server: ${uiUrl}`);
|
|
863
|
+
});
|
|
864
|
+
process.on('SIGINT', () => {
|
|
865
|
+
try {
|
|
866
|
+
runtime.flush();
|
|
867
|
+
}
|
|
868
|
+
catch {
|
|
869
|
+
// ignore flush errors
|
|
870
|
+
}
|
|
871
|
+
server.close(() => process.exit(0));
|
|
872
|
+
});
|
|
873
|
+
}
|
|
874
|
+
export function createHostBridge(options = {}) {
|
|
875
|
+
const runtime = createHostRuntime({
|
|
876
|
+
...options,
|
|
877
|
+
basePath: options.basePath || process.cwd(),
|
|
878
|
+
modeDefault: 'embedded',
|
|
879
|
+
});
|
|
880
|
+
const handlers = new Set();
|
|
881
|
+
let nextId = 1;
|
|
882
|
+
const emit = (notification) => {
|
|
883
|
+
for (const handler of handlers) {
|
|
884
|
+
try {
|
|
885
|
+
handler(notification);
|
|
886
|
+
}
|
|
887
|
+
catch (err) {
|
|
888
|
+
options.log?.error?.('AgentConnect bridge handler error', {
|
|
889
|
+
message: err instanceof Error ? err.message : String(err),
|
|
890
|
+
});
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
};
|
|
894
|
+
return {
|
|
895
|
+
request: async (method, params) => {
|
|
896
|
+
return new Promise((resolve, reject) => {
|
|
897
|
+
const payload = {
|
|
898
|
+
jsonrpc: '2.0',
|
|
899
|
+
id: nextId++,
|
|
900
|
+
method,
|
|
901
|
+
params,
|
|
902
|
+
};
|
|
903
|
+
const responder = {
|
|
904
|
+
reply: (_id, result) => resolve(result),
|
|
905
|
+
error: (_id, code, message) => reject(new Error(`${code}: ${message}`)),
|
|
906
|
+
emit,
|
|
907
|
+
};
|
|
908
|
+
runtime.handleRpc(payload, responder).catch((err) => {
|
|
909
|
+
reject(err instanceof Error ? err : new Error(String(err)));
|
|
910
|
+
});
|
|
911
|
+
});
|
|
912
|
+
},
|
|
913
|
+
onEvent: (handler) => {
|
|
914
|
+
handlers.add(handler);
|
|
915
|
+
return () => {
|
|
916
|
+
handlers.delete(handler);
|
|
917
|
+
};
|
|
918
|
+
},
|
|
919
|
+
};
|
|
920
|
+
}
|