clixad 0.0.1-beta.26 → 0.0.1-beta.27

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 +249 -32
  2. package/package.json +1 -1
package/dist/clixad.mjs CHANGED
@@ -302,6 +302,23 @@ 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) {
314
+ return typeof unlockUsd === "number" && unlockUsd > 0 ? `premium: a survey or ${usd(unlockUsd)} unlocks it` : "premium: a survey unlocks it";
315
+ }
316
+ function premiumHeadline(unlockUsd) {
317
+ return typeof unlockUsd === "number" && unlockUsd > 0 ? `Premium: complete a survey or unlock from ${usd(unlockUsd)}.` : "Premium: complete a survey to unlock it.";
318
+ }
319
+ function usd(v) {
320
+ return Number.isInteger(v) ? `$${v}` : `$${v.toFixed(2)}`;
321
+ }
305
322
  function adsPerTaskLabel(estAdsPerTask) {
306
323
  const effort = estAdsPerTask >= 1 ? `~${estAdsPerTask} ads/task` : "<1 ad/task";
307
324
  return `${effort} on completion (median)`;
@@ -405,16 +422,35 @@ function asSignupClosed(status, data) {
405
422
  const d = data;
406
423
  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
424
  }
425
+ function premiumLockedError(message, model, body) {
426
+ const num = (v) => typeof v === "number" && Number.isFinite(v) ? v : 0;
427
+ return new PremiumLockedError(
428
+ message,
429
+ typeof body?.model === "string" ? body.model : model,
430
+ num(body?.needed_credits),
431
+ num(body?.premium_credits),
432
+ typeof body?.unlock_usd === "number" && body.unlock_usd > 0 ? body.unlock_usd : null,
433
+ num(body?.grant_per_ad),
434
+ freeAlternatives(body?.free_models)
435
+ );
436
+ }
408
437
  function shortfallHeadline(err, model) {
409
438
  const n = (v) => Math.max(0, Math.round(v)).toLocaleString("en-US");
410
439
  if (err.balance <= 0) return `Out of credits (this needs about ${n(err.estimatedCost)}).`;
411
440
  return `Not enough credits for this turn${model ? ` on ${model}` : ""}: it needs about ${n(err.estimatedCost)}, and the balance is ${n(err.balance)}.`;
412
441
  }
413
442
  function paywallError(body) {
414
- const free = Array.isArray(body.free_models) ? body.free_models.filter(
443
+ return new PaywallError(body.balance, body.estimated_cost, body.grant_per_ad, freeAlternatives(body.free_models));
444
+ }
445
+ function freeAlternatives(list) {
446
+ return Array.isArray(list) ? list.filter(
415
447
  (m) => !!m && typeof m.id === "string" && m.id.length > 0 && Number.isFinite(m.turns_left)
416
448
  ).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);
449
+ }
450
+ async function refusal402(res, model) {
451
+ const body = await res.json();
452
+ if (body.error === "premium_locked") return premiumLockedError(body.message ?? "", model, body.premium);
453
+ return paywallError(body);
418
454
  }
419
455
  function neverConnected(code) {
420
456
  return code !== void 0 && NEVER_CONNECTED.has(code);
@@ -540,7 +576,7 @@ async function* parseSSE(body) {
540
576
  }
541
577
  }
542
578
  }
