free-pi-cli 0.6.4 → 0.6.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -5,7 +5,7 @@ for the user because ads pay for inference. All model calls route through the
5
5
  free-pi proxy — you never handle a model API key.
6
6
 
7
7
  ```
8
- npx free-pi-cli
8
+ npx free-pi-cli@latest
9
9
  ```
10
10
 
11
11
  First run shows a consent screen (free inference, ads pay for it, sessions
@@ -18,7 +18,7 @@ existing `~/.pi/agent` install.
18
18
  Closed the terminal? Just run the same command again — you stay signed in:
19
19
 
20
20
  ```
21
- npx free-pi-cli
21
+ npx free-pi-cli@latest
22
22
  ```
23
23
 
24
24
  Prefer a shorter command? Install it once, globally, then call it by name:
@@ -29,7 +29,7 @@ free-pi-cli
29
29
  ```
30
30
 
31
31
  Note: `free-pi-cli` is the command — **not** `pi`. If you see the built-in
32
- agent suggest `pi --session …` on exit, use `npx free-pi-cli --session <id>`
32
+ agent suggest `pi --session …` on exit, use `npx free-pi-cli@latest --session <id>`
33
33
  instead. In a folder with a recent session, some users are offered a
34
34
  one-keypress continue (default Yes). `--continue` resumes the last session
35
35
  here; `--new` starts fresh.
@@ -48,7 +48,7 @@ here; `--new` starts fresh.
48
48
  | `src/index.ts` | bin entry — wires real dependencies, calls `run()` |
49
49
  | `src/run.ts` | orchestration: consent → login → token check → launch |
50
50
  | `src/consent.ts` | consent screen text/parsing |
51
- | `src/auth-flow.ts` | GitHub device flow client (`/auth/github/device`, `/auth/consent`, `/auth/token`), `/me` token check |
51
+ | `src/auth-flow.ts` | Sign-in clients: browser "Connect this terminal" (`/auth/connect/start`, `/auth/connect/poll`) and the GitHub device flow fallback (`/auth/github/device`, `/auth/consent`, `/auth/token`), `/me` token check |
52
52
  | `src/credentials.ts` | JWT file storage (mode 0600) under the free-pi config dir |
53
53
  | `src/provider.ts` | pure builder for the `pi.registerProvider` config |
54
54
  | `src/pi-launch.ts` | the actual pi SDK integration (registers the provider, launches the TUI) |
package/dist/index.js CHANGED
@@ -17,7 +17,7 @@ function zstdSupportError(zlibModule = zlib, nodeVersion = process.versions.node
17
17
  `The pi agent needs Node with zstd support (Node >= 22.15, or >= 23.8 on the`,
18
18
  `Current line) — older Node crashes mid-request inside the agent. Upgrade Node:`,
19
19
  ``,
20
- ` nvm install 22 # then re-run: npx free-pi-cli`,
20
+ ` nvm install 22 # then re-run: npx free-pi-cli@latest`,
21
21
  ``,
22
22
  `(or update your system Node to the latest LTS.)`
23
23
  ].join(`
@@ -30,4 +30,4 @@ if (err) {
30
30
  console.error(err);
31
31
  process.exit(1);
32
32
  }
33
- await import("./main-2mvzsazb.js");
33
+ await import("./main-m7ct3546.js");
@@ -14407,6 +14407,21 @@ var AuthConsentRequestSchema = exports_external.object({
14407
14407
  signup_src: exports_external.string().optional(),
14408
14408
  referral_code: exports_external.string().optional()
14409
14409
  });
14410
+ var ConnectStartRequestSchema = exports_external.object({
14411
+ consent_version: exports_external.string().min(1).max(32),
14412
+ signup_src: exports_external.string().optional(),
14413
+ referral_code: exports_external.string().optional(),
14414
+ client: exports_external.object({ os: exports_external.string().max(64).optional(), cli_version: exports_external.string().max(64).optional() }).optional()
14415
+ });
14416
+ var ConnectStartResponseSchema = exports_external.object({
14417
+ session_id: exports_external.string().uuid(),
14418
+ poll_secret: exports_external.string(),
14419
+ token: exports_external.string(),
14420
+ confirm_code: exports_external.string(),
14421
+ connect_url: exports_external.string(),
14422
+ expires_in: exports_external.number(),
14423
+ interval: exports_external.number()
14424
+ });
14410
14425
  var GuestStartRequestSchema = exports_external.object({
14411
14426
  consent_version: exports_external.string().min(1).max(32),
14412
14427
  signup_src: exports_external.string().optional(),
@@ -14692,7 +14707,12 @@ var ConfigSchema = exports_external.object({
14692
14707
  referralMaxActivePerReferrer: exports_external.number().int().default(10),
14693
14708
  referralGithubMinAgeDays: exports_external.number().int().default(30),
14694
14709
  referralCreditGrantCredits: exports_external.number().int().default(0),
14695
- webClaimCredits: exports_external.number().int().default(0)
14710
+ webClaimCredits: exports_external.number().int().default(0),
14711
+ geoipDbPath: exports_external.string().optional(),
14712
+ sanctionedCountries: exports_external.array(exports_external.string()).default(["IR", "CU", "KP", "SY"]),
14713
+ regionLimitsStart: exports_external.date().optional(),
14714
+ fullAllowanceCountries: exports_external.array(exports_external.string()).default(["US", "CA", "GB", "AU", "NZ", "IE"]),
14715
+ regionLowCapMicros: exports_external.bigint().default(100000n)
14696
14716
  });
14697
14717
  function envNumber(value) {
14698
14718
  if (value === undefined || value === "")
@@ -14728,6 +14748,15 @@ function envList(value) {
14728
14748
  const items = value.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
14729
14749
  return items.length > 0 ? items : undefined;
14730
14750
  }
14751
+ function envCountries(value) {
14752
+ return envList(value)?.map((s) => s.toUpperCase());
14753
+ }
14754
+ function envDate(value) {
14755
+ if (!value?.trim())
14756
+ return;
14757
+ const d = new Date(value.trim());
14758
+ return Number.isNaN(d.getTime()) ? undefined : d;
14759
+ }
14731
14760
  function envDecimalString(value) {
14732
14761
  if (value === undefined || value.trim() === "")
14733
14762
  return;
@@ -14822,7 +14851,12 @@ function loadConfig(env = process.env) {
14822
14851
  referralMaxActivePerReferrer: envStrictInt("REFERRAL_MAX_ACTIVE_PER_REFERRER", env.REFERRAL_MAX_ACTIVE_PER_REFERRER),
14823
14852
  referralGithubMinAgeDays: envStrictInt("REFERRAL_GITHUB_MIN_AGE_DAYS", env.REFERRAL_GITHUB_MIN_AGE_DAYS),
14824
14853
  referralCreditGrantCredits: envStrictInt("REFERRAL_CREDIT_GRANT_CREDITS", env.REFERRAL_CREDIT_GRANT_CREDITS),
14825
- webClaimCredits: envStrictInt("WEB_CLAIM_CREDITS", env.WEB_CLAIM_CREDITS)
14854
+ webClaimCredits: envStrictInt("WEB_CLAIM_CREDITS", env.WEB_CLAIM_CREDITS),
14855
+ geoipDbPath: env.GEOIP_DB_PATH?.trim() || undefined,
14856
+ sanctionedCountries: envCountries(env.SANCTIONED_COUNTRIES),
14857
+ regionLimitsStart: envDate(env.REGION_LIMITS_START),
14858
+ fullAllowanceCountries: envCountries(env.FULL_ALLOWANCE_COUNTRIES),
14859
+ regionLowCapMicros: envMicros("REGION_LOW_CAP_USD", env.REGION_LOW_CAP_USD)
14826
14860
  });
14827
14861
  }
14828
14862
  // ../shared/src/credits.ts
@@ -15061,7 +15095,7 @@ var HiveClassifyResponseSchema = exports_external.object({
15061
15095
  // package.json
15062
15096
  var package_default = {
15063
15097
  name: "free-pi-cli",
15064
- version: "0.6.4",
15098
+ version: "0.6.6",
15065
15099
  description: "Free, ad-supported distribution of the pi coding agent. Inference paid for by ads.",
15066
15100
  license: "MIT",
15067
15101
  type: "module",
@@ -15104,6 +15138,7 @@ Usage:
15104
15138
  free-pi-cli --session <id> resume a previous session
15105
15139
  free-pi-cli --continue resume the last session in this folder
15106
15140
  free-pi-cli --new start a new session
15141
+ free-pi-cli --device-login sign in with a GitHub device code instead of the browser page
15107
15142
  free-pi-cli --ref <code> first-run referral code (from freepi.ai/?ref=)
15108
15143
  free-pi-cli --src <channel> first-run campaign attribution (from freepi.ai/?src=)
15109
15144
  free-pi-cli logout remove the stored sign-in token; the next run signs in again
@@ -15114,6 +15149,7 @@ Usage:
15114
15149
 
15115
15150
  Environment:
15116
15151
  FREEPI_BASE_URL override the backend base URL (default https://api.freepi.ai)
15152
+ FREEPI_DEVICE_LOGIN=1 same as --device-login
15117
15153
  FREEPI_NO_AUTO_UPDATE disable in-place self-update for this run
15118
15154
  FREEPI_NO_GRAVITY_PIXEL=1 do not run Gravity's ad-measurement pixel
15119
15155
  FREEPI_REF same as --ref when the flag is omitted
@@ -15139,7 +15175,7 @@ function parseCliArgs(argv) {
15139
15175
  let i = 0;
15140
15176
  while (i < argv.length) {
15141
15177
  const arg = argv[i];
15142
- if (arg === "--continue" || arg === "--resume" || arg === "--new") {
15178
+ if (arg === "--continue" || arg === "--resume" || arg === "--new" || arg === "--device-login") {
15143
15179
  i += 1;
15144
15180
  continue;
15145
15181
  }
@@ -15641,12 +15677,16 @@ function mapErrorCode(code) {
15641
15677
  case "account_review":
15642
15678
  return "this account is under review — contact support";
15643
15679
  case "ads_required":
15644
- return "ads aren't displaying for this account, so free completions are paused — run free-pi through `npx free-pi-cli`, then retry";
15680
+ return "ads aren't displaying for this account, so free completions are paused — run free-pi through `npx free-pi-cli@latest`, then retry";
15645
15681
  case "guest_allowance":
15646
15682
  return "guest trial used up — sign in with GitHub (/login) for 15,000 free credits a day";
15647
15683
  case "guest_unavailable":
15648
15684
  case "guest_login_required":
15649
15685
  return "this needs a GitHub sign-in — run /login for 15,000 free credits a day";
15686
+ case "region_unavailable":
15687
+ return "free-pi is not available in your region";
15688
+ case "model_requires_credits":
15689
+ return "this model needs purchased credits in your region — run /buy-credits, or switch to the default model";
15650
15690
  default:
15651
15691
  return assertExhaustiveErrorCode(code);
15652
15692
  }
@@ -15773,6 +15813,7 @@ function createAdsExtension(opts) {
15773
15813
  pi.on("session_start", async (_event, ctx) => {
15774
15814
  stopped = false;
15775
15815
  inline.reset(ctx);
15816
+ opts.registerMeterRefresh?.(() => renderMeter(deps, ctx));
15776
15817
  await Promise.all([renderBanner(ctx, true), renderMeter(deps, ctx)]);
15777
15818
  if (bannerInterval)
15778
15819
  clearInterval(bannerInterval);
@@ -20198,6 +20239,8 @@ var IMAGE_GUIDELINES = [
20198
20239
  "Each image uses part of the user's daily allowance, so do not regenerate unless the user asks."
20199
20240
  ];
20200
20241
  var ERROR_TEXT = {
20242
+ guest_login_required: "This tool needs a GitHub sign-in; the guest trial cannot use it. Do not retry or work around it; tell the user to run /login (it also gives 15,000 free credits a day), and continue without this tool.",
20243
+ guest_unavailable: "The guest trial is not available right now. Do not retry; tell the user to run /login to sign in with GitHub, and continue without this tool.",
20201
20244
  daily_cap: "The user's free allowance for today is used up. Do not retry. Tell the user they can get credits at freepi.ai/extra-credits, and continue without this tool.",
20202
20245
  rolling_cap: "The user's short-term usage limit is reached. Do not retry; continue without this tool.",
20203
20246
  rate_limited: "Too many image requests right now. Do not call generate_image again in this turn unless the user asks.",
@@ -20325,6 +20368,8 @@ var VISION_GUIDELINES = [
20325
20368
  "The answer comes from another model and can be wrong. Verify layout claims with DOM measurements when you can."
20326
20369
  ];
20327
20370
  var ERROR_TEXT2 = {
20371
+ guest_login_required: "This tool needs a GitHub sign-in; the guest trial cannot use it. Do not retry or work around it; tell the user to run /login (it also gives 15,000 free credits a day), and continue without this tool.",
20372
+ guest_unavailable: "The guest trial is not available right now. Do not retry; tell the user to run /login to sign in with GitHub, and continue without this tool.",
20328
20373
  daily_cap: "The user's free allowance for today is used up. Do not retry. Tell the user they can get credits at freepi.ai/extra-credits, and continue without this tool.",
20329
20374
  rolling_cap: "The user's short-term usage limit is reached. Do not retry; continue without this tool.",
20330
20375
  rate_limited: "Another image question is already running or requests are too frequent. Do not call look_at_image again in this turn unless the user asks.",
@@ -20603,7 +20648,7 @@ var FREE_PI_DOCS = `# free-pi
20603
20648
 
20604
20649
  ## What free-pi is
20605
20650
  free-pi is a free, ad-supported distribution of the pi coding agent.
20606
- You run \`npx free-pi-cli\`, sign in with GitHub, and get free inference with
20651
+ You run \`npx free-pi-cli@latest\`, sign in with GitHub, and get free inference with
20607
20652
  exciting models. Ads shown in the terminal pay for it. You never handle a
20608
20653
  model API key. Config and the sign-in token live under \`~/.free-pi/agent\`,
20609
20654
  separate from any \`~/.pi/agent\` install.
@@ -20629,7 +20674,7 @@ first. Purchased credits cover use beyond it and do not expire.
20629
20674
  ## Model and restarting
20630
20675
  \`/model\` shows the current model and lets you switch it; the model can change
20631
20676
  during a session. Credits are free-pi's usage unit, not model tokens. To
20632
- restart, exit and run \`npx free-pi-cli\` again; \`--continue\` resumes the last
20677
+ restart, exit and run \`npx free-pi-cli@latest\` again; \`--continue\` resumes the last
20633
20678
  session.
20634
20679
 
20635
20680
  ## Commands
@@ -21174,7 +21219,7 @@ var AGENT_RULES = `## Working rules
21174
21219
  failed. Never an empty reply.
21175
21220
  - For questions about free-pi itself (model, credits, limits, commands, restart), call the
21176
21221
  ${DOCS_TOOL_NAME} tool instead of searching files. Credits are free-pi's usage unit, not model tokens.
21177
- To restart, run npx free-pi-cli.
21222
+ To restart, run npx free-pi-cli@latest.
21178
21223
  - Write files longer than about 300 lines in parts, not one huge write.
21179
21224
  - Never print whole env, config or credential files. Search for the specific name and never
21180
21225
  echo secret values.
@@ -21400,6 +21445,14 @@ function mergeAttribution(a, b) {
21400
21445
 
21401
21446
  // src/changelog.ts
21402
21447
  var CHANGELOG_HIGHLIGHTS = [
21448
+ {
21449
+ version: "0.6.6",
21450
+ highlights: [
21451
+ 'Sign-in is now one click in your browser: press "Connect this terminal" on freepi.ai after checking the confirmation code. The GitHub device code is still used over SSH or without a browser, or with --device-login.',
21452
+ "Shorter consent screen and a one-line intro. The version check now runs before consent, and the footer credits refresh right after /login.",
21453
+ 'If free-pi is not available in your region, sign-in says so instead of showing "HTTP 403". Guests using look_at_image or generate_image are told to run /login.'
21454
+ ]
21455
+ },
21403
21456
  {
21404
21457
  version: "0.6.4",
21405
21458
  highlights: [
@@ -21693,12 +21746,39 @@ async function postJsonRetry(baseUrl, path, body, attempts = 4, sleep = (ms) =>
21693
21746
  }
21694
21747
  }
21695
21748
  }
21749
+ var REGION_UNAVAILABLE_LINE = "free-pi is not available in your region.";
21750
+ function refusalMessage(status, body) {
21751
+ if (body?.code === "region_unavailable") {
21752
+ return typeof body.message === "string" && body.message.trim() ? body.message : REGION_UNAVAILABLE_LINE;
21753
+ }
21754
+ if (status >= 400 && status < 500 && typeof body?.message === "string" && body.message.trim()) {
21755
+ return body.message;
21756
+ }
21757
+ return;
21758
+ }
21759
+ async function readRefusal(res) {
21760
+ return await res.json().catch(() => {
21761
+ return;
21762
+ });
21763
+ }
21696
21764
  async function startDeviceFlow(baseUrl) {
21697
21765
  const res = await postJsonRetry(baseUrl, "/auth/github/device", {});
21698
- if (!res.ok)
21699
- throw new Error(`device flow start failed: HTTP ${res.status}`);
21766
+ if (!res.ok) {
21767
+ throw new Error(refusalMessage(res.status, await readRefusal(res)) ?? `device flow start failed: HTTP ${res.status}`);
21768
+ }
21700
21769
  return GithubDeviceResponseSchema.parse(await res.json());
21701
21770
  }
21771
+ async function startConnect(baseUrl, consentVersion, attribution = {}, client = {}) {
21772
+ const res = await postJsonRetry(baseUrl, "/auth/connect/start", {
21773
+ consent_version: consentVersion,
21774
+ client,
21775
+ ...attribution.signup_src ? { signup_src: attribution.signup_src } : {},
21776
+ ...attribution.referral_code ? { referral_code: attribution.referral_code } : {}
21777
+ });
21778
+ if (!res.ok)
21779
+ throw new Error(refusalMessage(res.status, await readRefusal(res)) ?? `connect start failed: HTTP ${res.status}`);
21780
+ return ConnectStartResponseSchema.parse(await res.json());
21781
+ }
21702
21782
  async function submitConsent(baseUrl, sessionId, consentVersion, attribution = {}) {
21703
21783
  const res = await postJsonRetry(baseUrl, "/auth/consent", {
21704
21784
  session_id: sessionId,
@@ -21706,8 +21786,9 @@ async function submitConsent(baseUrl, sessionId, consentVersion, attribution = {
21706
21786
  ...attribution.signup_src ? { signup_src: attribution.signup_src } : {},
21707
21787
  ...attribution.referral_code ? { referral_code: attribution.referral_code } : {}
21708
21788
  });
21709
- if (!res.ok)
21710
- throw new Error(`consent submission failed: HTTP ${res.status}`);
21789
+ if (!res.ok) {
21790
+ throw new Error(refusalMessage(res.status, await readRefusal(res)) ?? `consent submission failed: HTTP ${res.status}`);
21791
+ }
21711
21792
  }
21712
21793
 
21713
21794
  class GuestUnavailableError extends Error {
@@ -21724,9 +21805,9 @@ async function startGuestSession(baseUrl, consentVersion, attribution = {}) {
21724
21805
  ...attribution.referral_code ? { referral_code: attribution.referral_code } : {}
21725
21806
  });
21726
21807
  if (res.status === 403) {
21727
- const body = await res.json().catch(() => {
21728
- return;
21729
- });
21808
+ const body = await readRefusal(res);
21809
+ if (body?.code === "region_unavailable")
21810
+ throw new Error(refusalMessage(res.status, body));
21730
21811
  throw new GuestUnavailableError(body?.reason ?? "disabled");
21731
21812
  }
21732
21813
  if (!res.ok)
@@ -21759,6 +21840,12 @@ class LoginTimeoutError extends Error {
21759
21840
  super("github_login_timeout");
21760
21841
  }
21761
21842
  }
21843
+
21844
+ class LoginDeniedError extends Error {
21845
+ constructor() {
21846
+ super("login_denied");
21847
+ }
21848
+ }
21762
21849
  async function pollForToken(baseUrl, sessionId, interval, sleep, opts = {}) {
21763
21850
  let consecutiveNetworkFailures = 0;
21764
21851
  const now = opts.now ?? Date.now;
@@ -21772,10 +21859,10 @@ async function pollForToken(baseUrl, sessionId, interval, sleep, opts = {}) {
21772
21859
  if (opts.jwt)
21773
21860
  headers.authorization = `Bearer ${opts.jwt}`;
21774
21861
  const doFetch = opts.fetchImpl ?? fetch;
21775
- res = await doFetch(new URL("/auth/token", baseUrl), {
21862
+ res = await doFetch(new URL(opts.endpoint ?? "/auth/token", baseUrl), {
21776
21863
  method: "POST",
21777
21864
  headers,
21778
- body: JSON.stringify({ session_id: sessionId })
21865
+ body: JSON.stringify({ session_id: sessionId, ...opts.pollSecret ? { poll_secret: opts.pollSecret } : {} })
21779
21866
  });
21780
21867
  consecutiveNetworkFailures = 0;
21781
21868
  } catch (err) {
@@ -21786,16 +21873,17 @@ async function pollForToken(baseUrl, sessionId, interval, sleep, opts = {}) {
21786
21873
  await sleep(Math.max(interval, 5) * 1000);
21787
21874
  continue;
21788
21875
  }
21789
- if (res.status === 403) {
21790
- const body = await res.json().catch(() => {
21791
- return;
21792
- });
21793
- if (body?.code === "consent_required") {
21876
+ if (!res.ok) {
21877
+ const body = await readRefusal(res);
21878
+ if (res.status === 403 && body?.code === "consent_required") {
21794
21879
  throw new Error("consent_required: server did not record consent before polling");
21795
21880
  }
21881
+ if (res.status === 403 && body?.code === "denied")
21882
+ throw new LoginDeniedError;
21883
+ if (res.status === 410 && body?.code === "expired")
21884
+ throw new LoginTimeoutError;
21885
+ throw new Error(refusalMessage(res.status, body) ?? `token poll failed: HTTP ${res.status}`);
21796
21886
  }
21797
- if (!res.ok)
21798
- throw new Error(`token poll failed: HTTP ${res.status}`);
21799
21887
  const parsed = AuthTokenResponseSchema.parse(await res.json());
21800
21888
  if ("token" in parsed)
21801
21889
  return parsed.token;
@@ -21894,40 +21982,18 @@ function clipboardCommand(platform) {
21894
21982
 
21895
21983
  // src/consent.ts
21896
21984
  var CONSENT_TEXT = `
21897
- free-pi — before you start
21898
-
21899
- * Inference is free. Ads pay for it, and so does training.
21900
- * Ad cards render in the terminal UI. Some ads come from a third-party ad
21901
- provider (Gravity), which receives excerpts of your recent conversation
21902
- to pick a relevant ad.
21903
- * Gravity also keeps a session and visitor ID on your machine (in
21904
- ~/.free-pi) and receives usage events from free-pi to measure and
21905
- attribute ads.
21906
- * Your coding sessions (prompts, code, files, and model responses) are saved
21907
- as traces and used to train models. We may share or sell that data to
21908
- third parties. Once you accept, there is no opt-out, except where the law
21909
- gives you one.
21910
- * We do our best to remove personal information from traces before we store
21911
- them. We cannot promise it is perfectly clean.
21912
- * free-pi is in alpha and testing. There are no warranties or guarantees,
21913
- expressed or implied, and no guarantee of uptime.
21914
- * If GitHub email capture is enabled, we collect your GitHub email and may
21915
- use it to contact you about the service or product.
21916
- * free-pi-cli auto-updates itself when a new version is available. Disable
21917
- with FREEPI_NO_AUTO_UPDATE or {"autoUpdate":false} in ~/.free-pi/config.json.
21918
-
21919
- By accepting you agree to the Terms of Service (https://freepi.ai/terms)
21920
- and the Privacy Policy (https://freepi.ai/privacy).
21921
-
21922
- Decline and nothing is sent to the free-pi server.
21985
+ free-pi is free. In exchange, we show you ads and may train on your data.
21986
+ If you want to use free-pi, you accept that. If you don't, don't use it.
21987
+ Terms: https://freepi.ai/terms Privacy: https://freepi.ai/privacy
21923
21988
  `.trim();
21924
21989
  var CONSENT_VERSION = "4";
21925
- var NON_INTERACTIVE_CONSENT_TEXT = "free-pi first-run consent needs an interactive terminal (stdin is not a TTY). " + "Run `npx free-pi-cli` in a terminal once to review and accept; " + "after that, non-interactive runs work.";
21990
+ var NON_INTERACTIVE_CONSENT_TEXT = "free-pi first-run consent needs an interactive terminal (stdin is not a TTY). " + "Run `npx free-pi-cli@latest` in a terminal once to review and accept; " + "after that, non-interactive runs work.";
21926
21991
  var CONSENT_PROMPT = "Accept? [y/N] ";
21927
21992
  function parseConsentAnswer(answer) {
21928
21993
  return /^\s*y(es)?\s*$/i.test(answer);
21929
21994
  }
21930
- var REPROMPT_CONSENT_TEXT = CONSENT_TEXT.replace("Decline and nothing is sent to the free-pi server.", "Decline and free-pi exits; you will be asked again next time.");
21995
+ var REPROMPT_CONSENT_TEXT = `${CONSENT_TEXT}
21996
+ If you decline, free-pi exits and asks again next time.`;
21931
21997
 
21932
21998
  // src/refill-card.ts
21933
21999
  var LOGIN_BONUS_UNLOCK_LINE = "Sign in with GitHub to unlock a one-time comeback bonus.";
@@ -22151,6 +22217,76 @@ async function runDeviceLogin(deps, attribution = {}) {
22151
22217
  throw err;
22152
22218
  }
22153
22219
  }
22220
+ var CONNECT_BROWSER_FAILED_LINE = "Couldn't open a browser here, so using the GitHub device code instead.";
22221
+ var CONNECT_FALLBACK_LINE = "Couldn't start the browser sign-in, so using the GitHub device code instead.";
22222
+ var LOGIN_DENIED_LINE = "Sign-in was cancelled in the browser. Run /login to try again.";
22223
+ var DEVICE_LOGIN_HINT = "Prefer a GitHub device code? Run: npx free-pi-cli@latest --device-login";
22224
+ function browserLikelyAvailable(env = process.env, platform = process.platform) {
22225
+ if (env.SSH_CONNECTION || env.SSH_CLIENT || env.SSH_TTY)
22226
+ return false;
22227
+ if (platform === "linux" && !env.DISPLAY && !env.WAYLAND_DISPLAY)
22228
+ return false;
22229
+ return true;
22230
+ }
22231
+ function formatConnectPrompt(connect) {
22232
+ return [
22233
+ "",
22234
+ LOGIN_REWARD_LINE,
22235
+ "",
22236
+ " Opening your browser to connect this terminal. If it doesn't open, go to:",
22237
+ ` ${connect.connect_url}`,
22238
+ "",
22239
+ ` Confirmation code: ${connect.confirm_code}`,
22240
+ " (the page must show the same code)",
22241
+ "",
22242
+ DEVICE_LOGIN_HINT,
22243
+ ""
22244
+ ].join(`
22245
+ `);
22246
+ }
22247
+ async function runConnectLogin(deps, attribution = {}) {
22248
+ const connect = await startConnect(deps.baseUrl, CONSENT_VERSION, attribution, {
22249
+ os: process.platform,
22250
+ cli_version: CLI_VERSION
22251
+ });
22252
+ deps.log(formatConnectPrompt(connect));
22253
+ let browserOpened = false;
22254
+ try {
22255
+ browserOpened = await deps.openBrowser(connect.connect_url) !== false;
22256
+ } catch {
22257
+ browserOpened = false;
22258
+ }
22259
+ if (!browserOpened) {
22260
+ deps.log(CONNECT_BROWSER_FAILED_LINE);
22261
+ return;
22262
+ }
22263
+ deps.log(LOGIN_WAITING_LINE.replace("GitHub", "browser sign-in"));
22264
+ const deadline = (deps.now ?? Date.now)() + connect.expires_in * 1000;
22265
+ return pollForToken(deps.baseUrl, connect.session_id, connect.interval, deps.sleep, {
22266
+ deadline,
22267
+ now: deps.now,
22268
+ jwt: deps.jwt,
22269
+ fetchImpl: deps.fetchImpl,
22270
+ endpoint: "/auth/connect/poll",
22271
+ pollSecret: connect.poll_secret
22272
+ });
22273
+ }
22274
+ async function runLoginAuto(deps, attribution = {}, opts = {}) {
22275
+ const env = opts.env ?? process.env;
22276
+ const forced = opts.forceDevice || env.FREEPI_DEVICE_LOGIN === "1" || env.FREEPI_DEVICE_LOGIN === "true";
22277
+ if (!forced && browserLikelyAvailable(env, opts.platform)) {
22278
+ try {
22279
+ const token = await runConnectLogin(deps, attribution);
22280
+ if (token)
22281
+ return token;
22282
+ } catch (err) {
22283
+ if (err instanceof LoginTimeoutError || err instanceof LoginDeniedError)
22284
+ throw err;
22285
+ deps.log(CONNECT_FALLBACK_LINE);
22286
+ }
22287
+ }
22288
+ return runDeviceLogin(deps, attribution);
22289
+ }
22154
22290
 
22155
22291
  // src/allowance-counter.ts
22156
22292
  var cached2;
@@ -22301,7 +22437,7 @@ async function runLogin(opts, ctx) {
22301
22437
  try {
22302
22438
  const attribution = opts.agentDir ? await loadAttribution(getAttributionPath(opts.agentDir)) : {};
22303
22439
  const variant = resolveHandoffVariant(opts);
22304
- const token = await runDeviceLogin({
22440
+ const token = await runLoginAuto({
22305
22441
  baseUrl: opts.baseUrl,
22306
22442
  openBrowser: opts.openBrowserImpl ?? openBrowser,
22307
22443
  sleep: (ms) => new Promise((r) => setTimeout(r, ms)),
@@ -22314,12 +22450,16 @@ async function runLogin(opts, ctx) {
22314
22450
  if (opts.credentialsPath)
22315
22451
  await saveJwt(opts.credentialsPath, token);
22316
22452
  opts.onLogin?.(token);
22317
- 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");
22453
+ ctx.ui.notify(isHandoffTreatment(variant) ? SIGNED_IN_CONTINUE_LINE : "Signed in. You now have 15,000 free credits a day, plus a one-time comeback bonus.", "info");
22318
22454
  } catch (err) {
22319
22455
  if (err instanceof LoginTimeoutError) {
22320
22456
  ctx.ui.notify(LOGIN_TIMEOUT_LINE, "warning");
22321
22457
  return;
22322
22458
  }
22459
+ if (err instanceof LoginDeniedError) {
22460
+ ctx.ui.notify(LOGIN_DENIED_LINE, "warning");
22461
+ return;
22462
+ }
22323
22463
  const reason = err instanceof Error && err.message ? ` (${err.message})` : "";
22324
22464
  ctx.ui.notify(`Couldn't finish GitHub sign-in${reason}. Run /login to try again.`, "error");
22325
22465
  }
@@ -24307,7 +24447,7 @@ var RESUME_COMMAND_RE = /(To resume this session:(?:\u001b\[[0-9;]*m|\s)*)pi(?=\
24307
24447
  function rewriteResumeLine(chunk) {
24308
24448
  if (!chunk.includes(RESUME_LABEL))
24309
24449
  return chunk;
24310
- return chunk.replace(RESUME_COMMAND_RE, "$1npx free-pi-cli");
24450
+ return chunk.replace(RESUME_COMMAND_RE, "$1npx free-pi-cli@latest");
24311
24451
  }
24312
24452
  function patchResumeLine(stream) {
24313
24453
  const original = stream.write.bind(stream);
@@ -24367,12 +24507,17 @@ function projectTrustForCwd(cwd, launch, agentDir, hasTrustRequiringResources =
24367
24507
  function createSessionAuth(initial) {
24368
24508
  let jwt2 = initial.jwt;
24369
24509
  let guest = initial.guest;
24510
+ let onSignedIn;
24370
24511
  return {
24371
24512
  getJwt: () => jwt2,
24372
24513
  isGuest: () => guest,
24373
24514
  becomeSignedIn(token) {
24374
24515
  jwt2 = token;
24375
24516
  guest = false;
24517
+ onSignedIn?.();
24518
+ },
24519
+ setOnSignedIn(fn) {
24520
+ onSignedIn = fn;
24376
24521
  }
24377
24522
  };
24378
24523
  }
@@ -24450,6 +24595,7 @@ function buildRuntimeOptions(opts, sessionId) {
24450
24595
  };
24451
24596
  let advertiserUrl;
24452
24597
  const adsExtension = createAdsExtension({
24598
+ registerMeterRefresh: (refresh) => auth.setOnSignedIn(() => void refresh().catch(() => {})),
24453
24599
  baseUrl: opts.baseUrl,
24454
24600
  getToken: () => auth.getJwt(),
24455
24601
  sessionId,
@@ -25008,18 +25154,7 @@ import { existsSync as existsSync6, readdirSync as readdirSync3, realpathSync as
25008
25154
  import { homedir as homedir6 } from "node:os";
25009
25155
  import { join as join12 } from "node:path";
25010
25156
  var INTRO_ENV_SKIP = "FREEPI_NO_INTRO";
25011
- var INTRO_TEXT = `
25012
- Welcome to free-pi — a free coding agent in your terminal.
25013
-
25014
- • It's the pi coding agent, free to use. Ads in the terminal pay for the
25015
- inference — ad content never reaches the model.
25016
- • Tell it what to build, fix, or explain, like any coding assistant.
25017
- • You can run free-pi in more than one terminal. If you close the terminal,
25018
- relaunch with "npx free-pi-cli" — or resume where you left off with
25019
- "npx free-pi-cli --session <id>" (the id prints when you exit).
25020
-
25021
- Set ${INTRO_ENV_SKIP}=1 to skip this next time.
25022
- `.trim();
25157
+ var INTRO_TEXT = "Tell free-pi what to build, fix, or explain. Resume later with: npx free-pi-cli@latest --continue";
25023
25158
  function introSkipped(env = process.env) {
25024
25159
  const v = env[INTRO_ENV_SKIP];
25025
25160
  return v === "1" || v === "true";
@@ -25052,7 +25187,7 @@ Try one of these on this project — just type it:
25052
25187
  `.trim();
25053
25188
  var FIRST_TASK_EMPTY_TEXT = `
25054
25189
  This folder doesn't look like a project. free-pi works best on code you
25055
- already have: quit, "cd" into a project, and run "npx free-pi-cli" there.
25190
+ already have: quit, "cd" into a project, and run "npx free-pi-cli@latest" there.
25056
25191
  Or just type: Create a small runnable hello-world app here and run it
25057
25192
  `.trim();
25058
25193
  function showFirstTaskHint(log, cwd = process.cwd(), home = homedir6()) {
@@ -25106,20 +25241,23 @@ async function loginFlow(deps, argv = []) {
25106
25241
  throw new Error(resolved.error);
25107
25242
  }
25108
25243
  try {
25109
- const token = await runDeviceLogin({
25244
+ const token = await runLoginAuto({
25110
25245
  baseUrl: deps.baseUrl,
25111
25246
  openBrowser: deps.openBrowser,
25112
25247
  sleep: deps.sleep,
25113
25248
  log: deps.log,
25114
25249
  jwt: await loadJwt(deps.credentialsPath),
25115
25250
  credentialsPath: deps.credentialsPath
25116
- }, resolved.attribution ?? {});
25251
+ }, resolved.attribution ?? {}, { forceDevice: argv.includes("--device-login"), env: deps.env, platform: deps.platform });
25117
25252
  await saveJwt(deps.credentialsPath, token);
25118
25253
  return token;
25119
25254
  } catch (err) {
25120
25255
  if (err instanceof LoginTimeoutError) {
25121
25256
  deps.log(LOGIN_TIMEOUT_LINE);
25122
25257
  }
25258
+ if (err instanceof LoginDeniedError) {
25259
+ deps.log(LOGIN_DENIED_LINE);
25260
+ }
25123
25261
  throw err;
25124
25262
  }
25125
25263
  }
@@ -25153,6 +25291,21 @@ function reportResumeExposure(deps, jwt2, event, experiment = RESUME_ON_LAUNCH_E
25153
25291
  async function defaultFindLatestSession(cwd, agentDir) {
25154
25292
  return findLatestSession(cwd, getSessionDir(cwd, agentDir));
25155
25293
  }
25294
+ async function handleVersionAction(deps, versionCheck) {
25295
+ const outcome = await deps.maybeSelfUpdate(versionCheck.action, versionCheck.latest);
25296
+ if (outcome === "updated-exit")
25297
+ return { code: 0 };
25298
+ if (outcome === "blocked") {
25299
+ const relaunched = await deps.relaunchLatest?.(versionCheck.latest);
25300
+ if (relaunched !== undefined)
25301
+ return { code: relaunched };
25302
+ deps.log(`free-pi-cli needs an update. From a trusted folder (for example: cd ~), run:
25303
+ npx --yes free-pi-cli@latest
25304
+ or: npm i -g free-pi-cli@latest`);
25305
+ return { code: 1 };
25306
+ }
25307
+ return outcome === "notice" ? { updateLatest: versionCheck.latest } : {};
25308
+ }
25156
25309
  async function run(deps, argv = process.argv) {
25157
25310
  const resumeFlags = parseResumeFlags(argv);
25158
25311
  if (resumeFlags.sessionError) {
@@ -25172,6 +25325,17 @@ async function run(deps, argv = process.argv) {
25172
25325
  ${FINISH_SIGNIN_LINE}
25173
25326
  `);
25174
25327
  let jwt2 = await loadJwt(deps.credentialsPath);
25328
+ const hadJwt = Boolean(jwt2);
25329
+ let versionCheck = await deps.checkClientVersion(deps.baseUrl, CLI_VERSION, jwt2);
25330
+ let updateLatest;
25331
+ let versionHandled = false;
25332
+ if (versionCheck.action === "block") {
25333
+ const handled = await handleVersionAction(deps, versionCheck);
25334
+ if (handled.code !== undefined)
25335
+ return handled.code;
25336
+ updateLatest = handled.updateLatest;
25337
+ versionHandled = true;
25338
+ }
25175
25339
  let guestHandoffVariant;
25176
25340
  let showFirstTask = false;
25177
25341
  let consentCurrent = false;
@@ -25202,7 +25366,7 @@ ${FINISH_SIGNIN_LINE}
25202
25366
  if (!jwt2) {
25203
25367
  const accepted = await deps.promptConsent();
25204
25368
  if (!accepted) {
25205
- deps.log("Declined. Exiting without contacting the free-pi server.");
25369
+ deps.log("Declined. Exiting.");
25206
25370
  return 1;
25207
25371
  }
25208
25372
  showIntro(deps.log);
@@ -25233,24 +25397,13 @@ Guest trial is not available right now.
25233
25397
  }
25234
25398
  consentCurrent = true;
25235
25399
  }
25236
- const versionCheck = await deps.checkClientVersion(deps.baseUrl, CLI_VERSION, jwt2);
25237
- let updateLatest;
25238
- if (versionCheck.action !== "ok") {
25239
- const outcome = await deps.maybeSelfUpdate(versionCheck.action, versionCheck.latest);
25240
- if (outcome === "updated-exit")
25241
- return 0;
25242
- if (outcome === "blocked") {
25243
- const relaunched = await deps.relaunchLatest?.(versionCheck.latest);
25244
- if (relaunched !== undefined)
25245
- return relaunched;
25246
- deps.log(`free-pi-cli needs an update. From a trusted folder (for example: cd ~), run:
25247
- npx --yes free-pi-cli@latest
25248
- or: npm i -g free-pi-cli@latest`);
25249
- return 1;
25250
- }
25251
- if (outcome === "notice") {
25252
- updateLatest = versionCheck.latest;
25253
- }
25400
+ if (!hadJwt)
25401
+ versionCheck = await deps.checkClientVersion(deps.baseUrl, CLI_VERSION, jwt2);
25402
+ if (!versionHandled && versionCheck.action !== "ok") {
25403
+ const handled = await handleVersionAction(deps, versionCheck);
25404
+ if (handled.code !== undefined)
25405
+ return handled.code;
25406
+ updateLatest = handled.updateLatest;
25254
25407
  }
25255
25408
  const whatsNew = maybeWhatsNew(CLI_VERSION, deps.agentDir);
25256
25409
  if (whatsNew)
@@ -25335,8 +25488,8 @@ ${exitCard}`);
25335
25488
  reportRefillCardShown({ baseUrl: deps.baseUrl, jwt: jwt2 });
25336
25489
  }
25337
25490
  deps.log(finalSessionId ? `
25338
- free-pi session ended. Start it again anytime: npx free-pi-cli --session ${finalSessionId}` : `
25339
- free-pi session ended. Start it again anytime: npx free-pi-cli`);
25491
+ free-pi session ended. Start it again anytime: npx free-pi-cli@latest --session ${finalSessionId}` : `
25492
+ free-pi session ended. Start it again anytime: npx free-pi-cli@latest`);
25340
25493
  deps.log(`(or install once for a shorter command: npm i -g free-pi-cli → free-pi-cli)
25341
25494
  `);
25342
25495
  return 0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "free-pi-cli",
3
- "version": "0.6.4",
3
+ "version": "0.6.6",
4
4
  "description": "Free, ad-supported distribution of the pi coding agent. Inference paid for by ads.",
5
5
  "license": "MIT",
6
6
  "type": "module",