@0dai-dev/cli 2.6.0 → 2.9.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 +413 -19
  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.6.0";
10
+ const VERSION = "2.9.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
@@ -145,7 +145,10 @@ function writeFiles(target, files) {
145
145
  return created + updated;
146
146
  }
147
147
 
148
- async function cmdInit(target) {
148
+ async function cmdInit(target, args = []) {
149
+ const dryRun = args.includes("--dry-run");
150
+ const minimal = args.includes("--minimal");
151
+
149
152
  if (fs.existsSync(path.join(target, "ai", "VERSION"))) {
150
153
  const v = fs.readFileSync(path.join(target, "ai", "VERSION"), "utf8").trim();
151
154
  log(`ai/ layer already exists (v${v}). Run '0dai sync' to update.`);
@@ -159,12 +162,15 @@ async function cmdInit(target) {
159
162
  }
160
163
 
161
164
  const { projectFiles, fileContents, clis } = collectMetadata(target);
162
- if (spinner) spinner.start(`Generating ai/ layer (${projectFiles.length} files, ${clis.length} CLIs)...`);
163
- else log(`sending to API (${projectFiles.length} files, ${clis.length} CLIs)...`);
165
+ if (dryRun) log(`${D}dry-run: would generate ai/ layer (${projectFiles.length} files, ${clis.length} CLIs)${R}`);
166
+ if (spinner) spinner.start(`${dryRun ? "[dry-run] " : ""}Generating ai/ layer (${projectFiles.length} files, ${clis.length} CLIs)...`);
167
+ else if (!dryRun) log(`sending to API (${projectFiles.length} files, ${clis.length} CLIs)...`);
164
168
  const result = await apiCall("/v1/init", {
165
169
  project_files: projectFiles,
166
170
  file_contents: fileContents,
167
171
  available_clis: clis,
172
+ dry_run: dryRun,
173
+ minimal: minimal,
168
174
  });
169
175
 
170
176
  if (result.error) {
@@ -177,8 +183,15 @@ async function cmdInit(target) {
177
183
  process.exit(1);
178
184
  }
179
185
 
180
- if (spinner) spinner.stop(`Detected: ${result.stack || "?"}`);
186
+ if (spinner) spinner.stop(`${dryRun ? "[dry-run] " : ""}Detected: ${result.stack || "?"}`);
181
187
  else log(`detected: ${result.stack || "?"}`);
188
+ if (dryRun) {
189
+ const files = Object.keys(result.files || {});
190
+ log(`${D}dry-run: would write ${files.length} files:${R}`);
191
+ for (const f of files.slice(0, 20)) console.log(` ${D}+ ${f}${R}`);
192
+ if (files.length > 20) console.log(` ${D}… and ${files.length - 20} more${R}`);
193
+ return;
194
+ }
182
195
  writeFiles(target, result.files || {});
183
196
 
184
197
  // Add to .gitignore
@@ -203,11 +216,14 @@ async function cmdInit(target) {
203
216
  console.log(` ${D}0dai feedback push${R}`);
204
217
  }
205
218
 
206
- async function cmdSync(target) {
207
- // Quick local check: skip API if already at current version
219
+ async function cmdSync(target, args = []) {
220
+ const dryRun = args.includes("--dry-run");
221
+ const quiet = args.includes("--quiet") || args.includes("-q");
222
+
223
+ // Quick local check: skip API if already at current version (unless dry-run)
208
224
  let version = "unknown";
209
225
  try { version = fs.readFileSync(path.join(target, "ai", "VERSION"), "utf8").trim(); } catch {}
210
- if (version === VERSION) {
226
+ if (!dryRun && version === VERSION) {
211
227
  log("already up to date (v" + version + ")");
212
228
  return;
213
229
  }
@@ -239,24 +255,57 @@ async function cmdSync(target) {
239
255
  walk(aiDir);
240
256
  }
241
257
 
258
+ if (dryRun) log(`${D}dry-run: checking what sync would change...${R}`);
259
+
242
260
  const result = await apiCall("/v1/sync", {
243
261
  ai_version: version, stack, agents: agents.length ? agents : clis,
244
262
  current_files: currentFiles, file_contents: fileContents,
263
+ dry_run: dryRun, quiet,
245
264
  });
246
265
 
247
266
  if (result.error) { log(`error: ${result.error}`); process.exit(1); }
248
267
 
249
268
  const updated = result.files_updated || {};
250
- if (Object.keys(updated).length) writeFiles(target, updated);
251
- else log("already up to date");
269
+ if (dryRun) {
270
+ const files = Object.keys(updated);
271
+ if (files.length) {
272
+ log(`${D}dry-run: would update ${files.length} file(s):${R}`);
273
+ for (const f of files) console.log(` ${D}~ ${f}${R}`);
274
+ } else {
275
+ log(`${D}dry-run: nothing to update${R}`);
276
+ }
277
+ return;
278
+ }
279
+ const changedCount = Object.keys(updated).length;
280
+ if (changedCount) {
281
+ writeFiles(target, updated);
282
+ if (!quiet) {
283
+ for (const f of Object.keys(updated)) console.log(` ~ ${f}`);
284
+ }
285
+ log(`sync: ${changedCount} file(s) updated`);
286
+ } else {
287
+ log("already up to date");
288
+ }
252
289
  }
253
290
 
254
291
  async function cmdDetect(target) {
292
+ const OPTIONAL_CLIS = ["gemini", "aider", "opencode"];
255
293
  const { projectFiles } = collectMetadata(target);
256
294
  const result = await apiCall("/v1/detect", { files: projectFiles });
257
295
  if (result.error) { log(`error: ${result.error}`); return; }
258
296
  console.log(`stack: ${result.stack || "?"}`);
259
- console.log(`clis: ${(result.available_clis || []).join(",")}`);
297
+ const clis = result.available_clis || [];
298
+ if (clis.length) {
299
+ console.log(`clis: ${clis.join(", ")}`);
300
+ } else {
301
+ console.log(`clis: none detected`);
302
+ console.log(` ${D}install claude, codex, or opencode to use 0dai${R}`);
303
+ }
304
+ // Explain optional CLIs so missing doesn't alarm users
305
+ const missing = OPTIONAL_CLIS.filter(c => !clis.includes(c));
306
+ if (missing.length && clis.length) {
307
+ console.log(` ${D}optional (not installed): ${missing.join(", ")}${R}`);
308
+ }
260
309
  }
261
310
 
262
311
  function cmdAudit(target) {
@@ -427,13 +476,27 @@ function cmdDoctor(target) {
427
476
  let errors = 0, warnings = 0;
428
477
  log(`v${v} | stack: ${stack}\n`);
429
478
 
479
+ const missingConfigs = [];
430
480
  console.log(" ai/ layer:");
431
481
  for (const [name, { path: p, sev }] of Object.entries(layerChecks)) {
432
482
  const exists = fs.existsSync(p);
433
- if (!exists) sev === "error" ? errors++ : warnings++;
483
+ if (!exists) {
484
+ sev === "error" ? errors++ : warnings++;
485
+ if (sev === "warn") missingConfigs.push(name);
486
+ }
434
487
  const mark = exists ? `${G}ok${R2}` : sev === "error" ? `${E}MISSING${R2}` : `${W}missing${R2}`;
435
488
  console.log(` ${mark.padEnd(22)} ${name}`);
436
489
  }
490
+ // Explain WHY native configs are missing and what to do
491
+ if (missingConfigs.length > 0) {
492
+ const hasDiscovery = fs.existsSync(path.join(ai, "manifest", "discovery.json"));
493
+ if (hasDiscovery) {
494
+ console.log(`\n ${W}→ Native configs not generated yet.${R2}`);
495
+ console.log(` ${D}Run: 0dai sync --target .${R2}`);
496
+ } else {
497
+ console.log(`\n ${W}→ ai/ layer incomplete — run '0dai init' first.${R2}`);
498
+ }
499
+ }
437
500
 
438
501
  console.log("\n credentials:");
439
502
  for (const c of credChecks) {
@@ -458,6 +521,50 @@ function cmdDoctor(target) {
458
521
  if (errors) process.exitCode = 1;
459
522
  }
460
523
 
524
+ function cmdValidate(target) {
525
+ const ai = path.join(target, "ai");
526
+ if (!fs.existsSync(ai)) {
527
+ log("no ai/ layer. Run '0dai init' first.");
528
+ process.exitCode = 1;
529
+ return;
530
+ }
531
+ const E = process.stdout.isTTY ? "\x1b[31m" : "";
532
+ const G = process.stdout.isTTY ? "\x1b[32m" : "";
533
+ const D2 = process.stdout.isTTY ? "\x1b[2m" : "";
534
+ const R2 = process.stdout.isTTY ? "\x1b[0m" : "";
535
+
536
+ const required = [
537
+ "ai/VERSION", "ai/VERSION_SCHEMA",
538
+ "ai/manifest/project.yaml", "ai/manifest/discovery.json",
539
+ "ai/manifest/applied-lock.json", "ai/manifest/environment.yaml",
540
+ "ai/manifest/commands.yaml",
541
+ ];
542
+
543
+ let agents = [];
544
+ try {
545
+ agents = JSON.parse(fs.readFileSync(path.join(ai, "manifest", "discovery.json"), "utf8")).selected_agents || [];
546
+ } catch {}
547
+
548
+ const agentFiles = {
549
+ codex: ["AGENTS.md", ".codex/config.toml"],
550
+ claude: [".claude/settings.json", ".claude/CLAUDE.md", ".mcp.json"],
551
+ opencode: ["opencode.json"],
552
+ };
553
+ for (const agent of agents) {
554
+ for (const f of agentFiles[agent] || []) required.push(f);
555
+ }
556
+
557
+ const missing = required.filter(f => !fs.existsSync(path.join(target, f)));
558
+ if (missing.length) {
559
+ log(`Validation FAILED: ${missing.length} missing file(s)\n`);
560
+ for (const f of missing) console.log(` ${E}✗${R2} ${f}`);
561
+ console.log(`\n ${D2}Run: 0dai sync --target . to fix missing files${R2}`);
562
+ process.exitCode = 1;
563
+ } else {
564
+ log(`${G}validate ok${R2} — all required files present`);
565
+ }
566
+ }
567
+
461
568
  // --- Session reflection --- (dogfood feedback #36)
462
569
  function cmdReflect(target, args) {
463
570
  const ai = path.join(target, "ai");
@@ -526,15 +633,77 @@ function cmdReflect(target, args) {
526
633
  if (totalPending) console.log(` ${B}Remaining${R2} ${W}${totalPending}${R2} tasks still pending`);
527
634
  if (successRate !== null) console.log(` ${B}Rate${R2} ${successRate >= 80 ? G : W}${successRate}%${R2} delegation success rate`);
528
635
 
529
- // By agent breakdown
636
+ // By agent breakdown with per-agent completion rate
637
+ const allPendingByAgent = {};
638
+ for (const t of [...allQueue, ...allActive]) {
639
+ const a = t.assigned_to || "unknown";
640
+ allPendingByAgent[a] = (allPendingByAgent[a] || 0) + 1;
641
+ }
642
+
530
643
  if (Object.keys(byAgent).length) {
531
644
  console.log(`\n ${B}By agent:${R2}`);
532
645
  for (const [agent, data] of Object.entries(byAgent).sort((a, b) => b[1].done - a[1].done)) {
646
+ const pending = allPendingByAgent[agent] || 0;
647
+ const total = data.done + pending;
648
+ const rate = total > 0 ? Math.round((data.done / total) * 100) : 100;
533
649
  const bar = "█".repeat(Math.min(data.done, 20));
534
- console.log(` ${(agent + " ").padEnd(14)} ${G}${bar}${R2} ${data.done}`);
650
+ const rateStr = total > 1 ? ` (${rate >= 80 ? G : W}${rate}%${R2})` : "";
651
+ console.log(` ${(agent + " ").padEnd(14)} ${G}${bar}${R2} ${data.done}/${total}${rateStr}`);
652
+ }
653
+ // Agents with only pending tasks (never completed anything in window)
654
+ for (const [agent, count] of Object.entries(allPendingByAgent)) {
655
+ if (!byAgent[agent]) {
656
+ console.log(` ${(agent + " ").padEnd(14)} ${W}${"░".repeat(Math.min(count, 20))}${R2} 0/${count} ${W}(pending)${R2}`);
657
+ }
535
658
  }
536
659
  }
537
660
 
661
+ // Budget summary from budget.json
662
+ const budgetFile = path.join(ai, "swarm", "budget.json");
663
+ try {
664
+ const budget = JSON.parse(fs.readFileSync(budgetFile, "utf8"));
665
+ const today = new Date().toISOString().slice(0, 10);
666
+ const sessionKey = process.env.ODAI_SESSION_ID ||
667
+ new Date().toISOString().slice(0, 13).replace("T", "-"); // YYYY-MM-DD-HH
668
+ const dailySpent = budget.daily?.[today] || 0;
669
+ const totalSpent = budget.total_spent || 0;
670
+ const sess = budget.sessions?.[sessionKey];
671
+
672
+ // Tier distribution across all tasks
673
+ const tierCount = { fast: 0, balanced: 0, deep: 0 };
674
+ for (const t of Object.values(budget.tasks || {})) {
675
+ if (t.tier && tierCount[t.tier] !== undefined) tierCount[t.tier]++;
676
+ }
677
+ const tieredTotal = tierCount.fast + tierCount.balanced + tierCount.deep;
678
+
679
+ if (dailySpent > 0 || totalSpent > 0 || sess) {
680
+ console.log(`\n ${B}Budget:${R2}`);
681
+ if (sess && sess.total_cost > 0) {
682
+ const taskCount = (sess.tasks || []).length;
683
+ const avgCost = taskCount > 0 ? (sess.total_cost / taskCount).toFixed(4) : "0";
684
+ console.log(` ${B}This session${R2} $${sess.total_cost.toFixed(4)} · ${taskCount} tasks · avg $${avgCost}/task`);
685
+ if (sess.tiers) {
686
+ const tiers = Object.entries(sess.tiers).filter(([, n]) => n > 0).map(([t, n]) => `${n}×${t}`).join(" ");
687
+ if (tiers) console.log(` ${D}Tiers ${tiers}${R2}`);
688
+ }
689
+ }
690
+ if (dailySpent > 0) {
691
+ const dailyLimit = parseFloat(process.env.ODAI_DAILY_BUDGET || "5");
692
+ const pct = Math.round((dailySpent / dailyLimit) * 100);
693
+ const bar = "█".repeat(Math.round(pct / 5)).padEnd(20, "░");
694
+ const col = pct < 50 ? G : pct < 80 ? W : "\x1b[31m";
695
+ console.log(` ${B}Today${R2} ${col}$${dailySpent.toFixed(4)}${R2} / $${dailyLimit.toFixed(2)} ${D}${bar} ${pct}%${R2}`);
696
+ }
697
+ if (totalSpent > 0) {
698
+ console.log(` ${B}All time${R2} ${D}$${totalSpent.toFixed(4)}${R2}`);
699
+ }
700
+ if (tieredTotal > 0) {
701
+ const fastPct = Math.round((tierCount.fast / tieredTotal) * 100);
702
+ console.log(` ${D}Model routing ${tierCount.fast}×fast ${tierCount.balanced}×balanced ${tierCount.deep}×deep (${fastPct}% cheap)${R2}`);
703
+ }
704
+ }
705
+ } catch {}
706
+
538
707
  // Remaining blockers
539
708
  if (allQueue.length) {
540
709
  console.log(`\n ${B}Blockers / queue:${R2}`);
@@ -554,6 +723,146 @@ function cmdReflect(target, args) {
554
723
  console.log();
555
724
  }
556
725
 
726
+ function cmdMetrics(target) {
727
+ const ai = path.join(target, "ai");
728
+ const G = "\x1b[32m", W = "\x1b[33m", R2 = "\x1b[0m", D = "\x1b[2m",
729
+ B = "\x1b[34m", T = "\x1b[36m", M = "\x1b[35m";
730
+
731
+ // --- Data sources ---
732
+ let stats = {}, budget = {}, discovery = {};
733
+ try { stats = JSON.parse(fs.readFileSync(path.join(ai, "feedback", ".usage_stats.json"), "utf8")); } catch {}
734
+ try { budget = JSON.parse(fs.readFileSync(path.join(ai, "swarm", "budget.json"), "utf8")); } catch {}
735
+ try { discovery = JSON.parse(fs.readFileSync(path.join(ai, "manifest", "discovery.json"), "utf8")); } catch {}
736
+
737
+ const projectName = discovery.project_name || path.basename(target);
738
+ const stack = discovery.stack || "?";
739
+ const totalSessions = stats.total_sessions || 0;
740
+ const agentBreakdown = stats.agents || {};
741
+ const layerInfo = stats.layer || {};
742
+ const lastSession = stats.last_session ? new Date(stats.last_session) : null;
743
+ const layerVersion = stats.version || "?";
744
+
745
+ // Swarm tasks: count done files
746
+ let tasksDone = 0, tasksQueue = 0;
747
+ const doneDir = path.join(ai, "swarm", "done");
748
+ const queueDir = path.join(ai, "swarm", "queue");
749
+ try { tasksDone = fs.readdirSync(doneDir).filter(f => f.endsWith(".json")).length; } catch {}
750
+ try { tasksQueue = fs.readdirSync(queueDir).filter(f => f.endsWith(".json")).length; } catch {}
751
+
752
+ // Activity: count events from activity.jsonl
753
+ let activityEvents = 0;
754
+ try {
755
+ const lines = fs.readFileSync(path.join(ai, "swarm", "activity.jsonl"), "utf8").trim().split("\n").filter(Boolean);
756
+ activityEvents = lines.length;
757
+ } catch {}
758
+
759
+ // Feedback submissions
760
+ let feedbackCount = layerInfo.feedback_reports || 0;
761
+
762
+ // Budget totals
763
+ const totalSpent = budget.total_spent || 0;
764
+ const sessionsWithBudget = Object.keys(budget.sessions || {}).length;
765
+
766
+ // --- Effectiveness score (0-100) ---
767
+ let score = 0, scoreNotes = [];
768
+
769
+ // Sessions depth: 1 = tried, 3 = habit forming, 7 = regular use
770
+ const sessionScore = Math.min(Math.floor((totalSessions / 7) * 35), 35);
771
+ score += sessionScore;
772
+ if (totalSessions === 0) scoreNotes.push("not started");
773
+ else if (totalSessions === 1) scoreNotes.push("first session");
774
+ else if (totalSessions < 3) scoreNotes.push("early");
775
+ else if (totalSessions < 7) scoreNotes.push("habit forming");
776
+ else scoreNotes.push("regular use");
777
+
778
+ // Delegation: did they delegate to swarm?
779
+ const delegationScore = tasksDone > 0 ? Math.min(Math.floor((tasksDone / 5) * 30), 30) : 0;
780
+ score += delegationScore;
781
+ if (tasksDone > 0) scoreNotes.push(`${tasksDone} tasks delegated`);
782
+
783
+ // Feedback: submitted = trust signal
784
+ const feedbackScore = feedbackCount > 0 ? 20 : 0;
785
+ score += feedbackScore;
786
+ if (feedbackCount > 0) scoreNotes.push("feedback submitted");
787
+
788
+ // Layer completeness: has playbooks and commands?
789
+ const layerScore = (layerInfo.playbooks && layerInfo.commands) ? 15 : (layerInfo.commands ? 8 : 0);
790
+ score += layerScore;
791
+
792
+ const scoreColor = score >= 70 ? G : score >= 40 ? W : "\x1b[31m";
793
+ const bar = "█".repeat(Math.round(score / 5)).padEnd(20, "░");
794
+
795
+ // --- Output ---
796
+ console.log(`\n ${T}Metrics${R2} ${D}${projectName} · ${stack} · ai v${layerVersion}${R2}\n`);
797
+
798
+ // Effectiveness score
799
+ console.log(` ${B}Effectiveness${R2}`);
800
+ console.log(` ${scoreColor}${score}/100${R2} ${D}${bar}${R2}`);
801
+ if (scoreNotes.length) console.log(` ${D}${scoreNotes.join(" · ")}${R2}`);
802
+
803
+ // Adoption funnel
804
+ console.log(`\n ${B}Adoption funnel${R2}`);
805
+ const funnelStep = (label, value, done, hint) => {
806
+ const icon = done ? `${G}✓${R2}` : `${D}○${R2}`;
807
+ const val = value !== null ? ` ${D}${value}${R2}` : "";
808
+ const h = !done && hint ? ` ${D}← ${hint}${R2}` : "";
809
+ console.log(` ${icon} ${label}${val}${h}`);
810
+ };
811
+ funnelStep("Initialized", `ai/ v${layerVersion}`, true);
812
+ funnelStep("Returned (>1 session)", `${totalSessions} total`, totalSessions > 1, "run 0dai reflect after each session");
813
+ funnelStep("Used swarm delegation", tasksDone > 0 ? `${tasksDone} tasks done` : null, tasksDone > 0, "try: 0dai swarm add --task '...' --to codex");
814
+ funnelStep("Submitted feedback", feedbackCount > 0 ? `${feedbackCount} reports` : null, feedbackCount > 0, "0dai feedback log + push");
815
+
816
+ // Session stats
817
+ if (totalSessions > 0) {
818
+ console.log(`\n ${B}Sessions${R2}`);
819
+ console.log(` Total ${totalSessions}`);
820
+ if (lastSession) {
821
+ const daysAgo = Math.floor((Date.now() - lastSession.getTime()) / 86400000);
822
+ const when = daysAgo === 0 ? "today" : daysAgo === 1 ? "yesterday" : `${daysAgo}d ago`;
823
+ console.log(` Last ${when}`);
824
+ }
825
+ const agentEntries = Object.entries(agentBreakdown).sort((a, b) => b[1] - a[1]);
826
+ if (agentEntries.length) {
827
+ console.log(` Agents ${agentEntries.map(([a, n]) => `${a}: ${n}`).join(" ")}`);
828
+ }
829
+ if (sessionsWithBudget > 0 && totalSpent > 0) {
830
+ console.log(` Cost $${totalSpent.toFixed(4)} total ${D}(${sessionsWithBudget} sessions tracked)${R2}`);
831
+ }
832
+ }
833
+
834
+ // Delegation stats
835
+ if (tasksDone > 0 || tasksQueue > 0) {
836
+ console.log(`\n ${B}Delegation${R2}`);
837
+ if (tasksDone > 0) console.log(` Done ${G}${tasksDone}${R2}`);
838
+ if (tasksQueue > 0) console.log(` Queue ${W}${tasksQueue}${R2}`);
839
+ if (activityEvents > 0) console.log(` Events ${activityEvents}`);
840
+ }
841
+
842
+ // Layer health
843
+ console.log(`\n ${B}ai/ layer${R2}`);
844
+ const checks = [
845
+ ["commands.yaml", layerInfo.commands],
846
+ ["playbooks", layerInfo.playbooks],
847
+ ["personas", layerInfo.personas],
848
+ ["session roaming", layerInfo.session_active],
849
+ ["swarm queue", (layerInfo.swarm_queue || 0) > 0],
850
+ ];
851
+ for (const [label, ok] of checks) {
852
+ const icon = ok ? `${G}✓${R2}` : `${D}—${R2}`;
853
+ console.log(` ${icon} ${label}`);
854
+ }
855
+
856
+ // Next suggested action
857
+ console.log(`\n ${B}Next${R2}`);
858
+ if (totalSessions === 0) console.log(` ${D}Start a Claude Code session — session_start hook will print project context${R2}`);
859
+ else if (tasksDone === 0) console.log(` ${D}Try delegating a task: 0dai swarm add --task "write tests for auth module" --to codex${R2}`);
860
+ else if (feedbackCount === 0) console.log(` ${D}Submit feedback: 0dai feedback log --type positive --detail "what worked"${R2}`);
861
+ else console.log(` ${D}Score ${score}/100 — keep delegating and submitting feedback${R2}`);
862
+
863
+ console.log();
864
+ }
865
+
557
866
  function cmdStatus(target) {
558
867
  const ai = path.join(target, "ai");
559
868
  let v = "?", stack = "?";
@@ -722,6 +1031,33 @@ function cmdAuthLogout() {
722
1031
  log("Logged out");
723
1032
  }
724
1033
 
1034
+ async function cmdRedeem(code) {
1035
+ if (!code) {
1036
+ console.log("Usage: 0dai redeem <CODE>");
1037
+ console.log("Example: 0dai redeem ESSE-ABCD-1234");
1038
+ process.exit(1);
1039
+ }
1040
+ try {
1041
+ JSON.parse(fs.readFileSync(AUTH_FILE, "utf8"));
1042
+ } catch {
1043
+ log("Not logged in. Run: 0dai auth login");
1044
+ process.exit(1);
1045
+ }
1046
+ log(`Redeeming code ${T}${code.toUpperCase()}${R}...`);
1047
+ const result = await apiCall("/v1/redeem", { code: code.toUpperCase().trim() });
1048
+ if (result.ok) {
1049
+ log(`${T}✓${R} ${result.message}`);
1050
+ if (result.duration_days) {
1051
+ log(` Plan active for ${result.duration_days} days`);
1052
+ }
1053
+ log(` Run ${D}0dai auth status${R} to see updated limits`);
1054
+ } else {
1055
+ log(`error: ${result.error || "unknown"}`);
1056
+ if (result.hint) log(`hint: ${result.hint}`);
1057
+ process.exit(1);
1058
+ }
1059
+ }
1060
+
725
1061
  async function cmdAuthStatus() {
726
1062
  try {
727
1063
  const auth = JSON.parse(fs.readFileSync(AUTH_FILE, "utf8"));
@@ -949,7 +1285,59 @@ function cmdSwarm(target, sub, args) {
949
1285
  const budgetFile = path.join(swarmDir, "budget.json");
950
1286
  if (!fs.existsSync(budgetFile)) { log("no budget data yet"); return; }
951
1287
  const b = JSON.parse(fs.readFileSync(budgetFile, "utf8"));
952
- log(`total: $${(b.total_spent || 0).toFixed(4)} (${Object.keys(b.tasks || {}).length} tasks)`);
1288
+ const B2 = process.stdout.isTTY ? "\x1b[1m" : "";
1289
+ const R2 = process.stdout.isTTY ? "\x1b[0m" : "";
1290
+ const D2 = process.stdout.isTTY ? "\x1b[2m" : "";
1291
+ const G2 = process.stdout.isTTY ? "\x1b[32m" : "";
1292
+ const W2 = process.stdout.isTTY ? "\x1b[33m" : "";
1293
+ const today = new Date().toISOString().slice(0, 10);
1294
+ const sessionKey = process.env.ODAI_SESSION_ID ||
1295
+ new Date().toISOString().slice(0, 13).replace("T", "-");
1296
+ const dailySpent = b.daily?.[today] || 0;
1297
+ const totalSpent = b.total_spent || 0;
1298
+ const sess = b.sessions?.[sessionKey];
1299
+ // Tier distribution across all tasks
1300
+ const tierCount = { fast: 0, balanced: 0, deep: 0 };
1301
+ for (const t of Object.values(b.tasks || {})) {
1302
+ if (t.tier && tierCount[t.tier] !== undefined) tierCount[t.tier]++;
1303
+ }
1304
+ const tieredTotal = tierCount.fast + tierCount.balanced + tierCount.deep;
1305
+ console.log(`\n ${B2}Swarm Budget${R2}`);
1306
+ if (sess && sess.total_cost > 0) {
1307
+ const taskCount = (sess.tasks || []).length;
1308
+ const avgCost = taskCount > 0 ? (sess.total_cost / taskCount).toFixed(4) : "0";
1309
+ console.log(` ${B2}This session${R2} $${sess.total_cost.toFixed(4)} · ${taskCount} tasks · avg $${avgCost}/task`);
1310
+ if (sess.tiers) {
1311
+ const tiers = Object.entries(sess.tiers).filter(([, n]) => n > 0).map(([t, n]) => `${n}×${t}`).join(" ");
1312
+ if (tiers) console.log(` ${D2}Tiers ${tiers}${R2}`);
1313
+ }
1314
+ } else {
1315
+ console.log(` ${D2}This session no tracked spend${R2}`);
1316
+ }
1317
+ if (dailySpent > 0) {
1318
+ const dailyLimit = parseFloat(process.env.ODAI_DAILY_BUDGET || "5");
1319
+ const pct = Math.round((dailySpent / dailyLimit) * 100);
1320
+ const bar = "█".repeat(Math.round(pct / 5)).padEnd(20, "░");
1321
+ const col = pct < 50 ? G2 : pct < 80 ? W2 : "\x1b[31m";
1322
+ console.log(` ${B2}Today${R2} ${col}$${dailySpent.toFixed(4)}${R2} / $${dailyLimit.toFixed(2)} ${D2}${bar} ${pct}%${R2}`);
1323
+ }
1324
+ console.log(` ${B2}All time${R2} ${D2}$${totalSpent.toFixed(4)} (${Object.keys(b.tasks || {}).length} tasks)${R2}`);
1325
+ if (tieredTotal > 0) {
1326
+ const fastPct = Math.round((tierCount.fast / tieredTotal) * 100);
1327
+ console.log(` ${D2}Model routing ${tierCount.fast}×fast ${tierCount.balanced}×balanced ${tierCount.deep}×deep (${fastPct}% cheap)${R2}`);
1328
+ }
1329
+ // Recent sessions (last 5)
1330
+ const sessions = Object.entries(b.sessions || {})
1331
+ .sort(([a], [bb]) => bb.localeCompare(a))
1332
+ .slice(0, 5);
1333
+ if (sessions.length > 1) {
1334
+ console.log(` ${D2}Recent sessions:${R2}`);
1335
+ for (const [key, s] of sessions) {
1336
+ const tasks = (s.tasks || []).length;
1337
+ console.log(` ${D2}${key} $${(s.total_cost || 0).toFixed(4)} · ${tasks} tasks${R2}`);
1338
+ }
1339
+ }
1340
+ console.log();
953
1341
  return;
954
1342
  }
955
1343
  console.log("Usage: 0dai swarm [status|add|delegate|budget] [--task '...'] [--to agent]");
@@ -1002,11 +1390,13 @@ async function main() {
1002
1390
 
1003
1391
  switch (cmd) {
1004
1392
  case "audit": cmdAudit(target); break;
1005
- case "init": await cmdInit(target); break;
1006
- case "sync": await cmdSync(target); break;
1393
+ case "init": await cmdInit(target, args); break;
1394
+ case "sync": await cmdSync(target, args); break;
1007
1395
  case "detect": await cmdDetect(target); break;
1008
1396
  case "doctor": cmdDoctor(target); break;
1397
+ case "validate": cmdValidate(target); break;
1009
1398
  case "reflect": cmdReflect(target, args); break;
1399
+ case "metrics": cmdMetrics(target); break;
1010
1400
  case "status": cmdStatus(target); break;
1011
1401
  case "auth":
1012
1402
  if (sub === "login") await cmdAuthLogin();
@@ -1020,6 +1410,7 @@ async function main() {
1020
1410
  case "swarm": cmdSwarm(target, sub, args); break;
1021
1411
  case "feedback": await cmdFeedback(target, sub, args); break;
1022
1412
  case "models": cmdModels(sub || args[1]); break;
1413
+ case "redeem": await cmdRedeem(sub || args[1]); break;
1023
1414
  case "terminal": case "term":
1024
1415
  try {
1025
1416
  const SessionManager = require("../lib/session-manager");
@@ -1046,11 +1437,13 @@ async function main() {
1046
1437
  console.log(`\n ${T}0dai${R} v${VERSION} — One config for 5 AI agent CLIs\n`);
1047
1438
  console.log("Commands:");
1048
1439
  console.log(" audit Scan ai/ and agent configs for leaked secrets");
1049
- console.log(" init Initialize ai/ layer (via API)");
1050
- console.log(" sync Update ai/ layer (via API)");
1440
+ console.log(" init Initialize ai/ layer (via API) [--dry-run] [--minimal]");
1441
+ console.log(" sync Update ai/ layer (via API) [--dry-run] [--quiet]");
1051
1442
  console.log(" detect Show detected stack");
1052
1443
  console.log(" doctor Check health + credentials checklist");
1444
+ console.log(" validate Validate ai/ layer completeness");
1053
1445
  console.log(" reflect Session reflection: delivered, delegation rate, blockers");
1446
+ console.log(" metrics Effectiveness score: adoption funnel, sessions, delegation");
1054
1447
  console.log(" status Show maturity, swarm, session");
1055
1448
  console.log(" session save Save session for roaming");
1056
1449
  console.log(" swarm status Task queue & delegation");
@@ -1063,6 +1456,7 @@ async function main() {
1063
1456
  console.log(" auth login Authenticate (device code flow)");
1064
1457
  console.log(" auth logout Remove credentials");
1065
1458
  console.log(" auth status Show account and usage");
1459
+ console.log(" redeem <CODE> Redeem a plan upgrade code");
1066
1460
  console.log(" --version\n");
1067
1461
  console.log("https://0dai.dev");
1068
1462
  break;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@0dai-dev/cli",
3
- "version": "2.6.0",
3
+ "version": "2.9.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"