@0dai-dev/cli 4.2.0 → 4.3.5

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 (52) hide show
  1. package/README.md +98 -10
  2. package/bin/0dai.js +298 -60
  3. package/lib/commands/audit.js +13 -0
  4. package/lib/commands/auth.js +344 -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 +39 -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 +504 -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 +104 -7
  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 +30 -1
  30. package/lib/commands/swarm.js +97 -4
  31. package/lib/commands/tui.js +81 -13
  32. package/lib/commands/upgrade.js +58 -0
  33. package/lib/commands/usage.js +87 -0
  34. package/lib/commands/vault.js +246 -0
  35. package/lib/onboarding.js +9 -3
  36. package/lib/shared.js +29 -14
  37. package/lib/utils/activation_telemetry.js +156 -0
  38. package/lib/utils/auth.js +1 -0
  39. package/lib/utils/canonical-counts.js +54 -0
  40. package/lib/utils/constants.js +7 -0
  41. package/lib/utils/diff-preview.js +192 -0
  42. package/lib/utils/identity.js +76 -18
  43. package/lib/utils/mcp-auth.js +607 -0
  44. package/lib/utils/plan.js +47 -2
  45. package/lib/utils/run_cost.js +91 -0
  46. package/lib/vault/cipher.js +125 -0
  47. package/lib/vault/identity.js +122 -0
  48. package/lib/vault/index.js +184 -0
  49. package/lib/vault/storage.js +84 -0
  50. package/lib/wizard.js +19 -12
  51. package/package.json +8 -4
  52. package/lib/tui/index.mjs +0 -34610
@@ -3,11 +3,196 @@ 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
+ const status = await fetchAuthStatus(exchanged.access_token);
137
+ if (!status || status.error || !status.email) {
138
+ throw new Error(status && status.error ? status.error : "cloud validation failed after auth exchange");
139
+ }
140
+ saveAuthState({
141
+ access_token: exchanged.access_token,
142
+ email: status.email,
143
+ plan: status.plan || "free",
144
+ name: status.name || exchanged.name || "",
145
+ license: status.license || {},
146
+ plan_expires_at: status.plan_expires_at || "",
147
+ authenticated_at: new Date().toISOString(),
148
+ });
149
+ return status;
150
+ }
151
+
152
+ async function loginWithPastedAccessToken(token) {
153
+ const clean = String(token || "").trim();
154
+ if (!clean) throw new Error("access token required");
155
+ const status = await fetchAuthStatus(clean);
156
+ if (!status || status.error || !status.email) {
157
+ throw new Error(status && status.error ? status.error : "cloud validation failed");
158
+ }
159
+ saveAuthState({
160
+ access_token: clean,
161
+ email: status.email,
162
+ plan: status.plan || "free",
163
+ name: status.name || "",
164
+ license: status.license || {},
165
+ plan_expires_at: status.plan_expires_at || "",
166
+ authenticated_at: new Date().toISOString(),
167
+ });
168
+ return status;
169
+ }
170
+
171
+ async function redeemActivationCode(code) {
172
+ const clean = String(code || "").trim().toUpperCase();
173
+ if (!clean) throw new Error("activation code required");
174
+ const result = await apiCall("/v1/redeem", { code: clean });
175
+ if (!result || !result.ok) {
176
+ throw new Error(result && result.error ? result.error : "activation code redeem failed");
177
+ }
178
+ const status = await fetchAuthStatus();
179
+ if (!status || status.error || !status.email) {
180
+ throw new Error(status && status.error ? status.error : "cloud validation failed after activation");
181
+ }
182
+ return { result, status };
183
+ }
184
+
185
+ async function promptOptionalActivationCode(p) {
186
+ if (!(process.stdout.isTTY && process.stdin.isTTY)) return "";
187
+ if (!p || typeof p.text !== "function") return "";
188
+ const value = await p.text({
189
+ message: "Activation / Team code (optional)",
190
+ placeholder: "TEAM-... or press Enter",
191
+ });
192
+ if (p.isCancel(value)) return "";
193
+ return String(value || "").trim();
194
+ }
195
+
11
196
  async function resolveSessionState() {
12
197
  const auth = loadAuthState();
13
198
  const hasCachedToken = !!(auth && (auth.api_key || auth.access_token || auth.token));
@@ -42,7 +227,22 @@ async function resolveSessionState() {
42
227
  };
43
228
  }
44
229
 
