@codegrammer/co-od 0.1.3 → 0.1.5

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.
@@ -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
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * `npx co-od` (no args) — the zero-config magic start.
3
+ *
4
+ * 1. Check login → open browser if needed
5
+ * 2. Detect project dir + available CLIs
6
+ * 3. Find or create room for this project
7
+ * 4. Start local bridge in background
8
+ * 5. Open browser to room
9
+ * 6. Done — user types a task in the browser
10
+ */
11
+ export declare function run(_args: string[]): Promise<void>;
@@ -0,0 +1,240 @@
1
+ /**
2
+ * `npx co-od` (no args) — the zero-config magic start.
3
+ *
4
+ * 1. Check login → open browser if needed
5
+ * 2. Detect project dir + available CLIs
6
+ * 3. Find or create room for this project
7
+ * 4. Start local bridge in background
8
+ * 5. Open browser to room
9
+ * 6. Done — user types a task in the browser
10
+ */
11
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
12
+ import { join, basename } from "node:path";
13
+ import { homedir } from "node:os";
14
+ import { spawn } from "node:child_process";
15
+ import * as api from "../api-client.js";
16
+ import { getAdapter } from "../adapters/index.js";
17
+ function log(msg) {
18
+ console.error(` ${msg}`);
19
+ }
20
+ function detectProvider() {
21
+ // Check what's available, prefer claude > codex > openclaw
22
+ const checks = [
23
+ { name: "claude", bin: "claude" },
24
+ { name: "codex", bin: "codex" },
25
+ { name: "openclaw", bin: "zeroclaw" },
26
+ { name: "openclaw", bin: "openclaw" },
27
+ ];
28
+ const paths = (process.env.PATH || "").split(":");
29
+ for (const { name, bin } of checks) {
30
+ if (paths.some((dir) => existsSync(join(dir, bin))))
31
+ return name;
32
+ }
33
+ return "claude"; // default, will fail with helpful message later
34
+ }
35
+ async function ensureLoggedIn() {
36
+ const token = api.getSessionToken();
37
+ if (token)
38
+ return true;
39
+ log("Not logged in. Opening browser...\n");
40
+ // Run login command
41
+ const { run: loginRun } = await import("./login.js");
42
+ await loginRun([]);
43
+ // Check again
44
+ return !!api.getSessionToken();
45
+ }
46
+ function findExistingConfig(dir) {
47
+ const configPath = join(dir, ".co-od.json");
48
+ if (!existsSync(configPath))
49
+ return null;
50
+ try {
51
+ const data = JSON.parse(readFileSync(configPath, "utf-8"));
52
+ if (data.roomId)
53
+ return { roomId: data.roomId, roomName: data.roomName || basename(dir) };
54
+ }
55
+ catch { /* ignore */ }
56
+ return null;
57
+ }
58
+ function startBridgeBackground() {
59
+ // Check if bridge is already running
60
+ try {
61
+ const controller = new AbortController();
62
+ const timeout = setTimeout(() => controller.abort(), 1000);
63
+ // Can't use fetch synchronously, so spawn a quick check
64
+ const check = spawn("sh", ["-c", "curl -sf http://127.0.0.1:4786/health > /dev/null 2>&1"], {
65
+ stdio: "ignore",
66
+ });
67
+ check.on("close", (code) => {
68
+ clearTimeout(timeout);
69
+ if (code === 0) {
70
+ log("✓ local bridge already running");
71
+ return;
72
+ }
73
+ // Not running — start it
74
+ spawnBridge();
75
+ });
76
+ check.on("error", () => {
77
+ clearTimeout(timeout);
78
+ spawnBridge();
79
+ });
80
+ }
81
+ catch {
82
+ spawnBridge();
83
+ }
84
+ }
85
+ function spawnBridge() {
86
+ // Find the bridge script — check common locations
87
+ const possiblePaths = [
88
+ join(process.cwd(), "services/local-bridge/src/index.ts"),
89
+ join(process.cwd(), "node_modules/.bin/co-od-bridge"),
90
+ // Monorepo structure
91
+ join(process.cwd(), "../../services/local-bridge/src/index.ts"),
92
+ ];
93
+ // For now, just try to start via the co-ode monorepo if we're in it
94
+ const monorepoRoot = findMonorepoRoot(process.cwd());
95
+ if (monorepoRoot) {
96
+ const bridgePath = join(monorepoRoot, "services/local-bridge/src/index.ts");
97
+ if (existsSync(bridgePath)) {
98
+ const child = spawn("npx", ["ts-node", "--transpile-only", bridgePath], {
99
+ cwd: join(monorepoRoot, "services/local-bridge"),
100
+ stdio: "ignore",
101
+ detached: true,
102
+ env: { ...process.env, LOCAL_BRIDGE_PROJECT_ROOT: process.cwd() },
103
+ });
104
+ child.unref();
105
+ log("✓ local bridge started in background (port 4786)");
106
+ return;
107
+ }
108
+ }
109
+ log("⚠ local bridge not started — run your bridge manually or install the co-od bridge package");
110
+ }
111
+ function findMonorepoRoot(from) {
112
+ let dir = from;
113
+ for (let i = 0; i < 5; i++) {
114
+ if (existsSync(join(dir, "services/local-bridge")))
115
+ return dir;
116
+ const parent = join(dir, "..");
117
+ if (parent === dir)
118
+ break;
119
+ dir = parent;
120
+ }
121
+ return null;
122
+ }
123
+ async function openBrowser(url) {
124
+ try {
125
+ const open = (await import("open")).default;
126
+ await open(url);
127
+ }
128
+ catch {
129
+ log(`Open in browser: ${url}`);
130
+ }
131
+ }
132
+ export async function run(_args) {
133
+ const dir = process.cwd();
134
+ const projectName = basename(dir);
135
+ console.error(`
136
+ ┌─────────────────────────────┐
137
+ │ co-od │
138
+ │ agents + humans, │
139
+ │ one workspace │
140
+ └─────────────────────────────┘
141
+ `);
142
+ // Step 1: Login
143
+ log("[1/5] Checking authentication...");
144
+ const loggedIn = await ensureLoggedIn();
145
+ if (!loggedIn) {
146
+ log("✗ Login required. Run: co-od login");
147
+ process.exit(1);
148
+ }
149
+ log("✓ logged in\n");
150
+ // Step 2: Detect environment
151
+ log("[2/5] Detecting environment...");
152
+ const provider = detectProvider();
153
+ const adapter = getAdapter(provider);
154
+ const available = await adapter.available();
155
+ if (available) {
156
+ log(`✓ ${adapter.name} CLI available`);
157
+ }
158
+ else {
159
+ log(`⚠ no agent CLI found — install Claude Code, Codex, or ZeroClaw`);
160
+ log(` brew install claude (or) npm install -g @openai/codex`);
161
+ }
162
+ log(`✓ project: ${dir}\n`);
163
+ // Step 3: Find or create room
164
+ log("[3/5] Setting up room...");
165
+ const existing = findExistingConfig(dir);
166
+ let roomId;
167
+ let roomName;
168
+ if (existing) {
169
+ roomId = existing.roomId;
170
+ roomName = existing.roomName;
171
+ log(`✓ found existing room: ${roomName}`);
172
+ }
173
+ else {
174
+ // Create new room
175
+ roomName = projectName;
176
+ try {
177
+ const res = await api.post("/api/rooms", { name: roomName });
178
+ roomId = res.roomId || res.room?._id || "";
179
+ if (!roomId)
180
+ throw new Error("No room ID");
181
+ log(`✓ room created: ${roomName}`);
182
+ // Add default agent
183
+ const agentName = provider === "codex" ? "Codex Builder"
184
+ : provider === "openclaw" ? "OpenClaw Agent"
185
+ : "Claude Builder";
186
+ try {
187
+ await api.post(`/api/rooms/${roomId}/agents`, {
188
+ name: agentName,
189
+ type: "builder",
190
+ executionMode: provider === "codex" ? "local_codex" : "local_claude_code",
191
+ provider: provider === "codex" ? "openai" : provider === "openclaw" ? "openclaw" : "anthropic",
192
+ permissions: { "fs.read": true, "fs.write": true, "fs.applyPatch": true, "exec.run": true, "net.egress": false, "ports.expose": false, "secrets.read": false },
193
+ });
194
+ log(`✓ agent "${agentName}" added`);
195
+ }
196
+ catch {
197
+ log(`⚠ could not add agent (add one manually in the UI)`);
198
+ }
199
+ // Save config
200
+ const config = { roomId, roomName, projectDir: dir, provider, server: api.getBaseUrl(), createdAt: new Date().toISOString() };
201
+ try {
202
+ writeFileSync(join(dir, ".co-od.json"), JSON.stringify(config, null, 2) + "\n");
203
+ log(`✓ config saved: .co-od.json`);
204
+ }
205
+ catch { /* non-fatal */ }
206
+ // Save to global rooms map
207
+ try {
208
+ const globalDir = join(homedir(), ".co-od");
209
+ mkdirSync(globalDir, { recursive: true });
210
+ const mapPath = join(globalDir, "rooms.json");
211
+ let rooms = {};
212
+ if (existsSync(mapPath))
213
+ rooms = JSON.parse(readFileSync(mapPath, "utf-8"));
214
+ rooms[roomId] = { roomId, projectDir: dir, name: roomName };
215
+ writeFileSync(mapPath, JSON.stringify(rooms, null, 2) + "\n");
216
+ }
217
+ catch { /* non-fatal */ }
218
+ }
219
+ catch (err) {
220
+ log(`✗ failed to create room: ${err}`);
221
+ process.exit(1);
222
+ }
223
+ }
224
+ console.error("");
225
+ // Step 4: Start bridge
226
+ log("[4/5] Starting local bridge...");
227
+ startBridgeBackground();
228
+ console.error("");
229
+ // Step 5: Open browser
230
+ log("[5/5] Opening workspace...\n");
231
+ const url = `${api.getBaseUrl()}/rooms/${roomId}`;
232
+ await openBrowser(url);
233
+ console.error(` ✓ Ready! Your workspace is open at:`);
234
+ console.error(` ${url}\n`);
235
+ console.error(` Type a task in the browser to get started.`);
236
+ console.error(` Or use the CLI:\n`);
237
+ console.error(` co-od run ${roomId.slice(0, 12)} "your task here"`);
238
+ console.error(` co-od daemon ${roomId.slice(0, 12)} --auto-execute`);
239
+ console.error(` co-od share\n`);
240
+ }
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]" },
@@ -37,10 +38,16 @@ async function main() {
37
38
  console.log(VERSION);
38
39
  process.exit(0);
39
40
  }
40
- if (args.includes("--help") || args.includes("-h") || args.length === 0) {
41
+ if (args.includes("--help") || args.includes("-h")) {
41
42
  printHelp();
42
43
  process.exit(0);
43
44
  }
45
+ // No args = magic zero-config start
46
+ if (args.length === 0) {
47
+ const { run } = await import("./commands/start.js");
48
+ await run([]);
49
+ return;
50
+ }
44
51
  const command = args[0];
45
52
  const commandArgs = args.slice(1);
46
53
  // Show command-specific help
@@ -58,6 +65,11 @@ async function main() {
58
65
  }
59
66
  // Route to command
60
67
  switch (command) {
68
+ case "init": {
69
+ const { run } = await import("./commands/init.js");
70
+ await run(commandArgs);
71
+ break;
72
+ }
61
73
  case "login": {
62
74
  const { run } = await import("./commands/login.js");
63
75
  await run(commandArgs);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codegrammer/co-od",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
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": {