@0dai-dev/cli 1.2.1 → 1.3.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.
Files changed (2) hide show
  1. package/bin/0dai.js +111 -9
  2. package/package.json +1 -1
package/bin/0dai.js CHANGED
@@ -27,16 +27,37 @@ const PROBE_DIRS = [
27
27
  "android", "ios", "linux", "macos", "windows",
28
28
  ];
29
29
 
30
+ function deviceFingerprint() {
31
+ const crypto = require("crypto");
32
+ const parts = [
33
+ os.hostname(),
34
+ os.userInfo().username,
35
+ os.platform(),
36
+ os.arch(),
37
+ os.cpus().length.toString(),
38
+ os.totalmem().toString(),
39
+ ];
40
+ // Try machine-id
41
+ try {
42
+ if (os.platform() === "linux") parts.push(fs.readFileSync("/etc/machine-id", "utf8").trim());
43
+ else if (os.platform() === "darwin") {
44
+ const { execSync } = require("child_process");
45
+ parts.push(execSync("ioreg -rd1 -c IOPlatformExpertDevice | awk '/IOPlatformUUID/'", { encoding: "utf8" }).trim());
46
+ }
47
+ } catch {}
48
+ return crypto.createHash("sha256").update(parts.join(":")).digest("hex").slice(0, 32);
49
+ }
50
+
30
51
  function apiCall(endpoint, data) {
31
52
  return new Promise((resolve, reject) => {
32
53
  const url = new URL(endpoint, API_URL);
33
54
  const mod = url.protocol === "https:" ? https : http;
34
55
  const body = data ? JSON.stringify(data) : null;
35
- const headers = { "Content-Type": "application/json" };
56
+ const headers = { "Content-Type": "application/json", "X-Device-ID": deviceFingerprint() };
36
57
 
37
58
  try {
38
59
  const auth = JSON.parse(fs.readFileSync(AUTH_FILE, "utf8"));
39
- const token = auth.api_key || auth.token;
60
+ const token = auth.api_key || auth.access_token || auth.token;
40
61
  if (token) headers["Authorization"] = `Bearer ${token}`;
41
62
  } catch {}
42
63
 
@@ -46,7 +67,7 @@ function apiCall(endpoint, data) {
46
67
  path: url.pathname,
47
68
  method: body ? "POST" : "GET",
48
69
  headers,
49
- timeout: 30000,
70
+ timeout: 60000,
50
71
  };
51
72
  if (body) opts.headers["Content-Length"] = Buffer.byteLength(body);
52
73
 
@@ -136,7 +157,15 @@ async function cmdInit(target) {
136
157
  available_clis: clis,
137
158
  });
138
159
 
139
- if (result.error) { console.error(`[0dai] error: ${result.error}`); process.exit(1); }
160
+ if (result.error) {
161
+ if (result.hint) {
162
+ console.log(`\n[0dai] ${result.message || result.error}`);
163
+ console.log(` ${result.hint}\n`);
164
+ } else {
165
+ console.error(`[0dai] error: ${result.error}`);
166
+ }
167
+ process.exit(1);
168
+ }
140
169
 
141
170
  console.log(`[0dai] detected: ${result.stack || "?"}`);
142
171
  writeFiles(target, result.files || {});
@@ -239,6 +268,67 @@ function cmdStatus(target) {
239
268
  } catch {}
240
269
  }
241
270
 
271
+ async function cmdAuthLogin() {
272
+ // Step 1: request device code
273
+ const result = await apiCall("/v1/auth/device", { client_id: "cli" });
274
+ if (result.error) { console.error(`[0dai] error: ${result.error}`); process.exit(1); }
275
+
276
+ console.log(`[0dai] Open this URL in your browser:\n`);
277
+ console.log(` ${result.verification_uri}\n`);
278
+ console.log(`[0dai] Enter code: ${result.user_code}\n`);
279
+ console.log("[0dai] Waiting for authorization...");
280
+
281
+ // Step 2: poll for token
282
+ const interval = (result.interval || 5) * 1000;
283
+ const deadline = Date.now() + (result.expires_in || 600) * 1000;
284
+
285
+ while (Date.now() < deadline) {
286
+ await new Promise(r => setTimeout(r, interval));
287
+ const poll = await apiCall("/v1/auth/token", { device_code: result.device_code });
288
+ if (poll.access_token) {
289
+ // Save token
290
+ fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
291
+ fs.writeFileSync(AUTH_FILE, JSON.stringify({
292
+ access_token: poll.access_token,
293
+ email: poll.email,
294
+ plan: poll.plan || "free",
295
+ authenticated_at: new Date().toISOString(),
296
+ expires_at: poll.expires_at,
297
+ }, null, 2) + "\n", { mode: 0o600 });
298
+ console.log(`[0dai] Logged in as ${poll.email} (${poll.plan} plan)`);
299
+ return;
300
+ }
301
+ if (poll.error && poll.error !== "authorization_pending") {
302
+ console.error(`[0dai] ${poll.error}`);
303
+ process.exit(1);
304
+ }
305
+ process.stdout.write(".");
306
+ }
307
+ console.error("\n[0dai] Authorization timed out. Try again.");
308
+ process.exit(1);
309
+ }
310
+
311
+ function cmdAuthLogout() {
312
+ try { fs.unlinkSync(AUTH_FILE); } catch {}
313
+ console.log("[0dai] Logged out");
314
+ }
315
+
316
+ async function cmdAuthStatus() {
317
+ try {
318
+ const auth = JSON.parse(fs.readFileSync(AUTH_FILE, "utf8"));
319
+ console.log(`[0dai] ${auth.email} (${auth.plan} plan)`);
320
+ // Get usage from API
321
+ const status = await apiCall("/v1/auth/status");
322
+ if (status.usage_today) {
323
+ console.log(" Usage today:");
324
+ for (const [k, v] of Object.entries(status.usage_today))
325
+ console.log(` ${k}: ${v} / ${status.limits[k]}`);
326
+ }
327
+ } catch {
328
+ console.log("[0dai] Not logged in. Run: 0dai auth login");
329
+ }
330
+ }
331
+
242
332
  async function main() {
243
333
  const args = process.argv.slice(2);
244
334
  let target = process.cwd();
@@ -246,6 +336,7 @@ async function main() {
246
336
  if (ti >= 0 && args[ti + 1]) { target = path.resolve(args[ti + 1]); args.splice(ti, 2); }
247
337
 
248
338
  const cmd = args[0] || "help";
339
+ const sub = args[1] || "";
249
340
 
250
341
  switch (cmd) {
251
342
  case "init": await cmdInit(target); break;
@@ -253,15 +344,26 @@ async function main() {
253
344
  case "detect": await cmdDetect(target); break;
254
345
  case "doctor": cmdDoctor(target); break;
255
346
  case "status": cmdStatus(target); break;
347
+ case "auth":
348
+ if (sub === "login") await cmdAuthLogin();
349
+ else if (sub === "logout") cmdAuthLogout();
350
+ else if (sub === "status") await cmdAuthStatus();
351
+ else {
352
+ console.log("Usage: 0dai auth [login|logout|status]");
353
+ }
354
+ break;
256
355
  case "--version": console.log(`0dai ${VERSION}`); break;
257
356
  case "help": case "--help": case "-h":
258
357
  console.log(`0dai v${VERSION} — One config for 5 AI agent CLIs\n`);
259
358
  console.log("Commands:");
260
- console.log(" init Initialize ai/ layer (via API)");
261
- console.log(" sync Update ai/ layer (via API)");
262
- console.log(" detect Show detected stack");
263
- console.log(" doctor Check health");
264
- console.log(" status Show maturity, swarm, session");
359
+ console.log(" init Initialize ai/ layer (via API)");
360
+ console.log(" sync Update ai/ layer (via API)");
361
+ console.log(" detect Show detected stack");
362
+ console.log(" doctor Check health");
363
+ console.log(" status Show maturity, swarm, session");
364
+ console.log(" auth login Authenticate (device code flow)");
365
+ console.log(" auth logout Remove credentials");
366
+ console.log(" auth status Show account and usage");
265
367
  console.log(" --version\n");
266
368
  console.log("https://0dai.dev");
267
369
  break;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@0dai-dev/cli",
3
- "version": "1.2.1",
3
+ "version": "1.3.1",
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"