free-pi-cli 0.5.2 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (24) hide show
  1. package/README.md +4 -2
  2. package/dist/index.js +1 -1
  3. package/dist/{main-43bb5vjt.js → main-98djq99p.js} +771 -158
  4. package/package.json +1 -1
  5. package/skills/{security-audit → freepi-security-audit}/SKILL.md +1 -1
  6. /package/skills/{security-audit → freepi-security-audit}/AI-AND-LLM.md +0 -0
  7. /package/skills/{security-audit → freepi-security-audit}/ATTACK-CLASSES.md +0 -0
  8. /package/skills/{security-audit → freepi-security-audit}/CLIENT-SIDE.md +0 -0
  9. /package/skills/{security-audit → freepi-security-audit}/CLOUD-AND-DEPLOYMENT.md +0 -0
  10. /package/skills/{security-audit → freepi-security-audit}/DATA-ISOLATION-AND-LIFECYCLE.md +0 -0
  11. /package/skills/{security-audit → freepi-security-audit}/DESKTOP-MOBILE-AND-LOCAL-IPC.md +0 -0
  12. /package/skills/{security-audit → freepi-security-audit}/HUNTING.md +0 -0
  13. /package/skills/{security-audit → freepi-security-audit}/LICENSE +0 -0
  14. /package/skills/{security-audit → freepi-security-audit}/MEMORY-SAFETY-AND-BINARY.md +0 -0
  15. /package/skills/{security-audit → freepi-security-audit}/PROTOCOLS-RPC-AND-MESSAGING.md +0 -0
  16. /package/skills/{security-audit → freepi-security-audit}/RECONNAISSANCE.md +0 -0
  17. /package/skills/{security-audit → freepi-security-audit}/RESOURCE-EXHAUSTION-AND-AVAILABILITY.md +0 -0
  18. /package/skills/{security-audit → freepi-security-audit}/SUPPLY-CHAIN-AND-RELEASE.md +0 -0
  19. /package/skills/{security-audit → freepi-security-audit}/UPSTREAM.md +0 -0
  20. /package/skills/{security-audit → freepi-security-audit}/VALIDATION-AND-REPORTING.md +0 -0
  21. /package/skills/{security-audit → freepi-security-audit}/WEB-PROTOCOL-AND-AUTH.md +0 -0
  22. /package/skills/{security-audit → freepi-security-audit}/report-schema.json +0 -0
  23. /package/skills/{security-audit → freepi-security-audit}/validate-coverage-ledger.cjs +0 -0
  24. /package/skills/{security-audit → freepi-security-audit}/validate-findings.cjs +0 -0
@@ -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`);
14886
+ }
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)}`);
14805
14892
  }
14806
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.2",
15032
+ version: "0.6.0",
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,
@@ -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;
@@ -20852,21 +20974,36 @@ function createToolGuardExtension(opts) {
20852
20974
  // src/bug-report.ts
20853
20975
  var BUG_USAGE = "Usage: /bug <what went wrong>";
20854
20976
  var BUG_DESCRIPTION = "Report a bug to free-pi";
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
+ }
20855
20990
  function buildBugRequest(description, ctx) {
20856
20991
  return { description, session_id: ctx.sessionId, cli_version: CLI_VERSION, model: ctx.model };
20857
20992
  }
20858
- function mapBugFailure(status) {
20993
+ function mapBugFailure(status, kind = "bug") {
20994
+ const fb = kind === "feedback";
20995
+ const what = fb ? "feedback" : "the bug report";
20859
20996
  if (status === 401)
20860
- 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.`;
20861
20998
  if (status === 429)
20862
- 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.`;
20863
21000
  if (status === 413)
20864
- return "Couldn't send the bug report: it is too large.";
21001
+ return `Couldn't send ${what}: it is too large.`;
20865
21002
  if (status === 400)
20866
- return "Couldn't send the bug report: the description is too long (4000 characters max).";
20867
- 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.`;
20868
21005
  }
20869
- async function sendBugReport(opts, body) {
21006
+ async function sendBugReport(opts, body, kind = "bug") {
20870
21007
  try {
20871
21008
  const res = await (opts.fetchImpl ?? fetch)(`${opts.baseUrl}/bug-reports`, {
20872
21009
  method: "POST",
@@ -20875,13 +21012,13 @@ async function sendBugReport(opts, body) {
20875
21012
  signal: AbortSignal.timeout(1e4)
20876
21013
  });
20877
21014
  if (!res.ok)
20878
- return { ok: false, message: mapBugFailure(res.status) };
21015
+ return { ok: false, message: mapBugFailure(res.status, kind) };
20879
21016
  const parsed = BugReportResponseSchema.safeParse(await res.json());
20880
21017
  if (!parsed.success)
20881
- return { ok: false, message: mapBugFailure(res.status) };
21018
+ return { ok: false, message: mapBugFailure(res.status, kind) };
20882
21019
  return { ok: true, id: parsed.data.id };
20883
21020
  } catch {
20884
- 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.` };
20885
21022
  }
20886
21023
  }
20887
21024
  function makeBugHandler(mode, opts) {
@@ -20909,7 +21046,7 @@ function registerBugAutocomplete(pi) {
20909
21046
  const r = await cur.getSuggestions(...a);
20910
21047
  return r && {
20911
21048
  ...r,
20912
- items: r.items.map((i) => i.value === "bug" && i.label === "bug" ? { ...i, description: BUG_DESCRIPTION } : i)
21049
+ items: r.items.map((i) => i.label === i.value && i.value === "bug" ? { ...i, description: BUG_DESCRIPTION } : i.label === i.value && i.value === "login" ? { ...i, description: LOGIN_DESCRIPTION } : i)
20913
21050
  };
20914
21051
  }
20915
21052
  }));
