@0dai-dev/cli 3.10.0 → 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.
- package/README.md +4 -2
- package/bin/0dai.js +96 -2153
- package/lib/commands/audit.js +129 -0
- package/lib/commands/auth.js +241 -0
- package/lib/commands/detect.js +31 -0
- package/lib/commands/doctor.js +194 -0
- package/lib/commands/experience.js +65 -0
- package/lib/commands/feedback.js +92 -0
- package/lib/commands/graph.js +171 -0
- package/lib/commands/init.js +282 -0
- package/lib/commands/metrics.js +145 -0
- package/lib/commands/models.js +57 -0
- package/lib/commands/portfolio.js +96 -0
- package/lib/commands/reflect.js +211 -0
- package/lib/commands/report.js +29 -0
- package/lib/commands/run.js +77 -0
- package/lib/commands/session.js +61 -0
- package/lib/commands/status.js +69 -0
- package/lib/commands/swarm.js +161 -0
- package/lib/commands/update.js +69 -0
- package/lib/commands/validate.js +71 -0
- package/lib/commands/watch.js +118 -0
- package/lib/onboarding.js +171 -0
- package/lib/shared.js +283 -0
- package/lib/utils/auth.js +142 -0
- package/lib/utils/constants.js +76 -0
- package/lib/utils/identity.js +147 -0
- package/lib/utils/plan.js +73 -0
- package/lib/wizard.js +311 -0
- package/package.json +5 -1
- package/scripts/postinstall.js +29 -0
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
const shared = require("../shared");
|
|
3
|
+
const { log, T, R, D, fs, path, https, requirePlan } = shared;
|
|
4
|
+
|
|
5
|
+
function cmdSwarm(target, sub, args) {
|
|
6
|
+
const swarmDir = path.join(target, "ai", "swarm");
|
|
7
|
+
const queueDir = path.join(swarmDir, "queue");
|
|
8
|
+
|
|
9
|
+
if (sub === "status") {
|
|
10
|
+
const count = (d) => { try { return fs.readdirSync(d).filter(f => f.endsWith(".json")).length; } catch { return 0; } };
|
|
11
|
+
const q = count(path.join(swarmDir, "queue"));
|
|
12
|
+
const a = count(path.join(swarmDir, "active"));
|
|
13
|
+
const d = count(path.join(swarmDir, "done"));
|
|
14
|
+
log(`swarm: ${q} queued, ${a} active, ${d} done`);
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
if (sub === "add" || sub === "delegate") {
|
|
18
|
+
const gate = requirePlan("pro", "Swarm", target);
|
|
19
|
+
if (gate) { log(gate.error); log(gate.hint); return; }
|
|
20
|
+
fs.mkdirSync(queueDir, { recursive: true });
|
|
21
|
+
const task = args.find((_, i) => args[i - 1] === "--task") || "untitled";
|
|
22
|
+
const forAgent = args.find((_, i) => ["--for", "--to"].includes(args[i - 1])) || "any";
|
|
23
|
+
const id = `swarm-${Date.now()}`;
|
|
24
|
+
const t = { id, title: task, assigned_to: forAgent, status: "pending", created_at: new Date().toISOString(), created_by: "cli" };
|
|
25
|
+
fs.writeFileSync(path.join(queueDir, `${id}.json`), JSON.stringify(t, null, 2));
|
|
26
|
+
log(`task created: ${id} → ${forAgent}`);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
if (sub === "webhook") {
|
|
30
|
+
const webhooksFile = path.join(swarmDir, "webhooks.json");
|
|
31
|
+
const loadHooks = () => { try { return JSON.parse(fs.readFileSync(webhooksFile, "utf8")); } catch { return []; } };
|
|
32
|
+
const saveHooks = (h) => { fs.mkdirSync(swarmDir, { recursive: true }); fs.writeFileSync(webhooksFile, JSON.stringify(h, null, 2)); };
|
|
33
|
+
const action = args[2] || "";
|
|
34
|
+
|
|
35
|
+
if (action === "add") {
|
|
36
|
+
const url = args[3] || args.find((_, i) => args[i-1] === "--url");
|
|
37
|
+
const event = args.find((_, i) => args[i-1] === "--event") || "all";
|
|
38
|
+
const secret = args.find((_, i) => args[i-1] === "--secret") || "";
|
|
39
|
+
if (!url || !url.startsWith("http")) { log("Usage: 0dai swarm webhook add <url> [--event task_done|task_failed|all] [--secret TOKEN]"); return; }
|
|
40
|
+
// MED: SSRF protection — block internal/metadata endpoints
|
|
41
|
+
try {
|
|
42
|
+
const u = new URL(url);
|
|
43
|
+
const host = u.hostname;
|
|
44
|
+
const BLOCKED = /^(127\.|10\.|192\.168\.|172\.(1[6-9]|2[0-9]|3[01])\.|169\.254\.|::1$|fc00:|fe80:|localhost$|0\.0\.0\.0$)/i;
|
|
45
|
+
if (BLOCKED.test(host) || host === "metadata.google.internal") {
|
|
46
|
+
log(`rejected: ${host} is a private/internal address (SSRF protection)`);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
if (u.protocol !== "https:" && u.protocol !== "http:") {
|
|
50
|
+
log(`rejected: only http/https allowed, got ${u.protocol}`);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
} catch {
|
|
54
|
+
log(`invalid URL: ${url}`);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
const hooks = loadHooks();
|
|
58
|
+
if (hooks.find(h => h.url === url)) { log(`already registered: ${url}`); return; }
|
|
59
|
+
hooks.push({ url, event, secret: secret || undefined, added_at: new Date().toISOString() });
|
|
60
|
+
saveHooks(hooks);
|
|
61
|
+
log(`webhook added: ${url} (event: ${event})`);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
if (action === "list") {
|
|
65
|
+
const hooks = loadHooks();
|
|
66
|
+
if (hooks.length === 0) { log("no webhooks registered. Use: 0dai swarm webhook add <url>"); return; }
|
|
67
|
+
console.log(`\n ${T}Registered webhooks${R}\n`);
|
|
68
|
+
hooks.forEach((h, i) => {
|
|
69
|
+
console.log(` ${i+1}. ${h.url}`);
|
|
70
|
+
console.log(` ${D}event: ${h.event} added: ${h.added_at?.slice(0,10)}${R}`);
|
|
71
|
+
});
|
|
72
|
+
console.log();
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
if (action === "remove") {
|
|
76
|
+
const url = args[3] || "";
|
|
77
|
+
if (!url) { log("Usage: 0dai swarm webhook remove <url>"); return; }
|
|
78
|
+
const hooks = loadHooks().filter(h => h.url !== url);
|
|
79
|
+
saveHooks(hooks);
|
|
80
|
+
log(`removed: ${url}`);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
if (action === "test") {
|
|
84
|
+
const url = args[3] || loadHooks()[0]?.url;
|
|
85
|
+
if (!url) { log("Usage: 0dai swarm webhook test <url>"); return; }
|
|
86
|
+
const payload = JSON.stringify({ event: "test", task_id: "test-ping", title: "Webhook test from 0dai", status: "done", timestamp: new Date().toISOString() });
|
|
87
|
+
const req = https.request(url, { method: "POST", headers: { "Content-Type": "application/json", "User-Agent": "0dai-swarm/1.0", "Content-Length": Buffer.byteLength(payload) } }, (res) => {
|
|
88
|
+
log(`test sent to ${url} → HTTP ${res.statusCode}`);
|
|
89
|
+
});
|
|
90
|
+
req.on("error", (e) => log(`test failed: ${e.message}`));
|
|
91
|
+
req.setTimeout(5000, () => { req.destroy(); log("test timed out"); });
|
|
92
|
+
req.write(payload);
|
|
93
|
+
req.end();
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
console.log("Usage: 0dai swarm webhook [add|list|remove|test] <url> [--event all|task_done|task_failed] [--secret TOKEN]");
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
if (sub === "budget") {
|
|
100
|
+
const budgetFile = path.join(swarmDir, "budget.json");
|
|
101
|
+
if (!fs.existsSync(budgetFile)) { log("no budget data yet"); return; }
|
|
102
|
+
const b = JSON.parse(fs.readFileSync(budgetFile, "utf8"));
|
|
103
|
+
const B2 = process.stdout.isTTY ? "\x1b[1m" : "";
|
|
104
|
+
const R2 = process.stdout.isTTY ? "\x1b[0m" : "";
|
|
105
|
+
const D2 = process.stdout.isTTY ? "\x1b[2m" : "";
|
|
106
|
+
const G2 = process.stdout.isTTY ? "\x1b[32m" : "";
|
|
107
|
+
const W2 = process.stdout.isTTY ? "\x1b[33m" : "";
|
|
108
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
109
|
+
const sessionKey = process.env.ODAI_SESSION_ID ||
|
|
110
|
+
new Date().toISOString().slice(0, 13).replace("T", "-");
|
|
111
|
+
const dailySpent = b.daily?.[today] || 0;
|
|
112
|
+
const totalSpent = b.total_spent || 0;
|
|
113
|
+
const sess = b.sessions?.[sessionKey];
|
|
114
|
+
// Tier distribution across all tasks
|
|
115
|
+
const tierCount = { fast: 0, balanced: 0, deep: 0 };
|
|
116
|
+
for (const t of Object.values(b.tasks || {})) {
|
|
117
|
+
if (t.tier && tierCount[t.tier] !== undefined) tierCount[t.tier]++;
|
|
118
|
+
}
|
|
119
|
+
const tieredTotal = tierCount.fast + tierCount.balanced + tierCount.deep;
|
|
120
|
+
console.log(`\n ${B2}Swarm Budget${R2}`);
|
|
121
|
+
if (sess && sess.total_cost > 0) {
|
|
122
|
+
const taskCount = (sess.tasks || []).length;
|
|
123
|
+
const avgCost = taskCount > 0 ? (sess.total_cost / taskCount).toFixed(4) : "0";
|
|
124
|
+
console.log(` ${B2}This session${R2} $${sess.total_cost.toFixed(4)} · ${taskCount} tasks · avg $${avgCost}/task`);
|
|
125
|
+
if (sess.tiers) {
|
|
126
|
+
const tiers = Object.entries(sess.tiers).filter(([, n]) => n > 0).map(([t, n]) => `${n}×${t}`).join(" ");
|
|
127
|
+
if (tiers) console.log(` ${D2}Tiers ${tiers}${R2}`);
|
|
128
|
+
}
|
|
129
|
+
} else {
|
|
130
|
+
console.log(` ${D2}This session no tracked spend${R2}`);
|
|
131
|
+
}
|
|
132
|
+
if (dailySpent > 0) {
|
|
133
|
+
const dailyLimit = parseFloat(process.env.ODAI_DAILY_BUDGET || "5");
|
|
134
|
+
const pct = Math.round((dailySpent / dailyLimit) * 100);
|
|
135
|
+
const bar = "█".repeat(Math.round(pct / 5)).padEnd(20, "░");
|
|
136
|
+
const col = pct < 50 ? G2 : pct < 80 ? W2 : "\x1b[31m";
|
|
137
|
+
console.log(` ${B2}Today${R2} ${col}$${dailySpent.toFixed(4)}${R2} / $${dailyLimit.toFixed(2)} ${D2}${bar} ${pct}%${R2}`);
|
|
138
|
+
}
|
|
139
|
+
console.log(` ${B2}All time${R2} ${D2}$${totalSpent.toFixed(4)} (${Object.keys(b.tasks || {}).length} tasks)${R2}`);
|
|
140
|
+
if (tieredTotal > 0) {
|
|
141
|
+
const fastPct = Math.round((tierCount.fast / tieredTotal) * 100);
|
|
142
|
+
console.log(` ${D2}Model routing ${tierCount.fast}×fast ${tierCount.balanced}×balanced ${tierCount.deep}×deep (${fastPct}% cheap)${R2}`);
|
|
143
|
+
}
|
|
144
|
+
// Recent sessions (last 5)
|
|
145
|
+
const sessions = Object.entries(b.sessions || {})
|
|
146
|
+
.sort(([a], [bb]) => bb.localeCompare(a))
|
|
147
|
+
.slice(0, 5);
|
|
148
|
+
if (sessions.length > 1) {
|
|
149
|
+
console.log(` ${D2}Recent sessions:${R2}`);
|
|
150
|
+
for (const [key, s] of sessions) {
|
|
151
|
+
const tasks = (s.tasks || []).length;
|
|
152
|
+
console.log(` ${D2}${key} $${(s.total_cost || 0).toFixed(4)} · ${tasks} tasks${R2}`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
console.log();
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
console.log("Usage: 0dai swarm [status|add|delegate|budget] [--task '...'] [--to agent]");
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
module.exports = { cmdSwarm };
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
const shared = require("../shared");
|
|
3
|
+
const { log, SUPPORTED_CLIS } = shared;
|
|
4
|
+
|
|
5
|
+
function cmdUpdate(args) {
|
|
6
|
+
const { execFileSync: _ef3, execSync } = require("child_process");
|
|
7
|
+
const dryRun = args.includes("--dry-run");
|
|
8
|
+
|
|
9
|
+
// 0dai self-update entry, then all supported agent CLIs.
|
|
10
|
+
const CLIS = [
|
|
11
|
+
{ name: "0dai", pkg: "@0dai-dev/cli", bin: "0dai", pkgType: "npm" },
|
|
12
|
+
...SUPPORTED_CLIS,
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
let updated = 0;
|
|
16
|
+
for (const cli of CLIS) {
|
|
17
|
+
let installed = false, ver = null;
|
|
18
|
+
try {
|
|
19
|
+
const out = _ef3(cli.bin, ["--version"], { timeout: 8000 }).toString().trim();
|
|
20
|
+
installed = true;
|
|
21
|
+
const m = out.match(/(\d+\.\d+\.\d+)/);
|
|
22
|
+
if (m) ver = m[1];
|
|
23
|
+
} catch {}
|
|
24
|
+
|
|
25
|
+
if (!installed) continue;
|
|
26
|
+
|
|
27
|
+
let latest = null;
|
|
28
|
+
if (cli.pkgType === "npm" && cli.pkg) {
|
|
29
|
+
try { latest = _ef3("npm", ["view", cli.pkg, "version"], { timeout: 5000 }).toString().trim(); } catch {}
|
|
30
|
+
} else if (cli.pkgType === "pip" && cli.pkg) {
|
|
31
|
+
try {
|
|
32
|
+
const out = _ef3("pip", ["index", "versions", cli.pkg], { timeout: 8000, encoding: "utf8" });
|
|
33
|
+
const m = out.match(/LATEST:\s*(\d+\.\d+\.\d+)/i) || out.match(/(\d+\.\d+\.\d+)/);
|
|
34
|
+
if (m) latest = m[1];
|
|
35
|
+
} catch {}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (!latest || latest === ver) {
|
|
39
|
+
console.log(` ${cli.name} ${ver || ""} — up to date`);
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
console.log(` ${cli.name} ${ver} → ${latest}`);
|
|
44
|
+
if (dryRun) { updated++; continue; }
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
if (cli.pkgType === "npm") {
|
|
48
|
+
log(`updating ${cli.name}...`);
|
|
49
|
+
execSync(`npm install -g ${cli.pkg}@latest`, { timeout: 60000, stdio: "pipe" });
|
|
50
|
+
log(`${cli.name} updated to ${latest}`);
|
|
51
|
+
updated++;
|
|
52
|
+
} else if (cli.pkgType === "pip") {
|
|
53
|
+
log(`updating ${cli.name}...`);
|
|
54
|
+
execSync(`pip install --upgrade ${cli.pkg}`, { timeout: 60000, stdio: "pipe" });
|
|
55
|
+
log(`${cli.name} updated to ${latest}`);
|
|
56
|
+
updated++;
|
|
57
|
+
}
|
|
58
|
+
} catch (e) {
|
|
59
|
+
log(`failed to update ${cli.name}: ${e.message.split("\n")[0]}`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
if (updated) {
|
|
63
|
+
log(`${dryRun ? "would update" : "updated"} ${updated} CLI(s)`);
|
|
64
|
+
} else {
|
|
65
|
+
log("all CLIs are up to date");
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
module.exports = { cmdUpdate };
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
const shared = require("../shared");
|
|
3
|
+
const { log, fs, path, SUPPORTED_CLIS } = shared;
|
|
4
|
+
|
|
5
|
+
function cmdValidate(target) {
|
|
6
|
+
const ai = path.join(target, "ai");
|
|
7
|
+
if (!fs.existsSync(ai)) {
|
|
8
|
+
log("No 0dai config found. Run: 0dai init");
|
|
9
|
+
process.exitCode = 1;
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
const E = process.stdout.isTTY ? "\x1b[31m" : "";
|
|
13
|
+
const G = process.stdout.isTTY ? "\x1b[32m" : "";
|
|
14
|
+
const D2 = process.stdout.isTTY ? "\x1b[2m" : "";
|
|
15
|
+
const R2 = process.stdout.isTTY ? "\x1b[0m" : "";
|
|
16
|
+
|
|
17
|
+
const required = [
|
|
18
|
+
"ai/VERSION", "ai/VERSION_SCHEMA",
|
|
19
|
+
"ai/manifest/project.yaml", "ai/manifest/discovery.json",
|
|
20
|
+
"ai/manifest/applied-lock.json", "ai/manifest/environment.yaml",
|
|
21
|
+
"ai/manifest/commands.yaml",
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
let agents = [];
|
|
25
|
+
try {
|
|
26
|
+
agents = JSON.parse(fs.readFileSync(path.join(ai, "manifest", "discovery.json"), "utf8")).selected_agents || [];
|
|
27
|
+
} catch {}
|
|
28
|
+
|
|
29
|
+
const agentFiles = Object.fromEntries(
|
|
30
|
+
SUPPORTED_CLIS
|
|
31
|
+
.filter((c) => c.agentFiles && c.agentFiles.length > 0)
|
|
32
|
+
.map((c) => [c.name, c.agentFiles])
|
|
33
|
+
);
|
|
34
|
+
for (const agent of agents) {
|
|
35
|
+
for (const f of agentFiles[agent] || []) required.push(f);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const FIX_HINTS = {
|
|
39
|
+
"ai/VERSION": "run: 0dai init",
|
|
40
|
+
"ai/VERSION_SCHEMA": "run: 0dai sync",
|
|
41
|
+
"ai/manifest/project.yaml": "run: 0dai init",
|
|
42
|
+
"ai/manifest/discovery.json": "run: 0dai init",
|
|
43
|
+
"ai/manifest/applied-lock.json": "run: 0dai sync",
|
|
44
|
+
"ai/manifest/environment.yaml": "run: 0dai sync",
|
|
45
|
+
"ai/manifest/commands.yaml": "run: 0dai sync",
|
|
46
|
+
"AGENTS.md": "run: 0dai sync",
|
|
47
|
+
".claude/settings.json": "run: 0dai sync",
|
|
48
|
+
".claude/CLAUDE.md": "run: 0dai sync",
|
|
49
|
+
".mcp.json": "run: 0dai sync",
|
|
50
|
+
".codex/config.toml": "install codex, then: 0dai sync",
|
|
51
|
+
"opencode.json": "install opencode, then: 0dai sync",
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const present = required.filter(f => fs.existsSync(path.join(target, f)));
|
|
55
|
+
const missing = required.filter(f => !fs.existsSync(path.join(target, f)));
|
|
56
|
+
|
|
57
|
+
for (const f of present) console.log(` ${G}✓${R2} ${f}`);
|
|
58
|
+
for (const f of missing) {
|
|
59
|
+
const hint = FIX_HINTS[f] || "run: 0dai sync";
|
|
60
|
+
console.log(` ${E}✗${R2} ${f} ${D2}— ${hint}${R2}`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (missing.length) {
|
|
64
|
+
console.log(`\n${E}${missing.length} missing${R2} / ${present.length + missing.length} total`);
|
|
65
|
+
process.exitCode = 1;
|
|
66
|
+
} else {
|
|
67
|
+
log(`${G}validate ok${R2} — all ${present.length} required files present`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
module.exports = { cmdValidate };
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
const shared = require("../shared");
|
|
3
|
+
const { T, fs, path } = shared;
|
|
4
|
+
|
|
5
|
+
function cmdWatch(target, args) {
|
|
6
|
+
const isTTY = process.stdout.isTTY;
|
|
7
|
+
const B2 = isTTY ? "\x1b[1m" : "";
|
|
8
|
+
const DIM = isTTY ? "\x1b[2m" : "";
|
|
9
|
+
const G = isTTY ? "\x1b[32m" : "";
|
|
10
|
+
const Y = isTTY ? "\x1b[33m" : "";
|
|
11
|
+
const C = isTTY ? "\x1b[36m" : "";
|
|
12
|
+
const M = isTTY ? "\x1b[35m" : "";
|
|
13
|
+
const R2 = isTTY ? "\x1b[0m" : "";
|
|
14
|
+
|
|
15
|
+
const swarmDir = path.join(target, "ai", "swarm");
|
|
16
|
+
const interval = parseInt(args.find((_, i) => args[i - 1] === "--interval") || "3", 10) * 1000;
|
|
17
|
+
|
|
18
|
+
function readDir(dir) {
|
|
19
|
+
try {
|
|
20
|
+
return fs.readdirSync(dir)
|
|
21
|
+
.filter(f => f.endsWith(".json"))
|
|
22
|
+
.map(f => { try { return JSON.parse(fs.readFileSync(path.join(dir, f), "utf8")); } catch { return null; } })
|
|
23
|
+
.filter(Boolean);
|
|
24
|
+
} catch { return []; }
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function ago(ts) {
|
|
28
|
+
if (!ts) return "—";
|
|
29
|
+
const s = Math.floor((Date.now() - new Date(ts).getTime()) / 1000);
|
|
30
|
+
if (s < 60) return `${s}s`;
|
|
31
|
+
if (s < 3600) return `${Math.floor(s / 60)}m`;
|
|
32
|
+
return `${Math.floor(s / 3600)}h`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function tierColor(tier) {
|
|
36
|
+
if (tier === "deep") return M;
|
|
37
|
+
if (tier === "fast") return C;
|
|
38
|
+
return G;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function statusColor(status) {
|
|
42
|
+
if (status === "done") return G;
|
|
43
|
+
if (status === "active" || status === "running") return Y;
|
|
44
|
+
if (status === "failed") return "\x1b[31m";
|
|
45
|
+
return DIM;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function render() {
|
|
49
|
+
const queue = readDir(path.join(swarmDir, "queue"));
|
|
50
|
+
const active = readDir(path.join(swarmDir, "active"));
|
|
51
|
+
const done = readDir(path.join(swarmDir, "done")).sort(
|
|
52
|
+
(a, b) => new Date(b.completed_at || b.created_at) - new Date(a.completed_at || a.created_at)
|
|
53
|
+
).slice(0, 8);
|
|
54
|
+
|
|
55
|
+
const lines = [];
|
|
56
|
+
const w = process.stdout.columns || 100;
|
|
57
|
+
const sep = DIM + "─".repeat(Math.min(w, 96)) + R2;
|
|
58
|
+
|
|
59
|
+
lines.push(`\n ${B2}${T}0dai watch${R2} ${DIM}${new Date().toLocaleTimeString()} (q to quit)${R2}`);
|
|
60
|
+
lines.push(sep);
|
|
61
|
+
|
|
62
|
+
const header = ` ${"STATUS".padEnd(9)} ${"AGENT".padEnd(10)} ${"TIER".padEnd(9)} ${"AGE".padEnd(6)} TITLE`;
|
|
63
|
+
lines.push(DIM + header + R2);
|
|
64
|
+
lines.push(sep);
|
|
65
|
+
|
|
66
|
+
const printTask = (t, statusOverride) => {
|
|
67
|
+
const status = statusOverride || t.status || "queued";
|
|
68
|
+
const sc = statusColor(status);
|
|
69
|
+
const tc = tierColor(t.model_tier);
|
|
70
|
+
const title = (t.title || "—").slice(0, Math.max(20, w - 42));
|
|
71
|
+
const age = ago(t.created_at);
|
|
72
|
+
lines.push(
|
|
73
|
+
` ${sc}${status.padEnd(9)}${R2} ` +
|
|
74
|
+
`${DIM}${(t.assigned_to || "—").padEnd(10)}${R2} ` +
|
|
75
|
+
`${tc}${(t.model_tier || "—").padEnd(9)}${R2} ` +
|
|
76
|
+
`${DIM}${age.padEnd(6)}${R2} ${title}`
|
|
77
|
+
);
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
if (active.length) {
|
|
81
|
+
active.forEach(t => printTask(t, "active"));
|
|
82
|
+
}
|
|
83
|
+
if (queue.length) {
|
|
84
|
+
queue.forEach(t => printTask(t, "queued"));
|
|
85
|
+
}
|
|
86
|
+
if (!active.length && !queue.length) {
|
|
87
|
+
lines.push(` ${DIM}No tasks in queue or active.${R2}`);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
lines.push(sep);
|
|
91
|
+
if (done.length) {
|
|
92
|
+
lines.push(` ${DIM}Recently done:${R2}`);
|
|
93
|
+
done.forEach(t => printTask(t, "done"));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
lines.push(`\n ${DIM}queue: ${queue.length} active: ${active.length} done (total): ${readDir(path.join(swarmDir, "done")).length}${R2}\n`);
|
|
97
|
+
|
|
98
|
+
if (isTTY) process.stdout.write("\x1b[2J\x1b[H");
|
|
99
|
+
console.log(lines.join("\n"));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
render();
|
|
103
|
+
const timer = setInterval(render, interval);
|
|
104
|
+
|
|
105
|
+
// Exit on 'q' or Ctrl+C
|
|
106
|
+
if (isTTY && process.stdin.setRawMode) {
|
|
107
|
+
process.stdin.setRawMode(true);
|
|
108
|
+
process.stdin.resume();
|
|
109
|
+
process.stdin.once("data", (key) => {
|
|
110
|
+
clearInterval(timer);
|
|
111
|
+
process.stdin.setRawMode(false);
|
|
112
|
+
process.exit(0);
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
process.on("SIGINT", () => { clearInterval(timer); process.exit(0); });
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
module.exports = { cmdWatch };
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Onboarding helpers — post-install welcome, post-init checklist,
|
|
3
|
+
* first-status tip, time-to-init tracking.
|
|
4
|
+
*/
|
|
5
|
+
"use strict";
|
|
6
|
+
|
|
7
|
+
const fs = require("fs");
|
|
8
|
+
const path = require("path");
|
|
9
|
+
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// State file
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
|
|
14
|
+
function _statePath(target) {
|
|
15
|
+
return path.join(target, "ai", ".0dai_state.json");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function _loadState(target) {
|
|
19
|
+
try {
|
|
20
|
+
return JSON.parse(fs.readFileSync(_statePath(target), "utf8"));
|
|
21
|
+
} catch {
|
|
22
|
+
return {};
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function _saveState(target, state) {
|
|
27
|
+
try {
|
|
28
|
+
const dir = path.join(target, "ai");
|
|
29
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
30
|
+
fs.writeFileSync(_statePath(target), JSON.stringify(state, null, 2) + "\n");
|
|
31
|
+
} catch {}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
// Post-init What's Next
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
|
|
38
|
+
function showWhatsNext(mode, isAuthed) {
|
|
39
|
+
console.log("");
|
|
40
|
+
console.log(" \u2705 0dai initialized! Your AI agents are configured.");
|
|
41
|
+
console.log("");
|
|
42
|
+
console.log(" What's next:");
|
|
43
|
+
console.log(" \u2610 0dai status \u2014 check your config");
|
|
44
|
+
console.log(" \u2610 0dai doctor \u2014 verify everything works");
|
|
45
|
+
if (mode === "local" && !isAuthed) {
|
|
46
|
+
console.log(" \u2610 0dai auth login \u2014 sign in for full features (optional)");
|
|
47
|
+
} else if (mode === "cloud") {
|
|
48
|
+
console.log(" \u2610 0dai sync \u2014 refresh configs from server");
|
|
49
|
+
}
|
|
50
|
+
console.log("");
|
|
51
|
+
console.log(" Pro features: 0dai upgrade");
|
|
52
|
+
console.log("");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
// First-status tip
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
|
|
59
|
+
function showFirstStatusTip(target) {
|
|
60
|
+
const state = _loadState(target);
|
|
61
|
+
if (state.first_status_shown) return;
|
|
62
|
+
console.log("");
|
|
63
|
+
console.log(" \ud83d\udca1 Tip: Run 0dai doctor to check config health");
|
|
64
|
+
state.first_status_shown = true;
|
|
65
|
+
_saveState(target, state);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
69
|
+
// Time-to-init tracking
|
|
70
|
+
// ---------------------------------------------------------------------------
|
|
71
|
+
|
|
72
|
+
function trackFirstRun(target) {
|
|
73
|
+
const state = _loadState(target);
|
|
74
|
+
if (!state.first_run_at) {
|
|
75
|
+
state.first_run_at = new Date().toISOString();
|
|
76
|
+
_saveState(target, state);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function trackFirstInit(target) {
|
|
81
|
+
const state = _loadState(target);
|
|
82
|
+
if (!state.first_init_at) {
|
|
83
|
+
state.first_init_at = new Date().toISOString();
|
|
84
|
+
_saveState(target, state);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function getTimeToInit(target) {
|
|
89
|
+
const state = _loadState(target);
|
|
90
|
+
if (!state.first_run_at || !state.first_init_at) return null;
|
|
91
|
+
const start = new Date(state.first_run_at).getTime();
|
|
92
|
+
const end = new Date(state.first_init_at).getTime();
|
|
93
|
+
if (isNaN(start) || isNaN(end)) return null;
|
|
94
|
+
return Math.round((end - start) / 1000);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ---------------------------------------------------------------------------
|
|
98
|
+
// Quickstart
|
|
99
|
+
// ---------------------------------------------------------------------------
|
|
100
|
+
|
|
101
|
+
async function cmdQuickstart(target, { cmdDoctor, cmdStatus, cmdInit, log, ensureAuthenticated }) {
|
|
102
|
+
const start = Date.now();
|
|
103
|
+
const steps = 5;
|
|
104
|
+
|
|
105
|
+
// Step 1: Auth
|
|
106
|
+
console.log(`\n [1/${steps}] Checking authentication...`);
|
|
107
|
+
let authInfo = "not signed in";
|
|
108
|
+
try {
|
|
109
|
+
const auth = JSON.parse(fs.readFileSync(path.join(require("os").homedir(), ".0dai", "auth.json"), "utf8"));
|
|
110
|
+
if (auth.access_token) {
|
|
111
|
+
authInfo = `signed in (${auth.plan || "free"} plan)`;
|
|
112
|
+
}
|
|
113
|
+
} catch {}
|
|
114
|
+
console.log(` \u2713 ${authInfo}`);
|
|
115
|
+
|
|
116
|
+
// Step 2: Init
|
|
117
|
+
console.log(` [2/${steps}] Checking project config...`);
|
|
118
|
+
const aiExists = fs.existsSync(path.join(target, "ai", "VERSION"));
|
|
119
|
+
if (aiExists) {
|
|
120
|
+
console.log(" \u2713 ai/ layer found");
|
|
121
|
+
} else {
|
|
122
|
+
console.log(" initializing...");
|
|
123
|
+
try {
|
|
124
|
+
await cmdInit(target, ["--quickstart"]);
|
|
125
|
+
trackFirstInit(target);
|
|
126
|
+
console.log(" \u2713 project initialized");
|
|
127
|
+
} catch (err) {
|
|
128
|
+
console.log(` \u2717 init failed: ${err.message || err}`);
|
|
129
|
+
console.log(" Run: 0dai init");
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Step 3: Doctor
|
|
134
|
+
console.log(` [3/${steps}] Running health check...`);
|
|
135
|
+
if (fs.existsSync(path.join(target, "ai"))) {
|
|
136
|
+
try {
|
|
137
|
+
cmdDoctor(target);
|
|
138
|
+
} catch {}
|
|
139
|
+
console.log(" \u2713 health check complete");
|
|
140
|
+
} else {
|
|
141
|
+
console.log(" \u2717 skipped (no ai/ layer)");
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Step 4: Status
|
|
145
|
+
console.log(` [4/${steps}] Project status:`);
|
|
146
|
+
if (fs.existsSync(path.join(target, "ai"))) {
|
|
147
|
+
try {
|
|
148
|
+
cmdStatus(target);
|
|
149
|
+
} catch {}
|
|
150
|
+
} else {
|
|
151
|
+
console.log(" (no project configured)");
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Step 5: Summary
|
|
155
|
+
const elapsed = ((Date.now() - start) / 1000).toFixed(1);
|
|
156
|
+
console.log(` [5/${steps}] Ready! (${elapsed}s)`);
|
|
157
|
+
console.log("");
|
|
158
|
+
console.log(" Your project is set up. Try:");
|
|
159
|
+
console.log(" 0dai swarm run --goal \"add auth\" (Pro)");
|
|
160
|
+
console.log(" 0dai graph push (Pro)");
|
|
161
|
+
console.log("");
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
module.exports = {
|
|
165
|
+
showWhatsNext,
|
|
166
|
+
showFirstStatusTip,
|
|
167
|
+
trackFirstRun,
|
|
168
|
+
trackFirstInit,
|
|
169
|
+
getTimeToInit,
|
|
170
|
+
cmdQuickstart,
|
|
171
|
+
};
|