clixad 0.0.1-beta.26 → 0.0.1-beta.28

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 +303 -34
  2. package/package.json +1 -1
package/dist/clixad.mjs CHANGED
@@ -302,6 +302,52 @@ import { gzipSync } from "node:zlib";
302
302
  function encodeChatBody(json) {
303
303
  return gzipSync(Buffer.from(json, "utf8"));
304
304
  }
305
+ function accessColumn(m, premium) {
306
+ if (m.access === void 0) return m.tier.padEnd(8);
307
+ if (m.access === "budget") return "budget ";
308
+ return premiumLocked(premium) ? "locked " : "premium ";
309
+ }
310
+ function premiumLocked(premium) {
311
+ return premium !== void 0 && !premium.unlocked && premium.credits <= 0;
312
+ }
313
+ function premiumUnlockHint(unlockUsd, streak) {
314
+ const base = typeof unlockUsd === "number" && unlockUsd > 0 ? `premium: a survey or ${usd(unlockUsd)} unlocks it` : "premium: a survey unlocks it";
315
+ const s = usableStreak(streak);
316
+ return s && s.day < s.min_day ? `${base}, or a ${s.min_day}-day streak` : base;
317
+ }
318
+ function premiumHeadline(unlockUsd) {
319
+ return typeof unlockUsd === "number" && unlockUsd > 0 ? `Premium: complete a survey or unlock from ${usd(unlockUsd)}.` : "Premium: complete a survey to unlock it.";
320
+ }
321
+ function usd(v) {
322
+ return Number.isInteger(v) ? `$${v}` : `$${v.toFixed(2)}`;
323
+ }
324
+ function usableStreak(streak) {
325
+ if (typeof streak !== "object" || streak === null) return void 0;
326
+ const s = streak;
327
+ const num = (v) => typeof v === "number" && Number.isFinite(v) ? v : void 0;
328
+ const day = num(s.day);
329
+ const minDay = num(s.min_day);
330
+ const daily = num(s.daily_credits);
331
+ const left = num(s.credits_left);
332
+ if (day === void 0 || minDay === void 0 || daily === void 0 || left === void 0 || daily <= 0) {
333
+ return void 0;
334
+ }
335
+ return { day, min_day: minDay, daily_credits: daily, credits_left: left };
336
+ }
337
+ function premiumStreakLine(streak) {
338
+ const s = usableStreak(streak);
339
+ if (!s) return null;
340
+ const n = (x) => x.toLocaleString("en-US");
341
+ if (s.day < s.min_day) {
342
+ const wait = s.min_day - s.day;
343
+ const when = wait === 1 ? "tomorrow" : wait === 2 ? "the day after tomorrow" : `in ${wait} days`;
344
+ return `From day ${s.min_day} of your login streak, ${n(s.daily_credits)} bonus credits a day work on premium models too. You are on day ${s.day}, so that starts ${when} if you open Clixad every day.`;
345
+ }
346
+ if (s.credits_left <= 0) {
347
+ return `Today's ${n(s.daily_credits)} bonus credits for premium models are used up. They come back at 00:00 UTC while your login streak holds.`;
348
+ }
349
+ return `Your login streak lets ${n(s.credits_left)} more bonus credits go to premium models today.`;
350
+ }
305
351
  function adsPerTaskLabel(estAdsPerTask) {
306
352
  const effort = estAdsPerTask >= 1 ? `~${estAdsPerTask} ads/task` : "<1 ad/task";
307
353
  return `${effort} on completion (median)`;
@@ -319,7 +365,18 @@ function streakLabel(streak) {
319
365
  if (!streak.claimed) return `daily streak: ${where} \xB7 today's already counted`;
320
366
  const tomorrow = streak.schedule[Math.min(streak.day, streak.length - 1)];
321
367
  const next = tomorrow === void 0 ? "" : ` \xB7 tomorrow ${n(tomorrow)}`;
322
- return `daily streak: ${where} \xB7 +${n(streak.credits)} credits${next}`;
368
+ return `daily streak: ${where} \xB7 +${n(streak.credits)} credits${next}${streakPremiumClause(streak)}`;
369
+ }
370
+ function streakPremiumClause(streak) {
371
+ const p = streak.premium;
372
+ if (!p) return "";
373
+ return streak.day < p.minDay ? ` \xB7 bonus credits on premium from day ${p.minDay}` : ` \xB7 ${p.dailyCredits.toLocaleString("en-US")} bonus credits a day on premium`;
374
+ }
375
+ function streakPremium(raw) {
376
+ if (typeof raw !== "object" || raw === null) return {};
377
+ const p = raw;
378
+ if (typeof p.min_day !== "number" || typeof p.daily_credits !== "number" || p.daily_credits <= 0) return {};
379
+ return { premium: { minDay: p.min_day, dailyCredits: p.daily_credits } };
323
380
  }
324
381
  function referLines(info) {
325
382
  const n = (x) => x.toLocaleString("en-US");
@@ -405,16 +462,36 @@ function asSignupClosed(status, data) {
405
462
  const d = data;
406
463
  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;
407
464
  }
465
+ function premiumLockedError(message, model, body) {
466
+ const num = (v) => typeof v === "number" && Number.isFinite(v) ? v : 0;
467
+ return new PremiumLockedError(
468
+ message,
469
+ typeof body?.model === "string" ? body.model : model,
470
+ num(body?.needed_credits),
471
+ num(body?.premium_credits),
472
+ typeof body?.unlock_usd === "number" && body.unlock_usd > 0 ? body.unlock_usd : null,
473
+ num(body?.grant_per_ad),
474
+ freeAlternatives(body?.free_models),
475
+ usableStreak(body?.streak)
476
+ );
477
+ }
408
478
  function shortfallHeadline(err, model) {
409
479
  const n = (v) => Math.max(0, Math.round(v)).toLocaleString("en-US");
410
480
  if (err.balance <= 0) return `Out of credits (this needs about ${n(err.estimatedCost)}).`;
411
481
  return `Not enough credits for this turn${model ? ` on ${model}` : ""}: it needs about ${n(err.estimatedCost)}, and the balance is ${n(err.balance)}.`;
412
482
  }
413
483
  function paywallError(body) {
414
- const free = Array.isArray(body.free_models) ? body.free_models.filter(
484
+ return new PaywallError(body.balance, body.estimated_cost, body.grant_per_ad, freeAlternatives(body.free_models));
485
+ }
486
+ function freeAlternatives(list) {
487
+ return Array.isArray(list) ? list.filter(
415
488
  (m) => !!m && typeof m.id === "string" && m.id.length > 0 && Number.isFinite(m.turns_left)
416
489
  ).map((m) => ({ id: m.id, turnsLeft: Math.max(0, Math.floor(m.turns_left)) })).filter((m) => m.turnsLeft > 0) : [];
417
- return new PaywallError(body.balance, body.estimated_cost, body.grant_per_ad, free);
490
+ }
491
+ async function refusal402(res, model) {
492
+ const body = await res.json();
493
+ if (body.error === "premium_locked") return premiumLockedError(body.message ?? "", model, body.premium);
494
+ return paywallError(body);
418
495
  }
419
496
  function neverConnected(code) {
420
497
  return code !== void 0 && NEVER_CONNECTED.has(code);
@@ -540,7 +617,7 @@ async function* parseSSE(body) {
540
617
  }
541
618
  }
542
619
  }
543
- var CHAT_ENCODED_CONTENT_TYPE, LEDGER_REASONS, SignupClosedError, PaywallError, GatewayUnreachableError, NEVER_CONNECTED, AuthError, GatewayClient;
620
+ var CHAT_ENCODED_CONTENT_TYPE, LEDGER_REASONS, SignupClosedError, PaywallError, PremiumLockedError, GatewayUnreachableError, NEVER_CONNECTED, AuthError, GatewayClient;
544
621
  var init_client = __esm({
545
622
  "src/client.ts"() {
546
623
  "use strict";
@@ -590,6 +667,26 @@ var init_client = __esm({
590
667
  grantPerAd;
591
668
  freeModels;
592
669
  };
670
+ PremiumLockedError = class extends Error {
671
+ constructor(message, model, neededCredits, premiumCredits, unlockUsd, grantPerAd, freeModels, streak) {
672
+ super(message);
673
+ this.model = model;
674
+ this.neededCredits = neededCredits;
675
+ this.premiumCredits = premiumCredits;
676
+ this.unlockUsd = unlockUsd;
677
+ this.grantPerAd = grantPerAd;
678
+ this.freeModels = freeModels;
679
+ this.streak = streak;
680
+ this.name = "PremiumLockedError";
681
+ }
682
+ model;
683
+ neededCredits;
684
+ premiumCredits;
685
+ unlockUsd;
686
+ grantPerAd;
687
+ freeModels;
688
+ streak;
689
+ };
593
690
  GatewayUnreachableError = class extends Error {
594
691
  constructor(host, code) {
595
692
  const where = `${host}${code ? ` (${code})` : ""}`;
@@ -747,7 +844,8 @@ var init_client = __esm({
747
844
  claimed: body.claimed === true,
748
845
  balance: typeof body.balance === "number" ? body.balance : 0,
749
846
  schedule: Array.isArray(body.schedule) ? body.schedule : [],
750
- length: typeof body.length === "number" ? body.length : 0
847
+ length: typeof body.length === "number" ? body.length : 0,
848
+ ...streakPremium(body.premium)
751
849
  };
752
850
  } catch {
753
851
  return null;
@@ -886,6 +984,7 @@ var init_client = __esm({
886
984
  if (!res.ok) throw new Error(`models failed: ${res.status}`);
887
985
  const data = await res.json();
888
986
  this.seen = { web_search: data.capabilities?.web_search === true };
987
+ this.unlockUsd = typeof data.premium_unlock_usd === "number" && data.premium_unlock_usd > 0 ? data.premium_unlock_usd : null;
889
988
  this.shape = parseTaskShape(data.task_shape);
890
989
  this.latest = typeof data.cli_latest === "string" ? data.cli_latest : void 0;
891
990
  const live = new Set(data.data.map((m) => m.id));
@@ -936,8 +1035,23 @@ var init_client = __esm({
936
1035
  return this.seen;
937
1036
  }
938
1037
  seen = { web_search: false };
939
- async wallet(signal) {
940
- const res = await gatewayFetch(`${this.config.gatewayUrl}/v1/wallet`, { headers: this.headers(), signal });
1038
+ /**
1039
+ * What unlocks premium for every credit on the account (the cheapest pack,
1040
+ * USD), as of the last catalog fetch. Null where the gateway sells none or
1041
+ * did not say, which the hints read as "a survey unlocks it".
1042
+ */
1043
+ get premiumUnlockUsd() {
1044
+ return this.unlockUsd;
1045
+ }
1046
+ unlockUsd = null;
1047
+ /**
1048
+ * `premium: true` asks for premium access too, which costs the gateway a read
1049
+ * of this account's whole ledger. Asked for where it is shown (`/model`,
1050
+ * `clixad models`), never by the polls `/earn` and a purchase run.
1051
+ */
1052
+ async wallet(signal, opts = {}) {
1053
+ const url = `${this.config.gatewayUrl}/v1/wallet${opts.premium ? "?premium=1" : ""}`;
1054
+ const res = await gatewayFetch(url, { headers: this.headers(), signal });
941
1055
  if (res.status === 401) throw await authFailure(res, "wallet");
942
1056
  if (!res.ok) throw new Error(`wallet failed: ${res.status}`);
943
1057
  return res.json();
@@ -1234,9 +1348,7 @@ var init_client = __esm({
1234
1348
  signal
1235
1349
  );
1236
1350
  if (res.status === 401) throw await authFailure(res, "chat");
1237
- if (res.status === 402) {
1238
- throw paywallError(await res.json());
1239
- }
1351
+ if (res.status === 402) throw await refusal402(res, model);
1240
1352
  if (!res.ok) throw new Error(await httpFailure(res, "chat"));
1241
1353
  const data = await res.json();
1242
1354
  const choice = data.choices[0];
@@ -1267,9 +1379,7 @@ var init_client = __esm({
1267
1379
  opts.signal
1268
1380
  );
1269
1381
  if (res.status === 401) throw await authFailure(res, "chat");
1270
- if (res.status === 402) {
1271
- throw paywallError(await res.json());
1272
- }
1382
+ if (res.status === 402) throw await refusal402(res, model);
1273
1383
  if (!res.ok || !res.body) throw new Error(await httpFailure(res, "chat"));
1274
1384
  let content = "";
1275
1385
  let finishReason = "stop";
@@ -1277,6 +1387,7 @@ var init_client = __esm({
1277
1387
  let free = {};
1278
1388
  let balance = 0;
1279
1389
  let streamError;
1390
+ let premiumRefusal;
1280
1391
  const calls = /* @__PURE__ */ new Map();
1281
1392
  const reasoning = [];
1282
1393
  for await (const evt of parseSSE(res.body)) {
@@ -1299,12 +1410,16 @@ var init_client = __esm({
1299
1410
  mergeReasoningDetails(reasoning, choice?.delta?.reasoning_details);
1300
1411
  if (choice?.finish_reason) finishReason = choice.finish_reason;
1301
1412
  if (json.error) streamError = json.error.message || "the model stream failed";
1413
+ if (json.error?.type === "premium_locked") {
1414
+ premiumRefusal = premiumLockedError(streamError ?? "", model, json.clixad?.premium);
1415
+ }
1302
1416
  if (json.clixad) {
1303
1417
  creditsCharged = json.clixad.credits_charged;
1304
1418
  balance = json.clixad.balance;
1305
1419
  free = freeMeta(json.clixad);
1306
1420
  }
1307
1421
  }
1422
+ if (premiumRefusal) throw premiumRefusal;
1308
1423
  if (streamError) throw new Error(streamError);
1309
1424
  const message = { role: "assistant", content: content || null };
1310
1425
  if (calls.size > 0) {
@@ -6150,6 +6265,16 @@ function App({
6150
6265
  const pendingFreeRef = useRef([]);
6151
6266
  const pendingGrantRef = useRef(0);
6152
6267
  const pendingBalanceRef = useRef(void 0);
6268
+ const pendingPremiumRef = useRef(null);
6269
+ const [premium, setPremiumState] = useState(wallet?.premium);
6270
+ const premiumRef = useRef(wallet?.premium);
6271
+ const setPremium = useCallback((next) => {
6272
+ premiumRef.current = next;
6273
+ setPremiumState(next);
6274
+ }, []);
6275
+ const refreshPremium = useCallback(() => {
6276
+ void client.wallet(void 0, { premium: true }).then((w) => setPremium(w.premium)).catch(() => void 0);
6277
+ }, [client, setPremium]);
6153
6278
  const mcpRef = useRef(McpHub.empty());
6154
6279
  const todosRef = useRef(new TodoList());
6155
6280
  const lastTurnRef = useRef(null);
@@ -6259,9 +6384,10 @@ function App({
6259
6384
  useEffect(() => {
6260
6385
  if (wallet) return;
6261
6386
  const ctl = new AbortController();
6262
- void client.wallet(ctl.signal).then((w) => {
6387
+ void client.wallet(ctl.signal, { premium: true }).then((w) => {
6263
6388
  setBalance((current) => current ?? w.balance);
6264
6389
  setFreeTier((current) => current ?? w.free_tier);
6390
+ if (premiumRef.current === void 0) setPremium(w.premium);
6265
6391
  }).catch(() => void 0);
6266
6392
  return () => ctl.abort();
6267
6393
  }, []);
@@ -6556,6 +6682,13 @@ function App({
6556
6682
  return;
6557
6683
  case "usage":
6558
6684
  setBalance(event.balance);
6685
+ if (event.creditsCharged > 0 && premiumRef.current) {
6686
+ const p = premiumRef.current;
6687
+ setPremium({
6688
+ ...p,
6689
+ credits: Math.max(0, p.unlocked ? event.balance : Math.min(event.balance, p.credits - event.creditsCharged))
6690
+ });
6691
+ }
6559
6692
  setSpent((s) => s + event.creditsCharged);
6560
6693
  if (event.free) {
6561
6694
  setFreeTier(
@@ -6567,11 +6700,12 @@ function App({
6567
6700
  return;
6568
6701
  }
6569
6702
  },
6570
- [cols, flushDelta, push]
6703
+ [cols, flushDelta, push, setPremium]
6571
6704
  );
6572
6705
  const runTurn2 = useCallback(
6573
6706
  async (task, images = []) => {
6574
6707
  pendingFreeRef.current = [];
6708
+ pendingPremiumRef.current = null;
6575
6709
  const model2 = modelRef.current;
6576
6710
  const ac = new AbortController();
6577
6711
  abortRef.current = ac;
@@ -6703,7 +6837,22 @@ ${NO_SEARCH_INSTRUCTION}`),
6703
6837
  }
6704
6838
  }
6705
6839
  } catch (err) {
6706
- if (err instanceof PaywallError) {
6840
+ if (err instanceof PremiumLockedError) {
6841
+ pendingTaskRef.current = { task, images };
6842
+ pendingPremiumRef.current = err;
6843
+ setPremium({ unlocked: false, credits: err.premiumCredits, ...err.streak ? { streak: err.streak } : {} });
6844
+ const short2 = err.premiumCredits > 0 ? ` This turn needs about ${err.neededCredits.toLocaleString("en-US")} of those credits, and you have ${err.premiumCredits.toLocaleString("en-US")}.
6845
+ ` : "";
6846
+ const streakLine = premiumStreakLine(err.streak);
6847
+ push({
6848
+ kind: "notice",
6849
+ tone: "warn",
6850
+ text: ` ${premiumHeadline(err.unlockUsd)}
6851
+ ${err.model} is a premium model. It runs on credits you earned on the offer walls or bought.
6852
+ ` + short2 + (streakLine ? ` ${streakLine}
6853
+ ` : "") + " Your bonus credits (the signup bonus, our own surveys) still work on budget models.\n Press enter to unlock it or to switch to a budget model. Your request is kept."
6854
+ });
6855
+ } else if (err instanceof PaywallError) {
6707
6856
  pendingTaskRef.current = { task, images };
6708
6857
  pendingFreeRef.current = err.freeModels;
6709
6858
  pendingGrantRef.current = err.grantPerAd;
@@ -6898,8 +7047,9 @@ ${NO_SEARCH_INSTRUCTION}`),
6898
7047
  text: " no credit yet \u2014 offers take a while to confirm, and a screenout pays nothing.\n That is normal: start another with /earn, or check /wallet later."
6899
7048
  });
6900
7049
  push({ kind: "notice", tone: "good", text: " credits added \u2014 continuing" });
7050
+ refreshPremium();
6901
7051
  if (pending) await runTurn2(pending.task, pending.images);
6902
- }, [balance, client, config.dashboardUrl, push, runBusy, runTurn2]);
7052
+ }, [balance, client, config.dashboardUrl, push, refreshPremium, runBusy, runTurn2]);
6903
7053
  const runPaywallChoice = useCallback(() => {
6904
7054
  const free = pendingFreeRef.current.filter((m) => m.id !== EARN_ROW);
6905
7055
  if (free.length === 0) return void runAdWall();
@@ -6969,6 +7119,47 @@ ${NO_SEARCH_INSTRUCTION}`),
6969
7119
  });
6970
7120
  });
6971
7121
  }, [client, push, runBusy]);
7122
+ const runPremiumChoice = useCallback(() => {
7123
+ const refusal = pendingPremiumRef.current;
7124
+ if (!refusal) return void runPaywallChoice();
7125
+ const items = [
7126
+ {
7127
+ value: EARN_ROW,
7128
+ label: "complete a survey",
7129
+ hint: `opens the offer walls, offers pay ${grantLabel(refusal.grantPerAd)}`
7130
+ },
7131
+ ...refusal.unlockUsd ? [
7132
+ {
7133
+ value: BUY_ROW,
7134
+ label: `unlock from ${usd(refusal.unlockUsd)}`,
7135
+ hint: "any pack, and premium then takes all your credits"
7136
+ }
7137
+ ] : [],
7138
+ ...refusal.freeModels.filter((m) => m.id !== EARN_ROW && m.id !== BUY_ROW).map((m) => ({
7139
+ value: m.id,
7140
+ label: m.id,
7141
+ hint: `budget, free: ~${m.turnsLeft} turn${m.turnsLeft === 1 ? "" : "s"} left today`
7142
+ }))
7143
+ ];
7144
+ setPickerSel(0);
7145
+ setPicker({
7146
+ title: premiumHeadline(refusal.unlockUsd),
7147
+ subtitle: "Unlock premium, or carry on with a budget model.",
7148
+ items,
7149
+ onPick: (choice) => {
7150
+ if (choice.value === EARN_ROW) return void runAdWall();
7151
+ if (choice.value === BUY_ROW) return void runBuy();
7152
+ const pending = pendingTaskRef.current;
7153
+ pendingTaskRef.current = null;
7154
+ pendingPremiumRef.current = null;
7155
+ config.model = choice.value;
7156
+ saveConfig(config);
7157
+ switchModel(choice.value);
7158
+ push({ kind: "notice", tone: "good", text: ` model \u2192 ${choice.value} (budget) \xB7 continuing where you left off` });
7159
+ if (pending) void runTurn2(pending.task, pending.images);
7160
+ }
7161
+ });
7162
+ }, [config, push, runAdWall, runBuy, runPaywallChoice, runTurn2, switchModel]);
6972
7163
  const runCheckout = useCallback(
6973
7164
  async (pack) => {
6974
7165
  let before = balance ?? (await client.wallet().catch(() => void 0))?.balance;
@@ -7006,11 +7197,20 @@ ${NO_SEARCH_INSTRUCTION}`),
7006
7197
  stopped = signal.aborted;
7007
7198
  });
7008
7199
  if (credited) {
7009
- return push({
7200
+ push({
7010
7201
  kind: "notice",
7011
7202
  tone: "good",
7012
7203
  text: " credits added \u2014 an invoice is on its way to the address on the payment."
7013
7204
  });
7205
+ refreshPremium();
7206
+ const pending = pendingPremiumRef.current ? pendingTaskRef.current : null;
7207
+ if (pending) {
7208
+ pendingTaskRef.current = null;
7209
+ pendingPremiumRef.current = null;
7210
+ push({ kind: "notice", tone: "good", text: " premium is unlocked \xB7 continuing where you left off" });
7211
+ await runTurn2(pending.task, pending.images);
7212
+ }
7213
+ return;
7014
7214
  }
7015
7215
  push({
7016
7216
  kind: "notice",
@@ -7018,7 +7218,7 @@ ${NO_SEARCH_INSTRUCTION}`),
7018
7218
  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."
7019
7219
  });
7020
7220
  },
7021
- [balance, client, push, runBusy]
7221
+ [balance, client, push, refreshPremium, runBusy, runTurn2]
7022
7222
  );
7023
7223
  const runCommand = useCallback(
7024
7224
  async (line2) => {
@@ -7099,15 +7299,21 @@ ${NO_SEARCH_INSTRUCTION}`),
7099
7299
  if (arg && !models.some((m) => m.id === arg)) {
7100
7300
  push({ kind: "notice", tone: "warn", text: ` unknown model: ${arg}` });
7101
7301
  }
7102
- const free = await client.wallet(signal).then((w) => {
7302
+ const free = await client.wallet(signal, { premium: true }).then((w) => {
7103
7303
  setBalance(w.balance);
7104
7304
  setFreeTier(w.free_tier);
7305
+ setPremium(w.premium);
7105
7306
  return w.free_tier;
7106
- }).catch(() => freeTier);
7307
+ }).catch(() => {
7308
+ setPremium(void 0);
7309
+ return freeTier;
7310
+ });
7311
+ const premium2 = premiumRef.current;
7312
+ const unlockUsd = client.premiumUnlockUsd;
7107
7313
  const items = models.map((m) => ({
7108
7314
  value: m.id,
7109
7315
  label: m.id,
7110
- hint: `${m.tier.padEnd(8)}${modelHint(m, free)}`,
7316
+ hint: accessColumn(m, premium2) + (m.access === "premium" && premiumLocked(premium2) ? premiumUnlockHint(unlockUsd, premium2?.streak) : modelHint(m, free)),
7111
7317
  current: m.id === model
7112
7318
  }));
7113
7319
  const preselect = items.findIndex((i) => i.value === arg);
@@ -7121,6 +7327,18 @@ ${NO_SEARCH_INSTRUCTION}`),
7121
7327
  saveConfig(config);
7122
7328
  switchModel(choice.value);
7123
7329
  push({ kind: "notice", text: ` model \u2192 ${choice.value}` });
7330
+ const picked = models.find((m) => m.id === choice.value);
7331
+ if (picked?.access === "premium" && premiumLocked(premium2)) {
7332
+ const streakLine = premiumStreakLine(premium2?.streak);
7333
+ push({
7334
+ kind: "notice",
7335
+ tone: "warn",
7336
+ text: ` ${premiumHeadline(unlockUsd)}
7337
+ ${choice.value} runs on credits you earned on the offer walls or bought.
7338
+ ` + (streakLine ? ` ${streakLine}
7339
+ ` : "") + " /earn opens the offer walls" + (unlockUsd ? `, /buy unlocks premium from ${usd(unlockUsd)}.` : ".") + " Your bonus credits work on budget models."
7340
+ });
7341
+ }
7124
7342
  }
7125
7343
  });
7126
7344
  });
@@ -7189,7 +7407,7 @@ ${NO_SEARCH_INSTRUCTION}`),
7189
7407
  catalogRef.current = ms;
7190
7408
  push({
7191
7409
  kind: "notice",
7192
- text: ms.map((m) => ` ${m.id.padEnd(24)} ${modelHint(m, freeTier)}`).join("\n")
7410
+ text: ms.map((m) => ` ${m.id.padEnd(24)} ${accessColumn(m, premium)}${modelHint(m, freeTier)}`).join("\n")
7193
7411
  });
7194
7412
  });
7195
7413
  return;
@@ -7351,7 +7569,7 @@ ${NO_SEARCH_INSTRUCTION}`),
7351
7569
  push({ kind: "notice", tone: "warn", text: ` unknown command: /${cmd} \u2014 try /help` });
7352
7570
  }
7353
7571
  },
7354
- [client, cols, config, cycleMode, exit, model, push, root, runAdWall, runBusy, runBuy, runLogin, runTurn2, writeToStdout]
7572
+ [client, cols, config, cycleMode, exit, model, premium, push, root, runAdWall, runBusy, runBuy, runLogin, runTurn2, setPremium, writeToStdout]
7355
7573
  );
7356
7574
  const runShell = useCallback(
7357
7575
  async (command) => {
@@ -7681,7 +7899,9 @@ ${dropped.map((l) => ` \xB7 ${l}`).join("\n")}`
7681
7899
  if (key.return) {
7682
7900
  if (key.meta || key.shift) return setEditor(newline(editor));
7683
7901
  if (endsWithContinuation(editor)) return setEditor(continueLine(editor));
7684
- if (!busy && pendingTaskRef.current && isEmpty(editor)) return void runPaywallChoice();
7902
+ if (!busy && pendingTaskRef.current && isEmpty(editor)) {
7903
+ return void (pendingPremiumRef.current ? runPremiumChoice() : runPaywallChoice());
7904
+ }
7685
7905
  return void submit(toText(editor));
7686
7906
  }
7687
7907
  if (key.ctrl && ch === "j") return setEditor(newline(editor));
@@ -7862,7 +8082,11 @@ ${dropped.map((l) => ` \xB7 ${l}`).join("\n")}`
7862
8082
  if (!busy && info && shape && balance !== void 0 && !effortRaised(info, resolveEffort(info, effortBy[model])) && typed.trim() !== "" && !/^[/!]/.test(typed.trimStart())) {
7863
8083
  const range = estimateTask(info, conversationUsed + schemaTokens + estimateTokens(typed), shape);
7864
8084
  const onFree = Boolean(info.free && freeTier?.models.includes(model));
7865
- const verdict = taskVerdict(range, { balance, ...onFree ? { freeLeft: freeTier.credits_left } : {} });
8085
+ const spendable = info.access === "premium" && premium && !premium.unlocked ? Math.min(balance, premium.credits) : balance;
8086
+ const verdict = taskVerdict(range, {
8087
+ balance: spendable,
8088
+ ...onFree ? { freeLeft: freeTier.credits_left } : {}
8089
+ });
7866
8090
  estimate = estimateClause(range, verdict, !onFree && (freeTier?.credits_left ?? 0) > 0);
7867
8091
  }
7868
8092
  const modeText = ` ${MODE_STYLE[mode].glyph} ${MODE_LABEL[mode]}`;
@@ -8147,7 +8371,7 @@ function tailLines(text, max) {
8147
8371
  const lines = text.split("\n");
8148
8372
  return lines.length <= max ? text : lines.slice(-max).join("\n");
8149
8373
  }
8150
- 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, INIT_HINT_AFTER_TURNS, EARN_ROW, CONTEXT_NOTICE_AT, KEEP_ALWAYS;
8374
+ 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, INIT_HINT_AFTER_TURNS, EARN_ROW, BUY_ROW, CONTEXT_NOTICE_AT, KEEP_ALWAYS;
8151
8375
  var init_app = __esm({
8152
8376
  "src/tui/app.tsx"() {
8153
8377
  "use strict";
@@ -8231,6 +8455,7 @@ var init_app = __esm({
8231
8455
  FALLBACK_COLS = 80;
8232
8456
  INIT_HINT_AFTER_TURNS = 3;
8233
8457
  EARN_ROW = "\0earn";
8458
+ BUY_ROW = "\0buy";
8234
8459
  CONTEXT_NOTICE_AT = 0.6;
8235
8460
  KEEP_ALWAYS = Number.POSITIVE_INFINITY;
8236
8461
  }
@@ -8469,17 +8694,42 @@ function earnedToday(w) {
8469
8694
  }
8470
8695
  async function listModels(client) {
8471
8696
  const models = await client.models();
8472
- const free = await client.wallet().then((w) => w.free_tier).catch(() => void 0);
8697
+ const wallet = await client.wallet(void 0, { premium: true }).catch(() => void 0);
8698
+ const free = wallet?.free_tier;
8699
+ const premium = wallet?.premium;
8473
8700
  console.log(c.bold("Models (credit prices per 1M tokens)"));
8474
8701
  for (const m of models) {
8475
8702
  const tag = m.ad_fundable ? "" : c.dim(" [paid/BYOK]");
8476
8703
  const turns2 = free?.models.includes(m.id) ? free.turns_left[m.id] ?? 0 : void 0;
8477
8704
  const freeTag = !m.free ? "" : turns2 === void 0 ? c.green(" [free]") : turns2 > 0 ? c.green(` [free: ~${turns2} turn${turns2 === 1 ? "" : "s"} left today]`) : c.dim(" [free: none left today]");
8705
+ const lockTag = m.access === "premium" && premiumLocked(premium) ? c.yellow(` [${premiumHeadline(client.premiumUnlockUsd).replace(/\.$/, "")}]`) : "";
8478
8706
  console.log(
8479
- ` ${c.cyan(m.id.padEnd(22))} ${m.tier.padEnd(8)} in ${String(m.credits_per_mtoken_input).padStart(9)} out ${String(m.credits_per_mtoken_output).padStart(9)} ${adsPerTaskLabel(m.est_ads_per_task)}${tag}${freeTag}`
8707
+ ` ${c.cyan(m.id.padEnd(22))} ${accessColumn(m, premium)} in ${String(m.credits_per_mtoken_input).padStart(9)} out ${String(m.credits_per_mtoken_output).padStart(9)} ${adsPerTaskLabel(m.est_ads_per_task)}${tag}${freeTag}${lockTag}`
8480
8708
  );
8481
8709
  }
8482
8710
  }
8711
+ function printPremiumRefusal(err) {
8712
+ console.log(c.yellow(premiumHeadline(err.unlockUsd)));
8713
+ console.log(
8714
+ c.yellow(`${err.model} is a premium model. It runs on credits you earned on the offer walls or bought.`)
8715
+ );
8716
+ if (err.premiumCredits > 0) {
8717
+ console.log(
8718
+ c.yellow(
8719
+ `This turn needs about ${err.neededCredits.toLocaleString("en-US")} of those credits, and you have ${err.premiumCredits.toLocaleString("en-US")}.`
8720
+ )
8721
+ );
8722
+ }
8723
+ const streakLine = premiumStreakLine(err.streak);
8724
+ if (streakLine) console.log(c.yellow(streakLine));
8725
+ console.log(c.yellow(`Run \`clixad earn\` for a survey on the offer walls. Offers pay ${grantLabel(err.grantPerAd)}.`));
8726
+ if (err.unlockUsd) {
8727
+ console.log(c.yellow(`Or unlock premium from ${usd(err.unlockUsd)}: start \`clixad\` and type /buy.`));
8728
+ }
8729
+ const freeLine = freeAlternativeLine(err.freeModels);
8730
+ console.log(c.green(freeLine ?? "Your bonus credits still work on budget models: `clixad models` lists them."));
8731
+ console.log(c.dim(" Nothing was charged for this request."));
8732
+ }
8483
8733
  async function setModel(client, config, model) {
8484
8734
  if (!model) return console.log(`current model: ${c.cyan(config.model)}`);
8485
8735
  const known = await client.models().catch(() => void 0);
@@ -8493,6 +8743,20 @@ async function setModel(client, config, model) {
8493
8743
  config.model = model;
8494
8744
  saveConfig(config);
8495
8745
  console.log(c.green(`\u2713 model set to ${model}`));
8746
+ const picked = known?.find((m) => m.id === model);
8747
+ if (picked?.access === "premium") {
8748
+ const w = await client.wallet(void 0, { premium: true }).catch(() => void 0);
8749
+ if (premiumLocked(w?.premium)) {
8750
+ console.log(c.yellow(premiumHeadline(client.premiumUnlockUsd)));
8751
+ console.log(
8752
+ c.yellow(
8753
+ `${model} runs on credits you earned on the offer walls or bought. Run \`clixad earn\` for a survey` + (client.premiumUnlockUsd ? `, or start \`clixad\` and type /buy.` : ".")
8754
+ )
8755
+ );
8756
+ const streakLine = premiumStreakLine(w?.premium?.streak);
8757
+ if (streakLine) console.log(c.yellow(streakLine));
8758
+ }
8759
+ }
8496
8760
  }
8497
8761
  async function showWallet(client) {
8498
8762
  const streak = await client.claimStreak();
@@ -8626,7 +8890,10 @@ ${NO_SEARCH_INSTRUCTION}`),
8626
8890
  [${cost} \xB7 balance ${res.balance} \xB7 ${res.steps} step(s)]`)
8627
8891
  );
8628
8892
  } catch (err) {
8629
- if (err instanceof PaywallError) {
8893
+ if (err instanceof PremiumLockedError) {
8894
+ console.log("");
8895
+ printPremiumRefusal(err);
8896
+ } else if (err instanceof PaywallError) {
8630
8897
  console.log(c.yellow(`
8631
8898
  ${shortfallHeadline(err, config.model)}`));
8632
8899
  const freeLine = freeAlternativeLine(err.freeModels);
@@ -8761,7 +9028,9 @@ async function runTurn(client, config, messages) {
8761
9028
  return result;
8762
9029
  } catch (err) {
8763
9030
  if (started) process.stdout.write("\n");
8764
- if (err instanceof PaywallError) {
9031
+ if (err instanceof PremiumLockedError) {
9032
+ printPremiumRefusal(err);
9033
+ } else if (err instanceof PaywallError) {
8765
9034
  console.log(c.yellow(shortfallHeadline(err, config.model)));
8766
9035
  const freeLine = freeAlternativeLine(err.freeModels);
8767
9036
  if (freeLine) console.log(c.green(freeLine));
@@ -8785,9 +9054,9 @@ async function showStreak(client) {
8785
9054
  console.log(streak.claimed ? c.green(` ${line2}`) : c.dim(` ${line2}`));
8786
9055
  return streak;
8787
9056
  }
8788
- async function safeWallet(client) {
9057
+ async function safeWallet(client, opts = {}) {
8789
9058
  try {
8790
- return await client.wallet();
9059
+ return await client.wallet(void 0, opts);
8791
9060
  } catch {
8792
9061
  return void 0;
8793
9062
  }
@@ -8813,7 +9082,7 @@ async function startupReads(client, gatewayUrl) {
8813
9082
  )
8814
9083
  ];
8815
9084
  try {
8816
- const [streak, wallet] = await Promise.all([client.claimStreak(), safeWallet(client)]);
9085
+ const [streak, wallet] = await Promise.all([client.claimStreak(), safeWallet(client, { premium: true })]);
8817
9086
  const balance = streak?.claimed ? streak.balance : wallet?.balance;
8818
9087
  return {
8819
9088
  streak,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clixad",
3
- "version": "0.0.1-beta.26",
3
+ "version": "0.0.1-beta.28",
4
4
  "description": "Free AI coding agent in your terminal, funded by rewarded ads.",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",