@0dai-dev/cli 2.0.0 → 2.1.0

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.
Files changed (2) hide show
  1. package/bin/0dai.js +219 -38
  2. package/package.json +1 -1
package/bin/0dai.js CHANGED
@@ -7,8 +7,12 @@ const fs = require("fs");
7
7
  const path = require("path");
8
8
  const os = require("os");
9
9
 
10
- const VERSION = "2.0.0";
10
+ const VERSION = "2.1.0";
11
11
  const API_URL = process.env.ODAI_API_URL || "https://api.0dai.dev";
12
+ const T = process.stdout.isTTY ? "\x1b[38;2;45;212;168m" : ""; // teal
13
+ const R = process.stdout.isTTY ? "\x1b[0m" : ""; // reset
14
+ const D = process.stdout.isTTY ? "\x1b[2m" : ""; // dim
15
+ const log = (msg) => console.log(`${T}[0dai]${R} ${msg}`);
12
16
  const CONFIG_DIR = path.join(os.homedir(), ".0dai");
13
17
  const AUTH_FILE = path.join(CONFIG_DIR, "auth.json");
14
18
  const VERSION_CHECK_FILE = path.join(CONFIG_DIR, ".version_check");
@@ -54,7 +58,7 @@ function apiCall(endpoint, data) {
54
58
  const url = new URL(endpoint, API_URL);
55
59
  const mod = url.protocol === "https:" ? https : http;
56
60
  const body = data ? JSON.stringify(data) : null;
57
- const headers = { "Content-Type": "application/json", "X-Device-ID": deviceFingerprint() };
61
+ const headers = { "Content-Type": "application/json", "X-Device-ID": deviceFingerprint(), "X-CLI-Version": VERSION };
58
62
 
59
63
  try {
60
64
  const auth = JSON.parse(fs.readFileSync(AUTH_FILE, "utf8"));
@@ -111,10 +115,10 @@ function collectMetadata(target) {
111
115
  }
112
116
 
113
117
  const clis = [];
118
+ const { execSync } = require("child_process");
114
119
  for (const cli of ["claude", "codex", "opencode", "gemini", "aider"]) {
115
120
  try {
116
- const { execSync } = require("child_process");
117
- execSync(`which ${cli}`, { stdio: "ignore" });
121
+ execSync(`command -v ${cli}`, { stdio: "ignore", shell: "/bin/sh", env: process.env });
118
122
  clis.push(cli);
119
123
  } catch {}
120
124
  }
@@ -137,21 +141,21 @@ function writeFiles(target, files) {
137
141
  } catch { created++; }
138
142
  fs.writeFileSync(p, content, "utf8");
139
143
  }
140
- console.log(`[0dai] ${created} created, ${updated} updated, ${unchanged} unchanged`);
144
+ log(`${created} created, ${updated} updated, ${unchanged} unchanged`);
141
145
  return created + updated;
142
146
  }
143
147
 
