free-pi-cli 0.2.8 → 0.2.10

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/dist/index.js CHANGED
@@ -30,4 +30,4 @@ if (err) {
30
30
  console.error(err);
31
31
  process.exit(1);
32
32
  }
33
- await import("./main-wd2zxa5n.js");
33
+ await import("./main-b5491jxx.js");
@@ -5,23 +5,114 @@ import {
5
5
  } from "./index-zqjhfwvz.js";
6
6
 
7
7
  // src/main.ts
8
- import { spawnSync } from "node:child_process";
9
8
  import { realpathSync } from "node:fs";
10
9
 
11
10
  // src/browser.ts
12
11
  import { spawn } from "node:child_process";
13
- function openBrowser(url) {
12
+ var CMD_METACHARACTERS = /[\^&|<>()]/g;
13
+ function safeBrowserUrl(url) {
14
+ try {
15
+ const parsed = new URL(url);
16
+ if (parsed.protocol !== "https:" && parsed.protocol !== "http:")
17
+ return null;
18
+ return parsed.href;
19
+ } catch {
20
+ return null;
21
+ }
22
+ }
23
+ function buildOpenCommand(url, platform) {
24
+ if (platform === "win32") {
25
+ return {
26
+ command: "cmd",
27
+ args: ["/c", "start", '""', url.replace(CMD_METACHARACTERS, (char) => `^${char}`)],
28
+ windowsVerbatimArguments: true
29
+ };
30
+ }
31
+ return { command: platform === "darwin" ? "open" : "xdg-open", args: [url] };
32
+ }
33
+ function openBrowser(url, platform = process.platform, spawnImpl = spawn) {
14
34
  try {
15
- const command = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
16
- const child = spawn(command, [url], {
35
+ const safeUrl = safeBrowserUrl(url);
36
+ if (safeUrl === null)
37
+ return;
38
+ const { command, args, windowsVerbatimArguments } = buildOpenCommand(safeUrl, platform);
39
+ const child = spawnImpl(command, args, {
17
40
  stdio: "ignore",
18
41
  detached: true,
19
- shell: process.platform === "win32"
42
+ windowsVerbatimArguments
20
43
  });
21
44
  child.on("error", () => {});
22
45
  child.unref();
23
46
  } catch {}
24
47
  }
48
+ // package.json
49
+ var package_default = {
50
+ name: "free-pi-cli",
51
+ version: "0.2.10",
52
+ description: "Free, ad-supported distribution of the pi coding agent. Inference paid for by ads.",
53
+ license: "MIT",
54
+ type: "module",
55
+ bin: {
56
+ "free-pi-cli": "dist/index.js"
57
+ },
58
+ files: [
59
+ "dist",
60
+ "README.md"
61
+ ],
62
+ engines: {
63
+ node: ">=22.19.0"
64
+ },
65
+ scripts: {
66
+ build: "rm -rf dist && bun build src/index.ts --target=node --outdir dist --splitting --external @earendil-works/pi-coding-agent --banner '#!/usr/bin/env node' && chmod +x dist/index.js",
67
+ prepublishOnly: "bun run build"
68
+ },
69
+ dependencies: {
70
+ "@earendil-works/pi-coding-agent": "0.84.4"
71
+ },
72
+ devDependencies: {
73
+ "@freepi/pi-ads": "workspace:*",
74
+ "@freepi/shared": "workspace:*",
75
+ typebox: "^1.3.7"
76
+ }
77
+ };
78
+
79
+ // src/version.ts
80
+ var CLI_VERSION = package_default.version;
81
+
82
+ // src/cli-args.ts
83
+ var HELP_TEXT = `
84
+ free-pi — free AI coding in your terminal; ads pay for the inference.
85
+
86
+ Usage:
87
+ free-pi-cli start the coding agent (first run: consent + GitHub sign-in)
88
+ free-pi-cli logout remove the stored sign-in token; the next run signs in again
89
+ free-pi-cli --version print the CLI version
90
+ free-pi-cli --help show this help
91
+
92
+ Environment:
93
+ FREEPI_BASE_URL override the backend base URL (default https://api.freepi.ai)
94
+ FREEPI_NO_AUTO_UPDATE disable in-place self-update for this run
95
+ `.trim();
96
+ function versionLine() {
97
+ return `free-pi-cli ${CLI_VERSION}`;
98
+ }
99
+ function parseCliArgs(argv) {
100
+ const [first, ...rest] = argv;
101
+ if (first === undefined)
102
+ return { kind: "run" };
103
+ let kind;
104
+ if (first === "--help" || first === "-h" || first === "help")
105
+ kind = "help";
106
+ else if (first === "--version" || first === "-v")
107
+ kind = "version";
108
+ else if (first === "logout")
109
+ kind = "logout";
110
+ else
111
+ return { kind: "unknown", arg: first };
112
+ if (rest.length > 0)
113
+ return { kind: "unknown", arg: rest[0] };
114
+ return { kind };
115
+ }
25
116
 
26
117
  // src/cli-config.ts
27
118
  import { readFileSync } from "node:fs";
@@ -35,12 +126,43 @@ function readAutoUpdate(configPath) {
35
126
  }
36
127
  }
37
128
 
129
+ // src/credentials.ts
130
+ import { mkdir, readFile, rm, writeFile, chmod } from "node:fs/promises";
131
+ import { dirname } from "node:path";
132
+ async function loadJwt(credentialsPath) {
133
+ try {
134
+ const raw = await readFile(credentialsPath, "utf8");
135
+ const parsed = JSON.parse(raw);
136
+ return typeof parsed.token === "string" && parsed.token.length > 0 ? parsed.token : null;
137
+ } catch {
138
+ return null;
139
+ }
140
+ }
141
+ async function saveJwt(credentialsPath, token) {
142
+ await mkdir(dirname(credentialsPath), { recursive: true });
143
+ await writeFile(credentialsPath, JSON.stringify({ token }), {
144
+ mode: 384
145
+ });
146
+ await chmod(credentialsPath, 384);
147
+ }
148
+ async function clearJwt(credentialsPath) {
149
+ await rm(credentialsPath, { force: true });
150
+ }
151
+
38
152
  // src/env.ts
39
153
  var DEFAULT_BASE_URL = "https://api.freepi.ai";
40
154
  function resolveBaseUrl(env = process.env) {
41
155
  return env.FREEPI_BASE_URL || DEFAULT_BASE_URL;
42
156
  }
43
157
 
158
+ // src/logout.ts
159
+ async function runLogout(deps) {
160
+ const hadToken = await deps.loadJwt(deps.credentialsPath) !== null;
161
+ await deps.clearJwt(deps.credentialsPath);
162
+ deps.log(hadToken ? `Logged out: removed ${deps.credentialsPath}. The next run signs in again.` : "Already logged out — no stored sign-in token.");
163
+ return 0;
164
+ }
165
+
44
166
  // src/paths.ts
45
167
  import { homedir } from "node:os";
46
168
  import { join } from "node:path";
@@ -14472,6 +14594,8 @@ var ConfigSchema = exports_external.object({
14472
14594
  contextCharBudget: exports_external.number().int().default(800000),
14473
14595
  accountsPerDayCeiling: exports_external.number().int().default(500),
14474
14596
  authRatePerIpPerHour: exports_external.number().int().default(30),
14597
+ tokenRatePerIpPerHour: exports_external.number().int().default(3600),
14598
+ consentRatePerIpPerHour: exports_external.number().int().default(60),
14475
14599
  adInlineTurnFrequency: exports_external.number().int().default(5),
14476
14600
  adMinColumns: exports_external.number().int().default(60),
14477
14601
  proxyBaseUrl: exports_external.string().default("https://api.freepi.ai"),
@@ -14506,7 +14630,8 @@ var ConfigSchema = exports_external.object({
14506
14630
  abuseAdBypassMinRequests: exports_external.number().int().default(20),
14507
14631
  abuseAdBypassRatio: exports_external.number().int().default(10),
14508
14632
  traceMaxInflightWrites: exports_external.number().int().default(32),
14509
- catalogCacheTtlSeconds: exports_external.number().int().default(60)
14633
+ catalogCacheTtlSeconds: exports_external.number().int().default(60),
14634
+ eventMaxInflightWrites: exports_external.number().int().default(128)
14510
14635
  });
14511
14636
  function envNumber(value) {
14512
14637
  if (value === undefined || value === "")
@@ -14534,6 +14659,8 @@ function loadConfig(env = process.env) {
14534
14659
  contextCharBudget: envNumber(env.CONTEXT_CHAR_BUDGET),
14535
14660
  accountsPerDayCeiling: envNumber(env.ACCOUNTS_PER_DAY_CEILING),
14536
14661
  authRatePerIpPerHour: envNumber(env.AUTH_RATE_PER_IP_PER_HOUR),
14662
+ tokenRatePerIpPerHour: envNumber(env.TOKEN_RATE_PER_IP_PER_HOUR),
14663
+ consentRatePerIpPerHour: envNumber(env.CONSENT_RATE_PER_IP_PER_HOUR),
14537
14664
  adInlineTurnFrequency: envNumber(env.AD_INLINE_TURN_FREQUENCY),
14538
14665
  adMinColumns: envNumber(env.AD_MIN_COLUMNS),
14539
14666
  proxyBaseUrl: env.PROXY_BASE_URL || undefined,
@@ -14568,7 +14695,8 @@ function loadConfig(env = process.env) {
14568
14695
  abuseAdBypassMinRequests: envNumber(env.ABUSE_AD_BYPASS_MIN_REQUESTS),
14569
14696
  abuseAdBypassRatio: envNumber(env.ABUSE_AD_BYPASS_RATIO),
14570
14697
  traceMaxInflightWrites: envNumber(env.TRACE_MAX_INFLIGHT_WRITES),
14571
- catalogCacheTtlSeconds: envNumber(env.CATALOG_CACHE_TTL_SECONDS)
14698
+ catalogCacheTtlSeconds: envNumber(env.CATALOG_CACHE_TTL_SECONDS),
14699
+ eventMaxInflightWrites: envNumber(env.EVENT_MAX_INFLIGHT_WRITES)
14572
14700
  });
14573
14701
  }
14574
14702
  // ../shared/src/version.ts
@@ -14843,39 +14971,6 @@ function createAdsExtension(opts) {
14843
14971
 
14844
14972
  // src/pi-launch.ts
14845
14973
  import { join as join2, resolve } from "node:path";
14846
- // package.json
14847
- var package_default = {
14848
- name: "free-pi-cli",
14849
- version: "0.2.8",
14850
- description: "Free, ad-supported distribution of the pi coding agent. Inference paid for by ads.",
14851
- license: "MIT",
14852
- type: "module",
14853
- bin: {
14854
- "free-pi-cli": "dist/index.js"
14855
- },
14856
- files: [
14857
- "dist",
14858
- "README.md"
14859
- ],
14860
- engines: {
14861
- node: ">=22.19.0"
14862
- },
14863
- scripts: {
14864
- build: "bun build src/index.ts --target=node --outdir dist --splitting --external @earendil-works/pi-coding-agent --banner '#!/usr/bin/env node' && chmod +x dist/index.js",
14865
- prepublishOnly: "bun run build"
14866
- },
14867
- dependencies: {
14868
- "@earendil-works/pi-coding-agent": "^0.84.2"
14869
- },
14870
- devDependencies: {
14871
- "@freepi/pi-ads": "workspace:*",
14872
- "@freepi/shared": "workspace:*",
14873
- typebox: "^1.3.7"
14874
- }
14875
- };
14876
-
14877
- // src/version.ts
14878
- var CLI_VERSION = package_default.version;
14879
14974
 
14880
14975
  // src/provider.ts
14881
14976
  var PROVIDER_NAME = "free-pi";
@@ -19245,6 +19340,20 @@ function createToolGuardExtension(opts) {
19245
19340
 
19246
19341
  // src/changelog.ts
19247
19342
  var CHANGELOG_HIGHLIGHTS = [
19343
+ {
19344
+ version: "0.2.10",
19345
+ highlights: [
19346
+ "free-pi now installs an exact, tested version of the underlying agent — an upstream release can no longer change your CLI without us testing it first."
19347
+ ]
19348
+ },
19349
+ {
19350
+ version: "0.2.9",
19351
+ highlights: [
19352
+ "Windows: self-update and browser sign-in actually work now — both had been broken since they shipped.",
19353
+ "Running without a terminal (CI, piped input) now says so and exits, instead of hanging or quietly doing nothing.",
19354
+ "New: `free-pi-cli logout`, `--version` and `--help`."
19355
+ ]
19356
+ },
19248
19357
  {
19249
19358
  version: "0.2.8",
19250
19359
  highlights: [
@@ -19320,7 +19429,10 @@ function isEphemeralNpx(env) {
19320
19429
  async function defaultInstallLatest() {
19321
19430
  const { spawn: spawn2 } = await import("node:child_process");
19322
19431
  return new Promise((resolve) => {
19323
- const child = spawn2("npm", ["install", "-g", "free-pi-cli@latest"], { stdio: "ignore" });
19432
+ const child = spawn2("npm", ["install", "-g", "free-pi-cli@latest"], {
19433
+ stdio: "ignore",
19434
+ shell: process.platform === "win32"
19435
+ });
19324
19436
  child.on("error", () => resolve({ ok: false }));
19325
19437
  child.on("close", (code) => resolve({ ok: code === 0 }));
19326
19438
  });
@@ -19505,21 +19617,48 @@ free-pi — before you start
19505
19617
  Decline and nothing is sent to the free-pi server.
19506
19618
  `.trim();
19507
19619
  var CONSENT_VERSION = "1";
19620
+ 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.";
19508
19621
  var CONSENT_PROMPT = "Accept? [y/N] ";
19509
19622
  function parseConsentAnswer(answer) {
19510
19623
  return /^\s*y(es)?\s*$/i.test(answer);
19511
19624
  }
19512
19625
 
19513
19626
  // src/prompt.ts
19514
- async function promptConsentInteractive() {
19515
- console.log(CONSENT_TEXT);
19516
- const rl = createInterface({ input: process.stdin, output: process.stdout });
19517
- try {
19518
- const answer = await rl.question(CONSENT_PROMPT);
19519
- return parseConsentAnswer(answer);
19520
- } finally {
19521
- rl.close();
19627
+ async function promptConsent(deps) {
19628
+ if (!deps.stdinIsTTY) {
19629
+ deps.log(NON_INTERACTIVE_CONSENT_TEXT);
19630
+ return false;
19522
19631
  }
19632
+ deps.log(CONSENT_TEXT);
19633
+ const answer = await deps.askLine(CONSENT_PROMPT);
19634
+ return parseConsentAnswer(answer);
19635
+ }
19636
+ async function promptConsentInteractive() {
19637
+ return promptConsent({
19638
+ stdinIsTTY: process.stdin.isTTY === true,
19639
+ log: (message) => console.log(message),
19640
+ askLine: async (prompt) => {
19641
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
19642
+ try {
19643
+ return await rl.question(prompt);
19644
+ } finally {
19645
+ rl.close();
19646
+ }
19647
+ }
19648
+ });
19649
+ }
19650
+
19651
+ // src/real-spawn.ts
19652
+ import { spawnSync } from "node:child_process";
19653
+ function createRealSpawn(io = { platform: process.platform, spawnSyncImpl: spawnSync }) {
19654
+ return (cmd, args, opts) => {
19655
+ const result = io.spawnSyncImpl(cmd, args, {
19656
+ timeout: opts.timeout,
19657
+ encoding: "utf8",
19658
+ shell: io.platform === "win32"
19659
+ });
19660
+ return { status: result.status, stdout: result.stdout ?? "", error: result.error };
19661
+ };
19523
19662
  }
19524
19663
 
19525
19664
  // src/auth-flow.ts
@@ -19607,29 +19746,6 @@ async function checkToken(baseUrl, jwt2) {
19607
19746
  // src/run.ts
19608
19747
  import { SessionManager as SessionManager2 } from "@earendil-works/pi-coding-agent";
19609
19748
 
19610
- // src/credentials.ts
19611
- import { mkdir, readFile, rm, writeFile, chmod } from "node:fs/promises";
19612
- import { dirname } from "node:path";
19613
- async function loadJwt(credentialsPath) {
19614
- try {
19615
- const raw = await readFile(credentialsPath, "utf8");
19616
- const parsed = JSON.parse(raw);
19617
- return typeof parsed.token === "string" && parsed.token.length > 0 ? parsed.token : null;
19618
- } catch {
19619
- return null;
19620
- }
19621
- }
19622
- async function saveJwt(credentialsPath, token) {
19623
- await mkdir(dirname(credentialsPath), { recursive: true });
19624
- await writeFile(credentialsPath, JSON.stringify({ token }), {
19625
- mode: 384
19626
- });
19627
- await chmod(credentialsPath, 384);
19628
- }
19629
- async function clearJwt(credentialsPath) {
19630
- await rm(credentialsPath, { force: true });
19631
- }
19632
-
19633
19749
  // src/onboarding.ts
19634
19750
  var INTRO_ENV_SKIP = "FREEPI_NO_INTRO";
19635
19751
  var INTRO_TEXT = `
@@ -19833,10 +19949,6 @@ async function checkClientVersion(baseUrl, cliVersion, fetchImpl = fetch) {
19833
19949
  }
19834
19950
 
19835
19951
  // src/main.ts
19836
- function realSpawn(cmd, args, opts) {
19837
- const result = spawnSync(cmd, args, { timeout: opts.timeout, encoding: "utf8" });
19838
- return { status: result.status, stdout: result.stdout ?? "", error: result.error };
19839
- }
19840
19952
  function resolveCliRealpath() {
19841
19953
  try {
19842
19954
  return realpathSync(process.argv[1] ?? "");
@@ -19845,6 +19957,30 @@ function resolveCliRealpath() {
19845
19957
  }
19846
19958
  }
19847
19959
  async function main() {
19960
+ const command = parseCliArgs(process.argv.slice(2));
19961
+ if (command.kind === "version") {
19962
+ console.log(versionLine());
19963
+ process.exit(0);
19964
+ }
19965
+ if (command.kind === "help") {
19966
+ console.log(HELP_TEXT);
19967
+ process.exit(0);
19968
+ }
19969
+ if (command.kind === "unknown") {
19970
+ console.error(`free-pi-cli: unknown argument "${command.arg}"
19971
+
19972
+ ${HELP_TEXT}`);
19973
+ process.exit(2);
19974
+ }
19975
+ if (command.kind === "logout") {
19976
+ const logoutCode = await runLogout({
19977
+ credentialsPath: getCredentialsPath(),
19978
+ loadJwt,
19979
+ clearJwt,
19980
+ log: (message) => console.log(message)
19981
+ });
19982
+ process.exit(logoutCode);
19983
+ }
19848
19984
  const agentDir = getFreePiAgentDir();
19849
19985
  const code = await run({
19850
19986
  baseUrl: resolveBaseUrl(),
@@ -19859,7 +19995,7 @@ async function main() {
19859
19995
  maybeSelfUpdate: (action2, latest) => maybeSelfUpdate(action2, latest, {
19860
19996
  env: process.env,
19861
19997
  autoUpdateEnabled: () => readAutoUpdate(getConfigPath()),
19862
- spawn: realSpawn,
19998
+ spawn: createRealSpawn(),
19863
19999
  cliRealpath: resolveCliRealpath(),
19864
20000
  log: (message) => console.log(message)
19865
20001
  })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "free-pi-cli",
3
- "version": "0.2.8",
3
+ "version": "0.2.10",
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",
@@ -15,11 +15,11 @@
15
15
  "node": ">=22.19.0"
16
16
  },
17
17
  "scripts": {
18
- "build": "bun build src/index.ts --target=node --outdir dist --splitting --external @earendil-works/pi-coding-agent --banner '#!/usr/bin/env node' && chmod +x dist/index.js",
18
+ "build": "rm -rf dist && bun build src/index.ts --target=node --outdir dist --splitting --external @earendil-works/pi-coding-agent --banner '#!/usr/bin/env node' && chmod +x dist/index.js",
19
19
  "prepublishOnly": "bun run build"
20
20
  },
21
21
  "dependencies": {
22
- "@earendil-works/pi-coding-agent": "^0.84.2"
22
+ "@earendil-works/pi-coding-agent": "0.84.4"
23
23
  },
24
24
  "devDependencies": {
25
25
  "@freepi/pi-ads": "workspace:*",