@codegrammer/co-od 0.1.2 → 0.1.4

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.
@@ -36,6 +36,9 @@ function parseArgs(args) {
36
36
  else if (args[i] === "--server" && args[i + 1]) {
37
37
  parsed.server = args[++i];
38
38
  }
39
+ else if (args[i] === "--handoff-to" && args[i + 1]) {
40
+ parsed.handoffTo = args[++i];
41
+ }
39
42
  else if (!args[i].startsWith("--")) {
40
43
  parsed.roomId = args[i];
41
44
  }
@@ -128,41 +131,66 @@ export async function run(args) {
128
131
  activeRuns++;
129
132
  console.error(`[${timestamp()}] Dispatching: ${goal.slice(0, 80)}`);
130
133
  // Create a run on the server
134
+ const sessionToken = api.getSessionToken();
135
+ const baseUrl = api.getBaseUrl();
131
136
  api
132
137
  .post(`/api/rooms/${parsed.roomId}/agent-runs`, { goal, provider: parsed.provider, agentId, eventId: event._id })
133
138
  .then(async ({ runId }) => {
134
139
  const result = await adapter.execute(goal, {
135
140
  workDir: parsed.dir,
136
141
  onOutput: (data) => process.stderr.write(data),
142
+ // Room context enables Claude Code hooks for live reporting
143
+ roomId: parsed.roomId,
144
+ runId,
145
+ serverUrl: baseUrl,
146
+ sessionToken: sessionToken || undefined,
137
147
  });
138
- console.error(`\n[${timestamp()}] Run ${runId} ${result.exitCode === 0 ? "succeeded" : "failed"}`);
148
+ const succeeded = result.exitCode === 0;
149
+ console.error(`\n[${timestamp()}] Run ${runId} ${succeeded ? "succeeded" : "failed"}`);
139
150
  // Report completion
140
151
  try {
141
- await api.post(`/api/rooms/${parsed.roomId}/agent-runs/${runId}/handoff`, {
142
- ok: result.exitCode === 0,
143
- run: {
144
- status: result.exitCode === 0 ? "succeeded" : "failed",
145
- steps: [
146
- {
147
- kind: result.exitCode === 0 ? "observation" : "error",
148
- input: { description: goal },
149
- output: {
150
- success: result.exitCode === 0,
151
- payload: {
152
- stdout: result.stdout.slice(-4096),
153
- stderr: result.stderr.slice(-2048),
154
- exitCode: result.exitCode,
155
- },
156
- },
157
- created_at: Date.now(),
158
- },
159
- ],
152
+ await api.post(`/api/rooms/${parsed.roomId}/agent-runs/${runId}/steps`, {
153
+ kind: succeeded ? "observation" : "error",
154
+ input: { description: succeeded ? "Task completed" : "Task failed" },
155
+ output: {
156
+ success: succeeded,
157
+ payload: {
158
+ stdout: result.stdout.slice(-4096),
159
+ stderr: result.stderr.slice(-2048),
160
+ exitCode: result.exitCode,
161
+ },
160
162
  },
161
163
  });
162
164
  }
163
165
  catch {
164
166
  console.error(`[${timestamp()}] Warning: failed to report completion`);
165
167
  }
168
+ // Agent-to-agent handoff
169
+ const handoffTarget = parsed.handoffTo ||
170
+ event.payload?.handoffTo ||
171
+ null;
172
+ if (succeeded && handoffTarget && parsed.roomId) {
173
+ console.error(`[${timestamp()}] Handing off to "${handoffTarget}"...`);
174
+ try {
175
+ // Find target agent by name
176
+ const agents = await api.get(`/api/rooms/${parsed.roomId}/agents`);
177
+ const target = agents.agents?.find((a) => a.name.toLowerCase() === handoffTarget.toLowerCase() || a._id === handoffTarget);
178
+ if (target) {
179
+ await api.post(`/api/rooms/${parsed.roomId}/agent-runs/${runId}/handoff`, {
180
+ targetAgentId: target._id,
181
+ context: `Completed: ${goal}. Output: ${result.stdout.slice(-500)}`,
182
+ goal: `Continue from "${agentName}": ${goal}`,
183
+ });
184
+ console.error(`[${timestamp()}] Handed off to "${target.name}" (${target._id})`);
185
+ }
186
+ else {
187
+ console.error(`[${timestamp()}] Handoff target "${handoffTarget}" not found in room`);
188
+ }
189
+ }
190
+ catch (err) {
191
+ console.error(`[${timestamp()}] Handoff failed: ${err}`);
192
+ }
193
+ }
166
194
  })
167
195
  .catch((err) => {
168
196
  console.error(`[${timestamp()}] Run failed: ${err}`);
@@ -0,0 +1 @@
1
+ export declare function run(args: string[]): Promise<void>;
@@ -0,0 +1,168 @@
1
+ import * as api from "../api-client.js";
2
+ import { getAdapter } from "../adapters/index.js";
3
+ import { writeFileSync, mkdirSync, existsSync } from "node:fs";
4
+ import { join, basename } from "node:path";
5
+ import { homedir } from "node:os";
6
+ function parseArgs(args) {
7
+ const parsed = {
8
+ dir: process.cwd(),
9
+ provider: "claude",
10
+ };
11
+ for (let i = 0; i < args.length; i++) {
12
+ if (args[i] === "--dir" && args[i + 1]) {
13
+ parsed.dir = args[++i];
14
+ }
15
+ else if (args[i] === "--name" && args[i + 1]) {
16
+ parsed.name = args[++i];
17
+ }
18
+ else if (args[i] === "--provider" && args[i + 1]) {
19
+ parsed.provider = args[++i];
20
+ }
21
+ else if (args[i] === "--server" && args[i + 1]) {
22
+ parsed.server = args[++i];
23
+ }
24
+ else if (!args[i].startsWith("--") && !parsed.name) {
25
+ parsed.name = args[i];
26
+ }
27
+ }
28
+ return parsed;
29
+ }
30
+ function timestamp() {
31
+ return new Date().toISOString().slice(11, 19);
32
+ }
33
+ export async function run(args) {
34
+ if (args.includes("--help") || args.includes("-h")) {
35
+ console.error(`Usage: co-od init [name] [--dir <path>] [--provider claude|codex|openclaw]
36
+
37
+ Sets up a new co-ode project:
38
+ 1. Creates a room on co-od.dev
39
+ 2. Links it to your local project directory
40
+ 3. Adds a default agent
41
+ 4. Generates a .co-od config file
42
+ 5. Ready to go — start giving tasks
43
+
44
+ Examples:
45
+ co-od init # Uses current dir name as room name
46
+ co-od init "my-saas-app" # Custom room name
47
+ co-od init --provider codex # Use Codex as default agent
48
+ co-od init --dir ~/projects/app # Different directory
49
+ `);
50
+ process.exit(0);
51
+ }
52
+ const parsed = parseArgs(args);
53
+ if (parsed.server) {
54
+ process.env.CO_ODE_SERVER = parsed.server;
55
+ }
56
+ // Ensure logged in
57
+ const token = api.getSessionToken();
58
+ if (!token) {
59
+ console.error(`[${timestamp()}] Not logged in. Run: co-od login`);
60
+ process.exit(1);
61
+ }
62
+ const projectDir = parsed.dir;
63
+ const roomName = parsed.name || basename(projectDir);
64
+ console.error(`\n co-od init\n`);
65
+ console.error(` project: ${projectDir}`);
66
+ console.error(` room: ${roomName}`);
67
+ console.error(` provider: ${parsed.provider}\n`);
68
+ // Step 1: Check provider is available
69
+ console.error(` [1/5] Checking ${parsed.provider} CLI...`);
70
+ const adapter = getAdapter(parsed.provider);
71
+ const available = await adapter.available();
72
+ if (!available) {
73
+ console.error(` ✗ ${adapter.name} not found on PATH`);
74
+ console.error(` Install it first, then run co-od init again.`);
75
+ process.exit(1);
76
+ }
77
+ console.error(` ✓ ${adapter.name} available`);
78
+ // Step 2: Create room
79
+ console.error(` [2/5] Creating room "${roomName}"...`);
80
+ let roomId;
81
+ try {
82
+ const res = await api.post("/api/rooms", { name: roomName });
83
+ roomId = res.roomId || res.room?._id || "";
84
+ if (!roomId)
85
+ throw new Error("No room ID returned");
86
+ console.error(` ✓ Room created: ${roomId.slice(0, 12)}...`);
87
+ }
88
+ catch (err) {
89
+ console.error(` ✗ Failed to create room: ${err}`);
90
+ process.exit(1);
91
+ }
92
+ // Step 3: Add default agent
93
+ console.error(` [3/5] Adding default agent...`);
94
+ let agentId;
95
+ try {
96
+ const agentName = parsed.provider === "codex" ? "Codex Builder"
97
+ : parsed.provider === "openclaw" ? "OpenClaw Agent"
98
+ : "Claude Builder";
99
+ const res = await api.post(`/api/rooms/${roomId}/agents`, {
100
+ name: agentName,
101
+ type: "builder",
102
+ executionMode: parsed.provider === "codex" ? "local_codex" : "local_claude_code",
103
+ provider: parsed.provider === "codex" ? "openai"
104
+ : parsed.provider === "openclaw" ? "openclaw"
105
+ : "anthropic",
106
+ permissions: {
107
+ "fs.read": true,
108
+ "fs.write": true,
109
+ "fs.applyPatch": true,
110
+ "exec.run": true,
111
+ "net.egress": false,
112
+ "ports.expose": false,
113
+ "secrets.read": false,
114
+ },
115
+ });
116
+ agentId = res.agentId || "";
117
+ console.error(` ✓ Agent "${agentName}" added`);
118
+ }
119
+ catch (err) {
120
+ console.error(` ✗ Failed to add agent: ${err}`);
121
+ agentId = "";
122
+ }
123
+ // Step 4: Save config
124
+ console.error(` [4/5] Saving config...`);
125
+ const config = {
126
+ roomId,
127
+ roomName,
128
+ projectDir,
129
+ provider: parsed.provider,
130
+ agentId,
131
+ server: api.getBaseUrl(),
132
+ createdAt: new Date().toISOString(),
133
+ };
134
+ // Save to project dir
135
+ const configPath = join(projectDir, ".co-od.json");
136
+ try {
137
+ writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
138
+ console.error(` ✓ ${configPath}`);
139
+ }
140
+ catch {
141
+ console.error(` ✗ Could not write ${configPath}`);
142
+ }
143
+ // Also save room mapping to global config
144
+ const globalConfigDir = join(homedir(), ".co-od");
145
+ const roomsMapPath = join(globalConfigDir, "rooms.json");
146
+ try {
147
+ mkdirSync(globalConfigDir, { recursive: true });
148
+ let rooms = {};
149
+ if (existsSync(roomsMapPath)) {
150
+ rooms = JSON.parse(require("node:fs").readFileSync(roomsMapPath, "utf-8"));
151
+ }
152
+ rooms[roomId] = { roomId, projectDir, name: roomName };
153
+ writeFileSync(roomsMapPath, JSON.stringify(rooms, null, 2) + "\n", "utf-8");
154
+ }
155
+ catch {
156
+ // Non-fatal
157
+ }
158
+ // Step 5: Done
159
+ console.error(` [5/5] Ready!\n`);
160
+ console.error(` Your room is live. Next steps:\n`);
161
+ console.error(` co-od run ${roomId.slice(0, 12)} "describe your task" # run a single task`);
162
+ console.error(` co-od daemon ${roomId.slice(0, 12)} --auto-execute # autonomous mode`);
163
+ console.error(` co-od share # invite teammates\n`);
164
+ console.error(` Or open in browser:`);
165
+ console.error(` ${api.getBaseUrl()}/rooms/${roomId}\n`);
166
+ // Output JSON for scripting
167
+ console.log(JSON.stringify(config, null, 2));
168
+ }
@@ -57,12 +57,16 @@ export async function run(args) {
57
57
  if (!parsed.json) {
58
58
  console.error(`[co-od] Run ${runId} created. Executing with ${adapter.name}...`);
59
59
  }
60
- // Execute locally
60
+ // Execute locally — with room context for live hooks reporting
61
61
  const result = await adapter.execute(parsed.goal, {
62
62
  workDir: parsed.dir,
63
63
  onOutput: parsed.json
64
64
  ? undefined
65
65
  : (data) => process.stderr.write(data),
66
+ roomId: parsed.roomId,
67
+ runId,
68
+ serverUrl: api.getBaseUrl(),
69
+ sessionToken: api.getSessionToken() || undefined,
66
70
  });
67
71
  // Report completion
68
72
  if (!parsed.json) {
package/dist/index.js CHANGED
@@ -2,11 +2,12 @@
2
2
  "use strict";
3
3
  const VERSION = "0.1.0";
4
4
  const COMMANDS = {
5
+ init: { desc: "Set up a new co-ode project", usage: "co-od init [name] [--dir <path>] [--provider claude|codex|openclaw]" },
5
6
  login: { desc: "Authenticate with co-ode", usage: "co-od login [--token <t>]" },
6
7
  rooms: { desc: "List your rooms", usage: "co-od rooms [--json]" },
7
8
  run: { desc: "Execute a single task in a room", usage: "co-od run <room> <goal> [--provider claude|codex|openclaw] [--dir <path>] [--json]" },
8
9
  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
+ daemon: { desc: "Autonomous watch mode for a room", usage: "co-od daemon <room> [--as <name>] [--auto-execute] [--handoff-to <agent>]" },
10
11
  share: { desc: "Generate a relay code for teammates", usage: "co-od share [--provider claude|codex|openclaw]" },
11
12
  connect: { desc: "Connect to a relay via code", usage: "co-od connect <code>" },
12
13
  status: { desc: "Show current login and running state", usage: "co-od status [--json]" },
@@ -58,6 +59,11 @@ async function main() {
58
59
  }
59
60
  // Route to command
60
61
  switch (command) {
62
+ case "init": {
63
+ const { run } = await import("./commands/init.js");
64
+ await run(commandArgs);
65
+ break;
66
+ }
61
67
  case "login": {
62
68
  const { run } = await import("./commands/login.js");
63
69
  await run(commandArgs);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codegrammer/co-od",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "CLI for co-ode \u2014 run AI agents in shared rooms from the command line",
5
5
  "type": "module",
6
6
  "bin": {