@0dai-dev/cli 3.10.1 → 4.1.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.
@@ -0,0 +1,145 @@
1
+ "use strict";
2
+ const shared = require("../shared");
3
+ const { fs, path } = shared;
4
+
5
+ function cmdMetrics(target) {
6
+ const ai = path.join(target, "ai");
7
+ const G = "\x1b[32m", W = "\x1b[33m", R2 = "\x1b[0m", D = "\x1b[2m",
8
+ B = "\x1b[34m", T = "\x1b[36m", M = "\x1b[35m";
9
+
10
+ // --- Data sources ---
11
+ let stats = {}, budget = {}, discovery = {};
12
+ try { stats = JSON.parse(fs.readFileSync(path.join(ai, "feedback", ".usage_stats.json"), "utf8")); } catch {}
13
+ try { budget = JSON.parse(fs.readFileSync(path.join(ai, "swarm", "budget.json"), "utf8")); } catch {}
14
+ try { discovery = JSON.parse(fs.readFileSync(path.join(ai, "manifest", "discovery.json"), "utf8")); } catch {}
15
+
16
+ const projectName = discovery.project_name || path.basename(target);
17
+ const stack = discovery.stack || "?";
18
+ const totalSessions = stats.total_sessions || 0;
19
+ const agentBreakdown = stats.agents || {};
20
+ const layerInfo = stats.layer || {};
21
+ const lastSession = stats.last_session ? new Date(stats.last_session) : null;
22
+ const layerVersion = stats.version || "?";
23
+
24
+ // Swarm tasks: count done files
25
+ let tasksDone = 0, tasksQueue = 0;
26
+ const doneDir = path.join(ai, "swarm", "done");
27
+ const queueDir = path.join(ai, "swarm", "queue");
28
+ try { tasksDone = fs.readdirSync(doneDir).filter(f => f.endsWith(".json")).length; } catch {}
29
+ try { tasksQueue = fs.readdirSync(queueDir).filter(f => f.endsWith(".json")).length; } catch {}
30
+
31
+ // Activity: count events from activity.jsonl
32
+ let activityEvents = 0;
33
+ try {
34
+ const lines = fs.readFileSync(path.join(ai, "swarm", "activity.jsonl"), "utf8").trim().split("\n").filter(Boolean);
35
+ activityEvents = lines.length;
36
+ } catch {}
37
+
38
+ // Feedback submissions
39
+ let feedbackCount = layerInfo.feedback_reports || 0;
40
+
41
+ // Budget totals
42
+ const totalSpent = budget.total_spent || 0;
43
+ const sessionsWithBudget = Object.keys(budget.sessions || {}).length;
44
+
45
+ // --- Effectiveness score (0-100) ---
46
+ let score = 0, scoreNotes = [];
47
+
48
+ // Sessions depth: 1 = tried, 3 = habit forming, 7 = regular use
49
+ const sessionScore = Math.min(Math.floor((totalSessions / 7) * 35), 35);
50
+ score += sessionScore;
51
+ if (totalSessions === 0) scoreNotes.push("not started");
52
+ else if (totalSessions === 1) scoreNotes.push("first session");
53
+ else if (totalSessions < 3) scoreNotes.push("early");
54
+ else if (totalSessions < 7) scoreNotes.push("habit forming");
55
+ else scoreNotes.push("regular use");
56
+
57
+ // Delegation: did they delegate to swarm?
58
+ const delegationScore = tasksDone > 0 ? Math.min(Math.floor((tasksDone / 5) * 30), 30) : 0;
59
+ score += delegationScore;
60
+ if (tasksDone > 0) scoreNotes.push(`${tasksDone} tasks delegated`);
61
+
62
+ // Feedback: submitted = trust signal
63
+ const feedbackScore = feedbackCount > 0 ? 20 : 0;
64
+ score += feedbackScore;
65
+ if (feedbackCount > 0) scoreNotes.push("feedback submitted");
66
+
67
+ // Layer completeness: has playbooks and commands?
68
+ const layerScore = (layerInfo.playbooks && layerInfo.commands) ? 15 : (layerInfo.commands ? 8 : 0);
69
+ score += layerScore;
70
+
71
+ const scoreColor = score >= 70 ? G : score >= 40 ? W : "\x1b[31m";
72
+ const bar = "█".repeat(Math.round(score / 5)).padEnd(20, "░");
73
+
74
+ // --- Output ---
75
+ console.log(`\n ${T}Metrics${R2} ${D}${projectName} · ${stack} · ai v${layerVersion}${R2}\n`);
76
+
77
+ // Effectiveness score
78
+ console.log(` ${B}Effectiveness${R2}`);
79
+ console.log(` ${scoreColor}${score}/100${R2} ${D}${bar}${R2}`);
80
+ if (scoreNotes.length) console.log(` ${D}${scoreNotes.join(" · ")}${R2}`);
81
+
82
+ // Adoption funnel
83
+ console.log(`\n ${B}Adoption funnel${R2}`);
84
+ const funnelStep = (label, value, done, hint) => {
85
+ const icon = done ? `${G}✓${R2}` : `${D}○${R2}`;
86
+ const val = value !== null ? ` ${D}${value}${R2}` : "";
87
+ const h = !done && hint ? ` ${D}← ${hint}${R2}` : "";
88
+ console.log(` ${icon} ${label}${val}${h}`);
89
+ };
90
+ funnelStep("Initialized", `ai/ v${layerVersion}`, true);
91
+ funnelStep("Returned (>1 session)", `${totalSessions} total`, totalSessions > 1, "run 0dai reflect after each session");
92
+ funnelStep("Used swarm delegation", tasksDone > 0 ? `${tasksDone} tasks done` : null, tasksDone > 0, "try: 0dai swarm add --task '...' --to codex");
93
+ funnelStep("Submitted feedback", feedbackCount > 0 ? `${feedbackCount} reports` : null, feedbackCount > 0, "0dai feedback log + push");
94
+
95
+ // Session stats
96
+ if (totalSessions > 0) {
97
+ console.log(`\n ${B}Sessions${R2}`);
98
+ console.log(` Total ${totalSessions}`);
99
+ if (lastSession) {
100
+ const daysAgo = Math.floor((Date.now() - lastSession.getTime()) / 86400000);
101
+ const when = daysAgo === 0 ? "today" : daysAgo === 1 ? "yesterday" : `${daysAgo}d ago`;
102
+ console.log(` Last ${when}`);
103
+ }
104
+ const agentEntries = Object.entries(agentBreakdown).sort((a, b) => b[1] - a[1]);
105
+ if (agentEntries.length) {
106
+ console.log(` Agents ${agentEntries.map(([a, n]) => `${a}: ${n}`).join(" ")}`);
107
+ }
108
+ if (sessionsWithBudget > 0 && totalSpent > 0) {
109
+ console.log(` Cost $${totalSpent.toFixed(4)} total ${D}(${sessionsWithBudget} sessions tracked)${R2}`);
110
+ }
111
+ }
112
+
113
+ // Delegation stats
114
+ if (tasksDone > 0 || tasksQueue > 0) {
115
+ console.log(`\n ${B}Delegation${R2}`);
116
+ if (tasksDone > 0) console.log(` Done ${G}${tasksDone}${R2}`);
117
+ if (tasksQueue > 0) console.log(` Queue ${W}${tasksQueue}${R2}`);
118
+ if (activityEvents > 0) console.log(` Events ${activityEvents}`);
119
+ }
120
+
121
+ // Layer health
122
+ console.log(`\n ${B}ai/ layer${R2}`);
123
+ const checks = [
124
+ ["commands.yaml", layerInfo.commands],
125
+ ["playbooks", layerInfo.playbooks],
126
+ ["personas", layerInfo.personas],
127
+ ["session roaming", layerInfo.session_active],
128
+ ["swarm queue", (layerInfo.swarm_queue || 0) > 0],
129
+ ];
130
+ for (const [label, ok] of checks) {
131
+ const icon = ok ? `${G}✓${R2}` : `${D}—${R2}`;
132
+ console.log(` ${icon} ${label}`);
133
+ }
134
+
135
+ // Next suggested action
136
+ console.log(`\n ${B}Next${R2}`);
137
+ if (totalSessions === 0) console.log(` ${D}Start a Claude Code session — session_start hook will print project context${R2}`);
138
+ else if (tasksDone === 0) console.log(` ${D}Try delegating a task: 0dai swarm add --task "write tests for auth module" --to codex${R2}`);
139
+ else if (feedbackCount === 0) console.log(` ${D}Submit feedback: 0dai feedback log --type positive --detail "what worked"${R2}`);
140
+ else console.log(` ${D}Score ${score}/100 — keep delegating and submitting feedback${R2}`);
141
+
142
+ console.log();
143
+ }
144
+
145
+ module.exports = { cmdMetrics };
@@ -0,0 +1,90 @@
1
+ "use strict";
2
+ const shared = require("../shared");
3
+ const { T, R } = shared;
4
+
5
+ function cmdModels(filter) {
6
+ // Scores from benchmark_models.py (3-task: read/count/review, 2026-04-06)
7
+ const MODELS = [
8
+ { name: "Claude Opus 4.6", tier: "deep", score: 95, cli: "claude", flag: "--model opus" },
9
+ { name: "GPT-5.4-mini", tier: "fast", score: 93, cli: "codex", flag: "-m gpt-5.4-mini", tested: true },
10
+ { name: "MiniMax M2.7", tier: "balanced", score: 93, cli: "opencode", flag: "-m opencode-go/minimax-m2.7", tested: true },
11
+ { name: "Claude Sonnet 4.6", tier: "balanced", score: 90, cli: "claude", flag: "--model sonnet" },
12
+ { name: "GPT-5.4", tier: "balanced", score: 90, cli: "codex", flag: "-m gpt-5.4", tested: true },
13
+ { name: "Kimi K2.5", tier: "balanced", score: 88, cli: "opencode", flag: "-m opencode-go/kimi-k2.5", tested: true },
14
+ { name: "Qwen 3.6+ Free", tier: "free", score: 88, cli: "opencode", flag: "-m opencode/qwen3.6-plus-free", tested: true },
15
+ { name: "Gemini 3.1 Pro", tier: "balanced", score: 85, cli: "gemini", flag: "-m gemini-3.1-pro" },
16
+ { name: "GPT-5.3 Codex", tier: "deep", score: 83, cli: "codex", flag: "-m gpt-5.3-codex", tested: true },
17
+ { name: "GPT-5.3 Spark", tier: "fast", score: 82, cli: "codex", flag: "-m gpt-5.3-codex-spark" },
18
+ { name: "Claude Haiku 4.5", tier: "fast", score: 78, cli: "claude", flag: "--model haiku" },
19
+ { name: "Gemini 3 Flash", tier: "fast", score: 77, cli: "gemini", flag: "-m gemini-3-flash" },
20
+ { name: "Mimo v2 Pro", tier: "fast", score: 74, cli: "opencode", flag: "-m opencode-go/mimo-v2-pro", tested: true },
21
+ { name: "GPT-5.4 (opencode)",tier: "fast", score: 74, cli: "opencode", flag: "-m openai/gpt-5.4", tested: true },
22
+ { name: "GPT-5.2", tier: "balanced", score: 87, cli: "codex", flag: "-m gpt-5.2", tested: true },
23
+ { name: "MiniMax M2.5", tier: "slow", score: 57, cli: "opencode", flag: "-m opencode-go/minimax-m2.5", tested: true },
24
+ ];
25
+
26
+ const { execFileSync } = require("child_process");
27
+ const available = new Set();
28
+ for (const cli of ["claude", "codex", "opencode", "gemini", "aider"]) {
29
+ try { execFileSync("/bin/sh", ["-c", `command -v ${cli}`], { stdio: "ignore" }); available.add(cli); } catch {}
30
+ }
31
+
32
+ const isTTY = process.stdout.isTTY;
33
+ const Y = isTTY ? "\x1b[33m" : "";
34
+ const G = isTTY ? "\x1b[32m" : "";
35
+ const DIM = isTTY ? "\x1b[2m" : "";
36
+
37
+ let models = [...MODELS].sort((a, b) => b.score - a.score);
38
+ if (filter === "--fast") models = models.filter(m => m.tier === "fast");
39
+ if (filter === "--balanced") models = models.filter(m => m.tier === "balanced");
40
+ if (filter === "--deep") models = models.filter(m => m.tier === "deep");
41
+ if (filter === "--available") models = models.filter(m => available.has(m.cli));
42
+
43
+ const tc = (t) => t === "deep" ? T : t === "balanced" ? G : DIM;
44
+ console.log(`\n ${T}0dai${R} model ratings — ${models.length} models\n`);
45
+ console.log(` ${"SCORE".padEnd(6)} ${"MODEL".padEnd(22)} ${"TIER".padEnd(10)} ${"CLI".padEnd(10)} FLAG`);
46
+ console.log(` ${"-".repeat(64)}`);
47
+ for (const m of models) {
48
+ const dim = available.has(m.cli) ? "" : DIM;
49
+ const mark = m.tested ? ` ${G}✓${R}` : "";
50
+ console.log(`${dim} ${Y}${String(m.score).padEnd(6)}${R} ${m.name.padEnd(22)} ${tc(m.tier)}${m.tier.padEnd(10)}${R} ${m.cli.padEnd(10)} ${DIM}${m.flag}${R}${mark}${dim ? R : ""}`);
51
+ }
52
+ console.log(`\n ${DIM}✓ = swarm-benchmarked | dimmed = CLI not installed${R}`);
53
+ console.log(` ${DIM}Filter: --fast --balanced --deep --available${R}`);
54
+ console.log(` ${DIM}Full table: https://0dai.dev/models${R}\n`);
55
+ }
56
+
57
+ async function cmdModelsRecommend(target, args) {
58
+ const shared = require("../shared");
59
+ const { log, T, R, D, findRepoScript, spawnSync, requirePlan } = shared;
60
+
61
+ const gate = requirePlan("pro", "Model Recommend", target);
62
+ if (gate) { log(gate.error); log(gate.hint); return; }
63
+
64
+ const taskType = args.find((_, i) => args[i - 1] === "--task") || "";
65
+ const goal = args.find((_, i) => args[i - 1] === "--goal") || "";
66
+ const maxCost = parseFloat(args.find((_, i) => args[i - 1] === "--max-cost") || "0");
67
+ const minQuality = parseFloat(args.find((_, i) => args[i - 1] === "--min-quality") || "0");
68
+ const asJson = args.includes("--json");
69
+
70
+ if (!taskType && !goal) {
71
+ console.log("Usage: 0dai models recommend --task TYPE [--goal '...'] [--max-cost N] [--min-quality N] [--json]");
72
+ console.log(" TYPE: feat, fix, refactor, test, docs");
73
+ return;
74
+ }
75
+
76
+ const recScript = findRepoScript(target, "model_router.py");
77
+ if (!recScript) { log("model router unavailable"); return; }
78
+
79
+ const fwd = [recScript, "recommend", "--target", target];
80
+ if (taskType) fwd.push("--task", taskType);
81
+ if (goal) fwd.push("--goal", goal);
82
+ if (maxCost > 0) fwd.push("--max-cost", String(maxCost));
83
+ if (minQuality > 0) fwd.push("--min-quality", String(minQuality));
84
+ if (asJson) fwd.push("--json");
85
+
86
+ const result = spawnSync("python3", fwd, { stdio: "inherit" });
87
+ if (typeof result.status === "number" && result.status !== 0) process.exit(result.status);
88
+ }
89
+
90
+ module.exports = { cmdModels, cmdModelsRecommend };
@@ -0,0 +1,96 @@
1
+ "use strict";
2
+ const shared = require("../shared");
3
+ const { log, T, R, D, fs, path, PROJECTS_FILE } = shared;
4
+
5
+ function cmdPortfolio() {
6
+ let projects = [];
7
+ try { projects = JSON.parse(fs.readFileSync(PROJECTS_FILE, "utf8")).projects || []; } catch {}
8
+
9
+ if (!projects.length) {
10
+ log(`no projects registered yet`);
11
+ console.log(` Run ${D}0dai init${R} in a project to start tracking it.`);
12
+ return;
13
+ }
14
+
15
+ const rows = [];
16
+ let totalSessions = 0, totalScore = 0, scored = 0;
17
+
18
+ for (const p of projects) {
19
+ if (!fs.existsSync(p.path)) continue;
20
+
21
+ let sessions = 0, lastSession = null, agentMap = {};
22
+ try {
23
+ const stats = JSON.parse(fs.readFileSync(path.join(p.path, "ai", "feedback", ".usage_stats.json"), "utf8"));
24
+ sessions = stats.total_sessions || 0;
25
+ lastSession = stats.last_session || null;
26
+ agentMap = stats.agents || {};
27
+ } catch {}
28
+
29
+ let name = p.name, stack = p.stack;
30
+ try {
31
+ const disc = JSON.parse(fs.readFileSync(path.join(p.path, "ai", "manifest", "discovery.json"), "utf8"));
32
+ name = disc.project_name || name;
33
+ stack = disc.stack || stack;
34
+ } catch {}
35
+
36
+ // Effectiveness score (mirrors metrics command)
37
+ let score = 0;
38
+ if (sessions > 0) {
39
+ score += Math.min(sessions * 5, 35);
40
+ let done = 0;
41
+ try { done = fs.readdirSync(path.join(p.path, "ai", "swarm", "done")).filter(f => f.endsWith(".json")).length; } catch {}
42
+ if (done > 0) score += Math.min(done * 6, 30);
43
+ try {
44
+ const hasFb = fs.readdirSync(path.join(p.path, "ai", "feedback")).some(f => f.endsWith("-report.json"));
45
+ if (hasFb) score += 20;
46
+ } catch {}
47
+ const layerFiles = ["ai/manifest/discovery.json", "ai/manifest/commands.yaml", "ai/playbooks/quick-start.md"];
48
+ score += Math.round(layerFiles.filter(f => fs.existsSync(path.join(p.path, f))).length / layerFiles.length * 15);
49
+ }
50
+
51
+ totalSessions += sessions;
52
+ if (sessions > 0) { totalScore += score; scored++; }
53
+
54
+ const agentList = Object.keys(agentMap).join("·") || "—";
55
+
56
+ let ago = "never";
57
+ if (lastSession) {
58
+ const h = Math.floor((Date.now() - new Date(lastSession).getTime()) / 3600000);
59
+ const d = Math.floor(h / 24);
60
+ if (h < 1) ago = "<1h ago";
61
+ else if (h < 24) ago = `${h}h ago`;
62
+ else if (d < 7) ago = `${d}d ago`;
63
+ else ago = `${Math.floor(d / 7)}w ago`;
64
+ }
65
+
66
+ rows.push({ name, stack, score: sessions > 0 ? score : null, sessions, agents: agentList, ago });
67
+ }
68
+
69
+ if (!rows.length) {
70
+ log("no projects found (paths may have moved)");
71
+ return;
72
+ }
73
+
74
+ const nameW = Math.min(Math.max(...rows.map(r => r.name.length), 4), 28);
75
+ const stackW = Math.min(Math.max(...rows.map(r => r.stack.length), 5), 16);
76
+
77
+ console.log(`\n ${T}Portfolio${R} — ${rows.length} project${rows.length === 1 ? "" : "s"}\n`);
78
+ for (const r of rows) {
79
+ const nm = r.name.slice(0, nameW).padEnd(nameW);
80
+ const st = r.stack.slice(0, stackW).padEnd(stackW);
81
+ const sc = r.score !== null ? `score ${String(r.score).padStart(3)}` : " ";
82
+ const se = `${String(r.sessions).padStart(2)} session${r.sessions === 1 ? " " : "s"}`;
83
+ console.log(` ${T}${nm}${R} ${D}${st}${R} ${sc} ${se} ${r.agents.padEnd(14)} ${D}${r.ago}${R}`);
84
+ }
85
+
86
+ if (totalSessions > 0) {
87
+ const avg = scored > 0 ? Math.round(totalScore / scored) : 0;
88
+ const stacks = [...new Set(rows.map(r => r.stack))].length;
89
+ console.log(`\n ${D}${"─".repeat(70)}`);
90
+ console.log(` Total: ${totalSessions} sessions ${stacks} stack${stacks === 1 ? "" : "s"} avg effectiveness: ${avg}${R}\n`);
91
+ } else {
92
+ console.log(`\n ${D}Tip: run '0dai init' in your projects to start tracking sessions.${R}\n`);
93
+ }
94
+ }
95
+
96
+ module.exports = { cmdPortfolio };
@@ -0,0 +1,211 @@
1
+ "use strict";
2
+ const shared = require("../shared");
3
+ const { T, R, D, fs, path } = shared;
4
+
5
+ function cmdReflect(target, args) {
6
+ const ai = path.join(target, "ai");
7
+ const W = process.stdout.isTTY ? "\x1b[33m" : "";
8
+ const G = process.stdout.isTTY ? "\x1b[32m" : "";
9
+ const R2 = process.stdout.isTTY ? "\x1b[0m" : "";
10
+ const B = process.stdout.isTTY ? "\x1b[1m" : "";
11
+
12
+ // Collect swarm done tasks
13
+ const doneDir = path.join(ai, "swarm", "done");
14
+ const queueDir = path.join(ai, "swarm", "queue");
15
+ const activeDir = path.join(ai, "swarm", "active");
16
+ const doneTasks = [], queueTasks = [];
17
+
18
+ // -- how many days to look back (default 7)
19
+ const daysArg = args.find((_, i) => args[i - 1] === "--days");
20
+ const days = parseInt(daysArg || "7");
21
+ const since = Date.now() - days * 24 * 60 * 60 * 1000;
22
+
23
+ const readJsonDir = (dir) => {
24
+ const out = [];
25
+ try {
26
+ for (const f of fs.readdirSync(dir).filter(f => f.endsWith(".json"))) {
27
+ try { out.push(JSON.parse(fs.readFileSync(path.join(dir, f), "utf8"))); } catch {}
28
+ }
29
+ } catch {}
30
+ return out;
31
+ };
32
+
33
+ const allDone = readJsonDir(doneDir).filter(t => {
34
+ const ts = t.completed_at || t.created_at;
35
+ return !ts || new Date(ts).getTime() >= since;
36
+ });
37
+ const allQueue = readJsonDir(queueDir);
38
+ const allActive = readJsonDir(activeDir);
39
+
40
+ // Aggregate by agent
41
+ const byAgent = {};
42
+ for (const t of allDone) {
43
+ const a = t.assigned_to || t.agent || "unknown";
44
+ if (!byAgent[a]) byAgent[a] = { done: 0, tasks: [] };
45
+ byAgent[a].done++;
46
+ byAgent[a].tasks.push(t.title || t.id || "?");
47
+ }
48
+
49
+ // Session data
50
+ let sessionGoal = "?";
51
+ try {
52
+ const active = JSON.parse(fs.readFileSync(path.join(ai, "sessions", "active.json"), "utf8"));
53
+ sessionGoal = (active.task || {}).goal || "?";
54
+ } catch {}
55
+
56
+ console.log(`\n ${B}${T}0dai reflect${R2}${R} — last ${days} days\n`);
57
+
58
+ // Goal
59
+ if (sessionGoal !== "?") console.log(` ${B}Goal${R2} ${sessionGoal}`);
60
+
61
+ // Delegation stats
62
+ const totalDone = allDone.length;
63
+ const totalPending = allQueue.length + allActive.length;
64
+ const successRate = totalDone + totalPending > 0
65
+ ? Math.round((totalDone / (totalDone + totalPending)) * 100)
66
+ : null;
67
+
68
+ console.log(` ${B}Delivered${R2} ${G}${totalDone}${R2} tasks completed`);
69
+ if (totalPending) console.log(` ${B}Remaining${R2} ${W}${totalPending}${R2} tasks still pending`);
70
+ if (successRate !== null) console.log(` ${B}Rate${R2} ${successRate >= 80 ? G : W}${successRate}%${R2} delegation success rate`);
71
+
72
+ // By agent breakdown with per-agent completion rate
73
+ const allPendingByAgent = {};
74
+ for (const t of [...allQueue, ...allActive]) {
75
+ const a = t.assigned_to || "unknown";
76
+ allPendingByAgent[a] = (allPendingByAgent[a] || 0) + 1;
77
+ }
78
+
79
+ if (Object.keys(byAgent).length) {
80
+ console.log(`\n ${B}By agent:${R2}`);
81
+ for (const [agent, data] of Object.entries(byAgent).sort((a, b) => b[1].done - a[1].done)) {
82
+ const pending = allPendingByAgent[agent] || 0;
83
+ const total = data.done + pending;
84
+ const rate = total > 0 ? Math.round((data.done / total) * 100) : 100;
85
+ const bar = "█".repeat(Math.min(data.done, 20));
86
+ const rateStr = total > 1 ? ` (${rate >= 80 ? G : W}${rate}%${R2})` : "";
87
+ console.log(` ${(agent + " ").padEnd(14)} ${G}${bar}${R2} ${data.done}/${total}${rateStr}`);
88
+ }
89
+ // Agents with only pending tasks (never completed anything in window)
90
+ for (const [agent, count] of Object.entries(allPendingByAgent)) {
91
+ if (!byAgent[agent]) {
92
+ console.log(` ${(agent + " ").padEnd(14)} ${W}${"░".repeat(Math.min(count, 20))}${R2} 0/${count} ${W}(pending)${R2}`);
93
+ }
94
+ }
95
+ }
96
+
97
+ // Budget summary from budget.json
98
+ const budgetFile = path.join(ai, "swarm", "budget.json");
99
+ try {
100
+ const budget = JSON.parse(fs.readFileSync(budgetFile, "utf8"));
101
+ const today = new Date().toISOString().slice(0, 10);
102
+ const sessionKey = process.env.ODAI_SESSION_ID ||
103
+ new Date().toISOString().slice(0, 13).replace("T", "-"); // YYYY-MM-DD-HH
104
+ const dailySpent = budget.daily?.[today] || 0;
105
+ const totalSpent = budget.total_spent || 0;
106
+ const sess = budget.sessions?.[sessionKey];
107
+
108
+ // Tier distribution across all tasks
109
+ const tierCount = { fast: 0, balanced: 0, deep: 0 };
110
+ for (const t of Object.values(budget.tasks || {})) {
111
+ if (t.tier && tierCount[t.tier] !== undefined) tierCount[t.tier]++;
112
+ }
113
+ const tieredTotal = tierCount.fast + tierCount.balanced + tierCount.deep;
114
+
115
+ if (dailySpent > 0 || totalSpent > 0 || sess) {
116
+ console.log(`\n ${B}Budget:${R2}`);
117
+ if (sess && sess.total_cost > 0) {
118
+ const taskCount = (sess.tasks || []).length;
119
+ const avgCost = taskCount > 0 ? (sess.total_cost / taskCount).toFixed(4) : "0";
120
+ console.log(` ${B}This session${R2} $${sess.total_cost.toFixed(4)} · ${taskCount} tasks · avg $${avgCost}/task`);
121
+ if (sess.tiers) {
122
+ const tiers = Object.entries(sess.tiers).filter(([, n]) => n > 0).map(([t, n]) => `${n}×${t}`).join(" ");
123
+ if (tiers) console.log(` ${D}Tiers ${tiers}${R2}`);
124
+ }
125
+ }
126
+ if (dailySpent > 0) {
127
+ const dailyLimit = parseFloat(process.env.ODAI_DAILY_BUDGET || "5");
128
+ const pct = Math.round((dailySpent / dailyLimit) * 100);
129
+ const bar = "█".repeat(Math.round(pct / 5)).padEnd(20, "░");
130
+ const col = pct < 50 ? G : pct < 80 ? W : "\x1b[31m";
131
+ console.log(` ${B}Today${R2} ${col}$${dailySpent.toFixed(4)}${R2} / $${dailyLimit.toFixed(2)} ${D}${bar} ${pct}%${R2}`);
132
+ }
133
+ if (totalSpent > 0) {
134
+ console.log(` ${B}All time${R2} ${D}$${totalSpent.toFixed(4)}${R2}`);
135
+ }
136
+ if (tieredTotal > 0) {
137
+ const fastPct = Math.round((tierCount.fast / tieredTotal) * 100);
138
+ console.log(` ${D}Model routing ${tierCount.fast}×fast ${tierCount.balanced}×balanced ${tierCount.deep}×deep (${fastPct}% cheap)${R2}`);
139
+ }
140
+ }
141
+ } catch {}
142
+
143
+ // Insights — learn from patterns
144
+ if (totalDone >= 3) {
145
+ const insights = [];
146
+ // Best agent by completion rate
147
+ const agentEntries = Object.entries(byAgent).map(([a, d]) => {
148
+ const pending = allPendingByAgent[a] || 0;
149
+ const total = d.done + pending;
150
+ return { agent: a, done: d.done, total, rate: total > 0 ? d.done / total : 1 };
151
+ });
152
+ const best = agentEntries.sort((a, b) => b.rate - a.rate)[0];
153
+ if (best && agentEntries.length > 1) {
154
+ insights.push(`${best.agent} has the highest success rate (${Math.round(best.rate * 100)}%)`);
155
+ }
156
+ // Fastest agent by avg elapsed
157
+ try {
158
+ const budget = JSON.parse(fs.readFileSync(path.join(ai, "swarm", "budget.json"), "utf8"));
159
+ const agentElapsed = {};
160
+ const agentCounts = {};
161
+ for (const t of Object.values(budget.tasks || {})) {
162
+ if (t.elapsed > 0) {
163
+ agentElapsed[t.agent] = (agentElapsed[t.agent] || 0) + t.elapsed;
164
+ agentCounts[t.agent] = (agentCounts[t.agent] || 0) + 1;
165
+ }
166
+ }
167
+ let fastest = null, fastestAvg = Infinity;
168
+ for (const [a, total] of Object.entries(agentElapsed)) {
169
+ const avg = total / agentCounts[a];
170
+ if (avg < fastestAvg) { fastest = a; fastestAvg = avg; }
171
+ }
172
+ if (fastest && Object.keys(agentElapsed).length > 1) {
173
+ insights.push(`${fastest} is fastest (avg ${Math.round(fastestAvg)}s per task)`);
174
+ }
175
+ } catch {}
176
+ // Cost efficiency
177
+ if (allDone.length > 0) {
178
+ try {
179
+ const budget = JSON.parse(fs.readFileSync(path.join(ai, "swarm", "budget.json"), "utf8"));
180
+ if (budget.total_spent > 0) {
181
+ const costPerTask = budget.total_spent / allDone.length;
182
+ insights.push(`avg cost: $${costPerTask.toFixed(4)}/task`);
183
+ }
184
+ } catch {}
185
+ }
186
+ if (insights.length) {
187
+ console.log(`\n ${B}Insights:${R2}`);
188
+ for (const i of insights) console.log(` ${G}→${R2} ${i}`);
189
+ }
190
+ }
191
+
192
+ // Remaining blockers
193
+ if (allQueue.length) {
194
+ console.log(`\n ${B}Blockers / queue:${R2}`);
195
+ for (const t of allQueue.slice(0, 8)) {
196
+ const agent = t.assigned_to ? ` → ${t.assigned_to}` : "";
197
+ console.log(` ${W}•${R2} ${(t.title || t.id || "?").slice(0, 60)}${agent}`);
198
+ }
199
+ if (allQueue.length > 8) console.log(` ${D}… and ${allQueue.length - 8} more${R2}`);
200
+ }
201
+
202
+ // No data
203
+ if (!totalDone && !totalPending) {
204
+ console.log(` ${D}No swarm tasks found in the last ${days} days.${R2}`);
205
+ console.log(` ${D}Use '0dai swarm add --task "..." --to codex' to delegate tasks.${R2}`);
206
+ }
207
+
208
+ console.log();
209
+ }
210
+
211
+ module.exports = { cmdReflect };
@@ -0,0 +1,29 @@
1
+ "use strict";
2
+ const shared = require("../shared");
3
+ const { log, D, R, spawnSync, findRepoScript, VERSION } = shared;
4
+
5
+ function cmdReport(target, sub, args) {
6
+ const reportScript = findRepoScript(target, "report_manager.py");
7
+ if (!reportScript) {
8
+ log("report manager unavailable in this environment");
9
+ console.log(` ${D}Expected scripts/report_manager.py in repo checkout${R}`);
10
+ process.exit(1);
11
+ }
12
+
13
+ const command = sub || "preview";
14
+ const forwarded = [reportScript, command, "--target", target, "--channel", "npm", "--cli-version", VERSION];
15
+
16
+ if (args.includes("--json")) forwarded.push("--json");
17
+ if (command === "auto") {
18
+ if (args.includes("--enable")) forwarded.push("--enable");
19
+ if (args.includes("--disable")) forwarded.push("--disable");
20
+ const idx = args.indexOf("--interval");
21
+ if (idx >= 0 && args[idx + 1]) forwarded.push("--interval", args[idx + 1]);
22
+ }
23
+
24
+ const result = spawnSync("python3", forwarded, { stdio: "inherit" });
25
+ if (typeof result.status === "number") process.exit(result.status);
26
+ process.exit(1);
27
+ }
28
+
29
+ module.exports = { cmdReport };
@@ -0,0 +1,77 @@
1
+ "use strict";
2
+ const shared = require("../shared");
3
+ const { log, T, R, D, fs, path, apiCall } = shared;
4
+
5
+ async function cmdRun(goal, target, args = []) {
6
+ if (!goal) {
7
+ console.log(`Usage: 0dai run <goal> [--dry-run] [--agent claude|codex|gemini]`);
8
+ console.log(` Example: 0dai run "add dark mode to settings page"`);
9
+ return;
10
+ }
11
+
12
+ const dryRun = args.includes("--dry-run");
13
+ const agentIdx = args.indexOf("--agent");
14
+ const agentOverride = agentIdx >= 0 ? args[agentIdx + 1] : null;
15
+
16
+ // Read project context
17
+ let stack = "generic", agents = ["claude"], commands = {};
18
+ try {
19
+ const disc = JSON.parse(fs.readFileSync(path.join(target, "ai", "manifest", "discovery.json"), "utf8"));
20
+ stack = disc.stack || "generic";
21
+ agents = disc.selected_agents || ["claude"];
22
+ } catch {}
23
+
24
+ if (!dryRun) process.stdout.write(`${T}[0dai]${R} decomposing goal...`);
25
+ const result = await apiCall("/v1/run", { goal, context: { stack, agents, commands } });
26
+ if (!dryRun) process.stdout.write("\r" + " ".repeat(40) + "\r");
27
+
28
+ if (result.error) { log(`error: ${result.error}`); return; }
29
+
30
+ const tasks = result.tasks || [];
31
+ if (!tasks.length) { log("no tasks returned"); return; }
32
+
33
+ console.log(`\n ${T}Goal:${R} ${goal}`);
34
+ console.log(` Decomposed into ${tasks.length} task${tasks.length === 1 ? "" : "s"}:\n`);
35
+
36
+ for (let i = 0; i < tasks.length; i++) {
37
+ const t = tasks[i];
38
+ const agent = agentOverride || t.assigned_to;
39
+ console.log(` ${T}${i + 1}.${R} ${t.title}`);
40
+ console.log(` ${D}→ ${agent} [${t.model_tier}]${t.description ? " " + t.description : ""}${R}`);
41
+ }
42
+
43
+ if (dryRun) {
44
+ console.log(`\n ${D}[dry-run] would create ${tasks.length} task(s) in swarm queue${R}\n`);
45
+ return;
46
+ }
47
+
48
+ // Create queue entries
49
+ const queueDir = path.join(target, "ai", "swarm", "queue");
50
+ try { fs.mkdirSync(queueDir, { recursive: true }); } catch {}
51
+
52
+ const created = [];
53
+ for (const t of tasks) {
54
+ const ts = Date.now();
55
+ const rand = Math.random().toString(36).slice(2, 6);
56
+ const id = `run-${ts}-${rand}`;
57
+ const entry = {
58
+ id,
59
+ title: t.title,
60
+ description: t.description || "",
61
+ assigned_to: agentOverride || t.assigned_to,
62
+ model_tier: t.model_tier,
63
+ created_by: "run",
64
+ created_at: new Date().toISOString(),
65
+ context: { goal },
66
+ };
67
+ try {
68
+ fs.writeFileSync(path.join(queueDir, `${id}.json`), JSON.stringify(entry, null, 2));
69
+ created.push(id);
70
+ } catch (e) { log(`warn: could not write task ${id}: ${e.message}`); }
71
+ }
72
+
73
+ console.log(`\n ${T}✓${R} ${created.length} task${created.length === 1 ? "" : "s"} added to swarm queue`);
74
+ console.log(` ${D}Monitor: 0dai watch | Queue: 0dai swarm status${R}\n`);
75
+ }
76
+
77
+ module.exports = { cmdRun };