@runtypelabs/cli 2.30.1 → 2.31.0

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 (2) hide show
  1. package/dist/index.js +621 -138
  2. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -72,12 +72,35 @@ var init_credential_store = __esm({
72
72
  }
73
73
  }
74
74
  }
75
+ /**
76
+ * Replace the entire backing store in one file write. Conf's runtime has a
77
+ * `store` setter (used for exactly this whole-store replacement), but its
78
+ * published typings declare the property read-only, so the assignment needs
79
+ * a cast. Single-write replacement is load-bearing for claim completion:
80
+ * the server has already revoked the previous key, so a multi-write
81
+ * sequence could lose the only copy of the fresh credential.
82
+ */
83
+ replaceStore(data) {
84
+ ;
85
+ this.config.store = data;
86
+ }
75
87
  getMachineKey() {
76
88
  const hostname4 = os.hostname();
77
89
  const username = os.userInfo().username;
78
90
  return crypto3.createHash("sha256").update(`runtype-cli-${hostname4}-${username}`).digest("hex").substring(0, 32);
79
91
  }
80
- async saveCredentials(credentials) {
92
+ /**
93
+ * Store credentials, optionally together with agent-auth claim state in the
94
+ * same write. Passing `claimState` here (rather than a follow-up
95
+ * `saveClaimState` call) keeps the temporary credential and its claim token
96
+ * atomic — a crash between two writes would otherwise orphan a pre-claim
97
+ * key that `auth status` then misreports as a full account.
98
+ *
99
+ * Storing credentials WITHOUT `claimState` invalidates any in-flight claim
100
+ * ceremony: every login path (api-key, browser, claim completion) must not
101
+ * leave stale claim state behind.
102
+ */
103
+ async saveCredentials(credentials, claimState) {
81
104
  const configData = {
82
105
  apiKey: this.encrypt(credentials.apiKey),
83
106
  userId: credentials.userId,
@@ -90,7 +113,21 @@ var init_credential_store = __esm({
90
113
  if (credentials.apiUrl !== void 0) {
91
114
  configData.apiUrl = credentials.apiUrl;
92
115
  }
93
- this.config.set(configData);
116
+ if (claimState) {
117
+ Object.assign(configData, this.claimStateToConfig(claimState));
118
+ }
119
+ this.replaceStore(configData);
120
+ }
121
+ claimStateToConfig(state) {
122
+ const data = {
123
+ claimToken: this.encrypt(state.claimToken),
124
+ registrationId: state.registrationId
125
+ };
126
+ if (state.claimEmail !== void 0) data.claimEmail = state.claimEmail;
127
+ if (state.credentialExpires !== void 0) data.credentialExpires = state.credentialExpires;
128
+ if (state.claimTokenExpires !== void 0) data.claimTokenExpires = state.claimTokenExpires;
129
+ if (state.apiUrl !== void 0) data.claimApiUrl = state.apiUrl;
130
+ return data;
94
131
  }
95
132
  async getApiKey() {
96
133
  const encrypted = this.config.get("apiKey");
@@ -109,6 +146,39 @@ var init_credential_store = __esm({
109
146
  async clearCredentials() {
110
147
  this.config.clear();
111
148
  }
149
+ /**
150
+ * Persist agent-auth claim ceremony state alongside the pre-claim credential
151
+ * so `runtype auth claim` / `runtype auth verify` can run as separate
152
+ * invocations (the agent asks the user for their email and OTP in between).
153
+ * The claim token is encrypted like the API key — it can mint the
154
+ * post-claim credential, so it is a secret.
155
+ */
156
+ async saveClaimState(state) {
157
+ this.config.set(this.claimStateToConfig(state));
158
+ }
159
+ async getClaimState() {
160
+ const claimToken = this.config.get("claimToken");
161
+ const registrationId = this.config.get("registrationId");
162
+ if (!claimToken || !registrationId) return null;
163
+ return {
164
+ claimToken: this.decrypt(claimToken),
165
+ registrationId,
166
+ claimEmail: this.config.get("claimEmail"),
167
+ credentialExpires: this.config.get("credentialExpires"),
168
+ claimTokenExpires: this.config.get("claimTokenExpires"),
169
+ apiUrl: this.config.get("claimApiUrl")
170
+ };
171
+ }
172
+ async clearClaimState() {
173
+ const store = { ...this.config.store };
174
+ delete store.claimToken;
175
+ delete store.registrationId;
176
+ delete store.claimEmail;
177
+ delete store.credentialExpires;
178
+ delete store.claimTokenExpires;
179
+ delete store.claimApiUrl;
180
+ this.replaceStore(store);
181
+ }
112
182
  async hasCredentials() {
113
183
  return !!this.config.get("apiKey");
114
184
  }
@@ -159,7 +229,7 @@ function loadEnv(path20 = resolve(process.cwd(), ".env")) {
159
229
 
160
230
  // src/commands/auth.ts
161
231
  import { Command } from "commander";
162
- import chalk from "chalk";
232
+ import chalk2 from "chalk";
163
233
  import React from "react";
164
234
  import { render } from "ink";
165
235
  import { useState as useState2, useEffect as useEffect3 } from "react";
@@ -38886,6 +38956,13 @@ var userProfileFeaturesSchema = external_exports.object({
38886
38956
  // until a rule enables it). Execution and tool listings are gated
38887
38957
  // server-side by the same flag.
38888
38958
  enableMassiveWebRender: external_exports.boolean(),
38959
+ // Gates the agent voice configuration UI in the dashboard (the Voice tab in
38960
+ // the agent editor). Driven by the `enable-voice` boolean flag (fail-closed
38961
+ // in production until a rule enables it; non-prod defaults on, preserving
38962
+ // the staging-first rollout the flag replaced). The voice API routes stay
38963
+ // ungated: they already require per-agent `config.voice.enabled` opt-in,
38964
+ // which production users can only set once this flag exposes the UI.
38965
+ enableVoice: external_exports.boolean(),
38889
38966
  // Gates the workspace-model arms of the dashboard signup / switcher UI
38890
38967
  // (docs/features/planning/2026-07-07-workspace-model-migration.md). Driven by
38891
38968
  // the `enable-workspace-model` boolean flag (fail-closed in production until a
@@ -45920,7 +45997,18 @@ var ApiKeyManager = class {
45920
45997
  try {
45921
45998
  data = await client.get("/auth/me");
45922
45999
  } catch {
45923
- throw new Error("Failed to get user information");
46000
+ try {
46001
+ const profile = await client.get(
46002
+ "/users/profile"
46003
+ );
46004
+ if (typeof profile.id === "string") {
46005
+ data = { userId: profile.id, orgId: profile.orgId ?? null, email: profile.email };
46006
+ }
46007
+ } catch {
46008
+ }
46009
+ if (!data) {
46010
+ throw new Error("Failed to get user information");
46011
+ }
45924
46012
  }
45925
46013
  if (!isAuthMeResponse(data)) {
45926
46014
  throw new Error("Invalid user response format");
@@ -46167,23 +46255,418 @@ function MutationResult({
46167
46255
  return null;
46168
46256
  }
46169
46257
 
46170
- // src/commands/auth.ts
46171
- var authCommand = new Command("auth").description("Manage authentication");
46172
- async function getBillingInfo(apiKey, apiUrl) {
46258
+ // src/commands/auth-agent.ts
46259
+ init_credential_store();
46260
+
46261
+ // src/auth/agent-auth-client.ts
46262
+ var AgentAuthError = class extends Error {
46263
+ constructor(code, status, message, hint) {
46264
+ super(message);
46265
+ this.code = code;
46266
+ this.status = status;
46267
+ this.hint = hint;
46268
+ this.name = "AgentAuthError";
46269
+ }
46270
+ code;
46271
+ status;
46272
+ hint;
46273
+ };
46274
+ var ERROR_HINTS = {
46275
+ 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",
46276
+ invalid_claim_token: "The claim token is invalid or expired. Run: runtype auth register --force",
46277
+ claim_expired: "The claim window expired. Run: runtype auth register --force",
46278
+ credential_expired: "The temporary credential expired. Run: runtype auth register --force",
46279
+ 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>",
46280
+ 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>",
46281
+ otp_expired: "The code expired. Re-send it with: runtype auth claim <email>",
46282
+ previously_claimed: "This registration was already claimed. Run: runtype auth status to inspect stored credentials.",
46283
+ email_send_failed: "The verification email could not be sent. Retry: runtype auth claim <email>",
46284
+ rate_limited: "Too many attempts. Wait a minute, then retry the same command.",
46285
+ service_disabled: "Agent registration is temporarily disabled. Try again later."
46286
+ };
46287
+ async function postJson(url2, body) {
46288
+ let response;
46173
46289
  try {
46174
- const client = createCliClient(apiKey, apiUrl);
46175
- return await client.get("/billing/status");
46290
+ response = await fetch(url2, {
46291
+ method: "POST",
46292
+ headers: {
46293
+ "content-type": "application/json",
46294
+ "user-agent": "runtype-cli"
46295
+ },
46296
+ body: JSON.stringify(body)
46297
+ });
46298
+ } catch (error51) {
46299
+ const message = error51 instanceof Error ? error51.message : String(error51);
46300
+ throw new AgentAuthError(
46301
+ "network_error",
46302
+ 0,
46303
+ `Could not reach ${url2}: ${message}`,
46304
+ "Check network access and the --api-url value, then retry the same command."
46305
+ );
46306
+ }
46307
+ let data = {};
46308
+ try {
46309
+ data = await response.json();
46176
46310
  } catch {
46177
- return null;
46178
46311
  }
46312
+ if (!response.ok) {
46313
+ const code = typeof data.error === "string" ? data.error : `http_${response.status}`;
46314
+ const message = typeof data.message === "string" ? data.message : `Request failed (${response.status})`;
46315
+ throw new AgentAuthError(code, response.status, message, ERROR_HINTS[code]);
46316
+ }
46317
+ return { status: response.status, data };
46179
46318
  }
46180
- 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) => {
46181
- if (!isTTY(options)) {
46319
+ function requireString(data, key) {
46320
+ const value = data[key];
46321
+ if (typeof value !== "string" || value.length === 0) {
46322
+ throw new AgentAuthError(
46323
+ "invalid_response",
46324
+ 0,
46325
+ `Registration response is missing "${key}". The API may be an incompatible version.`
46326
+ );
46327
+ }
46328
+ return value;
46329
+ }
46330
+ function stringArray(value) {
46331
+ return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
46332
+ }
46333
+ async function registerAnonymousAgent(apiUrl) {
46334
+ const base = apiUrl || getApiUrl();
46335
+ const { data } = await postJson(`${base}/agent/auth`, {
46336
+ // @snake-case-ok-start: auth.md protocol field names
46337
+ type: "anonymous",
46338
+ requested_credential_type: "api_key"
46339
+ // @snake-case-ok-end
46340
+ });
46341
+ return {
46342
+ // @snake-case-ok-start: auth.md protocol field names
46343
+ registrationId: requireString(data, "registration_id"),
46344
+ credential: requireString(data, "credential"),
46345
+ credentialExpires: requireString(data, "credential_expires"),
46346
+ scopes: stringArray(data.scopes),
46347
+ claimToken: requireString(data, "claim_token"),
46348
+ claimTokenExpires: requireString(data, "claim_token_expires"),
46349
+ postClaimScopes: stringArray(data.post_claim_scopes)
46350
+ // @snake-case-ok-end
46351
+ };
46352
+ }
46353
+ async function initiateClaim(claimToken, email3, apiUrl) {
46354
+ const base = apiUrl || getApiUrl();
46355
+ const { data } = await postJson(`${base}/agent/auth/claim`, {
46356
+ // @snake-case-ok-start: auth.md protocol field names
46357
+ claim_token: claimToken,
46358
+ email: email3
46359
+ // @snake-case-ok-end
46360
+ });
46361
+ return {
46362
+ // @snake-case-ok-start: auth.md protocol field names
46363
+ registrationId: requireString(data, "registration_id"),
46364
+ status: typeof data.status === "string" ? data.status : "initiated",
46365
+ otpExpiresAt: requireString(data, "expires_at")
46366
+ // @snake-case-ok-end
46367
+ };
46368
+ }
46369
+ async function completeClaim(claimToken, otp, apiUrl) {
46370
+ const base = apiUrl || getApiUrl();
46371
+ const { data } = await postJson(`${base}/agent/auth/claim/complete`, {
46372
+ // @snake-case-ok-start: auth.md protocol field names
46373
+ claim_token: claimToken,
46374
+ otp
46375
+ // @snake-case-ok-end
46376
+ });
46377
+ return {
46378
+ // @snake-case-ok-start: auth.md protocol field names
46379
+ registrationId: requireString(data, "registration_id"),
46380
+ credential: requireString(data, "credential"),
46381
+ scopes: stringArray(data.scopes)
46382
+ // @snake-case-ok-end
46383
+ };
46384
+ }
46385
+
46386
+ // src/lib/output.ts
46387
+ import chalk from "chalk";
46388
+ function printJson(data) {
46389
+ console.log(JSON.stringify(data, null, 2));
46390
+ }
46391
+ function printList(items, options) {
46392
+ console.log(chalk.cyan(`${options.title}:`));
46393
+ if (items.length === 0) {
46394
+ console.log(chalk.gray(` ${options.emptyMessage || "None found"}`));
46395
+ return;
46396
+ }
46397
+ for (const item of items) {
46398
+ const parts = [];
46399
+ for (const col of options.columns) {
46400
+ const value = item[col.key];
46401
+ if (value === void 0 || value === null) continue;
46402
+ const str = String(value);
46403
+ const colorFn = col.color ? chalk[col.color] : (s) => s;
46404
+ if (col.label) {
46405
+ parts.push(`${col.label}: ${colorFn(str)}`);
46406
+ } else {
46407
+ parts.push(colorFn(str));
46408
+ }
46409
+ }
46410
+ console.log(` ${parts.join(" - ")}`);
46411
+ }
46412
+ if (options.total !== void 0) {
46413
+ console.log(chalk.dim(`
46414
+ Total: ${options.total}`));
46415
+ }
46416
+ }
46417
+ function printDetail(title, fields) {
46418
+ console.log(chalk.cyan(`${title}:`));
46419
+ for (const field of fields) {
46420
+ if (field.value === void 0 || field.value === null) continue;
46421
+ const str = typeof field.value === "object" ? JSON.stringify(field.value) : String(field.value);
46422
+ console.log(` ${chalk.gray(field.label + ":")} ${str}`);
46423
+ }
46424
+ }
46425
+ function getTotalCount(pagination) {
46426
+ return pagination?.totalCount ?? pagination?.total ?? void 0;
46427
+ }
46428
+ function buildListParams(options, extra) {
46429
+ const params = {};
46430
+ if (options.limit) params.limit = options.limit;
46431
+ if (options.cursor) params.cursor = options.cursor;
46432
+ if (extra) {
46433
+ for (const [key, value] of Object.entries(extra)) {
46434
+ if (value !== void 0) params[key] = value;
46435
+ }
46436
+ }
46437
+ return params;
46438
+ }
46439
+ function printNextCursorHint(commandPath, pagination) {
46440
+ const hasMore = pagination?.hasMore ?? pagination?.hasNextPage ?? false;
46441
+ const nextCursor = pagination?.nextCursor !== void 0 ? pagination.nextCursor : pagination?.cursor ?? null;
46442
+ if (!hasMore || !nextCursor) return;
46443
+ console.log(
46444
+ chalk.yellow("\n More results available \u2014 fetch the next page with:") + chalk.dim(`
46445
+ ${commandPath} --cursor ${nextCursor}`)
46446
+ );
46447
+ }
46448
+
46449
+ // src/commands/auth-agent.ts
46450
+ var VERIFY_NEXT = "Ask the user for the 6-digit code from their email, then run: runtype auth verify <code>";
46451
+ function claimNext(email3) {
46452
+ return email3 ? `Send the verification email with: runtype auth claim ${email3}` : "Ask the user for their email address, then run: runtype auth claim <email>";
46453
+ }
46454
+ function fail(payload) {
46455
+ console.error(JSON.stringify(payload, null, 2));
46456
+ process.exit(1);
46457
+ }
46458
+ function failFromError(error51, fallbackNext) {
46459
+ if (error51 instanceof AgentAuthError) {
46460
+ fail({
46461
+ status: "error",
46462
+ code: error51.code,
46463
+ error: error51.message,
46464
+ ...error51.hint || fallbackNext ? { next: error51.hint || fallbackNext } : {}
46465
+ });
46466
+ }
46467
+ const message = error51 instanceof Error ? error51.message : String(error51);
46468
+ fail({ status: "error", error: message, ...fallbackNext ? { next: fallbackNext } : {} });
46469
+ }
46470
+ async function fetchIdentity(apiKey, apiUrl) {
46471
+ const client = createCliClient(apiKey, apiUrl);
46472
+ const profile = await client.get(
46473
+ "/users/profile"
46474
+ );
46475
+ return {
46476
+ userId: typeof profile.id === "string" ? profile.id : "unknown",
46477
+ orgId: typeof profile.orgId === "string" ? profile.orgId : void 0,
46478
+ email: typeof profile.email === "string" ? profile.email : void 0
46479
+ };
46480
+ }
46481
+ async function registerAction(options) {
46482
+ const store = new CredentialStore();
46483
+ const apiUrl = options.apiUrl || getApiUrl();
46484
+ const existingClaim = await store.getClaimState();
46485
+ const hasCredentials = await store.hasCredentials();
46486
+ if (existingClaim && !options.force) {
46487
+ const resumeDeadline = existingClaim.claimTokenExpires ?? existingClaim.credentialExpires;
46488
+ const expired = resumeDeadline && new Date(resumeDeadline) < /* @__PURE__ */ new Date();
46489
+ if (!expired) {
46490
+ const payload = {
46491
+ status: "registration_pending",
46492
+ registrationId: existingClaim.registrationId,
46493
+ credentialExpires: existingClaim.credentialExpires,
46494
+ next: existingClaim.claimEmail ? VERIFY_NEXT : claimNext(options.email)
46495
+ };
46496
+ if (existingClaim.claimEmail) payload.claimEmail = existingClaim.claimEmail;
46497
+ printJson(payload);
46498
+ return;
46499
+ }
46500
+ }
46501
+ if (hasCredentials && !existingClaim && !options.force) {
46502
+ fail({
46503
+ status: "error",
46504
+ code: "already_authenticated",
46505
+ error: "Credentials are already stored for a claimed account.",
46506
+ next: "Run: runtype auth whoami --no-tty to confirm, or runtype auth logout first if the user wants a different account."
46507
+ });
46508
+ }
46509
+ let registration;
46510
+ try {
46511
+ registration = await registerAnonymousAgent(apiUrl);
46512
+ } catch (error51) {
46513
+ failFromError(error51, "Retry: runtype auth register");
46514
+ }
46515
+ let identity = { userId: "unknown" };
46516
+ try {
46517
+ identity = await fetchIdentity(registration.credential, apiUrl);
46518
+ } catch {
46519
+ }
46520
+ const claimState = {
46521
+ claimToken: registration.claimToken,
46522
+ registrationId: registration.registrationId,
46523
+ credentialExpires: registration.credentialExpires,
46524
+ claimTokenExpires: registration.claimTokenExpires,
46525
+ apiUrl
46526
+ };
46527
+ await store.saveCredentials(
46528
+ {
46529
+ apiKey: registration.credential,
46530
+ userId: identity.userId,
46531
+ orgId: identity.orgId,
46532
+ apiUrl
46533
+ },
46534
+ claimState
46535
+ );
46536
+ const base = {
46537
+ status: "registered",
46538
+ accountState: "temporary",
46539
+ registrationId: registration.registrationId,
46540
+ userId: identity.userId,
46541
+ credentialExpires: registration.credentialExpires,
46542
+ 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."
46543
+ };
46544
+ if (options.email) {
46545
+ try {
46546
+ const claim = await initiateClaim(registration.claimToken, options.email, apiUrl);
46547
+ await store.saveClaimState({ ...claimState, claimEmail: options.email });
46548
+ printJson({
46549
+ ...base,
46550
+ status: "otp_sent",
46551
+ claimEmail: options.email,
46552
+ otpExpiresAt: claim.otpExpiresAt,
46553
+ next: VERIFY_NEXT
46554
+ });
46555
+ return;
46556
+ } catch (error51) {
46557
+ if (error51 instanceof AgentAuthError) {
46558
+ fail({
46559
+ ...base,
46560
+ status: "registered_claim_failed",
46561
+ code: error51.code,
46562
+ error: error51.message,
46563
+ next: error51.hint || claimNext(options.email)
46564
+ });
46565
+ }
46566
+ throw error51;
46567
+ }
46568
+ }
46569
+ printJson({ ...base, next: claimNext() });
46570
+ }
46571
+ async function claimAction(email3, options) {
46572
+ const store = new CredentialStore();
46573
+ const claimState = await store.getClaimState();
46574
+ const apiUrl = options.apiUrl || claimState?.apiUrl || (await store.getCredentials())?.apiUrl || getApiUrl();
46575
+ if (!claimState) {
46576
+ const hasCredentials = await store.hasCredentials();
46577
+ fail({
46578
+ status: "error",
46579
+ code: "no_pending_registration",
46580
+ error: hasCredentials ? "Stored credentials are already claimed; there is nothing to claim." : "No registration found.",
46581
+ next: hasCredentials ? "Run: runtype auth whoami --no-tty" : `Run: runtype auth register --email ${email3}`
46582
+ });
46583
+ }
46584
+ try {
46585
+ const claim = await initiateClaim(claimState.claimToken, email3, apiUrl);
46586
+ await store.saveClaimState({ ...claimState, claimEmail: email3 });
46587
+ printJson({
46588
+ status: "otp_sent",
46589
+ registrationId: claim.registrationId,
46590
+ claimEmail: email3,
46591
+ otpExpiresAt: claim.otpExpiresAt,
46592
+ next: VERIFY_NEXT
46593
+ });
46594
+ } catch (error51) {
46595
+ failFromError(error51, `Retry: runtype auth claim ${email3}`);
46596
+ }
46597
+ }
46598
+ async function verifyAction(code, options) {
46599
+ const store = new CredentialStore();
46600
+ const claimState = await store.getClaimState();
46601
+ const apiUrl = options.apiUrl || claimState?.apiUrl || (await store.getCredentials())?.apiUrl || getApiUrl();
46602
+ if (!claimState) {
46603
+ fail({
46604
+ status: "error",
46605
+ code: "no_pending_registration",
46606
+ error: "No claim in progress.",
46607
+ next: "Run: runtype auth register --email <email> first."
46608
+ });
46609
+ }
46610
+ if (!claimState.claimEmail) {
46611
+ fail({
46612
+ status: "error",
46613
+ code: "claim_not_initiated",
46614
+ error: "No verification email has been sent yet, so there is no code to verify.",
46615
+ next: claimNext()
46616
+ });
46617
+ }
46618
+ let completed;
46619
+ try {
46620
+ completed = await completeClaim(claimState.claimToken, code.trim(), apiUrl);
46621
+ } catch (error51) {
46622
+ failFromError(error51);
46623
+ }
46624
+ let identity = { userId: "unknown" };
46625
+ try {
46626
+ identity = await fetchIdentity(completed.credential, apiUrl);
46627
+ } catch {
46628
+ }
46629
+ await store.saveCredentials({
46630
+ apiKey: completed.credential,
46631
+ userId: identity.userId,
46632
+ orgId: identity.orgId,
46633
+ apiUrl
46634
+ });
46635
+ await store.clearClaimState();
46636
+ printJson({
46637
+ status: "authenticated",
46638
+ accountState: "full",
46639
+ registrationId: completed.registrationId,
46640
+ userId: identity.userId,
46641
+ email: identity.email ?? claimState.claimEmail,
46642
+ note: "The account is fully activated and the stored credential was rotated. All limits from the temporary account are removed.",
46643
+ next: "Continue with setup: runtype install-mcp (then confirm with: runtype auth whoami --no-tty)"
46644
+ });
46645
+ }
46646
+ function registerAgentAuthCommands(authCommand2) {
46647
+ authCommand2.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);
46648
+ authCommand2.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);
46649
+ authCommand2.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);
46650
+ }
46651
+
46652
+ // src/commands/auth.ts
46653
+ var authCommand = new Command("auth").description("Manage authentication");
46654
+ var NON_TTY_LOGIN_GUIDANCE = {
46655
+ status: "error",
46656
+ code: "tty_required",
46657
+ error: "Browser login needs an interactive terminal, which this environment does not have.",
46658
+ next: [
46659
+ "New account (no browser needed): ask the user for their email, then run: runtype auth register --email <email> \u2014 a 6-digit code is emailed to them; finish with: runtype auth verify <code>",
46660
+ "Existing account: ask the user for an API key from the dashboard (Settings > API keys), then run: runtype auth login --api-key <key>"
46661
+ ]
46662
+ };
46663
+ function refuseForcedTtyLogin(options) {
46664
+ if (options.tty === true && process.stdout.isTTY !== true) {
46182
46665
  console.error(
46183
46666
  JSON.stringify(
46184
46667
  {
46185
- status: "error",
46186
- error: "Browser login requires TTY mode. Use --api-key with auth login instead."
46668
+ ...NON_TTY_LOGIN_GUIDANCE,
46669
+ 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."
46187
46670
  },
46188
46671
  null,
46189
46672
  2
@@ -46191,6 +46674,22 @@ authCommand.command("signup").description('Create a new Runtype account (alias f
46191
46674
  );
46192
46675
  process.exit(1);
46193
46676
  }
46677
+ return false;
46678
+ }
46679
+ async function getBillingInfo(apiKey, apiUrl) {
46680
+ try {
46681
+ const client = createCliClient(apiKey, apiUrl);
46682
+ return await client.get("/billing/status");
46683
+ } catch {
46684
+ return null;
46685
+ }
46686
+ }
46687
+ 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) => {
46688
+ refuseForcedTtyLogin(options);
46689
+ if (!isTTY(options)) {
46690
+ console.error(JSON.stringify(NON_TTY_LOGIN_GUIDANCE, null, 2));
46691
+ process.exit(1);
46692
+ }
46194
46693
  const App = () => {
46195
46694
  const [loading, setLoading] = useState2(true);
46196
46695
  const [loadingLabel, setLoadingLabel] = useState2("Opening browser for authentication...");
@@ -46263,7 +46762,11 @@ async function loginAction(options) {
46263
46762
  });
46264
46763
  console.log(
46265
46764
  JSON.stringify(
46266
- { status: "authenticated", userId: userData.userId },
46765
+ {
46766
+ status: "authenticated",
46767
+ userId: userData.userId,
46768
+ next: "Continue with setup: runtype install-mcp (then confirm with: runtype auth whoami)"
46769
+ },
46267
46770
  null,
46268
46771
  2
46269
46772
  )
@@ -46321,14 +46824,9 @@ async function loginAction(options) {
46321
46824
  await waitUntilExit2();
46322
46825
  return;
46323
46826
  }
46827
+ refuseForcedTtyLogin(options);
46324
46828
  if (!isTTY(options)) {
46325
- console.error(
46326
- JSON.stringify(
46327
- { status: "error", error: "Browser login requires TTY mode. Use --api-key instead." },
46328
- null,
46329
- 2
46330
- )
46331
- );
46829
+ console.error(JSON.stringify(NON_TTY_LOGIN_GUIDANCE, null, 2));
46332
46830
  process.exit(1);
46333
46831
  }
46334
46832
  const App = () => {
@@ -46393,17 +46891,34 @@ authCommand.command("status").description("Show authentication status").option("
46393
46891
  try {
46394
46892
  const hasCredentials = await store.hasCredentials();
46395
46893
  if (!hasCredentials) {
46396
- console.log(JSON.stringify({ status: "unauthenticated" }, null, 2));
46894
+ console.log(
46895
+ JSON.stringify(
46896
+ {
46897
+ status: "unauthenticated",
46898
+ next: "New account: runtype auth register --email <email>. Existing account: runtype auth login --api-key <key>."
46899
+ },
46900
+ null,
46901
+ 2
46902
+ )
46903
+ );
46397
46904
  return;
46398
46905
  }
46399
46906
  const credentials = await store.getCredentials();
46907
+ const claimState = await store.getClaimState();
46400
46908
  console.log(
46401
46909
  JSON.stringify(
46402
46910
  {
46403
46911
  status: "authenticated",
46912
+ accountState: claimState ? "temporary" : "full",
46404
46913
  userId: credentials?.userId,
46405
46914
  apiUrl: credentials?.apiUrl || "default",
46406
- orgId: credentials?.orgId || null
46915
+ orgId: credentials?.orgId || null,
46916
+ ...claimState ? {
46917
+ registrationId: claimState.registrationId,
46918
+ credentialExpires: claimState.credentialExpires,
46919
+ claimEmail: claimState.claimEmail ?? null,
46920
+ 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>"
46921
+ } : {}
46407
46922
  },
46408
46923
  null,
46409
46924
  2
@@ -46486,6 +47001,7 @@ authCommand.command("export-key").description("Print the stored API key to stdou
46486
47001
  }
46487
47002
  process.stdout.write(apiKey);
46488
47003
  });
47004
+ registerAgentAuthCommands(authCommand);
46489
47005
  authCommand.command("logout").description("Remove stored credentials").option("--tty", "Force TTY mode").option("--no-tty", "Force non-TTY mode").action(async (options) => {
46490
47006
  const store = new CredentialStore();
46491
47007
  if (!isTTY(options)) {
@@ -46548,7 +47064,7 @@ authCommand.command("whoami").description("Display current user information").op
46548
47064
  if (!isTTY(options) || options.json) {
46549
47065
  console.log(JSON.stringify({ status: "unauthenticated" }, null, 2));
46550
47066
  } else {
46551
- console.log(chalk.yellow("Not logged in"));
47067
+ console.log(chalk2.yellow("Not logged in"));
46552
47068
  console.log("Run: runtype auth login");
46553
47069
  }
46554
47070
  return;
@@ -46640,17 +47156,17 @@ import { useState as useState3, useEffect as useEffect4 } from "react";
46640
47156
 
46641
47157
  // src/lib/file-utils.ts
46642
47158
  import { readFileSync as readFileSync3 } from "fs";
46643
- import chalk2 from "chalk";
47159
+ import chalk3 from "chalk";
46644
47160
  function parseJsonObject(raw, label) {
46645
47161
  let parsed;
46646
47162
  try {
46647
47163
  parsed = JSON.parse(raw);
46648
47164
  } catch {
46649
- console.error(chalk2.red(`Invalid JSON in ${label}`));
47165
+ console.error(chalk3.red(`Invalid JSON in ${label}`));
46650
47166
  process.exit(1);
46651
47167
  }
46652
47168
  if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
46653
- console.error(chalk2.red(`${label} must be a JSON object`));
47169
+ console.error(chalk3.red(`${label} must be a JSON object`));
46654
47170
  process.exit(1);
46655
47171
  }
46656
47172
  return parsed;
@@ -46661,7 +47177,7 @@ function readJsonFile(path20, opts = {}) {
46661
47177
  try {
46662
47178
  raw = readFileSync3(path20, "utf-8");
46663
47179
  } catch {
46664
- console.error(chalk2.red(`Failed to read ${label}: ${path20}`));
47180
+ console.error(chalk3.red(`Failed to read ${label}: ${path20}`));
46665
47181
  process.exit(1);
46666
47182
  }
46667
47183
  const obj = parseJsonObject(raw, label);
@@ -46675,11 +47191,11 @@ function readJsonFile(path20, opts = {}) {
46675
47191
  }
46676
47192
  function resolveJsonPayload(inline, fromFile, label) {
46677
47193
  if (inline !== void 0 && fromFile !== void 0) {
46678
- console.error(chalk2.red(`Error: provide only one of inline JSON or --from-file for ${label}`));
47194
+ console.error(chalk3.red(`Error: provide only one of inline JSON or --from-file for ${label}`));
46679
47195
  process.exit(1);
46680
47196
  }
46681
47197
  if (inline === void 0 && fromFile === void 0) {
46682
- console.error(chalk2.red(`Error: ${label} is required \u2014 pass inline JSON or --from-file <path>`));
47198
+ console.error(chalk3.red(`Error: ${label} is required \u2014 pass inline JSON or --from-file <path>`));
46683
47199
  process.exit(1);
46684
47200
  }
46685
47201
  if (fromFile !== void 0) {
@@ -46696,7 +47212,7 @@ import {
46696
47212
  // src/lib/ensure-auth.ts
46697
47213
  init_credential_store();
46698
47214
  import readline from "readline";
46699
- import chalk3 from "chalk";
47215
+ import chalk4 from "chalk";
46700
47216
  async function promptConfirm(message, options) {
46701
47217
  const defaultYes = options?.default !== false;
46702
47218
  const hint = defaultYes ? "Y/n" : "y/N";
@@ -46706,7 +47222,7 @@ async function promptConfirm(message, options) {
46706
47222
  terminal: true
46707
47223
  });
46708
47224
  return new Promise((resolve13) => {
46709
- rl.question(chalk3.cyan(`${message} (${hint}): `), (answer) => {
47225
+ rl.question(chalk4.cyan(`${message} (${hint}): `), (answer) => {
46710
47226
  rl.close();
46711
47227
  const trimmed2 = answer.trim().toLowerCase();
46712
47228
  if (trimmed2 === "") {
@@ -46735,9 +47251,9 @@ async function ensureAuth(options) {
46735
47251
  return apiKey;
46736
47252
  }
46737
47253
  if (options?.interactive === false) {
46738
- console.error(chalk3.yellow("\nAuthentication required."));
47254
+ console.error(chalk4.yellow("\nAuthentication required."));
46739
47255
  console.error(
46740
- chalk3.gray(
47256
+ chalk4.gray(
46741
47257
  'Run "runtype auth login" first or set RUNTYPE_API_KEY before reading command input from stdin.'
46742
47258
  )
46743
47259
  );
@@ -46746,10 +47262,10 @@ async function ensureAuth(options) {
46746
47262
  }
46747
47263
  return null;
46748
47264
  }
46749
- console.log(chalk3.yellow("\nAuthentication required."));
47265
+ console.log(chalk4.yellow("\nAuthentication required."));
46750
47266
  const shouldLogin = await promptConfirm("Would you like to log in now?", { default: true });
46751
47267
  if (!shouldLogin) {
46752
- console.log(chalk3.gray(`Run "runtype auth login" when you're ready.`));
47268
+ console.log(chalk4.gray(`Run "runtype auth login" when you're ready.`));
46753
47269
  if (options?.exitOnFailure !== false) {
46754
47270
  process.exit(1);
46755
47271
  }
@@ -46758,100 +47274,37 @@ async function ensureAuth(options) {
46758
47274
  return handleBrowserLogin(store, options?.apiUrl);
46759
47275
  }
46760
47276
  async function handleBrowserLogin(store, apiUrl) {
46761
- console.log(chalk3.gray("Opening browser for authentication..."));
47277
+ console.log(chalk4.gray("Opening browser for authentication..."));
46762
47278
  try {
46763
47279
  const oauth = new OAuthManager({
46764
47280
  clerkPublishableKey: getClerkPublishableKey(),
46765
47281
  dashboardUrl: getDashboardUrl()
46766
47282
  });
46767
- console.log(chalk3.gray("Waiting for browser authentication..."));
47283
+ console.log(chalk4.gray("Waiting for browser authentication..."));
46768
47284
  const sessionToken = await oauth.authenticate("sign-in");
46769
- console.log(chalk3.gray("Getting API key..."));
47285
+ console.log(chalk4.gray("Getting API key..."));
46770
47286
  const apiKeyManager = new ApiKeyManager();
46771
47287
  const { key, userId, orgId } = await apiKeyManager.exchangeSessionForApiKey(
46772
47288
  sessionToken,
46773
47289
  apiUrl || getApiUrl()
46774
47290
  );
46775
- console.log(chalk3.gray("Storing credentials..."));
47291
+ console.log(chalk4.gray("Storing credentials..."));
46776
47292
  await store.saveCredentials({
46777
47293
  apiKey: key,
46778
47294
  userId,
46779
47295
  orgId,
46780
47296
  apiUrl: apiUrl || getApiUrl()
46781
47297
  });
46782
- console.log(chalk3.green("Logged in successfully!"));
47298
+ console.log(chalk4.green("Logged in successfully!"));
46783
47299
  return key;
46784
47300
  } catch (error51) {
46785
47301
  const message = error51 instanceof Error ? error51.message : "Unknown error";
46786
- console.log(chalk3.red("Login failed"));
46787
- console.error(chalk3.red(message));
47302
+ console.log(chalk4.red("Login failed"));
47303
+ console.error(chalk4.red(message));
46788
47304
  return null;
46789
47305
  }
46790
47306
  }
46791
47307
 
46792
- // src/lib/output.ts
46793
- import chalk4 from "chalk";
46794
- function printJson(data) {
46795
- console.log(JSON.stringify(data, null, 2));
46796
- }
46797
- function printList(items, options) {
46798
- console.log(chalk4.cyan(`${options.title}:`));
46799
- if (items.length === 0) {
46800
- console.log(chalk4.gray(` ${options.emptyMessage || "None found"}`));
46801
- return;
46802
- }
46803
- for (const item of items) {
46804
- const parts = [];
46805
- for (const col of options.columns) {
46806
- const value = item[col.key];
46807
- if (value === void 0 || value === null) continue;
46808
- const str = String(value);
46809
- const colorFn = col.color ? chalk4[col.color] : (s) => s;
46810
- if (col.label) {
46811
- parts.push(`${col.label}: ${colorFn(str)}`);
46812
- } else {
46813
- parts.push(colorFn(str));
46814
- }
46815
- }
46816
- console.log(` ${parts.join(" - ")}`);
46817
- }
46818
- if (options.total !== void 0) {
46819
- console.log(chalk4.dim(`
46820
- Total: ${options.total}`));
46821
- }
46822
- }
46823
- function printDetail(title, fields) {
46824
- console.log(chalk4.cyan(`${title}:`));
46825
- for (const field of fields) {
46826
- if (field.value === void 0 || field.value === null) continue;
46827
- const str = typeof field.value === "object" ? JSON.stringify(field.value) : String(field.value);
46828
- console.log(` ${chalk4.gray(field.label + ":")} ${str}`);
46829
- }
46830
- }
46831
- function getTotalCount(pagination) {
46832
- return pagination?.totalCount ?? pagination?.total ?? void 0;
46833
- }
46834
- function buildListParams(options, extra) {
46835
- const params = {};
46836
- if (options.limit) params.limit = options.limit;
46837
- if (options.cursor) params.cursor = options.cursor;
46838
- if (extra) {
46839
- for (const [key, value] of Object.entries(extra)) {
46840
- if (value !== void 0) params[key] = value;
46841
- }
46842
- }
46843
- return params;
46844
- }
46845
- function printNextCursorHint(commandPath, pagination) {
46846
- const hasMore = pagination?.hasMore ?? pagination?.hasNextPage ?? false;
46847
- const nextCursor = pagination?.nextCursor !== void 0 ? pagination.nextCursor : pagination?.cursor ?? null;
46848
- if (!hasMore || !nextCursor) return;
46849
- console.log(
46850
- chalk4.yellow("\n More results available \u2014 fetch the next page with:") + chalk4.dim(`
46851
- ${commandPath} --cursor ${nextCursor}`)
46852
- );
46853
- }
46854
-
46855
47308
  // src/commands/flows.ts
46856
47309
  function flowsListParams(options) {
46857
47310
  return {
@@ -72823,32 +73276,54 @@ import chalk42 from "chalk";
72823
73276
  // ../shared/dist/agent-onboarding-content.mjs
72824
73277
  var AGENT_ONBOARDING_MARKDOWN = `# Runtype for Coding Agents
72825
73278
 
72826
- You are reading the setup script served by \`https://runtype.ai\`. Your durable goal is to leave this coding harness connected to Runtype's hosted MCP server with browser OAuth. Use the Runtype CLI as a bridge during this first session; do not treat it as the final integration.
73279
+ You are reading the setup script served by \`https://runtype.ai\`. Your durable goal is to leave this coding harness connected to Runtype's hosted MCP server. Use the Runtype CLI as a bridge during this first session; do not treat it as the final integration.
73280
+
73281
+ Signup never requires a browser. The only things you may ask the user for are their email address and the 6-digit code Runtype emails them. Do not tell the user to open a dashboard, sign-in page, or OAuth window during signup.
72827
73282
 
72828
73283
  ## Golden path \u2014 do exactly this
72829
73284
 
72830
73285
  If the live Runtype MCP tools are already available in this session, skip setup and go to "After MCP is active" below. Otherwise follow every step in order:
72831
73286
 
72832
- ### 1. Install and authenticate the CLI
73287
+ ### 1. Install the CLI and check auth
72833
73288
 
72834
73289
  \`\`\`bash
72835
73290
  npm install -g @runtypelabs/cli
72836
- runtype auth login
73291
+ runtype auth status
72837
73292
  \`\`\`
72838
73293
 
72839
- Complete the browser sign-in when it opens. The CLI makes this first session productive even though a running coding harness often cannot discover a newly installed MCP connection until it restarts.
73294
+ If status reports \`authenticated\` with \`accountState: "full"\`, skip to step 3. If it reports a pending signup, its \`next\` field tells you the exact command to resume with.
73295
+
73296
+ ### 2. Sign the user up from the terminal
72840
73297
 
72841
- ### 2. Install skills and the OAuth MCP connection
73298
+ Ask the user one question: "What email address should I use for your Runtype account?" Then run:
72842
73299
 
72843
73300
  \`\`\`bash
72844
- runtype install-mcp
73301
+ runtype auth register --email <their-email>
73302
+ \`\`\`
73303
+
73304
+ This creates the account and emails the user a 6-digit code. Ask the user: "Runtype emailed a 6-digit code to <their-email> \u2014 what is it?" Then run:
73305
+
73306
+ \`\`\`bash
73307
+ runtype auth verify <code>
72845
73308
  \`\`\`
72846
73309
 
72847
- This command detects the current coding harness, installs the Runtype agent skills for it, writes the user-level hosted MCP configuration, and starts the harness's own OAuth flow when possible. If it asks which harness to configure, select the harness running this session. If a browser opens, have the user complete OAuth.
73310
+ That completes signup. Every command in this flow prints JSON with a \`next\` field \u2014 follow it. Notes:
73311
+
73312
+ - These commands work without a TTY and never open a browser.
73313
+ - \`register\` is safe to re-run; it resumes a pending signup instead of creating a duplicate account.
73314
+ - After \`register\` (before \`verify\`) you already hold a temporary credential: CLI commands work with restricted models and quota, and \`verify\` upgrades the same stored login in place. Do not build anything substantial until \`verify\` succeeds.
73315
+ - If the email already has a Runtype account, the command reports \`email_already_registered\`. In that case ask the user for an API key from their dashboard (Settings > API keys) and run \`runtype auth login --api-key <key>\` instead. This is the only path where the user touches the dashboard, and only because they already have an account.
73316
+ - A wrong code returns \`otp_invalid\` \u2014 ask the user to re-check and run \`runtype auth verify\` again. An expired code returns \`otp_expired\` \u2014 re-send with \`runtype auth claim <email>\`. Codes expire after 10 minutes and lock after 5 wrong attempts, so confirm the code with the user rather than guessing; after a lockout or expiry, re-send with \`runtype auth claim <email>\`.
72848
73317
 
72849
- Do not manually create an API key, export the CLI key into MCP, or choose a different setup path merely to avoid a restart. CLI authentication and MCP OAuth are separate on purpose: the CLI is the first-session bridge; client-owned OAuth MCP is the durable connection.
73318
+ ### 3. Install skills and the MCP connection
72850
73319
 
72851
- ### 3. Keep working in this session with the CLI
73320
+ \`\`\`bash
73321
+ runtype install-mcp
73322
+ \`\`\`
73323
+
73324
+ This command detects the current coding harness, installs the Runtype agent skills for it, and writes the user-level hosted MCP configuration. If it asks which harness to configure, select the harness running this session. If it reports an OAuth action for the harness's own MCP connection, relay that instruction to the user for later \u2014 it happens on the harness's schedule, not during signup. That harness-side OAuth may open a browser when the user runs it; that is fine and expected \u2014 the no-browser rule above applies to Runtype signup, not to the harness's own connection ceremony.
73325
+
73326
+ ### 4. Keep working in this session with the CLI
72852
73327
 
72853
73328
  Do not stop just because the new MCP connection is not visible yet. Use CLI commands for useful first-session work:
72854
73329
 
@@ -72860,19 +73335,19 @@ runtype products list
72860
73335
  runtype --help
72861
73336
  \`\`\`
72862
73337
 
72863
- The CLI also supports dispatch, records, validation, product setup, and other account operations. Read command help before mutating resources, and report concrete IDs and URLs to the user.
73338
+ The CLI also supports dispatch, records, validation, product setup, and other account operations. Read command help before mutating resources, and report concrete IDs and URLs to the user. If you need Runtype MCP tools before the restart, the CLI bridges them: \`runtype mcp tools\` lists them and \`runtype mcp call <tool>\` invokes one \u2014 same hosted tools, no restart needed.
72864
73339
 
72865
- ### 4. Tell the user to restart or reload the harness
73340
+ ### 5. Tell the user to restart or reload the harness
72866
73341
 
72867
- Before finishing this session, read the final status and next steps from \`runtype install-mcp\`. Complete any OAuth action it requests and retry any failed skill installation. Do not call setup complete while the command reports \`action-required\`, \`partial\`, or an error.
73342
+ Before finishing this session, read the final status and next steps from \`runtype install-mcp\`. Retry any failed skill installation. Do not call setup complete while the command reports \`action-required\`, \`partial\`, or an error.
72868
73343
 
72869
- Only after skills and OAuth both succeed, tell the user exactly what changed and ask them to restart or reload the coding harness. Use direct language such as:
73344
+ Only after signup is verified and skills are installed, tell the user exactly what changed and ask them to restart or reload the coding harness. Use direct language such as:
72870
73345
 
72871
- > Runtype skills and the OAuth MCP connection are installed. Please restart or reload this coding agent (or start a new session) so it can activate the new MCP tools. I used the CLI to keep this first session productive; after restart I will use MCP by default.
73346
+ > Your Runtype account is verified and the skills plus MCP connection are installed. Please restart or reload this coding agent (or start a new session) so it can activate the new MCP tools. I used the CLI to keep this first session productive; after restart I will use MCP by default.
72872
73347
 
72873
73348
  If an action remains, report that exact action instead of using the success wording above. Do not claim the running agent can use MCP until its Runtype tools actually appear.
72874
73349
 
72875
- ### 5. Switch to MCP after restart
73350
+ ### 6. Switch to MCP after restart
72876
73351
 
72877
73352
  In the next session, use Runtype MCP first. Do not fall back to the CLI just because it worked previously. Start by calling:
72878
73353
 
@@ -72898,13 +73373,21 @@ runtype install-mcp --agent opencode
72898
73373
  runtype install-mcp --agent vscode
72899
73374
  \`\`\`
72900
73375
 
72901
- Use only the line for the current harness. Follow the command's OAuth and restart instructions. Use \`--force\` only when the command reports that an existing MCP server named \`runtype\` points to a different URL and the user agrees to replace it.
73376
+ Use only the line for the current harness. Follow the command's restart instructions. Use \`--force\` only when the command reports that an existing MCP server named \`runtype\` points to a different URL and the user agrees to replace it.
73377
+
73378
+ ## Auth reference
73379
+
73380
+ The signup commands wrap the agent auth protocol documented at \`https://runtype.com/auth.md\` (anonymous registration plus an email claim ceremony). You normally never need the raw protocol \u2014 the CLI is the supported client:
72902
73381
 
72903
- ## Headless exception \u2014 not the normal path
73382
+ - \`runtype auth register --email <email>\` \u2014 create the account and send the code (one step)
73383
+ - \`runtype auth claim <email>\` \u2014 re-send or change the email for a pending signup
73384
+ - \`runtype auth verify <code>\` \u2014 finish signup with the 6-digit code
73385
+ - \`runtype auth login --api-key <key>\` \u2014 existing accounts only
73386
+ - \`runtype auth status\` / \`runtype auth whoami\` \u2014 inspect the current login
72904
73387
 
72905
- Only use an API key when browser OAuth is genuinely impossible in the user's environment, not merely inconvenient. Ask the user before switching authentication modes, then follow the canonical headless registration and claim protocol at \`https://runtype.com/auth.md\`. Never print a secret into the transcript or write a bearer token directly into a committed config file.
73388
+ A user-supplied \`RUNTYPE_API_KEY\` environment variable also authenticates every CLI command without a stored login. Never print a credential into the transcript or write one into a committed file.
72906
73389
 
72907
- The hosted MCP endpoint is \`https://api.runtype.com/v1/mcp/protocol\`. A user-supplied \`RUNTYPE_API_KEY\` can authenticate CLI commands in a headless environment, but return to client-owned OAuth MCP when the harness can support it.
73390
+ Browser login (\`runtype auth login\` with no flags) exists for humans running the CLI in their own interactive terminal. Do not use it from a coding harness.
72908
73391
 
72909
73392
  ## After MCP is active
72910
73393
 
@@ -72923,7 +73406,7 @@ API: [api.runtype.com](https://api.runtype.com)
72923
73406
 
72924
73407
  // src/commands/onboard.ts
72925
73408
  init_credential_store();
72926
- var AUTH_LOGIN_INSTRUCTION = "Sign in to Runtype: run `runtype auth login` (or set RUNTYPE_API_KEY), then re-run `runtype onboard`.";
73409
+ var AUTH_LOGIN_INSTRUCTION = "Sign in to Runtype first. New account: `runtype auth register --email <email>` then `runtype auth verify <code>`. Existing account: `runtype auth login --api-key <key>` (or set RUNTYPE_API_KEY). Then re-run `runtype onboard`.";
72927
73410
  var AFTER_RESTART_STEPS = [
72928
73411
  "Use the Runtype MCP server first for all Runtype work.",
72929
73412
  "Start with the get_build_instructions tool before creating or changing resources.",
@@ -73208,7 +73691,7 @@ function collectBundleFiles(dir) {
73208
73691
  walk(dir);
73209
73692
  return zippable;
73210
73693
  }
73211
- function fail(message, jsonMode, code) {
73694
+ function fail2(message, jsonMode, code) {
73212
73695
  if (jsonMode) {
73213
73696
  printJson({ status: "error", error: message, code });
73214
73697
  } else {
@@ -73247,7 +73730,7 @@ appsCommand.command("list").description("List your apps").option("--json", "Stru
73247
73730
  console.log(` ${app.url} ${status} ${app.visibility}${serving}`);
73248
73731
  }
73249
73732
  } catch (error51) {
73250
- fail(
73733
+ fail2(
73251
73734
  `Failed to list apps: ${error51 instanceof Error ? error51.message : String(error51)}`,
73252
73735
  jsonMode,
73253
73736
  "LIST_FAILED"
@@ -73276,7 +73759,7 @@ appsCommand.command("create").description("Create an app (the hostname is {slug}
73276
73759
  console.log(` ${app.url}`);
73277
73760
  console.log(chalk43.dim(" Deploy a bundle with `runtype apps deploy <dir> --app " + app.id + "`"));
73278
73761
  } catch (error51) {
73279
- fail(
73762
+ fail2(
73280
73763
  `Failed to create app: ${error51 instanceof Error ? error51.message : String(error51)}`,
73281
73764
  jsonMode,
73282
73765
  "CREATE_FAILED"
@@ -73291,27 +73774,27 @@ appsCommand.command("deploy <dir>").description("Zip a bundle directory, upload
73291
73774
  const apiKey = await ensureAuth();
73292
73775
  if (!apiKey) return;
73293
73776
  if (!existsSync17(dir) || !statSync7(dir).isDirectory()) {
73294
- fail(`Not a directory: ${dir}`, jsonMode, "INVALID_DIR");
73777
+ fail2(`Not a directory: ${dir}`, jsonMode, "INVALID_DIR");
73295
73778
  }
73296
73779
  let bundleFiles = {};
73297
73780
  try {
73298
73781
  bundleFiles = collectBundleFiles(dir);
73299
73782
  } catch (error51) {
73300
- fail(
73783
+ fail2(
73301
73784
  `Failed to read ${dir}: ${error51 instanceof Error ? error51.message : String(error51)}`,
73302
73785
  jsonMode,
73303
73786
  "INVALID_DIR"
73304
73787
  );
73305
73788
  }
73306
73789
  if (!bundleFiles["index.html"]) {
73307
- fail(
73790
+ fail2(
73308
73791
  `${dir} is missing index.html at its root (symlinked files are not bundled).`,
73309
73792
  jsonMode,
73310
73793
  "MISSING_INDEX"
73311
73794
  );
73312
73795
  }
73313
73796
  if (!bundleFiles[MANIFEST_FILENAME]) {
73314
- fail(
73797
+ fail2(
73315
73798
  `${dir} is missing ${MANIFEST_FILENAME} at its root (symlinked files are not bundled). Every app bundle needs a manifest; see the build-runtype-app skill or the Runtype docs.`,
73316
73799
  jsonMode,
73317
73800
  "MISSING_MANIFEST"
@@ -73359,7 +73842,7 @@ appsCommand.command("deploy <dir>").description("Zip a bundle directory, upload
73359
73842
  console.log(chalk43.dim(` Uploaded but not activated. Activate with \`runtype apps activate ${options.app} ${version2.id}\``));
73360
73843
  }
73361
73844
  } catch (error51) {
73362
- fail(
73845
+ fail2(
73363
73846
  `Deploy failed: ${error51 instanceof Error ? error51.message : String(error51)}`,
73364
73847
  jsonMode,
73365
73848
  "DEPLOY_FAILED"
@@ -73390,7 +73873,7 @@ appsCommand.command("versions <appId>").description("List an app's versions, new
73390
73873
  );
73391
73874
  }
73392
73875
  } catch (error51) {
73393
- fail(
73876
+ fail2(
73394
73877
  `Failed to list versions: ${error51 instanceof Error ? error51.message : String(error51)}`,
73395
73878
  jsonMode,
73396
73879
  "VERSIONS_FAILED"
@@ -73412,7 +73895,7 @@ appsCommand.command("activate <appId> <versionId>").description("Activate an upl
73412
73895
  console.log(chalk43.green(`Activated version ${version2.versionNumber}`));
73413
73896
  console.log(` Live at ${chalk43.bold(app.url)}`);
73414
73897
  } catch (error51) {
73415
- fail(
73898
+ fail2(
73416
73899
  `Activation failed: ${error51 instanceof Error ? error51.message : String(error51)}`,
73417
73900
  jsonMode,
73418
73901
  "ACTIVATE_FAILED"
@@ -73433,7 +73916,7 @@ appsCommand.command("delete <appId>").description("Permanently delete an app, it
73433
73916
  }
73434
73917
  console.log(chalk43.green(`Deleted ${appId}`));
73435
73918
  } catch (error51) {
73436
- fail(
73919
+ fail2(
73437
73920
  `Delete failed: ${error51 instanceof Error ? error51.message : String(error51)}`,
73438
73921
  jsonMode,
73439
73922
  "DELETE_FAILED"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@runtypelabs/cli",
3
- "version": "2.30.1",
3
+ "version": "2.31.0",
4
4
  "description": "Command-line interface for Runtype AI platform",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -41,7 +41,7 @@
41
41
  "tsx": "^4.7.1",
42
42
  "typescript": "^6.0.3",
43
43
  "vitest": "^4.1.0",
44
- "@runtypelabs/shared": "3.4.3"
44
+ "@runtypelabs/shared": "3.5.0"
45
45
  },
46
46
  "engines": {
47
47
  "node": ">=22.12.0"