@0dai-dev/cli 2.8.0 → 3.1.1

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 +593 -25
  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.8.0";
10
+ const VERSION = "3.0.0";
11
11
  const API_URL = process.env.ODAI_API_URL || "https://api.0dai.dev";
12
12
  const T = process.stdout.isTTY ? "\x1b[38;2;45;212;168m" : ""; // teal
13
13
  const R = process.stdout.isTTY ? "\x1b[0m" : ""; // reset
@@ -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,27 +304,67 @@ 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
 
148
365
  async function cmdInit(target, args = []) {
149
366
  const dryRun = args.includes("--dry-run");
367
+ const minimal = args.includes("--minimal");
150
368
 
151
369
  if (fs.existsSync(path.join(target, "ai", "VERSION"))) {
152
370
  const v = fs.readFileSync(path.join(target, "ai", "VERSION"), "utf8").trim();
@@ -169,6 +387,7 @@ async function cmdInit(target, args = []) {
169
387
  file_contents: fileContents,
170
388
  available_clis: clis,
171
389
  dry_run: dryRun,
390
+ minimal: minimal,
172
391
  });
173
392
 
174
393
  if (result.error) {
@@ -199,23 +418,28 @@ async function cmdInit(target, args = []) {
199
418
  if (!text.includes(".0dai")) fs.appendFileSync(gi, "\n.0dai/\n");
200
419
  } catch {}
201
420
 
421
+ // Register in global portfolio
422
+ registerProject(target, path.basename(target), result.stack);
423
+
202
424
  log(`initialized (${result.file_count || "?"} files)`);
203
425
  console.log(" skills: /build /review /status /feedback /bugfix /delegate");
204
426
 
205
- // 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
206
434
  apiCall("/v1/feedback", { report: {
207
435
  stack_detected: result.stack || "?", _auto: true, _plan: result.plan || "trial",
208
436
  _cli_version: VERSION, _files_generated: result.file_count || 0,
209
437
  }}).catch(() => {});
210
-
211
- // Encourage feedback
212
- console.log(`\n ${T}Tip:${R} Send feedback to earn +5 init/day for 7 days:`);
213
- console.log(` ${D}0dai feedback log --type positive --detail "what worked"${R}`);
214
- console.log(` ${D}0dai feedback push${R}`);
215
438
  }
216
439
 
217
440
  async function cmdSync(target, args = []) {
218
441
  const dryRun = args.includes("--dry-run");
442
+ const quiet = args.includes("--quiet") || args.includes("-q");
219
443
 
220
444
  // Quick local check: skip API if already at current version (unless dry-run)
221
445
  let version = "unknown";
@@ -257,7 +481,7 @@ async function cmdSync(target, args = []) {
257
481
  const result = await apiCall("/v1/sync", {
258
482
  ai_version: version, stack, agents: agents.length ? agents : clis,
259
483
  current_files: currentFiles, file_contents: fileContents,
260
- dry_run: dryRun,
484
+ dry_run: dryRun, quiet,
261
485
  });
262
486
 
263
487
  if (result.error) { log(`error: ${result.error}`); process.exit(1); }
@@ -273,8 +497,20 @@ async function cmdSync(target, args = []) {
273
497
  }
274
498
  return;
275
499
  }
276
- if (Object.keys(updated).length) writeFiles(target, updated);
277
- else log("already up to date");
500
+ const changedCount = Object.keys(updated).length;
501
+ if (changedCount) {
502
+ writeFiles(target, updated);
503
+ if (!quiet) {
504
+ for (const f of Object.keys(updated)) console.log(` ~ ${f}`);
505
+ }
506
+ log(`sync: ${changedCount} file(s) updated`);
507
+ console.log(` ${D}Run: 0dai doctor to verify project health${R}`);
508
+ } else {
509
+ log("already up to date");
510
+ }
511
+
512
+ // Update portfolio registry
513
+ registerProject(target, path.basename(target), stack);
278
514
  }
279
515
 
280
516
  async function cmdDetect(target) {
@@ -543,14 +779,36 @@ function cmdValidate(target) {
543
779
  for (const f of agentFiles[agent] || []) required.push(f);
544
780
  }
545
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)));
546
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
+
547
807
  if (missing.length) {
548
- log(`Validation FAILED: ${missing.length} missing file(s)\n`);
549
- for (const f of missing) console.log(` ${E}✗${R2} ${f}`);
550
- 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`);
551
809
  process.exitCode = 1;
552
810
  } else {
553
- log(`${G}validate ok${R2} — all required files present`);
811
+ log(`${G}validate ok${R2} — all ${present.length} required files present`);
554
812
  }
555
813
  }
556
814
 
@@ -693,6 +951,55 @@ function cmdReflect(target, args) {
693
951
  }
694
952
  } catch {}
695
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
+
696
1003
  // Remaining blockers
697
1004
  if (allQueue.length) {
698
1005
  console.log(`\n ${B}Blockers / queue:${R2}`);
@@ -712,6 +1019,146 @@ function cmdReflect(target, args) {
712
1019
  console.log();
713
1020
  }
714
1021
 
1022
+ function cmdMetrics(target) {
1023
+ const ai = path.join(target, "ai");
1024
+ const G = "\x1b[32m", W = "\x1b[33m", R2 = "\x1b[0m", D = "\x1b[2m",
1025
+ B = "\x1b[34m", T = "\x1b[36m", M = "\x1b[35m";
1026
+
1027
+ // --- Data sources ---
1028
+ let stats = {}, budget = {}, discovery = {};
1029
+ try { stats = JSON.parse(fs.readFileSync(path.join(ai, "feedback", ".usage_stats.json"), "utf8")); } catch {}
1030
+ try { budget = JSON.parse(fs.readFileSync(path.join(ai, "swarm", "budget.json"), "utf8")); } catch {}
1031
+ try { discovery = JSON.parse(fs.readFileSync(path.join(ai, "manifest", "discovery.json"), "utf8")); } catch {}
1032
+
1033
+ const projectName = discovery.project_name || path.basename(target);
1034
+ const stack = discovery.stack || "?";
1035
+ const totalSessions = stats.total_sessions || 0;
1036
+ const agentBreakdown = stats.agents || {};
1037
+ const layerInfo = stats.layer || {};
1038
+ const lastSession = stats.last_session ? new Date(stats.last_session) : null;
1039
+ const layerVersion = stats.version || "?";
1040
+
1041
+ // Swarm tasks: count done files
1042
+ let tasksDone = 0, tasksQueue = 0;
1043
+ const doneDir = path.join(ai, "swarm", "done");
1044
+ const queueDir = path.join(ai, "swarm", "queue");
1045
+ try { tasksDone = fs.readdirSync(doneDir).filter(f => f.endsWith(".json")).length; } catch {}
1046
+ try { tasksQueue = fs.readdirSync(queueDir).filter(f => f.endsWith(".json")).length; } catch {}
1047
+
1048
+ // Activity: count events from activity.jsonl
1049
+ let activityEvents = 0;
1050
+ try {
1051
+ const lines = fs.readFileSync(path.join(ai, "swarm", "activity.jsonl"), "utf8").trim().split("\n").filter(Boolean);
1052
+ activityEvents = lines.length;
1053
+ } catch {}
1054
+
1055
+ // Feedback submissions
1056
+ let feedbackCount = layerInfo.feedback_reports || 0;
1057
+
1058
+ // Budget totals
1059
+ const totalSpent = budget.total_spent || 0;
1060
+ const sessionsWithBudget = Object.keys(budget.sessions || {}).length;
1061
+
1062
+ // --- Effectiveness score (0-100) ---
1063
+ let score = 0, scoreNotes = [];
1064
+
1065
+ // Sessions depth: 1 = tried, 3 = habit forming, 7 = regular use
1066
+ const sessionScore = Math.min(Math.floor((totalSessions / 7) * 35), 35);
1067
+ score += sessionScore;
1068
+ if (totalSessions === 0) scoreNotes.push("not started");
1069
+ else if (totalSessions === 1) scoreNotes.push("first session");
1070
+ else if (totalSessions < 3) scoreNotes.push("early");
1071
+ else if (totalSessions < 7) scoreNotes.push("habit forming");
1072
+ else scoreNotes.push("regular use");
1073
+
1074
+ // Delegation: did they delegate to swarm?
1075
+ const delegationScore = tasksDone > 0 ? Math.min(Math.floor((tasksDone / 5) * 30), 30) : 0;
1076
+ score += delegationScore;
1077
+ if (tasksDone > 0) scoreNotes.push(`${tasksDone} tasks delegated`);
1078
+
1079
+ // Feedback: submitted = trust signal
1080
+ const feedbackScore = feedbackCount > 0 ? 20 : 0;
1081
+ score += feedbackScore;
1082
+ if (feedbackCount > 0) scoreNotes.push("feedback submitted");
1083
+
1084
+ // Layer completeness: has playbooks and commands?
1085
+ const layerScore = (layerInfo.playbooks && layerInfo.commands) ? 15 : (layerInfo.commands ? 8 : 0);
1086
+ score += layerScore;
1087
+
1088
+ const scoreColor = score >= 70 ? G : score >= 40 ? W : "\x1b[31m";
1089
+ const bar = "█".repeat(Math.round(score / 5)).padEnd(20, "░");
1090
+
1091
+ // --- Output ---
1092
+ console.log(`\n ${T}Metrics${R2} ${D}${projectName} · ${stack} · ai v${layerVersion}${R2}\n`);
1093
+
1094
+ // Effectiveness score
1095
+ console.log(` ${B}Effectiveness${R2}`);
1096
+ console.log(` ${scoreColor}${score}/100${R2} ${D}${bar}${R2}`);
1097
+ if (scoreNotes.length) console.log(` ${D}${scoreNotes.join(" · ")}${R2}`);
1098
+
1099
+ // Adoption funnel
1100
+ console.log(`\n ${B}Adoption funnel${R2}`);
1101
+ const funnelStep = (label, value, done, hint) => {
1102
+ const icon = done ? `${G}✓${R2}` : `${D}○${R2}`;
1103
+ const val = value !== null ? ` ${D}${value}${R2}` : "";
1104
+ const h = !done && hint ? ` ${D}← ${hint}${R2}` : "";
1105
+ console.log(` ${icon} ${label}${val}${h}`);
1106
+ };
1107
+ funnelStep("Initialized", `ai/ v${layerVersion}`, true);
1108
+ funnelStep("Returned (>1 session)", `${totalSessions} total`, totalSessions > 1, "run 0dai reflect after each session");
1109
+ funnelStep("Used swarm delegation", tasksDone > 0 ? `${tasksDone} tasks done` : null, tasksDone > 0, "try: 0dai swarm add --task '...' --to codex");
1110
+ funnelStep("Submitted feedback", feedbackCount > 0 ? `${feedbackCount} reports` : null, feedbackCount > 0, "0dai feedback log + push");
1111
+
1112
+ // Session stats
1113
+ if (totalSessions > 0) {
1114
+ console.log(`\n ${B}Sessions${R2}`);
1115
+ console.log(` Total ${totalSessions}`);
1116
+ if (lastSession) {
1117
+ const daysAgo = Math.floor((Date.now() - lastSession.getTime()) / 86400000);
1118
+ const when = daysAgo === 0 ? "today" : daysAgo === 1 ? "yesterday" : `${daysAgo}d ago`;
1119
+ console.log(` Last ${when}`);
1120
+ }
1121
+ const agentEntries = Object.entries(agentBreakdown).sort((a, b) => b[1] - a[1]);
1122
+ if (agentEntries.length) {
1123
+ console.log(` Agents ${agentEntries.map(([a, n]) => `${a}: ${n}`).join(" ")}`);
1124
+ }
1125
+ if (sessionsWithBudget > 0 && totalSpent > 0) {
1126
+ console.log(` Cost $${totalSpent.toFixed(4)} total ${D}(${sessionsWithBudget} sessions tracked)${R2}`);
1127
+ }
1128
+ }
1129
+
1130
+ // Delegation stats
1131
+ if (tasksDone > 0 || tasksQueue > 0) {
1132
+ console.log(`\n ${B}Delegation${R2}`);
1133
+ if (tasksDone > 0) console.log(` Done ${G}${tasksDone}${R2}`);
1134
+ if (tasksQueue > 0) console.log(` Queue ${W}${tasksQueue}${R2}`);
1135
+ if (activityEvents > 0) console.log(` Events ${activityEvents}`);
1136
+ }
1137
+
1138
+ // Layer health
1139
+ console.log(`\n ${B}ai/ layer${R2}`);
1140
+ const checks = [
1141
+ ["commands.yaml", layerInfo.commands],
1142
+ ["playbooks", layerInfo.playbooks],
1143
+ ["personas", layerInfo.personas],
1144
+ ["session roaming", layerInfo.session_active],
1145
+ ["swarm queue", (layerInfo.swarm_queue || 0) > 0],
1146
+ ];
1147
+ for (const [label, ok] of checks) {
1148
+ const icon = ok ? `${G}✓${R2}` : `${D}—${R2}`;
1149
+ console.log(` ${icon} ${label}`);
1150
+ }
1151
+
1152
+ // Next suggested action
1153
+ console.log(`\n ${B}Next${R2}`);
1154
+ if (totalSessions === 0) console.log(` ${D}Start a Claude Code session — session_start hook will print project context${R2}`);
1155
+ else if (tasksDone === 0) console.log(` ${D}Try delegating a task: 0dai swarm add --task "write tests for auth module" --to codex${R2}`);
1156
+ else if (feedbackCount === 0) console.log(` ${D}Submit feedback: 0dai feedback log --type positive --detail "what worked"${R2}`);
1157
+ else console.log(` ${D}Score ${score}/100 — keep delegating and submitting feedback${R2}`);
1158
+
1159
+ console.log();
1160
+ }
1161
+
715
1162
  function cmdStatus(target) {
716
1163
  const ai = path.join(target, "ai");
717
1164
  let v = "?", stack = "?";
@@ -1225,6 +1672,119 @@ async function cmdFeedback(target, sub, args) {
1225
1672
  console.log("Usage: 0dai feedback [push|log|list] [--type ...] [--detail '...']");
1226
1673
  }
1227
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
+
1228
1788
  async function main() {
1229
1789
  const args = process.argv.slice(2);
1230
1790
  let target = process.cwd();
@@ -1238,6 +1798,8 @@ async function main() {
1238
1798
  checkVersion();
1239
1799
 
1240
1800
  switch (cmd) {
1801
+ case "run": await cmdRun(args[1] || "", target, args.slice(2)); break;
1802
+ case "watch": cmdWatch(target, args.slice(1)); break;
1241
1803
  case "audit": cmdAudit(target); break;
1242
1804
  case "init": await cmdInit(target, args); break;
1243
1805
  case "sync": await cmdSync(target, args); break;
@@ -1245,6 +1807,8 @@ async function main() {
1245
1807
  case "doctor": cmdDoctor(target); break;
1246
1808
  case "validate": cmdValidate(target); break;
1247
1809
  case "reflect": cmdReflect(target, args); break;
1810
+ case "metrics": cmdMetrics(target); break;
1811
+ case "portfolio": cmdPortfolio(); break;
1248
1812
  case "status": cmdStatus(target); break;
1249
1813
  case "auth":
1250
1814
  if (sub === "login") await cmdAuthLogin();
@@ -1284,13 +1848,17 @@ async function main() {
1284
1848
  case "help": case "--help": case "-h":
1285
1849
  console.log(`\n ${T}0dai${R} v${VERSION} — One config for 5 AI agent CLIs\n`);
1286
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]");
1287
1853
  console.log(" audit Scan ai/ and agent configs for leaked secrets");
1288
- console.log(" init Initialize ai/ layer (via API) [--dry-run]");
1289
- console.log(" sync Update ai/ layer (via API) [--dry-run]");
1854
+ console.log(" init Initialize ai/ layer (via API) [--dry-run] [--minimal]");
1855
+ console.log(" sync Update ai/ layer (via API) [--dry-run] [--quiet]");
1290
1856
  console.log(" detect Show detected stack");
1291
1857
  console.log(" doctor Check health + credentials checklist");
1292
1858
  console.log(" validate Validate ai/ layer completeness");
1293
1859
  console.log(" reflect Session reflection: delivered, delegation rate, blockers");
1860
+ console.log(" metrics Effectiveness score: adoption funnel, sessions, delegation");
1861
+ console.log(" portfolio All tracked projects: score, sessions, agents, last activity");
1294
1862
  console.log(" status Show maturity, swarm, session");
1295
1863
  console.log(" session save Save session for roaming");
1296
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.8.0",
3
+ "version": "3.1.1",
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"