@0dai-dev/cli 3.10.1 → 4.0.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,57 @@
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
+ module.exports = { cmdModels };
@@ -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 };
@@ -0,0 +1,61 @@
1
+ "use strict";
2
+ const shared = require("../shared");
3
+ const { log, fs, path, requirePlan } = shared;
4
+
5
+ function cmdSession(target, sub, args) {
6
+ const sessFile = path.join(target, "ai", "sessions", "active.json");
7
+ const sessDir = path.dirname(sessFile);
8
+
9
+ if (sub === "save") {
10
+ const gate = requirePlan("pro", "Session Roaming", target);
11
+ if (gate) {
12
+ log(gate.error);
13
+ log(gate.hint);
14
+ return;
15
+ }
16
+ fs.mkdirSync(sessDir, { recursive: true });
17
+ const goal = args.find((_, i) => args[i - 1] === "--goal") || "";
18
+ const summary = args.find((_, i) => args[i - 1] === "--summary") || "";
19
+ const session = {
20
+ id: `sess-${Date.now()}`,
21
+ started: new Date().toISOString(),
22
+ current_agent: "cli",
23
+ task: { goal: goal || summary || "active session", status: "in_progress" },
24
+ handoff_notes: summary,
25
+ context: { files_touched: [] },
26
+ };
27
+ if (fs.existsSync(sessFile)) {
28
+ const existing = JSON.parse(fs.readFileSync(sessFile, "utf8"));
29
+ existing.handoff_notes = summary || existing.handoff_notes;
30
+ if (goal) existing.task.goal = goal;
31
+ existing.updated = new Date().toISOString();
32
+ fs.writeFileSync(sessFile, JSON.stringify(existing, null, 2));
33
+ log("session updated");
34
+ } else {
35
+ fs.writeFileSync(sessFile, JSON.stringify(session, null, 2));
36
+ log(`session started: ${session.id}`);
37
+ }
38
+ return;
39
+ }
40
+ if (sub === "status") {
41
+ if (!fs.existsSync(sessFile)) { log("no active session"); return; }
42
+ const s = JSON.parse(fs.readFileSync(sessFile, "utf8"));
43
+ log(`session: ${(s.task || {}).goal || "?"}`);
44
+ console.log(` agent: ${s.current_agent || "?"}`);
45
+ if (s.handoff_notes) console.log(` handoff: ${s.handoff_notes}`);
46
+ return;
47
+ }
48
+ if (sub === "complete") {
49
+ if (!fs.existsSync(sessFile)) { log("no active session"); return; }
50
+ const archiveDir = path.join(target, "ai", "sessions", "archive");
51
+ fs.mkdirSync(archiveDir, { recursive: true });
52
+ const s = JSON.parse(fs.readFileSync(sessFile, "utf8"));
53
+ fs.writeFileSync(path.join(archiveDir, `${s.id || "session"}.json`), JSON.stringify(s, null, 2));
54
+ fs.unlinkSync(sessFile);
55
+ log(`session ${s.id} archived`);
56
+ return;
57
+ }
58
+ console.log("Usage: 0dai session [save|status|complete] [--goal '...'] [--summary '...']");
59
+ }
60
+
61
+ module.exports = { cmdSession };
@@ -0,0 +1,69 @@
1
+ "use strict";
2
+ const shared = require("../shared");
3
+ const { log, T, R, D, fs, path, spawnSync, findRepoScript, getSwarmQuotaLocal, _detectPlanLocal, PLAN_LEVELS } = shared;
4
+
5
+ function cmdStatus(target) {
6
+ const ai = path.join(target, "ai");
7
+ let v = "?", stack = "?";
8
+ try { v = fs.readFileSync(path.join(ai, "VERSION"), "utf8").trim(); } catch {}
9
+ try { stack = JSON.parse(fs.readFileSync(path.join(ai, "manifest", "discovery.json"), "utf8")).stack || "?"; } catch {}
10
+ log(`v${v} | stack: ${stack}`);
11
+
12
+ const count = (dir) => { try { return fs.readdirSync(dir).filter(f => f.endsWith(".json")).length; } catch { return 0; } };
13
+ const q = count(path.join(ai, "swarm", "queue"));
14
+ const a = count(path.join(ai, "swarm", "active"));
15
+ const d = count(path.join(ai, "swarm", "done"));
16
+ if (q || a || d) console.log(` swarm: ${q} queued, ${a} active, ${d} done`);
17
+
18
+ // Swarm quota
19
+ const quota = getSwarmQuotaLocal(target);
20
+ if (quota.plan === "free") {
21
+ console.log(` swarm quota: ${D}locked (Free) — upgrade for ${quota.daily_limit} tasks/day${R}`);
22
+ } else {
23
+ console.log(` swarm quota: ${quota.used_today}/${quota.daily_limit} tasks today (${quota.plan})`);
24
+ }
25
+
26
+ // Session roaming status
27
+ const sessPlan = _detectPlanLocal(target);
28
+ const sessLocked = PLAN_LEVELS[sessPlan] < PLAN_LEVELS["pro"];
29
+ if (sessLocked) {
30
+ console.log(` session roaming: ${D}locked (Free) — upgrade to save/resume sessions${R}`);
31
+ } else {
32
+ console.log(` session roaming: ${T}available (${sessPlan})${R}`);
33
+ }
34
+
35
+ try {
36
+ const s = JSON.parse(fs.readFileSync(path.join(ai, "sessions", "active.json"), "utf8"));
37
+ console.log(` session: ${(s.task || {}).goal || "?"} (agent: ${s.current_agent || "?"})`);
38
+ } catch {}
39
+
40
+ // Anti-pattern warnings count
41
+ try {
42
+ const ds = findRepoScript(target, "anti_pattern_detector.py");
43
+ if (ds) {
44
+ const wr = spawnSync("python3", [ds, "count", "--target", target],
45
+ { stdio: ["ignore", "pipe", "ignore"], encoding: "utf8", timeout: 5000 });
46
+ if (wr.status === 0 && wr.stdout) {
47
+ const wc = JSON.parse(wr.stdout.trim());
48
+ if (wc.count > 0) console.log(` warnings: ${wc.count} active — run: 0dai experience warnings`);
49
+ }
50
+ }
51
+ } catch {}
52
+
53
+ // First-status tip (shows once after init)
54
+ try { require("../onboarding").showFirstStatusTip(target); } catch {}
55
+
56
+ // Drift warning (lightweight)
57
+ try {
58
+ const ds = findRepoScript(target, "drift_detector.py");
59
+ if (ds) {
60
+ const dr = spawnSync("python3", [ds, "report", "--target", target],
61
+ { stdio: ["ignore", "pipe", "ignore"], encoding: "utf8", timeout: 5000 });
62
+ if (dr.stdout && (dr.stdout.includes("MODIFIED") || dr.stdout.includes("CONTRADICTS"))) {
63
+ console.log(` drift: config changes detected — run: 0dai doctor --drift`);
64
+ }
65
+ }
66
+ } catch {}
67
+ }
68
+
69
+ module.exports = { cmdStatus };