@runtypelabs/cli 2.59.1 → 2.59.2

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 (84) hide show
  1. package/dist/chunk-0-T9NWjH.js +3899 -0
  2. package/dist/chunk-0x7UKC1U.js +17 -0
  3. package/dist/chunk-2ySxXtcg.js +418 -0
  4. package/dist/chunk-3An_BRHv.js +501 -0
  5. package/dist/chunk-6uTxgdiY.js +16218 -0
  6. package/dist/chunk-87nRnNRG.js +1363 -0
  7. package/dist/chunk-B3wo8uRx.js +322 -0
  8. package/dist/chunk-BGp3ZdBy.js +560 -0
  9. package/dist/chunk-BJQv9Elp.js +107 -0
  10. package/dist/chunk-BOpO6xE-.js +6053 -0
  11. package/dist/chunk-BQXXCL8B.js +102 -0
  12. package/dist/chunk-BV-8isOd.js +34 -0
  13. package/dist/chunk-BVMhjGVe.js +826 -0
  14. package/dist/chunk-BW2fg46T.js +141 -0
  15. package/dist/chunk-BWJdY5Ag.js +36 -0
  16. package/dist/chunk-BYJOrLhh.js +302 -0
  17. package/dist/chunk-BYab8EVL.js +127 -0
  18. package/dist/chunk-BfEKvp7h.js +82 -0
  19. package/dist/chunk-BhgLSfH6.js +1123 -0
  20. package/dist/chunk-BirUxTtH.js +296 -0
  21. package/dist/chunk-BmB6UVZ9.js +1559 -0
  22. package/dist/chunk-BnWhIqW1.js +109 -0
  23. package/dist/chunk-Bpfnfk0t.js +123 -0
  24. package/dist/chunk-BrhgdnM3.js +108 -0
  25. package/dist/chunk-Bt0XAgX5.js +327 -0
  26. package/dist/chunk-Bu0FJr6S.js +84 -0
  27. package/dist/chunk-BvEoKpN3.js +255 -0
  28. package/dist/chunk-BxNJgYbE.js +28 -0
  29. package/dist/chunk-C1OFhd7F.js +6131 -0
  30. package/dist/chunk-C7e7WwbU.js +331 -0
  31. package/dist/chunk-CDTYP6bN.js +558 -0
  32. package/dist/chunk-CGDQoqDC.js +24 -0
  33. package/dist/chunk-CJraEBVd.js +127 -0
  34. package/dist/chunk-CQrNhOtv.js +40 -0
  35. package/dist/chunk-CW6AzBH6.js +128 -0
  36. package/dist/chunk-CXgL4w-9.js +317 -0
  37. package/dist/chunk-CZrqoPyf.js +233 -0
  38. package/dist/chunk-CcFCyqbB.js +83 -0
  39. package/dist/chunk-CdaIvO-y.js +485 -0
  40. package/dist/chunk-CehSXtgK.js +624 -0
  41. package/dist/chunk-Ciqkop65.js +218 -0
  42. package/dist/chunk-CoAWdSiE.js +43 -0
  43. package/dist/chunk-Crb4y7t4.js +830 -0
  44. package/dist/chunk-CrlqIfmb.js +337 -0
  45. package/dist/chunk-Cs4pwCQ-.js +324 -0
  46. package/dist/chunk-CwM_9y01.js +519 -0
  47. package/dist/chunk-CyRbIJg8.js +440 -0
  48. package/dist/chunk-D0OHsyMy.js +212 -0
  49. package/dist/chunk-D3y2bURs.js +1347 -0
  50. package/dist/chunk-D4_mJk03.js +2338 -0
  51. package/dist/chunk-D6C8rned.js +578 -0
  52. package/dist/chunk-D9ZbUnks.js +11 -0
  53. package/dist/chunk-DEPmLshX.js +834 -0
  54. package/dist/chunk-DFj1UPB2.js +330 -0
  55. package/dist/chunk-DGdFdZVf.js +197 -0
  56. package/dist/chunk-DGv7X9S_.js +63 -0
  57. package/dist/chunk-DJQ47WYZ.js +664 -0
  58. package/dist/chunk-DJw3O61j.js +1974 -0
  59. package/dist/chunk-DK4FbWVK.js +100 -0
  60. package/dist/chunk-DSRYhUus.js +993 -0
  61. package/dist/chunk-DajZ9Eg4.js +299 -0
  62. package/dist/chunk-Df4dunvU.js +195 -0
  63. package/dist/chunk-DjNTPhfO.js +481 -0
  64. package/dist/chunk-Dn8eaJuu.js +1042 -0
  65. package/dist/chunk-DwDWqSxM.js +637 -0
  66. package/dist/chunk-Dz7cEtxl.js +84 -0
  67. package/dist/chunk-E_0bgjrS.js +359 -0
  68. package/dist/chunk-KPYN2vUp.js +776 -0
  69. package/dist/chunk-KcqPQJBi.js +86 -0
  70. package/dist/chunk-N9H0JnoG.js +349 -0
  71. package/dist/chunk-NONFcXkn.js +728 -0
  72. package/dist/chunk-PrFjcpmH.js +256 -0
  73. package/dist/chunk-QSfNYiw9.js +11 -0
  74. package/dist/chunk-R265bzj9.js +226 -0
  75. package/dist/chunk-X9V_r6jM.js +166 -0
  76. package/dist/chunk-ZBtPUtyA.js +55 -0
  77. package/dist/chunk-ZCTfmOs7.js +4939 -0
  78. package/dist/chunk-h6oavj0t.js +97 -0
  79. package/dist/chunk-i7DwkvvD.js +42 -0
  80. package/dist/chunk-mG-4eZ6S.js +203 -0
  81. package/dist/chunk-o0ygUbCf.js +313 -0
  82. package/dist/chunk-ucu5lrDZ.js +5715 -0
  83. package/dist/index.js +11367 -73789
  84. package/package.json +4 -4
