@0dai-dev/cli 4.2.0 → 4.3.4

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 (48) hide show
  1. package/README.md +30 -5
  2. package/bin/0dai.js +289 -60
  3. package/lib/commands/audit.js +13 -0
  4. package/lib/commands/auth.js +341 -98
  5. package/lib/commands/boneyard.js +44 -0
  6. package/lib/commands/ci.js +329 -0
  7. package/lib/commands/compliance.js +20 -0
  8. package/lib/commands/doctor.js +20 -1
  9. package/lib/commands/experience.js +5 -1
  10. package/lib/commands/feedback.js +92 -5
  11. package/lib/commands/gh.js +506 -0
  12. package/lib/commands/graph.js +78 -10
  13. package/lib/commands/heatmap.js +17 -0
  14. package/lib/commands/import_claude_code_agents.js +367 -0
  15. package/lib/commands/init.js +440 -28
  16. package/lib/commands/loop.js +108 -0
  17. package/lib/commands/mcp.js +410 -0
  18. package/lib/commands/models.js +27 -3
  19. package/lib/commands/paste.js +114 -0
  20. package/lib/commands/play.js +173 -0
  21. package/lib/commands/provider.js +69 -0
  22. package/lib/commands/quota.js +76 -0
  23. package/lib/commands/receipt.js +53 -0
  24. package/lib/commands/report.js +29 -2
  25. package/lib/commands/run.js +44 -4
  26. package/lib/commands/runner.js +527 -0
  27. package/lib/commands/session.js +1 -7
  28. package/lib/commands/standup.js +40 -0
  29. package/lib/commands/status.js +26 -1
  30. package/lib/commands/swarm.js +97 -4
  31. package/lib/commands/tui.js +81 -13
  32. package/lib/commands/usage.js +87 -0
  33. package/lib/commands/vault.js +246 -0
  34. package/lib/onboarding.js +9 -3
  35. package/lib/shared.js +29 -14
  36. package/lib/tui/index.mjs +571 -187
  37. package/lib/utils/auth.js +1 -0
  38. package/lib/utils/canonical-counts.js +54 -0
  39. package/lib/utils/diff-preview.js +192 -0
  40. package/lib/utils/identity.js +76 -18
  41. package/lib/utils/mcp-auth.js +607 -0
  42. package/lib/utils/plan.js +37 -2
  43. package/lib/vault/cipher.js +125 -0
  44. package/lib/vault/identity.js +122 -0
  45. package/lib/vault/index.js +184 -0
  46. package/lib/vault/storage.js +84 -0
  47. package/lib/wizard.js +19 -12
  48. package/package.json +2 -2
@@ -3,11 +3,193 @@ const shared = require("../shared");
3
3
  const {
4
4
  T, R, D, log,
5
5
  fs, os,
6
- CONFIG_DIR, AUTH_FILE, API_URL,
6
+ AUTH_FILE, API_URL,
7
7
  apiCall, loadAuthState, fetchAuthStatus, updateAuthState,
8
+ saveAuthState,
8
9
  makeEnsureAuthenticated, ensureLicenseActivation,
9
10
  } = shared;
10
11
 
