clixad 0.0.1-beta.18 → 0.0.1-beta.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,8 +1,12 @@
1
1
  # clixad
2
2
 
3
3
  A terminal coding agent that routes model calls through the Clixad metering gateway. No
4
- subscription: you earn credits by completing rewarded offers in your browser and spend them on real
5
- model API calls.
4
+ subscription: the cheapest models are free to use up to a daily allowance, and past that you earn
5
+ credits by completing rewarded offers in your browser and spend them on real model API calls.
6
+
7
+ `/model` marks which models are free and shows roughly how many free turns the allowance still buys
8
+ on each — the same allowance goes a lot further on the cheapest one. The status line counts it down,
9
+ and once it is gone the model carries on against credits.
6
10
 
7
11
  **Early beta.** Commands, interfaces and credit mechanics may change between beta versions. Run the
8
12
  most recent published build with:
package/dist/clixad.mjs CHANGED
@@ -207,6 +207,14 @@ async function httpFailure(res, what) {
207
207
  const detail = safeDetail(await res.text().catch(() => ""));
208
208
  return detail ? `${what} failed: ${res.status} \u2014 ${detail}` : `${what} failed: ${res.status}`;
209
209
  }
210
+ function freeMeta(meta) {
211
+ if (meta.free !== true) return {};
212
+ return {
213
+ free: true,
214
+ creditsWaived: meta.credits_waived ?? 0,
215
+ freeCreditsLeft: meta.free_credits_left ?? 0
216
+ };
217
+ }
210
218
  function accumulateToolCall(calls, part, fallbackIndex) {
211
219
  const index = part.index ?? fallbackIndex;
212
220
  const current = calls.get(index) ?? { id: "", type: "function", function: { name: "", arguments: "" } };
@@ -441,7 +449,8 @@ var init_client = __esm({
441
449
  model: report.model,
442
450
  prompt: report.prompt,
443
451
  response: report.response,
444
- ...report.steps === void 0 ? {} : { steps: report.steps }
452
+ ...report.steps === void 0 ? {} : { steps: report.steps },
453
+ ...report.free ? { free: true } : {}
445
454
  })
446
455
  });
447
456
  if (!res.ok) return null;
@@ -770,7 +779,8 @@ var init_client = __esm({
770
779
  message: choice.message,
771
780
  finishReason: choice.finish_reason,
772
781
  creditsCharged: data.clixad.credits_charged,
773
- balance: data.clixad.balance
782
+ balance: data.clixad.balance,
783
+ ...freeMeta(data.clixad)
774
784
  };
775
785
  }
776
786
  /**
@@ -792,6 +802,7 @@ var init_client = __esm({
792
802
  let content = "";
793
803
  let finishReason = "stop";
794
804
  let creditsCharged = 0;
805
+ let free = {};
795
806
  let balance = 0;
796
807
  let streamError;
797
808
  const calls = /* @__PURE__ */ new Map();
@@ -817,6 +828,7 @@ var init_client = __esm({
817
828
  if (json.clixad) {
818
829
  creditsCharged = json.clixad.credits_charged;
819
830
  balance = json.clixad.balance;
831
+ free = freeMeta(json.clixad);
820
832
  }
821
833
  }
822
834
  if (streamError) throw new Error(streamError);
@@ -824,7 +836,7 @@ var init_client = __esm({
824
836
  if (calls.size > 0) {
825
837
  message.tool_calls = [...calls.entries()].sort(([a], [b]) => a - b).map(([, c2]) => c2);
826
838
  }
827
- return { message, finishReason, creditsCharged, balance };
839
+ return { message, finishReason, creditsCharged, balance, ...free };
828
840
  }
829
841
  };
830
842
  }
