@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.
- package/bin/0dai.js +413 -19
- 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.
|
|
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 (
|
|
163
|
-
|
|
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(
|
|
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
|
-
|
|
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 (
|
|
251
|
-
|
|
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
|
-
|
|
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)
|
|
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
|
-
|
|
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
|
-
|
|
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;
|