free-pi-cli 0.2.0 → 0.2.2

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 (3) hide show
  1. package/README.md +19 -0
  2. package/dist/index.js +135 -19
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -13,6 +13,25 @@ may be used to train models), then signs you in with GitHub. Config and the
13
13
  signed-in token live under `~/.free-pi/agent`, kept separate from any
14
14
  existing `~/.pi/agent` install.
15
15
 
16
+ ## Running it again
17
+
18
+ Closed the terminal? Just run the same command again — you stay signed in:
19
+
20
+ ```
21
+ npx free-pi-cli
22
+ ```
23
+
24
+ Prefer a shorter command? Install it once, globally, then call it by name:
25
+
26
+ ```
27
+ npm install -g free-pi-cli
28
+ free-pi-cli
29
+ ```
30
+
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` instead (each
33
+ run starts a fresh session).
34
+
16
35
  ## Development
17
36
 
18
37
  - `FREEPI_BASE_URL` overrides the backend base URL (default
package/dist/index.js CHANGED
@@ -14467,6 +14467,8 @@ function assertExhaustiveErrorCode(code) {
14467
14467
  // ../shared/src/config.ts
14468
14468
  var ConfigSchema = exports_external.object({
14469
14469
  perUserDailyCapUsd: exports_external.number().default(5),
14470
+ perUserRollingCapUsd: exports_external.number().default(0),
14471
+ rollingCapWindowHours: exports_external.number().int().default(5),
14470
14472
  globalDailyCapUsd: exports_external.number().default(50),
14471
14473
  contextCharBudget: exports_external.number().int().default(800000),
14472
14474
  accountsPerDayCeiling: exports_external.number().int().default(500),
@@ -14475,11 +14477,15 @@ var ConfigSchema = exports_external.object({
14475
14477
  adMinColumns: exports_external.number().int().default(60),
14476
14478
  proxyBaseUrl: exports_external.string().default("https://api.freepi.ai"),
14477
14479
  modelId: exports_external.string().default("deepseek/deepseek-v4-flash"),
14480
+ providerDataCollection: exports_external.enum(["allow", "deny"]).optional(),
14481
+ promptCacheEnabled: exports_external.boolean().default(false),
14482
+ emailCaptureEnabled: exports_external.boolean().default(false),
14483
+ providerOrder: exports_external.array(exports_external.string()).optional(),
14478
14484
  tierNewMaxDays: exports_external.number().int().default(30),
14479
14485
  tierYoungMaxDays: exports_external.number().int().default(365),
14480
14486
  tierCapNewUsd: exports_external.number().default(0.5),
14481
14487
  tierCapYoungUsd: exports_external.number().default(2),
14482
- sessionLeaseTtlMinutes: exports_external.number().int().default(10),
14488
+ sessionLeaseTtlMinutes: exports_external.number().int().default(30),
14483
14489
  minCliVersion: exports_external.string().default("0.0.0"),
14484
14490
  latestCliVersion: exports_external.string().default("0.2.0"),
14485
14491
  abuseWindowMinutes: exports_external.number().int().default(30),
@@ -14503,9 +14509,22 @@ function envNumber(value) {
14503
14509
  const n = Number(value);
14504
14510
  return Number.isNaN(n) ? undefined : n;
14505
14511
  }
14512
+ function envBoolean(value) {
14513
+ if (value === undefined || value === "")
14514
+ return;
14515
+ return value === "true" || value === "1";
14516
+ }
14517
+ function envList(value) {
14518
+ if (!value)
14519
+ return;
14520
+ const items = value.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
14521
+ return items.length > 0 ? items : undefined;
14522
+ }
14506
14523
  function loadConfig(env = process.env) {
14507
14524
  return ConfigSchema.parse({
14508
14525
  perUserDailyCapUsd: envNumber(env.PER_USER_DAILY_CAP_USD),
14526
+ perUserRollingCapUsd: envNumber(env.PER_USER_ROLLING_CAP_USD),
14527
+ rollingCapWindowHours: envNumber(env.ROLLING_CAP_WINDOW_HOURS),
14509
14528
  globalDailyCapUsd: envNumber(env.GLOBAL_DAILY_CAP_USD),
14510
14529
  contextCharBudget: envNumber(env.CONTEXT_CHAR_BUDGET),
14511
14530
  accountsPerDayCeiling: envNumber(env.ACCOUNTS_PER_DAY_CEILING),
@@ -14514,6 +14533,10 @@ function loadConfig(env = process.env) {
14514
14533
  adMinColumns: envNumber(env.AD_MIN_COLUMNS),
14515
14534
  proxyBaseUrl: env.PROXY_BASE_URL || undefined,
14516
14535
  modelId: env.MODEL_ID || undefined,
14536
+ providerDataCollection: env.PROVIDER_DATA_COLLECTION || undefined,
14537
+ promptCacheEnabled: envBoolean(env.PROMPT_CACHE_ENABLED),
14538
+ emailCaptureEnabled: envBoolean(env.EMAIL_CAPTURE_ENABLED),
14539
+ providerOrder: envList(env.PROVIDER_ORDER),
14517
14540
  tierNewMaxDays: envNumber(env.TIER_NEW_MAX_DAYS),
14518
14541
  tierYoungMaxDays: envNumber(env.TIER_YOUNG_MAX_DAYS),
14519
14542
  tierCapNewUsd: envNumber(env.TIER_CAP_NEW_USD),
@@ -14565,7 +14588,8 @@ async function authedFetch(deps, path, init) {
14565
14588
  headers: {
14566
14589
  ...init?.headers ?? {},
14567
14590
  Authorization: `Bearer ${token}`,
14568
- "Content-Type": "application/json"
14591
+ "Content-Type": "application/json",
14592
+ ...deps.sessionId && (path.startsWith("/ads/next") || path === "/ads/impression") ? { "x-session-id": deps.sessionId } : {}
14569
14593
  }
14570
14594
  });
14571
14595
  }
@@ -14642,7 +14666,7 @@ function renderAdCard(creative, clickUrl, theme, columns) {
14642
14666
  theme.fg("borderAccent", top),
14643
14667
  row(headline, { bold: true }),
14644
14668
  row(body),
14645
- row(`▸ ${cta} ${url2}`, { linkUrl: url2 }),
14669
+ row(`▸ ${cta}`, { linkUrl: url2 }),
14646
14670
  theme.fg("borderAccent", bottom)
14647
14671
  ];
14648
14672
  }
@@ -14650,12 +14674,9 @@ function renderPlainAdLine(creative, clickUrl, theme, columns) {
14650
14674
  const headline = sanitizeText(creative.headline);
14651
14675
  const cta = sanitizeText(creative.cta);
14652
14676
  const url2 = sanitizeText(clickUrl);
14653
- const plain = truncate(`AD ▚ ${headline} — ${cta} ${url2}`, Math.max(10, columns - 2));
14677
+ const plain = truncate(`AD ▚ ${headline} — ${cta}`, Math.max(10, columns - 2));
14654
14678
  return theme.fg("dim", osc8(plain, url2));
14655
14679
  }
14656
- function renderMeterLine(remainingUsdToday, theme) {
14657
- return theme.fg("dim", `free budget today: $${remainingUsdToday.toFixed(2)} remaining`);
14658
- }
14659
14680
  function renderErrorLine(message, theme) {
14660
14681
  return theme.fg("error", message);
14661
14682
  }
@@ -14725,6 +14746,8 @@ function mapErrorCode(code) {
14725
14746
  switch (code) {
14726
14747
  case "daily_cap":
14727
14748
  return "out of free tokens today, resets at 00:00 UTC";
14749
+ case "rolling_cap":
14750
+ return "over the rolling spend cap on the free tier — wait for the Retry-After window";
14728
14751
  case "concurrent":
14729
14752
  return "only one request at a time on the free tier — wait for the current one to finish";
14730
14753
  case "concurrent_session":
@@ -14761,14 +14784,22 @@ async function renderMeter(deps, ctx) {
14761
14784
  }
14762
14785
  const theme = ctx.ui.theme;
14763
14786
  const errorCode = inferErrorFromRemainingBudget(me.remaining_usd_today);
14764
- const line = errorCode !== undefined ? renderErrorLine(mapErrorCode(errorCode), theme) : renderMeterLine(me.remaining_usd_today, theme);
14765
- ctx.ui.setWidget(METER_WIDGET_ID, [line]);
14787
+ if (errorCode === undefined) {
14788
+ ctx.ui.setWidget(METER_WIDGET_ID, undefined);
14789
+ return;
14790
+ }
14791
+ ctx.ui.setWidget(METER_WIDGET_ID, [renderErrorLine(mapErrorCode(errorCode), theme)]);
14766
14792
  }
14767
14793
 
14768
14794
  // ../pi-ads/src/index.ts
14769
14795
  var BANNER_REFRESH_MS = 10 * 60 * 1000;
14770
14796
  function createAdsExtension(opts) {
14771
- const deps = { baseUrl: opts.baseUrl, getToken: opts.getToken, fetchImpl: opts.fetchImpl };
14797
+ const deps = {
14798
+ baseUrl: opts.baseUrl,
14799
+ getToken: opts.getToken,
14800
+ sessionId: opts.sessionId,
14801
+ fetchImpl: opts.fetchImpl
14802
+ };
14772
14803
  const config3 = loadConfig();
14773
14804
  return {
14774
14805
  name: "free-pi-ads",
@@ -14798,15 +14829,18 @@ function createAdsExtension(opts) {
14798
14829
  }
14799
14830
  };
14800
14831
  }
14832
+
14833
+ // src/pi-launch.ts
14834
+ import { join as join2, resolve } from "node:path";
14801
14835
  // package.json
14802
14836
  var package_default = {
14803
14837
  name: "free-pi-cli",
14804
- version: "0.2.0",
14838
+ version: "0.2.2",
14805
14839
  description: "Free, ad-supported distribution of the pi coding agent. Inference paid for by ads.",
14806
14840
  license: "MIT",
14807
14841
  type: "module",
14808
14842
  bin: {
14809
- "free-pi-cli": "./dist/index.js"
14843
+ "free-pi-cli": "dist/index.js"
14810
14844
  },
14811
14845
  files: [
14812
14846
  "dist",
@@ -19181,7 +19215,8 @@ function buildRuntimeOptions(opts, sessionId) {
19181
19215
  };
19182
19216
  const adsExtension = createAdsExtension({
19183
19217
  baseUrl: opts.baseUrl,
19184
- getToken: () => opts.jwt
19218
+ getToken: () => opts.jwt,
19219
+ sessionId
19185
19220
  });
19186
19221
  const usageToolExtension = createUsageToolExtension({
19187
19222
  baseUrl: opts.baseUrl,
@@ -19206,10 +19241,28 @@ function buildRuntimeOptions(opts, sessionId) {
19206
19241
  tools: ALLOWED_TOOL_NAMES
19207
19242
  };
19208
19243
  }
19244
+ function getSessionDir(cwd, agentDir) {
19245
+ const resolvedCwd = resolve(cwd);
19246
+ const safePath = `--${resolvedCwd.replace(/^[/\\]/, "").replace(/[/\\:]/g, "-")}--`;
19247
+ return join2(resolve(agentDir), "sessions", safePath);
19248
+ }
19249
+ async function createSessionManager(cwd, sessionDir, sessionId, api3 = SessionManager) {
19250
+ if (sessionId) {
19251
+ const infos = await api3.list(cwd, sessionDir);
19252
+ const info = infos.find((session) => session.id === sessionId);
19253
+ if (!info) {
19254
+ throw new Error(`No session found matching '${sessionId}' in this directory`);
19255
+ }
19256
+ return api3.open(info.path, sessionDir);
19257
+ }
19258
+ return api3.create(cwd, sessionDir);
19259
+ }
19209
19260
  async function launchPi(opts) {
19210
- const sessionId = crypto.randomUUID();
19211
- const { settingsManager, resourceLoaderOptions, tools } = buildRuntimeOptions(opts, sessionId);
19212
19261
  const cwd = process.cwd();
19262
+ const sessionDir = getSessionDir(cwd, opts.agentDir);
19263
+ const sm = await createSessionManager(cwd, sessionDir, opts.session);
19264
+ const sessionId = sm.getSessionId();
19265
+ const { settingsManager, resourceLoaderOptions, tools } = buildRuntimeOptions(opts, sessionId);
19213
19266
  const createRuntime = async ({
19214
19267
  cwd: rtCwd,
19215
19268
  sessionManager,
@@ -19235,13 +19288,14 @@ async function launchPi(opts) {
19235
19288
  const runtime = await createAgentSessionRuntime(createRuntime, {
19236
19289
  cwd,
19237
19290
  agentDir: opts.agentDir,
19238
- sessionManager: SessionManager.create(cwd)
19291
+ sessionManager: sm
19239
19292
  });
19240
19293
  const mode = new InteractiveMode(runtime, {
19241
19294
  initialImages: [],
19242
19295
  initialMessages: []
19243
19296
  });
19244
19297
  await mode.run();
19298
+ return sm.getSessionId();
19245
19299
  }
19246
19300
 
19247
19301
  // src/prompt.ts
@@ -19255,6 +19309,8 @@ free-pi — before you start
19255
19309
  * A persistent ad banner and occasional inline ad cards render in the
19256
19310
  terminal UI. Ad content never enters the model's context.
19257
19311
  * Your coding sessions may be used to train models.
19312
+ * If GitHub email capture is enabled, we collect your GitHub email and may
19313
+ use it to contact you about the service or product.
19258
19314
  * free-pi-cli auto-updates itself when a new version is available. Disable
19259
19315
  with FREEPI_NO_AUTO_UPDATE or {"autoUpdate":false} in ~/.free-pi/config.json.
19260
19316
 
@@ -19360,6 +19416,9 @@ async function checkToken(baseUrl, jwt2) {
19360
19416
  throw new UnauthorizedError;
19361
19417
  }
19362
19418
 
19419
+ // src/run.ts
19420
+ import { SessionManager as SessionManager2 } from "@earendil-works/pi-coding-agent";
19421
+
19363
19422
  // src/credentials.ts
19364
19423
  import { mkdir, readFile, rm, writeFile, chmod } from "node:fs/promises";
19365
19424
  import { dirname } from "node:path";
@@ -19383,6 +19442,32 @@ async function clearJwt(credentialsPath) {
19383
19442
  await rm(credentialsPath, { force: true });
19384
19443
  }
19385
19444
 
19445
+ // src/onboarding.ts
19446
+ var INTRO_ENV_SKIP = "FREEPI_NO_INTRO";
19447
+ var INTRO_TEXT = `
19448
+ Welcome to free-pi — a free coding agent in your terminal.
19449
+
19450
+ • It's the pi coding agent, free to use. Ads in the terminal pay for the
19451
+ inference — ad content never reaches the model.
19452
+ • Tell it what to build, fix, or explain, like any coding assistant.
19453
+ • One session per account at a time. If you close the terminal, relaunch
19454
+ with "npx free-pi-cli" — or resume where you left off with
19455
+ "npx free-pi-cli --session <id>" (the id prints when you exit).
19456
+
19457
+ Set ${INTRO_ENV_SKIP}=1 to skip this next time.
19458
+ `.trim();
19459
+ function introSkipped(env = process.env) {
19460
+ const v = env[INTRO_ENV_SKIP];
19461
+ return v === "1" || v === "true";
19462
+ }
19463
+ function showIntro(log, env = process.env) {
19464
+ if (introSkipped(env))
19465
+ return;
19466
+ log(`
19467
+ ${INTRO_TEXT}
19468
+ `);
19469
+ }
19470
+
19386
19471
  // src/run.ts
19387
19472
  async function loginFlow(deps) {
19388
19473
  const device = await startDeviceFlow(deps.baseUrl);
@@ -19395,7 +19480,27 @@ Open ${device.verification_uri} and enter code: ${device.user_code}
19395
19480
  await saveJwt(deps.credentialsPath, token);
19396
19481
  return token;
19397
19482
  }
19398
- async function run(deps) {
19483
+ function parseSessionArg(argv) {
19484
+ const flagIndex = argv.indexOf("--session");
19485
+ if (flagIndex === -1)
19486
+ return {};
19487
+ const value = argv[flagIndex + 1];
19488
+ if (!value || value.startsWith("--")) {
19489
+ return { error: "Missing value for --session" };
19490
+ }
19491
+ try {
19492
+ SessionManager2.inMemory(process.cwd(), { id: value });
19493
+ } catch (err) {
19494
+ return { error: `Invalid --session value: ${err instanceof Error ? err.message : String(err)}` };
19495
+ }
19496
+ return { session: value };
19497
+ }
19498
+ async function run(deps, argv = process.argv) {
19499
+ const sessionArg = parseSessionArg(argv);
19500
+ if (sessionArg.error) {
19501
+ deps.log(sessionArg.error);
19502
+ return 1;
19503
+ }
19399
19504
  let jwt2 = await loadJwt(deps.credentialsPath);
19400
19505
  if (jwt2) {
19401
19506
  try {
@@ -19413,6 +19518,7 @@ async function run(deps) {
19413
19518
  deps.log("Declined. Exiting without contacting the free-pi server.");
19414
19519
  return 1;
19415
19520
  }
19521
+ showIntro(deps.log);
19416
19522
  jwt2 = await loginFlow(deps);
19417
19523
  }
19418
19524
  const versionCheck = await deps.checkClientVersion(deps.baseUrl, CLI_VERSION);
@@ -19428,7 +19534,17 @@ async function run(deps) {
19428
19534
  deps.log(`update available: ${versionCheck.latest}, you have ${CLI_VERSION}; run \`npx free-pi-cli@latest\``);