12
+ function _argAfter(args, names) {
13
+ const list = Array.isArray(args) ? args : [];
14
+ const wanted = new Set(Array.isArray(names) ? names : [names]);
15
+ for (let i = 0; i < list.length; i++) {
16
+ const arg = String(list[i] || "");
17
+ if (wanted.has(arg) && list[i + 1]) return String(list[i + 1]);
18
+ for (const name of wanted) {
19
+ if (arg.startsWith(`${name}=`)) return arg.slice(name.length + 1);
20
+ }
21
+ }
22
+ return "";
23
+ }
24
+
25
+ function _looksLikePlanCode(code) {
26
+ const value = String(code || "").trim().toUpperCase();
27
+ return /^(PRO|PROD|TEAM|ENT|ENTERPRISE|ESSENTIAL)-/.test(value);
28
+ }
29
+
30
+ function parseActivationArgs(args = [], options = {}) {
31
+ const authCode = _argAfter(args, ["--auth-code", "--oauth-code", "--exchange-code"]);
32
+ const explicitActivationCode = _argAfter(args, ["--activation-code", "--redeem-code", "--plan-code"]);
33
+ const genericCode = _argAfter(args, "--code");
34
+ const genericCodePurpose = String(options.genericCode || "auto");
35
+ if (genericCodePurpose === "auth") {
36
+ return { authCode: authCode || genericCode, activationCode: explicitActivationCode };
37
+ }
38
+ if (genericCodePurpose === "activation") {
39
+ return { authCode, activationCode: explicitActivationCode || genericCode };
40
+ }
41
+ return {
42
+ authCode: authCode || (genericCode && !_looksLikePlanCode(genericCode) ? genericCode : ""),
43
+ activationCode: explicitActivationCode || (genericCode && _looksLikePlanCode(genericCode) ? genericCode : ""),
44
+ };
45
+ }
46
+
47
+ function parseAuthLoginFlags(args = []) {
48
+ const list = Array.isArray(args) ? args : [];
49
+ return {
50
+ device: list.includes("--device"),
51
+ mcp: list.includes("--mcp") || list.includes("--mcp-auth"),
52
+ noBrowser: list.includes("--no-browser"),
53
+ };
54
+ }
55
+
56
+ function _positiveNumber(value, fallback) {
57
+ const number = Number(value);
58
+ return Number.isFinite(number) && number > 0 ? number : fallback;
59
+ }
60
+
61
+ function _formatSeconds(seconds) {
62
+ const value = Math.round(_positiveNumber(seconds, 600));
63
+ if (value % 60 === 0) return `${value / 60} minute${value === 60 ? "" : "s"}`;
64
+ return `${value} seconds`;
65
+ }
66
+
67
+ function printDeviceLoginInstructions(result, writeLine = log) {
68
+ const expiresIn = _positiveNumber(result && result.expires_in, 600);
69
+ writeLine(`Open: ${result.verification_uri}`);
70
+ writeLine(`Code: ${result.user_code}`);
71
+ writeLine(`Expires in: ${_formatSeconds(expiresIn)}`);
72
+ }
73
+
74
+ async function loginWithDeviceCode(options = {}) {
75
+ const writeLine = typeof options.writeLine === "function" ? options.writeLine : log;
76
+ const spinner = options.spinner || null;
77
+ const result = await apiCall("/v1/auth/device", { client_id: "cli" });
78
+ if (!result || result.error) throw new Error(result && result.error ? result.error : "device auth failed");
79
+
80
+ printDeviceLoginInstructions(result, writeLine);
81
+ if (spinner && typeof spinner.start === "function") spinner.start("Waiting for confirmation...");
82
+
83
+ const intervalMs = Math.max(1, _positiveNumber(result.interval, 5) * 1000);
84
+ const expiresIn = _positiveNumber(result.expires_in, 600);
85
+ const deadline = Date.now() + expiresIn * 1000;
86
+ while (Date.now() < deadline) {
87
+ await new Promise(r => setTimeout(r, intervalMs));
88
+ const poll = await apiCall("/v1/auth/token", { device_code: result.device_code });
89
+ if (poll.access_token) {
90
+ if (spinner && typeof spinner.stop === "function") spinner.stop("Authorized!");
91
+ saveAuthState({
92
+ access_token: poll.access_token,
93
+ email: poll.email,
94
+ plan: poll.plan || "free",
95
+ authenticated_at: new Date().toISOString(),
96
+ expires_at: poll.expires_at,
97
+ });
98
+ return poll;
99
+ }
100
+ if (poll.error && poll.error !== "authorization_pending") {
101
+ if (spinner && typeof spinner.stop === "function") spinner.stop("Failed");
102
+ throw new Error(poll.error);
103
+ }
104
+ }
105
+
106
+ if (spinner && typeof spinner.stop === "function") spinner.stop("Device code expired");
107
+ throw new Error(`Device code expired after ${_formatSeconds(expiresIn)}. Run '0dai auth login --device --no-browser' to get a new code.`);
108
+ }
109
+
110
+ function _authAccessToken(auth) {
111
+ return auth && (auth.access_token || auth.api_key || auth.token) ? String(auth.access_token || auth.api_key || auth.token) : "";
112
+ }
113
+
114
+ function writeMcpTokenForAuth(auth, source = "0dai-account-token") {
115
+ const { writeMcpAuthTokenFromAccount } = require("../utils/mcp-auth");
116
+ return writeMcpAuthTokenFromAccount(_authAccessToken(auth), {
117
+ email: auth && auth.email,
118
+ plan: auth && auth.plan,
119
+ source,
120
+ });
121
+ }
122
+
123
+ function writeMcpTokenForCurrentAuth(source = "0dai-account-token", writeLine = log) {
124
+ const mcp = writeMcpTokenForAuth(loadAuthState(), source);
125
+ writeLine(`MCP auth token stored: ${mcp.tokenPath}`);
126
+ return mcp;
127
+ }
128
+
129
+ async function loginWithExchangeCode(code) {
130
+ const clean = String(code || "").trim();
131
+ if (!clean) throw new Error("auth code required");
132
+ const exchanged = await apiCall("/v1/auth/exchange", { code: clean });
133
+ if (!exchanged || exchanged.error || !exchanged.access_token) {
134
+ throw new Error(exchanged && exchanged.error ? exchanged.error : "invalid or expired auth code");
135
+ }
136
+ saveAuthState({
137
+ access_token: exchanged.access_token,
138
+ email: exchanged.email || "",
139
+ name: exchanged.name || "",
140
+ authenticated_at: new Date().toISOString(),
141
+ });
142
+ const status = await fetchAuthStatus();
143
+ if (!status || status.error || !status.email) {
144
+ throw new Error(status && status.error ? status.error : "cloud validation failed after auth exchange");
145
+ }
146
+ return status;
147
+ }
148
+
149
+ async function loginWithPastedAccessToken(token) {
150
+ const clean = String(token || "").trim();
151
+ if (!clean) throw new Error("access token required");
152
+ const status = await fetchAuthStatus(clean);
153
+ if (!status || status.error || !status.email) {
154
+ throw new Error(status && status.error ? status.error : "cloud validation failed");
155
+ }
156
+ saveAuthState({
157
+ access_token: clean,
158
+ email: status.email,
159
+ plan: status.plan || "free",
160
+ name: status.name || "",
161
+ license: status.license || {},
162
+ plan_expires_at: status.plan_expires_at || "",
163
+ authenticated_at: new Date().toISOString(),
164
+ });
165
+ return status;
166
+ }
167
+
168
+ async function redeemActivationCode(code) {
169
+ const clean = String(code || "").trim().toUpperCase();
170
+ if (!clean) throw new Error("activation code required");
171
+ const result = await apiCall("/v1/redeem", { code: clean });
172
+ if (!result || !result.ok) {
173
+ throw new Error(result && result.error ? result.error : "activation code redeem failed");
174
+ }
175
+ const status = await fetchAuthStatus();
176
+ if (!status || status.error || !status.email) {
177
+ throw new Error(status && status.error ? status.error : "cloud validation failed after activation");
178
+ }
179
+ return { result, status };
180
+ }
181
+
182
+ async function promptOptionalActivationCode(p) {
183
+ if (!(process.stdout.isTTY && process.stdin.isTTY)) return "";
184
+ if (!p || typeof p.text !== "function") return "";
185
+ const value = await p.text({
186
+ message: "Activation / Team code (optional)",
187
+ placeholder: "TEAM-... or press Enter",
188
+ });
189
+ if (p.isCancel(value)) return "";
190
+ return String(value || "").trim();
191
+ }
192
+
11
193
  async function resolveSessionState() {
12
194
  const auth = loadAuthState();
13
195
  const hasCachedToken = !!(auth && (auth.api_key || auth.access_token || auth.token));
@@ -42,7 +224,22 @@ async function resolveSessionState() {
42
224
  };
43
225
  }
