free-pi-cli 0.2.11 → 0.2.13

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-q2v4gzhd.js");
33
+ await import("./main-e58f4t8x.js");
@@ -48,7 +48,7 @@ function openBrowser(url, platform = process.platform, spawnImpl = spawn) {
48
48
  // package.json
49
49
  var package_default = {
50
50
  name: "free-pi-cli",
51
- version: "0.2.11",
51
+ version: "0.2.13",
52
52
  description: "Free, ad-supported distribution of the pi coding agent. Inference paid for by ads.",
53
53
  license: "MIT",
54
54
  type: "module",
@@ -14525,6 +14525,7 @@ var MeStatsResponseSchema = exports_external.object({
14525
14525
  spent_usd_today: exports_external.number(),
14526
14526
  remaining_usd_today: exports_external.number(),
14527
14527
  credit_usd: exports_external.number(),
14528
+ credit_credits: exports_external.number(),
14528
14529
  request_count_today: exports_external.number(),
14529
14530
  prompt_tokens_today: exports_external.number(),
14530
14531
  completion_tokens_today: exports_external.number(),
@@ -14591,7 +14592,7 @@ function assertExhaustiveErrorCode(code) {
14591
14592
  }
14592
14593
  // ../shared/src/config.ts
14593
14594
  var ConfigSchema = exports_external.object({
14594
- perUserDailyCapUsd: exports_external.number().default(5),
14595
+ perUserDailyCapUsd: exports_external.number().default(2.5),
14595
14596
  perUserRollingCapUsd: exports_external.number().default(0),
14596
14597
  rollingCapWindowHours: exports_external.number().int().default(5),
14597
14598
  globalDailyCapUsd: exports_external.number().default(50),
@@ -14719,6 +14720,11 @@ function compareVersions(a, b) {
14719
14720
  }
14720
14721
  return 0;
14721
14722
  }
14723
+ // ../shared/src/credits.ts
14724
+ var CREDITS_PER_USD = 40000 / 3;
14725
+ function usdToCredits(usd) {
14726
+ return Math.round(Number(usd) * CREDITS_PER_USD);
14727
+ }
14722
14728
  // ../pi-ads/src/api.ts
14723
14729
  var REQUEST_TIMEOUT_MS = 3000;
14724
14730
  async function authedFetch(deps, path, init) {
@@ -14919,6 +14925,12 @@ function inferErrorFromRemainingBudget(remainingUsdToday) {
14919
14925
 
14920
14926
  // ../pi-ads/src/meter.ts
14921
14927
  var METER_WIDGET_ID = "freepi-meter";
14928
+ var CONFIRM_TURNS = 3;
14929
+ var lastCreditUsd;
14930
+ var confirm;
14931
+ function creditAddedMessage(addedUsd) {
14932
+ return `Payment received — ${usdToCredits(addedUsd).toLocaleString("en-US")} credits added to your balance.`;
14933
+ }
14922
14934
  async function renderMeter(deps, ctx) {
14923
14935
  const me = await fetchMe(deps);
14924
14936
  if (!me) {
@@ -14926,6 +14938,17 @@ async function renderMeter(deps, ctx) {
14926
14938
  return;
14927
14939
  }
14928
14940
  const theme = ctx.ui.theme;
14941
+ const credit = typeof me.credit_usd === "number" ? me.credit_usd : undefined;
14942
+ const added = credit !== undefined && lastCreditUsd !== undefined ? credit - lastCreditUsd : 0;
14943
+ lastCreditUsd = credit;
14944
+ if (added > 0)
14945
+ confirm = { addedUsd: added, turnsLeft: CONFIRM_TURNS };
14946
+ if (confirm && confirm.turnsLeft > 0) {
14947
+ confirm.turnsLeft--;
14948
+ ctx.ui.setWidget(METER_WIDGET_ID, [renderErrorLine(creditAddedMessage(confirm.addedUsd), theme)]);
14949
+ return;
14950
+ }
14951
+ confirm = undefined;
14929
14952
  const notice = typeof me.notice === "string" ? sanitizeText(me.notice).trim() : "";
14930
14953
  if (notice !== "") {
14931
14954
  ctx.ui.setWidget(METER_WIDGET_ID, [renderErrorLine(notice, theme)]);
@@ -19270,17 +19293,38 @@ __export(exports_typebox, {
19270
19293
  // src/usage-tool.ts
19271
19294
  var USAGE_TOOL_NAME = "free_pi_usage";
19272
19295
  var PARAMS = exports_typebox.Object({});
19296
+ function percentUsed(spentUsd, capUsd) {
19297
+ if (!(capUsd > 0))
19298
+ return 100;
19299
+ return Math.min(100, Math.max(0, Math.round(spentUsd / capUsd * 100)));
19300
+ }
19273
19301
  function formatStats(stats) {
19274
- const credit = stats.credit_usd;
19302
+ const credits2 = stats.credit_credits;
19275
19303
  return [
19276
- `tier: ${stats.tier} (cap $${stats.cap_usd_today.toFixed(2)}/day)`,
19277
- `today: spent $${stats.spent_usd_today.toFixed(4)}, remaining $${stats.remaining_usd_today.toFixed(4)}, ${stats.request_count_today} request(s)`,
19278
- ...typeof credit === "number" ? [`credit: $${credit.toFixed(2)} purchased usage remaining`] : [],
19304
+ `tier: ${stats.tier}`,
19305
+ `today: ${percentUsed(stats.spent_usd_today, stats.cap_usd_today)}% of your free daily allowance used`,
19306
+ ...typeof credits2 === "number" ? [`credit: ${credits2.toLocaleString("en-US")} credits remaining`] : [],
19279
19307
  `today tokens: ${stats.prompt_tokens_today} prompt / ${stats.completion_tokens_today} completion`,
19280
- `lifetime: spent $${stats.lifetime.spent_usd.toFixed(4)}, ${stats.lifetime.request_count} request(s), ${stats.lifetime.prompt_tokens} prompt / ${stats.lifetime.completion_tokens} completion tokens`
19308
+ `lifetime: ${stats.lifetime.prompt_tokens} prompt / ${stats.lifetime.completion_tokens} completion tokens`
19281
19309
  ].join(`
19282
19310
  `);
19283
19311
  }
19312
+ async function fetchUsageText(opts) {
19313
+ try {
19314
+ const token = await opts.getToken();
19315
+ const doFetch = opts.fetchImpl ?? fetch;
19316
+ const res = await doFetch(`${opts.baseUrl}/me/stats`, {
19317
+ headers: { Authorization: `Bearer ${token}` },
19318
+ signal: AbortSignal.timeout(5000)
19319
+ });
19320
+ if (!res.ok)
19321
+ return { ok: false, text: `Could not fetch usage (HTTP ${res.status}).` };
19322
+ const stats = await res.json();
19323
+ return { ok: true, text: formatStats(stats), stats };
19324
+ } catch (err) {
19325
+ return { ok: false, text: `Could not fetch usage: ${String(err)}` };
19326
+ }
19327
+ }
19284
19328
  function createUsageToolExtension(opts) {
19285
19329
  return {
19286
19330
  name: "free-pi-usage-tool",
@@ -19288,34 +19332,15 @@ function createUsageToolExtension(opts) {
19288
19332
  pi.registerTool({
19289
19333
  name: USAGE_TOOL_NAME,
19290
19334
  label: "My Usage",
19291
- description: "Reports the caller's own free-pi spend, remaining daily budget, token counts, and account tier. Read-only, takes no arguments, makes no changes.",
19292
- promptSnippet: "Check your own free-pi usage, spend, and remaining daily budget",
19335
+ description: "Reports how much of the caller's free daily allowance is used (as a percentage), purchased credit, token counts, and account tier. Read-only, takes no arguments, makes no changes.",
19336
+ promptSnippet: "Check your own free-pi usage and remaining daily allowance",
19293
19337
  parameters: PARAMS,
19294
19338
  async execute() {
19295
- try {
19296
- const token = await opts.getToken();
19297
- const doFetch = opts.fetchImpl ?? fetch;
19298
- const res = await doFetch(`${opts.baseUrl}/me/stats`, {
19299
- headers: { Authorization: `Bearer ${token}` },
19300
- signal: AbortSignal.timeout(5000)
19301
- });
19302
- if (!res.ok) {
19303
- return {
19304
- content: [{ type: "text", text: `Could not fetch usage (HTTP ${res.status}).` }],
19305
- details: { ok: false }
19306
- };
19307
- }
19308
- const stats = await res.json();
19309
- return {
19310
- content: [{ type: "text", text: formatStats(stats) }],
19311
- details: stats
19312
- };
19313
- } catch (err) {
19314
- return {
19315
- content: [{ type: "text", text: `Could not fetch usage: ${String(err)}` }],
19316
- details: { ok: false }
19317
- };
19318
- }
19339
+ const r = await fetchUsageText(opts);
19340
+ return {
19341
+ content: [{ type: "text", text: r.text }],
19342
+ details: r.ok ? r.stats : { ok: false }
19343
+ };
19319
19344
  }
19320
19345
  });
19321
19346
  }
@@ -19323,15 +19348,31 @@ function createUsageToolExtension(opts) {
19323
19348
  }
19324
19349
 
19325
19350
  // src/buy-tool.ts
19326
- var BUY_TOOL_NAME = "free_pi_buy_credits";
19327
- var PARAMS2 = exports_typebox.Object({});
19328
- var NOT_AVAILABLE_TEXT = "Buying credits is not available on this server yet.";
19329
- function buyText(url2) {
19351
+ async function resolveBuyPage(opts) {
19352
+ try {
19353
+ const token = await opts.getToken();
19354
+ const doFetch = opts.fetchImpl ?? fetch;
19355
+ const res = await doFetch(`${opts.baseUrl}/me`, {
19356
+ headers: { Authorization: `Bearer ${token}` },
19357
+ signal: AbortSignal.timeout(5000)
19358
+ });
19359
+ if (!res.ok)
19360
+ return { kind: "error", text: `Could not fetch the buy page (HTTP ${res.status}).` };
19361
+ const me = await res.json();
19362
+ return me.buy_url ? { kind: "url", url: me.buy_url } : { kind: "unavailable" };
19363
+ } catch (err) {
19364
+ return { kind: "error", text: `Could not fetch the buy page: ${String(err)}` };
19365
+ }
19366
+ }
19367
+ var BUY_NOT_AVAILABLE_TEXT = "Buying credits is not available on this server yet.";
19368
+ function buyPageText(url2) {
19330
19369
  return `Opening the buy page in your browser. If it did not open (for example over SSH), visit:
19331
19370
  ${url2}
19332
19371
 
19333
- Packs: $5 → $4.00 of usage, $10 → $8.00. Card or USDC.`;
19372
+ Packs: $5 → 50,000 credits, $10 → 100,000 credits. Card or USDC.`;
19334
19373
  }
19374
+ var BUY_TOOL_NAME = "free_pi_buy_credits";
19375
+ var PARAMS2 = exports_typebox.Object({});
19335
19376
  function createBuyToolExtension(opts) {
19336
19377
  return {
19337
19378
  name: "free-pi-buy-tool",
@@ -19343,44 +19384,69 @@ function createBuyToolExtension(opts) {
19343
19384
  promptSnippet: "Buy more free-pi credits",
19344
19385
  parameters: PARAMS2,
19345
19386
  async execute() {
19346
- try {
19347
- const token = await opts.getToken();
19348
- const doFetch = opts.fetchImpl ?? fetch;
19349
- const openBrowserFn = opts.openBrowserImpl ?? openBrowser;
19350
- const res = await doFetch(`${opts.baseUrl}/me`, {
19351
- headers: { Authorization: `Bearer ${token}` },
19352
- signal: AbortSignal.timeout(5000)
19353
- });
19354
- if (!res.ok) {
19355
- return {
19356
- content: [{ type: "text", text: `Could not fetch the buy page (HTTP ${res.status}).` }],
19357
- details: { ok: false }
19358
- };
19359
- }
19360
- const me = await res.json();
19361
- if (!me.buy_url) {
19362
- return {
19363
- content: [{ type: "text", text: NOT_AVAILABLE_TEXT }],
19364
- details: { available: false }
19365
- };
19366
- }
19367
- openBrowserFn(me.buy_url);
19368
- return {
19369
- content: [{ type: "text", text: buyText(me.buy_url) }],
19370
- details: { buy_url: me.buy_url }
19371
- };
19372
- } catch (err) {
19387
+ const page = await resolveBuyPage(opts);
19388
+ if (page.kind === "error") {
19389
+ return { content: [{ type: "text", text: page.text }], details: { ok: false } };
19390
+ }
19391
+ if (page.kind === "unavailable") {
19373
19392
  return {
19374
- content: [{ type: "text", text: `Could not fetch the buy page: ${String(err)}` }],
19375
- details: { ok: false }
19393
+ content: [{ type: "text", text: BUY_NOT_AVAILABLE_TEXT }],
19394
+ details: { available: false }
19376
19395
  };
19377
19396
  }
19397
+ (opts.openBrowserImpl ?? openBrowser)(page.url);
19398
+ return {
19399
+ content: [{ type: "text", text: buyPageText(page.url) }],
19400
+ details: { buy_url: page.url }
19401
+ };
19378
19402
  }
19379
19403
  });
19380
19404
  }
19381
19405
  };
19382
19406
  }
19383
19407
 
19408
+ // src/error-notice.ts
19409
+ var NOTICE_DEDUPE_MS = 15000;
19410
+ function parseErrorEnvelope(errorMessage) {
19411
+ if (typeof errorMessage !== "string")
19412
+ return;
19413
+ const m = /^(\d{3}): (\{[\s\S]*\})\s*$/.exec(errorMessage);
19414
+ if (!m)
19415
+ return;
19416
+ try {
19417
+ const body = JSON.parse(m[2]);
19418
+ const message = body.message ?? body.error?.message;
19419
+ const code = body.code ?? body.error?.code;
19420
+ if (typeof message !== "string" || message.trim() === "")
19421
+ return;
19422
+ return { status: Number(m[1]), code: typeof code === "string" ? code : "unknown", message };
19423
+ } catch {
19424
+ return;
19425
+ }
19426
+ }
19427
+ function createErrorNoticeExtension(opts = {}) {
19428
+ const lastAt = new Map;
19429
+ const now = opts.now ?? Date.now;
19430
+ return {
19431
+ name: "free-pi-error-notice",
19432
+ factory(pi) {
19433
+ pi.on("turn_end", (event, ctx) => {
19434
+ const msg = event.message;
19435
+ if (!msg || msg.stopReason !== "error")
19436
+ return;
19437
+ const env = parseErrorEnvelope(msg.errorMessage);
19438
+ if (!env)
19439
+ return;
19440
+ const t = now();
19441
+ if (t - (lastAt.get(env.code) ?? -Infinity) < NOTICE_DEDUPE_MS)
19442
+ return;
19443
+ lastAt.set(env.code, t);
19444
+ ctx.ui.notify(env.message, env.status >= 500 ? "error" : "warning");
19445
+ });
19446
+ }
19447
+ };
19448
+ }
19449
+
19384
19450
  // src/tool-guard.ts
19385
19451
  async function reportToolGuardTrigger(opts, toolName) {
19386
19452
  try {
@@ -19410,6 +19476,21 @@ function createToolGuardExtension(opts) {
19410
19476
 
19411
19477
  // src/changelog.ts
19412
19478
  var CHANGELOG_HIGHLIGHTS = [
19479
+ {
19480
+ version: "0.2.13",
19481
+ highlights: [
19482
+ "Purchased balance is now shown as credits — $5 buys 50,000 credits, $10 buys 100,000.",
19483
+ "The usage summary is cleaner: your free allowance still shows as a percentage, with token counts, and no request tally."
19484
+ ]
19485
+ },
19486
+ {
19487
+ version: "0.2.12",
19488
+ highlights: [
19489
+ "New: /buy-credits and /usage slash commands — work instantly, even when you are out of usage.",
19490
+ "Free usage is shown as a percentage, never a dollar figure; purchased credit stays in dollars.",
19491
+ "Clearer messages: one readable line when a request is rejected, a heads-up at 80% of your daily allowance, and a confirmation when a purchase lands."
19492
+ ]
19493
+ },
19413
19494
  {
19414
19495
  version: "0.2.11",
19415
19496
  highlights: [
@@ -19528,6 +19609,19 @@ async function runUpdate(ctx, deps = {}) {
19528
19609
  ctx.ui.notify("Update failed. Try manually: npm install -g free-pi-cli@latest", "error");
19529
19610
  }
19530
19611
  }
19612
+ async function buyCredits(opts, ctx) {
19613
+ const page = await resolveBuyPage(opts);
19614
+ if (page.kind === "error")
19615
+ return ctx.ui.notify(page.text, "error");
19616
+ if (page.kind === "unavailable")
19617
+ return ctx.ui.notify(BUY_NOT_AVAILABLE_TEXT, "info");
19618
+ (opts.openBrowserImpl ?? openBrowser)(page.url);
19619
+ ctx.ui.notify(buyPageText(page.url), "info");
19620
+ }
19621
+ async function showUsage(opts, ctx) {
19622
+ const r = await fetchUsageText(opts);
19623
+ ctx.ui.notify(r.text, r.ok ? "info" : "error");
19624
+ }
19531
19625
  function createFreePiCommandsExtension(opts) {
19532
19626
  return {
19533
19627
  name: "free-pi-commands",
@@ -19544,6 +19638,14 @@ function createFreePiCommandsExtension(opts) {
19544
19638
  description: "Update free-pi-cli to the latest version.",
19545
19639
  handler: (_args, ctx) => runUpdate(ctx)
19546
19640
  });
19641
+ pi.registerCommand("usage", {
19642
+ description: "Show how much of today's free allowance you have used, and your purchased credit.",
19643
+ handler: (_args, ctx) => showUsage(opts, ctx)
19644
+ });
19645
+ pi.registerCommand("buy-credits", {
19646
+ description: "Buy more usage ($5 or $10 packs, card or USDC) — opens the buy page.",
19647
+ handler: (_args, ctx) => buyCredits(opts, ctx)
19648
+ });
19547
19649
  }
19548
19650
  };
19549
19651
  }
@@ -19590,6 +19692,7 @@ function buildRuntimeOptions(opts, sessionId) {
19590
19692
  baseUrl: opts.baseUrl,
19591
19693
  getToken: () => opts.jwt
19592
19694
  });
19695
+ const errorNoticeExtension = createErrorNoticeExtension();
19593
19696
  const toolGuardExtension = createToolGuardExtension({
19594
19697
  baseUrl: opts.baseUrl,
19595
19698
  getToken: () => opts.jwt,
@@ -19604,6 +19707,7 @@ function buildRuntimeOptions(opts, sessionId) {
19604
19707
  adsExtension,
19605
19708
  usageToolExtension,
19606
19709
  buyToolExtension,
19710
+ errorNoticeExtension,
19607
19711
  toolGuardExtension,
19608
19712
  commandsExtension
19609
19713
  ];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "free-pi-cli",
3
- "version": "0.2.11",
3
+ "version": "0.2.13",
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",