45
- async function cmdAuthLogin() {
230
+ async function cmdAuthLogin(args = []) {
231
+ const rawArgs = Array.isArray(args) ? args : [];
232
+ const flags = parseAuthLoginFlags(rawArgs);
233
+ const parsed = parseActivationArgs(rawArgs, { genericCode: "auth" });
234
+ if (parsed.authCode) {
235
+ try {
236
+ const status = await loginWithExchangeCode(parsed.authCode);
237
+ if (flags.mcp) writeMcpTokenForCurrentAuth("0dai-browser-oauth");
238
+ log(`Logged in as ${status.email} (${status.plan || "free"} plan)`);
239
+ return status;
240
+ } catch (err) {
241
+ log(`auth failed: ${err.message || err}`);
242
+ process.exit(1);
243
+ }
244
+ }
245
+
46
246
  const isTTY = process.stdout.isTTY && process.stdin.isTTY;
47
247
 
48
248
  // Check if already authenticated
@@ -55,13 +255,15 @@ async function cmdAuthLogin() {
55
255
  p.log.success(`Already logged in as ${T}${existing.email || "unknown"}${R} (${existing.plan || "free"} plan)`);
56
256
  const reauth = await p.confirm({ message: "Sign in with a different account?" });
57
257
  if (p.isCancel(reauth) || !reauth) {
258
+ if (flags.mcp) writeMcpTokenForCurrentAuth("0dai-account-token", p.log.success);
58
259
  p.outro("Current session kept");
59
- return;
260
+ return existing;
60
261
  }
61
262
  } else {
263
+ if (flags.mcp) writeMcpTokenForCurrentAuth("0dai-account-token");
62
264
  log(`Already logged in as ${existing.email || "unknown"} (${existing.plan || "free"} plan)`);
63
265
  log("To switch accounts, delete ~/.0dai/auth.json and run again");
64
- return;
266
+ return existing;
65
267
  }
66
268
  }
67
269
  } catch {}
@@ -78,15 +280,20 @@ async function cmdAuthLogin() {
78
280
  "Why sign in?"
79
281
  );
80
282
 
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); }
283
+ let method = "device";
284
+ if (!(flags.device || flags.noBrowser)) {
285
+ method = await p.select({
286
+ message: "How would you like to sign in?",
287
+ options: [
288
+ { value: "github", label: "GitHub", hint: "recommended" },
289
+ { value: "google", label: "Google" },
290
+ { value: "device", label: "Device code", hint: "no browser needed" },
291
+ ],
292
+ });
293
+ if (p.isCancel(method)) { p.cancel("Cancelled"); process.exit(0); }
294
+ } else {
295
+ p.log.info("Using device code flow (--device/--no-browser).");
296
+ }
90
297
 
91
298
  if (method === "github" || method === "google") {
92
299
  const url = `${API_URL}/v1/auth/${method}?cli=true`;
@@ -103,97 +310,71 @@ async function cmdAuthLogin() {
103
310
  const s = p.spinner();
104
311
  s.start("Waiting for browser confirmation...");
105
312
 
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
313
  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_...",
314
+ const code = await p.text({
315
+ message: "Paste the code from the success page (or press Enter for device code):",
316
+ placeholder: "code from browser success page",
112
317
  });
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}`);
318
+ if (code && !p.isCancel(code)) {
319
+ const pasted = String(code).trim();
320
+ try {
321
+ if (pasted.startsWith("0dai_at_")) {
322
+ const status = await loginWithPastedAccessToken(pasted);
323
+ if (flags.mcp) writeMcpTokenForCurrentAuth("0dai-pasted-token", p.log.success);
324
+ p.outro(`${T}Logged in${R} as ${status.email} (${status.plan || "free"} plan)`);
325
+ return status;
326
+ } else {
327
+ const status = await loginWithExchangeCode(pasted);
328
+ if (flags.mcp) writeMcpTokenForCurrentAuth("0dai-browser-oauth", p.log.success);
329
+ p.outro(`${T}Logged in${R} as ${status.email} (${status.plan || "free"} plan)`);
330
+ return status;
331
+ }
332
+ } catch (err) {
333
+ p.log.error(err.message || String(err));
334
+ if (pasted.startsWith("0dai_at_")) process.exit(1);
130
335
  }
131
- return;
132
336
  }
133
337
  p.log.info("Skipped. You can also use device code flow:");
134
338
  }
135
339
 
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
340
  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);
341
+ try {
342
+ const poll = await loginWithDeviceCode({
343
+ writeLine: (message) => p.log.step(message),
344
+ spinner: s,
345
+ });
346
+ if (flags.mcp) {
347
+ const mcp = writeMcpTokenForAuth(
348
+ { access_token: poll.access_token, email: poll.email, plan: poll.plan },
349
+ "0dai-device-code"
350
+ );
351
+ p.log.success(`MCP auth token stored: ${mcp.tokenPath}`);
166
352
  }
353
+ p.outro(`${T}Logged in${R} as ${poll.email} (${poll.plan || "free"} plan)`);
354
+ return poll;
355
+ } catch (err) {
356
+ p.log.error(err.message || String(err));
357
+ process.exit(1);
167
358
  }
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
359
 
172
360
  } else {
173
361
  // Non-interactive: device code only
174
362
  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;
363
+ try {
364
+ const poll = await loginWithDeviceCode({ writeLine: log });
365
+ if (flags.mcp) {
366
+ const mcp = writeMcpTokenForAuth(
367
+ { access_token: poll.access_token, email: poll.email, plan: poll.plan },
368
+ "0dai-device-code"
369
+ );
370
+ log(`MCP auth token stored: ${mcp.tokenPath}`);
193
371
  }
372
+ log(`Logged in as ${poll.email} (${poll.plan || "free"} plan)`);
373
+ return poll;
374
+ } catch (err) {
375
+ log(`error: ${err.message || err}`);
376
+ process.exit(1);
194
377
  }
195
- log("Device code expired after 10 minutes. Run '0dai auth login' again.");
196
- process.exit(1);
197
378
  }
198
379
  }
199
380
 
@@ -202,6 +383,31 @@ function cmdAuthLogout() {
202
383
  log("Logged out");
203
384
  }
204
385
 
386
+ async function cmdAuthMcp(args = []) {
387
+ const rawArgs = Array.isArray(args) ? args : [];
388
+ const flags = parseAuthLoginFlags(rawArgs);
389
+ let auth = loadAuthState();
390
+
391
+ if (flags.device || !_authAccessToken(auth)) {
392
+ log("Starting device code flow for MCP auth.");
393
+ const poll = await loginWithDeviceCode({ writeLine: log });
394
+ auth = {
395
+ access_token: poll.access_token,
396
+ email: poll.email,
397
+ plan: poll.plan || "free",
398
+ };
399
+ const mcp = writeMcpTokenForAuth(auth, "0dai-device-code");
400
+ log(`MCP auth token stored: ${mcp.tokenPath}`);
401
+ log("For bearer-env MCP clients, launch them with OPERATOR_MCP_TOKEN set from the saved 0dai auth token.");
402
+ return poll;
403
+ }
404
+
405
+ const mcp = writeMcpTokenForAuth(auth, "0dai-account-token");
406
+ log(`MCP auth token stored: ${mcp.tokenPath}`);
407
+ log(`Account: ${auth.email || "unknown"} (${auth.plan || "free"} plan)`);
408
+ return { email: auth.email || "", plan: auth.plan || "free" };
409
+ }
410
+
205
411
  async function cmdRedeem(code) {
206
412
  if (!code) {
207
413
  console.log("Usage: 0dai redeem <CODE>");
@@ -215,20 +421,43 @@ async function cmdRedeem(code) {
215
421
  process.exit(1);
216
422
  }
217
423
  log(`Redeeming code ${T}${code.toUpperCase()}${R}...`);
218
- const result = await apiCall("/v1/redeem", { code: code.toUpperCase().trim() });
219
- if (result.ok) {
424
+ try {
425
+ const { result, status } = await redeemActivationCode(code);
220
426
  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}`);
