@cloudagentfleet/worker 0.1.1

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.
@@ -0,0 +1,958 @@
1
+ #!/usr/bin/env node
2
+
3
+
4
+ // apps/node-agent/src/main.ts
5
+ import { spawnSync } from "node:child_process";
6
+ import { existsSync } from "node:fs";
7
+ import { homedir } from "node:os";
8
+ import { join as join2 } from "node:path";
9
+ import { createHash } from "node:crypto";
10
+
11
+ // apps/node-agent/src/codeman.ts
12
+ import { readdir, realpath } from "node:fs/promises";
13
+ import { basename, dirname, join, resolve } from "node:path";
14
+ var CodemanApiError = class extends Error {
15
+ status;
16
+ errorCode;
17
+ constructor(status, message, errorCode) {
18
+ super(message);
19
+ this.name = "CodemanApiError";
20
+ this.status = status;
21
+ this.errorCode = errorCode;
22
+ }
23
+ };
24
+ var CodemanClient = class {
25
+ baseUrl;
26
+ headers;
27
+ fetchImpl;
28
+ waitTimeoutMs;
29
+ inputSequence = Date.now();
30
+ constructor(options) {
31
+ const baseUrl = options.baseUrl.endsWith("/") ? options.baseUrl.slice(0, -1) : options.baseUrl;
32
+ const parsed = new URL(baseUrl);
33
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
34
+ throw new Error("Codeman URL must use HTTP or HTTPS");
35
+ }
36
+ this.baseUrl = baseUrl;
37
+ this.fetchImpl = options.fetchImpl ?? fetch;
38
+ this.waitTimeoutMs = options.waitTimeoutMs ?? 6e4;
39
+ this.headers = { accept: "application/json" };
40
+ if (options.username !== void 0 || options.password !== void 0) {
41
+ if (!options.username || options.password === void 0) {
42
+ throw new Error("Codeman username and password must be provided together");
43
+ }
44
+ this.headers.authorization = `Basic ${Buffer.from(
45
+ `${options.username}:${options.password}`
46
+ ).toString("base64")}`;
47
+ }
48
+ }
49
+ async createSession(options) {
50
+ const envelope = await this.request("/api/v1/sessions", {
51
+ method: "POST",
52
+ body: {
53
+ workingDir: options.workingDir,
54
+ mode: options.mode,
55
+ name: options.name
56
+ }
57
+ });
58
+ if (!envelope.data?.session?.id) {
59
+ throw new CodemanApiError(200, "Codeman returned no session ID");
60
+ }
61
+ return envelope.data.session;
62
+ }
63
+ async status() {
64
+ return this.requestData("/api/v1/status");
65
+ }
66
+ async listSessions(includeTerminal = false) {
67
+ const data = await this.requestData("/api/v1/sessions");
68
+ const sessions = Array.isArray(data) ? data : data.sessions ?? [];
69
+ if (!includeTerminal) return sessions;
70
+ return Promise.all(sessions.map(async (session) => {
71
+ const sessionId = typeof session.id === "string" ? session.id : "";
72
+ if (!sessionId) return session;
73
+ try {
74
+ const terminal = await this.requestData(`/api/v1/sessions/${encodeURIComponent(sessionId)}/terminal?tail=20000`);
75
+ return {
76
+ ...session,
77
+ ...terminal.terminalBuffer === void 0 ? {} : { terminalBuffer: terminal.terminalBuffer },
78
+ ...terminal.status === void 0 ? {} : { status: terminal.status }
79
+ };
80
+ } catch {
81
+ return session;
82
+ }
83
+ }));
84
+ }
85
+ async startInteractive(sessionId) {
86
+ await this.request(`/api/v1/sessions/${encodeURIComponent(sessionId)}/interactive`, {
87
+ method: "POST"
88
+ });
89
+ }
90
+ async sendPrompt(sessionId, prompt, clientId, seq, signal) {
91
+ const input = `${prompt.replaceAll(/\r?\n/g, " ")}\r`;
92
+ return this.requestData(
93
+ `/api/v1/sessions/${encodeURIComponent(sessionId)}/input`,
94
+ {
95
+ method: "POST",
96
+ body: {
97
+ input,
98
+ useMux: true,
99
+ clientId,
100
+ seq,
101
+ wait: true,
102
+ waitTimeout: this.waitTimeoutMs
103
+ },
104
+ signal
105
+ }
106
+ );
107
+ }
108
+ async sendInput(sessionId, input, raw = false) {
109
+ const deliveredInput = raw ? input : `${input.replaceAll(/\r?\n/g, " ")}${input.endsWith("\r") ? "" : "\r"}`;
110
+ await this.request(`/api/v1/sessions/${encodeURIComponent(sessionId)}/input`, {
111
+ method: "POST",
112
+ body: {
113
+ input: deliveredInput,
114
+ useMux: !raw,
115
+ clientId: "hosted-agents-connector",
116
+ seq: ++this.inputSequence
117
+ }
118
+ });
119
+ }
120
+ async resize(sessionId, cols, rows) {
121
+ await this.request(`/api/v1/sessions/${encodeURIComponent(sessionId)}/resize`, {
122
+ method: "POST",
123
+ body: { cols, rows, viewportType: "desktop" }
124
+ });
125
+ }
126
+ async waitForCompletion(sessionId, signal) {
127
+ while (true) {
128
+ const data = await this.requestData(
129
+ `/api/v1/sessions/${encodeURIComponent(sessionId)}/wait?until=stop,exit&timeout=${this.waitTimeoutMs}`,
130
+ { signal }
131
+ );
132
+ if (!data.wait?.timedOut) return data.wait;
133
+ }
134
+ }
135
+ async lastResponse(sessionId, signal) {
136
+ return this.requestData(
137
+ `/api/v1/sessions/${encodeURIComponent(sessionId)}/last-response`,
138
+ { signal }
139
+ );
140
+ }
141
+ async deleteSession(sessionId, signal) {
142
+ await this.request(
143
+ `/api/v1/sessions/${encodeURIComponent(sessionId)}`,
144
+ { method: "DELETE", signal }
145
+ );
146
+ }
147
+ subscribeEvents(onEvent) {
148
+ const abort = new AbortController();
149
+ void this.readEvents(abort.signal, onEvent);
150
+ return () => abort.abort();
151
+ }
152
+ async readEvents(signal, onEvent) {
153
+ const response = await this.fetchImpl(`${this.baseUrl}/api/v1/events`, {
154
+ headers: { ...this.headers, accept: "text/event-stream" },
155
+ signal
156
+ }).catch(() => void 0);
157
+ if (!response?.ok || !response.body) return;
158
+ const reader = response.body.getReader();
159
+ const decoder = new TextDecoder();
160
+ let buffer = "";
161
+ try {
162
+ while (!signal.aborted) {
163
+ const result = await reader.read();
164
+ if (result.done) return;
165
+ buffer += decoder.decode(result.value, { stream: true });
166
+ let boundary = sseBoundary(buffer);
167
+ while (boundary) {
168
+ const raw = buffer.slice(0, boundary.index);
169
+ buffer = buffer.slice(boundary.index + boundary.length);
170
+ boundary = sseBoundary(buffer);
171
+ const data = raw.split(/\r?\n/).filter((line) => line.startsWith("data:")).map((line) => line.slice(5).trimStart()).join("\n");
172
+ if (!data) continue;
173
+ try {
174
+ const parsed = JSON.parse(data);
175
+ const nestedSession = isRecord(parsed.session) ? parsed.session : void 0;
176
+ const nestedData = isRecord(parsed.data) ? parsed.data : void 0;
177
+ const sessionId = String(
178
+ parsed.sessionId ?? parsed.session_id ?? nestedSession?.id ?? nestedData?.sessionId ?? nestedData?.session_id ?? ""
179
+ );
180
+ if (!sessionId) continue;
181
+ onEvent({
182
+ sessionId,
183
+ type: String(
184
+ parsed.type ?? parsed.event ?? raw.split(/\r?\n/).find((line) => line.startsWith("event:"))?.slice("event:".length).trim() ?? ""
185
+ ).toLowerCase(),
186
+ data: parsed.data ?? parsed.output ?? parsed.text,
187
+ createdAt: providerTimestamp(parsed.createdAt ?? parsed.created_at)
188
+ });
189
+ } catch {
190
+ }
191
+ }
192
+ }
193
+ } catch {
194
+ } finally {
195
+ reader.releaseLock();
196
+ }
197
+ }
198
+ async requestData(path, options = {}) {
199
+ const envelope = await this.request(path, options);
200
+ if (envelope.data === void 0) {
201
+ throw new CodemanApiError(200, "Codeman response did not include data");
202
+ }
203
+ return envelope.data;
204
+ }
205
+ async request(path, options = {}) {
206
+ const response = await this.fetchImpl(`${this.baseUrl}${path}`, {
207
+ method: options.method ?? "GET",
208
+ headers: {
209
+ ...this.headers,
210
+ ...options.body === void 0 ? {} : { "content-type": "application/json" }
211
+ },
212
+ body: options.body === void 0 ? void 0 : JSON.stringify(options.body),
213
+ signal: options.signal
214
+ });
215
+ const text = await response.text();
216
+ let envelope;
217
+ try {
218
+ envelope = JSON.parse(text);
219
+ } catch {
220
+ throw new CodemanApiError(response.status, text || response.statusText);
221
+ }
222
+ if (!response.ok || envelope.success === false) {
223
+ throw new CodemanApiError(
224
+ response.status,
225
+ envelope.error ?? `Codeman request failed with HTTP ${response.status}`,
226
+ envelope.errorCode
227
+ );
228
+ }
229
+ return envelope;
230
+ }
231
+ };
232
+ var CodemanWorkerRuntime = class {
233
+ client;
234
+ nodeId;
235
+ instanceId;
236
+ workspaces;
237
+ workspacePolicy;
238
+ send;
239
+ mode;
240
+ agentProfiles;
241
+ cursorCommand;
242
+ claudeCommand;
243
+ activeJobs = /* @__PURE__ */ new Map();
244
+ constructor(options) {
245
+ this.client = options.client;
246
+ this.nodeId = options.nodeId;
247
+ this.instanceId = options.instanceId ?? options.nodeId;
248
+ this.workspaces = options.workspaces;
249
+ this.workspacePolicy = options.workspacePolicy;
250
+ this.send = options.send;
251
+ this.mode = options.mode ?? "claude";
252
+ this.agentProfiles = options.agentProfiles?.map((agent) => ({ ...agent })) ?? [];
253
+ this.cursorCommand = options.cursorCommand;
254
+ this.claudeCommand = options.claudeCommand;
255
+ }
256
+ async handle(message) {
257
+ if (message.type === "job.assign") {
258
+ await this.assign(message.job);
259
+ return;
260
+ }
261
+ if (message.type === "job.cancel") {
262
+ await this.cancel(message.jobId);
263
+ }
264
+ }
265
+ async handleConnectorRequest(request) {
266
+ if (request.instanceId !== this.instanceId) {
267
+ throw new Error("Connector instance authorization failed");
268
+ }
269
+ const payload = request.payload ?? {};
270
+ switch (request.operation) {
271
+ case "status":
272
+ return this.client.status();
273
+ case "capabilities": {
274
+ const status = await this.client.status();
275
+ const codemanAgents = [
276
+ {
277
+ id: this.mode,
278
+ name: this.mode === "claude" ? "Claude Code" : this.mode,
279
+ mode: this.mode,
280
+ ready: true
281
+ },
282
+ ...status.agents ?? [],
283
+ ...status.capabilities ?? []
284
+ ];
285
+ return [
286
+ ...codemanAgents,
287
+ ...this.agentProfiles.filter(
288
+ (agent) => !codemanAgents.some(
289
+ (candidate) => (typeof candidate === "string" ? candidate : candidate.id) === agent.id
290
+ )
291
+ )
292
+ ];
293
+ }
294
+ case "workspaces":
295
+ return this.workspaces;
296
+ case "workspaces.suggest":
297
+ return this.suggestWorkspacePaths(typeof payload.prefix === "string" ? payload.prefix : "");
298
+ case "sessions.list":
299
+ return this.client.listSessions(true);
300
+ case "sessions.create": {
301
+ const workspace = typeof payload.workspaceId === "string" ? this.resolveWorkspace(payload.workspaceId) : this.resolveWorkspacePath(requiredString(payload.workspacePath, "workspacePath"));
302
+ const agent = payload.agent;
303
+ if (!isAgentProfile(agent)) throw new Error("Invalid connector agent");
304
+ const session = await this.client.createSession({
305
+ workingDir: workspace.canonicalPath,
306
+ mode: agent.mode,
307
+ name: typeof payload.title === "string" ? payload.title : `${agent.name} \xB7 ${workspace.name}`
308
+ });
309
+ await this.client.startInteractive(session.id);
310
+ const providerCommand = agent.id === "cursor-agent" ? this.cursorCommand : agent.id === "claude-code" ? this.claudeCommand : void 0;
311
+ if (providerCommand) {
312
+ await this.client.sendInput(session.id, `${providerCommand}\r`, true);
313
+ }
314
+ return {
315
+ ...session,
316
+ workspaceId: workspace.id,
317
+ mode: agent.mode
318
+ };
319
+ }
320
+ case "sessions.input":
321
+ await this.client.sendInput(
322
+ requiredString(payload.sessionId, "sessionId"),
323
+ requiredInput(payload.input, "input"),
324
+ payload.raw === true
325
+ );
326
+ return void 0;
327
+ case "sessions.resize":
328
+ await this.client.resize(
329
+ requiredString(payload.sessionId, "sessionId"),
330
+ requiredInteger(payload.cols, "cols"),
331
+ requiredInteger(payload.rows, "rows")
332
+ );
333
+ return void 0;
334
+ case "sessions.stop":
335
+ await this.client.deleteSession(requiredString(payload.sessionId, "sessionId"));
336
+ return void 0;
337
+ }
338
+ }
339
+ startEventForwarding() {
340
+ return this.client.subscribeEvents((event) => {
341
+ const mapped = this.toSessionEvent(event);
342
+ if (!mapped) return;
343
+ void this.send({
344
+ type: "codeman.session.event",
345
+ instanceId: this.instanceId,
346
+ event: mapped
347
+ }).catch(() => void 0);
348
+ });
349
+ }
350
+ async assign(job) {
351
+ if (job.provider !== "codeman") {
352
+ throw new Error(`Codeman runtime cannot execute provider: ${job.provider}`);
353
+ }
354
+ if (this.activeJobs.has(job.id)) return;
355
+ const workspace = this.resolveWorkspace(job.workspaceId);
356
+ const abortController = new AbortController();
357
+ const session = await this.client.createSession({
358
+ workingDir: workspace.canonicalPath,
359
+ mode: this.mode,
360
+ name: `hosted-${this.nodeId}-${job.id}`
361
+ });
362
+ this.activeJobs.set(job.id, { sessionId: session.id, abortController });
363
+ try {
364
+ await this.send({
365
+ type: "job.accept",
366
+ jobId: job.id,
367
+ idempotencyKey: job.idempotencyKey
368
+ });
369
+ await this.send({ type: "job.event", jobId: job.id, event: "started" });
370
+ await this.client.startInteractive(session.id);
371
+ const input = await this.client.sendPrompt(
372
+ session.id,
373
+ job.prompt,
374
+ `hosted-agents-${this.nodeId}`,
375
+ 1,
376
+ abortController.signal
377
+ );
378
+ if (input.wait?.timedOut || !input.wait) {
379
+ await this.client.waitForCompletion(session.id, abortController.signal);
380
+ }
381
+ const response = await this.readResponse(session.id, abortController.signal);
382
+ await this.send({
383
+ type: "job.event",
384
+ jobId: job.id,
385
+ event: "completed",
386
+ data: response.text
387
+ });
388
+ } catch (error) {
389
+ if (abortController.signal.aborted) {
390
+ await this.send({ type: "job.event", jobId: job.id, event: "cancelled" });
391
+ } else {
392
+ await this.send({
393
+ type: "job.event",
394
+ jobId: job.id,
395
+ event: "failed",
396
+ data: error instanceof Error ? error.message : "Codeman job failed"
397
+ });
398
+ }
399
+ } finally {
400
+ this.activeJobs.delete(job.id);
401
+ }
402
+ }
403
+ async cancel(jobId) {
404
+ const active = this.activeJobs.get(jobId);
405
+ if (!active) return;
406
+ active.abortController.abort();
407
+ await this.client.deleteSession(active.sessionId);
408
+ }
409
+ async readResponse(sessionId, signal) {
410
+ let response = await this.client.lastResponse(sessionId, signal);
411
+ for (let attempt = 0; attempt < 10 && !response.text.trim(); attempt += 1) {
412
+ await delay(250, signal);
413
+ response = await this.client.lastResponse(sessionId, signal);
414
+ }
415
+ return response;
416
+ }
417
+ resolveWorkspace(workspaceId) {
418
+ const workspace = this.workspaces.find((candidate) => candidate.id === workspaceId);
419
+ if (!workspace) throw new Error(`Unknown workspace: ${workspaceId}`);
420
+ return this.validateWorkspace(workspace);
421
+ }
422
+ async suggestWorkspacePaths(prefix) {
423
+ const input = prefix.trim();
424
+ if (!input && this.workspacePolicy.mode === "folders") {
425
+ return this.workspacePolicy.roots.slice(0, 50).map((path) => ({ path: resolve(path), name: basename(path) || path }));
426
+ }
427
+ const trailingSeparator = /[\\/]/.test(input.at(-1) ?? "");
428
+ const basePath = input ? trailingSeparator ? input : dirname(input) : process.cwd();
429
+ const fragment = input && !trailingSeparator ? basename(input).toLowerCase() : "";
430
+ const directory = resolve(basePath);
431
+ if (!isAllowedPath(directory, this.workspacePolicy)) return [];
432
+ let entries;
433
+ try {
434
+ entries = await readdir(directory, { withFileTypes: true });
435
+ } catch (error) {
436
+ if (isFileSystemError(error, "ENOENT", "ENOTDIR")) return [];
437
+ throw error;
438
+ }
439
+ const suggestions = [];
440
+ for (const entry of entries) {
441
+ if (!entry.isDirectory() || entry.isSymbolicLink()) continue;
442
+ if (fragment && !entry.name.toLowerCase().startsWith(fragment)) continue;
443
+ const candidate = join(directory, entry.name);
444
+ let canonicalCandidate;
445
+ try {
446
+ canonicalCandidate = await realpath(candidate);
447
+ } catch (error) {
448
+ if (isFileSystemError(error, "ENOENT", "ELOOP")) continue;
449
+ throw error;
450
+ }
451
+ if (!isAllowedPath(canonicalCandidate, this.workspacePolicy)) continue;
452
+ suggestions.push({ path: canonicalCandidate, name: entry.name });
453
+ if (suggestions.length >= 50) break;
454
+ }
455
+ return suggestions.sort((left, right) => left.name.localeCompare(right.name));
456
+ }
457
+ resolveWorkspacePath(workspacePath) {
458
+ const existing = this.workspaces.find(
459
+ (candidate) => candidate.canonicalPath === workspacePath
460
+ );
461
+ if (existing) return this.validateWorkspace(existing);
462
+ if (!isAllowedPath(workspacePath, this.workspacePolicy)) {
463
+ throw new Error(`Workspace is outside the worker policy: ${workspacePath}`);
464
+ }
465
+ return {
466
+ id: workspacePath,
467
+ name: workspacePath.split(/[\\/]/).filter(Boolean).at(-1) ?? workspacePath,
468
+ canonicalPath: workspacePath,
469
+ health: "ready",
470
+ providers: { codeman: "ready" }
471
+ };
472
+ }
473
+ validateWorkspace(workspace) {
474
+ if (workspace.health !== "ready") {
475
+ throw new Error(`Workspace is not ready: ${workspace.id}`);
476
+ }
477
+ if (workspace.providers.codeman !== "ready") {
478
+ throw new Error("Codeman is not ready for the selected workspace");
479
+ }
480
+ if (!isAllowedPath(workspace.canonicalPath, this.workspacePolicy)) {
481
+ throw new Error(`Workspace is outside the worker policy: ${workspace.id}`);
482
+ }
483
+ return workspace;
484
+ }
485
+ toSessionEvent(event) {
486
+ const type = event.type;
487
+ let mapped;
488
+ if (type.includes("terminal") || type.includes("output")) mapped = "output";
489
+ else if (type.includes("permission") || type.includes("wait")) mapped = "waiting";
490
+ else if (type.includes("idle")) mapped = "idle";
491
+ else if (type.includes("fail") || type.includes("error")) mapped = "failed";
492
+ else if (type.includes("complete")) mapped = "completed";
493
+ else if (type.includes("exit") || type.includes("delet") || type.includes("stop")) mapped = "stopped";
494
+ else if (type.includes("start") || type.includes("run")) mapped = "started";
495
+ else if (type.includes("create")) mapped = "created";
496
+ else return void 0;
497
+ return {
498
+ instanceId: this.instanceId,
499
+ sessionId: event.sessionId,
500
+ event: mapped,
501
+ ...event.data === void 0 ? {} : { data: typeof event.data === "string" ? event.data : JSON.stringify(event.data) },
502
+ createdAt: event.createdAt
503
+ };
504
+ }
505
+ };
506
+ function requiredString(value, field) {
507
+ if (typeof value !== "string" || !value.trim()) throw new Error(`${field} is required`);
508
+ return value;
509
+ }
510
+ function requiredInput(value, field) {
511
+ if (typeof value !== "string" || value.length === 0) throw new Error(`${field} is required`);
512
+ return value;
513
+ }
514
+ function requiredInteger(value, field) {
515
+ if (typeof value !== "number" || !Number.isInteger(value) || value < 1 || value > 500) {
516
+ throw new Error(`${field} must be an integer between 1 and 500`);
517
+ }
518
+ return value;
519
+ }
520
+ function isAgentProfile(value) {
521
+ if (!isRecord(value)) return false;
522
+ const modes = [
523
+ "claude",
524
+ "shell",
525
+ "opencode",
526
+ "codex",
527
+ "gemini",
528
+ "antigravity",
529
+ "pi",
530
+ "grok",
531
+ "deepseek"
532
+ ];
533
+ return typeof value.name === "string" && modes.includes(value.mode);
534
+ }
535
+ function isAllowedPath(path, policy) {
536
+ if (policy.mode === "system") return true;
537
+ const normalizedPath = normalizePath(path);
538
+ return policy.roots.some((root) => {
539
+ const normalizedRoot = normalizePath(root);
540
+ return !normalizedRoot || normalizedPath === normalizedRoot || normalizedPath.startsWith(`${normalizedRoot}/`);
541
+ });
542
+ }
543
+ function normalizePath(path) {
544
+ const segments = [];
545
+ for (const segment of path.replaceAll("\\", "/").split("/")) {
546
+ if (!segment || segment === ".") continue;
547
+ if (segment === "..") {
548
+ segments.pop();
549
+ } else {
550
+ segments.push(segment);
551
+ }
552
+ }
553
+ return segments.join("/").toLowerCase();
554
+ }
555
+ function isFileSystemError(error, ...codes) {
556
+ return typeof error === "object" && error !== null && "code" in error && codes.includes(String(error.code));
557
+ }
558
+ function delay(milliseconds, signal) {
559
+ return new Promise((resolve2, reject) => {
560
+ const timer = setTimeout(resolve2, milliseconds);
561
+ signal.addEventListener(
562
+ "abort",
563
+ () => {
564
+ clearTimeout(timer);
565
+ reject(signal.reason ?? new Error("Operation aborted"));
566
+ },
567
+ { once: true }
568
+ );
569
+ });
570
+ }
571
+ function isRecord(value) {
572
+ return typeof value === "object" && value !== null && !Array.isArray(value);
573
+ }
574
+ function sseBoundary(value) {
575
+ const lineFeed = value.indexOf("\n\n");
576
+ const carriageReturn = value.indexOf("\r\n\r\n");
577
+ if (lineFeed < 0 && carriageReturn < 0) return void 0;
578
+ if (lineFeed < 0) return { index: carriageReturn, length: 4 };
579
+ if (carriageReturn < 0 || lineFeed < carriageReturn) {
580
+ return { index: lineFeed, length: 2 };
581
+ }
582
+ return { index: carriageReturn, length: 4 };
583
+ }
584
+ function providerTimestamp(value) {
585
+ if (typeof value === "string" && !Number.isNaN(Date.parse(value))) return value;
586
+ if (typeof value === "number" && Number.isFinite(value)) return new Date(value).toISOString();
587
+ return (/* @__PURE__ */ new Date()).toISOString();
588
+ }
589
+
590
+ // apps/node-agent/src/index.ts
591
+ function validateConfig(config) {
592
+ if (!config.nodeId.trim()) {
593
+ throw new Error("nodeId is required");
594
+ }
595
+ const controlPlane = new URL(config.controlPlaneUrl);
596
+ const localHost = ["localhost", "127.0.0.1", "::1"].includes(controlPlane.hostname);
597
+ if (controlPlane.protocol !== "https:" && !localHost) {
598
+ throw new Error("controlPlaneUrl must use HTTPS outside local development");
599
+ }
600
+ return {
601
+ ...config,
602
+ capabilities: [...new Set(config.capabilities)]
603
+ };
604
+ }
605
+
606
+ // packages/protocol/src/index.ts
607
+ var DEFAULT_PREVIEW_LEASE_MS = 15 * 6e4;
608
+ function isCodemanConnectorRequest(value) {
609
+ return isRecord2(value) && value.type === "codeman.request" && isNonEmptyString(value.requestId) && isNonEmptyString(value.instanceId) && isCodemanConnectorOperation(value.operation) && (value.payload === void 0 || isRecord2(value.payload));
610
+ }
611
+ function validateCodemanConnectorRequest(value) {
612
+ if (!isCodemanConnectorRequest(value)) {
613
+ throw new TypeError("Invalid Codeman connector request");
614
+ }
615
+ return value;
616
+ }
617
+ function isRecord2(value) {
618
+ return typeof value === "object" && value !== null && !Array.isArray(value);
619
+ }
620
+ function isNonEmptyString(value) {
621
+ return typeof value === "string" && value.trim().length > 0;
622
+ }
623
+ function isCodemanConnectorOperation(value) {
624
+ return value === "status" || value === "capabilities" || value === "workspaces" || value === "workspaces.suggest" || value === "sessions.list" || value === "sessions.create" || value === "sessions.input" || value === "sessions.resize" || value === "sessions.stop";
625
+ }
626
+
627
+ // apps/node-agent/src/transport.ts
628
+ var ControllerTransport = class {
629
+ config;
630
+ fetchImpl;
631
+ heartbeatIntervalMs;
632
+ reconnect;
633
+ reconnectDelayMs;
634
+ connectorInstanceId;
635
+ handledConnectorRequests = /* @__PURE__ */ new Set();
636
+ abortController = new AbortController();
637
+ heartbeatTimer;
638
+ constructor(options) {
639
+ this.config = options;
640
+ this.fetchImpl = options.fetchImpl ?? fetch;
641
+ this.heartbeatIntervalMs = options.heartbeatIntervalMs ?? 15e3;
642
+ this.reconnect = options.reconnect ?? false;
643
+ this.reconnectDelayMs = options.reconnectDelayMs ?? 1e3;
644
+ this.connectorInstanceId = options.connectorInstanceId;
645
+ }
646
+ async start(handle, connectorHandle) {
647
+ await this.register();
648
+ this.heartbeatTimer = setInterval(() => {
649
+ void this.sendHeartbeat();
650
+ }, this.heartbeatIntervalMs);
651
+ try {
652
+ do {
653
+ try {
654
+ const response = await this.fetchImpl(
655
+ `${this.baseUrl()}/api/workers/${encodeURIComponent(this.config.nodeId)}/connect`,
656
+ {
657
+ headers: this.headers(),
658
+ signal: this.abortController.signal
659
+ }
660
+ );
661
+ if (!response.ok || !response.body) {
662
+ throw new Error(`Controller connection failed with HTTP ${response.status}`);
663
+ }
664
+ await readServerSentEvents(
665
+ response.body,
666
+ handle,
667
+ this.abortController.signal,
668
+ (request) => this.handleConnectorRequest(request, connectorHandle)
669
+ );
670
+ } catch (error) {
671
+ if (!this.reconnect || this.abortController.signal.aborted) throw error;
672
+ }
673
+ if (!this.reconnect || this.abortController.signal.aborted) break;
674
+ await delay2(this.reconnectDelayMs, this.abortController.signal);
675
+ } while (!this.abortController.signal.aborted);
676
+ } finally {
677
+ this.stop();
678
+ }
679
+ }
680
+ stop() {
681
+ if (this.heartbeatTimer) clearInterval(this.heartbeatTimer);
682
+ this.heartbeatTimer = void 0;
683
+ this.abortController.abort();
684
+ }
685
+ async send(message) {
686
+ const response = await this.fetchImpl(
687
+ `${this.baseUrl()}/api/workers/${encodeURIComponent(this.config.nodeId)}/messages`,
688
+ {
689
+ method: "POST",
690
+ headers: { ...this.headers(), "content-type": "application/json" },
691
+ body: JSON.stringify(message),
692
+ signal: this.abortController.signal
693
+ }
694
+ );
695
+ if (!response.ok) throw new Error(`Controller message failed with HTTP ${response.status}`);
696
+ }
697
+ async handleConnectorRequest(request, connectorHandle) {
698
+ const response = {
699
+ type: "codeman.response",
700
+ requestId: request.requestId,
701
+ instanceId: request.instanceId,
702
+ success: false,
703
+ error: "Connector is not enabled",
704
+ errorCode: "connector_disabled"
705
+ };
706
+ try {
707
+ if (this.connectorInstanceId && request.instanceId !== this.connectorInstanceId) {
708
+ throw new Error("Connector instance authorization failed");
709
+ }
710
+ if (this.handledConnectorRequests.has(request.requestId)) {
711
+ throw new Error("Duplicate connector request");
712
+ }
713
+ this.handledConnectorRequests.add(request.requestId);
714
+ if (this.handledConnectorRequests.size > 1e3) {
715
+ const oldest = this.handledConnectorRequests.values().next().value;
716
+ if (typeof oldest === "string") this.handledConnectorRequests.delete(oldest);
717
+ }
718
+ if (!connectorHandle) throw new Error("Connector is not enabled");
719
+ response.success = true;
720
+ response.data = await connectorHandle(request);
721
+ delete response.error;
722
+ delete response.errorCode;
723
+ } catch (error) {
724
+ response.error = error instanceof Error ? error.message : "Connector request failed";
725
+ response.errorCode = "connector_request_failed";
726
+ }
727
+ await this.send(response);
728
+ }
729
+ async register() {
730
+ const registration = {
731
+ nodeId: this.config.nodeId,
732
+ name: this.config.name,
733
+ platform: this.config.platform,
734
+ capabilities: this.config.capabilities,
735
+ labels: this.config.labels,
736
+ workspacePolicy: this.config.workspacePolicy,
737
+ workspaces: this.config.workspaces
738
+ };
739
+ const response = await this.fetchImpl(`${this.baseUrl()}/api/workers`, {
740
+ method: "POST",
741
+ headers: { ...this.headers(), "content-type": "application/json" },
742
+ body: JSON.stringify(registration),
743
+ signal: this.abortController.signal
744
+ });
745
+ if (!response.ok) throw new Error(`Controller enrollment failed with HTTP ${response.status}`);
746
+ }
747
+ async sendHeartbeat() {
748
+ await this.send({
749
+ type: "heartbeat",
750
+ nodeId: this.config.nodeId,
751
+ sentAt: (/* @__PURE__ */ new Date()).toISOString(),
752
+ capabilities: this.config.capabilities
753
+ });
754
+ }
755
+ baseUrl() {
756
+ return this.config.controlPlaneUrl.endsWith("/") ? this.config.controlPlaneUrl.slice(0, -1) : this.config.controlPlaneUrl;
757
+ }
758
+ headers() {
759
+ return this.config.authToken ? { accept: "text/event-stream, application/json", authorization: `Bearer ${this.config.authToken}` } : { accept: "text/event-stream, application/json" };
760
+ }
761
+ };
762
+ async function readServerSentEvents(body, handle, signal, handleConnectorRequest) {
763
+ const reader = body.getReader();
764
+ const decoder = new TextDecoder();
765
+ let buffer = "";
766
+ while (!signal.aborted) {
767
+ const result = await reader.read();
768
+ if (result.done) return;
769
+ buffer += decoder.decode(result.value, { stream: true });
770
+ let boundary = sseBoundary2(buffer);
771
+ while (boundary) {
772
+ const event = buffer.slice(0, boundary.index);
773
+ buffer = buffer.slice(boundary.index + boundary.length);
774
+ boundary = sseBoundary2(buffer);
775
+ const data = event.split(/\r?\n/).filter((line) => line.startsWith("data:")).map((line) => line.slice(5).trimStart()).join("\n");
776
+ if (!data) continue;
777
+ const parsed = JSON.parse(data);
778
+ if (parsed.type === "codeman.request") {
779
+ await handleConnectorRequest(validateCodemanConnectorRequest(parsed));
780
+ } else if (typeof parsed.type === "string") {
781
+ await handle(parsed);
782
+ }
783
+ }
784
+ }
785
+ }
786
+ function delay2(milliseconds, signal) {
787
+ return new Promise((resolve2, reject) => {
788
+ const timer = setTimeout(resolve2, milliseconds);
789
+ signal.addEventListener("abort", () => {
790
+ clearTimeout(timer);
791
+ reject(signal.reason ?? new Error("Operation aborted"));
792
+ }, { once: true });
793
+ });
794
+ }
795
+ function sseBoundary2(value) {
796
+ const lineFeed = value.indexOf("\n\n");
797
+ const carriageReturn = value.indexOf("\r\n\r\n");
798
+ if (lineFeed < 0 && carriageReturn < 0) return void 0;
799
+ if (lineFeed < 0) return { index: carriageReturn, length: 4 };
800
+ if (carriageReturn < 0 || lineFeed < carriageReturn) {
801
+ return { index: lineFeed, length: 2 };
802
+ }
803
+ return { index: carriageReturn, length: 4 };
804
+ }
805
+
806
+ // apps/node-agent/src/main.ts
807
+ function required(name) {
808
+ const value = process.env[name]?.trim();
809
+ if (!value) throw new Error(`${name} is required`);
810
+ return value;
811
+ }
812
+ function parseWorkspaces() {
813
+ const raw = process.env.HOSTED_AGENTS_WORKSPACES ?? "[]";
814
+ const value = JSON.parse(raw);
815
+ if (!Array.isArray(value)) throw new Error("HOSTED_AGENTS_WORKSPACES must be a JSON array");
816
+ return value.map((workspace) => ({
817
+ ...workspace,
818
+ id: `ws_${createHash("sha256").update(workspace.canonicalPath).digest("hex").slice(0, 20)}`
819
+ }));
820
+ }
821
+ function parsePolicy() {
822
+ const mode2 = process.env.HOSTED_AGENTS_WORKSPACE_MODE === "system" ? "system" : "folders";
823
+ const roots = (process.env.HOSTED_AGENTS_WORKSPACE_ROOTS ?? "").split(";").map((root) => root.trim()).filter(Boolean);
824
+ return { mode: mode2, roots };
825
+ }
826
+ function platform() {
827
+ if (process.platform === "darwin") return "macos";
828
+ if (process.platform === "win32") return "windows";
829
+ return "linux";
830
+ }
831
+ function commandAvailable(commandName) {
832
+ const result = process.platform === "win32" ? spawnSync("where.exe", [commandName], { encoding: "utf8", stdio: "pipe" }) : spawnSync("sh", ["-lc", `command -v ${commandName}`], { encoding: "utf8", stdio: "pipe" });
833
+ return result.status === 0 && Boolean(result.stdout?.trim());
834
+ }
835
+ function detectCommand(commandNames) {
836
+ for (const commandName of commandNames) {
837
+ if (commandAvailable(commandName)) return commandName;
838
+ }
839
+ for (const commandName of commandNames) {
840
+ const installedPath = join2(homedir(), ".local", "bin", commandName);
841
+ if (existsSync(installedPath)) return installedPath;
842
+ }
843
+ return void 0;
844
+ }
845
+ function isCursorAuthenticated(commandName) {
846
+ const result = spawnSync(commandName, ["status"], {
847
+ encoding: "utf8",
848
+ stdio: "pipe",
849
+ timeout: 5e3
850
+ });
851
+ const output = `${result.stdout ?? ""}
852
+ ${result.stderr ?? ""}`;
853
+ return result.status === 0 && !/not logged in|login required|unauthenticated/i.test(output);
854
+ }
855
+ function detectAgentProfiles(providers) {
856
+ return [
857
+ ...providers.cursor ? [{
858
+ id: "cursor-agent",
859
+ name: "Cursor Agent",
860
+ mode: "shell",
861
+ ready: providers.cursor.ready
862
+ }] : [],
863
+ ...providers.claude ? [{
864
+ id: "claude-code",
865
+ name: "Claude Code",
866
+ mode: "shell",
867
+ ready: providers.claude.ready
868
+ }] : []
869
+ ];
870
+ }
871
+ function mode() {
872
+ const selected = process.env.HOSTED_AGENTS_CODEMAN_MODE ?? "claude";
873
+ const modes = [
874
+ "claude",
875
+ "shell",
876
+ "opencode",
877
+ "codex",
878
+ "gemini",
879
+ "antigravity",
880
+ "pi",
881
+ "grok",
882
+ "deepseek"
883
+ ];
884
+ if (!modes.includes(selected)) {
885
+ throw new Error(`Unsupported HOSTED_AGENTS_CODEMAN_MODE: ${selected}`);
886
+ }
887
+ return selected;
888
+ }
889
+ async function runWorker() {
890
+ const cursorCommand = detectCommand(["agent", "cursor-agent"]);
891
+ const claudeCommand = detectCommand(["claude"]);
892
+ const providers = {
893
+ ...cursorCommand ? { cursor: { command: cursorCommand, ready: isCursorAuthenticated(cursorCommand) } } : {},
894
+ ...claudeCommand ? { claude: { command: claudeCommand, ready: true } } : {}
895
+ };
896
+ const agentProfiles = detectAgentProfiles(providers);
897
+ const workspaces = parseWorkspaces().map((workspace) => ({
898
+ ...workspace,
899
+ providers: {
900
+ ...workspace.providers,
901
+ cursor: !providers.cursor ? "missing" : providers.cursor.ready ? "ready" : "unauthenticated",
902
+ claude: !providers.claude ? "missing" : "ready"
903
+ }
904
+ }));
905
+ const config = validateConfig({
906
+ nodeId: required("HOSTED_AGENTS_NODE_ID"),
907
+ name: process.env.HOSTED_AGENTS_NODE_NAME ?? "Hosted Agents Worker",
908
+ platform: platform(),
909
+ capabilities: ["codeman", ...agentProfiles.length > 0 ? ["cursor-worker"] : []],
910
+ workspacePolicy: parsePolicy(),
911
+ workspaces,
912
+ controlPlaneUrl: required("HOSTED_AGENTS_CONTROLLER_URL"),
913
+ authToken: process.env.HOSTED_AGENTS_AUTH_TOKEN
914
+ });
915
+ const instanceId = process.env.CODEMAN_INSTANCE_ID ?? config.nodeId;
916
+ const transport = new ControllerTransport({
917
+ ...config,
918
+ connectorInstanceId: instanceId,
919
+ reconnect: true
920
+ });
921
+ const codeman = new CodemanClient({
922
+ baseUrl: process.env.CODEMAN_URL ?? "http://127.0.0.1:3000",
923
+ username: process.env.CODEMAN_USERNAME,
924
+ password: process.env.CODEMAN_PASSWORD
925
+ });
926
+ const runtime = new CodemanWorkerRuntime({
927
+ client: codeman,
928
+ nodeId: config.nodeId,
929
+ instanceId,
930
+ workspaces,
931
+ workspacePolicy: config.workspacePolicy ?? { mode: "folders", roots: [] },
932
+ send: (message) => transport.send(message),
933
+ mode: mode(),
934
+ agentProfiles,
935
+ cursorCommand,
936
+ claudeCommand
937
+ });
938
+ const stopEventForwarding = runtime.startEventForwarding();
939
+ process.once("SIGINT", () => transport.stop());
940
+ process.once("SIGTERM", () => transport.stop());
941
+ try {
942
+ await transport.start(
943
+ (message) => runtime.handle(message),
944
+ (request) => runtime.handleConnectorRequest(request)
945
+ );
946
+ } finally {
947
+ stopEventForwarding();
948
+ }
949
+ }
950
+ if (process.argv[1]?.endsWith("/main.ts") || process.argv[1]?.endsWith("\\main.ts") || process.argv[1]?.endsWith("/worker.mjs") || process.argv[1]?.endsWith("\\worker.mjs")) {
951
+ runWorker().catch((error) => {
952
+ console.error(error instanceof Error ? error.message : error);
953
+ process.exitCode = 1;
954
+ });
955
+ }
956
+ export {
957
+ runWorker
958
+ };