clixad 0.0.1-beta.17 → 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: "" } };
@@ -246,6 +254,11 @@ var init_client = __esm({
246
254
  referral_bonus: "referral bonus",
247
255
  redeem_code: "code redeemed",
248
256
  ad_reward: "offer reward",
257
+ // Our own survey, and it is deliberately not called an offer. Every other
258
+ // earn line on this list is a network paying for something; this one is us.
259
+ // The name is the only place a user would ever learn the difference, and
260
+ // "offer reward" would file it next to the wall it did not come from.
261
+ own_survey_reward: "Clixad survey",
249
262
  ad_screenout_bonus: "screenout bonus (didn't qualify)",
250
263
  ad_sponsor_impression: "sponsored line shown",
251
264
  ad_reversal: "offer reward reversed by the provider",
@@ -436,7 +449,8 @@ var init_client = __esm({
436
449
  model: report.model,
437
450
  prompt: report.prompt,
438
451
  response: report.response,
439
- ...report.steps === void 0 ? {} : { steps: report.steps }
452
+ ...report.steps === void 0 ? {} : { steps: report.steps },
453
+ ...report.free ? { free: true } : {}
440
454
  })
441
455
  });
442
456
  if (!res.ok) return null;
@@ -765,7 +779,8 @@ var init_client = __esm({
765
779
  message: choice.message,
766
780
  finishReason: choice.finish_reason,
767
781
  creditsCharged: data.clixad.credits_charged,
768
- balance: data.clixad.balance
782
+ balance: data.clixad.balance,
783
+ ...freeMeta(data.clixad)
769
784
  };
770
785
  }
771
786
  /**
@@ -787,6 +802,7 @@ var init_client = __esm({
787
802
  let content = "";
788
803
  let finishReason = "stop";
789
804
  let creditsCharged = 0;
805
+ let free = {};
790
806
  let balance = 0;
791
807
  let streamError;
792
808
  const calls = /* @__PURE__ */ new Map();
@@ -812,6 +828,7 @@ var init_client = __esm({
812
828
  if (json.clixad) {
813
829
  creditsCharged = json.clixad.credits_charged;
814
830
  balance = json.clixad.balance;
831
+ free = freeMeta(json.clixad);
815
832
  }
816
833
  }
817
834
  if (streamError) throw new Error(streamError);
@@ -819,14 +836,25 @@ var init_client = __esm({
819
836
  if (calls.size > 0) {
820
837
  message.tool_calls = [...calls.entries()].sort(([a], [b]) => a - b).map(([, c2]) => c2);
821
838
  }
822
- return { message, finishReason, creditsCharged, balance };
839
+ return { message, finishReason, creditsCharged, balance, ...free };
823
840
  }
824
841
  };
825
842
  }
826
843
  });
827
844
 
828
845
  // src/config.ts
