@0dai-dev/cli 2.6.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 +258 -17
  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.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"));
@@ -949,7 +1134,59 @@ function cmdSwarm(target, sub, args) {
949
1134
  const budgetFile = path.join(swarmDir, "budget.json");
950
1135
  if (!fs.existsSync(budgetFile)) { log("no budget data yet"); return; }
951
1136
  const b = JSON.parse(fs.readFileSync(budgetFile, "utf8"));
952
- 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();
953
1190
  return;
954
1191
  }
955
1192
  console.log("Usage: 0dai swarm [status|add|delegate|budget] [--task '...'] [--to agent]");
@@ -1002,10 +1239,11 @@ async function main() {
1002
1239
 
1003
1240
  switch (cmd) {
1004
1241
  case "audit": cmdAudit(target); break;
1005
- case "init": await cmdInit(target); break;
1006
- case "sync": await cmdSync(target); break;
1242
+ case "init": await cmdInit(target, args); break;
1243
+ case "sync": await cmdSync(target, args); break;
1007
1244
  case "detect": await cmdDetect(target); break;
1008
1245
  case "doctor": cmdDoctor(target); break;
1246
+ case "validate": cmdValidate(target); break;
1009
1247
  case "reflect": cmdReflect(target, args); break;
1010
1248
  case "status": cmdStatus(target); break;
1011
1249
  case "auth":
@@ -1020,6 +1258,7 @@ async function main() {
1020
1258
  case "swarm": cmdSwarm(target, sub, args); break;
1021
1259
  case "feedback": await cmdFeedback(target, sub, args); break;
1022
1260
  case "models": cmdModels(sub || args[1]); break;
1261
+ case "redeem": await cmdRedeem(sub || args[1]); break;
1023
1262
  case "terminal": case "term":
1024
1263
  try {
1025
1264
  const SessionManager = require("../lib/session-manager");
@@ -1046,10 +1285,11 @@ async function main() {
1046
1285
  console.log(`\n ${T}0dai${R} v${VERSION} — One config for 5 AI agent CLIs\n`);
1047
1286
  console.log("Commands:");
1048
1287
  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)");
1288
+ console.log(" init Initialize ai/ layer (via API) [--dry-run]");
1289
+ console.log(" sync Update ai/ layer (via API) [--dry-run]");
1051
1290
  console.log(" detect Show detected stack");
1052
1291
  console.log(" doctor Check health + credentials checklist");
1292
+ console.log(" validate Validate ai/ layer completeness");
1053
1293
  console.log(" reflect Session reflection: delivered, delegation rate, blockers");
1054
1294
  console.log(" status Show maturity, swarm, session");
1055
1295
  console.log(" session save Save session for roaming");
@@ -1063,6 +1303,7 @@ async function main() {
1063
1303
  console.log(" auth login Authenticate (device code flow)");
1064
1304
  console.log(" auth logout Remove credentials");
1065
1305
  console.log(" auth status Show account and usage");
1306
+ console.log(" redeem <CODE> Redeem a plan upgrade code");
1066
1307
  console.log(" --version\n");
1067
1308
  console.log("https://0dai.dev");
1068
1309
  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.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"