@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.
- package/README.md +4 -2
- package/bin/0dai.js +153 -2154
- package/lib/commands/audit.js +129 -0
- package/lib/commands/auth.js +241 -0
- package/lib/commands/detect.js +31 -0
- package/lib/commands/doctor.js +194 -0
- package/lib/commands/experience.js +65 -0
- package/lib/commands/feedback.js +92 -0
- package/lib/commands/graph.js +270 -0
- package/lib/commands/init.js +327 -0
- package/lib/commands/metrics.js +145 -0
- package/lib/commands/models.js +90 -0
- package/lib/commands/portfolio.js +96 -0
- package/lib/commands/reflect.js +211 -0
- package/lib/commands/report.js +29 -0
- package/lib/commands/run.js +77 -0
- package/lib/commands/session.js +61 -0
- package/lib/commands/status.js +69 -0
- package/lib/commands/swarm.js +199 -0
- package/lib/commands/update.js +69 -0
- package/lib/commands/validate.js +71 -0
- package/lib/commands/watch.js +118 -0
- package/lib/commands/workspace.js +296 -0
- package/lib/onboarding.js +171 -0
- package/lib/shared.js +360 -0
- package/lib/utils/auth.js +142 -0
- package/lib/utils/constants.js +76 -0
- package/lib/utils/identity.js +147 -0
- package/lib/utils/plan.js +73 -0
- package/lib/wizard.js +311 -0
- package/package.json +13 -4
- package/scripts/postinstall.js +29 -0
|
@@ -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 };
|