@agentconnect/cli 0.1.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/README.md +58 -0
- package/dist/fs-utils.d.ts +9 -0
- package/dist/fs-utils.js +43 -0
- package/dist/host.d.ts +7 -0
- package/dist/host.js +663 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +3020 -0
- package/dist/manifest.d.ts +10 -0
- package/dist/manifest.js +34 -0
- package/dist/observed.d.ts +7 -0
- package/dist/observed.js +69 -0
- package/dist/paths.d.ts +2 -0
- package/dist/paths.js +36 -0
- package/dist/providers/claude.d.ts +10 -0
- package/dist/providers/claude.js +672 -0
- package/dist/providers/codex.d.ts +9 -0
- package/dist/providers/codex.js +509 -0
- package/dist/providers/index.d.ts +5 -0
- package/dist/providers/index.js +90 -0
- package/dist/providers/local.d.ts +8 -0
- package/dist/providers/local.js +111 -0
- package/dist/providers/utils.d.ts +32 -0
- package/dist/providers/utils.js +256 -0
- package/dist/registry-validate.d.ts +6 -0
- package/dist/registry-validate.js +209 -0
- package/dist/registry.d.ts +17 -0
- package/dist/registry.js +66 -0
- package/dist/types.d.ts +225 -0
- package/dist/types.js +1 -0
- package/dist/zip.d.ts +9 -0
- package/dist/zip.js +71 -0
- package/package.json +45 -0
package/dist/host.js
ADDED
|
@@ -0,0 +1,663 @@
|
|
|
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 { createObservedTracker } from './observed.js';
|
|
10
|
+
function send(socket, payload) {
|
|
11
|
+
socket.send(JSON.stringify(payload));
|
|
12
|
+
}
|
|
13
|
+
function reply(socket, id, result) {
|
|
14
|
+
send(socket, { jsonrpc: '2.0', id, result });
|
|
15
|
+
}
|
|
16
|
+
function replyError(socket, id, code, message) {
|
|
17
|
+
send(socket, {
|
|
18
|
+
jsonrpc: '2.0',
|
|
19
|
+
id,
|
|
20
|
+
error: { code, message },
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
function sessionEvent(socket, sessionId, type, data) {
|
|
24
|
+
send(socket, {
|
|
25
|
+
jsonrpc: '2.0',
|
|
26
|
+
method: 'acp.session.event',
|
|
27
|
+
params: { sessionId, type, data },
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
function buildProviderList(statuses) {
|
|
31
|
+
return Object.values(providers).map((provider) => {
|
|
32
|
+
const info = statuses[provider.id] || {};
|
|
33
|
+
return {
|
|
34
|
+
id: provider.id,
|
|
35
|
+
name: provider.name,
|
|
36
|
+
installed: info.installed ?? false,
|
|
37
|
+
loggedIn: info.loggedIn ?? false,
|
|
38
|
+
version: info.version,
|
|
39
|
+
};
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
export function startDevHost({ host = '127.0.0.1', port = 9630, appPath, uiUrl, } = {}) {
|
|
43
|
+
process.env.AGENTCONNECT_HOST_MODE ||= 'dev';
|
|
44
|
+
const server = http.createServer();
|
|
45
|
+
const wss = new WebSocketServer({ server });
|
|
46
|
+
const sessions = new Map();
|
|
47
|
+
const activeRuns = new Map();
|
|
48
|
+
const processTable = new Map();
|
|
49
|
+
const backendState = new Map();
|
|
50
|
+
const statusCache = new Map();
|
|
51
|
+
const statusCacheTtlMs = 2000;
|
|
52
|
+
const basePath = appPath || process.cwd();
|
|
53
|
+
const manifest = readManifest(basePath);
|
|
54
|
+
const appId = manifest?.id || 'agentconnect-dev-app';
|
|
55
|
+
const requestedCapabilities = Array.isArray(manifest?.capabilities) ? manifest.capabilities : [];
|
|
56
|
+
const observedTracker = createObservedTracker({
|
|
57
|
+
basePath,
|
|
58
|
+
appId,
|
|
59
|
+
requested: requestedCapabilities,
|
|
60
|
+
});
|
|
61
|
+
function resolveAppPathInternal(input) {
|
|
62
|
+
if (!input)
|
|
63
|
+
return basePath;
|
|
64
|
+
const value = String(input);
|
|
65
|
+
return path.isAbsolute(value) ? value : path.resolve(basePath, value);
|
|
66
|
+
}
|
|
67
|
+
function mapFileType(stat) {
|
|
68
|
+
if (stat.isFile())
|
|
69
|
+
return 'file';
|
|
70
|
+
if (stat.isDirectory())
|
|
71
|
+
return 'dir';
|
|
72
|
+
if (stat.isSymbolicLink())
|
|
73
|
+
return 'link';
|
|
74
|
+
return 'other';
|
|
75
|
+
}
|
|
76
|
+
async function allocatePort() {
|
|
77
|
+
return new Promise((resolve, reject) => {
|
|
78
|
+
const socket = net.createServer();
|
|
79
|
+
socket.listen(0, host, () => {
|
|
80
|
+
const address = socket.address();
|
|
81
|
+
if (!address || typeof address === 'string') {
|
|
82
|
+
socket.close();
|
|
83
|
+
reject(new Error('Failed to allocate port.'));
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
const portValue = address.port;
|
|
87
|
+
socket.close(() => resolve(portValue));
|
|
88
|
+
});
|
|
89
|
+
socket.on('error', reject);
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
async function waitForHealthcheck(url, timeoutMs = 15000) {
|
|
93
|
+
const start = Date.now();
|
|
94
|
+
while (Date.now() - start < timeoutMs) {
|
|
95
|
+
try {
|
|
96
|
+
const res = await fetch(url, { method: 'GET' });
|
|
97
|
+
if (res.ok)
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
// ignore
|
|
102
|
+
}
|
|
103
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
104
|
+
}
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
function readManifest(root) {
|
|
108
|
+
try {
|
|
109
|
+
const raw = fs.readFileSync(path.join(root, 'agentconnect.app.json'), 'utf8');
|
|
110
|
+
return JSON.parse(raw);
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
function recordCapability(capability) {
|
|
117
|
+
observedTracker.record(capability);
|
|
118
|
+
}
|
|
119
|
+
function recordModelCapability(model) {
|
|
120
|
+
const providerId = resolveProviderForModel(model);
|
|
121
|
+
if (!providerId)
|
|
122
|
+
return;
|
|
123
|
+
recordCapability(`model.${providerId}`);
|
|
124
|
+
}
|
|
125
|
+
async function getCachedStatus(provider) {
|
|
126
|
+
const cached = statusCache.get(provider.id);
|
|
127
|
+
const now = Date.now();
|
|
128
|
+
if (cached && now - cached.at < statusCacheTtlMs) {
|
|
129
|
+
return cached.status;
|
|
130
|
+
}
|
|
131
|
+
const status = await provider.status();
|
|
132
|
+
statusCache.set(provider.id, { status, at: now });
|
|
133
|
+
return status;
|
|
134
|
+
}
|
|
135
|
+
function invalidateStatus(providerId) {
|
|
136
|
+
if (!providerId)
|
|
137
|
+
return;
|
|
138
|
+
statusCache.delete(providerId);
|
|
139
|
+
}
|
|
140
|
+
wss.on('connection', (socket) => {
|
|
141
|
+
socket.on('message', async (raw) => {
|
|
142
|
+
let payload;
|
|
143
|
+
try {
|
|
144
|
+
payload = JSON.parse(String(raw));
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
if (!payload || payload.jsonrpc !== '2.0' || payload.id === undefined) {
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
const id = payload.id;
|
|
153
|
+
const method = payload.method;
|
|
154
|
+
const params = (payload.params ?? {});
|
|
155
|
+
if (typeof method === 'string' && method.startsWith('acp.')) {
|
|
156
|
+
recordCapability('agent.connect');
|
|
157
|
+
}
|
|
158
|
+
if (method === 'acp.hello') {
|
|
159
|
+
const loginExperience = process.env.AGENTCONNECT_LOGIN_EXPERIENCE ||
|
|
160
|
+
process.env.AGENTCONNECT_CLAUDE_LOGIN_EXPERIENCE ||
|
|
161
|
+
(process.env.AGENTCONNECT_HOST_MODE === 'dev' ? 'terminal' : 'embedded');
|
|
162
|
+
reply(socket, id, {
|
|
163
|
+
hostId: 'agentconnect-dev',
|
|
164
|
+
hostName: 'AgentConnect Dev Host',
|
|
165
|
+
hostVersion: '0.1.0',
|
|
166
|
+
protocolVersion: '0.1',
|
|
167
|
+
mode: 'local',
|
|
168
|
+
capabilities: [],
|
|
169
|
+
providers: Object.keys(providers),
|
|
170
|
+
loginExperience,
|
|
171
|
+
});
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
if (method === 'acp.providers.list') {
|
|
175
|
+
const statusEntries = await Promise.all(Object.values(providers).map(async (provider) => {
|
|
176
|
+
try {
|
|
177
|
+
return [provider.id, await getCachedStatus(provider)];
|
|
178
|
+
}
|
|
179
|
+
catch {
|
|
180
|
+
return [provider.id, { installed: false, loggedIn: false }];
|
|
181
|
+
}
|
|
182
|
+
}));
|
|
183
|
+
const statuses = Object.fromEntries(statusEntries);
|
|
184
|
+
reply(socket, id, { providers: buildProviderList(statuses) });
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
if (method === 'acp.providers.status') {
|
|
188
|
+
const providerId = params.provider;
|
|
189
|
+
const provider = providers[providerId];
|
|
190
|
+
if (!provider) {
|
|
191
|
+
replyError(socket, id, 'AC_ERR_UNSUPPORTED', 'Unknown provider');
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
const status = await getCachedStatus(provider);
|
|
195
|
+
reply(socket, id, {
|
|
196
|
+
provider: {
|
|
197
|
+
id: provider.id,
|
|
198
|
+
name: provider.name,
|
|
199
|
+
installed: status.installed,
|
|
200
|
+
loggedIn: status.loggedIn,
|
|
201
|
+
version: status.version,
|
|
202
|
+
},
|
|
203
|
+
});
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
if (method === 'acp.providers.ensureInstalled') {
|
|
207
|
+
const providerId = params.provider;
|
|
208
|
+
const provider = providers[providerId];
|
|
209
|
+
if (!provider) {
|
|
210
|
+
replyError(socket, id, 'AC_ERR_UNSUPPORTED', 'Unknown provider');
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
const result = await provider.ensureInstalled();
|
|
214
|
+
invalidateStatus(provider.id);
|
|
215
|
+
reply(socket, id, result);
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
if (method === 'acp.providers.login') {
|
|
219
|
+
const providerId = params.provider;
|
|
220
|
+
const provider = providers[providerId];
|
|
221
|
+
if (!provider) {
|
|
222
|
+
replyError(socket, id, 'AC_ERR_UNSUPPORTED', 'Unknown provider');
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
try {
|
|
226
|
+
const result = await provider.login(params.options);
|
|
227
|
+
invalidateStatus(provider.id);
|
|
228
|
+
reply(socket, id, result);
|
|
229
|
+
}
|
|
230
|
+
catch (err) {
|
|
231
|
+
replyError(socket, id, 'AC_ERR_INTERNAL', err?.message || 'Provider login failed.');
|
|
232
|
+
}
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
if (method === 'acp.providers.logout') {
|
|
236
|
+
const providerId = params.provider;
|
|
237
|
+
const provider = providers[providerId];
|
|
238
|
+
if (!provider) {
|
|
239
|
+
replyError(socket, id, 'AC_ERR_UNSUPPORTED', 'Unknown provider');
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
await provider.logout();
|
|
243
|
+
invalidateStatus(provider.id);
|
|
244
|
+
reply(socket, id, {});
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
if (method === 'acp.models.list') {
|
|
248
|
+
const models = await listModels();
|
|
249
|
+
const providerId = params.provider;
|
|
250
|
+
if (providerId) {
|
|
251
|
+
reply(socket, id, { models: models.filter((m) => m.provider === providerId) });
|
|
252
|
+
}
|
|
253
|
+
else {
|
|
254
|
+
reply(socket, id, { models });
|
|
255
|
+
}
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
if (method === 'acp.models.recent') {
|
|
259
|
+
const providerId = params.provider;
|
|
260
|
+
const models = await listRecentModels(providerId);
|
|
261
|
+
reply(socket, id, { models });
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
if (method === 'acp.models.info') {
|
|
265
|
+
const modelId = params.model;
|
|
266
|
+
const model = (await listModels()).find((m) => m.id === modelId);
|
|
267
|
+
if (!model) {
|
|
268
|
+
replyError(socket, id, 'AC_ERR_INVALID_ARGS', 'Unknown model');
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
reply(socket, id, { model });
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
if (method === 'acp.sessions.create') {
|
|
275
|
+
const sessionId = `sess_${Math.random().toString(36).slice(2, 10)}`;
|
|
276
|
+
const model = params.model || 'claude-opus';
|
|
277
|
+
const reasoningEffort = params.reasoningEffort || null;
|
|
278
|
+
const providerId = resolveProviderForModel(model);
|
|
279
|
+
recordModelCapability(model);
|
|
280
|
+
sessions.set(sessionId, {
|
|
281
|
+
id: sessionId,
|
|
282
|
+
providerId,
|
|
283
|
+
model,
|
|
284
|
+
providerSessionId: null,
|
|
285
|
+
reasoningEffort,
|
|
286
|
+
});
|
|
287
|
+
reply(socket, id, { sessionId });
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
if (method === 'acp.sessions.resume') {
|
|
291
|
+
const sessionId = params.sessionId;
|
|
292
|
+
const existing = sessions.get(sessionId);
|
|
293
|
+
if (!existing) {
|
|
294
|
+
const model = params.model || 'claude-opus';
|
|
295
|
+
const reasoningEffort = params.reasoningEffort || null;
|
|
296
|
+
recordModelCapability(model);
|
|
297
|
+
sessions.set(sessionId, {
|
|
298
|
+
id: sessionId,
|
|
299
|
+
providerId: resolveProviderForModel(model),
|
|
300
|
+
model,
|
|
301
|
+
providerSessionId: params.providerSessionId || null,
|
|
302
|
+
reasoningEffort,
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
else {
|
|
306
|
+
recordModelCapability(existing.model);
|
|
307
|
+
}
|
|
308
|
+
reply(socket, id, { sessionId });
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
if (method === 'acp.sessions.send') {
|
|
312
|
+
const sessionId = params.sessionId;
|
|
313
|
+
const message = params.message?.content || '';
|
|
314
|
+
const session = sessions.get(sessionId);
|
|
315
|
+
if (!session) {
|
|
316
|
+
replyError(socket, id, 'AC_ERR_INVALID_ARGS', 'Unknown session');
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
recordModelCapability(session.model);
|
|
320
|
+
const provider = providers[session.providerId];
|
|
321
|
+
if (!provider) {
|
|
322
|
+
replyError(socket, id, 'AC_ERR_UNSUPPORTED', 'Unknown provider');
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
const status = await provider.status();
|
|
326
|
+
if (!status.installed) {
|
|
327
|
+
const installed = await provider.ensureInstalled();
|
|
328
|
+
if (!installed.installed) {
|
|
329
|
+
replyError(socket, id, 'AC_ERR_NOT_INSTALLED', 'Provider CLI is not installed.');
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
const controller = new AbortController();
|
|
334
|
+
activeRuns.set(sessionId, controller);
|
|
335
|
+
let sawError = false;
|
|
336
|
+
provider
|
|
337
|
+
.runPrompt({
|
|
338
|
+
prompt: message,
|
|
339
|
+
resumeSessionId: session.providerSessionId,
|
|
340
|
+
model: session.model,
|
|
341
|
+
reasoningEffort: session.reasoningEffort,
|
|
342
|
+
repoRoot: basePath,
|
|
343
|
+
cwd: basePath,
|
|
344
|
+
signal: controller.signal,
|
|
345
|
+
onEvent: (event) => {
|
|
346
|
+
if (event.type === 'error') {
|
|
347
|
+
sawError = true;
|
|
348
|
+
}
|
|
349
|
+
if (sawError && event.type === 'final') {
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
sessionEvent(socket, sessionId, event.type, { ...event });
|
|
353
|
+
},
|
|
354
|
+
})
|
|
355
|
+
.then((result) => {
|
|
356
|
+
if (result?.sessionId) {
|
|
357
|
+
session.providerSessionId = result.sessionId;
|
|
358
|
+
}
|
|
359
|
+
})
|
|
360
|
+
.catch((err) => {
|
|
361
|
+
if (!sawError) {
|
|
362
|
+
sessionEvent(socket, sessionId, 'error', {
|
|
363
|
+
message: err?.message || 'Provider error',
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
})
|
|
367
|
+
.finally(() => {
|
|
368
|
+
activeRuns.delete(sessionId);
|
|
369
|
+
});
|
|
370
|
+
reply(socket, id, { accepted: true });
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
if (method === 'acp.sessions.cancel') {
|
|
374
|
+
const sessionId = params.sessionId;
|
|
375
|
+
const controller = activeRuns.get(sessionId);
|
|
376
|
+
if (controller) {
|
|
377
|
+
controller.abort();
|
|
378
|
+
activeRuns.delete(sessionId);
|
|
379
|
+
}
|
|
380
|
+
reply(socket, id, { cancelled: true });
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
if (method === 'acp.sessions.close') {
|
|
384
|
+
const sessionId = params.sessionId;
|
|
385
|
+
sessions.delete(sessionId);
|
|
386
|
+
reply(socket, id, { closed: true });
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
if (method === 'acp.fs.read') {
|
|
390
|
+
recordCapability('fs.read');
|
|
391
|
+
try {
|
|
392
|
+
const filePath = resolveAppPathInternal(params.path);
|
|
393
|
+
const encoding = params.encoding || 'utf8';
|
|
394
|
+
const content = await fsp.readFile(filePath, encoding);
|
|
395
|
+
reply(socket, id, { content, encoding });
|
|
396
|
+
}
|
|
397
|
+
catch (err) {
|
|
398
|
+
replyError(socket, id, 'AC_ERR_FS_READ', err?.message || 'Failed to read file.');
|
|
399
|
+
}
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
if (method === 'acp.fs.write') {
|
|
403
|
+
recordCapability('fs.write');
|
|
404
|
+
try {
|
|
405
|
+
const filePath = resolveAppPathInternal(params.path);
|
|
406
|
+
const encoding = params.encoding || 'utf8';
|
|
407
|
+
const content = params.content ?? '';
|
|
408
|
+
await fsp.mkdir(path.dirname(filePath), { recursive: true });
|
|
409
|
+
await fsp.writeFile(filePath, content, {
|
|
410
|
+
encoding,
|
|
411
|
+
mode: params.mode,
|
|
412
|
+
});
|
|
413
|
+
const bytes = Buffer.byteLength(String(content), encoding);
|
|
414
|
+
reply(socket, id, { bytes });
|
|
415
|
+
}
|
|
416
|
+
catch (err) {
|
|
417
|
+
replyError(socket, id, 'AC_ERR_FS_WRITE', err?.message || 'Failed to write file.');
|
|
418
|
+
}
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
if (method === 'acp.fs.list') {
|
|
422
|
+
recordCapability('fs.read');
|
|
423
|
+
try {
|
|
424
|
+
const dirPath = resolveAppPathInternal(params.path);
|
|
425
|
+
const entries = await fsp.readdir(dirPath, { withFileTypes: true });
|
|
426
|
+
const results = [];
|
|
427
|
+
for (const entry of entries) {
|
|
428
|
+
const entryPath = path.join(dirPath, entry.name);
|
|
429
|
+
let size = 0;
|
|
430
|
+
let type = 'other';
|
|
431
|
+
try {
|
|
432
|
+
const stat = await fsp.lstat(entryPath);
|
|
433
|
+
type = mapFileType(stat);
|
|
434
|
+
if (type === 'file')
|
|
435
|
+
size = stat.size;
|
|
436
|
+
}
|
|
437
|
+
catch {
|
|
438
|
+
type = entry.isDirectory() ? 'dir' : entry.isFile() ? 'file' : 'other';
|
|
439
|
+
}
|
|
440
|
+
results.push({
|
|
441
|
+
name: entry.name,
|
|
442
|
+
path: entryPath,
|
|
443
|
+
type,
|
|
444
|
+
size,
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
reply(socket, id, { entries: results });
|
|
448
|
+
}
|
|
449
|
+
catch (err) {
|
|
450
|
+
replyError(socket, id, 'AC_ERR_FS_LIST', err?.message || 'Failed to list directory.');
|
|
451
|
+
}
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
if (method === 'acp.fs.stat') {
|
|
455
|
+
recordCapability('fs.read');
|
|
456
|
+
try {
|
|
457
|
+
const filePath = resolveAppPathInternal(params.path);
|
|
458
|
+
const stat = await fsp.lstat(filePath);
|
|
459
|
+
reply(socket, id, {
|
|
460
|
+
type: mapFileType(stat),
|
|
461
|
+
size: stat.size,
|
|
462
|
+
mtime: stat.mtime.toISOString(),
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
catch (err) {
|
|
466
|
+
replyError(socket, id, 'AC_ERR_FS_STAT', err?.message || 'Failed to stat file.');
|
|
467
|
+
}
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
if (method === 'acp.process.spawn') {
|
|
471
|
+
recordCapability('process.spawn');
|
|
472
|
+
try {
|
|
473
|
+
const command = String(params.command || '');
|
|
474
|
+
const args = Array.isArray(params.args) ? params.args.map(String) : [];
|
|
475
|
+
const cwd = params.cwd ? resolveAppPathInternal(params.cwd) : basePath;
|
|
476
|
+
const env = { ...process.env, ...(params.env || {}) };
|
|
477
|
+
const useTty = Boolean(params.tty);
|
|
478
|
+
const child = spawn(command, args, {
|
|
479
|
+
cwd,
|
|
480
|
+
env,
|
|
481
|
+
stdio: useTty ? 'inherit' : ['pipe', 'pipe', 'pipe'],
|
|
482
|
+
});
|
|
483
|
+
if (!useTty) {
|
|
484
|
+
child.stdout?.on('data', () => undefined);
|
|
485
|
+
child.stderr?.on('data', () => undefined);
|
|
486
|
+
}
|
|
487
|
+
if (typeof params.stdin === 'string' && child.stdin) {
|
|
488
|
+
child.stdin.write(params.stdin);
|
|
489
|
+
child.stdin.end();
|
|
490
|
+
}
|
|
491
|
+
if (child.pid) {
|
|
492
|
+
processTable.set(child.pid, child);
|
|
493
|
+
child.on('close', () => {
|
|
494
|
+
if (child.pid)
|
|
495
|
+
processTable.delete(child.pid);
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
reply(socket, id, { pid: child.pid });
|
|
499
|
+
}
|
|
500
|
+
catch (err) {
|
|
501
|
+
replyError(socket, id, 'AC_ERR_PROCESS', err?.message || 'Failed to spawn process.');
|
|
502
|
+
}
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
if (method === 'acp.process.kill') {
|
|
506
|
+
recordCapability('process.kill');
|
|
507
|
+
const pid = Number(params.pid);
|
|
508
|
+
const signal = params.signal || 'SIGTERM';
|
|
509
|
+
const child = processTable.get(pid);
|
|
510
|
+
try {
|
|
511
|
+
const success = child ? child.kill(signal) : process.kill(pid, signal);
|
|
512
|
+
reply(socket, id, { success: Boolean(success) });
|
|
513
|
+
}
|
|
514
|
+
catch (err) {
|
|
515
|
+
replyError(socket, id, 'AC_ERR_PROCESS', err?.message || 'Failed to kill process.');
|
|
516
|
+
}
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
if (method === 'acp.net.request') {
|
|
520
|
+
recordCapability('network.request');
|
|
521
|
+
try {
|
|
522
|
+
if (typeof fetch !== 'function') {
|
|
523
|
+
replyError(socket, id, 'AC_ERR_NET', 'Fetch is not available.');
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
const controller = new AbortController();
|
|
527
|
+
const timeout = params.timeoutMs;
|
|
528
|
+
let timer = null;
|
|
529
|
+
if (timeout) {
|
|
530
|
+
timer = setTimeout(() => controller.abort(), Number(timeout));
|
|
531
|
+
}
|
|
532
|
+
const res = await fetch(String(params.url), {
|
|
533
|
+
method: params.method || 'GET',
|
|
534
|
+
headers: params.headers || {},
|
|
535
|
+
body: params.body,
|
|
536
|
+
signal: controller.signal,
|
|
537
|
+
});
|
|
538
|
+
if (timer)
|
|
539
|
+
clearTimeout(timer);
|
|
540
|
+
const body = await res.text();
|
|
541
|
+
const headers = {};
|
|
542
|
+
res.headers.forEach((value, key) => {
|
|
543
|
+
headers[key] = value;
|
|
544
|
+
});
|
|
545
|
+
reply(socket, id, { status: res.status, headers, body });
|
|
546
|
+
}
|
|
547
|
+
catch (err) {
|
|
548
|
+
replyError(socket, id, 'AC_ERR_NET', err?.message || 'Network request failed.');
|
|
549
|
+
}
|
|
550
|
+
return;
|
|
551
|
+
}
|
|
552
|
+
if (method === 'acp.backend.start') {
|
|
553
|
+
recordCapability('backend.run');
|
|
554
|
+
if (!manifest?.backend) {
|
|
555
|
+
reply(socket, id, { status: 'disabled' });
|
|
556
|
+
return;
|
|
557
|
+
}
|
|
558
|
+
const backendConfig = manifest.backend;
|
|
559
|
+
const existing = backendState.get(appId);
|
|
560
|
+
if (existing?.status === 'running') {
|
|
561
|
+
reply(socket, id, { status: 'running', url: existing.url });
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
564
|
+
try {
|
|
565
|
+
let assignedPort = null;
|
|
566
|
+
const declaredPort = backendConfig.env?.PORT;
|
|
567
|
+
if (declaredPort === undefined || String(declaredPort) === '0') {
|
|
568
|
+
assignedPort = await allocatePort();
|
|
569
|
+
}
|
|
570
|
+
else if (!Number.isNaN(Number(declaredPort))) {
|
|
571
|
+
assignedPort = Number(declaredPort);
|
|
572
|
+
}
|
|
573
|
+
const env = {
|
|
574
|
+
...process.env,
|
|
575
|
+
...(backendConfig.env || {}),
|
|
576
|
+
AGENTCONNECT_HOST: `ws://${host}:${port}`,
|
|
577
|
+
AGENTCONNECT_APP_ID: appId,
|
|
578
|
+
};
|
|
579
|
+
if (assignedPort) {
|
|
580
|
+
env.PORT = String(assignedPort);
|
|
581
|
+
env.AGENTCONNECT_APP_PORT = String(assignedPort);
|
|
582
|
+
}
|
|
583
|
+
const cwd = backendConfig.cwd ? resolveAppPathInternal(backendConfig.cwd) : basePath;
|
|
584
|
+
const args = backendConfig.args || [];
|
|
585
|
+
const child = spawn(backendConfig.command, args, {
|
|
586
|
+
cwd,
|
|
587
|
+
env,
|
|
588
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
589
|
+
});
|
|
590
|
+
child.stdout?.on('data', () => undefined);
|
|
591
|
+
child.stderr?.on('data', () => undefined);
|
|
592
|
+
const url = assignedPort ? `http://${host}:${assignedPort}` : undefined;
|
|
593
|
+
const record = { status: 'starting', pid: child.pid, url };
|
|
594
|
+
backendState.set(appId, record);
|
|
595
|
+
child.on('exit', () => {
|
|
596
|
+
backendState.set(appId, { status: 'stopped' });
|
|
597
|
+
});
|
|
598
|
+
if (backendConfig.healthcheck?.type === 'http' && assignedPort) {
|
|
599
|
+
const healthUrl = `http://${host}:${assignedPort}${backendConfig.healthcheck.path}`;
|
|
600
|
+
const ok = await waitForHealthcheck(healthUrl);
|
|
601
|
+
if (!ok) {
|
|
602
|
+
child.kill('SIGTERM');
|
|
603
|
+
backendState.set(appId, { status: 'error' });
|
|
604
|
+
reply(socket, id, { status: 'error' });
|
|
605
|
+
return;
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
backendState.set(appId, { status: 'running', pid: child.pid, url });
|
|
609
|
+
reply(socket, id, { status: 'running', url });
|
|
610
|
+
}
|
|
611
|
+
catch (err) {
|
|
612
|
+
replyError(socket, id, 'AC_ERR_BACKEND', err?.message || 'Failed to start backend.');
|
|
613
|
+
}
|
|
614
|
+
return;
|
|
615
|
+
}
|
|
616
|
+
if (method === 'acp.backend.stop') {
|
|
617
|
+
recordCapability('backend.run');
|
|
618
|
+
const current = backendState.get(appId);
|
|
619
|
+
if (!current?.pid) {
|
|
620
|
+
backendState.set(appId, { status: 'stopped' });
|
|
621
|
+
reply(socket, id, { status: 'stopped' });
|
|
622
|
+
return;
|
|
623
|
+
}
|
|
624
|
+
try {
|
|
625
|
+
process.kill(current.pid, 'SIGTERM');
|
|
626
|
+
}
|
|
627
|
+
catch {
|
|
628
|
+
// ignore
|
|
629
|
+
}
|
|
630
|
+
backendState.set(appId, { status: 'stopped' });
|
|
631
|
+
reply(socket, id, { status: 'stopped' });
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
634
|
+
if (method === 'acp.backend.status') {
|
|
635
|
+
recordCapability('backend.run');
|
|
636
|
+
const current = backendState.get(appId) || { status: 'stopped' };
|
|
637
|
+
reply(socket, id, { status: current.status, url: current.url });
|
|
638
|
+
return;
|
|
639
|
+
}
|
|
640
|
+
if (method === 'acp.capabilities.observed') {
|
|
641
|
+
reply(socket, id, { ...observedTracker.snapshot() });
|
|
642
|
+
return;
|
|
643
|
+
}
|
|
644
|
+
reply(socket, id, {});
|
|
645
|
+
});
|
|
646
|
+
});
|
|
647
|
+
server.listen(port, host, () => {
|
|
648
|
+
console.log(`AgentConnect dev host running at ws://${host}:${port}`);
|
|
649
|
+
if (appPath)
|
|
650
|
+
console.log(`App path: ${appPath}`);
|
|
651
|
+
if (uiUrl)
|
|
652
|
+
console.log(`UI dev server: ${uiUrl}`);
|
|
653
|
+
});
|
|
654
|
+
process.on('SIGINT', () => {
|
|
655
|
+
try {
|
|
656
|
+
observedTracker.flush();
|
|
657
|
+
}
|
|
658
|
+
catch {
|
|
659
|
+
// ignore flush errors
|
|
660
|
+
}
|
|
661
|
+
server.close(() => process.exit(0));
|
|
662
|
+
});
|
|
663
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|