@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.
@@ -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", ["exec", "--full-auto", "--", goal], {
85
+ const child = spawn("codex", args, {
16
86
  cwd: workDir,
17
- env: { ...process.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
- child.stdout?.on("data", (chunk) => {
25
- stdout += chunk;
26
- onOutput?.(chunk);
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) => {
@@ -1,3 +1,3 @@
1
1
  import type { AgentAdapter } from "./base.js";
2
2
  export declare function getAdapter(provider: string): AgentAdapter;
3
- export declare const PROVIDER_LIST = "claude|codex|openclaw";
3
+ export declare const PROVIDER_LIST = "claude|codex|openclaw|zeroclaw";
@@ -14,4 +14,4 @@ export function getAdapter(provider) {
14
14
  // Default to claude
15
15
  return new ClaudeAdapter();
16
16
  }
17
- export const PROVIDER_LIST = "claude|codex|openclaw";
17
+ export const PROVIDER_LIST = "claude|codex|openclaw|zeroclaw";
@@ -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
- * Falls back to the HTTP gateway if running as a daemon.
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: { ...process.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,
@@ -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-ode.vercel.app");
7
+ return (process.env.CO_ODE_SERVER || "https://co-od.dev");
8
8
  }
9
9
  function getSessionToken() {
10
10
  try {
@@ -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-ode.vercel.app";
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}`);
@@ -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-ode.vercel.app";
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
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- declare const VERSION = "0.1.0";
2
+ declare const VERSION = "0.1.6";
3
3
  declare const COMMANDS: Record<string, {
4
4
  desc: string;
5
5
  usage: string;
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  "use strict";
3
- const VERSION = "0.1.0";
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.6",
4
- "description": "CLI for co-ode \u2014 run AI agents in shared rooms from the command line",
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
+ }