@@ -3581,6 +3593,8 @@ async function runAgent(client, task, opts) {
3581
3593
  const depth = opts.depth ?? 0;
3582
3594
  const emit = (e) => opts.onEvent?.(e);
3583
3595
  let creditsCharged = 0;
3596
+ let creditsWaived = 0;
3597
+ let freeCreditsLeft;
3584
3598
  let balance = 0;
3585
3599
  let lastText = "";
3586
3600
  let pendingImages = [];
@@ -3637,6 +3651,8 @@ ${NO_SEARCH_INSTRUCTION}`,
3637
3651
  }
3638
3652
  });
3639
3653
  creditsCharged += sub.creditsCharged;
3654
+ creditsWaived += sub.creditsWaived;
3655
+ if (sub.freeCreditsLeft !== void 0) freeCreditsLeft = sub.freeCreditsLeft;
3640
3656
  if (sub.stopped === "aborted") return "the sub-agent was stopped before it finished";
3641
3657
  const answer = sub.content.trim();
3642
3658
  if (!answer) return "the sub-agent finished without reporting anything";
@@ -3658,15 +3674,26 @@ ${NO_SEARCH_INSTRUCTION}`,
3658
3674
  throw err;
3659
3675
  }
3660
3676
  creditsCharged += turn.creditsCharged;
3677
+ creditsWaived += turn.creditsWaived ?? 0;
3678
+ if (turn.freeCreditsLeft !== void 0) freeCreditsLeft = turn.freeCreditsLeft;
3661
3679
  balance = turn.balance;
3662
- emit({ type: "usage", creditsCharged: turn.creditsCharged, balance: turn.balance });
3680
+ emit({
3681
+ type: "usage",
3682
+ creditsCharged: turn.creditsCharged,
3683
+ balance: turn.balance,
3684
+ ...turn.free ? {
3685
+ free: true,
3686
+ creditsWaived: turn.creditsWaived ?? 0,
3687
+ freeCreditsLeft: turn.freeCreditsLeft ?? 0
3688
+ } : {}
3689
+ });
3663
3690
  messages.push(turn.message);
3664
3691
  const content = messageText(turn.message);
3665
3692
  if (content) lastText = content;
3666
3693
  const calls = turn.message.tool_calls ?? [];
3667
3694
  if (calls.length === 0) {
3668
3695
  emit({ type: "final", content, creditsCharged, balance });
3669
- return { content, messages, creditsCharged, balance, steps: step + 1 };
3696
+ return { content, messages, creditsCharged, balance, creditsWaived, freeCreditsLeft, steps: step + 1 };
3670
3697
  }
3671
3698
  if (content) emit({ type: "message", content });
3672
3699
  pendingImages = [];
@@ -3693,12 +3720,14 @@ ${NO_SEARCH_INSTRUCTION}`,
3693
3720
  messages,
3694
3721
  creditsCharged,
3695
3722
  balance,
3723
+ creditsWaived,
3724
+ freeCreditsLeft,
3696
3725
  steps: maxSteps,
3697
3726
  stopped: "max_steps"
3698
3727
  };
3699
3728
  function stop(reason) {
3700
3729
  emit({ type: reason });
3701
- return { content: lastText, messages, creditsCharged, balance, steps: 0, stopped: reason };
3730
+ return { content: lastText, messages, creditsCharged, balance, creditsWaived, freeCreditsLeft, steps: 0, stopped: reason };
3702
3731
  }
3703
3732
  }
3704
3733
  function toolMessage(call, content) {
@@ -5274,6 +5303,7 @@ function App({ client, config, wallet, streak, session, initialTask, notices: no
5274
5303
  const [live, setLive] = useState(null);
5275
5304
  const [balance, setBalance] = useState(wallet?.balance ?? 0);
5276
5305
  const [spent, setSpent] = useState(0);
5306
+ const [freeTier, setFreeTier] = useState(wallet?.free_tier);
5277
5307
  const [model, setModel2] = useState(config.model);
5278
5308
  const [mode, setMode] = useState("normal");
5279
5309
  const [hist, setHist] = useState(() => loadHistory());
@@ -5316,6 +5346,7 @@ function App({ client, config, wallet, streak, session, initialTask, notices: no
5316
5346
  const mcpRef = useRef(McpHub.empty());
5317
5347
  const todosRef = useRef(new TodoList());
5318
5348
  const lastTurnRef = useRef(null);
5349
+ const freeSpentNoticeRef = useRef(false);
5319
5350
  const turnCountRef = useRef(config.turns ?? 0);
5320
5351
  const deltaBufRef = useRef("");
5321
5352
  const deltaTimerRef = useRef(null);
@@ -5590,6 +5621,11 @@ function App({ client, config, wallet, streak, session, initialTask, notices: no
5590
5621
  case "usage":
5591
5622
  setBalance(event.balance);
5592
5623
  setSpent((s) => s + event.creditsCharged);
5624
+ if (event.free) {
5625
+ setFreeTier(
5626
+ (f) => f ? { ...f, credits_left: event.freeCreditsLeft ?? f.credits_left } : f
5627
+ );
5628
+ }
5593
5629
  return;
5594
5630
  default:
5595
5631
  return;
@@ -5681,14 +5717,28 @@ ${NO_SEARCH_INSTRUCTION}`),
5681
5717
  push({
5682
5718
  kind: "assistant",
5683
5719
  text: result.content,
5684
- meta: `[${result.creditsCharged} credits \xB7 balance ${result.balance.toLocaleString("en-US")}]`
5720
+ meta: turnMeta(result)
5685
5721
  });
