@0dai-dev/cli 2.0.1 → 2.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 +176 -7
- 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.1.1";
|
|
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
|
|
@@ -58,7 +58,7 @@ function apiCall(endpoint, data) {
|
|
|
58
58
|
const url = new URL(endpoint, API_URL);
|
|
59
59
|
const mod = url.protocol === "https:" ? https : http;
|
|
60
60
|
const body = data ? JSON.stringify(data) : null;
|
|
61
|
-
const headers = { "Content-Type": "application/json", "X-Device-ID": deviceFingerprint() };
|
|
61
|
+
const headers = { "Content-Type": "application/json", "X-Device-ID": deviceFingerprint(), "X-CLI-Version": VERSION };
|
|
62
62
|
|
|
63
63
|
try {
|
|
64
64
|
const auth = JSON.parse(fs.readFileSync(AUTH_FILE, "utf8"));
|
|
@@ -115,10 +115,10 @@ function collectMetadata(target) {
|
|
|
115
115
|
}
|
|
116
116
|
|
|
117
117
|
const clis = [];
|
|
118
|
+
const { execSync } = require("child_process");
|
|
118
119
|
for (const cli of ["claude", "codex", "opencode", "gemini", "aider"]) {
|
|
119
120
|
try {
|
|
120
|
-
|
|
121
|
-
execSync(`which ${cli}`, { stdio: "ignore" });
|
|
121
|
+
execSync(`command -v ${cli}`, { stdio: "ignore", shell: "/bin/sh", env: process.env });
|
|
122
122
|
clis.push(cli);
|
|
123
123
|
} catch {}
|
|
124
124
|
}
|
|
@@ -184,13 +184,30 @@ async function cmdInit(target) {
|
|
|
184
184
|
|
|
185
185
|
log(`initialized (${result.file_count || "?"} files)`);
|
|
186
186
|
console.log(" skills: /build /review /status /feedback /bugfix /delegate");
|
|
187
|
+
|
|
188
|
+
// Send anonymous usage ping (stack + file count, no project data)
|
|
189
|
+
apiCall("/v1/feedback", { report: {
|
|
190
|
+
stack_detected: result.stack || "?", _auto: true, _plan: result.plan || "trial",
|
|
191
|
+
_cli_version: VERSION, _files_generated: result.file_count || 0,
|
|
192
|
+
}}).catch(() => {});
|
|
193
|
+
|
|
194
|
+
// Encourage feedback
|
|
195
|
+
console.log(`\n ${T}Tip:${R} Send feedback to earn +5 init/day for 7 days:`);
|
|
196
|
+
console.log(` ${D}0dai feedback log --type positive --detail "what worked"${R}`);
|
|
197
|
+
console.log(` ${D}0dai feedback push${R}`);
|
|
187
198
|
}
|
|
188
199
|
|
|
189
200
|
async function cmdSync(target) {
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
let version = "unknown", stack = "generic", agents = [];
|
|
201
|
+
// Quick local check: skip API if already at current version
|
|
202
|
+
let version = "unknown";
|
|
193
203
|
try { version = fs.readFileSync(path.join(target, "ai", "VERSION"), "utf8").trim(); } catch {}
|
|
204
|
+
if (version === VERSION) {
|
|
205
|
+
log("already up to date (v" + version + ")");
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const { projectFiles, fileContents, clis } = collectMetadata(target);
|
|
210
|
+
let stack = "generic", agents = [];
|
|
194
211
|
try {
|
|
195
212
|
const d = JSON.parse(fs.readFileSync(path.join(target, "ai", "manifest", "discovery.json"), "utf8"));
|
|
196
213
|
stack = d.stack || "generic";
|
|
@@ -353,6 +370,152 @@ async function cmdAuthStatus() {
|
|
|
353
370
|
}
|
|
354
371
|
}
|
|
355
372
|
|
|
373
|
+
async function cmdFeedbackPush(target) {
|
|
374
|
+
const ai = path.join(target, "ai", "feedback");
|
|
375
|
+
const reports = [];
|
|
376
|
+
try {
|
|
377
|
+
for (const f of fs.readdirSync(ai)) {
|
|
378
|
+
if (f.endsWith("-report.json") || (f.endsWith(".json") && f.match(/^\d{8}/))) {
|
|
379
|
+
try {
|
|
380
|
+
const d = JSON.parse(fs.readFileSync(path.join(ai, f), "utf8"));
|
|
381
|
+
if (d.project || d.verdict) reports.push(d);
|
|
382
|
+
} catch {}
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
} catch {}
|
|
386
|
+
|
|
387
|
+
if (!reports.length) { log("no feedback reports found"); return; }
|
|
388
|
+
|
|
389
|
+
// Send each report via API
|
|
390
|
+
for (const report of reports) {
|
|
391
|
+
log(`pushing: ${report.project || "?"} (${report.verdict || "?"})`);
|
|
392
|
+
const result = await apiCall("/v1/feedback", { report });
|
|
393
|
+
if (result.received) {
|
|
394
|
+
log(`received${result.issue ? `: ${result.issue}` : ""}`);
|
|
395
|
+
if (result.bonus) log(`${T}bonus:${R} ${result.bonus}`);
|
|
396
|
+
} else {
|
|
397
|
+
log(`error: ${result.error || "unknown"}`);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// --- Session (local, file-based) ---
|
|
403
|
+
function cmdSession(target, sub, args) {
|
|
404
|
+
const sessFile = path.join(target, "ai", "sessions", "active.json");
|
|
405
|
+
const sessDir = path.dirname(sessFile);
|
|
406
|
+
|
|
407
|
+
if (sub === "save") {
|
|
408
|
+
fs.mkdirSync(sessDir, { recursive: true });
|
|
409
|
+
const goal = args.find((_, i) => args[i - 1] === "--goal") || "";
|
|
410
|
+
const summary = args.find((_, i) => args[i - 1] === "--summary") || "";
|
|
411
|
+
const session = {
|
|
412
|
+
id: `sess-${Date.now()}`,
|
|
413
|
+
started: new Date().toISOString(),
|
|
414
|
+
current_agent: "cli",
|
|
415
|
+
task: { goal: goal || summary || "active session", status: "in_progress" },
|
|
416
|
+
handoff_notes: summary,
|
|
417
|
+
context: { files_touched: [] },
|
|
418
|
+
};
|
|
419
|
+
if (fs.existsSync(sessFile)) {
|
|
420
|
+
const existing = JSON.parse(fs.readFileSync(sessFile, "utf8"));
|
|
421
|
+
existing.handoff_notes = summary || existing.handoff_notes;
|
|
422
|
+
if (goal) existing.task.goal = goal;
|
|
423
|
+
existing.updated = new Date().toISOString();
|
|
424
|
+
fs.writeFileSync(sessFile, JSON.stringify(existing, null, 2));
|
|
425
|
+
log("session updated");
|
|
426
|
+
} else {
|
|
427
|
+
fs.writeFileSync(sessFile, JSON.stringify(session, null, 2));
|
|
428
|
+
log(`session started: ${session.id}`);
|
|
429
|
+
}
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
if (sub === "status") {
|
|
433
|
+
if (!fs.existsSync(sessFile)) { log("no active session"); return; }
|
|
434
|
+
const s = JSON.parse(fs.readFileSync(sessFile, "utf8"));
|
|
435
|
+
log(`session: ${(s.task || {}).goal || "?"}`);
|
|
436
|
+
console.log(` agent: ${s.current_agent || "?"}`);
|
|
437
|
+
if (s.handoff_notes) console.log(` handoff: ${s.handoff_notes}`);
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
if (sub === "complete") {
|
|
441
|
+
if (!fs.existsSync(sessFile)) { log("no active session"); return; }
|
|
442
|
+
const archiveDir = path.join(target, "ai", "sessions", "archive");
|
|
443
|
+
fs.mkdirSync(archiveDir, { recursive: true });
|
|
444
|
+
const s = JSON.parse(fs.readFileSync(sessFile, "utf8"));
|
|
445
|
+
fs.writeFileSync(path.join(archiveDir, `${s.id || "session"}.json`), JSON.stringify(s, null, 2));
|
|
446
|
+
fs.unlinkSync(sessFile);
|
|
447
|
+
log(`session ${s.id} archived`);
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
console.log("Usage: 0dai session [save|status|complete] [--goal '...'] [--summary '...']");
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// --- Swarm (local, file-based) ---
|
|
454
|
+
function cmdSwarm(target, sub, args) {
|
|
455
|
+
const swarmDir = path.join(target, "ai", "swarm");
|
|
456
|
+
const queueDir = path.join(swarmDir, "queue");
|
|
457
|
+
|
|
458
|
+
if (sub === "status") {
|
|
459
|
+
const count = (d) => { try { return fs.readdirSync(d).filter(f => f.endsWith(".json")).length; } catch { return 0; } };
|
|
460
|
+
const q = count(path.join(swarmDir, "queue"));
|
|
461
|
+
const a = count(path.join(swarmDir, "active"));
|
|
462
|
+
const d = count(path.join(swarmDir, "done"));
|
|
463
|
+
log(`swarm: ${q} queued, ${a} active, ${d} done`);
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
if (sub === "add" || sub === "delegate") {
|
|
467
|
+
fs.mkdirSync(queueDir, { recursive: true });
|
|
468
|
+
const task = args.find((_, i) => args[i - 1] === "--task") || "untitled";
|
|
469
|
+
const forAgent = args.find((_, i) => ["--for", "--to"].includes(args[i - 1])) || "any";
|
|
470
|
+
const id = `swarm-${Date.now()}`;
|
|
471
|
+
const t = { id, title: task, assigned_to: forAgent, status: "pending", created_at: new Date().toISOString(), created_by: "cli" };
|
|
472
|
+
fs.writeFileSync(path.join(queueDir, `${id}.json`), JSON.stringify(t, null, 2));
|
|
473
|
+
log(`task created: ${id} → ${forAgent}`);
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
if (sub === "budget") {
|
|
477
|
+
const budgetFile = path.join(swarmDir, "budget.json");
|
|
478
|
+
if (!fs.existsSync(budgetFile)) { log("no budget data yet"); return; }
|
|
479
|
+
const b = JSON.parse(fs.readFileSync(budgetFile, "utf8"));
|
|
480
|
+
log(`total: $${(b.total_spent || 0).toFixed(4)} (${Object.keys(b.tasks || {}).length} tasks)`);
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
console.log("Usage: 0dai swarm [status|add|delegate|budget] [--task '...'] [--to agent]");
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// --- Feedback (local + API push) ---
|
|
487
|
+
async function cmdFeedback(target, sub, args) {
|
|
488
|
+
const fbDir = path.join(target, "ai", "feedback");
|
|
489
|
+
|
|
490
|
+
if (sub === "push") {
|
|
491
|
+
return cmdFeedbackPush(target);
|
|
492
|
+
}
|
|
493
|
+
if (sub === "log") {
|
|
494
|
+
const type = args.find((_, i) => args[i - 1] === "--type") || "suggestion";
|
|
495
|
+
const detail = args.find((_, i) => args[i - 1] === "--detail") || "";
|
|
496
|
+
if (!detail) { console.log("Usage: 0dai feedback log --type bug|suggestion|friction|positive --detail '...'"); return; }
|
|
497
|
+
fs.mkdirSync(fbDir, { recursive: true });
|
|
498
|
+
const entry = JSON.stringify({ ts: new Date().toISOString(), type, detail, agent: "cli" });
|
|
499
|
+
fs.appendFileSync(path.join(fbDir, "operational.jsonl"), entry + "\n");
|
|
500
|
+
log(`logged: [${type}] ${detail.slice(0, 60)}`);
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
if (sub === "list") {
|
|
504
|
+
try {
|
|
505
|
+
const files = fs.readdirSync(fbDir).filter(f => f.endsWith("-report.json"));
|
|
506
|
+
if (!files.length) { log("no reports"); return; }
|
|
507
|
+
for (const f of files) {
|
|
508
|
+
try {
|
|
509
|
+
const d = JSON.parse(fs.readFileSync(path.join(fbDir, f), "utf8"));
|
|
510
|
+
console.log(` ${f}: ${d.verdict || "?"} (${d.project || "?"})`);
|
|
511
|
+
} catch {}
|
|
512
|
+
}
|
|
513
|
+
} catch { log("no feedback directory"); }
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
console.log("Usage: 0dai feedback [push|log|list] [--type ...] [--detail '...']");
|
|
517
|
+
}
|
|
518
|
+
|
|
356
519
|
async function main() {
|
|
357
520
|
const args = process.argv.slice(2);
|
|
358
521
|
let target = process.cwd();
|
|
@@ -379,6 +542,9 @@ async function main() {
|
|
|
379
542
|
console.log("Usage: 0dai auth [login|logout|status]");
|
|
380
543
|
}
|
|
381
544
|
break;
|
|
545
|
+
case "session": cmdSession(target, sub, args); break;
|
|
546
|
+
case "swarm": cmdSwarm(target, sub, args); break;
|
|
547
|
+
case "feedback": await cmdFeedback(target, sub, args); break;
|
|
382
548
|
case "--version": console.log(`${T}0dai${R} ${VERSION}`); break;
|
|
383
549
|
case "help": case "--help": case "-h":
|
|
384
550
|
console.log(`\n ${T}0dai${R} v${VERSION} — One config for 5 AI agent CLIs\n`);
|
|
@@ -388,6 +554,9 @@ async function main() {
|
|
|
388
554
|
console.log(" detect Show detected stack");
|
|
389
555
|
console.log(" doctor Check health");
|
|
390
556
|
console.log(" status Show maturity, swarm, session");
|
|
557
|
+
console.log(" session save Save session for roaming");
|
|
558
|
+
console.log(" swarm status Task queue & delegation");
|
|
559
|
+
console.log(" feedback push Send feedback to 0dai");
|
|
391
560
|
console.log(" auth login Authenticate (device code flow)");
|
|
392
561
|
console.log(" auth logout Remove credentials");
|
|
393
562
|
console.log(" auth status Show account and usage");
|