free-pi-cli 0.5.3 → 0.6.1

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.
@@ -6,7 +6,7 @@ import {
6
6
 
7
7
  // src/main.ts
8
8
  import { realpathSync as realpathSync3, writeFileSync as writeFileSync3 } from "node:fs";
9
- import { dirname as dirname9, join as join10 } from "node:path";
9
+ import { dirname as dirname10, join as join11 } from "node:path";
10
10
 
11
11
  // src/browser.ts
12
12
  import { spawn } from "node:child_process";
@@ -14408,11 +14408,16 @@ var GuestStartRequestSchema = exports_external.object({
14408
14408
  signup_src: exports_external.string().optional(),
14409
14409
  referral_code: exports_external.string().optional()
14410
14410
  });
14411
+ var ExperimentVariantSchema = exports_external.enum(["control", "treatment"]);
14412
+ var ExperimentAssignmentSchema = exports_external.object({
14413
+ guest_handoff: ExperimentVariantSchema.optional()
14414
+ });
14411
14415
  var GuestStartResponseSchema = exports_external.object({
14412
14416
  token: exports_external.string(),
14413
14417
  guest: exports_external.literal(true),
14414
14418
  allowance_usd: exports_external.string(),
14415
- remaining_usd: exports_external.string()
14419
+ remaining_usd: exports_external.string(),
14420
+ experiments: ExperimentAssignmentSchema.optional()
14416
14421
  });
14417
14422
  var GuestUnavailableResponseSchema = exports_external.object({
14418
14423
  code: exports_external.literal("guest_unavailable"),
@@ -14424,10 +14429,15 @@ var FunnelEventRequestSchema = exports_external.object({
14424
14429
  session_id: exports_external.string().uuid().optional()
14425
14430
  });
14426
14431
  var ReturnBonusTelemetrySchema = exports_external.object({
14427
- type: exports_external.literal("card_shown")
14432
+ type: exports_external.enum(["card_shown", "credits_back"])
14433
+ });
14434
+ var ExperimentExposureSchema = exports_external.object({
14435
+ experiment: exports_external.enum(["resume_on_launch"]),
14436
+ event: exports_external.enum(["offer_shown", "offer_accepted"])
14428
14437
  });
14429
14438
  var ReturnBonusSurfaceSchema = exports_external.object({
14430
14439
  show_card: exports_external.boolean(),
14440
+ credits_back: exports_external.boolean(),
14431
14441
  day2_granted: exports_external.boolean(),
14432
14442
  day7_granted: exports_external.boolean()
14433
14443
  });
@@ -14448,9 +14458,15 @@ var MeResponseSchema = exports_external.object({
14448
14458
  refill_at: exports_external.string().optional(),
14449
14459
  lane: UsageLaneSchema.optional(),
14450
14460
  bonus_credits: exports_external.number().optional(),
14461
+ bonus_wallet_credits: exports_external.number().optional(),
14462
+ use_bonus: exports_external.string().optional(),
14451
14463
  guest: exports_external.boolean().optional(),
14452
14464
  guest_prompt_login: exports_external.boolean().optional(),
14453
- return_bonus: ReturnBonusSurfaceSchema.optional()
14465
+ guest_requests_remaining: exports_external.number().int().optional(),
14466
+ guest_near_wall: exports_external.boolean().optional(),
14467
+ experiments: ExperimentAssignmentSchema.optional(),
14468
+ return_bonus: ReturnBonusSurfaceSchema.optional(),
14469
+ visible_allowance: exports_external.boolean().optional()
14454
14470
  });
14455
14471
  var MeStatsResponseSchema = exports_external.object({
14456
14472
  user_id: exports_external.string(),
@@ -14466,6 +14482,8 @@ var MeStatsResponseSchema = exports_external.object({
14466
14482
  refill_at: exports_external.string(),
14467
14483
  lane: UsageLaneSchema,
14468
14484
  bonus_credits: exports_external.number(),
14485
+ bonus_wallet_credits: exports_external.number().optional(),
14486
+ use_bonus: exports_external.string().optional(),
14469
14487
  request_count_today: exports_external.number(),
14470
14488
  prompt_tokens_today: exports_external.number(),
14471
14489
  completion_tokens_today: exports_external.number(),
@@ -14477,6 +14495,9 @@ var MeStatsResponseSchema = exports_external.object({
14477
14495
  }),
14478
14496
  guest: exports_external.boolean().optional(),
14479
14497
  guest_prompt_login: exports_external.boolean().optional(),
14498
+ guest_requests_remaining: exports_external.number().int().optional(),
14499
+ guest_near_wall: exports_external.boolean().optional(),
14500
+ experiments: ExperimentAssignmentSchema.optional(),
14480
14501
  return_bonus: ReturnBonusSurfaceSchema.optional()
14481
14502
  });
14482
14503
  var AdSlotSchema = exports_external.enum(["banner", "inline"]);
@@ -14530,7 +14551,8 @@ var ClientVersionResponseSchema = exports_external.object({
14530
14551
  model: exports_external.string().optional(),
14531
14552
  notice: exports_external.string().optional(),
14532
14553
  models: exports_external.array(exports_external.object({ id: exports_external.string(), name: exports_external.string(), section: exports_external.string().optional() })).optional(),
14533
- fusion: exports_external.object({ planner: exports_external.string(), executor: exports_external.string() }).optional()
14554
+ fusion: exports_external.object({ planner: exports_external.string(), executor: exports_external.string() }).optional(),
14555
+ experiments: exports_external.record(exports_external.string(), exports_external.object({ variant: exports_external.enum(["control", "treatment"]) }).passthrough()).optional()
14534
14556
  });
14535
14557
  var SessionResetResponseSchema = exports_external.object({
14536
14558
  released: exports_external.boolean()
@@ -14596,6 +14618,12 @@ var ConfigSchema = exports_external.object({
14596
14618
  providerDataCollection: exports_external.enum(["allow", "deny"]).optional(),
14597
14619
  promptCacheEnabled: exports_external.boolean().default(false),
14598
14620
  emailCaptureEnabled: exports_external.boolean().default(false),
14621
+ lifecycleEmailExperimentEnabled: exports_external.boolean().default(false),
14622
+ lifecycleEmailEnabled: exports_external.boolean().default(false),
14623
+ lifecycleEmailSend: exports_external.boolean().default(false),
14624
+ emailProviderConfigured: exports_external.boolean().default(false),
14625
+ emailFrom: exports_external.string().optional(),
14626
+ emailProviderUrl: exports_external.string().optional(),
14599
14627
  hiveEnabled: exports_external.boolean().default(false),
14600
14628
  providerOrder: exports_external.array(exports_external.string()).optional(),
14601
14629
  tierNewMaxDays: exports_external.number().int().default(30),
@@ -14607,6 +14635,7 @@ var ConfigSchema = exports_external.object({
14607
14635
  fallbackLaneMinHeadroomBps: exports_external.number().int().default(1000),
14608
14636
  queueWaitRetryAfterSeconds: exports_external.number().int().default(0),
14609
14637
  sessionLeaseTtlMinutes: exports_external.number().int().default(30),
14638
+ signedInMaxCliSessions: exports_external.number().int().min(1).default(4),
14610
14639
  minCliVersion: exports_external.string().default("0.0.0"),
14611
14640
  latestCliVersion: exports_external.string().default("0.2.0"),
14612
14641
  abuseWindowMinutes: exports_external.number().int().default(30),
@@ -14623,13 +14652,14 @@ var ConfigSchema = exports_external.object({
14623
14652
  abuseRestrictedHourlyCap: exports_external.number().int().default(5),
14624
14653
  abuseNormalHourlyCap: exports_external.number().int().default(30),
14625
14654
  abuseSignedInNormalHourlyCap: exports_external.number().int().default(0),
14655
+ abuseDay0SkipHourly: exports_external.boolean().default(true),
14626
14656
  abuseFlipAlertThreshold: exports_external.number().int().default(10),
14627
14657
  abuseAdBypassMinRequests: exports_external.number().int().default(20),
14628
14658
  abuseAdBypassRatio: exports_external.number().int().default(10),
14629
14659
  traceMaxInflightWrites: exports_external.number().int().default(32),
14630
14660
  catalogCacheTtlSeconds: exports_external.number().int().default(60),
14631
14661
  eventMaxInflightWrites: exports_external.number().int().default(128),
14632
- guestEnabled: exports_external.boolean().default(true),
14662
+ guestEnabled: exports_external.boolean().default(false),
14633
14663
  guestAllowanceMicros: exports_external.bigint().default(30000n),
14634
14664
  guestGlobalDailyCapMicros: exports_external.bigint().default(3000000n),
14635
14665
  guestIpSessionsPerDay: exports_external.number().int().default(8),
@@ -14639,10 +14669,12 @@ var ConfigSchema = exports_external.object({
14639
14669
  guestIpHashRetentionDays: exports_external.number().int().default(7),
14640
14670
  returnBonusDay2Credits: exports_external.number().int().default(2000),
14641
14671
  returnBonusDay7Credits: exports_external.number().int().default(1000),
14672
+ returnNudgeEmailEnabled: exports_external.boolean().default(false),
14642
14673
  referralBonusCreditsPerDay: exports_external.number().int().default(5000),
14643
14674
  referralBonusDays: exports_external.number().int().default(30),
14644
14675
  referralMaxActivePerReferrer: exports_external.number().int().default(10),
14645
- referralGithubMinAgeDays: exports_external.number().int().default(30)
14676
+ referralGithubMinAgeDays: exports_external.number().int().default(30),
14677
+ referralCreditGrantCredits: exports_external.number().int().default(0)
14646
14678
  });
14647
14679
  function envNumber(value) {
14648
14680
  if (value === undefined || value === "")
@@ -14702,6 +14734,12 @@ function loadConfig(env = process.env) {
14702
14734
  providerDataCollection: env.PROVIDER_DATA_COLLECTION || undefined,
14703
14735
  promptCacheEnabled: envBoolean(env.PROMPT_CACHE_ENABLED),
14704
14736
  emailCaptureEnabled: envBoolean(env.EMAIL_CAPTURE_ENABLED),
14737
+ lifecycleEmailExperimentEnabled: envBoolean(env.LIFECYCLE_EMAIL_EXPERIMENT),
14738
+ lifecycleEmailEnabled: envBoolean(env.LIFECYCLE_EMAIL_ENABLED),
14739
+ lifecycleEmailSend: envBoolean(env.LIFECYCLE_EMAIL_SEND),
14740
+ emailProviderConfigured: env.EMAIL_PROVIDER_KEY ? env.EMAIL_PROVIDER_KEY.trim().length > 0 : undefined,
14741
+ emailFrom: env.EMAIL_FROM?.trim() || undefined,
14742
+ emailProviderUrl: env.EMAIL_PROVIDER_URL?.trim() || undefined,
14705
14743
  hiveEnabled: envBoolean(env.HIVE_ENABLED),
14706
14744
  providerOrder: envList(env.PROVIDER_ORDER),
14707
14745
  tierNewMaxDays: envNumber(env.TIER_NEW_MAX_DAYS),
@@ -14713,6 +14751,7 @@ function loadConfig(env = process.env) {
14713
14751
  fallbackLaneMinHeadroomBps: envNumber(env.FALLBACK_LANE_MIN_HEADROOM_BPS),
14714
14752
  queueWaitRetryAfterSeconds: envNumber(env.QUEUE_WAIT_RETRY_AFTER_SECONDS),
14715
14753
  sessionLeaseTtlMinutes: envNumber(env.SESSION_LEASE_TTL_MINUTES),
14754
+ signedInMaxCliSessions: envNumber(env.SIGNED_IN_MAX_CLI_SESSIONS),
14716
14755
  minCliVersion: env.MIN_CLI_VERSION || undefined,
14717
14756
  latestCliVersion: env.LATEST_CLI_VERSION || undefined,
14718
14757
  abuseWindowMinutes: envNumber(env.ABUSE_WINDOW_MINUTES),
@@ -14729,6 +14768,7 @@ function loadConfig(env = process.env) {
14729
14768
  abuseRestrictedHourlyCap: envNumber(env.ABUSE_RESTRICTED_HOURLY_CAP),
14730
14769
  abuseNormalHourlyCap: envNumber(env.ABUSE_NORMAL_HOURLY_CAP),
14731
14770
  abuseSignedInNormalHourlyCap: envNumber(env.ABUSE_SIGNED_IN_NORMAL_HOURLY_CAP),
14771
+ abuseDay0SkipHourly: envBoolean(env.ABUSE_DAY0_SKIP_HOURLY),
14732
14772
  abuseFlipAlertThreshold: envNumber(env.ABUSE_FLIP_ALERT_THRESHOLD),
14733
14773
  abuseAdBypassMinRequests: envNumber(env.ABUSE_AD_BYPASS_MIN_REQUESTS),
14734
14774
  abuseAdBypassRatio: envNumber(env.ABUSE_AD_BYPASS_RATIO),
@@ -14745,10 +14785,12 @@ function loadConfig(env = process.env) {
14745
14785
  guestIpHashRetentionDays: envStrictInt("GUEST_IP_HASH_RETENTION_DAYS", env.GUEST_IP_HASH_RETENTION_DAYS),
14746
14786
  returnBonusDay2Credits: envStrictInt("RETURN_BONUS_DAY2_CREDITS", env.RETURN_BONUS_DAY2_CREDITS),
14747
14787
  returnBonusDay7Credits: envStrictInt("RETURN_BONUS_DAY7_CREDITS", env.RETURN_BONUS_DAY7_CREDITS),
14788
+ returnNudgeEmailEnabled: envBoolean(env.RETURN_NUDGE_EMAIL_ENABLED),
14748
14789
  referralBonusCreditsPerDay: envStrictInt("REFERRAL_BONUS_CREDITS_PER_DAY", env.REFERRAL_BONUS_CREDITS_PER_DAY),
14749
14790
  referralBonusDays: envStrictInt("REFERRAL_BONUS_DAYS", env.REFERRAL_BONUS_DAYS),
14750
14791
  referralMaxActivePerReferrer: envStrictInt("REFERRAL_MAX_ACTIVE_PER_REFERRER", env.REFERRAL_MAX_ACTIVE_PER_REFERRER),
14751
- referralGithubMinAgeDays: envStrictInt("REFERRAL_GITHUB_MIN_AGE_DAYS", env.REFERRAL_GITHUB_MIN_AGE_DAYS)
14792
+ referralGithubMinAgeDays: envStrictInt("REFERRAL_GITHUB_MIN_AGE_DAYS", env.REFERRAL_GITHUB_MIN_AGE_DAYS),
14793
+ referralCreditGrantCredits: envStrictInt("REFERRAL_CREDIT_GRANT_CREDITS", env.REFERRAL_CREDIT_GRANT_CREDITS)
14752
14794
  });
14753
14795
  }
14754
14796
  // ../shared/src/credits.ts
@@ -14759,6 +14801,43 @@ function usdToCredits(usd) {
14759
14801
  function nextUtcMidnight(now) {
14760
14802
  return new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate() + 1));
14761
14803
  }
14804
+ function formatRefillLocal(date5, timeZone) {
14805
+ const opts = {
14806
+ hour: "numeric",
14807
+ minute: "2-digit",
14808
+ hour12: true,
14809
+ timeZoneName: "short"
14810
+ };
14811
+ if (timeZone)
14812
+ opts.timeZone = timeZone;
14813
+ return new Intl.DateTimeFormat("en-US", opts).format(date5);
14814
+ }
14815
+ function formatVisibleAllowanceLine(input) {
14816
+ const left = input.remaining.toLocaleString("en-US");
14817
+ if (input.guest)
14818
+ return `${left} credits left · sign in for 15,000 a day`;
14819
+ if (input.refillAt && !Number.isNaN(input.refillAt.getTime())) {
14820
+ return `${left} credits left today · resets ${formatRefillLocal(input.refillAt, input.timeZone)}`;
14821
+ }
14822
+ return `${left} credits left today`;
14823
+ }
14824
+ function formatVisibleAllowanceStatus(input) {
14825
+ const left = input.remaining.toLocaleString("en-US");
14826
+ const bought = typeof input.bonusCredits === "number" && input.bonusCredits > 0 ? ` + ${input.bonusCredits.toLocaleString("en-US")}` : "";
14827
+ if (input.guest)
14828
+ return `${left}${bought} credits left`;
14829
+ if (input.refillAt && !Number.isNaN(input.refillAt.getTime())) {
14830
+ return `${left}${bought} credits left today · resets ${formatRefillLocal(input.refillAt, input.timeZone)}`;
14831
+ }
14832
+ return `${left}${bought} credits left today`;
14833
+ }
14834
+ function formatLowAllowanceWarning(input) {
14835
+ const left = input.remaining.toLocaleString("en-US");
14836
+ if (input.guest)
14837
+ return `Low on credits: ${left} left${input.suffix ?? ""}`;
14838
+ const reset = input.refillAt && !Number.isNaN(input.refillAt.getTime()) ? ` · resets ${formatRefillLocal(input.refillAt, input.timeZone)}` : "";
14839
+ return `Low on credits: ${left} left today${reset}${input.suffix ?? ""}`;
14840
+ }
14762
14841
  function formatRefillEt(date5, timeZone = "America/New_York") {
14763
14842
  const formatted = new Intl.DateTimeFormat("en-US", {
14764
14843
  timeZone,
@@ -14786,11 +14865,13 @@ var LIMIT_NOTICE_LABELS = {
14786
14865
  };
14787
14866
  var EXTRA_CREDITS_URL = "freepi.ai/extra-credits";
14788
14867
  var LIMIT_NOTICE_LOCAL_TIME_AFTER_SECONDS = 90 * 60;
14868
+ var REFILL_CLOCK_LIMITS = new Set(["daily_cap", "global_cap", "queue_wait"]);
14789
14869
  function formatLimitNotice(fields, opts = {}) {
14790
14870
  const slow = fields.lane === "slow" && fields.limit === "hourly_ceiling" && fields.hourly_ceiling != null;
14791
14871
  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, " ");
14792
14872
  const parts = [label];
14793
14873
  const retry = fields.retry_after_seconds;
14874
+ const waitBits = [];
14794
14875
  if (retry != null && retry > 0) {
14795
14876
  const useClock = Boolean(opts.localTime) && Boolean(fields.clears_at) && retry >= LIMIT_NOTICE_LOCAL_TIME_AFTER_SECONDS;
14796
14877
  if (useClock) {
@@ -14799,13 +14880,21 @@ function formatLimitNotice(fields, opts = {}) {
14799
14880
  hour: "numeric",
14800
14881
  minute: "2-digit"
14801
14882
  }).format(new Date(fields.clears_at));
14802
- parts.push(`back at ${formatted}`);
14883
+ waitBits.push(`back at ${formatted}`);
14803
14884
  } else {
14804
- parts.push(`back in ${Math.max(1, Math.ceil(retry / 60))} min`);
14885
+ waitBits.push(`back in ${Math.max(1, Math.ceil(retry / 60))} min`);
14805
14886
  }
14806
14887
  }
14888
+ if (opts.copyStyle === "treatment" && REFILL_CLOCK_LIMITS.has(fields.limit) && fields.clears_at) {
14889
+ const clears = new Date(fields.clears_at);
14890
+ if (!Number.isNaN(clears.getTime())) {
14891
+ waitBits.push(`resets ${formatRefillEt(clears)}`);
14892
+ }
14893
+ }
14894
+ if (waitBits.length > 0)
14895
+ parts.push(waitBits.join(" · "));
14807
14896
  if (slow) {
14808
- const wait = parts.length > 1 ? ` ${parts[1][0].toUpperCase()}${parts[1].slice(1)} — or get` : " Get";
14897
+ const wait = waitBits[0] ? ` ${waitBits[0][0].toUpperCase()}${waitBits[0].slice(1)} — or get` : " Get";
14809
14898
  return `${label}${wait} credits at ${EXTRA_CREDITS_URL} to keep going at full speed.`;
14810
14899
  }
14811
14900
  parts.push(`${fields.remaining_credits_today.toLocaleString("en-US")} credits left today`);
@@ -14940,7 +15029,7 @@ var HiveClassifyResponseSchema = exports_external.object({
14940
15029
  // package.json
14941
15030
  var package_default = {
14942
15031
  name: "free-pi-cli",
14943
- version: "0.5.3",
15032
+ version: "0.6.1",
14944
15033
  description: "Free, ad-supported distribution of the pi coding agent. Inference paid for by ads.",
14945
15034
  license: "MIT",
14946
15035
  type: "module",
@@ -14981,6 +15070,8 @@ free-pi — free AI coding in your terminal; ads pay for the inference.
14981
15070
  Usage:
14982
15071
  free-pi-cli start the coding agent (first run: a short guest trial, then GitHub)
14983
15072
  free-pi-cli --session <id> resume a previous session
15073
+ free-pi-cli --continue resume the last session in this folder
15074
+ free-pi-cli --new start a new session
14984
15075
  free-pi-cli --ref <code> first-run referral code (from freepi.ai/?ref=)
14985
15076
  free-pi-cli --src <channel> first-run campaign attribution (from freepi.ai/?src=)
14986
15077
  free-pi-cli logout remove the stored sign-in token; the next run signs in again
@@ -15016,6 +15107,10 @@ function parseCliArgs(argv) {
15016
15107
  let i = 0;
15017
15108
  while (i < argv.length) {
15018
15109
  const arg = argv[i];
15110
+ if (arg === "--continue" || arg === "--resume" || arg === "--new") {
15111
+ i += 1;
15112
+ continue;
15113
+ }
15019
15114
  if (arg === "--session" || arg === "--ref" || arg === "--src") {
15020
15115
  i += 1;
15021
15116
  if (argv[i] && !String(argv[i]).startsWith("--"))
@@ -15326,7 +15421,11 @@ async function fetchMe(deps) {
15326
15421
  const res = await authedFetch(deps, "/me");
15327
15422
  if (!res.ok)
15328
15423
  return;
15329
- return await res.json();
15424
+ const me = await res.json();
15425
+ try {
15426
+ deps.onMe?.(me);
15427
+ } catch {}
15428
+ return me;
15330
15429
  } catch {
15331
15430
  return;
15332
15431
  }
@@ -15494,7 +15593,7 @@ function mapErrorCode(code) {
15494
15593
  case "concurrent":
15495
15594
  return "only one request at a time on the free tier — wait for the current one to finish";
15496
15595
  case "concurrent_session":
15497
- return "another free-pi session for your account is already active";
15596
+ return "this account is already at its session limit — run /close-other-session or wait for a slot";
15498
15597
  case "context_ceiling":
15499
15598
  return "this conversation is too large for free-pi's context limit";
15500
15599
  case "upstream_error":
@@ -15506,7 +15605,7 @@ function mapErrorCode(code) {
15506
15605
  case "upgrade_required":
15507
15606
  return "your free-pi-cli is out of date — run `npx free-pi-cli@latest`";
15508
15607
  case "hourly_ceiling":
15509
- return "too many requests this hour on the free tier — wait for the Retry-After window";
15608
+ return "daily allowance still applies — it refills at midnight UTC";
15510
15609
  case "account_review":
15511
15610
  return "this account is under review — contact support";
15512
15611
  case "ads_required":
@@ -15575,9 +15674,23 @@ async function renderMeter(deps, ctx) {
15575
15674
  if (!Number.isNaN(refill.getTime()) && me.lane !== "wait") {
15576
15675
  const left = me.remaining_credits_today;
15577
15676
  const bought = typeof me.bonus_credits === "number" && me.bonus_credits > 0 ? ` + ${me.bonus_credits.toLocaleString("en-US")}` : "";
15578
- ctx.ui.setStatus(METER_STATUS_KEY, theme.fg("muted", `${left.toLocaleString("en-US")}${bought} credits`));
15677
+ const refill2 = new Date(me.refill_at);
15678
+ const treatment = me.visible_allowance === true;
15679
+ const status = treatment ? formatVisibleAllowanceStatus({
15680
+ remaining: left,
15681
+ bonusCredits: typeof me.bonus_credits === "number" ? me.bonus_credits : undefined,
15682
+ refillAt: refill2,
15683
+ guest: me.guest === true
15684
+ }) : `${left.toLocaleString("en-US")}${bought} credits`;
15685
+ ctx.ui.setStatus(METER_STATUS_KEY, theme.fg("muted", status));
15579
15686
  const low = left > 0 && left <= me.allowance_credits * LOW_CREDIT_FRACTION;
15580
- ctx.ui.setWidget(METER_WIDGET_ID, low ? [renderWarningLine(`Low on credits: ${left.toLocaleString("en-US")} left today${buyMore(me.buy_url)}`, theme)] : undefined);
15687
+ const warning = treatment ? formatLowAllowanceWarning({
15688
+ remaining: left,
15689
+ refillAt: refill2,
15690
+ guest: me.guest === true,
15691
+ suffix: buyMore(me.buy_url)
15692
+ }) : `Low on credits: ${left.toLocaleString("en-US")} left today${buyMore(me.buy_url)}`;
15693
+ ctx.ui.setWidget(METER_WIDGET_ID, low ? [renderWarningLine(warning, theme)] : undefined);
15581
15694
  return;
15582
15695
  }
15583
15696
  }
@@ -15598,7 +15711,8 @@ function createAdsExtension(opts) {
15598
15711
  getToken: opts.getToken,
15599
15712
  sessionId: opts.sessionId,
15600
15713
  fetchImpl: opts.fetchImpl,
15601
- getGravityContext: opts.getGravityContext
15714
+ getGravityContext: opts.getGravityContext,
15715
+ onMe: opts.onMe
15602
15716
  };
15603
15717
  const config3 = loadConfig();
15604
15718
  return {
@@ -15651,7 +15765,7 @@ function createAdsExtension(opts) {
15651
15765
  }
15652
15766
 
15653
15767
  // src/pi-launch.ts
15654
- import { dirname as dirname7, join as join9, resolve as resolve3 } from "node:path";
15768
+ import { dirname as dirname8, join as join10, resolve as resolve3 } from "node:path";
15655
15769
  import { fileURLToPath as fileURLToPath4 } from "node:url";
15656
15770
 
15657
15771
  // src/provider.ts
@@ -15679,7 +15793,8 @@ function buildProviderConfig(baseUrl, jwt2, sessionId, models, name = PROVIDER_N
15679
15793
  models: models.map((m) => ({
15680
15794
  id: m.id,
15681
15795
  name: m.name,
15682
- reasoning: false,
15796
+ reasoning: true,
15797
+ compat: { supportsDeveloperRole: false },
15683
15798
  input: ["text"],
15684
15799
  cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
15685
15800
  contextWindow: 1048576,
@@ -20318,7 +20433,7 @@ at first run, before the GitHub sign-in.
20318
20433
  Each user gets a free allowance every day. The allowance can change with
20319
20434
  supply and demand. \`/usage\` shows how much of today's allowance is spent, as
20320
20435
  a percentage, with token counts.
20321
- One session per account runs at a time. If a previous session is stuck,
20436
+ You can run free-pi in more than one terminal. If a previous session is stuck,
20322
20437
  \`/close-other-session\` frees it.
20323
20438
 
20324
20439
  ## Credits
@@ -20328,6 +20443,7 @@ first. Purchased credits cover use beyond it and do not expire.
20328
20443
  ## Commands
20329
20444
  - \`/usage\` — spend and remaining budget today
20330
20445
  - \`/support\` — visit today's advertiser
20446
+ - \`/feedback <text>\` — send feedback to the free-pi team
20331
20447
  - \`/tos\` — open the Terms of Service
20332
20448
  - \`/privacy-policy\` — open the Privacy Policy
20333
20449
  - \`/buy-credits\` — get more usage
@@ -20763,14 +20879,20 @@ function parseErrorEnvelope(errorMessage) {
20763
20879
  parsed.lane = "slow";
20764
20880
  if (ceiling !== undefined)
20765
20881
  parsed.hourly_ceiling = ceiling;
20882
+ if (body.prompt === "github_signin")
20883
+ parsed.prompt = "github_signin";
20884
+ if (body.copy_variant === "control" || body.copy_variant === "treatment") {
20885
+ parsed.copy_variant = body.copy_variant;
20886
+ }
20766
20887
  return parsed;
20767
20888
  } catch {
20768
20889
  return;
20769
20890
  }
20770
20891
  }
20771
20892
  function formatLimitEnvelope(env, opts) {
20772
- if (env.code === "concurrent_session" || typeof env.remaining_credits_today !== "number")
20893
+ if (env.code === "concurrent_session" || env.prompt === "github_signin" || env.copy_variant === "treatment" || typeof env.remaining_credits_today !== "number") {
20773
20894
  return env.message;
20895
+ }
20774
20896
  return formatLimitNotice({
20775
20897
  limit: env.limit ?? env.code,
20776
20898
  retry_after_seconds: env.retry_after_seconds,
@@ -20779,7 +20901,7 @@ function formatLimitEnvelope(env, opts) {
20779
20901
  allowance_credits: env.allowance_credits,
20780
20902
  lane: env.lane,
20781
20903
  hourly_ceiling: env.hourly_ceiling
20782
- }, { localTime: true, timeZone: opts?.timeZone });
20904
+ }, { localTime: true, timeZone: opts?.timeZone, copyStyle: env.copy_variant });
20783
20905
  }
20784
20906
  function createErrorNoticeExtension(opts = {}) {
20785
20907
  const lastAt = new Map;
@@ -20853,21 +20975,35 @@ function createToolGuardExtension(opts) {
20853
20975
  var BUG_USAGE = "Usage: /bug <what went wrong>";
20854
20976
  var BUG_DESCRIPTION = "Report a bug to free-pi";
20855
20977
  var LOGIN_DESCRIPTION = "Sign in with GitHub to unlock daily credits and a comeback bonus.";
20978
+ var FEEDBACK_USAGE = "Usage: /feedback <what you think>";
20979
+ var FEEDBACK_DESCRIPTION = "Send feedback to the free-pi team";
20980
+ var FEEDBACK_PREFIX = "[feedback] ";
20981
+ var FEEDBACK_MAX = 4000 - FEEDBACK_PREFIX.length;
20982
+ function parseFeedbackArgs(args) {
20983
+ const text = args.trim();
20984
+ if (!text)
20985
+ return { kind: "usage" };
20986
+ if (text.length > FEEDBACK_MAX)
20987
+ return { kind: "too_long" };
20988
+ return { kind: "send", description: FEEDBACK_PREFIX + text };
20989
+ }
20856
20990
  function buildBugRequest(description, ctx) {
20857
20991
  return { description, session_id: ctx.sessionId, cli_version: CLI_VERSION, model: ctx.model };
20858
20992
  }
20859
- function mapBugFailure(status) {
20993
+ function mapBugFailure(status, kind = "bug") {
20994
+ const fb = kind === "feedback";
20995
+ const what = fb ? "feedback" : "the bug report";
20860
20996
  if (status === 401)
20861
- return "Couldn't send the bug report: you're signed out. Restart free-pi-cli to sign in again.";
20997
+ return `Couldn't send ${what}: you're signed out. Restart free-pi-cli to sign in again.`;
20862
20998
  if (status === 429)
20863
- return "Couldn't send the bug report: you've reached today's limit of 20 reports. Try again tomorrow.";
20999
+ return `Couldn't send ${what}: you've reached today's limit of 20 ${fb ? "reports and feedback messages" : "reports"}. Try again tomorrow.`;
20864
21000
  if (status === 413)
20865
- return "Couldn't send the bug report: it is too large.";
21001
+ return `Couldn't send ${what}: it is too large.`;
20866
21002
  if (status === 400)
20867
- return "Couldn't send the bug report: the description is too long (4000 characters max).";
20868
- return `Couldn't send the bug report (the server returned ${status}). Try again in a moment.`;
21003
+ return fb ? `Couldn't send feedback: it is too long (${FEEDBACK_MAX} characters max).` : "Couldn't send the bug report: the description is too long (4000 characters max).";
21004
+ return `Couldn't send ${what} (the server returned ${status}). Try again in a moment.`;
20869
21005
  }
20870
- async function sendBugReport(opts, body) {
21006
+ async function sendBugReport(opts, body, kind = "bug") {
20871
21007
  try {
20872
21008
  const res = await (opts.fetchImpl ?? fetch)(`${opts.baseUrl}/bug-reports`, {
20873
21009
  method: "POST",
@@ -20876,13 +21012,13 @@ async function sendBugReport(opts, body) {
20876
21012
  signal: AbortSignal.timeout(1e4)
20877
21013
  });
20878
21014
  if (!res.ok)
20879
- return { ok: false, message: mapBugFailure(res.status) };
21015
+ return { ok: false, message: mapBugFailure(res.status, kind) };
20880
21016
  const parsed = BugReportResponseSchema.safeParse(await res.json());
20881
21017
  if (!parsed.success)
20882
- return { ok: false, message: mapBugFailure(res.status) };
21018
+ return { ok: false, message: mapBugFailure(res.status, kind) };
20883
21019
  return { ok: true, id: parsed.data.id };
20884
21020
  } catch {
20885
- return { ok: false, message: "Couldn't reach free-pi to send the bug report — check your connection." };
21021
+ return { ok: false, message: `Couldn't reach free-pi to send ${kind === "feedback" ? "feedback" : "the bug report"} — check your connection.` };
20886
21022
  }
20887
21023
  }
20888
21024
  function makeBugHandler(mode, opts) {
@@ -20918,20 +21054,65 @@ function registerBugAutocomplete(pi) {
20918
21054
  }
20919
21055
 
20920
21056
  // src/footer-align.ts
20921
- var ANSI = /\x1b\[[0-9;]*m/g;
20922
- var SIMPLE = /^[\x20-\x7e\xa0-\xff]*$/;
21057
+ import { truncateToWidth as truncateToWidth4, visibleWidth as visibleWidth2 } from "@earendil-works/pi-tui";
21058
+ var MIN_GAP = 2;
21059
+ var MIN_CWD = 8;
21060
+ var LEADING_SGR = /^(?:\x1b\[[0-9;]*m)+/;
21061
+ function mergeStatusIntoCwdLine(lines, width) {
21062
+ if (lines.length < 3)
21063
+ return lines;
21064
+ const [cwd, stats] = lines;
21065
+ const status = lines[lines.length - 1];
21066
+ const statusW = visibleWidth2(status);
21067
+ let left = cwd;
21068
+ if (visibleWidth2(left) + MIN_GAP + statusW > width) {
21069
+ const room = width - MIN_GAP - statusW;
21070
+ if (room < MIN_CWD)
21071
+ return lines;
21072
+ const style = LEADING_SGR.exec(cwd)?.[0] ?? "";
21073
+ left = truncateToWidth4(cwd, room, style ? `${style}...\x1B[0m` : "...");
21074
+ }
21075
+ const gap = " ".repeat(Math.max(MIN_GAP, width - visibleWidth2(left) - statusW));
21076
+ return [left + gap + status, stats, ...lines.slice(2, -1)];
21077
+ }
20923
21078
  function installFooterRightAlign(mode) {
20924
21079
  const footer = mode.footer;
20925
21080
  const render = footer.render.bind(footer);
20926
- footer.render = (w) => {
20927
- const lines = render(w);
20928
- if (lines.length <= 2)
20929
- return lines;
20930
- const last = lines[lines.length - 1];
20931
- const text = last.replace(ANSI, "");
20932
- if (!SIMPLE.test(text))
20933
- return lines;
20934
- return [...lines.slice(0, -1), " ".repeat(Math.max(0, w - text.length)) + last];
21081
+ footer.render = (w) => mergeStatusIntoCwdLine(render(w), w);
21082
+ }
21083
+
21084
+ // src/guest-handoff.ts
21085
+ var LOGIN_VALUE_LINE = "sign in free with GitHub: full daily allowance, FREE abliterated DeepSeek, history saved";
21086
+ var GUEST_NEAR_WALL_LINE = `A few guest requests left. ${LOGIN_VALUE_LINE}`;
21087
+ var SIGNED_IN_CONTINUE_LINE = "Signed in. Your conversation continues — full daily allowance is unlocked.";
21088
+ var HEADLESS_FALLBACK_LINE = "No browser or clipboard here — open the URL and enter the code above.";
21089
+ function isHandoffTreatment(variant) {
21090
+ return variant === "treatment";
21091
+ }
21092
+ var GUEST_NOTES = [
21093
+ "Signed-in users get the Defense models in /model, for security work on code and systems you own. Sign in free with /login.",
21094
+ "With the Defense models you can audit your own repo for vulnerabilities and get fixes, without the usual refusals. /login to unlock.",
21095
+ "Write detection rules and review your server configs with the Defense models. Sign in free with /login.",
21096
+ "More on security work with free-pi: https://freepi.ai/cybersecurity. /login to unlock the Defense models."
21097
+ ];
21098
+ var GUEST_NOTE_EVERY_TURNS = 3;
21099
+ function createGuestNotesExtension(opts) {
21100
+ let turns = 0;
21101
+ let shown = 0;
21102
+ return {
21103
+ name: "free-pi-guest-notes",
21104
+ factory(pi) {
21105
+ pi.on("agent_end", (event, ctx) => {
21106
+ if (!opts.isGuest() || shown >= GUEST_NOTES.length)
21107
+ return;
21108
+ const last = [...event.messages].reverse().find((m) => m.role === "assistant");
21109
+ if (last?.stopReason !== "stop")
21110
+ return;
21111
+ if (++turns % GUEST_NOTE_EVERY_TURNS !== 0)
21112
+ return;
21113
+ ctx.ui.notify(GUEST_NOTES[shown++], "info");
21114
+ });
21115
+ }
20935
21116
  };
20936
21117
  }
20937
21118
 
@@ -20977,6 +21158,25 @@ function mergeAttribution(a, b) {
20977
21158
 
20978
21159
  // src/changelog.ts
20979
21160
  var CHANGELOG_HIGHLIGHTS = [
21161
+ {
21162
+ version: "0.6.1",
21163
+ highlights: [
21164
+ "/model shows the Defense models by their real names.",
21165
+ "Five more Defense models: Abliterated Large V2, Qwen 3.6 Plus Uncensored, Gemma 4 Uncensored, GLM 4.7 Flash Heretic and Uncensored 1.2.",
21166
+ "The footer is two lines: credits left now sit next to the folder name.",
21167
+ "The welcome text no longer says you can only run one session."
21168
+ ]
21169
+ },
21170
+ {
21171
+ version: "0.6.0",
21172
+ highlights: [
21173
+ "Send feedback with /feedback <what you think>.",
21174
+ "Turn thinking on with the thinking-level control (Shift+Tab). It starts off.",
21175
+ "Run free-pi in more than one terminal at once.",
21176
+ "Pick up your last session in this folder when you start free-pi (some users).",
21177
+ "The Defense models now show their real model names."
21178
+ ]
21179
+ },
20980
21180
  {
20981
21181
  version: "0.5.3",
20982
21182
  highlights: [
@@ -20986,7 +21186,7 @@ var CHANGELOG_HIGHLIGHTS = [
20986
21186
  },
20987
21187
  {
20988
21188
  version: "0.5.2",
20989
- highlights: ["/model now lists Defense Flash right under Fusion."]
21189
+ highlights: ["/model now lists the Defense models right under Fusion."]
20990
21190
  },
20991
21191
  {
20992
21192
  version: "0.5.1",
@@ -21267,7 +21467,8 @@ async function startGuestSession(baseUrl, consentVersion, attribution = {}) {
21267
21467
  return {
21268
21468
  token: parsed.token,
21269
21469
  allowance_usd: parsed.allowance_usd,
21270
- remaining_usd: parsed.remaining_usd
21470
+ remaining_usd: parsed.remaining_usd,
21471
+ guestHandoffVariant: parsed.experiments?.guest_handoff
21271
21472
  };
21272
21473
  }
21273
21474
  async function reportFunnelEvent(baseUrl, event, opts = {}) {
@@ -21298,7 +21499,15 @@ async function pollForToken(baseUrl, sessionId, interval, sleep, opts = {}) {
21298
21499
  }
21299
21500
  let res;
21300
21501
  try {
21301
- res = await postJson(baseUrl, "/auth/token", { session_id: sessionId });
21502
+ const headers = { "content-type": "application/json" };
21503
+ if (opts.jwt)
21504
+ headers.authorization = `Bearer ${opts.jwt}`;
21505
+ const doFetch = opts.fetchImpl ?? fetch;
21506
+ res = await doFetch(new URL("/auth/token", baseUrl), {
21507
+ method: "POST",
21508
+ headers,
21509
+ body: JSON.stringify({ session_id: sessionId })
21510
+ });
21302
21511
  consecutiveNetworkFailures = 0;
21303
21512
  } catch (err) {
21304
21513
  consecutiveNetworkFailures++;
@@ -21369,8 +21578,11 @@ async function submitUserConsent(baseUrl, jwt2, consentVersion) {
21369
21578
 
21370
21579
  // src/clipboard.ts
21371
21580
  import { spawn as spawn2 } from "node:child_process";
21372
- function copyToClipboard(text, platform = process.platform, spawnImpl = spawn2) {
21581
+ import { existsSync as existsSync2 } from "node:fs";
21582
+ function copyToClipboard(text, platform = process.platform, spawnImpl = spawn2, env = process.env) {
21373
21583
  try {
21584
+ if (!clipboardAvailable(platform, env))
21585
+ return false;
21374
21586
  const cmd = clipboardCommand(platform);
21375
21587
  if (!cmd)
21376
21588
  return false;
@@ -21383,6 +21595,24 @@ function copyToClipboard(text, platform = process.platform, spawnImpl = spawn2)
21383
21595
  return false;
21384
21596
  }
21385
21597
  }
21598
+ function clipboardAvailable(platform = process.platform, env = process.env) {
21599
+ if (platform === "darwin" || platform === "win32")
21600
+ return commandOnPath(clipboardCommand(platform)?.command, env);
21601
+ if (platform !== "linux")
21602
+ return false;
21603
+ if (!env.DISPLAY && !env.WAYLAND_DISPLAY)
21604
+ return false;
21605
+ const cmd = clipboardCommand(platform);
21606
+ return commandOnPath(cmd?.command, env);
21607
+ }
21608
+ function commandOnPath(command, env) {
21609
+ if (!command)
21610
+ return false;
21611
+ if (command.includes("/"))
21612
+ return existsSync2(command);
21613
+ const path = env.PATH ?? "";
21614
+ return path.split(":").some((dir) => dir.length > 0 && existsSync2(`${dir}/${command}`));
21615
+ }
21386
21616
  function clipboardCommand(platform) {
21387
21617
  if (platform === "darwin")
21388
21618
  return { command: "pbcopy", args: [] };
@@ -21444,7 +21674,7 @@ function allowanceIsLow(remaining, allowance) {
21444
21674
  return false;
21445
21675
  return remaining * LOW_ALLOWANCE_DENOMINATOR <= allowance * LOW_ALLOWANCE_NUMERATOR;
21446
21676
  }
21447
- function formatRefillLocal(refillAt, timeZone) {
21677
+ function formatRefillLocal2(refillAt, timeZone) {
21448
21678
  const opts = {
21449
21679
  hour: "numeric",
21450
21680
  minute: "2-digit",
@@ -21464,7 +21694,7 @@ function refillAtFromIso(iso, now = new Date) {
21464
21694
  return nextUtcMidnight(now);
21465
21695
  }
21466
21696
  function formatRefillCard(input) {
21467
- const refill = formatRefillLocal(input.refillAt, input.timeZone);
21697
+ const refill = formatRefillLocal2(input.refillAt, input.timeZone);
21468
21698
  const lines = [
21469
21699
  `${input.allowance.toLocaleString("en-US")} free credits refill at ${refill}.`
21470
21700
  ];
@@ -21493,6 +21723,7 @@ async function fetchRefillStats(baseUrl, jwt2, fetchImpl = fetch) {
21493
21723
  allowanceCredits: body.allowance_credits,
21494
21724
  refillAt: typeof body.refill_at === "string" ? body.refill_at : undefined,
21495
21725
  showCard: body.return_bonus?.show_card === true,
21726
+ creditsBack: body.return_bonus?.credits_back === true,
21496
21727
  guest: body.guest === true
21497
21728
  };
21498
21729
  } catch {
@@ -21523,6 +21754,12 @@ function sessionFromStats(stats) {
21523
21754
  shown: false
21524
21755
  };
21525
21756
  }
21757
+ function formatCreditsBackNudge(stats, timeZone) {
21758
+ if (stats.guest || !stats.creditsBack)
21759
+ return;
21760
+ const refill = formatRefillLocal2(refillAtFromIso(stats.refillAt), timeZone);
21761
+ return `Your credits are back. ${stats.allowanceCredits.toLocaleString("en-US")} free credits refill at ${refill}.`;
21762
+ }
21526
21763
  function formatLowAllowanceCard(session, remaining, timeZone) {
21527
21764
  if (!session.show || session.shown || session.allowance === undefined)
21528
21765
  return;
@@ -21551,14 +21788,48 @@ function formatExitRefillCard(session, end, timeZone) {
21551
21788
  });
21552
21789
  }
21553
21790
 
21791
+ // src/pending-login.ts
21792
+ import { mkdir as mkdir4, readFile as readFile3, rm as rm4, writeFile as writeFile3 } from "node:fs/promises";
21793
+ import { dirname as dirname5, join as join6 } from "node:path";
21794
+ var FINISH_SIGNIN_LINE = "one step to finish signing in";
21795
+ function pendingLoginPath(credentialsPath) {
21796
+ return join6(dirname5(credentialsPath), "pending-login.json");
21797
+ }
21798
+ async function loadPendingLogin(credentialsPath) {
21799
+ try {
21800
+ const raw = await readFile3(pendingLoginPath(credentialsPath), "utf8");
21801
+ const parsed = JSON.parse(raw);
21802
+ if (typeof parsed.session_id !== "string" || typeof parsed.user_code !== "string" || typeof parsed.verification_uri !== "string" || typeof parsed.expires_at !== "number") {
21803
+ return null;
21804
+ }
21805
+ return {
21806
+ session_id: parsed.session_id,
21807
+ user_code: parsed.user_code,
21808
+ verification_uri: parsed.verification_uri,
21809
+ expires_at: parsed.expires_at
21810
+ };
21811
+ } catch {
21812
+ return null;
21813
+ }
21814
+ }
21815
+ async function savePendingLogin(credentialsPath, pending) {
21816
+ const path = pendingLoginPath(credentialsPath);
21817
+ await mkdir4(dirname5(path), { recursive: true });
21818
+ await writeFile3(path, JSON.stringify(pending), { mode: 384 });
21819
+ }
21820
+ async function clearPendingLogin(credentialsPath) {
21821
+ await rm4(pendingLoginPath(credentialsPath), { force: true });
21822
+ }
21823
+
21554
21824
  // src/login-ux.ts
21555
21825
  var LOGIN_REWARD_LINE = "Sign in with GitHub to get 15,000 free credits a day";
21556
21826
  var LOGIN_WAITING_LINE = "Waiting for GitHub…";
21557
21827
  var LOGIN_TIMEOUT_LINE = "GitHub sign-in timed out. Run /login to try again.";
21558
- function formatDeviceCodePrompt(device) {
21828
+ function formatDeviceCodePrompt(device, variant) {
21829
+ const value = isHandoffTreatment(variant) ? LOGIN_VALUE_LINE : LOGIN_REWARD_LINE;
21559
21830
  return [
21560
21831
  "",
21561
- LOGIN_REWARD_LINE,
21832
+ value,
21562
21833
  LOGIN_BONUS_UNLOCK_LINE,
21563
21834
  "",
21564
21835
  ` Open ${device.verification_uri}`,
@@ -21569,7 +21840,15 @@ function formatDeviceCodePrompt(device) {
21569
21840
  }
21570
21841
  async function runDeviceLogin(deps, attribution = {}) {
21571
21842
  const device = await startDeviceFlow(deps.baseUrl);
21572
- deps.log(formatDeviceCodePrompt(device));
21843
+ deps.log(formatDeviceCodePrompt(device, deps.variant));
21844
+ if (deps.credentialsPath) {
21845
+ await savePendingLogin(deps.credentialsPath, {
21846
+ session_id: device.session_id,
21847
+ user_code: device.user_code,
21848
+ verification_uri: device.verification_uri,
21849
+ expires_at: (deps.now ?? Date.now)() + (device.expires_in ?? 900) * 1000
21850
+ });
21851
+ }
21573
21852
  const copied = (deps.copy ?? copyToClipboard)(device.user_code);
21574
21853
  if (copied)
21575
21854
  deps.log(` (code copied to clipboard)
@@ -21587,12 +21866,66 @@ async function runDeviceLogin(deps, attribution = {}) {
21587
21866
  jwt: deps.jwt,
21588
21867
  fetchImpl: deps.fetchImpl
21589
21868
  });
21869
+ } else if (!copied) {
21870
+ deps.log(HEADLESS_FALLBACK_LINE);
21590
21871
  }
21591
21872
  await submitConsent(deps.baseUrl, device.session_id, CONSENT_VERSION, attribution);
21592
21873
  const expiresInMs = (device.expires_in ?? 900) * 1000;
21593
21874
  const deadline = (deps.now ?? Date.now)() + expiresInMs;
21594
- const token = await pollForToken(deps.baseUrl, device.session_id, device.interval, deps.sleep, { deadline, now: deps.now });
21595
- return token;
21875
+ try {
21876
+ const token = await pollForToken(deps.baseUrl, device.session_id, device.interval, deps.sleep, { deadline, now: deps.now, jwt: deps.jwt, fetchImpl: deps.fetchImpl });
21877
+ if (deps.credentialsPath)
21878
+ await clearPendingLogin(deps.credentialsPath);
21879
+ return token;
21880
+ } catch (err) {
21881
+ throw err;
21882
+ }
21883
+ }
21884
+
21885
+ // src/allowance-counter.ts
21886
+ var cached2;
21887
+ function updateAllowanceCache(input) {
21888
+ if (typeof input.remaining_credits_today !== "number" || typeof input.allowance_credits !== "number") {
21889
+ return cached2;
21890
+ }
21891
+ const refill = typeof input.refill_at === "string" && !Number.isNaN(new Date(input.refill_at).getTime()) ? new Date(input.refill_at) : undefined;
21892
+ cached2 = {
21893
+ remaining: input.remaining_credits_today,
21894
+ allowance: input.allowance_credits,
21895
+ refillAt: refill,
21896
+ bonusCredits: typeof input.bonus_credits === "number" ? input.bonus_credits : undefined,
21897
+ guest: input.guest === true,
21898
+ visible: input.visible_allowance === true
21899
+ };
21900
+ return cached2;
21901
+ }
21902
+ function allowanceLine(snapshot = cached2, timeZone) {
21903
+ if (!snapshot?.visible)
21904
+ return;
21905
+ return formatVisibleAllowanceLine({
21906
+ remaining: snapshot.remaining,
21907
+ refillAt: snapshot.refillAt,
21908
+ guest: snapshot.guest,
21909
+ timeZone
21910
+ });
21911
+ }
21912
+ function installModelAllowanceHandler(mode, getLine = () => allowanceLine()) {
21913
+ const m = mode;
21914
+ if (typeof m.handleModelCommand !== "function" || typeof m.showStatus !== "function")
21915
+ return false;
21916
+ const original = m.handleModelCommand.bind(mode);
21917
+ m.handleModelCommand = () => {
21918
+ const line = getLine();
21919
+ if (line)
21920
+ m.showStatus(line);
21921
+ return original();
21922
+ };
21923
+ return true;
21924
+ }
21925
+ function notifyAllowance(ctx) {
21926
+ const line = allowanceLine();
21927
+ if (line)
21928
+ ctx.ui.notify(line, "info");
21596
21929
  }
21597
21930
 
21598
21931
  // src/commands.ts
@@ -21615,6 +21948,18 @@ async function closeOtherSession(opts, ctx) {
21615
21948
  ctx.ui.notify("Couldn't reach free-pi to reset your session — check your connection.", "error");
21616
21949
  }
21617
21950
  }
21951
+ async function sendFeedback(opts, args, ctx) {
21952
+ const parsed = parseFeedbackArgs(args);
21953
+ if (parsed.kind === "usage")
21954
+ return ctx.ui.notify(FEEDBACK_USAGE, "info");
21955
+ if (parsed.kind === "too_long")
21956
+ return ctx.ui.notify(`Couldn't send feedback: it is too long (${FEEDBACK_MAX} characters max).`, "error");
21957
+ const r = await sendBugReport({ baseUrl: opts.baseUrl, getToken: opts.getToken, fetchImpl: opts.fetchImpl }, buildBugRequest(parsed.description, { sessionId: ctx.sessionManager?.getSessionId(), model: ctx.model?.id }), "feedback");
21958
+ if (r.ok)
21959
+ ctx.ui.notify(`Thanks for the feedback (#${r.id}).`, "info");
21960
+ else
21961
+ ctx.ui.notify(r.message, "error");
21962
+ }
21618
21963
  function showWhatsNew(ctx) {
21619
21964
  const lines = CHANGELOG_HIGHLIGHTS.slice(0, 3).flatMap((e) => [
21620
21965
  `free-pi ${e.version}:`,
@@ -21679,21 +22024,27 @@ async function openPrivacyPolicy(opts, ctx) {
21679
22024
  (opts.openBrowserImpl ?? openBrowser)(PRIVACY_URL);
21680
22025
  ctx.ui.notify(`Opening the Privacy Policy in your browser: ${PRIVACY_URL}`, "info");
21681
22026
  }
22027
+ function resolveHandoffVariant(opts) {
22028
+ return typeof opts.variant === "function" ? opts.variant() : opts.variant;
22029
+ }
21682
22030
  async function runLogin(opts, ctx) {
21683
22031
  try {
21684
22032
  const attribution = opts.agentDir ? await loadAttribution(getAttributionPath(opts.agentDir)) : {};
22033
+ const variant = resolveHandoffVariant(opts);
21685
22034
  const token = await runDeviceLogin({
21686
22035
  baseUrl: opts.baseUrl,
21687
22036
  openBrowser: opts.openBrowserImpl ?? openBrowser,
21688
22037
  sleep: (ms) => new Promise((r) => setTimeout(r, ms)),
21689
22038
  log: (message) => ctx.ui.notify(message.trim(), "info"),
21690
22039
  jwt: await opts.getToken(),
21691
- fetchImpl: opts.fetchImpl
22040
+ fetchImpl: opts.fetchImpl,
22041
+ variant,
22042
+ credentialsPath: opts.credentialsPath
21692
22043
  }, attribution);
21693
22044
  if (opts.credentialsPath)
21694
22045
  await saveJwt(opts.credentialsPath, token);
21695
22046
  opts.onLogin?.(token);
21696
- ctx.ui.notify("Signed in. Restart free-pi-cli to unlock 15,000 free credits a day and a one-time comeback bonus.", "info");
22047
+ ctx.ui.notify(isHandoffTreatment(variant) ? SIGNED_IN_CONTINUE_LINE : "Signed in. Restart free-pi-cli to unlock 15,000 free credits a day and a one-time comeback bonus.", "info");
21697
22048
  } catch (err) {
21698
22049
  if (err instanceof LoginTimeoutError) {
21699
22050
  ctx.ui.notify(LOGIN_TIMEOUT_LINE, "warning");
@@ -21807,23 +22158,34 @@ function loadRea(pi, opts, ctx) {
21807
22158
  }
21808
22159
  function createGuestLoginExtension(opts) {
21809
22160
  let started = false;
22161
+ let nearWallWarned = false;
21810
22162
  const start = (ctx) => {
21811
22163
  started = true;
21812
- ctx.ui.notify(LOGIN_REWARD_LINE, "info");
22164
+ const variant = resolveHandoffVariant(opts);
22165
+ ctx.ui.notify(isHandoffTreatment(variant) ? LOGIN_VALUE_LINE : LOGIN_REWARD_LINE, "info");
21813
22166
  runLogin(opts, ctx);
21814
22167
  };
21815
22168
  return {
21816
22169
  name: "free-pi-guest-login",
21817
22170
  factory(pi) {
21818
22171
  pi.on("turn_end", (event, ctx) => {
21819
- if (!opts.isGuest?.() || started)
22172
+ if (!opts.isGuest?.())
21820
22173
  return;
21821
22174
  const msg = event.message;
21822
- if (msg?.stopReason !== "error")
22175
+ if (!started && msg?.stopReason === "error" && parseErrorEnvelope(msg.errorMessage)?.code === "guest_allowance") {
22176
+ start(ctx);
21823
22177
  return;
21824
- if (parseErrorEnvelope(msg.errorMessage)?.code !== "guest_allowance")
22178
+ }
22179
+ if (started || nearWallWarned || !isHandoffTreatment(resolveHandoffVariant(opts)))
21825
22180
  return;
21826
- start(ctx);
22181
+ if (msg?.stopReason === "error")
22182
+ return;
22183
+ Promise.resolve(opts.getToken()).then((jwt2) => fetchMe2(opts.baseUrl, jwt2)).then((me) => {
22184
+ if (nearWallWarned || started || !me?.guest_near_wall)
22185
+ return;
22186
+ nearWallWarned = true;
22187
+ ctx.ui.notify(GUEST_NEAR_WALL_LINE, "warning");
22188
+ }).catch(() => {});
21827
22189
  });
21828
22190
  pi.on("agent_end", (event, ctx) => {
21829
22191
  if (!opts.isGuest?.() || started)
@@ -21857,10 +22219,17 @@ function createFreePiCommandsExtension(opts) {
21857
22219
  description: "Show credits left today, refill time, and purchased credit.",
21858
22220
  handler: (_args, ctx) => showUsage(opts, ctx)
21859
22221
  });
22222
+ pi.on("model_select", (_event, ctx) => {
22223
+ notifyAllowance(ctx);
22224
+ });
21860
22225
  pi.registerCommand("buy-credits", {
21861
22226
  description: "Buy more usage ($5 or $10 packs, card or USDC) — opens the buy page.",
21862
22227
  handler: (_args, ctx) => buyCredits(opts, ctx)
21863
22228
  });
22229
+ pi.registerCommand("feedback", {
22230
+ description: FEEDBACK_DESCRIPTION,
22231
+ handler: (args, ctx) => sendFeedback(opts, args, ctx)
22232
+ });
21864
22233
  pi.registerCommand("support", {
21865
22234
  description: "Open today's advertiser in your browser to support free-pi.",
21866
22235
  handler: (_args, ctx) => openAdvertiser(opts, ctx)
@@ -21936,7 +22305,7 @@ function truncate2(text, maxWidth) {
21936
22305
  return text.slice(0, maxWidth);
21937
22306
  return `${text.slice(0, maxWidth - 1)}…`;
21938
22307
  }
21939
- function headerLines(theme, modelName, expanded, width = Number.POSITIVE_INFINITY, updateLatest, guest) {
22308
+ function headerLines(theme, modelName, expanded, width = Number.POSITIVE_INFINITY, updateLatest, guest, allowanceLine2) {
21940
22309
  const inner = width - 1;
21941
22310
  const fit2 = (plain, styled) => plain.length <= inner ? styled() : theme.fg("dim", truncate2(plain, inner));
21942
22311
  const titleRest = ` · ${modelName} · ctrl+o for help`;
@@ -21952,6 +22321,7 @@ function headerLines(theme, modelName, expanded, width = Number.POSITIVE_INFINIT
21952
22321
  "",
21953
22322
  dim(WELCOME_LINE),
21954
22323
  ...guest ? [dim(GUEST_LINE)] : [],
22324
+ ...allowanceLine2 ? [dim(allowanceLine2)] : [],
21955
22325
  "",
21956
22326
  dim(CONSENT_LINE_1),
21957
22327
  dim(CONSENT_LINE_2),
@@ -21975,7 +22345,7 @@ function createHeaderExtension(opts) {
21975
22345
  let expanded = false;
21976
22346
  return {
21977
22347
  render(width) {
21978
- return headerLines(theme, opts.modelName, expanded, width, opts.updateLatest, opts.guest);
22348
+ return headerLines(theme, opts.modelName, expanded, width, opts.updateLatest, opts.guest, opts.allowanceLine);
21979
22349
  },
21980
22350
  invalidate() {},
21981
22351
  setExpanded(next) {
@@ -22199,12 +22569,12 @@ function resolveFreePiScope(modelRuntime, providerName, modelIds, env = process.
22199
22569
  import { createHash as createHash3 } from "node:crypto";
22200
22570
  import { execFileSync } from "node:child_process";
22201
22571
  import { lstatSync as lstatSync2, readFileSync as readFileSync3, readlinkSync as readlinkSync2 } from "node:fs";
22202
- import { join as join7 } from "node:path";
22572
+ import { join as join8 } from "node:path";
22203
22573
 
22204
22574
  // src/hive-guard.ts
22205
- import { existsSync as existsSync2, lstatSync, readdirSync, readlinkSync, realpathSync } from "node:fs";
22575
+ import { existsSync as existsSync3, lstatSync, readdirSync, readlinkSync, realpathSync } from "node:fs";
22206
22576
  import { homedir as homedir3 } from "node:os";
22207
- import { basename, dirname as dirname5, isAbsolute, join as join6, relative, resolve, sep } from "node:path";
22577
+ import { basename, dirname as dirname6, isAbsolute, join as join7, relative, resolve, sep } from "node:path";
22208
22578
  import { fileURLToPath as fileURLToPath3 } from "node:url";
22209
22579
  var GUARD_INPUT_LIMIT = 1500;
22210
22580
  var CLASSIFY_TIMEOUT_MS = 2500;
@@ -22216,7 +22586,7 @@ var SAFE_READ_TOOLS = ["read", "grep", "find", "ls"];
22216
22586
  var ALWAYS_ASK = ["codemode", "tool_search", FUSION_PLAN_TOOL, WEB_SEARCH_TOOL_NAME, DOCS_LOOKUP_TOOL_NAME];
22217
22587
  var CLASSIFIED = ["bash", "write", "edit"];
22218
22588
  var SECRET_BASENAME = [/^\.env(\..*)?$/, /\.pem$/, /^id_(rsa|dsa|ecdsa|ed25519)/];
22219
- var SECRET_HOME_DIRS = [".ssh", ".aws", join6(".config", "freepi"), ".free-pi"];
22589
+ var SECRET_HOME_DIRS = [".ssh", ".aws", join7(".config", "freepi"), ".free-pi"];
22220
22590
  var SECRET_IN_TEXT = /(?:^|[\s"'=:/])\.env(?:\.[\w.-]+)?(?=$|[\s"';|&)])|\.pem\b|\bid_(?:rsa|dsa|ecdsa|ed25519)|\.ssh\b|\.aws\b|\.config\/freepi|\.free-pi\b/;
22221
22591
  var BASH_DENY = [
22222
22592
  [/\brm\s+(?:-\S+\s+)*(?:\/|~|\$HOME)\*?(?:\s|$)/, "destructive command"],
@@ -22239,7 +22609,7 @@ function protectedLinkTargets(root, limit) {
22239
22609
  const note = (link) => {
22240
22610
  let target2;
22241
22611
  try {
22242
- target2 = resolve(dirname5(link), readlinkSync(link));
22612
+ target2 = resolve(dirname6(link), readlinkSync(link));
22243
22613
  } catch {
22244
22614
  return;
22245
22615
  }
@@ -22249,7 +22619,7 @@ function protectedLinkTargets(root, limit) {
22249
22619
  out.push(real);
22250
22620
  };
22251
22621
  for (const entry of [".git/hooks", ".git/config"]) {
22252
- const link = join6(root, entry);
22622
+ const link = join7(root, entry);
22253
22623
  try {
22254
22624
  if (lstatSync(link).isSymbolicLink())
22255
22625
  note(link);
@@ -22268,7 +22638,7 @@ function protectedLinkTargets(root, limit) {
22268
22638
  for (const entry of entries) {
22269
22639
  if (++seen > limit)
22270
22640
  return null;
22271
- const path = join6(dir, entry.name);
22641
+ const path = join7(dir, entry.name);
22272
22642
  if (entry.isSymbolicLink()) {
22273
22643
  if (protectedByName(relative(root, path)))
22274
22644
  note(path);
@@ -22318,7 +22688,7 @@ function resolveLikePi(raw, cwd, home, platform) {
22318
22688
  if (p === "~")
22319
22689
  p = home;
22320
22690
  else if (p.startsWith("~/") || platform === "win32" && p.startsWith("~\\"))
22321
- p = join6(home, p.slice(2));
22691
+ p = join7(home, p.slice(2));
22322
22692
  if (/^file:\/\//.test(p))
22323
22693
  p = fileURLToPath3(p);
22324
22694
  return resolve(cwd, p);
@@ -22328,14 +22698,14 @@ function realpathDeepest(p) {
22328
22698
  const rest3 = [];
22329
22699
  for (;; ) {
22330
22700
  try {
22331
- return join6(realpathSync(current), ...rest3.reverse());
22701
+ return join7(realpathSync(current), ...rest3.reverse());
22332
22702
  } catch {
22333
22703
  try {
22334
22704
  lstatSync(current);
22335
22705
  return null;
22336
22706
  } catch {}
22337
22707
  }
22338
- const parent = dirname5(current);
22708
+ const parent = dirname6(current);
22339
22709
  if (parent === current)
22340
22710
  return null;
22341
22711
  rest3.push(basename(current));
@@ -22350,7 +22720,7 @@ function isSecret(target2, home) {
22350
22720
  if (SECRET_BASENAME.some((re) => re.test(basename(target2))))
22351
22721
  return true;
22352
22722
  return SECRET_HOME_DIRS.some((dir) => {
22353
- const lexical = join6(home, dir);
22723
+ const lexical = join7(home, dir);
22354
22724
  return isInside(lexical, target2) || isInside(realpathDeepest(lexical) ?? lexical, target2);
22355
22725
  });
22356
22726
  }
@@ -22371,7 +22741,7 @@ function scanForSecret(dir) {
22371
22741
  if (SECRET_BASENAME.some((re) => re.test(entry.name)))
22372
22742
  return "yes";
22373
22743
  if (entry.isDirectory())
22374
- queue.push(join6(current, entry.name));
22744
+ queue.push(join7(current, entry.name));
22375
22745
  }
22376
22746
  }
22377
22747
  return "no";
@@ -22411,7 +22781,7 @@ function pathVerdict(tool, input, opts, home) {
22411
22781
  const verdict = checkPath(raw, search, opts, home);
22412
22782
  if (verdict)
22413
22783
  return verdict;
22414
- if (tool === "read" && !existsSync2(resolveLikePi(raw, opts.cwd, home, opts.platform ?? process.platform))) {
22784
+ if (tool === "read" && !existsSync3(resolveLikePi(raw, opts.cwd, home, opts.platform ?? process.platform))) {
22415
22785
  return { ask: "file not found; pi may open a similarly named file" };
22416
22786
  }
22417
22787
  return;
@@ -22546,7 +22916,7 @@ function worktreeSnapshot(g) {
22546
22916
  const status = entry.slice(0, 2);
22547
22917
  if (status[0] === "R" || status[0] === "C")
22548
22918
  i++;
22549
- files.set(entry.slice(3), `${status}:${fingerprint(join7(root, entry.slice(3)))}`);
22919
+ files.set(entry.slice(3), `${status}:${fingerprint(join8(root, entry.slice(3)))}`);
22550
22920
  }
22551
22921
  return { files, head: runGit(g, "rev-parse", "HEAD")?.trim() };
22552
22922
  } catch (e) {
@@ -22999,7 +23369,7 @@ ${decision.reason}`, ACTION_DIALOG_MS);
22999
23369
  import { spawnSync } from "node:child_process";
23000
23370
  import {
23001
23371
  chmodSync,
23002
- existsSync as existsSync3,
23372
+ existsSync as existsSync4,
23003
23373
  lstatSync as lstatSync3,
23004
23374
  mkdirSync,
23005
23375
  mkdtempSync,
@@ -23010,7 +23380,7 @@ import {
23010
23380
  writeFileSync
23011
23381
  } from "node:fs";
23012
23382
  import { homedir as homedir4, tmpdir } from "node:os";
23013
- import { dirname as dirname6, join as join8 } from "node:path";
23383
+ import { dirname as dirname7, join as join9 } from "node:path";
23014
23384
  var SECRET_NAME_PATTERNS = [
23015
23385
  "\\.env(\\..*)?",
23016
23386
  "[^/]*\\.pem",
@@ -23045,8 +23415,8 @@ var TOOLCHAIN_ROOTS = [
23045
23415
  ".local/share/pnpm",
23046
23416
  "Library/pnpm"
23047
23417
  ];
23048
- function toolchainRoots(home, exists = existsSync3) {
23049
- return TOOLCHAIN_ROOTS.map((r) => join8(home, r)).filter((p) => exists(p));
23418
+ function toolchainRoots(home, exists = existsSync4) {
23419
+ return TOOLCHAIN_ROOTS.map((r) => join9(home, r)).filter((p) => exists(p));
23050
23420
  }
23051
23421
  var ENV_ALLOW = ["PATH", "LANG", "LC_ALL", "LC_CTYPE", "TERM", "COLORTERM", "TZ", "USER", "LOGNAME", "SHELL", "PI_PROVIDER", "PI_MODEL"];
23052
23422
  function under(path, dir) {
@@ -23064,10 +23434,10 @@ function sandboxEnv(base, o) {
23064
23434
  for (const k of ENV_ALLOW)
23065
23435
  if (base[k] !== undefined)
23066
23436
  env[k] = base[k];
23067
- env.PATH = [...o.developerDir ? [join8(o.developerDir, "usr/bin")] : [], ...o.pathPrefix ?? [], ...(base.PATH ?? "/usr/bin:/bin").split(":")].filter((p) => p.startsWith("/") && !under(p, o.project) && !under(realOrSelf(p), o.project)).join(":");
23437
+ env.PATH = [...o.developerDir ? [join9(o.developerDir, "usr/bin")] : [], ...o.pathPrefix ?? [], ...(base.PATH ?? "/usr/bin:/bin").split(":")].filter((p) => p.startsWith("/") && !under(p, o.project) && !under(realOrSelf(p), o.project)).join(":");
23068
23438
  if (o.developerDir)
23069
23439
  env.DEVELOPER_DIR = o.developerDir;
23070
- env.HOME = join8(o.scratch, "home");
23440
+ env.HOME = join9(o.scratch, "home");
23071
23441
  env.TMP = env.TEMP = o.scratch;
23072
23442
  env.TMPDIR = `${o.scratch}/`;
23073
23443
  env.BUN_OPTIONS = "--no-env-file";
@@ -23077,7 +23447,7 @@ function sandboxEnv(base, o) {
23077
23447
  env.GIT_CONFIG_COUNT = "1";
23078
23448
  env.GIT_CONFIG_KEY_0 = "credential.helper";
23079
23449
  env.GIT_CONFIG_VALUE_0 = "";
23080
- env.npm_config_cache = join8(o.scratch, "npm-cache");
23450
+ env.npm_config_cache = join9(o.scratch, "npm-cache");
23081
23451
  env.FREEPI_HIVE_CALL = o.callId;
23082
23452
  return env;
23083
23453
  }
@@ -23123,7 +23493,7 @@ function scanProject(project, limit = SCAN_LIMIT2) {
23123
23493
  scan.overflow = true;
23124
23494
  break;
23125
23495
  }
23126
- const path = join8(dir, e.name);
23496
+ const path = join9(dir, e.name);
23127
23497
  if (e.isSymbolicLink())
23128
23498
  continue;
23129
23499
  if (e.isDirectory()) {
@@ -23206,7 +23576,7 @@ var MAC_MACH_SERVICES = [
23206
23576
  ["com.apple.system.opendirectoryd.libinfo", "user and group names: id, ls -l, python pwd, git author"]
23207
23577
  ];
23208
23578
  function developerDir(o = {}) {
23209
- const exists = o.exists ?? existsSync3;
23579
+ const exists = o.exists ?? existsSync4;
23210
23580
  const realpath = o.realpath ?? realpathSync2;
23211
23581
  const run = o.exec ?? defaultExec;
23212
23582
  const candidates2 = [];
@@ -23223,14 +23593,14 @@ function developerDir(o = {}) {
23223
23593
  continue;
23224
23594
  try {
23225
23595
  const real = realpath(c);
23226
- if (exists(join8(real, "usr/bin/git")))
23596
+ if (exists(join9(real, "usr/bin/git")))
23227
23597
  return real;
23228
23598
  } catch {}
23229
23599
  }
23230
23600
  return null;
23231
23601
  }
23232
23602
  function opensslFiles(o = {}) {
23233
- const exists = o.exists ?? existsSync3;
23603
+ const exists = o.exists ?? existsSync4;
23234
23604
  const list = o.list ?? ((d) => {
23235
23605
  try {
23236
23606
  return readdirSync2(d);
@@ -23241,18 +23611,18 @@ function opensslFiles(o = {}) {
23241
23611
  const files = [];
23242
23612
  const dirs = [];
23243
23613
  for (const prefix of o.prefixes ?? ["/opt/homebrew", "/usr/local"]) {
23244
- const etc = join8(prefix, "etc");
23614
+ const etc = join9(prefix, "etc");
23245
23615
  for (const name of list(etc)) {
23246
23616
  if (!/^openssl(@[0-9.]+)?$/.test(name))
23247
23617
  continue;
23248
23618
  for (const f of ["openssl.cnf", "ct_log_list.cnf", "cert.pem"])
23249
- if (exists(join8(etc, name, f)))
23250
- files.push(join8(etc, name, f));
23251
- if (exists(join8(etc, name, "certs")))
23252
- dirs.push(join8(etc, name, "certs"));
23619
+ if (exists(join9(etc, name, f)))
23620
+ files.push(join9(etc, name, f));
23621
+ if (exists(join9(etc, name, "certs")))
23622
+ dirs.push(join9(etc, name, "certs"));
23253
23623
  }
23254
- if (exists(join8(etc, "ca-certificates/cert.pem")))
23255
- files.push(join8(etc, "ca-certificates/cert.pem"));
23624
+ if (exists(join9(etc, "ca-certificates/cert.pem")))
23625
+ files.push(join9(etc, "ca-certificates/cert.pem"));
23256
23626
  }
23257
23627
  return { files, dirs };
23258
23628
  }
@@ -23260,7 +23630,7 @@ var MAC_DEV_READ = ["/dev/null", "/dev/zero", "/dev/random", "/dev/urandom", "/d
23260
23630
  var MAC_DEV_WRITE = ["/dev/null", "/dev/tty", "/dev/zero", "/dev/dtracehelper"];
23261
23631
  function ancestors(p) {
23262
23632
  const out = [];
23263
- for (let d = dirname6(p);; d = dirname6(d)) {
23633
+ for (let d = dirname7(p);; d = dirname7(d)) {
23264
23634
  out.push(d);
23265
23635
  if (d === "/")
23266
23636
  break;
@@ -23296,9 +23666,9 @@ function buildMacProfile(i) {
23296
23666
  L.push(`(deny file-read* file-write* (literal ${sbString(s)}))`);
23297
23667
  for (const f of i.links)
23298
23668
  L.push(`(deny file-write* (literal ${sbString(f)}))`);
23299
- const guarded = new Set([i.project, join8(i.project, ".git"), ...i.nodeModules ?? []]);
23300
- for (const f of [...i.sockets, ...i.links, ...i.nodeModules ?? [], join8(i.project, ".git")]) {
23301
- for (let d = dirname6(f);d.length >= i.project.length && (d === i.project || d.startsWith(i.project + "/")); d = dirname6(d))
23669
+ const guarded = new Set([i.project, join9(i.project, ".git"), ...i.nodeModules ?? []]);
23670
+ for (const f of [...i.sockets, ...i.links, ...i.nodeModules ?? [], join9(i.project, ".git")]) {
23671
+ for (let d = dirname7(f);d.length >= i.project.length && (d === i.project || d.startsWith(i.project + "/")); d = dirname7(d))
23302
23672
  guarded.add(d);
23303
23673
  }
23304
23674
  for (const d of [...guarded].sort())
@@ -23351,7 +23721,7 @@ var BWRAP_ETC = [
23351
23721
  "resolv.conf"
23352
23722
  ].map((e) => `/etc/${e}`);
23353
23723
  function buildBwrapArgs(i) {
23354
- const exists = i.exists ?? existsSync3;
23724
+ const exists = i.exists ?? existsSync4;
23355
23725
  const a = ["--unshare-all", "--die-with-parent", "--new-session"];
23356
23726
  const ro = (p) => a.push("--ro-bind", p, p);
23357
23727
  for (const d of BWRAP_SYSTEM_DIRS)
@@ -23367,10 +23737,10 @@ function buildBwrapArgs(i) {
23367
23737
  a.push("--bind", i.scratch, i.scratch);
23368
23738
  for (const d of i.nodeModules)
23369
23739
  ro(d);
23370
- if (exists(join8(i.project, ".git/hooks")))
23371
- ro(join8(i.project, ".git/hooks"));
23372
- if (exists(join8(i.project, ".git/config")))
23373
- ro(join8(i.project, ".git/config"));
23740
+ if (exists(join9(i.project, ".git/hooks")))
23741
+ ro(join9(i.project, ".git/hooks"));
23742
+ if (exists(join9(i.project, ".git/config")))
23743
+ ro(join9(i.project, ".git/config"));
23374
23744
  for (const f of i.links)
23375
23745
  ro(f);
23376
23746
  for (const s of i.sockets)
@@ -23400,7 +23770,7 @@ function removeTree(path) {
23400
23770
  return;
23401
23771
  chmodSync(p, 448);
23402
23772
  for (const e of readdirSync2(p))
23403
- walk(join8(p, e));
23773
+ walk(join9(p, e));
23404
23774
  } catch {}
23405
23775
  };
23406
23776
  walk(path);
@@ -23438,16 +23808,16 @@ function createShellSandbox(deps = {}) {
23438
23808
  if (deps.python !== undefined)
23439
23809
  return deps.python;
23440
23810
  const dev = platform === "darwin" ? macHost().developerDir : null;
23441
- return dev && existsSync3(join8(dev, "usr/bin/python3")) ? join8(dev, "usr/bin/python3") : null;
23811
+ return dev && existsSync4(join9(dev, "usr/bin/python3")) ? join9(dev, "usr/bin/python3") : null;
23442
23812
  };
23443
- let cached2;
23813
+ let cached3;
23444
23814
  function probe() {
23445
23815
  if (platform === "win32")
23446
23816
  return { supported: false, reason: "Windows has no sandbox" };
23447
23817
  if (platform !== "darwin" && platform !== "linux")
23448
23818
  return { supported: false, reason: `No shell sandbox for ${platform}` };
23449
23819
  const mac3 = platform === "darwin";
23450
- if (!existsSync3(launcher.sandbox))
23820
+ if (!existsSync4(launcher.sandbox))
23451
23821
  return { supported: false, reason: mac3 ? "sandbox-exec is unavailable" : "bubblewrap is not installed" };
23452
23822
  if (checkLauncher(launcher.sandbox, undefined, stat) !== null)
23453
23823
  return { supported: false, reason: NOT_TRUSTWORTHY };
@@ -23457,7 +23827,7 @@ function createShellSandbox(deps = {}) {
23457
23827
  }
23458
23828
  const args = ["--unshare-all"];
23459
23829
  for (const d of ["/usr", "/bin", "/lib", "/lib64"])
23460
- if (existsSync3(d))
23830
+ if (existsSync4(d))
23461
23831
  args.push("--ro-bind", d, d);
23462
23832
  args.push("--proc", "/proc", "--dev", "/dev", "/bin/true");
23463
23833
  const r = exec(launcher.sandbox, args, { timeout: 5000 });
@@ -23532,12 +23902,12 @@ function createShellSandbox(deps = {}) {
23532
23902
  return {
23533
23903
  enabled: false,
23534
23904
  capability() {
23535
- return cached2 ??= probe();
23905
+ return cached3 ??= probe();
23536
23906
  },
23537
23907
  prepare(toolCallId, cwd) {
23538
23908
  dispose(toolCallId);
23539
23909
  decisions.delete(toolCallId);
23540
- const cap = cached2 ??= probe();
23910
+ const cap = cached3 ??= probe();
23541
23911
  if (!cap.supported)
23542
23912
  return { ready: false, reason: cap.reason };
23543
23913
  let project;
@@ -23556,12 +23926,12 @@ function createShellSandbox(deps = {}) {
23556
23926
  return { ready: false, reason: "a folder in the project cannot be read, so it cannot be checked for sockets and hard links" };
23557
23927
  let scratch;
23558
23928
  try {
23559
- scratch = mkdtempSync(join8(realpathSync2(tmpdir()), "freepi-hive-"));
23929
+ scratch = mkdtempSync(join9(realpathSync2(tmpdir()), "freepi-hive-"));
23560
23930
  const dir = scratch;
23561
- mkdirSync(join8(dir, "home"));
23562
- mkdirSync(join8(dir, "npm-cache"));
23563
- const emptyFile = join8(dir, "empty");
23564
- const marker = join8(dir, "call.fd");
23931
+ mkdirSync(join9(dir, "home"));
23932
+ mkdirSync(join9(dir, "npm-cache"));
23933
+ const emptyFile = join9(dir, "empty");
23934
+ const marker = join9(dir, "call.fd");
23565
23935
  writeFileSync(emptyFile, "", { mode: 292 });
23566
23936
  const roots = toolchainRoots(home);
23567
23937
  const mac3 = platform === "darwin";
@@ -23722,6 +24092,20 @@ function projectTrustForCwd(cwd, launch, agentDir, hasTrustRequiringResources =
23722
24092
  return new ProjectTrustStore(agentDir).get(cwd) === true;
23723
24093
  }
23724
24094
 
24095
+ // src/session-auth.ts
24096
+ function createSessionAuth(initial) {
24097
+ let jwt2 = initial.jwt;
24098
+ let guest = initial.guest;
24099
+ return {
24100
+ getJwt: () => jwt2,
24101
+ isGuest: () => guest,
24102
+ becomeSignedIn(token) {
24103
+ jwt2 = token;
24104
+ guest = false;
24105
+ }
24106
+ };
24107
+ }
24108
+
23725
24109
  // src/pi-launch.ts
23726
24110
  function catalogModelsFor(opts) {
23727
24111
  const main = splitCatalogModels(opts.models ?? []).main;
@@ -23753,7 +24137,7 @@ var SDK_TOOLS = [
23753
24137
  "list_mcp_resource_templates",
23754
24138
  "read_mcp_resource"
23755
24139
  ];
23756
- var SKILLS_DIR = resolve3(dirname7(fileURLToPath4(import.meta.url)), "..", "skills");
24140
+ var SKILLS_DIR = resolve3(dirname8(fileURLToPath4(import.meta.url)), "..", "skills");
23757
24141
  function resolveModelName(opts) {
23758
24142
  return opts.models?.[0]?.name || opts.model || MODEL_ID;
23759
24143
  }
@@ -23764,6 +24148,7 @@ function buildSettingsManager(opts, models, projectTrusted) {
23764
24148
  defaultTools: [...SAFE_TOOLS, TODO_TOOL_NAME],
23765
24149
  packages: [],
23766
24150
  quietStartup: true,
24151
+ defaultThinkingLevel: "off",
23767
24152
  hideThinkingBlock: true,
23768
24153
  tuiMode: "regular",
23769
24154
  theme: "light/dark",
@@ -23776,42 +24161,50 @@ function settingsForRuntime(opts, rtCwd, launch) {
23776
24161
  function buildRuntimeOptions(opts, sessionId) {
23777
24162
  process.env.PI_CODING_AGENT_DIR = opts.agentDir;
23778
24163
  const models = catalogModelsFor(opts);
24164
+ const auth = opts.sessionAuth ?? createSessionAuth({ jwt: opts.jwt, guest: Boolean(opts.guest) });
24165
+ opts.sessionAuth = auth;
23779
24166
  const providerExtension = {
23780
24167
  name: "free-pi-provider",
23781
24168
  factory: (pi) => {
23782
- pi.registerProvider(PROVIDER_NAME, buildProviderConfig(opts.baseUrl, opts.jwt, sessionId, models));
24169
+ pi.registerProvider(PROVIDER_NAME, buildProviderConfig(opts.baseUrl, auth.getJwt(), sessionId, models));
23783
24170
  const defense = defenseModelsFor(opts);
23784
24171
  if (defense.length > 0) {
23785
- pi.registerProvider(DEFENSE_PROVIDER_NAME, buildProviderConfig(opts.baseUrl, opts.jwt, sessionId, defense, DEFENSE_PROVIDER_NAME));
24172
+ pi.registerProvider(DEFENSE_PROVIDER_NAME, buildProviderConfig(opts.baseUrl, auth.getJwt(), sessionId, defense, DEFENSE_PROVIDER_NAME));
23786
24173
  }
24174
+ pi.on("before_provider_headers", (event) => {
24175
+ event.headers.Authorization = `Bearer ${auth.getJwt()}`;
24176
+ });
23787
24177
  }
23788
24178
  };
23789
24179
  let advertiserUrl;
23790
24180
  const adsExtension = createAdsExtension({
23791
24181
  baseUrl: opts.baseUrl,
23792
- getToken: () => opts.jwt,
24182
+ getToken: () => auth.getJwt(),
23793
24183
  sessionId,
23794
24184
  getGravityContext: opts.getGravityContext,
23795
24185
  onBannerAd: (u) => {
23796
24186
  advertiserUrl = u;
24187
+ },
24188
+ onMe: (me) => {
24189
+ updateAllowanceCache(me);
23797
24190
  }
23798
24191
  });
23799
24192
  const usageToolExtension = createUsageToolExtension({
23800
24193
  baseUrl: opts.baseUrl,
23801
- getToken: () => opts.jwt
24194
+ getToken: () => auth.getJwt()
23802
24195
  });
23803
24196
  const buyToolExtension = createBuyToolExtension({
23804
24197
  baseUrl: opts.baseUrl,
23805
- getToken: () => opts.jwt
24198
+ getToken: () => auth.getJwt()
23806
24199
  });
23807
24200
  const docsToolExtension = createDocsToolExtension();
23808
24201
  const webToolsExtension = createWebToolsExtension({
23809
24202
  baseUrl: opts.baseUrl,
23810
- getToken: () => opts.jwt
24203
+ getToken: () => auth.getJwt()
23811
24204
  });
23812
24205
  const imageToolExtension = createImageToolExtension({
23813
24206
  baseUrl: opts.baseUrl,
23814
- getToken: () => opts.jwt
24207
+ getToken: () => auth.getJwt()
23815
24208
  });
23816
24209
  const errorNoticeExtension = createErrorNoticeExtension({
23817
24210
  extraNotice: (env) => {
@@ -23822,34 +24215,39 @@ function buildRuntimeOptions(opts, sessionId) {
23822
24215
  if (!card)
23823
24216
  return;
23824
24217
  session.shown = true;
23825
- reportRefillCardShown({ baseUrl: opts.baseUrl, jwt: opts.jwt });
24218
+ reportRefillCardShown({ baseUrl: opts.baseUrl, jwt: auth.getJwt() });
23826
24219
  return card;
23827
24220
  }
23828
24221
  });
23829
24222
  const toolGuardExtension = createToolGuardExtension({
23830
24223
  baseUrl: opts.baseUrl,
23831
- getToken: () => opts.jwt,
24224
+ getToken: () => auth.getJwt(),
23832
24225
  allowedTools: ALLOWED_TOOL_NAMES
23833
24226
  });
23834
24227
  const commandsExtension = createFreePiCommandsExtension({
23835
24228
  baseUrl: opts.baseUrl,
23836
- getToken: () => opts.jwt,
24229
+ getToken: () => auth.getJwt(),
23837
24230
  getAdvertiserUrl: () => advertiserUrl,
23838
24231
  credentialsPath: opts.credentialsPath,
23839
24232
  agentDir: opts.agentDir,
23840
- isGuest: () => Boolean(opts.guest)
24233
+ isGuest: () => auth.isGuest(),
24234
+ onLogin: (token) => auth.becomeSignedIn(token),
24235
+ variant: () => opts.guestHandoffVariant
23841
24236
  });
23842
24237
  const guestLoginExtension = opts.guest ? createGuestLoginExtension({
23843
24238
  baseUrl: opts.baseUrl,
23844
- getToken: () => opts.jwt,
24239
+ getToken: () => auth.getJwt(),
23845
24240
  credentialsPath: opts.credentialsPath,
23846
24241
  agentDir: opts.agentDir,
23847
- isGuest: () => Boolean(opts.guest)
24242
+ isGuest: () => auth.isGuest(),
24243
+ onLogin: (token) => auth.becomeSignedIn(token),
24244
+ variant: () => opts.guestHandoffVariant
23848
24245
  }) : undefined;
23849
24246
  const headerExtension = createHeaderExtension({
23850
24247
  modelName: resolveModelName(opts),
23851
24248
  updateLatest: opts.updateLatest,
23852
- guest: opts.guest
24249
+ guest: opts.guest,
24250
+ allowanceLine: opts.allowanceLine
23853
24251
  });
23854
24252
  const todoToolExtension = createTodoToolExtension();
23855
24253
  const shellSandbox = opts.shellSandbox ?? createShellSandbox();
@@ -23859,7 +24257,7 @@ function buildRuntimeOptions(opts, sessionId) {
23859
24257
  const toolSearchExtension = { name: "pi-tool-search", factory: createToolSearchExtension() };
23860
24258
  const hiveExtension = createHiveExtension({
23861
24259
  baseUrl: opts.baseUrl,
23862
- getToken: () => opts.jwt,
24260
+ getToken: () => auth.getJwt(),
23863
24261
  knownTools: ALLOWED_TOOL_NAMES,
23864
24262
  sandbox: shellSandbox
23865
24263
  });
@@ -23882,7 +24280,8 @@ function buildRuntimeOptions(opts, sessionId) {
23882
24280
  codemodeExtension,
23883
24281
  toolSearchExtension,
23884
24282
  hiveExtension,
23885
- ...guestLoginExtension ? [guestLoginExtension] : []
24283
+ ...guestLoginExtension ? [guestLoginExtension] : [],
24284
+ ...opts.guest ? [createGuestNotesExtension({ isGuest: () => auth.isGuest() })] : []
23886
24285
  ];
23887
24286
  const settingsManager = buildSettingsManager(opts, models, opts.projectTrusted === true);
23888
24287
  return {
@@ -23899,7 +24298,20 @@ function buildRuntimeOptions(opts, sessionId) {
23899
24298
  function getSessionDir(cwd, agentDir) {
23900
24299
  const resolvedCwd = resolve3(cwd);
23901
24300
  const safePath = `--${resolvedCwd.replace(/^[/\\]/, "").replace(/[/\\:]/g, "-")}--`;
23902
- return join9(resolve3(agentDir), "sessions", safePath);
24301
+ return join10(resolve3(agentDir), "sessions", safePath);
24302
+ }
24303
+ async function findLatestSession(cwd, sessionDir, api3 = SessionManager) {
24304
+ const infos = await api3.list(cwd, sessionDir);
24305
+ if (infos.length === 0)
24306
+ return;
24307
+ const latest = [...infos].sort((a, b) => +new Date(b.modified) - +new Date(a.modified))[0];
24308
+ if (!latest)
24309
+ return;
24310
+ return {
24311
+ id: latest.id,
24312
+ modified: latest.modified instanceof Date ? latest.modified : new Date(latest.modified),
24313
+ firstMessage: latest.firstMessage ?? ""
24314
+ };
23903
24315
  }
23904
24316
  async function createSessionManager(cwd, sessionDir, sessionId, api3 = SessionManager) {
23905
24317
  if (sessionId) {
@@ -23925,9 +24337,11 @@ async function launchPi(opts) {
23925
24337
  const cwd = process.cwd();
23926
24338
  const sessionDir = getSessionDir(cwd, opts.agentDir);
23927
24339
  const sm = await createSessionManager(cwd, sessionDir, opts.session);
24340
+ const auth = opts.sessionAuth ?? createSessionAuth({ jwt: opts.jwt, guest: Boolean(opts.guest) });
24341
+ const launchOpts = { ...opts, sessionAuth: auth };
23928
24342
  const sessionId = sm.getSessionId();
23929
24343
  const projectTrusted = await decideProjectTrustInteractive(cwd, opts.agentDir);
23930
- const { resourceLoaderOptions, tools } = buildRuntimeOptions({ ...opts, projectTrusted }, sessionId);
24344
+ const { resourceLoaderOptions, tools } = buildRuntimeOptions({ ...launchOpts, projectTrusted }, sessionId);
23931
24345
  const launchTrust = { cwd, trusted: projectTrusted };
23932
24346
  const createRuntime = async ({
23933
24347
  cwd: rtCwd,
@@ -23962,13 +24376,17 @@ async function launchPi(opts) {
23962
24376
  initialImages: [],
23963
24377
  initialMessages: []
23964
24378
  });
23965
- installBugHandler(mode, { baseUrl: opts.baseUrl, getToken: () => opts.jwt });
24379
+ installBugHandler(mode, { baseUrl: opts.baseUrl, getToken: () => auth.getJwt() });
23966
24380
  installLoginHandler(mode, {
23967
24381
  baseUrl: opts.baseUrl,
23968
- getToken: () => opts.jwt,
24382
+ getToken: () => auth.getJwt(),
23969
24383
  credentialsPath: opts.credentialsPath,
23970
- agentDir: opts.agentDir
24384
+ agentDir: opts.agentDir,
24385
+ isGuest: () => auth.isGuest(),
24386
+ onLogin: (token) => auth.becomeSignedIn(token),
24387
+ variant: () => opts.guestHandoffVariant
23971
24388
  });
24389
+ installModelAllowanceHandler(mode);
23972
24390
  installFooterRightAlign(mode);
23973
24391
  await mode.run();
23974
24392
  return sm.getSessionId();
@@ -24018,6 +24436,154 @@ function createRealSpawn(io = { platform: process.platform, spawnSyncImpl: spawn
24018
24436
  // src/run.ts
24019
24437
  import { SessionManager as SessionManager2 } from "@earendil-works/pi-coding-agent";
24020
24438
 
24439
+ // src/resume-on-launch.ts
24440
+ var RESUME_ON_LAUNCH_EXPERIMENT = "resume_on_launch";
24441
+ var DEFAULT_RESUME_RECENT_HOURS = 48;
24442
+ var RESUME_PROMPT_SUFFIX = " [Y/n] ";
24443
+ function userArgv(argv) {
24444
+ if (/(^|[\\/])(node|bun)(\.exe)?$/i.test(argv[0] ?? ""))
24445
+ return argv.slice(2);
24446
+ return argv;
24447
+ }
24448
+ function parseResumeFlags(argv) {
24449
+ const args = userArgv(argv);
24450
+ let session;
24451
+ let sessionError;
24452
+ let continueLast = false;
24453
+ let startNew = false;
24454
+ let hasPrompt = false;
24455
+ for (let i = 0;i < args.length; i++) {
24456
+ const arg = args[i];
24457
+ if (arg === "--session") {
24458
+ const value = args[i + 1];
24459
+ if (!value || value.startsWith("--")) {
24460
+ sessionError = "Missing value for --session";
24461
+ } else {
24462
+ session = value;
24463
+ i += 1;
24464
+ }
24465
+ continue;
24466
+ }
24467
+ if (arg.startsWith("--session=")) {
24468
+ const value = arg.slice("--session=".length);
24469
+ if (!value)
24470
+ sessionError = "Missing value for --session";
24471
+ else
24472
+ session = value;
24473
+ continue;
24474
+ }
24475
+ if (arg === "--continue" || arg === "--resume") {
24476
+ continueLast = true;
24477
+ continue;
24478
+ }
24479
+ if (arg === "--new") {
24480
+ startNew = true;
24481
+ continue;
24482
+ }
24483
+ if (arg === "--ref" || arg === "--src") {
24484
+ if (args[i + 1] && !String(args[i + 1]).startsWith("--"))
24485
+ i += 1;
24486
+ continue;
24487
+ }
24488
+ if (arg.startsWith("--ref=") || arg.startsWith("--src="))
24489
+ continue;
24490
+ if (!arg.startsWith("-"))
24491
+ hasPrompt = true;
24492
+ }
24493
+ return { session, sessionError, continueLast, startNew, hasPrompt };
24494
+ }
24495
+ function isRecentSession(modified, recentHours, now) {
24496
+ const ageMs = now.getTime() - modified.getTime();
24497
+ return ageMs >= 0 && ageMs <= recentHours * 3600000;
24498
+ }
24499
+ function recentHoursFromEnv(env = process.env, serverHours) {
24500
+ const raw = env.FREEPI_RESUME_RECENT_HOURS;
24501
+ if (raw !== undefined && raw !== "") {
24502
+ const n = Number(raw);
24503
+ if (Number.isInteger(n) && n >= 1)
24504
+ return n;
24505
+ }
24506
+ if (typeof serverHours === "number" && Number.isInteger(serverHours) && serverHours >= 1) {
24507
+ return serverHours;
24508
+ }
24509
+ return DEFAULT_RESUME_RECENT_HOURS;
24510
+ }
24511
+ function formatResumeAge(modified, now) {
24512
+ const deltaSec = Math.max(0, Math.floor((now.getTime() - modified.getTime()) / 1000));
24513
+ if (deltaSec < 60)
24514
+ return "just now";
24515
+ const minutes = Math.floor(deltaSec / 60);
24516
+ if (minutes < 60)
24517
+ return minutes === 1 ? "1 minute ago" : `${minutes} minutes ago`;
24518
+ const hours = Math.floor(minutes / 60);
24519
+ if (hours < 24)
24520
+ return hours === 1 ? "1 hour ago" : `${hours} hours ago`;
24521
+ const days = Math.floor(hours / 24);
24522
+ if (days === 1)
24523
+ return "yesterday";
24524
+ return `${days} days ago`;
24525
+ }
24526
+ function previewText(firstMessage) {
24527
+ if (!firstMessage)
24528
+ return;
24529
+ const oneLine = firstMessage.replace(/\s+/g, " ").trim();
24530
+ if (!oneLine)
24531
+ return;
24532
+ return oneLine.length > 60 ? `${oneLine.slice(0, 57)}...` : oneLine;
24533
+ }
24534
+ function formatResumePrompt(session, now = new Date) {
24535
+ const age = formatResumeAge(session.modified, now);
24536
+ const preview = previewText(session.firstMessage);
24537
+ const body = preview ? `Continue last session from ${age}: ${preview}?` : `Continue last session from ${age}?`;
24538
+ return `${body}${RESUME_PROMPT_SUFFIX}`;
24539
+ }
24540
+ function parseResumeKeypress(raw) {
24541
+ if (raw === "\x03")
24542
+ return null;
24543
+ const ch = raw.slice(0, 1);
24544
+ if (ch === "n" || ch === "N")
24545
+ return false;
24546
+ return true;
24547
+ }
24548
+ async function promptResume(session, deps) {
24549
+ if (!deps.stdinIsTTY)
24550
+ return false;
24551
+ const prompt = formatResumePrompt(session, deps.now ?? new Date);
24552
+ const raw = await deps.askKey(prompt);
24553
+ const parsed = parseResumeKeypress(raw);
24554
+ if (parsed === null)
24555
+ return "aborted";
24556
+ deps.write(`
24557
+ `);
24558
+ return parsed;
24559
+ }
24560
+ async function readOneKeypress(prompt, stdin = process.stdin, stdout = process.stdout) {
24561
+ stdout.write(prompt);
24562
+ const wasRaw = stdin.isRaw;
24563
+ if (typeof stdin.setRawMode === "function")
24564
+ stdin.setRawMode(true);
24565
+ stdin.resume();
24566
+ return await new Promise((resolve4) => {
24567
+ const onData = (chunk) => {
24568
+ stdin.off("data", onData);
24569
+ if (typeof stdin.setRawMode === "function")
24570
+ stdin.setRawMode(wasRaw ?? false);
24571
+ stdin.pause();
24572
+ resolve4(typeof chunk === "string" ? chunk : chunk.toString("utf8"));
24573
+ };
24574
+ stdin.once("data", onData);
24575
+ });
24576
+ }
24577
+ async function promptResumeInteractive(session) {
24578
+ return promptResume(session, {
24579
+ stdinIsTTY: process.stdin.isTTY === true,
24580
+ write: (text) => {
24581
+ process.stdout.write(text);
24582
+ },
24583
+ askKey: (prompt) => readOneKeypress(prompt)
24584
+ });
24585
+ }
24586
+
24021
24587
  // src/onboarding.ts
24022
24588
  var INTRO_ENV_SKIP = "FREEPI_NO_INTRO";
24023
24589
  var INTRO_TEXT = `
@@ -24026,8 +24592,8 @@ Welcome to free-pi — a free coding agent in your terminal.
24026
24592
  • It's the pi coding agent, free to use. Ads in the terminal pay for the
24027
24593
  inference — ad content never reaches the model.
24028
24594
  • Tell it what to build, fix, or explain, like any coding assistant.
24029
- • One session per account at a time. If you close the terminal, relaunch
24030
- with "npx free-pi-cli" — or resume where you left off with
24595
+ • You can run free-pi in more than one terminal. If you close the terminal,
24596
+ relaunch with "npx free-pi-cli" — or resume where you left off with
24031
24597
  "npx free-pi-cli --session <id>" (the id prints when you exit).
24032
24598
 
24033
24599
  Set ${INTRO_ENV_SKIP}=1 to skip this next time.
@@ -24045,12 +24611,12 @@ ${INTRO_TEXT}
24045
24611
  }
24046
24612
 
24047
24613
  // src/whats-new.ts
24048
- import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "node:fs";
24049
- import { dirname as dirname8 } from "node:path";
24614
+ import { existsSync as existsSync5, mkdirSync as mkdirSync2, readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "node:fs";
24615
+ import { dirname as dirname9 } from "node:path";
24050
24616
  function maybeWhatsNew(cliVersion, agentDir) {
24051
24617
  try {
24052
24618
  const path = getLastSeenVersionPath(agentDir);
24053
- const lastSeen = existsSync4(path) ? readFileSync4(path, "utf-8").trim() : undefined;
24619
+ const lastSeen = existsSync5(path) ? readFileSync4(path, "utf-8").trim() : undefined;
24054
24620
  writeLastSeen(path, cliVersion);
24055
24621
  if (lastSeen === undefined)
24056
24622
  return;
@@ -24066,7 +24632,7 @@ function maybeWhatsNew(cliVersion, agentDir) {
24066
24632
  }
24067
24633
  }
24068
24634
  function writeLastSeen(path, version3) {
24069
- mkdirSync2(dirname8(path), { recursive: true });
24635
+ mkdirSync2(dirname9(path), { recursive: true });
24070
24636
  writeFileSync2(path, version3, "utf-8");
24071
24637
  }
24072
24638
 
@@ -24092,7 +24658,8 @@ async function loginFlow(deps, argv = []) {
24092
24658
  openBrowser: deps.openBrowser,
24093
24659
  sleep: deps.sleep,
24094
24660
  log: deps.log,
24095
- jwt: await loadJwt(deps.credentialsPath)
24661
+ jwt: await loadJwt(deps.credentialsPath),
24662
+ credentialsPath: deps.credentialsPath
24096
24663
  }, resolved.attribution ?? {});
24097
24664
  await saveJwt(deps.credentialsPath, token);
24098
24665
  return token;
@@ -24103,28 +24670,52 @@ async function loginFlow(deps, argv = []) {
24103
24670
  throw err;
24104
24671
  }
24105
24672
  }
24106
- function parseSessionArg(argv) {
24107
- const flagIndex = argv.indexOf("--session");
24108
- if (flagIndex === -1)
24109
- return {};
24110
- const value = argv[flagIndex + 1];
24111
- if (!value || value.startsWith("--")) {
24112
- return { error: "Missing value for --session" };
24113
- }
24673
+ function validateSessionId(value) {
24114
24674
  try {
24115
24675
  SessionManager2.inMemory(process.cwd(), { id: value });
24676
+ return;
24116
24677
  } catch (err) {
24117
- return { error: `Invalid --session value: ${err instanceof Error ? err.message : String(err)}` };
24678
+ return `Invalid --session value: ${err instanceof Error ? err.message : String(err)}`;
24118
24679
  }
24119
- return { session: value };
24680
+ }
24681
+ function reportResumeExposure(deps, jwt2, event) {
24682
+ if (deps.reportResumeExposure) {
24683
+ deps.reportResumeExposure(event);
24684
+ return;
24685
+ }
24686
+ fetch(new URL("/telemetry/experiment", deps.baseUrl), {
24687
+ method: "POST",
24688
+ headers: {
24689
+ authorization: `Bearer ${jwt2}`,
24690
+ "content-type": "application/json"
24691
+ },
24692
+ body: JSON.stringify({ experiment: RESUME_ON_LAUNCH_EXPERIMENT, event }),
24693
+ signal: AbortSignal.timeout(3000)
24694
+ }).catch(() => {});
24695
+ }
24696
+ async function defaultFindLatestSession(cwd, agentDir) {
24697
+ return findLatestSession(cwd, getSessionDir(cwd, agentDir));
24120
24698
  }
24121
24699
  async function run(deps, argv = process.argv) {
24122
- const sessionArg = parseSessionArg(argv);
24123
- if (sessionArg.error) {
24124
- deps.log(sessionArg.error);
24700
+ const resumeFlags = parseResumeFlags(argv);
24701
+ if (resumeFlags.sessionError) {
24702
+ deps.log(resumeFlags.sessionError);
24125
24703
  return 1;
24126
24704
  }
24705
+ if (resumeFlags.session) {
24706
+ const invalid = validateSessionId(resumeFlags.session);
24707
+ if (invalid) {
24708
+ deps.log(invalid);
24709
+ return 1;
24710
+ }
24711
+ }
24712
+ const pending = await loadPendingLogin(deps.credentialsPath);
24713
+ if (pending)
24714
+ deps.log(`
24715
+ ${FINISH_SIGNIN_LINE}
24716
+ `);
24127
24717
  let jwt2 = await loadJwt(deps.credentialsPath);
24718
+ let guestHandoffVariant;
24128
24719
  let consentCurrent = false;
24129
24720
  if (jwt2) {
24130
24721
  try {
@@ -24167,9 +24758,12 @@ async function run(deps, argv = process.argv) {
24167
24758
  const guest = await startGuestSession(deps.baseUrl, CONSENT_VERSION, attribution);
24168
24759
  await saveJwt(deps.credentialsPath, guest.token);
24169
24760
  jwt2 = guest.token;
24170
- deps.log(`
24761
+ deps.log(isHandoffTreatment(guest.guestHandoffVariant) ? `
24762
+ You're on a short guest trial — ads pay for it. ${LOGIN_VALUE_LINE}
24763
+ ` : `
24171
24764
  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.
24172
24765
  `);
24766
+ guestHandoffVariant = guest.guestHandoffVariant;
24173
24767
  } catch (err) {
24174
24768
  if (!(err instanceof GuestUnavailableError))
24175
24769
  throw err;
@@ -24180,7 +24774,7 @@ Guest trial is not available right now.
24180
24774
  }
24181
24775
  consentCurrent = true;
24182
24776
  }
24183
- const versionCheck = await deps.checkClientVersion(deps.baseUrl, CLI_VERSION);
24777
+ const versionCheck = await deps.checkClientVersion(deps.baseUrl, CLI_VERSION, jwt2);
24184
24778
  let updateLatest;
24185
24779
  if (versionCheck.action !== "ok") {
24186
24780
  const outcome = await deps.maybeSelfUpdate(versionCheck.action, versionCheck.latest);
@@ -24206,23 +24800,61 @@ Guest trial is not available right now.
24206
24800
  } catch {}
24207
24801
  }
24208
24802
  const me = await fetchMe2(deps.baseUrl, jwt2).catch(() => null);
24803
+ if (me)
24804
+ updateAllowanceCache(me);
24805
+ guestHandoffVariant = me?.experiments?.guest_handoff ?? guestHandoffVariant;
24209
24806
  const startRefill = await fetchRefillStats(deps.baseUrl, jwt2).catch(() => {
24210
24807
  return;
24211
24808
  });
24212
24809
  const refillSession = sessionFromStats(startRefill);
24810
+ const creditsBack = startRefill ? formatCreditsBackNudge(startRefill) : undefined;
24811
+ if (creditsBack) {
24812
+ deps.log(`
24813
+ ${creditsBack}`);
24814
+ reportRefillCardShown({ baseUrl: deps.baseUrl, jwt: jwt2 });
24815
+ }
24816
+ let session = resumeFlags.session;
24817
+ const lookupLatest = deps.findLatestSession ?? defaultFindLatestSession;
24818
+ if (!session && resumeFlags.continueLast && !resumeFlags.startNew) {
24819
+ session = (await lookupLatest(process.cwd(), deps.agentDir))?.id;
24820
+ if (!session)
24821
+ deps.log("No earlier session in this folder; starting a new one.");
24822
+ }
24823
+ const experiment = versionCheck.experiments?.[RESUME_ON_LAUNCH_EXPERIMENT];
24824
+ const variant = experiment?.variant === "treatment" ? "treatment" : "control";
24825
+ const recentHours = recentHoursFromEnv(process.env, experiment?.recent_hours);
24826
+ const interactive = deps.stdinIsTTY ?? process.stdin.isTTY === true;
24827
+ if (!session && !resumeFlags.startNew && !resumeFlags.hasPrompt && variant === "treatment" && interactive) {
24828
+ const latest = await lookupLatest(process.cwd(), deps.agentDir);
24829
+ const now = deps.now?.() ?? new Date;
24830
+ if (latest && isRecentSession(latest.modified, recentHours, now)) {
24831
+ reportResumeExposure(deps, jwt2, "offer_shown");
24832
+ const answer = await (deps.promptResume ?? promptResumeInteractive)(latest);
24833
+ if (answer === "aborted") {
24834
+ deps.log("Aborted.");
24835
+ return 1;
24836
+ }
24837
+ if (answer) {
24838
+ reportResumeExposure(deps, jwt2, "offer_accepted");
24839
+ session = latest.id;
24840
+ }
24841
+ }
24842
+ }
24213
24843
  const finalSessionId = await deps.launchPi({
24214
24844
  baseUrl: deps.baseUrl,
24215
24845
  jwt: jwt2,
24216
24846
  agentDir: deps.agentDir,
24217
24847
  credentialsPath: deps.credentialsPath,
24218
- session: sessionArg.session,
24848
+ session,
24219
24849
  model: versionCheck.model,
24220
24850
  models: versionCheck.models,
24221
24851
  fusion: versionCheck.fusion,
24222
24852
  updateLatest,
24223
24853
  getGravityContext,
24224
24854
  guest: Boolean(me?.guest),
24225
- refillCard: refillSession
24855
+ refillCard: refillSession,
24856
+ allowanceLine: allowanceLine(),
24857
+ guestHandoffVariant
24226
24858
  });
24227
24859
  const endRefill = await fetchRefillStats(deps.baseUrl, jwt2).catch(() => {
24228
24860
  return;
@@ -24277,17 +24909,18 @@ async function maybeSelfUpdate(action2, latest, deps) {
24277
24909
  }
24278
24910
 
24279
24911
  // src/update-check.ts
24280
- async function checkClientVersion(baseUrl, cliVersion, fetchImpl = fetch) {
24912
+ async function checkClientVersion(baseUrl, cliVersion, fetchImpl = fetch, jwt2) {
24281
24913
  try {
24282
24914
  const res = await fetchImpl(new URL("/client-version", baseUrl), {
24283
- signal: AbortSignal.timeout(3000)
24915
+ signal: AbortSignal.timeout(3000),
24916
+ headers: jwt2 ? { authorization: `Bearer ${jwt2}` } : undefined
24284
24917
  });
24285
24918
  if (!res.ok)
24286
24919
  return { action: "ok" };
24287
24920
  const parsed = ClientVersionResponseSchema.safeParse(await res.json());
24288
24921
  if (!parsed.success)
24289
24922
  return { action: "ok" };
24290
- const { min, latest, model, models, notice, fusion } = parsed.data;
24923
+ const { min, latest, model, models, notice, fusion, experiments } = parsed.data;
24291
24924
  const extra = {};
24292
24925
  if (model !== undefined)
24293
24926
  extra.model = model;
@@ -24297,6 +24930,8 @@ async function checkClientVersion(baseUrl, cliVersion, fetchImpl = fetch) {
24297
24930
  extra.notice = notice;
24298
24931
  if (fusion !== undefined)
24299
24932
  extra.fusion = fusion;
24933
+ if (experiments !== undefined)
24934
+ extra.experiments = experiments;
24300
24935
  if (compareVersions(cliVersion, min) === -1)
24301
24936
  return { action: "block", latest, ...extra };
24302
24937
  if (compareVersions(cliVersion, latest) === -1)
@@ -24364,8 +24999,8 @@ ${HELP_TEXT}`);
24364
24999
  sleep: (ms) => new Promise((resolve4) => setTimeout(resolve4, ms)),
24365
25000
  launchPi,
24366
25001
  log: (message) => console.log(message),
24367
- checkClientVersion,
24368
- startGravityPixel: (jwt2) => startGravityPixel(jwt2, join10(dirname9(agentDir), "gravity")),
25002
+ checkClientVersion: (baseUrl, cliVersion, jwt2) => checkClientVersion(baseUrl, cliVersion, fetch, jwt2),
25003
+ startGravityPixel: (jwt2) => startGravityPixel(jwt2, join11(dirname10(agentDir), "gravity")),
24369
25004
  maybeSelfUpdate: (action2, latest) => maybeSelfUpdate(action2, latest, {
24370
25005
  env: process.env,
24371
25006
  autoUpdateEnabled: () => readAutoUpdate(getConfigPath()),