5722
+ if (freeTier?.models.includes(model) && result.creditsWaived === 0 && result.creditsCharged > 0 && (result.freeCreditsLeft ?? freeTier.credits_left) === 0 && !freeSpentNoticeRef.current) {
5723
+ freeSpentNoticeRef.current = true;
5724
+ push({
5725
+ kind: "notice",
5726
+ tone: "warn",
5727
+ text: ` Today's free turns are used up. This one cost ${result.creditsCharged.toLocaleString("en-US")} credits.
5728
+ The free allowance resets at midnight UTC. /earn tops up before then.`
5729
+ });
5730
+ }
5686
5731
  lastTurnRef.current = {
5687
5732
  turn: turnCountRef.current,
5688
5733
  model,
5689
5734
  prompt: task,
5690
5735
  response: result.content,
5691
- steps: result.steps
5736
+ steps: result.steps,
5737
+ // Whether the free tier paid for it. Sent with the rating, where it
5738
+ // can only ever lower the grant — see `FeedbackReport.free`. A turn
5739
+ // that was partly free counts as free: we funded some of it, and of
5740
+ // the two roundings this is the one that is ours to absorb.
5741
+ free: result.creditsWaived > 0
5692
5742
  };
5693
5743
  turnCountRef.current += 1;
5694
5744
  config.turns = turnCountRef.current;
@@ -5937,10 +5987,15 @@ ${NO_SEARCH_INSTRUCTION}`),
5937
5987
  if (arg && !models.some((m) => m.id === arg)) {
5938
5988
  push({ kind: "notice", tone: "warn", text: ` unknown model: ${arg}` });
5939
5989
  }
5990
+ const free = await client.wallet(signal).then((w) => {
5991
+ setBalance(w.balance);
5992
+ setFreeTier(w.free_tier);
5993
+ return w.free_tier;
5994
+ }).catch(() => freeTier);
5940
5995
  const items = models.map((m) => ({
5941
5996
  value: m.id,
5942
5997
  label: m.id,
5943
- hint: `${m.tier.padEnd(8)}${adsPerTaskLabel(m.est_ads_per_task)}`,
5998
+ hint: `${m.tier.padEnd(8)}${modelHint(m, free)}`,
5944
5999
  current: m.id === model
5945
6000
  }));
5946
6001
  const preselect = items.findIndex((i) => i.value === arg);
@@ -5964,7 +6019,7 @@ ${NO_SEARCH_INSTRUCTION}`),
5964
6019
  catalogRef.current = ms;
5965
6020
  push({
5966
6021
  kind: "notice",
5967
- text: ms.map((m) => ` ${m.id.padEnd(24)} ${adsPerTaskLabel(m.est_ads_per_task)}`).join("\n")
6022
+ text: ms.map((m) => ` ${m.id.padEnd(24)} ${modelHint(m, freeTier)}`).join("\n")
5968
6023
  });
5969
6024
  });
5970
6025
  return;
