@0dai-dev/cli 2.0.1 → 2.1.1

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 +176 -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.1";
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,30 @@ 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
+ // Send anonymous usage ping (stack + file count, no project data)
189
+ apiCall("/v1/feedback", { report: {
190
+ stack_detected: result.stack || "?", _auto: true, _plan: result.plan || "trial",
191
+ _cli_version: VERSION, _files_generated: result.file_count || 0,
192
+ }}).catch(() => {});
193
+
194
+ // Encourage feedback
195
+ console.log(`\n ${T}Tip:${R} Send feedback to earn +5 init/day for 7 days:`);
196
+ console.log(` ${D}0dai feedback log --type positive --detail "what worked"${R}`);
197
+ console.log(` ${D}0dai feedback push${R}`);
187
198
  }
188
199
 
189
200
  async function cmdSync(target) {
190
- const { projectFiles, fileContents, clis } = collectMetadata(target);
191
-
192
- let version = "unknown", stack = "generic", agents = [];
201
+ // Quick local check: skip API if already at current version
202
+ let version = "unknown";
193
203
  try { version = fs.readFileSync(path.join(target, "ai", "VERSION"), "utf8").trim(); } catch {}
204
+ if (version === VERSION) {
205
+ log("already up to date (v" + version + ")");
206
+ return;
207
+ }
208
+
209
+ const { projectFiles, fileContents, clis } = collectMetadata(target);
210
+ let stack = "generic", agents = [];
194
211
  try {
195
212
  const d = JSON.parse(fs.readFileSync(path.join(target, "ai", "manifest", "discovery.json"), "utf8"));
196
213
  stack = d.stack || "generic";
@@ -353,6 +370,152 @@ async function cmdAuthStatus() {
353
370
  }
354
371
  }
355
372
 
373
+ async function cmdFeedbackPush(target) {
374
+ const ai = path.join(target, "ai", "feedback");
375
+ const reports = [];
376
+ try {
377
+ for (const f of fs.readdirSync(ai)) {
378
+ if (f.endsWith("-report.json") || (f.endsWith(".json") && f.match(/^\d{8}/))) {
379
+ try {
380
+ const d = JSON.parse(fs.readFileSync(path.join(ai, f), "utf8"));
381
+ if (d.project || d.verdict) reports.push(d);
382
+ } catch {}
383
+ }
384
+ }
385
+ } catch {}
386
+
387
+ if (!reports.length) { log("no feedback reports found"); return; }
388
+
389
+ // Send each report via API
390
+ for (const report of reports) {
391
+ log(`pushing: ${report.project || "?"} (${report.verdict || "?"})`);
392
+ const result = await apiCall("/v1/feedback", { report });
393
+ if (result.received) {
394
+ log(`received${result.issue ? `: ${result.issue}` : ""}`);
395
+ if (result.bonus) log(`${T}bonus:${R} ${result.bonus}`);
396
+ } else {
397
+ log(`error: ${result.error || "unknown"}`);
398
+ }
399
+ }
400
+ }
401
+
402
+ // --- Session (local, file-based) ---
403
+ function cmdSession(target, sub, args) {
404
+ const sessFile = path.join(target, "ai", "sessions", "active.json");
405
+ const sessDir = path.dirname(sessFile);
406
+
407
+ if (sub === "save") {
408
+ fs.mkdirSync(sessDir, { recursive: true });
409
+ const goal = args.find((_, i) => args[i - 1] === "--goal") || "";
410
+ const summary = args.find((_, i) => args[i - 1] === "--summary") || "";
411
+ const session = {
412
+ id: `sess-${Date.now()}`,
413
+ started: new Date().toISOString(),
414
+ current_agent: "cli",
415
+ task: { goal: goal || summary || "active session", status: "in_progress" },
416
+ handoff_notes: summary,
417
+ context: { files_touched: [] },
418
+ };
419
+ if (fs.existsSync(sessFile)) {
420
+ const existing = JSON.parse(fs.readFileSync(sessFile, "utf8"));
421
+ existing.handoff_notes = summary || existing.handoff_notes;
422
+ if (goal) existing.task.goal = goal;
423
+ existing.updated = new Date().toISOString();
424
+ fs.writeFileSync(sessFile, JSON.stringify(existing, null, 2));
425
+ log("session updated");
426
+ } else {
427
+ fs.writeFileSync(sessFile, JSON.stringify(session, null, 2));
428
+ log(`session started: ${session.id}`);
429
+ }
430
+ return;
431
+ }
432
+ if (sub === "status") {
433
+ if (!fs.existsSync(sessFile)) { log("no active session"); return; }
434
+ const s = JSON.parse(fs.readFileSync(sessFile, "utf8"));
435
+ log(`session: ${(s.task || {}).goal || "?"}`);
436
+ console.log(` agent: ${s.current_agent || "?"}`);
437
+ if (s.handoff_notes) console.log(` handoff: ${s.handoff_notes}`);
438
+ return;
439
+ }
440
+ if (sub === "complete") {
441
+ if (!fs.existsSync(sessFile)) { log("no active session"); return; }
442
+ const archiveDir = path.join(target, "ai", "sessions", "archive");
443
+ fs.mkdirSync(archiveDir, { recursive: true });
444
+ const s = JSON.parse(fs.readFileSync(sessFile, "utf8"));
445
+ fs.writeFileSync(path.join(archiveDir, `${s.id || "session"}.json`), JSON.stringify(s, null, 2));
446
+ fs.unlinkSync(sessFile);
447
+ log(`session ${s.id} archived`);
448
+ return;
449
+ }
450
+ console.log("Usage: 0dai session [save|status|complete] [--goal '...'] [--summary '...']");
451
+ }
452
+
453
+ // --- Swarm (local, file-based) ---
454
+ function cmdSwarm(target, sub, args) {
455
+ const swarmDir = path.join(target, "ai", "swarm");
456
+ const queueDir = path.join(swarmDir, "queue");
457
+
458
+ if (sub === "status") {
459
+ const count = (d) => { try { return fs.readdirSync(d).filter(f => f.endsWith(".json")).length; } catch { return 0; } };
460
+ const q = count(path.join(swarmDir, "queue"));
461
+ const a = count(path.join(swarmDir, "active"));
462
+ const d = count(path.join(swarmDir, "done"));
463
+ log(`swarm: ${q} queued, ${a} active, ${d} done`);
464
+ return;
465
+ }
466
+ if (sub === "add" || sub === "delegate") {
467
+ fs.mkdirSync(queueDir, { recursive: true });
468
+ const task = args.find((_, i) => args[i - 1] === "--task") || "untitled";
469
+ const forAgent = args.find((_, i) => ["--for", "--to"].includes(args[i - 1])) || "any";
470
+ const id = `swarm-${Date.now()}`;
471
+ const t = { id, title: task, assigned_to: forAgent, status: "pending", created_at: new Date().toISOString(), created_by: "cli" };
472
+ fs.writeFileSync(path.join(queueDir, `${id}.json`), JSON.stringify(t, null, 2));
473
+ log(`task created: ${id} → ${forAgent}`);
474
+ return;
475
+ }
476
+ if (sub === "budget") {
477
+ const budgetFile = path.join(swarmDir, "budget.json");
478
+ if (!fs.existsSync(budgetFile)) { log("no budget data yet"); return; }
479
+ const b = JSON.parse(fs.readFileSync(budgetFile, "utf8"));
480
+ log(`total: $${(b.total_spent || 0).toFixed(4)} (${Object.keys(b.tasks || {}).length} tasks)`);
481
+ return;
482
+ }
483
+ console.log("Usage: 0dai swarm [status|add|delegate|budget] [--task '...'] [--to agent]");
484
+ }
485
+
486
+ // --- Feedback (local + API push) ---
487
+ async function cmdFeedback(target, sub, args) {
488
+ const fbDir = path.join(target, "ai", "feedback");
489
+
490
+ if (sub === "push") {
491
+ return cmdFeedbackPush(target);
492
+ }
493
+ if (sub === "log") {
494
+ const type = args.find((_, i) => args[i - 1] === "--type") || "suggestion";
495
+ const detail = args.find((_, i) => args[i - 1] === "--detail") || "";
496
+ if (!detail) { console.log("Usage: 0dai feedback log --type bug|suggestion|friction|positive --detail '...'"); return; }
497
+ fs.mkdirSync(fbDir, { recursive: true });
498
+ const entry = JSON.stringify({ ts: new Date().toISOString(), type, detail, agent: "cli" });
499
+ fs.appendFileSync(path.join(fbDir, "operational.jsonl"), entry + "\n");
500
+ log(`logged: [${type}] ${detail.slice(0, 60)}`);
501
+ return;
502
+ }
503
+ if (sub === "list") {
504
+ try {
505
+ const files = fs.readdirSync(fbDir).filter(f => f.endsWith("-report.json"));
506
+ if (!files.length) { log("no reports"); return; }
507
+ for (const f of files) {
508
+ try {
509
+ const d = JSON.parse(fs.readFileSync(path.join(fbDir, f), "utf8"));
510
+ console.log(` ${f}: ${d.verdict || "?"} (${d.project || "?"})`);
511
+ } catch {}
512
+ }
513
+ } catch { log("no feedback directory"); }
514
+ return;
515
+ }
516
+ console.log("Usage: 0dai feedback [push|log|list] [--type ...] [--detail '...']");
517
+ }
518
+
356
519
  async function main() {
357
520
  const args = process.argv.slice(2);
358
521
  let target = process.cwd();
@@ -379,6 +542,9 @@ async function main() {
379
542
  console.log("Usage: 0dai auth [login|logout|status]");
380
543
  }
381
544
  break;
545
+ case "session": cmdSession(target, sub, args); break;
546
+ case "swarm": cmdSwarm(target, sub, args); break;
547
+ case "feedback": await cmdFeedback(target, sub, args); break;
382
548
  case "--version": console.log(`${T}0dai${R} ${VERSION}`); break;
383
549
  case "help": case "--help": case "-h":
384
550
  console.log(`\n ${T}0dai${R} v${VERSION} — One config for 5 AI agent CLIs\n`);
@@ -388,6 +554,9 @@ async function main() {
388
554
  console.log(" detect Show detected stack");
389
555
  console.log(" doctor Check health");
390
556
  console.log(" status Show maturity, swarm, session");
557
+ console.log(" session save Save session for roaming");
558
+ console.log(" swarm status Task queue & delegation");
559
+ console.log(" feedback push Send feedback to 0dai");
391
560
  console.log(" auth login Authenticate (device code flow)");
392
561
  console.log(" auth logout Remove credentials");
393
562
  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.1",
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"