@codegrammer/co-od 0.1.4 → 0.1.6
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/commands/start.d.ts +11 -0
- package/dist/commands/start.js +276 -0
- package/dist/index.js +7 -1
- package/package.json +1 -1
|
@@ -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,276 @@
|
|
|
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
|
+
async function isBridgeRunning() {
|
|
133
|
+
try {
|
|
134
|
+
const res = await fetch("http://127.0.0.1:4786/health", { signal: AbortSignal.timeout(1500) });
|
|
135
|
+
return res.ok;
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
export async function run(args) {
|
|
142
|
+
const dir = process.cwd();
|
|
143
|
+
const projectName = basename(dir);
|
|
144
|
+
const existing = findExistingConfig(dir);
|
|
145
|
+
const bridgeUp = await isBridgeRunning();
|
|
146
|
+
const hasToken = !!api.getSessionToken();
|
|
147
|
+
// ── FAST PATH: everything's already set up ──
|
|
148
|
+
// If config exists + bridge running + logged in → just show status, don't open browser
|
|
149
|
+
if (existing && bridgeUp && hasToken && !args.includes("--open")) {
|
|
150
|
+
const url = `${api.getBaseUrl()}/rooms/${existing.roomId}`;
|
|
151
|
+
console.error(` co-od · ${existing.roomName}\n`);
|
|
152
|
+
console.error(` room: ${url}`);
|
|
153
|
+
console.error(` bridge: running (port 4786)`);
|
|
154
|
+
console.error(` project: ${dir}\n`);
|
|
155
|
+
console.error(` Everything is running. Commands:\n`);
|
|
156
|
+
console.error(` co-od run ${existing.roomId.slice(0, 12)} "your task" # single task`);
|
|
157
|
+
console.error(` co-od daemon ${existing.roomId.slice(0, 12)} --auto-execute # autonomous`);
|
|
158
|
+
console.error(` co-od share # invite teammate`);
|
|
159
|
+
console.error(` co-od --open # open browser\n`);
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
// ── SETUP PATH: first time or something needs fixing ──
|
|
163
|
+
console.error(`
|
|
164
|
+
┌─────────────────────────────┐
|
|
165
|
+
│ co-od │
|
|
166
|
+
│ agents + humans, │
|
|
167
|
+
│ one workspace │
|
|
168
|
+
└─────────────────────────────┘
|
|
169
|
+
`);
|
|
170
|
+
// Step 1: Login
|
|
171
|
+
if (!hasToken) {
|
|
172
|
+
log("[1/5] Checking authentication...");
|
|
173
|
+
const loggedIn = await ensureLoggedIn();
|
|
174
|
+
if (!loggedIn) {
|
|
175
|
+
log("✗ Login required. Run: co-od login");
|
|
176
|
+
process.exit(1);
|
|
177
|
+
}
|
|
178
|
+
log("✓ logged in\n");
|
|
179
|
+
}
|
|
180
|
+
else {
|
|
181
|
+
log("[1/5] ✓ logged in");
|
|
182
|
+
}
|
|
183
|
+
// Step 2: Detect environment
|
|
184
|
+
log("[2/5] Detecting environment...");
|
|
185
|
+
const provider = detectProvider();
|
|
186
|
+
const adapter = getAdapter(provider);
|
|
187
|
+
const available = await adapter.available();
|
|
188
|
+
if (available) {
|
|
189
|
+
log(`✓ ${adapter.name} CLI available`);
|
|
190
|
+
}
|
|
191
|
+
else {
|
|
192
|
+
log(`⚠ no agent CLI found — install Claude Code, Codex, or ZeroClaw`);
|
|
193
|
+
log(` brew install claude (or) npm install -g @openai/codex`);
|
|
194
|
+
}
|
|
195
|
+
log(`✓ project: ${dir}\n`);
|
|
196
|
+
// Step 3: Find or create room
|
|
197
|
+
log("[3/5] Setting up room...");
|
|
198
|
+
let roomId;
|
|
199
|
+
let roomName;
|
|
200
|
+
if (existing) {
|
|
201
|
+
roomId = existing.roomId;
|
|
202
|
+
roomName = existing.roomName;
|
|
203
|
+
log(`✓ found existing room: ${roomName}`);
|
|
204
|
+
}
|
|
205
|
+
else {
|
|
206
|
+
roomName = projectName;
|
|
207
|
+
try {
|
|
208
|
+
const res = await api.post("/api/rooms", { name: roomName });
|
|
209
|
+
roomId = res.roomId || res.room?._id || "";
|
|
210
|
+
if (!roomId)
|
|
211
|
+
throw new Error("No room ID");
|
|
212
|
+
log(`✓ room created: ${roomName}`);
|
|
213
|
+
// Add default agent
|
|
214
|
+
const agentName = provider === "codex" ? "Codex Builder"
|
|
215
|
+
: provider === "openclaw" ? "OpenClaw Agent"
|
|
216
|
+
: "Claude Builder";
|
|
217
|
+
try {
|
|
218
|
+
await api.post(`/api/rooms/${roomId}/agents`, {
|
|
219
|
+
name: agentName,
|
|
220
|
+
type: "builder",
|
|
221
|
+
executionMode: provider === "codex" ? "local_codex" : "local_claude_code",
|
|
222
|
+
provider: provider === "codex" ? "openai" : provider === "openclaw" ? "openclaw" : "anthropic",
|
|
223
|
+
permissions: { "fs.read": true, "fs.write": true, "fs.applyPatch": true, "exec.run": true, "net.egress": false, "ports.expose": false, "secrets.read": false },
|
|
224
|
+
});
|
|
225
|
+
log(`✓ agent "${agentName}" added`);
|
|
226
|
+
}
|
|
227
|
+
catch {
|
|
228
|
+
log(`⚠ could not add agent (add one manually in the UI)`);
|
|
229
|
+
}
|
|
230
|
+
// Save config
|
|
231
|
+
const config = { roomId, roomName, projectDir: dir, provider, server: api.getBaseUrl(), createdAt: new Date().toISOString() };
|
|
232
|
+
try {
|
|
233
|
+
writeFileSync(join(dir, ".co-od.json"), JSON.stringify(config, null, 2) + "\n");
|
|
234
|
+
log(`✓ config saved: .co-od.json`);
|
|
235
|
+
}
|
|
236
|
+
catch { /* non-fatal */ }
|
|
237
|
+
// Save to global rooms map
|
|
238
|
+
try {
|
|
239
|
+
const globalDir = join(homedir(), ".co-od");
|
|
240
|
+
mkdirSync(globalDir, { recursive: true });
|
|
241
|
+
const mapPath = join(globalDir, "rooms.json");
|
|
242
|
+
let rooms = {};
|
|
243
|
+
if (existsSync(mapPath))
|
|
244
|
+
rooms = JSON.parse(readFileSync(mapPath, "utf-8"));
|
|
245
|
+
rooms[roomId] = { roomId, projectDir: dir, name: roomName };
|
|
246
|
+
writeFileSync(mapPath, JSON.stringify(rooms, null, 2) + "\n");
|
|
247
|
+
}
|
|
248
|
+
catch { /* non-fatal */ }
|
|
249
|
+
}
|
|
250
|
+
catch (err) {
|
|
251
|
+
log(`✗ failed to create room: ${err}`);
|
|
252
|
+
process.exit(1);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
console.error("");
|
|
256
|
+
// Step 4: Start bridge (only if not already running)
|
|
257
|
+
if (bridgeUp) {
|
|
258
|
+
log("[4/5] ✓ local bridge already running");
|
|
259
|
+
}
|
|
260
|
+
else {
|
|
261
|
+
log("[4/5] Starting local bridge...");
|
|
262
|
+
startBridgeBackground();
|
|
263
|
+
}
|
|
264
|
+
console.error("");
|
|
265
|
+
// Step 5: Open browser
|
|
266
|
+
log("[5/5] Opening workspace...\n");
|
|
267
|
+
const url = `${api.getBaseUrl()}/rooms/${roomId}`;
|
|
268
|
+
await openBrowser(url);
|
|
269
|
+
console.error(` ✓ Ready! Your workspace is open at:`);
|
|
270
|
+
console.error(` ${url}\n`);
|
|
271
|
+
console.error(` Type a task in the browser to get started.`);
|
|
272
|
+
console.error(` Or use the CLI:\n`);
|
|
273
|
+
console.error(` co-od run ${roomId.slice(0, 12)} "your task here"`);
|
|
274
|
+
console.error(` co-od daemon ${roomId.slice(0, 12)} --auto-execute`);
|
|
275
|
+
console.error(` co-od share\n`);
|
|
276
|
+
}
|
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")
|
|
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
|