clixad 0.0.1-beta.20 → 0.0.1-beta.21

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.
Files changed (2) hide show
  1. package/dist/clixad.mjs +212 -120
  2. package/package.json +1 -1
package/dist/clixad.mjs CHANGED
@@ -168,13 +168,13 @@ function encodeChatBody(json) {
168
168
  }
169
169
  function adsPerTaskLabel(estAdsPerTask) {
170
170
  const effort = estAdsPerTask >= 1 ? `~${estAdsPerTask} ads/task` : "<1 ad/task";
171
- return `${effort} on completion`;
171
+ return `${effort} on completion (median)`;
172
172
  }
173
- function grantRangeLabel(range) {
174
- if (!range) return "credits on completion";
175
- const [low, high] = range;
176
- const n = (v) => v.toLocaleString("en-US");
177
- return low === high ? `${n(low)} credits on completion` : `${n(low)}\u2013${n(high)} credits on completion`;
173
+ function grantLabel(grantPerAd) {
174
+ if (typeof grantPerAd !== "number" || !Number.isFinite(grantPerAd) || grantPerAd <= 0) {
175
+ return "credits on completion";
176
+ }
177
+ return `~${grantPerAd.toLocaleString("en-US")} credits on completion (median)`;
178
178
  }
179
179
  function streakLabel(streak) {
180
180
  if (streak.length <= 0 || streak.day <= 0) return null;
@@ -252,12 +252,7 @@ function asSignupClosed(status, data) {
252
252
  return d && d.error === "signup_closed" && typeof d.message === "string" ? { error: d.error, message: d.message, waitlist_url: d.waitlist_url ?? "https://clixad.io" } : void 0;
253
253
  }
254
254
  function paywallError(body) {
255
- return new PaywallError(
256
- body.balance,
257
- body.estimated_cost,
258
- body.grant_per_ad,
259
- body.grant_per_ad_range
260
- );
255
+ return new PaywallError(body.balance, body.estimated_cost, body.grant_per_ad);
261
256
  }
262
257
  async function authFailure(res, what) {
263
258
  const body = await res.text().catch(() => "");
@@ -366,18 +361,16 @@ var init_client = __esm({
366
361
  waitlistUrl;
367
362
  };
368
363
  PaywallError = class extends Error {
369
- constructor(balance, estimatedCost, grantPerAd, grantRange) {
364
+ constructor(balance, estimatedCost, grantPerAd) {
370
365
  super("Out of credits");
371
366
  this.balance = balance;
372
367
  this.estimatedCost = estimatedCost;
373
368
  this.grantPerAd = grantPerAd;
374
- this.grantRange = grantRange;
375
369
  this.name = "PaywallError";
376
370
  }
377
371
  balance;
378
372
  estimatedCost;
379
373
  grantPerAd;
380
- grantRange;
381
374
  };
382
375
  AuthError = class extends Error {
383
376
  constructor() {
@@ -610,8 +603,13 @@ var init_client = __esm({
610
603
  balance: typeof body.balance === "number" ? body.balance : 0
611
604
  };
612
605
  }
613
- async creditPacks() {
614
- const res = await fetch(`${this.config.gatewayUrl}/v1/billing/packs`);
606
+ /**
607
+ * Both billing calls take a signal, for the reason `models` does: the only
608
+ * surface that buys credits is now `/buy` at the REPL prompt, which awaits
609
+ * them with the input box locked and "esc to stop" on screen.
610
+ */
611
+ async creditPacks(signal) {
612
+ const res = await fetch(`${this.config.gatewayUrl}/v1/billing/packs`, { signal });
615
613
  if (!res.ok) throw new Error(`packs failed: ${res.status}`);
616
614
  return res.json();
617
615
  }
@@ -621,10 +619,11 @@ var init_client = __esm({
621
619
  * the gateway refuses a checkout without it, because starting a purchase we
622
620
  * could not deliver instantly is worse than not starting one.
623
621
  */
624
- async checkout(pack, consent) {
622
+ async checkout(pack, consent, signal) {
625
623
  const res = await fetch(`${this.config.gatewayUrl}/v1/billing/checkout`, {
626
624
  method: "POST",
627
625
  headers: this.headers(),
626
+ ...signal ? { signal } : {},
628
627
  body: JSON.stringify({ pack, consent })
629
628
  });
630
629
  if (res.status === 401) throw await authFailure(res, "checkout");
@@ -632,7 +631,7 @@ var init_client = __esm({
632
631
  if (!res.ok) throw new Error(await httpFailure(res, "checkout"));
633
632
  return res.json();
634
633
  }
635
- // Both take an optional signal for the REPL's sake: they are awaited while the
634
+ // These take an optional signal for the REPL's sake: they are awaited while the
636
635
  // input box is locked, and a gateway that is cold-starting (Render's free tier,
637
636
  // the better part of a minute) would otherwise be indistinguishable from a
638
637
  // hung terminal. Nothing else passes one — a one-shot CLI command has Ctrl+C.
@@ -4730,7 +4729,7 @@ var init_lines = __esm({
4730
4729
  { id: "wallet", text: "/wallet shows your balance and what you've earned today" },
4731
4730
  { id: "cheapest-first", text: "/model lists cheapest-turn-first" },
4732
4731
  { id: "model-cost", text: "current model: {ads_per_task}", needs: "ads_per_task" },
4733
- { id: "earn", text: "/earn: offers pay {grant_range}", needs: "grant_range" },
4732
+ { id: "earn", text: "/earn: offers pay {grant}", needs: "grant" },
4734
4733
  { id: "screenout", text: "a screenout pays nothing and is normal \u2014 just start another" }
4735
4734
  ];
4736
4735
  }
@@ -4739,7 +4738,7 @@ var init_lines = __esm({
4739
4738
  // src/sponsor/provider.ts
4740
4739
  function fill(line2, facts) {
4741
4740
  if (!line2.needs) return line2.text;
4742
- const value = line2.needs === "grant_range" ? facts.grantRange : facts.adsPerTask;
4741
+ const value = line2.needs === "grant" ? facts.grant : facts.adsPerTask;
4743
4742
  if (!value) return null;
4744
4743
  return line2.text.replace(`{${line2.needs}}`, value);
4745
4744
  }
@@ -4934,6 +4933,12 @@ var init_commands = __esm({
4934
4933
  // completing to "/redeem " is the helpful outcome rather than the surprising
4935
4934
  // one.
4936
4935
  { name: "redeem", args: "<code>", desc: "spend a code you were given" },
4936
+ // The only purchase surface in the CLI. It used to be `clixad buy`, a
4937
+ // subcommand you had to quit the REPL to reach — and one nobody found, since
4938
+ // the REPL is the default entry point and never mentioned it. Deliberately
4939
+ // *no* `args` hint, for `/model`'s reason: an arg hint makes enter complete
4940
+ // the command instead of running it, and the pack picker is the point.
4941
+ { name: "buy", desc: "buy a credit pack (optional \u2014 offers are free)" },
4937
4942
  // Reachable from inside the REPL because that is where the 401 is printed.
4938
4943
  // "Run `clixad login` first" is not a runnable instruction at this prompt —
4939
4944
  // anything without a leading slash is a prompt, so it went to the model and
@@ -5383,6 +5388,23 @@ var init_views = __esm({
5383
5388
  import { useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
5384
5389
  import { Box as Box2, Static, Text as Text2, useApp, useInput, usePaste, useStdout } from "ink";
5385
5390
  import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
5391
+ function buyDeclarationLines(pack) {
5392
+ return [
5393
+ `${pack.label} \u2014 ${pack.credits.toLocaleString("en-US")} credits for $${pack.price_usd.toFixed(2)}`,
5394
+ "",
5395
+ "Before you pay, please confirm:",
5396
+ " \xB7 You request that we deliver the credits immediately, before the 14-day withdrawal period has expired.",
5397
+ " \xB7 You acknowledge that your right of withdrawal lapses once they have been delivered."
5398
+ ].join("\n");
5399
+ }
5400
+ function buyConfirmLines(pack) {
5401
+ return [
5402
+ buyDeclarationLines(pack),
5403
+ "",
5404
+ "One purchase. No subscription, nothing to cancel.",
5405
+ `Terms: ${TERMS_URL}`
5406
+ ].join("\n");
5407
+ }
5386
5408
  function useTerminalSize() {
5387
5409
  const { stdout } = useStdout();
5388
5410
  const snapshot2 = useRef({
@@ -5453,6 +5475,7 @@ function App({ client, config, wallet, streak, session, initialTask, notices: no
5453
5475
  const [plan, setPlan] = useState(null);
5454
5476
  const [picker, setPicker] = useState(null);
5455
5477
  const [pickerSel, setPickerSel] = useState(0);
5478
+ const [buy, setBuy] = useState(null);
5456
5479
  const [tick, setTick] = useState(0);
5457
5480
  const [startedAt, setStartedAt] = useState(0);
5458
5481
  const [quitHint, setQuitHint] = useState(false);
@@ -5479,6 +5502,7 @@ function App({ client, config, wallet, streak, session, initialTask, notices: no
5479
5502
  const outputsRef = useRef([]);
5480
5503
  const expandedRef = useRef(/* @__PURE__ */ new Set());
5481
5504
  const pickerWindowRef = useRef({ from: 0, count: 0 });
5505
+ const buyFitsRef = useRef(false);
5482
5506
  const spinnerRef = useRef(spinnerFrames());
5483
5507
  const runningToolRef = useRef(null);
5484
5508
  const pendingTaskRef = useRef(null);
@@ -5625,7 +5649,7 @@ function App({ client, config, wallet, streak, session, initialTask, notices: no
5625
5649
  }
5626
5650
  const est = catalogRef.current.find((m) => m.id === modelId)?.est_ads_per_task;
5627
5651
  const facts = {
5628
- grantRange: wallet?.grant_per_ad_range ? grantRangeLabel(wallet.grant_per_ad_range) : void 0,
5652
+ grant: wallet?.grant_per_ad ? grantLabel(wallet.grant_per_ad) : void 0,
5629
5653
  adsPerTask: est === void 0 ? void 0 : adsPerTaskLabel(est)
5630
5654
  };
5631
5655
  const picked = sponsorRef.current.pick(sponsorIdxRef.current++, sponsorPrevRef.current, facts);
@@ -5890,9 +5914,10 @@ ${NO_SEARCH_INSTRUCTION}`),
5890
5914
  kind: "notice",
5891
5915
  tone: "warn",
5892
5916
  text: ` Out of credits (balance ${err.balance}, need ~${err.estimatedCost}).
5893
- Press enter to open the ad wall \u2014 offers pay ${grantRangeLabel(err.grantRange)}.
5917
+ Press enter to open the ad wall \u2014 offers pay ${grantLabel(err.grantPerAd)}.
5894
5918
  I'll continue automatically. Offers you are screened out of pay nothing;
5895
- that is normal, just start another.`
5919
+ that is normal, just start another.
5920
+ /buy is there too, if you would rather pay than do an offer.`
5896
5921
  });
5897
5922
  } else if (err instanceof AuthError) {
5898
5923
  push({ kind: "notice", tone: "error", text: NOT_SIGNED_IN });
@@ -6048,6 +6073,83 @@ ${NO_SEARCH_INSTRUCTION}`),
6048
6073
  push({ kind: "notice", tone: "good", text: " credits added \u2014 continuing" });
6049
6074
  if (pending) await runTurn2(pending.task, pending.images);
6050
6075
  }, [balance, client, config.dashboardUrl, push, runBusy, runTurn2]);
6076
+ const runBuy = useCallback(async () => {
6077
+ await runBusy(LOADING, async (signal) => {
6078
+ const { enabled, packs } = await client.creditPacks(signal);
6079
+ if (!enabled || !packs.length) {
6080
+ return void push({
6081
+ kind: "notice",
6082
+ tone: "warn",
6083
+ text: " buying credits isn't enabled on this gateway \u2014 /earn tops up the wallet for free."
6084
+ });
6085
+ }
6086
+ const items = packs.map((pack) => ({
6087
+ value: pack.id,
6088
+ label: pack.label,
6089
+ hint: `${pack.credits.toLocaleString("en-US")} credits $${pack.price_usd.toFixed(2)}`
6090
+ }));
6091
+ setPickerSel(0);
6092
+ setPicker({
6093
+ title: "Buy credits",
6094
+ subtitle: "Optional \u2014 everything here can be paid for with credits you earn.",
6095
+ items,
6096
+ // No `onCancel`: nothing is awaiting this. Closing it is the end of it,
6097
+ // exactly as with `/model`.
6098
+ onPick: (choice) => {
6099
+ const pack = packs.find((x) => x.id === choice.value);
6100
+ if (pack) setBuy({ pack, typed: "" });
6101
+ }
6102
+ });
6103
+ });
6104
+ }, [client, push, runBusy]);
6105
+ const runCheckout = useCallback(
6106
+ async (pack) => {
6107
+ const before = balance;
6108
+ let credited = false;
6109
+ let stopped = false;
6110
+ await runBusy(WAITING_FOR_PAYMENT, async (signal) => {
6111
+ const checkout = await client.checkout(
6112
+ pack.id,
6113
+ {
6114
+ requestImmediatePerformance: true,
6115
+ acknowledgeLossOfWithdrawal: true,
6116
+ at: (/* @__PURE__ */ new Date()).toISOString()
6117
+ },
6118
+ signal
6119
+ );
6120
+ openBrowser(checkout.url);
6121
+ push({
6122
+ kind: "notice",
6123
+ text: ` opened Stripe Checkout \u2014 ${checkout.credits.toLocaleString("en-US")} credits for $${checkout.price_usd.toFixed(2)}
6124
+ ${checkout.url}`
6125
+ });
6126
+ const deadline = Date.now() + 5 * 6e4;
6127
+ while (Date.now() < deadline && !credited && !signal.aborted) {
6128
+ await sleep4(3e3, signal);
6129
+ if (signal.aborted) break;
6130
+ const w = await client.wallet(signal).catch(() => void 0);
6131
+ if (w && w.balance > before) {
6132
+ setBalance(w.balance);
6133
+ credited = true;
6134
+ }
6135
+ }
6136
+ stopped = signal.aborted;
6137
+ });
6138
+ if (credited) {
6139
+ return push({
6140
+ kind: "notice",
6141
+ tone: "good",
6142
+ text: " credits added \u2014 an invoice is on its way to the address on the payment."
6143
+ });
6144
+ }
6145
+ push({
6146
+ kind: "notice",
6147
+ tone: "warn",
6148
+ text: stopped ? " stopped waiting. Credits still land when the payment clears \u2014 /wallet checks the balance." : " no credits yet \u2014 the payment may still be clearing. /wallet checks the balance."
6149
+ });
6150
+ },
6151
+ [balance, client, push, runBusy]
6152
+ );
6051
6153
  const runCommand = useCallback(
6052
6154
  async (line2) => {
6053
6155
  const [cmd, ...rest] = line2.slice(1).split(" ");
@@ -6293,6 +6395,9 @@ ${NO_SEARCH_INSTRUCTION}`),
6293
6395
  push({ kind: "notice", text: referLines(info).map((line3) => ` ${line3}`).join("\n") });
6294
6396
  });
6295
6397
  return;
6398
+ case "buy":
6399
+ await runBuy();
6400
+ return;
6296
6401
  case "redeem": {
6297
6402
  if (!arg) {
6298
6403
  push({ kind: "notice", tone: "warn", text: " usage: /redeem <code>" });
@@ -6317,7 +6422,7 @@ ${NO_SEARCH_INSTRUCTION}`),
6317
6422
  push({ kind: "notice", tone: "warn", text: ` unknown command: /${cmd} \u2014 try /help` });
6318
6423
  }
6319
6424
  },
6320
- [client, cols, config, cycleMode, exit, model, push, root, runAdWall, runBusy, runLogin, runTurn2, writeToStdout]
6425
+ [client, cols, config, cycleMode, exit, model, push, root, runAdWall, runBusy, runBuy, runLogin, runTurn2, writeToStdout]
6321
6426
  );
6322
6427
  const runShell = useCallback(
6323
6428
  async (command) => {
@@ -6460,7 +6565,7 @@ ${output}` }
6460
6565
  }) : [],
6461
6566
  [query, root]
6462
6567
  );
6463
- const menu = menuOff || ask2 || picker || plan ? [] : matches;
6568
+ const menu = menuOff || ask2 || picker || plan || buy ? [] : matches;
6464
6569
  useEffect(() => {
6465
6570
  setSel(0);
6466
6571
  setMenuOff(false);
@@ -6474,7 +6579,7 @@ ${output}` }
6474
6579
  [cursor, query, text]
6475
6580
  );
6476
6581
  usePaste((text2) => {
6477
- if (ask2 || picker || plan) return;
6582
+ if (ask2 || picker || plan || buy) return;
6478
6583
  setEditor((state) => insert(state, text2));
6479
6584
  setHistIdx(-1);
6480
6585
  });
@@ -6492,7 +6597,8 @@ ${output}` }
6492
6597
  setPicker(null);
6493
6598
  cancel?.();
6494
6599
  }
6495
- }, [ask2, picker, plan]);
6600
+ if (buy) setBuy(null);
6601
+ }, [ask2, buy, picker, plan]);
6496
6602
  useInput((ch, key) => {
6497
6603
  if (key.ctrl && ch === "c") {
6498
6604
  if (busy) {
@@ -6568,6 +6674,37 @@ ${output}` }
6568
6674
  }
6569
6675
  return;
6570
6676
  }
6677
+ if (buy) {
6678
+ const cancelled = () => push({ kind: "notice", tone: "warn", text: " cancelled \u2014 you have not been charged." });
6679
+ if (key.escape) {
6680
+ setBuy(null);
6681
+ cancelled();
6682
+ return;
6683
+ }
6684
+ if (key.return) {
6685
+ const confirmed = buy.typed.trim().toLowerCase() === BUY_CONFIRM_WORD;
6686
+ if (confirmed && !buyFitsRef.current) {
6687
+ return void push({
6688
+ kind: "notice",
6689
+ tone: "warn",
6690
+ text: " the confirmation does not fit on this terminal \u2014 make it taller, then answer."
6691
+ });
6692
+ }
6693
+ const { pack } = buy;
6694
+ setBuy(null);
6695
+ if (!confirmed) return void cancelled();
6696
+ void runCheckout(pack);
6697
+ return;
6698
+ }
6699
+ if (key.backspace || key.delete) {
6700
+ setBuy((b) => b ? { ...b, typed: b.typed.slice(0, -1) } : b);
6701
+ return;
6702
+ }
6703
+ if (ch && !key.ctrl && !key.meta && /^[\x20-\x7e]+$/.test(ch)) {
6704
+ setBuy((b) => b ? { ...b, typed: (b.typed + ch).slice(0, BUY_TYPED_MAX) } : b);
6705
+ }
6706
+ return;
6707
+ }
6571
6708
  if (busy && key.escape) {
6572
6709
  const dropped = queueRef.current;
6573
6710
  if (dropped.length) {
@@ -6648,7 +6785,8 @@ ${dropped.map((l) => ` \xB7 ${l}`).join("\n")}`
6648
6785
  ...queueView.length > MAX_QUEUE_ROWS ? [`+${queueView.length - MAX_QUEUE_ROWS} more queued`] : []
6649
6786
  ].map((line2) => fitRow(` ${QUEUE_MARKER} ${line2}`, cols));
6650
6787
  const inner = dialogInner(cols);
6651
- const modalFloor = picker ? PICKER_CHROME_COMPACT + 1 : ask2 ? ASK_CHROME + 1 : plan ? PLAN_CHROME + 1 : 0;
6788
+ const buyEssential = buy ? lineCount(buyDeclarationLines(buy.pack), inner) : 0;
6789
+ const modalFloor = picker ? PICKER_CHROME_COMPACT + 1 : ask2 ? ASK_CHROME + 1 : plan ? PLAN_CHROME + 1 : buy ? BUY_CHROME + buyEssential : 0;
6652
6790
  let slack = Math.max(0, viewport - inputRows - modalFloor - menu.length);
6653
6791
  const grant = (want) => {
6654
6792
  if (want <= 0 || want > slack) return 0;
@@ -6714,6 +6852,14 @@ ${dropped.map((l) => ` \xB7 ${l}`).join("\n")}`
6714
6852
  const planBlock = plan ? headRows(renderMarkdown(plan.plan), inner, Math.max(1, budget - PLAN_CHROME)) : "";
6715
6853
  const planHeight = plan ? lineCount(planBlock, inner) + PLAN_CHROME : 0;
6716
6854
  budget -= planHeight;
6855
+ const buyCompact = Boolean(buy) && budget < BUY_CHROME + 1;
6856
+ const buyChrome = buyCompact ? BUY_CHROME_COMPACT : BUY_CHROME;
6857
+ const buyRoom = Math.max(1, budget - buyChrome);
6858
+ const buyFits = Boolean(buy) && buyRoom >= buyEssential;
6859
+ const buyBlock = buy ? headRows(buyFits ? buyConfirmLines(buy.pack) : BUY_TOO_SHORT, inner, buyRoom) : "";
6860
+ const buyHeight = buy ? lineCount(buyBlock, inner) + buyChrome : 0;
6861
+ budget -= buyHeight;
6862
+ buyFitsRef.current = buyFits;
6717
6863
  const liveRaw = [
6718
6864
  // Trailing blank lines are rows the clamp would spend on nothing, and a
6719
6865
  // streamed answer ends on one more often than not.
@@ -6735,7 +6881,7 @@ ${dropped.map((l) => ` \xB7 ${l}`).join("\n")}`
6735
6881
  return n + h;
6736
6882
  }, 0);
6737
6883
  }, [entries, cols]);
6738
- const used = chromeHeight + menu.length + pickerHeight + askHeight + planHeight + busyHeight + liveHeight;
6884
+ const used = chromeHeight + menu.length + pickerHeight + askHeight + planHeight + buyHeight + busyHeight + liveHeight;
6739
6885
  const spacer = Math.max(0, viewport - printed - used);
6740
6886
  const labelW = Math.min(
6741
6887
  Math.max(1, Math.floor(cols * 0.5)),
@@ -6800,6 +6946,16 @@ ${dropped.map((l) => ` \xB7 ${l}`).join("\n")}`
6800
6946
  /* @__PURE__ */ jsx2(Text2, { children: planBlock }),
6801
6947
  /* @__PURE__ */ jsx2(Text2, { dimColor: true, wrap: "truncate-end", children: pickHint(PLAN_HINT, inner) })
6802
6948
  ] }) : null,
6949
+ buy ? /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", marginTop: 1, borderStyle: "round", borderColor: MANGO3, paddingX: 1, children: [
6950
+ buyCompact ? null : /* @__PURE__ */ jsx2(Text2, { bold: true, color: MANGO_BRIGHT, wrap: "truncate-end", children: fitRow("Buy credits", inner) }),
6951
+ buyCompact ? null : /* @__PURE__ */ jsx2(Box2, { height: 1 }),
6952
+ /* @__PURE__ */ jsx2(Text2, { children: buyBlock }),
6953
+ /* @__PURE__ */ jsxs2(Box2, { children: [
6954
+ /* @__PURE__ */ jsx2(Text2, { color: MANGO_BRIGHT, children: "\u276F " }),
6955
+ /* @__PURE__ */ jsx2(Text2, { bold: true, wrap: "truncate-end", children: fitRow(buy.typed, Math.max(1, inner - 2)) })
6956
+ ] }),
6957
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, wrap: "truncate-end", children: pickHint(BUY_HINT, inner) })
6958
+ ] }) : null,
6803
6959
  picker ? /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", marginTop: 1, borderStyle: "round", borderColor: MANGO3, paddingX: 1, children: [
6804
6960
  /* @__PURE__ */ jsx2(Text2, { bold: true, color: MANGO_BRIGHT, children: pickerTitle }),
6805
6961
  pickerCompact ? null : /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: pickerSubtitle }),
@@ -6962,7 +7118,7 @@ function tailLines(text, max) {
6962
7118
  const lines = text.split("\n");
6963
7119
  return lines.length <= max ? text : lines.slice(-max).join("\n");
6964
7120
  }
6965
- var WORKING, WAITING_FOR_REWARD, LOADING, COMPACTING, SIGNING_IN, RUNNING, SENDING_FEEDBACK, FEEDBACK_TURN_INTERVAL, NOT_SIGNED_IN, LIVE_OUTPUT_LINES, COMMITTED_OUTPUT_LINES, DELTA_FLUSH_MS, MAX_INPUT_ROWS, MAX_PICKER_ROWS, MAX_QUEUE_ROWS, QUEUE_MARKER, PICKER_CURRENT, MIN_PICKER_LABEL_COLS, PICKER_CHROME, PICKER_CHROME_COMPACT, ASK_CHROME, PLAN_CHROME, ASK_HINT, PLAN_HINT, EXPAND_MAX_LINES, FALLBACK_ROWS, FALLBACK_COLS, CONTEXT_NOTICE_AT, KEEP_ALWAYS;
7121
+ var WORKING, WAITING_FOR_REWARD, LOADING, COMPACTING, SIGNING_IN, RUNNING, SENDING_FEEDBACK, WAITING_FOR_PAYMENT, FEEDBACK_TURN_INTERVAL, NOT_SIGNED_IN, LIVE_OUTPUT_LINES, COMMITTED_OUTPUT_LINES, DELTA_FLUSH_MS, MAX_INPUT_ROWS, MAX_PICKER_ROWS, MAX_QUEUE_ROWS, QUEUE_MARKER, PICKER_CURRENT, MIN_PICKER_LABEL_COLS, PICKER_CHROME, PICKER_CHROME_COMPACT, ASK_CHROME, PLAN_CHROME, BUY_CHROME, BUY_CHROME_COMPACT, ASK_HINT, PLAN_HINT, BUY_HINT, BUY_CONFIRM_WORD, BUY_TYPED_MAX, BUY_TOO_SHORT, EXPAND_MAX_LINES, FALLBACK_ROWS, FALLBACK_COLS, CONTEXT_NOTICE_AT, KEEP_ALWAYS;
6966
7122
  var init_app = __esm({
6967
7123
  "src/tui/app.tsx"() {
6968
7124
  "use strict";
@@ -7000,6 +7156,7 @@ var init_app = __esm({
7000
7156
  SIGNING_IN = "signing in\u2026";
7001
7157
  RUNNING = "running\u2026";
7002
7158
  SENDING_FEEDBACK = "sending\u2026";
7159
+ WAITING_FOR_PAYMENT = "waiting for the payment\u2026";
7003
7160
  FEEDBACK_TURN_INTERVAL = 50;
7004
7161
  NOT_SIGNED_IN = " Not signed in, or the stored token isn't valid for this gateway.\n Run /login to sign in.";
7005
7162
  LIVE_OUTPUT_LINES = 5;
@@ -7015,6 +7172,8 @@ var init_app = __esm({
7015
7172
  PICKER_CHROME_COMPACT = 5;
7016
7173
  ASK_CHROME = 4;
7017
7174
  PLAN_CHROME = 6;
7175
+ BUY_CHROME = 7;
7176
+ BUY_CHROME_COMPACT = 5;
7018
7177
  ASK_HINT = [
7019
7178
  "[y/\u23CE] once \xB7 [a] always \xB7 [n] no \xB7 esc stops the turn",
7020
7179
  "y once \xB7 a always \xB7 n no \xB7 esc stops",
@@ -7025,6 +7184,14 @@ var init_app = __esm({
7025
7184
  "y ask first \xB7 a auto-accept \xB7 n keep planning \xB7 esc stops",
7026
7185
  "y/a/n \xB7 esc"
7027
7186
  ];
7187
+ BUY_HINT = [
7188
+ "type yes and press \u23CE to open Stripe Checkout \xB7 esc cancels \u2014 nothing is charged either way",
7189
+ "type yes + \u23CE to continue \xB7 esc cancels \u2014 nothing is charged",
7190
+ "yes + \u23CE \xB7 esc"
7191
+ ];
7192
+ BUY_CONFIRM_WORD = "yes";
7193
+ BUY_TYPED_MAX = 8;
7194
+ BUY_TOO_SHORT = "Make this terminal taller \u2014 the confirmation has to be readable before you can agree to it.";
7028
7195
  EXPAND_MAX_LINES = 400;
7029
7196
  FALLBACK_ROWS = 24;
7030
7197
  FALLBACK_COLS = 80;
@@ -7072,7 +7239,6 @@ init_kimi();
7072
7239
  init_banner();
7073
7240
  init_browser();
7074
7241
  init_color();
7075
- import { createInterface } from "node:readline/promises";
7076
7242
 
7077
7243
  // src/title.ts
7078
7244
  var APP_TITLE = "Clixad";
@@ -7130,8 +7296,16 @@ async function main() {
7130
7296
  return refer(client, config);
7131
7297
  case "redeem":
7132
7298
  return redeem(client, config, rest[0]);
7299
+ // `clixad buy` is gone, and it is the one removed subcommand that gets a
7300
+ // signpost rather than the ordinary "anything that is not a command is a
7301
+ // task" fallback. A typo reaching the model is the better failure; this is
7302
+ // not a typo — it is what the help output, the terms and every older CLI
7303
+ // have been telling people to run — so left alone it would send "buy
7304
+ // starter" to a model, spend credits and do nothing.
7133
7305
  case "buy":
7134
- return buyCmd(client, config, rest[0]);
7306
+ console.log(c.yellow("Buying credits moved into the REPL."));
7307
+ console.log(c.dim(" Run `clixad` and type /buy \u2014 it shows the packs and asks you to confirm."));
7308
+ return;
7135
7309
  case "ask":
7136
7310
  return ask(client, config, rest.join(" "));
7137
7311
  case "agent":
@@ -7280,7 +7454,7 @@ async function showWallet(client) {
7280
7454
  const streak = await client.claimStreak();
7281
7455
  const w = await client.wallet();
7282
7456
  console.log(`balance: ${c.bold(w.balance.toLocaleString("en-US"))} credits`);
7283
- console.log(`${earnedToday(w)} \xB7 offers pay ${grantRangeLabel(w.grant_per_ad_range)}`);
7457
+ console.log(`${earnedToday(w)} \xB7 offers pay ${grantLabel(w.grant_per_ad)}`);
7284
7458
  const free = w.free_tier;
7285
7459
  if (free && free.daily_credits > 0) {
7286
7460
  const best = free.models.map((id) => ({ id, turns: free.turns_left[id] ?? 0 })).filter((m) => m.turns > 0).sort((a, b) => b.turns - a.turns)[0];
@@ -7372,89 +7546,6 @@ async function redeem(client, config, code) {
7372
7546
  console.log(c.green(`
7373
7547
  \u2713 ${text}`) + ` \xB7 balance ${c.bold(credits(result.balance))}`);
7374
7548
  }
7375
- async function buyCmd(client, config, pack) {
7376
- if (!config.token) return console.log(c.yellow("Not logged in. Run `clixad login` first."));
7377
- const { enabled, packs } = await client.creditPacks();
7378
- if (!enabled) return console.log(c.yellow("Buying credits isn't available (Stripe not configured on the gateway)."));
7379
- if (!pack) {
7380
- console.log(c.bold("Credit packs:"));
7381
- for (const p of packs) {
7382
- console.log(
7383
- ` ${c.cyan(p.id.padEnd(9))} ${p.credits.toLocaleString("en-US").padStart(11)} credits ${c.bold("$" + p.price_usd.toFixed(2))}`
7384
- );
7385
- }
7386
- console.log(c.dim("\n buy one with: clixad buy <id>"));
7387
- return;
7388
- }
7389
- const chosen = packs.find((x) => x.id === pack);
7390
- if (!chosen) {
7391
- console.log(c.red(` unknown pack: ${pack}`));
7392
- return console.log(c.dim(` try one of: ${packs.map((x) => x.id).join(", ")}`));
7393
- }
7394
- const consented = await confirmWithdrawalWaiver(chosen);
7395
- if (!consented) return;
7396
- let checkout;
7397
- try {
7398
- checkout = await client.checkout(pack, {
7399
- requestImmediatePerformance: true,
7400
- acknowledgeLossOfWithdrawal: true,
7401
- at: (/* @__PURE__ */ new Date()).toISOString()
7402
- });
7403
- } catch (err) {
7404
- return console.log(c.red(err.message));
7405
- }
7406
- const before = (await safeWallet(client))?.balance ?? 0;
7407
- console.log(`
7408
- Stripe Checkout: ${c.bold(checkout.pack)} \u2014 ${checkout.credits.toLocaleString("en-US")} credits for ${c.bold("$" + checkout.price_usd.toFixed(2))}`);
7409
- console.log(c.dim(` ${checkout.url}`));
7410
- openBrowser(checkout.url);
7411
- if (c.enabled) process.stdout.write(c.dim(" waiting for payment to clear"));
7412
- const deadline = Date.now() + 5 * 60 * 1e3;
7413
- while (Date.now() < deadline) {
7414
- await sleep5(3e3);
7415
- if (c.enabled) process.stdout.write(c.dim("."));
7416
- const balance = (await safeWallet(client))?.balance ?? before;
7417
- if (balance > before) {
7418
- console.log(c.green(`
7419
- \u2713 +${credits(balance - before)} credits`) + ` \xB7 balance ${c.bold(credits(balance))}`);
7420
- return;
7421
- }
7422
- }
7423
- console.log(c.yellow("\n No credits yet \u2014 the webhook may be delayed. Check `clixad wallet`."));
7424
- }
7425
- async function confirmWithdrawalWaiver(pack) {
7426
- if (!process.stdin.isTTY) {
7427
- console.log(c.yellow("\n Buying credits needs an interactive terminal."));
7428
- console.log(
7429
- c.dim(" We have to record your confirmation before charging you, and a pipe has nobody to ask.")
7430
- );
7431
- return false;
7432
- }
7433
- const price = "$" + pack.price_usd.toFixed(2);
7434
- console.log(`
7435
- ${c.bold(pack.label)} \u2014 ${credits(pack.credits)} credits for ${c.bold(price)}`);
7436
- console.log(c.dim(` One purchase. No subscription, nothing to cancel. Terms: ${TERMS_URL}`));
7437
- console.log("\n Before you pay, please confirm:");
7438
- console.log(" \xB7 You request that we deliver the credits immediately, before the");
7439
- console.log(" 14-day withdrawal period has expired.");
7440
- console.log(" \xB7 You acknowledge that your right of withdrawal lapses once they");
7441
- console.log(" have been delivered.");
7442
- const rl = createInterface({ input: process.stdin, output: process.stdout });
7443
- let answer = "";
7444
- try {
7445
- answer = (await rl.question(`
7446
- Type ${c.bold("yes")} to continue: `)).trim().toLowerCase();
7447
- } catch {
7448
- answer = "";
7449
- } finally {
7450
- rl.close();
7451
- }
7452
- if (answer !== "yes" && answer !== "y") {
7453
- console.log(c.dim("\n Cancelled. You have not been charged."));
7454
- return false;
7455
- }
7456
- return true;
7457
- }
7458
7549
  async function ask(client, config, prompt) {
7459
7550
  if (!prompt) return console.error(c.red('usage: clixad ask "your prompt"'));
7460
7551
  const messages = [{ role: "user", content: prompt }];
@@ -7495,9 +7586,10 @@ ${NO_SEARCH_INSTRUCTION}`),
7495
7586
  Out of credits (balance ${err.balance}, need ~${err.estimatedCost}).`));
7496
7587
  console.log(
7497
7588
  c.yellow(
7498
- `Run \`clixad earn\` to open the offerwall \u2014 offers pay ${grantRangeLabel(err.grantRange)} \u2014 or buy credits.`
7589
+ `Run \`clixad earn\` to open the offerwall \u2014 offers pay ${grantLabel(err.grantPerAd)}.`
7499
7590
  )
7500
7591
  );
7592
+ console.log(c.yellow("Credits can also be bought: start `clixad` and type /buy."));
7501
7593
  console.log(c.dim(" Offers you are screened out of pay nothing \u2014 that is normal, just start another."));
7502
7594
  } else if (err instanceof AuthError) {
7503
7595
  console.log(c.red(err.message));
@@ -7611,8 +7703,9 @@ async function runTurn(client, config, messages) {
7611
7703
  if (err instanceof PaywallError) {
7612
7704
  console.log(c.yellow(`Out of credits (balance ${err.balance}, need ~${err.estimatedCost}).`));
7613
7705
  console.log(
7614
- c.yellow(`Run /earn to open the offerwall \u2014 offers pay ${grantRangeLabel(err.grantRange)} \u2014 or buy credits.`)
7706
+ c.yellow(`Run \`clixad earn\` to open the offerwall \u2014 offers pay ${grantLabel(err.grantPerAd)}.`)
7615
7707
  );
7708
+ console.log(c.yellow("Credits can also be bought: start `clixad` and type /buy."));
7616
7709
  console.log(c.dim(" Offers you are screened out of pay nothing \u2014 that is normal, just start another."));
7617
7710
  } else if (err instanceof AuthError) {
7618
7711
  console.log(c.red(err.message));
@@ -7648,7 +7741,6 @@ function printHelp() {
7648
7741
  ${c.cyan("earn")} open the ad wall to earn credits
7649
7742
  ${c.cyan("refer")} your invite code, and what it has earned
7650
7743
  ${c.cyan("redeem")} <code> spend a code you were given
7651
- ${c.cyan("buy")} [pack] list credit packs / buy one via Stripe
7652
7744
  ${c.cyan("ask")} "<prompt>" one-shot completion (no tools)
7653
7745
  ${c.cyan("agent")} "<task>" open the REPL with a task already running
7654
7746
  ${c.cyan("-p")} "<task>" headless agent run (read-only tools)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clixad",
3
- "version": "0.0.1-beta.20",
3
+ "version": "0.0.1-beta.21",
4
4
  "description": "Free AI coding agent in your terminal, funded by rewarded ads.",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",