427
+ if (result.duration_days) log(` Plan active for ${result.duration_days} days`);
428
+ log(` Account: ${status.email} · plan: ${status.plan || "free"}`);
429
+ } catch (err) {
430
+ log(`error: ${err.message || err}`);
228
431
  process.exit(1);
229
432
  }
230
433
  }
231
434
 
435
+ async function ensureAccountForActivation(actionLabel, args = []) {
436
+ const parsed = parseActivationArgs(args, { genericCode: "activation" });
437
+ if (parsed.authCode) {
438
+ log("using auth code from command line");
439
+ await loginWithExchangeCode(parsed.authCode);
440
+ }
441
+
442
+ const ensureAuthenticated = makeEnsureAuthenticated(cmdAuthLogin);
443
+ let status = await ensureAuthenticated(actionLabel);
444
+
445
+ let activationCode = parsed.activationCode;
446
+ let prompts = null;
447
+ if (!activationCode && process.stdout.isTTY && process.stdin.isTTY) {
448
+ try { prompts = require("@clack/prompts"); } catch {}
449
+ activationCode = await promptOptionalActivationCode(prompts);
450
+ }
451
+ if (activationCode) {
452
+ log(`redeeming activation code ${T}${activationCode.toUpperCase()}${R}`);
453
+ const redeemed = await redeemActivationCode(activationCode);
454
+ status = redeemed.status;
455
+ log(`plan active: ${status.plan || "free"}`);
456
+ }
457
+
458
+ return status;
459
+ }
460
+
232
461
  async function cmdAuthStatus() {
233
462
  const session = await resolveSessionState();
234
463
  if (!session.ok) {
@@ -277,4 +506,21 @@ async function cmdActivateStatus() {
277
506
  console.log(` plan: ${license.plan || session.status.plan || session.auth.plan || "free"}`);
278
507
  }
279
508
 
280
- module.exports = { cmdAuthLogin, cmdAuthLogout, cmdRedeem, cmdAuthStatus, cmdActivateFree, cmdActivateStatus, resolveSessionState };
509
+ module.exports = {
510
+ cmdAuthLogin,
511
+ cmdAuthLogout,
512
+ cmdAuthMcp,
513
+ cmdRedeem,
514
+ cmdAuthStatus,
515
+ cmdActivateFree,
516
+ cmdActivateStatus,
517
+ resolveSessionState,
518
+ ensureAccountForActivation,
519
+ loginWithExchangeCode,
520
+ loginWithPastedAccessToken,
521
+ loginWithDeviceCode,
522
+ redeemActivationCode,
523
+ parseActivationArgs,
524
+ parseAuthLoginFlags,
525
+ printDeviceLoginInstructions,
526
+ };
@@ -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 };