@codegrammer/co-od 0.1.4 → 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,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
@@ -38,10 +38,16 @@ async function main() {
38
38
  console.log(VERSION);
39
39
  process.exit(0);
40
40
  }
41
- if (args.includes("--help") || args.includes("-h") || args.length === 0) {
41
+ if (args.includes("--help") || args.includes("-h")) {
42
42
  printHelp();
43
43
  process.exit(0);
44
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
+ }
45
51
  const command = args[0];
46
52
  const commandArgs = args.slice(1);
47
53
  // Show command-specific help
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codegrammer/co-od",
3
- "version": "0.1.4",
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": {