@0dai-dev/cli 2.9.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.
- package/bin/0dai.js +435 -20
- 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 = "
|
|
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,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
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
-
|
|
140
|
-
|
|
141
|
-
}
|
|
142
|
-
|
|
353
|
+
} else {
|
|
354
|
+
created++;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
fs.writeFileSync(p, finalContent, "utf8");
|
|
143
358
|
}
|
|
144
|
-
|
|
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
|
-
//
|
|
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(
|
|
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");
|