@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.
- package/SKILL.md +43 -0
- package/dist/adapters/base.d.ts +15 -0
- package/dist/adapters/base.js +1 -0
- package/dist/adapters/claude.d.ts +6 -0
- package/dist/adapters/claude.js +45 -0
- package/dist/adapters/codex.d.ts +6 -0
- package/dist/adapters/codex.js +45 -0
- package/dist/api-client.d.ts +13 -0
- package/dist/api-client.js +89 -0
- package/dist/auth.d.ts +11 -0
- package/dist/auth.js +154 -0
- package/dist/commands/connect.d.ts +1 -0
- package/dist/commands/connect.js +43 -0
- package/dist/commands/daemon.d.ts +1 -0
- package/dist/commands/daemon.js +202 -0
- package/dist/commands/join.d.ts +1 -0
- package/dist/commands/join.js +49 -0
- package/dist/commands/login.d.ts +1 -0
- package/dist/commands/login.js +93 -0
- package/dist/commands/rooms.d.ts +1 -0
- package/dist/commands/rooms.js +57 -0
- package/dist/commands/run.d.ts +1 -0
- package/dist/commands/run.js +119 -0
- package/dist/commands/share.d.ts +1 -0
- package/dist/commands/share.js +118 -0
- package/dist/commands/status.d.ts +1 -0
- package/dist/commands/status.js +38 -0
- package/dist/connect.d.ts +10 -0
- package/dist/connect.js +145 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +110 -0
- package/dist/promptRunner.d.ts +17 -0
- package/dist/promptRunner.js +172 -0
- package/dist/pty.d.ts +13 -0
- package/dist/pty.js +52 -0
- package/package.json +52 -0
|
@@ -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>;
|