@0dai-dev/cli 2.1.0 → 2.2.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.
Files changed (2) hide show
  1. package/bin/0dai.js +142 -58
  2. package/package.json +15 -3
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.0";
10
+ const VERSION = "2.2.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
@@ -185,22 +191,16 @@ async function cmdInit(target) {
185
191
  log(`initialized (${result.file_count || "?"} files)`);
186
192
  console.log(" skills: /build /review /status /feedback /bugfix /delegate");
187
193
 
188
- // Auto-feedback for free tiers (non-optional provides usage data for product improvement)
189
- const plan = result.plan || "trial";
190
- if (plan === "trial" || plan === "free") {
191
- const autoReport = {
192
- project: path.basename(target),
193
- stack_detected: result.stack || "?",
194
- agent_cli: "cli",
195
- verdict: "auto",
196
- _auto: true,
197
- _plan: plan,
198
- _cli_version: VERSION,
199
- _files_generated: result.file_count || 0,
200
- };
201
- apiCall("/v1/feedback", { report: autoReport }).catch(() => {});
202
- console.log(` ${D}usage data sent to improve 0dai (disable with Essential+ plan)${R}`);
203
- }
194
+ // Send anonymous usage ping (stack + file count, no project data)
195
+ apiCall("/v1/feedback", { report: {
196
+ stack_detected: result.stack || "?", _auto: true, _plan: result.plan || "trial",
197
+ _cli_version: VERSION, _files_generated: result.file_count || 0,
198
+ }}).catch(() => {});
199
+
200
+ // Encourage feedback
201
+ console.log(`\n ${T}Tip:${R} Send feedback to earn +5 init/day for 7 days:`);
202
+ console.log(` ${D}0dai feedback log --type positive --detail "what worked"${R}`);
203
+ console.log(` ${D}0dai feedback push${R}`);
204
204
  }
205
205
 
206
206
  async function cmdSync(target) {
@@ -316,43 +316,128 @@ async function checkVersion() {
316
316
  }
317
317
 
318
318
  async function cmdAuthLogin() {
319
- // Step 1: request device code
320
- const result = await apiCall("/v1/auth/device", { client_id: "cli" });
321
- 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
+ }
346
+
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:");
378
+ }
322
379
 
323
- log(`Open this URL in your browser:\n`);
324
- console.log(` ${result.verification_uri}\n`);
325
- log(`Enter code: ${result.user_code}\n`);
326
- log("Waiting for authorization...");
327
-
328
- // Step 2: poll for token
329
- const interval = (result.interval || 5) * 1000;
330
- const deadline = Date.now() + (result.expires_in || 600) * 1000;
331
-
332
- while (Date.now() < deadline) {
333
- await new Promise(r => setTimeout(r, interval));
334
- const poll = await apiCall("/v1/auth/token", { device_code: result.device_code });
335
- if (poll.access_token) {
336
- // Save token
337
- fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
338
- fs.writeFileSync(AUTH_FILE, JSON.stringify({
339
- access_token: poll.access_token,
340
- email: poll.email,
341
- plan: poll.plan || "free",
342
- authenticated_at: new Date().toISOString(),
343
- expires_at: poll.expires_at,
344
- }, null, 2) + "\n", { mode: 0o600 });
345
- log(`Logged in as ${poll.email} (${poll.plan} plan)`);
346
- return;
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
+ }
347
411
  }
348
- if (poll.error && poll.error !== "authorization_pending") {
349
- log(`${poll.error}`);
350
- process.exit(1);
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
+ }
351
437
  }
352
- process.stdout.write(".");
438
+ log("Timed out");
439
+ process.exit(1);
353
440
  }
354
- log("Authorization timed out. Try again.");
355
- process.exit(1);
356
441
  }
357
442
 
358
443
  function cmdAuthLogout() {
@@ -396,10 +481,9 @@ async function cmdFeedbackPush(target) {
396
481
  for (const report of reports) {
397
482
  log(`pushing: ${report.project || "?"} (${report.verdict || "?"})`);
398
483
  const result = await apiCall("/v1/feedback", { report });
399
- if (result.issue) {
400
- log(`issue created: ${result.issue}`);
401
- } else if (result.received) {
402
- log("received by server");
484
+ if (result.received) {
485
+ log(`received${result.issue ? `: ${result.issue}` : ""}`);
486
+ if (result.bonus) log(`${T}bonus:${R} ${result.bonus}`);
403
487
  } else {
404
488
  log(`error: ${result.error || "unknown"}`);
405
489
  }
package/package.json CHANGED
@@ -1,11 +1,20 @@
1
1
  {
2
2
  "name": "@0dai-dev/cli",
3
- "version": "2.1.0",
3
+ "version": "2.2.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,8 @@
20
29
  "bin/",
21
30
  "lib/",
22
31
  "README.md"
23
- ]
32
+ ],
33
+ "dependencies": {
34
+ "@clack/prompts": "^1.2.0"
35
+ }
24
36
  }