@agent-relay/daemon 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/dist/agent-manager.d.ts +134 -0
- package/dist/agent-manager.d.ts.map +1 -0
- package/dist/agent-manager.js +578 -0
- package/dist/agent-manager.js.map +1 -0
- package/dist/agent-registry.d.ts +99 -0
- package/dist/agent-registry.d.ts.map +1 -0
- package/dist/agent-registry.js +213 -0
- package/dist/agent-registry.js.map +1 -0
- package/dist/agent-signing.d.ts +158 -0
- package/dist/agent-signing.d.ts.map +1 -0
- package/dist/agent-signing.js +523 -0
- package/dist/agent-signing.js.map +1 -0
- package/dist/api.d.ts +106 -0
- package/dist/api.d.ts.map +1 -0
- package/dist/api.js +876 -0
- package/dist/api.js.map +1 -0
- package/dist/auth.d.ts +94 -0
- package/dist/auth.d.ts.map +1 -0
- package/dist/auth.js +197 -0
- package/dist/auth.js.map +1 -0
- package/dist/channel-membership-store.d.ts +55 -0
- package/dist/channel-membership-store.d.ts.map +1 -0
- package/dist/channel-membership-store.js +176 -0
- package/dist/channel-membership-store.js.map +1 -0
- package/dist/cli-auth.d.ts +89 -0
- package/dist/cli-auth.d.ts.map +1 -0
- package/dist/cli-auth.js +792 -0
- package/dist/cli-auth.js.map +1 -0
- package/dist/cloud-sync.d.ts +150 -0
- package/dist/cloud-sync.d.ts.map +1 -0
- package/dist/cloud-sync.js +446 -0
- package/dist/cloud-sync.js.map +1 -0
- package/dist/connection.d.ts +130 -0
- package/dist/connection.d.ts.map +1 -0
- package/dist/connection.js +438 -0
- package/dist/connection.js.map +1 -0
- package/dist/consensus-integration.d.ts +167 -0
- package/dist/consensus-integration.d.ts.map +1 -0
- package/dist/consensus-integration.js +371 -0
- package/dist/consensus-integration.js.map +1 -0
- package/dist/consensus.d.ts +271 -0
- package/dist/consensus.d.ts.map +1 -0
- package/dist/consensus.js +632 -0
- package/dist/consensus.js.map +1 -0
- package/dist/delivery-tracker.d.ts +34 -0
- package/dist/delivery-tracker.d.ts.map +1 -0
- package/dist/delivery-tracker.js +104 -0
- package/dist/delivery-tracker.js.map +1 -0
- package/dist/enhanced-features.d.ts +118 -0
- package/dist/enhanced-features.d.ts.map +1 -0
- package/dist/enhanced-features.js +176 -0
- package/dist/enhanced-features.js.map +1 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +37 -0
- package/dist/index.js.map +1 -0
- package/dist/migrations/index.d.ts +73 -0
- package/dist/migrations/index.d.ts.map +1 -0
- package/dist/migrations/index.js +241 -0
- package/dist/migrations/index.js.map +1 -0
- package/dist/orchestrator.d.ts +217 -0
- package/dist/orchestrator.d.ts.map +1 -0
- package/dist/orchestrator.js +1143 -0
- package/dist/orchestrator.js.map +1 -0
- package/dist/rate-limiter.d.ts +68 -0
- package/dist/rate-limiter.d.ts.map +1 -0
- package/dist/rate-limiter.js +130 -0
- package/dist/rate-limiter.js.map +1 -0
- package/dist/registry.d.ts +9 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +9 -0
- package/dist/registry.js.map +1 -0
- package/dist/relay-ledger.d.ts +261 -0
- package/dist/relay-ledger.d.ts.map +1 -0
- package/dist/relay-ledger.js +532 -0
- package/dist/relay-ledger.js.map +1 -0
- package/dist/relay-watchdog.d.ts +125 -0
- package/dist/relay-watchdog.d.ts.map +1 -0
- package/dist/relay-watchdog.js +611 -0
- package/dist/relay-watchdog.js.map +1 -0
- package/dist/repo-manager.d.ts +116 -0
- package/dist/repo-manager.d.ts.map +1 -0
- package/dist/repo-manager.js +384 -0
- package/dist/repo-manager.js.map +1 -0
- package/dist/router.d.ts +370 -0
- package/dist/router.d.ts.map +1 -0
- package/dist/router.js +1437 -0
- package/dist/router.js.map +1 -0
- package/dist/server.d.ts +174 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +1001 -0
- package/dist/server.js.map +1 -0
- package/dist/spawn-manager.d.ts +78 -0
- package/dist/spawn-manager.d.ts.map +1 -0
- package/dist/spawn-manager.js +165 -0
- package/dist/spawn-manager.js.map +1 -0
- package/dist/sync-queue.d.ts +116 -0
- package/dist/sync-queue.d.ts.map +1 -0
- package/dist/sync-queue.js +361 -0
- package/dist/sync-queue.js.map +1 -0
- package/dist/types.d.ts +133 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +6 -0
- package/dist/types.js.map +1 -0
- package/dist/workspace-manager.d.ts +80 -0
- package/dist/workspace-manager.d.ts.map +1 -0
- package/dist/workspace-manager.js +314 -0
- package/dist/workspace-manager.js.map +1 -0
- package/package.json +52 -0
package/dist/api.js
ADDED
|
@@ -0,0 +1,876 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Daemon API
|
|
3
|
+
* REST and WebSocket API for dashboard communication.
|
|
4
|
+
*/
|
|
5
|
+
import * as http from 'http';
|
|
6
|
+
import WebSocket, { WebSocketServer } from 'ws';
|
|
7
|
+
import { EventEmitter } from 'events';
|
|
8
|
+
import { createLogger, metrics } from '@agent-relay/resiliency';
|
|
9
|
+
import { getWorkspaceManager } from './workspace-manager.js';
|
|
10
|
+
import { getAgentManager } from './agent-manager.js';
|
|
11
|
+
import { startCLIAuth, getAuthSession, submitAuthCode, cancelAuthSession, getSupportedProviders, } from './cli-auth.js';
|
|
12
|
+
import { getRepoManager, initRepoManager } from './repo-manager.js';
|
|
13
|
+
const logger = createLogger('daemon-api');
|
|
14
|
+
export class DaemonApi extends EventEmitter {
|
|
15
|
+
server;
|
|
16
|
+
wss;
|
|
17
|
+
workspaceManager;
|
|
18
|
+
agentManager;
|
|
19
|
+
sessions = new Map();
|
|
20
|
+
routes = new Map();
|
|
21
|
+
config;
|
|
22
|
+
allowedOrigins;
|
|
23
|
+
allowAllOrigins;
|
|
24
|
+
// Track output subscriptions per agent/client
|
|
25
|
+
outputSubscribers = new Map();
|
|
26
|
+
wsSubscriptions = new WeakMap();
|
|
27
|
+
// Track alive status for ping/pong keepalive
|
|
28
|
+
clientAlive = new WeakMap();
|
|
29
|
+
pingInterval;
|
|
30
|
+
constructor(config) {
|
|
31
|
+
super();
|
|
32
|
+
this.config = config;
|
|
33
|
+
this.workspaceManager = getWorkspaceManager(config.dataDir);
|
|
34
|
+
this.agentManager = getAgentManager(config.dataDir);
|
|
35
|
+
const configuredOrigins = this.loadAllowedOrigins(config);
|
|
36
|
+
this.allowAllOrigins = configuredOrigins.includes('*');
|
|
37
|
+
this.allowedOrigins = new Set(configuredOrigins
|
|
38
|
+
.map(origin => origin.trim())
|
|
39
|
+
.filter(origin => origin && origin !== '*'));
|
|
40
|
+
// Setup routes
|
|
41
|
+
this.setupRoutes();
|
|
42
|
+
// Forward events to WebSocket clients
|
|
43
|
+
this.workspaceManager.on('event', (event) => this.broadcastEvent(event));
|
|
44
|
+
this.agentManager.on('event', (event) => this.broadcastEvent(event));
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Resolve allowed origins from config/env (comma-separated list).
|
|
48
|
+
* Empty list means no cross-origin access is permitted.
|
|
49
|
+
*/
|
|
50
|
+
loadAllowedOrigins(config) {
|
|
51
|
+
if (config.allowedOrigins?.length) {
|
|
52
|
+
return config.allowedOrigins;
|
|
53
|
+
}
|
|
54
|
+
const envOrigins = process.env.AGENT_RELAY_API_ALLOWED_ORIGINS;
|
|
55
|
+
if (envOrigins?.trim()) {
|
|
56
|
+
return envOrigins.split(',').map(origin => origin.trim()).filter(Boolean);
|
|
57
|
+
}
|
|
58
|
+
return [];
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Return allowed origin for CORS or null if explicitly blocked.
|
|
62
|
+
* Undefined means no CORS header will be set (same-origin/server-to-server).
|
|
63
|
+
*/
|
|
64
|
+
resolveAllowedOrigin(originHeader, requestHost) {
|
|
65
|
+
if (!originHeader)
|
|
66
|
+
return undefined; // Non-browser or requests without Origin header
|
|
67
|
+
if (this.allowAllOrigins)
|
|
68
|
+
return originHeader;
|
|
69
|
+
if (this.allowedOrigins.has(originHeader))
|
|
70
|
+
return originHeader;
|
|
71
|
+
if (requestHost) {
|
|
72
|
+
try {
|
|
73
|
+
const originHost = new URL(originHeader).host;
|
|
74
|
+
// Allow same-origin requests even if not explicitly configured
|
|
75
|
+
if (originHost === requestHost) {
|
|
76
|
+
return originHeader;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
// Malformed origin; treat as blocked below
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Track an output subscription for a client/agent pair
|
|
87
|
+
*/
|
|
88
|
+
addOutputSubscription(ws, agentId) {
|
|
89
|
+
const subscribers = this.outputSubscribers.get(agentId) ?? new Set();
|
|
90
|
+
subscribers.add(ws);
|
|
91
|
+
this.outputSubscribers.set(agentId, subscribers);
|
|
92
|
+
const wsAgents = this.wsSubscriptions.get(ws) ?? new Set();
|
|
93
|
+
wsAgents.add(agentId);
|
|
94
|
+
this.wsSubscriptions.set(ws, wsAgents);
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Remove output subscriptions for a client. If agentId is provided, only that
|
|
98
|
+
* subscription is removed.
|
|
99
|
+
*/
|
|
100
|
+
removeOutputSubscription(ws, agentId) {
|
|
101
|
+
const wsAgents = this.wsSubscriptions.get(ws);
|
|
102
|
+
if (!wsAgents)
|
|
103
|
+
return;
|
|
104
|
+
const removeForAgent = (id) => {
|
|
105
|
+
const agentSubscribers = this.outputSubscribers.get(id);
|
|
106
|
+
agentSubscribers?.delete(ws);
|
|
107
|
+
if (agentSubscribers && agentSubscribers.size === 0) {
|
|
108
|
+
this.outputSubscribers.delete(id);
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
if (agentId) {
|
|
112
|
+
wsAgents.delete(agentId);
|
|
113
|
+
removeForAgent(agentId);
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
for (const id of wsAgents) {
|
|
117
|
+
removeForAgent(id);
|
|
118
|
+
}
|
|
119
|
+
this.wsSubscriptions.delete(ws);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Send a snapshot of recent output to the client.
|
|
124
|
+
*/
|
|
125
|
+
sendOutputSnapshot(ws, agentId, limit) {
|
|
126
|
+
const normalizedLimit = this.normalizeLimit(limit) ?? 200;
|
|
127
|
+
const output = this.agentManager.getOutput(agentId, normalizedLimit);
|
|
128
|
+
if (output === null) {
|
|
129
|
+
this.sendToClient(ws, { type: 'error', data: { message: `Agent ${agentId} not found` } });
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
const agent = this.agentManager.get(agentId);
|
|
133
|
+
this.sendToClient(ws, {
|
|
134
|
+
type: 'output:init',
|
|
135
|
+
data: {
|
|
136
|
+
agentId,
|
|
137
|
+
workspaceId: agent?.workspaceId,
|
|
138
|
+
output,
|
|
139
|
+
},
|
|
140
|
+
});
|
|
141
|
+
return true;
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Broadcast output events only to subscribed clients
|
|
145
|
+
*/
|
|
146
|
+
broadcastOutputEvent(event) {
|
|
147
|
+
if (!this.wss || !event.agentId)
|
|
148
|
+
return;
|
|
149
|
+
const subscribers = this.outputSubscribers.get(event.agentId);
|
|
150
|
+
if (!subscribers?.size)
|
|
151
|
+
return;
|
|
152
|
+
const data = event.data;
|
|
153
|
+
const message = JSON.stringify({
|
|
154
|
+
type: 'output:append',
|
|
155
|
+
data: {
|
|
156
|
+
agentId: event.agentId,
|
|
157
|
+
workspaceId: event.workspaceId,
|
|
158
|
+
output: data?.output ?? event.data,
|
|
159
|
+
agentName: data?.agentName,
|
|
160
|
+
timestamp: event.timestamp,
|
|
161
|
+
},
|
|
162
|
+
});
|
|
163
|
+
for (const ws of subscribers) {
|
|
164
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
165
|
+
ws.send(message);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Clamp and normalize requested output limit
|
|
171
|
+
*/
|
|
172
|
+
normalizeLimit(value) {
|
|
173
|
+
const num = typeof value === 'number' ? value : Number(value);
|
|
174
|
+
if (!Number.isFinite(num))
|
|
175
|
+
return undefined;
|
|
176
|
+
const limit = Math.floor(num);
|
|
177
|
+
return Math.min(2000, Math.max(1, limit));
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Start the API server
|
|
181
|
+
*/
|
|
182
|
+
async start() {
|
|
183
|
+
// Initialize repo manager (scans for existing repos, syncs from env)
|
|
184
|
+
// This runs in background - don't block server startup
|
|
185
|
+
initRepoManager().catch((err) => {
|
|
186
|
+
logger.warn('Failed to initialize repo manager', { error: String(err) });
|
|
187
|
+
});
|
|
188
|
+
return new Promise((resolve) => {
|
|
189
|
+
this.server = http.createServer((req, res) => this.handleRequest(req, res));
|
|
190
|
+
// Setup WebSocket server (disable compression for compatibility)
|
|
191
|
+
this.wss = new WebSocketServer({ server: this.server, perMessageDeflate: false });
|
|
192
|
+
this.wss.on('connection', (ws, req) => this.handleWebSocketConnection(ws, req));
|
|
193
|
+
// Setup ping/pong keepalive (30 second interval)
|
|
194
|
+
this.pingInterval = setInterval(() => {
|
|
195
|
+
this.wss?.clients.forEach((ws) => {
|
|
196
|
+
if (this.clientAlive.get(ws) === false) {
|
|
197
|
+
logger.info('WebSocket client unresponsive, closing');
|
|
198
|
+
ws.terminate();
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
this.clientAlive.set(ws, false);
|
|
202
|
+
ws.ping();
|
|
203
|
+
});
|
|
204
|
+
}, 30000);
|
|
205
|
+
this.server.listen(this.config.port, this.config.host, () => {
|
|
206
|
+
logger.info('Daemon API started', { port: this.config.port, host: this.config.host });
|
|
207
|
+
resolve();
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Stop the API server
|
|
213
|
+
*/
|
|
214
|
+
async stop() {
|
|
215
|
+
// Clear ping interval
|
|
216
|
+
if (this.pingInterval) {
|
|
217
|
+
clearInterval(this.pingInterval);
|
|
218
|
+
this.pingInterval = undefined;
|
|
219
|
+
}
|
|
220
|
+
// Close all WebSocket connections
|
|
221
|
+
if (this.wss) {
|
|
222
|
+
for (const ws of this.wss.clients) {
|
|
223
|
+
ws.close();
|
|
224
|
+
}
|
|
225
|
+
this.wss.close();
|
|
226
|
+
}
|
|
227
|
+
// Stop agent manager
|
|
228
|
+
await this.agentManager.shutdown();
|
|
229
|
+
// Close HTTP server
|
|
230
|
+
if (this.server) {
|
|
231
|
+
return new Promise((resolve) => {
|
|
232
|
+
this.server.close(() => {
|
|
233
|
+
logger.info('Daemon API stopped');
|
|
234
|
+
resolve();
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Setup API routes
|
|
241
|
+
*/
|
|
242
|
+
setupRoutes() {
|
|
243
|
+
// Health check
|
|
244
|
+
this.routes.set('GET /', async () => ({
|
|
245
|
+
status: 200,
|
|
246
|
+
body: { status: 'ok', version: '1.0.0' },
|
|
247
|
+
}));
|
|
248
|
+
// Metrics endpoint
|
|
249
|
+
this.routes.set('GET /metrics', async () => ({
|
|
250
|
+
status: 200,
|
|
251
|
+
body: metrics.toPrometheus(),
|
|
252
|
+
headers: { 'Content-Type': 'text/plain' },
|
|
253
|
+
}));
|
|
254
|
+
// === Workspaces ===
|
|
255
|
+
// List workspaces
|
|
256
|
+
this.routes.set('GET /workspaces', async () => {
|
|
257
|
+
const workspaces = this.workspaceManager.getAll();
|
|
258
|
+
const active = this.workspaceManager.getActive();
|
|
259
|
+
const response = {
|
|
260
|
+
workspaces,
|
|
261
|
+
activeWorkspaceId: active?.id,
|
|
262
|
+
};
|
|
263
|
+
return { status: 200, body: response };
|
|
264
|
+
});
|
|
265
|
+
// Add workspace
|
|
266
|
+
this.routes.set('POST /workspaces', async (req) => {
|
|
267
|
+
const body = req.body;
|
|
268
|
+
if (!body?.path) {
|
|
269
|
+
return { status: 400, body: { error: 'path is required' } };
|
|
270
|
+
}
|
|
271
|
+
try {
|
|
272
|
+
const workspace = this.workspaceManager.add(body);
|
|
273
|
+
return { status: 201, body: workspace };
|
|
274
|
+
}
|
|
275
|
+
catch (err) {
|
|
276
|
+
return { status: 400, body: { error: String(err) } };
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
// Get workspace
|
|
280
|
+
this.routes.set('GET /workspaces/:id', async (req) => {
|
|
281
|
+
const workspace = this.workspaceManager.get(req.params.id);
|
|
282
|
+
if (!workspace) {
|
|
283
|
+
return { status: 404, body: { error: 'Workspace not found' } };
|
|
284
|
+
}
|
|
285
|
+
return { status: 200, body: workspace };
|
|
286
|
+
});
|
|
287
|
+
// Delete workspace
|
|
288
|
+
this.routes.set('DELETE /workspaces/:id', async (req) => {
|
|
289
|
+
const removed = this.workspaceManager.remove(req.params.id);
|
|
290
|
+
if (!removed) {
|
|
291
|
+
return { status: 404, body: { error: 'Workspace not found' } };
|
|
292
|
+
}
|
|
293
|
+
return { status: 204, body: null };
|
|
294
|
+
});
|
|
295
|
+
// Switch workspace
|
|
296
|
+
this.routes.set('POST /workspaces/:id/switch', async (req) => {
|
|
297
|
+
try {
|
|
298
|
+
const workspace = this.workspaceManager.switchTo(req.params.id);
|
|
299
|
+
return { status: 200, body: workspace };
|
|
300
|
+
}
|
|
301
|
+
catch (err) {
|
|
302
|
+
return { status: 404, body: { error: String(err) } };
|
|
303
|
+
}
|
|
304
|
+
});
|
|
305
|
+
// === Agents ===
|
|
306
|
+
// List agents in workspace
|
|
307
|
+
this.routes.set('GET /workspaces/:id/agents', async (req) => {
|
|
308
|
+
const workspace = this.workspaceManager.get(req.params.id);
|
|
309
|
+
if (!workspace) {
|
|
310
|
+
return { status: 404, body: { error: 'Workspace not found' } };
|
|
311
|
+
}
|
|
312
|
+
const agents = this.agentManager.getByWorkspace(req.params.id);
|
|
313
|
+
const response = {
|
|
314
|
+
agents,
|
|
315
|
+
workspaceId: req.params.id,
|
|
316
|
+
};
|
|
317
|
+
return { status: 200, body: response };
|
|
318
|
+
});
|
|
319
|
+
// Spawn agent in workspace
|
|
320
|
+
this.routes.set('POST /workspaces/:id/agents', async (req) => {
|
|
321
|
+
const workspace = this.workspaceManager.get(req.params.id);
|
|
322
|
+
if (!workspace) {
|
|
323
|
+
return { status: 404, body: { error: 'Workspace not found' } };
|
|
324
|
+
}
|
|
325
|
+
const body = req.body;
|
|
326
|
+
if (!body?.name) {
|
|
327
|
+
return { status: 400, body: { error: 'name is required' } };
|
|
328
|
+
}
|
|
329
|
+
try {
|
|
330
|
+
const agent = await this.agentManager.spawn(req.params.id, workspace.path, body);
|
|
331
|
+
return { status: 201, body: agent };
|
|
332
|
+
}
|
|
333
|
+
catch (err) {
|
|
334
|
+
return { status: 400, body: { error: String(err) } };
|
|
335
|
+
}
|
|
336
|
+
});
|
|
337
|
+
// Get agent
|
|
338
|
+
this.routes.set('GET /agents/:id', async (req) => {
|
|
339
|
+
const agent = this.agentManager.get(req.params.id);
|
|
340
|
+
if (!agent) {
|
|
341
|
+
return { status: 404, body: { error: 'Agent not found' } };
|
|
342
|
+
}
|
|
343
|
+
return { status: 200, body: agent };
|
|
344
|
+
});
|
|
345
|
+
// Stop agent
|
|
346
|
+
this.routes.set('DELETE /agents/:id', async (req) => {
|
|
347
|
+
const stopped = await this.agentManager.stop(req.params.id);
|
|
348
|
+
if (!stopped) {
|
|
349
|
+
return { status: 404, body: { error: 'Agent not found' } };
|
|
350
|
+
}
|
|
351
|
+
return { status: 204, body: null };
|
|
352
|
+
});
|
|
353
|
+
// Get agent output
|
|
354
|
+
this.routes.set('GET /agents/:id/output', async (req) => {
|
|
355
|
+
const limit = req.query.limit ? parseInt(req.query.limit, 10) : undefined;
|
|
356
|
+
const output = this.agentManager.getOutput(req.params.id, limit);
|
|
357
|
+
if (output === null) {
|
|
358
|
+
return { status: 404, body: { error: 'Agent not found' } };
|
|
359
|
+
}
|
|
360
|
+
return { status: 200, body: { output } };
|
|
361
|
+
});
|
|
362
|
+
// Send input to agent
|
|
363
|
+
this.routes.set('POST /agents/:id/input', async (req) => {
|
|
364
|
+
const body = req.body;
|
|
365
|
+
if (!body?.input) {
|
|
366
|
+
return { status: 400, body: { error: 'input is required' } };
|
|
367
|
+
}
|
|
368
|
+
const sent = this.agentManager.sendInput(req.params.id, body.input);
|
|
369
|
+
if (!sent) {
|
|
370
|
+
return { status: 404, body: { error: 'Agent not found' } };
|
|
371
|
+
}
|
|
372
|
+
return { status: 200, body: { success: true } };
|
|
373
|
+
});
|
|
374
|
+
// Interrupt agent by ID (send Ctrl+C to break out of stuck loops)
|
|
375
|
+
this.routes.set('POST /agents/:id/interrupt', async (req) => {
|
|
376
|
+
const interrupted = this.agentManager.interrupt(req.params.id);
|
|
377
|
+
if (!interrupted) {
|
|
378
|
+
return { status: 404, body: { error: 'Agent not found' } };
|
|
379
|
+
}
|
|
380
|
+
return { status: 200, body: { success: true } };
|
|
381
|
+
});
|
|
382
|
+
// Interrupt agent by name (for dashboard where only name is available)
|
|
383
|
+
this.routes.set('POST /agents/by-name/:name/interrupt', async (req) => {
|
|
384
|
+
const interrupted = this.agentManager.interruptByName(req.params.name);
|
|
385
|
+
if (!interrupted) {
|
|
386
|
+
return { status: 404, body: { error: 'Agent not found' } };
|
|
387
|
+
}
|
|
388
|
+
return { status: 200, body: { success: true } };
|
|
389
|
+
});
|
|
390
|
+
// === All Agents ===
|
|
391
|
+
// List all agents
|
|
392
|
+
this.routes.set('GET /agents', async () => {
|
|
393
|
+
const agents = this.agentManager.getAll();
|
|
394
|
+
return { status: 200, body: { agents } };
|
|
395
|
+
});
|
|
396
|
+
// === CLI Auth (for cloud server to call) ===
|
|
397
|
+
// List supported providers
|
|
398
|
+
this.routes.set('GET /auth/providers', async () => {
|
|
399
|
+
return { status: 200, body: { providers: getSupportedProviders() } };
|
|
400
|
+
});
|
|
401
|
+
// Start CLI auth flow
|
|
402
|
+
this.routes.set('POST /auth/cli/:provider/start', async (req) => {
|
|
403
|
+
const { provider } = req.params;
|
|
404
|
+
try {
|
|
405
|
+
const session = await startCLIAuth(provider);
|
|
406
|
+
return {
|
|
407
|
+
status: 200,
|
|
408
|
+
body: {
|
|
409
|
+
sessionId: session.id,
|
|
410
|
+
status: session.status,
|
|
411
|
+
authUrl: session.authUrl,
|
|
412
|
+
},
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
catch (err) {
|
|
416
|
+
return {
|
|
417
|
+
status: 400,
|
|
418
|
+
body: { error: err instanceof Error ? err.message : 'Failed to start auth' },
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
});
|
|
422
|
+
// Get auth session status
|
|
423
|
+
this.routes.set('GET /auth/cli/:provider/status/:sessionId', async (req) => {
|
|
424
|
+
const { sessionId } = req.params;
|
|
425
|
+
const session = getAuthSession(sessionId);
|
|
426
|
+
if (!session) {
|
|
427
|
+
return { status: 404, body: { error: 'Session not found' } };
|
|
428
|
+
}
|
|
429
|
+
return {
|
|
430
|
+
status: 200,
|
|
431
|
+
body: {
|
|
432
|
+
sessionId: session.id,
|
|
433
|
+
status: session.status,
|
|
434
|
+
authUrl: session.authUrl,
|
|
435
|
+
error: session.error,
|
|
436
|
+
errorHint: session.errorHint,
|
|
437
|
+
recoverable: session.recoverable,
|
|
438
|
+
promptsHandled: session.promptsHandled,
|
|
439
|
+
},
|
|
440
|
+
};
|
|
441
|
+
});
|
|
442
|
+
// Get credentials from completed auth
|
|
443
|
+
this.routes.set('GET /auth/cli/:provider/creds/:sessionId', async (req) => {
|
|
444
|
+
const { sessionId } = req.params;
|
|
445
|
+
const session = getAuthSession(sessionId);
|
|
446
|
+
if (!session) {
|
|
447
|
+
return { status: 404, body: { error: 'Session not found' } };
|
|
448
|
+
}
|
|
449
|
+
// Check for error state first
|
|
450
|
+
if (session.status === 'error') {
|
|
451
|
+
return {
|
|
452
|
+
status: 400,
|
|
453
|
+
body: {
|
|
454
|
+
error: session.error || 'Authentication failed',
|
|
455
|
+
errorHint: session.errorHint,
|
|
456
|
+
recoverable: session.recoverable,
|
|
457
|
+
status: session.status,
|
|
458
|
+
},
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
// Check if auth is complete AND we have credentials
|
|
462
|
+
// Status can be 'success' before credentials are extracted (race condition)
|
|
463
|
+
if (session.status !== 'success' || !session.token) {
|
|
464
|
+
return {
|
|
465
|
+
status: 400,
|
|
466
|
+
body: {
|
|
467
|
+
error: 'Auth not complete or credentials not yet available',
|
|
468
|
+
status: session.status,
|
|
469
|
+
hasToken: !!session.token,
|
|
470
|
+
},
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
return {
|
|
474
|
+
status: 200,
|
|
475
|
+
body: {
|
|
476
|
+
token: session.token,
|
|
477
|
+
refreshToken: session.refreshToken,
|
|
478
|
+
tokenExpiresAt: session.tokenExpiresAt,
|
|
479
|
+
provider: session.provider,
|
|
480
|
+
},
|
|
481
|
+
};
|
|
482
|
+
});
|
|
483
|
+
// Submit auth code to PTY session
|
|
484
|
+
this.routes.set('POST /auth/cli/:provider/code/:sessionId', async (req) => {
|
|
485
|
+
const { sessionId } = req.params;
|
|
486
|
+
const { code, state } = req.body;
|
|
487
|
+
if (!code || typeof code !== 'string') {
|
|
488
|
+
return { status: 400, body: { error: 'Auth code is required' } };
|
|
489
|
+
}
|
|
490
|
+
const result = await submitAuthCode(sessionId, code, state);
|
|
491
|
+
if (!result.success) {
|
|
492
|
+
return {
|
|
493
|
+
status: 400,
|
|
494
|
+
body: {
|
|
495
|
+
error: result.error || 'Failed to submit auth code',
|
|
496
|
+
needsRestart: result.needsRestart,
|
|
497
|
+
},
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
return { status: 200, body: { success: true, message: 'Auth code submitted' } };
|
|
501
|
+
});
|
|
502
|
+
// Complete auth and wait for credentials
|
|
503
|
+
this.routes.set('POST /auth/cli/:provider/complete/:sessionId', async (req) => {
|
|
504
|
+
const { sessionId } = req.params;
|
|
505
|
+
const { authCode, state } = req.body;
|
|
506
|
+
// For Codex, we need to forward the authCode to the CLI's callback server
|
|
507
|
+
// The Codex CLI starts a callback server at localhost:1455
|
|
508
|
+
if (authCode) {
|
|
509
|
+
try {
|
|
510
|
+
// Forward the OAuth callback to the Codex CLI's callback server
|
|
511
|
+
const callbackUrl = `http://localhost:1455/auth/callback?code=${encodeURIComponent(authCode)}${state ? `&state=${encodeURIComponent(state)}` : ''}`;
|
|
512
|
+
logger.info('Forwarding OAuth callback to Codex CLI', { callbackUrl: callbackUrl.replace(authCode, '[REDACTED]') });
|
|
513
|
+
const callbackResponse = await fetch(callbackUrl, {
|
|
514
|
+
signal: AbortSignal.timeout(5000),
|
|
515
|
+
});
|
|
516
|
+
if (!callbackResponse.ok) {
|
|
517
|
+
logger.error('Failed to forward callback to Codex CLI', { status: callbackResponse.status });
|
|
518
|
+
return {
|
|
519
|
+
status: 400,
|
|
520
|
+
body: {
|
|
521
|
+
error: 'Failed to deliver OAuth callback to Codex CLI. The CLI may have timed out.',
|
|
522
|
+
needsRestart: true,
|
|
523
|
+
},
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
logger.info('Successfully forwarded OAuth callback to Codex CLI');
|
|
527
|
+
}
|
|
528
|
+
catch (err) {
|
|
529
|
+
logger.error('Error forwarding callback to Codex CLI', { error: String(err) });
|
|
530
|
+
return {
|
|
531
|
+
status: 500,
|
|
532
|
+
body: {
|
|
533
|
+
error: 'Failed to reach Codex CLI callback server. The CLI may not be running.',
|
|
534
|
+
needsRestart: true,
|
|
535
|
+
},
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
// Wait for credentials to be available (polls for up to 15 seconds)
|
|
540
|
+
const { completeAuthSession } = await import('./cli-auth.js');
|
|
541
|
+
const completeResult = await completeAuthSession(sessionId);
|
|
542
|
+
if (!completeResult.success) {
|
|
543
|
+
return {
|
|
544
|
+
status: 400,
|
|
545
|
+
body: {
|
|
546
|
+
error: completeResult.error || 'Authentication failed',
|
|
547
|
+
},
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
return { status: 200, body: { success: true, token: completeResult.token } };
|
|
551
|
+
});
|
|
552
|
+
// Cancel auth session
|
|
553
|
+
this.routes.set('POST /auth/cli/:provider/cancel/:sessionId', async (req) => {
|
|
554
|
+
const { sessionId } = req.params;
|
|
555
|
+
const cancelled = cancelAuthSession(sessionId);
|
|
556
|
+
if (!cancelled) {
|
|
557
|
+
return { status: 404, body: { error: 'Session not found' } };
|
|
558
|
+
}
|
|
559
|
+
return { status: 200, body: { success: true } };
|
|
560
|
+
});
|
|
561
|
+
// Check if provider is authenticated (credentials exist)
|
|
562
|
+
this.routes.set('GET /auth/cli/:provider/check', async (req) => {
|
|
563
|
+
const { provider } = req.params;
|
|
564
|
+
const userId = req.query.userId;
|
|
565
|
+
const { checkProviderAuth } = await import('./cli-auth.js');
|
|
566
|
+
const authenticated = await checkProviderAuth(provider, userId);
|
|
567
|
+
return { status: 200, body: { authenticated } };
|
|
568
|
+
});
|
|
569
|
+
// === Repository Management ===
|
|
570
|
+
// Dynamic repo management without workspace restart
|
|
571
|
+
// List all repos
|
|
572
|
+
this.routes.set('GET /repos', async () => {
|
|
573
|
+
try {
|
|
574
|
+
const repoManager = getRepoManager();
|
|
575
|
+
const repos = repoManager.getRepos();
|
|
576
|
+
return { status: 200, body: { repos } };
|
|
577
|
+
}
|
|
578
|
+
catch (err) {
|
|
579
|
+
logger.error('Failed to list repos', { error: String(err) });
|
|
580
|
+
return { status: 500, body: { error: 'Failed to list repositories' } };
|
|
581
|
+
}
|
|
582
|
+
});
|
|
583
|
+
// Get a specific repo
|
|
584
|
+
this.routes.set('GET /repos/:name', async (req) => {
|
|
585
|
+
try {
|
|
586
|
+
const repoManager = getRepoManager();
|
|
587
|
+
// Handle encoded slashes (e.g., "owner%2Frepo" -> "owner/repo")
|
|
588
|
+
const fullName = decodeURIComponent(req.params.name);
|
|
589
|
+
const repo = repoManager.getRepo(fullName);
|
|
590
|
+
if (!repo) {
|
|
591
|
+
return { status: 404, body: { error: 'Repository not found' } };
|
|
592
|
+
}
|
|
593
|
+
return { status: 200, body: repo };
|
|
594
|
+
}
|
|
595
|
+
catch (err) {
|
|
596
|
+
logger.error('Failed to get repo', { error: String(err) });
|
|
597
|
+
return { status: 500, body: { error: 'Failed to get repository' } };
|
|
598
|
+
}
|
|
599
|
+
});
|
|
600
|
+
// Sync (clone or update) a repo
|
|
601
|
+
this.routes.set('POST /repos/sync', async (req) => {
|
|
602
|
+
const body = req.body;
|
|
603
|
+
// Support single repo or batch
|
|
604
|
+
const reposToSync = [];
|
|
605
|
+
if (body.repo) {
|
|
606
|
+
reposToSync.push(body.repo);
|
|
607
|
+
}
|
|
608
|
+
if (body.repos && Array.isArray(body.repos)) {
|
|
609
|
+
reposToSync.push(...body.repos);
|
|
610
|
+
}
|
|
611
|
+
if (reposToSync.length === 0) {
|
|
612
|
+
return { status: 400, body: { error: 'repo or repos field is required' } };
|
|
613
|
+
}
|
|
614
|
+
try {
|
|
615
|
+
const repoManager = getRepoManager();
|
|
616
|
+
const results = await repoManager.syncRepos(reposToSync);
|
|
617
|
+
const allSuccess = results.every(r => r.success);
|
|
618
|
+
return {
|
|
619
|
+
status: allSuccess ? 200 : 207, // 207 Multi-Status if partial success
|
|
620
|
+
body: { results },
|
|
621
|
+
};
|
|
622
|
+
}
|
|
623
|
+
catch (err) {
|
|
624
|
+
logger.error('Failed to sync repos', { error: String(err) });
|
|
625
|
+
return { status: 500, body: { error: 'Failed to sync repositories' } };
|
|
626
|
+
}
|
|
627
|
+
});
|
|
628
|
+
// Remove a repo
|
|
629
|
+
this.routes.set('DELETE /repos/:name', async (req) => {
|
|
630
|
+
try {
|
|
631
|
+
const repoManager = getRepoManager();
|
|
632
|
+
const fullName = decodeURIComponent(req.params.name);
|
|
633
|
+
const deleteFiles = req.query.deleteFiles === 'true';
|
|
634
|
+
const removed = await repoManager.removeRepo(fullName, deleteFiles);
|
|
635
|
+
if (!removed) {
|
|
636
|
+
return { status: 404, body: { error: 'Repository not found' } };
|
|
637
|
+
}
|
|
638
|
+
return { status: 200, body: { success: true, deleted: deleteFiles } };
|
|
639
|
+
}
|
|
640
|
+
catch (err) {
|
|
641
|
+
logger.error('Failed to remove repo', { error: String(err) });
|
|
642
|
+
return { status: 500, body: { error: 'Failed to remove repository' } };
|
|
643
|
+
}
|
|
644
|
+
});
|
|
645
|
+
}
|
|
646
|
+
/**
|
|
647
|
+
* Handle HTTP request
|
|
648
|
+
*/
|
|
649
|
+
async handleRequest(req, res) {
|
|
650
|
+
const originHeader = typeof req.headers.origin === 'string' ? req.headers.origin : undefined;
|
|
651
|
+
const allowedOrigin = this.resolveAllowedOrigin(originHeader, req.headers.host);
|
|
652
|
+
// CORS headers (default denies cross-origin unless explicitly allowed)
|
|
653
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
|
|
654
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
|
|
655
|
+
res.setHeader('Vary', 'Origin');
|
|
656
|
+
if (allowedOrigin === null) {
|
|
657
|
+
logger.warn('CORS origin blocked', { origin: originHeader });
|
|
658
|
+
res.writeHead(403, { 'Content-Type': 'application/json' });
|
|
659
|
+
res.end(JSON.stringify({ error: 'CORS origin not allowed' }));
|
|
660
|
+
return;
|
|
661
|
+
}
|
|
662
|
+
if (allowedOrigin) {
|
|
663
|
+
res.setHeader('Access-Control-Allow-Origin', allowedOrigin);
|
|
664
|
+
}
|
|
665
|
+
if (req.method === 'OPTIONS') {
|
|
666
|
+
res.writeHead(204);
|
|
667
|
+
res.end();
|
|
668
|
+
return;
|
|
669
|
+
}
|
|
670
|
+
try {
|
|
671
|
+
const url = new URL(req.url || '/', `http://${req.headers.host}`);
|
|
672
|
+
const apiReq = await this.parseRequest(req, url);
|
|
673
|
+
// Find matching route
|
|
674
|
+
const response = await this.routeRequest(apiReq);
|
|
675
|
+
// Send response
|
|
676
|
+
res.writeHead(response.status, {
|
|
677
|
+
'Content-Type': 'application/json',
|
|
678
|
+
...response.headers,
|
|
679
|
+
});
|
|
680
|
+
if (response.body !== null) {
|
|
681
|
+
const body = typeof response.body === 'string' ? response.body : JSON.stringify(response.body);
|
|
682
|
+
res.end(body);
|
|
683
|
+
}
|
|
684
|
+
else {
|
|
685
|
+
res.end();
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
catch (err) {
|
|
689
|
+
logger.error('Request error', { error: String(err) });
|
|
690
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
691
|
+
res.end(JSON.stringify({ error: 'Internal server error' }));
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
/**
|
|
695
|
+
* Parse incoming request
|
|
696
|
+
*/
|
|
697
|
+
async parseRequest(req, url) {
|
|
698
|
+
// Parse query params
|
|
699
|
+
const query = {};
|
|
700
|
+
url.searchParams.forEach((value, key) => {
|
|
701
|
+
query[key] = value;
|
|
702
|
+
});
|
|
703
|
+
// Parse body for POST/PUT
|
|
704
|
+
let body;
|
|
705
|
+
if (req.method === 'POST' || req.method === 'PUT') {
|
|
706
|
+
body = await this.parseBody(req);
|
|
707
|
+
}
|
|
708
|
+
return {
|
|
709
|
+
method: req.method || 'GET',
|
|
710
|
+
path: url.pathname,
|
|
711
|
+
body,
|
|
712
|
+
params: {},
|
|
713
|
+
query,
|
|
714
|
+
};
|
|
715
|
+
}
|
|
716
|
+
/**
|
|
717
|
+
* Parse request body
|
|
718
|
+
*/
|
|
719
|
+
parseBody(req) {
|
|
720
|
+
return new Promise((resolve, reject) => {
|
|
721
|
+
let data = '';
|
|
722
|
+
req.on('data', (chunk) => {
|
|
723
|
+
data += chunk;
|
|
724
|
+
});
|
|
725
|
+
req.on('end', () => {
|
|
726
|
+
try {
|
|
727
|
+
resolve(data ? JSON.parse(data) : undefined);
|
|
728
|
+
}
|
|
729
|
+
catch {
|
|
730
|
+
reject(new Error('Invalid JSON body'));
|
|
731
|
+
}
|
|
732
|
+
});
|
|
733
|
+
req.on('error', reject);
|
|
734
|
+
});
|
|
735
|
+
}
|
|
736
|
+
/**
|
|
737
|
+
* Route request to handler
|
|
738
|
+
*/
|
|
739
|
+
async routeRequest(req) {
|
|
740
|
+
for (const [pattern, handler] of this.routes) {
|
|
741
|
+
const match = this.matchRoute(pattern, req.method, req.path);
|
|
742
|
+
if (match) {
|
|
743
|
+
req.params = match.params;
|
|
744
|
+
return handler(req);
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
return { status: 404, body: { error: 'Not found' } };
|
|
748
|
+
}
|
|
749
|
+
/**
|
|
750
|
+
* Match route pattern against request
|
|
751
|
+
*/
|
|
752
|
+
matchRoute(pattern, method, path) {
|
|
753
|
+
const [patternMethod, patternPath] = pattern.split(' ');
|
|
754
|
+
if (patternMethod !== method) {
|
|
755
|
+
return null;
|
|
756
|
+
}
|
|
757
|
+
const patternParts = patternPath.split('/');
|
|
758
|
+
const pathParts = path.split('/');
|
|
759
|
+
if (patternParts.length !== pathParts.length) {
|
|
760
|
+
return null;
|
|
761
|
+
}
|
|
762
|
+
const params = {};
|
|
763
|
+
for (let i = 0; i < patternParts.length; i++) {
|
|
764
|
+
const patternPart = patternParts[i];
|
|
765
|
+
const pathPart = pathParts[i];
|
|
766
|
+
if (patternPart.startsWith(':')) {
|
|
767
|
+
params[patternPart.slice(1)] = pathPart;
|
|
768
|
+
}
|
|
769
|
+
else if (patternPart !== pathPart) {
|
|
770
|
+
return null;
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
return { params };
|
|
774
|
+
}
|
|
775
|
+
/**
|
|
776
|
+
* Handle WebSocket connection
|
|
777
|
+
*/
|
|
778
|
+
handleWebSocketConnection(ws, req) {
|
|
779
|
+
logger.info('WebSocket client connected', { url: req.url });
|
|
780
|
+
// Mark client as alive for ping/pong keepalive
|
|
781
|
+
this.clientAlive.set(ws, true);
|
|
782
|
+
// Handle pong responses
|
|
783
|
+
ws.on('pong', () => {
|
|
784
|
+
this.clientAlive.set(ws, true);
|
|
785
|
+
});
|
|
786
|
+
// Create session
|
|
787
|
+
const session = {
|
|
788
|
+
userId: 'anonymous', // Would be set from auth
|
|
789
|
+
githubUsername: 'anonymous',
|
|
790
|
+
connectedAt: new Date(),
|
|
791
|
+
};
|
|
792
|
+
this.sessions.set(ws, session);
|
|
793
|
+
// Send initial state
|
|
794
|
+
this.sendInitialState(ws);
|
|
795
|
+
// Handle messages
|
|
796
|
+
ws.on('message', (data) => {
|
|
797
|
+
try {
|
|
798
|
+
const message = JSON.parse(data.toString());
|
|
799
|
+
this.handleWebSocketMessage(ws, session, message);
|
|
800
|
+
}
|
|
801
|
+
catch (err) {
|
|
802
|
+
logger.error('WebSocket message error', { error: String(err) });
|
|
803
|
+
}
|
|
804
|
+
});
|
|
805
|
+
ws.on('close', () => {
|
|
806
|
+
logger.info('WebSocket client disconnected');
|
|
807
|
+
this.sessions.delete(ws);
|
|
808
|
+
});
|
|
809
|
+
ws.on('error', (err) => {
|
|
810
|
+
logger.error('WebSocket error', { error: String(err) });
|
|
811
|
+
});
|
|
812
|
+
}
|
|
813
|
+
/**
|
|
814
|
+
* Send initial state to WebSocket client
|
|
815
|
+
*/
|
|
816
|
+
sendInitialState(ws) {
|
|
817
|
+
const workspaces = this.workspaceManager.getAll();
|
|
818
|
+
const active = this.workspaceManager.getActive();
|
|
819
|
+
const agents = this.agentManager.getAll();
|
|
820
|
+
this.sendToClient(ws, {
|
|
821
|
+
type: 'init',
|
|
822
|
+
data: {
|
|
823
|
+
workspaces,
|
|
824
|
+
activeWorkspaceId: active?.id,
|
|
825
|
+
agents,
|
|
826
|
+
},
|
|
827
|
+
});
|
|
828
|
+
}
|
|
829
|
+
/**
|
|
830
|
+
* Handle WebSocket message from client
|
|
831
|
+
*/
|
|
832
|
+
handleWebSocketMessage(ws, session, message) {
|
|
833
|
+
switch (message.type) {
|
|
834
|
+
case 'switch_workspace':
|
|
835
|
+
if (typeof message.data === 'string') {
|
|
836
|
+
try {
|
|
837
|
+
this.workspaceManager.switchTo(message.data);
|
|
838
|
+
session.activeWorkspaceId = message.data;
|
|
839
|
+
}
|
|
840
|
+
catch (err) {
|
|
841
|
+
this.sendToClient(ws, { type: 'error', data: String(err) });
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
break;
|
|
845
|
+
case 'subscribe_output':
|
|
846
|
+
// Subscribe to agent output stream
|
|
847
|
+
// TODO: Implement output streaming
|
|
848
|
+
break;
|
|
849
|
+
case 'ping':
|
|
850
|
+
this.sendToClient(ws, { type: 'pong' });
|
|
851
|
+
break;
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
/**
|
|
855
|
+
* Send message to WebSocket client
|
|
856
|
+
*/
|
|
857
|
+
sendToClient(ws, message) {
|
|
858
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
859
|
+
ws.send(JSON.stringify(message));
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
/**
|
|
863
|
+
* Broadcast event to all WebSocket clients
|
|
864
|
+
*/
|
|
865
|
+
broadcastEvent(event) {
|
|
866
|
+
if (!this.wss)
|
|
867
|
+
return;
|
|
868
|
+
const message = JSON.stringify({ type: 'event', data: event });
|
|
869
|
+
for (const ws of this.wss.clients) {
|
|
870
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
871
|
+
ws.send(message);
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
//# sourceMappingURL=api.js.map
|