@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.
- package/README.md +30 -5
- package/bin/0dai.js +289 -60
- package/lib/commands/audit.js +13 -0
- package/lib/commands/auth.js +341 -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 +20 -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 +440 -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 +44 -4
- 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 +26 -1
- package/lib/commands/swarm.js +97 -4
- package/lib/commands/tui.js +81 -13
- 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/tui/index.mjs +571 -187
- package/lib/utils/auth.js +1 -0
- package/lib/utils/canonical-counts.js +54 -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 +37 -2
- 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 +2 -2
package/lib/commands/auth.js
CHANGED
|
@@ -3,11 +3,193 @@ 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
|
+
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
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
|
110
|
-
message: "Paste
|
|
111
|
-
placeholder: "
|
|
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 (
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
-
|
|
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);
|
|
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
|
-
|
|
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;
|
|
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
|
-
|
|
219
|
-
|
|
421
|
+
try {
|
|
422
|
+
const { result, status } = await redeemActivationCode(code);
|
|
220
423
|
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}`);
|
|
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 = {
|
|
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 };
|