543
- var CHAT_ENCODED_CONTENT_TYPE, LEDGER_REASONS, SignupClosedError, PaywallError, GatewayUnreachableError, NEVER_CONNECTED, AuthError, GatewayClient;
579
+ var CHAT_ENCODED_CONTENT_TYPE, LEDGER_REASONS, SignupClosedError, PaywallError, PremiumLockedError, GatewayUnreachableError, NEVER_CONNECTED, AuthError, GatewayClient;
544
580
  var init_client = __esm({
545
581
  "src/client.ts"() {
546
582
  "use strict";
@@ -590,6 +626,24 @@ var init_client = __esm({
590
626
  grantPerAd;
591
627
  freeModels;
592
628
  };
629
+ PremiumLockedError = class extends Error {
630
+ constructor(message, model, neededCredits, premiumCredits, unlockUsd, grantPerAd, freeModels) {
631
+ super(message);
632
+ this.model = model;
633
+ this.neededCredits = neededCredits;
634
+ this.premiumCredits = premiumCredits;
635
+ this.unlockUsd = unlockUsd;
636
+ this.grantPerAd = grantPerAd;
637
+ this.freeModels = freeModels;
638
+ this.name = "PremiumLockedError";
639
+ }
640
+ model;
641
+ neededCredits;
642
+ premiumCredits;
643
+ unlockUsd;
644
+ grantPerAd;
645
+ freeModels;
646
+ };
593
647
  GatewayUnreachableError = class extends Error {
594
648
  constructor(host, code) {
595
649
  const where = `${host}${code ? ` (${code})` : ""}`;
@@ -886,6 +940,7 @@ var init_client = __esm({
886
940
  if (!res.ok) throw new Error(`models failed: ${res.status}`);
887
941
  const data = await res.json();
888
942
  this.seen = { web_search: data.capabilities?.web_search === true };
943
+ this.unlockUsd = typeof data.premium_unlock_usd === "number" && data.premium_unlock_usd > 0 ? data.premium_unlock_usd : null;
889
944
  this.shape = parseTaskShape(data.task_shape);
890
945
  this.latest = typeof data.cli_latest === "string" ? data.cli_latest : void 0;
891
946
  const live = new Set(data.data.map((m) => m.id));
@@ -936,8 +991,23 @@ var init_client = __esm({
936
991
  return this.seen;
937
992
  }
938
993
  seen = { web_search: false };
939
- async wallet(signal) {
940
- const res = await gatewayFetch(`${this.config.gatewayUrl}/v1/wallet`, { headers: this.headers(), signal });
994
+ /**
995
+ * What unlocks premium for every credit on the account (the cheapest pack,
996
+ * USD), as of the last catalog fetch. Null where the gateway sells none or
997
+ * did not say, which the hints read as "a survey unlocks it".
998
+ */
999
+ get premiumUnlockUsd() {
1000
+ return this.unlockUsd;
1001
+ }
1002
+ unlockUsd = null;
1003
+ /**
1004
+ * `premium: true` asks for premium access too, which costs the gateway a read
1005
+ * of this account's whole ledger. Asked for where it is shown (`/model`,
1006
+ * `clixad models`), never by the polls `/earn` and a purchase run.
1007
+ */
1008
+ async wallet(signal, opts = {}) {
1009
+ const url = `${this.config.gatewayUrl}/v1/wallet${opts.premium ? "?premium=1" : ""}`;
1010
+ const res = await gatewayFetch(url, { headers: this.headers(), signal });
941
1011
  if (res.status === 401) throw await authFailure(res, "wallet");
942
1012
  if (!res.ok) throw new Error(`wallet failed: ${res.status}`);
943
1013
  return res.json();
@@ -1234,9 +1304,7 @@ var init_client = __esm({
1234
1304
  signal
1235
1305
  );
1236
1306
  if (res.status === 401) throw await authFailure(res, "chat");
1237
- if (res.status === 402) {
1238
- throw paywallError(await res.json());
1239
- }
1307
+ if (res.status === 402) throw await refusal402(res, model);
1240
1308
  if (!res.ok) throw new Error(await httpFailure(res, "chat"));
1241
1309
  const data = await res.json();
1242
1310
  const choice = data.choices[0];
@@ -1267,9 +1335,7 @@ var init_client = __esm({
1267
1335
  opts.signal
1268
1336
  );
1269
1337
  if (res.status === 401) throw await authFailure(res, "chat");
1270
- if (res.status === 402) {
1271
- throw paywallError(await res.json());
1272
- }
1338
+ if (res.status === 402) throw await refusal402(res, model);
1273
1339
  if (!res.ok || !res.body) throw new Error(await httpFailure(res, "chat"));
1274
1340
  let content = "";
1275
1341
  let finishReason = "stop";
@@ -1277,6 +1343,7 @@ var init_client = __esm({
1277
1343
  let free = {};
1278
1344
  let balance = 0;
1279
1345
  let streamError;
1346
+ let premiumRefusal;
1280
1347
  const calls = /* @__PURE__ */ new Map();
1281
1348
  const reasoning = [];
1282
1349
  for await (const evt of parseSSE(res.body)) {
@@ -1299,12 +1366,16 @@ var init_client = __esm({
1299
1366
  mergeReasoningDetails(reasoning, choice?.delta?.reasoning_details);
1300
1367
  if (choice?.finish_reason) finishReason = choice.finish_reason;
1301
1368
  if (json.error) streamError = json.error.message || "the model stream failed";
1369
+ if (json.error?.type === "premium_locked") {
1370
+ premiumRefusal = premiumLockedError(streamError ?? "", model, json.clixad?.premium);
1371
+ }
1302
1372
  if (json.clixad) {
1303
1373
  creditsCharged = json.clixad.credits_charged;
1304
1374
  balance = json.clixad.balance;
1305
1375
  free = freeMeta(json.clixad);
1306
1376
  }
1307
1377
  }
1378
+ if (premiumRefusal) throw premiumRefusal;
1308
1379
  if (streamError) throw new Error(streamError);
1309
1380
  const message = { role: "assistant", content: content || null };
1310
1381
  if (calls.size > 0) {
@@ -6150,6 +6221,16 @@ function App({
6150
6221
  const pendingFreeRef = useRef([]);
6151
6222
  const pendingGrantRef = useRef(0);
6152
6223
  const pendingBalanceRef = useRef(void 0);
6224
+ const pendingPremiumRef = useRef(null);
6225
+ const [premium, setPremiumState] = useState(wallet?.premium);
6226
+ const premiumRef = useRef(wallet?.premium);
6227
+ const setPremium = useCallback((next) => {
6228
+ premiumRef.current = next;
6229
+ setPremiumState(next);
6230
+ }, []);
6231
+ const refreshPremium = useCallback(() => {
6232
+ void client.wallet(void 0, { premium: true }).then((w) => setPremium(w.premium)).catch(() => void 0);
6233
+ }, [client, setPremium]);
6153
6234
  const mcpRef = useRef(McpHub.empty());
6154
6235
  const todosRef = useRef(new TodoList());
6155
6236
  const lastTurnRef = useRef(null);
@@ -6259,9 +6340,10 @@ function App({
6259
6340
  useEffect(() => {
6260
6341
  if (wallet) return;
6261
6342
  const ctl = new AbortController();
6262
- void client.wallet(ctl.signal).then((w) => {
6343
+ void client.wallet(ctl.signal, { premium: true }).then((w) => {
6263
6344
  setBalance((current) => current ?? w.balance);
6264
6345
  setFreeTier((current) => current ?? w.free_tier);
6346
+ if (premiumRef.current === void 0) setPremium(w.premium);
6265
6347
  }).catch(() => void 0);
6266
6348
  return () => ctl.abort();
6267
6349
  }, []);
@@ -6556,6 +6638,13 @@ function App({
6556
6638
  return;
6557
6639
  case "usage":
6558
6640
  setBalance(event.balance);
6641
+ if (event.creditsCharged > 0 && premiumRef.current) {
6642
+ const p = premiumRef.current;
6643
+ setPremium({
6644
+ unlocked: p.unlocked,
6645
+ credits: Math.max(0, p.unlocked ? event.balance : Math.min(event.balance, p.credits - event.creditsCharged))
6646
+ });
6647
+ }
6559
6648
  setSpent((s) => s + event.creditsCharged);
6560
6649
  if (event.free) {
6561
6650
  setFreeTier(
@@ -6567,11 +6656,12 @@ function App({
6567
6656
  return;
6568
6657
  }
6569
6658
  },
6570
- [cols, flushDelta, push]
6659
+ [cols, flushDelta, push, setPremium]
6571
6660
  );
6572
6661
  const runTurn2 = useCallback(
6573
6662
  async (task, images = []) => {
6574
6663
  pendingFreeRef.current = [];
6664
+ pendingPremiumRef.current = null;
6575
6665
  const model2 = modelRef.current;
6576
6666
  const ac = new AbortController();
6577
6667
  abortRef.current = ac;
@@ -6703,7 +6793,20 @@ ${NO_SEARCH_INSTRUCTION}`),
6703
6793
  }
6704
6794
  }
6705
6795
  } catch (err) {
6706
- if (err instanceof PaywallError) {
6796
+ if (err instanceof PremiumLockedError) {
6797
+ pendingTaskRef.current = { task, images };
6798
+ pendingPremiumRef.current = err;
6799
+ setPremium({ unlocked: false, credits: err.premiumCredits });
6800
+ 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")}.
6801
+ ` : "";
6802
+ push({
6803
+ kind: "notice",
6804
+ tone: "warn",
6805
+ text: ` ${premiumHeadline(err.unlockUsd)}
6806
+ ${err.model} is a premium model. It runs on credits you earned on the offer walls or bought.
6807
+ ` + short2 + " 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."
6808
+ });
6809
+ } else if (err instanceof PaywallError) {
6707
6810
  pendingTaskRef.current = { task, images };
6708
6811
  pendingFreeRef.current = err.freeModels;
6709
6812
  pendingGrantRef.current = err.grantPerAd;
@@ -6898,8 +7001,9 @@ ${NO_SEARCH_INSTRUCTION}`),
6898
7001
  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
7002
  });
6900
7003
  push({ kind: "notice", tone: "good", text: " credits added \u2014 continuing" });
7004
+ refreshPremium();
6901
7005
  if (pending) await runTurn2(pending.task, pending.images);
6902
- }, [balance, client, config.dashboardUrl, push, runBusy, runTurn2]);
7006
+ }, [balance, client, config.dashboardUrl, push, refreshPremium, runBusy, runTurn2]);
6903
7007
  const runPaywallChoice = useCallback(() => {
6904
7008
  const free = pendingFreeRef.current.filter((m) => m.id !== EARN_ROW);
6905
7009
  if (free.length === 0) return void runAdWall();
@@ -6969,6 +7073,47 @@ ${NO_SEARCH_INSTRUCTION}`),
6969
7073
  });
6970
7074
  });
6971
7075
  }, [client, push, runBusy]);
7076
+ const runPremiumChoice = useCallback(() => {
7077
+ const refusal = pendingPremiumRef.current;
7078
+ if (!refusal) return void runPaywallChoice();
7079
+ const items = [
7080
+ {
7081
+ value: EARN_ROW,
7082
+ label: "complete a survey",
7083
+ hint: `opens the offer walls, offers pay ${grantLabel(refusal.grantPerAd)}`
7084
+ },
7085
+ ...refusal.unlockUsd ? [
7086
+ {
7087
+ value: BUY_ROW,
7088
+ label: `unlock from ${usd(refusal.unlockUsd)}`,
7089
+ hint: "any pack, and premium then takes all your credits"
7090
+ }
7091
+ ] : [],
7092
+ ...refusal.freeModels.filter((m) => m.id !== EARN_ROW && m.id !== BUY_ROW).map((m) => ({
7093
+ value: m.id,
7094
+ label: m.id,
7095
+ hint: `budget, free: ~${m.turnsLeft} turn${m.turnsLeft === 1 ? "" : "s"} left today`
7096
+ }))
7097
+ ];
7098
+ setPickerSel(0);
7099
+ setPicker({
7100
+ title: premiumHeadline(refusal.unlockUsd),
7101
+ subtitle: "Unlock premium, or carry on with a budget model.",
7102
+ items,
7103
+ onPick: (choice) => {
7104
+ if (choice.value === EARN_ROW) return void runAdWall();
7105
+ if (choice.value === BUY_ROW) return void runBuy();
7106
+ const pending = pendingTaskRef.current;
7107
+ pendingTaskRef.current = null;
7108
+ pendingPremiumRef.current = null;
7109
+ config.model = choice.value;
7110
+ saveConfig(config);
7111
+ switchModel(choice.value);
7112
+ push({ kind: "notice", tone: "good", text: ` model \u2192 ${choice.value} (budget) \xB7 continuing where you left off` });
7113
+ if (pending) void runTurn2(pending.task, pending.images);
7114
+ }
7115
+ });
7116
+ }, [config, push, runAdWall, runBuy, runPaywallChoice, runTurn2, switchModel]);
6972
7117
  const runCheckout = useCallback(
6973
7118
  async (pack) => {
6974
7119
  let before = balance ?? (await client.wallet().catch(() => void 0))?.balance;
@@ -7006,11 +7151,20 @@ ${NO_SEARCH_INSTRUCTION}`),
7006
7151
  stopped = signal.aborted;
7007
7152
  });
7008
7153
  if (credited) {
7009
- return push({
7154
+ push({
7010
7155
  kind: "notice",
7011
7156
  tone: "good",
7012
7157
  text: " credits added \u2014 an invoice is on its way to the address on the payment."
7013
7158
  });
7159
+ refreshPremium();
7160
+ const pending = pendingPremiumRef.current ? pendingTaskRef.current : null;
7161
+ if (pending) {
7162
+ pendingTaskRef.current = null;
7163
+ pendingPremiumRef.current = null;
7164
+ push({ kind: "notice", tone: "good", text: " premium is unlocked \xB7 continuing where you left off" });
7165
+ await runTurn2(pending.task, pending.images);
7166
+ }
7167
+ return;
7014
7168
  }
7015
7169
  push({
7016
7170
  kind: "notice",
@@ -7018,7 +7172,7 @@ ${NO_SEARCH_INSTRUCTION}`),
7018
7172
  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
7173
  });
7020
7174
  },
7021
- [balance, client, push, runBusy]
7175
+ [balance, client, push, refreshPremium, runBusy, runTurn2]
7022
7176
  );
7023
7177
  const runCommand = useCallback(
7024
7178
  async (line2) => {
@@ -7099,15 +7253,21 @@ ${NO_SEARCH_INSTRUCTION}`),
7099
7253
  if (arg && !models.some((m) => m.id === arg)) {
7100
7254
  push({ kind: "notice", tone: "warn", text: ` unknown model: ${arg}` });
7101
7255
  }
7102
- const free = await client.wallet(signal).then((w) => {
7256
+ const free = await client.wallet(signal, { premium: true }).then((w) => {
7103
7257
  setBalance(w.balance);
7104
7258
  setFreeTier(w.free_tier);
7259
+ setPremium(w.premium);
7105
7260
  return w.free_tier;
7106
- }).catch(() => freeTier);
7261
+ }).catch(() => {
7262
+ setPremium(void 0);
7263
+ return freeTier;
7264
+ });
7265
+ const premium2 = premiumRef.current;
7266
+ const unlockUsd = client.premiumUnlockUsd;
7107
7267
  const items = models.map((m) => ({
7108
7268
  value: m.id,
7109
7269
  label: m.id,
7110
- hint: `${m.tier.padEnd(8)}${modelHint(m, free)}`,
7270
+ hint: accessColumn(m, premium2) + (m.access === "premium" && premiumLocked(premium2) ? premiumUnlockHint(unlockUsd) : modelHint(m, free)),
7111
7271
  current: m.id === model
7112
7272
  }));
7113
7273
  const preselect = items.findIndex((i) => i.value === arg);
@@ -7121,6 +7281,16 @@ ${NO_SEARCH_INSTRUCTION}`),
7121
7281
  saveConfig(config);
7122
7282
  switchModel(choice.value);
7123
7283
  push({ kind: "notice", text: ` model \u2192 ${choice.value}` });
7284
+ const picked = models.find((m) => m.id === choice.value);
7285
+ if (picked?.access === "premium" && premiumLocked(premium2)) {
7286
+ push({
7287
+ kind: "notice",
7288
+ tone: "warn",
7289
+ text: ` ${premiumHeadline(unlockUsd)}
7290
+ ${choice.value} runs on credits you earned on the offer walls or bought.
7291
+ /earn opens the offer walls` + (unlockUsd ? `, /buy unlocks premium from ${usd(unlockUsd)}.` : ".") + " Your bonus credits work on budget models."
7292
+ });
7293
+ }
7124
7294
  }
7125
7295
  });
7126
7296
  });
@@ -7189,7 +7359,7 @@ ${NO_SEARCH_INSTRUCTION}`),
7189
7359
  catalogRef.current = ms;
7190
7360
  push({
7191
7361
  kind: "notice",
7192
- text: ms.map((m) => ` ${m.id.padEnd(24)} ${modelHint(m, freeTier)}`).join("\n")
7362
+ text: ms.map((m) => ` ${m.id.padEnd(24)} ${accessColumn(m, premium)}${modelHint(m, freeTier)}`).join("\n")
7193
7363
  });
7194
7364
  });
7195
7365
  return;
@@ -7351,7 +7521,7 @@ ${NO_SEARCH_INSTRUCTION}`),
7351
7521
  push({ kind: "notice", tone: "warn", text: ` unknown command: /${cmd} \u2014 try /help` });
7352
7522
  }
7353
7523
  },
7354
- [client, cols, config, cycleMode, exit, model, push, root, runAdWall, runBusy, runBuy, runLogin, runTurn2, writeToStdout]
7524
+ [client, cols, config, cycleMode, exit, model, premium, push, root, runAdWall, runBusy, runBuy, runLogin, runTurn2, setPremium, writeToStdout]
7355
7525
  );
7356
7526
  const runShell = useCallback(
7357
7527
  async (command) => {
@@ -7681,7 +7851,9 @@ ${dropped.map((l) => ` \xB7 ${l}`).join("\n")}`
7681
7851
  if (key.return) {
7682
7852
  if (key.meta || key.shift) return setEditor(newline(editor));
7683
7853
  if (endsWithContinuation(editor)) return setEditor(continueLine(editor));
7684
- if (!busy && pendingTaskRef.current && isEmpty(editor)) return void runPaywallChoice();
7854
+ if (!busy && pendingTaskRef.current && isEmpty(editor)) {
7855
+ return void (pendingPremiumRef.current ? runPremiumChoice() : runPaywallChoice());
7856
+ }
7685
7857
  return void submit(toText(editor));
7686
7858
  }
7687
7859
  if (key.ctrl && ch === "j") return setEditor(newline(editor));
@@ -7862,7 +8034,11 @@ ${dropped.map((l) => ` \xB7 ${l}`).join("\n")}`
7862
8034
  if (!busy && info && shape && balance !== void 0 && !effortRaised(info, resolveEffort(info, effortBy[model])) && typed.trim() !== "" && !/^[/!]/.test(typed.trimStart())) {
7863
8035
  const range = estimateTask(info, conversationUsed + schemaTokens + estimateTokens(typed), shape);
7864
8036
  const onFree = Boolean(info.free && freeTier?.models.includes(model));
7865
- const verdict = taskVerdict(range, { balance, ...onFree ? { freeLeft: freeTier.credits_left } : {} });
8037
+ const spendable = info.access === "premium" && premium && !premium.unlocked ? Math.min(balance, premium.credits) : balance;
8038
+ const verdict = taskVerdict(range, {
8039
+ balance: spendable,
8040
+ ...onFree ? { freeLeft: freeTier.credits_left } : {}
8041
+ });
7866
8042
  estimate = estimateClause(range, verdict, !onFree && (freeTier?.credits_left ?? 0) > 0);
7867
8043
  }
7868
8044
  const modeText = ` ${MODE_STYLE[mode].glyph} ${MODE_LABEL[mode]}`;
@@ -8147,7 +8323,7 @@ function tailLines(text, max) {
8147
8323
  const lines = text.split("\n");
8148
8324
  return lines.length <= max ? text : lines.slice(-max).join("\n");
8149
8325
  }
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;
8326
+ 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
8327
  var init_app = __esm({
8152
8328
  "src/tui/app.tsx"() {
8153
8329
  "use strict";
@@ -8231,6 +8407,7 @@ var init_app = __esm({
8231
8407
  FALLBACK_COLS = 80;
8232
8408
  INIT_HINT_AFTER_TURNS = 3;
8233
8409
  EARN_ROW = "\0earn";
8410
+ BUY_ROW = "\0buy";
8234
8411
  CONTEXT_NOTICE_AT = 0.6;
8235
8412
  KEEP_ALWAYS = Number.POSITIVE_INFINITY;
8236
8413
  }
@@ -8469,16 +8646,39 @@ function earnedToday(w) {
8469
8646
  }
8470
8647
  async function listModels(client) {
8471
8648
  const models = await client.models();
8472
- const free = await client.wallet().then((w) => w.free_tier).catch(() => void 0);
8649
+ const wallet = await client.wallet(void 0, { premium: true }).catch(() => void 0);
8650
+ const free = wallet?.free_tier;
8651
+ const premium = wallet?.premium;
8473
8652
  console.log(c.bold("Models (credit prices per 1M tokens)"));
8474
8653
  for (const m of models) {
8475
8654
  const tag = m.ad_fundable ? "" : c.dim(" [paid/BYOK]");
8476
8655
  const turns2 = free?.models.includes(m.id) ? free.turns_left[m.id] ?? 0 : void 0;
8477
8656
  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]");
8657
+ const lockTag = m.access === "premium" && premiumLocked(premium) ? c.yellow(` [${premiumHeadline(client.premiumUnlockUsd).replace(/\.$/, "")}]`) : "";
8658
+ console.log(
8659
+ ` ${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}`
8660
+ );
8661
+ }
8662
+ }
8663
+ function printPremiumRefusal(err) {
8664
+ console.log(c.yellow(premiumHeadline(err.unlockUsd)));
8665
+ console.log(
8666
+ c.yellow(`${err.model} is a premium model. It runs on credits you earned on the offer walls or bought.`)
8667
+ );
8668
+ if (err.premiumCredits > 0) {
8478
8669
  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}`
8670
+ c.yellow(
8671
+ `This turn needs about ${err.neededCredits.toLocaleString("en-US")} of those credits, and you have ${err.premiumCredits.toLocaleString("en-US")}.`
8672
+ )
8480
8673
  );
8481
8674
  }
8675
+ console.log(c.yellow(`Run \`clixad earn\` for a survey on the offer walls. Offers pay ${grantLabel(err.grantPerAd)}.`));
8676
+ if (err.unlockUsd) {
8677
+ console.log(c.yellow(`Or unlock premium from ${usd(err.unlockUsd)}: start \`clixad\` and type /buy.`));
8678
+ }
8679
+ const freeLine = freeAlternativeLine(err.freeModels);
8680
+ console.log(c.green(freeLine ?? "Your bonus credits still work on budget models: `clixad models` lists them."));
8681
+ console.log(c.dim(" Nothing was charged for this request."));
8482
8682
  }
8483
8683
  async function setModel(client, config, model) {
8484
8684
  if (!model) return console.log(`current model: ${c.cyan(config.model)}`);
@@ -8493,6 +8693,18 @@ async function setModel(client, config, model) {
8493
8693
  config.model = model;
8494
8694
  saveConfig(config);
8495
8695
  console.log(c.green(`\u2713 model set to ${model}`));
8696
+ const picked = known?.find((m) => m.id === model);
8697
+ if (picked?.access === "premium") {
8698
+ const w = await client.wallet(void 0, { premium: true }).catch(() => void 0);
8699
+ if (premiumLocked(w?.premium)) {
8700
+ console.log(c.yellow(premiumHeadline(client.premiumUnlockUsd)));
8701
+ console.log(
8702
+ c.yellow(
8703
+ `${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.` : ".")
8704
+ )
8705
+ );
8706
+ }
8707
+ }
8496
8708
  }
8497
8709
  async function showWallet(client) {
8498
8710
  const streak = await client.claimStreak();
@@ -8626,7 +8838,10 @@ ${NO_SEARCH_INSTRUCTION}`),
8626
8838
  [${cost} \xB7 balance ${res.balance} \xB7 ${res.steps} step(s)]`)
8627
8839
  );
8628
8840
  } catch (err) {
8629
- if (err instanceof PaywallError) {
8841
+ if (err instanceof PremiumLockedError) {
8842
+ console.log("");
8843
+ printPremiumRefusal(err);
8844
+ } else if (err instanceof PaywallError) {
8630
8845
  console.log(c.yellow(`
8631
8846
  ${shortfallHeadline(err, config.model)}`));
8632
8847
  const freeLine = freeAlternativeLine(err.freeModels);
@@ -8761,7 +8976,9 @@ async function runTurn(client, config, messages) {
8761
8976
  return result;
8762
8977
  } catch (err) {
8763
8978
  if (started) process.stdout.write("\n");
8764
- if (err instanceof PaywallError) {
8979
+ if (err instanceof PremiumLockedError) {
8980
+ printPremiumRefusal(err);
8981
+ } else if (err instanceof PaywallError) {
8765
8982
  console.log(c.yellow(shortfallHeadline(err, config.model)));
8766
8983
  const freeLine = freeAlternativeLine(err.freeModels);
8767
8984
  if (freeLine) console.log(c.green(freeLine));
@@ -8785,9 +9002,9 @@ async function showStreak(client) {
8785
9002
  console.log(streak.claimed ? c.green(` ${line2}`) : c.dim(` ${line2}`));
8786
9003
  return streak;
8787
9004
  }
8788
- async function safeWallet(client) {
9005
+ async function safeWallet(client, opts = {}) {
8789
9006
  try {
8790
- return await client.wallet();
9007
+ return await client.wallet(void 0, opts);
8791
9008
  } catch {
8792
9009
  return void 0;
8793
9010
  }
@@ -8813,7 +9030,7 @@ async function startupReads(client, gatewayUrl) {
8813
9030
  )
8814
9031
  ];
8815
9032
  try {
8816
- const [streak, wallet] = await Promise.all([client.claimStreak(), safeWallet(client)]);
9033
+ const [streak, wallet] = await Promise.all([client.claimStreak(), safeWallet(client, { premium: true })]);
8817
9034
  const balance = streak?.claimed ? streak.balance : wallet?.balance;
8818
9035
  return {
8819
9036
  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.27",
4
4
  "description": "Free AI coding agent in your terminal, funded by rewarded ads.",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",