19429
19535
  }
19430
19536
  }
19431
- await deps.launchPi({ baseUrl: deps.baseUrl, jwt: jwt2, agentDir: deps.agentDir });
19537
+ const finalSessionId = await deps.launchPi({
19538
+ baseUrl: deps.baseUrl,
19539
+ jwt: jwt2,
19540
+ agentDir: deps.agentDir,
19541
+ session: sessionArg.session
19542
+ });
19543
+ deps.log(finalSessionId ? `
19544
+ free-pi session ended. Start it again anytime: npx free-pi-cli --session ${finalSessionId}` : `
19545
+ free-pi session ended. Start it again anytime: npx free-pi-cli`);
19546
+ deps.log(`(or install once for a shorter command: npm i -g free-pi-cli → free-pi-cli)
19547
+ `);
19432
19548
  return 0;
19433
19549
  }
19434
19550
 
@@ -19508,7 +19624,7 @@ async function main() {
19508
19624
  credentialsPath: getCredentialsPath(agentDir),
19509
19625
  promptConsent: promptConsentInteractive,
19510
19626
  openBrowser,
19511
- sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
19627
+ sleep: (ms) => new Promise((resolve2) => setTimeout(resolve2, ms)),
19512
19628
  launchPi,
19513
19629
  log: (message) => console.log(message),
19514
19630
  checkClientVersion,
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "free-pi-cli",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
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",
7
7
  "bin": {
8
- "free-pi-cli": "./dist/index.js"
8
+ "free-pi-cli": "dist/index.js"
9
9
  },
10
10
  "files": [
11
11
  "dist",