free-pi-cli 0.2.11 → 0.2.12

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-mh0wmxjc.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.12",
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",
@@ -14919,6 +14919,12 @@ function inferErrorFromRemainingBudget(remainingUsdToday) {
14919
14919
 
14920
14920
  // ../pi-ads/src/meter.ts
14921
14921
  var METER_WIDGET_ID = "freepi-meter";
14922
+ var CONFIRM_TURNS = 3;
14923
+ var lastCreditUsd;
14924
+ var confirm;
14925
+ function creditAddedMessage(addedUsd) {
14926
+ return `Payment received — $${addedUsd.toFixed(2)} of usage added to your balance.`;
14927
+ }
14922
14928
  async function renderMeter(deps, ctx) {
14923
14929
  const me = await fetchMe(deps);
14924
14930
  if (!me) {
@@ -14926,6 +14932,17 @@ async function renderMeter(deps, ctx) {
14926
14932
  return;
14927
14933
  }
14928
14934
  const theme = ctx.ui.theme;
14935
+ const credit = typeof me.credit_usd === "number" ? me.credit_usd : undefined;
14936
+ const added = credit !== undefined && lastCreditUsd !== undefined ? credit - lastCreditUsd : 0;
14937
+ lastCreditUsd = credit;
14938
+ if (added > 0)
14939
+ confirm = { addedUsd: added, turnsLeft: CONFIRM_TURNS };
14940
+ if (confirm && confirm.turnsLeft > 0) {
14941
+ confirm.turnsLeft--;
14942
+ ctx.ui.setWidget(METER_WIDGET_ID, [renderErrorLine(creditAddedMessage(confirm.addedUsd), theme)]);
14943
+ return;
14944
+ }
14945
+ confirm = undefined;
14929
14946
  const notice = typeof me.notice === "string" ? sanitizeText(me.notice).trim() : "";
14930
14947
  if (notice !== "") {
14931
14948
  ctx.ui.setWidget(METER_WIDGET_ID, [renderErrorLine(notice, theme)]);
@@ -19270,17 +19287,38 @@ __export(exports_typebox, {
19270
19287
  // src/usage-tool.ts
19271
19288
  var USAGE_TOOL_NAME = "free_pi_usage";
19272
19289
  var PARAMS = exports_typebox.Object({});
19290
+ function percentUsed(spentUsd, capUsd) {
19291
+ if (!(capUsd > 0))
19292
+ return 100;
19293
+ return Math.min(100, Math.max(0, Math.round(spentUsd / capUsd * 100)));
19294
+ }
19273
19295
  function formatStats(stats) {
19274
19296
  const credit = stats.credit_usd;
19275
19297
  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)`,
19298
+ `tier: ${stats.tier}`,
19299
+ `today: ${percentUsed(stats.spent_usd_today, stats.cap_usd_today)}% of your free daily allowance used, ${stats.request_count_today} request(s)`,
19278
19300
  ...typeof credit === "number" ? [`credit: $${credit.toFixed(2)} purchased usage remaining`] : [],
19279
19301
  `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`
19302
+ `lifetime: ${stats.lifetime.request_count} request(s), ${stats.lifetime.prompt_tokens} prompt / ${stats.lifetime.completion_tokens} completion tokens`
19281
19303
  ].join(`
19282
19304
  `);
19283
19305
  }
19306
+ async function fetchUsageText(opts) {
19307
+ try {
19308
+ const token = await opts.getToken();
19309
+ const doFetch = opts.fetchImpl ?? fetch;
19310
+ const res = await doFetch(`${opts.baseUrl}/me/stats`, {
19311
+ headers: { Authorization: `Bearer ${token}` },
19312
+ signal: AbortSignal.timeout(5000)
19313
+ });
19314
+ if (!res.ok)
19315
+ return { ok: false, text: `Could not fetch usage (HTTP ${res.status}).` };
19316
+ const stats = await res.json();
19317
+ return { ok: true, text: formatStats(stats), stats };
19318
+ } catch (err) {
19319
+ return { ok: false, text: `Could not fetch usage: ${String(err)}` };
19320
+ }
19321
+ }
19284
19322
  function createUsageToolExtension(opts) {
19285
19323
  return {
19286
19324
  name: "free-pi-usage-tool",
@@ -19288,34 +19326,15 @@ function createUsageToolExtension(opts) {
19288
19326
  pi.registerTool({
19289
19327
  name: USAGE_TOOL_NAME,
19290
19328
  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",
19329
+ 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.",
19330
+ promptSnippet: "Check your own free-pi usage and remaining daily allowance",
19293
19331
  parameters: PARAMS,
19294
19332
  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
- }
19333
+ const r = await fetchUsageText(opts);
19334
+ return {
19335
+ content: [{ type: "text", text: r.text }],
19336
+ details: r.ok ? r.stats : { ok: false }
19337
+ };
19319
19338
  }
19320
19339
  });
19321
19340
  }
@@ -19323,15 +19342,31 @@ function createUsageToolExtension(opts) {
19323
19342
  }
19324
19343
 
19325
19344
  // 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) {
19345
+ async function resolveBuyPage(opts) {
19346
+ try {
19347
+ const token = await opts.getToken();
19348
+ const doFetch = opts.fetchImpl ?? fetch;
19349
+ const res = await doFetch(`${opts.baseUrl}/me`, {
19350
+ headers: { Authorization: `Bearer ${token}` },
19351
+ signal: AbortSignal.timeout(5000)
19352
+ });
19353
+ if (!res.ok)
19354
+ return { kind: "error", text: `Could not fetch the buy page (HTTP ${res.status}).` };
19355
+ const me = await res.json();
19356
+ return me.buy_url ? { kind: "url", url: me.buy_url } : { kind: "unavailable" };
19357
+ } catch (err) {
19358
+ return { kind: "error", text: `Could not fetch the buy page: ${String(err)}` };
19359
+ }
19360
+ }
19361
+ var BUY_NOT_AVAILABLE_TEXT = "Buying credits is not available on this server yet.";
19362
+ function buyPageText(url2) {
19330
19363
  return `Opening the buy page in your browser. If it did not open (for example over SSH), visit:
19331
19364
  ${url2}
19332
19365
 
19333
19366
  Packs: $5 → $4.00 of usage, $10 → $8.00. Card or USDC.`;
19334
19367
  }
19368
+ var BUY_TOOL_NAME = "free_pi_buy_credits";
19369
+ var PARAMS2 = exports_typebox.Object({});
19335
19370
  function createBuyToolExtension(opts) {
19336
19371
  return {
19337
19372
  name: "free-pi-buy-tool",
@@ -19343,44 +19378,69 @@ function createBuyToolExtension(opts) {
19343
19378
  promptSnippet: "Buy more free-pi credits",
19344
19379
  parameters: PARAMS2,
19345
19380
  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) {
19381
+ const page = await resolveBuyPage(opts);
19382
+ if (page.kind === "error") {
19383
+ return { content: [{ type: "text", text: page.text }], details: { ok: false } };
19384
+ }
19385
+ if (page.kind === "unavailable") {
19373
19386
  return {
19374
- content: [{ type: "text", text: `Could not fetch the buy page: ${String(err)}` }],
19375
- details: { ok: false }
19387
+ content: [{ type: "text", text: BUY_NOT_AVAILABLE_TEXT }],
19388
+ details: { available: false }
19376
19389
  };
19377
19390
  }
19391
+ (opts.openBrowserImpl ?? openBrowser)(page.url);
19392
+ return {
19393
+ content: [{ type: "text", text: buyPageText(page.url) }],
19394
+ details: { buy_url: page.url }
19395
+ };
19378
19396
  }
19379
19397
  });
19380
19398
  }
19381
19399
  };
19382
19400
  }
19383
19401
 
19402
+ // src/error-notice.ts
19403
+ var NOTICE_DEDUPE_MS = 15000;
19404
+ function parseErrorEnvelope(errorMessage) {
19405
+ if (typeof errorMessage !== "string")
19406
+ return;
19407
+ const m = /^(\d{3}): (\{[\s\S]*\})\s*$/.exec(errorMessage);
19408
+ if (!m)
19409
+ return;
19410
+ try {
19411
+ const body = JSON.parse(m[2]);
19412
+ const message = body.message ?? body.error?.message;
19413
+ const code = body.code ?? body.error?.code;
19414
+ if (typeof message !== "string" || message.trim() === "")
19415
+ return;
19416
+ return { status: Number(m[1]), code: typeof code === "string" ? code : "unknown", message };
19417
+ } catch {
19418
+ return;
19419
+ }
19420
+ }
19421
+ function createErrorNoticeExtension(opts = {}) {
19422
+ const lastAt = new Map;
19423
+ const now = opts.now ?? Date.now;
19424
+ return {
19425
+ name: "free-pi-error-notice",
19426
+ factory(pi) {
19427
+ pi.on("turn_end", (event, ctx) => {
19428
+ const msg = event.message;
19429
+ if (!msg || msg.stopReason !== "error")
19430
+ return;
19431
+ const env = parseErrorEnvelope(msg.errorMessage);
19432
+ if (!env)
19433
+ return;
19434
+ const t = now();
19435
+ if (t - (lastAt.get(env.code) ?? -Infinity) < NOTICE_DEDUPE_MS)
19436
+ return;
19437
+ lastAt.set(env.code, t);
19438
+ ctx.ui.notify(env.message, env.status >= 500 ? "error" : "warning");
19439
+ });
19440
+ }
19441
+ };
19442
+ }
19443
+
19384
19444
  // src/tool-guard.ts
19385
19445
  async function reportToolGuardTrigger(opts, toolName) {
19386
19446
  try {
@@ -19410,6 +19470,14 @@ function createToolGuardExtension(opts) {
19410
19470
 
19411
19471
  // src/changelog.ts
19412
19472
  var CHANGELOG_HIGHLIGHTS = [
19473
+ {
19474
+ version: "0.2.12",
19475
+ highlights: [
19476
+ "New: /buy-credits and /usage slash commands — work instantly, even when you are out of usage.",
19477
+ "Free usage is shown as a percentage, never a dollar figure; purchased credit stays in dollars.",
19478
+ "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."
19479
+ ]
19480
+ },
19413
19481
  {
19414
19482
  version: "0.2.11",
19415
19483
  highlights: [
@@ -19528,6 +19596,19 @@ async function runUpdate(ctx, deps = {}) {
19528
19596
  ctx.ui.notify("Update failed. Try manually: npm install -g free-pi-cli@latest", "error");
19529
19597
  }
19530
19598
  }
19599
+ async function buyCredits(opts, ctx) {
19600
+ const page = await resolveBuyPage(opts);
19601
+ if (page.kind === "error")
19602
+ return ctx.ui.notify(page.text, "error");
19603
+ if (page.kind === "unavailable")
19604
+ return ctx.ui.notify(BUY_NOT_AVAILABLE_TEXT, "info");
19605
+ (opts.openBrowserImpl ?? openBrowser)(page.url);
19606
+ ctx.ui.notify(buyPageText(page.url), "info");
19607
+ }
19608
+ async function showUsage(opts, ctx) {
19609
+ const r = await fetchUsageText(opts);
19610
+ ctx.ui.notify(r.text, r.ok ? "info" : "error");
19611
+ }
19531
19612
  function createFreePiCommandsExtension(opts) {
19532
19613
  return {
19533
19614
  name: "free-pi-commands",
@@ -19544,6 +19625,14 @@ function createFreePiCommandsExtension(opts) {
19544
19625
  description: "Update free-pi-cli to the latest version.",
19545
19626
  handler: (_args, ctx) => runUpdate(ctx)
19546
19627
  });
19628
+ pi.registerCommand("usage", {
19629
+ description: "Show how much of today's free allowance you have used, and your purchased credit.",
19630
+ handler: (_args, ctx) => showUsage(opts, ctx)
19631
+ });
19632
+ pi.registerCommand("buy-credits", {
19633
+ description: "Buy more usage ($5 or $10 packs, card or USDC) — opens the buy page.",
19634
+ handler: (_args, ctx) => buyCredits(opts, ctx)
19635
+ });
19547
19636
  }
19548
19637
  };
19549
19638
  }
@@ -19590,6 +19679,7 @@ function buildRuntimeOptions(opts, sessionId) {
19590
19679
  baseUrl: opts.baseUrl,
19591
19680
  getToken: () => opts.jwt
19592
19681
  });
19682
+ const errorNoticeExtension = createErrorNoticeExtension();
19593
19683
  const toolGuardExtension = createToolGuardExtension({
19594
19684
  baseUrl: opts.baseUrl,
19595
19685
  getToken: () => opts.jwt,
@@ -19604,6 +19694,7 @@ function buildRuntimeOptions(opts, sessionId) {
19604
19694
  adsExtension,
19605
19695
  usageToolExtension,
19606
19696
  buyToolExtension,
19697
+ errorNoticeExtension,
19607
19698
  toolGuardExtension,
19608
19699
  commandsExtension
19609
19700
  ];
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.12",
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",