@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.
- package/README.md +98 -10
- package/bin/0dai.js +298 -60
- package/lib/commands/audit.js +13 -0
- package/lib/commands/auth.js +344 -98
- package/lib/commands/boneyard.js +44 -0
- package/lib/commands/ci.js +329 -0
- package/lib/commands/compliance.js +20 -0
- package/lib/commands/doctor.js +39 -1
- package/lib/commands/experience.js +5 -1
- package/lib/commands/feedback.js +92 -5
- package/lib/commands/gh.js +506 -0
- package/lib/commands/graph.js +78 -10
- package/lib/commands/heatmap.js +17 -0
- package/lib/commands/import_claude_code_agents.js +367 -0
- package/lib/commands/init.js +504 -28
- package/lib/commands/loop.js +108 -0
- package/lib/commands/mcp.js +410 -0
- package/lib/commands/models.js +27 -3
- package/lib/commands/paste.js +114 -0
- package/lib/commands/play.js +173 -0
- package/lib/commands/provider.js +69 -0
- package/lib/commands/quota.js +76 -0
- package/lib/commands/receipt.js +53 -0
- package/lib/commands/report.js +29 -2
- package/lib/commands/run.js +104 -7
- package/lib/commands/runner.js +527 -0
- package/lib/commands/session.js +1 -7
- package/lib/commands/standup.js +40 -0
- package/lib/commands/status.js +30 -1
- package/lib/commands/swarm.js +97 -4
- package/lib/commands/tui.js +81 -13
- package/lib/commands/upgrade.js +58 -0
- package/lib/commands/usage.js +87 -0
- package/lib/commands/vault.js +246 -0
- package/lib/onboarding.js +9 -3
- package/lib/shared.js +29 -14
- package/lib/utils/activation_telemetry.js +156 -0
- package/lib/utils/auth.js +1 -0
- package/lib/utils/canonical-counts.js +54 -0
- package/lib/utils/constants.js +7 -0
- package/lib/utils/diff-preview.js +192 -0
- package/lib/utils/identity.js +76 -18
- package/lib/utils/mcp-auth.js +607 -0
- package/lib/utils/plan.js +47 -2
- package/lib/utils/run_cost.js +91 -0
- package/lib/vault/cipher.js +125 -0
- package/lib/vault/identity.js +122 -0
- package/lib/vault/index.js +184 -0
- package/lib/vault/storage.js +84 -0
- package/lib/wizard.js +19 -12
- package/package.json +8 -4
- package/lib/tui/index.mjs +0 -34610
package/lib/commands/auth.js
CHANGED
|
@@ -3,11 +3,196 @@ const shared = require("../shared");
|
|
|
3
3
|
const {
|
|
4
4
|
T, R, D, log,
|
|
5
5
|
fs, os,
|
|
6
|
-
|
|
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
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
|
110
|
-
message: "Paste
|
|
111
|
-
placeholder: "
|
|
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 (
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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
|
-
|
|
219
|
-
|
|
424
|
+
try {
|
|
425
|
+
const { result, status } = await redeemActivationCode(code);
|
|
220
426
|
log(`${T}✓${R} ${result.message}`);
|
|
221
|
-
if (result.duration_days) {
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
log(`
|
|
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 = {
|
|
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 };
|