@0dai-dev/cli 2.5.0 → 2.8.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/bin/0dai.js +315 -18
- 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.
|
|
10
|
+
const VERSION = "2.8.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
|
|
@@ -145,7 +145,9 @@ function writeFiles(target, files) {
|
|
|
145
145
|
return created + updated;
|
|
146
146
|
}
|
|
147
147
|
|
|
148
|
-
async function cmdInit(target) {
|
|
148
|
+
async function cmdInit(target, args = []) {
|
|
149
|
+
const dryRun = args.includes("--dry-run");
|
|
150
|
+
|
|
149
151
|
if (fs.existsSync(path.join(target, "ai", "VERSION"))) {
|
|
150
152
|
const v = fs.readFileSync(path.join(target, "ai", "VERSION"), "utf8").trim();
|
|
151
153
|
log(`ai/ layer already exists (v${v}). Run '0dai sync' to update.`);
|
|
@@ -159,12 +161,14 @@ async function cmdInit(target) {
|
|
|
159
161
|
}
|
|
160
162
|
|
|
161
163
|
const { projectFiles, fileContents, clis } = collectMetadata(target);
|
|
162
|
-
if (
|
|
163
|
-
|
|
164
|
+
if (dryRun) log(`${D}dry-run: would generate ai/ layer (${projectFiles.length} files, ${clis.length} CLIs)${R}`);
|
|
165
|
+
if (spinner) spinner.start(`${dryRun ? "[dry-run] " : ""}Generating ai/ layer (${projectFiles.length} files, ${clis.length} CLIs)...`);
|
|
166
|
+
else if (!dryRun) log(`sending to API (${projectFiles.length} files, ${clis.length} CLIs)...`);
|
|
164
167
|
const result = await apiCall("/v1/init", {
|
|
165
168
|
project_files: projectFiles,
|
|
166
169
|
file_contents: fileContents,
|
|
167
170
|
available_clis: clis,
|
|
171
|
+
dry_run: dryRun,
|
|
168
172
|
});
|
|
169
173
|
|
|
170
174
|
if (result.error) {
|
|
@@ -177,8 +181,15 @@ async function cmdInit(target) {
|
|
|
177
181
|
process.exit(1);
|
|
178
182
|
}
|
|
179
183
|
|
|
180
|
-
if (spinner) spinner.stop(
|
|
184
|
+
if (spinner) spinner.stop(`${dryRun ? "[dry-run] " : ""}Detected: ${result.stack || "?"}`);
|
|
181
185
|
else log(`detected: ${result.stack || "?"}`);
|
|
186
|
+
if (dryRun) {
|
|
187
|
+
const files = Object.keys(result.files || {});
|
|
188
|
+
log(`${D}dry-run: would write ${files.length} files:${R}`);
|
|
189
|
+
for (const f of files.slice(0, 20)) console.log(` ${D}+ ${f}${R}`);
|
|
190
|
+
if (files.length > 20) console.log(` ${D}… and ${files.length - 20} more${R}`);
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
182
193
|
writeFiles(target, result.files || {});
|
|
183
194
|
|
|
184
195
|
// Add to .gitignore
|
|
@@ -203,11 +214,13 @@ async function cmdInit(target) {
|
|
|
203
214
|
console.log(` ${D}0dai feedback push${R}`);
|
|
204
215
|
}
|
|
205
216
|
|
|
206
|
-
async function cmdSync(target) {
|
|
207
|
-
|
|
217
|
+
async function cmdSync(target, args = []) {
|
|
218
|
+
const dryRun = args.includes("--dry-run");
|
|
219
|
+
|
|
220
|
+
// Quick local check: skip API if already at current version (unless dry-run)
|
|
208
221
|
let version = "unknown";
|
|
209
222
|
try { version = fs.readFileSync(path.join(target, "ai", "VERSION"), "utf8").trim(); } catch {}
|
|
210
|
-
if (version === VERSION) {
|
|
223
|
+
if (!dryRun && version === VERSION) {
|
|
211
224
|
log("already up to date (v" + version + ")");
|
|
212
225
|
return;
|
|
213
226
|
}
|
|
@@ -239,24 +252,49 @@ async function cmdSync(target) {
|
|
|
239
252
|
walk(aiDir);
|
|
240
253
|
}
|
|
241
254
|
|
|
255
|
+
if (dryRun) log(`${D}dry-run: checking what sync would change...${R}`);
|
|
256
|
+
|
|
242
257
|
const result = await apiCall("/v1/sync", {
|
|
243
258
|
ai_version: version, stack, agents: agents.length ? agents : clis,
|
|
244
259
|
current_files: currentFiles, file_contents: fileContents,
|
|
260
|
+
dry_run: dryRun,
|
|
245
261
|
});
|
|
246
262
|
|
|
247
263
|
if (result.error) { log(`error: ${result.error}`); process.exit(1); }
|
|
248
264
|
|
|
249
265
|
const updated = result.files_updated || {};
|
|
266
|
+
if (dryRun) {
|
|
267
|
+
const files = Object.keys(updated);
|
|
268
|
+
if (files.length) {
|
|
269
|
+
log(`${D}dry-run: would update ${files.length} file(s):${R}`);
|
|
270
|
+
for (const f of files) console.log(` ${D}~ ${f}${R}`);
|
|
271
|
+
} else {
|
|
272
|
+
log(`${D}dry-run: nothing to update${R}`);
|
|
273
|
+
}
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
250
276
|
if (Object.keys(updated).length) writeFiles(target, updated);
|
|
251
277
|
else log("already up to date");
|
|
252
278
|
}
|
|
253
279
|
|
|
254
280
|
async function cmdDetect(target) {
|
|
281
|
+
const OPTIONAL_CLIS = ["gemini", "aider", "opencode"];
|
|
255
282
|
const { projectFiles } = collectMetadata(target);
|
|
256
283
|
const result = await apiCall("/v1/detect", { files: projectFiles });
|
|
257
284
|
if (result.error) { log(`error: ${result.error}`); return; }
|
|
258
285
|
console.log(`stack: ${result.stack || "?"}`);
|
|
259
|
-
|
|
286
|
+
const clis = result.available_clis || [];
|
|
287
|
+
if (clis.length) {
|
|
288
|
+
console.log(`clis: ${clis.join(", ")}`);
|
|
289
|
+
} else {
|
|
290
|
+
console.log(`clis: none detected`);
|
|
291
|
+
console.log(` ${D}install claude, codex, or opencode to use 0dai${R}`);
|
|
292
|
+
}
|
|
293
|
+
// Explain optional CLIs so missing doesn't alarm users
|
|
294
|
+
const missing = OPTIONAL_CLIS.filter(c => !clis.includes(c));
|
|
295
|
+
if (missing.length && clis.length) {
|
|
296
|
+
console.log(` ${D}optional (not installed): ${missing.join(", ")}${R}`);
|
|
297
|
+
}
|
|
260
298
|
}
|
|
261
299
|
|
|
262
300
|
function cmdAudit(target) {
|
|
@@ -427,13 +465,27 @@ function cmdDoctor(target) {
|
|
|
427
465
|
let errors = 0, warnings = 0;
|
|
428
466
|
log(`v${v} | stack: ${stack}\n`);
|
|
429
467
|
|
|
468
|
+
const missingConfigs = [];
|
|
430
469
|
console.log(" ai/ layer:");
|
|
431
470
|
for (const [name, { path: p, sev }] of Object.entries(layerChecks)) {
|
|
432
471
|
const exists = fs.existsSync(p);
|
|
433
|
-
if (!exists)
|
|
472
|
+
if (!exists) {
|
|
473
|
+
sev === "error" ? errors++ : warnings++;
|
|
474
|
+
if (sev === "warn") missingConfigs.push(name);
|
|
475
|
+
}
|
|
434
476
|
const mark = exists ? `${G}ok${R2}` : sev === "error" ? `${E}MISSING${R2}` : `${W}missing${R2}`;
|
|
435
477
|
console.log(` ${mark.padEnd(22)} ${name}`);
|
|
436
478
|
}
|
|
479
|
+
// Explain WHY native configs are missing and what to do
|
|
480
|
+
if (missingConfigs.length > 0) {
|
|
481
|
+
const hasDiscovery = fs.existsSync(path.join(ai, "manifest", "discovery.json"));
|
|
482
|
+
if (hasDiscovery) {
|
|
483
|
+
console.log(`\n ${W}→ Native configs not generated yet.${R2}`);
|
|
484
|
+
console.log(` ${D}Run: 0dai sync --target .${R2}`);
|
|
485
|
+
} else {
|
|
486
|
+
console.log(`\n ${W}→ ai/ layer incomplete — run '0dai init' first.${R2}`);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
437
489
|
|
|
438
490
|
console.log("\n credentials:");
|
|
439
491
|
for (const c of credChecks) {
|
|
@@ -458,6 +510,50 @@ function cmdDoctor(target) {
|
|
|
458
510
|
if (errors) process.exitCode = 1;
|
|
459
511
|
}
|
|
460
512
|
|
|
513
|
+
function cmdValidate(target) {
|
|
514
|
+
const ai = path.join(target, "ai");
|
|
515
|
+
if (!fs.existsSync(ai)) {
|
|
516
|
+
log("no ai/ layer. Run '0dai init' first.");
|
|
517
|
+
process.exitCode = 1;
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
const E = process.stdout.isTTY ? "\x1b[31m" : "";
|
|
521
|
+
const G = process.stdout.isTTY ? "\x1b[32m" : "";
|
|
522
|
+
const D2 = process.stdout.isTTY ? "\x1b[2m" : "";
|
|
523
|
+
const R2 = process.stdout.isTTY ? "\x1b[0m" : "";
|
|
524
|
+
|
|
525
|
+
const required = [
|
|
526
|
+
"ai/VERSION", "ai/VERSION_SCHEMA",
|
|
527
|
+
"ai/manifest/project.yaml", "ai/manifest/discovery.json",
|
|
528
|
+
"ai/manifest/applied-lock.json", "ai/manifest/environment.yaml",
|
|
529
|
+
"ai/manifest/commands.yaml",
|
|
530
|
+
];
|
|
531
|
+
|
|
532
|
+
let agents = [];
|
|
533
|
+
try {
|
|
534
|
+
agents = JSON.parse(fs.readFileSync(path.join(ai, "manifest", "discovery.json"), "utf8")).selected_agents || [];
|
|
535
|
+
} catch {}
|
|
536
|
+
|
|
537
|
+
const agentFiles = {
|
|
538
|
+
codex: ["AGENTS.md", ".codex/config.toml"],
|
|
539
|
+
claude: [".claude/settings.json", ".claude/CLAUDE.md", ".mcp.json"],
|
|
540
|
+
opencode: ["opencode.json"],
|
|
541
|
+
};
|
|
542
|
+
for (const agent of agents) {
|
|
543
|
+
for (const f of agentFiles[agent] || []) required.push(f);
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
const missing = required.filter(f => !fs.existsSync(path.join(target, f)));
|
|
547
|
+
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}`);
|
|
551
|
+
process.exitCode = 1;
|
|
552
|
+
} else {
|
|
553
|
+
log(`${G}validate ok${R2} — all required files present`);
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
|
|
461
557
|
// --- Session reflection --- (dogfood feedback #36)
|
|
462
558
|
function cmdReflect(target, args) {
|
|
463
559
|
const ai = path.join(target, "ai");
|
|
@@ -526,15 +622,77 @@ function cmdReflect(target, args) {
|
|
|
526
622
|
if (totalPending) console.log(` ${B}Remaining${R2} ${W}${totalPending}${R2} tasks still pending`);
|
|
527
623
|
if (successRate !== null) console.log(` ${B}Rate${R2} ${successRate >= 80 ? G : W}${successRate}%${R2} delegation success rate`);
|
|
528
624
|
|
|
529
|
-
// By agent breakdown
|
|
625
|
+
// By agent breakdown with per-agent completion rate
|
|
626
|
+
const allPendingByAgent = {};
|
|
627
|
+
for (const t of [...allQueue, ...allActive]) {
|
|
628
|
+
const a = t.assigned_to || "unknown";
|
|
629
|
+
allPendingByAgent[a] = (allPendingByAgent[a] || 0) + 1;
|
|
630
|
+
}
|
|
631
|
+
|
|
530
632
|
if (Object.keys(byAgent).length) {
|
|
531
633
|
console.log(`\n ${B}By agent:${R2}`);
|
|
532
634
|
for (const [agent, data] of Object.entries(byAgent).sort((a, b) => b[1].done - a[1].done)) {
|
|
635
|
+
const pending = allPendingByAgent[agent] || 0;
|
|
636
|
+
const total = data.done + pending;
|
|
637
|
+
const rate = total > 0 ? Math.round((data.done / total) * 100) : 100;
|
|
533
638
|
const bar = "█".repeat(Math.min(data.done, 20));
|
|
534
|
-
|
|
639
|
+
const rateStr = total > 1 ? ` (${rate >= 80 ? G : W}${rate}%${R2})` : "";
|
|
640
|
+
console.log(` ${(agent + " ").padEnd(14)} ${G}${bar}${R2} ${data.done}/${total}${rateStr}`);
|
|
641
|
+
}
|
|
642
|
+
// Agents with only pending tasks (never completed anything in window)
|
|
643
|
+
for (const [agent, count] of Object.entries(allPendingByAgent)) {
|
|
644
|
+
if (!byAgent[agent]) {
|
|
645
|
+
console.log(` ${(agent + " ").padEnd(14)} ${W}${"░".repeat(Math.min(count, 20))}${R2} 0/${count} ${W}(pending)${R2}`);
|
|
646
|
+
}
|
|
535
647
|
}
|
|
536
648
|
}
|
|
537
649
|
|
|
650
|
+
// Budget summary from budget.json
|
|
651
|
+
const budgetFile = path.join(ai, "swarm", "budget.json");
|
|
652
|
+
try {
|
|
653
|
+
const budget = JSON.parse(fs.readFileSync(budgetFile, "utf8"));
|
|
654
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
655
|
+
const sessionKey = process.env.ODAI_SESSION_ID ||
|
|
656
|
+
new Date().toISOString().slice(0, 13).replace("T", "-"); // YYYY-MM-DD-HH
|
|
657
|
+
const dailySpent = budget.daily?.[today] || 0;
|
|
658
|
+
const totalSpent = budget.total_spent || 0;
|
|
659
|
+
const sess = budget.sessions?.[sessionKey];
|
|
660
|
+
|
|
661
|
+
// Tier distribution across all tasks
|
|
662
|
+
const tierCount = { fast: 0, balanced: 0, deep: 0 };
|
|
663
|
+
for (const t of Object.values(budget.tasks || {})) {
|
|
664
|
+
if (t.tier && tierCount[t.tier] !== undefined) tierCount[t.tier]++;
|
|
665
|
+
}
|
|
666
|
+
const tieredTotal = tierCount.fast + tierCount.balanced + tierCount.deep;
|
|
667
|
+
|
|
668
|
+
if (dailySpent > 0 || totalSpent > 0 || sess) {
|
|
669
|
+
console.log(`\n ${B}Budget:${R2}`);
|
|
670
|
+
if (sess && sess.total_cost > 0) {
|
|
671
|
+
const taskCount = (sess.tasks || []).length;
|
|
672
|
+
const avgCost = taskCount > 0 ? (sess.total_cost / taskCount).toFixed(4) : "0";
|
|
673
|
+
console.log(` ${B}This session${R2} $${sess.total_cost.toFixed(4)} · ${taskCount} tasks · avg $${avgCost}/task`);
|
|
674
|
+
if (sess.tiers) {
|
|
675
|
+
const tiers = Object.entries(sess.tiers).filter(([, n]) => n > 0).map(([t, n]) => `${n}×${t}`).join(" ");
|
|
676
|
+
if (tiers) console.log(` ${D}Tiers ${tiers}${R2}`);
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
if (dailySpent > 0) {
|
|
680
|
+
const dailyLimit = parseFloat(process.env.ODAI_DAILY_BUDGET || "5");
|
|
681
|
+
const pct = Math.round((dailySpent / dailyLimit) * 100);
|
|
682
|
+
const bar = "█".repeat(Math.round(pct / 5)).padEnd(20, "░");
|
|
683
|
+
const col = pct < 50 ? G : pct < 80 ? W : "\x1b[31m";
|
|
684
|
+
console.log(` ${B}Today${R2} ${col}$${dailySpent.toFixed(4)}${R2} / $${dailyLimit.toFixed(2)} ${D}${bar} ${pct}%${R2}`);
|
|
685
|
+
}
|
|
686
|
+
if (totalSpent > 0) {
|
|
687
|
+
console.log(` ${B}All time${R2} ${D}$${totalSpent.toFixed(4)}${R2}`);
|
|
688
|
+
}
|
|
689
|
+
if (tieredTotal > 0) {
|
|
690
|
+
const fastPct = Math.round((tierCount.fast / tieredTotal) * 100);
|
|
691
|
+
console.log(` ${D}Model routing ${tierCount.fast}×fast ${tierCount.balanced}×balanced ${tierCount.deep}×deep (${fastPct}% cheap)${R2}`);
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
} catch {}
|
|
695
|
+
|
|
538
696
|
// Remaining blockers
|
|
539
697
|
if (allQueue.length) {
|
|
540
698
|
console.log(`\n ${B}Blockers / queue:${R2}`);
|
|
@@ -722,6 +880,33 @@ function cmdAuthLogout() {
|
|
|
722
880
|
log("Logged out");
|
|
723
881
|
}
|
|
724
882
|
|
|
883
|
+
async function cmdRedeem(code) {
|
|
884
|
+
if (!code) {
|
|
885
|
+
console.log("Usage: 0dai redeem <CODE>");
|
|
886
|
+
console.log("Example: 0dai redeem ESSE-ABCD-1234");
|
|
887
|
+
process.exit(1);
|
|
888
|
+
}
|
|
889
|
+
try {
|
|
890
|
+
JSON.parse(fs.readFileSync(AUTH_FILE, "utf8"));
|
|
891
|
+
} catch {
|
|
892
|
+
log("Not logged in. Run: 0dai auth login");
|
|
893
|
+
process.exit(1);
|
|
894
|
+
}
|
|
895
|
+
log(`Redeeming code ${T}${code.toUpperCase()}${R}...`);
|
|
896
|
+
const result = await apiCall("/v1/redeem", { code: code.toUpperCase().trim() });
|
|
897
|
+
if (result.ok) {
|
|
898
|
+
log(`${T}✓${R} ${result.message}`);
|
|
899
|
+
if (result.duration_days) {
|
|
900
|
+
log(` Plan active for ${result.duration_days} days`);
|
|
901
|
+
}
|
|
902
|
+
log(` Run ${D}0dai auth status${R} to see updated limits`);
|
|
903
|
+
} else {
|
|
904
|
+
log(`error: ${result.error || "unknown"}`);
|
|
905
|
+
if (result.hint) log(`hint: ${result.hint}`);
|
|
906
|
+
process.exit(1);
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
|
|
725
910
|
async function cmdAuthStatus() {
|
|
726
911
|
try {
|
|
727
912
|
const auth = JSON.parse(fs.readFileSync(AUTH_FILE, "utf8"));
|
|
@@ -892,11 +1077,116 @@ function cmdSwarm(target, sub, args) {
|
|
|
892
1077
|
log(`task created: ${id} → ${forAgent}`);
|
|
893
1078
|
return;
|
|
894
1079
|
}
|
|
1080
|
+
if (sub === "webhook") {
|
|
1081
|
+
const webhooksFile = path.join(swarmDir, "webhooks.json");
|
|
1082
|
+
const loadHooks = () => { try { return JSON.parse(fs.readFileSync(webhooksFile, "utf8")); } catch { return []; } };
|
|
1083
|
+
const saveHooks = (h) => { fs.mkdirSync(swarmDir, { recursive: true }); fs.writeFileSync(webhooksFile, JSON.stringify(h, null, 2)); };
|
|
1084
|
+
const action = args[2] || "";
|
|
1085
|
+
|
|
1086
|
+
if (action === "add") {
|
|
1087
|
+
const url = args[3] || args.find((_, i) => args[i-1] === "--url");
|
|
1088
|
+
const event = args.find((_, i) => args[i-1] === "--event") || "all";
|
|
1089
|
+
const secret = args.find((_, i) => args[i-1] === "--secret") || "";
|
|
1090
|
+
if (!url || !url.startsWith("http")) { log("Usage: 0dai swarm webhook add <url> [--event task_done|task_failed|all] [--secret TOKEN]"); return; }
|
|
1091
|
+
const hooks = loadHooks();
|
|
1092
|
+
if (hooks.find(h => h.url === url)) { log(`already registered: ${url}`); return; }
|
|
1093
|
+
hooks.push({ url, event, secret: secret || undefined, added_at: new Date().toISOString() });
|
|
1094
|
+
saveHooks(hooks);
|
|
1095
|
+
log(`webhook added: ${url} (event: ${event})`);
|
|
1096
|
+
return;
|
|
1097
|
+
}
|
|
1098
|
+
if (action === "list") {
|
|
1099
|
+
const hooks = loadHooks();
|
|
1100
|
+
if (hooks.length === 0) { log("no webhooks registered. Use: 0dai swarm webhook add <url>"); return; }
|
|
1101
|
+
console.log(`\n ${T}Registered webhooks${R}\n`);
|
|
1102
|
+
hooks.forEach((h, i) => {
|
|
1103
|
+
console.log(` ${i+1}. ${h.url}`);
|
|
1104
|
+
console.log(` ${D}event: ${h.event} added: ${h.added_at?.slice(0,10)}${R}`);
|
|
1105
|
+
});
|
|
1106
|
+
console.log();
|
|
1107
|
+
return;
|
|
1108
|
+
}
|
|
1109
|
+
if (action === "remove") {
|
|
1110
|
+
const url = args[3] || "";
|
|
1111
|
+
if (!url) { log("Usage: 0dai swarm webhook remove <url>"); return; }
|
|
1112
|
+
const hooks = loadHooks().filter(h => h.url !== url);
|
|
1113
|
+
saveHooks(hooks);
|
|
1114
|
+
log(`removed: ${url}`);
|
|
1115
|
+
return;
|
|
1116
|
+
}
|
|
1117
|
+
if (action === "test") {
|
|
1118
|
+
const url = args[3] || loadHooks()[0]?.url;
|
|
1119
|
+
if (!url) { log("Usage: 0dai swarm webhook test <url>"); return; }
|
|
1120
|
+
const payload = JSON.stringify({ event: "test", task_id: "test-ping", title: "Webhook test from 0dai", status: "done", timestamp: new Date().toISOString() });
|
|
1121
|
+
const req = https.request(url, { method: "POST", headers: { "Content-Type": "application/json", "User-Agent": "0dai-swarm/1.0", "Content-Length": Buffer.byteLength(payload) } }, (res) => {
|
|
1122
|
+
log(`test sent to ${url} → HTTP ${res.statusCode}`);
|
|
1123
|
+
});
|
|
1124
|
+
req.on("error", (e) => log(`test failed: ${e.message}`));
|
|
1125
|
+
req.setTimeout(5000, () => { req.destroy(); log("test timed out"); });
|
|
1126
|
+
req.write(payload);
|
|
1127
|
+
req.end();
|
|
1128
|
+
return;
|
|
1129
|
+
}
|
|
1130
|
+
console.log("Usage: 0dai swarm webhook [add|list|remove|test] <url> [--event all|task_done|task_failed] [--secret TOKEN]");
|
|
1131
|
+
return;
|
|
1132
|
+
}
|
|
895
1133
|
if (sub === "budget") {
|
|
896
1134
|
const budgetFile = path.join(swarmDir, "budget.json");
|
|
897
1135
|
if (!fs.existsSync(budgetFile)) { log("no budget data yet"); return; }
|
|
898
1136
|
const b = JSON.parse(fs.readFileSync(budgetFile, "utf8"));
|
|
899
|
-
|
|
1137
|
+
const B2 = process.stdout.isTTY ? "\x1b[1m" : "";
|
|
1138
|
+
const R2 = process.stdout.isTTY ? "\x1b[0m" : "";
|
|
1139
|
+
const D2 = process.stdout.isTTY ? "\x1b[2m" : "";
|
|
1140
|
+
const G2 = process.stdout.isTTY ? "\x1b[32m" : "";
|
|
1141
|
+
const W2 = process.stdout.isTTY ? "\x1b[33m" : "";
|
|
1142
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
1143
|
+
const sessionKey = process.env.ODAI_SESSION_ID ||
|
|
1144
|
+
new Date().toISOString().slice(0, 13).replace("T", "-");
|
|
1145
|
+
const dailySpent = b.daily?.[today] || 0;
|
|
1146
|
+
const totalSpent = b.total_spent || 0;
|
|
1147
|
+
const sess = b.sessions?.[sessionKey];
|
|
1148
|
+
// Tier distribution across all tasks
|
|
1149
|
+
const tierCount = { fast: 0, balanced: 0, deep: 0 };
|
|
1150
|
+
for (const t of Object.values(b.tasks || {})) {
|
|
1151
|
+
if (t.tier && tierCount[t.tier] !== undefined) tierCount[t.tier]++;
|
|
1152
|
+
}
|
|
1153
|
+
const tieredTotal = tierCount.fast + tierCount.balanced + tierCount.deep;
|
|
1154
|
+
console.log(`\n ${B2}Swarm Budget${R2}`);
|
|
1155
|
+
if (sess && sess.total_cost > 0) {
|
|
1156
|
+
const taskCount = (sess.tasks || []).length;
|
|
1157
|
+
const avgCost = taskCount > 0 ? (sess.total_cost / taskCount).toFixed(4) : "0";
|
|
1158
|
+
console.log(` ${B2}This session${R2} $${sess.total_cost.toFixed(4)} · ${taskCount} tasks · avg $${avgCost}/task`);
|
|
1159
|
+
if (sess.tiers) {
|
|
1160
|
+
const tiers = Object.entries(sess.tiers).filter(([, n]) => n > 0).map(([t, n]) => `${n}×${t}`).join(" ");
|
|
1161
|
+
if (tiers) console.log(` ${D2}Tiers ${tiers}${R2}`);
|
|
1162
|
+
}
|
|
1163
|
+
} else {
|
|
1164
|
+
console.log(` ${D2}This session no tracked spend${R2}`);
|
|
1165
|
+
}
|
|
1166
|
+
if (dailySpent > 0) {
|
|
1167
|
+
const dailyLimit = parseFloat(process.env.ODAI_DAILY_BUDGET || "5");
|
|
1168
|
+
const pct = Math.round((dailySpent / dailyLimit) * 100);
|
|
1169
|
+
const bar = "█".repeat(Math.round(pct / 5)).padEnd(20, "░");
|
|
1170
|
+
const col = pct < 50 ? G2 : pct < 80 ? W2 : "\x1b[31m";
|
|
1171
|
+
console.log(` ${B2}Today${R2} ${col}$${dailySpent.toFixed(4)}${R2} / $${dailyLimit.toFixed(2)} ${D2}${bar} ${pct}%${R2}`);
|
|
1172
|
+
}
|
|
1173
|
+
console.log(` ${B2}All time${R2} ${D2}$${totalSpent.toFixed(4)} (${Object.keys(b.tasks || {}).length} tasks)${R2}`);
|
|
1174
|
+
if (tieredTotal > 0) {
|
|
1175
|
+
const fastPct = Math.round((tierCount.fast / tieredTotal) * 100);
|
|
1176
|
+
console.log(` ${D2}Model routing ${tierCount.fast}×fast ${tierCount.balanced}×balanced ${tierCount.deep}×deep (${fastPct}% cheap)${R2}`);
|
|
1177
|
+
}
|
|
1178
|
+
// Recent sessions (last 5)
|
|
1179
|
+
const sessions = Object.entries(b.sessions || {})
|
|
1180
|
+
.sort(([a], [bb]) => bb.localeCompare(a))
|
|
1181
|
+
.slice(0, 5);
|
|
1182
|
+
if (sessions.length > 1) {
|
|
1183
|
+
console.log(` ${D2}Recent sessions:${R2}`);
|
|
1184
|
+
for (const [key, s] of sessions) {
|
|
1185
|
+
const tasks = (s.tasks || []).length;
|
|
1186
|
+
console.log(` ${D2}${key} $${(s.total_cost || 0).toFixed(4)} · ${tasks} tasks${R2}`);
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
console.log();
|
|
900
1190
|
return;
|
|
901
1191
|
}
|
|
902
1192
|
console.log("Usage: 0dai swarm [status|add|delegate|budget] [--task '...'] [--to agent]");
|
|
@@ -949,10 +1239,11 @@ async function main() {
|
|
|
949
1239
|
|
|
950
1240
|
switch (cmd) {
|
|
951
1241
|
case "audit": cmdAudit(target); break;
|
|
952
|
-
case "init": await cmdInit(target); break;
|
|
953
|
-
case "sync": await cmdSync(target); break;
|
|
1242
|
+
case "init": await cmdInit(target, args); break;
|
|
1243
|
+
case "sync": await cmdSync(target, args); break;
|
|
954
1244
|
case "detect": await cmdDetect(target); break;
|
|
955
1245
|
case "doctor": cmdDoctor(target); break;
|
|
1246
|
+
case "validate": cmdValidate(target); break;
|
|
956
1247
|
case "reflect": cmdReflect(target, args); break;
|
|
957
1248
|
case "status": cmdStatus(target); break;
|
|
958
1249
|
case "auth":
|
|
@@ -967,6 +1258,7 @@ async function main() {
|
|
|
967
1258
|
case "swarm": cmdSwarm(target, sub, args); break;
|
|
968
1259
|
case "feedback": await cmdFeedback(target, sub, args); break;
|
|
969
1260
|
case "models": cmdModels(sub || args[1]); break;
|
|
1261
|
+
case "redeem": await cmdRedeem(sub || args[1]); break;
|
|
970
1262
|
case "terminal": case "term":
|
|
971
1263
|
try {
|
|
972
1264
|
const SessionManager = require("../lib/session-manager");
|
|
@@ -993,20 +1285,25 @@ async function main() {
|
|
|
993
1285
|
console.log(`\n ${T}0dai${R} v${VERSION} — One config for 5 AI agent CLIs\n`);
|
|
994
1286
|
console.log("Commands:");
|
|
995
1287
|
console.log(" audit Scan ai/ and agent configs for leaked secrets");
|
|
996
|
-
console.log(" init Initialize ai/ layer (via API)");
|
|
997
|
-
console.log(" sync Update ai/ layer (via API)");
|
|
1288
|
+
console.log(" init Initialize ai/ layer (via API) [--dry-run]");
|
|
1289
|
+
console.log(" sync Update ai/ layer (via API) [--dry-run]");
|
|
998
1290
|
console.log(" detect Show detected stack");
|
|
999
1291
|
console.log(" doctor Check health + credentials checklist");
|
|
1292
|
+
console.log(" validate Validate ai/ layer completeness");
|
|
1000
1293
|
console.log(" reflect Session reflection: delivered, delegation rate, blockers");
|
|
1001
1294
|
console.log(" status Show maturity, swarm, session");
|
|
1002
1295
|
console.log(" session save Save session for roaming");
|
|
1003
|
-
console.log(" swarm status
|
|
1296
|
+
console.log(" swarm status Task queue & delegation");
|
|
1297
|
+
console.log(" swarm webhook add Register webhook (fires on task done/failed)");
|
|
1298
|
+
console.log(" swarm webhook list Show registered webhooks");
|
|
1299
|
+
console.log(" swarm webhook test Send test ping to a webhook URL");
|
|
1004
1300
|
console.log(" feedback push Send feedback to 0dai");
|
|
1005
1301
|
console.log(" models Show model ratings (--fast/--balanced/--deep/--available)");
|
|
1006
1302
|
console.log(" terminal Launch interactive agent session");
|
|
1007
1303
|
console.log(" auth login Authenticate (device code flow)");
|
|
1008
1304
|
console.log(" auth logout Remove credentials");
|
|
1009
1305
|
console.log(" auth status Show account and usage");
|
|
1306
|
+
console.log(" redeem <CODE> Redeem a plan upgrade code");
|
|
1010
1307
|
console.log(" --version\n");
|
|
1011
1308
|
console.log("https://0dai.dev");
|
|
1012
1309
|
break;
|