144
148
  async function cmdInit(target) {
145
149
  if (fs.existsSync(path.join(target, "ai", "VERSION"))) {
146
150
  const v = fs.readFileSync(path.join(target, "ai", "VERSION"), "utf8").trim();
147
- console.log(`[0dai] ai/ layer already exists (v${v}). Run '0dai sync' to update.`);
151
+ log(`ai/ layer already exists (v${v}). Run '0dai sync' to update.`);
148
152
  return;
149
153
  }
150
154
 
151
- console.log("[0dai] collecting project metadata...");
155
+ log("collecting project metadata...");
152
156
  const { projectFiles, fileContents, clis } = collectMetadata(target);
153
157
 
154
- console.log(`[0dai] sending to API (${projectFiles.length} files, ${clis.length} CLIs)...`);
158
+ log(`sending to API (${projectFiles.length} files, ${clis.length} CLIs)...`);
155
159
  const result = await apiCall("/v1/init", {
156
160
  project_files: projectFiles,
157
161
  file_contents: fileContents,
@@ -160,15 +164,15 @@ async function cmdInit(target) {
160
164
 
161
165
  if (result.error) {
162
166
  if (result.hint) {
163
- console.log(`\n[0dai] ${result.message || result.error}`);
167
+ log(`${result.message || result.error}`);
164
168
  console.log(` ${result.hint}\n`);
165
169
  } else {
166
- console.error(`[0dai] error: ${result.error}`);
170
+ log(`error: ${result.error}`);
167
171
  }
168
172
  process.exit(1);
169
173
  }
170
174
 
171
- console.log(`[0dai] detected: ${result.stack || "?"}`);
175
+ log(`detected: ${result.stack || "?"}`);
172
176
  writeFiles(target, result.files || {});
173
177
 
174
178
  // Add to .gitignore
@@ -178,15 +182,38 @@ async function cmdInit(target) {
178
182
  if (!text.includes(".0dai")) fs.appendFileSync(gi, "\n.0dai/\n");
179
183
  } catch {}
180
184
 
181
- console.log(`[0dai] initialized (${result.file_count || "?"} files)`);
185
+ log(`initialized (${result.file_count || "?"} files)`);
182
186
  console.log(" skills: /build /review /status /feedback /bugfix /delegate");
187
+
188
+ // Auto-feedback for free tiers (non-optional — provides usage data for product improvement)
189
+ const plan = result.plan || "trial";
190
+ if (plan === "trial" || plan === "free") {
191
+ const autoReport = {
192
+ project: path.basename(target),
193
+ stack_detected: result.stack || "?",
194
+ agent_cli: "cli",
195
+ verdict: "auto",
196
+ _auto: true,
197
+ _plan: plan,
198
+ _cli_version: VERSION,
199
+ _files_generated: result.file_count || 0,
200
+ };
201
+ apiCall("/v1/feedback", { report: autoReport }).catch(() => {});
202
+ console.log(` ${D}usage data sent to improve 0dai (disable with Essential+ plan)${R}`);
203
+ }
183
204
  }
184
205
 
185
206
  async function cmdSync(target) {
186
- const { projectFiles, fileContents, clis } = collectMetadata(target);
187
-
188
- let version = "unknown", stack = "generic", agents = [];
207
+ // Quick local check: skip API if already at current version
208
+ let version = "unknown";
189
209
  try { version = fs.readFileSync(path.join(target, "ai", "VERSION"), "utf8").trim(); } catch {}
210
+ if (version === VERSION) {
211
+ log("already up to date (v" + version + ")");
212
+ return;
213
+ }
214
+
215
+ const { projectFiles, fileContents, clis } = collectMetadata(target);
216
+ let stack = "generic", agents = [];
190
217
  try {
191
218
  const d = JSON.parse(fs.readFileSync(path.join(target, "ai", "manifest", "discovery.json"), "utf8"));
192
219
  stack = d.stack || "generic";
@@ -217,24 +244,24 @@ async function cmdSync(target) {
217
244
  current_files: currentFiles, file_contents: fileContents,
218
245
  });
219
246
 
220
- if (result.error) { console.error(`[0dai] error: ${result.error}`); process.exit(1); }
247
+ if (result.error) { log(`error: ${result.error}`); process.exit(1); }
221
248
 
222
249
  const updated = result.files_updated || {};
223
250
  if (Object.keys(updated).length) writeFiles(target, updated);
224
- else console.log("[0dai] already up to date");
251
+ else log("already up to date");
225
252
  }
226
253
 
227
254
  async function cmdDetect(target) {
228
255
  const { projectFiles } = collectMetadata(target);
229
256
  const result = await apiCall("/v1/detect", { files: projectFiles });
230
- if (result.error) { console.error(`[0dai] error: ${result.error}`); return; }
257
+ if (result.error) { log(`error: ${result.error}`); return; }
231
258
  console.log(`stack: ${result.stack || "?"}`);
232
259
  console.log(`clis: ${(result.available_clis || []).join(",")}`);
233
260
  }
234
261
 
235
262
  function cmdDoctor(target) {
236
263
  const ai = path.join(target, "ai");
237
- if (!fs.existsSync(ai)) { console.log("[0dai] no ai/ layer. Run '0dai init' first."); return; }
264
+ if (!fs.existsSync(ai)) { log("no ai/ layer. Run '0dai init' first."); return; }
238
265
  let v = "?";
239
266
  try { v = fs.readFileSync(path.join(ai, "VERSION"), "utf8").trim(); } catch {}
240
267
  const checks = {
@@ -246,7 +273,7 @@ function cmdDoctor(target) {
246
273
  "AGENTS.md": fs.existsSync(path.join(target, "AGENTS.md")),
247
274
  };
248
275
  const ok = Object.values(checks).every(Boolean);
249
- console.log(`[0dai] v${v} — ${ok ? "healthy" : "issues found"}`);
276
+ log(`v${v} — ${ok ? "healthy" : "issues found"}`);
250
277
  for (const [name, exists] of Object.entries(checks)) console.log(` ${exists ? "ok" : "MISSING"}: ${name}`);
251
278
  }
252
279
 
@@ -255,7 +282,7 @@ function cmdStatus(target) {
255
282
  let v = "?", stack = "?";
256
283
  try { v = fs.readFileSync(path.join(ai, "VERSION"), "utf8").trim(); } catch {}
257
284
  try { stack = JSON.parse(fs.readFileSync(path.join(ai, "manifest", "discovery.json"), "utf8")).stack || "?"; } catch {}
258
- console.log(`[0dai] v${v} | stack: ${stack}`);
285
+ log(`v${v} | stack: ${stack}`);
259
286
 
260
287
  const count = (dir) => { try { return fs.readdirSync(dir).filter(f => f.endsWith(".json")).length; } catch { return 0; } };
261
288
  const q = count(path.join(ai, "swarm", "queue"));
@@ -271,17 +298,18 @@ function cmdStatus(target) {
271
298
 
272
299
  async function checkVersion() {
273
300
  try {
274
- // Only check once per day
301
+ // Check interval: 1 hour during debug, configurable via env
302
+ const intervalSec = parseInt(process.env.ODAI_UPDATE_CHECK_INTERVAL || "3600");
275
303
  let lastCheck = 0;
276
304
  try { lastCheck = parseFloat(fs.readFileSync(VERSION_CHECK_FILE, "utf8")); } catch {}
277
- if (Date.now() / 1000 - lastCheck < 86400) return;
305
+ if (Date.now() / 1000 - lastCheck < intervalSec) return;
278
306
 
279
307
  fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
280
308
  fs.writeFileSync(VERSION_CHECK_FILE, String(Date.now() / 1000));
281
309
 
282
310
  const result = await apiCall("/v1/version");
283
311
  if (result.version && result.version !== VERSION) {
284
- console.log(`\n[0dai] Update available: ${VERSION} → ${result.version}`);
312
+ log(`Update available: ${VERSION} → ${result.version}`);
285
313
  console.log(` Run: npm update -g @0dai-dev/cli\n`);
286
314
  }
287
315
  } catch {}
@@ -290,12 +318,12 @@ async function checkVersion() {
290
318
  async function cmdAuthLogin() {
291
319
  // Step 1: request device code
292
320
  const result = await apiCall("/v1/auth/device", { client_id: "cli" });
293
- if (result.error) { console.error(`[0dai] error: ${result.error}`); process.exit(1); }
321
+ if (result.error) { log(`error: ${result.error}`); process.exit(1); }
294
322
 
295
- console.log(`[0dai] Open this URL in your browser:\n`);
323
+ log(`Open this URL in your browser:\n`);
296
324
  console.log(` ${result.verification_uri}\n`);
297
- console.log(`[0dai] Enter code: ${result.user_code}\n`);
298
- console.log("[0dai] Waiting for authorization...");
325
+ log(`Enter code: ${result.user_code}\n`);
326
+ log("Waiting for authorization...");
299
327
 
300
328
  // Step 2: poll for token
301
329
  const interval = (result.interval || 5) * 1000;
@@ -314,28 +342,28 @@ async function cmdAuthLogin() {
314
342
  authenticated_at: new Date().toISOString(),
315
343
  expires_at: poll.expires_at,
316
344
  }, null, 2) + "\n", { mode: 0o600 });
317
- console.log(`[0dai] Logged in as ${poll.email} (${poll.plan} plan)`);
345
+ log(`Logged in as ${poll.email} (${poll.plan} plan)`);
318
346
  return;
319
347
  }
320
348
  if (poll.error && poll.error !== "authorization_pending") {
321
- console.error(`[0dai] ${poll.error}`);
349
+ log(`${poll.error}`);
322
350
  process.exit(1);
323
351
  }
324
352
  process.stdout.write(".");
325
353
  }
326
- console.error("\n[0dai] Authorization timed out. Try again.");
354
+ log("Authorization timed out. Try again.");
327
355
  process.exit(1);
328
356
  }
329
357
 
330
358
  function cmdAuthLogout() {
331
359
  try { fs.unlinkSync(AUTH_FILE); } catch {}
332
- console.log("[0dai] Logged out");
360
+ log("Logged out");
333
361
  }
334
362
 
335
363
  async function cmdAuthStatus() {
336
364
  try {
337
365
  const auth = JSON.parse(fs.readFileSync(AUTH_FILE, "utf8"));
338
- console.log(`[0dai] ${auth.email} (${auth.plan} plan)`);
366
+ log(`${auth.email} (${auth.plan} plan)`);
339
367
  // Get usage from API
340
368
  const status = await apiCall("/v1/auth/status");
341
369
  if (status.usage_today) {
@@ -344,8 +372,155 @@ async function cmdAuthStatus() {
344
372
  console.log(` ${k}: ${v} / ${status.limits[k]}`);
345
373
  }
346
374
  } catch {
347
- console.log("[0dai] Not logged in. Run: 0dai auth login");
375
+ log("Not logged in. Run: 0dai auth login");
376
+ }
377
+ }
378
+
379
+ async function cmdFeedbackPush(target) {
380
+ const ai = path.join(target, "ai", "feedback");
381
+ const reports = [];
382
+ try {
383
+ for (const f of fs.readdirSync(ai)) {
384
+ if (f.endsWith("-report.json") || (f.endsWith(".json") && f.match(/^\d{8}/))) {
385
+ try {
386
+ const d = JSON.parse(fs.readFileSync(path.join(ai, f), "utf8"));
387
+ if (d.project || d.verdict) reports.push(d);
388
+ } catch {}
389
+ }
390
+ }
391
+ } catch {}
392
+
393
+ if (!reports.length) { log("no feedback reports found"); return; }
394
+
395
+ // Send each report via API
396
+ for (const report of reports) {
397
+ log(`pushing: ${report.project || "?"} (${report.verdict || "?"})`);
398
+ const result = await apiCall("/v1/feedback", { report });
399
+ if (result.issue) {
400
+ log(`issue created: ${result.issue}`);
401
+ } else if (result.received) {
402
+ log("received by server");
403
+ } else {
404
+ log(`error: ${result.error || "unknown"}`);
405
+ }
406
+ }
407
+ }
408
+
409
+ // --- Session (local, file-based) ---
410
+ function cmdSession(target, sub, args) {
411
+ const sessFile = path.join(target, "ai", "sessions", "active.json");
412
+ const sessDir = path.dirname(sessFile);
413
+
414
+ if (sub === "save") {
415
+ fs.mkdirSync(sessDir, { recursive: true });
416
+ const goal = args.find((_, i) => args[i - 1] === "--goal") || "";
417
+ const summary = args.find((_, i) => args[i - 1] === "--summary") || "";
418
+ const session = {
419
+ id: `sess-${Date.now()}`,
420
+ started: new Date().toISOString(),
421
+ current_agent: "cli",
422
+ task: { goal: goal || summary || "active session", status: "in_progress" },
423
+ handoff_notes: summary,
424
+ context: { files_touched: [] },
425
+ };
426
+ if (fs.existsSync(sessFile)) {
427
+ const existing = JSON.parse(fs.readFileSync(sessFile, "utf8"));
428
+ existing.handoff_notes = summary || existing.handoff_notes;
429
+ if (goal) existing.task.goal = goal;
430
+ existing.updated = new Date().toISOString();
431
+ fs.writeFileSync(sessFile, JSON.stringify(existing, null, 2));
432
+ log("session updated");
433
+ } else {
434
+ fs.writeFileSync(sessFile, JSON.stringify(session, null, 2));
435
+ log(`session started: ${session.id}`);
436
+ }
437
+ return;
438
+ }
439
+ if (sub === "status") {
440
+ if (!fs.existsSync(sessFile)) { log("no active session"); return; }
441
+ const s = JSON.parse(fs.readFileSync(sessFile, "utf8"));
442
+ log(`session: ${(s.task || {}).goal || "?"}`);
443
+ console.log(` agent: ${s.current_agent || "?"}`);
444
+ if (s.handoff_notes) console.log(` handoff: ${s.handoff_notes}`);
445
+ return;
446
+ }
447
+ if (sub === "complete") {
448
+ if (!fs.existsSync(sessFile)) { log("no active session"); return; }
449
+ const archiveDir = path.join(target, "ai", "sessions", "archive");
450
+ fs.mkdirSync(archiveDir, { recursive: true });
451
+ const s = JSON.parse(fs.readFileSync(sessFile, "utf8"));
452
+ fs.writeFileSync(path.join(archiveDir, `${s.id || "session"}.json`), JSON.stringify(s, null, 2));
453
+ fs.unlinkSync(sessFile);
454
+ log(`session ${s.id} archived`);
455
+ return;
456
+ }
457
+ console.log("Usage: 0dai session [save|status|complete] [--goal '...'] [--summary '...']");
458
+ }
459
+
460
+ // --- Swarm (local, file-based) ---
461
+ function cmdSwarm(target, sub, args) {
462
+ const swarmDir = path.join(target, "ai", "swarm");
463
+ const queueDir = path.join(swarmDir, "queue");
464
+
465
+ if (sub === "status") {
466
+ const count = (d) => { try { return fs.readdirSync(d).filter(f => f.endsWith(".json")).length; } catch { return 0; } };
467
+ const q = count(path.join(swarmDir, "queue"));
468
+ const a = count(path.join(swarmDir, "active"));
469
+ const d = count(path.join(swarmDir, "done"));
470
+ log(`swarm: ${q} queued, ${a} active, ${d} done`);
471
+ return;
472
+ }
473
+ if (sub === "add" || sub === "delegate") {
474
+ fs.mkdirSync(queueDir, { recursive: true });
475
+ const task = args.find((_, i) => args[i - 1] === "--task") || "untitled";
476
+ const forAgent = args.find((_, i) => ["--for", "--to"].includes(args[i - 1])) || "any";
477
+ const id = `swarm-${Date.now()}`;
478
+ const t = { id, title: task, assigned_to: forAgent, status: "pending", created_at: new Date().toISOString(), created_by: "cli" };
479
+ fs.writeFileSync(path.join(queueDir, `${id}.json`), JSON.stringify(t, null, 2));
480
+ log(`task created: ${id} → ${forAgent}`);
481
+ return;
482
+ }
483
+ if (sub === "budget") {
484
+ const budgetFile = path.join(swarmDir, "budget.json");
485
+ if (!fs.existsSync(budgetFile)) { log("no budget data yet"); return; }
486
+ const b = JSON.parse(fs.readFileSync(budgetFile, "utf8"));
487
+ log(`total: $${(b.total_spent || 0).toFixed(4)} (${Object.keys(b.tasks || {}).length} tasks)`);
488
+ return;
489
+ }
490
+ console.log("Usage: 0dai swarm [status|add|delegate|budget] [--task '...'] [--to agent]");
491
+ }
492
+
493
+ // --- Feedback (local + API push) ---
494
+ async function cmdFeedback(target, sub, args) {
495
+ const fbDir = path.join(target, "ai", "feedback");
496
+
497
+ if (sub === "push") {
498
+ return cmdFeedbackPush(target);
499
+ }
500
+ if (sub === "log") {
501
+ const type = args.find((_, i) => args[i - 1] === "--type") || "suggestion";
502
+ const detail = args.find((_, i) => args[i - 1] === "--detail") || "";
503
+ if (!detail) { console.log("Usage: 0dai feedback log --type bug|suggestion|friction|positive --detail '...'"); return; }
504
+ fs.mkdirSync(fbDir, { recursive: true });
505
+ const entry = JSON.stringify({ ts: new Date().toISOString(), type, detail, agent: "cli" });
506
+ fs.appendFileSync(path.join(fbDir, "operational.jsonl"), entry + "\n");
507
+ log(`logged: [${type}] ${detail.slice(0, 60)}`);
508
+ return;
509
+ }
510
+ if (sub === "list") {
511
+ try {
512
+ const files = fs.readdirSync(fbDir).filter(f => f.endsWith("-report.json"));
513
+ if (!files.length) { log("no reports"); return; }
514
+ for (const f of files) {
515
+ try {
516
+ const d = JSON.parse(fs.readFileSync(path.join(fbDir, f), "utf8"));
517
+ console.log(` ${f}: ${d.verdict || "?"} (${d.project || "?"})`);
518
+ } catch {}
519
+ }
520
+ } catch { log("no feedback directory"); }
521
+ return;
348
522
  }
523
+ console.log("Usage: 0dai feedback [push|log|list] [--type ...] [--detail '...']");
349
524
  }
350
525
 
351
526
  async function main() {
@@ -374,15 +549,21 @@ async function main() {
374
549
  console.log("Usage: 0dai auth [login|logout|status]");
375
550
  }
376
551
  break;
377
- case "--version": console.log(`0dai ${VERSION}`); break;
552
+ case "session": cmdSession(target, sub, args); break;
553
+ case "swarm": cmdSwarm(target, sub, args); break;
554
+ case "feedback": await cmdFeedback(target, sub, args); break;
555
+ case "--version": console.log(`${T}0dai${R} ${VERSION}`); break;
378
556
  case "help": case "--help": case "-h":
379
- console.log(`0dai v${VERSION} — One config for 5 AI agent CLIs\n`);
557
+ console.log(`\n ${T}0dai${R} v${VERSION} — One config for 5 AI agent CLIs\n`);
380
558
  console.log("Commands:");
381
559
  console.log(" init Initialize ai/ layer (via API)");
382
560
  console.log(" sync Update ai/ layer (via API)");
383
561
  console.log(" detect Show detected stack");
384
562
  console.log(" doctor Check health");
385
563
  console.log(" status Show maturity, swarm, session");
564
+ console.log(" session save Save session for roaming");
565
+ console.log(" swarm status Task queue & delegation");
566
+ console.log(" feedback push Send feedback to 0dai");
386
567
  console.log(" auth login Authenticate (device code flow)");
387
568
  console.log(" auth logout Remove credentials");
388
569
  console.log(" auth status Show account and usage");
@@ -390,9 +571,9 @@ async function main() {
390
571
  console.log("https://0dai.dev");
391
572
  break;
392
573
  default:
393
- console.error(`[0dai] unknown command: ${cmd}. Run '0dai --help'`);
574
+ log(`unknown command: ${cmd}. Run '0dai --help'`);
394
575
  process.exit(1);
395
576
  }
396
577
  }
397
578
 
398
- main().catch((e) => { console.error(`[0dai] ${e.message}`); process.exit(1); });
579
+ main().catch((e) => { log(`${e.message}`); process.exit(1); });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@0dai-dev/cli",
3
- "version": "2.0.0",
3
+ "version": "2.1.0",
4
4
  "description": "One config layer for 5 AI agent CLIs — Claude Code, Codex, OpenCode, Gemini, Aider",
5
5
  "bin": {
6
6
  "0dai": "./bin/0dai.js"