@0dai-dev/cli 2.5.0 → 2.8.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 +315 -18
  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.5.0";
10
+ const VERSION = "2.8.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,9 @@ 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
+
149
151
  if (fs.existsSync(path.join(target, "ai", "VERSION"))) {
150
152
  const v = fs.readFileSync(path.join(target, "ai", "VERSION"), "utf8").trim();
151
153
  log(`ai/ layer already exists (v${v}). Run '0dai sync' to update.`);
@@ -159,12 +161,14 @@ async function cmdInit(target) {
159
161
  }
160
162
 
161
163
  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)...`);
164
+ if (dryRun) log(`${D}dry-run: would generate ai/ layer (${projectFiles.length} files, ${clis.length} CLIs)${R}`);
165
+ if (spinner) spinner.start(`${dryRun ? "[dry-run] " : ""}Generating ai/ layer (${projectFiles.length} files, ${clis.length} CLIs)...`);
166
+ else if (!dryRun) log(`sending to API (${projectFiles.length} files, ${clis.length} CLIs)...`);
164
167
  const result = await apiCall("/v1/init", {
165
168
  project_files: projectFiles,
166
169
  file_contents: fileContents,
167
170
  available_clis: clis,
171
+ dry_run: dryRun,
168
172
  });
169
173
 
170
174
  if (result.error) {
@@ -177,8 +181,15 @@ async function cmdInit(target) {
177
181
  process.exit(1);
178
182
  }
179
183
 
180
- if (spinner) spinner.stop(`Detected: ${result.stack || "?"}`);
184
+ if (spinner) spinner.stop(`${dryRun ? "[dry-run] " : ""}Detected: ${result.stack || "?"}`);
181
185
  else log(`detected: ${result.stack || "?"}`);
186
+ if (dryRun) {
187
+ const files = Object.keys(result.files || {});
188
+ log(`${D}dry-run: would write ${files.length} files:${R}`);
189
+ for (const f of files.slice(0, 20)) console.log(` ${D}+ ${f}${R}`);
190
+ if (files.length > 20) console.log(` ${D}… and ${files.length - 20} more${R}`);
191
+ return;
192
+ }
182
193
  writeFiles(target, result.files || {});
183
194
 
184
195
  // Add to .gitignore
@@ -203,11 +214,13 @@ async function cmdInit(target) {
203
214
  console.log(` ${D}0dai feedback push${R}`);
204
215
  }
205
216
 
206
- async function cmdSync(target) {
207
- // Quick local check: skip API if already at current version
217
+ async function cmdSync(target, args = []) {
218
+ const dryRun = args.includes("--dry-run");
219
+
220
+ // Quick local check: skip API if already at current version (unless dry-run)
208
221
  let version = "unknown";
209
222
  try { version = fs.readFileSync(path.join(target, "ai", "VERSION"), "utf8").trim(); } catch {}
210
- if (version === VERSION) {
223
+ if (!dryRun && version === VERSION) {
211
224
  log("already up to date (v" + version + ")");
212
225
  return;
213
226
  }
@@ -239,24 +252,49 @@ async function cmdSync(target) {
239
252
  walk(aiDir);
240
253
  }
241
254
 
255
+ if (dryRun) log(`${D}dry-run: checking what sync would change...${R}`);
256
+
242
257
  const result = await apiCall("/v1/sync", {
243
258
  ai_version: version, stack, agents: agents.length ? agents : clis,
244
259
  current_files: currentFiles, file_contents: fileContents,
260
+ dry_run: dryRun,
245
261
  });
246
262
 
247
263
  if (result.error) { log(`error: ${result.error}`); process.exit(1); }
248
264
 
249
265
  const updated = result.files_updated || {};
266
+ if (dryRun) {
267
+ const files = Object.keys(updated);
268
+ if (files.length) {
269
+ log(`${D}dry-run: would update ${files.length} file(s):${R}`);
270
+ for (const f of files) console.log(` ${D}~ ${f}${R}`);
271
+ } else {
272
+ log(`${D}dry-run: nothing to update${R}`);
273
+ }
274
+ return;
275
+ }
250
276
  if (Object.keys(updated).length) writeFiles(target, updated);
251
277
  else log("already up to date");
252
278
  }
253
279
 
254
280
  async function cmdDetect(target) {
281
+ const OPTIONAL_CLIS = ["gemini", "aider", "opencode"];
255
282
  const { projectFiles } = collectMetadata(target);
256
283
  const result = await apiCall("/v1/detect", { files: projectFiles });
257
284
  if (result.error) { log(`error: ${result.error}`); return; }
258
285
  console.log(`stack: ${result.stack || "?"}`);
259
- console.log(`clis: ${(result.available_clis || []).join(",")}`);
286
+ const clis = result.available_clis || [];
287
+ if (clis.length) {
288
+ console.log(`clis: ${clis.join(", ")}`);
289
+ } else {
290
+ console.log(`clis: none detected`);
291
+ console.log(` ${D}install claude, codex, or opencode to use 0dai${R}`);
292
+ }
293
+ // Explain optional CLIs so missing doesn't alarm users
294
+ const missing = OPTIONAL_CLIS.filter(c => !clis.includes(c));
295
+ if (missing.length && clis.length) {
296
+ console.log(` ${D}optional (not installed): ${missing.join(", ")}${R}`);
297
+ }
260
298
  }
261
299
 
262
300
  function cmdAudit(target) {
@@ -427,13 +465,27 @@ function cmdDoctor(target) {
427
465
  let errors = 0, warnings = 0;
428
466
  log(`v${v} | stack: ${stack}\n`);
429
467
 
468
+ const missingConfigs = [];
430
469
  console.log(" ai/ layer:");
431
470
  for (const [name, { path: p, sev }] of Object.entries(layerChecks)) {
432
471
  const exists = fs.existsSync(p);
433
- if (!exists) sev === "error" ? errors++ : warnings++;
472
+ if (!exists) {
473
+ sev === "error" ? errors++ : warnings++;
474
+ if (sev === "warn") missingConfigs.push(name);
475
+ }
434
476
  const mark = exists ? `${G}ok${R2}` : sev === "error" ? `${E}MISSING${R2}` : `${W}missing${R2}`;
435
477
  console.log(` ${mark.padEnd(22)} ${name}`);
436
478
  }
479
+ // Explain WHY native configs are missing and what to do
480
+ if (missingConfigs.length > 0) {
481
+ const hasDiscovery = fs.existsSync(path.join(ai, "manifest", "discovery.json"));
482
+ if (hasDiscovery) {
483
+ console.log(`\n ${W}→ Native configs not generated yet.${R2}`);
484
+ console.log(` ${D}Run: 0dai sync --target .${R2}`);
485
+ } else {
486
+ console.log(`\n ${W}→ ai/ layer incomplete — run '0dai init' first.${R2}`);
487
+ }
488
+ }
437
489
 
438
490
  console.log("\n credentials:");
439
491
  for (const c of credChecks) {
@@ -458,6 +510,50 @@ function cmdDoctor(target) {
458
510
  if (errors) process.exitCode = 1;
459
511
  }
460
512
 
513
+ function cmdValidate(target) {
514
+ const ai = path.join(target, "ai");
515
+ if (!fs.existsSync(ai)) {
516
+ log("no ai/ layer. Run '0dai init' first.");
517
+ process.exitCode = 1;
518
+ return;
519
+ }
520
+ const E = process.stdout.isTTY ? "\x1b[31m" : "";
521
+ const G = process.stdout.isTTY ? "\x1b[32m" : "";
522
+ const D2 = process.stdout.isTTY ? "\x1b[2m" : "";
523
+ const R2 = process.stdout.isTTY ? "\x1b[0m" : "";
524
+
525
+ const required = [
526
+ "ai/VERSION", "ai/VERSION_SCHEMA",
527
+ "ai/manifest/project.yaml", "ai/manifest/discovery.json",
528
+ "ai/manifest/applied-lock.json", "ai/manifest/environment.yaml",
529
+ "ai/manifest/commands.yaml",
530
+ ];
531
+
532
+ let agents = [];
533
+ try {
534
+ agents = JSON.parse(fs.readFileSync(path.join(ai, "manifest", "discovery.json"), "utf8")).selected_agents || [];
535
+ } catch {}
536
+
537
+ const agentFiles = {
538
+ codex: ["AGENTS.md", ".codex/config.toml"],
539
+ claude: [".claude/settings.json", ".claude/CLAUDE.md", ".mcp.json"],
540
+ opencode: ["opencode.json"],
541
+ };
542
+ for (const agent of agents) {
543
+ for (const f of agentFiles[agent] || []) required.push(f);
544
+ }
545
+
546
+ const missing = required.filter(f => !fs.existsSync(path.join(target, f)));
547
+ if (missing.length) {
548
+ log(`Validation FAILED: ${missing.length} missing file(s)\n`);
549
+ for (const f of missing) console.log(` ${E}✗${R2} ${f}`);
550
+ console.log(`\n ${D2}Run: 0dai sync --target . to fix missing files${R2}`);
551
+ process.exitCode = 1;
552
+ } else {
553
+ log(`${G}validate ok${R2} — all required files present`);
554
+ }
555
+ }
556
+
461
557
  // --- Session reflection --- (dogfood feedback #36)
462
558
  function cmdReflect(target, args) {
463
559
  const ai = path.join(target, "ai");
@@ -526,15 +622,77 @@ function cmdReflect(target, args) {
526
622
  if (totalPending) console.log(` ${B}Remaining${R2} ${W}${totalPending}${R2} tasks still pending`);
527
623
  if (successRate !== null) console.log(` ${B}Rate${R2} ${successRate >= 80 ? G : W}${successRate}%${R2} delegation success rate`);
528
624
 
529
- // By agent breakdown
625
+ // By agent breakdown with per-agent completion rate
626
+ const allPendingByAgent = {};
627
+ for (const t of [...allQueue, ...allActive]) {
628
+ const a = t.assigned_to || "unknown";
629
+ allPendingByAgent[a] = (allPendingByAgent[a] || 0) + 1;
630
+ }
631
+
530
632
  if (Object.keys(byAgent).length) {
531
633
  console.log(`\n ${B}By agent:${R2}`);
532
634
  for (const [agent, data] of Object.entries(byAgent).sort((a, b) => b[1].done - a[1].done)) {
635
+ const pending = allPendingByAgent[agent] || 0;
636
+ const total = data.done + pending;
637
+ const rate = total > 0 ? Math.round((data.done / total) * 100) : 100;
533
638
  const bar = "█".repeat(Math.min(data.done, 20));
534
- console.log(` ${(agent + " ").padEnd(14)} ${G}${bar}${R2} ${data.done}`);
639
+ const rateStr = total > 1 ? ` (${rate >= 80 ? G : W}${rate}%${R2})` : "";
640
+ console.log(` ${(agent + " ").padEnd(14)} ${G}${bar}${R2} ${data.done}/${total}${rateStr}`);
641
+ }
642
+ // Agents with only pending tasks (never completed anything in window)
643
+ for (const [agent, count] of Object.entries(allPendingByAgent)) {
644
+ if (!byAgent[agent]) {
645
+ console.log(` ${(agent + " ").padEnd(14)} ${W}${"░".repeat(Math.min(count, 20))}${R2} 0/${count} ${W}(pending)${R2}`);
646
+ }
535
647
  }
536
648
  }
537
649
 
650
+ // Budget summary from budget.json
651
+ const budgetFile = path.join(ai, "swarm", "budget.json");
652
+ try {
653
+ const budget = JSON.parse(fs.readFileSync(budgetFile, "utf8"));
654
+ const today = new Date().toISOString().slice(0, 10);
655
+ const sessionKey = process.env.ODAI_SESSION_ID ||
656
+ new Date().toISOString().slice(0, 13).replace("T", "-"); // YYYY-MM-DD-HH
657
+ const dailySpent = budget.daily?.[today] || 0;
658
+ const totalSpent = budget.total_spent || 0;
659
+ const sess = budget.sessions?.[sessionKey];
660
+
661
+ // Tier distribution across all tasks
662
+ const tierCount = { fast: 0, balanced: 0, deep: 0 };
663
+ for (const t of Object.values(budget.tasks || {})) {
664
+ if (t.tier && tierCount[t.tier] !== undefined) tierCount[t.tier]++;
665
+ }
666
+ const tieredTotal = tierCount.fast + tierCount.balanced + tierCount.deep;
667
+
668
+ if (dailySpent > 0 || totalSpent > 0 || sess) {
669
+ console.log(`\n ${B}Budget:${R2}`);
670
+ if (sess && sess.total_cost > 0) {
671
+ const taskCount = (sess.tasks || []).length;
672
+ const avgCost = taskCount > 0 ? (sess.total_cost / taskCount).toFixed(4) : "0";
673
+ console.log(` ${B}This session${R2} $${sess.total_cost.toFixed(4)} · ${taskCount} tasks · avg $${avgCost}/task`);
674
+ if (sess.tiers) {
675
+ const tiers = Object.entries(sess.tiers).filter(([, n]) => n > 0).map(([t, n]) => `${n}×${t}`).join(" ");
676
+ if (tiers) console.log(` ${D}Tiers ${tiers}${R2}`);
677
+ }
678
+ }
679
+ if (dailySpent > 0) {
680
+ const dailyLimit = parseFloat(process.env.ODAI_DAILY_BUDGET || "5");
681
+ const pct = Math.round((dailySpent / dailyLimit) * 100);
682
+ const bar = "█".repeat(Math.round(pct / 5)).padEnd(20, "░");
683
+ const col = pct < 50 ? G : pct < 80 ? W : "\x1b[31m";
684
+ console.log(` ${B}Today${R2} ${col}$${dailySpent.toFixed(4)}${R2} / $${dailyLimit.toFixed(2)} ${D}${bar} ${pct}%${R2}`);
685
+ }
686
+ if (totalSpent > 0) {
687
+ console.log(` ${B}All time${R2} ${D}$${totalSpent.toFixed(4)}${R2}`);
688
+ }
689
+ if (tieredTotal > 0) {
690
+ const fastPct = Math.round((tierCount.fast / tieredTotal) * 100);
691
+ console.log(` ${D}Model routing ${tierCount.fast}×fast ${tierCount.balanced}×balanced ${tierCount.deep}×deep (${fastPct}% cheap)${R2}`);
692
+ }
693
+ }
694
+ } catch {}
695
+
538
696
  // Remaining blockers
539
697
  if (allQueue.length) {
540
698
  console.log(`\n ${B}Blockers / queue:${R2}`);
@@ -722,6 +880,33 @@ function cmdAuthLogout() {
722
880
  log("Logged out");
723
881
  }
724
882
 
883
+ async function cmdRedeem(code) {
884
+ if (!code) {
885
+ console.log("Usage: 0dai redeem <CODE>");
886
+ console.log("Example: 0dai redeem ESSE-ABCD-1234");
887
+ process.exit(1);
888
+ }
889
+ try {
890
+ JSON.parse(fs.readFileSync(AUTH_FILE, "utf8"));
891
+ } catch {
892
+ log("Not logged in. Run: 0dai auth login");
893
+ process.exit(1);
894
+ }
895
+ log(`Redeeming code ${T}${code.toUpperCase()}${R}...`);
896
+ const result = await apiCall("/v1/redeem", { code: code.toUpperCase().trim() });
897
+ if (result.ok) {
898
+ log(`${T}✓${R} ${result.message}`);
899
+ if (result.duration_days) {
900
+ log(` Plan active for ${result.duration_days} days`);
901
+ }
902
+ log(` Run ${D}0dai auth status${R} to see updated limits`);
903
+ } else {
904
+ log(`error: ${result.error || "unknown"}`);
905
+ if (result.hint) log(`hint: ${result.hint}`);
906
+ process.exit(1);
907
+ }
908
+ }
909
+
725
910
  async function cmdAuthStatus() {
726
911
  try {
727
912
  const auth = JSON.parse(fs.readFileSync(AUTH_FILE, "utf8"));
@@ -892,11 +1077,116 @@ function cmdSwarm(target, sub, args) {
892
1077
  log(`task created: ${id} → ${forAgent}`);
893
1078
  return;
894
1079
  }
1080
+ if (sub === "webhook") {
1081
+ const webhooksFile = path.join(swarmDir, "webhooks.json");
1082
+ const loadHooks = () => { try { return JSON.parse(fs.readFileSync(webhooksFile, "utf8")); } catch { return []; } };
1083
+ const saveHooks = (h) => { fs.mkdirSync(swarmDir, { recursive: true }); fs.writeFileSync(webhooksFile, JSON.stringify(h, null, 2)); };
1084
+ const action = args[2] || "";
1085
+
1086
+ if (action === "add") {
1087
+ const url = args[3] || args.find((_, i) => args[i-1] === "--url");
1088
+ const event = args.find((_, i) => args[i-1] === "--event") || "all";
1089
+ const secret = args.find((_, i) => args[i-1] === "--secret") || "";
1090
+ if (!url || !url.startsWith("http")) { log("Usage: 0dai swarm webhook add <url> [--event task_done|task_failed|all] [--secret TOKEN]"); return; }
1091
+ const hooks = loadHooks();
1092
+ if (hooks.find(h => h.url === url)) { log(`already registered: ${url}`); return; }
1093
+ hooks.push({ url, event, secret: secret || undefined, added_at: new Date().toISOString() });
1094
+ saveHooks(hooks);
1095
+ log(`webhook added: ${url} (event: ${event})`);
1096
+ return;
1097
+ }
1098
+ if (action === "list") {
1099
+ const hooks = loadHooks();
1100
+ if (hooks.length === 0) { log("no webhooks registered. Use: 0dai swarm webhook add <url>"); return; }
1101
+ console.log(`\n ${T}Registered webhooks${R}\n`);
1102
+ hooks.forEach((h, i) => {
1103
+ console.log(` ${i+1}. ${h.url}`);
1104
+ console.log(` ${D}event: ${h.event} added: ${h.added_at?.slice(0,10)}${R}`);
1105
+ });
1106
+ console.log();
1107
+ return;
1108
+ }
1109
+ if (action === "remove") {
1110
+ const url = args[3] || "";
1111
+ if (!url) { log("Usage: 0dai swarm webhook remove <url>"); return; }
1112
+ const hooks = loadHooks().filter(h => h.url !== url);
1113
+ saveHooks(hooks);
1114
+ log(`removed: ${url}`);
1115
+ return;
1116
+ }
1117
+ if (action === "test") {
1118
+ const url = args[3] || loadHooks()[0]?.url;
1119
+ if (!url) { log("Usage: 0dai swarm webhook test <url>"); return; }
1120
+ const payload = JSON.stringify({ event: "test", task_id: "test-ping", title: "Webhook test from 0dai", status: "done", timestamp: new Date().toISOString() });
1121
+ const req = https.request(url, { method: "POST", headers: { "Content-Type": "application/json", "User-Agent": "0dai-swarm/1.0", "Content-Length": Buffer.byteLength(payload) } }, (res) => {
1122
+ log(`test sent to ${url} → HTTP ${res.statusCode}`);
1123
+ });
1124
+ req.on("error", (e) => log(`test failed: ${e.message}`));
1125
+ req.setTimeout(5000, () => { req.destroy(); log("test timed out"); });
1126
+ req.write(payload);
1127
+ req.end();
1128
+ return;
1129
+ }
1130
+ console.log("Usage: 0dai swarm webhook [add|list|remove|test] <url> [--event all|task_done|task_failed] [--secret TOKEN]");
1131
+ return;
1132
+ }
895
1133
  if (sub === "budget") {
896
1134
  const budgetFile = path.join(swarmDir, "budget.json");
897
1135
  if (!fs.existsSync(budgetFile)) { log("no budget data yet"); return; }
898
1136
  const b = JSON.parse(fs.readFileSync(budgetFile, "utf8"));
899
- log(`total: $${(b.total_spent || 0).toFixed(4)} (${Object.keys(b.tasks || {}).length} tasks)`);
1137
+ const B2 = process.stdout.isTTY ? "\x1b[1m" : "";
1138
+ const R2 = process.stdout.isTTY ? "\x1b[0m" : "";
1139
+ const D2 = process.stdout.isTTY ? "\x1b[2m" : "";
1140
+ const G2 = process.stdout.isTTY ? "\x1b[32m" : "";
1141
+ const W2 = process.stdout.isTTY ? "\x1b[33m" : "";
1142
+ const today = new Date().toISOString().slice(0, 10);
1143
+ const sessionKey = process.env.ODAI_SESSION_ID ||
1144
+ new Date().toISOString().slice(0, 13).replace("T", "-");
1145
+ const dailySpent = b.daily?.[today] || 0;
1146
+ const totalSpent = b.total_spent || 0;
1147
+ const sess = b.sessions?.[sessionKey];
1148
+ // Tier distribution across all tasks
1149
+ const tierCount = { fast: 0, balanced: 0, deep: 0 };
1150
+ for (const t of Object.values(b.tasks || {})) {
1151
+ if (t.tier && tierCount[t.tier] !== undefined) tierCount[t.tier]++;
1152
+ }
1153
+ const tieredTotal = tierCount.fast + tierCount.balanced + tierCount.deep;
1154
+ console.log(`\n ${B2}Swarm Budget${R2}`);
1155
+ if (sess && sess.total_cost > 0) {
1156
+ const taskCount = (sess.tasks || []).length;
1157
+ const avgCost = taskCount > 0 ? (sess.total_cost / taskCount).toFixed(4) : "0";
1158
+ console.log(` ${B2}This session${R2} $${sess.total_cost.toFixed(4)} · ${taskCount} tasks · avg $${avgCost}/task`);
1159
+ if (sess.tiers) {
1160
+ const tiers = Object.entries(sess.tiers).filter(([, n]) => n > 0).map(([t, n]) => `${n}×${t}`).join(" ");
1161
+ if (tiers) console.log(` ${D2}Tiers ${tiers}${R2}`);
1162
+ }
1163
+ } else {
1164
+ console.log(` ${D2}This session no tracked spend${R2}`);
1165
+ }
1166
+ if (dailySpent > 0) {
1167
+ const dailyLimit = parseFloat(process.env.ODAI_DAILY_BUDGET || "5");
1168
+ const pct = Math.round((dailySpent / dailyLimit) * 100);
1169
+ const bar = "█".repeat(Math.round(pct / 5)).padEnd(20, "░");
1170
+ const col = pct < 50 ? G2 : pct < 80 ? W2 : "\x1b[31m";
1171
+ console.log(` ${B2}Today${R2} ${col}$${dailySpent.toFixed(4)}${R2} / $${dailyLimit.toFixed(2)} ${D2}${bar} ${pct}%${R2}`);
1172
+ }
1173
+ console.log(` ${B2}All time${R2} ${D2}$${totalSpent.toFixed(4)} (${Object.keys(b.tasks || {}).length} tasks)${R2}`);
1174
+ if (tieredTotal > 0) {
1175
+ const fastPct = Math.round((tierCount.fast / tieredTotal) * 100);
1176
+ console.log(` ${D2}Model routing ${tierCount.fast}×fast ${tierCount.balanced}×balanced ${tierCount.deep}×deep (${fastPct}% cheap)${R2}`);
1177
+ }
1178
+ // Recent sessions (last 5)
1179
+ const sessions = Object.entries(b.sessions || {})
1180
+ .sort(([a], [bb]) => bb.localeCompare(a))
1181
+ .slice(0, 5);
1182
+ if (sessions.length > 1) {
1183
+ console.log(` ${D2}Recent sessions:${R2}`);
1184
+ for (const [key, s] of sessions) {
1185
+ const tasks = (s.tasks || []).length;
1186
+ console.log(` ${D2}${key} $${(s.total_cost || 0).toFixed(4)} · ${tasks} tasks${R2}`);
1187
+ }
1188
+ }
1189
+ console.log();
900
1190
  return;
901
1191
  }
902
1192
  console.log("Usage: 0dai swarm [status|add|delegate|budget] [--task '...'] [--to agent]");
@@ -949,10 +1239,11 @@ async function main() {
949
1239
 
950
1240
  switch (cmd) {
951
1241
  case "audit": cmdAudit(target); break;
952
- case "init": await cmdInit(target); break;
953
- case "sync": await cmdSync(target); break;
1242
+ case "init": await cmdInit(target, args); break;
1243
+ case "sync": await cmdSync(target, args); break;
954
1244
  case "detect": await cmdDetect(target); break;
955
1245
  case "doctor": cmdDoctor(target); break;
1246
+ case "validate": cmdValidate(target); break;
956
1247
  case "reflect": cmdReflect(target, args); break;
957
1248
  case "status": cmdStatus(target); break;
958
1249
  case "auth":
@@ -967,6 +1258,7 @@ async function main() {
967
1258
  case "swarm": cmdSwarm(target, sub, args); break;
968
1259
  case "feedback": await cmdFeedback(target, sub, args); break;
969
1260
  case "models": cmdModels(sub || args[1]); break;
1261
+ case "redeem": await cmdRedeem(sub || args[1]); break;
970
1262
  case "terminal": case "term":
971
1263
  try {
972
1264
  const SessionManager = require("../lib/session-manager");
@@ -993,20 +1285,25 @@ async function main() {
993
1285
  console.log(`\n ${T}0dai${R} v${VERSION} — One config for 5 AI agent CLIs\n`);
994
1286
  console.log("Commands:");
995
1287
  console.log(" audit Scan ai/ and agent configs for leaked secrets");
996
- console.log(" init Initialize ai/ layer (via API)");
997
- console.log(" sync Update ai/ layer (via API)");
1288
+ console.log(" init Initialize ai/ layer (via API) [--dry-run]");
1289
+ console.log(" sync Update ai/ layer (via API) [--dry-run]");
998
1290
  console.log(" detect Show detected stack");
999
1291
  console.log(" doctor Check health + credentials checklist");
1292
+ console.log(" validate Validate ai/ layer completeness");
1000
1293
  console.log(" reflect Session reflection: delivered, delegation rate, blockers");
1001
1294
  console.log(" status Show maturity, swarm, session");
1002
1295
  console.log(" session save Save session for roaming");
1003
- console.log(" swarm status Task queue & delegation");
1296
+ console.log(" swarm status Task queue & delegation");
1297
+ console.log(" swarm webhook add Register webhook (fires on task done/failed)");
1298
+ console.log(" swarm webhook list Show registered webhooks");
1299
+ console.log(" swarm webhook test Send test ping to a webhook URL");
1004
1300
  console.log(" feedback push Send feedback to 0dai");
1005
1301
  console.log(" models Show model ratings (--fast/--balanced/--deep/--available)");
1006
1302
  console.log(" terminal Launch interactive agent session");
1007
1303
  console.log(" auth login Authenticate (device code flow)");
1008
1304
  console.log(" auth logout Remove credentials");
1009
1305
  console.log(" auth status Show account and usage");
1306
+ console.log(" redeem <CODE> Redeem a plan upgrade code");
1010
1307
  console.log(" --version\n");
1011
1308
  console.log("https://0dai.dev");
1012
1309
  break;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@0dai-dev/cli",
3
- "version": "2.5.0",
3
+ "version": "2.8.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"