@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,38 @@
|
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { getSession, getBaseUrl } from "../api-client.js";
|
|
4
|
+
const CONFIG_DIR = join(homedir(), ".co-ode");
|
|
5
|
+
const SESSION_FILE = join(CONFIG_DIR, "session.json");
|
|
6
|
+
function parseArgs(args) {
|
|
7
|
+
return { json: args.includes("--json") };
|
|
8
|
+
}
|
|
9
|
+
function getStatusInfo() {
|
|
10
|
+
const session = getSession();
|
|
11
|
+
const server = getBaseUrl();
|
|
12
|
+
return {
|
|
13
|
+
loggedIn: session !== null,
|
|
14
|
+
user: session?.user || null,
|
|
15
|
+
sessionFile: SESSION_FILE,
|
|
16
|
+
sessionExpires: session
|
|
17
|
+
? new Date(session.expiresAt).toLocaleString()
|
|
18
|
+
: null,
|
|
19
|
+
server,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
export async function run(args) {
|
|
23
|
+
const parsed = parseArgs(args);
|
|
24
|
+
const info = getStatusInfo();
|
|
25
|
+
if (parsed.json) {
|
|
26
|
+
console.log(JSON.stringify(info, null, 2));
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
console.log(`co-od status`);
|
|
30
|
+
console.log(`${"=".repeat(40)}`);
|
|
31
|
+
console.log(`Logged in: ${info.loggedIn ? "yes" : "no"}`);
|
|
32
|
+
if (info.loggedIn) {
|
|
33
|
+
console.log(`User: ${info.user || "(unknown)"}`);
|
|
34
|
+
console.log(`Session expires: ${info.sessionExpires}`);
|
|
35
|
+
}
|
|
36
|
+
console.log(`Session file: ${info.sessionFile}`);
|
|
37
|
+
console.log(`Server: ${info.server}`);
|
|
38
|
+
}
|
package/dist/connect.js
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import WebSocket from "ws";
|
|
2
|
+
import { createLocalPty } from "./pty.js";
|
|
3
|
+
import { runPrompt, isRunning } from "./promptRunner.js";
|
|
4
|
+
export async function connectToRoom(options) {
|
|
5
|
+
const { terminalWsUrl, realtimeToken, roomId, workDir, serverUrl, sessionToken } = options;
|
|
6
|
+
// Build the PTY provider WebSocket URL
|
|
7
|
+
// terminalWsUrl is like ws://host:port — append the pty-provider path
|
|
8
|
+
const baseUrl = terminalWsUrl.replace(/\/$/, "");
|
|
9
|
+
const wsUrl = `${baseUrl}/pty-provider?roomId=${encodeURIComponent(roomId)}&token=${encodeURIComponent(realtimeToken)}`;
|
|
10
|
+
console.error(`[co-ode] Connecting to terminal gateway...`);
|
|
11
|
+
const ws = new WebSocket(wsUrl);
|
|
12
|
+
const pty = createLocalPty(workDir);
|
|
13
|
+
let reconnecting = false;
|
|
14
|
+
let tokenRefreshTimer = null;
|
|
15
|
+
// Wire PTY output → WebSocket
|
|
16
|
+
pty.onData((data) => {
|
|
17
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
18
|
+
ws.send(JSON.stringify({ type: "output", data }));
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
ws.on("open", () => {
|
|
22
|
+
console.error(`[co-ode] Connected! Terminal is now shared with the room.`);
|
|
23
|
+
console.error(`[co-ode] Room members can see your terminal and send Claude Code prompts.`);
|
|
24
|
+
console.error(`[co-ode] Press Ctrl+C to disconnect.\n`);
|
|
25
|
+
// Send initial connection info
|
|
26
|
+
ws.send(JSON.stringify({
|
|
27
|
+
type: "connected",
|
|
28
|
+
cwd: workDir,
|
|
29
|
+
data: `[co-ode] Remote CLI connected from ${workDir}\r\n\r\n`,
|
|
30
|
+
}));
|
|
31
|
+
// Schedule token refresh (45 minutes — tokens last 1 hour)
|
|
32
|
+
tokenRefreshTimer = setTimeout(() => refreshToken(options), 45 * 60 * 1000);
|
|
33
|
+
});
|
|
34
|
+
ws.on("message", (raw) => {
|
|
35
|
+
try {
|
|
36
|
+
const msg = JSON.parse(raw.toString());
|
|
37
|
+
switch (msg.type) {
|
|
38
|
+
case "input":
|
|
39
|
+
// Browser client typed something → forward to local PTY
|
|
40
|
+
if (msg.data) {
|
|
41
|
+
pty.write(msg.data);
|
|
42
|
+
}
|
|
43
|
+
break;
|
|
44
|
+
case "resize":
|
|
45
|
+
if (msg.cols && msg.rows) {
|
|
46
|
+
pty.resize(msg.cols, msg.rows);
|
|
47
|
+
}
|
|
48
|
+
break;
|
|
49
|
+
case "ping":
|
|
50
|
+
ws.send(JSON.stringify({ type: "pong" }));
|
|
51
|
+
break;
|
|
52
|
+
case "prompt.dispatch":
|
|
53
|
+
// Room member sent a Claude Code prompt
|
|
54
|
+
if (isRunning()) {
|
|
55
|
+
ws.send(JSON.stringify({ type: "prompt.busy", runId: msg.runId }));
|
|
56
|
+
break;
|
|
57
|
+
}
|
|
58
|
+
ws.send(JSON.stringify({ type: "prompt.ack", runId: msg.runId }));
|
|
59
|
+
void runPrompt({
|
|
60
|
+
runId: msg.runId,
|
|
61
|
+
goal: msg.goal,
|
|
62
|
+
token: msg.token,
|
|
63
|
+
completeUrl: msg.completeUrl,
|
|
64
|
+
heartbeatUrl: msg.heartbeatUrl,
|
|
65
|
+
heartbeatIntervalMs: msg.heartbeatIntervalMs || 10000,
|
|
66
|
+
}, {
|
|
67
|
+
workDir,
|
|
68
|
+
roomId,
|
|
69
|
+
serverUrl,
|
|
70
|
+
onOutput: (data) => {
|
|
71
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
72
|
+
ws.send(JSON.stringify({ type: "output", data }));
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
}).then(() => {
|
|
76
|
+
ws.send(JSON.stringify({
|
|
77
|
+
type: "prompt.complete",
|
|
78
|
+
runId: msg.runId,
|
|
79
|
+
status: "done",
|
|
80
|
+
}));
|
|
81
|
+
});
|
|
82
|
+
break;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
// Ignore malformed messages
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
ws.on("close", () => {
|
|
90
|
+
if (!reconnecting) {
|
|
91
|
+
console.error(`\n[co-ode] Disconnected from room.`);
|
|
92
|
+
cleanup();
|
|
93
|
+
process.exit(0);
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
ws.on("error", (err) => {
|
|
97
|
+
console.error(`[co-ode] WebSocket error: ${err.message}`);
|
|
98
|
+
});
|
|
99
|
+
// Handle Ctrl+C gracefully
|
|
100
|
+
const cleanup = () => {
|
|
101
|
+
if (tokenRefreshTimer)
|
|
102
|
+
clearTimeout(tokenRefreshTimer);
|
|
103
|
+
pty.kill();
|
|
104
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
105
|
+
ws.close();
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
process.on("SIGINT", () => {
|
|
109
|
+
console.error(`\n[co-ode] Disconnecting...`);
|
|
110
|
+
cleanup();
|
|
111
|
+
process.exit(0);
|
|
112
|
+
});
|
|
113
|
+
process.on("SIGTERM", () => {
|
|
114
|
+
cleanup();
|
|
115
|
+
process.exit(0);
|
|
116
|
+
});
|
|
117
|
+
// Keep the process alive
|
|
118
|
+
await new Promise(() => {
|
|
119
|
+
// Never resolves — process stays alive until SIGINT/SIGTERM or ws close
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
async function refreshToken(options) {
|
|
123
|
+
try {
|
|
124
|
+
console.error(`[co-ode] Refreshing realtime token...`);
|
|
125
|
+
const res = await fetch(`${options.serverUrl}/api/rooms/${options.roomId}/realtime/session`, {
|
|
126
|
+
method: "POST",
|
|
127
|
+
headers: {
|
|
128
|
+
"content-type": "application/json",
|
|
129
|
+
authorization: `Bearer ${options.sessionToken}`,
|
|
130
|
+
},
|
|
131
|
+
body: JSON.stringify({ ttlSeconds: 3600 }),
|
|
132
|
+
});
|
|
133
|
+
if (res.ok) {
|
|
134
|
+
const data = (await res.json());
|
|
135
|
+
options.realtimeToken = data.token;
|
|
136
|
+
console.error(`[co-ode] Token refreshed`);
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
console.error(`[co-ode] Warning: token refresh failed (${res.status})`);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
catch (err) {
|
|
143
|
+
console.error(`[co-ode] Warning: token refresh error: ${err}`);
|
|
144
|
+
}
|
|
145
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
declare const VERSION = "0.1.0";
|
|
3
|
+
declare const COMMANDS: Record<string, {
|
|
4
|
+
desc: string;
|
|
5
|
+
usage: string;
|
|
6
|
+
}>;
|
|
7
|
+
declare function printHelp(): void;
|
|
8
|
+
declare function getArg(args: string[], flag: string): string | undefined;
|
|
9
|
+
declare function main(): Promise<void>;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
const VERSION = "0.1.0";
|
|
4
|
+
const COMMANDS = {
|
|
5
|
+
login: { desc: "Authenticate with co-ode", usage: "co-od login [--token <t>]" },
|
|
6
|
+
rooms: { desc: "List your rooms", usage: "co-od rooms [--json]" },
|
|
7
|
+
run: { desc: "Execute a single task in a room", usage: "co-od run <room> <goal> [--provider claude|codex] [--dir <path>] [--json]" },
|
|
8
|
+
join: { desc: "Join a room as an interactive agent", usage: "co-od join <room> [--invite-token <tok>] [--dir <path>]" },
|
|
9
|
+
daemon: { desc: "Autonomous watch mode for a room", usage: "co-od daemon <room> [--as <name>] [--auto-execute] [--watch <events>]" },
|
|
10
|
+
share: { desc: "Generate a relay code for teammates", usage: "co-od share [--provider claude|codex]" },
|
|
11
|
+
connect: { desc: "Connect to a relay via code", usage: "co-od connect <code>" },
|
|
12
|
+
status: { desc: "Show current login and running state", usage: "co-od status [--json]" },
|
|
13
|
+
};
|
|
14
|
+
function printHelp() {
|
|
15
|
+
console.error(`co-od v${VERSION} — CLI for co-ode rooms\n`);
|
|
16
|
+
console.error("Usage: co-od <command> [options]\n");
|
|
17
|
+
console.error("Commands:");
|
|
18
|
+
const maxLen = Math.max(...Object.keys(COMMANDS).map((k) => k.length));
|
|
19
|
+
for (const [name, { desc }] of Object.entries(COMMANDS)) {
|
|
20
|
+
console.error(` ${name.padEnd(maxLen + 2)}${desc}`);
|
|
21
|
+
}
|
|
22
|
+
console.error(`\nRun 'co-od <command> --help' for command-specific usage.`);
|
|
23
|
+
console.error(`\nOptions:`);
|
|
24
|
+
console.error(` --help, -h Show this help message`);
|
|
25
|
+
console.error(` --version, -v Show version`);
|
|
26
|
+
}
|
|
27
|
+
function getArg(args, flag) {
|
|
28
|
+
const idx = args.indexOf(flag);
|
|
29
|
+
if (idx === -1 || idx + 1 >= args.length)
|
|
30
|
+
return undefined;
|
|
31
|
+
return args[idx + 1];
|
|
32
|
+
}
|
|
33
|
+
async function main() {
|
|
34
|
+
const args = process.argv.slice(2);
|
|
35
|
+
// Global flags
|
|
36
|
+
if (args.includes("--version") || args.includes("-v")) {
|
|
37
|
+
console.log(VERSION);
|
|
38
|
+
process.exit(0);
|
|
39
|
+
}
|
|
40
|
+
if (args.includes("--help") || args.includes("-h") || args.length === 0) {
|
|
41
|
+
printHelp();
|
|
42
|
+
process.exit(0);
|
|
43
|
+
}
|
|
44
|
+
const command = args[0];
|
|
45
|
+
const commandArgs = args.slice(1);
|
|
46
|
+
// Show command-specific help
|
|
47
|
+
if (commandArgs.includes("--help") || commandArgs.includes("-h")) {
|
|
48
|
+
const cmd = COMMANDS[command];
|
|
49
|
+
if (cmd) {
|
|
50
|
+
console.error(`${cmd.desc}\n`);
|
|
51
|
+
console.error(`Usage: ${cmd.usage}`);
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
console.error(`Unknown command: ${command}`);
|
|
55
|
+
printHelp();
|
|
56
|
+
}
|
|
57
|
+
process.exit(0);
|
|
58
|
+
}
|
|
59
|
+
// Route to command
|
|
60
|
+
switch (command) {
|
|
61
|
+
case "login": {
|
|
62
|
+
const { run } = await import("./commands/login.js");
|
|
63
|
+
await run(commandArgs);
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
case "rooms": {
|
|
67
|
+
const { run } = await import("./commands/rooms.js");
|
|
68
|
+
await run(commandArgs);
|
|
69
|
+
break;
|
|
70
|
+
}
|
|
71
|
+
case "run": {
|
|
72
|
+
const { run } = await import("./commands/run.js");
|
|
73
|
+
await run(commandArgs);
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
case "join": {
|
|
77
|
+
const { run } = await import("./commands/join.js");
|
|
78
|
+
await run(commandArgs);
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
case "daemon": {
|
|
82
|
+
const { run } = await import("./commands/daemon.js");
|
|
83
|
+
await run(commandArgs);
|
|
84
|
+
break;
|
|
85
|
+
}
|
|
86
|
+
case "share": {
|
|
87
|
+
const { run } = await import("./commands/share.js");
|
|
88
|
+
await run(commandArgs);
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
91
|
+
case "connect": {
|
|
92
|
+
const { run } = await import("./commands/connect.js");
|
|
93
|
+
await run(commandArgs);
|
|
94
|
+
break;
|
|
95
|
+
}
|
|
96
|
+
case "status": {
|
|
97
|
+
const { run } = await import("./commands/status.js");
|
|
98
|
+
await run(commandArgs);
|
|
99
|
+
break;
|
|
100
|
+
}
|
|
101
|
+
default:
|
|
102
|
+
console.error(`Unknown command: ${command}\n`);
|
|
103
|
+
printHelp();
|
|
104
|
+
process.exit(1);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
main().catch((err) => {
|
|
108
|
+
console.error(`[co-od] Fatal: ${err.message || err}`);
|
|
109
|
+
process.exit(1);
|
|
110
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
interface PromptDispatch {
|
|
2
|
+
runId: string;
|
|
3
|
+
goal: string;
|
|
4
|
+
token: string;
|
|
5
|
+
completeUrl: string;
|
|
6
|
+
heartbeatUrl: string;
|
|
7
|
+
heartbeatIntervalMs: number;
|
|
8
|
+
}
|
|
9
|
+
interface PromptRunnerOptions {
|
|
10
|
+
workDir: string;
|
|
11
|
+
onOutput: (data: string) => void;
|
|
12
|
+
roomId?: string;
|
|
13
|
+
serverUrl?: string;
|
|
14
|
+
}
|
|
15
|
+
export declare function isRunning(): boolean;
|
|
16
|
+
export declare function runPrompt(dispatch: PromptDispatch, options: PromptRunnerOptions): Promise<void>;
|
|
17
|
+
export {};
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { writeFileSync, mkdirSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
let currentRun = null;
|
|
6
|
+
export function isRunning() {
|
|
7
|
+
return currentRun !== null;
|
|
8
|
+
}
|
|
9
|
+
export async function runPrompt(dispatch, options) {
|
|
10
|
+
if (currentRun) {
|
|
11
|
+
throw new Error("A prompt is already running");
|
|
12
|
+
}
|
|
13
|
+
const { runId, goal, token, completeUrl, heartbeatUrl, heartbeatIntervalMs } = dispatch;
|
|
14
|
+
const { workDir, onOutput } = options;
|
|
15
|
+
onOutput(`\r\n[co-ode] Received prompt: ${goal}\r\n`);
|
|
16
|
+
onOutput(`[co-ode] Running Claude Code...\r\n\r\n`);
|
|
17
|
+
// Start heartbeat loop
|
|
18
|
+
const heartbeatTimer = setInterval(async () => {
|
|
19
|
+
try {
|
|
20
|
+
const res = await fetch(heartbeatUrl, {
|
|
21
|
+
method: "POST",
|
|
22
|
+
headers: {
|
|
23
|
+
"content-type": "application/json",
|
|
24
|
+
authorization: `Bearer ${token}`,
|
|
25
|
+
"x-local-bridge-token": token,
|
|
26
|
+
},
|
|
27
|
+
body: JSON.stringify({ runId }),
|
|
28
|
+
});
|
|
29
|
+
if (res.ok) {
|
|
30
|
+
const body = (await res.json());
|
|
31
|
+
if (body.continue === false) {
|
|
32
|
+
onOutput(`\r\n[co-ode] Server requested cancellation\r\n`);
|
|
33
|
+
cancelCurrentRun();
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
// Heartbeat failure is non-fatal
|
|
39
|
+
}
|
|
40
|
+
}, heartbeatIntervalMs || 10000);
|
|
41
|
+
// Write MCP config so Claude Code discovers co-od tools natively
|
|
42
|
+
const apiOrigin = completeUrl ? new URL(completeUrl).origin : (options.serverUrl || "");
|
|
43
|
+
const mcpConfigDir = join(tmpdir(), `co-ode-mcp-${runId}`);
|
|
44
|
+
const mcpConfigPath = join(mcpConfigDir, "mcp.json");
|
|
45
|
+
try {
|
|
46
|
+
mkdirSync(mcpConfigDir, { recursive: true });
|
|
47
|
+
writeFileSync(mcpConfigPath, JSON.stringify({
|
|
48
|
+
mcpServers: {
|
|
49
|
+
"co-ode": {
|
|
50
|
+
command: "npx",
|
|
51
|
+
args: ["@co-ode/mcp-server"],
|
|
52
|
+
env: {
|
|
53
|
+
CO_ODE_API_URL: apiOrigin,
|
|
54
|
+
CO_ODE_ROOM_ID: options.roomId || "",
|
|
55
|
+
CO_ODE_TOKEN: token,
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
}, null, 2));
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
// Non-fatal — Claude Code will work without MCP tools
|
|
63
|
+
}
|
|
64
|
+
// Spawn claude -p with MCP config
|
|
65
|
+
const child = spawn("claude", ["-p", goal, "--add-dir", workDir, "--mcp-config", mcpConfigPath], {
|
|
66
|
+
cwd: workDir,
|
|
67
|
+
env: {
|
|
68
|
+
...process.env,
|
|
69
|
+
CO_ODE_API_URL: apiOrigin,
|
|
70
|
+
CO_ODE_ROOM_ID: options.roomId || "",
|
|
71
|
+
CO_ODE_TOKEN: token,
|
|
72
|
+
CO_ODE_RUN_ID: runId,
|
|
73
|
+
},
|
|
74
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
75
|
+
});
|
|
76
|
+
currentRun = { runId, process: child, heartbeatTimer };
|
|
77
|
+
let stdout = "";
|
|
78
|
+
let stderr = "";
|
|
79
|
+
child.stdout?.setEncoding("utf-8");
|
|
80
|
+
child.stderr?.setEncoding("utf-8");
|
|
81
|
+
child.stdout?.on("data", (chunk) => {
|
|
82
|
+
stdout += chunk;
|
|
83
|
+
onOutput(chunk);
|
|
84
|
+
});
|
|
85
|
+
child.stderr?.on("data", (chunk) => {
|
|
86
|
+
stderr += chunk;
|
|
87
|
+
onOutput(chunk);
|
|
88
|
+
});
|
|
89
|
+
return new Promise((resolve) => {
|
|
90
|
+
child.on("exit", async (code) => {
|
|
91
|
+
clearInterval(heartbeatTimer);
|
|
92
|
+
const succeeded = code === 0;
|
|
93
|
+
onOutput(`\r\n[co-ode] Claude Code ${succeeded ? "completed" : `exited with code ${code}`}\r\n`);
|
|
94
|
+
// Post completion
|
|
95
|
+
try {
|
|
96
|
+
await fetch(completeUrl, {
|
|
97
|
+
method: "POST",
|
|
98
|
+
headers: {
|
|
99
|
+
"content-type": "application/json",
|
|
100
|
+
authorization: `Bearer ${token}`,
|
|
101
|
+
"x-local-bridge-token": token,
|
|
102
|
+
},
|
|
103
|
+
body: JSON.stringify({
|
|
104
|
+
ok: succeeded,
|
|
105
|
+
run: {
|
|
106
|
+
status: succeeded ? "succeeded" : "failed",
|
|
107
|
+
steps: [
|
|
108
|
+
{
|
|
109
|
+
kind: succeeded ? "observation" : "error",
|
|
110
|
+
input: { description: goal },
|
|
111
|
+
output: {
|
|
112
|
+
success: succeeded,
|
|
113
|
+
payload: {
|
|
114
|
+
stdout: stdout.slice(-4096),
|
|
115
|
+
stderr: stderr.slice(-2048),
|
|
116
|
+
exitCode: code,
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
created_at: Date.now(),
|
|
120
|
+
},
|
|
121
|
+
],
|
|
122
|
+
},
|
|
123
|
+
}),
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
catch (err) {
|
|
127
|
+
onOutput(`[co-ode] Warning: failed to post completion: ${err}\r\n`);
|
|
128
|
+
}
|
|
129
|
+
currentRun = null;
|
|
130
|
+
resolve();
|
|
131
|
+
});
|
|
132
|
+
child.on("error", async (err) => {
|
|
133
|
+
clearInterval(heartbeatTimer);
|
|
134
|
+
onOutput(`\r\n[co-ode] Failed to spawn Claude Code: ${err.message}\r\n`);
|
|
135
|
+
onOutput(`[co-ode] Make sure 'claude' CLI is installed and in PATH\r\n`);
|
|
136
|
+
try {
|
|
137
|
+
await fetch(completeUrl, {
|
|
138
|
+
method: "POST",
|
|
139
|
+
headers: {
|
|
140
|
+
"content-type": "application/json",
|
|
141
|
+
authorization: `Bearer ${token}`,
|
|
142
|
+
"x-local-bridge-token": token,
|
|
143
|
+
},
|
|
144
|
+
body: JSON.stringify({
|
|
145
|
+
ok: false,
|
|
146
|
+
run: {
|
|
147
|
+
status: "failed",
|
|
148
|
+
steps: [
|
|
149
|
+
{
|
|
150
|
+
kind: "error",
|
|
151
|
+
input: { description: "Failed to spawn Claude Code" },
|
|
152
|
+
output: { success: false, payload: { message: err.message } },
|
|
153
|
+
created_at: Date.now(),
|
|
154
|
+
},
|
|
155
|
+
],
|
|
156
|
+
},
|
|
157
|
+
}),
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
catch {
|
|
161
|
+
// Non-fatal
|
|
162
|
+
}
|
|
163
|
+
currentRun = null;
|
|
164
|
+
resolve();
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
function cancelCurrentRun() {
|
|
169
|
+
if (currentRun) {
|
|
170
|
+
currentRun.process.kill("SIGTERM");
|
|
171
|
+
}
|
|
172
|
+
}
|
package/dist/pty.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface PtyHandle {
|
|
2
|
+
onData: (cb: (data: string) => void) => void;
|
|
3
|
+
write: (data: string) => void;
|
|
4
|
+
resize: (cols: number, rows: number) => void;
|
|
5
|
+
kill: () => void;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Creates a local PTY-like shell process.
|
|
9
|
+
* Uses child_process.spawn with a pseudo-TTY via stdio pipes.
|
|
10
|
+
* For a production version, node-pty would give true PTY support,
|
|
11
|
+
* but this works without native compilation.
|
|
12
|
+
*/
|
|
13
|
+
export declare function createLocalPty(cwd: string): PtyHandle;
|
package/dist/pty.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
/**
|
|
3
|
+
* Creates a local PTY-like shell process.
|
|
4
|
+
* Uses child_process.spawn with a pseudo-TTY via stdio pipes.
|
|
5
|
+
* For a production version, node-pty would give true PTY support,
|
|
6
|
+
* but this works without native compilation.
|
|
7
|
+
*/
|
|
8
|
+
export function createLocalPty(cwd) {
|
|
9
|
+
const shell = process.env.SHELL || "/bin/bash";
|
|
10
|
+
const cols = process.stdout.columns || 80;
|
|
11
|
+
const rows = process.stdout.rows || 24;
|
|
12
|
+
const child = spawn(shell, ["-i"], {
|
|
13
|
+
cwd,
|
|
14
|
+
env: {
|
|
15
|
+
...process.env,
|
|
16
|
+
TERM: "xterm-256color",
|
|
17
|
+
COLUMNS: String(cols),
|
|
18
|
+
LINES: String(rows),
|
|
19
|
+
},
|
|
20
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
21
|
+
});
|
|
22
|
+
const dataCallbacks = [];
|
|
23
|
+
child.stdout?.setEncoding("utf-8");
|
|
24
|
+
child.stderr?.setEncoding("utf-8");
|
|
25
|
+
child.stdout?.on("data", (chunk) => {
|
|
26
|
+
for (const cb of dataCallbacks)
|
|
27
|
+
cb(chunk);
|
|
28
|
+
});
|
|
29
|
+
child.stderr?.on("data", (chunk) => {
|
|
30
|
+
for (const cb of dataCallbacks)
|
|
31
|
+
cb(chunk);
|
|
32
|
+
});
|
|
33
|
+
child.on("exit", () => {
|
|
34
|
+
for (const cb of dataCallbacks)
|
|
35
|
+
cb("\r\n[co-ode] Shell process exited\r\n");
|
|
36
|
+
});
|
|
37
|
+
return {
|
|
38
|
+
onData(cb) {
|
|
39
|
+
dataCallbacks.push(cb);
|
|
40
|
+
},
|
|
41
|
+
write(data) {
|
|
42
|
+
child.stdin?.write(data);
|
|
43
|
+
},
|
|
44
|
+
resize(_cols, _rows) {
|
|
45
|
+
// With child_process we can't truly resize without node-pty
|
|
46
|
+
// But we update env for new subprocesses
|
|
47
|
+
},
|
|
48
|
+
kill() {
|
|
49
|
+
child.kill("SIGTERM");
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@codegrammer/co-od",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "CLI for co-ode — run AI agents in shared rooms from the command line",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"co-ode": "./dist/index.js",
|
|
8
|
+
"co-od": "./dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"main": "dist/index.js",
|
|
11
|
+
"files": [
|
|
12
|
+
"dist",
|
|
13
|
+
"SKILL.md"
|
|
14
|
+
],
|
|
15
|
+
"scripts": {
|
|
16
|
+
"build": "tsc -p tsconfig.json",
|
|
17
|
+
"dev": "tsc -w -p tsconfig.json",
|
|
18
|
+
"prepublishOnly": "npm run build",
|
|
19
|
+
"lint": "echo noop"
|
|
20
|
+
},
|
|
21
|
+
"keywords": [
|
|
22
|
+
"ai",
|
|
23
|
+
"agents",
|
|
24
|
+
"cli",
|
|
25
|
+
"claude",
|
|
26
|
+
"codex",
|
|
27
|
+
"multiplayer",
|
|
28
|
+
"coding",
|
|
29
|
+
"collaboration",
|
|
30
|
+
"rooms"
|
|
31
|
+
],
|
|
32
|
+
"repository": {
|
|
33
|
+
"type": "git",
|
|
34
|
+
"url": "https://github.com/hacksurvivor/co-ode.git",
|
|
35
|
+
"directory": "packages/cli"
|
|
36
|
+
},
|
|
37
|
+
"homepage": "https://co-od.dev",
|
|
38
|
+
"license": "MIT",
|
|
39
|
+
"author": "co-ode contributors",
|
|
40
|
+
"engines": {
|
|
41
|
+
"node": ">=18"
|
|
42
|
+
},
|
|
43
|
+
"dependencies": {
|
|
44
|
+
"ws": "^8.18.0",
|
|
45
|
+
"open": "^10.1.0"
|
|
46
|
+
},
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"@types/node": "^22.10.2",
|
|
49
|
+
"@types/ws": "^8.5.13",
|
|
50
|
+
"typescript": "^5.6.2"
|
|
51
|
+
}
|
|
52
|
+
}
|