@@ -0,0 +1,834 @@
1
+ #!/usr/bin/env node
2
+ import { b as CredentialStore, c as loadEffectiveContext, i as assertExpectedApiUrl, l as normalizeApiUrl, n as CliContextError, s as getInvocationOptions, t as isTTY } from "./index.js";
3
+ import { EntityCard, MutationResult, React, loadUi, mutationResultProps, render, useEffect, useMutationState, useState } from "./chunk-CoAWdSiE.js";
4
+ import { i as getApiUrl, n as ApiKeyValidationUnavailableError, r as createCliClient } from "./chunk-BW2fg46T.js";
5
+ import { n as browserLogin, r as orgMismatchWarning, t as apiKeyLogin } from "./chunk-BnWhIqW1.js";
6
+ import { n as ensureAuth, r as getAuthenticatedUser, t as authRequiredMessage } from "./chunk-CJraEBVd.js";
7
+ import { a as printJson } from "./chunk-CcFCyqbB.js";
8
+ import { Command } from "commander";
9
+ import chalk from "chalk";
10
+ // src/auth/agent-auth-client.ts
11
+ /**
12
+ * Protocol error with the spec error code and an agent-actionable hint.
13
+ * `hint` is safe to surface verbatim in JSON output as the next step.
14
+ */
15
+ var AgentAuthError = class extends Error {
16
+ code;
17
+ status;
18
+ hint;
19
+ constructor(code, status, message, hint) {
20
+ super(message);
21
+ this.code = code;
22
+ this.status = status;
23
+ this.hint = hint;
24
+ this.name = "AgentAuthError";
25
+ }
26
+ };
27
+ const ERROR_HINTS = {
28
+ email_already_registered: "This email already has a Runtype account, so it cannot claim a new agent account. Ask the user to sign in at the dashboard, create an API key (Settings > API keys), and give it to you. Then run: runtype auth login --api-key <key> --no-tty",
29
+ invalid_claim_token: "The claim token is invalid or expired. Run: runtype auth register --force",
30
+ claim_expired: "The claim window expired. Run: runtype auth register --force",
31
+ credential_expired: "The temporary credential expired. Run: runtype auth register --force",
32
+ claimed_or_in_flight: "A claim is already in progress. Ask the user for the 6-digit code from their email, then run: runtype auth verify <code>. If the code expired, re-run: runtype auth claim <email>",
33
+ otp_invalid: "That code is not correct. Ask the user to re-check the 6-digit code from the most recent Runtype email and run: runtype auth verify <code>",
34
+ otp_expired: "The code expired. Re-send it with: runtype auth claim <email>",
35
+ previously_claimed: "This registration was already claimed. Run: runtype auth status to inspect stored credentials.",
36
+ email_send_failed: "The verification email could not be sent. Retry: runtype auth claim <email>",
37
+ rate_limited: "Too many attempts. Wait a minute, then retry the same command.",
38
+ service_disabled: "Agent registration is temporarily disabled. Try again later."
39
+ };
40
+ async function postJson(apiUrl, endpoint, body) {
41
+ assertExpectedApiUrl(apiUrl);
42
+ if (getInvocationOptions().expectOrg !== void 0) throw new CliContextError("--expect-org is not supported for anonymous registration or claim/verify actions.");
43
+ const url = `${apiUrl}${endpoint}`;
44
+ let response;
45
+ try {
46
+ response = await fetch(url, {
47
+ method: "POST",
48
+ headers: {
49
+ "content-type": "application/json",
50
+ "user-agent": "runtype-cli"
51
+ },
52
+ body: JSON.stringify(body)
53
+ });
54
+ } catch (error) {
55
+ throw new AgentAuthError("network_error", 0, `Could not reach ${url}: ${error instanceof Error ? error.message : String(error)}`, "Check network access and the --api-url value, then retry the same command.");
56
+ }
57
+ let data = {};
58
+ try {
59
+ data = await response.json();
60
+ } catch {}
61
+ if (!response.ok) {
62
+ const code = typeof data.error === "string" ? data.error : `http_${response.status}`;
63
+ const message = typeof data.message === "string" ? data.message : `Request failed (${response.status})`;
64
+ throw new AgentAuthError(code, response.status, message, ERROR_HINTS[code]);
65
+ }
66
+ return {
67
+ status: response.status,
68
+ data
69
+ };
70
+ }
71
+ function requireString(data, key) {
72
+ const value = data[key];
73
+ if (typeof value !== "string" || value.length === 0) throw new AgentAuthError("invalid_response", 0, `Registration response is missing "${key}". The API may be an incompatible version.`);
74
+ return value;
75
+ }
76
+ function stringArray(value) {
77
+ return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
78
+ }
79
+ /** POST /agent/auth — anonymous registration returning a temporary credential + claim token. */
80
+ async function registerAnonymousAgent(apiUrl) {
81
+ const { data } = await postJson(apiUrl || getApiUrl(), "/agent/auth", {
82
+ type: "anonymous",
83
+ requested_credential_type: "api_key"
84
+ });
85
+ return {
86
+ registrationId: requireString(data, "registration_id"),
87
+ credential: requireString(data, "credential"),
88
+ credentialExpires: requireString(data, "credential_expires"),
89
+ scopes: stringArray(data.scopes),
90
+ claimToken: requireString(data, "claim_token"),
91
+ claimTokenExpires: requireString(data, "claim_token_expires"),
92
+ postClaimScopes: stringArray(data.post_claim_scopes)
93
+ };
94
+ }
95
+ /** POST /agent/auth/claim — email the user a 6-digit code that binds the account to them. */
96
+ async function initiateClaim(claimToken, email, apiUrl) {
97
+ const { data } = await postJson(apiUrl || getApiUrl(), "/agent/auth/claim", {
98
+ claim_token: claimToken,
99
+ email
100
+ });
101
+ return {
102
+ registrationId: requireString(data, "registration_id"),
103
+ status: typeof data.status === "string" ? data.status : "initiated",
104
+ otpExpiresAt: requireString(data, "expires_at")
105
+ };
106
+ }
107
+ /** POST /agent/auth/claim/complete — exchange the OTP for a fresh full-access credential. */
108
+ async function completeClaim(claimToken, otp, apiUrl) {
109
+ const { data } = await postJson(apiUrl || getApiUrl(), "/agent/auth/claim/complete", {
110
+ claim_token: claimToken,
111
+ otp
112
+ });
113
+ return {
114
+ registrationId: requireString(data, "registration_id"),
115
+ credential: requireString(data, "credential"),
116
+ scopes: stringArray(data.scopes)
117
+ };
118
+ }
119
+ // src/commands/auth-agent.ts
120
+ const VERIFY_NEXT = "Ask the user for the 6-digit code from their email, then run: runtype auth verify <code>";
121
+ function claimNext(email) {
122
+ return email ? `Send the verification email with: runtype auth claim ${email}` : "Ask the user for their email address, then run: runtype auth claim <email>";
123
+ }
124
+ function fail(payload) {
125
+ console.error(JSON.stringify(payload, null, 2));
126
+ process.exit(1);
127
+ }
128
+ function failFromError(error, fallbackNext) {
129
+ if (error instanceof AgentAuthError) fail({
130
+ status: "error",
131
+ code: error.code,
132
+ error: error.message,
133
+ ...error.hint || fallbackNext ? { next: error.hint || fallbackNext } : {}
134
+ });
135
+ fail({
136
+ status: "error",
137
+ error: error instanceof Error ? error.message : String(error),
138
+ ...fallbackNext ? { next: fallbackNext } : {}
139
+ });
140
+ }
141
+ async function fetchIdentity(apiKey, apiUrl) {
142
+ const profile = await createCliClient(apiKey, apiUrl).get("/users/profile");
143
+ return {
144
+ userId: typeof profile.id === "string" ? profile.id : "unknown",
145
+ orgId: typeof profile.orgId === "string" ? profile.orgId : void 0,
146
+ email: typeof profile.email === "string" ? profile.email : void 0
147
+ };
148
+ }
149
+ async function registerAction(options) {
150
+ const store = new CredentialStore();
151
+ const apiUrl = options.apiUrl || getApiUrl();
152
+ const existingClaim = await store.getClaimState();
153
+ const hasCredentials = await store.hasCredentials();
154
+ if (existingClaim && !options.force) {
155
+ const resumeDeadline = existingClaim.claimTokenExpires ?? existingClaim.credentialExpires;
156
+ if (!(resumeDeadline && new Date(resumeDeadline) < /* @__PURE__ */ new Date())) {
157
+ const payload = {
158
+ status: "registration_pending",
159
+ registrationId: existingClaim.registrationId,
160
+ credentialExpires: existingClaim.credentialExpires,
161
+ next: existingClaim.claimEmail ? VERIFY_NEXT : claimNext(options.email)
162
+ };
163
+ if (existingClaim.claimEmail) payload.claimEmail = existingClaim.claimEmail;
164
+ printJson(payload);
165
+ return;
166
+ }
167
+ }
168
+ if (hasCredentials && !existingClaim && !options.force) fail({
169
+ status: "error",
170
+ code: "already_authenticated",
171
+ error: "Credentials are already stored for a claimed account.",
172
+ next: "Run: runtype auth whoami --no-tty to confirm, or runtype auth logout first if the user wants a different account."
173
+ });
174
+ let registration;
175
+ try {
176
+ registration = await registerAnonymousAgent(apiUrl);
177
+ } catch (error) {
178
+ failFromError(error, "Retry: runtype auth register");
179
+ }
180
+ let identity = { userId: "unknown" };
181
+ try {
182
+ identity = await fetchIdentity(registration.credential, apiUrl);
183
+ } catch {}
184
+ const claimState = {
185
+ claimToken: registration.claimToken,
186
+ registrationId: registration.registrationId,
187
+ credentialExpires: registration.credentialExpires,
188
+ claimTokenExpires: registration.claimTokenExpires,
189
+ apiUrl
190
+ };
191
+ const registered = await store.saveCredentials({
192
+ apiKey: registration.credential,
193
+ userId: identity.userId,
194
+ orgId: identity.orgId,
195
+ apiUrl
196
+ }, claimState);
197
+ const base = {
198
+ status: "registered",
199
+ accountState: "temporary",
200
+ registrationId: registration.registrationId,
201
+ userId: identity.userId,
202
+ credentialExpires: registration.credentialExpires,
203
+ note: "A temporary account is active: CLI commands work now with limited models and quota. Verifying an email upgrades it to a full account and replaces the stored credential automatically."
204
+ };
205
+ if (options.email) try {
206
+ const claim = await initiateClaim(registration.claimToken, options.email, apiUrl);
207
+ await store.saveClaimState({
208
+ ...claimState,
209
+ claimEmail: options.email,
210
+ accountId: registered.id
211
+ });
212
+ printJson({
213
+ ...base,
214
+ status: "otp_sent",
215
+ claimEmail: options.email,
216
+ otpExpiresAt: claim.otpExpiresAt,
217
+ next: VERIFY_NEXT
218
+ });
219
+ return;
220
+ } catch (error) {
221
+ if (error instanceof AgentAuthError) fail({
222
+ ...base,
223
+ status: "registered_claim_failed",
224
+ code: error.code,
225
+ error: error.message,
226
+ next: error.hint || claimNext(options.email)
227
+ });
228
+ throw error;
229
+ }
230
+ printJson({
231
+ ...base,
232
+ next: claimNext()
233
+ });
234
+ }
235
+ async function claimAction(email, options) {
236
+ const store = new CredentialStore();
237
+ const claimState = await store.getClaimState();
238
+ const apiUrl = options.apiUrl || claimState?.apiUrl || (await store.getCredentials())?.apiUrl || getApiUrl();
239
+ if (!claimState) {
240
+ const hasCredentials = await store.hasCredentials();
241
+ fail({
242
+ status: "error",
243
+ code: "no_pending_registration",
244
+ error: hasCredentials ? "Stored credentials are already claimed; there is nothing to claim." : "No registration found.",
245
+ next: hasCredentials ? "Run: runtype auth whoami --no-tty" : `Run: runtype auth register --email ${email}`
246
+ });
247
+ }
248
+ try {
249
+ const claim = await initiateClaim(claimState.claimToken, email, apiUrl);
250
+ await store.saveClaimState({
251
+ ...claimState,
252
+ claimEmail: email
253
+ });
254
+ printJson({
255
+ status: "otp_sent",
256
+ registrationId: claim.registrationId,
257
+ claimEmail: email,
258
+ otpExpiresAt: claim.otpExpiresAt,
259
+ next: VERIFY_NEXT
260
+ });
261
+ } catch (error) {
262
+ failFromError(error, `Retry: runtype auth claim ${email}`);
263
+ }
264
+ }
265
+ async function verifyAction(code, options) {
266
+ const store = new CredentialStore();
267
+ const claimState = await store.getClaimState();
268
+ const apiUrl = options.apiUrl || claimState?.apiUrl || (await store.getCredentials())?.apiUrl || getApiUrl();
269
+ if (!claimState) fail({
270
+ status: "error",
271
+ code: "no_pending_registration",
272
+ error: "No claim in progress.",
273
+ next: "Run: runtype auth register --email <email> first."
274
+ });
275
+ if (!claimState.claimEmail) fail({
276
+ status: "error",
277
+ code: "claim_not_initiated",
278
+ error: "No verification email has been sent yet, so there is no code to verify.",
279
+ next: claimNext()
280
+ });
281
+ let completed;
282
+ try {
283
+ completed = await completeClaim(claimState.claimToken, code.trim(), apiUrl);
284
+ } catch (error) {
285
+ failFromError(error);
286
+ }
287
+ let identity = { userId: "unknown" };
288
+ try {
289
+ identity = await fetchIdentity(completed.credential, apiUrl);
290
+ } catch {}
291
+ const preClaimAccountId = claimState.accountId;
292
+ const wasDefault = (await store.getDefaultAccount())?.id === preClaimAccountId;
293
+ const saved = await store.saveCredentials({
294
+ apiKey: completed.credential,
295
+ userId: identity.userId,
296
+ orgId: identity.orgId,
297
+ apiUrl
298
+ }, void 0, { makeDefault: wasDefault });
299
+ if (preClaimAccountId && preClaimAccountId !== saved.id) await store.removeAccount(preClaimAccountId);
300
+ printJson({
301
+ status: "authenticated",
302
+ accountState: "full",
303
+ registrationId: completed.registrationId,
304
+ userId: identity.userId,
305
+ email: identity.email ?? claimState.claimEmail,
306
+ note: "The account is fully activated and the stored credential was rotated. All limits from the temporary account are removed.",
307
+ next: "Continue with setup: runtype install-mcp (then confirm with: runtype auth whoami --no-tty)"
308
+ });
309
+ }
310
+ function registerAgentAuthCommands(authCommand) {
311
+ authCommand.command("register").description("Create a Runtype account without a browser (agent signup, step 1 of 2)").option("--email <email>", "Also send the verification email in the same step").option("--force", "Register a new account even if one is stored or pending").option("--api-url <url>", "Custom API URL").option("--tty", "Force TTY mode").option("--no-tty", "Force non-TTY mode").action(registerAction);
312
+ authCommand.command("claim <email>").description("Email the user a 6-digit code to attach the account to them").option("--api-url <url>", "Custom API URL").option("--tty", "Force TTY mode").option("--no-tty", "Force non-TTY mode").action(claimAction);
313
+ authCommand.command("verify <code>").description("Complete signup with the 6-digit code from the verification email (step 2 of 2)").option("--api-url <url>", "Custom API URL").option("--tty", "Force TTY mode").option("--no-tty", "Force non-TTY mode").action(verifyAction);
314
+ }
315
+ // src/commands/auth.ts
316
+ const authCommand = new Command("auth").description("Manage authentication");
317
+ const NON_TTY_LOGIN_GUIDANCE = {
318
+ status: "error",
319
+ code: "tty_required",
320
+ error: "Browser login needs an interactive terminal, which this environment does not have.",
321
+ next: ["New account (no browser needed): ask the user for their email, then run: runtype auth register --email <email> — a 6-digit code is emailed to them; finish with: runtype auth verify <code>", "Existing account: ask the user for an API key from the dashboard (Settings > API keys), then run: runtype auth login --api-key <key>"]
322
+ };
323
+ const API_UNREACHABLE_NEXT = ["Confirm this environment can reach the Runtype API directly over HTTPS.", "If egress is restricted, allowlist api.runtype.com (staging: api.runtype-staging.com), then re-run: runtype auth login --api-key <key>"];
324
+ /**
325
+ * Forcing --tty from a non-terminal cannot make browser login work.
326
+ * The flag only switches output rendering; the OAuth callback still cannot complete here.
327
+ * Observed in the field: `auth login --tty` hangs until the harness kills it.
328
+ * Refuse up front with the same guidance instead of hanging.
329
+ */
330
+ function refuseForcedTtyLogin(options) {
331
+ if (options.tty === true && process.stdout.isTTY !== true) {
332
+ console.error(JSON.stringify({
333
+ ...NON_TTY_LOGIN_GUIDANCE,
334
+ error: "Browser login needs a real interactive terminal. Forcing --tty from a non-terminal only changes output formatting; the browser OAuth callback still cannot complete here, so the command would hang."
335
+ }, null, 2));
336
+ process.exit(1);
337
+ }
338
+ return false;
339
+ }
340
+ async function getBillingInfo(apiKey, apiUrl) {
341
+ try {
342
+ return await createCliClient(apiKey, apiUrl).get("/billing/status");
343
+ } catch {
344
+ return null;
345
+ }
346
+ }
347
+ /** Run one async step under the shared spinner/result chrome. */
348
+ async function renderTask(loadingLabel, task) {
349
+ await loadUi();
350
+ const App = () => {
351
+ const [label, setLabel] = useState(loadingLabel);
352
+ const [message, setMessage] = useState("");
353
+ const mutation = useMutationState();
354
+ useEffect(() => {
355
+ task(setLabel).then((value) => {
356
+ setMessage(value.message);
357
+ mutation.succeed(value.result);
358
+ }).catch((err) => mutation.fail(err));
359
+ }, []);
360
+ return React.createElement(MutationResult, {
361
+ ...mutationResultProps(mutation.state),
362
+ loadingLabel: label,
363
+ successMessage: message
364
+ });
365
+ };
366
+ const { waitUntilExit } = render(React.createElement(App));
367
+ await waitUntilExit();
368
+ }
369
+ function loginFields(login) {
370
+ return [
371
+ {
372
+ label: "Organization",
373
+ value: login.orgId ?? "Personal"
374
+ },
375
+ {
376
+ label: "User ID",
377
+ value: login.userId
378
+ },
379
+ {
380
+ label: "API URL",
381
+ value: login.apiUrl
382
+ },
383
+ {
384
+ label: "Default login",
385
+ value: login.isDefault ? "yes" : "no (run: runtype auth switch)"
386
+ }
387
+ ];
388
+ }
389
+ function loginJson(login) {
390
+ const warning = orgMismatchWarning(login);
391
+ return {
392
+ status: "authenticated",
393
+ userId: login.userId,
394
+ orgId: login.orgId ?? null,
395
+ apiUrl: login.apiUrl,
396
+ accountId: login.account.id,
397
+ isDefault: login.isDefault,
398
+ secretStore: login.account.secretStore,
399
+ ...warning ? { warning } : {}
400
+ };
401
+ }
402
+ function loginOutcome(login) {
403
+ return {
404
+ message: orgMismatchWarning(login) ?? `Logged in to ${login.orgId ?? "your personal account"}.`,
405
+ result: React.createElement(EntityCard, { fields: loginFields(login) })
406
+ };
407
+ }
408
+ authCommand.command("signup").description("Create a new Runtype account (alias for \"auth login\")").option("--api-url <url>", "Custom API URL").option("--tty", "Force TTY mode").option("--no-tty", "Force non-TTY mode").action(async (options) => {
409
+ await loadEffectiveContext(options.apiUrl);
410
+ refuseForcedTtyLogin(options);
411
+ if (!isTTY(options)) {
412
+ console.error(JSON.stringify(NON_TTY_LOGIN_GUIDANCE, null, 2));
413
+ process.exit(1);
414
+ }
415
+ await renderTask("Opening browser for authentication...", async (setLabel) => loginOutcome(await browserLogin({
416
+ ...options,
417
+ onProgress: setLabel
418
+ })));
419
+ });
420
+ function failApiKeyLogin(error) {
421
+ if (error instanceof ApiKeyValidationUnavailableError) {
422
+ console.error(JSON.stringify({
423
+ status: "error",
424
+ code: "api_unreachable",
425
+ error: error.message,
426
+ next: API_UNREACHABLE_NEXT
427
+ }, null, 2));
428
+ process.exit(1);
429
+ }
430
+ const message = error instanceof Error ? error.message : "Unknown error";
431
+ console.error(JSON.stringify({
432
+ status: "error",
433
+ error: message
434
+ }, null, 2));
435
+ process.exit(1);
436
+ }
437
+ async function loginAction(options) {
438
+ await loadEffectiveContext(options.apiUrl);
439
+ if (options.apiKey) {
440
+ const apiKey = options.apiKey;
441
+ if (!isTTY(options)) {
442
+ let login;
443
+ try {
444
+ login = await apiKeyLogin(apiKey, { apiUrl: options.apiUrl });
445
+ } catch (error) {
446
+ failApiKeyLogin(error);
447
+ return;
448
+ }
449
+ console.log(JSON.stringify({
450
+ ...loginJson(login),
451
+ next: "Continue with setup: runtype install-mcp (then confirm with: runtype auth whoami)"
452
+ }, null, 2));
453
+ return;
454
+ }
455
+ await renderTask("Validating API key...", async () => loginOutcome(await apiKeyLogin(apiKey, { apiUrl: options.apiUrl })));
456
+ return;
457
+ }
458
+ refuseForcedTtyLogin(options);
459
+ if (!isTTY(options)) {
460
+ console.error(JSON.stringify(NON_TTY_LOGIN_GUIDANCE, null, 2));
461
+ process.exit(1);
462
+ }
463
+ await renderTask("Initializing authentication...", async (setLabel) => loginOutcome(await browserLogin({
464
+ ...options,
465
+ onProgress: setLabel
466
+ })));
467
+ }
468
+ const LOGIN_DESCRIPTION = "Log in to an organization (adds a login; the target org comes from --org, RUNTYPE_ORG, or .runtype/project.json)";
469
+ authCommand.command("login").description(LOGIN_DESCRIPTION).option("--api-url <url>", "Custom API URL").option("--api-key <key>", "Use API key directly").option("--tty", "Force TTY mode").option("--no-tty", "Force non-TTY mode").action(loginAction);
470
+ const loginCommand = new Command("login").description(LOGIN_DESCRIPTION).option("--api-url <url>", "Custom API URL").option("--api-key <key>", "Use API key directly").option("--tty", "Force TTY mode").option("--no-tty", "Force non-TTY mode").action(loginAction);
471
+ function describeAccountSource(context) {
472
+ switch (context.accountSource) {
473
+ case "flag": return "--org flag";
474
+ case "environment": return "RUNTYPE_ORG";
475
+ case "project": return context.projectLink ? `project link (${context.projectLink.path})` : "project link";
476
+ default: return "default login";
477
+ }
478
+ }
479
+ authCommand.command("status").description("Show which login this directory uses").option("--tty", "Force TTY mode").option("--no-tty", "Force non-TTY mode").action(async (options) => {
480
+ const store = new CredentialStore();
481
+ if (!isTTY(options)) {
482
+ try {
483
+ const context = await loadEffectiveContext();
484
+ const storedLogins = (await store.listAccounts()).length;
485
+ const credentials = context.credentials;
486
+ if (!credentials) {
487
+ console.log(JSON.stringify({
488
+ status: "unauthenticated",
489
+ requestedOrgId: context.requestedOrgId ?? null,
490
+ accountSource: context.accountSource,
491
+ projectLink: context.projectLink?.path ?? null,
492
+ storedLogins,
493
+ next: context.requestedOrgId ? authRequiredMessage(context) : "New account: runtype auth register --email <email>. Existing account: runtype auth login --api-key <key>."
494
+ }, null, 2));
495
+ return;
496
+ }
497
+ const pendingClaim = await store.getClaimState();
498
+ const claimState = pendingClaim?.accountId === credentials.accountId ? pendingClaim : null;
499
+ console.log(JSON.stringify({
500
+ status: "authenticated",
501
+ accountState: claimState ? "temporary" : "full",
502
+ userId: credentials.userId,
503
+ apiUrl: credentials.apiUrl,
504
+ orgId: credentials.orgId || null,
505
+ accountId: credentials.accountId,
506
+ accountSource: context.accountSource,
507
+ projectLink: context.projectLink?.path ?? null,
508
+ secretStore: context.account?.secretStore ?? null,
509
+ storedLogins,
510
+ ...context.credentialSource === "environment" ? { note: "RUNTYPE_API_KEY is set and takes precedence over the stored login." } : {},
511
+ ...claimState ? {
512
+ registrationId: claimState.registrationId,
513
+ credentialExpires: claimState.credentialExpires,
514
+ claimEmail: claimState.claimEmail ?? null,
515
+ next: claimState.claimEmail ? "Finish signup: ask the user for the 6-digit code from their email, then run: runtype auth verify <code>" : "Finish signup: ask the user for their email, then run: runtype auth claim <email>"
516
+ } : {}
517
+ }, null, 2));
518
+ } catch (error) {
519
+ const message = error instanceof Error ? error.message : "Unknown error";
520
+ console.error(JSON.stringify({
521
+ status: "error",
522
+ error: message
523
+ }, null, 2));
524
+ }
525
+ return;
526
+ }
527
+ await renderTask("Checking authentication...", async () => {
528
+ const context = await loadEffectiveContext();
529
+ const storedLogins = (await store.listAccounts()).length;
530
+ const credentials = context.credentials;
531
+ if (!credentials) return {
532
+ message: "Auth Status",
533
+ result: React.createElement(EntityCard, { fields: [
534
+ {
535
+ label: "Status",
536
+ value: "Not authenticated",
537
+ color: "yellow"
538
+ },
539
+ {
540
+ label: "Selected by",
541
+ value: describeAccountSource(context)
542
+ },
543
+ {
544
+ label: "Stored logins",
545
+ value: String(storedLogins)
546
+ },
547
+ {
548
+ label: "Hint",
549
+ value: context.requestedOrgId ? authRequiredMessage(context) : "Run runtype auth login"
550
+ }
551
+ ] })
552
+ };
553
+ return {
554
+ message: "Auth Status",
555
+ result: React.createElement(EntityCard, { fields: [
556
+ {
557
+ label: "Status",
558
+ value: "Authenticated",
559
+ color: "green"
560
+ },
561
+ {
562
+ label: "Organization",
563
+ value: credentials.orgId || "Personal"
564
+ },
565
+ {
566
+ label: "User ID",
567
+ value: credentials.userId
568
+ },
569
+ {
570
+ label: "API URL",
571
+ value: credentials.apiUrl
572
+ },
573
+ {
574
+ label: "API Key",
575
+ value: credentials.apiKey.substring(0, 12) + "..."
576
+ },
577
+ {
578
+ label: "Selected by",
579
+ value: describeAccountSource(context)
580
+ },
581
+ {
582
+ label: "Stored in",
583
+ value: context.account?.secretStore === "file" ? "credential file" : "OS keychain"
584
+ },
585
+ {
586
+ label: "Stored logins",
587
+ value: String(storedLogins)
588
+ },
589
+ ...context.credentialSource === "environment" ? [{
590
+ label: "Note",
591
+ value: "RUNTYPE_API_KEY overrides the stored login",
592
+ color: "yellow"
593
+ }] : []
594
+ ] })
595
+ };
596
+ });
597
+ });
598
+ authCommand.command("accounts").alias("list").description("List stored logins (one per organization and API URL)").option("--json", "Output as JSON").option("--tty", "Force TTY mode").option("--no-tty", "Force non-TTY mode").action(async (options) => {
599
+ const store = new CredentialStore();
600
+ const [accounts, defaultAccount, context] = await Promise.all([
601
+ store.listAccounts(),
602
+ store.getDefaultAccount(),
603
+ loadEffectiveContext()
604
+ ]);
605
+ const rows = accounts.map((account) => ({
606
+ accountId: account.id,
607
+ orgId: account.orgId,
608
+ userId: account.userId,
609
+ apiUrl: account.apiUrl,
610
+ isDefault: account.id === defaultAccount?.id,
611
+ isActive: account.id === context.account?.id,
612
+ secretStore: account.secretStore,
613
+ pendingClaim: Boolean(account.claim),
614
+ lastUsedAt: account.lastUsedAt ?? null
615
+ }));
616
+ if (!isTTY(options) || options.json) {
617
+ console.log(JSON.stringify({ accounts: rows }, null, 2));
618
+ return;
619
+ }
620
+ if (rows.length === 0) {
621
+ console.log(chalk.yellow("No stored logins. Run: runtype login"));
622
+ return;
623
+ }
624
+ for (const row of rows) {
625
+ const marker = row.isActive ? chalk.green("*") : " ";
626
+ const tags = [row.isDefault ? "default" : null, row.pendingClaim ? "unclaimed" : null].filter(Boolean).join(", ");
627
+ console.log(`${marker} ${chalk.bold(row.orgId ?? "Personal")} ${chalk.gray(row.apiUrl)}${tags ? ` ${chalk.cyan(`(${tags})`)}` : ""}`);
628
+ }
629
+ console.log(chalk.gray("\n* = login used in this directory"));
630
+ });
631
+ /** Resolve an `auth switch`/`logout` argument (org id or account id) to exactly one stored login. */
632
+ async function findAccountArgument(store, value, apiUrl) {
633
+ const accounts = await store.listAccounts();
634
+ const byId = accounts.find((account) => account.id === value);
635
+ if (byId) return byId;
636
+ const target = apiUrl ? normalizeApiUrl(apiUrl) : void 0;
637
+ const matches = accounts.filter((account) => account.orgId === value && (!target || account.apiUrl === target));
638
+ if (matches.length === 1) return matches[0];
639
+ if (matches.length > 1) throw new CliContextError(`${value} has logins on several API URLs (${matches.map((m) => m.apiUrl).join(", ")}); pass --api-url to pick one.`);
640
+ throw new CliContextError(`No stored login for ${value}. Run: runtype login --org ${value}`);
641
+ }
642
+ authCommand.command("switch [org]").description("Set the default login used outside linked projects").option("--api-url <url>", "Disambiguate when the organization has logins on several API URLs").option("--tty", "Force TTY mode").option("--no-tty", "Force non-TTY mode").action(async (org, options) => {
643
+ const store = new CredentialStore();
644
+ const accounts = await store.listAccounts();
645
+ if (accounts.length === 0) {
646
+ console.error(chalk.yellow("No stored logins. Run: runtype login"));
647
+ process.exit(1);
648
+ return;
649
+ }
650
+ let account;
651
+ if (org) account = await findAccountArgument(store, org, options.apiUrl ?? getInvocationOptions().apiUrl);
652
+ else if (!isTTY(options)) {
653
+ console.error(JSON.stringify({
654
+ status: "error",
655
+ error: "Pass the organization to switch to.",
656
+ next: "runtype auth accounts --json, then: runtype auth switch <orgId>"
657
+ }));
658
+ process.exit(1);
659
+ return;
660
+ } else {
661
+ const { promptForAccount } = await import("./chunk-CGDQoqDC.js");
662
+ account = await promptForAccount(accounts);
663
+ }
664
+ if (!account) return;
665
+ await store.setDefaultAccount(account.id);
666
+ const context = await loadEffectiveContext();
667
+ const shadowed = context.accountSource !== "default" && context.account?.id !== account.id ? `This directory still uses ${context.requestedOrgId} (${describeAccountSource(context)}).` : null;
668
+ if (!isTTY(options)) {
669
+ console.log(JSON.stringify({
670
+ status: "switched",
671
+ accountId: account.id,
672
+ orgId: account.orgId,
673
+ apiUrl: account.apiUrl,
674
+ ...shadowed ? { note: shadowed } : {}
675
+ }, null, 2));
676
+ return;
677
+ }
678
+ console.log(chalk.green(`Default login is now ${account.orgId ?? "Personal"} (${account.apiUrl}).`));
679
+ if (shadowed) console.log(chalk.yellow(shadowed));
680
+ });
681
+ authCommand.command("export-key").description("Print the active login's API key to stdout (for use in scripts and .env files)").action(async () => {
682
+ const apiKey = await new CredentialStore().getApiKey();
683
+ if (!apiKey) {
684
+ console.error("Not authenticated. Run `runtype auth login` first.");
685
+ process.exit(1);
686
+ return;
687
+ }
688
+ process.stdout.write(apiKey);
689
+ });
690
+ registerAgentAuthCommands(authCommand);
691
+ authCommand.command("logout [org]").description("Remove the login this directory uses (or the named org's login, or --all)").option("--all", "Remove every stored login").option("--api-url <url>", "Disambiguate when the organization has logins on several API URLs").option("--tty", "Force TTY mode").option("--no-tty", "Force non-TTY mode").action(async (org, options) => {
692
+ const store = new CredentialStore();
693
+ const logout = async () => {
694
+ if (options.all) {
695
+ const accounts = await store.listAccounts();
696
+ for (const account of accounts) await store.removeAccount(account.id);
697
+ return { removed: accounts };
698
+ }
699
+ if (org) {
700
+ const account = await findAccountArgument(store, org, options.apiUrl ?? getInvocationOptions().apiUrl);
701
+ await store.removeAccount(account.id);
702
+ return { removed: [account] };
703
+ }
704
+ const removed = await store.clearCredentials();
705
+ return { removed: removed ? [removed] : [] };
706
+ };
707
+ if (!isTTY(options)) {
708
+ const { removed } = await logout();
709
+ if (removed.length === 0) {
710
+ console.log(JSON.stringify({ status: "no_credentials" }, null, 2));
711
+ return;
712
+ }
713
+ console.log(JSON.stringify({
714
+ status: "logged_out",
715
+ removed: removed.map((account) => ({
716
+ orgId: account.orgId,
717
+ apiUrl: account.apiUrl
718
+ })),
719
+ remainingLogins: (await store.listAccounts()).length
720
+ }, null, 2));
721
+ return;
722
+ }
723
+ await renderTask("Logging out...", async () => {
724
+ const { removed } = await logout();
725
+ if (removed.length === 0) return { message: "No stored credentials found" };
726
+ return { message: `Logged out of ${removed.map((account) => account.orgId ?? "Personal").join(", ")}` };
727
+ });
728
+ });
729
+ authCommand.command("whoami").description("Display current user information").option("--json", "Output as JSON").option("--tty", "Force TTY mode").option("--no-tty", "Force non-TTY mode").action(async (options) => {
730
+ const apiKey = await ensureAuth({
731
+ interactive: false,
732
+ exitOnFailure: false
733
+ });
734
+ const context = await loadEffectiveContext();
735
+ const credentials = {
736
+ ...context.credentialSource === "stored" ? context.credentials : {},
737
+ apiKey,
738
+ apiUrl: context.apiUrl
739
+ };
740
+ if (!credentials || !credentials.apiKey) {
741
+ if (!isTTY(options) || options.json) console.log(JSON.stringify({
742
+ status: "unauthenticated",
743
+ ...context.requestedOrgId ? {
744
+ requestedOrgId: context.requestedOrgId,
745
+ next: authRequiredMessage(context)
746
+ } : {}
747
+ }, null, 2));
748
+ else {
749
+ console.log(chalk.yellow(context.requestedOrgId ? authRequiredMessage(context) : "Not logged in"));
750
+ if (!context.requestedOrgId) console.log("Run: runtype auth login");
751
+ }
752
+ return;
753
+ }
754
+ if (!isTTY(options) || options.json) {
755
+ try {
756
+ const user = await getAuthenticatedUser();
757
+ const billingInfo = await getBillingInfo(credentials.apiKey, credentials.apiUrl);
758
+ console.log(JSON.stringify({
759
+ userId: user.userId,
760
+ orgId: user.orgId,
761
+ apiUrl: credentials.apiUrl || getApiUrl(),
762
+ credentialSource: context.credentialSource,
763
+ accountSource: context.accountSource,
764
+ projectLink: context.projectLink?.path ?? null,
765
+ billing: billingInfo
766
+ }, null, 2));
767
+ } catch (error) {
768
+ const message = error instanceof Error ? error.message : "Unknown error";
769
+ console.error(JSON.stringify({
770
+ status: "error",
771
+ error: message
772
+ }, null, 2));
773
+ process.exit(1);
774
+ }
775
+ return;
776
+ }
777
+ await renderTask("Fetching user information...", async () => {
778
+ const user = await getAuthenticatedUser();
779
+ const billingInfo = await getBillingInfo(credentials.apiKey, credentials.apiUrl);
780
+ const fields = [
781
+ {
782
+ label: "User ID",
783
+ value: user.userId
784
+ },
785
+ {
786
+ label: "Organization",
787
+ value: user.orgId || "Personal"
788
+ },
789
+ {
790
+ label: "API Key",
791
+ value: credentials.apiKey.substring(0, 10) + "..."
792
+ },
793
+ {
794
+ label: "API URL",
795
+ value: credentials.apiUrl || getApiUrl()
796
+ },
797
+ {
798
+ label: "Selected by",
799
+ value: context.credentialSource === "environment" ? "RUNTYPE_API_KEY" : describeAccountSource(context)
800
+ },
801
+ {
802
+ label: "Created",
803
+ value: credentials.createdAt
804
+ },
805
+ {
806
+ label: "Last Used",
807
+ value: credentials.lastUsed
808
+ }
809
+ ];
810
+ if (billingInfo) {
811
+ const plan = billingInfo.planName || billingInfo.plan;
812
+ if (plan) fields.push({
813
+ label: "Plan",
814
+ value: plan,
815
+ color: "green"
816
+ });
817
+ const usage = billingInfo.usage;
818
+ if (usage) {
819
+ const used = usage.executionsToday ?? usage.executionsThisMonth;
820
+ const value = usage.executionsThisMonthAvailable === false || typeof used !== "number" ? "unavailable" : used.toLocaleString();
821
+ fields.push({
822
+ label: "Executions today",
823
+ value
824
+ });
825
+ }
826
+ }
827
+ return {
828
+ message: "Current User",
829
+ result: React.createElement(EntityCard, { fields })
830
+ };
831
+ });
832
+ });
833
+ export { authCommand, loginCommand };
834
+