@@ -20976,9 +21113,26 @@ function mergeAttribution(a, b) {
20976
21113
 
20977
21114
  // src/changelog.ts
20978
21115
  var CHANGELOG_HIGHLIGHTS = [
21116
+ {
21117
+ version: "0.6.0",
21118
+ highlights: [
21119
+ "Send feedback with /feedback <what you think>.",
21120
+ "Turn thinking on with the thinking-level control (Shift+Tab). It starts off.",
21121
+ "Run free-pi in more than one terminal at once.",
21122
+ "Pick up your last session in this folder when you start free-pi (some users).",
21123
+ "The Defense models now show their real model names."
21124
+ ]
21125
+ },
21126
+ {
21127
+ version: "0.5.3",
21128
+ highlights: [
21129
+ "No more startup warning about /login.",
21130
+ "The bundled audit skill is now freepi-security-audit, so it no longer clashes with Cloudflare's security-audit skill."
21131
+ ]
21132
+ },
20979
21133
  {
20980
21134
  version: "0.5.2",
20981
- highlights: ["/model now lists Defense Flash right under Fusion."]
21135
+ highlights: ["/model now lists the Defense models right under Fusion."]
20982
21136
  },
20983
21137
  {
20984
21138
  version: "0.5.1",
@@ -21259,7 +21413,8 @@ async function startGuestSession(baseUrl, consentVersion, attribution = {}) {
21259
21413
  return {
21260
21414
  token: parsed.token,
21261
21415
  allowance_usd: parsed.allowance_usd,
21262
- remaining_usd: parsed.remaining_usd
21416
+ remaining_usd: parsed.remaining_usd,
21417
+ guestHandoffVariant: parsed.experiments?.guest_handoff
21263
21418
  };
21264
21419
  }
21265
21420
  async function reportFunnelEvent(baseUrl, event, opts = {}) {
@@ -21290,7 +21445,15 @@ async function pollForToken(baseUrl, sessionId, interval, sleep, opts = {}) {
21290
21445
  }
21291
21446
  let res;
21292
21447
  try {
21293
- res = await postJson(baseUrl, "/auth/token", { session_id: sessionId });
21448
+ const headers = { "content-type": "application/json" };
21449
+ if (opts.jwt)
21450
+ headers.authorization = `Bearer ${opts.jwt}`;
21451
+ const doFetch = opts.fetchImpl ?? fetch;
21452
+ res = await doFetch(new URL("/auth/token", baseUrl), {
21453
+ method: "POST",
21454
+ headers,
21455
+ body: JSON.stringify({ session_id: sessionId })
21456
+ });
21294
21457
  consecutiveNetworkFailures = 0;
21295
21458
  } catch (err) {
21296
21459
  consecutiveNetworkFailures++;
@@ -21359,10 +21522,22 @@ async function submitUserConsent(baseUrl, jwt2, consentVersion) {
21359
21522
  throw new Error(`consent submission failed: HTTP ${res.status}`);
21360
21523
  }
21361
21524
 
21525
+ // src/guest-handoff.ts
21526
+ var LOGIN_VALUE_LINE = "sign in free with GitHub: full daily allowance, FREE abliterated DeepSeek, history saved";
21527
+ var GUEST_NEAR_WALL_LINE = `A few guest requests left. ${LOGIN_VALUE_LINE}`;
21528
+ var SIGNED_IN_CONTINUE_LINE = "Signed in. Your conversation continues — full daily allowance is unlocked.";
21529
+ var HEADLESS_FALLBACK_LINE = "No browser or clipboard here — open the URL and enter the code above.";
21530
+ function isHandoffTreatment(variant) {
21531
+ return variant === "treatment";
21532
+ }
21533
+
21362
21534
  // src/clipboard.ts
21363
21535
  import { spawn as spawn2 } from "node:child_process";
21364
- function copyToClipboard(text, platform = process.platform, spawnImpl = spawn2) {
21536
+ import { existsSync as existsSync2 } from "node:fs";
21537
+ function copyToClipboard(text, platform = process.platform, spawnImpl = spawn2, env = process.env) {
21365
21538
  try {
21539
+ if (!clipboardAvailable(platform, env))
21540
+ return false;
21366
21541
  const cmd = clipboardCommand(platform);
21367
21542
  if (!cmd)
21368
21543
  return false;
@@ -21375,6 +21550,24 @@ function copyToClipboard(text, platform = process.platform, spawnImpl = spawn2)
21375
21550
  return false;
21376
21551
  }
21377
21552
  }
21553
+ function clipboardAvailable(platform = process.platform, env = process.env) {
21554
+ if (platform === "darwin" || platform === "win32")
21555
+ return commandOnPath(clipboardCommand(platform)?.command, env);
21556
+ if (platform !== "linux")
21557
+ return false;
21558
+ if (!env.DISPLAY && !env.WAYLAND_DISPLAY)
21559
+ return false;
21560
+ const cmd = clipboardCommand(platform);
21561
+ return commandOnPath(cmd?.command, env);
21562
+ }
21563
+ function commandOnPath(command, env) {
21564
+ if (!command)
21565
+ return false;
21566
+ if (command.includes("/"))
21567
+ return existsSync2(command);
21568
+ const path = env.PATH ?? "";
21569
+ return path.split(":").some((dir) => dir.length > 0 && existsSync2(`${dir}/${command}`));
21570
+ }
21378
21571
  function clipboardCommand(platform) {
21379
21572
  if (platform === "darwin")
21380
21573
  return { command: "pbcopy", args: [] };
@@ -21436,7 +21629,7 @@ function allowanceIsLow(remaining, allowance) {
21436
21629
  return false;
21437
21630
  return remaining * LOW_ALLOWANCE_DENOMINATOR <= allowance * LOW_ALLOWANCE_NUMERATOR;
21438
21631
  }
21439
- function formatRefillLocal(refillAt, timeZone) {
21632
+ function formatRefillLocal2(refillAt, timeZone) {
21440
21633
  const opts = {
21441
21634
  hour: "numeric",
21442
21635
  minute: "2-digit",
@@ -21456,7 +21649,7 @@ function refillAtFromIso(iso, now = new Date) {
21456
21649
  return nextUtcMidnight(now);
21457
21650
  }
21458
21651
  function formatRefillCard(input) {
21459
- const refill = formatRefillLocal(input.refillAt, input.timeZone);
21652
+ const refill = formatRefillLocal2(input.refillAt, input.timeZone);
21460
21653
  const lines = [
21461
21654
  `${input.allowance.toLocaleString("en-US")} free credits refill at ${refill}.`
21462
21655
  ];
@@ -21485,6 +21678,7 @@ async function fetchRefillStats(baseUrl, jwt2, fetchImpl = fetch) {
21485
21678
  allowanceCredits: body.allowance_credits,
21486
21679
  refillAt: typeof body.refill_at === "string" ? body.refill_at : undefined,
21487
21680
  showCard: body.return_bonus?.show_card === true,
21681
+ creditsBack: body.return_bonus?.credits_back === true,
21488
21682
  guest: body.guest === true
21489
21683
  };
21490
21684
  } catch {
@@ -21515,6 +21709,12 @@ function sessionFromStats(stats) {
21515
21709
  shown: false
21516
21710
  };
21517
21711
  }
21712
+ function formatCreditsBackNudge(stats, timeZone) {
21713
+ if (stats.guest || !stats.creditsBack)
21714
+ return;
21715
+ const refill = formatRefillLocal2(refillAtFromIso(stats.refillAt), timeZone);
21716
+ return `Your credits are back. ${stats.allowanceCredits.toLocaleString("en-US")} free credits refill at ${refill}.`;
21717
+ }
21518
21718
  function formatLowAllowanceCard(session, remaining, timeZone) {
21519
21719
  if (!session.show || session.shown || session.allowance === undefined)
21520
21720
  return;
@@ -21543,14 +21743,48 @@ function formatExitRefillCard(session, end, timeZone) {
21543
21743
  });
21544
21744
  }
21545
21745
 
21746
+ // src/pending-login.ts
21747
+ import { mkdir as mkdir4, readFile as readFile3, rm as rm4, writeFile as writeFile3 } from "node:fs/promises";
21748
+ import { dirname as dirname5, join as join6 } from "node:path";
21749
+ var FINISH_SIGNIN_LINE = "one step to finish signing in";
21750
+ function pendingLoginPath(credentialsPath) {
21751
+ return join6(dirname5(credentialsPath), "pending-login.json");
21752
+ }
21753
+ async function loadPendingLogin(credentialsPath) {
21754
+ try {
21755
+ const raw = await readFile3(pendingLoginPath(credentialsPath), "utf8");
21756
+ const parsed = JSON.parse(raw);
21757
+ if (typeof parsed.session_id !== "string" || typeof parsed.user_code !== "string" || typeof parsed.verification_uri !== "string" || typeof parsed.expires_at !== "number") {
21758
+ return null;
21759
+ }
21760
+ return {
21761
+ session_id: parsed.session_id,
21762
+ user_code: parsed.user_code,
21763
+ verification_uri: parsed.verification_uri,
21764
+ expires_at: parsed.expires_at
21765
+ };
21766
+ } catch {
21767
+ return null;
21768
+ }
21769
+ }
21770
+ async function savePendingLogin(credentialsPath, pending) {
21771
+ const path = pendingLoginPath(credentialsPath);
21772
+ await mkdir4(dirname5(path), { recursive: true });
21773
+ await writeFile3(path, JSON.stringify(pending), { mode: 384 });
21774
+ }
21775
+ async function clearPendingLogin(credentialsPath) {
21776
+ await rm4(pendingLoginPath(credentialsPath), { force: true });
21777
+ }
21778
+
21546
21779
  // src/login-ux.ts
21547
21780
  var LOGIN_REWARD_LINE = "Sign in with GitHub to get 15,000 free credits a day";
21548
21781
  var LOGIN_WAITING_LINE = "Waiting for GitHub…";
21549
21782
  var LOGIN_TIMEOUT_LINE = "GitHub sign-in timed out. Run /login to try again.";
21550
- function formatDeviceCodePrompt(device) {
21783
+ function formatDeviceCodePrompt(device, variant) {
21784
+ const value = isHandoffTreatment(variant) ? LOGIN_VALUE_LINE : LOGIN_REWARD_LINE;
21551
21785
  return [
21552
21786
  "",
21553
- LOGIN_REWARD_LINE,
21787
+ value,
21554
21788
  LOGIN_BONUS_UNLOCK_LINE,
21555
21789
  "",
21556
21790
  ` Open ${device.verification_uri}`,
@@ -21561,7 +21795,15 @@ function formatDeviceCodePrompt(device) {
21561
21795
  }
21562
21796
  async function runDeviceLogin(deps, attribution = {}) {
21563
21797
  const device = await startDeviceFlow(deps.baseUrl);
21564
- deps.log(formatDeviceCodePrompt(device));
21798
+ deps.log(formatDeviceCodePrompt(device, deps.variant));
21799
+ if (deps.credentialsPath) {
21800
+ await savePendingLogin(deps.credentialsPath, {
21801
+ session_id: device.session_id,
21802
+ user_code: device.user_code,
21803
+ verification_uri: device.verification_uri,
21804
+ expires_at: (deps.now ?? Date.now)() + (device.expires_in ?? 900) * 1000
21805
+ });
21806
+ }
21565
21807
  const copied = (deps.copy ?? copyToClipboard)(device.user_code);
21566
21808
  if (copied)
21567
21809
  deps.log(` (code copied to clipboard)
@@ -21579,12 +21821,66 @@ async function runDeviceLogin(deps, attribution = {}) {
21579
21821
  jwt: deps.jwt,
21580
21822
  fetchImpl: deps.fetchImpl
21581
21823
  });
21824
+ } else if (!copied) {
21825
+ deps.log(HEADLESS_FALLBACK_LINE);
21582
21826
  }
21583
21827
  await submitConsent(deps.baseUrl, device.session_id, CONSENT_VERSION, attribution);
21584
21828
  const expiresInMs = (device.expires_in ?? 900) * 1000;
21585
21829
  const deadline = (deps.now ?? Date.now)() + expiresInMs;
21586
- const token = await pollForToken(deps.baseUrl, device.session_id, device.interval, deps.sleep, { deadline, now: deps.now });
21587
- return token;
21830
+ try {
21831
+ const token = await pollForToken(deps.baseUrl, device.session_id, device.interval, deps.sleep, { deadline, now: deps.now, jwt: deps.jwt, fetchImpl: deps.fetchImpl });
21832
+ if (deps.credentialsPath)
21833
+ await clearPendingLogin(deps.credentialsPath);
21834
+ return token;
21835
+ } catch (err) {
21836
+ throw err;
21837
+ }
21838
+ }
21839
+
21840
+ // src/allowance-counter.ts
21841
+ var cached2;
21842
+ function updateAllowanceCache(input) {
21843
+ if (typeof input.remaining_credits_today !== "number" || typeof input.allowance_credits !== "number") {
21844
+ return cached2;
21845
+ }
21846
+ const refill = typeof input.refill_at === "string" && !Number.isNaN(new Date(input.refill_at).getTime()) ? new Date(input.refill_at) : undefined;
21847
+ cached2 = {
21848
+ remaining: input.remaining_credits_today,
21849
+ allowance: input.allowance_credits,
21850
+ refillAt: refill,
21851
+ bonusCredits: typeof input.bonus_credits === "number" ? input.bonus_credits : undefined,
21852
+ guest: input.guest === true,
21853
+ visible: input.visible_allowance === true
21854
+ };
21855
+ return cached2;
21856
+ }
21857
+ function allowanceLine(snapshot = cached2, timeZone) {
21858
+ if (!snapshot?.visible)
21859
+ return;
21860
+ return formatVisibleAllowanceLine({
21861
+ remaining: snapshot.remaining,
21862
+ refillAt: snapshot.refillAt,
21863
+ guest: snapshot.guest,
21864
+ timeZone
21865
+ });
21866
+ }
21867
+ function installModelAllowanceHandler(mode, getLine = () => allowanceLine()) {
21868
+ const m = mode;
21869
+ if (typeof m.handleModelCommand !== "function" || typeof m.showStatus !== "function")
21870
+ return false;
21871
+ const original = m.handleModelCommand.bind(mode);
21872
+ m.handleModelCommand = () => {
21873
+ const line = getLine();
21874
+ if (line)
21875
+ m.showStatus(line);
21876
+ return original();
21877
+ };
21878
+ return true;
21879
+ }
21880
+ function notifyAllowance(ctx) {
21881
+ const line = allowanceLine();
21882
+ if (line)
21883
+ ctx.ui.notify(line, "info");
21588
21884
  }
21589
21885
 
21590
21886
  // src/commands.ts
@@ -21607,6 +21903,18 @@ async function closeOtherSession(opts, ctx) {
21607
21903
  ctx.ui.notify("Couldn't reach free-pi to reset your session — check your connection.", "error");
21608
21904
  }
21609
21905
  }
21906
+ async function sendFeedback(opts, args, ctx) {
21907
+ const parsed = parseFeedbackArgs(args);
21908
+ if (parsed.kind === "usage")
21909
+ return ctx.ui.notify(FEEDBACK_USAGE, "info");
21910
+ if (parsed.kind === "too_long")
21911
+ return ctx.ui.notify(`Couldn't send feedback: it is too long (${FEEDBACK_MAX} characters max).`, "error");
21912
+ 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");
21913
+ if (r.ok)
21914
+ ctx.ui.notify(`Thanks for the feedback (#${r.id}).`, "info");
21915
+ else
21916
+ ctx.ui.notify(r.message, "error");
21917
+ }
21610
21918
  function showWhatsNew(ctx) {
21611
21919
  const lines = CHANGELOG_HIGHLIGHTS.slice(0, 3).flatMap((e) => [
21612
21920
  `free-pi ${e.version}:`,
@@ -21671,23 +21979,27 @@ async function openPrivacyPolicy(opts, ctx) {
21671
21979
  (opts.openBrowserImpl ?? openBrowser)(PRIVACY_URL);
21672
21980
  ctx.ui.notify(`Opening the Privacy Policy in your browser: ${PRIVACY_URL}`, "info");
21673
21981
  }
21982
+ function resolveHandoffVariant(opts) {
21983
+ return typeof opts.variant === "function" ? opts.variant() : opts.variant;
21984
+ }
21674
21985
  async function runLogin(opts, ctx) {
21675
- ctx.ui.notify("Sign in with GitHub to get 15,000 free credits a day.", "info");
21676
- ctx.ui.notify(LOGIN_BONUS_UNLOCK_LINE, "info");
21677
21986
  try {
21678
21987
  const attribution = opts.agentDir ? await loadAttribution(getAttributionPath(opts.agentDir)) : {};
21988
+ const variant = resolveHandoffVariant(opts);
21679
21989
  const token = await runDeviceLogin({
21680
21990
  baseUrl: opts.baseUrl,
21681
21991
  openBrowser: opts.openBrowserImpl ?? openBrowser,
21682
21992
  sleep: (ms) => new Promise((r) => setTimeout(r, ms)),
21683
21993
  log: (message) => ctx.ui.notify(message.trim(), "info"),
21684
21994
  jwt: await opts.getToken(),
21685
- fetchImpl: opts.fetchImpl
21995
+ fetchImpl: opts.fetchImpl,
21996
+ variant,
21997
+ credentialsPath: opts.credentialsPath
21686
21998
  }, attribution);
21687
21999
  if (opts.credentialsPath)
21688
22000
  await saveJwt(opts.credentialsPath, token);
21689
22001
  opts.onLogin?.(token);
21690
- ctx.ui.notify("Signed in. Restart free-pi-cli to unlock 15,000 free credits a day and a one-time comeback bonus.", "info");
22002
+ 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");
21691
22003
  } catch (err) {
21692
22004
  if (err instanceof LoginTimeoutError) {
21693
22005
  ctx.ui.notify(LOGIN_TIMEOUT_LINE, "warning");
@@ -21696,6 +22008,22 @@ async function runLogin(opts, ctx) {
21696
22008
  ctx.ui.notify("Couldn't finish GitHub sign-in. Run /login to try again.", "error");
21697
22009
  }
21698
22010
  }
22011
+ function installLoginHandler(mode, opts) {
22012
+ const m = mode;
22013
+ mode.handleLoginCommand = () => {
22014
+ const lines = [];
22015
+ const notify = (message, type) => {
22016
+ if (type === "error")
22017
+ return m.showError(message);
22018
+ if (type === "warning")
22019
+ return m.showWarning(message);
22020
+ lines.push(message);
22021
+ m.showStatus(lines.join(`
22022
+ `));
22023
+ };
22024
+ return runLogin(opts, { ui: { notify } });
22025
+ };
22026
+ }
21699
22027
  async function showUsage(opts, ctx) {
21700
22028
  const r = await fetchUsageText(opts);
21701
22029
  ctx.ui.notify(r.text, r.ok ? "info" : "error");
@@ -21785,23 +22113,34 @@ function loadRea(pi, opts, ctx) {
21785
22113
  }
21786
22114
  function createGuestLoginExtension(opts) {
21787
22115
  let started = false;
22116
+ let nearWallWarned = false;
21788
22117
  const start = (ctx) => {
21789
22118
  started = true;
21790
- ctx.ui.notify(LOGIN_REWARD_LINE, "info");
22119
+ const variant = resolveHandoffVariant(opts);
22120
+ ctx.ui.notify(isHandoffTreatment(variant) ? LOGIN_VALUE_LINE : LOGIN_REWARD_LINE, "info");
21791
22121
  runLogin(opts, ctx);
21792
22122
  };
21793
22123
  return {
21794
22124
  name: "free-pi-guest-login",
21795
22125
  factory(pi) {
21796
22126
  pi.on("turn_end", (event, ctx) => {
21797
- if (!opts.isGuest?.() || started)
22127
+ if (!opts.isGuest?.())
21798
22128
  return;
21799
22129
  const msg = event.message;
21800
- if (msg?.stopReason !== "error")
22130
+ if (!started && msg?.stopReason === "error" && parseErrorEnvelope(msg.errorMessage)?.code === "guest_allowance") {
22131
+ start(ctx);
21801
22132
  return;
21802
- if (parseErrorEnvelope(msg.errorMessage)?.code !== "guest_allowance")
22133
+ }
22134
+ if (started || nearWallWarned || !isHandoffTreatment(resolveHandoffVariant(opts)))
21803
22135
  return;
21804
- start(ctx);
22136
+ if (msg?.stopReason === "error")
22137
+ return;
22138
+ Promise.resolve(opts.getToken()).then((jwt2) => fetchMe2(opts.baseUrl, jwt2)).then((me) => {
22139
+ if (nearWallWarned || started || !me?.guest_near_wall)
22140
+ return;
22141
+ nearWallWarned = true;
22142
+ ctx.ui.notify(GUEST_NEAR_WALL_LINE, "warning");
22143
+ }).catch(() => {});
21805
22144
  });
21806
22145
  pi.on("agent_end", (event, ctx) => {
21807
22146
  if (!opts.isGuest?.() || started)
@@ -21831,18 +22170,21 @@ function createFreePiCommandsExtension(opts) {
21831
22170
  description: "Update free-pi-cli to the latest version.",
21832
22171
  handler: (_args, ctx) => runUpdate(ctx)
21833
22172
  });
21834
- pi.registerCommand("login", {
21835
- description: "Sign in with GitHub to unlock daily credits and a comeback bonus.",
21836
- handler: (_args, ctx) => runLogin(opts, ctx)
21837
- });
21838
22173
  pi.registerCommand("usage", {
21839
22174
  description: "Show credits left today, refill time, and purchased credit.",
21840
22175
  handler: (_args, ctx) => showUsage(opts, ctx)
21841
22176
  });
22177
+ pi.on("model_select", (_event, ctx) => {
22178
+ notifyAllowance(ctx);
22179
+ });
21842
22180
  pi.registerCommand("buy-credits", {
21843
22181
  description: "Buy more usage ($5 or $10 packs, card or USDC) — opens the buy page.",
21844
22182
  handler: (_args, ctx) => buyCredits(opts, ctx)
21845
22183
  });
22184
+ pi.registerCommand("feedback", {
22185
+ description: FEEDBACK_DESCRIPTION,
22186
+ handler: (args, ctx) => sendFeedback(opts, args, ctx)
22187
+ });
21846
22188
  pi.registerCommand("support", {
21847
22189
  description: "Open today's advertiser in your browser to support free-pi.",
21848
22190
  handler: (_args, ctx) => openAdvertiser(opts, ctx)
@@ -21918,7 +22260,7 @@ function truncate2(text, maxWidth) {
21918
22260
  return text.slice(0, maxWidth);
21919
22261
  return `${text.slice(0, maxWidth - 1)}…`;
21920
22262
  }
21921
- function headerLines(theme, modelName, expanded, width = Number.POSITIVE_INFINITY, updateLatest, guest) {
22263
+ function headerLines(theme, modelName, expanded, width = Number.POSITIVE_INFINITY, updateLatest, guest, allowanceLine2) {
21922
22264
  const inner = width - 1;
21923
22265
  const fit2 = (plain, styled) => plain.length <= inner ? styled() : theme.fg("dim", truncate2(plain, inner));
21924
22266
  const titleRest = ` · ${modelName} · ctrl+o for help`;
@@ -21934,6 +22276,7 @@ function headerLines(theme, modelName, expanded, width = Number.POSITIVE_INFINIT
21934
22276
  "",
21935
22277
  dim(WELCOME_LINE),
21936
22278
  ...guest ? [dim(GUEST_LINE)] : [],
22279
+ ...allowanceLine2 ? [dim(allowanceLine2)] : [],
21937
22280
  "",
21938
22281
  dim(CONSENT_LINE_1),
21939
22282
  dim(CONSENT_LINE_2),
@@ -21957,7 +22300,7 @@ function createHeaderExtension(opts) {
21957
22300
  let expanded = false;
21958
22301
  return {
21959
22302
  render(width) {
21960
- return headerLines(theme, opts.modelName, expanded, width, opts.updateLatest, opts.guest);
22303
+ return headerLines(theme, opts.modelName, expanded, width, opts.updateLatest, opts.guest, opts.allowanceLine);
21961
22304
  },
21962
22305
  invalidate() {},
21963
22306
  setExpanded(next) {
@@ -22181,12 +22524,12 @@ function resolveFreePiScope(modelRuntime, providerName, modelIds, env = process.
22181
22524
  import { createHash as createHash3 } from "node:crypto";
22182
22525
  import { execFileSync } from "node:child_process";
22183
22526
  import { lstatSync as lstatSync2, readFileSync as readFileSync3, readlinkSync as readlinkSync2 } from "node:fs";
22184
- import { join as join7 } from "node:path";
22527
+ import { join as join8 } from "node:path";
22185
22528
 
22186
22529
  // src/hive-guard.ts
22187
- import { existsSync as existsSync2, lstatSync, readdirSync, readlinkSync, realpathSync } from "node:fs";
22530
+ import { existsSync as existsSync3, lstatSync, readdirSync, readlinkSync, realpathSync } from "node:fs";
22188
22531
  import { homedir as homedir3 } from "node:os";
22189
- import { basename, dirname as dirname5, isAbsolute, join as join6, relative, resolve, sep } from "node:path";
22532
+ import { basename, dirname as dirname6, isAbsolute, join as join7, relative, resolve, sep } from "node:path";
22190
22533
  import { fileURLToPath as fileURLToPath3 } from "node:url";
22191
22534
  var GUARD_INPUT_LIMIT = 1500;
22192
22535
  var CLASSIFY_TIMEOUT_MS = 2500;
@@ -22198,7 +22541,7 @@ var SAFE_READ_TOOLS = ["read", "grep", "find", "ls"];
22198
22541
  var ALWAYS_ASK = ["codemode", "tool_search", FUSION_PLAN_TOOL, WEB_SEARCH_TOOL_NAME, DOCS_LOOKUP_TOOL_NAME];
22199
22542
  var CLASSIFIED = ["bash", "write", "edit"];
22200
22543
  var SECRET_BASENAME = [/^\.env(\..*)?$/, /\.pem$/, /^id_(rsa|dsa|ecdsa|ed25519)/];
22201
- var SECRET_HOME_DIRS = [".ssh", ".aws", join6(".config", "freepi"), ".free-pi"];
22544
+ var SECRET_HOME_DIRS = [".ssh", ".aws", join7(".config", "freepi"), ".free-pi"];
22202
22545
  var SECRET_IN_TEXT = /(?:^|[\s"'=:/])\.env(?:\.[\w.-]+)?(?=$|[\s"';|&)])|\.pem\b|\bid_(?:rsa|dsa|ecdsa|ed25519)|\.ssh\b|\.aws\b|\.config\/freepi|\.free-pi\b/;
22203
22546
  var BASH_DENY = [
22204
22547
  [/\brm\s+(?:-\S+\s+)*(?:\/|~|\$HOME)\*?(?:\s|$)/, "destructive command"],
@@ -22221,7 +22564,7 @@ function protectedLinkTargets(root, limit) {
22221
22564
  const note = (link) => {
22222
22565
  let target2;
22223
22566
  try {
22224
- target2 = resolve(dirname5(link), readlinkSync(link));
22567
+ target2 = resolve(dirname6(link), readlinkSync(link));
22225
22568
  } catch {
22226
22569
  return;
22227
22570
  }
@@ -22231,7 +22574,7 @@ function protectedLinkTargets(root, limit) {
22231
22574
  out.push(real);
22232
22575
  };
22233
22576
  for (const entry of [".git/hooks", ".git/config"]) {
22234
- const link = join6(root, entry);
22577
+ const link = join7(root, entry);
22235
22578
  try {
22236
22579
  if (lstatSync(link).isSymbolicLink())
22237
22580
  note(link);
@@ -22250,7 +22593,7 @@ function protectedLinkTargets(root, limit) {
22250
22593
  for (const entry of entries) {
22251
22594
  if (++seen > limit)
22252
22595
  return null;
22253
- const path = join6(dir, entry.name);
22596
+ const path = join7(dir, entry.name);
22254
22597
  if (entry.isSymbolicLink()) {
22255
22598
  if (protectedByName(relative(root, path)))
22256
22599
  note(path);
@@ -22300,7 +22643,7 @@ function resolveLikePi(raw, cwd, home, platform) {
22300
22643
  if (p === "~")
22301
22644
  p = home;
22302
22645
  else if (p.startsWith("~/") || platform === "win32" && p.startsWith("~\\"))
22303
- p = join6(home, p.slice(2));
22646
+ p = join7(home, p.slice(2));
22304
22647
  if (/^file:\/\//.test(p))
22305
22648
  p = fileURLToPath3(p);
22306
22649
  return resolve(cwd, p);
@@ -22310,14 +22653,14 @@ function realpathDeepest(p) {
22310
22653
  const rest3 = [];
22311
22654
  for (;; ) {
22312
22655
  try {
22313
- return join6(realpathSync(current), ...rest3.reverse());
22656
+ return join7(realpathSync(current), ...rest3.reverse());
22314
22657
  } catch {
22315
22658
  try {
22316
22659
  lstatSync(current);
22317
22660
  return null;
22318
22661
  } catch {}
22319
22662
  }
22320
- const parent = dirname5(current);
22663
+ const parent = dirname6(current);
22321
22664
  if (parent === current)
22322
22665
  return null;
22323
22666
  rest3.push(basename(current));
@@ -22332,7 +22675,7 @@ function isSecret(target2, home) {
22332
22675
  if (SECRET_BASENAME.some((re) => re.test(basename(target2))))
22333
22676
  return true;
22334
22677
  return SECRET_HOME_DIRS.some((dir) => {
22335
- const lexical = join6(home, dir);
22678
+ const lexical = join7(home, dir);
22336
22679
  return isInside(lexical, target2) || isInside(realpathDeepest(lexical) ?? lexical, target2);
22337
22680
  });
22338
22681
  }
@@ -22353,7 +22696,7 @@ function scanForSecret(dir) {
22353
22696
  if (SECRET_BASENAME.some((re) => re.test(entry.name)))
22354
22697
  return "yes";
22355
22698
  if (entry.isDirectory())
22356
- queue.push(join6(current, entry.name));
22699
+ queue.push(join7(current, entry.name));
22357
22700
  }
22358
22701
  }
22359
22702
  return "no";
@@ -22393,7 +22736,7 @@ function pathVerdict(tool, input, opts, home) {
22393
22736
  const verdict = checkPath(raw, search, opts, home);
22394
22737
  if (verdict)
22395
22738
  return verdict;
22396
- if (tool === "read" && !existsSync2(resolveLikePi(raw, opts.cwd, home, opts.platform ?? process.platform))) {
22739
+ if (tool === "read" && !existsSync3(resolveLikePi(raw, opts.cwd, home, opts.platform ?? process.platform))) {
22397
22740
  return { ask: "file not found; pi may open a similarly named file" };
22398
22741
  }
22399
22742
  return;
@@ -22528,7 +22871,7 @@ function worktreeSnapshot(g) {
22528
22871
  const status = entry.slice(0, 2);
22529
22872
  if (status[0] === "R" || status[0] === "C")
22530
22873
  i++;
22531
- files.set(entry.slice(3), `${status}:${fingerprint(join7(root, entry.slice(3)))}`);
22874
+ files.set(entry.slice(3), `${status}:${fingerprint(join8(root, entry.slice(3)))}`);
22532
22875
  }
22533
22876
  return { files, head: runGit(g, "rev-parse", "HEAD")?.trim() };
22534
22877
  } catch (e) {
@@ -22981,7 +23324,7 @@ ${decision.reason}`, ACTION_DIALOG_MS);
22981
23324
  import { spawnSync } from "node:child_process";
22982
23325
  import {
22983
23326
  chmodSync,
22984
- existsSync as existsSync3,
23327
+ existsSync as existsSync4,
22985
23328
  lstatSync as lstatSync3,
22986
23329
  mkdirSync,
22987
23330
  mkdtempSync,
@@ -22992,7 +23335,7 @@ import {
22992
23335
  writeFileSync
22993
23336
  } from "node:fs";
22994
23337
  import { homedir as homedir4, tmpdir } from "node:os";
22995
- import { dirname as dirname6, join as join8 } from "node:path";
23338
+ import { dirname as dirname7, join as join9 } from "node:path";
22996
23339
  var SECRET_NAME_PATTERNS = [
22997
23340
  "\\.env(\\..*)?",
22998
23341
  "[^/]*\\.pem",
@@ -23027,8 +23370,8 @@ var TOOLCHAIN_ROOTS = [
23027
23370
  ".local/share/pnpm",
23028
23371
  "Library/pnpm"
23029
23372
  ];
23030
- function toolchainRoots(home, exists = existsSync3) {
23031
- return TOOLCHAIN_ROOTS.map((r) => join8(home, r)).filter((p) => exists(p));
23373
+ function toolchainRoots(home, exists = existsSync4) {
23374
+ return TOOLCHAIN_ROOTS.map((r) => join9(home, r)).filter((p) => exists(p));
23032
23375
  }
23033
23376
  var ENV_ALLOW = ["PATH", "LANG", "LC_ALL", "LC_CTYPE", "TERM", "COLORTERM", "TZ", "USER", "LOGNAME", "SHELL", "PI_PROVIDER", "PI_MODEL"];
23034
23377
  function under(path, dir) {
@@ -23046,10 +23389,10 @@ function sandboxEnv(base, o) {
23046
23389
  for (const k of ENV_ALLOW)
23047
23390
  if (base[k] !== undefined)
23048
23391
  env[k] = base[k];
23049
- 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(":");
23392
+ 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(":");
23050
23393
  if (o.developerDir)
23051
23394
  env.DEVELOPER_DIR = o.developerDir;
23052
- env.HOME = join8(o.scratch, "home");
23395
+ env.HOME = join9(o.scratch, "home");
23053
23396
  env.TMP = env.TEMP = o.scratch;
23054
23397
  env.TMPDIR = `${o.scratch}/`;
23055
23398
  env.BUN_OPTIONS = "--no-env-file";
@@ -23059,7 +23402,7 @@ function sandboxEnv(base, o) {
23059
23402
  env.GIT_CONFIG_COUNT = "1";
23060
23403
  env.GIT_CONFIG_KEY_0 = "credential.helper";
23061
23404
  env.GIT_CONFIG_VALUE_0 = "";
23062
- env.npm_config_cache = join8(o.scratch, "npm-cache");
23405
+ env.npm_config_cache = join9(o.scratch, "npm-cache");
23063
23406
  env.FREEPI_HIVE_CALL = o.callId;
23064
23407
  return env;
23065
23408
  }
@@ -23105,7 +23448,7 @@ function scanProject(project, limit = SCAN_LIMIT2) {
23105
23448
  scan.overflow = true;
23106
23449
  break;
23107
23450
  }
23108
- const path = join8(dir, e.name);
23451
+ const path = join9(dir, e.name);
23109
23452
  if (e.isSymbolicLink())
23110
23453
  continue;
23111
23454
  if (e.isDirectory()) {
@@ -23188,7 +23531,7 @@ var MAC_MACH_SERVICES = [
23188
23531
  ["com.apple.system.opendirectoryd.libinfo", "user and group names: id, ls -l, python pwd, git author"]
23189
23532
  ];
23190
23533
  function developerDir(o = {}) {
23191
- const exists = o.exists ?? existsSync3;
23534
+ const exists = o.exists ?? existsSync4;
23192
23535
  const realpath = o.realpath ?? realpathSync2;
23193
23536
  const run = o.exec ?? defaultExec;
23194
23537
  const candidates2 = [];
@@ -23205,14 +23548,14 @@ function developerDir(o = {}) {
23205
23548
  continue;
23206
23549
  try {
23207
23550
  const real = realpath(c);
23208
- if (exists(join8(real, "usr/bin/git")))
23551
+ if (exists(join9(real, "usr/bin/git")))
23209
23552
  return real;
23210
23553
  } catch {}
23211
23554
  }
23212
23555
  return null;
23213
23556
  }
23214
23557
  function opensslFiles(o = {}) {
23215
- const exists = o.exists ?? existsSync3;
23558
+ const exists = o.exists ?? existsSync4;
23216
23559
  const list = o.list ?? ((d) => {
23217
23560
  try {
23218
23561
  return readdirSync2(d);
@@ -23223,18 +23566,18 @@ function opensslFiles(o = {}) {
23223
23566
  const files = [];
23224
23567
  const dirs = [];
23225
23568
  for (const prefix of o.prefixes ?? ["/opt/homebrew", "/usr/local"]) {
23226
- const etc = join8(prefix, "etc");
23569
+ const etc = join9(prefix, "etc");
23227
23570
  for (const name of list(etc)) {
23228
23571
  if (!/^openssl(@[0-9.]+)?$/.test(name))
23229
23572
  continue;
23230
23573
  for (const f of ["openssl.cnf", "ct_log_list.cnf", "cert.pem"])
23231
- if (exists(join8(etc, name, f)))
23232
- files.push(join8(etc, name, f));
23233
- if (exists(join8(etc, name, "certs")))
23234
- dirs.push(join8(etc, name, "certs"));
23574
+ if (exists(join9(etc, name, f)))
23575
+ files.push(join9(etc, name, f));
23576
+ if (exists(join9(etc, name, "certs")))
23577
+ dirs.push(join9(etc, name, "certs"));
23235
23578
  }
23236
- if (exists(join8(etc, "ca-certificates/cert.pem")))
23237
- files.push(join8(etc, "ca-certificates/cert.pem"));
23579
+ if (exists(join9(etc, "ca-certificates/cert.pem")))
23580
+ files.push(join9(etc, "ca-certificates/cert.pem"));
23238
23581
  }
23239
23582
  return { files, dirs };
23240
23583
  }
@@ -23242,7 +23585,7 @@ var MAC_DEV_READ = ["/dev/null", "/dev/zero", "/dev/random", "/dev/urandom", "/d
23242
23585
  var MAC_DEV_WRITE = ["/dev/null", "/dev/tty", "/dev/zero", "/dev/dtracehelper"];
23243
23586
  function ancestors(p) {
23244
23587
  const out = [];
23245
- for (let d = dirname6(p);; d = dirname6(d)) {
23588
+ for (let d = dirname7(p);; d = dirname7(d)) {
23246
23589
  out.push(d);
23247
23590
  if (d === "/")
23248
23591
  break;
@@ -23278,9 +23621,9 @@ function buildMacProfile(i) {
23278
23621
  L.push(`(deny file-read* file-write* (literal ${sbString(s)}))`);
23279
23622
  for (const f of i.links)
23280
23623
  L.push(`(deny file-write* (literal ${sbString(f)}))`);
23281
- const guarded = new Set([i.project, join8(i.project, ".git"), ...i.nodeModules ?? []]);
23282
- for (const f of [...i.sockets, ...i.links, ...i.nodeModules ?? [], join8(i.project, ".git")]) {
23283
- for (let d = dirname6(f);d.length >= i.project.length && (d === i.project || d.startsWith(i.project + "/")); d = dirname6(d))
23624
+ const guarded = new Set([i.project, join9(i.project, ".git"), ...i.nodeModules ?? []]);
23625
+ for (const f of [...i.sockets, ...i.links, ...i.nodeModules ?? [], join9(i.project, ".git")]) {
23626
+ for (let d = dirname7(f);d.length >= i.project.length && (d === i.project || d.startsWith(i.project + "/")); d = dirname7(d))
23284
23627
  guarded.add(d);
23285
23628
  }
23286
23629
  for (const d of [...guarded].sort())
@@ -23333,7 +23676,7 @@ var BWRAP_ETC = [
23333
23676
  "resolv.conf"
23334
23677
  ].map((e) => `/etc/${e}`);
23335
23678
  function buildBwrapArgs(i) {
23336
- const exists = i.exists ?? existsSync3;
23679
+ const exists = i.exists ?? existsSync4;
23337
23680
  const a = ["--unshare-all", "--die-with-parent", "--new-session"];
23338
23681
  const ro = (p) => a.push("--ro-bind", p, p);
23339
23682
  for (const d of BWRAP_SYSTEM_DIRS)
@@ -23349,10 +23692,10 @@ function buildBwrapArgs(i) {
23349
23692
  a.push("--bind", i.scratch, i.scratch);
23350
23693
  for (const d of i.nodeModules)
23351
23694
  ro(d);
23352
- if (exists(join8(i.project, ".git/hooks")))
23353
- ro(join8(i.project, ".git/hooks"));
23354
- if (exists(join8(i.project, ".git/config")))
23355
- ro(join8(i.project, ".git/config"));
23695
+ if (exists(join9(i.project, ".git/hooks")))
23696
+ ro(join9(i.project, ".git/hooks"));
23697
+ if (exists(join9(i.project, ".git/config")))
23698
+ ro(join9(i.project, ".git/config"));
23356
23699
  for (const f of i.links)
23357
23700
  ro(f);
23358
23701
  for (const s of i.sockets)
@@ -23382,7 +23725,7 @@ function removeTree(path) {
23382
23725
  return;
23383
23726
  chmodSync(p, 448);
23384
23727
  for (const e of readdirSync2(p))
23385
- walk(join8(p, e));
23728
+ walk(join9(p, e));
23386
23729
  } catch {}
23387
23730
  };
23388
23731
  walk(path);
@@ -23420,16 +23763,16 @@ function createShellSandbox(deps = {}) {
23420
23763
  if (deps.python !== undefined)
23421
23764
  return deps.python;
23422
23765
  const dev = platform === "darwin" ? macHost().developerDir : null;
23423
- return dev && existsSync3(join8(dev, "usr/bin/python3")) ? join8(dev, "usr/bin/python3") : null;
23766
+ return dev && existsSync4(join9(dev, "usr/bin/python3")) ? join9(dev, "usr/bin/python3") : null;
23424
23767
  };
23425
- let cached2;
23768
+ let cached3;
23426
23769
  function probe() {
23427
23770
  if (platform === "win32")
23428
23771
  return { supported: false, reason: "Windows has no sandbox" };
23429
23772
  if (platform !== "darwin" && platform !== "linux")
23430
23773
  return { supported: false, reason: `No shell sandbox for ${platform}` };
23431
23774
  const mac3 = platform === "darwin";
23432
- if (!existsSync3(launcher.sandbox))
23775
+ if (!existsSync4(launcher.sandbox))
23433
23776
  return { supported: false, reason: mac3 ? "sandbox-exec is unavailable" : "bubblewrap is not installed" };
23434
23777
  if (checkLauncher(launcher.sandbox, undefined, stat) !== null)
23435
23778
  return { supported: false, reason: NOT_TRUSTWORTHY };
@@ -23439,7 +23782,7 @@ function createShellSandbox(deps = {}) {
23439
23782
  }
23440
23783
  const args = ["--unshare-all"];
23441
23784
  for (const d of ["/usr", "/bin", "/lib", "/lib64"])
23442
- if (existsSync3(d))
23785
+ if (existsSync4(d))
23443
23786
  args.push("--ro-bind", d, d);
23444
23787
  args.push("--proc", "/proc", "--dev", "/dev", "/bin/true");
23445
23788
  const r = exec(launcher.sandbox, args, { timeout: 5000 });
@@ -23514,12 +23857,12 @@ function createShellSandbox(deps = {}) {
23514
23857
  return {
23515
23858
  enabled: false,
23516
23859
  capability() {
23517
- return cached2 ??= probe();
23860
+ return cached3 ??= probe();
23518
23861
  },
23519
23862
  prepare(toolCallId, cwd) {
23520
23863
  dispose(toolCallId);
23521
23864
  decisions.delete(toolCallId);
23522
- const cap = cached2 ??= probe();
23865
+ const cap = cached3 ??= probe();
23523
23866
  if (!cap.supported)
23524
23867
  return { ready: false, reason: cap.reason };
23525
23868
  let project;
@@ -23538,12 +23881,12 @@ function createShellSandbox(deps = {}) {
23538
23881
  return { ready: false, reason: "a folder in the project cannot be read, so it cannot be checked for sockets and hard links" };
23539
23882
  let scratch;
23540
23883
  try {
23541
- scratch = mkdtempSync(join8(realpathSync2(tmpdir()), "freepi-hive-"));
23884
+ scratch = mkdtempSync(join9(realpathSync2(tmpdir()), "freepi-hive-"));
23542
23885
  const dir = scratch;
23543
- mkdirSync(join8(dir, "home"));
23544
- mkdirSync(join8(dir, "npm-cache"));
23545
- const emptyFile = join8(dir, "empty");
23546
- const marker = join8(dir, "call.fd");
23886
+ mkdirSync(join9(dir, "home"));
23887
+ mkdirSync(join9(dir, "npm-cache"));
23888
+ const emptyFile = join9(dir, "empty");
23889
+ const marker = join9(dir, "call.fd");
23547
23890
  writeFileSync(emptyFile, "", { mode: 292 });
23548
23891
  const roots = toolchainRoots(home);
23549
23892
  const mac3 = platform === "darwin";
@@ -23704,6 +24047,20 @@ function projectTrustForCwd(cwd, launch, agentDir, hasTrustRequiringResources =
23704
24047
  return new ProjectTrustStore(agentDir).get(cwd) === true;
23705
24048
  }
23706
24049
 
24050
+ // src/session-auth.ts
24051
+ function createSessionAuth(initial) {
24052
+ let jwt2 = initial.jwt;
24053
+ let guest = initial.guest;
24054
+ return {
24055
+ getJwt: () => jwt2,
24056
+ isGuest: () => guest,
24057
+ becomeSignedIn(token) {
24058
+ jwt2 = token;
24059
+ guest = false;
24060
+ }
24061
+ };
24062
+ }
24063
+
23707
24064
  // src/pi-launch.ts
23708
24065
  function catalogModelsFor(opts) {
23709
24066
  const main = splitCatalogModels(opts.models ?? []).main;
@@ -23735,7 +24092,7 @@ var SDK_TOOLS = [
23735
24092
  "list_mcp_resource_templates",
23736
24093
  "read_mcp_resource"
23737
24094
  ];
23738
- var SKILLS_DIR = resolve3(dirname7(fileURLToPath4(import.meta.url)), "..", "skills");
24095
+ var SKILLS_DIR = resolve3(dirname8(fileURLToPath4(import.meta.url)), "..", "skills");
23739
24096
  function resolveModelName(opts) {
23740
24097
  return opts.models?.[0]?.name || opts.model || MODEL_ID;
23741
24098
  }
@@ -23746,6 +24103,7 @@ function buildSettingsManager(opts, models, projectTrusted) {
23746
24103
  defaultTools: [...SAFE_TOOLS, TODO_TOOL_NAME],
23747
24104
  packages: [],
23748
24105
  quietStartup: true,
24106
+ defaultThinkingLevel: "off",
23749
24107
  hideThinkingBlock: true,
23750
24108
  tuiMode: "regular",
23751
24109
  theme: "light/dark",
@@ -23758,42 +24116,50 @@ function settingsForRuntime(opts, rtCwd, launch) {
23758
24116
  function buildRuntimeOptions(opts, sessionId) {
23759
24117
  process.env.PI_CODING_AGENT_DIR = opts.agentDir;
23760
24118
  const models = catalogModelsFor(opts);
24119
+ const auth = opts.sessionAuth ?? createSessionAuth({ jwt: opts.jwt, guest: Boolean(opts.guest) });
24120
+ opts.sessionAuth = auth;
23761
24121
  const providerExtension = {
23762
24122
  name: "free-pi-provider",
23763
24123
  factory: (pi) => {
23764
- pi.registerProvider(PROVIDER_NAME, buildProviderConfig(opts.baseUrl, opts.jwt, sessionId, models));
24124
+ pi.registerProvider(PROVIDER_NAME, buildProviderConfig(opts.baseUrl, auth.getJwt(), sessionId, models));
23765
24125
  const defense = defenseModelsFor(opts);
23766
24126
  if (defense.length > 0) {
23767
- pi.registerProvider(DEFENSE_PROVIDER_NAME, buildProviderConfig(opts.baseUrl, opts.jwt, sessionId, defense, DEFENSE_PROVIDER_NAME));
24127
+ pi.registerProvider(DEFENSE_PROVIDER_NAME, buildProviderConfig(opts.baseUrl, auth.getJwt(), sessionId, defense, DEFENSE_PROVIDER_NAME));
23768
24128
  }
24129
+ pi.on("before_provider_headers", (event) => {
24130
+ event.headers.Authorization = `Bearer ${auth.getJwt()}`;
24131
+ });
23769
24132
  }
23770
24133
  };
23771
24134
  let advertiserUrl;
23772
24135
  const adsExtension = createAdsExtension({
23773
24136
  baseUrl: opts.baseUrl,
23774
- getToken: () => opts.jwt,
24137
+ getToken: () => auth.getJwt(),
23775
24138
  sessionId,
23776
24139
  getGravityContext: opts.getGravityContext,
23777
24140
  onBannerAd: (u) => {
23778
24141
  advertiserUrl = u;
24142
+ },
24143
+ onMe: (me) => {
24144
+ updateAllowanceCache(me);
23779
24145
  }
23780
24146
  });
23781
24147
  const usageToolExtension = createUsageToolExtension({
23782
24148
  baseUrl: opts.baseUrl,
23783
- getToken: () => opts.jwt
24149
+ getToken: () => auth.getJwt()
23784
24150
  });
23785
24151
  const buyToolExtension = createBuyToolExtension({
23786
24152
  baseUrl: opts.baseUrl,
23787
- getToken: () => opts.jwt
24153
+ getToken: () => auth.getJwt()
23788
24154
  });
23789
24155
  const docsToolExtension = createDocsToolExtension();
23790
24156
  const webToolsExtension = createWebToolsExtension({
23791
24157
  baseUrl: opts.baseUrl,
23792
- getToken: () => opts.jwt
24158
+ getToken: () => auth.getJwt()
23793
24159
  });
23794
24160
  const imageToolExtension = createImageToolExtension({
23795
24161
  baseUrl: opts.baseUrl,
23796
- getToken: () => opts.jwt
24162
+ getToken: () => auth.getJwt()
23797
24163
  });
23798
24164
  const errorNoticeExtension = createErrorNoticeExtension({
23799
24165
  extraNotice: (env) => {
@@ -23804,34 +24170,39 @@ function buildRuntimeOptions(opts, sessionId) {
23804
24170
  if (!card)
23805
24171
  return;
23806
24172
  session.shown = true;
23807
- reportRefillCardShown({ baseUrl: opts.baseUrl, jwt: opts.jwt });
24173
+ reportRefillCardShown({ baseUrl: opts.baseUrl, jwt: auth.getJwt() });
23808
24174
  return card;
23809
24175
  }
23810
24176
  });
23811
24177
  const toolGuardExtension = createToolGuardExtension({
23812
24178
  baseUrl: opts.baseUrl,
23813
- getToken: () => opts.jwt,
24179
+ getToken: () => auth.getJwt(),
23814
24180
  allowedTools: ALLOWED_TOOL_NAMES
23815
24181
  });
23816
24182
  const commandsExtension = createFreePiCommandsExtension({
23817
24183
  baseUrl: opts.baseUrl,
23818
- getToken: () => opts.jwt,
24184
+ getToken: () => auth.getJwt(),
23819
24185
  getAdvertiserUrl: () => advertiserUrl,
23820
24186
  credentialsPath: opts.credentialsPath,
23821
24187
  agentDir: opts.agentDir,
23822
- isGuest: () => Boolean(opts.guest)
24188
+ isGuest: () => auth.isGuest(),
24189
+ onLogin: (token) => auth.becomeSignedIn(token),
24190
+ variant: () => opts.guestHandoffVariant
23823
24191
  });
23824
24192
  const guestLoginExtension = opts.guest ? createGuestLoginExtension({
23825
24193
  baseUrl: opts.baseUrl,
23826
- getToken: () => opts.jwt,
24194
+ getToken: () => auth.getJwt(),
23827
24195
  credentialsPath: opts.credentialsPath,
23828
24196
  agentDir: opts.agentDir,
23829
- isGuest: () => Boolean(opts.guest)
24197
+ isGuest: () => auth.isGuest(),
24198
+ onLogin: (token) => auth.becomeSignedIn(token),
24199
+ variant: () => opts.guestHandoffVariant
23830
24200
  }) : undefined;
23831
24201
  const headerExtension = createHeaderExtension({
23832
24202
  modelName: resolveModelName(opts),
23833
24203
  updateLatest: opts.updateLatest,
23834
- guest: opts.guest
24204
+ guest: opts.guest,
24205
+ allowanceLine: opts.allowanceLine
23835
24206
  });
23836
24207
  const todoToolExtension = createTodoToolExtension();
23837
24208
  const shellSandbox = opts.shellSandbox ?? createShellSandbox();
@@ -23841,7 +24212,7 @@ function buildRuntimeOptions(opts, sessionId) {
23841
24212
  const toolSearchExtension = { name: "pi-tool-search", factory: createToolSearchExtension() };
23842
24213
  const hiveExtension = createHiveExtension({
23843
24214
  baseUrl: opts.baseUrl,
23844
- getToken: () => opts.jwt,
24215
+ getToken: () => auth.getJwt(),
23845
24216
  knownTools: ALLOWED_TOOL_NAMES,
23846
24217
  sandbox: shellSandbox
23847
24218
  });
@@ -23881,7 +24252,20 @@ function buildRuntimeOptions(opts, sessionId) {
23881
24252
  function getSessionDir(cwd, agentDir) {
23882
24253
  const resolvedCwd = resolve3(cwd);
23883
24254
  const safePath = `--${resolvedCwd.replace(/^[/\\]/, "").replace(/[/\\:]/g, "-")}--`;
23884
- return join9(resolve3(agentDir), "sessions", safePath);
24255
+ return join10(resolve3(agentDir), "sessions", safePath);
24256
+ }
24257
+ async function findLatestSession(cwd, sessionDir, api3 = SessionManager) {
24258
+ const infos = await api3.list(cwd, sessionDir);
24259
+ if (infos.length === 0)
24260
+ return;
24261
+ const latest = [...infos].sort((a, b) => +new Date(b.modified) - +new Date(a.modified))[0];
24262
+ if (!latest)
24263
+ return;
24264
+ return {
24265
+ id: latest.id,
24266
+ modified: latest.modified instanceof Date ? latest.modified : new Date(latest.modified),
24267
+ firstMessage: latest.firstMessage ?? ""
24268
+ };
23885
24269
  }
23886
24270
  async function createSessionManager(cwd, sessionDir, sessionId, api3 = SessionManager) {
23887
24271
  if (sessionId) {
@@ -23907,9 +24291,11 @@ async function launchPi(opts) {
23907
24291
  const cwd = process.cwd();
23908
24292
  const sessionDir = getSessionDir(cwd, opts.agentDir);
23909
24293
  const sm = await createSessionManager(cwd, sessionDir, opts.session);
24294
+ const auth = opts.sessionAuth ?? createSessionAuth({ jwt: opts.jwt, guest: Boolean(opts.guest) });
24295
+ const launchOpts = { ...opts, sessionAuth: auth };
23910
24296
  const sessionId = sm.getSessionId();
23911
24297
  const projectTrusted = await decideProjectTrustInteractive(cwd, opts.agentDir);
23912
- const { resourceLoaderOptions, tools } = buildRuntimeOptions({ ...opts, projectTrusted }, sessionId);
24298
+ const { resourceLoaderOptions, tools } = buildRuntimeOptions({ ...launchOpts, projectTrusted }, sessionId);
23913
24299
  const launchTrust = { cwd, trusted: projectTrusted };
23914
24300
  const createRuntime = async ({
23915
24301
  cwd: rtCwd,
@@ -23944,7 +24330,17 @@ async function launchPi(opts) {
23944
24330
  initialImages: [],
23945
24331
  initialMessages: []
23946
24332
  });
23947
- installBugHandler(mode, { baseUrl: opts.baseUrl, getToken: () => opts.jwt });
24333
+ installBugHandler(mode, { baseUrl: opts.baseUrl, getToken: () => auth.getJwt() });
24334
+ installLoginHandler(mode, {
24335
+ baseUrl: opts.baseUrl,
24336
+ getToken: () => auth.getJwt(),
24337
+ credentialsPath: opts.credentialsPath,
24338
+ agentDir: opts.agentDir,
24339
+ isGuest: () => auth.isGuest(),
24340
+ onLogin: (token) => auth.becomeSignedIn(token),
24341
+ variant: () => opts.guestHandoffVariant
24342
+ });
24343
+ installModelAllowanceHandler(mode);
23948
24344
  installFooterRightAlign(mode);
23949
24345
  await mode.run();
23950
24346
  return sm.getSessionId();
@@ -23994,6 +24390,154 @@ function createRealSpawn(io = { platform: process.platform, spawnSyncImpl: spawn
23994
24390
  // src/run.ts
23995
24391
  import { SessionManager as SessionManager2 } from "@earendil-works/pi-coding-agent";
23996
24392
 
24393
+ // src/resume-on-launch.ts
24394
+ var RESUME_ON_LAUNCH_EXPERIMENT = "resume_on_launch";
24395
+ var DEFAULT_RESUME_RECENT_HOURS = 48;
24396
+ var RESUME_PROMPT_SUFFIX = " [Y/n] ";
24397
+ function userArgv(argv) {
24398
+ if (/(^|[\\/])(node|bun)(\.exe)?$/i.test(argv[0] ?? ""))
24399
+ return argv.slice(2);
24400
+ return argv;
24401
+ }
24402
+ function parseResumeFlags(argv) {
24403
+ const args = userArgv(argv);
24404
+ let session;
24405
+ let sessionError;
24406
+ let continueLast = false;
24407
+ let startNew = false;
24408
+ let hasPrompt = false;
24409
+ for (let i = 0;i < args.length; i++) {
24410
+ const arg = args[i];
24411
+ if (arg === "--session") {
24412
+ const value = args[i + 1];
24413
+ if (!value || value.startsWith("--")) {
24414
+ sessionError = "Missing value for --session";
24415
+ } else {
24416
+ session = value;
24417
+ i += 1;
24418
+ }
24419
+ continue;
24420
+ }
24421
+ if (arg.startsWith("--session=")) {
24422
+ const value = arg.slice("--session=".length);
24423
+ if (!value)
24424
+ sessionError = "Missing value for --session";
24425
+ else
24426
+ session = value;
24427
+ continue;
24428
+ }
24429
+ if (arg === "--continue" || arg === "--resume") {
24430
+ continueLast = true;
24431
+ continue;
24432
+ }
24433
+ if (arg === "--new") {
24434
+ startNew = true;
24435
+ continue;
24436
+ }
24437
+ if (arg === "--ref" || arg === "--src") {
24438
+ if (args[i + 1] && !String(args[i + 1]).startsWith("--"))
24439
+ i += 1;
24440
+ continue;
24441
+ }
24442
+ if (arg.startsWith("--ref=") || arg.startsWith("--src="))
24443
+ continue;
24444
+ if (!arg.startsWith("-"))
24445
+ hasPrompt = true;
24446
+ }
24447
+ return { session, sessionError, continueLast, startNew, hasPrompt };
24448
+ }
24449
+ function isRecentSession(modified, recentHours, now) {
24450
+ const ageMs = now.getTime() - modified.getTime();
24451
+ return ageMs >= 0 && ageMs <= recentHours * 3600000;
24452
+ }
24453
+ function recentHoursFromEnv(env = process.env, serverHours) {
24454
+ const raw = env.FREEPI_RESUME_RECENT_HOURS;
24455
+ if (raw !== undefined && raw !== "") {
24456
+ const n = Number(raw);
24457
+ if (Number.isInteger(n) && n >= 1)
24458
+ return n;
24459
+ }
24460
+ if (typeof serverHours === "number" && Number.isInteger(serverHours) && serverHours >= 1) {
24461
+ return serverHours;
24462
+ }
24463
+ return DEFAULT_RESUME_RECENT_HOURS;
24464
+ }
24465
+ function formatResumeAge(modified, now) {
24466
+ const deltaSec = Math.max(0, Math.floor((now.getTime() - modified.getTime()) / 1000));
24467
+ if (deltaSec < 60)
24468
+ return "just now";
24469
+ const minutes = Math.floor(deltaSec / 60);
24470
+ if (minutes < 60)
24471
+ return minutes === 1 ? "1 minute ago" : `${minutes} minutes ago`;
24472
+ const hours = Math.floor(minutes / 60);
24473
+ if (hours < 24)
24474
+ return hours === 1 ? "1 hour ago" : `${hours} hours ago`;
24475
+ const days = Math.floor(hours / 24);
24476
+ if (days === 1)
24477
+ return "yesterday";
24478
+ return `${days} days ago`;
24479
+ }
24480
+ function previewText(firstMessage) {
24481
+ if (!firstMessage)
24482
+ return;
24483
+ const oneLine = firstMessage.replace(/\s+/g, " ").trim();
24484
+ if (!oneLine)
24485
+ return;
24486
+ return oneLine.length > 60 ? `${oneLine.slice(0, 57)}...` : oneLine;
24487
+ }
24488
+ function formatResumePrompt(session, now = new Date) {
24489
+ const age = formatResumeAge(session.modified, now);
24490
+ const preview = previewText(session.firstMessage);
24491
+ const body = preview ? `Continue last session from ${age}: ${preview}?` : `Continue last session from ${age}?`;
24492
+ return `${body}${RESUME_PROMPT_SUFFIX}`;
24493
+ }
24494
+ function parseResumeKeypress(raw) {
24495
+ if (raw === "\x03")
24496
+ return null;
24497
+ const ch = raw.slice(0, 1);
24498
+ if (ch === "n" || ch === "N")
24499
+ return false;
24500
+ return true;
24501
+ }
24502
+ async function promptResume(session, deps) {
24503
+ if (!deps.stdinIsTTY)
24504
+ return false;
24505
+ const prompt = formatResumePrompt(session, deps.now ?? new Date);
24506
+ const raw = await deps.askKey(prompt);
24507
+ const parsed = parseResumeKeypress(raw);
24508
+ if (parsed === null)
24509
+ return "aborted";
24510
+ deps.write(`
24511
+ `);
24512
+ return parsed;
24513
+ }
24514
+ async function readOneKeypress(prompt, stdin = process.stdin, stdout = process.stdout) {
24515
+ stdout.write(prompt);
24516
+ const wasRaw = stdin.isRaw;
24517
+ if (typeof stdin.setRawMode === "function")
24518
+ stdin.setRawMode(true);
24519
+ stdin.resume();
24520
+ return await new Promise((resolve4) => {
24521
+ const onData = (chunk) => {
24522
+ stdin.off("data", onData);
24523
+ if (typeof stdin.setRawMode === "function")
24524
+ stdin.setRawMode(wasRaw ?? false);
24525
+ stdin.pause();
24526
+ resolve4(typeof chunk === "string" ? chunk : chunk.toString("utf8"));
24527
+ };
24528
+ stdin.once("data", onData);
24529
+ });
24530
+ }
24531
+ async function promptResumeInteractive(session) {
24532
+ return promptResume(session, {
24533
+ stdinIsTTY: process.stdin.isTTY === true,
24534
+ write: (text) => {
24535
+ process.stdout.write(text);
24536
+ },
24537
+ askKey: (prompt) => readOneKeypress(prompt)
24538
+ });
24539
+ }
24540
+
23997
24541
  // src/onboarding.ts
23998
24542
  var INTRO_ENV_SKIP = "FREEPI_NO_INTRO";
23999
24543
  var INTRO_TEXT = `
@@ -24021,12 +24565,12 @@ ${INTRO_TEXT}
24021
24565
  }
24022
24566
 
24023
24567
  // src/whats-new.ts
24024
- import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "node:fs";
24025
- import { dirname as dirname8 } from "node:path";
24568
+ import { existsSync as existsSync5, mkdirSync as mkdirSync2, readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "node:fs";
24569
+ import { dirname as dirname9 } from "node:path";
24026
24570
  function maybeWhatsNew(cliVersion, agentDir) {
24027
24571
  try {
24028
24572
  const path = getLastSeenVersionPath(agentDir);
24029
- const lastSeen = existsSync4(path) ? readFileSync4(path, "utf-8").trim() : undefined;
24573
+ const lastSeen = existsSync5(path) ? readFileSync4(path, "utf-8").trim() : undefined;
24030
24574
  writeLastSeen(path, cliVersion);
24031
24575
  if (lastSeen === undefined)
24032
24576
  return;
@@ -24042,7 +24586,7 @@ function maybeWhatsNew(cliVersion, agentDir) {
24042
24586
  }
24043
24587
  }
24044
24588
  function writeLastSeen(path, version3) {
24045
- mkdirSync2(dirname8(path), { recursive: true });
24589
+ mkdirSync2(dirname9(path), { recursive: true });
24046
24590
  writeFileSync2(path, version3, "utf-8");
24047
24591
  }
24048
24592
 
@@ -24068,7 +24612,8 @@ async function loginFlow(deps, argv = []) {
24068
24612
  openBrowser: deps.openBrowser,
24069
24613
  sleep: deps.sleep,
24070
24614
  log: deps.log,
24071
- jwt: await loadJwt(deps.credentialsPath)
24615
+ jwt: await loadJwt(deps.credentialsPath),
24616
+ credentialsPath: deps.credentialsPath
24072
24617
  }, resolved.attribution ?? {});
24073
24618
  await saveJwt(deps.credentialsPath, token);
24074
24619
  return token;
@@ -24079,28 +24624,52 @@ async function loginFlow(deps, argv = []) {
24079
24624
  throw err;
24080
24625
  }
24081
24626
  }
24082
- function parseSessionArg(argv) {
24083
- const flagIndex = argv.indexOf("--session");
24084
- if (flagIndex === -1)
24085
- return {};
24086
- const value = argv[flagIndex + 1];
24087
- if (!value || value.startsWith("--")) {
24088
- return { error: "Missing value for --session" };
24089
- }
24627
+ function validateSessionId(value) {
24090
24628
  try {
24091
24629
  SessionManager2.inMemory(process.cwd(), { id: value });
24630
+ return;
24092
24631
  } catch (err) {
24093
- return { error: `Invalid --session value: ${err instanceof Error ? err.message : String(err)}` };
24632
+ return `Invalid --session value: ${err instanceof Error ? err.message : String(err)}`;
24094
24633
  }
24095
- return { session: value };
24634
+ }
24635
+ function reportResumeExposure(deps, jwt2, event) {
24636
+ if (deps.reportResumeExposure) {
24637
+ deps.reportResumeExposure(event);
24638
+ return;
24639
+ }
24640
+ fetch(new URL("/telemetry/experiment", deps.baseUrl), {
24641
+ method: "POST",
24642
+ headers: {
24643
+ authorization: `Bearer ${jwt2}`,
24644
+ "content-type": "application/json"
24645
+ },
24646
+ body: JSON.stringify({ experiment: RESUME_ON_LAUNCH_EXPERIMENT, event }),
24647
+ signal: AbortSignal.timeout(3000)
24648
+ }).catch(() => {});
24649
+ }
24650
+ async function defaultFindLatestSession(cwd, agentDir) {
24651
+ return findLatestSession(cwd, getSessionDir(cwd, agentDir));
24096
24652
  }
24097
24653
  async function run(deps, argv = process.argv) {
24098
- const sessionArg = parseSessionArg(argv);
24099
- if (sessionArg.error) {
24100
- deps.log(sessionArg.error);
24654
+ const resumeFlags = parseResumeFlags(argv);
24655
+ if (resumeFlags.sessionError) {
24656
+ deps.log(resumeFlags.sessionError);
24101
24657
  return 1;
24102
24658
  }
24659
+ if (resumeFlags.session) {
24660
+ const invalid = validateSessionId(resumeFlags.session);
24661
+ if (invalid) {
24662
+ deps.log(invalid);
24663
+ return 1;
24664
+ }
24665
+ }
24666
+ const pending = await loadPendingLogin(deps.credentialsPath);
24667
+ if (pending)
24668
+ deps.log(`
24669
+ ${FINISH_SIGNIN_LINE}
24670
+ `);
24103
24671
  let jwt2 = await loadJwt(deps.credentialsPath);
24672
+ let guestHandoffVariant;
24104
24673
  let consentCurrent = false;
24105
24674
  if (jwt2) {
24106
24675
  try {
@@ -24143,9 +24712,12 @@ async function run(deps, argv = process.argv) {
24143
24712
  const guest = await startGuestSession(deps.baseUrl, CONSENT_VERSION, attribution);
24144
24713
  await saveJwt(deps.credentialsPath, guest.token);
24145
24714
  jwt2 = guest.token;
24146
- deps.log(`
24715
+ deps.log(isHandoffTreatment(guest.guestHandoffVariant) ? `
24716
+ You're on a short guest trial — ads pay for it. ${LOGIN_VALUE_LINE}
24717
+ ` : `
24147
24718
  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.
24148
24719
  `);
24720
+ guestHandoffVariant = guest.guestHandoffVariant;
24149
24721
  } catch (err) {
24150
24722
  if (!(err instanceof GuestUnavailableError))
24151
24723
  throw err;
@@ -24156,7 +24728,7 @@ Guest trial is not available right now.
24156
24728
  }
24157
24729
  consentCurrent = true;
24158
24730
  }
24159
- const versionCheck = await deps.checkClientVersion(deps.baseUrl, CLI_VERSION);
24731
+ const versionCheck = await deps.checkClientVersion(deps.baseUrl, CLI_VERSION, jwt2);
24160
24732
  let updateLatest;
24161
24733
  if (versionCheck.action !== "ok") {
24162
24734
  const outcome = await deps.maybeSelfUpdate(versionCheck.action, versionCheck.latest);
@@ -24182,23 +24754,61 @@ Guest trial is not available right now.
24182
24754
  } catch {}
24183
24755
  }
24184
24756
  const me = await fetchMe2(deps.baseUrl, jwt2).catch(() => null);
24757
+ if (me)
24758
+ updateAllowanceCache(me);
24759
+ guestHandoffVariant = me?.experiments?.guest_handoff ?? guestHandoffVariant;
24185
24760
  const startRefill = await fetchRefillStats(deps.baseUrl, jwt2).catch(() => {
24186
24761
  return;
24187
24762
  });
24188
24763
  const refillSession = sessionFromStats(startRefill);
24764
+ const creditsBack = startRefill ? formatCreditsBackNudge(startRefill) : undefined;
24765
+ if (creditsBack) {
24766
+ deps.log(`
24767
+ ${creditsBack}`);
24768
+ reportRefillCardShown({ baseUrl: deps.baseUrl, jwt: jwt2 });
24769
+ }
24770
+ let session = resumeFlags.session;
24771
+ const lookupLatest = deps.findLatestSession ?? defaultFindLatestSession;
24772
+ if (!session && resumeFlags.continueLast && !resumeFlags.startNew) {
24773
+ session = (await lookupLatest(process.cwd(), deps.agentDir))?.id;
24774
+ if (!session)
24775
+ deps.log("No earlier session in this folder; starting a new one.");
24776
+ }
24777
+ const experiment = versionCheck.experiments?.[RESUME_ON_LAUNCH_EXPERIMENT];
24778
+ const variant = experiment?.variant === "treatment" ? "treatment" : "control";
24779
+ const recentHours = recentHoursFromEnv(process.env, experiment?.recent_hours);
24780
+ const interactive = deps.stdinIsTTY ?? process.stdin.isTTY === true;
24781
+ if (!session && !resumeFlags.startNew && !resumeFlags.hasPrompt && variant === "treatment" && interactive) {
24782
+ const latest = await lookupLatest(process.cwd(), deps.agentDir);
24783
+ const now = deps.now?.() ?? new Date;
24784
+ if (latest && isRecentSession(latest.modified, recentHours, now)) {
24785
+ reportResumeExposure(deps, jwt2, "offer_shown");
24786
+ const answer = await (deps.promptResume ?? promptResumeInteractive)(latest);
24787
+ if (answer === "aborted") {
24788
+ deps.log("Aborted.");
24789
+ return 1;
24790
+ }
24791
+ if (answer) {
24792
+ reportResumeExposure(deps, jwt2, "offer_accepted");
24793
+ session = latest.id;
24794
+ }
24795
+ }
24796
+ }
24189
24797
  const finalSessionId = await deps.launchPi({
24190
24798
  baseUrl: deps.baseUrl,
24191
24799
  jwt: jwt2,
24192
24800
  agentDir: deps.agentDir,
24193
24801
  credentialsPath: deps.credentialsPath,
24194
- session: sessionArg.session,
24802
+ session,
24195
24803
  model: versionCheck.model,
24196
24804
  models: versionCheck.models,
24197
24805
  fusion: versionCheck.fusion,
24198
24806
  updateLatest,
24199
24807
  getGravityContext,
24200
24808
  guest: Boolean(me?.guest),
24201
- refillCard: refillSession
24809
+ refillCard: refillSession,
24810
+ allowanceLine: allowanceLine(),
24811
+ guestHandoffVariant
24202
24812
  });
24203
24813
  const endRefill = await fetchRefillStats(deps.baseUrl, jwt2).catch(() => {
24204
24814
  return;
@@ -24253,17 +24863,18 @@ async function maybeSelfUpdate(action2, latest, deps) {
24253
24863
  }
24254
24864
 
24255
24865
  // src/update-check.ts
24256
- async function checkClientVersion(baseUrl, cliVersion, fetchImpl = fetch) {
24866
+ async function checkClientVersion(baseUrl, cliVersion, fetchImpl = fetch, jwt2) {
24257
24867
  try {
24258
24868
  const res = await fetchImpl(new URL("/client-version", baseUrl), {
24259
- signal: AbortSignal.timeout(3000)
24869
+ signal: AbortSignal.timeout(3000),
24870
+ headers: jwt2 ? { authorization: `Bearer ${jwt2}` } : undefined
24260
24871
  });
24261
24872
  if (!res.ok)
24262
24873
  return { action: "ok" };
24263
24874
  const parsed = ClientVersionResponseSchema.safeParse(await res.json());
24264
24875
  if (!parsed.success)
24265
24876
  return { action: "ok" };
24266
- const { min, latest, model, models, notice, fusion } = parsed.data;
24877
+ const { min, latest, model, models, notice, fusion, experiments } = parsed.data;
24267
24878
  const extra = {};
24268
24879
  if (model !== undefined)
24269
24880
  extra.model = model;
@@ -24273,6 +24884,8 @@ async function checkClientVersion(baseUrl, cliVersion, fetchImpl = fetch) {
24273
24884
  extra.notice = notice;
24274
24885
  if (fusion !== undefined)
24275
24886
  extra.fusion = fusion;
24887
+ if (experiments !== undefined)
24888
+ extra.experiments = experiments;
24276
24889
  if (compareVersions(cliVersion, min) === -1)
24277
24890
  return { action: "block", latest, ...extra };
24278
24891
  if (compareVersions(cliVersion, latest) === -1)
@@ -24340,8 +24953,8 @@ ${HELP_TEXT}`);
24340
24953
  sleep: (ms) => new Promise((resolve4) => setTimeout(resolve4, ms)),
24341
24954
  launchPi,
24342
24955
  log: (message) => console.log(message),
24343
- checkClientVersion,
24344
- startGravityPixel: (jwt2) => startGravityPixel(jwt2, join10(dirname9(agentDir), "gravity")),
24956
+ checkClientVersion: (baseUrl, cliVersion, jwt2) => checkClientVersion(baseUrl, cliVersion, fetch, jwt2),
24957
+ startGravityPixel: (jwt2) => startGravityPixel(jwt2, join11(dirname10(agentDir), "gravity")),
24345
24958
  maybeSelfUpdate: (action2, latest) => maybeSelfUpdate(action2, latest, {
24346
24959
  env: process.env,
24347
24960
  autoUpdateEnabled: () => readAutoUpdate(getConfigPath()),