free-pi-cli 0.6.7 → 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-tm302552.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()
@@ -14391,7 +14392,10 @@ var AuthTokenPendingSchema = exports_external.object({
14391
14392
  status: exports_external.literal("pending"),
14392
14393
  retry_after: exports_external.number().optional()
14393
14394
  });
14394
- var AuthTokenIssuedSchema = exports_external.object({ token: exports_external.string() });
14395
+ var AuthTokenIssuedSchema = exports_external.object({
14396
+ token: exports_external.string(),
14397
+ guest_bonus_credits_granted: exports_external.number().int().optional()
14398
+ });
14395
14399
  var AuthTokenConsentRequiredSchema = exports_external.object({
14396
14400
  code: exports_external.literal("consent_required"),
14397
14401
  message: exports_external.string()
@@ -14437,15 +14441,24 @@ var GuestStartResponseSchema = exports_external.object({
14437
14441
  guest: exports_external.literal(true),
14438
14442
  allowance_usd: exports_external.string(),
14439
14443
  remaining_usd: exports_external.string(),
14440
- experiments: ExperimentAssignmentSchema.optional()
14444
+ experiments: ExperimentAssignmentSchema.optional(),
14445
+ claim_code: exports_external.string().optional(),
14446
+ github_bonus_credits: exports_external.number().int().optional()
14441
14447
  });
14442
14448
  var GuestUnavailableResponseSchema = exports_external.object({
14443
14449
  code: exports_external.literal("guest_unavailable"),
14444
14450
  message: exports_external.string(),
14445
14451
  reason: exports_external.enum(["disabled", "global_budget", "ip_sessions", "ip_allowance"])
14446
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
+ ]);
14447
14460
  var FunnelEventRequestSchema = exports_external.object({
14448
- event: exports_external.enum(["device_code_shown", "browser_opened"]),
14461
+ event: FunnelEventNameSchema,
14449
14462
  session_id: exports_external.string().uuid().optional()
14450
14463
  });
14451
14464
  var ReturnBonusTelemetrySchema = exports_external.object({
@@ -14624,6 +14637,11 @@ var ConfigSchema = exports_external.object({
14624
14637
  maxOutputTokens: exports_external.number().int().min(1).default(32768),
14625
14638
  accountsPerDayCeiling: exports_external.number().int().default(500),
14626
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),
14627
14645
  webBuyUrlRatePerProviderPerHour: exports_external.number().int().default(20),
14628
14646
  handoffTokenRatePerProviderPerHour: exports_external.number().int().default(60),
14629
14647
  handoffHourlyCompletionCap: exports_external.number().int().default(120),
@@ -14643,6 +14661,7 @@ var ConfigSchema = exports_external.object({
14643
14661
  lifecycleEmailSend: exports_external.boolean().default(false),
14644
14662
  emailProviderConfigured: exports_external.boolean().default(false),
14645
14663
  emailFrom: exports_external.string().optional(),
14664
+ emailPostalAddress: exports_external.string().optional(),
14646
14665
  emailProviderUrl: exports_external.string().optional(),
14647
14666
  hiveEnabled: exports_external.boolean().default(false),
14648
14667
  providerOrder: exports_external.array(exports_external.string()).optional(),
@@ -14701,6 +14720,7 @@ var ConfigSchema = exports_external.object({
14701
14720
  guestIpHashRetentionDays: exports_external.number().int().default(7),
14702
14721
  returnBonusDay2Credits: exports_external.number().int().default(2000),
14703
14722
  returnBonusDay7Credits: exports_external.number().int().default(1000),
14723
+ guestGithubBonusCredits: exports_external.number().int().default(1e4),
14704
14724
  returnNudgeEmailEnabled: exports_external.boolean().default(false),
14705
14725
  referralBonusCreditsPerDay: exports_external.number().int().default(5000),
14706
14726
  referralBonusDays: exports_external.number().int().default(30),
@@ -14742,6 +14762,9 @@ function envBoolean(value) {
14742
14762
  return;
14743
14763
  return value === "true" || value === "1";
14744
14764
  }
14765
+ function envFlagOn(value) {
14766
+ return ["true", "1", "on", "yes"].includes((value ?? "").trim().toLowerCase());
14767
+ }
14745
14768
  function envList(value) {
14746
14769
  if (!value)
14747
14770
  return;
@@ -14773,6 +14796,11 @@ function loadConfig(env = process.env) {
14773
14796
  maxOutputTokens: envNumber(env.MAX_OUTPUT_TOKENS),
14774
14797
  accountsPerDayCeiling: envNumber(env.ACCOUNTS_PER_DAY_CEILING),
14775
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),
14776
14804
  webBuyUrlRatePerProviderPerHour: envNumber(env.WEB_BUY_URL_RATE_PER_PROVIDER_PER_HOUR),
14777
14805
  handoffTokenRatePerProviderPerHour: envNumber(env.HANDOFF_TOKEN_RATE_PER_PROVIDER_PER_HOUR),
14778
14806
  handoffHourlyCompletionCap: envNumber(env.HANDOFF_HOURLY_COMPLETION_CAP),
@@ -14792,6 +14820,7 @@ function loadConfig(env = process.env) {
14792
14820
  lifecycleEmailSend: envBoolean(env.LIFECYCLE_EMAIL_SEND),
14793
14821
  emailProviderConfigured: env.EMAIL_PROVIDER_KEY ? env.EMAIL_PROVIDER_KEY.trim().length > 0 : undefined,
14794
14822
  emailFrom: env.EMAIL_FROM?.trim() || undefined,
14823
+ emailPostalAddress: env.EMAIL_POSTAL_ADDRESS?.trim() || undefined,
14795
14824
  emailProviderUrl: env.EMAIL_PROVIDER_URL?.trim() || undefined,
14796
14825
  hiveEnabled: envBoolean(env.HIVE_ENABLED),
14797
14826
  providerOrder: envList(env.PROVIDER_ORDER),
@@ -14845,6 +14874,7 @@ function loadConfig(env = process.env) {
14845
14874
  guestIpHashRetentionDays: envStrictInt("GUEST_IP_HASH_RETENTION_DAYS", env.GUEST_IP_HASH_RETENTION_DAYS),
14846
14875
  returnBonusDay2Credits: envStrictInt("RETURN_BONUS_DAY2_CREDITS", env.RETURN_BONUS_DAY2_CREDITS),
14847
14876
  returnBonusDay7Credits: envStrictInt("RETURN_BONUS_DAY7_CREDITS", env.RETURN_BONUS_DAY7_CREDITS),
14877
+ guestGithubBonusCredits: envStrictInt("GUEST_GITHUB_BONUS_CREDITS", env.GUEST_GITHUB_BONUS_CREDITS),
14848
14878
  returnNudgeEmailEnabled: envBoolean(env.RETURN_NUDGE_EMAIL_ENABLED),
14849
14879
  referralBonusCreditsPerDay: envStrictInt("REFERRAL_BONUS_CREDITS_PER_DAY", env.REFERRAL_BONUS_CREDITS_PER_DAY),
14850
14880
  referralBonusDays: envStrictInt("REFERRAL_BONUS_DAYS", env.REFERRAL_BONUS_DAYS),
@@ -14929,42 +14959,46 @@ var LIMIT_NOTICE_LABELS = {
14929
14959
  concurrent_session: "another session is active",
14930
14960
  guest_allowance: "guest trial"
14931
14961
  };
14932
- var EXTRA_CREDITS_URL = "freepi.ai/extra-credits";
14933
14962
  var LIMIT_NOTICE_LOCAL_TIME_AFTER_SECONDS = 90 * 60;
14934
14963
  var REFILL_CLOCK_LIMITS = new Set(["daily_cap", "global_cap", "queue_wait"]);
14935
14964
  function formatLimitNotice(fields, opts = {}) {
14936
14965
  const slow = fields.lane === "slow" && fields.limit === "hourly_ceiling" && fields.hourly_ceiling != null;
14937
- 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, " ");
14938
- const parts = [label];
14939
14966
  const retry = fields.retry_after_seconds;
14940
- const waitBits = [];
14967
+ let when;
14941
14968
  if (retry != null && retry > 0) {
14942
14969
  const useClock = Boolean(opts.localTime) && Boolean(fields.clears_at) && retry >= LIMIT_NOTICE_LOCAL_TIME_AFTER_SECONDS;
14943
- if (useClock) {
14944
- const formatted = new Intl.DateTimeFormat("en-US", {
14945
- timeZone: opts.timeZone,
14946
- hour: "numeric",
14947
- minute: "2-digit"
14948
- }).format(new Date(fields.clears_at));
14949
- waitBits.push(`back at ${formatted}`);
14950
- } else {
14951
- waitBits.push(`back in ${Math.max(1, Math.ceil(retry / 60))} min`);
14952
- }
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`;
14953
14971
  }
14954
- 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 "";
14955
14975
  const clears = new Date(fields.clears_at);
14956
- if (!Number.isNaN(clears.getTime())) {
14957
- waitBits.push(`resets ${formatRefillEt(clears)}`);
14958
- }
14959
- }
14960
- if (waitBits.length > 0)
14961
- 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()}`;
14962
14979
  if (slow) {
14963
- const wait = waitBits[0] ? ` ${waitBits[0][0].toUpperCase()}${waitBits[0].slice(1)} — or get` : " Get";
14964
- 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.`;
14965
14983
  }
14966
- parts.push(`${fields.remaining_credits_today.toLocaleString("en-US")} credits left today`);
14967
- 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(", ");
14968
15002
  }
14969
15003
  // ../shared/src/version.ts
14970
15004
  function compareVersions(a, b) {
@@ -15095,7 +15129,7 @@ var HiveClassifyResponseSchema = exports_external.object({
15095
15129
  // package.json
15096
15130
  var package_default = {
15097
15131
  name: "free-pi-cli",
15098
- version: "0.6.7",
15132
+ version: "0.6.9",
15099
15133
  description: "Free, ad-supported distribution of the pi coding agent. Inference paid for by ads.",
15100
15134
  license: "MIT",
15101
15135
  type: "module",
@@ -15732,13 +15766,13 @@ async function renderMeter(deps, ctx) {
15732
15766
  confirm = { addedUsd: added, turnsLeft: CONFIRM_TURNS };
15733
15767
  if (confirm && confirm.turnsLeft > 0) {
15734
15768
  confirm.turnsLeft--;
15735
- ctx.ui.setWidget(METER_WIDGET_ID, [renderErrorLine(creditAddedMessage(confirm.addedUsd), theme)]);
15769
+ ctx.ui.setWidget(METER_WIDGET_ID, [theme.fg("accent", creditAddedMessage(confirm.addedUsd))]);
15736
15770
  return;
15737
15771
  }
15738
15772
  confirm = undefined;
15739
15773
  const notice = typeof me.notice === "string" ? sanitizeText(me.notice).trim() : "";
15740
15774
  if (notice !== "") {
15741
- ctx.ui.setWidget(METER_WIDGET_ID, [renderErrorLine(notice, theme)]);
15775
+ ctx.ui.setWidget(METER_WIDGET_ID, [theme.fg("accent", notice)]);
15742
15776
  return;
15743
15777
  }
15744
15778
  if (me.lane === "fallback") {
@@ -20178,12 +20212,14 @@ function formatStats(stats) {
20178
20212
  const allowance = stats.allowance_credits;
20179
20213
  const refillAt = stats.refill_at;
20180
20214
  const lane2 = stats.lane;
20215
+ const bonus = stats.bonus_wallet_credits;
20181
20216
  const todayLine = typeof remaining === "number" && typeof allowance === "number" && typeof refillAt === "string" ? formatCreditsLeftLine(remaining, allowance, new Date(refillAt)) : `today: ${percentUsed(stats.spent_usd_today, stats.cap_usd_today)}% of your free daily allowance used`;
20182
20217
  return [
20183
20218
  `tier: ${stats.tier}`,
20184
20219
  todayLine,
20185
20220
  ...lane2 === "fallback" ? [SLOW_LANE_METER_LINE] : [],
20186
20221
  ...typeof credits2 === "number" ? [`credit: ${credits2.toLocaleString("en-US")} credits remaining`] : [],
20222
+ ...typeof bonus === "number" && bonus > 0 ? [`bonus: ${bonus.toLocaleString("en-US")} credits remaining`] : [],
20187
20223
  `today tokens: ${stats.prompt_tokens_today} prompt / ${stats.completion_tokens_today} completion`,
20188
20224
  `lifetime: ${stats.lifetime.prompt_tokens} prompt / ${stats.lifetime.completion_tokens} completion tokens`,
20189
20225
  "buy more credits: /buy-credits",
@@ -21186,6 +21222,22 @@ function formatLimitEnvelope(env, opts) {
21186
21222
  hourly_ceiling: env.hourly_ceiling
21187
21223
  }, { localTime: true, timeZone: opts?.timeZone, copyStyle: env.copy_variant });
21188
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
+ ]);
21189
21241
  function createErrorNoticeExtension(opts = {}) {
21190
21242
  const lastAt = new Map;
21191
21243
  const now = opts.now ?? Date.now;
@@ -21204,15 +21256,97 @@ function createErrorNoticeExtension(opts = {}) {
21204
21256
  if (t - (lastAt.get(env.code) ?? -Infinity) < NOTICE_DEDUPE_MS)
21205
21257
  return;
21206
21258
  lastAt.set(env.code, t);
21207
- ctx.ui.notify(formatLimitEnvelope(env, { timeZone }), env.status >= 500 ? "error" : "warning");
21208
21259
  const extra = opts.extraNotice?.(env);
21209
- if (extra)
21210
- 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");
21211
21263
  });
21212
21264
  }
21213
21265
  };
21214
21266
  }
21215
21267
 
21268
+ // src/upgrade-notice.ts
21269
+ var UPGRADE_NOTICE_MARKER = ": freepi_notice";
21270
+ var UPGRADE_NOTICE_TEXT = "free-pi is upgrading — short delay. Your request will continue automatically.";
21271
+ var SLOW_SERVER_HINT = "Waiting for the server — free-pi may be upgrading; your request will continue automatically.";
21272
+ var SLOW_HEADERS_MS = 8000;
21273
+ var UPGRADE_NOTICE_DEDUPE_MS = 60000;
21274
+ function isCompletionsCall(input, completionsUrl) {
21275
+ try {
21276
+ const raw = input instanceof Request ? input.url : String(input);
21277
+ const got = new URL(raw);
21278
+ const want = new URL(completionsUrl);
21279
+ return got.origin === want.origin && got.pathname === want.pathname;
21280
+ } catch {
21281
+ return false;
21282
+ }
21283
+ }
21284
+ function wrapFetchForUpgrade(base, opts) {
21285
+ return async (input, init) => {
21286
+ if (!isCompletionsCall(input, opts.completionsUrl))
21287
+ return base(input, init);
21288
+ const timer = setTimeout(opts.onSlow, opts.slowMs ?? SLOW_HEADERS_MS);
21289
+ let res;
21290
+ try {
21291
+ res = await base(input, init);
21292
+ } finally {
21293
+ clearTimeout(timer);
21294
+ }
21295
+ if (!res.body || !(res.headers.get("content-type") ?? "").includes("text/event-stream"))
21296
+ return res;
21297
+ const decoder = new TextDecoder;
21298
+ let tail = `
21299
+ `;
21300
+ let seen = false;
21301
+ const body = res.body.pipeThrough(new TransformStream({
21302
+ transform(chunk, ctl) {
21303
+ ctl.enqueue(chunk);
21304
+ if (seen)
21305
+ return;
21306
+ const text = tail + decoder.decode(chunk, { stream: true });
21307
+ if (text.includes(`
21308
+ ${UPGRADE_NOTICE_MARKER}`)) {
21309
+ seen = true;
21310
+ opts.onNotice();
21311
+ } else
21312
+ tail = text.slice(-UPGRADE_NOTICE_MARKER.length);
21313
+ }
21314
+ }));
21315
+ return new Response(body, { status: res.status, statusText: res.statusText, headers: res.headers });
21316
+ };
21317
+ }
21318
+ var notify;
21319
+ var lastAt = new Map;
21320
+ var WRAPPED = Symbol.for("free-pi.upgrade-notice");
21321
+ function createUpgradeNoticeExtension(opts) {
21322
+ const now = opts.now ?? Date.now;
21323
+ const show = (message) => {
21324
+ const t = now();
21325
+ if (t - (lastAt.get(message) ?? -Infinity) < UPGRADE_NOTICE_DEDUPE_MS)
21326
+ return;
21327
+ lastAt.set(message, t);
21328
+ notify?.(message);
21329
+ };
21330
+ return {
21331
+ name: "free-pi-upgrade-notice",
21332
+ factory(pi) {
21333
+ pi.on("session_start", (_event, ctx) => {
21334
+ notify = (m) => ctx.ui.notify(m, "info");
21335
+ });
21336
+ if (globalThis.fetch[WRAPPED])
21337
+ return;
21338
+ const wrapped = wrapFetchForUpgrade(globalThis.fetch, {
21339
+ completionsUrl: `${completionsBaseUrl(opts.baseUrl)}/chat/completions`,
21340
+ slowMs: opts.slowMs,
21341
+ onNotice: () => show(UPGRADE_NOTICE_TEXT),
21342
+ onSlow: () => show(SLOW_SERVER_HINT)
21343
+ });
21344
+ wrapped[WRAPPED] = true;
21345
+ globalThis.fetch = wrapped;
21346
+ }
21347
+ };
21348
+ }
21349
+
21216
21350
  // src/agent-rules.ts
21217
21351
  var AGENT_RULES = `## Working rules
21218
21352
  - When you say you will run, check, or change something, make that tool call in the same
@@ -21380,6 +21514,17 @@ var LOGIN_VALUE_LINE = "sign in free with GitHub: full daily allowance, FREE abl
21380
21514
  var GUEST_NEAR_WALL_LINE = `A few guest requests left. ${LOGIN_VALUE_LINE}`;
21381
21515
  var SIGNED_IN_CONTINUE_LINE = "Signed in. Your conversation continues — full daily allowance is unlocked.";
21382
21516
  var HEADLESS_FALLBACK_LINE = "No browser or clipboard here — open the URL and enter the code above.";
21517
+ function formatGuestBonusBox(claimCode, credits2) {
21518
+ const amount = credits2.toLocaleString("en-US");
21519
+ const rows = [
21520
+ `GUEST BONUS * code ${claimCode}`,
21521
+ `Connect GitHub with /login -> +${amount} credits`,
21522
+ "One time, on your first GitHub sign-in."
21523
+ ];
21524
+ const inner = Math.max(...rows.map((r) => r.length)) + 4;
21525
+ const edge = `+${"-".repeat(inner)}+`;
21526
+ return [edge, ...rows.map((r) => `| ${r.padEnd(inner - 2)}|`), edge];
21527
+ }
21383
21528
  function isHandoffTreatment(variant) {
21384
21529
  return variant === "treatment";
21385
21530
  }
@@ -21452,6 +21597,22 @@ function mergeAttribution(a, b) {
21452
21597
 
21453
21598
  // src/changelog.ts
21454
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
+ },
21608
+ {
21609
+ version: "0.6.8",
21610
+ highlights: [
21611
+ "During a server upgrade free-pi now says it is upgrading and that your request will continue automatically.",
21612
+ "Guest trials show a bonus code: sign in with GitHub (/login) for a one-time +10,000 credits.",
21613
+ "/usage now shows your bonus credits on their own line, matching the footer."
21614
+ ]
21615
+ },
21455
21616
  {
21456
21617
  version: "0.6.7",
21457
21618
  highlights: [
@@ -21836,7 +21997,9 @@ async function startGuestSession(baseUrl, consentVersion, attribution = {}) {
21836
21997
  allowance_usd: parsed.allowance_usd,
21837
21998
  remaining_usd: parsed.remaining_usd,
21838
21999
  guestHandoffVariant: parsed.experiments?.guest_handoff,
21839
- firstTaskVariant: parsed.experiments?.first_task
22000
+ firstTaskVariant: parsed.experiments?.first_task,
22001
+ claimCode: parsed.claim_code,
22002
+ githubBonusCredits: parsed.github_bonus_credits
21840
22003
  };
21841
22004
  }
21842
22005
  async function reportFunnelEvent(baseUrl, event, opts = {}) {
@@ -21864,8 +22027,18 @@ class LoginDeniedError extends Error {
21864
22027
  super("login_denied");
21865
22028
  }
21866
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
+ }
21867
22039
  async function pollForToken(baseUrl, sessionId, interval, sleep, opts = {}) {
21868
22040
  let consecutiveNetworkFailures = 0;
22041
+ let waitSeconds = Math.max(0, interval);
21869
22042
  const now = opts.now ?? Date.now;
21870
22043
  for (;; ) {
21871
22044
  if (opts.deadline !== undefined && now() >= opts.deadline) {
@@ -21888,7 +22061,7 @@ async function pollForToken(baseUrl, sessionId, interval, sleep, opts = {}) {
21888
22061
  if (consecutiveNetworkFailures >= 6) {
21889
22062
  throw new Error(`token poll failed: network unreachable after ${consecutiveNetworkFailures} attempts (${err instanceof Error ? err.message : String(err)})`);
21890
22063
  }
21891
- await sleep(Math.max(interval, 5) * 1000);
22064
+ await sleep(Math.max(waitSeconds, 5) * 1000);
21892
22065
  continue;
21893
22066
  }
21894
22067
  if (!res.ok) {
@@ -21900,6 +22073,8 @@ async function pollForToken(baseUrl, sessionId, interval, sleep, opts = {}) {
21900
22073
  throw new LoginDeniedError;
21901
22074
  if (res.status === 410 && body?.code === "expired")
21902
22075
  throw new LoginTimeoutError;
22076
+ if (isExpiredTokenPoll(body))
22077
+ throw new DeviceCodeExpiredError;
21903
22078
  throw new Error(refusalMessage(res.status, body) ?? `token poll failed: HTTP ${res.status}`);
21904
22079
  }
21905
22080
  const parsed = AuthTokenResponseSchema.parse(await res.json());
@@ -21909,7 +22084,13 @@ async function pollForToken(baseUrl, sessionId, interval, sleep, opts = {}) {
21909
22084
  throw new Error("consent_required: server did not record consent before polling");
21910
22085
  }
21911
22086
  const retryAfter = "retry_after" in parsed && typeof parsed.retry_after === "number" ? parsed.retry_after : 0;
21912
- 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);
21913
22094
  }
21914
22095
  }
21915
22096
  async function checkToken(baseUrl, jwt2) {
@@ -22002,6 +22183,7 @@ function clipboardCommand(platform) {
22002
22183
  var CONSENT_TEXT = `
22003
22184
  free-pi is free. In exchange, we show you ads and may train on your data.
22004
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.
22005
22187
  Terms: https://freepi.ai/terms Privacy: https://freepi.ai/privacy
22006
22188
  `.trim();
22007
22189
  var CONSENT_VERSION = "4";
@@ -22179,6 +22361,37 @@ var LOGIN_REWARD_LINE = "Sign in with GitHub to get 15,000 free credits a day";
22179
22361
  var LOGIN_WAITING_LINE = "Waiting for GitHub…";
22180
22362
  var LOGIN_TIMEOUT_LINE = "GitHub sign-in timed out. Run /login to try again.";
22181
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
+ }
22182
22395
  function formatDeviceCodePrompt(device, variant) {
22183
22396
  const value = isHandoffTreatment(variant) ? LOGIN_VALUE_LINE : LOGIN_REWARD_LINE;
22184
22397
  return [
@@ -22188,12 +22401,51 @@ function formatDeviceCodePrompt(device, variant) {
22188
22401
  "",
22189
22402
  ` Open ${device.verification_uri}`,
22190
22403
  ` Enter code: ${device.user_code}`,
22404
+ formatCodeExpiryLine(device.expires_in ?? 900),
22191
22405
  ""
22192
22406
  ].join(`
22193
22407
  `);
22194
22408
  }
22195
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) {
22196
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
+ }
22197
22449
  deps.log(formatDeviceCodePrompt(device, deps.variant));
22198
22450
  if (deps.credentialsPath) {
22199
22451
  await savePendingLogin(deps.credentialsPath, {
@@ -22207,10 +22459,12 @@ async function runDeviceLogin(deps, attribution = {}) {
22207
22459
  if (copied)
22208
22460
  deps.log(` (code copied to clipboard)
22209
22461
  `);
22210
- deps.log(LOGIN_WAITING_LINE);
22462
+ const expiresIn = device.expires_in ?? 900;
22463
+ deps.log(formatWaitingLine(expiresIn));
22464
+ const openUrl = deviceBrowserUrl(device);
22211
22465
  let browserOpened = false;
22212
22466
  try {
22213
- browserOpened = await deps.openBrowser(device.verification_uri) !== false;
22467
+ browserOpened = await deps.openBrowser(openUrl) !== false;
22214
22468
  } catch {
22215
22469
  browserOpened = false;
22216
22470
  }
@@ -22224,16 +22478,23 @@ async function runDeviceLogin(deps, attribution = {}) {
22224
22478
  deps.log(copied ? BROWSER_FAILED_LINE : `${BROWSER_FAILED_LINE} ${HEADLESS_FALLBACK_LINE}`);
22225
22479
  }
22226
22480
  await submitConsent(deps.baseUrl, device.session_id, CONSENT_VERSION, attribution);
22227
- const expiresInMs = (device.expires_in ?? 900) * 1000;
22228
- const deadline = (deps.now ?? Date.now)() + expiresInMs;
22229
- try {
22230
- const token = await pollForToken(deps.baseUrl, device.session_id, device.interval, deps.sleep, { deadline, now: deps.now, jwt: deps.jwt, fetchImpl: deps.fetchImpl });
22231
- if (deps.credentialsPath)
22232
- await clearPendingLogin(deps.credentialsPath);
22233
- return token;
22234
- } catch (err) {
22235
- throw err;
22236
- }
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;
22237
22498
  }
22238
22499
  var CONNECT_BROWSER_FAILED_LINE = "Couldn't open a browser here, so using the GitHub device code instead.";
22239
22500
  var CONNECT_FALLBACK_LINE = "Couldn't start the browser sign-in, so using the GitHub device code instead.";
@@ -22454,7 +22715,25 @@ async function openPrivacyPolicy(opts, ctx) {
22454
22715
  function resolveHandoffVariant(opts) {
22455
22716
  return typeof opts.variant === "function" ? opts.variant() : opts.variant;
22456
22717
  }
22457
- 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");
22458
22737
  try {
22459
22738
  const attribution = opts.agentDir ? await loadAttribution(getAttributionPath(opts.agentDir)) : {};
22460
22739
  const variant = resolveHandoffVariant(opts);
@@ -22462,7 +22741,11 @@ async function runLogin(opts, ctx) {
22462
22741
  baseUrl: opts.baseUrl,
22463
22742
  openBrowser: opts.openBrowserImpl ?? openBrowser,
22464
22743
  sleep: (ms) => new Promise((r) => setTimeout(r, ms)),
22465
- 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
+ },
22466
22749
  jwt: await opts.getToken(),
22467
22750
  fetchImpl: opts.fetchImpl,
22468
22751
  variant,
@@ -22472,33 +22755,32 @@ async function runLogin(opts, ctx) {
22472
22755
  await saveJwt(opts.credentialsPath, token);
22473
22756
  opts.onLogin?.(token);
22474
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;
22475
22759
  } catch (err) {
22476
22760
  if (err instanceof LoginTimeoutError) {
22477
- ctx.ui.notify(LOGIN_TIMEOUT_LINE, "warning");
22478
- return;
22761
+ ctx.ui.notify(LOGIN_TIMEOUT_LINE, "info");
22762
+ return false;
22479
22763
  }
22480
22764
  if (err instanceof LoginDeniedError) {
22481
- ctx.ui.notify(LOGIN_DENIED_LINE, "warning");
22482
- return;
22765
+ ctx.ui.notify(LOGIN_DENIED_LINE, "info");
22766
+ return false;
22483
22767
  }
22484
22768
  const reason = err instanceof Error && err.message ? ` (${err.message})` : "";
22485
22769
  ctx.ui.notify(`Couldn't finish GitHub sign-in${reason}. Run /login to try again.`, "error");
22770
+ return false;
22486
22771
  }
22487
22772
  }
22488
22773
  function installLoginHandler(mode, opts) {
22489
22774
  const m = mode;
22490
22775
  mode.handleLoginCommand = () => {
22491
- const lines = [];
22492
- const notify = (message, type) => {
22776
+ const notify2 = (message, type) => {
22493
22777
  if (type === "error")
22494
22778
  return m.showError(message);
22495
22779
  if (type === "warning")
22496
22780
  return m.showWarning(message);
22497
- lines.push(message);
22498
- m.showStatus(lines.join(`
22499
- `));
22781
+ m.showStatus(message);
22500
22782
  };
22501
- return runLogin(opts, { ui: { notify } });
22783
+ return runLogin(opts, { ui: { notify: notify2 } });
22502
22784
  };
22503
22785
  }
22504
22786
  var LOGOUT_DONE_LINE = "Signed out of free-pi. Restart free-pi to sign in again.";
@@ -22615,14 +22897,24 @@ function loadRea(pi, opts, ctx) {
22615
22897
  }
22616
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");
22617
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)";
22618
22903
  function createGuestLoginExtension(opts) {
22619
22904
  let started = false;
22905
+ let inFlight = false;
22906
+ let signedIn = false;
22620
22907
  let nearWallWarned = false;
22621
- const start = (ctx) => {
22908
+ const start = (ctx, line) => {
22622
22909
  started = true;
22910
+ inFlight = true;
22623
22911
  const variant = resolveHandoffVariant(opts);
22624
- ctx.ui.notify(isHandoffTreatment(variant) ? LOGIN_VALUE_LINE : LOGIN_REWARD_LINE, "info");
22625
- 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
+ });
22626
22918
  };
22627
22919
  return {
22628
22920
  name: "free-pi-guest-login",
@@ -22631,8 +22923,12 @@ function createGuestLoginExtension(opts) {
22631
22923
  if (!opts.isGuest?.())
22632
22924
  return;
22633
22925
  const msg = event.message;
22634
- if (!started && msg?.stopReason === "error" && parseErrorEnvelope(msg.errorMessage)?.code === "guest_allowance") {
22635
- 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
+ }
22636
22932
  return;
22637
22933
  }
22638
22934
  if (started || nearWallWarned || !isHandoffTreatment(resolveHandoffVariant(opts)))
@@ -23452,7 +23748,7 @@ function createHiveExtension(opts) {
23452
23748
  let connectTimer;
23453
23749
  let roomWaiter;
23454
23750
  const dialogs = new Set;
23455
- const notify = (message, type = "info") => ctxRef?.ui.notify(message, type);
23751
+ const notify2 = (message, type = "info") => ctxRef?.ui.notify(message, type);
23456
23752
  function send(frame) {
23457
23753
  if (ws && ws.readyState === OPEN)
23458
23754
  ws.send(JSON.stringify(frame));
@@ -23496,13 +23792,13 @@ function createHiveExtension(opts) {
23496
23792
  const message = [notice, changes].filter(Boolean).join(`
23497
23793
  `);
23498
23794
  if (message)
23499
- notify(message, "info");
23795
+ notify2(message, "info");
23500
23796
  roomWaiter?.();
23501
23797
  roomWaiter = undefined;
23502
23798
  }
23503
23799
  async function start(ctx) {
23504
23800
  if (ws || starting) {
23505
- notify(roomUrl ? `Hive room: ${roomUrl}` : "Hive: still connecting...");
23801
+ notify2(roomUrl ? `Hive room: ${roomUrl}` : "Hive: still connecting...");
23506
23802
  return;
23507
23803
  }
23508
23804
  starting = true;
@@ -23511,7 +23807,7 @@ function createHiveExtension(opts) {
23511
23807
  token = await opts.getToken();
23512
23808
  } catch {
23513
23809
  starting = false;
23514
- notify("Hive: couldn't read your free-pi login. Try again after signing in.", "error");
23810
+ notify2("Hive: couldn't read your free-pi login. Try again after signing in.", "error");
23515
23811
  return;
23516
23812
  }
23517
23813
  starting = false;
@@ -23578,7 +23874,7 @@ Sandboxing is available: when you approve a guest's shell command you can run it
23578
23874
  clearTimeout(connectTimer);
23579
23875
  if (!settled)
23580
23876
  send({ type: "busy", settled: false });
23581
- notify(`Hive room open: ${frame.url}
23877
+ notify2(`Hive room open: ${frame.url}
23582
23878
  Guests sign in with GitHub to watch and chat. They can ask to drive; you approve each request, and risky actions still ask you.${sandboxLine()}`);
23583
23879
  updateStatus();
23584
23880
  roomWaiter?.();
@@ -23610,7 +23906,7 @@ Guests sign in with GitHub to watch and chat. They can ask to drive; you approve
23610
23906
  break;
23611
23907
  case "error":
23612
23908
  if (frame.code !== "stale")
23613
- notify(`Hive: ${frame.code}`, "warning");
23909
+ notify2(`Hive: ${frame.code}`, "warning");
23614
23910
  break;
23615
23911
  case "closed":
23616
23912
  endRoom(`Hive room closed (${frame.reason}).`);
@@ -23783,7 +24079,7 @@ ${decision.reason}`, ACTION_DIALOG_MS);
23783
24079
  handler: async (args, ctx) => {
23784
24080
  ctxRef = ctx;
23785
24081
  const [sub = "", ...rest3] = args.trim().split(/\s+/);
23786
- const noRoom = () => notify("No hive room is open. Run /hive to start one.");
24082
+ const noRoom = () => notify2("No hive room is open. Run /hive to start one.");
23787
24083
  switch (sub.toLowerCase()) {
23788
24084
  case "":
23789
24085
  return start(ctx);
@@ -23802,20 +24098,20 @@ ${decision.reason}`, ACTION_DIALOG_MS);
23802
24098
  const handle2 = (rest3[0] ?? "").replace(/^@/, "").toLowerCase();
23803
24099
  const ids = new Set(members.filter((m) => handle2 && m.handle.toLowerCase() === handle2).map((m) => m.provider_id));
23804
24100
  if (ids.size === 0)
23805
- return notify(`No guest named "${handle2}" in the room. Usage: /hive kick <handle>`);
24101
+ return notify2(`No guest named "${handle2}" in the room. Usage: /hive kick <handle>`);
23806
24102
  dropRecorded();
23807
24103
  for (const provider_id of ids) {
23808
24104
  kicked.add(provider_id);
23809
24105
  send({ type: "kick", provider_id });
23810
24106
  }
23811
- return notify(`Kicked @${handle2}. They cannot rejoin this room.`);
24107
+ return notify2(`Kicked @${handle2}. They cannot rejoin this room.`);
23812
24108
  }
23813
24109
  case "end":
23814
24110
  if (!ws)
23815
24111
  return noRoom();
23816
24112
  return endRoom("Hive room ended.", true);
23817
24113
  default:
23818
- return notify("Usage: /hive, /hive take, /hive kick <handle>, /hive end");
24114
+ return notify2("Usage: /hive, /hive take, /hive kick <handle>, /hive end");
23819
24115
  }
23820
24116
  }
23821
24117
  });
@@ -24743,6 +25039,7 @@ function buildRuntimeOptions(opts, sessionId) {
24743
25039
  imageToolExtension,
24744
25040
  visionToolExtension,
24745
25041
  errorNoticeExtension,
25042
+ createUpgradeNoticeExtension({ baseUrl: opts.baseUrl }),
24746
25043
  createOutputLimitExtension(),
24747
25044
  toolGuardExtension,
24748
25045
  commandsExtension,
@@ -25284,32 +25581,56 @@ async function resolveAttribution(agentDir, argv) {
25284
25581
  await saveAttribution(getAttributionPath(agentDir), merged);
25285
25582
  return { attribution: merged };
25286
25583
  }
25287
- async function loginFlow(deps, argv = []) {
25288
- const resolved = await resolveAttribution(deps.agentDir, argv);
25289
- if ("error" in resolved && resolved.error) {
25290
- 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;
25291
25604
  }
25605
+ }
25606
+ function storedTokenKind(jwt2) {
25292
25607
  try {
25293
- const token = await runLoginAuto({
25294
- baseUrl: deps.baseUrl,
25295
- openBrowser: deps.openBrowser,
25296
- sleep: deps.sleep,
25297
- log: deps.log,
25298
- jwt: await loadJwt(deps.credentialsPath),
25299
- credentialsPath: deps.credentialsPath
25300
- }, resolved.attribution ?? {}, { forceDevice: argv.includes("--device-login"), env: deps.env, platform: deps.platform });
25301
- await saveJwt(deps.credentialsPath, token);
25302
- return token;
25303
- } catch (err) {
25304
- if (err instanceof LoginTimeoutError) {
25305
- deps.log(LOGIN_TIMEOUT_LINE);
25306
- }
25307
- if (err instanceof LoginDeniedError) {
25308
- deps.log(LOGIN_DENIED_LINE);
25309
- }
25310
- 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";
25311
25614
  }
25312
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.";
25313
25634
  function validateSessionId(value) {
25314
25635
  try {
25315
25636
  SessionManager2.inMemory(process.cwd(), { id: value });
@@ -25385,6 +25706,12 @@ ${FINISH_SIGNIN_LINE}
25385
25706
  updateLatest = handled.updateLatest;
25386
25707
  versionHandled = true;
25387
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;
25388
25715
  let guestHandoffVariant;
25389
25716
  let showFirstTask = false;
25390
25717
  let consentCurrent = false;
@@ -25394,8 +25721,10 @@ ${FINISH_SIGNIN_LINE}
25394
25721
  consentCurrent = serverConsent === CONSENT_VERSION;
25395
25722
  if (serverConsent !== undefined && serverConsent !== CONSENT_VERSION) {
25396
25723
  if (!await deps.promptConsent(REPROMPT_CONSENT_TEXT)) {
25397
- deps.log("Declined. free-pi needs you to accept the updated terms to continue; you will be asked again next time.");
25398
- return 1;
25724
+ if (!interactive)
25725
+ return 1;
25726
+ deps.log(DECLINED_CALM_LINE);
25727
+ return 0;
25399
25728
  }
25400
25729
  consentCurrent = true;
25401
25730
  try {
@@ -25407,6 +25736,8 @@ ${FINISH_SIGNIN_LINE}
25407
25736
  }
25408
25737
  } catch (err) {
25409
25738
  if (err instanceof UnauthorizedError) {
25739
+ rejectedKind = storedTokenKind(jwt2);
25740
+ rejectedJwt = jwt2;
25410
25741
  await clearJwt(deps.credentialsPath);
25411
25742
  jwt2 = null;
25412
25743
  }
@@ -25415,9 +25746,45 @@ ${FINISH_SIGNIN_LINE}
25415
25746
  if (!jwt2) {
25416
25747
  const accepted = await deps.promptConsent();
25417
25748
  if (!accepted) {
25418
- deps.log("Declined. Exiting.");
25419
- 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
+ }
25420
25785
  }
25786
+ }
25787
+ if (!jwt2) {
25421
25788
  showIntro(deps.log);
25422
25789
  const resolved = await resolveAttribution(deps.agentDir, argv);
25423
25790
  if ("error" in resolved && resolved.error) {
@@ -25429,11 +25796,17 @@ ${FINISH_SIGNIN_LINE}
25429
25796
  const guest = await startGuestSession(deps.baseUrl, CONSENT_VERSION, attribution);
25430
25797
  await saveJwt(deps.credentialsPath, guest.token);
25431
25798
  jwt2 = guest.token;
25799
+ startedGuestThisRun = true;
25432
25800
  deps.log(isHandoffTreatment(guest.guestHandoffVariant) ? `
25433
25801
  You're on a short guest trial — ads pay for it. ${LOGIN_VALUE_LINE}
25434
25802
  ` : `
25435
25803
  You're on a short guest trial — ads pay for it. Sign in with GitHub anytime (/login) to get 15,000 free credits a day and unlock a one-time comeback bonus.
25436
25804
  `);
25805
+ if (guest.claimCode && guest.githubBonusCredits) {
25806
+ deps.log(emphasize(formatGuestBonusBox(guest.claimCode, guest.githubBonusCredits).join(`
25807
+ `)) + `
25808
+ `);
25809
+ }
25437
25810
  guestHandoffVariant = guest.guestHandoffVariant;
25438
25811
  showFirstTask = guest.firstTaskVariant === "treatment" && !resumeFlags.hasPrompt;
25439
25812
  } catch (err) {
@@ -25442,11 +25815,19 @@ You're on a short guest trial — ads pay for it. Sign in with GitHub anytime (/
25442
25815
  deps.log(`
25443
25816
  Guest trial is not available right now.
25444
25817
  `);
25445
- 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
+ }
25446
25827
  }
25447
25828
  consentCurrent = true;
25448
25829
  }
25449
- if (!hadJwt)
25830
+ if (!hadJwt || signedInAfterExpiry)
25450
25831
  versionCheck = await deps.checkClientVersion(deps.baseUrl, CLI_VERSION, jwt2);
25451
25832
  if (!versionHandled && versionCheck.action !== "ok") {
25452
25833
  const handled = await handleVersionAction(deps, versionCheck);
@@ -25459,13 +25840,37 @@ Guest trial is not available right now.
25459
25840
  deps.log(whatsNew);
25460
25841
  if (versionCheck.notice)
25461
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
+ }
25462
25868
  let getGravityContext;
25463
25869
  if (consentCurrent) {
25464
25870
  try {
25465
25871
  getGravityContext = deps.startGravityPixel?.(jwt2);
25466
25872
  } catch {}
25467
25873
  }
25468
- const me = await fetchMe2(deps.baseUrl, jwt2).catch(() => null);
25469
25874
  if (me)
25470
25875
  updateAllowanceCache(me);
25471
25876
  guestHandoffVariant = me?.experiments?.guest_handoff ?? guestHandoffVariant;
@@ -25493,7 +25898,6 @@ ${creditsBack}`);
25493
25898
  const experiment = versionCheck.experiments?.[RESUME_ON_LAUNCH_EXPERIMENT];
25494
25899
  const variant = experiment?.variant === "treatment" ? "treatment" : "control";
25495
25900
  const recentHours = recentHoursFromEnv(process.env, experiment?.recent_hours);
25496
- const interactive = deps.stdinIsTTY ?? process.stdin.isTTY === true;
25497
25901
  if (!session && !resumeFlags.startNew && !resumeFlags.hasPrompt && variant === "treatment" && interactive) {
25498
25902
  const latest = await lookupLatest(process.cwd(), deps.agentDir);
25499
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.7",
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",