@codegrammer/co-od 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.
@@ -0,0 +1,202 @@
1
+ import * as api from "../api-client.js";
2
+ import { ClaudeAdapter } from "../adapters/claude.js";
3
+ import { CodexAdapter } from "../adapters/codex.js";
4
+ function parseArgs(args) {
5
+ const parsed = {
6
+ provider: "claude",
7
+ watch: [],
8
+ autoExecute: false,
9
+ maxConcurrent: 1,
10
+ dir: process.cwd(),
11
+ };
12
+ for (let i = 0; i < args.length; i++) {
13
+ if (args[i] === "--as" && args[i + 1]) {
14
+ parsed.as = args[++i];
15
+ }
16
+ else if (args[i] === "--role" && args[i + 1]) {
17
+ parsed.role = args[++i];
18
+ }
19
+ else if (args[i] === "--provider" && args[i + 1]) {
20
+ parsed.provider = args[++i];
21
+ }
22
+ else if (args[i] === "--watch" && args[i + 1]) {
23
+ parsed.watch = args[++i].split(",");
24
+ }
25
+ else if (args[i] === "--auto-execute") {
26
+ parsed.autoExecute = true;
27
+ }
28
+ else if (args[i] === "--instructions" && args[i + 1]) {
29
+ parsed.instructions = args[++i];
30
+ }
31
+ else if (args[i] === "--max-concurrent" && args[i + 1]) {
32
+ parsed.maxConcurrent = parseInt(args[++i], 10) || 1;
33
+ }
34
+ else if (args[i] === "--dir" && args[i + 1]) {
35
+ parsed.dir = args[++i];
36
+ }
37
+ else if (args[i] === "--server" && args[i + 1]) {
38
+ parsed.server = args[++i];
39
+ }
40
+ else if (!args[i].startsWith("--")) {
41
+ parsed.roomId = args[i];
42
+ }
43
+ }
44
+ // Default watch events if none specified
45
+ if (parsed.watch.length === 0) {
46
+ parsed.watch = ["task.assigned", "patch.proposed", "run.requested"];
47
+ }
48
+ return parsed;
49
+ }
50
+ function getAdapter(provider) {
51
+ switch (provider) {
52
+ case "codex":
53
+ return new CodexAdapter();
54
+ case "claude":
55
+ default:
56
+ return new ClaudeAdapter();
57
+ }
58
+ }
59
+ function timestamp() {
60
+ return new Date().toISOString().slice(11, 19);
61
+ }
62
+ export async function run(args) {
63
+ const parsed = parseArgs(args);
64
+ if (!parsed.roomId) {
65
+ console.error("Usage: co-od daemon <room-id> [--as <name>] [--role <role>] [--provider claude|codex] [--watch <events>] [--auto-execute] [--max-concurrent <n>]");
66
+ process.exit(1);
67
+ }
68
+ if (parsed.server) {
69
+ process.env.CO_ODE_SERVER = parsed.server;
70
+ }
71
+ const adapter = getAdapter(parsed.provider);
72
+ const isAvailable = await adapter.available();
73
+ if (!isAvailable) {
74
+ console.error(`Error: '${adapter.name}' is not installed or not in PATH.`);
75
+ process.exit(1);
76
+ }
77
+ // Register as an agent in the room
78
+ const agentName = parsed.as || `cli-${process.env.USER || "agent"}`;
79
+ console.error(`[${timestamp()}] Registering agent "${agentName}" in room ${parsed.roomId}...`);
80
+ let agentId;
81
+ try {
82
+ const reg = await api.post(`/api/rooms/${parsed.roomId}/agents`, {
83
+ name: agentName,
84
+ role: parsed.role || "worker",
85
+ provider: parsed.provider,
86
+ capabilities: parsed.watch,
87
+ instructions: parsed.instructions,
88
+ });
89
+ agentId = reg.agentId;
90
+ console.error(`[${timestamp()}] Registered as agent ${agentId}`);
91
+ }
92
+ catch (err) {
93
+ console.error(`[${timestamp()}] Failed to register agent: ${err}`);
94
+ process.exit(1);
95
+ }
96
+ console.error(`[${timestamp()}] Watching for events: ${parsed.watch.join(", ")}`);
97
+ console.error(`[${timestamp()}] Auto-execute: ${parsed.autoExecute}`);
98
+ console.error(`[${timestamp()}] Max concurrent: ${parsed.maxConcurrent}`);
99
+ console.error(`[${timestamp()}] Press Ctrl+C to stop.\n`);
100
+ let cursor;
101
+ let activeRuns = 0;
102
+ const processedEvents = new Set();
103
+ // Handle graceful shutdown
104
+ let stopping = false;
105
+ process.on("SIGINT", () => {
106
+ if (stopping)
107
+ process.exit(1);
108
+ stopping = true;
109
+ console.error(`\n[${timestamp()}] Stopping daemon... (Ctrl+C again to force)`);
110
+ if (activeRuns === 0)
111
+ process.exit(0);
112
+ });
113
+ process.on("SIGTERM", () => {
114
+ stopping = true;
115
+ process.exit(0);
116
+ });
117
+ // Event polling loop
118
+ while (!stopping) {
119
+ try {
120
+ const queryParams = cursor ? `?cursor=${cursor}` : "";
121
+ const data = await api.get(`/api/rooms/${parsed.roomId}/events${queryParams}`);
122
+ if (data.cursor) {
123
+ cursor = data.cursor;
124
+ }
125
+ const events = (data.events || []).filter((e) => parsed.watch.includes(e.type) && !processedEvents.has(e._id));
126
+ for (const event of events) {
127
+ processedEvents.add(event._id);
128
+ console.error(`[${timestamp()}] Event: ${event.type} ${JSON.stringify(event.payload || {}).slice(0, 100)}`);
129
+ if (activeRuns >= parsed.maxConcurrent) {
130
+ console.error(`[${timestamp()}] Skipping — at max concurrent (${parsed.maxConcurrent})`);
131
+ continue;
132
+ }
133
+ if (parsed.autoExecute) {
134
+ // Auto-dispatch
135
+ const goal = event.payload?.goal ||
136
+ event.payload?.description ||
137
+ `Handle event: ${event.type}`;
138
+ activeRuns++;
139
+ console.error(`[${timestamp()}] Dispatching: ${goal.slice(0, 80)}`);
140
+ // Create a run on the server
141
+ api
142
+ .post(`/api/rooms/${parsed.roomId}/agent-runs`, { goal, provider: parsed.provider, agentId, eventId: event._id })
143
+ .then(async ({ runId }) => {
144
+ const result = await adapter.execute(goal, {
145
+ workDir: parsed.dir,
146
+ onOutput: (data) => process.stderr.write(data),
147
+ });
148
+ console.error(`\n[${timestamp()}] Run ${runId} ${result.exitCode === 0 ? "succeeded" : "failed"}`);
149
+ // Report completion
150
+ try {
151
+ await api.post(`/api/rooms/${parsed.roomId}/agent-runs/${runId}/handoff`, {
152
+ ok: result.exitCode === 0,
153
+ run: {
154
+ status: result.exitCode === 0 ? "succeeded" : "failed",
155
+ steps: [
156
+ {
157
+ kind: result.exitCode === 0 ? "observation" : "error",
158
+ input: { description: goal },
159
+ output: {
160
+ success: result.exitCode === 0,
161
+ payload: {
162
+ stdout: result.stdout.slice(-4096),
163
+ stderr: result.stderr.slice(-2048),
164
+ exitCode: result.exitCode,
165
+ },
166
+ },
167
+ created_at: Date.now(),
168
+ },
169
+ ],
170
+ },
171
+ });
172
+ }
173
+ catch {
174
+ console.error(`[${timestamp()}] Warning: failed to report completion`);
175
+ }
176
+ })
177
+ .catch((err) => {
178
+ console.error(`[${timestamp()}] Run failed: ${err}`);
179
+ })
180
+ .finally(() => {
181
+ activeRuns--;
182
+ if (stopping && activeRuns === 0)
183
+ process.exit(0);
184
+ });
185
+ }
186
+ else {
187
+ // Print event, wait for user confirmation
188
+ const goal = event.payload?.goal ||
189
+ event.payload?.description ||
190
+ `Handle event: ${event.type}`;
191
+ console.error(`[${timestamp()}] Pending: ${goal.slice(0, 120)}`);
192
+ console.error(`[${timestamp()}] (use --auto-execute to handle automatically)`);
193
+ }
194
+ }
195
+ }
196
+ catch (err) {
197
+ console.error(`[${timestamp()}] Poll error: ${err}`);
198
+ }
199
+ // Wait before next poll
200
+ await new Promise((r) => setTimeout(r, 3000));
201
+ }
202
+ }
@@ -0,0 +1 @@
1
+ export declare function run(args: string[]): Promise<void>;
@@ -0,0 +1,49 @@
1
+ import { authenticate } from "../auth.js";
2
+ import { connectToRoom } from "../connect.js";
3
+ function parseArgs(args) {
4
+ const parsed = { dir: process.cwd() };
5
+ for (let i = 0; i < args.length; i++) {
6
+ if (args[i] === "--server" && args[i + 1]) {
7
+ parsed.server = args[++i];
8
+ }
9
+ else if (args[i] === "--invite-token" && args[i + 1]) {
10
+ parsed.inviteToken = args[++i];
11
+ }
12
+ else if (args[i] === "--dir" && args[i + 1]) {
13
+ parsed.dir = args[++i];
14
+ }
15
+ else if (!args[i].startsWith("--")) {
16
+ parsed.roomId = args[i];
17
+ }
18
+ }
19
+ return parsed;
20
+ }
21
+ export async function run(args) {
22
+ const parsed = parseArgs(args);
23
+ if (!parsed.roomId) {
24
+ console.error("Usage: co-od join <room-id> [--invite-token <tok>] [--dir <path>]");
25
+ process.exit(1);
26
+ }
27
+ const serverUrl = parsed.server ||
28
+ process.env.CO_ODE_SERVER ||
29
+ "https://co-ode.vercel.app";
30
+ console.error(`[co-od] Joining room ${parsed.roomId}...`);
31
+ console.error(`[co-od] Server: ${serverUrl}`);
32
+ console.error(`[co-od] Working directory: ${parsed.dir}`);
33
+ // Authenticate
34
+ const { sessionToken, realtimeToken, terminalWsUrl } = await authenticate({
35
+ serverUrl,
36
+ roomId: parsed.roomId,
37
+ inviteToken: parsed.inviteToken,
38
+ });
39
+ console.error(`[co-od] Authenticated successfully`);
40
+ // Connect to room (blocks until disconnect)
41
+ await connectToRoom({
42
+ terminalWsUrl,
43
+ realtimeToken,
44
+ roomId: parsed.roomId,
45
+ workDir: parsed.dir,
46
+ serverUrl,
47
+ sessionToken,
48
+ });
49
+ }
@@ -0,0 +1 @@
1
+ export declare function run(args: string[]): Promise<void>;
@@ -0,0 +1,93 @@
1
+ import { writeFileSync, mkdirSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { join } from "node:path";
4
+ const CONFIG_DIR = join(homedir(), ".co-ode");
5
+ const SESSION_FILE = join(CONFIG_DIR, "session.json");
6
+ function parseArgs(args) {
7
+ const parsed = {};
8
+ for (let i = 0; i < args.length; i++) {
9
+ if (args[i] === "--token" && args[i + 1]) {
10
+ parsed.token = args[++i];
11
+ }
12
+ else if (args[i] === "--server" && args[i + 1]) {
13
+ parsed.server = args[++i];
14
+ }
15
+ }
16
+ return parsed;
17
+ }
18
+ function saveSession(sessionToken, user) {
19
+ mkdirSync(CONFIG_DIR, { recursive: true });
20
+ writeFileSync(SESSION_FILE, JSON.stringify({
21
+ sessionToken,
22
+ expiresAt: Date.now() + 23 * 60 * 60 * 1000,
23
+ user: user || "api-token-user",
24
+ }, null, 2));
25
+ }
26
+ async function deviceAuthFlow(serverUrl) {
27
+ // Request a device code
28
+ const authorizeRes = await fetch(`${serverUrl}/api/auth/cli/authorize`, {
29
+ method: "POST",
30
+ headers: { "content-type": "application/json" },
31
+ body: JSON.stringify({}),
32
+ });
33
+ if (!authorizeRes.ok) {
34
+ throw new Error(`Failed to start auth flow: ${authorizeRes.status}`);
35
+ }
36
+ const { code, verificationUrl, expiresAt } = (await authorizeRes.json());
37
+ // Open browser for approval
38
+ console.error(`\nTo authenticate, open this URL in your browser:\n`);
39
+ console.error(` ${verificationUrl}\n`);
40
+ console.error(`Your code: ${code}\n`);
41
+ try {
42
+ const open = (await import("open")).default;
43
+ await open(verificationUrl);
44
+ console.error(`Browser opened. Waiting for approval...`);
45
+ }
46
+ catch {
47
+ console.error(`Could not open browser automatically. Please open the URL manually.`);
48
+ }
49
+ // Poll for token
50
+ const pollInterval = 2000;
51
+ const deadline = expiresAt;
52
+ while (Date.now() < deadline) {
53
+ await new Promise((r) => setTimeout(r, pollInterval));
54
+ const tokenRes = await fetch(`${serverUrl}/api/auth/cli/token`, {
55
+ method: "POST",
56
+ headers: { "content-type": "application/json" },
57
+ body: JSON.stringify({ code }),
58
+ });
59
+ if (tokenRes.status === 410) {
60
+ throw new Error("Auth code expired. Please try again.");
61
+ }
62
+ if (tokenRes.status === 202) {
63
+ continue;
64
+ }
65
+ if (tokenRes.ok) {
66
+ const body = (await tokenRes.json());
67
+ if (body.token) {
68
+ return { token: body.token, user: body.user };
69
+ }
70
+ }
71
+ throw new Error(`Unexpected auth response: ${tokenRes.status}`);
72
+ }
73
+ throw new Error("Auth timed out. Please try again.");
74
+ }
75
+ export async function run(args) {
76
+ const parsed = parseArgs(args);
77
+ const serverUrl = parsed.server ||
78
+ process.env.CO_ODE_SERVER ||
79
+ "https://co-ode.vercel.app";
80
+ // --token flag: headless/CI mode
81
+ if (parsed.token) {
82
+ saveSession(parsed.token);
83
+ console.log(`Logged in with API token. Session saved to ${SESSION_FILE}`);
84
+ return;
85
+ }
86
+ // Interactive browser auth
87
+ console.error(`[co-od] Starting browser authentication...`);
88
+ console.error(`[co-od] Server: ${serverUrl}`);
89
+ const { token, user } = await deviceAuthFlow(serverUrl);
90
+ saveSession(token, user);
91
+ const displayName = user || "authenticated user";
92
+ console.log(`Logged in as ${displayName}. Session saved to ${SESSION_FILE}`);
93
+ }
@@ -0,0 +1 @@
1
+ export declare function run(args: string[]): Promise<void>;
@@ -0,0 +1,57 @@
1
+ import * as api from "../api-client.js";
2
+ function parseArgs(args) {
3
+ const parsed = { json: false };
4
+ for (let i = 0; i < args.length; i++) {
5
+ if (args[i] === "--json") {
6
+ parsed.json = true;
7
+ }
8
+ else if (args[i] === "--server" && args[i + 1]) {
9
+ parsed.server = args[++i];
10
+ }
11
+ }
12
+ return parsed;
13
+ }
14
+ function padRight(str, len) {
15
+ if (str.length >= len)
16
+ return str.slice(0, len);
17
+ return str + " ".repeat(len - str.length);
18
+ }
19
+ function printTable(rooms) {
20
+ if (rooms.length === 0) {
21
+ console.log("No rooms found.");
22
+ return;
23
+ }
24
+ const header = [
25
+ padRight("ID", 26),
26
+ padRight("NAME", 30),
27
+ padRight("STATUS", 12),
28
+ padRight("AGENTS", 8),
29
+ padRight("MEMBERS", 8),
30
+ ].join(" ");
31
+ console.log(header);
32
+ console.log("-".repeat(header.length));
33
+ for (const room of rooms) {
34
+ const row = [
35
+ padRight(room._id || "", 26),
36
+ padRight(room.name || "(unnamed)", 30),
37
+ padRight(room.status || "idle", 12),
38
+ padRight(String(room.agents?.length ?? 0), 8),
39
+ padRight(String(room.members?.length ?? 0), 8),
40
+ ].join(" ");
41
+ console.log(row);
42
+ }
43
+ }
44
+ export async function run(args) {
45
+ const parsed = parseArgs(args);
46
+ if (parsed.server) {
47
+ process.env.CO_ODE_SERVER = parsed.server;
48
+ }
49
+ const data = await api.get("/api/rooms");
50
+ const rooms = data.rooms || [];
51
+ if (parsed.json) {
52
+ console.log(JSON.stringify(rooms, null, 2));
53
+ }
54
+ else {
55
+ printTable(rooms);
56
+ }
57
+ }
@@ -0,0 +1 @@
1
+ export declare function run(args: string[]): Promise<void>;
@@ -0,0 +1,119 @@
1
+ import * as api from "../api-client.js";
2
+ import { ClaudeAdapter } from "../adapters/claude.js";
3
+ import { CodexAdapter } from "../adapters/codex.js";
4
+ function parseArgs(args) {
5
+ const parsed = {
6
+ provider: "claude",
7
+ dir: process.cwd(),
8
+ json: false,
9
+ };
10
+ const positional = [];
11
+ for (let i = 0; i < args.length; i++) {
12
+ if (args[i] === "--provider" && args[i + 1]) {
13
+ parsed.provider = args[++i];
14
+ }
15
+ else if (args[i] === "--dir" && args[i + 1]) {
16
+ parsed.dir = args[++i];
17
+ }
18
+ else if (args[i] === "--json") {
19
+ parsed.json = true;
20
+ }
21
+ else if (args[i] === "--server" && args[i + 1]) {
22
+ parsed.server = args[++i];
23
+ }
24
+ else if (!args[i].startsWith("--")) {
25
+ positional.push(args[i]);
26
+ }
27
+ }
28
+ parsed.roomId = positional[0];
29
+ parsed.goal = positional.slice(1).join(" ");
30
+ return parsed;
31
+ }
32
+ function getAdapter(provider) {
33
+ switch (provider) {
34
+ case "codex":
35
+ return new CodexAdapter();
36
+ case "claude":
37
+ default:
38
+ return new ClaudeAdapter();
39
+ }
40
+ }
41
+ export async function run(args) {
42
+ const parsed = parseArgs(args);
43
+ if (!parsed.roomId) {
44
+ console.error("Usage: co-od run <room-id> <goal> [--provider claude|codex] [--dir <path>] [--json]");
45
+ process.exit(1);
46
+ }
47
+ if (!parsed.goal) {
48
+ console.error("Error: goal is required");
49
+ console.error("Usage: co-od run <room-id> <goal>");
50
+ process.exit(1);
51
+ }
52
+ if (parsed.server) {
53
+ process.env.CO_ODE_SERVER = parsed.server;
54
+ }
55
+ const adapter = getAdapter(parsed.provider);
56
+ const isAvailable = await adapter.available();
57
+ if (!isAvailable) {
58
+ console.error(`Error: '${adapter.name}' is not installed or not in PATH.`);
59
+ process.exit(1);
60
+ }
61
+ // Create the agent run on the server
62
+ if (!parsed.json) {
63
+ console.error(`[co-od] Creating run in room ${parsed.roomId}...`);
64
+ }
65
+ const createRes = await api.post(`/api/rooms/${parsed.roomId}/agent-runs`, { goal: parsed.goal, provider: parsed.provider });
66
+ const runId = createRes.runId;
67
+ if (!parsed.json) {
68
+ console.error(`[co-od] Run ${runId} created. Executing with ${adapter.name}...`);
69
+ }
70
+ // Execute locally
71
+ const result = await adapter.execute(parsed.goal, {
72
+ workDir: parsed.dir,
73
+ onOutput: parsed.json
74
+ ? undefined
75
+ : (data) => process.stderr.write(data),
76
+ });
77
+ // Report completion
78
+ if (!parsed.json) {
79
+ console.error(`\n[co-od] Run ${result.exitCode === 0 ? "succeeded" : "failed"} (exit ${result.exitCode})`);
80
+ }
81
+ try {
82
+ await api.post(`/api/rooms/${parsed.roomId}/agent-runs/${runId}/handoff`, {
83
+ ok: result.exitCode === 0,
84
+ run: {
85
+ status: result.exitCode === 0 ? "succeeded" : "failed",
86
+ steps: [
87
+ {
88
+ kind: result.exitCode === 0 ? "observation" : "error",
89
+ input: { description: parsed.goal },
90
+ output: {
91
+ success: result.exitCode === 0,
92
+ payload: {
93
+ stdout: result.stdout.slice(-4096),
94
+ stderr: result.stderr.slice(-2048),
95
+ exitCode: result.exitCode,
96
+ },
97
+ },
98
+ created_at: Date.now(),
99
+ },
100
+ ],
101
+ },
102
+ });
103
+ }
104
+ catch {
105
+ if (!parsed.json) {
106
+ console.error("[co-od] Warning: failed to report completion to server");
107
+ }
108
+ }
109
+ if (parsed.json) {
110
+ console.log(JSON.stringify({
111
+ runId,
112
+ status: result.exitCode === 0 ? "succeeded" : "failed",
113
+ exitCode: result.exitCode,
114
+ stdout: result.stdout,
115
+ stderr: result.stderr,
116
+ }));
117
+ }
118
+ process.exit(result.exitCode === 0 ? 0 : 1);
119
+ }
@@ -0,0 +1 @@
1
+ export declare function run(args: string[]): Promise<void>;
@@ -0,0 +1,118 @@
1
+ import * as api from "../api-client.js";
2
+ import { ClaudeAdapter } from "../adapters/claude.js";
3
+ import { CodexAdapter } from "../adapters/codex.js";
4
+ function parseArgs(args) {
5
+ const parsed = {
6
+ provider: "claude",
7
+ dir: process.cwd(),
8
+ };
9
+ for (let i = 0; i < args.length; i++) {
10
+ if (args[i] === "--provider" && args[i + 1]) {
11
+ parsed.provider = args[++i];
12
+ }
13
+ else if (args[i] === "--dir" && args[i + 1]) {
14
+ parsed.dir = args[++i];
15
+ }
16
+ else if (args[i] === "--server" && args[i + 1]) {
17
+ parsed.server = args[++i];
18
+ }
19
+ }
20
+ return parsed;
21
+ }
22
+ function getAdapter(provider) {
23
+ switch (provider) {
24
+ case "codex":
25
+ return new CodexAdapter();
26
+ case "claude":
27
+ default:
28
+ return new ClaudeAdapter();
29
+ }
30
+ }
31
+ function timestamp() {
32
+ return new Date().toISOString().slice(11, 19);
33
+ }
34
+ export async function run(args) {
35
+ const parsed = parseArgs(args);
36
+ if (parsed.server) {
37
+ process.env.CO_ODE_SERVER = parsed.server;
38
+ }
39
+ // Register a relay
40
+ console.error(`[co-od] Registering relay...`);
41
+ const reg = await api.post("/api/relay/register", {
42
+ provider: parsed.provider,
43
+ });
44
+ console.log(`\nRelay code: ${reg.code}`);
45
+ console.log(`Share this with teammates to let them dispatch tasks to your machine.\n`);
46
+ console.error(`[${timestamp()}] Relay active. Expires at ${new Date(reg.expiresAt).toLocaleTimeString()}`);
47
+ console.error(`[${timestamp()}] Waiting for jobs... (Ctrl+C to stop)\n`);
48
+ const adapter = getAdapter(parsed.provider);
49
+ // Handle graceful shutdown
50
+ let stopping = false;
51
+ let activeJobs = 0;
52
+ process.on("SIGINT", () => {
53
+ if (stopping)
54
+ process.exit(1);
55
+ stopping = true;
56
+ console.error(`\n[${timestamp()}] Stopping relay... (Ctrl+C again to force)`);
57
+ if (activeJobs === 0)
58
+ process.exit(0);
59
+ });
60
+ process.on("SIGTERM", () => {
61
+ stopping = true;
62
+ process.exit(0);
63
+ });
64
+ // Poll for jobs
65
+ while (!stopping) {
66
+ try {
67
+ const data = await api.post("/api/relay/poll", {
68
+ code: reg.code,
69
+ });
70
+ if (data.job) {
71
+ const job = data.job;
72
+ console.error(`[${timestamp()}] Job received: ${job.goal.slice(0, 100)}`);
73
+ const jobAdapter = job.provider ? getAdapter(job.provider) : adapter;
74
+ activeJobs++;
75
+ jobAdapter
76
+ .execute(job.goal, {
77
+ workDir: parsed.dir,
78
+ onOutput: (out) => process.stderr.write(out),
79
+ })
80
+ .then(async (result) => {
81
+ console.error(`\n[${timestamp()}] Job ${job.jobId} ${result.exitCode === 0 ? "succeeded" : "failed"}`);
82
+ // Report completion
83
+ try {
84
+ await api.post("/api/relay/complete", {
85
+ code: reg.code,
86
+ jobId: job.jobId,
87
+ ok: result.exitCode === 0,
88
+ stdout: result.stdout.slice(-4096),
89
+ stderr: result.stderr.slice(-2048),
90
+ exitCode: result.exitCode,
91
+ });
92
+ }
93
+ catch {
94
+ console.error(`[${timestamp()}] Warning: failed to report job completion`);
95
+ }
96
+ })
97
+ .catch((err) => {
98
+ console.error(`[${timestamp()}] Job error: ${err}`);
99
+ })
100
+ .finally(() => {
101
+ activeJobs--;
102
+ if (stopping && activeJobs === 0)
103
+ process.exit(0);
104
+ });
105
+ }
106
+ }
107
+ catch (err) {
108
+ // Relay may have expired
109
+ const errMsg = String(err);
110
+ if (errMsg.includes("404") || errMsg.includes("410")) {
111
+ console.error(`[${timestamp()}] Relay expired. Exiting.`);
112
+ process.exit(0);
113
+ }
114
+ console.error(`[${timestamp()}] Poll error: ${err}`);
115
+ }
116
+ await new Promise((r) => setTimeout(r, 3000));
117
+ }
118
+ }
@@ -0,0 +1 @@
1
+ export declare function run(args: string[]): Promise<void>;