@codegrammer/co-od 0.1.6 → 0.1.7
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/dist/adapters/codex.js +127 -6
- package/dist/adapters/index.d.ts +1 -1
- package/dist/adapters/index.js +1 -1
- package/dist/adapters/openclaw.js +92 -3
- package/dist/api-client.js +1 -1
- package/dist/commands/join.js +1 -1
- package/dist/commands/login.js +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/package.json +3 -3
package/dist/adapters/codex.js
CHANGED
|
@@ -1,9 +1,73 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
+
import { createInterface } from "node:readline";
|
|
2
3
|
import { existsSync } from "node:fs";
|
|
3
4
|
function which(cmd) {
|
|
4
5
|
const paths = (process.env.PATH || "").split(":");
|
|
5
6
|
return paths.some((dir) => existsSync(`${dir}/${cmd}`));
|
|
6
7
|
}
|
|
8
|
+
/**
|
|
9
|
+
* Parse a Codex JSONL event and report it as an agent step to co-ode.
|
|
10
|
+
* Codex events: thread.started, turn.started, turn.completed,
|
|
11
|
+
* item.started, item.completed (with item types: command_execution,
|
|
12
|
+
* file_change, mcp_tool_call, agent_message, reasoning, plan_update)
|
|
13
|
+
*/
|
|
14
|
+
async function reportCodexEvent(event, config) {
|
|
15
|
+
const type = event.type;
|
|
16
|
+
const item = event.item;
|
|
17
|
+
// Only report completed items as steps
|
|
18
|
+
if (type !== "item.completed" || !item)
|
|
19
|
+
return;
|
|
20
|
+
const itemType = item.type;
|
|
21
|
+
let stepKind;
|
|
22
|
+
let description;
|
|
23
|
+
let payload = {};
|
|
24
|
+
switch (itemType) {
|
|
25
|
+
case "command_execution":
|
|
26
|
+
stepKind = "tool_call";
|
|
27
|
+
description = item.command || "exec";
|
|
28
|
+
payload = { command: item.command, exitCode: item.exit_code };
|
|
29
|
+
break;
|
|
30
|
+
case "file_change":
|
|
31
|
+
stepKind = "tool_call";
|
|
32
|
+
description = `write ${item.path || "file"}`;
|
|
33
|
+
payload = { path: item.path };
|
|
34
|
+
break;
|
|
35
|
+
case "mcp_tool_call":
|
|
36
|
+
stepKind = "tool_call";
|
|
37
|
+
description = item.tool_name || "mcp_tool";
|
|
38
|
+
payload = { tool: item.tool_name };
|
|
39
|
+
break;
|
|
40
|
+
case "agent_message":
|
|
41
|
+
stepKind = "observation";
|
|
42
|
+
description = (item.text || "").slice(0, 200);
|
|
43
|
+
payload = { text: (item.text || "").slice(0, 4000) };
|
|
44
|
+
break;
|
|
45
|
+
case "plan_update":
|
|
46
|
+
stepKind = "plan";
|
|
47
|
+
description = "plan updated";
|
|
48
|
+
payload = { plan: item.plan };
|
|
49
|
+
break;
|
|
50
|
+
default:
|
|
51
|
+
return; // Skip reasoning, web_search, etc.
|
|
52
|
+
}
|
|
53
|
+
try {
|
|
54
|
+
await fetch(`${config.serverUrl}/api/rooms/${config.roomId}/agent-runs/${config.runId}/steps`, {
|
|
55
|
+
method: "POST",
|
|
56
|
+
headers: {
|
|
57
|
+
"content-type": "application/json",
|
|
58
|
+
authorization: `Bearer ${config.sessionToken}`,
|
|
59
|
+
},
|
|
60
|
+
body: JSON.stringify({
|
|
61
|
+
kind: stepKind,
|
|
62
|
+
input: { description, payload },
|
|
63
|
+
output: { success: true, payload },
|
|
64
|
+
}),
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
// Non-fatal — don't break Codex execution
|
|
69
|
+
}
|
|
70
|
+
}
|
|
7
71
|
export class CodexAdapter {
|
|
8
72
|
name = "codex";
|
|
9
73
|
async available() {
|
|
@@ -11,25 +75,82 @@ export class CodexAdapter {
|
|
|
11
75
|
}
|
|
12
76
|
async execute(goal, options) {
|
|
13
77
|
const { workDir, onOutput, signal } = options;
|
|
78
|
+
const hasRoomContext = Boolean(options.roomId && options.runId && options.serverUrl && options.sessionToken);
|
|
79
|
+
// Use --json for JSONL event streaming when we have room context
|
|
80
|
+
const args = ["exec", "--full-auto"];
|
|
81
|
+
if (hasRoomContext)
|
|
82
|
+
args.push("--json");
|
|
83
|
+
args.push("--", goal);
|
|
14
84
|
return new Promise((resolve, reject) => {
|
|
15
|
-
const child = spawn("codex",
|
|
85
|
+
const child = spawn("codex", args, {
|
|
16
86
|
cwd: workDir,
|
|
17
|
-
env: {
|
|
87
|
+
env: {
|
|
88
|
+
...process.env,
|
|
89
|
+
CO_OD_ROOM_ID: options.roomId || "",
|
|
90
|
+
CO_OD_RUN_ID: options.runId || "",
|
|
91
|
+
},
|
|
18
92
|
stdio: ["pipe", "pipe", "pipe"],
|
|
19
93
|
});
|
|
20
94
|
let stdout = "";
|
|
21
95
|
let stderr = "";
|
|
96
|
+
let lastAgentMessage = "";
|
|
22
97
|
child.stdout?.setEncoding("utf-8");
|
|
23
98
|
child.stderr?.setEncoding("utf-8");
|
|
24
|
-
|
|
25
|
-
stdout
|
|
26
|
-
|
|
27
|
-
|
|
99
|
+
if (hasRoomContext) {
|
|
100
|
+
// Parse JSONL events from stdout for real-time reporting
|
|
101
|
+
const rl = createInterface({ input: child.stdout, crlfDelay: Infinity });
|
|
102
|
+
rl.on("line", (line) => {
|
|
103
|
+
stdout += line + "\n";
|
|
104
|
+
try {
|
|
105
|
+
const event = JSON.parse(line);
|
|
106
|
+
// Extract final agent message for display
|
|
107
|
+
if (event.type === "item.completed" &&
|
|
108
|
+
event.item?.type === "agent_message") {
|
|
109
|
+
lastAgentMessage =
|
|
110
|
+
event.item.text || "";
|
|
111
|
+
}
|
|
112
|
+
// Report progress to co-ode
|
|
113
|
+
const config = {
|
|
114
|
+
roomId: options.roomId,
|
|
115
|
+
runId: options.runId,
|
|
116
|
+
serverUrl: options.serverUrl,
|
|
117
|
+
sessionToken: options.sessionToken,
|
|
118
|
+
};
|
|
119
|
+
void reportCodexEvent(event, config);
|
|
120
|
+
// Feed human-readable output to onOutput
|
|
121
|
+
const itemType = event.item?.type;
|
|
122
|
+
if (event.type === "item.completed" && itemType === "command_execution") {
|
|
123
|
+
onOutput?.(`$ ${event.item.command}\n`);
|
|
124
|
+
}
|
|
125
|
+
else if (event.type === "item.completed" && itemType === "file_change") {
|
|
126
|
+
onOutput?.(`[write] ${event.item.path}\n`);
|
|
127
|
+
}
|
|
128
|
+
else if (event.type === "item.completed" && itemType === "agent_message") {
|
|
129
|
+
onOutput?.(`${(event.item.text || "").slice(0, 500)}\n`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
// Not valid JSON — pass through as raw output
|
|
134
|
+
onOutput?.(line + "\n");
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
// No room context — raw output mode (original behavior)
|
|
140
|
+
child.stdout?.on("data", (chunk) => {
|
|
141
|
+
stdout += chunk;
|
|
142
|
+
onOutput?.(chunk);
|
|
143
|
+
});
|
|
144
|
+
}
|
|
28
145
|
child.stderr?.on("data", (chunk) => {
|
|
29
146
|
stderr += chunk;
|
|
30
147
|
onOutput?.(chunk);
|
|
31
148
|
});
|
|
32
149
|
child.on("exit", (code) => {
|
|
150
|
+
// If we parsed JSONL, include the last agent message in stdout for the caller
|
|
151
|
+
if (hasRoomContext && lastAgentMessage) {
|
|
152
|
+
stdout += `\n--- Agent Response ---\n${lastAgentMessage}\n`;
|
|
153
|
+
}
|
|
33
154
|
resolve({ exitCode: code ?? 1, stdout, stderr });
|
|
34
155
|
});
|
|
35
156
|
child.on("error", (err) => {
|
package/dist/adapters/index.d.ts
CHANGED
package/dist/adapters/index.js
CHANGED
|
@@ -4,7 +4,7 @@ import { spawn } from "node:child_process";
|
|
|
4
4
|
*
|
|
5
5
|
* Supports both the legacy `openclaw` CLI and the newer `zeroclaw` CLI.
|
|
6
6
|
* Sends tasks via `zeroclaw agent -m <goal>` (non-interactive single-shot mode).
|
|
7
|
-
*
|
|
7
|
+
* Reports tool calls and file changes as agent steps when room context is available.
|
|
8
8
|
*/
|
|
9
9
|
const ZEROCLAW_BIN = process.env.ZEROCLAW_BIN || "zeroclaw";
|
|
10
10
|
const OPENCLAW_BIN = process.env.OPENCLAW_BIN || "openclaw";
|
|
@@ -28,6 +28,62 @@ async function findBinary() {
|
|
|
28
28
|
}
|
|
29
29
|
return null;
|
|
30
30
|
}
|
|
31
|
+
/**
|
|
32
|
+
* Report a step to the co-ode room.
|
|
33
|
+
*/
|
|
34
|
+
async function reportStep(config, kind, description, payload = {}) {
|
|
35
|
+
try {
|
|
36
|
+
await fetch(`${config.serverUrl}/api/rooms/${config.roomId}/agent-runs/${config.runId}/steps`, {
|
|
37
|
+
method: "POST",
|
|
38
|
+
headers: {
|
|
39
|
+
"content-type": "application/json",
|
|
40
|
+
authorization: `Bearer ${config.sessionToken}`,
|
|
41
|
+
},
|
|
42
|
+
body: JSON.stringify({
|
|
43
|
+
kind,
|
|
44
|
+
input: { description, payload },
|
|
45
|
+
output: { success: true, payload },
|
|
46
|
+
}),
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
// Non-fatal
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Parse openclaw/zeroclaw output lines for tool calls and file changes.
|
|
55
|
+
* ZeroClaw outputs structured markers like:
|
|
56
|
+
* [TOOL] tool_name: description
|
|
57
|
+
* [FILE] path/to/file
|
|
58
|
+
* [EXEC] command
|
|
59
|
+
* [DONE] summary
|
|
60
|
+
*/
|
|
61
|
+
function parseOutputLine(line) {
|
|
62
|
+
const trimmed = line.trim();
|
|
63
|
+
if (trimmed.startsWith("[TOOL]")) {
|
|
64
|
+
return { kind: "tool_call", description: trimmed.slice(6).trim(), payload: {} };
|
|
65
|
+
}
|
|
66
|
+
if (trimmed.startsWith("[FILE]")) {
|
|
67
|
+
const path = trimmed.slice(6).trim();
|
|
68
|
+
return { kind: "tool_call", description: `write ${path}`, payload: { path } };
|
|
69
|
+
}
|
|
70
|
+
if (trimmed.startsWith("[EXEC]")) {
|
|
71
|
+
const cmd = trimmed.slice(6).trim();
|
|
72
|
+
return { kind: "tool_call", description: cmd, payload: { command: cmd } };
|
|
73
|
+
}
|
|
74
|
+
if (trimmed.startsWith("[DONE]")) {
|
|
75
|
+
return { kind: "observation", description: trimmed.slice(6).trim(), payload: {} };
|
|
76
|
+
}
|
|
77
|
+
// Also detect common patterns from unstructured output
|
|
78
|
+
if (trimmed.match(/^(reading|writing|creating|editing|modifying)\s/i)) {
|
|
79
|
+
return { kind: "tool_call", description: trimmed.slice(0, 100), payload: {} };
|
|
80
|
+
}
|
|
81
|
+
if (trimmed.match(/^\$\s+/)) {
|
|
82
|
+
const cmd = trimmed.slice(2).trim();
|
|
83
|
+
return { kind: "tool_call", description: cmd, payload: { command: cmd } };
|
|
84
|
+
}
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
31
87
|
export class OpenClawAdapter {
|
|
32
88
|
name = "openclaw";
|
|
33
89
|
async available() {
|
|
@@ -44,26 +100,52 @@ export class OpenClawAdapter {
|
|
|
44
100
|
};
|
|
45
101
|
}
|
|
46
102
|
const { bin } = found;
|
|
103
|
+
const hasRoomContext = Boolean(options.roomId && options.runId && options.serverUrl && options.sessionToken);
|
|
104
|
+
const reportConfig = hasRoomContext
|
|
105
|
+
? {
|
|
106
|
+
roomId: options.roomId,
|
|
107
|
+
runId: options.runId,
|
|
108
|
+
serverUrl: options.serverUrl,
|
|
109
|
+
sessionToken: options.sessionToken,
|
|
110
|
+
}
|
|
111
|
+
: null;
|
|
47
112
|
// zeroclaw agent -m "goal" — single-shot non-interactive mode
|
|
48
113
|
const args = ["agent", "-m", goal];
|
|
49
114
|
return new Promise((resolve) => {
|
|
50
115
|
const child = spawn(bin, args, {
|
|
51
116
|
cwd: options.workDir,
|
|
52
|
-
env: {
|
|
117
|
+
env: {
|
|
118
|
+
...process.env,
|
|
119
|
+
CO_OD_ROOM_ID: options.roomId || "",
|
|
120
|
+
CO_OD_RUN_ID: options.runId || "",
|
|
121
|
+
},
|
|
53
122
|
stdio: ["pipe", "pipe", "pipe"],
|
|
54
123
|
});
|
|
55
124
|
if (options.signal) {
|
|
56
125
|
options.signal.addEventListener("abort", () => {
|
|
57
126
|
child.kill("SIGTERM");
|
|
58
|
-
});
|
|
127
|
+
}, { once: true });
|
|
59
128
|
}
|
|
60
129
|
let stdout = "";
|
|
61
130
|
let stderr = "";
|
|
131
|
+
let lineBuffer = "";
|
|
62
132
|
child.stdout?.setEncoding("utf-8");
|
|
63
133
|
child.stderr?.setEncoding("utf-8");
|
|
64
134
|
child.stdout?.on("data", (chunk) => {
|
|
65
135
|
stdout += chunk;
|
|
66
136
|
options.onOutput?.(chunk);
|
|
137
|
+
// Parse lines for step reporting
|
|
138
|
+
if (reportConfig) {
|
|
139
|
+
lineBuffer += chunk;
|
|
140
|
+
const lines = lineBuffer.split("\n");
|
|
141
|
+
lineBuffer = lines.pop() || "";
|
|
142
|
+
for (const line of lines) {
|
|
143
|
+
const parsed = parseOutputLine(line);
|
|
144
|
+
if (parsed) {
|
|
145
|
+
void reportStep(reportConfig, parsed.kind, parsed.description, parsed.payload);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
67
149
|
});
|
|
68
150
|
child.stderr?.on("data", (chunk) => {
|
|
69
151
|
stderr += chunk;
|
|
@@ -77,6 +159,13 @@ export class OpenClawAdapter {
|
|
|
77
159
|
});
|
|
78
160
|
});
|
|
79
161
|
child.on("close", (code) => {
|
|
162
|
+
// Flush remaining line buffer
|
|
163
|
+
if (reportConfig && lineBuffer.trim()) {
|
|
164
|
+
const parsed = parseOutputLine(lineBuffer);
|
|
165
|
+
if (parsed) {
|
|
166
|
+
void reportStep(reportConfig, parsed.kind, parsed.description, parsed.payload);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
80
169
|
resolve({
|
|
81
170
|
exitCode: code ?? 1,
|
|
82
171
|
stdout,
|
package/dist/api-client.js
CHANGED
|
@@ -4,7 +4,7 @@ import { join } from "node:path";
|
|
|
4
4
|
const CONFIG_DIR = join(homedir(), ".co-ode");
|
|
5
5
|
const SESSION_FILE = join(CONFIG_DIR, "session.json");
|
|
6
6
|
function getBaseUrl() {
|
|
7
|
-
return (process.env.CO_ODE_SERVER || "https://co-
|
|
7
|
+
return (process.env.CO_ODE_SERVER || "https://co-od.dev");
|
|
8
8
|
}
|
|
9
9
|
function getSessionToken() {
|
|
10
10
|
try {
|
package/dist/commands/join.js
CHANGED
|
@@ -26,7 +26,7 @@ export async function run(args) {
|
|
|
26
26
|
}
|
|
27
27
|
const serverUrl = parsed.server ||
|
|
28
28
|
process.env.CO_ODE_SERVER ||
|
|
29
|
-
"https://co-
|
|
29
|
+
"https://co-od.dev";
|
|
30
30
|
console.error(`[co-od] Joining room ${parsed.roomId}...`);
|
|
31
31
|
console.error(`[co-od] Server: ${serverUrl}`);
|
|
32
32
|
console.error(`[co-od] Working directory: ${parsed.dir}`);
|
package/dist/commands/login.js
CHANGED
|
@@ -76,7 +76,7 @@ export async function run(args) {
|
|
|
76
76
|
const parsed = parseArgs(args);
|
|
77
77
|
const serverUrl = parsed.server ||
|
|
78
78
|
process.env.CO_ODE_SERVER ||
|
|
79
|
-
"https://co-
|
|
79
|
+
"https://co-od.dev";
|
|
80
80
|
// --token flag: headless/CI mode
|
|
81
81
|
if (parsed.token) {
|
|
82
82
|
saveSession(parsed.token);
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
"use strict";
|
|
3
|
-
const VERSION = "0.1.
|
|
3
|
+
const VERSION = "0.1.6";
|
|
4
4
|
const COMMANDS = {
|
|
5
5
|
init: { desc: "Set up a new co-ode project", usage: "co-od init [name] [--dir <path>] [--provider claude|codex|openclaw]" },
|
|
6
6
|
login: { desc: "Authenticate with co-ode", usage: "co-od login [--token <t>]" },
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@codegrammer/co-od",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "CLI for co-ode
|
|
3
|
+
"version": "0.1.7",
|
|
4
|
+
"description": "CLI for co-ode — run AI agents in shared rooms from the command line",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"co-ode": "./dist/index.js",
|
|
@@ -49,4 +49,4 @@
|
|
|
49
49
|
"@types/ws": "^8.5.13",
|
|
50
50
|
"typescript": "^5.6.2"
|
|
51
51
|
}
|
|
52
|
-
}
|
|
52
|
+
}
|