829
- import { chmodSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
846
+ import {
847
+ chmodSync,
848
+ closeSync,
849
+ fsyncSync,
850
+ mkdirSync,
851
+ openSync,
852
+ readFileSync,
853
+ renameSync,
854
+ unlinkSync,
855
+ writeFileSync,
856
+ writeSync
857
+ } from "node:fs";
830
858
  import { homedir } from "node:os";
831
859
  import { dirname, join } from "node:path";
832
860
  function sameEndpoint(a, b) {
@@ -841,11 +869,42 @@ function sameEndpoint(a, b) {
841
869
  };
842
870
  return norm(a) === norm(b);
843
871
  }
844
- function loadConfig() {
845
- let stored = {};
872
+ function takeConfigNotices() {
873
+ const out = notices;
874
+ notices = [];
875
+ return out;
876
+ }
877
+ function readStored() {
878
+ let raw;
846
879
  try {
847
- stored = JSON.parse(readFileSync(CONFIG_PATH, "utf8"));
880
+ raw = readFileSync(CONFIG_PATH, "utf8");
881
+ } catch (err) {
882
+ const code = err.code;
883
+ if (code === "ENOENT") return { stored: {} };
884
+ return { stored: {}, problem: `could not be read (${code ?? err.message})` };
885
+ }
886
+ if (raw.trim() === "") return { stored: {}, problem: "was empty", raw };
887
+ let parsed;
888
+ try {
889
+ parsed = JSON.parse(raw);
848
890
  } catch {
891
+ return { stored: {}, problem: "is not valid JSON", raw };
892
+ }
893
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
894
+ return { stored: {}, problem: "is not a JSON object", raw };
895
+ }
896
+ return { stored: parsed };
897
+ }
898
+ function loadConfig() {
899
+ notices = [];
900
+ envApplied.clear();
901
+ const read = readStored();
902
+ const stored = read.stored;
903
+ if (read.problem) {
904
+ notices.push(
905
+ `Your Clixad config (${CONFIG_PATH}) ${read.problem}, so this session starts signed out.
906
+ It is kept at ${QUARANTINE_PATH} the next time anything is saved \u2014 sign in again with \`/login\`.`
907
+ );
849
908
  }
850
909
  const mintedAgainst = stored.gatewayUrl;
851
910
  for (const [key, stale] of Object.entries(SUPERSEDED_DEFAULTS)) {
@@ -857,17 +916,76 @@ function loadConfig() {
857
916
  const config = { ...DEFAULTS, ...stored };
858
917
  for (const [key, envVar] of ENV_OVERRIDES) {
859
918
  const value = process.env[envVar];
860
- if (value) config[key] = value;
919
+ if (value) {
920
+ config[key] = value;
921
+ envApplied.add(key);
922
+ }
861
923
  }
862
- const migratedGateway = mintedAgainst !== void 0 && stored.gatewayUrl === void 0;
863
- if (migratedGateway && !sameEndpoint(mintedAgainst, config.gatewayUrl)) {
924
+ const boundTo = stored.tokenGateway ?? mintedAgainst;
925
+ const redirected = envApplied.has("gatewayUrl");
926
+ if (!redirected && boundTo !== void 0 && !sameEndpoint(boundTo, config.gatewayUrl)) {
927
+ if (stored.token !== void 0) {
928
+ notices.push(
929
+ `Signed out: the saved account belongs to ${boundTo}, and this run talks to ${config.gatewayUrl}.
930
+ Sign in again with \`/login\` (or \`clixad login\`).`
931
+ );
932
+ }
864
933
  for (const key of IDENTITY_KEYS) delete config[key];
865
934
  }
935
+ baseline = { ...config };
866
936
  return config;
867
937
  }
938
+ function changedKeys(config) {
939
+ const current = config;
940
+ if (!baseline) return Object.keys(current);
941
+ const before = baseline;
942
+ const keys = /* @__PURE__ */ new Set([...Object.keys(before), ...Object.keys(current)]);
943
+ return [...keys].filter((key) => before[key] !== current[key]);
944
+ }
945
+ function writeAtomically(contents) {
946
+ const tmp = `${CONFIG_PATH}.${process.pid}.tmp`;
947
+ try {
948
+ const fd = openSync(tmp, "w", 384);
949
+ try {
950
+ writeSync(fd, contents);
951
+ fsyncSync(fd);
952
+ } finally {
953
+ closeSync(fd);
954
+ }
955
+ renameSync(tmp, CONFIG_PATH);
956
+ } catch (err) {
957
+ try {
958
+ unlinkSync(tmp);
959
+ } catch {
960
+ }
961
+ throw err;
962
+ }
963
+ }
868
964
  function saveConfig(config) {
869
965
  mkdirSync(dirname(CONFIG_PATH), { recursive: true, mode: 448 });
870
- writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2), { encoding: "utf8", mode: 384 });
966
+ const changed = changedKeys(config);
967
+ const current = readStored();
968
+ let base = {};
969
+ if (current.problem) {
970
+ if (current.raw !== void 0) {
971
+ try {
972
+ writeFileSync(QUARANTINE_PATH, current.raw, { encoding: "utf8", mode: 384 });
973
+ } catch {
974
+ }
975
+ }
976
+ } else {
977
+ base = current.stored;
978
+ }
979
+ const next = { ...base };
980
+ const source = config;
981
+ for (const key of changed) {
982
+ const value = source[key];
983
+ if (value === void 0) delete next[key];
984
+ else next[key] = value;
985
+ }
986
+ writeAtomically(`${JSON.stringify(next, null, 2)}
987
+ `);
988
+ baseline = { ...config };
871
989
  try {
872
990
  chmodSync(CONFIG_PATH, 384);
873
991
  } catch {
@@ -876,11 +994,12 @@ function saveConfig(config) {
876
994
  function configPath() {
877
995
  return CONFIG_PATH;
878
996
  }
879
- var CONFIG_PATH, DEFAULTS, ENV_OVERRIDES, SUPERSEDED_DEFAULTS, IDENTITY_KEYS;
997
+ var CONFIG_PATH, QUARANTINE_PATH, DEFAULTS, ENV_OVERRIDES, SUPERSEDED_DEFAULTS, IDENTITY_KEYS, baseline, envApplied, notices;
880
998
  var init_config = __esm({
881
999
  "src/config.ts"() {
882
1000
  "use strict";
883
1001
  CONFIG_PATH = join(homedir(), ".clixad", "config.json");
1002
+ QUARANTINE_PATH = `${CONFIG_PATH}.corrupt`;
884
1003
  DEFAULTS = {
885
1004
  gatewayUrl: "https://clixad.onrender.com",
886
1005
  // Same origin as the gateway: the ad wall is served by the gateway process
@@ -898,7 +1017,9 @@ var init_config = __esm({
898
1017
  gatewayUrl: ["http://127.0.0.1:8787", "http://localhost:8787"],
899
1018
  dashboardUrl: ["http://127.0.0.1:8788", "http://localhost:8788"]
900
1019
  };
901
- IDENTITY_KEYS = ["token", "userId", "email", "login"];
1020
+ IDENTITY_KEYS = ["token", "userId", "email", "login", "tokenGateway"];
1021
+ envApplied = /* @__PURE__ */ new Set();
1022
+ notices = [];
902
1023
  }
903
1024
  });
904
1025
 
@@ -958,6 +1079,7 @@ async function githubLogin(client, config, start, invite, report, signal) {
958
1079
  config.userId = poll.userId;
959
1080
  config.email = poll.email;
960
1081
  config.login = poll.login;
1082
+ config.tokenGateway = config.gatewayUrl;
961
1083
  saveConfig(config);
962
1084
  return {
963
1085
  status: "ok",
@@ -987,6 +1109,7 @@ async function devLogin(client, config, email, invite) {
987
1109
  config.token = res.token;
988
1110
  config.userId = res.userId;
989
1111
  config.email = res.email;
1112
+ config.tokenGateway = config.gatewayUrl;
990
1113
  delete config.login;
991
1114
  saveConfig(config);
992
1115
  return { status: "ok", account: { email: res.email, balance: res.balance, created: true } };
@@ -3470,6 +3593,8 @@ async function runAgent(client, task, opts) {
3470
3593
  const depth = opts.depth ?? 0;
3471
3594
  const emit = (e) => opts.onEvent?.(e);
3472
3595
  let creditsCharged = 0;
3596
+ let creditsWaived = 0;
3597
+ let freeCreditsLeft;
3473
3598
  let balance = 0;
3474
3599
  let lastText = "";
3475
3600
  let pendingImages = [];
@@ -3526,6 +3651,8 @@ ${NO_SEARCH_INSTRUCTION}`,
3526
3651
  }
3527
3652
  });
3528
3653
  creditsCharged += sub.creditsCharged;
3654
+ creditsWaived += sub.creditsWaived;
3655
+ if (sub.freeCreditsLeft !== void 0) freeCreditsLeft = sub.freeCreditsLeft;
3529
3656
  if (sub.stopped === "aborted") return "the sub-agent was stopped before it finished";
3530
3657
  const answer = sub.content.trim();
3531
3658
  if (!answer) return "the sub-agent finished without reporting anything";
@@ -3547,15 +3674,26 @@ ${NO_SEARCH_INSTRUCTION}`,
3547
3674
  throw err;
3548
3675
  }
3549
3676
  creditsCharged += turn.creditsCharged;
3677
+ creditsWaived += turn.creditsWaived ?? 0;
3678
+ if (turn.freeCreditsLeft !== void 0) freeCreditsLeft = turn.freeCreditsLeft;
3550
3679
  balance = turn.balance;
3551
- 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
+ });
3552
3690
  messages.push(turn.message);
3553
3691
  const content = messageText(turn.message);
3554
3692
  if (content) lastText = content;
3555
3693
  const calls = turn.message.tool_calls ?? [];
3556
3694
  if (calls.length === 0) {
3557
3695
  emit({ type: "final", content, creditsCharged, balance });
3558
- return { content, messages, creditsCharged, balance, steps: step + 1 };
3696
+ return { content, messages, creditsCharged, balance, creditsWaived, freeCreditsLeft, steps: step + 1 };
3559
3697
  }
3560
3698
  if (content) emit({ type: "message", content });
3561
3699
  pendingImages = [];
@@ -3582,12 +3720,14 @@ ${NO_SEARCH_INSTRUCTION}`,
3582
3720
  messages,
3583
3721
  creditsCharged,
3584
3722
  balance,
3723
+ creditsWaived,
3724
+ freeCreditsLeft,
3585
3725
  steps: maxSteps,
3586
3726
  stopped: "max_steps"
3587
3727
  };
3588
3728
  function stop(reason) {
3589
3729
  emit({ type: reason });
3590
- return { content: lastText, messages, creditsCharged, balance, steps: 0, stopped: reason };
3730
+ return { content: lastText, messages, creditsCharged, balance, creditsWaived, freeCreditsLeft, steps: 0, stopped: reason };
3591
3731
  }
3592
3732
  }
3593
3733
  function toolMessage(call, content) {
@@ -5129,7 +5269,7 @@ function useTerminalSize() {
5129
5269
  }, [stdout]);
5130
5270
  return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
5131
5271
  }
5132
- function App({ client, config, wallet, streak, session, initialTask, sponsorServe }) {
5272
+ function App({ client, config, wallet, streak, session, initialTask, notices: notices2, sponsorServe }) {
5133
5273
  const { exit } = useApp();
5134
5274
  const { stdout, write: writeToStdout } = useStdout();
5135
5275
  const idRef = useRef(1);
@@ -5163,6 +5303,7 @@ function App({ client, config, wallet, streak, session, initialTask, sponsorServ
5163
5303
  const [live, setLive] = useState(null);
5164
5304
  const [balance, setBalance] = useState(wallet?.balance ?? 0);
5165
5305
  const [spent, setSpent] = useState(0);
5306
+ const [freeTier, setFreeTier] = useState(wallet?.free_tier);
5166
5307
  const [model, setModel2] = useState(config.model);
5167
5308
  const [mode, setMode] = useState("normal");
5168
5309
  const [hist, setHist] = useState(() => loadHistory());
@@ -5205,6 +5346,7 @@ function App({ client, config, wallet, streak, session, initialTask, sponsorServ
5205
5346
  const mcpRef = useRef(McpHub.empty());
5206
5347
  const todosRef = useRef(new TodoList());
5207
5348
  const lastTurnRef = useRef(null);
5349
+ const freeSpentNoticeRef = useRef(false);
5208
5350
  const turnCountRef = useRef(config.turns ?? 0);
5209
5351
  const deltaBufRef = useRef("");
5210
5352
  const deltaTimerRef = useRef(null);
@@ -5283,6 +5425,7 @@ function App({ client, config, wallet, streak, session, initialTask, sponsorServ
5283
5425
  useEffect(() => dropDelta, [dropDelta]);
5284
5426
  useEffect(() => {
5285
5427
  client.onNotice = (text2) => push({ kind: "notice", tone: "warn", text: text2 });
5428
+ for (const text2 of notices2 ?? []) push({ kind: "notice", tone: "warn", text: ` ${text2}` });
5286
5429
  const files = contextRef.current.files;
5287
5430
  if (files.length) push({ kind: "notice", text: ` context: ${files.join(", ")}` });
5288
5431
  const streakLine = streak && streak.claimed ? streakLabel(streak) : null;
@@ -5478,6 +5621,11 @@ function App({ client, config, wallet, streak, session, initialTask, sponsorServ
5478
5621
  case "usage":
5479
5622
  setBalance(event.balance);
5480
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
+ }
5481
5629
  return;
5482
5630
  default:
5483
5631
  return;
@@ -5569,14 +5717,28 @@ ${NO_SEARCH_INSTRUCTION}`),
5569
5717
  push({
5570
5718
  kind: "assistant",
5571
5719
  text: result.content,
5572
- meta: `[${result.creditsCharged} credits \xB7 balance ${result.balance.toLocaleString("en-US")}]`
5720
+ meta: turnMeta(result)
5573
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
+ }
5574
5731
  lastTurnRef.current = {
5575
5732
  turn: turnCountRef.current,
5576
5733
  model,
5577
5734
  prompt: task,
5578
5735
  response: result.content,
5579
- 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
5580
5742
  };
5581
5743
  turnCountRef.current += 1;
5582
5744
  config.turns = turnCountRef.current;
@@ -5825,10 +5987,15 @@ ${NO_SEARCH_INSTRUCTION}`),
5825
5987
  if (arg && !models.some((m) => m.id === arg)) {
5826
5988
  push({ kind: "notice", tone: "warn", text: ` unknown model: ${arg}` });
5827
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);
5828
5995
  const items = models.map((m) => ({
5829
5996
  value: m.id,
5830
5997
  label: m.id,
5831
- hint: `${m.tier.padEnd(8)}${adsPerTaskLabel(m.est_ads_per_task)}`,
5998
+ hint: `${m.tier.padEnd(8)}${modelHint(m, free)}`,
5832
5999
  current: m.id === model
5833
6000
  }));
5834
6001
  const preselect = items.findIndex((i) => i.value === arg);
@@ -5852,7 +6019,7 @@ ${NO_SEARCH_INSTRUCTION}`),
5852
6019
  catalogRef.current = ms;
5853
6020
  push({
5854
6021
  kind: "notice",
5855
- 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")
5856
6023
  });
5857
6024
  });
5858
6025
  return;
@@ -6075,7 +6242,7 @@ ${output}` }
6075
6242
  const turn = lastTurnRef.current;
6076
6243
  if (!turn) return;
6077
6244
  lastTurnRef.current = null;
6078
- const amount = wallet?.rewards?.feedback_credits;
6245
+ const amount = turn.free ? wallet?.rewards?.feedback_credits_free ?? wallet?.rewards?.feedback_credits : wallet?.rewards?.feedback_credits;
6079
6246
  const pay = amount ? `Both answers pay ${amount.toLocaleString("en-US")} credits` : "Both answers pay the same";
6080
6247
  const choice = await new Promise((resolve2) => {
6081
6248
  setPickerSel(0);
@@ -6447,7 +6614,17 @@ ${dropped.map((l) => ` \xB7 ${l}`).join("\n")}`
6447
6614
  const modeText = ` ${MODE_STYLE[mode].glyph} ${MODE_LABEL[mode]}`;
6448
6615
  const statusText = fitRow(
6449
6616
  ` ${statusLine(
6450
- { 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
+ },
6451
6628
  Math.max(1, cols - visibleLength(modeText) - 2)
6452
6629
  )}`,
6453
6630
  Math.max(0, cols - visibleLength(modeText))
@@ -6555,11 +6732,35 @@ function renderInput(state, from, rows) {
6555
6732
  ] }, row);
6556
6733
  });
6557
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
+ }
6558
6752
  function statusLine(o, cols) {
6559
6753
  const burn = o.spent > 0 && o.minutes >= 1 ? `${Math.round(o.spent / o.minutes).toLocaleString("en-US")} cr/min` : void 0;
6560
6754
  const clauses = [
6561
6755
  { text: o.model, rank: KEEP_ALWAYS },
6562
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
+ },
6563
6764
  { text: o.spent > 0 ? `\u2212${o.spent.toLocaleString("en-US")}` : void 0, rank: 2 },
6564
6765
  { text: burn, rank: 1 },
6565
6766
  {
@@ -6570,7 +6771,7 @@ function statusLine(o, cols) {
6570
6771
  ].filter((c2) => Boolean(c2.text));
6571
6772
  const join7 = (minRank) => clauses.filter((c2) => c2.rank >= minRank).map((c2) => c2.text).join(" \xB7 ");
6572
6773
  let line2 = join7(0);
6573
- 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);
6574
6775
  return fitRow(line2, cols);
6575
6776
  }
6576
6777
  function windowStart(sel, total, size) {
@@ -6769,6 +6970,8 @@ async function main() {
6769
6970
  const [cmd, ...rest] = process.argv.slice(2);
6770
6971
  const config = loadConfig();
6771
6972
  const client = new GatewayClient(config);
6973
+ const configNotices = takeConfigNotices();
6974
+ for (const text of configNotices) console.error(c.yellow(text));
6772
6975
  switch (cmd) {
6773
6976
  case "login":
6774
6977
  return login(client, config, rest);
@@ -6793,15 +6996,15 @@ async function main() {
6793
6996
  case "ask":
6794
6997
  return ask(client, config, rest.join(" "));
6795
6998
  case "agent":
6796
- return repl(client, config, { task: rest.join(" ").trim() || void 0 });
6999
+ return repl(client, config, { task: rest.join(" ").trim() || void 0, notices: configNotices });
6797
7000
  case "-p":
6798
7001
  case "--print":
6799
7002
  return printCmd(client, config, rest.join(" ").trim());
6800
7003
  case "-c":
6801
7004
  case "--continue":
6802
- return repl(client, config, { resume: "latest" });
7005
+ return repl(client, config, { resume: "latest", notices: configNotices });
6803
7006
  case "--resume":
6804
- return repl(client, config, { resume: rest[0] ?? "pick" });
7007
+ return repl(client, config, { resume: rest[0] ?? "pick", notices: configNotices });
6805
7008
  case "code":
6806
7009
  return codeCmd(client, config, rest);
6807
7010
  case "help":
@@ -6816,7 +7019,7 @@ async function main() {
6816
7019
  case "-v":
6817
7020
  return void console.log(VERSION);
6818
7021
  case void 0:
6819
- return repl(client, config);
7022
+ return repl(client, config, { notices: configNotices });
6820
7023
  default:
6821
7024
  if (cmd.startsWith("-")) {
6822
7025
  console.error(c.red(`unknown option: ${cmd}`));
@@ -6824,7 +7027,7 @@ async function main() {
6824
7027
  process.exitCode = 1;
6825
7028
  return;
6826
7029
  }
6827
- return repl(client, config, { task: [cmd, ...rest].join(" ").trim() });
7030
+ return repl(client, config, { task: [cmd, ...rest].join(" ").trim(), notices: configNotices });
6828
7031
  }
6829
7032
  }
6830
7033
  async function login(client, config, args) {
@@ -6893,10 +7096,7 @@ function wrapPlain(text, width) {
6893
7096
  var sleep4 = (ms) => new Promise((r) => setTimeout(r, ms));
6894
7097
  var credits = (n) => n.toLocaleString("en-US");
6895
7098
  function logout(config) {
6896
- delete config.token;
6897
- delete config.userId;
6898
- delete config.email;
6899
- delete config.login;
7099
+ for (const key of IDENTITY_KEYS) delete config[key];
6900
7100
  saveConfig(config);
6901
7101
  console.log(c.green("\u2713 logged out"));
6902
7102
  }
@@ -6912,11 +7112,14 @@ function earnedToday(w) {
6912
7112
  }
6913
7113
  async function listModels(client) {
6914
7114
  const models = await client.models();
7115
+ const free = await client.wallet().then((w) => w.free_tier).catch(() => void 0);
6915
7116
  console.log(c.bold("Models (credit prices per 1M tokens)"));
6916
7117
  for (const m of models) {
6917
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]");
6918
7121
  console.log(
6919
- ` ${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}`
6920
7123
  );
6921
7124
  }
6922
7125
  }
@@ -6939,6 +7142,15 @@ async function showWallet(client) {
6939
7142
  const w = await client.wallet();
6940
7143
  console.log(`balance: ${c.bold(w.balance.toLocaleString("en-US"))} credits`);
6941
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
+ }
6942
7154
  const line2 = streak && streakLabel(streak);
6943
7155
  if (line2) console.log(streak.claimed ? c.green(line2) : c.dim(line2));
6944
7156
  if (w.ledger.length) {
@@ -7133,9 +7345,10 @@ ${NO_SEARCH_INSTRUCTION}`),
7133
7345
  todos: new TodoList(),
7134
7346
  onEvent: printAgentEvent
7135
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`;
7136
7349
  console.log(
7137
7350
  c.dim(`
7138
- [${res.creditsCharged} credits \xB7 balance ${res.balance} \xB7 ${res.steps} step(s)]`)
7351
+ [${cost} \xB7 balance ${res.balance} \xB7 ${res.steps} step(s)]`)
7139
7352
  );
7140
7353
  } catch (err) {
7141
7354
  if (err instanceof PaywallError) {
@@ -7235,7 +7448,15 @@ async function repl(client, config, opts = {}) {
7235
7448
  const wallet = await safeWallet(client);
7236
7449
  clearScreen();
7237
7450
  const { startTui: startTui2 } = await Promise.resolve().then(() => (init_tui(), tui_exports));
7238
- await startTui2({ client, config, wallet, streak, session, initialTask: opts.task });
7451
+ await startTui2({
7452
+ client,
7453
+ config,
7454
+ wallet,
7455
+ streak,
7456
+ session,
7457
+ initialTask: opts.task,
7458
+ ...opts.notices?.length ? { notices: opts.notices } : {}
7459
+ });
7239
7460
  }
7240
7461
  async function runTurn(client, config, messages) {
7241
7462
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clixad",
3
- "version": "0.0.1-beta.17",
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",