free-pi-cli 0.6.4 → 0.6.7

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-tm302552.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.7",
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
  }
@@ -15666,6 +15706,7 @@ function buyMore(buyUrl) {
15666
15706
  }
15667
15707
  var CONFIRM_TURNS = 3;
15668
15708
  var lastCreditUsd;
15709
+ var lastToken;
15669
15710
  var confirm;
15670
15711
  function creditAddedMessage(addedUsd) {
15671
15712
  return `Payment received — ${usdToCredits(addedUsd).toLocaleString("en-US")} credits added to your balance.`;
@@ -15679,6 +15720,12 @@ async function renderMeter(deps, ctx) {
15679
15720
  }
15680
15721
  const theme = ctx.ui.theme;
15681
15722
  const credit = typeof me.credit_usd === "number" ? me.credit_usd : undefined;
15723
+ const token = await deps.getToken();
15724
+ if (token !== lastToken) {
15725
+ lastToken = token;
15726
+ lastCreditUsd = undefined;
15727
+ confirm = undefined;
15728
+ }
15682
15729
  const added = credit !== undefined && lastCreditUsd !== undefined ? credit - lastCreditUsd : 0;
15683
15730
  lastCreditUsd = credit;
15684
15731
  if (added > 0)
@@ -15773,6 +15820,7 @@ function createAdsExtension(opts) {
15773
15820
  pi.on("session_start", async (_event, ctx) => {
15774
15821
  stopped = false;
15775
15822
  inline.reset(ctx);
15823
+ opts.registerMeterRefresh?.(() => renderMeter(deps, ctx));
15776
15824
  await Promise.all([renderBanner(ctx, true), renderMeter(deps, ctx)]);
15777
15825
  if (bannerInterval)
15778
15826
  clearInterval(bannerInterval);
@@ -20198,6 +20246,8 @@ var IMAGE_GUIDELINES = [
20198
20246
  "Each image uses part of the user's daily allowance, so do not regenerate unless the user asks."
20199
20247
  ];
20200
20248
  var ERROR_TEXT = {
20249
+ 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.",
20250
+ 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
20251
  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
20252
  rolling_cap: "The user's short-term usage limit is reached. Do not retry; continue without this tool.",
20203
20253
  rate_limited: "Too many image requests right now. Do not call generate_image again in this turn unless the user asks.",
@@ -20325,6 +20375,8 @@ var VISION_GUIDELINES = [
20325
20375
  "The answer comes from another model and can be wrong. Verify layout claims with DOM measurements when you can."
20326
20376
  ];
20327
20377
  var ERROR_TEXT2 = {
20378
+ 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.",
20379
+ 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
20380
  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
20381
  rolling_cap: "The user's short-term usage limit is reached. Do not retry; continue without this tool.",
20330
20382
  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 +20655,7 @@ var FREE_PI_DOCS = `# free-pi
20603
20655
 
20604
20656
  ## What free-pi is
20605
20657
  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
20658
+ You run \`npx free-pi-cli@latest\`, sign in with GitHub, and get free inference with
20607
20659
  exciting models. Ads shown in the terminal pay for it. You never handle a
20608
20660
  model API key. Config and the sign-in token live under \`~/.free-pi/agent\`,
20609
20661
  separate from any \`~/.pi/agent\` install.
@@ -20629,7 +20681,7 @@ first. Purchased credits cover use beyond it and do not expire.
20629
20681
  ## Model and restarting
20630
20682
  \`/model\` shows the current model and lets you switch it; the model can change
20631
20683
  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
20684
+ restart, exit and run \`npx free-pi-cli@latest\` again; \`--continue\` resumes the last
20633
20685
  session.
20634
20686
 
20635
20687
  ## Commands
@@ -21174,7 +21226,7 @@ var AGENT_RULES = `## Working rules
21174
21226
  failed. Never an empty reply.
21175
21227
  - For questions about free-pi itself (model, credits, limits, commands, restart), call the
21176
21228
  ${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.
21229
+ To restart, run npx free-pi-cli@latest.
21178
21230
  - Write files longer than about 300 lines in parts, not one huge write.
21179
21231
  - Never print whole env, config or credential files. Search for the specific name and never
21180
21232
  echo secret values.
@@ -21400,6 +21452,22 @@ function mergeAttribution(a, b) {
21400
21452
 
21401
21453
  // src/changelog.ts
21402
21454
  var CHANGELOG_HIGHLIGHTS = [
21455
+ {
21456
+ version: "0.6.7",
21457
+ highlights: [
21458
+ "/logout now signs you out of free-pi: it removes your saved sign-in and exits. Restart free-pi to sign in again. Guests are told they are not signed in.",
21459
+ "The confirmation code shown during browser sign-in is now bold and bright, so it is easy to compare with the one on freepi.ai.",
21460
+ 'Fixed a wrong "Payment received" line that could appear right after signing in.'
21461
+ ]
21462
+ },
21463
+ {
21464
+ version: "0.6.6",
21465
+ highlights: [
21466
+ '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.',
21467
+ "Shorter consent screen and a one-line intro. The version check now runs before consent, and the footer credits refresh right after /login.",
21468
+ '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.'
21469
+ ]
21470
+ },
21403
21471
  {
21404
21472
  version: "0.6.4",
21405
21473
  highlights: [
@@ -21668,6 +21736,9 @@ function highlightsFor(version3) {
21668
21736
  return CHANGELOG_HIGHLIGHTS.find((e) => e.version === version3)?.highlights;
21669
21737
  }
21670
21738
 
21739
+ // src/commands.ts
21740
+ import { access } from "node:fs/promises";
21741
+
21671
21742
  // src/auth-flow.ts
21672
21743
  class UnauthorizedError extends Error {
21673
21744
  constructor() {
@@ -21693,12 +21764,39 @@ async function postJsonRetry(baseUrl, path, body, attempts = 4, sleep = (ms) =>
21693
21764
  }
21694
21765
  }
21695
21766
  }
21767
+ var REGION_UNAVAILABLE_LINE = "free-pi is not available in your region.";
21768
+ function refusalMessage(status, body) {
21769
+ if (body?.code === "region_unavailable") {
21770
+ return typeof body.message === "string" && body.message.trim() ? body.message : REGION_UNAVAILABLE_LINE;
21771
+ }
21772
+ if (status >= 400 && status < 500 && typeof body?.message === "string" && body.message.trim()) {
21773
+ return body.message;
21774
+ }
21775
+ return;
21776
+ }
21777
+ async function readRefusal(res) {
21778
+ return await res.json().catch(() => {
21779
+ return;
21780
+ });
21781
+ }
21696
21782
  async function startDeviceFlow(baseUrl) {
21697
21783
  const res = await postJsonRetry(baseUrl, "/auth/github/device", {});
21698
- if (!res.ok)
21699
- throw new Error(`device flow start failed: HTTP ${res.status}`);
21784
+ if (!res.ok) {
21785
+ throw new Error(refusalMessage(res.status, await readRefusal(res)) ?? `device flow start failed: HTTP ${res.status}`);
21786
+ }
21700
21787
  return GithubDeviceResponseSchema.parse(await res.json());
21701
21788
  }
21789
+ async function startConnect(baseUrl, consentVersion, attribution = {}, client = {}) {
21790
+ const res = await postJsonRetry(baseUrl, "/auth/connect/start", {
21791
+ consent_version: consentVersion,
21792
+ client,
21793
+ ...attribution.signup_src ? { signup_src: attribution.signup_src } : {},
21794
+ ...attribution.referral_code ? { referral_code: attribution.referral_code } : {}
21795
+ });
21796
+ if (!res.ok)
21797
+ throw new Error(refusalMessage(res.status, await readRefusal(res)) ?? `connect start failed: HTTP ${res.status}`);
21798
+ return ConnectStartResponseSchema.parse(await res.json());
21799
+ }
21702
21800
  async function submitConsent(baseUrl, sessionId, consentVersion, attribution = {}) {
21703
21801
  const res = await postJsonRetry(baseUrl, "/auth/consent", {
21704
21802
  session_id: sessionId,
@@ -21706,8 +21804,9 @@ async function submitConsent(baseUrl, sessionId, consentVersion, attribution = {
21706
21804
  ...attribution.signup_src ? { signup_src: attribution.signup_src } : {},
21707
21805
  ...attribution.referral_code ? { referral_code: attribution.referral_code } : {}
21708
21806
  });
21709
- if (!res.ok)
21710
- throw new Error(`consent submission failed: HTTP ${res.status}`);
21807
+ if (!res.ok) {
21808
+ throw new Error(refusalMessage(res.status, await readRefusal(res)) ?? `consent submission failed: HTTP ${res.status}`);
21809
+ }
21711
21810
  }
21712
21811
 
21713
21812
  class GuestUnavailableError extends Error {
@@ -21724,9 +21823,9 @@ async function startGuestSession(baseUrl, consentVersion, attribution = {}) {
21724
21823
  ...attribution.referral_code ? { referral_code: attribution.referral_code } : {}
21725
21824
  });
21726
21825
  if (res.status === 403) {
21727
- const body = await res.json().catch(() => {
21728
- return;
21729
- });
21826
+ const body = await readRefusal(res);
21827
+ if (body?.code === "region_unavailable")
21828
+ throw new Error(refusalMessage(res.status, body));
21730
21829
  throw new GuestUnavailableError(body?.reason ?? "disabled");
21731
21830
  }
21732
21831
  if (!res.ok)
@@ -21759,6 +21858,12 @@ class LoginTimeoutError extends Error {
21759
21858
  super("github_login_timeout");
21760
21859
  }
21761
21860
  }
21861
+
21862
+ class LoginDeniedError extends Error {
21863
+ constructor() {
21864
+ super("login_denied");
21865
+ }
21866
+ }
21762
21867
  async function pollForToken(baseUrl, sessionId, interval, sleep, opts = {}) {
21763
21868
  let consecutiveNetworkFailures = 0;
21764
21869
  const now = opts.now ?? Date.now;
@@ -21772,10 +21877,10 @@ async function pollForToken(baseUrl, sessionId, interval, sleep, opts = {}) {
21772
21877
  if (opts.jwt)
21773
21878
  headers.authorization = `Bearer ${opts.jwt}`;
21774
21879
  const doFetch = opts.fetchImpl ?? fetch;
21775
- res = await doFetch(new URL("/auth/token", baseUrl), {
21880
+ res = await doFetch(new URL(opts.endpoint ?? "/auth/token", baseUrl), {
21776
21881
  method: "POST",
21777
21882
  headers,
21778
- body: JSON.stringify({ session_id: sessionId })
21883
+ body: JSON.stringify({ session_id: sessionId, ...opts.pollSecret ? { poll_secret: opts.pollSecret } : {} })
21779
21884
  });
21780
21885
  consecutiveNetworkFailures = 0;
21781
21886
  } catch (err) {
@@ -21786,16 +21891,17 @@ async function pollForToken(baseUrl, sessionId, interval, sleep, opts = {}) {
21786
21891
  await sleep(Math.max(interval, 5) * 1000);
21787
21892
  continue;
21788
21893
  }
21789
- if (res.status === 403) {
21790
- const body = await res.json().catch(() => {
21791
- return;
21792
- });
21793
- if (body?.code === "consent_required") {
21894
+ if (!res.ok) {
21895
+ const body = await readRefusal(res);
21896
+ if (res.status === 403 && body?.code === "consent_required") {
21794
21897
  throw new Error("consent_required: server did not record consent before polling");
21795
21898
  }
21899
+ if (res.status === 403 && body?.code === "denied")
21900
+ throw new LoginDeniedError;
21901
+ if (res.status === 410 && body?.code === "expired")
21902
+ throw new LoginTimeoutError;
21903
+ throw new Error(refusalMessage(res.status, body) ?? `token poll failed: HTTP ${res.status}`);
21796
21904
  }
21797
- if (!res.ok)
21798
- throw new Error(`token poll failed: HTTP ${res.status}`);
21799
21905
  const parsed = AuthTokenResponseSchema.parse(await res.json());
21800
21906
  if ("token" in parsed)
21801
21907
  return parsed.token;
@@ -21894,40 +22000,18 @@ function clipboardCommand(platform) {
21894
22000
 
21895
22001
  // src/consent.ts
21896
22002
  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.
22003
+ free-pi is free. In exchange, we show you ads and may train on your data.
22004
+ If you want to use free-pi, you accept that. If you don't, don't use it.
22005
+ Terms: https://freepi.ai/terms Privacy: https://freepi.ai/privacy
21923
22006
  `.trim();
21924
22007
  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.";
22008
+ 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
22009
  var CONSENT_PROMPT = "Accept? [y/N] ";
21927
22010
  function parseConsentAnswer(answer) {
21928
22011
  return /^\s*y(es)?\s*$/i.test(answer);
21929
22012
  }
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.");
22013
+ var REPROMPT_CONSENT_TEXT = `${CONSENT_TEXT}
22014
+ If you decline, free-pi exits and asks again next time.`;
21931
22015
 
21932
22016
  // src/refill-card.ts
21933
22017
  var LOGIN_BONUS_UNLOCK_LINE = "Sign in with GitHub to unlock a one-time comeback bonus.";
@@ -22151,6 +22235,79 @@ async function runDeviceLogin(deps, attribution = {}) {
22151
22235
  throw err;
22152
22236
  }
22153
22237
  }
22238
+ var CONNECT_BROWSER_FAILED_LINE = "Couldn't open a browser here, so using the GitHub device code instead.";
22239
+ var CONNECT_FALLBACK_LINE = "Couldn't start the browser sign-in, so using the GitHub device code instead.";
22240
+ var LOGIN_DENIED_LINE = "Sign-in was cancelled in the browser. Run /login to try again.";
22241
+ var DEVICE_LOGIN_HINT = "Prefer a GitHub device code? Run: npx free-pi-cli@latest --device-login";
22242
+ function browserLikelyAvailable(env = process.env, platform = process.platform) {
22243
+ if (env.SSH_CONNECTION || env.SSH_CLIENT || env.SSH_TTY)
22244
+ return false;
22245
+ if (platform === "linux" && !env.DISPLAY && !env.WAYLAND_DISPLAY)
22246
+ return false;
22247
+ return true;
22248
+ }
22249
+ function emphasize(text, color = !process.env.NO_COLOR && process.stdout.isTTY === true) {
22250
+ return color ? `\x1B[1;97m${text}\x1B[0m` : text;
22251
+ }
22252
+ function formatConnectPrompt(connect, color) {
22253
+ return [
22254
+ "",
22255
+ LOGIN_REWARD_LINE,
22256
+ "",
22257
+ " Opening your browser to connect this terminal. If it doesn't open, go to:",
22258
+ ` ${connect.connect_url}`,
22259
+ "",
22260
+ ` Confirmation code: ${emphasize(connect.confirm_code, color)}`,
22261
+ " (the page must show the same code)",
22262
+ "",
22263
+ DEVICE_LOGIN_HINT,
22264
+ ""
22265
+ ].join(`
22266
+ `);
22267
+ }
22268
+ async function runConnectLogin(deps, attribution = {}) {
22269
+ const connect = await startConnect(deps.baseUrl, CONSENT_VERSION, attribution, {
22270
+ os: process.platform,
22271
+ cli_version: CLI_VERSION
22272
+ });
22273
+ deps.log(formatConnectPrompt(connect));
22274
+ let browserOpened = false;
22275
+ try {
22276
+ browserOpened = await deps.openBrowser(connect.connect_url) !== false;
22277
+ } catch {
22278
+ browserOpened = false;
22279
+ }
22280
+ if (!browserOpened) {
22281
+ deps.log(CONNECT_BROWSER_FAILED_LINE);
22282
+ return;
22283
+ }
22284
+ deps.log(LOGIN_WAITING_LINE.replace("GitHub", "browser sign-in"));
22285
+ const deadline = (deps.now ?? Date.now)() + connect.expires_in * 1000;
22286
+ return pollForToken(deps.baseUrl, connect.session_id, connect.interval, deps.sleep, {
22287
+ deadline,
22288
+ now: deps.now,
22289
+ jwt: deps.jwt,
22290
+ fetchImpl: deps.fetchImpl,
22291
+ endpoint: "/auth/connect/poll",
22292
+ pollSecret: connect.poll_secret
22293
+ });
22294
+ }
22295
+ async function runLoginAuto(deps, attribution = {}, opts = {}) {
22296
+ const env = opts.env ?? process.env;
22297
+ const forced = opts.forceDevice || env.FREEPI_DEVICE_LOGIN === "1" || env.FREEPI_DEVICE_LOGIN === "true";
22298
+ if (!forced && browserLikelyAvailable(env, opts.platform)) {
22299
+ try {
22300
+ const token = await runConnectLogin(deps, attribution);
22301
+ if (token)
22302
+ return token;
22303
+ } catch (err) {
22304
+ if (err instanceof LoginTimeoutError || err instanceof LoginDeniedError)
22305
+ throw err;
22306
+ deps.log(CONNECT_FALLBACK_LINE);
22307
+ }
22308
+ }
22309
+ return runDeviceLogin(deps, attribution);
22310
+ }
22154
22311
 
22155
22312
  // src/allowance-counter.ts
22156
22313
  var cached2;
@@ -22301,7 +22458,7 @@ async function runLogin(opts, ctx) {
22301
22458
  try {
22302
22459
  const attribution = opts.agentDir ? await loadAttribution(getAttributionPath(opts.agentDir)) : {};
22303
22460
  const variant = resolveHandoffVariant(opts);
22304
- const token = await runDeviceLogin({
22461
+ const token = await runLoginAuto({
22305
22462
  baseUrl: opts.baseUrl,
22306
22463
  openBrowser: opts.openBrowserImpl ?? openBrowser,
22307
22464
  sleep: (ms) => new Promise((r) => setTimeout(r, ms)),
@@ -22314,12 +22471,16 @@ async function runLogin(opts, ctx) {
22314
22471
  if (opts.credentialsPath)
22315
22472
  await saveJwt(opts.credentialsPath, token);
22316
22473
  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");
22474
+ 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
22475
  } catch (err) {
22319
22476
  if (err instanceof LoginTimeoutError) {
22320
22477
  ctx.ui.notify(LOGIN_TIMEOUT_LINE, "warning");
22321
22478
  return;
22322
22479
  }
22480
+ if (err instanceof LoginDeniedError) {
22481
+ ctx.ui.notify(LOGIN_DENIED_LINE, "warning");
22482
+ return;
22483
+ }
22323
22484
  const reason = err instanceof Error && err.message ? ` (${err.message})` : "";
22324
22485
  ctx.ui.notify(`Couldn't finish GitHub sign-in${reason}. Run /login to try again.`, "error");
22325
22486
  }
@@ -22340,6 +22501,33 @@ function installLoginHandler(mode, opts) {
22340
22501
  return runLogin(opts, { ui: { notify } });
22341
22502
  };
22342
22503
  }
22504
+ var LOGOUT_DONE_LINE = "Signed out of free-pi. Restart free-pi to sign in again.";
22505
+ var LOGOUT_NOT_SIGNED_IN_LINE = "You're not signed in to free-pi.";
22506
+ function installLogoutHandler(mode, opts) {
22507
+ const printAtExit = opts.printAtExit ?? ((line) => void process.once("exit", () => process.stdout.write(`${line}
22508
+ `)));
22509
+ const m = mode;
22510
+ mode.showOAuthSelector = async () => {
22511
+ if (!opts.credentialsPath || opts.isGuest?.()) {
22512
+ m.showStatus(LOGOUT_NOT_SIGNED_IN_LINE);
22513
+ return;
22514
+ }
22515
+ try {
22516
+ try {
22517
+ await runLogout({ credentialsPath: opts.credentialsPath, loadJwt, clearJwt, log: () => {} });
22518
+ } catch (err) {
22519
+ if (await access(opts.credentialsPath).then(() => true, () => false)) {
22520
+ const reason = err instanceof Error && err.message ? err.message : "unknown error";
22521
+ m.showError(`Couldn't sign out: ${reason}. Try: npx free-pi-cli@latest logout`);
22522
+ return;
22523
+ }
22524
+ }
22525
+ m.showStatus(LOGOUT_DONE_LINE);
22526
+ printAtExit(LOGOUT_DONE_LINE);
22527
+ await m.shutdown();
22528
+ } catch {}
22529
+ };
22530
+ }
22343
22531
  async function showUsage(opts, ctx) {
22344
22532
  const r = await fetchUsageText(opts);
22345
22533
  ctx.ui.notify(r.text, r.ok ? "info" : "error");
@@ -24307,7 +24495,7 @@ var RESUME_COMMAND_RE = /(To resume this session:(?:\u001b\[[0-9;]*m|\s)*)pi(?=\
24307
24495
  function rewriteResumeLine(chunk) {
24308
24496
  if (!chunk.includes(RESUME_LABEL))
24309
24497
  return chunk;
24310
- return chunk.replace(RESUME_COMMAND_RE, "$1npx free-pi-cli");
24498
+ return chunk.replace(RESUME_COMMAND_RE, "$1npx free-pi-cli@latest");
24311
24499
  }
24312
24500
  function patchResumeLine(stream) {
24313
24501
  const original = stream.write.bind(stream);
@@ -24367,12 +24555,17 @@ function projectTrustForCwd(cwd, launch, agentDir, hasTrustRequiringResources =
24367
24555
  function createSessionAuth(initial) {
24368
24556
  let jwt2 = initial.jwt;
24369
24557
  let guest = initial.guest;
24558
+ let onSignedIn;
24370
24559
  return {
24371
24560
  getJwt: () => jwt2,
24372
24561
  isGuest: () => guest,
24373
24562
  becomeSignedIn(token) {
24374
24563
  jwt2 = token;
24375
24564
  guest = false;
24565
+ onSignedIn?.();
24566
+ },
24567
+ setOnSignedIn(fn) {
24568
+ onSignedIn = fn;
24376
24569
  }
24377
24570
  };
24378
24571
  }
@@ -24450,6 +24643,7 @@ function buildRuntimeOptions(opts, sessionId) {
24450
24643
  };
24451
24644
  let advertiserUrl;
24452
24645
  const adsExtension = createAdsExtension({
24646
+ registerMeterRefresh: (refresh) => auth.setOnSignedIn(() => void refresh().catch(() => {})),
24453
24647
  baseUrl: opts.baseUrl,
24454
24648
  getToken: () => auth.getJwt(),
24455
24649
  sessionId,
@@ -24665,6 +24859,7 @@ async function launchPi(opts) {
24665
24859
  onLogin: (token) => auth.becomeSignedIn(token),
24666
24860
  variant: () => opts.guestHandoffVariant
24667
24861
  });
24862
+ installLogoutHandler(mode, { credentialsPath: opts.credentialsPath, isGuest: () => auth.isGuest() });
24668
24863
  installModelAllowanceHandler(mode);
24669
24864
  installFooterRightAlign(mode);
24670
24865
  await mode.run();
@@ -25008,18 +25203,7 @@ import { existsSync as existsSync6, readdirSync as readdirSync3, realpathSync as
25008
25203
  import { homedir as homedir6 } from "node:os";
25009
25204
  import { join as join12 } from "node:path";
25010
25205
  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();
25206
+ var INTRO_TEXT = "Tell free-pi what to build, fix, or explain. Resume later with: npx free-pi-cli@latest --continue";
25023
25207
  function introSkipped(env = process.env) {
25024
25208
  const v = env[INTRO_ENV_SKIP];
25025
25209
  return v === "1" || v === "true";
@@ -25052,7 +25236,7 @@ Try one of these on this project — just type it:
25052
25236
  `.trim();
25053
25237
  var FIRST_TASK_EMPTY_TEXT = `
25054
25238
  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.
25239
+ already have: quit, "cd" into a project, and run "npx free-pi-cli@latest" there.
25056
25240
  Or just type: Create a small runnable hello-world app here and run it
25057
25241
  `.trim();
25058
25242
  function showFirstTaskHint(log, cwd = process.cwd(), home = homedir6()) {
@@ -25106,20 +25290,23 @@ async function loginFlow(deps, argv = []) {
25106
25290
  throw new Error(resolved.error);
25107
25291
  }
25108
25292
  try {
25109
- const token = await runDeviceLogin({
25293
+ const token = await runLoginAuto({
25110
25294
  baseUrl: deps.baseUrl,
25111
25295
  openBrowser: deps.openBrowser,
25112
25296
  sleep: deps.sleep,
25113
25297
  log: deps.log,
25114
25298
  jwt: await loadJwt(deps.credentialsPath),
25115
25299
  credentialsPath: deps.credentialsPath
25116
- }, resolved.attribution ?? {});
25300
+ }, resolved.attribution ?? {}, { forceDevice: argv.includes("--device-login"), env: deps.env, platform: deps.platform });
25117
25301
  await saveJwt(deps.credentialsPath, token);
25118
25302
  return token;
25119
25303
  } catch (err) {
25120
25304
  if (err instanceof LoginTimeoutError) {
25121
25305
  deps.log(LOGIN_TIMEOUT_LINE);
25122
25306
  }
25307
+ if (err instanceof LoginDeniedError) {
25308
+ deps.log(LOGIN_DENIED_LINE);
25309
+ }
25123
25310
  throw err;
25124
25311
  }
25125
25312
  }
@@ -25153,6 +25340,21 @@ function reportResumeExposure(deps, jwt2, event, experiment = RESUME_ON_LAUNCH_E
25153
25340
  async function defaultFindLatestSession(cwd, agentDir) {
25154
25341
  return findLatestSession(cwd, getSessionDir(cwd, agentDir));
25155
25342
  }
25343
+ async function handleVersionAction(deps, versionCheck) {
25344
+ const outcome = await deps.maybeSelfUpdate(versionCheck.action, versionCheck.latest);
25345
+ if (outcome === "updated-exit")
25346
+ return { code: 0 };
25347
+ if (outcome === "blocked") {
25348
+ const relaunched = await deps.relaunchLatest?.(versionCheck.latest);
25349
+ if (relaunched !== undefined)
25350
+ return { code: relaunched };
25351
+ deps.log(`free-pi-cli needs an update. From a trusted folder (for example: cd ~), run:
25352
+ npx --yes free-pi-cli@latest
25353
+ or: npm i -g free-pi-cli@latest`);
25354
+ return { code: 1 };
25355
+ }
25356
+ return outcome === "notice" ? { updateLatest: versionCheck.latest } : {};
25357
+ }
25156
25358
  async function run(deps, argv = process.argv) {
25157
25359
  const resumeFlags = parseResumeFlags(argv);
25158
25360
  if (resumeFlags.sessionError) {
@@ -25172,6 +25374,17 @@ async function run(deps, argv = process.argv) {
25172
25374
  ${FINISH_SIGNIN_LINE}
25173
25375
  `);
25174
25376
  let jwt2 = await loadJwt(deps.credentialsPath);
25377
+ const hadJwt = Boolean(jwt2);
25378
+ let versionCheck = await deps.checkClientVersion(deps.baseUrl, CLI_VERSION, jwt2);
25379
+ let updateLatest;
25380
+ let versionHandled = false;
25381
+ if (versionCheck.action === "block") {
25382
+ const handled = await handleVersionAction(deps, versionCheck);
25383
+ if (handled.code !== undefined)
25384
+ return handled.code;
25385
+ updateLatest = handled.updateLatest;
25386
+ versionHandled = true;
25387
+ }
25175
25388
  let guestHandoffVariant;
25176
25389
  let showFirstTask = false;
25177
25390
  let consentCurrent = false;
@@ -25202,7 +25415,7 @@ ${FINISH_SIGNIN_LINE}
25202
25415
  if (!jwt2) {
25203
25416
  const accepted = await deps.promptConsent();
25204
25417
  if (!accepted) {
25205
- deps.log("Declined. Exiting without contacting the free-pi server.");
25418
+ deps.log("Declined. Exiting.");
25206
25419
  return 1;
25207
25420
  }
25208
25421
  showIntro(deps.log);
@@ -25233,24 +25446,13 @@ Guest trial is not available right now.
25233
25446
  }
25234
25447
  consentCurrent = true;
25235
25448
  }
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
- }
25449
+ if (!hadJwt)
25450
+ versionCheck = await deps.checkClientVersion(deps.baseUrl, CLI_VERSION, jwt2);
25451
+ if (!versionHandled && versionCheck.action !== "ok") {
25452
+ const handled = await handleVersionAction(deps, versionCheck);
25453
+ if (handled.code !== undefined)
25454
+ return handled.code;
25455
+ updateLatest = handled.updateLatest;
25254
25456
  }
25255
25457
  const whatsNew = maybeWhatsNew(CLI_VERSION, deps.agentDir);
25256
25458
  if (whatsNew)
@@ -25335,8 +25537,8 @@ ${exitCard}`);
25335
25537
  reportRefillCardShown({ baseUrl: deps.baseUrl, jwt: jwt2 });
25336
25538
  }
25337
25539
  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`);
25540
+ free-pi session ended. Start it again anytime: npx free-pi-cli@latest --session ${finalSessionId}` : `
25541
+ free-pi session ended. Start it again anytime: npx free-pi-cli@latest`);
25340
25542
  deps.log(`(or install once for a shorter command: npm i -g free-pi-cli → free-pi-cli)
25341
25543
  `);
25342
25544
  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.7",
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",