free-pi-cli 0.6.8 → 0.6.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -30,4 +30,4 @@ if (err) {
30
30
  console.error(err);
31
31
  process.exit(1);
32
32
  }
33
- await import("./main-tyjwqhr5.js");
33
+ await import("./main-cgxt6te1.js");
@@ -14382,7 +14382,8 @@ var GithubDeviceResponseSchema = exports_external.object({
14382
14382
  user_code: exports_external.string(),
14383
14383
  verification_uri: exports_external.string(),
14384
14384
  interval: exports_external.number(),
14385
- expires_in: exports_external.number().optional()
14385
+ expires_in: exports_external.number().optional(),
14386
+ verification_uri_complete: exports_external.string().optional()
14386
14387
  });
14387
14388
  var AuthTokenRequestSchema = exports_external.object({
14388
14389
  session_id: exports_external.string().uuid()
@@ -14449,8 +14450,15 @@ var GuestUnavailableResponseSchema = exports_external.object({
14449
14450
  message: exports_external.string(),
14450
14451
  reason: exports_external.enum(["disabled", "global_budget", "ip_sessions", "ip_allowance"])
14451
14452
  });
14453
+ var FunnelEventNameSchema = exports_external.enum([
14454
+ "device_code_shown",
14455
+ "browser_opened",
14456
+ "device_code_reminted",
14457
+ "device_code_expired",
14458
+ "device_poll_slow_down"
14459
+ ]);
14452
14460
  var FunnelEventRequestSchema = exports_external.object({
14453
- event: exports_external.enum(["device_code_shown", "browser_opened"]),
14461
+ event: FunnelEventNameSchema,
14454
14462
  session_id: exports_external.string().uuid().optional()
14455
14463
  });
14456
14464
  var ReturnBonusTelemetrySchema = exports_external.object({
@@ -14629,6 +14637,11 @@ var ConfigSchema = exports_external.object({
14629
14637
  maxOutputTokens: exports_external.number().int().min(1).default(32768),
14630
14638
  accountsPerDayCeiling: exports_external.number().int().default(500),
14631
14639
  authRatePerIpPerHour: exports_external.number().int().default(30),
14640
+ siteLoginStartRateLimitEnabled: exports_external.boolean().default(false),
14641
+ siteLoginStartPerIp: exports_external.number().int().min(0).default(5),
14642
+ siteLoginStartPerSubject: exports_external.number().int().min(0).default(5),
14643
+ siteLoginStartWindowSeconds: exports_external.number().int().min(1).default(600),
14644
+ siteLoginStartCooldownSeconds: exports_external.number().int().min(0).default(60),
14632
14645
  webBuyUrlRatePerProviderPerHour: exports_external.number().int().default(20),
14633
14646
  handoffTokenRatePerProviderPerHour: exports_external.number().int().default(60),
14634
14647
  handoffHourlyCompletionCap: exports_external.number().int().default(120),
@@ -14648,6 +14661,7 @@ var ConfigSchema = exports_external.object({
14648
14661
  lifecycleEmailSend: exports_external.boolean().default(false),
14649
14662
  emailProviderConfigured: exports_external.boolean().default(false),
14650
14663
  emailFrom: exports_external.string().optional(),
14664
+ emailPostalAddress: exports_external.string().optional(),
14651
14665
  emailProviderUrl: exports_external.string().optional(),
14652
14666
  hiveEnabled: exports_external.boolean().default(false),
14653
14667
  providerOrder: exports_external.array(exports_external.string()).optional(),
@@ -14748,6 +14762,9 @@ function envBoolean(value) {
14748
14762
  return;
14749
14763
  return value === "true" || value === "1";
14750
14764
  }
14765
+ function envFlagOn(value) {
14766
+ return ["true", "1", "on", "yes"].includes((value ?? "").trim().toLowerCase());
14767
+ }
14751
14768
  function envList(value) {
14752
14769
  if (!value)
14753
14770
  return;
@@ -14779,6 +14796,11 @@ function loadConfig(env = process.env) {
14779
14796
  maxOutputTokens: envNumber(env.MAX_OUTPUT_TOKENS),
14780
14797
  accountsPerDayCeiling: envNumber(env.ACCOUNTS_PER_DAY_CEILING),
14781
14798
  authRatePerIpPerHour: envNumber(env.AUTH_RATE_PER_IP_PER_HOUR),
14799
+ siteLoginStartRateLimitEnabled: envFlagOn(env.SITE_LOGIN_START_RATE_LIMIT),
14800
+ siteLoginStartPerIp: envNumber(env.SITE_LOGIN_START_PER_IP),
14801
+ siteLoginStartPerSubject: envNumber(env.SITE_LOGIN_START_PER_SUBJECT),
14802
+ siteLoginStartWindowSeconds: envNumber(env.SITE_LOGIN_START_WINDOW_SECONDS),
14803
+ siteLoginStartCooldownSeconds: envNumber(env.SITE_LOGIN_START_COOLDOWN_SECONDS),
14782
14804
  webBuyUrlRatePerProviderPerHour: envNumber(env.WEB_BUY_URL_RATE_PER_PROVIDER_PER_HOUR),
14783
14805
  handoffTokenRatePerProviderPerHour: envNumber(env.HANDOFF_TOKEN_RATE_PER_PROVIDER_PER_HOUR),
14784
14806
  handoffHourlyCompletionCap: envNumber(env.HANDOFF_HOURLY_COMPLETION_CAP),
@@ -14798,6 +14820,7 @@ function loadConfig(env = process.env) {
14798
14820
  lifecycleEmailSend: envBoolean(env.LIFECYCLE_EMAIL_SEND),
14799
14821
  emailProviderConfigured: env.EMAIL_PROVIDER_KEY ? env.EMAIL_PROVIDER_KEY.trim().length > 0 : undefined,
14800
14822
  emailFrom: env.EMAIL_FROM?.trim() || undefined,
14823
+ emailPostalAddress: env.EMAIL_POSTAL_ADDRESS?.trim() || undefined,
14801
14824
  emailProviderUrl: env.EMAIL_PROVIDER_URL?.trim() || undefined,
14802
14825
  hiveEnabled: envBoolean(env.HIVE_ENABLED),
14803
14826
  providerOrder: envList(env.PROVIDER_ORDER),
@@ -14936,42 +14959,46 @@ var LIMIT_NOTICE_LABELS = {
14936
14959
  concurrent_session: "another session is active",
14937
14960
  guest_allowance: "guest trial"
14938
14961
  };
14939
- var EXTRA_CREDITS_URL = "freepi.ai/extra-credits";
14940
14962
  var LIMIT_NOTICE_LOCAL_TIME_AFTER_SECONDS = 90 * 60;
14941
14963
  var REFILL_CLOCK_LIMITS = new Set(["daily_cap", "global_cap", "queue_wait"]);
14942
14964
  function formatLimitNotice(fields, opts = {}) {
14943
14965
  const slow = fields.lane === "slow" && fields.limit === "hourly_ceiling" && fields.hourly_ceiling != null;
14944
- const label = slow ? `You've used today's free allowance, so you're on the slow lane (${fields.hourly_ceiling.toLocaleString("en-US")} requests an hour).` : LIMIT_NOTICE_LABELS[fields.limit] ?? fields.limit.replace(/_/g, " ");
14945
- const parts = [label];
14946
14966
  const retry = fields.retry_after_seconds;
14947
- const waitBits = [];
14967
+ let when;
14948
14968
  if (retry != null && retry > 0) {
14949
14969
  const useClock = Boolean(opts.localTime) && Boolean(fields.clears_at) && retry >= LIMIT_NOTICE_LOCAL_TIME_AFTER_SECONDS;
14950
- if (useClock) {
14951
- const formatted = new Intl.DateTimeFormat("en-US", {
14952
- timeZone: opts.timeZone,
14953
- hour: "numeric",
14954
- minute: "2-digit"
14955
- }).format(new Date(fields.clears_at));
14956
- waitBits.push(`back at ${formatted}`);
14957
- } else {
14958
- waitBits.push(`back in ${Math.max(1, Math.ceil(retry / 60))} min`);
14959
- }
14970
+ when = useClock ? `at ${new Intl.DateTimeFormat("en-US", { timeZone: opts.timeZone, hour: "numeric", minute: "2-digit" }).format(new Date(fields.clears_at))}` : `in ${Math.max(1, Math.ceil(retry / 60))} min`;
14960
14971
  }
14961
- if (opts.copyStyle === "treatment" && REFILL_CLOCK_LIMITS.has(fields.limit) && fields.clears_at) {
14972
+ const resets = () => {
14973
+ if (opts.copyStyle !== "treatment" || !REFILL_CLOCK_LIMITS.has(fields.limit) || !fields.clears_at)
14974
+ return "";
14962
14975
  const clears = new Date(fields.clears_at);
14963
- if (!Number.isNaN(clears.getTime())) {
14964
- waitBits.push(`resets ${formatRefillEt(clears)}`);
14965
- }
14966
- }
14967
- if (waitBits.length > 0)
14968
- parts.push(waitBits.join(" · "));
14976
+ return Number.isNaN(clears.getTime()) ? "" : ` Resets ${formatRefillEt(clears)}.`;
14977
+ };
14978
+ const refillOrBuy = (lead) => `${lead} ${when ? `Credits refill ${when}, or keep going with /buy-credits.` : "Keep going with /buy-credits."}${resets()}`;
14969
14979
  if (slow) {
14970
- const wait = waitBits[0] ? ` ${waitBits[0][0].toUpperCase()}${waitBits[0].slice(1)} — or get` : " Get";
14971
- return `${label}${wait} credits at ${EXTRA_CREDITS_URL} to keep going at full speed.`;
14980
+ const label2 = `You've used today's free allowance, so you're on the slow lane (${fields.hourly_ceiling.toLocaleString("en-US")} requests an hour).`;
14981
+ const wait = when ? ` Back ${when} — or use` : " Use";
14982
+ return `${label2}${wait} /buy-credits to keep going at full speed.`;
14972
14983
  }
14973
- parts.push(`${fields.remaining_credits_today.toLocaleString("en-US")} credits left today`);
14974
- return parts.join(", ");
14984
+ switch (fields.limit) {
14985
+ case "guest_allowance":
14986
+ return fields.remaining_credits_today > 0 ? "Guest access is busy right now." : "Your guest trial is used up.";
14987
+ case "daily_cap":
14988
+ return refillOrBuy("You've used today's free credits.");
14989
+ case "rolling_cap":
14990
+ return `You've reached your usage limit for now.${when ? ` Back ${when}.` : ""}`;
14991
+ case "queue_wait":
14992
+ return `free-pi is busy right now. ${when ? `Try again ${when}, or keep going with /buy-credits.` : "Try again soon, or keep going with /buy-credits."}${resets()}`;
14993
+ case "global_cap":
14994
+ return `free-pi has reached its shared daily limit; this isn't your limit.${when ? ` Back ${when}.` : ""}${resets()}`;
14995
+ }
14996
+ const label = LIMIT_NOTICE_LABELS[fields.limit] ?? fields.limit.replace(/_/g, " ");
14997
+ const bits = [label];
14998
+ if (when)
14999
+ bits.push(`back ${when}`);
15000
+ bits.push(`${fields.remaining_credits_today.toLocaleString("en-US")} credits left today`);
15001
+ return bits.join(", ");
14975
15002
  }
14976
15003
  // ../shared/src/version.ts
14977
15004
  function compareVersions(a, b) {
@@ -15102,7 +15129,7 @@ var HiveClassifyResponseSchema = exports_external.object({
15102
15129
  // package.json
15103
15130
  var package_default = {
15104
15131
  name: "free-pi-cli",
15105
- version: "0.6.8",
15132
+ version: "0.6.9",
15106
15133
  description: "Free, ad-supported distribution of the pi coding agent. Inference paid for by ads.",
15107
15134
  license: "MIT",
15108
15135
  type: "module",
@@ -15739,13 +15766,13 @@ async function renderMeter(deps, ctx) {
15739
15766
  confirm = { addedUsd: added, turnsLeft: CONFIRM_TURNS };
15740
15767
  if (confirm && confirm.turnsLeft > 0) {
15741
15768
  confirm.turnsLeft--;
15742
- ctx.ui.setWidget(METER_WIDGET_ID, [renderErrorLine(creditAddedMessage(confirm.addedUsd), theme)]);
15769
+ ctx.ui.setWidget(METER_WIDGET_ID, [theme.fg("accent", creditAddedMessage(confirm.addedUsd))]);
15743
15770
  return;
15744
15771
  }
15745
15772
  confirm = undefined;
15746
15773
  const notice = typeof me.notice === "string" ? sanitizeText(me.notice).trim() : "";
15747
15774
  if (notice !== "") {
15748
- ctx.ui.setWidget(METER_WIDGET_ID, [renderErrorLine(notice, theme)]);
15775
+ ctx.ui.setWidget(METER_WIDGET_ID, [theme.fg("accent", notice)]);
15749
15776
  return;
15750
15777
  }
15751
15778
  if (me.lane === "fallback") {
@@ -21195,6 +21222,22 @@ function formatLimitEnvelope(env, opts) {
21195
21222
  hourly_ceiling: env.hourly_ceiling
21196
21223
  }, { localTime: true, timeZone: opts?.timeZone, copyStyle: env.copy_variant });
21197
21224
  }
21225
+ var LIMIT_CODES = new Set([
21226
+ "guest_allowance",
21227
+ "daily_cap",
21228
+ "queue_wait",
21229
+ "rolling_cap",
21230
+ "hourly_ceiling",
21231
+ "concurrent",
21232
+ "concurrent_session",
21233
+ "model_requires_credits",
21234
+ "global_cap",
21235
+ "upgrade_required",
21236
+ "guest_unavailable",
21237
+ "account_review",
21238
+ "ads_required",
21239
+ "region_unavailable"
21240
+ ]);
21198
21241
  function createErrorNoticeExtension(opts = {}) {
21199
21242
  const lastAt = new Map;
21200
21243
  const now = opts.now ?? Date.now;
@@ -21213,10 +21256,10 @@ function createErrorNoticeExtension(opts = {}) {
21213
21256
  if (t - (lastAt.get(env.code) ?? -Infinity) < NOTICE_DEDUPE_MS)
21214
21257
  return;
21215
21258
  lastAt.set(env.code, t);
21216
- ctx.ui.notify(formatLimitEnvelope(env, { timeZone }), env.status >= 500 ? "error" : "warning");
21217
21259
  const extra = opts.extraNotice?.(env);
21218
- if (extra)
21219
- ctx.ui.notify(extra, "info");
21260
+ const text = formatLimitEnvelope(env, { timeZone }) + (extra ? `
21261
+ ${extra}` : "");
21262
+ ctx.ui.notify(text, LIMIT_CODES.has(env.code) ? "info" : env.status >= 500 ? "error" : "warning");
21220
21263
  });
21221
21264
  }
21222
21265
  };
@@ -21554,6 +21597,14 @@ function mergeAttribution(a, b) {
21554
21597
 
21555
21598
  // src/changelog.ts
21556
21599
  var CHANGELOG_HIGHLIGHTS = [
21600
+ {
21601
+ version: "0.6.9",
21602
+ highlights: [
21603
+ "When the guest trial runs out, sign-in opens in your browser automatically, and the link and code stay on screen.",
21604
+ "An expired sign-in takes you back to GitHub sign-in instead of starting a new guest trial.",
21605
+ "Limits (daily credits, slow lane, busy, usage limit) now show as information with a next step, not as errors."
21606
+ ]
21607
+ },
21557
21608
  {
21558
21609
  version: "0.6.8",
21559
21610
  highlights: [
@@ -21976,8 +22027,18 @@ class LoginDeniedError extends Error {
21976
22027
  super("login_denied");
21977
22028
  }
21978
22029
  }
22030
+
22031
+ class DeviceCodeExpiredError extends Error {
22032
+ constructor() {
22033
+ super("expired_token");
22034
+ }
22035
+ }
22036
+ function isExpiredTokenPoll(body) {
22037
+ return body?.message === "expired_token";
22038
+ }
21979
22039
  async function pollForToken(baseUrl, sessionId, interval, sleep, opts = {}) {
21980
22040
  let consecutiveNetworkFailures = 0;
22041
+ let waitSeconds = Math.max(0, interval);
21981
22042
  const now = opts.now ?? Date.now;
21982
22043
  for (;; ) {
21983
22044
  if (opts.deadline !== undefined && now() >= opts.deadline) {
@@ -22000,7 +22061,7 @@ async function pollForToken(baseUrl, sessionId, interval, sleep, opts = {}) {
22000
22061
  if (consecutiveNetworkFailures >= 6) {
22001
22062
  throw new Error(`token poll failed: network unreachable after ${consecutiveNetworkFailures} attempts (${err instanceof Error ? err.message : String(err)})`);
22002
22063
  }
22003
- await sleep(Math.max(interval, 5) * 1000);
22064
+ await sleep(Math.max(waitSeconds, 5) * 1000);
22004
22065
  continue;
22005
22066
  }
22006
22067
  if (!res.ok) {
@@ -22012,6 +22073,8 @@ async function pollForToken(baseUrl, sessionId, interval, sleep, opts = {}) {
22012
22073
  throw new LoginDeniedError;
22013
22074
  if (res.status === 410 && body?.code === "expired")
22014
22075
  throw new LoginTimeoutError;
22076
+ if (isExpiredTokenPoll(body))
22077
+ throw new DeviceCodeExpiredError;
22015
22078
  throw new Error(refusalMessage(res.status, body) ?? `token poll failed: HTTP ${res.status}`);
22016
22079
  }
22017
22080
  const parsed = AuthTokenResponseSchema.parse(await res.json());
@@ -22021,7 +22084,13 @@ async function pollForToken(baseUrl, sessionId, interval, sleep, opts = {}) {
22021
22084
  throw new Error("consent_required: server did not record consent before polling");
22022
22085
  }
22023
22086
  const retryAfter = "retry_after" in parsed && typeof parsed.retry_after === "number" ? parsed.retry_after : 0;
22024
- await sleep(Math.max(interval, retryAfter) * 1000);
22087
+ if (retryAfter > waitSeconds) {
22088
+ waitSeconds = retryAfter;
22089
+ opts.onSlowDown?.(waitSeconds);
22090
+ } else if (retryAfter > 0) {
22091
+ opts.onSlowDown?.(waitSeconds);
22092
+ }
22093
+ await sleep(waitSeconds * 1000);
22025
22094
  }
22026
22095
  }
22027
22096
  async function checkToken(baseUrl, jwt2) {
@@ -22114,6 +22183,7 @@ function clipboardCommand(platform) {
22114
22183
  var CONSENT_TEXT = `
22115
22184
  free-pi is free. In exchange, we show you ads and may train on your data.
22116
22185
  If you want to use free-pi, you accept that. If you don't, don't use it.
22186
+ You must be at least 13 in the US, 16 in the EEA/UK, or 18 elsewhere.
22117
22187
  Terms: https://freepi.ai/terms Privacy: https://freepi.ai/privacy
22118
22188
  `.trim();
22119
22189
  var CONSENT_VERSION = "4";
@@ -22291,6 +22361,37 @@ var LOGIN_REWARD_LINE = "Sign in with GitHub to get 15,000 free credits a day";
22291
22361
  var LOGIN_WAITING_LINE = "Waiting for GitHub…";
22292
22362
  var LOGIN_TIMEOUT_LINE = "GitHub sign-in timed out. Run /login to try again.";
22293
22363
  var BROWSER_FAILED_LINE = "Couldn't open a browser here. Open the URL above yourself and enter the code.";
22364
+ var LOGIN_CODE_EXPIRED_LINE = "That code expired. Enter this new one before it times out.";
22365
+ var LOGIN_CODE_EXPIRY_HINT = "Enter the code on GitHub before it expires.";
22366
+ var MAX_DEVICE_CODE_REMINTS = 2;
22367
+ function formatRemaining(seconds) {
22368
+ const s = Math.max(0, Math.floor(seconds));
22369
+ if (s >= 90)
22370
+ return `${Math.round(s / 60)} min`;
22371
+ if (s >= 60)
22372
+ return "1 min";
22373
+ if (s > 0)
22374
+ return `${s} sec`;
22375
+ return "0 sec";
22376
+ }
22377
+ function formatCodeExpiryLine(expiresInSeconds) {
22378
+ return ` Code expires in ${formatRemaining(expiresInSeconds)}. ${LOGIN_CODE_EXPIRY_HINT}`;
22379
+ }
22380
+ function formatWaitingLine(expiresInSeconds) {
22381
+ return `${LOGIN_WAITING_LINE} code expires in ${formatRemaining(expiresInSeconds)}.`;
22382
+ }
22383
+ function deviceBrowserUrl(device) {
22384
+ if (device.verification_uri_complete?.trim())
22385
+ return device.verification_uri_complete.trim();
22386
+ try {
22387
+ const url2 = new URL(device.verification_uri);
22388
+ if (!url2.searchParams.has("user_code"))
22389
+ url2.searchParams.set("user_code", device.user_code);
22390
+ return url2.toString();
22391
+ } catch {
22392
+ return device.verification_uri;
22393
+ }
22394
+ }
22294
22395
  function formatDeviceCodePrompt(device, variant) {
22295
22396
  const value = isHandoffTreatment(variant) ? LOGIN_VALUE_LINE : LOGIN_REWARD_LINE;
22296
22397
  return [
@@ -22300,12 +22401,51 @@ function formatDeviceCodePrompt(device, variant) {
22300
22401
  "",
22301
22402
  ` Open ${device.verification_uri}`,
22302
22403
  ` Enter code: ${device.user_code}`,
22404
+ formatCodeExpiryLine(device.expires_in ?? 900),
22303
22405
  ""
22304
22406
  ].join(`
22305
22407
  `);
22306
22408
  }
22307
22409
  async function runDeviceLogin(deps, attribution = {}) {
22410
+ let remints = 0;
22411
+ let lastSessionId;
22412
+ for (;; ) {
22413
+ try {
22414
+ return await runDeviceLoginOnce(deps, attribution, {
22415
+ reminted: remints > 0,
22416
+ onSession: (id) => {
22417
+ lastSessionId = id;
22418
+ }
22419
+ });
22420
+ } catch (err) {
22421
+ const expired = err instanceof DeviceCodeExpiredError || err instanceof LoginTimeoutError;
22422
+ if (!expired || remints >= MAX_DEVICE_CODE_REMINTS) {
22423
+ throw err instanceof DeviceCodeExpiredError ? new LoginTimeoutError : err;
22424
+ }
22425
+ if (lastSessionId) {
22426
+ await reportFunnelEvent(deps.baseUrl, "device_code_expired", {
22427
+ sessionId: lastSessionId,
22428
+ jwt: deps.jwt,
22429
+ fetchImpl: deps.fetchImpl
22430
+ });
22431
+ }
22432
+ remints += 1;
22433
+ deps.log(`
22434
+ ${LOGIN_CODE_EXPIRED_LINE}
22435
+ `);
22436
+ }
22437
+ }
22438
+ }
22439
+ async function runDeviceLoginOnce(deps, attribution, once) {
22308
22440
  const device = await startDeviceFlow(deps.baseUrl);
22441
+ once.onSession(device.session_id);
22442
+ if (once.reminted) {
22443
+ await reportFunnelEvent(deps.baseUrl, "device_code_reminted", {
22444
+ sessionId: device.session_id,
22445
+ jwt: deps.jwt,
22446
+ fetchImpl: deps.fetchImpl
22447
+ });
22448
+ }
22309
22449
  deps.log(formatDeviceCodePrompt(device, deps.variant));
22310
22450
  if (deps.credentialsPath) {
22311
22451
  await savePendingLogin(deps.credentialsPath, {
@@ -22319,10 +22459,12 @@ async function runDeviceLogin(deps, attribution = {}) {
22319
22459
  if (copied)
22320
22460
  deps.log(` (code copied to clipboard)
22321
22461
  `);
22322
- deps.log(LOGIN_WAITING_LINE);
22462
+ const expiresIn = device.expires_in ?? 900;
22463
+ deps.log(formatWaitingLine(expiresIn));
22464
+ const openUrl = deviceBrowserUrl(device);
22323
22465
  let browserOpened = false;
22324
22466
  try {
22325
- browserOpened = await deps.openBrowser(device.verification_uri) !== false;
22467
+ browserOpened = await deps.openBrowser(openUrl) !== false;
22326
22468
  } catch {
22327
22469
  browserOpened = false;
22328
22470
  }
@@ -22336,16 +22478,23 @@ async function runDeviceLogin(deps, attribution = {}) {
22336
22478
  deps.log(copied ? BROWSER_FAILED_LINE : `${BROWSER_FAILED_LINE} ${HEADLESS_FALLBACK_LINE}`);
22337
22479
  }
22338
22480
  await submitConsent(deps.baseUrl, device.session_id, CONSENT_VERSION, attribution);
22339
- const expiresInMs = (device.expires_in ?? 900) * 1000;
22340
- const deadline = (deps.now ?? Date.now)() + expiresInMs;
22341
- try {
22342
- const token = await pollForToken(deps.baseUrl, device.session_id, device.interval, deps.sleep, { deadline, now: deps.now, jwt: deps.jwt, fetchImpl: deps.fetchImpl });
22343
- if (deps.credentialsPath)
22344
- await clearPendingLogin(deps.credentialsPath);
22345
- return token;
22346
- } catch (err) {
22347
- throw err;
22348
- }
22481
+ const deadline = (deps.now ?? Date.now)() + expiresIn * 1000;
22482
+ const token = await pollForToken(deps.baseUrl, device.session_id, device.interval, deps.sleep, {
22483
+ deadline,
22484
+ now: deps.now,
22485
+ jwt: deps.jwt,
22486
+ fetchImpl: deps.fetchImpl,
22487
+ onSlowDown: () => {
22488
+ reportFunnelEvent(deps.baseUrl, "device_poll_slow_down", {
22489
+ sessionId: device.session_id,
22490
+ jwt: deps.jwt,
22491
+ fetchImpl: deps.fetchImpl
22492
+ });
22493
+ }
22494
+ });
22495
+ if (deps.credentialsPath)
22496
+ await clearPendingLogin(deps.credentialsPath);
22497
+ return token;
22349
22498
  }
22350
22499
  var CONNECT_BROWSER_FAILED_LINE = "Couldn't open a browser here, so using the GitHub device code instead.";
22351
22500
  var CONNECT_FALLBACK_LINE = "Couldn't start the browser sign-in, so using the GitHub device code instead.";
@@ -22566,7 +22715,25 @@ async function openPrivacyPolicy(opts, ctx) {
22566
22715
  function resolveHandoffVariant(opts) {
22567
22716
  return typeof opts.variant === "function" ? opts.variant() : opts.variant;
22568
22717
  }
22569
- async function runLogin(opts, ctx) {
22718
+ var LOGIN_ATTEMPT_MAX_MS = 900000;
22719
+ var loginAttempt;
22720
+ async function runLogin(opts, ctx, lead) {
22721
+ const startedAt = (opts.now ?? Date.now)();
22722
+ if (loginAttempt && startedAt - loginAttempt.startedAt < LOGIN_ATTEMPT_MAX_MS)
22723
+ return false;
22724
+ const attempt = { startedAt };
22725
+ loginAttempt = attempt;
22726
+ try {
22727
+ return await runLoginUnguarded(opts, ctx, lead);
22728
+ } finally {
22729
+ if (loginAttempt === attempt)
22730
+ loginAttempt = undefined;
22731
+ }
22732
+ }
22733
+ async function runLoginUnguarded(opts, ctx, lead) {
22734
+ const lines = lead ? [lead] : [];
22735
+ if (lead)
22736
+ ctx.ui.notify(lead, "info");
22570
22737
  try {
22571
22738
  const attribution = opts.agentDir ? await loadAttribution(getAttributionPath(opts.agentDir)) : {};
22572
22739
  const variant = resolveHandoffVariant(opts);
@@ -22574,7 +22741,11 @@ async function runLogin(opts, ctx) {
22574
22741
  baseUrl: opts.baseUrl,
22575
22742
  openBrowser: opts.openBrowserImpl ?? openBrowser,
22576
22743
  sleep: (ms) => new Promise((r) => setTimeout(r, ms)),
22577
- log: (message) => ctx.ui.notify(message.trim(), "info"),
22744
+ log: (message) => {
22745
+ lines.push(message.trim());
22746
+ ctx.ui.notify(lines.filter(Boolean).join(`
22747
+ `), "info");
22748
+ },
22578
22749
  jwt: await opts.getToken(),
22579
22750
  fetchImpl: opts.fetchImpl,
22580
22751
  variant,
@@ -22584,31 +22755,30 @@ async function runLogin(opts, ctx) {
22584
22755
  await saveJwt(opts.credentialsPath, token);
22585
22756
  opts.onLogin?.(token);
22586
22757
  ctx.ui.notify(isHandoffTreatment(variant) ? SIGNED_IN_CONTINUE_LINE : "Signed in. You now have 15,000 free credits a day, plus a one-time comeback bonus.", "info");
22758
+ return true;
22587
22759
  } catch (err) {
22588
22760
  if (err instanceof LoginTimeoutError) {
22589
- ctx.ui.notify(LOGIN_TIMEOUT_LINE, "warning");
22590
- return;
22761
+ ctx.ui.notify(LOGIN_TIMEOUT_LINE, "info");
22762
+ return false;
22591
22763
  }
22592
22764
  if (err instanceof LoginDeniedError) {
22593
- ctx.ui.notify(LOGIN_DENIED_LINE, "warning");
22594
- return;
22765
+ ctx.ui.notify(LOGIN_DENIED_LINE, "info");
22766
+ return false;
22595
22767
  }
22596
22768
  const reason = err instanceof Error && err.message ? ` (${err.message})` : "";
22597
22769
  ctx.ui.notify(`Couldn't finish GitHub sign-in${reason}. Run /login to try again.`, "error");
22770
+ return false;
22598
22771
  }
22599
22772
  }
22600
22773
  function installLoginHandler(mode, opts) {
22601
22774
  const m = mode;
22602
22775
  mode.handleLoginCommand = () => {
22603
- const lines = [];
22604
22776
  const notify2 = (message, type) => {
22605
22777
  if (type === "error")
22606
22778
  return m.showError(message);
22607
22779
  if (type === "warning")
22608
22780
  return m.showWarning(message);
22609
- lines.push(message);
22610
- m.showStatus(lines.join(`
22611
- `));
22781
+ m.showStatus(message);
22612
22782
  };
22613
22783
  return runLogin(opts, { ui: { notify: notify2 } });
22614
22784
  };
@@ -22727,14 +22897,24 @@ function loadRea(pi, opts, ctx) {
22727
22897
  }
22728
22898
  ctx.ui.notify("Loading REA (reverse-engineering tools) for this session. The first run downloads rea-agents, so it can take a minute. Deep native analysis needs your own Hopper, Ghidra, or IDA install.", "info");
22729
22899
  }
22900
+ var GUEST_WALL_LINE = "Your guest trial is used up. Opening GitHub sign-in in your browser… (15,000 free credits a day)";
22901
+ var GUEST_BUSY_LINE = "Guest access is busy right now. Opening GitHub sign-in in your browser… (15,000 free credits a day)";
22902
+ var GUEST_UNAVAILABLE_LINE = "Sign in with GitHub to keep going. Opening your browser… (15,000 free credits a day)";
22730
22903
  function createGuestLoginExtension(opts) {
22731
22904
  let started = false;
22905
+ let inFlight = false;
22906
+ let signedIn = false;
22732
22907
  let nearWallWarned = false;
22733
- const start = (ctx) => {
22908
+ const start = (ctx, line) => {
22734
22909
  started = true;
22910
+ inFlight = true;
22735
22911
  const variant = resolveHandoffVariant(opts);
22736
- ctx.ui.notify(isHandoffTreatment(variant) ? LOGIN_VALUE_LINE : LOGIN_REWARD_LINE, "info");
22737
- runLogin(opts, ctx);
22912
+ const lead = line ?? (isHandoffTreatment(variant) ? LOGIN_VALUE_LINE : LOGIN_REWARD_LINE);
22913
+ runLogin(opts, ctx, lead).then((ok) => {
22914
+ signedIn = ok;
22915
+ }).catch(() => {}).finally(() => {
22916
+ inFlight = false;
22917
+ });
22738
22918
  };
22739
22919
  return {
22740
22920
  name: "free-pi-guest-login",
@@ -22743,8 +22923,12 @@ function createGuestLoginExtension(opts) {
22743
22923
  if (!opts.isGuest?.())
22744
22924
  return;
22745
22925
  const msg = event.message;
22746
- if (!started && msg?.stopReason === "error" && parseErrorEnvelope(msg.errorMessage)?.code === "guest_allowance") {
22747
- start(ctx);
22926
+ const envelope = msg?.stopReason === "error" ? parseErrorEnvelope(msg.errorMessage) : undefined;
22927
+ if (envelope && (envelope.code === "guest_allowance" || envelope.code === "guest_unavailable" || envelope.prompt === "github_signin")) {
22928
+ if (!inFlight && !signedIn) {
22929
+ const busy = envelope.code === "guest_allowance" && (envelope.remaining_credits_today ?? 0) > 0;
22930
+ start(ctx, envelope.code === "guest_unavailable" ? GUEST_UNAVAILABLE_LINE : busy ? GUEST_BUSY_LINE : GUEST_WALL_LINE);
22931
+ }
22748
22932
  return;
22749
22933
  }
22750
22934
  if (started || nearWallWarned || !isHandoffTreatment(resolveHandoffVariant(opts)))
@@ -25397,32 +25581,56 @@ async function resolveAttribution(agentDir, argv) {
25397
25581
  await saveAttribution(getAttributionPath(agentDir), merged);
25398
25582
  return { attribution: merged };
25399
25583
  }
25400
- async function loginFlow(deps, argv = []) {
25401
- const resolved = await resolveAttribution(deps.agentDir, argv);
25402
- if ("error" in resolved && resolved.error) {
25403
- throw new Error(resolved.error);
25584
+ var GUEST_SIGNIN_PROMPT = "You're using free-pi as a guest. Press Enter to sign in with GitHub (15,000 free credits a day), or Esc to keep going as a guest.";
25585
+ function parseGuestSigninKey(raw) {
25586
+ if (raw === "\r" || raw === `
25587
+ `)
25588
+ return "signin";
25589
+ if (raw === "\x1B")
25590
+ return "guest";
25591
+ if (raw === "\x03")
25592
+ return "aborted";
25593
+ return;
25594
+ }
25595
+ async function promptGuestSigninInteractive() {
25596
+ let prompt = `
25597
+ ${GUEST_SIGNIN_PROMPT}
25598
+ `;
25599
+ for (;; ) {
25600
+ const choice = parseGuestSigninKey(await readOneKeypress(prompt));
25601
+ prompt = "";
25602
+ if (choice)
25603
+ return choice;
25404
25604
  }
25605
+ }
25606
+ function storedTokenKind(jwt2) {
25405
25607
  try {
25406
- const token = await runLoginAuto({
25407
- baseUrl: deps.baseUrl,
25408
- openBrowser: deps.openBrowser,
25409
- sleep: deps.sleep,
25410
- log: deps.log,
25411
- jwt: await loadJwt(deps.credentialsPath),
25412
- credentialsPath: deps.credentialsPath
25413
- }, resolved.attribution ?? {}, { forceDevice: argv.includes("--device-login"), env: deps.env, platform: deps.platform });
25414
- await saveJwt(deps.credentialsPath, token);
25415
- return token;
25416
- } catch (err) {
25417
- if (err instanceof LoginTimeoutError) {
25418
- deps.log(LOGIN_TIMEOUT_LINE);
25419
- }
25420
- if (err instanceof LoginDeniedError) {
25421
- deps.log(LOGIN_DENIED_LINE);
25422
- }
25423
- throw err;
25608
+ const claims = JSON.parse(Buffer.from(jwt2.split(".")[1] ?? "", "base64url").toString("utf8"));
25609
+ if (typeof claims !== "object" || claims === null)
25610
+ return "unknown";
25611
+ return claims.guest === true ? "guest" : "github";
25612
+ } catch {
25613
+ return "unknown";
25424
25614
  }
25425
25615
  }
25616
+ async function loginFlow(deps, argv = [], priorJwt) {
25617
+ const resolved = await resolveAttribution(deps.agentDir, argv);
25618
+ if ("error" in resolved && resolved.error) {
25619
+ throw new Error(resolved.error);
25620
+ }
25621
+ const token = await runLoginAuto({
25622
+ baseUrl: deps.baseUrl,
25623
+ openBrowser: deps.openBrowser,
25624
+ sleep: deps.sleep,
25625
+ log: deps.log,
25626
+ jwt: priorJwt ?? await loadJwt(deps.credentialsPath),
25627
+ credentialsPath: deps.credentialsPath
25628
+ }, resolved.attribution ?? {}, { forceDevice: argv.includes("--device-login"), env: deps.env, platform: deps.platform });
25629
+ await saveJwt(deps.credentialsPath, token);
25630
+ return token;
25631
+ }
25632
+ var SIGNIN_UNFINISHED_LINE = "Sign-in didn't finish. Run npx free-pi-cli again when you're ready.";
25633
+ var DECLINED_CALM_LINE = "OK, free-pi won't start. Run it again if you change your mind.";
25426
25634
  function validateSessionId(value) {
25427
25635
  try {
25428
25636
  SessionManager2.inMemory(process.cwd(), { id: value });
@@ -25498,6 +25706,12 @@ ${FINISH_SIGNIN_LINE}
25498
25706
  updateLatest = handled.updateLatest;
25499
25707
  versionHandled = true;
25500
25708
  }
25709
+ const interactive = deps.stdinIsTTY ?? process.stdin.isTTY === true;
25710
+ const canPrompt = interactive && !resumeFlags.hasPrompt && !argv.some((a) => a === "-p" || a === "--print");
25711
+ let rejectedJwt;
25712
+ let rejectedKind = "unknown";
25713
+ let startedGuestThisRun = false;
25714
+ let signedInAfterExpiry = false;
25501
25715
  let guestHandoffVariant;
25502
25716
  let showFirstTask = false;
25503
25717
  let consentCurrent = false;
@@ -25507,8 +25721,10 @@ ${FINISH_SIGNIN_LINE}
25507
25721
  consentCurrent = serverConsent === CONSENT_VERSION;
25508
25722
  if (serverConsent !== undefined && serverConsent !== CONSENT_VERSION) {
25509
25723
  if (!await deps.promptConsent(REPROMPT_CONSENT_TEXT)) {
25510
- deps.log("Declined. free-pi needs you to accept the updated terms to continue; you will be asked again next time.");
25511
- return 1;
25724
+ if (!interactive)
25725
+ return 1;
25726
+ deps.log(DECLINED_CALM_LINE);
25727
+ return 0;
25512
25728
  }
25513
25729
  consentCurrent = true;
25514
25730
  try {
@@ -25520,6 +25736,8 @@ ${FINISH_SIGNIN_LINE}
25520
25736
  }
25521
25737
  } catch (err) {
25522
25738
  if (err instanceof UnauthorizedError) {
25739
+ rejectedKind = storedTokenKind(jwt2);
25740
+ rejectedJwt = jwt2;
25523
25741
  await clearJwt(deps.credentialsPath);
25524
25742
  jwt2 = null;
25525
25743
  }
@@ -25528,9 +25746,45 @@ ${FINISH_SIGNIN_LINE}
25528
25746
  if (!jwt2) {
25529
25747
  const accepted = await deps.promptConsent();
25530
25748
  if (!accepted) {
25531
- deps.log("Declined. Exiting.");
25532
- return 1;
25749
+ if (!interactive)
25750
+ return 1;
25751
+ deps.log(DECLINED_CALM_LINE);
25752
+ return 0;
25753
+ }
25754
+ if (canPrompt && rejectedKind !== "unknown") {
25755
+ try {
25756
+ if (rejectedKind === "github") {
25757
+ deps.log(`
25758
+ Your free-pi sign-in has expired. Signing in with GitHub again.
25759
+ `);
25760
+ }
25761
+ const choice = rejectedKind === "github" ? "signin" : await (deps.promptGuestSignin ?? promptGuestSigninInteractive)();
25762
+ if (choice === "aborted") {
25763
+ deps.log("Aborted.");
25764
+ return 1;
25765
+ }
25766
+ if (choice === "signin") {
25767
+ jwt2 = await loginFlow(deps, argv, rejectedKind === "guest" ? rejectedJwt : undefined);
25768
+ consentCurrent = true;
25769
+ signedInAfterExpiry = true;
25770
+ }
25771
+ } catch (err) {
25772
+ if (rejectedKind === "github") {
25773
+ if (err instanceof LoginTimeoutError || err instanceof LoginDeniedError) {
25774
+ deps.log(SIGNIN_UNFINISHED_LINE);
25775
+ return 0;
25776
+ }
25777
+ const reason = err instanceof Error && err.message ? err.message : "unknown error";
25778
+ deps.log(`Couldn't finish GitHub sign-in: ${reason}. Run npx free-pi-cli again to retry.`);
25779
+ return 1;
25780
+ }
25781
+ deps.log(`
25782
+ Couldn't finish GitHub sign-in. Continuing as a guest.
25783
+ `);
25784
+ }
25533
25785
  }
25786
+ }
25787
+ if (!jwt2) {
25534
25788
  showIntro(deps.log);
25535
25789
  const resolved = await resolveAttribution(deps.agentDir, argv);
25536
25790
  if ("error" in resolved && resolved.error) {
@@ -25542,6 +25796,7 @@ ${FINISH_SIGNIN_LINE}
25542
25796
  const guest = await startGuestSession(deps.baseUrl, CONSENT_VERSION, attribution);
25543
25797
  await saveJwt(deps.credentialsPath, guest.token);
25544
25798
  jwt2 = guest.token;
25799
+ startedGuestThisRun = true;
25545
25800
  deps.log(isHandoffTreatment(guest.guestHandoffVariant) ? `
25546
25801
  You're on a short guest trial — ads pay for it. ${LOGIN_VALUE_LINE}
25547
25802
  ` : `
@@ -25560,11 +25815,19 @@ You're on a short guest trial — ads pay for it. Sign in with GitHub anytime (/
25560
25815
  deps.log(`
25561
25816
  Guest trial is not available right now.
25562
25817
  `);
25563
- jwt2 = await loginFlow(deps, argv);
25818
+ try {
25819
+ jwt2 = await loginFlow(deps, argv);
25820
+ } catch (loginErr) {
25821
+ if (loginErr instanceof LoginTimeoutError || loginErr instanceof LoginDeniedError) {
25822
+ deps.log(SIGNIN_UNFINISHED_LINE);
25823
+ return 0;
25824
+ }
25825
+ throw loginErr;
25826
+ }
25564
25827
  }
25565
25828
  consentCurrent = true;
25566
25829
  }
25567
- if (!hadJwt)
25830
+ if (!hadJwt || signedInAfterExpiry)
25568
25831
  versionCheck = await deps.checkClientVersion(deps.baseUrl, CLI_VERSION, jwt2);
25569
25832
  if (!versionHandled && versionCheck.action !== "ok") {
25570
25833
  const handled = await handleVersionAction(deps, versionCheck);
@@ -25577,13 +25840,37 @@ Guest trial is not available right now.
25577
25840
  deps.log(whatsNew);
25578
25841
  if (versionCheck.notice)
25579
25842
  deps.log(versionCheck.notice);
25843
+ let me = await fetchMe2(deps.baseUrl, jwt2).catch(() => null);
25844
+ if (me?.guest && !startedGuestThisRun && canPrompt && consentCurrent) {
25845
+ try {
25846
+ const choice = await (deps.promptGuestSignin ?? promptGuestSigninInteractive)();
25847
+ if (choice === "aborted") {
25848
+ deps.log("Aborted.");
25849
+ return 1;
25850
+ }
25851
+ if (choice === "signin") {
25852
+ jwt2 = await loginFlow(deps, argv);
25853
+ me = await fetchMe2(deps.baseUrl, jwt2).catch(() => null);
25854
+ versionCheck = await deps.checkClientVersion(deps.baseUrl, CLI_VERSION, jwt2).catch(() => versionCheck);
25855
+ if (versionCheck.action !== "ok") {
25856
+ const handled = await handleVersionAction(deps, versionCheck);
25857
+ if (handled.code !== undefined)
25858
+ return handled.code;
25859
+ updateLatest = handled.updateLatest;
25860
+ }
25861
+ }
25862
+ } catch {
25863
+ deps.log(`
25864
+ Couldn't finish GitHub sign-in. Continuing as a guest.
25865
+ `);
25866
+ }
25867
+ }
25580
25868
  let getGravityContext;
25581
25869
  if (consentCurrent) {
25582
25870
  try {
25583
25871
  getGravityContext = deps.startGravityPixel?.(jwt2);
25584
25872
  } catch {}
25585
25873
  }
25586
- const me = await fetchMe2(deps.baseUrl, jwt2).catch(() => null);
25587
25874
  if (me)
25588
25875
  updateAllowanceCache(me);
25589
25876
  guestHandoffVariant = me?.experiments?.guest_handoff ?? guestHandoffVariant;
@@ -25611,7 +25898,6 @@ ${creditsBack}`);
25611
25898
  const experiment = versionCheck.experiments?.[RESUME_ON_LAUNCH_EXPERIMENT];
25612
25899
  const variant = experiment?.variant === "treatment" ? "treatment" : "control";
25613
25900
  const recentHours = recentHoursFromEnv(process.env, experiment?.recent_hours);
25614
- const interactive = deps.stdinIsTTY ?? process.stdin.isTTY === true;
25615
25901
  if (!session && !resumeFlags.startNew && !resumeFlags.hasPrompt && variant === "treatment" && interactive) {
25616
25902
  const latest = await lookupLatest(process.cwd(), deps.agentDir);
25617
25903
  const now = deps.now?.() ?? new Date;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "free-pi-cli",
3
- "version": "0.6.8",
3
+ "version": "0.6.9",
4
4
  "description": "Free, ad-supported distribution of the pi coding agent. Inference paid for by ads.",
5
5
  "license": "MIT",
6
6
  "type": "module",