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.
- package/README.md +19 -0
- package/dist/index.js +135 -19
- 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(
|
|
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}
|
|
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}
|
|
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
|
-
|
|
14765
|
-
|
|
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 = {
|
|
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.
|
|
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": "
|
|
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:
|
|
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
|
-
|
|
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({
|
|
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((
|
|
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.
|
|
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": "
|
|
8
|
+
"free-pi-cli": "dist/index.js"
|
|
9
9
|
},
|
|
10
10
|
"files": [
|
|
11
11
|
"dist",
|