@0dai-dev/cli 2.1.1 → 2.3.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 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.1.1";
10
+ const VERSION = "2.3.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
@@ -152,10 +152,15 @@ async function cmdInit(target) {
152
152
  return;
153
153
  }
154
154
 
155
- log("collecting project metadata...");
156
- const { projectFiles, fileContents, clis } = collectMetadata(target);
155
+ const isTTY = process.stdout.isTTY;
156
+ let spinner = null;
157
+ if (isTTY) {
158
+ try { spinner = require("@clack/prompts").spinner(); } catch {}
159
+ }
157
160
 
158
- log(`sending to API (${projectFiles.length} files, ${clis.length} CLIs)...`);
161
+ const { projectFiles, fileContents, clis } = collectMetadata(target);
162
+ if (spinner) spinner.start(`Generating ai/ layer (${projectFiles.length} files, ${clis.length} CLIs)...`);
163
+ else log(`sending to API (${projectFiles.length} files, ${clis.length} CLIs)...`);
159
164
  const result = await apiCall("/v1/init", {
160
165
  project_files: projectFiles,
161
166
  file_contents: fileContents,
@@ -172,7 +177,8 @@ async function cmdInit(target) {
172
177
  process.exit(1);
173
178
  }
174
179
 
175
- log(`detected: ${result.stack || "?"}`);
180
+ if (spinner) spinner.stop(`Detected: ${result.stack || "?"}`);
181
+ else log(`detected: ${result.stack || "?"}`);
176
182
  writeFiles(target, result.files || {});
177
183
 
178
184
  // Add to .gitignore
@@ -310,43 +316,128 @@ async function checkVersion() {
310
316
  }
311
317
 
312
318
  async function cmdAuthLogin() {
313
- // Step 1: request device code
314
- const result = await apiCall("/v1/auth/device", { client_id: "cli" });
315
- if (result.error) { log(`error: ${result.error}`); process.exit(1); }
319
+ const isTTY = process.stdout.isTTY && process.stdin.isTTY;
320
+
321
+ if (isTTY) {
322
+ // Interactive TUI flow
323
+ const p = require("@clack/prompts");
324
+ p.intro(`${T}0dai${R} authentication`);
325
+
326
+ const method = await p.select({
327
+ message: "How would you like to sign in?",
328
+ options: [
329
+ { value: "github", label: "GitHub", hint: "recommended" },
330
+ { value: "google", label: "Google" },
331
+ { value: "device", label: "Device code", hint: "no browser needed" },
332
+ ],
333
+ });
334
+ if (p.isCancel(method)) { p.cancel("Cancelled"); process.exit(0); }
335
+
336
+ if (method === "github" || method === "google") {
337
+ const url = `${API_URL}/v1/auth/${method}?cli=true`;
338
+ p.log.info(`Opening browser: ${url}`);
339
+ try {
340
+ const { execSync } = require("child_process");
341
+ const cmd = os.platform() === "darwin" ? "open" : os.platform() === "win32" ? "start" : "xdg-open";
342
+ execSync(`${cmd} "${url}"`, { stdio: "ignore" });
343
+ } catch {
344
+ p.log.warn(`Could not open browser. Visit manually:\n ${url}`);
345
+ }
316
346
 
317
- log(`Open this URL in your browser:\n`);
318
- console.log(` ${result.verification_uri}\n`);
319
- log(`Enter code: ${result.user_code}\n`);
320
- log("Waiting for authorization...");
321
-
322
- // Step 2: poll for token
323
- const interval = (result.interval || 5) * 1000;
324
- const deadline = Date.now() + (result.expires_in || 600) * 1000;
325
-
326
- while (Date.now() < deadline) {
327
- await new Promise(r => setTimeout(r, interval));
328
- const poll = await apiCall("/v1/auth/token", { device_code: result.device_code });
329
- if (poll.access_token) {
330
- // Save token
331
- fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
332
- fs.writeFileSync(AUTH_FILE, JSON.stringify({
333
- access_token: poll.access_token,
334
- email: poll.email,
335
- plan: poll.plan || "free",
336
- authenticated_at: new Date().toISOString(),
337
- expires_at: poll.expires_at,
338
- }, null, 2) + "\n", { mode: 0o600 });
339
- log(`Logged in as ${poll.email} (${poll.plan} plan)`);
340
- return;
347
+ const s = p.spinner();
348
+ s.start("Waiting for browser confirmation...");
349
+
350
+ // Poll auth/status until we get a new token (check every 3s, 5min timeout)
351
+ // For now, ask user to paste token from success page
352
+ s.stop("Browser opened");
353
+ const token = await p.text({
354
+ message: "Paste your token from the success page (or press Enter to skip):",
355
+ placeholder: "0dai_at_...",
356
+ });
357
+ if (token && !p.isCancel(token) && token.startsWith("0dai_at_")) {
358
+ fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
359
+ fs.writeFileSync(AUTH_FILE, JSON.stringify({
360
+ access_token: token,
361
+ authenticated_at: new Date().toISOString(),
362
+ }, null, 2) + "\n", { mode: 0o600 });
363
+ // Fetch profile
364
+ const status = await apiCall("/v1/auth/status");
365
+ if (status.email) {
366
+ const auth = JSON.parse(fs.readFileSync(AUTH_FILE, "utf8"));
367
+ auth.email = status.email;
368
+ auth.plan = status.plan;
369
+ auth.name = status.name;
370
+ fs.writeFileSync(AUTH_FILE, JSON.stringify(auth, null, 2) + "\n", { mode: 0o600 });
371
+ p.outro(`${T}Logged in${R} as ${status.email} (${status.plan} plan)`);
372
+ } else {
373
+ p.outro(`${T}Token saved${R}`);
374
+ }
375
+ return;
376
+ }
377
+ p.log.info("Skipped. You can also use device code flow:");
341
378
  }
342
- if (poll.error && poll.error !== "authorization_pending") {
343
- log(`${poll.error}`);
344
- process.exit(1);
379
+
380
+ // Device code fallback
381
+ const result = await apiCall("/v1/auth/device", { client_id: "cli" });
382
+ if (result.error) { p.log.error(result.error); process.exit(1); }
383
+
384
+ p.log.step(`Open: ${result.verification_uri}`);
385
+ p.log.step(`Code: ${T}${result.user_code}${R}`);
386
+
387
+ const s = p.spinner();
388
+ s.start("Waiting for confirmation...");
389
+
390
+ const interval = (result.interval || 5) * 1000;
391
+ const deadline = Date.now() + (result.expires_in || 600) * 1000;
392
+ while (Date.now() < deadline) {
393
+ await new Promise(r => setTimeout(r, interval));
394
+ const poll = await apiCall("/v1/auth/token", { device_code: result.device_code });
395
+ if (poll.access_token) {
396
+ s.stop("Authorized!");
397
+ fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
398
+ fs.writeFileSync(AUTH_FILE, JSON.stringify({
399
+ access_token: poll.access_token, email: poll.email,
400
+ plan: poll.plan || "free", authenticated_at: new Date().toISOString(),
401
+ expires_at: poll.expires_at,
402
+ }, null, 2) + "\n", { mode: 0o600 });
403
+ p.outro(`${T}Logged in${R} as ${poll.email} (${poll.plan} plan)`);
404
+ return;
405
+ }
406
+ if (poll.error && poll.error !== "authorization_pending") {
407
+ s.stop("Failed");
408
+ p.log.error(poll.error);
409
+ process.exit(1);
410
+ }
411
+ }
412
+ s.stop("Timed out");
413
+ p.log.error("Try again.");
414
+ process.exit(1);
415
+
416
+ } else {
417
+ // Non-interactive: device code only
418
+ const result = await apiCall("/v1/auth/device", { client_id: "cli" });
419
+ if (result.error) { log(`error: ${result.error}`); process.exit(1); }
420
+ log(`Open: ${result.verification_uri}`);
421
+ log(`Code: ${result.user_code}`);
422
+ log("Waiting...");
423
+ const interval = (result.interval || 5) * 1000;
424
+ const deadline = Date.now() + (result.expires_in || 600) * 1000;
425
+ while (Date.now() < deadline) {
426
+ await new Promise(r => setTimeout(r, interval));
427
+ const poll = await apiCall("/v1/auth/token", { device_code: result.device_code });
428
+ if (poll.access_token) {
429
+ fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
430
+ fs.writeFileSync(AUTH_FILE, JSON.stringify({
431
+ access_token: poll.access_token, email: poll.email,
432
+ plan: poll.plan || "free", authenticated_at: new Date().toISOString(),
433
+ }, null, 2) + "\n", { mode: 0o600 });
434
+ log(`Logged in as ${poll.email}`);
435
+ return;
436
+ }
345
437
  }
346
- process.stdout.write(".");
438
+ log("Timed out");
439
+ process.exit(1);
347
440
  }
348
- log("Authorization timed out. Try again.");
349
- process.exit(1);
350
441
  }
351
442
 
352
443
  function cmdAuthLogout() {
@@ -399,6 +490,55 @@ async function cmdFeedbackPush(target) {
399
490
  }
400
491
  }
401
492
 
493
+ // --- Models ---
494
+ function cmdModels(filter) {
495
+ const MODELS = [
496
+ { name: "Claude Opus 4.6", tier: "deep", score: 95, cli: "claude", flag: "--model opus" },
497
+ { name: "Claude Sonnet 4.6", tier: "balanced", score: 90, cli: "claude", flag: "--model sonnet" },
498
+ { name: "Claude Haiku 4.5", tier: "fast", score: 78, cli: "claude", flag: "--model haiku" },
499
+ { name: "GPT-5.3 Codex", tier: "deep", score: 91, cli: "codex", flag: "-m gpt-5.3-codex" },
500
+ { name: "GPT-5.4", tier: "balanced", score: 89, cli: "codex", flag: "-m gpt-5.4", tested: true },
501
+ { name: "GPT-5.4-mini", tier: "fast", score: 76, cli: "codex", flag: "-m gpt-5.4-mini", tested: true },
502
+ { name: "Gemini 3.1 Pro", tier: "balanced", score: 85, cli: "gemini", flag: "-m gemini-3.1-pro" },
503
+ { name: "Gemini 3 Flash", tier: "fast", score: 77, cli: "gemini", flag: "-m gemini-3-flash" },
504
+ { name: "Kimi K2.5", tier: "balanced", score: 74, cli: "opencode", flag: "-m opencode-go/kimi-k2.5", tested: true },
505
+ { name: "MiniMax M2.7", tier: "balanced", score: 72, cli: "opencode", flag: "-m opencode-go/minimax-m2.7", tested: true },
506
+ { name: "GLM-5", tier: "fast", score: 68, cli: "opencode", flag: "-m opencode-go/glm-5", tested: true },
507
+ { name: "MiniMax M2.5", tier: "fast", score: 66, cli: "opencode", flag: "-m opencode-go/minimax-m2.5", tested: true },
508
+ { name: "Qwen 3.6+ Free", tier: "fast", score: 64, cli: "opencode", flag: "-m opencode/qwen3.6-plus-free", tested: true },
509
+ ];
510
+
511
+ const { execFileSync } = require("child_process");
512
+ const available = new Set();
513
+ for (const cli of ["claude", "codex", "opencode", "gemini", "aider"]) {
514
+ try { execFileSync("/bin/sh", ["-c", `command -v ${cli}`], { stdio: "ignore" }); available.add(cli); } catch {}
515
+ }
516
+
517
+ const isTTY = process.stdout.isTTY;
518
+ const Y = isTTY ? "\x1b[33m" : "";
519
+ const G = isTTY ? "\x1b[32m" : "";
520
+ const DIM = isTTY ? "\x1b[2m" : "";
521
+
522
+ let models = [...MODELS].sort((a, b) => b.score - a.score);
523
+ if (filter === "--fast") models = models.filter(m => m.tier === "fast");
524
+ if (filter === "--balanced") models = models.filter(m => m.tier === "balanced");
525
+ if (filter === "--deep") models = models.filter(m => m.tier === "deep");
526
+ if (filter === "--available") models = models.filter(m => available.has(m.cli));
527
+
528
+ const tc = (t) => t === "deep" ? T : t === "balanced" ? G : DIM;
529
+ console.log(`\n ${T}0dai${R} model ratings — ${models.length} models\n`);
530
+ console.log(` ${"SCORE".padEnd(6)} ${"MODEL".padEnd(22)} ${"TIER".padEnd(10)} ${"CLI".padEnd(10)} FLAG`);
531
+ console.log(` ${"-".repeat(64)}`);
532
+ for (const m of models) {
533
+ const dim = available.has(m.cli) ? "" : DIM;
534
+ const mark = m.tested ? ` ${G}✓${R}` : "";
535
+ console.log(`${dim} ${Y}${String(m.score).padEnd(6)}${R} ${m.name.padEnd(22)} ${tc(m.tier)}${m.tier.padEnd(10)}${R} ${m.cli.padEnd(10)} ${DIM}${m.flag}${R}${mark}${dim ? R : ""}`);
536
+ }
537
+ console.log(`\n ${DIM}✓ = swarm-benchmarked | dimmed = CLI not installed${R}`);
538
+ console.log(` ${DIM}Filter: --fast --balanced --deep --available${R}`);
539
+ console.log(` ${DIM}Full table: https://0dai.dev/models${R}\n`);
540
+ }
541
+
402
542
  // --- Session (local, file-based) ---
403
543
  function cmdSession(target, sub, args) {
404
544
  const sessFile = path.join(target, "ai", "sessions", "active.json");
@@ -545,6 +685,28 @@ async function main() {
545
685
  case "session": cmdSession(target, sub, args); break;
546
686
  case "swarm": cmdSwarm(target, sub, args); break;
547
687
  case "feedback": await cmdFeedback(target, sub, args); break;
688
+ case "models": cmdModels(sub || args[1]); break;
689
+ case "terminal": case "term":
690
+ try {
691
+ const SessionManager = require("../lib/session-manager");
692
+ const sm = new SessionManager();
693
+ if (sub === "launch" || !sub) {
694
+ const tool = args.find((_, i) => args[i - 1] === "--tool") || "codex";
695
+ const id = sm.spawn(tool, [], target);
696
+ log(`session ${id.slice(0, 8)} started (${tool})`);
697
+ sm.attach(id);
698
+ } else if (sub === "list") {
699
+ const sessions = sm.list();
700
+ if (!sessions.length) { log("no active sessions"); break; }
701
+ for (const s of sessions) console.log(` ${s.id.slice(0, 8)} [${s.tool}] ${s.status} ${s.attached ? "(attached)" : ""}`);
702
+ } else {
703
+ console.log("Usage: 0dai terminal [launch|list] [--tool codex|claude|gemini]");
704
+ }
705
+ } catch (e) {
706
+ if (e.code === "MODULE_NOT_FOUND") log("install node-pty first: cd ~/.0dai && npm install node-pty");
707
+ else log(`error: ${e.message}`);
708
+ }
709
+ break;
548
710
  case "--version": console.log(`${T}0dai${R} ${VERSION}`); break;
549
711
  case "help": case "--help": case "-h":
550
712
  console.log(`\n ${T}0dai${R} v${VERSION} — One config for 5 AI agent CLIs\n`);
@@ -557,6 +719,8 @@ async function main() {
557
719
  console.log(" session save Save session for roaming");
558
720
  console.log(" swarm status Task queue & delegation");
559
721
  console.log(" feedback push Send feedback to 0dai");
722
+ console.log(" models Show model ratings (--fast/--balanced/--deep/--available)");
723
+ console.log(" terminal Launch interactive agent session");
560
724
  console.log(" auth login Authenticate (device code flow)");
561
725
  console.log(" auth logout Remove credentials");
562
726
  console.log(" auth status Show account and usage");
@@ -0,0 +1,103 @@
1
+ "use strict";
2
+
3
+ const { randomUUID } = require("crypto");
4
+ const pty = require("node-pty");
5
+
6
+ class SessionManager {
7
+ constructor(opts = {}) {
8
+ this.sessions = new Map();
9
+ this.activeId = null;
10
+ this.bufferSize = opts.bufferSize || 65536;
11
+ this._stdin = null;
12
+ this._resize = null;
13
+ this._cleanup = () => this.detach();
14
+ process.once("exit", this._cleanup);
15
+ process.once("SIGINT", () => { this.detach(); process.exit(130); });
16
+ process.once("SIGTERM", () => { this.detach(); process.exit(143); });
17
+ }
18
+
19
+ spawn(tool, args = [], cwd = process.cwd()) {
20
+ const id = randomUUID();
21
+ const proc = pty.spawn(tool, args, {
22
+ cwd,
23
+ env: process.env,
24
+ name: "xterm-color",
25
+ cols: process.stdout.columns || 80,
26
+ rows: process.stdout.rows || 24,
27
+ });
28
+ const session = {
29
+ id,
30
+ tool,
31
+ command: [tool].concat(args).join(" "),
32
+ cwd,
33
+ status: "running",
34
+ createdAt: new Date().toISOString(),
35
+ lastActivityAt: new Date().toISOString(),
36
+ buffer: "",
37
+ exitCode: null,
38
+ signal: null,
39
+ proc,
40
+ };
41
+ proc.onData((data) => {
42
+ session.lastActivityAt = new Date().toISOString();
43
+ session.buffer = (session.buffer + data).slice(-this.bufferSize);
44
+ if (this.activeId === id) process.stdout.write(data);
45
+ });
46
+ proc.onExit(({ exitCode, signal }) => {
47
+ session.status = "exited";
48
+ session.exitCode = exitCode;
49
+ session.signal = signal;
50
+ if (this.activeId === id) this.detach();
51
+ });
52
+ this.sessions.set(id, session);
53
+ return id;
54
+ }
55
+
56
+ attach(sessionId) {
57
+ const session = this.sessions.get(sessionId);
58
+ if (!session) throw new Error(`Unknown session: ${sessionId}`);
59
+ if (session.status !== "running") throw new Error(`Session is ${session.status}: ${sessionId}`);
60
+ if (this.activeId && this.activeId !== sessionId) this.detach();
61
+ this.activeId = sessionId;
62
+ if (session.buffer) process.stdout.write(session.buffer);
63
+ this._stdin = (data) => session.proc.write(data);
64
+ this._resize = () => session.proc.resize(process.stdout.columns || 80, process.stdout.rows || 24);
65
+ if (process.stdin.isTTY) {
66
+ process.stdin.setRawMode(true);
67
+ process.stdin.resume();
68
+ }
69
+ process.stdin.on("data", this._stdin);
70
+ process.stdout.on("resize", this._resize);
71
+ }
72
+
73
+ detach() {
74
+ if (!this.activeId) return;
75
+ if (this._stdin) process.stdin.off("data", this._stdin);
76
+ if (this._resize) process.stdout.off("resize", this._resize);
77
+ if (process.stdin.isTTY) process.stdin.setRawMode(false);
78
+ this._stdin = null;
79
+ this._resize = null;
80
+ this.activeId = null;
81
+ }
82
+
83
+ list() {
84
+ return Array.from(this.sessions.values())
85
+ .filter((session) => session.status === "running")
86
+ .map(({ proc, buffer, ...session }) => ({
87
+ ...session,
88
+ attached: session.id === this.activeId,
89
+ bufferedBytes: Buffer.byteLength(buffer),
90
+ }));
91
+ }
92
+
93
+ kill(sessionId) {
94
+ const session = this.sessions.get(sessionId);
95
+ if (!session) return false;
96
+ if (this.activeId === sessionId) this.detach();
97
+ session.proc.kill();
98
+ this.sessions.delete(sessionId);
99
+ return true;
100
+ }
101
+ }
102
+
103
+ module.exports = SessionManager;
package/package.json CHANGED
@@ -1,11 +1,20 @@
1
1
  {
2
2
  "name": "@0dai-dev/cli",
3
- "version": "2.1.1",
3
+ "version": "2.3.0",
4
4
  "description": "One config layer for 5 AI agent CLIs — Claude Code, Codex, OpenCode, Gemini, Aider",
5
5
  "bin": {
6
6
  "0dai": "./bin/0dai.js"
7
7
  },
8
- "keywords": ["ai", "agents", "claude", "codex", "gemini", "aider", "developer-tools", "mcp"],
8
+ "keywords": [
9
+ "ai",
10
+ "agents",
11
+ "claude",
12
+ "codex",
13
+ "gemini",
14
+ "aider",
15
+ "developer-tools",
16
+ "mcp"
17
+ ],
9
18
  "author": "0dai-dev <dev@0dai.dev>",
10
19
  "license": "MIT",
11
20
  "repository": {
@@ -20,5 +29,9 @@
20
29
  "bin/",
21
30
  "lib/",
22
31
  "README.md"
23
- ]
32
+ ],
33
+ "dependencies": {
34
+ "@clack/prompts": "^1.2.0",
35
+ "node-pty": "^1.0.0"
36
+ }
24
37
  }