44
226
 
45
- async function cmdAuthLogin() {
227
+ async function cmdAuthLogin(args = []) {
228
+ const rawArgs = Array.isArray(args) ? args : [];
229
+ const flags = parseAuthLoginFlags(rawArgs);
230
+ const parsed = parseActivationArgs(rawArgs, { genericCode: "auth" });
231
+ if (parsed.authCode) {
232
+ try {
233
+ const status = await loginWithExchangeCode(parsed.authCode);
234
+ if (flags.mcp) writeMcpTokenForCurrentAuth("0dai-browser-oauth");
235
+ log(`Logged in as ${status.email} (${status.plan || "free"} plan)`);
236
+ return status;
237
+ } catch (err) {
238
+ log(`auth failed: ${err.message || err}`);
239
+ process.exit(1);
240
+ }
241
+ }
242
+
46
243
  const isTTY = process.stdout.isTTY && process.stdin.isTTY;
47
244
 
48
245
  // Check if already authenticated
@@ -55,13 +252,15 @@ async function cmdAuthLogin() {
55
252
  p.log.success(`Already logged in as ${T}${existing.email || "unknown"}${R} (${existing.plan || "free"} plan)`);
56
253
  const reauth = await p.confirm({ message: "Sign in with a different account?" });
57
254
  if (p.isCancel(reauth) || !reauth) {
255
+ if (flags.mcp) writeMcpTokenForCurrentAuth("0dai-account-token", p.log.success);
58
256
  p.outro("Current session kept");
59
- return;
257
+ return existing;
60
258
  }
61
259
  } else {
260
+ if (flags.mcp) writeMcpTokenForCurrentAuth("0dai-account-token");
62
261
  log(`Already logged in as ${existing.email || "unknown"} (${existing.plan || "free"} plan)`);
63
262
  log("To switch accounts, delete ~/.0dai/auth.json and run again");
64
- return;
263
+ return existing;
65
264
  }
66
265
  }
67
266
  } catch {}
