free-pi-cli 0.2.10 → 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-b5491jxx.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.10",
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",
@@ -14512,7 +14512,10 @@ var MeResponseSchema = exports_external.object({
14512
14512
  handle: exports_external.string(),
14513
14513
  remaining_usd_today: exports_external.number(),
14514
14514
  cap_usd_today: exports_external.number(),
14515
- tier: AccountTierSchema
14515
+ tier: AccountTierSchema,
14516
+ credit_usd: exports_external.number(),
14517
+ notice: exports_external.string().optional(),
14518
+ buy_url: exports_external.string().optional()
14516
14519
  });
14517
14520
  var MeStatsResponseSchema = exports_external.object({
14518
14521
  user_id: exports_external.string(),
@@ -14521,6 +14524,7 @@ var MeStatsResponseSchema = exports_external.object({
14521
14524
  cap_usd_today: exports_external.number(),
14522
14525
  spent_usd_today: exports_external.number(),
14523
14526
  remaining_usd_today: exports_external.number(),
14527
+ credit_usd: exports_external.number(),
14524
14528
  request_count_today: exports_external.number(),
14525
14529
  prompt_tokens_today: exports_external.number(),
14526
14530
  completion_tokens_today: exports_external.number(),
@@ -14915,6 +14919,12 @@ function inferErrorFromRemainingBudget(remainingUsdToday) {
14915
14919
 
14916
14920
  // ../pi-ads/src/meter.ts
14917
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
+ }
14918
14928
  async function renderMeter(deps, ctx) {
14919
14929
  const me = await fetchMe(deps);
14920
14930
  if (!me) {
@@ -14922,6 +14932,22 @@ async function renderMeter(deps, ctx) {
14922
14932
  return;
14923
14933
  }
14924
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;
14946
+ const notice = typeof me.notice === "string" ? sanitizeText(me.notice).trim() : "";
14947
+ if (notice !== "") {
14948
+ ctx.ui.setWidget(METER_WIDGET_ID, [renderErrorLine(notice, theme)]);
14949
+ return;
14950
+ }
14925
14951
  const errorCode = inferErrorFromRemainingBudget(me.remaining_usd_today);
14926
14952
  if (errorCode === undefined) {
14927
14953
  ctx.ui.setWidget(METER_WIDGET_ID, undefined);
@@ -19261,15 +19287,38 @@ __export(exports_typebox, {
19261
19287
  // src/usage-tool.ts
19262
19288
  var USAGE_TOOL_NAME = "free_pi_usage";
19263
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
+ }
19264
19295
  function formatStats(stats) {
19296
+ const credit = stats.credit_usd;
19265
19297
  return [
19266
- `tier: ${stats.tier} (cap $${stats.cap_usd_today.toFixed(2)}/day)`,
19267
- `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)`,
19300
+ ...typeof credit === "number" ? [`credit: $${credit.toFixed(2)} purchased usage remaining`] : [],
19268
19301
  `today tokens: ${stats.prompt_tokens_today} prompt / ${stats.completion_tokens_today} completion`,
19269
- `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`
19270
19303
  ].join(`
19271
19304
  `);
19272
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
+ }
19273
19322
  function createUsageToolExtension(opts) {
19274
19323
  return {
19275
19324
  name: "free-pi-usage-tool",
@@ -19277,40 +19326,121 @@ function createUsageToolExtension(opts) {
19277
19326
  pi.registerTool({
19278
19327
  name: USAGE_TOOL_NAME,
19279
19328
  label: "My Usage",
19280
- 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.",
19281
- 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",
19282
19331
  parameters: PARAMS,
19283
19332
  async execute() {
19284
- try {
19285
- const token = await opts.getToken();
19286
- const doFetch = opts.fetchImpl ?? fetch;
19287
- const res = await doFetch(`${opts.baseUrl}/me/stats`, {
19288
- headers: { Authorization: `Bearer ${token}` },
19289
- signal: AbortSignal.timeout(5000)
19290
- });
19291
- if (!res.ok) {
19292
- return {
19293
- content: [{ type: "text", text: `Could not fetch usage (HTTP ${res.status}).` }],
19294
- details: { ok: false }
19295
- };
19296
- }
19297
- const stats = await res.json();
19298
- return {
19299
- content: [{ type: "text", text: formatStats(stats) }],
19300
- details: stats
19301
- };
19302
- } catch (err) {
19333
+ const r = await fetchUsageText(opts);
19334
+ return {
19335
+ content: [{ type: "text", text: r.text }],
19336
+ details: r.ok ? r.stats : { ok: false }
19337
+ };
19338
+ }
19339
+ });
19340
+ }
19341
+ };
19342
+ }
19343
+
19344
+ // src/buy-tool.ts
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) {
19363
+ return `Opening the buy page in your browser. If it did not open (for example over SSH), visit:
19364
+ ${url2}
19365
+
19366
+ Packs: $5 → $4.00 of usage, $10 → $8.00. Card or USDC.`;
19367
+ }
19368
+ var BUY_TOOL_NAME = "free_pi_buy_credits";
19369
+ var PARAMS2 = exports_typebox.Object({});
19370
+ function createBuyToolExtension(opts) {
19371
+ return {
19372
+ name: "free-pi-buy-tool",
19373
+ factory(pi) {
19374
+ pi.registerTool({
19375
+ name: BUY_TOOL_NAME,
19376
+ label: "Buy Credits",
19377
+ description: "Opens the free-pi buy-credits page in your browser (and prints the URL, for SSH sessions with no browser). Read-only against the server, takes no arguments.",
19378
+ promptSnippet: "Buy more free-pi credits",
19379
+ parameters: PARAMS2,
19380
+ async execute() {
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") {
19303
19386
  return {
19304
- content: [{ type: "text", text: `Could not fetch usage: ${String(err)}` }],
19305
- details: { ok: false }
19387
+ content: [{ type: "text", text: BUY_NOT_AVAILABLE_TEXT }],
19388
+ details: { available: false }
19306
19389
  };
19307
19390
  }
19391
+ (opts.openBrowserImpl ?? openBrowser)(page.url);
19392
+ return {
19393
+ content: [{ type: "text", text: buyPageText(page.url) }],
19394
+ details: { buy_url: page.url }
19395
+ };
19308
19396
  }
19309
19397
  });
19310
19398
  }
19311
19399
  };
19312
19400
  }
19313
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
+
19314
19444
  // src/tool-guard.ts
19315
19445
  async function reportToolGuardTrigger(opts, toolName) {
19316
19446
  try {
@@ -19340,6 +19470,21 @@ function createToolGuardExtension(opts) {
19340
19470
 
19341
19471
  // src/changelog.ts
19342
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
+ },
19481
+ {
19482
+ version: "0.2.11",
19483
+ highlights: [
19484
+ 'Out of free usage? You can now buy more — $5 or $10 packs, card or USDC. Ask pi to "buy credits" or follow the link in the out-of-usage notice.',
19485
+ "The out-of-usage notice now comes from the server, and `free_pi_usage` shows your purchased balance."
19486
+ ]
19487
+ },
19343
19488
  {
19344
19489
  version: "0.2.10",
19345
19490
  highlights: [
@@ -19451,6 +19596,19 @@ async function runUpdate(ctx, deps = {}) {
19451
19596
  ctx.ui.notify("Update failed. Try manually: npm install -g free-pi-cli@latest", "error");
19452
19597
  }
19453
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
+ }
19454
19612
  function createFreePiCommandsExtension(opts) {
19455
19613
  return {
19456
19614
  name: "free-pi-commands",
@@ -19467,6 +19625,14 @@ function createFreePiCommandsExtension(opts) {
19467
19625
  description: "Update free-pi-cli to the latest version.",
19468
19626
  handler: (_args, ctx) => runUpdate(ctx)
19469
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
+ });
19470
19636
  }
19471
19637
  };
19472
19638
  }
@@ -19491,7 +19657,7 @@ function catalogModelsFor(opts) {
19491
19657
  const id = opts.model || MODEL_ID;
19492
19658
  return [{ id, name: id }];
19493
19659
  }
19494
- var ALLOWED_TOOL_NAMES = [...SAFE_TOOLS, USAGE_TOOL_NAME];
19660
+ var ALLOWED_TOOL_NAMES = [...SAFE_TOOLS, USAGE_TOOL_NAME, BUY_TOOL_NAME];
19495
19661
  function buildRuntimeOptions(opts, sessionId) {
19496
19662
  const models = catalogModelsFor(opts);
19497
19663
  const providerExtension = {
@@ -19509,6 +19675,11 @@ function buildRuntimeOptions(opts, sessionId) {
19509
19675
  baseUrl: opts.baseUrl,
19510
19676
  getToken: () => opts.jwt
19511
19677
  });
19678
+ const buyToolExtension = createBuyToolExtension({
19679
+ baseUrl: opts.baseUrl,
19680
+ getToken: () => opts.jwt
19681
+ });
19682
+ const errorNoticeExtension = createErrorNoticeExtension();
19512
19683
  const toolGuardExtension = createToolGuardExtension({
19513
19684
  baseUrl: opts.baseUrl,
19514
19685
  getToken: () => opts.jwt,
@@ -19522,6 +19693,8 @@ function buildRuntimeOptions(opts, sessionId) {
19522
19693
  providerExtension,
19523
19694
  adsExtension,
19524
19695
  usageToolExtension,
19696
+ buyToolExtension,
19697
+ errorNoticeExtension,
19525
19698
  toolGuardExtension,
19526
19699
  commandsExtension
19527
19700
  ];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "free-pi-cli",
3
- "version": "0.2.10",
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",