@0dai-dev/cli 2.0.1 → 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 +183 -7
  2. package/package.json +1 -1
package/bin/0dai.js CHANGED
@@ -7,7 +7,7 @@ const fs = require("fs");
7
7
  const path = require("path");
8
8
  const os = require("os");
9
9
 
10
- const VERSION = "2.0.1";
10
+ const VERSION = "2.1.0";
11
11
  const API_URL = process.env.ODAI_API_URL || "https://api.0dai.dev";
12
12
  const T = process.stdout.isTTY ? "\x1b[38;2;45;212;168m" : ""; // teal
13
13
  const R = process.stdout.isTTY ? "\x1b[0m" : ""; // reset
@@ -58,7 +58,7 @@ function apiCall(endpoint, data) {
58
58
  const url = new URL(endpoint, API_URL);
59
59
  const mod = url.protocol === "https:" ? https : http;
60
60
  const body = data ? JSON.stringify(data) : null;
61
- 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 };
62
62
 
63
63
  try {
64
64
  const auth = JSON.parse(fs.readFileSync(AUTH_FILE, "utf8"));
@@ -115,10 +115,10 @@ function collectMetadata(target) {
115
115
  }
116
116
 
117
117
  const clis = [];
118
+ const { execSync } = require("child_process");
118
119
  for (const cli of ["claude", "codex", "opencode", "gemini", "aider"]) {
119
120
  try {
120
- const { execSync } = require("child_process");
121
- execSync(`which ${cli}`, { stdio: "ignore" });
121
+ execSync(`command -v ${cli}`, { stdio: "ignore", shell: "/bin/sh", env: process.env });
122
122
  clis.push(cli);
123
123
  } catch {}
124
124
  }
@@ -184,13 +184,36 @@ async function cmdInit(target) {
184
184
 
185
185
  log(`initialized (${result.file_count || "?"} files)`);
186
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
+ }
187
204
  }
188
205
 
189
206
  async function cmdSync(target) {
190
- const { projectFiles, fileContents, clis } = collectMetadata(target);
191
-
192
- let version = "unknown", stack = "generic", agents = [];
207
+ // Quick local check: skip API if already at current version
208
+ let version = "unknown";
193
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 = [];
194
217
  try {
195
218
  const d = JSON.parse(fs.readFileSync(path.join(target, "ai", "manifest", "discovery.json"), "utf8"));
196
219
  stack = d.stack || "generic";
@@ -353,6 +376,153 @@ async function cmdAuthStatus() {
353
376
  }
354
377
  }
355
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;
522
+ }
523
+ console.log("Usage: 0dai feedback [push|log|list] [--type ...] [--detail '...']");
524
+ }
525
+
356
526
  async function main() {
357
527
  const args = process.argv.slice(2);
358
528
  let target = process.cwd();
@@ -379,6 +549,9 @@ async function main() {
379
549
  console.log("Usage: 0dai auth [login|logout|status]");
380
550
  }
381
551
  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;
382
555
  case "--version": console.log(`${T}0dai${R} ${VERSION}`); break;
383
556
  case "help": case "--help": case "-h":
384
557
  console.log(`\n ${T}0dai${R} v${VERSION} — One config for 5 AI agent CLIs\n`);
@@ -388,6 +561,9 @@ async function main() {
388
561
  console.log(" detect Show detected stack");
389
562
  console.log(" doctor Check health");
390
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");
391
567
  console.log(" auth login Authenticate (device code flow)");
392
568
  console.log(" auth logout Remove credentials");
393
569
  console.log(" auth status Show account and usage");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@0dai-dev/cli",
3
- "version": "2.0.1",
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"