@0dai-dev/cli 2.9.0 → 3.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/bin/0dai.js +435 -20
  2. package/package.json +1 -1
package/bin/0dai.js CHANGED
@@ -7,7 +7,7 @@ const fs = require("fs");
7
7
  const path = require("path");
8
8
  const os = require("os");
9
9
 
10
- const VERSION = "2.9.0";
10
+ const VERSION = require("../package.json").version;
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
@@ -16,6 +16,7 @@ const log = (msg) => console.log(`${T}[0dai]${R} ${msg}`);
16
16
  const CONFIG_DIR = path.join(os.homedir(), ".0dai");
17
17
  const AUTH_FILE = path.join(CONFIG_DIR, "auth.json");
18
18
  const VERSION_CHECK_FILE = path.join(CONFIG_DIR, ".version_check");
19
+ const PROJECTS_FILE = path.join(CONFIG_DIR, "projects.json");
19
20
 
20
21
  const MANIFEST_FILES = [
21
22
  "package.json", "go.mod", "pyproject.toml", "requirements.txt",
@@ -53,6 +54,183 @@ function deviceFingerprint() {
53
54
  return crypto.createHash("sha256").update(parts.join(":")).digest("hex").slice(0, 32);
54
55
  }
55
56
 
57
+ function registerProject(projectPath, name, stack) {
58
+ try {
59
+ fs.mkdirSync(CONFIG_DIR, { recursive: true });
60
+ let projects = [];
61
+ try { projects = JSON.parse(fs.readFileSync(PROJECTS_FILE, "utf8")).projects || []; } catch {}
62
+ const abs = path.resolve(projectPath);
63
+ const idx = projects.findIndex(p => p.path === abs);
64
+ const entry = { path: abs, name: name || path.basename(abs), stack: stack || "?", last_seen: new Date().toISOString() };
65
+ if (idx >= 0) projects[idx] = entry;
66
+ else projects.unshift(entry);
67
+ fs.writeFileSync(PROJECTS_FILE, JSON.stringify({ projects: projects.slice(0, 50) }, null, 2));
68
+ } catch {}
69
+ }
70
+
71
+ function cmdPortfolio() {
72
+ let projects = [];
73
+ try { projects = JSON.parse(fs.readFileSync(PROJECTS_FILE, "utf8")).projects || []; } catch {}
74
+
75
+ if (!projects.length) {
76
+ log(`no projects registered yet`);
77
+ console.log(` Run ${D}0dai init${R} in a project to start tracking it.`);
78
+ return;
79
+ }
80
+
81
+ const rows = [];
82
+ let totalSessions = 0, totalScore = 0, scored = 0;
83
+
84
+ for (const p of projects) {
85
+ if (!fs.existsSync(p.path)) continue;
86
+
87
+ let sessions = 0, lastSession = null, agentMap = {};
88
+ try {
89
+ const stats = JSON.parse(fs.readFileSync(path.join(p.path, "ai", "feedback", ".usage_stats.json"), "utf8"));
90
+ sessions = stats.total_sessions || 0;
91
+ lastSession = stats.last_session || null;
92
+ agentMap = stats.agents || {};
93
+ } catch {}
94
+
95
+ let name = p.name, stack = p.stack;
96
+ try {
97
+ const disc = JSON.parse(fs.readFileSync(path.join(p.path, "ai", "manifest", "discovery.json"), "utf8"));
98
+ name = disc.project_name || name;
99
+ stack = disc.stack || stack;
100
+ } catch {}
101
+
102
+ // Effectiveness score (mirrors metrics command)
103
+ let score = 0;
104
+ if (sessions > 0) {
105
+ score += Math.min(sessions * 5, 35);
106
+ let done = 0;
107
+ try { done = fs.readdirSync(path.join(p.path, "ai", "swarm", "done")).filter(f => f.endsWith(".json")).length; } catch {}
108
+ if (done > 0) score += Math.min(done * 6, 30);
109
+ try {
110
+ const hasFb = fs.readdirSync(path.join(p.path, "ai", "feedback")).some(f => f.endsWith("-report.json"));
111
+ if (hasFb) score += 20;
112
+ } catch {}
113
+ const layerFiles = ["ai/manifest/discovery.json", "ai/manifest/commands.yaml", "ai/playbooks/quick-start.md"];
114
+ score += Math.round(layerFiles.filter(f => fs.existsSync(path.join(p.path, f))).length / layerFiles.length * 15);
115
+ }
116
+
117
+ totalSessions += sessions;
118
+ if (sessions > 0) { totalScore += score; scored++; }
119
+
120
+ const agentList = Object.keys(agentMap).join("·") || "—";
121
+
122
+ let ago = "never";
123
+ if (lastSession) {
124
+ const h = Math.floor((Date.now() - new Date(lastSession).getTime()) / 3600000);
125
+ const d = Math.floor(h / 24);
126
+ if (h < 1) ago = "<1h ago";
127
+ else if (h < 24) ago = `${h}h ago`;
128
+ else if (d < 7) ago = `${d}d ago`;
129
+ else ago = `${Math.floor(d / 7)}w ago`;
130
+ }
131
+
132
+ rows.push({ name, stack, score: sessions > 0 ? score : null, sessions, agents: agentList, ago });
133
+ }
134
+
135
+ if (!rows.length) {
136
+ log("no projects found (paths may have moved)");
137
+ return;
138
+ }
139
+
140
+ const nameW = Math.min(Math.max(...rows.map(r => r.name.length), 4), 28);
141
+ const stackW = Math.min(Math.max(...rows.map(r => r.stack.length), 5), 16);
142
+
143
+ console.log(`\n ${T}Portfolio${R} — ${rows.length} project${rows.length === 1 ? "" : "s"}\n`);
144
+ for (const r of rows) {
145
+ const nm = r.name.slice(0, nameW).padEnd(nameW);
146
+ const st = r.stack.slice(0, stackW).padEnd(stackW);
147
+ const sc = r.score !== null ? `score ${String(r.score).padStart(3)}` : " ";
148
+ const se = `${String(r.sessions).padStart(2)} session${r.sessions === 1 ? " " : "s"}`;
149
+ console.log(` ${T}${nm}${R} ${D}${st}${R} ${sc} ${se} ${r.agents.padEnd(14)} ${D}${r.ago}${R}`);
150
+ }
151
+
152
+ if (totalSessions > 0) {
153
+ const avg = scored > 0 ? Math.round(totalScore / scored) : 0;
154
+ const stacks = [...new Set(rows.map(r => r.stack))].length;
155
+ console.log(`\n ${D}${"─".repeat(70)}`);
156
+ console.log(` Total: ${totalSessions} sessions ${stacks} stack${stacks === 1 ? "" : "s"} avg effectiveness: ${avg}${R}\n`);
157
+ } else {
158
+ console.log(`\n ${D}Tip: run '0dai init' in your projects to start tracking sessions.${R}\n`);
159
+ }
160
+ }
161
+
162
+ async function cmdRun(goal, target, args = []) {
163
+ if (!goal) {
164
+ console.log(`Usage: 0dai run <goal> [--dry-run] [--agent claude|codex|gemini]`);
165
+ console.log(` Example: 0dai run "add dark mode to settings page"`);
166
+ return;
167
+ }
168
+
169
+ const dryRun = args.includes("--dry-run");
170
+ const agentIdx = args.indexOf("--agent");
171
+ const agentOverride = agentIdx >= 0 ? args[agentIdx + 1] : null;
172
+
173
+ // Read project context
174
+ let stack = "generic", agents = ["claude"], commands = {};
175
+ try {
176
+ const disc = JSON.parse(fs.readFileSync(path.join(target, "ai", "manifest", "discovery.json"), "utf8"));
177
+ stack = disc.stack || "generic";
178
+ agents = disc.selected_agents || ["claude"];
179
+ } catch {}
180
+
181
+ if (!dryRun) process.stdout.write(`${T}[0dai]${R} decomposing goal...`);
182
+ const result = await apiCall("/v1/run", { goal, context: { stack, agents, commands } });
183
+ if (!dryRun) process.stdout.write("\r" + " ".repeat(40) + "\r");
184
+
185
+ if (result.error) { log(`error: ${result.error}`); return; }
186
+
187
+ const tasks = result.tasks || [];
188
+ if (!tasks.length) { log("no tasks returned"); return; }
189
+
190
+ console.log(`\n ${T}Goal:${R} ${goal}`);
191
+ console.log(` Decomposed into ${tasks.length} task${tasks.length === 1 ? "" : "s"}:\n`);
192
+
193
+ for (let i = 0; i < tasks.length; i++) {
194
+ const t = tasks[i];
195
+ const agent = agentOverride || t.assigned_to;
196
+ console.log(` ${T}${i + 1}.${R} ${t.title}`);
197
+ console.log(` ${D}→ ${agent} [${t.model_tier}]${t.description ? " " + t.description : ""}${R}`);
198
+ }
199
+
200
+ if (dryRun) {
201
+ console.log(`\n ${D}[dry-run] would create ${tasks.length} task(s) in swarm queue${R}\n`);
202
+ return;
203
+ }
204
+
205
+ // Create queue entries
206
+ const queueDir = path.join(target, "ai", "swarm", "queue");
207
+ try { fs.mkdirSync(queueDir, { recursive: true }); } catch {}
208
+
209
+ const created = [];
210
+ for (const t of tasks) {
211
+ const ts = Date.now();
212
+ const rand = Math.random().toString(36).slice(2, 6);
213
+ const id = `run-${ts}-${rand}`;
214
+ const entry = {
215
+ id,
216
+ title: t.title,
217
+ description: t.description || "",
218
+ assigned_to: agentOverride || t.assigned_to,
219
+ model_tier: t.model_tier,
220
+ created_by: "run",
221
+ created_at: new Date().toISOString(),
222
+ context: { goal },
223
+ };
224
+ try {
225
+ fs.writeFileSync(path.join(queueDir, `${id}.json`), JSON.stringify(entry, null, 2));
226
+ created.push(id);
227
+ } catch (e) { log(`warn: could not write task ${id}: ${e.message}`); }
228
+ }
229
+
230
+ console.log(`\n ${T}✓${R} ${created.length} task${created.length === 1 ? "" : "s"} added to swarm queue`);
231
+ console.log(` ${D}Monitor: 0dai watch | Queue: 0dai swarm status${R}\n`);
232
+ }
233
+
56
234
  function apiCall(endpoint, data) {
57
235
  return new Promise((resolve, reject) => {
58
236
  const url = new URL(endpoint, API_URL);
@@ -126,22 +304,61 @@ function collectMetadata(target) {
126
304
  return { projectFiles, fileContents, clis };
127
305
  }
128
306
 
307
+ // Fields in settings.json that belong to the user, not to 0dai
308
+ const SETTINGS_PRESERVE_FIELDS = ["model", "permissionMode", "effortLevel"];
309
+
310
+ function mergeSettingsJson(existing, incoming) {
311
+ try {
312
+ const base = JSON.parse(incoming);
313
+ const user = JSON.parse(existing);
314
+ // Preserve user-owned fields
315
+ for (const field of SETTINGS_PRESERVE_FIELDS) {
316
+ if (field in user && user[field] !== base[field]) {
317
+ base[field] = user[field];
318
+ }
319
+ }
320
+ return JSON.stringify(base, null, 2) + "\n";
321
+ } catch { return incoming; }
322
+ }
323
+
324
+ function mergeAgentsMd(existing, incoming) {
325
+ // If user marked as unmanaged, don't touch
326
+ if (existing.includes("managed: false")) return existing;
327
+ // If managed, update with new content but preserve user additions after managed block
328
+ return incoming;
329
+ }
330
+
129
331
  function writeFiles(target, files) {
130
- let created = 0, updated = 0, unchanged = 0;
332
+ let created = 0, updated = 0, unchanged = 0, merged = 0;
131
333
  for (const [rel, content] of Object.entries(files)) {
132
334
  const p = path.join(target, rel);
133
335
  fs.mkdirSync(path.dirname(p), { recursive: true });
134
- try {
135
- if (fs.existsSync(p) && fs.readFileSync(p, "utf8") === content) {
136
- unchanged++;
336
+
337
+ let finalContent = content;
338
+
339
+ // Smart merge for specific files
340
+ if (fs.existsSync(p)) {
341
+ const existing = fs.readFileSync(p, "utf8");
342
+ if (existing === content) { unchanged++; continue; }
343
+
344
+ if (rel.endsWith("settings.json")) {
345
+ finalContent = mergeSettingsJson(existing, content);
346
+ merged++;
347
+ } else if (rel === "AGENTS.md" && existing.includes("managed: false")) {
348
+ unchanged++; // User owns this file, skip
137
349
  continue;
350
+ } else {
351
+ updated++;
138
352
  }
139
- if (fs.existsSync(p)) updated++;
140
- else created++;
141
- } catch { created++; }
142
- fs.writeFileSync(p, content, "utf8");
353
+ } else {
354
+ created++;
355
+ }
356
+
357
+ fs.writeFileSync(p, finalContent, "utf8");
143
358
  }
144
- log(`${created} created, ${updated} updated, ${unchanged} unchanged`);
359
+ const parts = [`${created} created`, `${updated} updated`, `${unchanged} unchanged`];
360
+ if (merged) parts.push(`${merged} merged`);
361
+ log(parts.join(", "));
145
362
  return created + updated;
146
363
  }
147
364
 
@@ -201,19 +418,23 @@ async function cmdInit(target, args = []) {
201
418
  if (!text.includes(".0dai")) fs.appendFileSync(gi, "\n.0dai/\n");
202
419
  } catch {}
203
420
 
421
+ // Register in global portfolio
422
+ registerProject(target, path.basename(target), result.stack);
423
+
204
424
  log(`initialized (${result.file_count || "?"} files)`);
205
425
  console.log(" skills: /build /review /status /feedback /bugfix /delegate");
206
426
 
207
- // Send anonymous usage ping (stack + file count, no project data)
427
+ // Next steps guide user to first value
428
+ console.log(`\n ${T}Next steps:${R}`);
429
+ console.log(` ${D}1.${R} Check health: ${D}0dai doctor${R}`);
430
+ console.log(` ${D}2.${R} Try delegation: ${D}0dai run "write tests for auth"${R}`);
431
+ console.log(` ${D}3.${R} Open dashboard: ${D}https://0dai.dev/dashboard${R}`);
432
+
433
+ // Send anonymous usage ping
208
434
  apiCall("/v1/feedback", { report: {
209
435
  stack_detected: result.stack || "?", _auto: true, _plan: result.plan || "trial",
210
436
  _cli_version: VERSION, _files_generated: result.file_count || 0,
211
437
  }}).catch(() => {});
212
-
213
- // Encourage feedback
214
- console.log(`\n ${T}Tip:${R} Send feedback to earn +5 init/day for 7 days:`);
215
- console.log(` ${D}0dai feedback log --type positive --detail "what worked"${R}`);
216
- console.log(` ${D}0dai feedback push${R}`);
217
438
  }
218
439
 
219
440
  async function cmdSync(target, args = []) {
@@ -283,9 +504,13 @@ async function cmdSync(target, args = []) {
283
504
  for (const f of Object.keys(updated)) console.log(` ~ ${f}`);
284
505
  }
285
506
  log(`sync: ${changedCount} file(s) updated`);
507
+ console.log(` ${D}Run: 0dai doctor to verify project health${R}`);
286
508
  } else {
287
509
  log("already up to date");
288
510
  }
511
+
512
+ // Update portfolio registry
513
+ registerProject(target, path.basename(target), stack);
289
514
  }
290
515
 
291
516
  async function cmdDetect(target) {
@@ -554,14 +779,36 @@ function cmdValidate(target) {
554
779
  for (const f of agentFiles[agent] || []) required.push(f);
555
780
  }
556
781
 
782
+ const FIX_HINTS = {
783
+ "ai/VERSION": "run: 0dai init",
784
+ "ai/VERSION_SCHEMA": "run: 0dai sync",
785
+ "ai/manifest/project.yaml": "run: 0dai init",
786
+ "ai/manifest/discovery.json": "run: 0dai init",
787
+ "ai/manifest/applied-lock.json": "run: 0dai sync",
788
+ "ai/manifest/environment.yaml": "run: 0dai sync",
789
+ "ai/manifest/commands.yaml": "run: 0dai sync",
790
+ "AGENTS.md": "run: 0dai sync",
791
+ ".claude/settings.json": "run: 0dai sync",
792
+ ".claude/CLAUDE.md": "run: 0dai sync",
793
+ ".mcp.json": "run: 0dai sync",
794
+ ".codex/config.toml": "install codex, then: 0dai sync",
795
+ "opencode.json": "install opencode, then: 0dai sync",
796
+ };
797
+
798
+ const present = required.filter(f => fs.existsSync(path.join(target, f)));
557
799
  const missing = required.filter(f => !fs.existsSync(path.join(target, f)));
800
+
801
+ for (const f of present) console.log(` ${G}✓${R2} ${f}`);
802
+ for (const f of missing) {
803
+ const hint = FIX_HINTS[f] || "run: 0dai sync";
804
+ console.log(` ${E}✗${R2} ${f} ${D2}— ${hint}${R2}`);
805
+ }
806
+
558
807
  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}`);
808
+ console.log(`\n${E}${missing.length} missing${R2} / ${present.length + missing.length} total`);
562
809
  process.exitCode = 1;
563
810
  } else {
564
- log(`${G}validate ok${R2} — all required files present`);
811
+ log(`${G}validate ok${R2} — all ${present.length} required files present`);
565
812
  }
566
813
  }
567
814
 
@@ -704,6 +951,55 @@ function cmdReflect(target, args) {
704
951
  }
705
952
  } catch {}
706
953
 
954
+ // Insights — learn from patterns
955
+ if (totalDone >= 3) {
956
+ const insights = [];
957
+ // Best agent by completion rate
958
+ const agentEntries = Object.entries(byAgent).map(([a, d]) => {
959
+ const pending = allPendingByAgent[a] || 0;
960
+ const total = d.done + pending;
961
+ return { agent: a, done: d.done, total, rate: total > 0 ? d.done / total : 1 };
962
+ });
963
+ const best = agentEntries.sort((a, b) => b.rate - a.rate)[0];
964
+ if (best && agentEntries.length > 1) {
965
+ insights.push(`${best.agent} has the highest success rate (${Math.round(best.rate * 100)}%)`);
966
+ }
967
+ // Fastest agent by avg elapsed
968
+ try {
969
+ const budget = JSON.parse(fs.readFileSync(path.join(ai, "swarm", "budget.json"), "utf8"));
970
+ const agentElapsed = {};
971
+ const agentCounts = {};
972
+ for (const t of Object.values(budget.tasks || {})) {
973
+ if (t.elapsed > 0) {
974
+ agentElapsed[t.agent] = (agentElapsed[t.agent] || 0) + t.elapsed;
975
+ agentCounts[t.agent] = (agentCounts[t.agent] || 0) + 1;
976
+ }
977
+ }
978
+ let fastest = null, fastestAvg = Infinity;
979
+ for (const [a, total] of Object.entries(agentElapsed)) {
980
+ const avg = total / agentCounts[a];
981
+ if (avg < fastestAvg) { fastest = a; fastestAvg = avg; }
982
+ }
983
+ if (fastest && Object.keys(agentElapsed).length > 1) {
984
+ insights.push(`${fastest} is fastest (avg ${Math.round(fastestAvg)}s per task)`);
985
+ }
986
+ } catch {}
987
+ // Cost efficiency
988
+ if (allDone.length > 0) {
989
+ try {
990
+ const budget = JSON.parse(fs.readFileSync(path.join(ai, "swarm", "budget.json"), "utf8"));
991
+ if (budget.total_spent > 0) {
992
+ const costPerTask = budget.total_spent / allDone.length;
993
+ insights.push(`avg cost: $${costPerTask.toFixed(4)}/task`);
994
+ }
995
+ } catch {}
996
+ }
997
+ if (insights.length) {
998
+ console.log(`\n ${B}Insights:${R2}`);
999
+ for (const i of insights) console.log(` ${G}→${R2} ${i}`);
1000
+ }
1001
+ }
1002
+
707
1003
  // Remaining blockers
708
1004
  if (allQueue.length) {
709
1005
  console.log(`\n ${B}Blockers / queue:${R2}`);
@@ -1376,6 +1672,119 @@ async function cmdFeedback(target, sub, args) {
1376
1672
  console.log("Usage: 0dai feedback [push|log|list] [--type ...] [--detail '...']");
1377
1673
  }
1378
1674
 
1675
+ function cmdWatch(target, args) {
1676
+ const isTTY = process.stdout.isTTY;
1677
+ const B2 = isTTY ? "\x1b[1m" : "";
1678
+ const DIM = isTTY ? "\x1b[2m" : "";
1679
+ const G = isTTY ? "\x1b[32m" : "";
1680
+ const Y = isTTY ? "\x1b[33m" : "";
1681
+ const C = isTTY ? "\x1b[36m" : "";
1682
+ const M = isTTY ? "\x1b[35m" : "";
1683
+ const R2 = isTTY ? "\x1b[0m" : "";
1684
+
1685
+ const swarmDir = path.join(target, "ai", "swarm");
1686
+ const interval = parseInt(args.find((_, i) => args[i - 1] === "--interval") || "3", 10) * 1000;
1687
+
1688
+ function readDir(dir) {
1689
+ try {
1690
+ return fs.readdirSync(dir)
1691
+ .filter(f => f.endsWith(".json"))
1692
+ .map(f => { try { return JSON.parse(fs.readFileSync(path.join(dir, f), "utf8")); } catch { return null; } })
1693
+ .filter(Boolean);
1694
+ } catch { return []; }
1695
+ }
1696
+
1697
+ function ago(ts) {
1698
+ if (!ts) return "—";
1699
+ const s = Math.floor((Date.now() - new Date(ts).getTime()) / 1000);
1700
+ if (s < 60) return `${s}s`;
1701
+ if (s < 3600) return `${Math.floor(s / 60)}m`;
1702
+ return `${Math.floor(s / 3600)}h`;
1703
+ }
1704
+
1705
+ function tierColor(tier) {
1706
+ if (tier === "deep") return M;
1707
+ if (tier === "fast") return C;
1708
+ return G;
1709
+ }
1710
+
1711
+ function statusColor(status) {
1712
+ if (status === "done") return G;
1713
+ if (status === "active" || status === "running") return Y;
1714
+ if (status === "failed") return "\x1b[31m";
1715
+ return DIM;
1716
+ }
1717
+
1718
+ function render() {
1719
+ const queue = readDir(path.join(swarmDir, "queue"));
1720
+ const active = readDir(path.join(swarmDir, "active"));
1721
+ const done = readDir(path.join(swarmDir, "done")).sort(
1722
+ (a, b) => new Date(b.completed_at || b.created_at) - new Date(a.completed_at || a.created_at)
1723
+ ).slice(0, 8);
1724
+
1725
+ const lines = [];
1726
+ const w = process.stdout.columns || 100;
1727
+ const sep = DIM + "─".repeat(Math.min(w, 96)) + R2;
1728
+
1729
+ lines.push(`\n ${B2}${T}0dai watch${R2} ${DIM}${new Date().toLocaleTimeString()} (q to quit)${R2}`);
1730
+ lines.push(sep);
1731
+
1732
+ const header = ` ${"STATUS".padEnd(9)} ${"AGENT".padEnd(10)} ${"TIER".padEnd(9)} ${"AGE".padEnd(6)} TITLE`;
1733
+ lines.push(DIM + header + R2);
1734
+ lines.push(sep);
1735
+
1736
+ const printTask = (t, statusOverride) => {
1737
+ const status = statusOverride || t.status || "queued";
1738
+ const sc = statusColor(status);
1739
+ const tc = tierColor(t.model_tier);
1740
+ const title = (t.title || "—").slice(0, Math.max(20, w - 42));
1741
+ const age = ago(t.created_at);
1742
+ lines.push(
1743
+ ` ${sc}${status.padEnd(9)}${R2} ` +
1744
+ `${DIM}${(t.assigned_to || "—").padEnd(10)}${R2} ` +
1745
+ `${tc}${(t.model_tier || "—").padEnd(9)}${R2} ` +
1746
+ `${DIM}${age.padEnd(6)}${R2} ${title}`
1747
+ );
1748
+ };
1749
+
1750
+ if (active.length) {
1751
+ active.forEach(t => printTask(t, "active"));
1752
+ }
1753
+ if (queue.length) {
1754
+ queue.forEach(t => printTask(t, "queued"));
1755
+ }
1756
+ if (!active.length && !queue.length) {
1757
+ lines.push(` ${DIM}No tasks in queue or active.${R2}`);
1758
+ }
1759
+
1760
+ lines.push(sep);
1761
+ if (done.length) {
1762
+ lines.push(` ${DIM}Recently done:${R2}`);
1763
+ done.forEach(t => printTask(t, "done"));
1764
+ }
1765
+
1766
+ lines.push(`\n ${DIM}queue: ${queue.length} active: ${active.length} done (total): ${readDir(path.join(swarmDir, "done")).length}${R2}\n`);
1767
+
1768
+ if (isTTY) process.stdout.write("\x1b[2J\x1b[H");
1769
+ console.log(lines.join("\n"));
1770
+ }
1771
+
1772
+ render();
1773
+ const timer = setInterval(render, interval);
1774
+
1775
+ // Exit on 'q' or Ctrl+C
1776
+ if (isTTY && process.stdin.setRawMode) {
1777
+ process.stdin.setRawMode(true);
1778
+ process.stdin.resume();
1779
+ process.stdin.once("data", (key) => {
1780
+ clearInterval(timer);
1781
+ process.stdin.setRawMode(false);
1782
+ process.exit(0);
1783
+ });
1784
+ }
1785
+ process.on("SIGINT", () => { clearInterval(timer); process.exit(0); });
1786
+ }
1787
+
1379
1788
  async function main() {
1380
1789
  const args = process.argv.slice(2);
1381
1790
  let target = process.cwd();
@@ -1389,6 +1798,8 @@ async function main() {
1389
1798
  checkVersion();
1390
1799
 
1391
1800
  switch (cmd) {
1801
+ case "run": await cmdRun(args[1] || "", target, args.slice(2)); break;
1802
+ case "watch": cmdWatch(target, args.slice(1)); break;
1392
1803
  case "audit": cmdAudit(target); break;
1393
1804
  case "init": await cmdInit(target, args); break;
1394
1805
  case "sync": await cmdSync(target, args); break;
@@ -1397,6 +1808,7 @@ async function main() {
1397
1808
  case "validate": cmdValidate(target); break;
1398
1809
  case "reflect": cmdReflect(target, args); break;
1399
1810
  case "metrics": cmdMetrics(target); break;
1811
+ case "portfolio": cmdPortfolio(); break;
1400
1812
  case "status": cmdStatus(target); break;
1401
1813
  case "auth":
1402
1814
  if (sub === "login") await cmdAuthLogin();
@@ -1436,6 +1848,8 @@ async function main() {
1436
1848
  case "help": case "--help": case "-h":
1437
1849
  console.log(`\n ${T}0dai${R} v${VERSION} — One config for 5 AI agent CLIs\n`);
1438
1850
  console.log("Commands:");
1851
+ console.log(" run <goal> AI-decompose goal → swarm tasks (auto-routed) [--dry-run]");
1852
+ console.log(" watch Live task monitor: queue, active, recently done [--interval N]");
1439
1853
  console.log(" audit Scan ai/ and agent configs for leaked secrets");
1440
1854
  console.log(" init Initialize ai/ layer (via API) [--dry-run] [--minimal]");
1441
1855
  console.log(" sync Update ai/ layer (via API) [--dry-run] [--quiet]");
@@ -1444,6 +1858,7 @@ async function main() {
1444
1858
  console.log(" validate Validate ai/ layer completeness");
1445
1859
  console.log(" reflect Session reflection: delivered, delegation rate, blockers");
1446
1860
  console.log(" metrics Effectiveness score: adoption funnel, sessions, delegation");
1861
+ console.log(" portfolio All tracked projects: score, sessions, agents, last activity");
1447
1862
  console.log(" status Show maturity, swarm, session");
1448
1863
  console.log(" session save Save session for roaming");
1449
1864
  console.log(" swarm status Task queue & delegation");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@0dai-dev/cli",
3
- "version": "2.9.0",
3
+ "version": "3.1.2",
4
4
  "description": "One config layer for 5 AI agent CLIs — Claude Code, Codex, OpenCode, Gemini, Aider",
5
5
  "bin": {
6
6
  "0dai": "./bin/0dai.js"