@@ -6187,7 +6242,7 @@ ${output}` }
6187
6242
  const turn = lastTurnRef.current;
6188
6243
  if (!turn) return;
6189
6244
  lastTurnRef.current = null;
6190
- const amount = wallet?.rewards?.feedback_credits;
6245
+ const amount = turn.free ? wallet?.rewards?.feedback_credits_free ?? wallet?.rewards?.feedback_credits : wallet?.rewards?.feedback_credits;
6191
6246
  const pay = amount ? `Both answers pay ${amount.toLocaleString("en-US")} credits` : "Both answers pay the same";
6192
6247
  const choice = await new Promise((resolve2) => {
6193
6248
  setPickerSel(0);
@@ -6559,7 +6614,17 @@ ${dropped.map((l) => ` \xB7 ${l}`).join("\n")}`
6559
6614
  const modeText = ` ${MODE_STYLE[mode].glyph} ${MODE_LABEL[mode]}`;
6560
6615
  const statusText = fitRow(
6561
6616
  ` ${statusLine(
6562
- { model, balance, spent, contextPct, minutes: (Date.now() - runStartedAtRef.current) / 6e4 },
6617
+ {
6618
+ model,
6619
+ balance,
6620
+ spent,
6621
+ contextPct,
6622
+ minutes: (Date.now() - runStartedAtRef.current) / 6e4,
6623
+ // Only while a free model is selected: on a premium one the allowance
6624
+ // is real but irrelevant, and a countdown that has nothing to do with
6625
+ // the next turn is a number that teaches people to stop reading the row.
6626
+ ...freeTier?.models.includes(model) ? { freeLeft: freeTier.credits_left } : {}
6627
+ },
6563
6628
  Math.max(1, cols - visibleLength(modeText) - 2)
6564
6629
  )}`,
6565
6630
  Math.max(0, cols - visibleLength(modeText))
@@ -6667,11 +6732,35 @@ function renderInput(state, from, rows) {
6667
6732
  ] }, row);
6668
6733
  });
6669
6734
  }
