@0dai-dev/cli 4.1.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 +308 -60
- package/lib/commands/audit.js +13 -0
- package/lib/commands/auth.js +404 -122
- 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 +79 -14
- 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 +553 -53
- package/lib/commands/loop.js +108 -0
- package/lib/commands/mcp.js +410 -0
- package/lib/commands/models.js +42 -12
- package/lib/commands/paste.js +114 -0
- package/lib/commands/persona-simulate.js +19 -0
- package/lib/commands/play.js +173 -0
- package/lib/commands/provider.js +87 -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/ssh.js +416 -0
- package/lib/commands/standup.js +40 -0
- package/lib/commands/status.js +131 -36
- package/lib/commands/swarm.js +97 -4
- package/lib/commands/tui.js +117 -0
- package/lib/commands/usage.js +87 -0
- package/lib/commands/vault.js +246 -0
- package/lib/commands/workspace.js +1 -0
- package/lib/onboarding.js +30 -10
- package/lib/shared.js +153 -96
- package/lib/tui/index.mjs +34994 -0
- 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/model_ratings.js +77 -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 +13 -5
- package/scripts/build-tui.js +77 -0
package/lib/commands/auth.js
CHANGED
|
@@ -3,12 +3,243 @@ 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
|
|
|
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
|
+
|
|
193
|
+
async function resolveSessionState() {
|
|
194
|
+
const auth = loadAuthState();
|
|
195
|
+
const hasCachedToken = !!(auth && (auth.api_key || auth.access_token || auth.token));
|
|
196
|
+
if (!hasCachedToken) {
|
|
197
|
+
return {
|
|
198
|
+
ok: false,
|
|
199
|
+
degraded: false,
|
|
200
|
+
auth: null,
|
|
201
|
+
status: null,
|
|
202
|
+
message: "Not logged in. Run: 0dai auth login",
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const status = await fetchAuthStatus();
|
|
207
|
+
if (!status || status.error || !status.email) {
|
|
208
|
+
const email = auth.email || auth.user || "unknown";
|
|
209
|
+
return {
|
|
210
|
+
ok: false,
|
|
211
|
+
degraded: true,
|
|
212
|
+
auth,
|
|
213
|
+
status,
|
|
214
|
+
message: `Saved session for ${email}, but cloud validation failed. Run: 0dai auth login`,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return {
|
|
219
|
+
ok: true,
|
|
220
|
+
degraded: false,
|
|
221
|
+
auth,
|
|
222
|
+
status,
|
|
223
|
+
message: "",
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
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
|
+
|
|
12
243
|
const isTTY = process.stdout.isTTY && process.stdin.isTTY;
|
|
13
244
|
|
|
14
245
|
// Check if already authenticated
|
|
@@ -21,13 +252,15 @@ async function cmdAuthLogin() {
|
|
|
21
252
|
p.log.success(`Already logged in as ${T}${existing.email || "unknown"}${R} (${existing.plan || "free"} plan)`);
|
|
22
253
|
const reauth = await p.confirm({ message: "Sign in with a different account?" });
|
|
23
254
|
if (p.isCancel(reauth) || !reauth) {
|
|
255
|
+
if (flags.mcp) writeMcpTokenForCurrentAuth("0dai-account-token", p.log.success);
|
|
24
256
|
p.outro("Current session kept");
|
|
25
|
-
return;
|
|
257
|
+
return existing;
|
|
26
258
|
}
|
|
27
259
|
} else {
|
|
260
|
+
if (flags.mcp) writeMcpTokenForCurrentAuth("0dai-account-token");
|
|
28
261
|
log(`Already logged in as ${existing.email || "unknown"} (${existing.plan || "free"} plan)`);
|
|
29
262
|
log("To switch accounts, delete ~/.0dai/auth.json and run again");
|
|
30
|
-
return;
|
|
263
|
+
return existing;
|
|
31
264
|
}
|
|
32
265
|
}
|
|
33
266
|
} catch {}
|
|
@@ -44,15 +277,20 @@ async function cmdAuthLogin() {
|
|
|
44
277
|
"Why sign in?"
|
|
45
278
|
);
|
|
46
279
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
+
}
|
|
56
294
|
|
|
57
295
|
if (method === "github" || method === "google") {
|
|
58
296
|
const url = `${API_URL}/v1/auth/${method}?cli=true`;
|
|
@@ -69,97 +307,71 @@ async function cmdAuthLogin() {
|
|
|
69
307
|
const s = p.spinner();
|
|
70
308
|
s.start("Waiting for browser confirmation...");
|
|
71
309
|
|
|
72
|
-
// Poll auth/status until we get a new token (check every 3s, 5min timeout)
|
|
73
|
-
// For now, ask user to paste token from success page
|
|
74
310
|
s.stop("Browser opened");
|
|
75
|
-
const
|
|
76
|
-
message: "Paste
|
|
77
|
-
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",
|
|
78
314
|
});
|
|
79
|
-
if (
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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);
|
|
96
332
|
}
|
|
97
|
-
return;
|
|
98
333
|
}
|
|
99
334
|
p.log.info("Skipped. You can also use device code flow:");
|
|
100
335
|
}
|
|
101
336
|
|
|
102
|
-
// Device code fallback
|
|
103
|
-
const result = await apiCall("/v1/auth/device", { client_id: "cli" });
|
|
104
|
-
if (result.error) { p.log.error(result.error); process.exit(1); }
|
|
105
|
-
|
|
106
|
-
p.log.step(`Open: ${result.verification_uri}`);
|
|
107
|
-
p.log.step(`Code: ${T}${result.user_code}${R}`);
|
|
108
|
-
|
|
109
337
|
const s = p.spinner();
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
access_token: poll.access_token, email: poll.email,
|
|
122
|
-
plan: poll.plan || "free", authenticated_at: new Date().toISOString(),
|
|
123
|
-
expires_at: poll.expires_at,
|
|
124
|
-
}, null, 2) + "\n", { mode: 0o600 });
|
|
125
|
-
p.outro(`${T}Logged in${R} as ${poll.email} (${poll.plan} plan)`);
|
|
126
|
-
return;
|
|
127
|
-
}
|
|
128
|
-
if (poll.error && poll.error !== "authorization_pending") {
|
|
129
|
-
s.stop("Failed");
|
|
130
|
-
p.log.error(poll.error);
|
|
131
|
-
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}`);
|
|
132
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);
|
|
133
355
|
}
|
|
134
|
-
s.stop("Timed out");
|
|
135
|
-
p.log.error("Try again.");
|
|
136
|
-
process.exit(1);
|
|
137
356
|
|
|
138
357
|
} else {
|
|
139
358
|
// Non-interactive: device code only
|
|
140
359
|
log("0dai auth is separate from agent CLIs. It tracks projects, limits, and team features.");
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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
|
-
fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
|
|
153
|
-
fs.writeFileSync(AUTH_FILE, JSON.stringify({
|
|
154
|
-
access_token: poll.access_token, email: poll.email,
|
|
155
|
-
plan: poll.plan || "free", authenticated_at: new Date().toISOString(),
|
|
156
|
-
}, null, 2) + "\n", { mode: 0o600 });
|
|
157
|
-
log(`Logged in as ${poll.email}`);
|
|
158
|
-
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}`);
|
|
159
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);
|
|
160
374
|
}
|
|
161
|
-
log("Timed out");
|
|
162
|
-
process.exit(1);
|
|
163
375
|
}
|
|
164
376
|
}
|
|
165
377
|
|
|
@@ -168,6 +380,31 @@ function cmdAuthLogout() {
|
|
|
168
380
|
log("Logged out");
|
|
169
381
|
}
|
|
170
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
|
+
|
|
171
408
|
async function cmdRedeem(code) {
|
|
172
409
|
if (!code) {
|
|
173
410
|
console.log("Usage: 0dai redeem <CODE>");
|
|
@@ -181,41 +418,63 @@ async function cmdRedeem(code) {
|
|
|
181
418
|
process.exit(1);
|
|
182
419
|
}
|
|
183
420
|
log(`Redeeming code ${T}${code.toUpperCase()}${R}...`);
|
|
184
|
-
|
|
185
|
-
|
|
421
|
+
try {
|
|
422
|
+
const { result, status } = await redeemActivationCode(code);
|
|
186
423
|
log(`${T}✓${R} ${result.message}`);
|
|
187
|
-
if (result.duration_days) {
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
log(`
|
|
191
|
-
} else {
|
|
192
|
-
log(`error: ${result.error || "unknown"}`);
|
|
193
|
-
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}`);
|
|
194
428
|
process.exit(1);
|
|
195
429
|
}
|
|
196
430
|
}
|
|
197
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
|
+
|
|
198
458
|
async function cmdAuthStatus() {
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
}
|
|
218
|
-
log("Not logged in. Run: 0dai auth login");
|
|
459
|
+
const session = await resolveSessionState();
|
|
460
|
+
if (!session.ok) {
|
|
461
|
+
log(session.message);
|
|
462
|
+
process.exitCode = 1;
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
const { auth, status } = session;
|
|
467
|
+
const email = status.email || auth.email || auth.user || "unknown";
|
|
468
|
+
log(`${email} (${status.plan || auth.plan || "free"} plan)`);
|
|
469
|
+
if (status.usage_today) {
|
|
470
|
+
console.log(" Usage today:");
|
|
471
|
+
for (const [k, v] of Object.entries(status.usage_today))
|
|
472
|
+
console.log(` ${k}: ${v} / ${status.limits[k]}`);
|
|
473
|
+
}
|
|
474
|
+
const license = status.license || auth.license || { status: "inactive" };
|
|
475
|
+
console.log(` Activation: ${license.status || "inactive"}${license.activation_id ? ` (${license.activation_id})` : ""}`);
|
|
476
|
+
if (status.projects && status.projects.length) {
|
|
477
|
+
console.log(` Projects bound: ${status.projects.length} / ${status.project_limit || "?"}`);
|
|
219
478
|
}
|
|
220
479
|
}
|
|
221
480
|
|
|
@@ -229,13 +488,36 @@ async function cmdActivateFree() {
|
|
|
229
488
|
}
|
|
230
489
|
|
|
231
490
|
async function cmdActivateStatus() {
|
|
232
|
-
const
|
|
233
|
-
|
|
234
|
-
|
|
491
|
+
const session = await resolveSessionState();
|
|
492
|
+
if (!session.ok) {
|
|
493
|
+
log(session.message);
|
|
494
|
+
process.exitCode = 1;
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
const licenseResult = await apiCall("/v1/licenses/status");
|
|
499
|
+
const license = licenseResult.license || session.status.license || session.auth.license || { status: "inactive" };
|
|
235
500
|
updateAuthState({ license });
|
|
236
501
|
log(`license ${license.status || "inactive"}`);
|
|
237
502
|
if (license.activation_id) console.log(` activation id: ${license.activation_id}`);
|
|
238
|
-
console.log(` plan: ${license.plan || status.plan || "free"}`);
|
|
503
|
+
console.log(` plan: ${license.plan || session.status.plan || session.auth.plan || "free"}`);
|
|
239
504
|
}
|
|
240
505
|
|
|
241
|
-
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 };
|