@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
package/SKILL.md
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: co-od
|
|
3
|
+
description: CLI for connecting local dev environments to co-ode rooms
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# co-od CLI
|
|
8
|
+
|
|
9
|
+
Run AI agents in co-ode rooms from the command line.
|
|
10
|
+
|
|
11
|
+
## Commands
|
|
12
|
+
|
|
13
|
+
- `co-od login` -- authenticate via browser (or `--token <t>` for CI)
|
|
14
|
+
- `co-od rooms` -- list rooms (supports `--json`)
|
|
15
|
+
- `co-od run <room> <goal>` -- execute a single task, fire-and-forget
|
|
16
|
+
- `co-od join <room>` -- join a room as an interactive agent (PTY + WebSocket)
|
|
17
|
+
- `co-od daemon <room>` -- autonomous watch mode, polls for events and executes
|
|
18
|
+
- `co-od share` -- generate a relay code for teammates
|
|
19
|
+
- `co-od connect <code>` -- connect to a shared relay
|
|
20
|
+
- `co-od status` -- show login state and session info
|
|
21
|
+
|
|
22
|
+
## Provider Support
|
|
23
|
+
|
|
24
|
+
Use `--provider claude|codex` on `run`, `daemon`, and `share` commands.
|
|
25
|
+
|
|
26
|
+
- `claude` (default) -- spawns `claude -p --dangerously-skip-permissions`
|
|
27
|
+
- `codex` -- spawns `codex exec --full-auto`
|
|
28
|
+
|
|
29
|
+
## Usage with Claude Code
|
|
30
|
+
|
|
31
|
+
Dispatch tasks to a room from within Claude Code hooks:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
co-od run room_abc123 "Fix the failing tests in src/utils" --json
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Environment Variables
|
|
38
|
+
|
|
39
|
+
- `CO_ODE_SERVER` -- override the server URL (default: https://co-ode.vercel.app)
|
|
40
|
+
|
|
41
|
+
## Session
|
|
42
|
+
|
|
43
|
+
Sessions are cached at `~/.co-ode/session.json` (valid for 23 hours).
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface AgentAdapter {
|
|
2
|
+
name: string;
|
|
3
|
+
available(): Promise<boolean>;
|
|
4
|
+
execute(goal: string, options: ExecuteOptions): Promise<ExecuteResult>;
|
|
5
|
+
}
|
|
6
|
+
export interface ExecuteOptions {
|
|
7
|
+
workDir: string;
|
|
8
|
+
onOutput?: (data: string) => void;
|
|
9
|
+
signal?: AbortSignal;
|
|
10
|
+
}
|
|
11
|
+
export interface ExecuteResult {
|
|
12
|
+
exitCode: number;
|
|
13
|
+
stdout: string;
|
|
14
|
+
stderr: string;
|
|
15
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
function which(cmd) {
|
|
4
|
+
const paths = (process.env.PATH || "").split(":");
|
|
5
|
+
return paths.some((dir) => existsSync(`${dir}/${cmd}`));
|
|
6
|
+
}
|
|
7
|
+
export class ClaudeAdapter {
|
|
8
|
+
name = "claude";
|
|
9
|
+
async available() {
|
|
10
|
+
return which("claude");
|
|
11
|
+
}
|
|
12
|
+
async execute(goal, options) {
|
|
13
|
+
const { workDir, onOutput, signal } = options;
|
|
14
|
+
return new Promise((resolve, reject) => {
|
|
15
|
+
const child = spawn("claude", ["-p", "--dangerously-skip-permissions", "--", goal], {
|
|
16
|
+
cwd: workDir,
|
|
17
|
+
env: { ...process.env },
|
|
18
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
19
|
+
});
|
|
20
|
+
let stdout = "";
|
|
21
|
+
let stderr = "";
|
|
22
|
+
child.stdout?.setEncoding("utf-8");
|
|
23
|
+
child.stderr?.setEncoding("utf-8");
|
|
24
|
+
child.stdout?.on("data", (chunk) => {
|
|
25
|
+
stdout += chunk;
|
|
26
|
+
onOutput?.(chunk);
|
|
27
|
+
});
|
|
28
|
+
child.stderr?.on("data", (chunk) => {
|
|
29
|
+
stderr += chunk;
|
|
30
|
+
onOutput?.(chunk);
|
|
31
|
+
});
|
|
32
|
+
child.on("exit", (code) => {
|
|
33
|
+
resolve({ exitCode: code ?? 1, stdout, stderr });
|
|
34
|
+
});
|
|
35
|
+
child.on("error", (err) => {
|
|
36
|
+
reject(err);
|
|
37
|
+
});
|
|
38
|
+
if (signal) {
|
|
39
|
+
signal.addEventListener("abort", () => {
|
|
40
|
+
child.kill("SIGTERM");
|
|
41
|
+
}, { once: true });
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
function which(cmd) {
|
|
4
|
+
const paths = (process.env.PATH || "").split(":");
|
|
5
|
+
return paths.some((dir) => existsSync(`${dir}/${cmd}`));
|
|
6
|
+
}
|
|
7
|
+
export class CodexAdapter {
|
|
8
|
+
name = "codex";
|
|
9
|
+
async available() {
|
|
10
|
+
return which("codex");
|
|
11
|
+
}
|
|
12
|
+
async execute(goal, options) {
|
|
13
|
+
const { workDir, onOutput, signal } = options;
|
|
14
|
+
return new Promise((resolve, reject) => {
|
|
15
|
+
const child = spawn("codex", ["exec", "--full-auto", "--", goal], {
|
|
16
|
+
cwd: workDir,
|
|
17
|
+
env: { ...process.env },
|
|
18
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
19
|
+
});
|
|
20
|
+
let stdout = "";
|
|
21
|
+
let stderr = "";
|
|
22
|
+
child.stdout?.setEncoding("utf-8");
|
|
23
|
+
child.stderr?.setEncoding("utf-8");
|
|
24
|
+
child.stdout?.on("data", (chunk) => {
|
|
25
|
+
stdout += chunk;
|
|
26
|
+
onOutput?.(chunk);
|
|
27
|
+
});
|
|
28
|
+
child.stderr?.on("data", (chunk) => {
|
|
29
|
+
stderr += chunk;
|
|
30
|
+
onOutput?.(chunk);
|
|
31
|
+
});
|
|
32
|
+
child.on("exit", (code) => {
|
|
33
|
+
resolve({ exitCode: code ?? 1, stdout, stderr });
|
|
34
|
+
});
|
|
35
|
+
child.on("error", (err) => {
|
|
36
|
+
reject(err);
|
|
37
|
+
});
|
|
38
|
+
if (signal) {
|
|
39
|
+
signal.addEventListener("abort", () => {
|
|
40
|
+
child.kill("SIGTERM");
|
|
41
|
+
}, { once: true });
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
interface CachedSession {
|
|
2
|
+
sessionToken: string;
|
|
3
|
+
expiresAt: number;
|
|
4
|
+
user?: string;
|
|
5
|
+
}
|
|
6
|
+
declare function getBaseUrl(): string;
|
|
7
|
+
declare function getSessionToken(): string | null;
|
|
8
|
+
export declare function getSession(): CachedSession | null;
|
|
9
|
+
declare function requireToken(): string;
|
|
10
|
+
export declare function get<T = unknown>(path: string): Promise<T>;
|
|
11
|
+
export declare function post<T = unknown>(path: string, body?: unknown): Promise<T>;
|
|
12
|
+
export declare function poll<T = unknown>(path: string, intervalMs: number, condition: (data: T) => boolean, signal?: AbortSignal): Promise<T>;
|
|
13
|
+
export { getBaseUrl, getSessionToken, requireToken };
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { readFileSync } 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 getBaseUrl() {
|
|
7
|
+
return (process.env.CO_ODE_SERVER || "https://co-ode.vercel.app");
|
|
8
|
+
}
|
|
9
|
+
function getSessionToken() {
|
|
10
|
+
try {
|
|
11
|
+
const data = JSON.parse(readFileSync(SESSION_FILE, "utf-8"));
|
|
12
|
+
if (data.expiresAt > Date.now()) {
|
|
13
|
+
return data.sessionToken;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
// No session
|
|
18
|
+
}
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
export function getSession() {
|
|
22
|
+
try {
|
|
23
|
+
const data = JSON.parse(readFileSync(SESSION_FILE, "utf-8"));
|
|
24
|
+
if (data.expiresAt > Date.now()) {
|
|
25
|
+
return data;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
// No session
|
|
30
|
+
}
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
function requireToken() {
|
|
34
|
+
const token = getSessionToken();
|
|
35
|
+
if (!token) {
|
|
36
|
+
console.error("[co-od] Not logged in. Run `co-od login` first.");
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
39
|
+
return token;
|
|
40
|
+
}
|
|
41
|
+
function headers(token) {
|
|
42
|
+
return {
|
|
43
|
+
"content-type": "application/json",
|
|
44
|
+
authorization: `Bearer ${token}`,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
export async function get(path) {
|
|
48
|
+
const token = requireToken();
|
|
49
|
+
const url = `${getBaseUrl()}${path}`;
|
|
50
|
+
const res = await fetch(url, { headers: headers(token) });
|
|
51
|
+
if (!res.ok) {
|
|
52
|
+
throw new Error(`GET ${path} failed: ${res.status} ${res.statusText}`);
|
|
53
|
+
}
|
|
54
|
+
return res.json();
|
|
55
|
+
}
|
|
56
|
+
export async function post(path, body) {
|
|
57
|
+
const token = requireToken();
|
|
58
|
+
const url = `${getBaseUrl()}${path}`;
|
|
59
|
+
const res = await fetch(url, {
|
|
60
|
+
method: "POST",
|
|
61
|
+
headers: headers(token),
|
|
62
|
+
body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
63
|
+
});
|
|
64
|
+
if (!res.ok) {
|
|
65
|
+
throw new Error(`POST ${path} failed: ${res.status} ${res.statusText}`);
|
|
66
|
+
}
|
|
67
|
+
return res.json();
|
|
68
|
+
}
|
|
69
|
+
export async function poll(path, intervalMs, condition, signal) {
|
|
70
|
+
while (true) {
|
|
71
|
+
if (signal?.aborted) {
|
|
72
|
+
throw new Error("Polling aborted");
|
|
73
|
+
}
|
|
74
|
+
const data = await get(path);
|
|
75
|
+
if (condition(data)) {
|
|
76
|
+
return data;
|
|
77
|
+
}
|
|
78
|
+
await new Promise((resolve, reject) => {
|
|
79
|
+
const timer = setTimeout(resolve, intervalMs);
|
|
80
|
+
if (signal) {
|
|
81
|
+
signal.addEventListener("abort", () => {
|
|
82
|
+
clearTimeout(timer);
|
|
83
|
+
reject(new Error("Polling aborted"));
|
|
84
|
+
}, { once: true });
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
export { getBaseUrl, getSessionToken, requireToken };
|
package/dist/auth.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
interface AuthResult {
|
|
2
|
+
sessionToken: string;
|
|
3
|
+
realtimeToken: string;
|
|
4
|
+
terminalWsUrl: string;
|
|
5
|
+
}
|
|
6
|
+
export declare function authenticate({ serverUrl, roomId, inviteToken, }: {
|
|
7
|
+
serverUrl: string;
|
|
8
|
+
roomId: string;
|
|
9
|
+
inviteToken?: string;
|
|
10
|
+
}): Promise<AuthResult>;
|
|
11
|
+
export {};
|
package/dist/auth.js
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { readFileSync, 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 loadCachedSession() {
|
|
7
|
+
try {
|
|
8
|
+
const data = JSON.parse(readFileSync(SESSION_FILE, "utf-8"));
|
|
9
|
+
if (data.expiresAt > Date.now()) {
|
|
10
|
+
return data;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
// No cached session
|
|
15
|
+
}
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
function saveCachedSession(session) {
|
|
19
|
+
try {
|
|
20
|
+
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
21
|
+
writeFileSync(SESSION_FILE, JSON.stringify(session, null, 2));
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
// Non-fatal
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
async function deviceAuthFlow(serverUrl) {
|
|
28
|
+
// Step 1: Request a device code
|
|
29
|
+
const authorizeRes = await fetch(`${serverUrl}/api/auth/cli/authorize`, {
|
|
30
|
+
method: "POST",
|
|
31
|
+
headers: { "content-type": "application/json" },
|
|
32
|
+
body: JSON.stringify({}),
|
|
33
|
+
});
|
|
34
|
+
if (!authorizeRes.ok) {
|
|
35
|
+
throw new Error(`Failed to start auth flow: ${authorizeRes.status}`);
|
|
36
|
+
}
|
|
37
|
+
const { code, verificationUrl, expiresAt } = (await authorizeRes.json());
|
|
38
|
+
// Step 2: Open browser for approval
|
|
39
|
+
console.error(`\n[co-ode] To authenticate, open this URL in your browser:\n`);
|
|
40
|
+
console.error(` ${verificationUrl}\n`);
|
|
41
|
+
console.error(`[co-ode] Your code: ${code}\n`);
|
|
42
|
+
try {
|
|
43
|
+
const open = (await import("open")).default;
|
|
44
|
+
await open(verificationUrl);
|
|
45
|
+
console.error(`[co-ode] Browser opened. Waiting for approval...`);
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
console.error(`[co-ode] Could not open browser automatically. Please open the URL manually.`);
|
|
49
|
+
}
|
|
50
|
+
// Step 3: Poll for token
|
|
51
|
+
const pollInterval = 2000;
|
|
52
|
+
const deadline = expiresAt;
|
|
53
|
+
while (Date.now() < deadline) {
|
|
54
|
+
await sleep(pollInterval);
|
|
55
|
+
const tokenRes = await fetch(`${serverUrl}/api/auth/cli/token`, {
|
|
56
|
+
method: "POST",
|
|
57
|
+
headers: { "content-type": "application/json" },
|
|
58
|
+
body: JSON.stringify({ code }),
|
|
59
|
+
});
|
|
60
|
+
if (tokenRes.status === 410) {
|
|
61
|
+
throw new Error("Auth code expired. Please try again.");
|
|
62
|
+
}
|
|
63
|
+
if (tokenRes.status === 202) {
|
|
64
|
+
// Still pending
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
if (tokenRes.ok) {
|
|
68
|
+
const { token } = (await tokenRes.json());
|
|
69
|
+
if (token) {
|
|
70
|
+
// Cache for 23 hours (tokens last 24h)
|
|
71
|
+
saveCachedSession({
|
|
72
|
+
sessionToken: token,
|
|
73
|
+
expiresAt: Date.now() + 23 * 60 * 60 * 1000,
|
|
74
|
+
});
|
|
75
|
+
return token;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
throw new Error(`Unexpected auth response: ${tokenRes.status}`);
|
|
79
|
+
}
|
|
80
|
+
throw new Error("Auth timed out. Please try again.");
|
|
81
|
+
}
|
|
82
|
+
export async function authenticate({ serverUrl, roomId, inviteToken, }) {
|
|
83
|
+
// Try cached session first
|
|
84
|
+
let sessionToken;
|
|
85
|
+
const cached = loadCachedSession();
|
|
86
|
+
if (cached) {
|
|
87
|
+
console.error(`[co-ode] Using cached session`);
|
|
88
|
+
sessionToken = cached.sessionToken;
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
// If invite token provided, redeem it first
|
|
92
|
+
if (inviteToken) {
|
|
93
|
+
console.error(`[co-ode] Redeeming invite token...`);
|
|
94
|
+
const inviteRes = await fetch(`${serverUrl}/api/invites`, {
|
|
95
|
+
method: "POST",
|
|
96
|
+
headers: { "content-type": "application/json" },
|
|
97
|
+
body: JSON.stringify({ token: inviteToken }),
|
|
98
|
+
});
|
|
99
|
+
if (!inviteRes.ok) {
|
|
100
|
+
console.error(`[co-ode] Warning: invite redemption failed (${inviteRes.status}), continuing with auth...`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
sessionToken = await deviceAuthFlow(serverUrl);
|
|
104
|
+
}
|
|
105
|
+
// Get realtime session for the room
|
|
106
|
+
console.error(`[co-ode] Getting realtime session for room ${roomId}...`);
|
|
107
|
+
const realtimeRes = await fetch(`${serverUrl}/api/rooms/${roomId}/realtime/session`, {
|
|
108
|
+
method: "POST",
|
|
109
|
+
headers: {
|
|
110
|
+
"content-type": "application/json",
|
|
111
|
+
authorization: `Bearer ${sessionToken}`,
|
|
112
|
+
},
|
|
113
|
+
body: JSON.stringify({ ttlSeconds: 3600 }),
|
|
114
|
+
});
|
|
115
|
+
if (!realtimeRes.ok) {
|
|
116
|
+
// Cached session may be invalid, clear it and retry
|
|
117
|
+
if (cached) {
|
|
118
|
+
console.error(`[co-ode] Cached session invalid, re-authenticating...`);
|
|
119
|
+
try {
|
|
120
|
+
const { unlinkSync } = await import("node:fs");
|
|
121
|
+
unlinkSync(SESSION_FILE);
|
|
122
|
+
}
|
|
123
|
+
catch { /* ignore */ }
|
|
124
|
+
sessionToken = await deviceAuthFlow(serverUrl);
|
|
125
|
+
const retryRes = await fetch(`${serverUrl}/api/rooms/${roomId}/realtime/session`, {
|
|
126
|
+
method: "POST",
|
|
127
|
+
headers: {
|
|
128
|
+
"content-type": "application/json",
|
|
129
|
+
authorization: `Bearer ${sessionToken}`,
|
|
130
|
+
},
|
|
131
|
+
body: JSON.stringify({ ttlSeconds: 3600 }),
|
|
132
|
+
});
|
|
133
|
+
if (!retryRes.ok) {
|
|
134
|
+
throw new Error(`Failed to get realtime session: ${retryRes.status}`);
|
|
135
|
+
}
|
|
136
|
+
const data = (await retryRes.json());
|
|
137
|
+
return {
|
|
138
|
+
sessionToken,
|
|
139
|
+
realtimeToken: data.token,
|
|
140
|
+
terminalWsUrl: data.terminalWsUrl,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
throw new Error(`Failed to get realtime session: ${realtimeRes.status}`);
|
|
144
|
+
}
|
|
145
|
+
const realtimeData = (await realtimeRes.json());
|
|
146
|
+
return {
|
|
147
|
+
sessionToken,
|
|
148
|
+
realtimeToken: realtimeData.token,
|
|
149
|
+
terminalWsUrl: realtimeData.terminalWsUrl,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
function sleep(ms) {
|
|
153
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
154
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function run(args: string[]): Promise<void>;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import * as api from "../api-client.js";
|
|
2
|
+
function parseArgs(args) {
|
|
3
|
+
const parsed = {
|
|
4
|
+
provider: "claude",
|
|
5
|
+
dir: process.cwd(),
|
|
6
|
+
};
|
|
7
|
+
for (let i = 0; i < args.length; i++) {
|
|
8
|
+
if (args[i] === "--provider" && args[i + 1]) {
|
|
9
|
+
parsed.provider = args[++i];
|
|
10
|
+
}
|
|
11
|
+
else if (args[i] === "--dir" && args[i + 1]) {
|
|
12
|
+
parsed.dir = args[++i];
|
|
13
|
+
}
|
|
14
|
+
else if (args[i] === "--server" && args[i + 1]) {
|
|
15
|
+
parsed.server = args[++i];
|
|
16
|
+
}
|
|
17
|
+
else if (!args[i].startsWith("--")) {
|
|
18
|
+
parsed.code = args[i];
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return parsed;
|
|
22
|
+
}
|
|
23
|
+
export async function run(args) {
|
|
24
|
+
const parsed = parseArgs(args);
|
|
25
|
+
if (!parsed.code) {
|
|
26
|
+
console.error("Usage: co-od connect <relay-code>");
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
if (parsed.server) {
|
|
30
|
+
process.env.CO_ODE_SERVER = parsed.server;
|
|
31
|
+
}
|
|
32
|
+
console.error(`[co-od] Connecting to relay ${parsed.code}...`);
|
|
33
|
+
const res = await api.post("/api/relay/join", {
|
|
34
|
+
code: parsed.code,
|
|
35
|
+
});
|
|
36
|
+
if (!res.ok) {
|
|
37
|
+
console.error(`[co-od] Failed to join relay. Code may be invalid or expired.`);
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
40
|
+
console.log(`Connected to relay ${parsed.code}`);
|
|
41
|
+
console.log(`You can now dispatch tasks to the shared machine.`);
|
|
42
|
+
console.log(`\nRelay ID: ${res.relayId}`);
|
|
43
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function run(args: string[]): Promise<void>;
|