@@ -78,15 +277,20 @@ async function cmdAuthLogin() {
78
277
  "Why sign in?"
79
278
  );
80
279
 
81
- const method = await p.select({
82
- message: "How would you like to sign in?",
83
- options: [
84
- { value: "github", label: "GitHub", hint: "recommended" },
85
- { value: "google", label: "Google" },
86
- { value: "device", label: "Device code", hint: "no browser needed" },
87
- ],
88
- });
89
- if (p.isCancel(method)) { p.cancel("Cancelled"); process.exit(0); }
280
+ let method = "device";
281
+ if (!(flags.device || flags.noBrowser)) {
282
+ method = await p.select({
283
+ message: "How would you like to sign in?",
284
+ options: [
285
+ { value: "github", label: "GitHub", hint: "recommended" },
286
+ { value: "google", label: "Google" },
287
+ { value: "device", label: "Device code", hint: "no browser needed" },
288
+ ],
289
+ });
290
+ if (p.isCancel(method)) { p.cancel("Cancelled"); process.exit(0); }
291
+ } else {
292
+ p.log.info("Using device code flow (--device/--no-browser).");
293
+ }
90
294
 
91
295
  if (method === "github" || method === "google") {
92
296
  const url = `${API_URL}/v1/auth/${method}?cli=true`;
@@ -103,97 +307,71 @@ async function cmdAuthLogin() {
103
307
  const s = p.spinner();
104
308
  s.start("Waiting for browser confirmation...");
105
309
 
106
- // Poll auth/status until we get a new token (check every 3s, 5min timeout)
107
- // For now, ask user to paste token from success page
108
310
  s.stop("Browser opened");
109
- const token = await p.text({
110
- message: "Paste your token from the success page (or press Enter to skip):",
111
- placeholder: "0dai_at_...",
311
+ const code = await p.text({
312
+ message: "Paste the code from the success page (or press Enter for device code):",
313
+ placeholder: "code from browser success page",
112
314
  });
113
- if (token && !p.isCancel(token) && token.startsWith("0dai_at_")) {
114
- fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
115
- fs.writeFileSync(AUTH_FILE, JSON.stringify({
116
- access_token: token,
117
- authenticated_at: new Date().toISOString(),
118
- }, null, 2) + "\n", { mode: 0o600 });
119
- // Fetch profile
120
- const status = await apiCall("/v1/auth/status");
121
- if (status.email) {
122
- const auth = JSON.parse(fs.readFileSync(AUTH_FILE, "utf8"));
123
- auth.email = status.email;
124
- auth.plan = status.plan;
125
- auth.name = status.name;
126
- fs.writeFileSync(AUTH_FILE, JSON.stringify(auth, null, 2) + "\n", { mode: 0o600 });
127
- p.outro(`${T}Logged in${R} as ${status.email} (${status.plan} plan)`);
128
- } else {
129
- p.outro(`${T}Token saved${R}`);
315
+ if (code && !p.isCancel(code)) {
316
+ const pasted = String(code).trim();
317
+ try {
318
+ if (pasted.startsWith("0dai_at_")) {
319
+ const status = await loginWithPastedAccessToken(pasted);
320
+ if (flags.mcp) writeMcpTokenForCurrentAuth("0dai-pasted-token", p.log.success);
321
+ p.outro(`${T}Logged in${R} as ${status.email} (${status.plan || "free"} plan)`);
322
+ return status;
323
+ } else {
324
+ const status = await loginWithExchangeCode(pasted);
325
+ if (flags.mcp) writeMcpTokenForCurrentAuth("0dai-browser-oauth", p.log.success);
326
+ p.outro(`${T}Logged in${R} as ${status.email} (${status.plan || "free"} plan)`);
327
+ return status;
328
+ }
329
+ } catch (err) {
330
+ p.log.error(err.message || String(err));
331
+ if (pasted.startsWith("0dai_at_")) process.exit(1);
130
332
  }
131
- return;
132
333
  }
133
334
  p.log.info("Skipped. You can also use device code flow:");
134
335
  }
135
336
 
136
- // Device code fallback
137
- const result = await apiCall("/v1/auth/device", { client_id: "cli" });
138
- if (result.error) { p.log.error(result.error); process.exit(1); }
139
-
140
- p.log.step(`Open: ${result.verification_uri}`);
141
- p.log.step(`Code: ${T}${result.user_code}${R}`);
142
-
143
337
  const s = p.spinner();
144
- s.start("Waiting for confirmation...");
145
-
146
- const interval = (result.interval || 5) * 1000;
147
- const deadline = Date.now() + (result.expires_in || 600) * 1000;
148
- while (Date.now() < deadline) {
149
- await new Promise(r => setTimeout(r, interval));
150
- const poll = await apiCall("/v1/auth/token", { device_code: result.device_code });
151
- if (poll.access_token) {
152
- s.stop("Authorized!");
153
- fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
154
- fs.writeFileSync(AUTH_FILE, JSON.stringify({
155
- access_token: poll.access_token, email: poll.email,
156
- plan: poll.plan || "free", authenticated_at: new Date().toISOString(),
157
- expires_at: poll.expires_at,
158
- }, null, 2) + "\n", { mode: 0o600 });
159
- p.outro(`${T}Logged in${R} as ${poll.email} (${poll.plan} plan)`);
160
- return;
161
- }
162
- if (poll.error && poll.error !== "authorization_pending") {
163
- s.stop("Failed");
164
- p.log.error(poll.error);
165
- process.exit(1);
338
+ try {
339
+ const poll = await loginWithDeviceCode({
340
+ writeLine: (message) => p.log.step(message),
341
+ spinner: s,
342
+ });
343
+ if (flags.mcp) {
344
+ const mcp = writeMcpTokenForAuth(
345
+ { access_token: poll.access_token, email: poll.email, plan: poll.plan },
346
+ "0dai-device-code"
347
+ );
348
+ p.log.success(`MCP auth token stored: ${mcp.tokenPath}`);
166
349
  }
350
+ p.outro(`${T}Logged in${R} as ${poll.email} (${poll.plan || "free"} plan)`);
351
+ return poll;
352
+ } catch (err) {
353
+ p.log.error(err.message || String(err));
354
+ process.exit(1);
167
355
  }
168
- s.stop("Device code expired");
169
- p.log.error("The code expired after 10 minutes. Run '0dai auth login' to get a new code.");
170
- process.exit(1);
171
356
 
172
357
  } else {
173
358
  // Non-interactive: device code only
174
359
  log("0dai auth is separate from agent CLIs. It tracks projects, limits, and team features.");
175
- const result = await apiCall("/v1/auth/device", { client_id: "cli" });
176
- if (result.error) { log(`error: ${result.error}`); process.exit(1); }
177
- log(`Open: ${result.verification_uri}`);
178
- log(`Code: ${result.user_code}`);
179
- log("Waiting...");
180
- const interval = (result.interval || 5) * 1000;
181
- const deadline = Date.now() + (result.expires_in || 600) * 1000;
182
- while (Date.now() < deadline) {
183
- await new Promise(r => setTimeout(r, interval));
184
- const poll = await apiCall("/v1/auth/token", { device_code: result.device_code });
185
- if (poll.access_token) {
186
- fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
187
- fs.writeFileSync(AUTH_FILE, JSON.stringify({
188
- access_token: poll.access_token, email: poll.email,
189
- plan: poll.plan || "free", authenticated_at: new Date().toISOString(),
190
- }, null, 2) + "\n", { mode: 0o600 });
191
- log(`Logged in as ${poll.email}`);
192
- return;
360
+ try {
361
+ const poll = await loginWithDeviceCode({ writeLine: log });
362
+ if (flags.mcp) {
363
+ const mcp = writeMcpTokenForAuth(
364
+ { access_token: poll.access_token, email: poll.email, plan: poll.plan },
365
+ "0dai-device-code"
366
+ );
367
+ log(`MCP auth token stored: ${mcp.tokenPath}`);
193
368
  }
369
+ log(`Logged in as ${poll.email} (${poll.plan || "free"} plan)`);
370
+ return poll;
371
+ } catch (err) {
372
+ log(`error: ${err.message || err}`);
373
+ process.exit(1);
194
374
  }
195
- log("Device code expired after 10 minutes. Run '0dai auth login' again.");
196
- process.exit(1);
197
375
  }
198
376
  }
199
377
 
@@ -202,6 +380,31 @@ function cmdAuthLogout() {
202
380
  log("Logged out");
203
381
  }
204
382
 
383
+ async function cmdAuthMcp(args = []) {
384
+ const rawArgs = Array.isArray(args) ? args : [];
385
+ const flags = parseAuthLoginFlags(rawArgs);
386
+ let auth = loadAuthState();
387
+
388
+ if (flags.device || !_authAccessToken(auth)) {
389
+ log("Starting device code flow for MCP auth.");
390
+ const poll = await loginWithDeviceCode({ writeLine: log });
391
+ auth = {
392
+ access_token: poll.access_token,
393
+ email: poll.email,
394
+ plan: poll.plan || "free",
395
+ };
396
+ const mcp = writeMcpTokenForAuth(auth, "0dai-device-code");
397
+ log(`MCP auth token stored: ${mcp.tokenPath}`);
398
+ log("For bearer-env MCP clients, launch them with OPERATOR_MCP_TOKEN set from the saved 0dai auth token.");
399
+ return poll;
400
+ }
401
+
402
+ const mcp = writeMcpTokenForAuth(auth, "0dai-account-token");
403
+ log(`MCP auth token stored: ${mcp.tokenPath}`);
404
+ log(`Account: ${auth.email || "unknown"} (${auth.plan || "free"} plan)`);
405
+ return { email: auth.email || "", plan: auth.plan || "free" };
406
+ }
407
+
205
408
  async function cmdRedeem(code) {
206
409
  if (!code) {
207
410
  console.log("Usage: 0dai redeem <CODE>");
@@ -215,20 +418,43 @@ async function cmdRedeem(code) {
215
418
  process.exit(1);
216
419
  }
217
420
  log(`Redeeming code ${T}${code.toUpperCase()}${R}...`);
218
- const result = await apiCall("/v1/redeem", { code: code.toUpperCase().trim() });
219
- if (result.ok) {
421
+ try {
422
+ const { result, status } = await redeemActivationCode(code);
220
423
  log(`${T}✓${R} ${result.message}`);
221
- if (result.duration_days) {
222
- log(` Plan active for ${result.duration_days} days`);
223
- }
224
- log(` Run ${D}0dai auth status${R} to see updated limits`);
225
- } else {
226
- log(`error: ${result.error || "unknown"}`);
227
- if (result.hint) log(`hint: ${result.hint}`);
424
+ if (result.duration_days) log(` Plan active for ${result.duration_days} days`);
425
+ log(` Account: ${status.email} · plan: ${status.plan || "free"}`);
426
+ } catch (err) {
427
+ log(`error: ${err.message || err}`);
228
428
  process.exit(1);
229
429
  }
230
430
  }
231
431
 
432
+ async function ensureAccountForActivation(actionLabel, args = []) {
433
+ const parsed = parseActivationArgs(args, { genericCode: "activation" });
434
+ if (parsed.authCode) {
435
+ log("using auth code from command line");
436
+ await loginWithExchangeCode(parsed.authCode);
437
+ }
438
+
439
+ const ensureAuthenticated = makeEnsureAuthenticated(cmdAuthLogin);
440
+ let status = await ensureAuthenticated(actionLabel);
441
+
442
+ let activationCode = parsed.activationCode;
443
+ let prompts = null;
444
+ if (!activationCode && process.stdout.isTTY && process.stdin.isTTY) {
445
+ try { prompts = require("@clack/prompts"); } catch {}
446
+ activationCode = await promptOptionalActivationCode(prompts);
447
+ }
448
+ if (activationCode) {
449
+ log(`redeeming activation code ${T}${activationCode.toUpperCase()}${R}`);
450
+ const redeemed = await redeemActivationCode(activationCode);
451
+ status = redeemed.status;
452
+ log(`plan active: ${status.plan || "free"}`);
453
+ }
454
+
455
+ return status;
456
+ }
457
+
232
458
  async function cmdAuthStatus() {
233
459
  const session = await resolveSessionState();
234
460
  if (!session.ok) {
@@ -277,4 +503,21 @@ async function cmdActivateStatus() {
277
503
  console.log(` plan: ${license.plan || session.status.plan || session.auth.plan || "free"}`);
278
504
  }
279
505
 
280
- module.exports = { cmdAuthLogin, cmdAuthLogout, cmdRedeem, cmdAuthStatus, cmdActivateFree, cmdActivateStatus, resolveSessionState };
506
+ module.exports = {
507
+ cmdAuthLogin,
508
+ cmdAuthLogout,
509
+ cmdAuthMcp,
510
+ cmdRedeem,
511
+ cmdAuthStatus,
512
+ cmdActivateFree,
513
+ cmdActivateStatus,
514
+ resolveSessionState,
515
+ ensureAccountForActivation,
516
+ loginWithExchangeCode,
517
+ loginWithPastedAccessToken,
518
+ loginWithDeviceCode,
519
+ redeemActivationCode,
520
+ parseActivationArgs,
521
+ parseAuthLoginFlags,
522
+ printDeviceLoginInstructions,
523
+ };
@@ -0,0 +1,44 @@
1
+ "use strict";
2
+ /**
3
+ * `0dai boneyard` — weekly digest of worst agent moves with lessons.
4
+ *
5
+ * Wraps scripts/boneyard.py which aggregates the week's regressions,
6
+ * high-cost failures, and blunders from outcome_tracker + quality_scorer.
7
+ *
8
+ * Issue: #538 (Boneyard); follow-up #578 (wire-up).
9
+ */
10
+ const shared = require("../shared");
11
+ const { log, D, R, findRepoScript, spawnSync } = shared;
12
+
13
+ function _findArg(args, flag) {
14
+ const idx = args.indexOf(flag);
15
+ if (idx < 0 || !args[idx + 1]) return null;
16
+ return args[idx + 1];
17
+ }
18
+
19
+ function cmdBoneyard(target, args) {
20
+ const script = findRepoScript(target, "boneyard.py");
21
+ if (!script) {
22
+ log("boneyard unavailable — missing scripts/boneyard.py");
23
+ console.log(` ${D}Run from a 0dai-initialized project root.${R}`);
24
+ return;
25
+ }
26
+
27
+ const fwd = [script, "--target", target];
28
+
29
+ const week = _findArg(args, "--week");
30
+ if (week) fwd.push("--week", week);
31
+
32
+ const count = _findArg(args, "--count");
33
+ if (count) fwd.push("--count", count);
34
+
35
+ if (args.includes("--json")) fwd.push("--format", "json");
36
+ if (args.includes("--save")) fwd.push("--save");
37
+
38
+ const result = spawnSync("python3", fwd, { stdio: "inherit" });
39
+ if (typeof result.status === "number" && result.status !== 0) {
40
+ process.exit(result.status);
41
+ }
42
+ }
43
+
44
+ module.exports = { cmdBoneyard };