free-pi-cli 0.2.1 → 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 +113 -14
  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),
@@ -14477,12 +14479,13 @@ var ConfigSchema = exports_external.object({
14477
14479
  modelId: exports_external.string().default("deepseek/deepseek-v4-flash"),
14478
14480
  providerDataCollection: exports_external.enum(["allow", "deny"]).optional(),
14479
14481
  promptCacheEnabled: exports_external.boolean().default(false),
14482
+ emailCaptureEnabled: exports_external.boolean().default(false),
14480
14483
  providerOrder: exports_external.array(exports_external.string()).optional(),
14481
14484
  tierNewMaxDays: exports_external.number().int().default(30),
14482
14485
  tierYoungMaxDays: exports_external.number().int().default(365),
14483
14486
  tierCapNewUsd: exports_external.number().default(0.5),
14484
14487
  tierCapYoungUsd: exports_external.number().default(2),
14485
- sessionLeaseTtlMinutes: exports_external.number().int().default(10),
14488
+ sessionLeaseTtlMinutes: exports_external.number().int().default(30),
14486
14489
  minCliVersion: exports_external.string().default("0.0.0"),
14487
14490
  latestCliVersion: exports_external.string().default("0.2.0"),
14488
14491
  abuseWindowMinutes: exports_external.number().int().default(30),
@@ -14520,6 +14523,8 @@ function envList(value) {
14520
14523
  function loadConfig(env = process.env) {
14521
14524
  return ConfigSchema.parse({
14522
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),
14523
14528
  globalDailyCapUsd: envNumber(env.GLOBAL_DAILY_CAP_USD),
14524
14529
  contextCharBudget: envNumber(env.CONTEXT_CHAR_BUDGET),
14525
14530
  accountsPerDayCeiling: envNumber(env.ACCOUNTS_PER_DAY_CEILING),
@@ -14530,6 +14535,7 @@ function loadConfig(env = process.env) {
14530
14535
  modelId: env.MODEL_ID || undefined,
14531
14536
  providerDataCollection: env.PROVIDER_DATA_COLLECTION || undefined,
14532
14537
  promptCacheEnabled: envBoolean(env.PROMPT_CACHE_ENABLED),
14538
+ emailCaptureEnabled: envBoolean(env.EMAIL_CAPTURE_ENABLED),
14533
14539
  providerOrder: envList(env.PROVIDER_ORDER),
14534
14540
  tierNewMaxDays: envNumber(env.TIER_NEW_MAX_DAYS),
14535
14541
  tierYoungMaxDays: envNumber(env.TIER_YOUNG_MAX_DAYS),
@@ -14582,7 +14588,8 @@ async function authedFetch(deps, path, init) {
14582
14588
  headers: {
14583
14589
  ...init?.headers ?? {},
14584
14590
  Authorization: `Bearer ${token}`,
14585
- "Content-Type": "application/json"
14591
+ "Content-Type": "application/json",
14592
+ ...deps.sessionId && (path.startsWith("/ads/next") || path === "/ads/impression") ? { "x-session-id": deps.sessionId } : {}
14586
14593
  }
14587
14594
  });
14588
14595
  }
@@ -14659,7 +14666,7 @@ function renderAdCard(creative, clickUrl, theme, columns) {
14659
14666
  theme.fg("borderAccent", top),
14660
14667
  row(headline, { bold: true }),
14661
14668
  row(body),
14662
- row(`▸ ${cta} ${url2}`, { linkUrl: url2 }),
14669
+ row(`▸ ${cta}`, { linkUrl: url2 }),
14663
14670
  theme.fg("borderAccent", bottom)
14664
14671
  ];
14665
14672
  }
@@ -14667,7 +14674,7 @@ function renderPlainAdLine(creative, clickUrl, theme, columns) {
14667
14674
  const headline = sanitizeText(creative.headline);
14668
14675
  const cta = sanitizeText(creative.cta);
14669
14676
  const url2 = sanitizeText(clickUrl);
14670
- const plain = truncate(`AD ▚ ${headline} — ${cta} ${url2}`, Math.max(10, columns - 2));
14677
+ const plain = truncate(`AD ▚ ${headline} — ${cta}`, Math.max(10, columns - 2));
14671
14678
  return theme.fg("dim", osc8(plain, url2));
14672
14679
  }
14673
14680
  function renderErrorLine(message, theme) {
@@ -14739,6 +14746,8 @@ function mapErrorCode(code) {
14739
14746
  switch (code) {
14740
14747
  case "daily_cap":
14741
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";
14742
14751
  case "concurrent":
14743
14752
  return "only one request at a time on the free tier — wait for the current one to finish";
14744
14753
  case "concurrent_session":
@@ -14785,7 +14794,12 @@ async function renderMeter(deps, ctx) {
14785
14794
  // ../pi-ads/src/index.ts
14786
14795
  var BANNER_REFRESH_MS = 10 * 60 * 1000;
14787
14796
  function createAdsExtension(opts) {
14788
- 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
+ };
14789
14803
  const config3 = loadConfig();
14790
14804
  return {
14791
14805
  name: "free-pi-ads",
@@ -14815,15 +14829,18 @@ function createAdsExtension(opts) {
14815
14829
  }
14816
14830
  };
14817
14831
  }
14832
+
14833
+ // src/pi-launch.ts
14834
+ import { join as join2, resolve } from "node:path";
14818
14835
  // package.json
14819
14836
  var package_default = {
14820
14837
  name: "free-pi-cli",
14821
- version: "0.2.1",
14838
+ version: "0.2.2",
14822
14839
  description: "Free, ad-supported distribution of the pi coding agent. Inference paid for by ads.",
14823
14840
  license: "MIT",
14824
14841
  type: "module",
14825
14842
  bin: {
14826
- "free-pi-cli": "./dist/index.js"
14843
+ "free-pi-cli": "dist/index.js"
14827
14844
  },
14828
14845
  files: [
14829
14846
  "dist",
@@ -19198,7 +19215,8 @@ function buildRuntimeOptions(opts, sessionId) {
19198
19215
  };
19199
19216
  const adsExtension = createAdsExtension({
19200
19217
  baseUrl: opts.baseUrl,
19201
- getToken: () => opts.jwt
19218
+ getToken: () => opts.jwt,
19219
+ sessionId
19202
19220
  });
19203
19221
  const usageToolExtension = createUsageToolExtension({
19204
19222
  baseUrl: opts.baseUrl,
@@ -19223,10 +19241,28 @@ function buildRuntimeOptions(opts, sessionId) {
19223
19241
  tools: ALLOWED_TOOL_NAMES
19224
19242
  };
19225
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
+ }
19226
19260
  async function launchPi(opts) {
19227
- const sessionId = crypto.randomUUID();
19228
- const { settingsManager, resourceLoaderOptions, tools } = buildRuntimeOptions(opts, sessionId);
19229
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);
19230
19266
  const createRuntime = async ({
19231
19267
  cwd: rtCwd,
19232
19268
  sessionManager,
@@ -19252,13 +19288,14 @@ async function launchPi(opts) {
19252
19288
  const runtime = await createAgentSessionRuntime(createRuntime, {
19253
19289
  cwd,
19254
19290
  agentDir: opts.agentDir,
19255
- sessionManager: SessionManager.create(cwd)
19291
+ sessionManager: sm
19256
19292
  });
19257
19293
  const mode = new InteractiveMode(runtime, {
19258
19294
  initialImages: [],
19259
19295
  initialMessages: []
19260
19296
  });
19261
19297
  await mode.run();
19298
+ return sm.getSessionId();
19262
19299
  }
19263
19300
 
19264
19301
  // src/prompt.ts
@@ -19272,6 +19309,8 @@ free-pi — before you start
19272
19309
  * A persistent ad banner and occasional inline ad cards render in the
19273
19310
  terminal UI. Ad content never enters the model's context.
19274
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.
19275
19314
  * free-pi-cli auto-updates itself when a new version is available. Disable
19276
19315
  with FREEPI_NO_AUTO_UPDATE or {"autoUpdate":false} in ~/.free-pi/config.json.
19277
19316
 
@@ -19377,6 +19416,9 @@ async function checkToken(baseUrl, jwt2) {
19377
19416
  throw new UnauthorizedError;
19378
19417
  }
19379
19418
 
19419
+ // src/run.ts
19420
+ import { SessionManager as SessionManager2 } from "@earendil-works/pi-coding-agent";
19421
+
19380
19422
  // src/credentials.ts
19381
19423
  import { mkdir, readFile, rm, writeFile, chmod } from "node:fs/promises";
19382
19424
  import { dirname } from "node:path";
@@ -19400,6 +19442,32 @@ async function clearJwt(credentialsPath) {
19400
19442
  await rm(credentialsPath, { force: true });
19401
19443
  }
19402
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
+
19403
19471
  // src/run.ts
19404
19472
  async function loginFlow(deps) {
19405
19473
  const device = await startDeviceFlow(deps.baseUrl);
@@ -19412,7 +19480,27 @@ Open ${device.verification_uri} and enter code: ${device.user_code}
19412
19480
  await saveJwt(deps.credentialsPath, token);
19413
19481
  return token;
19414
19482
  }
19415
- 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
+ }
19416
19504
  let jwt2 = await loadJwt(deps.credentialsPath);
19417
19505
  if (jwt2) {
19418
19506
  try {
@@ -19430,6 +19518,7 @@ async function run(deps) {
19430
19518
  deps.log("Declined. Exiting without contacting the free-pi server.");
19431
19519
  return 1;
19432
19520
  }
19521
+ showIntro(deps.log);
19433
19522
  jwt2 = await loginFlow(deps);
19434
19523
  }
19435
19524
  const versionCheck = await deps.checkClientVersion(deps.baseUrl, CLI_VERSION);
@@ -19445,7 +19534,17 @@ async function run(deps) {
19445
19534
  deps.log(`update available: ${versionCheck.latest}, you have ${CLI_VERSION}; run \`npx free-pi-cli@latest\``);
19446
19535
  }
19447
19536
  }
19448
- 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
+ `);
19449
19548
  return 0;
19450
19549
  }
19451
19550
 
@@ -19525,7 +19624,7 @@ async function main() {
19525
19624
  credentialsPath: getCredentialsPath(agentDir),
19526
19625
  promptConsent: promptConsentInteractive,
19527
19626
  openBrowser,
19528
- sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
19627
+ sleep: (ms) => new Promise((resolve2) => setTimeout(resolve2, ms)),
19529
19628
  launchPi,
19530
19629
  log: (message) => console.log(message),
19531
19630
  checkClientVersion,
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "free-pi-cli",
3
- "version": "0.2.1",
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",