@0dai-dev/cli 3.10.1 → 4.1.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 +153 -2154
- 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 +270 -0
- package/lib/commands/init.js +327 -0
- package/lib/commands/metrics.js +145 -0
- package/lib/commands/models.js +90 -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 +199 -0
- package/lib/commands/update.js +69 -0
- package/lib/commands/validate.js +71 -0
- package/lib/commands/watch.js +118 -0
- package/lib/commands/workspace.js +296 -0
- package/lib/onboarding.js +171 -0
- package/lib/shared.js +360 -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 +13 -4
- package/scripts/postinstall.js +29 -0
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
const shared = require("../shared");
|
|
3
|
+
const { log, fs, path, requirePlan } = shared;
|
|
4
|
+
|
|
5
|
+
function cmdSession(target, sub, args) {
|
|
6
|
+
const sessFile = path.join(target, "ai", "sessions", "active.json");
|
|
7
|
+
const sessDir = path.dirname(sessFile);
|
|
8
|
+
|
|
9
|
+
if (sub === "save") {
|
|
10
|
+
const gate = requirePlan("pro", "Session Roaming", target);
|
|
11
|
+
if (gate) {
|
|
12
|
+
log(gate.error);
|
|
13
|
+
log(gate.hint);
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
fs.mkdirSync(sessDir, { recursive: true });
|
|
17
|
+
const goal = args.find((_, i) => args[i - 1] === "--goal") || "";
|
|
18
|
+
const summary = args.find((_, i) => args[i - 1] === "--summary") || "";
|
|
19
|
+
const session = {
|
|
20
|
+
id: `sess-${Date.now()}`,
|
|
21
|
+
started: new Date().toISOString(),
|
|
22
|
+
current_agent: "cli",
|
|
23
|
+
task: { goal: goal || summary || "active session", status: "in_progress" },
|
|
24
|
+
handoff_notes: summary,
|
|
25
|
+
context: { files_touched: [] },
|
|
26
|
+
};
|
|
27
|
+
if (fs.existsSync(sessFile)) {
|
|
28
|
+
const existing = JSON.parse(fs.readFileSync(sessFile, "utf8"));
|
|
29
|
+
existing.handoff_notes = summary || existing.handoff_notes;
|
|
30
|
+
if (goal) existing.task.goal = goal;
|
|
31
|
+
existing.updated = new Date().toISOString();
|
|
32
|
+
fs.writeFileSync(sessFile, JSON.stringify(existing, null, 2));
|
|
33
|
+
log("session updated");
|
|
34
|
+
} else {
|
|
35
|
+
fs.writeFileSync(sessFile, JSON.stringify(session, null, 2));
|
|
36
|
+
log(`session started: ${session.id}`);
|
|
37
|
+
}
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
if (sub === "status") {
|
|
41
|
+
if (!fs.existsSync(sessFile)) { log("no active session"); return; }
|
|
42
|
+
const s = JSON.parse(fs.readFileSync(sessFile, "utf8"));
|
|
43
|
+
log(`session: ${(s.task || {}).goal || "?"}`);
|
|
44
|
+
console.log(` agent: ${s.current_agent || "?"}`);
|
|
45
|
+
if (s.handoff_notes) console.log(` handoff: ${s.handoff_notes}`);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
if (sub === "complete") {
|
|
49
|
+
if (!fs.existsSync(sessFile)) { log("no active session"); return; }
|
|
50
|
+
const archiveDir = path.join(target, "ai", "sessions", "archive");
|
|
51
|
+
fs.mkdirSync(archiveDir, { recursive: true });
|
|
52
|
+
const s = JSON.parse(fs.readFileSync(sessFile, "utf8"));
|
|
53
|
+
fs.writeFileSync(path.join(archiveDir, `${s.id || "session"}.json`), JSON.stringify(s, null, 2));
|
|
54
|
+
fs.unlinkSync(sessFile);
|
|
55
|
+
log(`session ${s.id} archived`);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
console.log("Usage: 0dai session [save|status|complete] [--goal '...'] [--summary '...']");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
module.exports = { cmdSession };
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
const shared = require("../shared");
|
|
3
|
+
const { log, T, R, D, fs, path, spawnSync, findRepoScript, getSwarmQuotaLocal, _detectPlanLocal, PLAN_LEVELS } = shared;
|
|
4
|
+
|
|
5
|
+
function cmdStatus(target) {
|
|
6
|
+
const ai = path.join(target, "ai");
|
|
7
|
+
let v = "?", stack = "?";
|
|
8
|
+
try { v = fs.readFileSync(path.join(ai, "VERSION"), "utf8").trim(); } catch {}
|
|
9
|
+
try { stack = JSON.parse(fs.readFileSync(path.join(ai, "manifest", "discovery.json"), "utf8")).stack || "?"; } catch {}
|
|
10
|
+
log(`v${v} | stack: ${stack}`);
|
|
11
|
+
|
|
12
|
+
const count = (dir) => { try { return fs.readdirSync(dir).filter(f => f.endsWith(".json")).length; } catch { return 0; } };
|
|
13
|
+
const q = count(path.join(ai, "swarm", "queue"));
|
|
14
|
+
const a = count(path.join(ai, "swarm", "active"));
|
|
15
|
+
const d = count(path.join(ai, "swarm", "done"));
|
|
16
|
+
if (q || a || d) console.log(` swarm: ${q} queued, ${a} active, ${d} done`);
|
|
17
|
+
|
|
18
|
+
// Swarm quota
|
|
19
|
+
const quota = getSwarmQuotaLocal(target);
|
|
20
|
+
if (quota.plan === "free") {
|
|
21
|
+
console.log(` swarm quota: ${D}locked (Free) — upgrade for ${quota.daily_limit} tasks/day${R}`);
|
|
22
|
+
} else {
|
|
23
|
+
console.log(` swarm quota: ${quota.used_today}/${quota.daily_limit} tasks today (${quota.plan})`);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Session roaming status
|
|
27
|
+
const sessPlan = _detectPlanLocal(target);
|
|
28
|
+
const sessLocked = PLAN_LEVELS[sessPlan] < PLAN_LEVELS["pro"];
|
|
29
|
+
if (sessLocked) {
|
|
30
|
+
console.log(` session roaming: ${D}locked (Free) — upgrade to save/resume sessions${R}`);
|
|
31
|
+
} else {
|
|
32
|
+
console.log(` session roaming: ${T}available (${sessPlan})${R}`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
const s = JSON.parse(fs.readFileSync(path.join(ai, "sessions", "active.json"), "utf8"));
|
|
37
|
+
console.log(` session: ${(s.task || {}).goal || "?"} (agent: ${s.current_agent || "?"})`);
|
|
38
|
+
} catch {}
|
|
39
|
+
|
|
40
|
+
// Anti-pattern warnings count
|
|
41
|
+
try {
|
|
42
|
+
const ds = findRepoScript(target, "anti_pattern_detector.py");
|
|
43
|
+
if (ds) {
|
|
44
|
+
const wr = spawnSync("python3", [ds, "count", "--target", target],
|
|
45
|
+
{ stdio: ["ignore", "pipe", "ignore"], encoding: "utf8", timeout: 5000 });
|
|
46
|
+
if (wr.status === 0 && wr.stdout) {
|
|
47
|
+
const wc = JSON.parse(wr.stdout.trim());
|
|
48
|
+
if (wc.count > 0) console.log(` warnings: ${wc.count} active — run: 0dai experience warnings`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
} catch {}
|
|
52
|
+
|
|
53
|
+
// First-status tip (shows once after init)
|
|
54
|
+
try { require("../onboarding").showFirstStatusTip(target); } catch {}
|
|
55
|
+
|
|
56
|
+
// Drift warning (lightweight)
|
|
57
|
+
try {
|
|
58
|
+
const ds = findRepoScript(target, "drift_detector.py");
|
|
59
|
+
if (ds) {
|
|
60
|
+
const dr = spawnSync("python3", [ds, "report", "--target", target],
|
|
61
|
+
{ stdio: ["ignore", "pipe", "ignore"], encoding: "utf8", timeout: 5000 });
|
|
62
|
+
if (dr.stdout && (dr.stdout.includes("MODIFIED") || dr.stdout.includes("CONTRADICTS"))) {
|
|
63
|
+
console.log(` drift: config changes detected — run: 0dai doctor --drift`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
} catch {}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
module.exports = { cmdStatus };
|
|
@@ -0,0 +1,199 @@
|
|
|
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
|
+
if (sub === "estimate") {
|
|
159
|
+
const gate = requirePlan("pro", "Swarm Estimate", target);
|
|
160
|
+
if (gate) { log(gate.error); log(gate.hint); return; }
|
|
161
|
+
const goal = args.find((_, i) => args[i - 1] === "--goal") || "";
|
|
162
|
+
if (!goal) { console.log("Usage: 0dai swarm estimate --goal '...' [--agent claude|codex] [--model tier] [--json]"); return; }
|
|
163
|
+
const agent = args.find((_, i) => args[i - 1] === "--agent") || "";
|
|
164
|
+
const model = args.find((_, i) => args[i - 1] === "--model") || "";
|
|
165
|
+
const asJson = args.includes("--json");
|
|
166
|
+
// Call API for cost estimate
|
|
167
|
+
const identity = shared.buildProjectIdentity(target, shared.collectMetadata(target));
|
|
168
|
+
shared.apiCall("/v1/swarm/estimate", { goal, agent, model_tier: model, project_id: identity.project_id }).then((result) => {
|
|
169
|
+
if (result.error) { log(`error: ${result.error}`); return; }
|
|
170
|
+
if (asJson) { console.log(JSON.stringify(result, null, 2)); return; }
|
|
171
|
+
log(`Cost estimate for: ${goal}`);
|
|
172
|
+
console.log(` ${D}Estimated: $${(result.estimated_cost_usd || 0).toFixed(4)} · ${result.estimated_tokens || "?"} tokens · ${result.estimated_time_s || "?"}s${R}`);
|
|
173
|
+
if (result.model_recommendation) console.log(` ${T}Recommended: ${result.model_recommendation}${R}`);
|
|
174
|
+
if (result.tier_breakdown) {
|
|
175
|
+
for (const [tier, cost] of Object.entries(result.tier_breakdown)) {
|
|
176
|
+
console.log(` ${tier}: $${cost.toFixed(4)}`);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
if (sub === "quality") {
|
|
183
|
+
const scorerScript = findRepoScript(target, "quality_scorer.py");
|
|
184
|
+
if (!scorerScript) { log("quality scorer unavailable"); return; }
|
|
185
|
+
const fwd = [scorerScript, "--target", target];
|
|
186
|
+
if (args.includes("--json")) fwd.push("--json");
|
|
187
|
+
for (let i = 2; i < args.length; i++) {
|
|
188
|
+
if (args[i] === "--last" && args[i + 1]) { fwd.push("--last", args[i + 1]); i++; }
|
|
189
|
+
else if (args[i] === "--details" && args[i + 1]) { fwd.push("--details", args[i + 1]); i++; }
|
|
190
|
+
}
|
|
191
|
+
const result = spawnSync("python3", fwd, { stdio: "inherit", timeout: 15000 });
|
|
192
|
+
if (typeof result.status === "number" && result.status !== 0) process.exit(result.status);
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
console.log("Usage: 0dai swarm [status|add|delegate|budget|estimate|quality] [--task '...'] [--to agent]");
|
|
196
|
+
console.log(" quality Show quality scores for recent tasks (--last N / --details TASK_ID)");
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
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 };
|