6735
+ function modelHint(m, free) {
6736
+ const effort = adsPerTaskLabel(m.est_ads_per_task);
6737
+ if (!m.free || !free?.models.includes(m.id)) return effort;
6738
+ const turns = free.turns_left[m.id] ?? 0;
6739
+ if (turns <= 0) return `free: none left today \xB7 ${effort}`;
6740
+ return `free: ~${turns} turn${turns === 1 ? "" : "s"} left \xB7 ${effort}`;
6741
+ }
6742
+ function turnMeta(result) {
6743
+ const n = (v) => v.toLocaleString("en-US");
6744
+ const balance = `balance ${n(result.balance)}`;
6745
+ if (result.creditsWaived <= 0) return `[${result.creditsCharged} credits \xB7 ${balance}]`;
6746
+ const left = result.freeCreditsLeft === void 0 ? "" : ` \xB7 ${n(result.freeCreditsLeft)} free credits left`;
6747
+ if (result.creditsCharged > 0) {
6748
+ return `[free: ${n(result.creditsWaived)} credits \xB7 then ${n(result.creditsCharged)} charged \xB7 ${balance}]`;
6749
+ }
6750
+ return `[free \xB7 ${n(result.creditsWaived)} credits waived${left}]`;
6751
+ }
6670
6752
  function statusLine(o, cols) {
6671
6753
  const burn = o.spent > 0 && o.minutes >= 1 ? `${Math.round(o.spent / o.minutes).toLocaleString("en-US")} cr/min` : void 0;
6672
6754
  const clauses = [
6673
6755
  { text: o.model, rank: KEEP_ALWAYS },
6674
6756
  { text: `${o.balance.toLocaleString("en-US")} cr`, rank: KEEP_ALWAYS },
6757
+ // Above the context warning, below the balance. It is the second number
6758
+ // deciding whether the next turn costs anything, and unlike the spend it is
6759
+ // *news* — it only exists while a free model is selected and it counts down.
6760
+ {
6761
+ text: o.freeLeft === void 0 ? void 0 : `free ${o.freeLeft.toLocaleString("en-US")}`,
6762
+ rank: 4
6763
+ },
6675
6764
  { text: o.spent > 0 ? `\u2212${o.spent.toLocaleString("en-US")}` : void 0, rank: 2 },
6676
6765
  { text: burn, rank: 1 },
6677
6766
  {
@@ -6682,7 +6771,7 @@ function statusLine(o, cols) {
6682
6771
  ].filter((c2) => Boolean(c2.text));
6683
6772
  const join7 = (minRank) => clauses.filter((c2) => c2.rank >= minRank).map((c2) => c2.text).join(" \xB7 ");
6684
6773
  let line2 = join7(0);
6685
- for (let minRank = 1; minRank <= 4 && visibleLength(line2) > cols; minRank++) line2 = join7(minRank);
6774
+ for (let minRank = 1; minRank <= 5 && visibleLength(line2) > cols; minRank++) line2 = join7(minRank);
6686
6775
  return fitRow(line2, cols);
6687
6776
  }
6688
6777
  function windowStart(sel, total, size) {
@@ -7023,11 +7112,14 @@ function earnedToday(w) {
7023
7112
  }
7024
7113
  async function listModels(client) {
7025
7114
  const models = await client.models();
7115
+ const free = await client.wallet().then((w) => w.free_tier).catch(() => void 0);
7026
7116
  console.log(c.bold("Models (credit prices per 1M tokens)"));
7027
7117
  for (const m of models) {
7028
7118
  const tag = m.ad_fundable ? "" : c.dim(" [paid/BYOK]");
7119
+ const turns = free?.models.includes(m.id) ? free.turns_left[m.id] ?? 0 : void 0;
7120
+ const freeTag = !m.free ? "" : turns === void 0 ? c.green(" [free]") : turns > 0 ? c.green(` [free: ~${turns} turn${turns === 1 ? "" : "s"} left today]`) : c.dim(" [free: none left today]");
7029
7121
  console.log(
7030
- ` ${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}`
7122
+ ` ${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}`
7031
7123
  );
7032
7124
  }
7033
7125
  }
@@ -7050,6 +7142,15 @@ async function showWallet(client) {
7050
7142
  const w = await client.wallet();
7051
7143
  console.log(`balance: ${c.bold(w.balance.toLocaleString("en-US"))} credits`);
7052
7144
  console.log(`${earnedToday(w)} \xB7 offers pay ${grantRangeLabel(w.grant_per_ad_range)}`);
7145
+ const free = w.free_tier;
7146
+ if (free && free.daily_credits > 0) {
7147
+ 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];
7148
+ console.log(
7149
+ free.credits_left > 0 ? c.green(
7150
+ `free today: ${credits(free.credits_left)} of ${credits(free.daily_credits)} credits left` + (best ? ` \xB7 ~${best.turns} turn${best.turns === 1 ? "" : "s"} on ${best.id}` : "")
7151
+ ) : c.dim(`free today: none left of ${credits(free.daily_credits)} credits \u2014 resets at midnight UTC`)
7152
+ );
7153
+ }
7053
7154
  const line2 = streak && streakLabel(streak);
7054
7155
  if (line2) console.log(streak.claimed ? c.green(line2) : c.dim(line2));
7055
7156
  if (w.ledger.length) {
@@ -7244,9 +7345,10 @@ ${NO_SEARCH_INSTRUCTION}`),
7244
7345
  todos: new TodoList(),
7245
7346
  onEvent: printAgentEvent
7246
7347
  });
7348
+ const cost = res.creditsWaived > 0 ? res.creditsCharged > 0 ? `free: ${res.creditsWaived} credits \xB7 then ${res.creditsCharged} charged` : `free \xB7 ${res.creditsWaived} credits waived` : `${res.creditsCharged} credits`;
7247
7349
  console.log(
7248
7350
  c.dim(`
7249
- [${res.creditsCharged} credits \xB7 balance ${res.balance} \xB7 ${res.steps} step(s)]`)
7351
+ [${cost} \xB7 balance ${res.balance} \xB7 ${res.steps} step(s)]`)
7250
7352
  );
7251
7353
  } catch (err) {
7252
7354
  if (err instanceof PaywallError) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clixad",
3
- "version": "0.0.1-beta.18",
3
+ "version": "0.0.1-beta.19",
4
4
  "description": "Free AI coding agent in your terminal, funded by rewarded ads.",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",